diff --git a/public/modelConstants.js b/public/modelConstants.js index 1dc5fd94..f05575c4 100644 --- a/public/modelConstants.js +++ b/public/modelConstants.js @@ -11,7 +11,8 @@ export const CLAUDE_MODELS = { { value: "default", label: "Default (recommended)", - description: "Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok", + description: + "Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok", }, { value: "sonnet", @@ -23,6 +24,12 @@ export const CLAUDE_MODELS = { label: "Sonnet (1M context)", description: "Sonnet 4.6 for long sessions · $3/$15 per Mtok", }, + { + value: "opus[1m]", + label: "Opus 4.8 (1M context)", + description: + "Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok", + }, { value: "haiku", label: "Haiku", diff --git a/server/modules/providers/list/claude/claude-models.provider.ts b/server/modules/providers/list/claude/claude-models.provider.ts index 81e89027..b1b6ba02 100644 --- a/server/modules/providers/list/claude/claude-models.provider.ts +++ b/server/modules/providers/list/claude/claude-models.provider.ts @@ -18,18 +18,23 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = { { value: 'default', label: 'Default (recommended)', - description: 'Use the default model (currently Opus 4.7 (1M context)) · $5/$25 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", + label: "Sonnet", + description: "Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok", }, { value: 'sonnet[1m]', label: 'Sonnet (1M context)', description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok', }, + { + value: 'opus[1m]', + label: 'Opus 4.8 (1M context)', + description: 'Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok', + }, { value: 'haiku', label: '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');