From cdcac182d458a24908777568979c8e756f94428c Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:14:32 +0300 Subject: [PATCH] fix: load claude models directly from provider Claude's model catalog changes quickly enough that a shared three-day cache can leave users selecting stale defaults or missing newly available model aliases. Route Claude model lookups through the provider every time so the UI and slash commands reflect the current provider result instead of an old disk snapshot. Keep the static fallback catalog aligned with the latest Claude defaults so the provider still has a sensible response when live discovery is unavailable. --- public/modelConstants.js | 13 ++++++-- .../list/claude/claude-models.provider.ts | 11 +++++-- .../services/provider-models.service.ts | 33 +++++++++++++++++++ .../tests/provider-models.service.test.ts | 31 +++++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/public/modelConstants.js b/public/modelConstants.js index b1e660f1..f05575c4 100644 --- a/public/modelConstants.js +++ b/public/modelConstants.js @@ -11,7 +11,13 @@ export const CLAUDE_MODELS = { { value: "default", label: "Default (recommended)", - description: "Use the default model (currently Sonnet 4.6) · $3/$15 per Mtok", + description: + "Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok", + }, + { + value: "sonnet", + label: "Sonnet", + description: "Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok", }, { value: "sonnet[1m]", @@ -20,8 +26,9 @@ export const CLAUDE_MODELS = { }, { value: "opus[1m]", - label: "Opus 4.7 (1M context)", - description: "Opus 4.7 with 1M context · Most capable for complex work · $5/$25 per Mtok", + label: "Opus 4.8 (1M context)", + description: + "Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok", }, { value: "haiku", diff --git a/server/modules/providers/list/claude/claude-models.provider.ts b/server/modules/providers/list/claude/claude-models.provider.ts index 832ee56d..b1b6ba02 100644 --- a/server/modules/providers/list/claude/claude-models.provider.ts +++ b/server/modules/providers/list/claude/claude-models.provider.ts @@ -18,7 +18,12 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = { { value: 'default', label: 'Default (recommended)', - description: 'Use the default model (currently Sonnet 4.6) · $3/$15 per Mtok', + description: 'Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok', + }, + { + value: "sonnet", + label: "Sonnet", + description: "Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok", }, { value: 'sonnet[1m]', @@ -27,8 +32,8 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = { }, { value: 'opus[1m]', - label: 'Opus 4.7 (1M context)', - description: 'Opus 4.7 with 1M context · Most capable for complex work · $5/$25 per Mtok', + label: 'Opus 4.8 (1M context)', + description: 'Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok', }, { value: 'haiku', diff --git a/server/modules/providers/services/provider-models.service.ts b/server/modules/providers/services/provider-models.service.ts index 5cb62433..9d3402b5 100644 --- a/server/modules/providers/services/provider-models.service.ts +++ b/server/modules/providers/services/provider-models.service.ts @@ -17,6 +17,7 @@ import { readProviderSessionActiveModelChange } from '@/shared/utils.js'; export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000; const PROVIDER_MODELS_CACHE_VERSION = 1; +const UNCACHED_PROVIDERS = new Set(['claude']); type ProviderModelsServiceDependencies = { resolveProvider?: (provider: LLMProvider) => Pick; @@ -232,10 +233,42 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD return request; }; + const loadDirectModels = ( + provider: LLMProvider, + ): Promise => { + const request = resolveProvider(provider).models.getSupportedModels() + .then((models) => { + const currentTime = now(); + return { + models, + cache: { + updatedAt: new Date(currentTime).toISOString(), + expiresAt: new Date(currentTime).toISOString(), + source: 'fresh' as const, + }, + }; + }) + .finally(() => { + pendingRequests.delete(provider); + }); + + pendingRequests.set(provider, request); + return request; + }; + const getProviderModels = async ( provider: LLMProvider, options: ProviderModelsOptions = {}, ): Promise => { + if (UNCACHED_PROVIDERS.has(provider)) { + const pendingRequest = pendingRequests.get(provider); + if (pendingRequest) { + return pendingRequest; + } + + return loadDirectModels(provider); + } + if (options.bypassCache) { const pendingRequest = pendingRequests.get(provider); if (pendingRequest) { diff --git a/server/modules/providers/tests/provider-models.service.test.ts b/server/modules/providers/tests/provider-models.service.test.ts index fb9ebf7d..36cbfc6d 100644 --- a/server/modules/providers/tests/provider-models.service.test.ts +++ b/server/modules/providers/tests/provider-models.service.test.ts @@ -130,6 +130,37 @@ test('provider models are cached for the three-day ttl', async () => { } }); +test('claude provider models are always loaded directly from the provider', async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-claude-direct-')); + let loadCount = 0; + + try { + const service = createProviderModelsService({ + cachePath: path.join(tempRoot, 'models-cache.json'), + resolveProvider: (provider) => ({ + models: { + getSupportedModels: async () => { + loadCount += 1; + return createModels(`${provider}-${loadCount}`); + }, + getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`), + changeActiveModel: async (input) => createSessionActiveModelChange(provider, input), + }, + }), + }); + + const first = await service.getProviderModels('claude'); + const second = await service.getProviderModels('claude'); + + assert.equal(loadCount, 2); + assert.equal(first.models.DEFAULT, 'claude-1'); + assert.equal(second.models.DEFAULT, 'claude-2'); + assert.equal(second.cache.source, 'fresh'); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + test('provider model cache is persisted across service instances', async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-file-')); const cachePath = path.join(tempRoot, 'models-cache.json');