feat(providers): refresh Fable, Cursor, and Devin catalogs (#12367)

Validado em lote numa worktree combinada com #12524, #12538, #12277 e #12367 sobre o tip de release/v3.8.51: os quatro boardaram sem conflito (áreas disjuntas — zai-web, nvidia, clova, cursor/devin/fable). typecheck:core limpo, check:provider-consistency OK (272 entradas REGISTRY, 355 providers canônicos), check:known-symbols OK, e 305/305 nos testes tocados pelos quatro PRs. Os IDs de modelo adicionados foram conferidos individualmente. Obrigado, @backryun.
This commit is contained in:
backryun
2026-09-03 19:50:41 +09:00
committed by GitHub
parent 82c64d76d3
commit 6795783228
42 changed files with 1835 additions and 921 deletions

View File

@@ -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": {

View File

@@ -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)))
);
}

View File

@@ -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)))
);
}
}

View File

@@ -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",

View File

@@ -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)",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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/<id> 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 <version>-<family> ("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 <family>-<version> 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 <version>-<family> ("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 <family>-<version> 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" },
],
};

View File

@@ -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"],
]),
];

View File

@@ -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" },

View File

@@ -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" },

View File

@@ -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({

View File

@@ -78,8 +78,9 @@ const FAMILY_FALLBACK_TEMPLATES: Record<string, readonly string[]> = {
"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

View File

@@ -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<string, ModelPricing> = {
"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<string, ModelPricing> = {
};
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 };
}

View File

@@ -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<Record<number, InteractionUpdateDecoder>> = {
[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<string, bytes>; 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<string, unknown> = {};
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<string, unknown> = {};
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<Record<number, ExecEventDecoder>> = {
[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<Record<number, ProtobufValueDecoder>> = {
[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<string, unknown> {
const result: Record<string, unknown> = {};
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);
}

View File

@@ -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)
);
}

View File

@@ -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;

View File

@@ -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<Active
.filter((connection): connection is ProviderConnectionRef => 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<Active
// NON-authoritative: #9294's live-catalog gating is about what an active
// connection actually serves, so a node-backed catalog must inform metadata
// without ever being used to reject a model as unavailable.
return { authoritative: false, models: await getSyncedAvailableModels(storedProviderId) };
return {
authoritative: false,
models: enrichCursorCatalog(
storedProviderId,
await getSyncedAvailableModels(storedProviderId)
),
};
} catch {
return { authoritative: false, models: [] };
}
@@ -161,7 +184,10 @@ export async function getAllActiveSyncedModels(): Promise<Record<string, SyncedA
Array.from(connectionIdsByProvider.entries()).map(async ([providerId, connectionIds]) => {
const modelsByConnection = await getSyncedAvailableModelsByConnection(providerId);
const models = collectModelsForConnections(modelsByConnection, connectionIds);
const models = enrichCursorCatalog(
providerId,
collectModelsForConnections(modelsByConnection, connectionIds)
);
if (models.length > 0) {
result[providerId] = models;

View File

@@ -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<T extends CursorAutoCatalogEntry>(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");

View File

@@ -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<string, unknown>, id: string): string {
return humanizeCursorModelId(id);
}
function collectArrays(record: Record<string, unknown>, 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<string, unknown>): 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<string, unknown>, 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<string>();
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<CursorAgentModelEntry[]> {

View File

@@ -35,6 +35,7 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => 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" },

View File

@@ -45,7 +45,7 @@ export const CLI_TOOLS: Record<string, CliCatalogEntry> = {
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,
},
{

View File

@@ -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<string, ModelSpec> = {
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<T extends Record<string, unknown>>(
getModelSpec(modelId)?.rejectsThinkingDisabled
) {
const { thinking: _omitted, ...rest } = body as Record<string, unknown>;
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<T extends Record<string, unknown>>(
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<string, unknown>).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 {

View File

@@ -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,
};

View File

@@ -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<string, DevinTokenPricing> = {
...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 },
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
});
});

View File

@@ -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",

View File

@@ -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);
});
});

View File

@@ -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");

View File

@@ -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}`);
}
});

View File

@@ -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" },
],
});
});

View File

@@ -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<string> {
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);
}
});

View File

@@ -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);
});

View File

@@ -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",

View File

@@ -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 ─────────────────────────────────────────

View File

@@ -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");

View File

@@ -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<string, object>).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<string>();

View File

@@ -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,
});
});

View File

@@ -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]));