diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 7074fcb3dd..81cfb498a7 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -245,11 +245,7 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "kilo-gateway", modelId: "nvidia/nemotron-3-ultra-550b-a55b:free", displayName: "NVIDIA Nemotron 3 Ultra (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "kilo-gateway-free", tos: "caution" }, { provider: "kilo-gateway", modelId: "nvidia/nemotron-3-super-120b-a12b:free", displayName: "NVIDIA Nemotron 3 Super (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "kilo-gateway-free", tos: "caution" }, { provider: "kilo-gateway", modelId: "nex-agi/nex-n2-pro:free", displayName: "Nex-N2-Pro (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "kilo-gateway-free", tos: "caution" }, - { provider: "kiro", modelId: "auto-kiro", displayName: "Auto (Kiro picks best model)", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, - { provider: "kiro", modelId: "claude-opus-4.8", displayName: "Claude Opus 4.8", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, - { provider: "kiro", modelId: "claude-opus-4.7", displayName: "Claude Opus 4.7", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, - { provider: "kiro", modelId: "claude-opus-4.6", displayName: "Claude Opus 4.6", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, - { provider: "kiro", modelId: "claude-sonnet-4.6", displayName: "Claude Sonnet 4.6", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, + { provider: "kiro", modelId: "claude-sonnet-4.5", displayName: "Claude Sonnet 4.5", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, { provider: "kiro", modelId: "claude-haiku-4.5", displayName: "Claude Haiku 4.5", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, { provider: "kiro", modelId: "deepseek-3.2", displayName: "DeepSeek V3.2", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, { provider: "kiro", modelId: "minimax-m2.5", displayName: "MiniMax M2.5", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" }, diff --git a/open-sse/config/providers/registry/kiro/index.ts b/open-sse/config/providers/registry/kiro/index.ts index 6c4d41cf6e..71262a4f09 100644 --- a/open-sse/config/providers/registry/kiro/index.ts +++ b/open-sse/config/providers/registry/kiro/index.ts @@ -15,32 +15,14 @@ export const kiroProvider: RegistryEntry = { tokenUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken", authUrl: "https://prod.us-east-1.auth.desktop.kiro.dev", }, + // Model IDs must match Kiro's real upstream catalog exactly — an unknown id + // makes Kiro return `400 "Invalid model. Please select a different model"`. + // Fabricated ids (auto-kiro, claude-opus-4.x, claude-fable-5, claude-sonnet-4.6) + // were removed after live VPS validation: Kiro offers no Opus/Fable, its Sonnet + // is 4.5 (not 4.6), and there is no "auto" model id (it was sent verbatim and + // 400'd). claude-sonnet-5 is a real Kiro model but plan-gated per account — + // kept so entitled accounts can use it. See kiro cluster #6112/#6113/#6099. models: [ - { id: "auto-kiro", name: "Auto (Kiro picks best model)" }, - { - id: "claude-fable-5", - name: "Claude Fable 5", - contextLength: 1000000, - maxOutputTokens: 128000, - }, - { - id: "claude-opus-4.8", - name: "Claude Opus 4.8", - contextLength: 1000000, - maxOutputTokens: 128000, - }, - { - id: "claude-opus-4.7", - name: "Claude Opus 4.7", - contextLength: 1000000, - maxOutputTokens: 128000, - }, - { - id: "claude-opus-4.6", - name: "Claude Opus 4.6", - contextLength: 1000000, - maxOutputTokens: 128000, - }, { id: "claude-sonnet-5", name: "Claude Sonnet 5", @@ -48,8 +30,8 @@ export const kiroProvider: RegistryEntry = { maxOutputTokens: 128000, }, { - id: "claude-sonnet-4.6", - name: "Claude Sonnet 4.6", + id: "claude-sonnet-4.5", + name: "Claude Sonnet 4.5", contextLength: 200000, maxOutputTokens: 64000, }, diff --git a/open-sse/translator/request/openai-to-kiro.ts b/open-sse/translator/request/openai-to-kiro.ts index fc6d39abfd..02cbd4aa1e 100644 --- a/open-sse/translator/request/openai-to-kiro.ts +++ b/open-sse/translator/request/openai-to-kiro.ts @@ -57,9 +57,9 @@ function convertMessages(messages, tools, model) { let toolsAttached = false; // Only Claude models support images in Kiro. Kiro also routes non-Claude - // models (deepseek, minimax, glm, qwen3-coder-next, auto-kiro) that do not - // accept image attachments — gate image extraction behind a Claude check so - // we never attach images those models would reject. + // models (deepseek, minimax, glm, qwen3-coder-next) that do not accept image + // attachments — gate image extraction behind a Claude check so we never + // attach images those models would reject. const supportsImages = typeof model === "string" && model.toLowerCase().includes("claude"); const flushPending = () => { diff --git a/src/shared/constants/pricing/oauth-subscriptions.ts b/src/shared/constants/pricing/oauth-subscriptions.ts index b5aa770351..04792b5ea1 100644 --- a/src/shared/constants/pricing/oauth-subscriptions.ts +++ b/src/shared/constants/pricing/oauth-subscriptions.ts @@ -584,7 +584,10 @@ export const DEFAULT_PRICING_OAUTH = { reasoning: 8.0, cache_creation: 2.0, }, - // Kiro "Auto" model — routes to best available + // Kiro "Auto" pricing — retained as a fallback price for any legacy "auto" + // reference. The "auto-kiro" registry model was removed (Kiro's API has no + // "auto" model id — it 400'd "Invalid model"), so its dedicated price key + // was dropped with it. See kiro cluster #6112/#6113/#6099. auto: { input: 3.0, output: 15.0, @@ -592,13 +595,5 @@ export const DEFAULT_PRICING_OAUTH = { reasoning: 15.0, cache_creation: 3.0, }, - // Registry exposes the Auto model as id "auto-kiro" — keep both keys priced. - "auto-kiro": { - input: 3.0, - output: 15.0, - cached: 1.5, - reasoning: 15.0, - cache_creation: 3.0, - }, }, }; diff --git a/tests/unit/catalog-updates-v3x.test.ts b/tests/unit/catalog-updates-v3x.test.ts index a246171cea..0f3bbc80ee 100644 --- a/tests/unit/catalog-updates-v3x.test.ts +++ b/tests/unit/catalog-updates-v3x.test.ts @@ -47,23 +47,19 @@ test("NVIDIA catalog includes the verified 2026 additions and GPT OSS 20B alias }); }); -test("Fable 5 catalog exposes claude-fable-5 in cc and kiro providers with matching pricing", () => { +test("Fable 5 catalog exposes claude-fable-5 in cc — but NOT via Kiro (fabricated)", () => { + // claude-fable-5 is a real Claude flagship served by the Claude Code (cc) channel, + // but Kiro's upstream never served it — it had been copied verbatim into the Kiro + // registry from OmniRoute's own Anthropic catalog and returned upstream 400 + // "Invalid model". #6170 removed the fabricated Kiro copy. const ccIds = new Set(getModelsByProviderId("cc").map((m) => m.id)); assert.ok(ccIds.has("claude-fable-5"), "cc must expose claude-fable-5"); - const kiroModels = getModelsByProviderId("kiro"); - const kiroIds = new Set(kiroModels.map((m) => m.id)); - assert.ok(kiroIds.has("claude-fable-5"), "kiro must expose claude-fable-5"); - - const fable = kiroModels.find((m) => m.id === "claude-fable-5"); - assert.equal(fable?.contextLength, 1000000); - assert.equal(fable?.maxOutputTokens, 128000); - const ccPricing = (DEFAULT_PRICING as Record>).cc; assert.ok(ccPricing["claude-fable-5"], "cc pricing must include claude-fable-5"); - const kiroPricing = (DEFAULT_PRICING as Record>).kiro; - assert.ok(kiroPricing["claude-fable-5"], "kiro pricing must include claude-fable-5"); + const kiroIds = new Set(getModelsByProviderId("kiro").map((m) => m.id)); + assert.equal(kiroIds.has("claude-fable-5"), false, "kiro must NOT expose claude-fable-5 (fabricated)"); }); test("Sonnet 5 catalog exposes claude-sonnet-5 across cc/kiro/anthropic/blackbox with Sonnet-tier pricing", () => { @@ -89,20 +85,15 @@ test("Sonnet 5 catalog exposes claude-sonnet-5 across cc/kiro/anthropic/blackbox assert.equal(kiroSonnet5Price.output, 15.0); }); -test("Kiro catalog exposes Claude Opus 4.8 alongside 4.7 with matching pricing", () => { - const models = getModelsByProviderId("kiro"); - const ids = new Set(models.map((model) => model.id)); +test("Kiro catalog does NOT expose Claude Opus (fabricated — Kiro upstream has no Opus)", () => { + // Kiro's real upstream never served any Opus model; the Opus 4.8/4.7/4.6 ids had been + // copied into the Kiro registry from OmniRoute's Anthropic catalog and returned upstream + // 400 "Invalid model. Please select a different model". #6170 removed them. + const ids = new Set(getModelsByProviderId("kiro").map((model) => model.id)); - assert.ok(ids.has("claude-opus-4.8"), "kiro must expose claude-opus-4.8"); - assert.ok(ids.has("claude-opus-4.7"), "kiro must still expose claude-opus-4.7"); - - const opus48 = models.find((model) => model.id === "claude-opus-4.8"); - assert.equal(opus48?.contextLength, 1000000); - assert.equal(opus48?.maxOutputTokens, 128000); - - // Pricing for the Kiro channel must cover the new model so usage cost is non-zero. - const kiroPricing = (DEFAULT_PRICING as Record>).kiro; - assert.ok(kiroPricing["claude-opus-4.8"], "kiro pricing must include claude-opus-4.8"); + assert.equal(ids.has("claude-opus-4.8"), false, "kiro must NOT expose claude-opus-4.8 (fabricated)"); + assert.equal(ids.has("claude-opus-4.7"), false, "kiro must NOT expose claude-opus-4.7 (fabricated)"); + assert.equal(ids.has("claude-opus-4.6"), false, "kiro must NOT expose claude-opus-4.6 (fabricated)"); }); test("Every Kiro registry model resolves a non-zero pricing row (no $0.00 usage)", async () => { @@ -124,14 +115,15 @@ test("Every Kiro registry model resolves a non-zero pricing row (no $0.00 usage) ); } - // Regression guard for the reported issue: Sonnet 4.6 must be priced like Sonnet 4.5. - const sonnet46 = getPricingForModel("kiro", "claude-sonnet-4.6") as { + // Regression guard: Kiro's real Sonnet is 4.5 (the "4.6" id was fabricated — #6170) and + // must carry Sonnet-tier pricing ($3/$15). + const sonnet45 = getPricingForModel("kiro", "claude-sonnet-4.5") as { input: number; output: number; } | null; - assert.ok(sonnet46, "kiro pricing must include claude-sonnet-4.6"); - assert.equal(sonnet46?.input, 3.0); - assert.equal(sonnet46?.output, 15.0); + assert.ok(sonnet45, "kiro pricing must include claude-sonnet-4.5"); + assert.equal(sonnet45?.input, 3.0); + assert.equal(sonnet45?.output, 15.0); }); test("Every OpenAI registry model resolves a non-zero pricing row (alias: openai)", async () => { diff --git a/tests/unit/kiro-catalog-real-models.test.ts b/tests/unit/kiro-catalog-real-models.test.ts new file mode 100644 index 0000000000..e896ca5e44 --- /dev/null +++ b/tests/unit/kiro-catalog-real-models.test.ts @@ -0,0 +1,58 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { REGISTRY } from "@omniroute/open-sse/config/providers/index.ts"; +import { FREE_MODEL_BUDGETS } from "@omniroute/open-sse/config/freeModelCatalog.data.ts"; + +// Kiro's upstream (`generateAssistantResponse`) returns 400 "Invalid model. +// Please select a different model" for any model id it does not recognize. The +// registry must therefore expose ONLY ids Kiro actually serves. These ids were +// fabricated (copied from OmniRoute's own Anthropic catalog) and live-verified +// to 400 on the VPS — they must never reappear. Regression guard for the kiro +// cluster (#6112/#6113/#6099). +const FABRICATED_KIRO_IDS = [ + "auto-kiro", // no "auto" model id on Kiro — was sent verbatim and 400'd + "claude-fable-5", // Kiro offers no Fable + "claude-opus-4.8", // Kiro offers no Opus + "claude-opus-4.7", + "claude-opus-4.6", + "claude-sonnet-4.6", // Kiro's Sonnet is 4.5, not 4.6 +]; + +// Ids proven to return 200 on the VPS (or a real, plan-gated Kiro model). +const REAL_KIRO_IDS = [ + "claude-sonnet-5", // real model, plan-gated per account (kept) + "claude-sonnet-4.5", // proven 200 (replaces the fabricated 4.6) + "claude-haiku-4.5", // proven 200 + "deepseek-3.2", // proven 200 + "glm-5", // proven 200 + "minimax-m2.5", // proven 200 + "minimax-m2.1", // proven 200 + "qwen3-coder-next", // proven 200 +]; + +test("kiro registry exposes no fabricated model ids", () => { + const ids = new Set((REGISTRY.kiro?.models || []).map((m) => m.id)); + for (const bad of FABRICATED_KIRO_IDS) { + assert.ok(!ids.has(bad), `kiro registry must not expose fabricated id "${bad}"`); + } +}); + +test("kiro registry exposes exactly the real Kiro model ids", () => { + const ids = (REGISTRY.kiro?.models || []).map((m) => m.id).sort(); + assert.deepEqual(ids, [...REAL_KIRO_IDS].sort()); +}); + +test("kiro free-model catalog carries no fabricated ids", () => { + const kiroCatalogIds = new Set( + FREE_MODEL_BUDGETS.filter((e) => e.provider === "kiro").map((e) => e.modelId) + ); + for (const bad of FABRICATED_KIRO_IDS) { + assert.ok(!kiroCatalogIds.has(bad), `free catalog must not list fabricated kiro id "${bad}"`); + } + // Every kiro free-catalog entry must exist in the registry (no orphans). + const registryIds = new Set((REGISTRY.kiro?.models || []).map((m) => m.id)); + for (const id of kiroCatalogIds) { + assert.ok(registryIds.has(id), `free catalog kiro id "${id}" is not in the registry`); + } +}); diff --git a/tests/unit/model-family-fallback-notation.test.ts b/tests/unit/model-family-fallback-notation.test.ts index bda527d98d..b52e58e8d1 100644 --- a/tests/unit/model-family-fallback-notation.test.ts +++ b/tests/unit/model-family-fallback-notation.test.ts @@ -4,8 +4,10 @@ import assert from "node:assert/strict"; // Covers getNextFamilyFallback's dot-vs-hyphen notation resolution (the hunk // added alongside Claude Fable 5 in #3524 that affects ALL families): the // lookup normalizes dots→hyphens, and each candidate is resolved to the -// notation the provider's registry actually exposes (kiro uses dot notation -// `claude-opus-4.8`, cc uses hyphen `claude-opus-4-8`). +// notation the provider's registry actually exposes (anthropic uses dot notation +// `claude-opus-4.8`, cc uses hyphen `claude-opus-4-8`). Kiro is NOT used as the +// dot-notation example any more — its upstream never served Opus/Fable and #6170 +// removed the fabricated ids; `anthropic` genuinely serves them in dot notation. const { getNextFamilyFallback } = await import("../../open-sse/services/modelFamilyFallback.ts"); test("Fable 5 falls back to the next-best Opus tier first (not Sonnet) — cc→claude", () => { @@ -14,16 +16,16 @@ test("Fable 5 falls back to the next-best Opus tier first (not Sonnet) — cc→ assert.equal(next, "claude/claude-opus-4-8"); }); -test("Fable 5 fallback resolves to kiro's dot-notation model id", () => { - // kiro registry exposes `claude-opus-4.8` (dot), not `claude-opus-4-8`. - const next = getNextFamilyFallback("kiro/claude-fable-5", new Set(["kiro/claude-fable-5"])); - assert.equal(next, "kiro/claude-opus-4.8"); +test("Fable 5 fallback resolves to anthropic's dot-notation model id", () => { + // anthropic registry exposes `claude-opus-4.8` (dot), not `claude-opus-4-8`. + const next = getNextFamilyFallback("anthropic/claude-fable-5", new Set(["anthropic/claude-fable-5"])); + assert.equal(next, "anthropic/claude-opus-4.8"); }); test("dot-notation current model is normalized for the family lookup", () => { - // kiro/claude-opus-4.8 must find the claude-opus-4-8 family entry. - const next = getNextFamilyFallback("kiro/claude-opus-4.8", new Set(["kiro/claude-opus-4.8"])); - assert.equal(next, "kiro/claude-opus-4.7"); + // anthropic/claude-opus-4.8 must find the claude-opus-4-8 family entry. + const next = getNextFamilyFallback("anthropic/claude-opus-4.8", new Set(["anthropic/claude-opus-4.8"])); + assert.equal(next, "anthropic/claude-opus-4.7"); }); test("skips already-tried candidates and advances down the Fable chain", () => { diff --git a/tests/unit/provider-models-config.test.ts b/tests/unit/provider-models-config.test.ts index be6acd59ab..747085b828 100644 --- a/tests/unit/provider-models-config.test.ts +++ b/tests/unit/provider-models-config.test.ts @@ -105,11 +105,15 @@ 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])); - assert.ok(byId.has("claude-opus-4.7")); - assert.equal(byId.get("claude-opus-4.7")?.contextLength, 1000000); - assert.ok(byId.has("claude-sonnet-4.6")); + // Kiro's real upstream Claude lineup (#6170): Sonnet 5 / Sonnet 4.5 / Haiku 4.5. + // The Opus 4.x and Sonnet 4.6 ids were fabricated (copied from the Anthropic + // catalog) and returned upstream 400 "Invalid model" — removed. + assert.ok(byId.has("claude-sonnet-5")); + assert.equal(byId.get("claude-sonnet-5")?.contextLength, 1000000); + assert.ok(byId.has("claude-sonnet-4.5")); assert.ok(byId.has("claude-haiku-4.5")); - assert.equal(byId.has("claude-opus-4-7"), false); + assert.equal(byId.has("claude-opus-4.7"), false); + assert.equal(byId.has("claude-sonnet-4.6"), false); assert.equal(byId.has("claude-sonnet-4-6"), false); assert.equal(byId.has("claude-haiku-4-5"), false); }); diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index f83c81e5a3..ed8e5d5e8d 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -734,9 +734,9 @@ test("provider models route returns the expanded local catalog for Kiro", async assert.equal(response.status, 200); assert.equal(body.provider, "kiro"); assert.equal(body.source, "local_catalog"); - assert.ok(body.models.some((model) => model.id === "claude-haiku-4.5")); - assert.ok(body.models.some((model) => model.id === "claude-opus-4.7")); - assert.ok(body.models.some((model) => model.id === "claude-sonnet-4.6")); + const kiroIds = new Set(body.models.map((model) => model.id)); // #6170: real upstream lineup + assert.ok(kiroIds.has("claude-sonnet-5") && kiroIds.has("claude-sonnet-4.5") && kiroIds.has("claude-haiku-4.5")); + assert.equal(kiroIds.has("claude-opus-4.7") || kiroIds.has("claude-sonnet-4.6"), false); // fabricated ids removed }); test("provider models route returns the local catalog for new built-in chat-openai-compat providers", async () => {