diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 90f949af47..e10d1f6551 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -220,11 +220,6 @@ "count": 26 } }, - "open-sse/handlers/chatCore/clientUsageBuffer.ts": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, "open-sse/handlers/chatCore/executorHelpers.ts": { "@typescript-eslint/no-unused-vars": { "count": 1 @@ -608,11 +603,6 @@ "count": 1 } }, - "open-sse/services/providerCostData.ts": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, "open-sse/services/rateLimitManager.ts": { "@typescript-eslint/no-unused-vars": { "count": 2 @@ -763,7 +753,7 @@ }, "open-sse/utils/cursorAgentProtobuf.ts": { "@typescript-eslint/no-unused-vars": { - "count": 3 + "count": 2 } }, "open-sse/utils/earlyStreamKeepalive.ts": { @@ -1722,16 +1712,6 @@ "count": 2 } }, - "src/lib/oneproxyRotator.ts": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/lib/oneproxySync.ts": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, "src/lib/piiSanitizer.ts": { "@typescript-eslint/no-unused-vars": { "count": 1 @@ -4812,7 +4792,7 @@ }, "tests/unit/responses-translation-fixes.test.ts": { "@typescript-eslint/no-explicit-any": { - "count": 35 + "count": 34 } }, "tests/unit/route-edge-coverage.test.ts": { diff --git a/open-sse/config/claudeCodeCompatibleIdentity.ts b/open-sse/config/claudeCodeCompatibleIdentity.ts index 31b7997f2a..b614eea324 100644 --- a/open-sse/config/claudeCodeCompatibleIdentity.ts +++ b/open-sse/config/claudeCodeCompatibleIdentity.ts @@ -9,15 +9,19 @@ export const CLAUDE_CODE_COMPATIBLE_VERSION = CLAUDE_CODE_CLIENT_VERSION; export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = getClaudeCodeUserAgent("sdk-cli"); export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = CLAUDE_CODE_SDK_PACKAGE_VERSION; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = CLAUDE_CODE_RUNTIME_VERSION; -const CONTEXT_1M_NATIVE_MODELS = ["claude-opus-5"]; +const CONTEXT_1M_NATIVE_MODELS = ["claude-fable-5-1", "claude-opus-5"]; export function modelHasNativeContext1m(model: string | null | undefined): boolean { const normalizedModel = String(model || "") .trim() .toLowerCase() + .replace(/^.*?(?=claude-)/, "") .replace(/-\d{8}$/, ""); return CONTEXT_1M_NATIVE_MODELS.some( - (supported) => normalizedModel === supported || normalizedModel.startsWith(`${supported}-`) + (supported) => + normalizedModel === supported || + (normalizedModel.startsWith(`${supported}-`) && + !/^\d/.test(normalizedModel.slice(supported.length + 1))) ); } diff --git a/open-sse/config/context1m.ts b/open-sse/config/context1m.ts index dab5c405b1..134df22bf8 100644 --- a/open-sse/config/context1m.ts +++ b/open-sse/config/context1m.ts @@ -34,6 +34,9 @@ export function modelSupportsContext1mBeta(model: string | null | undefined): bo .replace(/-\d{8}$/, ""); return CONTEXT_1M_SUPPORTED_MODELS.some( - (supported) => normalizedModel === supported || normalizedModel.startsWith(`${supported}-`) + (supported) => + normalizedModel === supported || + (normalizedModel.startsWith(`${supported}-`) && + !/^\d/.test(normalizedModel.slice(supported.length + 1))) ); -} \ No newline at end of file +} diff --git a/open-sse/config/providers/registry/anthropic/index.ts b/open-sse/config/providers/registry/anthropic/index.ts index 2a3726276d..2845296b15 100644 --- a/open-sse/config/providers/registry/anthropic/index.ts +++ b/open-sse/config/providers/registry/anthropic/index.ts @@ -16,6 +16,17 @@ export const anthropicProvider: RegistryEntry = { "Anthropic-Beta": ANTHROPIC_BETA_API_KEY, }, models: [ + { + id: "claude-fable-5-1", + name: "Claude Fable 5.1", + contextLength: 1000000, + maxOutputTokens: 128000, + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], + supportsXHighEffort: true, + supportsVision: true, + unsupportedParams: ["temperature", "top_p", "top_k"], + }, { id: "claude-fable-5", name: "Claude Fable 5", diff --git a/open-sse/config/providers/registry/bedrock/index.ts b/open-sse/config/providers/registry/bedrock/index.ts index bb9273fda0..ce70091a27 100644 --- a/open-sse/config/providers/registry/bedrock/index.ts +++ b/open-sse/config/providers/registry/bedrock/index.ts @@ -9,6 +9,17 @@ export const bedrockProvider: RegistryEntry = { authHeader: "bearer", defaultContextLength: 200000, models: [ + { + id: "anthropic.claude-fable-5-1", + name: "Claude Fable 5.1 (Bedrock)", + toolCalling: true, + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], + supportsXHighEffort: true, + supportsVision: true, + contextLength: 1000000, + maxOutputTokens: 128000, + }, { id: "anthropic.claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Bedrock)", diff --git a/open-sse/config/providers/registry/claude/index.ts b/open-sse/config/providers/registry/claude/index.ts index 481c831c2f..ba3129678b 100644 --- a/open-sse/config/providers/registry/claude/index.ts +++ b/open-sse/config/providers/registry/claude/index.ts @@ -28,6 +28,17 @@ export const claudeProvider: RegistryEntry = { tokenUrl: "https://api.anthropic.com/v1/oauth/token", }, models: [ + { + id: "claude-fable-5-1", + name: "Claude Fable 5.1", + contextLength: 1000000, + maxOutputTokens: 128000, + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], + supportsXHighEffort: true, + supportsVision: true, + unsupportedParams: ["temperature", "top_p", "top_k"], + }, { id: "claude-fable-5", name: "Claude Fable 5", diff --git a/open-sse/config/providers/registry/claude/web/index.ts b/open-sse/config/providers/registry/claude/web/index.ts index c701ff9aa3..07c4872362 100644 --- a/open-sse/config/providers/registry/claude/web/index.ts +++ b/open-sse/config/providers/registry/claude/web/index.ts @@ -9,6 +9,17 @@ export const claude_webProvider: RegistryEntry = { authType: "apikey", authHeader: "cookie", models: [ + { + id: "claude-fable-5-1", + name: "Claude Fable 5.1 (web)", + toolCalling: false, + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], + supportsXHighEffort: true, + supportsVision: true, + contextLength: 1000000, + maxOutputTokens: 128000, + }, { id: "claude-fable-5", name: "Claude Fable 5 (web)", toolCalling: false }, { id: "claude-opus-5", diff --git a/open-sse/config/providers/registry/cursor/index.ts b/open-sse/config/providers/registry/cursor/index.ts index cc5a78f8ce..4b5b3a86de 100644 --- a/open-sse/config/providers/registry/cursor/index.ts +++ b/open-sse/config/providers/registry/cursor/index.ts @@ -1,6 +1,37 @@ -import type { RegistryEntry } from "../../shared.ts"; +import type { RegistryEntry, RegistryModel } from "../../shared.ts"; import { CURSOR_REGISTRY_VERSION, getCursorRegistryHeaders } from "../../shared.ts"; +const CLAUDE_FABLE_5_1_CAPABILITIES = { + maxOutputTokens: 128_000, +} as const; + +const ONE_MILLION_CONTEXT = 1_000_000; + +function withOneMillionContext( + models: RegistryModel[], + familyName: string, + defaultContextLength: number, + liveCatalogId: string, + supportsOneMillion: (model: RegistryModel) => boolean = () => true +): RegistryModel[] { + return models.flatMap((model) => { + const defaultContextModel = { + ...model, + contextLength: defaultContextLength, + liveCatalogIds: model.liveCatalogIds ?? [liveCatalogId], + ...(familyName.startsWith("GPT-") ? {} : { scoresAs: model.scoresAs ?? liveCatalogId }), + }; + if (!supportsOneMillion(model)) return [defaultContextModel]; + const oneMillionModel = { + ...defaultContextModel, + id: `${model.id}-1m`, + name: model.name.replace(familyName, `${familyName} 1M`), + contextLength: ONE_MILLION_CONTEXT, + }; + return [oneMillionModel, defaultContextModel]; + }); +} + export const cursorProvider: RegistryEntry = { id: "cursor", alias: "cu", @@ -18,259 +49,214 @@ export const cursorProvider: RegistryEntry = { { id: "auto-cost", name: "Auto (cost)" }, { id: "auto-balance", name: "Auto (balance)" }, { id: "auto-intelligence", name: "Auto (intelligence)" }, - // Legacy combo ids kept so existing cu/ targets are not orphaned. - { id: "composer-2", name: "Composer 2" }, - { id: "composer-2-fast", name: "Composer 2 Fast" }, - { id: "gpt-5.4-low-fast", name: "GPT 5.4 Low Fast" }, - { id: "gpt-5.3-codex-spark-preview-low", name: "GPT 5.3 Codex Spark Preview Low" }, - { id: "gpt-5.3-codex-spark-preview", name: "GPT 5.3 Codex Spark Preview" }, - { id: "gpt-5.3-codex-spark-preview-high", name: "GPT 5.3 Codex Spark Preview High" }, - { id: "gpt-5.3-codex-spark-preview-xhigh", name: "GPT 5.3 Codex Spark Preview XHigh" }, - // #11489: cursor/agy spell Claude ids - ("claude-4.6-opus-high"); - // the effort splitter strips those to "claude-4.6-opus", which is not a catalog id. - // `scoresAs` points each at the canonical - spelling so quality - // scores are inherited. Operational metadata stays on these entries. - { - id: "claude-4.6-opus-high-thinking-fast", - name: "Claude 4.6 Opus High Thinking Fast", - scoresAs: "claude-opus-4-6", - }, - { - id: "claude-4.6-opus-max-thinking-fast", - name: "Claude 4.6 Opus Max Thinking Fast", - scoresAs: "claude-opus-4-6", - }, - { - id: "claude-4.6-sonnet-medium", - name: "Claude 4.6 Sonnet Medium", - scoresAs: "claude-sonnet-4-6", - }, - { - id: "claude-4.6-sonnet-medium-thinking", - name: "Claude 4.6 Sonnet Medium Thinking", - scoresAs: "claude-sonnet-4-6", - }, - { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, - { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" }, - { id: "gemini-3-flash", name: "Gemini 3 Flash" }, - { id: "grok-4.6-medium", name: "Grok 4.6 Medium" }, - { id: "grok-4.6-fast-medium", name: "Grok 4.6 Fast Medium" }, - { id: "grok-4.6-high", name: "Grok 4.6 High" }, - { id: "grok-4.6-fast-high", name: "Grok 4.6 Fast High" }, - { id: "grok-4.6-xhigh", name: "Grok 4.6 XHigh" }, - { id: "grok-4.6-fast-xhigh", name: "Grok 4.6 Fast XHigh" }, - { id: "kimi-k3", name: "Kimi K3" }, - { id: "kimi-k2.7-code", name: "Kimi K2.7 Code" }, - { id: "grok-4.3", name: "Grok 4.3" }, - { id: "grok-4.5-medium", name: "Grok 4.5 Medium" }, - { id: "grok-4.5-fast-medium", name: "Grok 4.5 Fast Medium" }, - { id: "grok-4.5-high", name: "Grok 4.5 High" }, - { id: "grok-4.5-fast-high", name: "Grok 4.5 Fast High" }, - { id: "grok-4.5-xhigh", name: "Grok 4.5 XHigh" }, - { id: "grok-4.5-fast-xhigh", name: "Grok 4.5 Fast XHigh" }, - { id: "kimi-k2.5", name: "Kimi K2.5" }, - { id: "gpt-5.3-codex-low", name: "Codex 5.3 Low" }, - { id: "gpt-5.3-codex-low-fast", name: "Codex 5.3 Low Fast" }, - { id: "gpt-5.3-codex", name: "Codex 5.3" }, - { id: "gpt-5.3-codex-fast", name: "Codex 5.3 Fast" }, - { id: "gpt-5.3-codex-high", name: "Codex 5.3 High" }, - { id: "gpt-5.3-codex-high-fast", name: "Codex 5.3 High Fast" }, - { id: "gpt-5.3-codex-xhigh", name: "Codex 5.3 Extra High" }, - { id: "gpt-5.3-codex-xhigh-fast", name: "Codex 5.3 Extra High Fast" }, - { id: "gpt-5.2", name: "GPT-5.2" }, - { id: "cursor-grok-4.5-high", name: "Cursor Grok 4.5" }, - { id: "cursor-grok-4.5-high-fast", name: "Cursor Grok 4.5 Fast" }, - { id: "composer-2.5", name: "Composer 2.5" }, - { id: "claude-opus-5-thinking-high", name: "Opus 5 1M Thinking" }, - { id: "claude-opus-5-thinking-high-fast", name: "Opus 5 1M Thinking Fast" }, - { id: "claude-opus-5-thinking-xhigh", name: "Opus 5 1M Extra High Thinking" }, - { id: "claude-opus-5-thinking-xhigh-fast", name: "Opus 5 1M Extra High Thinking Fast" }, - { id: "claude-opus-4-8-thinking-high", name: "Opus 4.8 1M Thinking" }, - { id: "claude-opus-4-8-thinking-high-fast", name: "Opus 4.8 1M Thinking Fast" }, - { id: "gpt-5.6-sol-high", name: "GPT-5.6 Sol 1M High" }, - { id: "gpt-5.6-sol-high-fast", name: "GPT-5.6 Sol High Fast" }, - { id: "gpt-5.6-sol-xhigh", name: "GPT-5.6 Sol 1M Extra High" }, - { id: "gpt-5.6-sol-xhigh-fast", name: "GPT-5.6 Sol Extra High Fast" }, - { id: "gpt-5.5-high", name: "GPT-5.5 1M High" }, - { id: "gpt-5.5-high-fast", name: "GPT-5.5 High Fast" }, - { id: "claude-fable-5-thinking-high", name: "Fable 5 1M Thinking (NO ZDR)" }, - { id: "claude-fable-5-thinking-xhigh", name: "Fable 5 1M Extra High Thinking (NO ZDR)" }, - { id: "claude-sonnet-5-thinking-high", name: "Sonnet 5 1M Thinking" }, - { id: "claude-sonnet-5-thinking-xhigh", name: "Sonnet 5 1M Extra High Thinking" }, - { id: "kimi-k3-high", name: "Kimi K3 High" }, - { id: "cursor-grok-4.5-low", name: "Cursor Grok 4.5 Low" }, - { id: "cursor-grok-4.5-low-fast", name: "Cursor Grok 4.5 Low Fast" }, - { id: "cursor-grok-4.5-medium", name: "Cursor Grok 4.5 Medium" }, - { id: "cursor-grok-4.5-medium-fast", name: "Cursor Grok 4.5 Medium Fast" }, + { id: "cursor-grok-4.6-xhigh-fast", name: "Cursor Grok 4.6 Xhigh Fast" }, + { id: "cursor-grok-4.6-xhigh", name: "Cursor Grok 4.6 Xhigh" }, + { id: "cursor-grok-4.6-high-fast", name: "Cursor Grok 4.6 High Fast" }, + { id: "cursor-grok-4.6-high", name: "Cursor Grok 4.6 High" }, + { id: "cursor-grok-4.6-medium-fast", name: "Cursor Grok 4.6 Medium Fast" }, + { id: "cursor-grok-4.6-medium", name: "Cursor Grok 4.6 Medium" }, + { id: "cursor-grok-4.6-low-fast", name: "Cursor Grok 4.6 Low Fast" }, + { id: "cursor-grok-4.6-low", name: "Cursor Grok 4.6 Low" }, { id: "composer-2.5-fast", name: "Composer 2.5 Fast" }, - { id: "claude-opus-5-low", name: "Opus 5 1M Low" }, - { id: "claude-opus-5-low-fast", name: "Opus 5 1M Low Fast" }, - { id: "claude-opus-5-medium", name: "Opus 5 1M Medium" }, - { id: "claude-opus-5-medium-fast", name: "Opus 5 1M Medium Fast" }, - { id: "claude-opus-5-high", name: "Opus 5 1M" }, - { id: "claude-opus-5-high-fast", name: "Opus 5 1M Fast" }, - { id: "claude-opus-5-thinking-low", name: "Opus 5 1M Low Thinking" }, - { id: "claude-opus-5-thinking-low-fast", name: "Opus 5 1M Low Thinking Fast" }, - { id: "claude-opus-5-thinking-medium", name: "Opus 5 1M Medium Thinking" }, - { id: "claude-opus-5-thinking-medium-fast", name: "Opus 5 1M Medium Thinking Fast" }, - { id: "claude-opus-5-thinking-max", name: "Opus 5 1M Max Thinking" }, - { id: "claude-opus-5-thinking-max-fast", name: "Opus 5 1M Max Thinking Fast" }, - { id: "claude-opus-4-8-low", name: "Opus 4.8 1M Low" }, - { id: "claude-opus-4-8-low-fast", name: "Opus 4.8 1M Low Fast" }, - { id: "claude-opus-4-8-medium", name: "Opus 4.8 1M Medium" }, - { id: "claude-opus-4-8-medium-fast", name: "Opus 4.8 1M Medium Fast" }, - { id: "claude-opus-4-8-high", name: "Opus 4.8 1M" }, - { id: "claude-opus-4-8-high-fast", name: "Opus 4.8 1M Fast" }, - { id: "claude-opus-4-8-xhigh", name: "Opus 4.8 1M Extra High" }, - { id: "claude-opus-4-8-xhigh-fast", name: "Opus 4.8 1M Extra High Fast" }, - { id: "claude-opus-4-8-max", name: "Opus 4.8 1M Max" }, - { id: "claude-opus-4-8-max-fast", name: "Opus 4.8 1M Max Fast" }, - { id: "claude-opus-4-8-thinking-low", name: "Opus 4.8 1M Low Thinking" }, - { id: "claude-opus-4-8-thinking-low-fast", name: "Opus 4.8 1M Low Thinking Fast" }, - { id: "claude-opus-4-8-thinking-medium", name: "Opus 4.8 1M Medium Thinking" }, - { id: "claude-opus-4-8-thinking-medium-fast", name: "Opus 4.8 1M Medium Thinking Fast" }, - { id: "claude-opus-4-8-thinking-xhigh", name: "Opus 4.8 1M Extra High Thinking" }, - { id: "claude-opus-4-8-thinking-xhigh-fast", name: "Opus 4.8 1M Extra High Thinking Fast" }, - { id: "claude-opus-4-8-thinking-max", name: "Opus 4.8 1M Max Thinking" }, - { id: "claude-opus-4-8-thinking-max-fast", name: "Opus 4.8 1M Max Thinking Fast" }, - { id: "gpt-5.6-sol-none", name: "GPT-5.6 Sol 1M None" }, - { id: "gpt-5.6-sol-none-fast", name: "GPT-5.6 Sol None Fast" }, - { id: "gpt-5.6-sol-low", name: "GPT-5.6 Sol 1M Low" }, - { id: "gpt-5.6-sol-low-fast", name: "GPT-5.6 Sol Low Fast" }, - { id: "gpt-5.6-sol-medium", name: "GPT-5.6 Sol 1M" }, - { id: "gpt-5.6-sol-medium-fast", name: "GPT-5.6 Sol Fast" }, - { id: "gpt-5.6-sol-max", name: "GPT-5.6 Sol 1M Max" }, - { id: "gpt-5.6-sol-max-fast", name: "GPT-5.6 Sol Max Fast" }, - { id: "gpt-5.5-none", name: "GPT-5.5 1M None" }, - { id: "gpt-5.5-none-fast", name: "GPT-5.5 None Fast" }, - { id: "gpt-5.5-low", name: "GPT-5.5 1M Low" }, - { id: "gpt-5.5-low-fast", name: "GPT-5.5 Low Fast" }, - { id: "gpt-5.5-medium", name: "GPT-5.5 1M" }, - { id: "gpt-5.5-medium-fast", name: "GPT-5.5 Fast" }, - { id: "gpt-5.5-extra-high", name: "GPT-5.5 1M Extra High" }, - { id: "gpt-5.5-extra-high-fast", name: "GPT-5.5 Extra High Fast" }, - { id: "claude-fable-5-low", name: "Fable 5 1M Low (NO ZDR)" }, - { id: "claude-fable-5-medium", name: "Fable 5 1M Medium (NO ZDR)" }, - { id: "claude-fable-5-high", name: "Fable 5 1M (NO ZDR)" }, - { id: "claude-fable-5-xhigh", name: "Fable 5 1M Extra High (NO ZDR)" }, - { id: "claude-fable-5-max", name: "Fable 5 1M Max (NO ZDR)" }, - { id: "claude-fable-5-thinking-low", name: "Fable 5 1M Low Thinking (NO ZDR)" }, - { id: "claude-fable-5-thinking-medium", name: "Fable 5 1M Medium Thinking (NO ZDR)" }, - { id: "claude-fable-5-thinking-max", name: "Fable 5 1M Max Thinking (NO ZDR)" }, - { id: "claude-sonnet-5-low", name: "Sonnet 5 1M Low" }, - { id: "claude-sonnet-5-medium", name: "Sonnet 5 1M Medium" }, - { id: "claude-sonnet-5-high", name: "Sonnet 5 1M" }, - { id: "claude-sonnet-5-xhigh", name: "Sonnet 5 1M Extra High" }, - { id: "claude-sonnet-5-max", name: "Sonnet 5 1M Max" }, - { id: "claude-sonnet-5-thinking-low", name: "Sonnet 5 1M Low Thinking" }, - { id: "claude-sonnet-5-thinking-medium", name: "Sonnet 5 1M Medium Thinking" }, - { id: "claude-sonnet-5-thinking-max", name: "Sonnet 5 1M Max Thinking" }, - { id: "gpt-5.6-terra-none", name: "GPT-5.6 Terra 1M None" }, - { id: "gpt-5.6-terra-none-fast", name: "GPT-5.6 Terra None Fast" }, - { id: "gpt-5.6-terra-low", name: "GPT-5.6 Terra 1M Low" }, - { id: "gpt-5.6-terra-low-fast", name: "GPT-5.6 Terra Low Fast" }, - { id: "gpt-5.6-terra-medium", name: "GPT-5.6 Terra 1M" }, - { id: "gpt-5.6-terra-medium-fast", name: "GPT-5.6 Terra Fast" }, - { id: "gpt-5.6-terra-high", name: "GPT-5.6 Terra 1M High" }, - { id: "gpt-5.6-terra-high-fast", name: "GPT-5.6 Terra High Fast" }, - { id: "gpt-5.6-terra-xhigh", name: "GPT-5.6 Terra 1M Extra High" }, - { id: "gpt-5.6-terra-xhigh-fast", name: "GPT-5.6 Terra Extra High Fast" }, - { id: "gpt-5.6-terra-max", name: "GPT-5.6 Terra 1M Max" }, - { id: "gpt-5.6-terra-max-fast", name: "GPT-5.6 Terra Max Fast" }, - { id: "claude-opus-4-7-low", name: "Opus 4.7 1M Low" }, - { id: "claude-opus-4-7-low-fast", name: "Opus 4.7 1M Low Fast" }, - { id: "claude-opus-4-7-medium", name: "Opus 4.7 1M Medium" }, - { id: "claude-opus-4-7-medium-fast", name: "Opus 4.7 1M Medium Fast" }, - { id: "claude-opus-4-7-high", name: "Opus 4.7 1M High" }, - { id: "claude-opus-4-7-high-fast", name: "Opus 4.7 1M High Fast" }, - { id: "claude-opus-4-7-xhigh", name: "Opus 4.7 1M" }, - { id: "claude-opus-4-7-xhigh-fast", name: "Opus 4.7 1M Fast" }, - { id: "claude-opus-4-7-max", name: "Opus 4.7 1M Max" }, - { id: "claude-opus-4-7-max-fast", name: "Opus 4.7 1M Max Fast" }, - { id: "claude-opus-4-7-thinking-low", name: "Opus 4.7 1M Low Thinking" }, - { id: "claude-opus-4-7-thinking-low-fast", name: "Opus 4.7 1M Low Thinking Fast" }, - { id: "claude-opus-4-7-thinking-medium", name: "Opus 4.7 1M Medium Thinking" }, - { id: "claude-opus-4-7-thinking-medium-fast", name: "Opus 4.7 1M Medium Thinking Fast" }, - { id: "claude-opus-4-7-thinking-high", name: "Opus 4.7 1M High Thinking" }, - { id: "claude-opus-4-7-thinking-high-fast", name: "Opus 4.7 1M High Thinking Fast" }, - { id: "claude-opus-4-7-thinking-xhigh", name: "Opus 4.7 1M Thinking" }, - { id: "claude-opus-4-7-thinking-xhigh-fast", name: "Opus 4.7 1M Thinking Fast" }, - { id: "claude-opus-4-7-thinking-max", name: "Opus 4.7 1M Max Thinking" }, - { id: "claude-opus-4-7-thinking-max-fast", name: "Opus 4.7 1M Max Thinking Fast" }, - { id: "gpt-5.4-low", name: "GPT-5.4 1M Low" }, - { id: "gpt-5.4-medium", name: "GPT-5.4 1M" }, - { id: "gpt-5.4-medium-fast", name: "GPT-5.4 Fast" }, - { id: "gpt-5.4-high", name: "GPT-5.4 1M High" }, - { id: "gpt-5.4-high-fast", name: "GPT-5.4 High Fast" }, - { id: "gpt-5.4-xhigh", name: "GPT-5.4 1M Extra High" }, - { id: "gpt-5.4-xhigh-fast", name: "GPT-5.4 Extra High Fast" }, - // #11489: cursor/agy spell Claude ids - ("claude-4.6-opus-high"); - // the effort splitter strips those to "claude-4.6-opus", which is not a catalog id. - // `scoresAs` points each at the canonical - spelling so quality - // scores are inherited. Operational metadata stays on these entries. - { id: "claude-4.6-opus-high", name: "Opus 4.6 1M", scoresAs: "claude-opus-4-6" }, - { id: "claude-4.6-opus-max", name: "Opus 4.6 1M Max", scoresAs: "claude-opus-4-6" }, - { - id: "claude-4.6-opus-high-thinking", - name: "Opus 4.6 1M Thinking", - scoresAs: "claude-opus-4-6", - }, - { - id: "claude-4.6-opus-max-thinking", - name: "Opus 4.6 1M Max Thinking", - scoresAs: "claude-opus-4-6", - }, - { id: "claude-4.5-opus-high", name: "Opus 4.5", scoresAs: "claude-opus-4-5" }, - { id: "claude-4.5-opus-high-thinking", name: "Opus 4.5 Thinking", scoresAs: "claude-opus-4-5" }, - { id: "gpt-5.2-low", name: "GPT-5.2 Low" }, - { id: "gpt-5.2-low-fast", name: "GPT-5.2 Low Fast" }, - { id: "gpt-5.2-fast", name: "GPT-5.2 Fast" }, - { id: "gpt-5.2-high", name: "GPT-5.2 High" }, - { id: "gpt-5.2-high-fast", name: "GPT-5.2 High Fast" }, - { id: "gpt-5.2-xhigh", name: "GPT-5.2 Extra High" }, - { id: "gpt-5.2-xhigh-fast", name: "GPT-5.2 Extra High Fast" }, - { id: "gpt-5.6-luna-none", name: "GPT-5.6 Luna 1M None" }, - { id: "gpt-5.6-luna-none-fast", name: "GPT-5.6 Luna None Fast" }, - { id: "gpt-5.6-luna-low", name: "GPT-5.6 Luna 1M Low" }, - { id: "gpt-5.6-luna-low-fast", name: "GPT-5.6 Luna Low Fast" }, - { id: "gpt-5.6-luna-medium", name: "GPT-5.6 Luna 1M" }, - { id: "gpt-5.6-luna-medium-fast", name: "GPT-5.6 Luna Fast" }, - { id: "gpt-5.6-luna-high", name: "GPT-5.6 Luna 1M High" }, - { id: "gpt-5.6-luna-high-fast", name: "GPT-5.6 Luna High Fast" }, - { id: "gpt-5.6-luna-xhigh", name: "GPT-5.6 Luna 1M Extra High" }, - { id: "gpt-5.6-luna-xhigh-fast", name: "GPT-5.6 Luna Extra High Fast" }, - { id: "gpt-5.6-luna-max", name: "GPT-5.6 Luna 1M Max" }, - { id: "gpt-5.6-luna-max-fast", name: "GPT-5.6 Luna Max Fast" }, - { id: "gemini-3.6-flash-minimal", name: "Gemini 3.6 Flash Minimal" }, - { id: "gemini-3.6-flash-low", name: "Gemini 3.6 Flash Low" }, - { id: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash Medium" }, - { id: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash" }, - { id: "gpt-5.4-mini-none", name: "GPT-5.4 Mini None" }, - { id: "gpt-5.4-mini-low", name: "GPT-5.4 Mini Low" }, - { id: "gpt-5.4-mini-medium", name: "GPT-5.4 Mini" }, - { id: "gpt-5.4-mini-high", name: "GPT-5.4 Mini High" }, - { id: "gpt-5.4-mini-xhigh", name: "GPT-5.4 Mini Extra High" }, - { id: "gpt-5.4-nano-none", name: "GPT-5.4 Nano None" }, - { id: "gpt-5.4-nano-low", name: "GPT-5.4 Nano Low" }, - { id: "gpt-5.4-nano-medium", name: "GPT-5.4 Nano" }, - { id: "gpt-5.4-nano-high", name: "GPT-5.4 Nano High" }, - { id: "gpt-5.4-nano-xhigh", name: "GPT-5.4 Nano Extra High" }, - { id: "claude-4.5-sonnet", name: "Sonnet 4.5", scoresAs: "claude-sonnet-4-5" }, - { - id: "claude-4.5-sonnet-thinking", - name: "Sonnet 4.5 Thinking", - scoresAs: "claude-sonnet-4-5", - }, - { id: "gpt-5.1-low", name: "GPT-5.1 Low" }, - { id: "gpt-5.1", name: "GPT-5.1" }, - { id: "gpt-5.1-high", name: "GPT-5.1 High" }, - { id: "claude-4-sonnet", name: "Sonnet 4", scoresAs: "claude-sonnet-4" }, - { id: "claude-4-sonnet-thinking", name: "Sonnet 4 Thinking", scoresAs: "claude-sonnet-4" }, - { id: "gpt-5-mini", name: "GPT-5 Mini" }, + { id: "composer-2.5", name: "Composer 2.5" }, + ...withOneMillionContext( + [ + { + id: "claude-fable-5-1-thinking-max", + name: "Claude Fable 5.1 Max Thinking", + ...CLAUDE_FABLE_5_1_CAPABILITIES, + }, + { + id: "claude-fable-5-1-thinking-xhigh", + name: "Claude Fable 5.1 Xhigh Thinking", + ...CLAUDE_FABLE_5_1_CAPABILITIES, + }, + { + id: "claude-fable-5-1-thinking-high", + name: "Claude Fable 5.1 High Thinking", + ...CLAUDE_FABLE_5_1_CAPABILITIES, + }, + { + id: "claude-fable-5-1-thinking-medium", + name: "Claude Fable 5.1 Medium Thinking", + ...CLAUDE_FABLE_5_1_CAPABILITIES, + }, + { + id: "claude-fable-5-1-thinking-low", + name: "Claude Fable 5.1 Low Thinking", + ...CLAUDE_FABLE_5_1_CAPABILITIES, + }, + ], + "Claude Fable 5.1", + 300_000, + "claude-fable-5-1" + ), + ...withOneMillionContext( + [ + { id: "claude-opus-5-thinking-max-fast", name: "Claude Opus 5 Max Thinking Fast" }, + { id: "claude-opus-5-thinking-max", name: "Claude Opus 5 Max Thinking" }, + { + id: "claude-opus-5-thinking-xhigh-fast", + name: "Claude Opus 5 Xhigh Thinking Fast", + }, + { id: "claude-opus-5-thinking-xhigh", name: "Claude Opus 5 Xhigh Thinking" }, + { id: "claude-opus-5-thinking-high-fast", name: "Claude Opus 5 High Thinking Fast" }, + { id: "claude-opus-5-thinking-high", name: "Claude Opus 5 High Thinking" }, + { id: "claude-opus-5-high-fast", name: "Claude Opus 5 High Fast" }, + { id: "claude-opus-5-high", name: "Claude Opus 5 High" }, + { + id: "claude-opus-5-thinking-medium-fast", + name: "Claude Opus 5 Medium Thinking Fast", + }, + { id: "claude-opus-5-thinking-medium", name: "Claude Opus 5 Medium Thinking" }, + { id: "claude-opus-5-medium-fast", name: "Claude Opus 5 Medium Fast" }, + { id: "claude-opus-5-medium", name: "Claude Opus 5 Medium" }, + { id: "claude-opus-5-thinking-low-fast", name: "Claude Opus 5 Low Thinking Fast" }, + { id: "claude-opus-5-thinking-low", name: "Claude Opus 5 Low Thinking" }, + { id: "claude-opus-5-low-fast", name: "Claude Opus 5 Low Fast" }, + { id: "claude-opus-5-low", name: "Claude Opus 5 Low" }, + ], + "Claude Opus 5", + 300_000, + "claude-opus-5" + ), + ...withOneMillionContext( + [ + { id: "claude-opus-4-8-thinking-max-fast", name: "Claude Opus 4.8 Max Thinking Fast" }, + { id: "claude-opus-4-8-thinking-max", name: "Claude Opus 4.8 Max Thinking" }, + { id: "claude-opus-4-8-max-fast", name: "Claude Opus 4.8 Max Fast" }, + { id: "claude-opus-4-8-max", name: "Claude Opus 4.8 Max" }, + { + id: "claude-opus-4-8-thinking-xhigh-fast", + name: "Claude Opus 4.8 Xhigh Thinking Fast", + }, + { id: "claude-opus-4-8-thinking-xhigh", name: "Claude Opus 4.8 Xhigh Thinking" }, + { id: "claude-opus-4-8-xhigh-fast", name: "Claude Opus 4.8 Xhigh Fast" }, + { id: "claude-opus-4-8-xhigh", name: "Claude Opus 4.8 Xhigh" }, + { id: "claude-opus-4-8-thinking-high-fast", name: "Claude Opus 4.8 High Thinking Fast" }, + { id: "claude-opus-4-8-thinking-high", name: "Claude Opus 4.8 High Thinking" }, + { id: "claude-opus-4-8-high-fast", name: "Claude Opus 4.8 High Fast" }, + { id: "claude-opus-4-8-high", name: "Claude Opus 4.8 High" }, + { + id: "claude-opus-4-8-thinking-medium-fast", + name: "Claude Opus 4.8 Medium Thinking Fast", + }, + { id: "claude-opus-4-8-thinking-medium", name: "Claude Opus 4.8 Medium Thinking" }, + { id: "claude-opus-4-8-medium-fast", name: "Claude Opus 4.8 Medium Fast" }, + { id: "claude-opus-4-8-medium", name: "Claude Opus 4.8 Medium" }, + { id: "claude-opus-4-8-thinking-low-fast", name: "Claude Opus 4.8 Low Thinking Fast" }, + { id: "claude-opus-4-8-thinking-low", name: "Claude Opus 4.8 Low Thinking" }, + { id: "claude-opus-4-8-low-fast", name: "Claude Opus 4.8 Low Fast" }, + { id: "claude-opus-4-8-low", name: "Claude Opus 4.8 Low" }, + ], + "Claude Opus 4.8", + 300_000, + "claude-opus-4-8" + ), + ...withOneMillionContext( + [ + { id: "claude-sonnet-5-thinking-max", name: "Claude Sonnet 5 Max Thinking" }, + { id: "claude-sonnet-5-max", name: "Claude Sonnet 5 Max" }, + { id: "claude-sonnet-5-thinking-xhigh", name: "Claude Sonnet 5 Xhigh Thinking" }, + { id: "claude-sonnet-5-xhigh", name: "Claude Sonnet 5 Xhigh" }, + { id: "claude-sonnet-5-thinking-high", name: "Claude Sonnet 5 High Thinking" }, + { id: "claude-sonnet-5-high", name: "Claude Sonnet 5 High" }, + { id: "claude-sonnet-5-thinking-medium", name: "Claude Sonnet 5 Medium Thinking" }, + { id: "claude-sonnet-5-medium", name: "Claude Sonnet 5 Medium" }, + { id: "claude-sonnet-5-thinking-low", name: "Claude Sonnet 5 Low Thinking" }, + { id: "claude-sonnet-5-low", name: "Claude Sonnet 5 Low" }, + ], + "Claude Sonnet 5", + 300_000, + "claude-sonnet-5" + ), + ...withOneMillionContext( + [ + { id: "claude-4.6-sonnet-max-thinking", name: "Claude Sonnet 4.6 Max Thinking" }, + { id: "claude-4.6-sonnet-max", name: "Claude Sonnet 4.6 Max" }, + { id: "claude-4.6-sonnet-high-thinking", name: "Claude Sonnet 4.6 High Thinking" }, + { id: "claude-4.6-sonnet-high", name: "Claude Sonnet 4.6 High" }, + { id: "claude-4.6-sonnet-medium-thinking", name: "Claude Sonnet 4.6 Medium Thinking" }, + { id: "claude-4.6-sonnet-medium", name: "Claude Sonnet 4.6 Medium" }, + { id: "claude-4.6-sonnet-low-thinking", name: "Claude Sonnet 4.6 Low Thinking" }, + { id: "claude-4.6-sonnet-low", name: "Claude Sonnet 4.6 Low" }, + ], + "Claude Sonnet 4.6", + 200_000, + "claude-sonnet-4-6" + ), + { id: "claude-4.5-haiku-thinking", name: "Claude Haiku 4.5 Thinking" }, + { id: "claude-4.5-haiku", name: "Claude Haiku 4.5" }, + ...withOneMillionContext( + [ + { id: "gpt-5.6-sol-max-fast", name: "GPT-5.6 Sol Max Fast" }, + { id: "gpt-5.6-sol-max", name: "GPT-5.6 Sol Max" }, + { id: "gpt-5.6-sol-xhigh-fast", name: "GPT-5.6 Sol Xhigh Fast" }, + { id: "gpt-5.6-sol-xhigh", name: "GPT-5.6 Sol Xhigh" }, + { id: "gpt-5.6-sol-high-fast", name: "GPT-5.6 Sol High Fast" }, + { id: "gpt-5.6-sol-high", name: "GPT-5.6 Sol High" }, + { id: "gpt-5.6-sol-medium-fast", name: "GPT-5.6 Sol Medium Fast" }, + { id: "gpt-5.6-sol-medium", name: "GPT-5.6 Sol Medium" }, + { id: "gpt-5.6-sol-low-fast", name: "GPT-5.6 Sol Low Fast" }, + { id: "gpt-5.6-sol-low", name: "GPT-5.6 Sol Low" }, + { id: "gpt-5.6-sol-none-fast", name: "GPT-5.6 Sol None Fast" }, + { id: "gpt-5.6-sol-none", name: "GPT-5.6 Sol None" }, + ], + "GPT-5.6 Sol", + 272_000, + "gpt-5.6-sol", + (model) => !model.id.endsWith("-fast") + ), + ...withOneMillionContext( + [ + { id: "gpt-5.6-terra-max-fast", name: "GPT-5.6 Terra Max Fast" }, + { id: "gpt-5.6-terra-max", name: "GPT-5.6 Terra Max" }, + { id: "gpt-5.6-terra-xhigh-fast", name: "GPT-5.6 Terra Xhigh Fast" }, + { id: "gpt-5.6-terra-xhigh", name: "GPT-5.6 Terra Xhigh" }, + { id: "gpt-5.6-terra-high-fast", name: "GPT-5.6 Terra High Fast" }, + { id: "gpt-5.6-terra-high", name: "GPT-5.6 Terra High" }, + { id: "gpt-5.6-terra-medium-fast", name: "GPT-5.6 Terra Medium Fast" }, + { id: "gpt-5.6-terra-medium", name: "GPT-5.6 Terra Medium" }, + { id: "gpt-5.6-terra-low-fast", name: "GPT-5.6 Terra Low Fast" }, + { id: "gpt-5.6-terra-low", name: "GPT-5.6 Terra Low" }, + { id: "gpt-5.6-terra-none-fast", name: "GPT-5.6 Terra None Fast" }, + { id: "gpt-5.6-terra-none", name: "GPT-5.6 Terra None" }, + ], + "GPT-5.6 Terra", + 272_000, + "gpt-5.6-terra", + (model) => !model.id.endsWith("-fast") + ), + ...withOneMillionContext( + [ + { id: "gpt-5.6-luna-max-fast", name: "GPT-5.6 Luna Max Fast" }, + { id: "gpt-5.6-luna-max", name: "GPT-5.6 Luna Max" }, + { id: "gpt-5.6-luna-xhigh-fast", name: "GPT-5.6 Luna Xhigh Fast" }, + { id: "gpt-5.6-luna-xhigh", name: "GPT-5.6 Luna Xhigh" }, + { id: "gpt-5.6-luna-high-fast", name: "GPT-5.6 Luna High Fast" }, + { id: "gpt-5.6-luna-high", name: "GPT-5.6 Luna High" }, + { id: "gpt-5.6-luna-medium-fast", name: "GPT-5.6 Luna Medium Fast" }, + { id: "gpt-5.6-luna-medium", name: "GPT-5.6 Luna Medium" }, + { id: "gpt-5.6-luna-low-fast", name: "GPT-5.6 Luna Low Fast" }, + { id: "gpt-5.6-luna-low", name: "GPT-5.6 Luna Low" }, + { id: "gpt-5.6-luna-none-fast", name: "GPT-5.6 Luna None Fast" }, + { id: "gpt-5.6-luna-none", name: "GPT-5.6 Luna None" }, + ], + "GPT-5.6 Luna", + 272_000, + "gpt-5.6-luna", + (model) => !model.id.endsWith("-fast") + ), + { id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash High" }, + { id: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash Medium" }, + { id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash Low" }, + { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, + { id: "kimi-k3-max", name: "Kimi K3 Max" }, + { id: "kimi-k3-high", name: "Kimi K3 High" }, { id: "kimi-k3-low", name: "Kimi K3 Low" }, - { id: "kimi-k3-max", name: "Kimi K3" }, - { id: "glm-5.2-high", name: "GLM 5.2" }, + { id: "kimi-k2.7-code", name: "Kimi K2.7 Code" }, { id: "glm-5.2-max", name: "GLM 5.2 Max" }, + { id: "glm-5.2-high", name: "GLM 5.2 High" }, ], }; diff --git a/open-sse/config/providers/registry/devin/catalog.ts b/open-sse/config/providers/registry/devin/catalog.ts index d1e5c75894..6ab53fda12 100644 --- a/open-sse/config/providers/registry/devin/catalog.ts +++ b/open-sse/config/providers/registry/devin/catalog.ts @@ -1,115 +1,150 @@ import type { RegistryModel } from "../../shared.ts"; +type EffortVariant = readonly [suffix: string, label: string]; + +const QUALITY_EFFORTS: readonly EffortVariant[] = [ + ["max", "Max"], + ["xhigh", "XHigh"], + ["high", "High"], + ["medium", "Medium"], + ["low", "Low"], +]; + +const GPT_EFFORTS: readonly EffortVariant[] = [ + ["max", "Max Thinking"], + ["xhigh", "XHigh Thinking"], + ["high", "High Thinking"], + ["medium", "Medium Thinking"], + ["low", "Low Thinking"], + ["none", "No Thinking"], +]; + +function model( + id: string, + name: string, + maxOutputTokens?: number, + contextLength?: number +): RegistryModel { + return { + id, + name, + ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }), + ...(contextLength === undefined ? {} : { contextLength }), + }; +} + +function effortModels( + id: string, + name: string, + maxOutputTokens: number, + contextLength: number | undefined, + efforts: readonly EffortVariant[] = QUALITY_EFFORTS +): RegistryModel[] { + return efforts.map(([suffix, label]) => + model(`${id}-${suffix}`, `${name} ${label}`, maxOutputTokens, contextLength) + ); +} + +function fastEffortModels( + id: string, + name: string, + maxOutputTokens: number, + contextLength: number +): RegistryModel[] { + return QUALITY_EFFORTS.flatMap(([suffix, label]) => [ + model(`${id}-${suffix}-fast`, `${name} ${label} Fast`, maxOutputTokens, contextLength), + model(`${id}-${suffix}`, `${name} ${label}`, maxOutputTokens, contextLength), + ]); +} + +function gptModels(id: string, name: string): RegistryModel[] { + return GPT_EFFORTS.flatMap(([suffix, label]) => [ + model(`${id}-${suffix}-priority`, `${name} ${label} Fast`, 128_000, 1_000_000), + model(`${id}-${suffix}`, `${name} ${label}`, 128_000, 1_000_000), + ]); +} + +/** + * Curated from the authenticated `devin models list --format json` response on + * 2026-09-02. Keep this deliberately smaller than Devin's full live catalog: + * these are the operator-selected models OmniRoute intends to expose. + */ export const DEVIN_MODEL_CATALOG: RegistryModel[] = [ - // Cognition / SWE — default model family recommended for coding tasks - { id: "swe-1-7-lightning", name: "SWE-1.7 Lightning", contextLength: 202752 }, - { id: "swe-1-7", name: "SWE-1.7", contextLength: 262000 }, - { id: "swe-1-6-fast", name: "SWE-1.6 Fast" }, - { id: "swe-1-6", name: "SWE-1.6" }, - // Claude Fable 5 - { id: "claude-5-fable-max", name: "Claude Fable 5 Max", contextLength: 1000000 }, - { id: "claude-5-fable-xhigh", name: "Claude Fable 5 XHigh", contextLength: 1000000 }, - { id: "claude-5-fable-high", name: "Claude Fable 5 High", contextLength: 1000000 }, - { id: "claude-5-fable-medium", name: "Claude Fable 5 Medium", contextLength: 1000000 }, - { id: "claude-5-fable-low", name: "Claude Fable 5 Low", contextLength: 1000000 }, - // Claude Opus 5 - { id: "claude-opus-5-max", name: "Claude Opus 5 Max", contextLength: 1000000 }, - { id: "claude-opus-5-xhigh", name: "Claude Opus 5 XHigh", contextLength: 1000000 }, - { id: "claude-opus-5-high", name: "Claude Opus 5 High", contextLength: 1000000 }, - { id: "claude-opus-5-medium", name: "Claude Opus 5 Medium", contextLength: 1000000 }, - { id: "claude-opus-5-low", name: "Claude Opus 5 Low", contextLength: 1000000 }, - // Claude Opus 4.8 - { id: "claude-opus-4-8-max", name: "Claude Opus 4.8 Max", contextLength: 1000000 }, - { id: "claude-opus-4-8-xhigh", name: "Claude Opus 4.8 XHigh", contextLength: 1000000 }, - { id: "claude-opus-4-8-high", name: "Claude Opus 4.8 High", contextLength: 1000000 }, - { id: "claude-opus-4-8-medium", name: "Claude Opus 4.8 Medium", contextLength: 1000000 }, - { id: "claude-opus-4-8-low", name: "Claude Opus 4.8 Low", contextLength: 1000000 }, - // Claude Opus 4.7 - { id: "claude-opus-4-7-max", name: "Claude Opus 4.7 Max", contextLength: 1000000 }, - { id: "claude-opus-4-7-xhigh", name: "Claude Opus 4.7 XHigh", contextLength: 1000000 }, - { id: "claude-opus-4-7-high", name: "Claude Opus 4.7 High", contextLength: 1000000 }, - { id: "claude-opus-4-7-medium", name: "Claude Opus 4.7 Medium", contextLength: 1000000 }, - { id: "claude-opus-4-7-low", name: "Claude Opus 4.7 Low", contextLength: 1000000 }, - // Claude Opus 4.6 - { - id: "claude-opus-4-6-thinking-1m", - name: "Claude Opus 4.6 Thinking 1M", - contextLength: 1000000, - }, - { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking", contextLength: 200000 }, - { id: "claude-opus-4-6-1m", name: "Claude Opus 4.6 1M", contextLength: 1000000 }, - { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextLength: 200000 }, - // Claude Sonnet 5 - { id: "claude-sonnet-5-max", name: "Claude Sonnet 5 Max", contextLength: 1000000 }, - { id: "claude-sonnet-5-xhigh", name: "Claude Sonnet 5 XHigh", contextLength: 1000000 }, - { id: "claude-sonnet-5-high", name: "Claude Sonnet 5 High", contextLength: 1000000 }, - { id: "claude-sonnet-5-medium", name: "Claude Sonnet 5 Medium", contextLength: 1000000 }, - { id: "claude-sonnet-5-low", name: "Claude Sonnet 5 Low", contextLength: 1000000 }, - // Claude Sonnet 4.6 - { - id: "claude-sonnet-4-6-thinking-1m", - name: "Claude Sonnet 4.6 Thinking 1M", - contextLength: 1000000, - }, - { - id: "claude-sonnet-4-6-thinking", - name: "Claude Sonnet 4.6 Thinking", - contextLength: 200000, - }, - { id: "claude-sonnet-4-6-1m", name: "Claude Sonnet 4.6 1M", contextLength: 1000000 }, - { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextLength: 200000 }, - // GPT-5.6 - { id: "gpt-5-6-sol-max", name: "GPT-5.6 Sol Max", contextLength: 1000000 }, - { id: "gpt-5-6-sol-xhigh", name: "GPT-5.6 Sol XHigh", contextLength: 1000000 }, - { id: "gpt-5-6-sol-high", name: "GPT-5.6 Sol High", contextLength: 1000000 }, - { id: "gpt-5-6-sol-medium", name: "GPT-5.6 Sol Medium", contextLength: 1000000 }, - { id: "gpt-5-6-sol-low", name: "GPT-5.6 Sol Low", contextLength: 1000000 }, - /// Terra - { id: "gpt-5-6-terra-max", name: "GPT-5.6 Terra Max", contextLength: 1000000 }, - { id: "gpt-5-6-terra-xhigh", name: "GPT-5.6 Terra XHigh", contextLength: 1000000 }, - { id: "gpt-5-6-terra-high", name: "GPT-5.6 Terra High", contextLength: 1000000 }, - { id: "gpt-5-6-terra-medium", name: "GPT-5.6 Terra Medium", contextLength: 1000000 }, - { id: "gpt-5-6-terra-low", name: "GPT-5.6 Terra Low", contextLength: 1000000 }, - /// Luna - { id: "gpt-5-6-luna-max", name: "GPT-5.6 Luna Max", contextLength: 1000000 }, - { id: "gpt-5-6-luna-xhigh", name: "GPT-5.6 Luna XHigh", contextLength: 1000000 }, - { id: "gpt-5-6-luna-high", name: "GPT-5.6 Luna High", contextLength: 1000000 }, - { id: "gpt-5-6-luna-medium", name: "GPT-5.6 Luna Medium", contextLength: 1000000 }, - { id: "gpt-5-6-luna-low", name: "GPT-5.6 Luna Low", contextLength: 1000000 }, - // GPT-5.5 - { id: "gpt-5-5-xhigh", name: "GPT-5.5 XHigh", contextLength: 272000 }, - { id: "gpt-5-5-high", name: "GPT-5.5 High", contextLength: 272000 }, - { id: "gpt-5-5-medium", name: "GPT-5.5 Medium", contextLength: 272000 }, - { id: "gpt-5-5-low", name: "GPT-5.5 Low", contextLength: 272000 }, - // Gemini - { id: "gemini-3-1-pro-high", name: "Gemini 3.1 Pro High", contextLength: 1048576 }, - { id: "gemini-3-1-pro-low", name: "Gemini 3.1 Pro Low", contextLength: 1048576 }, - { id: "gemini-3-7-flash-high", name: "Gemini 3.7 Flash High" }, - { id: "gemini-3-7-flash-medium", name: "Gemini 3.7 Flash Medium" }, - { id: "gemini-3-7-flash-low", name: "Gemini 3.7 Flash Low" }, - { id: "gemini-3-7-flash-minimal", name: "Gemini 3.7 Flash Minimal" }, - // Grok - { id: "grok-4-5-high", name: "Grok 4.5 High", contextLength: 500000 }, - { id: "grok-4-5-medium", name: "Grok 4.5 Medium", contextLength: 500000 }, - { id: "grok-4-5-low", name: "Grok 4.5 Low", contextLength: 500000 }, - // GLM - { id: "glm-5-2-max-1m", name: "GLM-5.2 Max 1M", contextLength: 1000000 }, - { id: "glm-5-2-max", name: "GLM-5.2 Max" }, - { id: "glm-5-2-1m", name: "GLM-5.2 High 1M", contextLength: 1000000 }, - { id: "glm-5-2", name: "GLM-5.2 High" }, - // Kimi - { id: "kimi-k3-max", name: "Kimi K3 Max" }, - { id: "kimi-k3-high", name: "Kimi K3 High" }, - { id: "kimi-k3-low", name: "Kimi K3 Low" }, - { id: "kimi-k2-7", name: "Kimi K2.7", contextLength: 262144 }, - // Inkling - { id: "inkling-max", name: "Inkling Max" }, - { id: "inkling-xhigh", name: "Inkling XHigh" }, - { id: "inkling-high", name: "Inkling High" }, - { id: "inkling-medium", name: "Inkling Medium" }, - { id: "inkling-low", name: "Inkling Low" }, - { id: "inkling-none", name: "Inkling None" }, - // Others - { id: "deepseek-v4", name: "DeepSeek V4 Pro", contextLength: 1048576 }, - { id: "nemotron-3-ultra-nvfp4", name: "Nemotron 3 Ultra", contextLength: 262144 }, + ...effortModels("claude-fable-5-1", "Claude Fable 5.1", 128_000, 1_000_000), + ...fastEffortModels("claude-opus-5", "Claude Opus 5", 128_000, 1_000_000), + ...fastEffortModels("claude-opus-4-8", "Claude Opus 4.8", 128_000, 1_000_000), + ...effortModels("claude-sonnet-5", "Claude Sonnet 5", 128_000, 1_000_000), + + model("claude-sonnet-4-6-thinking-1m", "Claude Sonnet 4.6 Thinking 1M", 128_000, 1_000_000), + model("claude-sonnet-4-6-1m", "Claude Sonnet 4.6 1M", 128_000, 1_000_000), + model("claude-sonnet-4-6-thinking", "Claude Sonnet 4.6 Thinking", 128_000, 200_000), + model("claude-sonnet-4-6", "Claude Sonnet 4.6", 128_000, 200_000), + model("MODEL_PRIVATE_11", "Claude Haiku 4.5", 64_000, 200_000), + + ...gptModels("gpt-5-6-sol", "GPT-5.6 Sol"), + ...gptModels("gpt-5-6-terra", "GPT-5.6 Terra"), + ...gptModels("gpt-5-6-luna", "GPT-5.6 Luna"), + + ...effortModels("kimi-k3", "Kimi K3", 131_072, 1_048_576, [ + ["max", "Max"], + ["high", "High"], + ["low", "Low"], + ]), + model("kimi-k2-7", "Kimi K2.7", 16_000, 262_144), + + ...effortModels("glm-5-3", "GLM-5.3", 128_000, 1_000_000, [ + ["max", "Max"], + ["high", "High"], + ["low", "Low"], + ]), + ...effortModels("glm-5-3-flash", "GLM-5.3 Flash", 128_000, 1_000_000, [ + ["max", "Max"], + ["high", "High"], + ["low", "Low"], + ]), + + model("swe-1-7", "SWE-1.7 Max", 128_000, 262_000), + model("swe-1-7-medium", "SWE-1.7 Medium", 128_000, 262_000), + model("swe-1-7-lightning", "SWE-1.7 Lightning Max", 96_000, 202_752), + model("swe-1-7-lightning-medium", "SWE-1.7 Lightning Medium", 96_000, 202_752), + model("adaptive", "Adaptive"), + + ...effortModels("grok-4-6", "Grok 4.6", 100_000, 500_000, [ + ["xhigh", "XHigh"], + ["high", "High"], + ["medium", "Medium"], + ["low", "Low"], + ]), + ...effortModels("inkling", "Inkling", 131_072, undefined, [ + ["max", "Max"], + ["xhigh", "X-High"], + ["high", "High"], + ["medium", "Medium"], + ["low", "Low"], + ["none", "None"], + ]), + ...effortModels("deepseek-v4-flash", "DeepSeek V4 Flash", 384_000, 1_000_000, [ + ["max", "Max"], + ["high", "High"], + ["low", "Low"], + ]), + ...effortModels("nemotron-3-ultra", "Nemotron 3 Ultra", 32_768, 262_144, [ + ["high", "High"], + ["medium", "Medium"], + ["none", "None"], + ]), + ...effortModels("gemini-3-7-flash", "Gemini 3.7 Flash", 65_535, 1_048_576, [ + ["high", "High"], + ["medium", "Medium"], + ["low", "Low"], + ]), + ...effortModels("gemini-3-1-pro", "Gemini 3.1 Pro", 65_535, 1_048_576, [ + ["high", "High Thinking"], + ["low", "Low Thinking"], + ]), + ...effortModels("deepseek-v4-pro", "DeepSeek V4 Pro", 384_000, 1_000_000, [ + ["max", "Max"], + ["high", "High"], + ["low", "Low"], + ]), ]; diff --git a/open-sse/config/providers/registry/vertex/index.ts b/open-sse/config/providers/registry/vertex/index.ts index 1eac20fc15..4b72b9f7c4 100644 --- a/open-sse/config/providers/registry/vertex/index.ts +++ b/open-sse/config/providers/registry/vertex/index.ts @@ -27,6 +27,7 @@ export const vertexProvider: RegistryEntry = { { id: "DeepSeek-V4-Pro", name: "DeepSeek V4 Pro (Vertex Partner)" }, { id: "Qwen3.6-35B-A3B", name: "Qwen3.6 35B A3B (Vertex Partner)" }, { id: "GLM-5.1-FP8", name: "GLM-5.1 (Vertex Partner)" }, + { id: "claude-fable-5-1", name: "Claude Fable 5.1 (Vertex)", targetFormat: "claude" }, { id: "claude-fable-5", name: "Claude Fable 5 (Vertex)", targetFormat: "claude" }, { id: "claude-opus-5", name: "Claude Opus 5 (Vertex)", targetFormat: "claude" }, { id: "claude-sonnet-5", name: "Claude Sonnet 5 (Vertex)", targetFormat: "claude" }, diff --git a/open-sse/config/providers/registry/vertex/partner/index.ts b/open-sse/config/providers/registry/vertex/partner/index.ts index 4cdcd5d0b5..6524cd6a2b 100644 --- a/open-sse/config/providers/registry/vertex/partner/index.ts +++ b/open-sse/config/providers/registry/vertex/partner/index.ts @@ -13,6 +13,7 @@ export const vertex_partnerProvider: RegistryEntry = { { id: "DeepSeek-V4-Pro", name: "DeepSeek V4 Pro" }, { id: "Qwen3.6-35B-A3B", name: "Qwen 3.6 35B A3B" }, { id: "GLM-5.1-FP8", name: "GLM 5.1" }, + { id: "claude-fable-5-1", name: "Claude Fable 5.1", targetFormat: "claude" }, { id: "claude-fable-5", name: "Claude Fable 5", targetFormat: "claude" }, { id: "claude-opus-5", name: "Claude Opus 5", targetFormat: "claude" }, { id: "claude-sonnet-5", name: "Claude Sonnet 5", targetFormat: "claude" }, diff --git a/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts b/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts index b1d8d31b36..b3f719558f 100644 --- a/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts +++ b/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts @@ -59,11 +59,9 @@ describe("#11489 resolveScoresAs", () => { expect(resolveScoresAs("claude-sonnet-5")).toEqual({ base: "claude-sonnet-5", via: null }); }); - it("resolves the cursor/agy spelling of a Claude model to its canonical id", () => { - // `claude-4.6-opus-high` strips to `claude-4.6-opus`, which is not a catalog - // id — the canonical spelling is `claude-opus-4-6`. Explicit registry data. - expect(resolveScoresAs("claude-4.6-opus-high")).toEqual({ - base: "claude-opus-4-6", + it("resolves curated Cursor Claude variants to their canonical ids", () => { + expect(resolveScoresAs("claude-fable-5-1-thinking-high")).toEqual({ + base: "claude-fable-5-1", via: "explicit", }); expect(resolveScoresAs("claude-4.6-sonnet-medium")).toEqual({ diff --git a/open-sse/services/modelFamilyFallback.ts b/open-sse/services/modelFamilyFallback.ts index 5e80fe0d40..6226c1217b 100644 --- a/open-sse/services/modelFamilyFallback.ts +++ b/open-sse/services/modelFamilyFallback.ts @@ -78,8 +78,9 @@ const FAMILY_FALLBACK_TEMPLATES: Record = { "gemini-2.5-pro": ["gemini-2.5-pro-preview-06-05", "gemini-2.5-pro-exp-03-25"], "gemini-2.5-pro-preview-06-05": ["gemini-2.5-pro", "gemini-2.5-pro-exp-03-25"], - // Claude Mythos family (Fable 5) — flagship falls to the next-best Opus - // tiers before the cheaper Sonnet, matching the Opus family ordering. + // Claude Mythos family — prefer the previous Fable before falling to Opus + // tiers and then the cheaper Sonnet, matching the flagship ordering. + "claude-fable-5-1": ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"], "claude-fable-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-sonnet-5"], // Claude Opus family diff --git a/open-sse/services/providerCostData.ts b/open-sse/services/providerCostData.ts index d9a4e0a8ab..8464635c92 100644 --- a/open-sse/services/providerCostData.ts +++ b/open-sse/services/providerCostData.ts @@ -1,4 +1,4 @@ -import type { TierAssignment } from "./tierTypes"; +import { getPricingForModel as getDefaultPricingForModel } from "@/shared/constants/pricing"; import type { TierConfig } from "./tierTypes"; export interface ModelPricing { @@ -11,6 +11,7 @@ export interface ModelPricing { export const KNOWN_MODEL_PRICING: Record = { "gpt-4o": { inputCostPer1M: 2.5, outputCostPer1M: 10.0, isFree: false }, "gpt-4o-mini": { inputCostPer1M: 0.15, outputCostPer1M: 0.6, isFree: false }, + "claude-fable-5-1": { inputCostPer1M: 10.0, outputCostPer1M: 50.0, isFree: false }, "claude-fable-5": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false }, "claude-opus-5": { inputCostPer1M: 5.0, outputCostPer1M: 25.0, isFree: false }, "claude-opus-4-8": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false }, @@ -37,14 +38,26 @@ export const KNOWN_MODEL_PRICING: Record = { }; export function getModelPricing(provider: string, model: string): ModelPricing { - const directKey = model.toLowerCase(); - if (KNOWN_MODEL_PRICING[directKey]) { - return KNOWN_MODEL_PRICING[directKey]; - } const providerKey = `${provider}/${model}`.toLowerCase(); if (KNOWN_MODEL_PRICING[providerKey]) { return KNOWN_MODEL_PRICING[providerKey]; } + const providerPricing = getDefaultPricingForModel(provider, model); + if (providerPricing) { + const inputCostPer1M = Number(providerPricing.input); + const outputCostPer1M = Number(providerPricing.output); + if (Number.isFinite(inputCostPer1M) && Number.isFinite(outputCostPer1M)) { + return { + inputCostPer1M, + outputCostPer1M, + isFree: inputCostPer1M === 0 && outputCostPer1M === 0, + }; + } + } + const directKey = model.toLowerCase(); + if (KNOWN_MODEL_PRICING[directKey]) { + return KNOWN_MODEL_PRICING[directKey]; + } return { inputCostPer1M: 5.0, outputCostPer1M: 15.0, isFree: false }; } diff --git a/open-sse/utils/cursorAgentProtobuf.ts b/open-sse/utils/cursorAgentProtobuf.ts index 21164b6ed1..0e3f642682 100644 --- a/open-sse/utils/cursorAgentProtobuf.ts +++ b/open-sse/utils/cursorAgentProtobuf.ts @@ -24,6 +24,10 @@ import { encodeSelectedImageBody, type EncodedImage, } from "./cursorAgentProtobuf/imageEncoding.ts"; +import { + CURSOR_EFFORT_SUFFIXES, + resolveOneMillionContextModel, +} from "./cursorAgentProtobuf/requestedModelParameters.ts"; import { WT_VARINT, WT_LEN, @@ -41,6 +45,7 @@ import { findField, decodeStringField, decodeVarintField, + type Field, } from "./cursorAgentProtobuf/wire.ts"; // ─── Field numbers (from agent.proto descriptor) ─────────────────────────── @@ -311,8 +316,6 @@ export function normalizeCursorModelId(modelId: string): string { // Grok (`cursor-grok-*` / legacy `grok-*`) follows the Claude-style `effort` // parameter. Without the split, ids like `cursor-grok-4.5-high` return empty // turns (same symptom as #7289). Combined `-high-fast` is supported. -const CURSOR_EFFORT_SUFFIXES = ["low", "medium", "high", "xhigh", "max"] as const; - /** * If `normalized` starts with `prefix` and ends with one of the known effort * suffixes, split it into the base model id plus a `{id: paramId, value}` @@ -432,6 +435,8 @@ export function resolveRequestedModel( }; } } + const oneMillionContext = resolveOneMillionContextModel(normalized); + if (oneMillionContext) return oneMillionContext; // Live catalog is authoritative for exact ids (flattened effort variants). if (opts?.liveCatalogIds?.has(normalized)) { return { modelId: normalized, parameters: [] }; @@ -652,6 +657,41 @@ export type DecodedDelta = | { kind: "kv_server_message" } | { kind: "unknown"; field: number }; +type InteractionUpdateDecoder = (field: Field) => DecodedDelta[]; + +const INTERACTION_UPDATE_DECODERS: Partial> = { + [IU_TEXT_DELTA]: (field) => + field.wireType === WT_LEN + ? [{ kind: "text", text: decodeStringField(field.bytes, TDU_TEXT) }] + : [], + [IU_THINKING_DELTA]: (field) => + field.wireType === WT_LEN + ? [{ kind: "thinking", text: decodeStringField(field.bytes, TDU_TEXT) }] + : [], + [IU_THINKING_COMPLETED]: () => [{ kind: "thinking_complete" }], + [IU_TOOL_CALL_STARTED]: () => [{ kind: "tool_call_started" }], + [IU_TOOL_CALL_COMPLETED]: (field) => { + const deltas: DecodedDelta[] = []; + if (field.wireType === WT_LEN) { + const todoWrite = decodeNativeTodoWriteCompletion(field.bytes); + if (todoWrite) deltas.push(todoWrite); + } + deltas.push({ kind: "tool_call_completed" }); + return deltas; + }, + [IU_TOKEN_DELTA]: (field) => + field.wireType === WT_LEN + ? [{ kind: "token_delta", tokens: decodeVarintField(field.bytes, 1) }] + : [], + [IU_HEARTBEAT]: () => [{ kind: "heartbeat" }], + [IU_TURN_ENDED]: () => [{ kind: "turn_ended" }], +}; + +function decodeInteractionUpdate(field: Field): DecodedDelta[] { + const decoder = INTERACTION_UPDATE_DECODERS[field.fieldNumber]; + return decoder ? decoder(field) : [{ kind: "unknown", field: field.fieldNumber }]; +} + export function decodeAgentServerMessage(payload: Buffer): DecodedDelta[] { const out: DecodedDelta[] = []; for (const top of decodeFields(payload)) { @@ -661,45 +701,7 @@ export function decodeAgentServerMessage(payload: Buffer): DecodedDelta[] { } if (top.fieldNumber !== ASM_INTERACTION_UPDATE || top.wireType !== 2) continue; for (const update of decodeFields(top.bytes)) { - if (update.wireType !== 2 && update.wireType !== 0) continue; - switch (update.fieldNumber) { - case IU_TEXT_DELTA: - if (update.wireType === 2) { - out.push({ kind: "text", text: decodeStringField(update.bytes, TDU_TEXT) }); - } - break; - case IU_THINKING_DELTA: - if (update.wireType === 2) { - out.push({ kind: "thinking", text: decodeStringField(update.bytes, TDU_TEXT) }); - } - break; - case IU_THINKING_COMPLETED: - out.push({ kind: "thinking_complete" }); - break; - case IU_TOOL_CALL_STARTED: - out.push({ kind: "tool_call_started" }); - break; - case IU_TOOL_CALL_COMPLETED: - if (update.wireType === 2) { - const todoWrite = decodeNativeTodoWriteCompletion(update.bytes); - if (todoWrite) out.push(todoWrite); - } - out.push({ kind: "tool_call_completed" }); - break; - case IU_TOKEN_DELTA: - if (update.wireType === 2) { - out.push({ kind: "token_delta", tokens: decodeVarintField(update.bytes, 1) }); - } - break; - case IU_HEARTBEAT: - out.push({ kind: "heartbeat" }); - break; - case IU_TURN_ENDED: - out.push({ kind: "turn_ended" }); - break; - default: - out.push({ kind: "unknown", field: update.fieldNumber }); - } + out.push(...decodeInteractionUpdate(update)); } } return out; @@ -750,52 +752,44 @@ export type KvServerEvent = requestMetadata: Buffer | null; }; +function findLengthDelimitedField(fields: Field[], fieldNumber: number): Buffer | null { + const field = findField(fields, fieldNumber); + return field?.wireType === WT_LEN ? field.bytes : null; +} + +function decodeBlobId(payload: Buffer, fieldNumber: number): Buffer { + return findLengthDelimitedField(decodeFields(payload), fieldNumber) ?? Buffer.alloc(0); +} + +function decodeSetBlobArgs(payload: Buffer): { blobId: Buffer; blobData: Buffer } { + const fields = decodeFields(payload); + return { + blobId: findLengthDelimitedField(fields, SBA_BLOB_ID) ?? Buffer.alloc(0), + blobData: findLengthDelimitedField(fields, SBA_BLOB_DATA) ?? Buffer.alloc(0), + }; +} + export function decodeKvServerEvent(payload: Buffer): KvServerEvent | null { - for (const top of decodeFields(payload)) { - if (top.fieldNumber !== ASM_KV_SERVER_MESSAGE || top.wireType !== 2) continue; + const top = findField(decodeFields(payload), ASM_KV_SERVER_MESSAGE); + if (top?.wireType !== WT_LEN) return null; - let kvId = 0; - let getBlobArgs: Buffer | null = null; - let setBlobArgs: Buffer | null = null; - let requestMetadata: Buffer | null = null; - - for (const f of decodeFields(top.bytes)) { - if (f.fieldNumber === KSM_ID && f.wireType === 0) { - kvId = Number(f.varint); - } else if (f.fieldNumber === KSM_GET_BLOB_ARGS && f.wireType === 2) { - getBlobArgs = f.bytes; - } else if (f.fieldNumber === KSM_SET_BLOB_ARGS && f.wireType === 2) { - setBlobArgs = f.bytes; - } else if (f.fieldNumber === KSM_REQUEST_METADATA && f.wireType === 2) { - requestMetadata = f.bytes; - } - } - - if (getBlobArgs) { - // GetBlobArgs { blob_id (1): bytes } - let blobId: Buffer = Buffer.alloc(0); - for (const f of decodeFields(getBlobArgs)) { - if (f.fieldNumber === GBA_BLOB_ID && f.wireType === 2) { - blobId = f.bytes; - } - } - return { kind: "kv_get_blob", kvId, blobId, requestMetadata }; - } - if (setBlobArgs) { - // SetBlobArgs { blob_id (1): bytes, blob_data (2): bytes } - let blobId: Buffer = Buffer.alloc(0); - let blobData: Buffer = Buffer.alloc(0); - for (const f of decodeFields(setBlobArgs)) { - if (f.fieldNumber === SBA_BLOB_ID && f.wireType === 2) { - blobId = f.bytes; - } else if (f.fieldNumber === SBA_BLOB_DATA && f.wireType === 2) { - blobData = f.bytes; - } - } - return { kind: "kv_set_blob", kvId, blobId, blobData, requestMetadata }; - } + const fields = decodeFields(top.bytes); + const idField = findField(fields, KSM_ID); + const kvId = idField?.wireType === WT_VARINT ? Number(idField.varint) : 0; + const requestMetadata = findLengthDelimitedField(fields, KSM_REQUEST_METADATA); + const getBlobArgs = findLengthDelimitedField(fields, KSM_GET_BLOB_ARGS); + if (getBlobArgs) { + return { + kind: "kv_get_blob", + kvId, + blobId: decodeBlobId(getBlobArgs, GBA_BLOB_ID), + requestMetadata, + }; } - return null; + + const setBlobArgs = findLengthDelimitedField(fields, KSM_SET_BLOB_ARGS); + if (!setBlobArgs) return null; + return { kind: "kv_set_blob", kvId, ...decodeSetBlobArgs(setBlobArgs), requestMetadata }; } // ─── Phase 2: full ExecServerMessage variant decoder ─────────────────────── @@ -886,143 +880,121 @@ function decodeShellArgs(payload: Buffer): DecodedShellArgs { return decoded; } -export function decodeExecServerEvent(payload: Buffer): ExecServerEvent | null { - for (const top of decodeFields(payload)) { - if (top.fieldNumber !== ASM_EXEC_SERVER_MESSAGE || top.wireType !== 2) continue; +type ExecEventContext = { + execMsgId: number; + execId: string; + variantBytes: Buffer; +}; - let execMsgId = 0; - let execId = ""; - let variantField = 0; - let variantBytes: Buffer | null = null; +type ExecEventDecoder = (context: ExecEventContext) => ExecServerEvent; +type PathExecKind = "exec_read" | "exec_write" | "exec_delete" | "exec_ls"; +type ShellExecKind = "exec_shell" | "exec_shell_stream" | "exec_bg_shell"; - for (const f of decodeFields(top.bytes)) { - if (f.fieldNumber === ESM_ID && f.wireType === 0) { - execMsgId = Number(f.varint); - } else if (f.fieldNumber === ESM_EXEC_ID && f.wireType === 2) { - execId = f.bytes.toString("utf8"); - } else if (f.wireType === 2) { - // Any other LEN field is the variant payload. Take the first one we - // see — variants don't co-occur in a well-formed message. - if (variantField === 0) { - variantField = f.fieldNumber; - variantBytes = f.bytes; - } - } - } +function createPathExecEvent(kind: PathExecKind, context: ExecEventContext): ExecServerEvent { + return { + kind, + execMsgId: context.execMsgId, + execId: context.execId, + path: decodeStringField(context.variantBytes, ARG_PATH), + }; +} - if (variantBytes === null) continue; +function createShellExecEvent(kind: ShellExecKind, context: ExecEventContext): ExecServerEvent { + return { + kind, + execMsgId: context.execMsgId, + execId: context.execId, + ...decodeShellArgs(context.variantBytes), + }; +} - switch (variantField) { - case ESM_REQUEST_CONTEXT_ARGS: - return { kind: "exec_request_context", execMsgId, execId }; - case ESM_READ_ARGS: - return { - kind: "exec_read", - execMsgId, - execId, - path: decodeStringField(variantBytes, ARG_PATH), - }; - case ESM_WRITE_ARGS: - return { - kind: "exec_write", - execMsgId, - execId, - path: decodeStringField(variantBytes, ARG_PATH), - }; - case ESM_DELETE_ARGS: - return { - kind: "exec_delete", - execMsgId, - execId, - path: decodeStringField(variantBytes, ARG_PATH), - }; - case ESM_LS_ARGS: - return { - kind: "exec_ls", - execMsgId, - execId, - path: decodeStringField(variantBytes, ARG_PATH), - }; - case ESM_GREP_ARGS: - return { kind: "exec_grep", execMsgId, execId }; - case ESM_DIAGNOSTICS_ARGS: - return { kind: "exec_diagnostics", execMsgId, execId }; - case ESM_SHELL_ARGS: { - const shell = decodeShellArgs(variantBytes); - return { - kind: "exec_shell", - execMsgId, - execId, - ...shell, - }; - } - case ESM_SHELL_STREAM_ARGS: { - const shell = decodeShellArgs(variantBytes); - return { - kind: "exec_shell_stream", - execMsgId, - execId, - ...shell, - }; - } - case ESM_BACKGROUND_SHELL_SPAWN: { - const shell = decodeShellArgs(variantBytes); - return { - kind: "exec_bg_shell", - execMsgId, - execId, - ...shell, - }; - } - case ESM_FETCH_ARGS: - return { - kind: "exec_fetch", - execMsgId, - execId, - url: decodeStringField(variantBytes, ARG_FETCH_URL), - }; - case ESM_WRITE_SHELL_STDIN_ARGS: - return { kind: "exec_write_shell_stdin", execMsgId, execId }; - case ESM_MCP_ARGS: { - // McpArgs.args is map; each value is a protobuf- - // encoded google.protobuf.Value. Decode keys and value-bytes here, - // then convert each Value to its JSON shape. - let toolName = ""; - let toolCallId = ""; - const args: Record = {}; - for (const f of decodeFields(variantBytes)) { - if (f.wireType !== 2) continue; - if (f.fieldNumber === MCA_TOOL_NAME) { - toolName = f.bytes.toString("utf8"); - } else if (f.fieldNumber === MCA_NAME && !toolName) { - // tool_name (5) takes precedence; fall back to name (1) - toolName = f.bytes.toString("utf8"); - } else if (f.fieldNumber === MCA_TOOL_CALL_ID) { - toolCallId = f.bytes.toString("utf8"); - } else if (f.fieldNumber === MCA_ARGS) { - // FieldsEntry { key (1): string, value (2): bytes } - let key = ""; - let valueBytes: Buffer | null = null; - for (const entry of decodeFields(f.bytes)) { - if (entry.fieldNumber === MAP_KEY && entry.wireType === 2) { - key = entry.bytes.toString("utf8"); - } else if (entry.fieldNumber === MAP_VALUE && entry.wireType === 2) { - valueBytes = entry.bytes; - } - } - if (key && valueBytes !== null) { - args[key] = decodeProtobufValue(valueBytes); - } - } - } - return { kind: "exec_mcp", execMsgId, execId, toolName, toolCallId, args }; - } - default: - // Unknown variant — return null so caller can keep buffering. - return null; - } +function decodeMcpMapEntry(payload: Buffer): { key: string; value: unknown } | null { + const fields = decodeFields(payload); + const key = findLengthDelimitedField(fields, MAP_KEY)?.toString("utf8") ?? ""; + const valueBytes = findLengthDelimitedField(fields, MAP_VALUE); + return key && valueBytes ? { key, value: decodeProtobufValue(valueBytes) } : null; +} + +function decodeMcpExecEvent(context: ExecEventContext): ExecServerEvent { + const fields = decodeFields(context.variantBytes); + const canonicalName = findLengthDelimitedField(fields, MCA_TOOL_NAME); + const fallbackName = findLengthDelimitedField(fields, MCA_NAME); + const toolName = (canonicalName ?? fallbackName)?.toString("utf8") ?? ""; + const toolCallId = findLengthDelimitedField(fields, MCA_TOOL_CALL_ID)?.toString("utf8") ?? ""; + const args: Record = {}; + for (const field of fields) { + if (field.fieldNumber !== MCA_ARGS || field.wireType !== WT_LEN) continue; + const entry = decodeMcpMapEntry(field.bytes); + if (entry) args[entry.key] = entry.value; } - return null; + return { + kind: "exec_mcp", + execMsgId: context.execMsgId, + execId: context.execId, + toolName, + toolCallId, + args, + }; +} + +const EXEC_EVENT_DECODERS: Partial> = { + [ESM_REQUEST_CONTEXT_ARGS]: ({ execMsgId, execId }) => ({ + kind: "exec_request_context", + execMsgId, + execId, + }), + [ESM_READ_ARGS]: (context) => createPathExecEvent("exec_read", context), + [ESM_WRITE_ARGS]: (context) => createPathExecEvent("exec_write", context), + [ESM_DELETE_ARGS]: (context) => createPathExecEvent("exec_delete", context), + [ESM_LS_ARGS]: (context) => createPathExecEvent("exec_ls", context), + [ESM_GREP_ARGS]: ({ execMsgId, execId }) => ({ kind: "exec_grep", execMsgId, execId }), + [ESM_DIAGNOSTICS_ARGS]: ({ execMsgId, execId }) => ({ + kind: "exec_diagnostics", + execMsgId, + execId, + }), + [ESM_SHELL_ARGS]: (context) => createShellExecEvent("exec_shell", context), + [ESM_SHELL_STREAM_ARGS]: (context) => createShellExecEvent("exec_shell_stream", context), + [ESM_BACKGROUND_SHELL_SPAWN]: (context) => createShellExecEvent("exec_bg_shell", context), + [ESM_FETCH_ARGS]: ({ execMsgId, execId, variantBytes }) => ({ + kind: "exec_fetch", + execMsgId, + execId, + url: decodeStringField(variantBytes, ARG_FETCH_URL), + }), + [ESM_WRITE_SHELL_STDIN_ARGS]: ({ execMsgId, execId }) => ({ + kind: "exec_write_shell_stdin", + execMsgId, + execId, + }), + [ESM_MCP_ARGS]: decodeMcpExecEvent, +}; + +function decodeExecEventContext( + payload: Buffer +): (ExecEventContext & { variantField: number }) | null { + const top = findField(decodeFields(payload), ASM_EXEC_SERVER_MESSAGE); + if (top?.wireType !== WT_LEN) return null; + + const fields = decodeFields(top.bytes); + const idField = findField(fields, ESM_ID); + const variant = fields.find( + (field) => field.wireType === WT_LEN && field.fieldNumber !== ESM_EXEC_ID + ); + if (!variant || variant.wireType !== WT_LEN) return null; + return { + execMsgId: idField?.wireType === WT_VARINT ? Number(idField.varint) : 0, + execId: findLengthDelimitedField(fields, ESM_EXEC_ID)?.toString("utf8") ?? "", + variantField: variant.fieldNumber, + variantBytes: variant.bytes, + }; +} + +export function decodeExecServerEvent(payload: Buffer): ExecServerEvent | null { + const context = decodeExecEventContext(payload); + if (!context) return null; + const decoder = EXEC_EVENT_DECODERS[context.variantField]; + return decoder?.(context) ?? null; } /** @@ -1316,6 +1288,87 @@ export function jsonSchemaToProtobufValue(json: unknown): Buffer { * Handles all six Value variants: null, number (double), string, bool, * struct (object), list (array). Unknown fields are skipped. */ +type ProtobufValueDecodeResult = { value: unknown; nextPos: number }; +type ProtobufValueDecoder = ( + buf: Buffer, + pos: number, + wireType: number +) => ProtobufValueDecodeResult; + +function readLengthDelimitedPayload( + buf: Buffer, + pos: number, + wireType: number +): { payload: Buffer; nextPos: number } | null { + if (wireType !== WT_LEN) return null; + const [len, afterLength] = decodeVarint(buf, pos); + const lenN = checkedLen(len, afterLength, buf); + return { + payload: buf.subarray(afterLength, afterLength + lenN), + nextPos: afterLength + lenN, + }; +} + +function decodeNullValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult { + const nextPos = wireType === WT_VARINT ? decodeVarint(buf, pos)[1] : pos; + return { value: null, nextPos }; +} + +function decodeNumberValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult { + const valid = wireType === 1 && pos + 8 <= buf.length; + return { value: valid ? buf.readDoubleLE(pos) : 0, nextPos: valid ? pos + 8 : pos }; +} + +function decodeStringValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult { + const decoded = readLengthDelimitedPayload(buf, pos, wireType); + return { + value: decoded?.payload.toString("utf8") ?? "", + nextPos: decoded?.nextPos ?? pos, + }; +} + +function decodeBoolValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult { + if (wireType !== WT_VARINT) return { value: false, nextPos: pos }; + const [value, nextPos] = decodeVarint(buf, pos); + return { value: value !== 0n, nextPos }; +} + +function decodeStructValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult { + const decoded = readLengthDelimitedPayload(buf, pos, wireType); + return { + value: decoded ? decodeProtobufStruct(decoded.payload) : {}, + nextPos: decoded?.nextPos ?? pos, + }; +} + +function decodeListValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult { + const decoded = readLengthDelimitedPayload(buf, pos, wireType); + return { + value: decoded ? decodeProtobufList(decoded.payload) : [], + nextPos: decoded?.nextPos ?? pos, + }; +} + +const PROTOBUF_VALUE_DECODERS: Partial> = { + [VAL_NULL]: decodeNullValue, + [VAL_NUMBER]: decodeNumberValue, + [VAL_STRING]: decodeStringValue, + [VAL_BOOL]: decodeBoolValue, + [VAL_STRUCT]: decodeStructValue, + [VAL_LIST]: decodeListValue, +}; + +function skipUnknownProtobufField(buf: Buffer, pos: number, wireType: number): number { + if (wireType === WT_VARINT) return decodeVarint(buf, pos)[1]; + if (wireType === WT_LEN) { + const [len, afterLength] = decodeVarint(buf, pos); + return afterLength + checkedLen(len, afterLength, buf); + } + if (wireType === 1) return pos + 8; + if (wireType === 5) return pos + 4; + return pos; +} + export function decodeProtobufValue(buf: Buffer): unknown { let pos = 0; while (pos < buf.length) { @@ -1323,97 +1376,26 @@ export function decodeProtobufValue(buf: Buffer): unknown { pos = np; const fieldNumber = Number(t >> 3n); const wireType = Number(t & 0x7n); - switch (fieldNumber) { - case VAL_NULL: { - if (wireType === WT_VARINT) { - [, pos] = decodeVarint(buf, pos); - } - return null; - } - case VAL_NUMBER: { - if (wireType === 1 && pos + 8 <= buf.length) { - const value = buf.readDoubleLE(pos); - pos += 8; - return value; - } - return 0; - } - case VAL_STRING: { - if (wireType === WT_LEN) { - const [len, np2] = decodeVarint(buf, pos); - pos = np2; - const lenN = checkedLen(len, pos, buf); - const value = buf.subarray(pos, pos + lenN).toString("utf8"); - pos += lenN; - return value; - } - return ""; - } - case VAL_BOOL: { - if (wireType === WT_VARINT) { - const [val, np2] = decodeVarint(buf, pos); - pos = np2; - return val !== 0n; - } - return false; - } - case VAL_STRUCT: { - if (wireType === WT_LEN) { - const [len, np2] = decodeVarint(buf, pos); - pos = np2; - const lenN = checkedLen(len, pos, buf); - const inner = buf.subarray(pos, pos + lenN); - pos += lenN; - return decodeProtobufStruct(inner); - } - return {}; - } - case VAL_LIST: { - if (wireType === WT_LEN) { - const [len, np2] = decodeVarint(buf, pos); - pos = np2; - const lenN = checkedLen(len, pos, buf); - const inner = buf.subarray(pos, pos + lenN); - pos += lenN; - return decodeProtobufList(inner); - } - return []; - } - default: - // Skip unknown field - if (wireType === WT_VARINT) { - [, pos] = decodeVarint(buf, pos); - } else if (wireType === WT_LEN) { - const [len, np2] = decodeVarint(buf, pos); - pos = np2; - pos += Number(len); - } else if (wireType === 1) { - pos += 8; - } else if (wireType === 5) { - pos += 4; - } - } + const decoder = PROTOBUF_VALUE_DECODERS[fieldNumber]; + if (decoder) return decoder(buf, pos, wireType).value; + pos = skipUnknownProtobufField(buf, pos, wireType); } return null; } +function decodeProtobufStructEntry(payload: Buffer): { key: string; value: unknown } | null { + const fields = decodeFields(payload); + const key = findLengthDelimitedField(fields, MAP_KEY)?.toString("utf8") ?? ""; + const valueBytes = findLengthDelimitedField(fields, MAP_VALUE); + return key && valueBytes ? { key, value: decodeProtobufValue(valueBytes) } : null; +} + function decodeProtobufStruct(buf: Buffer): Record { const result: Record = {}; - for (const f of decodeFields(buf)) { - if (f.fieldNumber === STRUCT_FIELDS && f.wireType === 2) { - let key = ""; - let valueBytes: Buffer | null = null; - for (const entry of decodeFields(f.bytes)) { - if (entry.fieldNumber === MAP_KEY && entry.wireType === 2) { - key = entry.bytes.toString("utf8"); - } else if (entry.fieldNumber === MAP_VALUE && entry.wireType === 2) { - valueBytes = entry.bytes; - } - } - if (key && valueBytes) { - result[key] = decodeProtobufValue(valueBytes); - } - } + for (const field of decodeFields(buf)) { + if (field.fieldNumber !== STRUCT_FIELDS || field.wireType !== WT_LEN) continue; + const entry = decodeProtobufStructEntry(field.bytes); + if (entry) result[entry.key] = entry.value; } return result; } @@ -1477,6 +1459,39 @@ export type ChatMessage = { tool_call_id?: string; }; +function messageContentToText(content: ChatMessage["content"]): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content + .map((part) => (typeof part?.text === "string" ? part.text : "")) + .filter(Boolean) + .join("\n"); +} + +function assistantMessageLines(message: ChatMessage, text: string): string[] { + const lines = text ? [`Assistant: ${text}`] : []; + for (const toolCall of message.tool_calls ?? []) { + const name = toolCall.function?.name ?? "(unknown)"; + const args = toolCall.function?.arguments ?? ""; + lines.push(`Assistant called tool ${name} (${toolCall.id}) with arguments: ${args}`); + } + return lines; +} + +function chatMessageLines(message: ChatMessage): string[] { + const text = messageContentToText(message.content); + if (message.role === "user") return text ? [`User: ${text}`] : []; + if (message.role === "assistant") return assistantMessageLines(message, text); + if (message.role === "tool") { + return [`Tool result (${message.tool_call_id ?? "(unknown)"}): ${text}`]; + } + return text ? [`${message.role}: ${text}`] : []; +} + +function joinSystemText(systemTexts: string[], body: string): string { + return systemTexts.length > 0 ? `${systemTexts.join("\n\n")}\n\n${body}` : body; +} + /** * Flatten an OpenAI-shaped message list down to a single user-text string * suitable for cursor's UserMessage. The agent endpoint expects ONE user @@ -1490,57 +1505,23 @@ export type ChatMessage = { export function flattenMessages(messages: ChatMessage[]): string { if (!Array.isArray(messages) || messages.length === 0) return ""; - const partsToText = (content: ChatMessage["content"]): string => { - if (typeof content === "string") return content; - if (content == null) return ""; - if (!Array.isArray(content)) return ""; - return content - .map((p) => (typeof p?.text === "string" ? p.text : "")) - .filter(Boolean) - .join("\n"); - }; - // System instructions go first as a labeled prefix. (The cursor executor // routes system messages through the KV blob channel — see Phase 7 — but // this branch is kept for non-cursor callers.) const systemTexts = messages .filter((m) => m.role === "system") - .map((m) => partsToText(m.content)) + .map((m) => messageContentToText(m.content)) .filter(Boolean); const turn = messages.filter((m) => m.role !== "system"); // Single-user-message fast path (no tool_calls, no labels). if (turn.length === 1 && turn[0].role === "user" && !turn[0].tool_calls) { - const userText = partsToText(turn[0].content); - return systemTexts.length > 0 ? `${systemTexts.join("\n\n")}\n\n${userText}` : userText; + return joinSystemText(systemTexts, messageContentToText(turn[0].content)); } // Multi-turn / tool-using format. Each message is labeled. Tool calls // and tool results get their own labeled lines. - const lines: string[] = []; - for (const m of turn) { - const text = partsToText(m.content); - if (m.role === "user") { - if (text) lines.push(`User: ${text}`); - } else if (m.role === "assistant") { - if (text) lines.push(`Assistant: ${text}`); - if (Array.isArray(m.tool_calls)) { - for (const tc of m.tool_calls) { - const args = tc.function?.arguments ?? ""; - lines.push( - `Assistant called tool ${tc.function?.name ?? "(unknown)"} ` + - `(${tc.id}) with arguments: ${args}` - ); - } - } - } else if (m.role === "tool") { - const callId = m.tool_call_id ?? "(unknown)"; - lines.push(`Tool result (${callId}): ${text}`); - } else { - if (text) lines.push(`${m.role}: ${text}`); - } - } - const labelled = lines.join("\n\n"); - return systemTexts.length > 0 ? `${systemTexts.join("\n\n")}\n\n${labelled}` : labelled; + const labelled = turn.flatMap(chatMessageLines).join("\n\n"); + return joinSystemText(systemTexts, labelled); } diff --git a/open-sse/utils/cursorAgentProtobuf/requestedModelParameters.ts b/open-sse/utils/cursorAgentProtobuf/requestedModelParameters.ts new file mode 100644 index 0000000000..da431746eb --- /dev/null +++ b/open-sse/utils/cursorAgentProtobuf/requestedModelParameters.ts @@ -0,0 +1,113 @@ +export const CURSOR_EFFORT_SUFFIXES = ["low", "medium", "high", "xhigh", "max"] as const; + +type CursorRequestedModel = { + modelId: string; + parameters: Array<{ id: string; value: string }>; +}; + +const CURSOR_ONE_MILLION_SUFFIX = "-1m"; +const CURSOR_GPT_REASONING_LEVELS = ["none", ...CURSOR_EFFORT_SUFFIXES] as const; + +const CURSOR_CLAUDE_ONE_MILLION_FAMILIES = [ + { + legacyPrefix: "claude-fable-5-1", + modelId: "claude-fable-5-1", + supportsFast: false, + trailingThinking: false, + }, + { + legacyPrefix: "claude-opus-5", + modelId: "claude-opus-5", + supportsFast: true, + trailingThinking: false, + }, + { + legacyPrefix: "claude-opus-4-8", + modelId: "claude-opus-4-8", + supportsFast: true, + trailingThinking: false, + }, + { + legacyPrefix: "claude-sonnet-5", + modelId: "claude-sonnet-5", + supportsFast: false, + trailingThinking: false, + }, + { + legacyPrefix: "claude-4.6-sonnet", + modelId: "claude-sonnet-4-6", + supportsFast: false, + trailingThinking: true, + }, +] as const; + +type CursorClaudeOneMillionFamily = (typeof CURSOR_CLAUDE_ONE_MILLION_FAMILIES)[number]; + +function isCursorEffort(value: string): value is (typeof CURSOR_EFFORT_SUFFIXES)[number] { + return CURSOR_EFFORT_SUFFIXES.some((effort) => effort === value); +} + +function resolveGptOneMillionContextModel(legacyId: string): CursorRequestedModel | null { + const match = /^(gpt-5\.6-(?:sol|terra|luna))-(none|low|medium|high|xhigh|max)$/.exec(legacyId); + if (!match) return null; + + const [, modelId, reasoning] = match; + if (!CURSOR_GPT_REASONING_LEVELS.some((level) => level === reasoning)) return null; + return { + modelId, + parameters: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: reasoning }, + { id: "fast", value: "false" }, + ], + }; +} + +function resolveClaudeOneMillionVariant( + legacyId: string, + family: CursorClaudeOneMillionFamily +): CursorRequestedModel | null { + const prefix = `${family.legacyPrefix}-`; + if (!legacyId.startsWith(prefix)) return null; + + let variant = legacyId.slice(prefix.length); + const fast = variant.endsWith("-fast"); + if (fast) variant = variant.slice(0, -"-fast".length); + if (fast && !family.supportsFast) return null; + + const trailingThinking = family.trailingThinking && variant.endsWith("-thinking"); + const leadingThinking = !family.trailingThinking && variant.startsWith("thinking-"); + if (trailingThinking) variant = variant.slice(0, -"-thinking".length); + if (leadingThinking) variant = variant.slice("thinking-".length); + if (!isCursorEffort(variant)) return null; + + const parameters = [ + { id: "thinking", value: String(trailingThinking || leadingThinking) }, + { id: "context", value: "1m" }, + { id: "effort", value: variant }, + ]; + if (family.supportsFast) parameters.push({ id: "fast", value: String(fast) }); + return { modelId: family.modelId, parameters }; +} + +function resolveClaudeOneMillionContextModel(legacyId: string): CursorRequestedModel | null { + for (const family of CURSOR_CLAUDE_ONE_MILLION_FAMILIES) { + const resolved = resolveClaudeOneMillionVariant(legacyId, family); + if (resolved) return resolved; + } + return null; +} + +/** + * Cursor reuses each legacy slug for both its default and 1M context variants, + * so the public catalog adds a terminal `-1m` discriminator. Translate that + * synthetic id to the canonical wire model plus the complete parameter set + * reported by Cursor's AvailableModels metadata. + */ +export function resolveOneMillionContextModel(normalized: string): CursorRequestedModel | null { + if (!normalized.endsWith(CURSOR_ONE_MILLION_SUFFIX)) return null; + const legacyId = normalized.slice(0, -CURSOR_ONE_MILLION_SUFFIX.length); + return ( + resolveGptOneMillionContextModel(legacyId) ?? resolveClaudeOneMillionContextModel(legacyId) + ); +} diff --git a/open-sse/utils/registeredEffortVariants.ts b/open-sse/utils/registeredEffortVariants.ts index 06e2d5dfc6..2cef8d6721 100644 --- a/open-sse/utils/registeredEffortVariants.ts +++ b/open-sse/utils/registeredEffortVariants.ts @@ -14,10 +14,9 @@ export function getRegisteredProviderEffortBaseModelId( modelId: string ): string | null { const providerModels = getProviderModels(providerId); + const registeredVariant = providerModels.find((candidate) => candidate.id === modelId); - if (!providerModels.some((candidate) => candidate.id === modelId)) { - return null; - } + if (!registeredVariant) return null; for (const effort of REGISTERED_EFFORT_SUFFIXES) { const suffix = `-${effort}`; @@ -25,7 +24,15 @@ export function getRegisteredProviderEffortBaseModelId( const baseModelId = modelId.slice(0, -suffix.length); - return providerModels.some((candidate) => candidate.id === baseModelId) ? baseModelId : null; + if (providerModels.some((candidate) => candidate.id === baseModelId)) return baseModelId; + + // Curated providers may intentionally expose only useful variants while the + // authoritative live catalog exposes their unsuffixed wire model. The registry + // declaration is the proof; never infer this relationship from spelling alone. + const declaredLiveBase = registeredVariant.liveCatalogIds?.find( + (candidate) => candidate === baseModelId || !candidate.endsWith(`-${effort}`) + ); + return declaredLiveBase ?? null; } return null; diff --git a/src/lib/db/models/activeSyncedCatalog.ts b/src/lib/db/models/activeSyncedCatalog.ts index 981175219f..18ade8260b 100644 --- a/src/lib/db/models/activeSyncedCatalog.ts +++ b/src/lib/db/models/activeSyncedCatalog.ts @@ -1,5 +1,6 @@ import { providerUsesAuthoritativeLiveCatalog } from "@omniroute/open-sse/config/providerRegistry"; import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.ts"; +import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog"; import { getSyncedAvailableModels, getSyncedAvailableModelsByConnection, @@ -76,6 +77,19 @@ function collectModelsForConnections( return Array.from(models.values()); } +function enrichCursorCatalog( + providerId: string, + models: SyncedAvailableModel[] +): SyncedAvailableModel[] { + // An empty sync means discovery has not completed (or failed). Do not let the + // synthetic Cursor auto-router rows turn that empty state into an authoritative + // catalog, otherwise every built-in model is incorrectly marked unavailable. + if (models.length === 0) return models; + return providerId === "cursor" || providerId === "cursor-api" + ? ensureCursorAutoCatalogEntry(models) + : models; +} + /** * Return the unioned synced catalog belonging only to active connections. * @@ -105,7 +119,10 @@ export async function getActiveSyncedCatalog(providerId: string): Promise connection !== null) .map((connection) => connection.id); - const models = collectModelsForConnections(modelsByConnection, activeConnectionIds); + const models = enrichCursorCatalog( + storedProviderId, + collectModelsForConnections(modelsByConnection, activeConnectionIds) + ); if (models.length > 0) { return { authoritative: providerUsesAuthoritativeLiveCatalog(providerId), @@ -125,7 +142,13 @@ export async function getActiveSyncedCatalog(providerId: string): Promise { const modelsByConnection = await getSyncedAvailableModelsByConnection(providerId); - const models = collectModelsForConnections(modelsByConnection, connectionIds); + const models = enrichCursorCatalog( + providerId, + collectModelsForConnections(modelsByConnection, connectionIds) + ); if (models.length > 0) { result[providerId] = models; diff --git a/src/lib/providerModels/cursorAutoCatalog.ts b/src/lib/providerModels/cursorAutoCatalog.ts index a9c0f2dca2..ddea216560 100644 --- a/src/lib/providerModels/cursorAutoCatalog.ts +++ b/src/lib/providerModels/cursorAutoCatalog.ts @@ -8,7 +8,6 @@ export type CursorAutoCatalogEntry = { id: string; name: string; owned_by?: string; - [key: string]: unknown; }; export const CURSOR_AUTO_ROUTER_VARIANT_IDS = [ @@ -26,10 +25,56 @@ const CURSOR_AUTO_ROUTER_VARIANT_NAMES: Record< "auto-intelligence": "Auto (intelligence)", }; +const CURSOR_ONE_MILLION_CONTEXT = 1_000_000; +const CURSOR_CONTEXT_EFFORT = "(?:low|medium|high|xhigh|max)"; +const CURSOR_ONE_MILLION_MODEL_PATTERNS = [ + new RegExp(`^claude-fable-5-1-thinking-${CURSOR_CONTEXT_EFFORT}$`), + new RegExp(`^claude-opus-5-(?:thinking-)?${CURSOR_CONTEXT_EFFORT}(?:-fast)?$`), + new RegExp(`^claude-opus-4-8-(?:thinking-)?${CURSOR_CONTEXT_EFFORT}(?:-fast)?$`), + new RegExp(`^claude-sonnet-5-(?:thinking-)?${CURSOR_CONTEXT_EFFORT}$`), + new RegExp(`^claude-4\\.6-sonnet-${CURSOR_CONTEXT_EFFORT}(?:-thinking)?$`), + new RegExp(`^gpt-5\\.6-(?:sol|terra|luna)-(?:none|${CURSOR_CONTEXT_EFFORT})$`), +] as const; + +const CURSOR_CONTEXT_FAMILY_NAMES = [ + "Claude Fable 5.1", + "Claude Opus 5", + "Claude Opus 4.8", + "Claude Sonnet 5", + "Claude Sonnet 4.6", + "GPT-5.6 Sol", + "GPT-5.6 Terra", + "GPT-5.6 Luna", +] as const; + +function supportsCursorOneMillionContext(id: string): boolean { + return CURSOR_ONE_MILLION_MODEL_PATTERNS.some((pattern) => pattern.test(id)); +} + +function oneMillionDisplayName(name: string): string { + const family = CURSOR_CONTEXT_FAMILY_NAMES.find((candidate) => name.startsWith(candidate)); + return family ? `${family} 1M${name.slice(family.length)}` : `${name} 1M`; +} + /** Cursor auto-router: catalog id `auto`, wire id `default`. Always keep `auto` visible. */ export function ensureCursorAutoCatalogEntry(models: T[]): T[] { const byId = new Map(models.map((m) => [m.id, m])); - const out = [...models]; + const out: T[] = []; + + for (const model of models) { + const oneMillionId = `${model.id}-1m`; + if (supportsCursorOneMillionContext(model.id) && !byId.has(oneMillionId)) { + const oneMillionEntry = { + ...model, + id: oneMillionId, + name: oneMillionDisplayName(model.name), + contextLength: CURSOR_ONE_MILLION_CONTEXT, + } as T; + out.push(oneMillionEntry); + byId.set(oneMillionId, oneMillionEntry); + } + out.push(model); + } if (!byId.has("auto")) { const defaultEntry = byId.get("default"); diff --git a/src/lib/providerModels/cursorAvailableModels.ts b/src/lib/providerModels/cursorAvailableModels.ts index 0fdc3d1939..8c4eb7b04a 100644 --- a/src/lib/providerModels/cursorAvailableModels.ts +++ b/src/lib/providerModels/cursorAvailableModels.ts @@ -9,8 +9,11 @@ import { humanizeCursorModelId, type CursorAgentModelEntry, } from "@/lib/providerModels/cursorAgent"; +import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog"; import { getConsistentMachineId } from "@/shared/utils/machineId"; +export { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog"; + export type FetchCursorAvailableModelsOptions = { accessToken: string; machineId?: string | null; @@ -40,6 +43,45 @@ function pickModelName(entry: Record, id: string): string { return humanizeCursorModelId(id); } +function collectArrays(record: Record, keys: string[]): unknown[] { + return keys.flatMap((key) => (Array.isArray(record[key]) ? record[key] : [])); +} + +function collectModelCandidates(payload: unknown): unknown[] { + const root = asRecord(payload) ?? {}; + const candidates = collectArrays(root, [ + "models", + "availableModels", + "available_models", + "model", + ]); + const nestedModels = asRecord(root.models); + if (nestedModels) candidates.push(...collectArrays(nestedModels, ["models", "items", "list"])); + if (Array.isArray(payload)) candidates.push(...payload); + return candidates; +} + +function isUnavailableModel(entry: Record): boolean { + return ( + entry.disabled === true || + entry.isDisabled === true || + entry.usable === false || + entry.isUsable === false + ); +} + +function normalizeModelCandidate(item: unknown): CursorAgentModelEntry | null { + if (typeof item === "string") { + const id = item.trim(); + return id ? { id, name: humanizeCursorModelId(id), owned_by: "cursor" } : null; + } + + const entry = asRecord(item); + if (!entry || isUnavailableModel(entry)) return null; + const id = pickModelId(entry); + return id ? { id, name: pickModelName(entry, id), owned_by: "cursor" } : null; +} + /** * Normalize AvailableModels JSON (Connect JSON or protobuf-json) into catalog rows. * Exported for unit tests. @@ -48,99 +90,18 @@ function pickModelName(entry: Record, id: string): string { * only). OmniRoute clients request `cu/auto`; resolveRequestedModel maps it to `default`. */ export function normalizeCursorAvailableModelsPayload(payload: unknown): CursorAgentModelEntry[] { - const root = asRecord(payload) ?? {}; - const candidates: unknown[] = []; - - for (const key of ["models", "availableModels", "available_models", "model"]) { - const v = root[key]; - if (Array.isArray(v)) candidates.push(...v); - } - - // Some Connect JSON responses nest under `models.models` or similar - const nestedModels = asRecord(root.models); - if (nestedModels) { - for (const key of ["models", "items", "list"]) { - const v = nestedModels[key]; - if (Array.isArray(v)) candidates.push(...v); - } - } - - if (Array.isArray(payload)) candidates.push(...payload); - const seen = new Set(); const out: CursorAgentModelEntry[] = []; - for (const item of candidates) { - if (typeof item === "string" && item.trim()) { - const id = item.trim(); - if (seen.has(id)) continue; - seen.add(id); - out.push({ id, name: humanizeCursorModelId(id), owned_by: "cursor" }); - continue; - } - const rec = asRecord(item); - if (!rec) continue; - const id = pickModelId(rec); - if (!id || seen.has(id)) continue; - // Prefer usable / non-disabled when flags exist - if (rec.disabled === true || rec.isDisabled === true) continue; - if (rec.usable === false || rec.isUsable === false) continue; - seen.add(id); - out.push({ id, name: pickModelName(rec, id), owned_by: "cursor" }); + for (const item of collectModelCandidates(payload)) { + const model = normalizeModelCandidate(item); + if (!model || seen.has(model.id)) continue; + seen.add(model.id); + out.push(model); } return ensureCursorAutoCatalogEntry(out); } -/** OpenCodex-style Cursor Router optimization modes (catalog ids). */ -export const CURSOR_AUTO_ROUTER_VARIANT_IDS = [ - "auto-cost", - "auto-balance", - "auto-intelligence", -] as const; - -const CURSOR_AUTO_ROUTER_VARIANT_NAMES: Record< - (typeof CURSOR_AUTO_ROUTER_VARIANT_IDS)[number], - string -> = { - "auto-cost": "Auto (cost)", - "auto-balance": "Auto (balance)", - "auto-intelligence": "Auto (intelligence)", -}; - -/** Cursor auto-router: catalog id `auto`, wire id `default`. Always keep `auto` visible. */ -export function ensureCursorAutoCatalogEntry( - models: CursorAgentModelEntry[] -): CursorAgentModelEntry[] { - const byId = new Map(models.map((m) => [m.id, m])); - const out = [...models]; - - if (!byId.has("auto")) { - const defaultEntry = byId.get("default"); - const autoEntry: CursorAgentModelEntry = { - id: "auto", - name: defaultEntry?.name || "Auto (current, default)", - owned_by: "cursor", - }; - // Prefer `auto` as the public id; keep `default` for wire-compat listings. - out.unshift(autoEntry); - byId.set("auto", autoEntry); - } - - // Always expose Cost/Balance/Intelligence router modes (OpenCodex CURSOR_ROUTER_MODEL_IDS). - for (const id of CURSOR_AUTO_ROUTER_VARIANT_IDS) { - if (byId.has(id)) continue; - const entry: CursorAgentModelEntry = { - id, - name: CURSOR_AUTO_ROUTER_VARIANT_NAMES[id], - owned_by: "cursor", - }; - out.push(entry); - byId.set(id, entry); - } - - return out; -} - export async function fetchCursorAvailableModels( options: FetchCursorAvailableModelsOptions ): Promise { diff --git a/src/lib/providers/staticModels.ts b/src/lib/providers/staticModels.ts index 63eebdce28..5bb6598db5 100644 --- a/src/lib/providers/staticModels.ts +++ b/src/lib/providers/staticModels.ts @@ -35,6 +35,7 @@ const STATIC_MODEL_PROVIDERS: Record Array<{ id: string; name: str ], antigravity: () => ANTIGRAVITY_PUBLIC_MODELS.map((model) => ({ ...model })), claude: () => [ + { id: "claude-fable-5-1", name: "Claude Fable 5.1" }, { id: "claude-fable-5", name: "Claude Fable 5" }, { id: "claude-opus-5", name: "Claude Opus 5" }, { id: "claude-opus-4-8", name: "Claude Opus 4.8" }, diff --git a/src/shared/constants/cliTools.ts b/src/shared/constants/cliTools.ts index 95d956356b..01401ea986 100644 --- a/src/shared/constants/cliTools.ts +++ b/src/shared/constants/cliTools.ts @@ -45,7 +45,7 @@ export const CLI_TOOLS: Record = { name: "Claude Fable", alias: "fable", envKey: "ANTHROPIC_DEFAULT_FABLE_MODEL", - defaultValue: _cc.fable ? `cc/${_cc.fable}` : "cc/claude-fable-5", + defaultValue: _cc.fable ? `cc/${_cc.fable}` : "cc/claude-fable-5-1", isTopLevel: true, }, { diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index fcc96c8e9b..c8b6faaf4b 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -24,9 +24,13 @@ export interface ModelSpec { // Model ONLY supports adaptive thinking: manual extended thinking was removed. Sending // `thinking.type:"enabled"` or any `thinking.budget_tokens` returns HTTP 400; reasoning // is steered exclusively by `output_config.effort` (low/medium/high/xhigh/max). True for - // Claude Opus 4.7 and later (Opus 4.7/4.8/5, Fable 5). Per Anthropic's migration guide, + // Claude Opus 4.7 and later (Opus 4.7/4.8/5, Fable 5/5.1). Per Anthropic's migration guide, // any request that tries to set a fixed thinking budget gets a 400 error. adaptiveThinkingOnly?: boolean; + // The model rejects tool_choice values that require a tool call. Keep tools available, + // but normalize a forced choice to the default auto behavior before dispatch. Fable 5.1 always runs + // adaptive thinking, so forced tool use cannot be combined with any valid request. + rejectsForcedToolChoice?: boolean; // Highest effort accepted while `thinking.type:"disabled"` is present. Claude Opus 5 // rejects disabled thinking with xhigh/max, while accepting it through high. maxEffortWhenThinkingDisabled?: "high"; @@ -371,6 +375,21 @@ export const MODEL_SPECS: Record = { aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-4-7", "claude-opus-4.7"), }, + // ── Claude Fable 5.1 ──────────────────────────────────────────── + "claude-fable-5-1": { + maxOutputTokens: 128000, + contextWindow: 1000000, + defaultThinkingBudget: 32000, + thinkingBudgetCap: 120000, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + rejectsThinkingDisabled: true, + adaptiveThinkingOnly: true, + rejectsForcedToolChoice: true, + aliases: BEDROCK_CLAUDE_ALIASES("claude-fable-5-1"), + }, + // ── Claude Fable 5 ────────────────────────────────────────────── "claude-fable-5": { maxOutputTokens: 128000, @@ -849,9 +868,39 @@ export function normalizeThinkingForModel>( getModelSpec(modelId)?.rejectsThinkingDisabled ) { const { thinking: _omitted, ...rest } = body as Record; - return rest as T; + return normalizeForcedToolChoiceForModel(rest as T, modelId); } - return body; + return normalizeForcedToolChoiceForModel(body, modelId); +} + +/** + * Normalize tool-choice constraints that a resolved model cannot accept. + * + * Claude Fable 5.1 always uses adaptive thinking and rejects tool choices that force + * either any tool or one named tool. Preserve the declared tools and every unrelated + * request field, but drop the choice to select the default `auto` behavior so routing a + * request to Fable 5.1 does not turn a recoverable preference into an upstream 400. + */ +export function normalizeForcedToolChoiceForModel>( + body: T, + modelId: string +): T { + if (!getModelSpec(modelId)?.rejectsForcedToolChoice) return body; + + const toolChoice = body.tool_choice; + const forced = + toolChoice === "required" || + toolChoice === "any" || + (toolChoice !== null && + typeof toolChoice === "object" && + !Array.isArray(toolChoice) && + ["any", "tool", "function"].includes( + String((toolChoice as Record).type || "").toLowerCase() + )); + if (!forced) return body; + + const { tool_choice: _omitted, ...rest } = body; + return rest as T; } export function capMaxOutputTokens(modelId: string, requested?: number): number | undefined { diff --git a/src/shared/constants/pricing/default-pricing.ts b/src/shared/constants/pricing/default-pricing.ts index 08b84a86e9..a36654bd06 100644 --- a/src/shared/constants/pricing/default-pricing.ts +++ b/src/shared/constants/pricing/default-pricing.ts @@ -7,10 +7,12 @@ import { DEFAULT_PRICING_OAUTH } from "./oauth-subscriptions"; import { DEFAULT_PRICING_FRONTIER } from "./frontier-labs"; import { DEFAULT_PRICING_INFERENCE } from "./inference-hosts"; import { DEFAULT_PRICING_REGIONAL } from "./regional"; +import { DEFAULT_PRICING_DEVIN } from "./devin"; export const DEFAULT_PRICING = { ...DEFAULT_PRICING_OAUTH, ...DEFAULT_PRICING_FRONTIER, ...DEFAULT_PRICING_INFERENCE, ...DEFAULT_PRICING_REGIONAL, + ...DEFAULT_PRICING_DEVIN, }; diff --git a/src/shared/constants/pricing/devin.ts b/src/shared/constants/pricing/devin.ts new file mode 100644 index 0000000000..36a0e49b3a --- /dev/null +++ b/src/shared/constants/pricing/devin.ts @@ -0,0 +1,142 @@ +type DevinTokenPricing = { + input: number; + cached: number; + output: number; +}; + +const QUALITY_EFFORTS = ["max", "xhigh", "high", "medium", "low"] as const; +const GPT_EFFORTS = ["max", "xhigh", "high", "medium", "low", "none"] as const; + +function variantIds(base: string, efforts: readonly string[]): string[] { + return efforts.map((effort) => `${base}-${effort}`); +} + +function fastVariantIds(base: string): string[] { + return QUALITY_EFFORTS.map((effort) => `${base}-${effort}-fast`); +} + +function priorityVariantIds(base: string): string[] { + return GPT_EFFORTS.map((effort) => `${base}-${effort}-priority`); +} + +function priced(ids: readonly string[], pricing: DevinTokenPricing) { + return Object.fromEntries(ids.map((id) => [id, pricing])); +} + +const CLAUDE_FABLE_5_1 = { input: 10, cached: 0.25, output: 50 }; +const CLAUDE_OPUS = { input: 5, cached: 0.5, output: 25 }; +const CLAUDE_OPUS_FAST = { input: 10, cached: 1, output: 50 }; +const CLAUDE_SONNET_5 = { input: 2, cached: 0.2, output: 10 }; +const CLAUDE_SONNET_4_6 = { input: 3, cached: 0.3, output: 15 }; +const CLAUDE_HAIKU_4_5 = { input: 1, cached: 0.1, output: 5 }; + +const GPT_5_6_SOL = { input: 4, cached: 0.4, output: 20 }; +const GPT_5_6_SOL_FAST = { input: 8, cached: 0.8, output: 40 }; +const GPT_5_6_TERRA = { input: 2, cached: 0.2, output: 12 }; +const GPT_5_6_TERRA_FAST = { input: 4, cached: 0.4, output: 24 }; +const GPT_5_6_LUNA = { input: 0.2, cached: 0.02, output: 1.2 }; +const GPT_5_6_LUNA_FAST = { input: 0.4, cached: 0.04, output: 2.4 }; + +/** + * Exact per-UID rates returned by Devin's authenticated live catalog on + * 2026-09-02. Rates are USD per one million tokens. + */ +export const DEVIN_MODEL_PRICING: Record = { + ...priced(variantIds("claude-fable-5-1", QUALITY_EFFORTS), CLAUDE_FABLE_5_1), + ...priced(variantIds("claude-opus-5", QUALITY_EFFORTS), CLAUDE_OPUS), + ...priced(fastVariantIds("claude-opus-5"), CLAUDE_OPUS_FAST), + ...priced(variantIds("claude-opus-4-8", QUALITY_EFFORTS), CLAUDE_OPUS), + ...priced(fastVariantIds("claude-opus-4-8"), CLAUDE_OPUS_FAST), + ...priced(variantIds("claude-sonnet-5", QUALITY_EFFORTS), CLAUDE_SONNET_5), + ...priced( + [ + "claude-sonnet-4-6", + "claude-sonnet-4-6-thinking", + "claude-sonnet-4-6-1m", + "claude-sonnet-4-6-thinking-1m", + ], + CLAUDE_SONNET_4_6 + ), + MODEL_PRIVATE_11: CLAUDE_HAIKU_4_5, + + ...priced(variantIds("gpt-5-6-sol", GPT_EFFORTS), GPT_5_6_SOL), + ...priced(priorityVariantIds("gpt-5-6-sol"), GPT_5_6_SOL_FAST), + ...priced(variantIds("gpt-5-6-terra", GPT_EFFORTS), GPT_5_6_TERRA), + ...priced(priorityVariantIds("gpt-5-6-terra"), GPT_5_6_TERRA_FAST), + ...priced(variantIds("gpt-5-6-luna", GPT_EFFORTS), GPT_5_6_LUNA), + ...priced(priorityVariantIds("gpt-5-6-luna"), GPT_5_6_LUNA_FAST), + + ...priced(variantIds("kimi-k3", ["max", "high", "low"]), { + input: 3, + cached: 0.3, + output: 15, + }), + "kimi-k2-7": { input: 0.95, cached: 0.19, output: 4 }, + ...priced(variantIds("glm-5-3", ["max", "high", "low"]), { + input: 1.4, + cached: 0.26, + output: 4.4, + }), + ...priced(variantIds("glm-5-3-flash", ["max", "high", "low"]), { + input: 0.15, + cached: 0.03, + output: 0.5, + }), + + ...priced(["swe-1-7", "swe-1-7-medium"], { + input: 0.5, + cached: 0.2, + output: 2.5, + }), + ...priced(["swe-1-7-lightning", "swe-1-7-lightning-medium"], { + input: 2.5, + cached: 1, + output: 12.5, + }), + adaptive: { input: 0.5, cached: 0.1, output: 2 }, + ...priced(variantIds("grok-4-6", ["xhigh", "high", "medium", "low"]), { + input: 2, + cached: 0.3, + output: 6, + }), + ...priced(variantIds("inkling", ["max", "xhigh", "high", "medium", "low", "none"]), { + input: 1.4, + cached: 0.26, + output: 4.4, + }), + ...priced(variantIds("deepseek-v4-flash", ["max", "high", "low"]), { + input: 0.14, + cached: 0.03, + output: 0.28, + }), + ...priced(variantIds("nemotron-3-ultra", ["high", "medium", "none"]), { + input: 0.6, + cached: 0.12, + output: 2.4, + }), + ...priced(variantIds("gemini-3-7-flash", ["high", "medium", "low"]), { + input: 1.5, + cached: 0.15, + output: 7.5, + }), + ...priced(variantIds("gemini-3-1-pro", ["high", "low"]), { + input: 2, + cached: 0.2, + output: 12, + }), + ...priced(variantIds("deepseek-v4-pro", ["max", "high", "low"]), { + input: 1.32, + cached: 0.04, + output: 3.96, + }), +}; + +// Each transport gets its own provider namespace. They share today's upstream +// rate snapshot, but can diverge independently if Devin changes one channel. +export const DEFAULT_PRICING_DEVIN = { + "devin-cli": { ...DEVIN_MODEL_PRICING }, + dv: { ...DEVIN_MODEL_PRICING }, + "devin-desktop": { ...DEVIN_MODEL_PRICING }, + "devin-cli-agentic": { ...DEVIN_MODEL_PRICING }, + dva: { ...DEVIN_MODEL_PRICING }, +}; diff --git a/src/shared/constants/pricing/frontier-labs.ts b/src/shared/constants/pricing/frontier-labs.ts index d20a187720..b8a80d3bf1 100644 --- a/src/shared/constants/pricing/frontier-labs.ts +++ b/src/shared/constants/pricing/frontier-labs.ts @@ -8,6 +8,7 @@ import { GPT_5_6_LUNA_PRICING, GPT_5_6_SOL_PRICING, GPT_5_6_TERRA_PRICING, + CLAUDE_FABLE_5_1_PRICING, CLAUDE_FABLE_5_PRICING, CLAUDE_OPUS_5_PRICING, CLAUDE_OPUS_4_PRICING, @@ -213,6 +214,7 @@ export const DEFAULT_PRICING_FRONTIER = { // Common model IDs (without dates) used across providers // Intentional duplicates of dot-notation variants (e.g. claude-opus-4.6) // to cover hyphen-notation IDs (claude-opus-4-6) used by some clients + "claude-fable-5-1": CLAUDE_FABLE_5_1_PRICING, "claude-fable-5": CLAUDE_FABLE_5_PRICING, "claude-opus-5": CLAUDE_OPUS_5_PRICING, "claude-sonnet-5": CLAUDE_SONNET_5_PRICING, diff --git a/src/shared/constants/pricing/oauth-subscriptions.ts b/src/shared/constants/pricing/oauth-subscriptions.ts index 87f5aa320f..9d255e0e24 100644 --- a/src/shared/constants/pricing/oauth-subscriptions.ts +++ b/src/shared/constants/pricing/oauth-subscriptions.ts @@ -3,6 +3,7 @@ * Pure data; merged by default-pricing.ts via spread (god-file decomposition; semantic split). */ import { + CLAUDE_FABLE_5_1_PRICING, CLAUDE_OPUS_5_PRICING, GEMINI_3_7_FLASH_PROMO_PRICING, GPT_5_3_CODEX_PRICING, @@ -20,6 +21,7 @@ const ANTIGRAVITY_GEMINI_3_7_PRICING = { export const DEFAULT_PRICING_OAUTH = { cc: { + "claude-fable-5-1": CLAUDE_FABLE_5_1_PRICING, "claude-fable-5": { input: 10.0, output: 50.0, diff --git a/src/shared/constants/pricing/shared-tiers.ts b/src/shared/constants/pricing/shared-tiers.ts index 542e03521a..7cb674df09 100644 --- a/src/shared/constants/pricing/shared-tiers.ts +++ b/src/shared/constants/pricing/shared-tiers.ts @@ -60,6 +60,14 @@ export const CLAUDE_FABLE_5_PRICING = { cache_creation: 15.0, }; +export const CLAUDE_FABLE_5_1_PRICING = { + input: 10.0, + output: 50.0, + cached: 0.25, + reasoning: 50.0, + cache_creation: 12.5, +}; + export const CLAUDE_OPUS_5_PRICING = { input: 5.0, output: 25.0, diff --git a/tests/unit/claude-fable-5-1.test.ts b/tests/unit/claude-fable-5-1.test.ts new file mode 100644 index 0000000000..b494db3d3a --- /dev/null +++ b/tests/unit/claude-fable-5-1.test.ts @@ -0,0 +1,172 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + getModelTargetFormat, + getModelsByProviderId, + supportsClaudeMaxEffort, + supportsXHighEffort, +} from "../../open-sse/config/providerModels.ts"; +import { getUnsupportedParams } from "../../open-sse/config/providerRegistry.ts"; +import { modelHasNativeContext1m } from "../../open-sse/config/claudeCodeCompatibleIdentity.ts"; +import { modelSupportsContext1mBeta } from "../../open-sse/config/context1m.ts"; +import { normalizeClaudeAdaptiveThinking } from "../../open-sse/services/claudeAdaptiveThinking.ts"; +import { getNextFamilyFallback } from "../../open-sse/services/modelFamilyFallback.ts"; +import { getModelPricing } from "../../open-sse/services/providerCostData.ts"; +import { getStaticModelsForProvider } from "../../src/lib/providers/staticModels.ts"; +import { getDefaultPricing } from "../../src/shared/constants/pricing.ts"; +import { + getModelSpec, + normalizeForcedToolChoiceForModel, + normalizeThinkingForModel, +} from "../../src/shared/constants/modelSpecs.ts"; + +const MODEL_ID = "claude-fable-5-1"; +const BEDROCK_MODEL_ID = "anthropic.claude-fable-5-1"; +const EFFORTS = ["low", "medium", "high", "xhigh", "max"]; + +test("Claude Fable 5.1 is registered only on verified launch surfaces", () => { + for (const [providerId, modelId] of [ + ["anthropic", MODEL_ID], + ["claude", MODEL_ID], + ["claude-web", MODEL_ID], + ["bedrock", BEDROCK_MODEL_ID], + ["vertex", MODEL_ID], + ["vertex-partner", MODEL_ID], + ] as const) { + const model = getModelsByProviderId(providerId).find((entry) => entry.id === modelId); + assert.ok(model, `${providerId} must expose ${modelId}`); + if (providerId === "vertex" || providerId === "vertex-partner") { + assert.equal(model.targetFormat, "claude", `${providerId} wire format`); + } else { + assert.equal(model.contextLength, 1_000_000, `${providerId} context window`); + assert.equal(model.maxOutputTokens, 128_000, `${providerId} max output`); + assert.deepEqual(model.supportedThinkingEfforts, EFFORTS, `${providerId} effort levels`); + } + } + + assert.equal(getModelTargetFormat("vertex", MODEL_ID), "claude"); + assert.equal(getModelTargetFormat("vertex-partner", MODEL_ID), "claude"); + + for (const providerId of ["github", "ghe-copilot", "kiro"]) { + const ids = new Set(getModelsByProviderId(providerId).map((entry) => entry.id)); + assert.equal(ids.has(MODEL_ID), false, `${providerId} availability is not verified`); + } + + const cursorModels = new Map( + getModelsByProviderId("cursor").map((entry) => [entry.id, entry] as const) + ); + assert.equal(cursorModels.has(MODEL_ID), false, "cursor exposes only selectable variants"); + + const cursorApiIds = new Set(getModelsByProviderId("cursor-api").map((entry) => entry.id)); + assert.equal(cursorApiIds.has(MODEL_ID), false); + for (const effort of EFFORTS) { + const cursorId = `${MODEL_ID}-thinking-${effort}`; + const cursor1mId = `${cursorId}-1m`; + assert.equal(cursorModels.has(`${MODEL_ID}-${effort}`), false); + assert.equal(cursorModels.get(cursorId)?.contextLength, 300_000, cursorId); + assert.equal(cursorModels.get(cursorId)?.maxOutputTokens, 128_000, cursorId); + assert.equal(cursorApiIds.has(cursorId), true, `cursor-api must expose ${cursorId}`); + assert.equal(cursorModels.get(cursor1mId)?.contextLength, 1_000_000, cursor1mId); + assert.equal(cursorModels.get(cursor1mId)?.maxOutputTokens, 128_000, cursor1mId); + assert.equal(cursorApiIds.has(cursor1mId), true, `cursor-api must expose ${cursor1mId}`); + } + + assert.ok( + getStaticModelsForProvider("claude")?.some((entry) => entry.id === MODEL_ID), + "Claude OAuth static discovery must expose Fable 5.1" + ); + assert.equal( + getNextFamilyFallback(`claude/${MODEL_ID}`, new Set([`claude/${MODEL_ID}`])), + "claude/claude-fable-5" + ); +}); + +test("Claude Fable 5.1 has native 1M context and adaptive-only thinking", () => { + assert.equal(modelHasNativeContext1m(MODEL_ID), true); + assert.equal(modelHasNativeContext1m(BEDROCK_MODEL_ID), true); + assert.equal(modelSupportsContext1mBeta(MODEL_ID), false); + + const spec = getModelSpec(MODEL_ID); + assert.equal(spec?.contextWindow, 1_000_000); + assert.equal(spec?.maxOutputTokens, 128_000); + assert.equal(spec?.supportsThinking, true); + assert.equal(spec?.supportsTools, true); + assert.equal(spec?.supportsVision, true); + assert.equal(spec?.adaptiveThinkingOnly, true); + assert.equal(spec?.rejectsThinkingDisabled, true); + assert.equal( + (spec as typeof spec & { rejectsForcedToolChoice?: boolean })?.rejectsForcedToolChoice, + true + ); + + assert.equal(getModelSpec(`global.${BEDROCK_MODEL_ID}`), spec); + assert.equal(supportsXHighEffort("claude", MODEL_ID), true); + assert.equal(supportsClaudeMaxEffort(MODEL_ID), true); +}); + +test("Claude Fable 5.1 strips unsupported sampling parameters", () => { + for (const providerId of ["anthropic", "claude"] as const) { + const unsupported = getUnsupportedParams(providerId, MODEL_ID); + for (const param of ["temperature", "top_p", "top_k"]) { + assert.ok(unsupported.includes(param), `${providerId}/${MODEL_ID} must strip ${param}`); + } + } +}); + +test("Claude Fable 5.1 normalizes disabled and manual thinking to adaptive", () => { + const withoutDisabled = normalizeThinkingForModel( + { model: MODEL_ID, thinking: { type: "disabled" }, marker: true }, + MODEL_ID + ); + assert.equal("thinking" in withoutDisabled, false); + assert.equal(withoutDisabled.marker, true); + + const adaptive = normalizeClaudeAdaptiveThinking( + { model: MODEL_ID, thinking: { type: "enabled", budget_tokens: 64_000 } }, + MODEL_ID + ); + assert.deepEqual(adaptive.thinking, { type: "adaptive" }); +}); + +test("Claude Fable 5.1 relaxes forced tool choices without removing tools", () => { + for (const toolChoice of [ + "required", + "any", + { type: "any" }, + { type: "tool", name: "read_file" }, + { type: "function", function: { name: "read_file" } }, + ]) { + const tools = [{ name: "read_file", input_schema: { type: "object" } }]; + const result = normalizeForcedToolChoiceForModel( + { model: MODEL_ID, tools, tool_choice: toolChoice, marker: true }, + MODEL_ID + ); + assert.equal("tool_choice" in result, false); + assert.equal(result.tools, tools); + assert.equal(result.marker, true); + } + + const auto = { model: MODEL_ID, tool_choice: { type: "auto" } }; + assert.equal(normalizeForcedToolChoiceForModel(auto, MODEL_ID), auto); + + const older = { model: "claude-fable-5", tool_choice: { type: "tool", name: "read_file" } }; + assert.equal(normalizeForcedToolChoiceForModel(older, "claude-fable-5"), older); +}); + +test("Claude Fable 5.1 pricing matches Anthropic's published rates", () => { + for (const providerId of ["anthropic", "cc"] as const) { + const price = getDefaultPricing()[providerId][MODEL_ID]; + assert.equal(price.input, 10); + assert.equal(price.output, 50); + assert.equal(price.cached, 0.25); + assert.equal(price.reasoning, 50); + assert.equal(price.cache_creation, 12.5); + } + + assert.deepEqual(getModelPricing("anthropic", MODEL_ID), { + inputCostPer1M: 10, + outputCostPer1M: 50, + isFree: false, + }); +}); diff --git a/tests/unit/claude-web-sonnet5-registry-6209.test.ts b/tests/unit/claude-web-sonnet5-registry-6209.test.ts index 9930a04ae2..a2f568fc9c 100644 --- a/tests/unit/claude-web-sonnet5-registry-6209.test.ts +++ b/tests/unit/claude-web-sonnet5-registry-6209.test.ts @@ -9,6 +9,7 @@ test("claude-web registry matches the current selectable model set", () => { assert.deepEqual( ids, [ + "claude-fable-5-1", "claude-fable-5", "claude-haiku-4-5-20251001", "claude-opus-5", diff --git a/tests/unit/cursor-auto-catalog-entry.test.ts b/tests/unit/cursor-auto-catalog-entry.test.ts index a194885927..aea92b7394 100644 --- a/tests/unit/cursor-auto-catalog-entry.test.ts +++ b/tests/unit/cursor-auto-catalog-entry.test.ts @@ -28,4 +28,31 @@ describe("ensureCursorAutoCatalogEntry", () => { assert.equal(models.filter((m) => m.id === "auto").length, 1); assert.equal(models.filter((m) => m.id === "auto-cost").length, 1); }); + + it("injects supported 1M context variants immediately before their base ids", () => { + const models = ensureCursorAutoCatalogEntry([ + { id: "claude-opus-5-thinking-max-fast", name: "Claude Opus 5 Max Thinking Fast" }, + { id: "gpt-5.6-sol-max", name: "GPT-5.6 Sol Max" }, + { id: "gpt-5.6-sol-max-fast", name: "GPT-5.6 Sol Max Fast" }, + ]); + const ids = models.map((model) => model.id); + for (const baseId of ["claude-opus-5-thinking-max-fast", "gpt-5.6-sol-max"]) { + const oneMillionPosition = ids.indexOf(`${baseId}-1m`); + assert.ok(oneMillionPosition >= 0); + assert.equal(ids[oneMillionPosition + 1], baseId); + assert.equal( + (models[oneMillionPosition] as { contextLength?: number }).contextLength, + 1_000_000 + ); + } + assert.equal(ids.includes("gpt-5.6-sol-max-fast-1m"), false); + }); + + it("does not duplicate a discovered 1M context variant", () => { + const models = ensureCursorAutoCatalogEntry([ + { id: "gpt-5.6-luna-max-1m", name: "GPT-5.6 Luna 1M Max" }, + { id: "gpt-5.6-luna-max", name: "GPT-5.6 Luna Max" }, + ]); + assert.equal(models.filter((model) => model.id === "gpt-5.6-luna-max-1m").length, 1); + }); }); diff --git a/tests/unit/cursor-available-models.test.ts b/tests/unit/cursor-available-models.test.ts index bf4335b388..28e822427d 100644 --- a/tests/unit/cursor-available-models.test.ts +++ b/tests/unit/cursor-available-models.test.ts @@ -17,7 +17,9 @@ describe("normalizeCursorAvailableModelsPayload", () => { }); assert.equal(models[0].id, "auto"); assert.ok(models.some((m) => m.id === "claude-opus-5-high")); + assert.ok(models.some((m) => m.id === "claude-opus-5-high-1m")); assert.ok(models.some((m) => m.id === "gpt-5.6-sol-high")); + assert.ok(models.some((m) => m.id === "gpt-5.6-sol-high-1m")); assert.ok(models.some((m) => m.id === "auto-cost")); assert.equal(models.find((m) => m.id === "claude-opus-5-high")?.name, "Opus 5"); assert.equal(models.find((m) => m.id === "claude-opus-5-high")?.owned_by, "cursor"); diff --git a/tests/unit/cursor-catalog-combo-compat.test.ts b/tests/unit/cursor-catalog-combo-compat.test.ts index cd3a8f481a..d27613b317 100644 --- a/tests/unit/cursor-catalog-combo-compat.test.ts +++ b/tests/unit/cursor-catalog-combo-compat.test.ts @@ -33,11 +33,11 @@ const LEGACY_GROK_ALIASES = { "grok-4.5-fast-xhigh": "cursor-grok-4.5-xhigh-fast", } as const; -test("keeps legacy Cursor combo model ids in the static catalog", () => { +test("keeps legacy Cursor combo model ids out of the curated static catalog", () => { const catalogIds = new Set(cursorProvider.models.map((model) => model.id)); for (const modelId of LEGACY_CURSOR_COMBO_MODEL_IDS) { - assert.ok(catalogIds.has(modelId), `missing Cursor catalog model: ${modelId}`); + assert.equal(catalogIds.has(modelId), false, `unexpected Cursor catalog model: ${modelId}`); } }); diff --git a/tests/unit/cursor-model-effort-suffix-7289.test.ts b/tests/unit/cursor-model-effort-suffix-7289.test.ts index 10e4023e14..aeb2f9cdb1 100644 --- a/tests/unit/cursor-model-effort-suffix-7289.test.ts +++ b/tests/unit/cursor-model-effort-suffix-7289.test.ts @@ -80,3 +80,35 @@ test("resolveRequestedModel splits cursor-grok effort + fast together", () => { ], }); }); + +test("resolveRequestedModel expands Claude 1M catalog ids into complete wire parameters", () => { + assert.deepEqual(resolveRequestedModel("claude-opus-5-thinking-max-fast-1m"), { + modelId: "claude-opus-5", + parameters: [ + { id: "thinking", value: "true" }, + { id: "context", value: "1m" }, + { id: "effort", value: "max" }, + { id: "fast", value: "true" }, + ], + }); + assert.deepEqual(resolveRequestedModel("claude-4.6-sonnet-high-thinking-1m"), { + modelId: "claude-sonnet-4-6", + parameters: [ + { id: "thinking", value: "true" }, + { id: "context", value: "1m" }, + { id: "effort", value: "high" }, + ], + }); +}); + +test("resolveRequestedModel expands GPT-5.6 1M ids and keeps fast disabled", () => { + const id = "gpt-5.6-sol-xhigh-1m"; + assert.deepEqual(resolveRequestedModel(id, { liveCatalogIds: new Set([id]) }), { + modelId: "gpt-5.6-sol", + parameters: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "xhigh" }, + { id: "fast", value: "false" }, + ], + }); +}); diff --git a/tests/unit/cursor-registry-claude-families.test.ts b/tests/unit/cursor-registry-claude-families.test.ts index 46a41970a0..0236f99682 100644 --- a/tests/unit/cursor-registry-claude-families.test.ts +++ b/tests/unit/cursor-registry-claude-families.test.ts @@ -1,51 +1,196 @@ -import test from "node:test"; import assert from "node:assert/strict"; +import test from "node:test"; + import { cursorProvider } from "../../open-sse/config/providers/registry/cursor/index.ts"; -const EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const; +const CURSOR_FAMILY_REPRESENTATIVES = [ + "cursor-grok-4.6-high-fast", + "composer-2.5", + "claude-fable-5-1-thinking-high", + "claude-opus-5-thinking-high", + "claude-opus-4-8-thinking-high", + "claude-sonnet-5-thinking-high", + "claude-4.6-sonnet-medium-thinking", + "claude-4.5-haiku-thinking", + "gpt-5.6-sol-medium", + "gpt-5.6-terra-medium", + "gpt-5.6-luna-medium", + "gemini-3.7-flash-high", + "gemini-3.1-pro", + "kimi-k3-max", + "kimi-k2.7-code", + "glm-5.2-high", +] as const; -function modelIds(): Set { - return new Set(cursorProvider.models.map((m) => m.id)); -} - -test("cursor registry excludes retired Gemini 3.5 Flash", () => { - assert.equal(modelIds().has("gemini-3.5-flash"), false); +test("cursor registry keeps every selected model family", () => { + const allIds = cursorProvider.models.map((model) => model.id); + const ids = new Set(allIds); + assert.equal(ids.size, allIds.length, "Cursor catalog model ids must be unique"); + for (const id of CURSOR_FAMILY_REPRESENTATIVES) { + assert.ok(ids.has(id), `missing Cursor model: ${id}`); + } }); -test("cursor registry includes Claude Opus 4.8 effort + thinking + fast variants", () => { - const ids = modelIds(); - for (const effort of EFFORTS) { - assert.ok(ids.has(`claude-opus-4-8-${effort}`), `missing claude-opus-4-8-${effort}`); - assert.ok(ids.has(`claude-opus-4-8-${effort}-fast`), `missing claude-opus-4-8-${effort}-fast`); +test("cursor registry omits redundant bare ids for parameterized models", () => { + const ids = new Set(cursorProvider.models.map((model) => model.id)); + for (const id of [ + "grok-4.6", + "claude-fable-5-1", + "claude-opus-5", + "claude-opus-4-8", + "claude-sonnet-5", + "claude-sonnet-4-6", + "claude-haiku-4-5", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gemini-3.7-flash", + "kimi-k3", + "glm-5.2", + ]) { + assert.equal(ids.has(id), false, `unexpected bare Cursor model: ${id}`); + } +}); + +test("cursor registry keeps thinking, effort/reasoning and fast variants selectable", () => { + const ids = new Set(cursorProvider.models.map((model) => model.id)); + for (const id of [ + "cursor-grok-4.6-xhigh-fast", + "composer-2.5-fast", + "claude-fable-5-1-thinking-max", + "claude-opus-5-thinking-xhigh-fast", + "claude-opus-4-8-thinking-max-fast", + "claude-sonnet-5-thinking-max", + "claude-4.6-sonnet-max-thinking", + "claude-4.5-haiku-thinking", + "gpt-5.6-sol-max-fast", + "gpt-5.6-terra-max-fast", + "gpt-5.6-luna-max-fast", + "gemini-3.7-flash-high", + "kimi-k3-max", + "glm-5.2-max", + ]) { + assert.ok(ids.has(id), `missing selectable Cursor variant: ${id}`); + } +}); + +test("cursor registry exposes every supported 1M context variant", () => { + const ids = cursorProvider.models.map((model) => model.id); + const oneMillionVariants = cursorProvider.models.filter((model) => model.id.endsWith("-1m")); + assert.equal(oneMillionVariants.length, 77); + for (const variant of oneMillionVariants) { + assert.match(variant.name, /\b1M\b/); + assert.equal(variant.contextLength, 1_000_000); + const position = ids.indexOf(variant.id); + assert.equal(ids[position + 1], variant.id.slice(0, -"-1m".length)); + } + for (const id of [ + "claude-fable-5-1-thinking-max-1m", + "claude-opus-5-thinking-max-fast-1m", + "claude-opus-4-8-thinking-max-fast-1m", + "claude-sonnet-5-thinking-max-1m", + "claude-4.6-sonnet-max-thinking-1m", + "gpt-5.6-sol-max-1m", + "gpt-5.6-terra-max-1m", + "gpt-5.6-luna-max-1m", + ]) { assert.ok( - ids.has(`claude-opus-4-8-thinking-${effort}`), - `missing claude-opus-4-8-thinking-${effort}` + oneMillionVariants.some((model) => model.id === id), + `missing 1M variant: ${id}` ); + } + assert.equal( + oneMillionVariants.some( + (model) => model.id.startsWith("gpt-5.6-") && model.id.includes("-fast") + ), + false, + "Cursor does not offer fast processing with GPT-5.6 1M context" + ); +}); + +test("cursor registry records the default context for context-selectable families", () => { + const models = new Map(cursorProvider.models.map((model) => [model.id, model])); + assert.equal(models.get("claude-fable-5-1-thinking-max")?.contextLength, 300_000); + assert.equal(models.get("claude-opus-5-thinking-max")?.contextLength, 300_000); + assert.equal(models.get("claude-opus-4-8-thinking-max")?.contextLength, 300_000); + assert.equal(models.get("claude-sonnet-5-thinking-max")?.contextLength, 300_000); + assert.equal(models.get("claude-4.6-sonnet-max-thinking")?.contextLength, 200_000); + assert.equal(models.get("gpt-5.6-sol-max")?.contextLength, 272_000); + assert.equal(models.get("gpt-5.6-terra-max")?.contextLength, 272_000); + assert.equal(models.get("gpt-5.6-luna-max")?.contextLength, 272_000); +}); + +test("cursor registry orders each model family by quality, thinking and speed", () => { + const ids = cursorProvider.models.map((model) => model.id); + for (const orderedIds of [ + ["cursor-grok-4.6-xhigh-fast", "cursor-grok-4.6-xhigh", "cursor-grok-4.6-low"], + ["composer-2.5-fast", "composer-2.5"], + ["claude-fable-5-1-thinking-max", "claude-fable-5-1-thinking-low"], + ["claude-opus-5-thinking-high-fast", "claude-opus-5-high-fast", "claude-opus-5-low"], + ["claude-opus-4-8-thinking-max-fast", "claude-opus-4-8-max-fast", "claude-opus-4-8-low"], + ["claude-sonnet-5-thinking-max", "claude-sonnet-5-max", "claude-sonnet-5-low"], + ["claude-4.6-sonnet-max-thinking", "claude-4.6-sonnet-max", "claude-4.6-sonnet-low"], + ["claude-4.5-haiku-thinking", "claude-4.5-haiku"], + ["gpt-5.6-sol-max-fast", "gpt-5.6-sol-max", "gpt-5.6-sol-none"], + ["gpt-5.6-terra-max-fast", "gpt-5.6-terra-max", "gpt-5.6-terra-none"], + ["gpt-5.6-luna-max-fast", "gpt-5.6-luna-max", "gpt-5.6-luna-none"], + ["gemini-3.7-flash-high", "gemini-3.7-flash-low"], + ["kimi-k3-max", "kimi-k3-low"], + ["glm-5.2-max", "glm-5.2-high"], + ]) { + const positions = orderedIds.map((id) => ids.indexOf(id)); assert.ok( - ids.has(`claude-opus-4-8-thinking-${effort}-fast`), - `missing claude-opus-4-8-thinking-${effort}-fast` + positions.every((position) => position >= 0), + `missing ordered ids: ${orderedIds}` + ); + assert.deepEqual( + positions, + [...positions].sort((left, right) => left - right) ); } }); -test("cursor registry includes Claude Fable 5 effort + thinking variants", () => { - const ids = modelIds(); - for (const effort of EFFORTS) { - assert.ok(ids.has(`claude-fable-5-${effort}`), `missing claude-fable-5-${effort}`); - assert.ok( - ids.has(`claude-fable-5-thinking-${effort}`), - `missing claude-fable-5-thinking-${effort}` +test("cursor registry uses compact Xhigh labels", () => { + const xhighVariants = cursorProvider.models.filter((model) => model.id.includes("xhigh")); + assert.ok(xhighVariants.length > 0); + for (const variant of xhighVariants) { + assert.match(variant.name, /\bXhigh\b/); + assert.doesNotMatch(variant.name, /Extra High/); + } +}); + +test("cursor registry excludes unrelated model families", () => { + const ids = cursorProvider.models.map((model) => model.id); + for (const fragment of [ + "grok-4.5", + "gpt-5.5", + "gpt-5.4", + "gpt-5.3", + "gpt-5.2", + "claude-fable-5-thinking", + "claude-opus-4-7", + "gemini-3.6-flash", + "gemini-3.5-flash", + "gemini-3-flash", + ]) { + assert.equal( + ids.some((id) => id.includes(fragment)), + false, + `unexpected Cursor model family: ${fragment}` ); } }); -test("cursor registry includes Claude Sonnet 5 effort + thinking variants", () => { - const ids = modelIds(); - for (const effort of EFFORTS) { - assert.ok(ids.has(`claude-sonnet-5-${effort}`), `missing claude-sonnet-5-${effort}`); - assert.ok( - ids.has(`claude-sonnet-5-thinking-${effort}`), - `missing claude-sonnet-5-thinking-${effort}` - ); +test("cursor registry keeps Fable 5.1 capability metadata on every selectable variant", () => { + const variants = cursorProvider.models.filter((model) => + model.id.startsWith("claude-fable-5-1-thinking-") + ); + assert.equal(variants.length, 10); + assert.deepEqual( + new Set(variants.map((variant) => variant.contextLength)), + new Set([300_000, 1_000_000]) + ); + for (const variant of variants) { + assert.equal(variant.maxOutputTokens, 128_000); } }); diff --git a/tests/unit/devin-cli-catalog.test.ts b/tests/unit/devin-cli-catalog.test.ts index 7cd92e15b1..330ee8ac31 100644 --- a/tests/unit/devin-cli-catalog.test.ts +++ b/tests/unit/devin-cli-catalog.test.ts @@ -2,54 +2,124 @@ import assert from "node:assert/strict"; import test from "node:test"; import { devin_cliProvider } from "../../open-sse/config/providers/registry/devin-cli/index.ts"; +import { devin_cli_agenticProvider } from "../../open-sse/config/providers/registry/devin-cli-agentic/index.ts"; import { devin_desktopProvider } from "../../open-sse/config/providers/registry/devin-desktop/index.ts"; import { DEVIN_MODEL_CATALOG } from "../../open-sse/config/providers/registry/devin/catalog.ts"; +import { DEVIN_MODEL_PRICING } from "../../src/shared/constants/pricing/devin.ts"; +import { DEFAULT_PRICING, getPricingForModel } from "../../src/shared/constants/pricing.ts"; -test("Devin CLI and Desktop use the shared catalog without duplicate model ids", () => { - const ids = DEVIN_MODEL_CATALOG.map((model) => model.id); +const catalogIds = DEVIN_MODEL_CATALOG.map((model) => model.id); +test("Devin transports expose the same curated catalog without duplicate ids", () => { assert.equal(devin_cliProvider.models, DEVIN_MODEL_CATALOG); assert.equal(devin_desktopProvider.models, DEVIN_MODEL_CATALOG); - assert.equal(new Set(ids).size, ids.length); - assert.ok(ids.every((id) => !id.toLowerCase().includes("byok"))); + assert.deepEqual( + devin_cli_agenticProvider.models.map((model) => model.id), + catalogIds + ); + assert.equal(catalogIds.length, 110); + assert.equal(new Set(catalogIds).size, catalogIds.length); + assert.ok(catalogIds.every((id) => !id.toLowerCase().includes("byok"))); }); -test("Devin CLI catalog includes the refreshed native model ids", () => { - const ids = new Set(DEVIN_MODEL_CATALOG.map((model) => model.id)); +test("Devin catalog contains only the operator-selected model families", () => { + const required = [ + "claude-fable-5-1-max", + "claude-opus-5-max-fast", + "claude-opus-4-8-max-fast", + "claude-sonnet-5-max", + "claude-sonnet-4-6-thinking-1m", + "MODEL_PRIVATE_11", + "gpt-5-6-sol-max-priority", + "gpt-5-6-terra-max-priority", + "gpt-5-6-luna-max-priority", + "kimi-k3-max", + "kimi-k2-7", + "glm-5-3-max", + "glm-5-3-flash-max", + "swe-1-7", + "swe-1-7-lightning", + "adaptive", + "grok-4-6-xhigh", + "inkling-max", + "deepseek-v4-flash-max", + "nemotron-3-ultra-high", + "gemini-3-7-flash-high", + "gemini-3-1-pro-high", + "deepseek-v4-pro-max", + ]; + + for (const id of required) { + assert.ok(catalogIds.includes(id), `expected selected Devin model id: ${id}`); + } for (const id of [ - "swe-1-7-lightning", "claude-5-fable-max", - "gpt-5-6-sol-max", + "claude-opus-4-7-max", "gpt-5-5-high", - "glm-5-2-max-1m", - "claude-opus-5-low", - "claude-opus-5-medium", - "claude-opus-5-high", - "claude-opus-5-xhigh", - "claude-opus-5-max", - "gemini-3-7-flash-minimal", - "gemini-3-7-flash-low", - "gemini-3-7-flash-medium", - "gemini-3-7-flash-high", - "kimi-k3-low", - "kimi-k3-high", - "kimi-k3-max", - "inkling-none", - "inkling-low", - "inkling-medium", - "inkling-high", - "inkling-xhigh", - "inkling-max", + "gemini-3-6-flash-high", + "grok-4-5-high", + "deepseek-v4", + "nemotron-3-ultra-nvfp4", + "swe-1-6-fast", ]) { - assert.ok(ids.has(id), `expected refreshed Devin model id: ${id}`); + assert.equal(catalogIds.includes(id), false, `unselected Devin model must stay absent: ${id}`); } }); -test("Devin CLI catalog does not expose retired dotted or review model ids", () => { - const ids = new Set(DEVIN_MODEL_CATALOG.map((model) => model.id)); +test("Devin catalog keeps higher-quality choices first", () => { + assert.deepEqual(catalogIds.slice(0, 5), [ + "claude-fable-5-1-max", + "claude-fable-5-1-xhigh", + "claude-fable-5-1-high", + "claude-fable-5-1-medium", + "claude-fable-5-1-low", + ]); + assert.deepEqual(catalogIds.slice(5, 9), [ + "claude-opus-5-max-fast", + "claude-opus-5-max", + "claude-opus-5-xhigh-fast", + "claude-opus-5-xhigh", + ]); +}); - for (const id of ["swe-1.6-fast", "swe-1.6", "claude-opus-4.7-review"]) { - assert.equal(ids.has(id), false, `retired Devin model id must stay absent: ${id}`); +test("every curated Devin model has an exact live provider price", () => { + assert.deepEqual(new Set(Object.keys(DEVIN_MODEL_PRICING)), new Set(catalogIds)); + + for (const provider of ["devin-cli", "dv", "devin-desktop", "devin-cli-agentic", "dva"]) { + for (const id of catalogIds) { + assert.ok(getPricingForModel(provider, id), `missing ${provider}/${id} pricing`); + } } }); + +test("Devin pricing remains provider-bound and preserves fast-tier rates", () => { + assert.notEqual(DEFAULT_PRICING["devin-cli"], DEFAULT_PRICING.anthropic); + assert.deepEqual(getPricingForModel("devin-cli", "claude-sonnet-5-max"), { + input: 2, + cached: 0.2, + output: 10, + }); + assert.deepEqual(getPricingForModel("anthropic", "claude-sonnet-5"), { + input: 3, + output: 15, + cached: 1.5, + reasoning: 22.5, + cache_creation: 3, + }); + assert.deepEqual(getPricingForModel("devin-cli", "gpt-5-6-sol-max-priority"), { + input: 8, + cached: 0.8, + output: 40, + }); +}); + +test("Devin catalog carries the live output limits for representative models", () => { + const models = new Map(DEVIN_MODEL_CATALOG.map((entry) => [entry.id, entry])); + + assert.equal(models.get("claude-fable-5-1-max")?.maxOutputTokens, 128_000); + assert.equal(models.get("MODEL_PRIVATE_11")?.maxOutputTokens, 64_000); + assert.equal(models.get("kimi-k2-7")?.maxOutputTokens, 16_000); + assert.equal(models.get("grok-4-6-xhigh")?.maxOutputTokens, 100_000); + assert.equal(models.get("gemini-3-7-flash-high")?.maxOutputTokens, 65_535); +}); diff --git a/tests/unit/fixtures/cursor-rewrite-failure-ids.ts b/tests/unit/fixtures/cursor-rewrite-failure-ids.ts index dd064537c6..641d383cb8 100644 --- a/tests/unit/fixtures/cursor-rewrite-failure-ids.ts +++ b/tests/unit/fixtures/cursor-rewrite-failure-ids.ts @@ -4,11 +4,16 @@ * Smoke checklist for catalog-aware pass-through. */ export const CURSOR_REWRITE_FAILURE_IDS = [ - // Claude (52) + // Claude (57) "claude-4.5-opus-high", "claude-4.6-opus-high", "claude-4.6-opus-max", "claude-4.6-sonnet-medium", + "claude-fable-5-1-thinking-low", + "claude-fable-5-1-thinking-medium", + "claude-fable-5-1-thinking-high", + "claude-fable-5-1-thinking-xhigh", + "claude-fable-5-1-thinking-max", "claude-fable-5-low", "claude-fable-5-medium", "claude-fable-5-high", diff --git a/tests/unit/guardrails/visionBridgeRouter.test.ts b/tests/unit/guardrails/visionBridgeRouter.test.ts index 3197f08e20..dc685a61ff 100644 --- a/tests/unit/guardrails/visionBridgeRouter.test.ts +++ b/tests/unit/guardrails/visionBridgeRouter.test.ts @@ -263,12 +263,12 @@ test("getFallbackModels — excludes fallbacks missing from an authoritative liv test("getFallbackModels — keeps registered effort variants backed by a live base model", async () => { const fallbacks = await getFallbackModels( - "cu/gpt-5.3-codex", + "cu/claude-fable-5-1-thinking-max", { maxFallbackAttempts: 6 }, - authoritativeCatalogDeps("cu", () => ["gpt-5.3-codex"]) + authoritativeCatalogDeps("cu", () => ["claude-fable-5-1"]) ); - assert.ok(fallbacks.includes("cu/gpt-5.3-codex-low")); + assert.ok(fallbacks.includes("cu/claude-fable-5-1-thinking-high")); }); // ── recordLatency / getLatencyStats ───────────────────────────────────────── diff --git a/tests/unit/live-model-catalog-reconciliation-8926.test.ts b/tests/unit/live-model-catalog-reconciliation-8926.test.ts index 4bd415dee3..5bc1d15c54 100644 --- a/tests/unit/live-model-catalog-reconciliation-8926.test.ts +++ b/tests/unit/live-model-catalog-reconciliation-8926.test.ts @@ -117,24 +117,24 @@ test("#8926: explicit custom model overrides live-catalog exclusion", async () = }); test("#8926: effort helper identifies only explicitly registered variants", () => { - assert.equal(isRegisteredProviderEffortVariant("cursor", "gpt-5.3-codex-high"), true); + assert.equal(isRegisteredProviderEffortVariant("cursor", "claude-fable-5-1-thinking-high"), true); assert.equal( - isRegisteredProviderEffortVariant("cursor", "gpt-5.3-codex-max"), + isRegisteredProviderEffortVariant("cursor", "claude-fable-5-1-thinking-ultra"), false, "an invented suffix must not bypass live-catalog authority" ); }); test("#8926: registered effort route survives while invented effort route is rejected", async () => { - await seedProviderCatalog("cursor", "cursor-live-8926", ["gpt-5.3-codex"]); + await seedProviderCatalog("cursor", "cursor-live-8926", ["claude-fable-5-1"]); - const registered = await getModelInfo("cursor/gpt-5.3-codex-high"); + const registered = await getModelInfo("cursor/claude-fable-5-1-thinking-high"); assert.equal(registered.provider, "cursor"); - assert.equal(registered.model, "gpt-5.3-codex-high"); + assert.equal(registered.model, "claude-fable-5-1-thinking-high"); - const invented = await getModelInfo("cursor/gpt-5.3-codex-max"); + const invented = await getModelInfo("cursor/claude-fable-5-1-thinking-ultra"); assert.equal(invented.provider, null); assert.equal(invented.errorType, "model_not_found"); @@ -170,13 +170,13 @@ test("#8926: providers without an authoritative live catalog retain static fallb test("#8926: registered effort variant is rejected when its live base is absent", async () => { await seedProviderCatalog("cursor", "cursor-live-without-base-8926", ["cursor-live-only-8926"]); - const explicit = await getModelInfo("cursor/gpt-5.3-codex-high"); + const explicit = await getModelInfo("cursor/claude-fable-5-1-thinking-high"); assert.equal(explicit.provider, null); assert.equal(explicit.errorType, "model_not_found"); assert.match(explicit.errorMessage, /active live catalog/i); - const bare = await getModelInfo("gpt-5.3-codex-high"); + const bare = await getModelInfo("claude-fable-5-1-thinking-high"); assert.equal(bare.provider, null); assert.equal(bare.errorType, "model_not_found"); diff --git a/tests/unit/pricing-constants-split.test.ts b/tests/unit/pricing-constants-split.test.ts index 28acb759c4..90970e1730 100644 --- a/tests/unit/pricing-constants-split.test.ts +++ b/tests/unit/pricing-constants-split.test.ts @@ -1,5 +1,5 @@ // Characterization of the pricing.ts split (god-file decomposition): the host became a barrel that -// re-exports DEFAULT_PRICING (now merged from 4 semantic family files that import shared tier consts) +// re-exports DEFAULT_PRICING (merged from semantic family files that import shared tier consts) // and keeps the helper functions. Pure-data move → behavior identical. Locks: public surface, the // spread-merge integrity, and that lookups/cost math resolve unchanged. import { test } from "node:test"; @@ -14,13 +14,14 @@ test("barrel still exports DEFAULT_PRICING + supported helpers", () => { assert.equal(Object.hasOwn(P, "calculateCostFromTokens"), false); }); -test("DEFAULT_PRICING merges the 4 family files; families partition all entries", async () => { +test("DEFAULT_PRICING merges every family file; families partition all entries", async () => { const merged = Object.keys((P as Record).DEFAULT_PRICING).length; const families: [string, string][] = [ ["oauth-subscriptions", "DEFAULT_PRICING_OAUTH"], ["frontier-labs", "DEFAULT_PRICING_FRONTIER"], ["inference-hosts", "DEFAULT_PRICING_INFERENCE"], ["regional", "DEFAULT_PRICING_REGIONAL"], + ["devin", "DEFAULT_PRICING_DEVIN"], ]; let famTotal = 0; const seen = new Set(); diff --git a/tests/unit/provider-cost-data.test.ts b/tests/unit/provider-cost-data.test.ts new file mode 100644 index 0000000000..157769dfbf --- /dev/null +++ b/tests/unit/provider-cost-data.test.ts @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { getModelPricing, KNOWN_MODEL_PRICING } from "../../open-sse/services/providerCostData.ts"; + +test("provider-specific pricing wins over a generic model fallback", () => { + const genericKey = "provider-price-test-model"; + const providerKey = `devin-cli/${genericKey}`; + const previousGeneric = KNOWN_MODEL_PRICING[genericKey]; + const previousProvider = KNOWN_MODEL_PRICING[providerKey]; + + KNOWN_MODEL_PRICING[genericKey] = { + inputCostPer1M: 9, + outputCostPer1M: 90, + isFree: false, + }; + KNOWN_MODEL_PRICING[providerKey] = { + inputCostPer1M: 1, + outputCostPer1M: 10, + isFree: false, + }; + + try { + assert.deepEqual(getModelPricing("devin-cli", genericKey), { + inputCostPer1M: 1, + outputCostPer1M: 10, + isFree: false, + }); + } finally { + if (previousGeneric) KNOWN_MODEL_PRICING[genericKey] = previousGeneric; + else delete KNOWN_MODEL_PRICING[genericKey]; + if (previousProvider) KNOWN_MODEL_PRICING[providerKey] = previousProvider; + else delete KNOWN_MODEL_PRICING[providerKey]; + } +}); + +test("tier pricing reads the exact Devin provider/model rate", () => { + assert.deepEqual(getModelPricing("devin-cli", "gpt-5-6-luna-max"), { + inputCostPer1M: 0.2, + outputCostPer1M: 1.2, + isFree: false, + }); + assert.deepEqual(getModelPricing("devin-cli", "gpt-5-6-luna-max-priority"), { + inputCostPer1M: 0.4, + outputCostPer1M: 2.4, + isFree: false, + }); +}); diff --git a/tests/unit/provider-models-config.test.ts b/tests/unit/provider-models-config.test.ts index 793605cc4f..51a5f62d18 100644 --- a/tests/unit/provider-models-config.test.ts +++ b/tests/unit/provider-models-config.test.ts @@ -141,16 +141,22 @@ test("GitHub Copilot registry reflects the current supported model lineup", () = assert.equal(ids.includes("gemini-3-flash-preview"), false); }); -test("Claude flagship catalogs keep Fable 5 first", () => { - for (const provider of ["anthropic", "cc", "cw", "gh", "ghe-copilot"]) { +test("verified Anthropic launch catalogs keep Fable 5.1 first", () => { + for (const provider of ["anthropic", "cc", "cw"]) { assert.equal( getProviderModels(provider)[0]?.id, - "claude-fable-5", + "claude-fable-5-1", `${provider} must list the strongest Claude model first` ); } }); +test("Copilot catalogs retain Fable 5 until their Fable 5.1 IDs are verified", () => { + for (const provider of ["gh", "ghe-copilot"]) { + assert.equal(getProviderModels(provider)[0]?.id, "claude-fable-5"); + } +}); + test("Kiro registry exposes the current CLI model lineup with context windows", () => { const kiroModels = getProviderModels("kr"); const byId = new Map(kiroModels.map((model) => [model.id, model]));