mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
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:
@@ -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": {
|
||||
|
||||
@@ -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)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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"],
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
113
open-sse/utils/cursorAgentProtobuf/requestedModelParameters.ts
Normal file
113
open-sse/utils/cursorAgentProtobuf/requestedModelParameters.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
142
src/shared/constants/pricing/devin.ts
Normal file
142
src/shared/constants/pricing/devin.ts
Normal 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 },
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
172
tests/unit/claude-fable-5-1.test.ts
Normal file
172
tests/unit/claude-fable-5-1.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ─────────────────────────────────────────
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>();
|
||||
|
||||
48
tests/unit/provider-cost-data.test.ts
Normal file
48
tests/unit/provider-cost-data.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
@@ -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]));
|
||||
|
||||
Reference in New Issue
Block a user