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