diff --git a/.env.example b/.env.example index a48b9bf3b1c9..c6851242bcc1 100644 --- a/.env.example +++ b/.env.example @@ -1421,7 +1421,7 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500 #OMNIROUTE_LOG_EXPORT_CHILD_MAX_OLD_SPACE_MB=1024 # Emergency budget-exhaustion fallback (set false or 0 to disable the reroute to -# nvidia/openai/gpt-oss-120b when a request fails with a 402 budget error). +# nvidia/nvidia/nemotron-3.5-super-vl-120b-a12b when a request fails with a 402 budget error). # Used by: open-sse/services/emergencyFallback.ts. Default: enabled. #OMNIROUTE_EMERGENCY_FALLBACK=true diff --git a/README.md b/README.md index 473bae65933c..1a958567d1f1 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ > **Bring your own provider accounts.** This is an estimated aggregate across separately eligible third-party free tiers, not a token grant from OmniRoute. Sign up, obtain credentials where required, and connect the providers you can use; each provider controls its limits, availability, and terms. > -> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **485 free-tier entries across 34 recurring pool keys** and computes the token headline from the **17 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`). +> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **484 free-tier entries across 34 recurring pool keys** and computes the token headline from the **17 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`). -OmniRoute free-tier budget card: ~1.62B free tokens per month steady, up to ~2.22B in the first month with signup credits, from 34 documented recurring pool keys covering 485 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 17 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 12 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, xKiro 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers. +OmniRoute free-tier budget card: ~1.62B free tokens per month steady, up to ~2.22B in the first month with signup credits, from 34 documented recurring pool keys covering 484 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 17 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 12 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, xKiro 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers. > Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**. > @@ -1376,7 +1376,7 @@ Métricas canônicas em 2026-10-18: **1.976 vídeos únicos** · **20.220.289 vi Resilience GuideCircuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing Auto-Combo Engine16-factor scoring, mode packs, self-healing Proxy Guide3-level proxy system, 1proxy marketplace, registry CRUD - Free TiersConsolidated directory: 34 documented recurring pools / 485 cataloged free-tier entries + Free TiersConsolidated directory: 34 documented recurring pools / 484 cataloged free-tier entries Features GalleryVisual dashboard tour with screenshots Codebase DocumentationBeginner-friendly codebase walkthrough diff --git a/changelog.d/fixes/18418-nvidia-retired-free-endpoints.md b/changelog.d/fixes/18418-nvidia-retired-free-endpoints.md new file mode 100644 index 000000000000..bff202877e26 --- /dev/null +++ b/changelog.d/fixes/18418-nvidia-retired-free-endpoints.md @@ -0,0 +1 @@ +- **fix(nvidia):** drop `openai/gpt-oss-120b`, `moonshotai/kimi-k3`, `deepseek-ai/deepseek-v4-pro-0813` and `deepseek-ai/deepseek-v4-flash-0731` from the NVIDIA catalog, because NVIDIA retired their free endpoints and every request now gets a 410 end of life answer. The budget exhaustion emergency fallback, which defaulted to `nvidia` + `openai/gpt-oss-120b` and so always failed, now defaults to `nvidia` + `nvidia/nemotron-3.5-super-vl-120b-a12b`, a model on NVIDIA's free endpoint that OmniRoute already lists as free, with a zero-price row so USD-limited API keys are not failed closed after a fallback ([#18418](https://github.com/diegosouzapw/OmniRoute/issues/18418)) diff --git a/docs/diagrams/free-tier-budget.svg b/docs/diagrams/free-tier-budget.svg index ff29bdfa1b37..b946798c2eb9 100644 --- a/docs/diagrams/free-tier-budget.svg +++ b/docs/diagrams/free-tier-budget.svg @@ -1,5 +1,5 @@ - OmniRoute free-tier budget: about 1.62 billion free tokens per month steady, up to about 2.22 billion in the first month with signup credits. The catalog contains 485 rows, 478 active and 7 discontinued, grouped into 34 recurring pool keys; 17 pools have a published positive monthly token budget and 17 have a zero, uncapped, or keyless budget. Honest pool-deduped math counts each shared free pool once; 12 providers carry a terms-of-service avoid flag. The 17 quantified pools plus five per-model Groq caps are Mistral 1 billion, NaraRouter 210 million, LLM7.io 150 million, xKiro 150 million, Groq 30 million across five per-model caps, Cloudflare Workers AI 30 million, Api.airforce 24 million, BluesMinds 7.2 million, SambaNova 6 million, Arcee AI 4.8 million, NavyAI 4.5 million, BazaarLink 3.6 million, OpenRouter 1.2 million, Cohere 800 thousand, HuggingChat 500 thousand, Morph 400 thousand, HuggingFace 200 thousand, Kiro AI 25 thousand. One-time signup credits add about 601 million. Uncapped providers and the OpenRouter top-up boost are shown separately so they do not inflate the headline. A further 6 million behind regional identity verification (ModelScope) is also shown apart. Live usage remains available at /dashboard/free-tiers. + OmniRoute free-tier budget: about 1.62 billion free tokens per month steady, up to about 2.22 billion in the first month with signup credits. The catalog contains 484 rows, 477 active and 7 discontinued, grouped into 34 recurring pool keys; 17 pools have a published positive monthly token budget and 17 have a zero, uncapped, or keyless budget. Honest pool-deduped math counts each shared free pool once; 12 providers carry a terms-of-service avoid flag. The 17 quantified pools plus five per-model Groq caps are Mistral 1 billion, NaraRouter 210 million, LLM7.io 150 million, xKiro 150 million, Groq 30 million across five per-model caps, Cloudflare Workers AI 30 million, Api.airforce 24 million, BluesMinds 7.2 million, SambaNova 6 million, Arcee AI 4.8 million, NavyAI 4.5 million, BazaarLink 3.6 million, OpenRouter 1.2 million, Cohere 800 thousand, HuggingChat 500 thousand, Morph 400 thousand, HuggingFace 200 thousand, Kiro AI 25 thousand. One-time signup credits add about 601 million. Uncapped providers and the OpenRouter top-up boost are shown separately so they do not inflate the headline. A further 6 million behind regional identity verification (ModelScope) is also shown apart. Live usage remains available at /dashboard/free-tiers. Pool-deduplicated chart of the 17 recurring free-token pools with positive published budgets (plus Groq's five per-model caps as one segment), plus signup credits and uncapped providers shown separately. diff --git a/docs/guides/USER_GUIDE.md b/docs/guides/USER_GUIDE.md index 579cf2963d87..d34a389fa00d 100644 --- a/docs/guides/USER_GUIDE.md +++ b/docs/guides/USER_GUIDE.md @@ -654,7 +654,7 @@ For the full environment variable reference, see the [README](../README.md). **Cohere (`cohere/`)** — RAG-focused: `cohere/command-a-reasoning-08-2025`, `cohere/command-a-vision-07-2025`, `cohere/command-a-03-2025`, `cohere/command-r-08-2024` -**NVIDIA NIM (`nvidia/`)** — Enterprise: `nvidia/z-ai/glm-5.1`, `nvidia/minimaxai/minimax-m2.7`, `nvidia/google/gemma-4-31b-it`, `nvidia/mistralai/mistral-small-4-119b-2603`, `nvidia/mistralai/mistral-large-3-675b-instruct-2512`, `nvidia/qwen/qwen3.5-397b-a17b`, `nvidia/deepseek-ai/deepseek-v4-pro`, `nvidia/openai/gpt-oss-120b`, `nvidia/nvidia/nemotron-3.5-super-vl-120b-a12b` +**NVIDIA NIM (`nvidia/`)** — Enterprise: `nvidia/google/gemma-4-31b-it`, `nvidia/nvidia/nemotron-3.5-super-vl-120b-a12b`, `nvidia/nvidia/nemotron-3-ultra-550b-a55b`, `nvidia/nvidia/nemotron-3.5-lightning-30b-a3b` **Baidu Qianfan (`qianfan/`)** — ERNIE: `qianfan/ernie-5.1`, `qianfan/ernie-5.0-thinking-latest`, `qianfan/ernie-x1.1` diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 4c202b582683..26c95fb591c5 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -1577,7 +1577,7 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy), | `CONTEXT_RESERVE_TOKENS` | `1024` | `open-sse/services/contextManager.ts` | Tokens reserved for completion output when computing prompt budgets. | | `CONTEXT_KEEP_LATEST_IMAGES` | `2` | `open-sse/services/contextManager.ts` | How many of the newest inline images to keep when pruning older ones to fit the context window (#8560). | | `MODEL_ALIAS_COMPAT_ENABLED` | enabled | `open-sse/services/model.ts` | Toggle the legacy model-alias compatibility layer used by older clients. | -| `OMNIROUTE_EMERGENCY_FALLBACK` | enabled | `open-sse/services/emergencyFallback.ts` | Set `false` (or `0`) to disable the emergency budget-exhaustion fallback that reroutes failed requests to the free `nvidia`/`openai/gpt-oss-120b` model. Effective precedence is Feature Flags DB override > env var > default; if unavailable, the service falls back to the raw env value. | +| `OMNIROUTE_EMERGENCY_FALLBACK` | enabled | `open-sse/services/emergencyFallback.ts` | Set `false` (or `0`) to disable the emergency budget-exhaustion fallback that reroutes failed requests to the free `nvidia`/`nvidia/nemotron-3.5-super-vl-120b-a12b` model. Effective precedence is Feature Flags DB override > env var > default; if unavailable, the service falls back to the raw env value. | | `COMMAND_CODE_CALLBACK_PORT` | _(unset)_ | `src/app/api/providers/command-code/auth/shared.ts` | Local port used for OAuth-style callbacks from the Command Code CLI helper. | | `COMMAND_CODE_VERSION` | `0.33.2` | `open-sse/executors/commandCode.ts` | Value sent as the `x-command-code-version` header to the Command Code upstream. Override to bump the CLI version. | | `COMMANDCODE_API_URL` | `https://api.commandcode.ai` | `open-sse/services/usage/command-code.ts` | Base URL for the Command Code usage/quota upstream used by the smartphone quota-fetcher telemetry. Override for a self-hosted/alternative Command Code API. | diff --git a/docs/screenshots/free-tier-budget-card.svg b/docs/screenshots/free-tier-budget-card.svg index 36f8d4e5d6ab..230f879e6455 100644 --- a/docs/screenshots/free-tier-budget-card.svg +++ b/docs/screenshots/free-tier-budget-card.svg @@ -3,7 +3,7 @@ OmniRoute · /dashboard/free-tiers · preview mockup Monthly free-token budget -22 free pools · 485 models · one endpoint +22 free pools · 484 models · one endpoint Steady / month ~1.62B First month (+ signup credits) diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 366404715d3e..791b61cb33ac 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -318,7 +318,6 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "nscale", modelId: "meta-llama/Llama-3.3-70B-Instruct", displayName: "meta-llama/Llama-3.3-70B-Instruct", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "nscale", tos: "caution" }, { provider: "nvidia", modelId: "google/gemma-4-31b-it", displayName: "Gemma 4 31B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" }, { provider: "nvidia", modelId: "nvidia/nemotron-3.5-super-vl-120b-a12b", displayName: "Nemotron 3.5 Super VL 120B A12B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" }, - { provider: "nvidia", modelId: "openai/gpt-oss-120b", displayName: "GPT OSS 120B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" }, // evidence: public-page https://ollama.com/pricing (2026-09-02) — Free plan: "Starter usage credits // included · Includes access to starter models · Add credits to unlock all models"; docs.ollama.com/cloud: // "usage resets monthly". No token figure and no named starter-model list ⇒ recurring-uncapped. diff --git a/open-sse/config/nvidiaHostedModels.snapshot.json b/open-sse/config/nvidiaHostedModels.snapshot.json index eca36002c4a4..12615d202714 100644 --- a/open-sse/config/nvidiaHostedModels.snapshot.json +++ b/open-sse/config/nvidiaHostedModels.snapshot.json @@ -1,7 +1,4 @@ [ - "moonshotai/kimi-k3", - "deepseek-ai/deepseek-v4-pro-0813", - "deepseek-ai/deepseek-v4-flash-0731", "meta/muse-glimmer-30b", "poolside/laguna-xs-2.1", "google/gemma-4-31b-it", @@ -9,6 +6,5 @@ "nvidia/nemotron-3-ultra-550b-a55b", "nvidia/nemotron-3.5-super-vl-120b-a12b", "nvidia/nemotron-3.5-lightning-30b-a3b", - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - "openai/gpt-oss-120b" + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning" ] diff --git a/open-sse/config/providers/registry/nvidia/index.ts b/open-sse/config/providers/registry/nvidia/index.ts index ccbc8885d06a..9419991cbeb7 100644 --- a/open-sse/config/providers/registry/nvidia/index.ts +++ b/open-sse/config/providers/registry/nvidia/index.ts @@ -17,17 +17,10 @@ export const nvidiaProvider: RegistryEntry = { // hasPerModelQuota doc comment; matches modelscope/synthetic/kilo-gateway). passthroughModels: true, models: [ - { id: "moonshotai/kimi-k3", name: "Kimi K3" }, - { - id: "deepseek-ai/deepseek-v4-pro-0813", - name: "DeepSeek V4 Pro 0813", - supportsReasoning: true, - }, - { - id: "deepseek-ai/deepseek-v4-flash-0731", - name: "DeepSeek V4 Flash 0731", - supportsReasoning: true, - }, + // #18418: NVIDIA retired the free endpoints for moonshotai/kimi-k3 (2026-10-15), + // deepseek-ai/deepseek-v4-pro-0813 (2026-09-14), deepseek-ai/deepseek-v4-flash-0731 + // (2026-09-21) and openai/gpt-oss-120b (2026-09-03); each now answers 410 "reached its + // end of life", so they are no longer listed here. { id: "meta/muse-glimmer-30b", name: "Muse Glimmer 30B" }, { id: "poolside/laguna-xs-2.1", name: "Laguna XS 2.1" }, { id: "google/gemma-4-31b-it", name: "Gemma 4 31B" }, @@ -50,6 +43,5 @@ export const nvidiaProvider: RegistryEntry = { supportsReasoning: true, supportsVision: true, }, - { id: "openai/gpt-oss-120b", name: "GPT OSS 120B", toolCalling: false }, ], }; diff --git a/open-sse/services/emergencyFallback.ts b/open-sse/services/emergencyFallback.ts index 60e3914172a5..59191d092521 100644 --- a/open-sse/services/emergencyFallback.ts +++ b/open-sse/services/emergencyFallback.ts @@ -3,10 +3,14 @@ * * When a request fails due to budget exhaustion (HTTP 402 or budget keywords * in the error body), optionally redirect to a free-tier model - * (default provider/model: nvidia + openai/gpt-oss-120b at $0.00/M tokens). + * (default provider/model: nvidia + nvidia/nemotron-3.5-super-vl-120b-a12b on + * NVIDIA's free API trial endpoint). * - * Inspired by ClawRouter: "gpt-oss-120b costs nothing and serves as - * automatic fallback when wallet is empty." + * Inspired by ClawRouter's free fallback model for an empty wallet. The original + * default, nvidia + openai/gpt-oss-120b, reached end of life on NVIDIA's free + * endpoint on 2026-09-03 and now answers 410 (#18418). The default must always + * be a model listed as free for nvidia in freeModelCatalog.data.ts; the NVIDIA + * EOL catalog test enforces that. * * Operators can disable the redirect entirely with * `OMNIROUTE_EMERGENCY_FALLBACK=false` (or `0`). Default remains enabled. @@ -29,7 +33,7 @@ export interface EmergencyFallbackConfig { triggerOn402: boolean; triggerOnBudgetKeywords: boolean; budgetKeywords: string[]; - /** Skip fallback for tool requests (gpt-oss-120b may not support structured tool calling) */ + /** Skip fallback for tool requests (the free fallback model is not vetted for tool calling) */ skipForToolRequests: boolean; maxOutputTokens: number; } @@ -37,7 +41,7 @@ export interface EmergencyFallbackConfig { export const EMERGENCY_FALLBACK_CONFIG: EmergencyFallbackConfig = { enabled: true, provider: "nvidia", - model: "openai/gpt-oss-120b", + model: "nvidia/nemotron-3.5-super-vl-120b-a12b", triggerOn402: true, triggerOnBudgetKeywords: true, budgetKeywords: [ diff --git a/src/shared/constants/pricing/inference-hosts.ts b/src/shared/constants/pricing/inference-hosts.ts index 4815a98b2c84..a2164013ccf1 100644 --- a/src/shared/constants/pricing/inference-hosts.ts +++ b/src/shared/constants/pricing/inference-hosts.ts @@ -414,6 +414,15 @@ export const DEFAULT_PRICING_INFERENCE = { reasoning: 0, cache_creation: 0, }, + // #18418: emergency fallback default (free endpoint). Without a row, usage is + // unpriced and USD-limited API keys fail closed (#12341) after one fallback. + "nvidia/nemotron-3.5-super-vl-120b-a12b": { + input: 0, + output: 0, + cached: 0, + reasoning: 0, + cache_creation: 0, + }, }, nebius: { "openai/gpt-oss-120b": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 }, diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 0ceb431ce9a9..dcdb2d0f8d97 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -2545,7 +2545,7 @@ async function handleSingleModelChat( } // Emergency fallback for budget exhaustion (402 / billing / quota keywords): - // reroute to a free model (default provider/model: nvidia + openai/gpt-oss-120b) exactly once. + // reroute to a free model (default provider/model: nvidia + nvidia/nemotron-3.5-super-vl-120b-a12b) exactly once. // Combo targets never emergency-hop: the combo is the operator's fallback policy // (target-level orchestration plus the global fallback #689 after it), and a // per-target hop burns extra upstream calls against exhausted providers (#1731). diff --git a/tests/integration/combo-provider-exhaustion.test.ts b/tests/integration/combo-provider-exhaustion.test.ts index 92f22a229243..ce5abe7efbff 100644 --- a/tests/integration/combo-provider-exhaustion.test.ts +++ b/tests/integration/combo-provider-exhaustion.test.ts @@ -594,7 +594,7 @@ test("allow rate-limited connections after transient 429 on subsequent targets i test("emergency fallback never sends the failing provider's credentials to another provider's endpoint", async () => { // Single-model request (no combo). On a quota/billing 429 the emergency - // fallback redirects to nvidia/openai/gpt-oss-120b. The hop must go through + // fallback redirects to the NVIDIA free default (EMERGENCY_FALLBACK_CONFIG). The hop must go through // credential selection for the EMERGENCY provider — it must never re-send // the exhausted provider's API key to a different provider's URL (the old // executor-level hop shipped the OpenAI key to integrate.api.nvidia.com). diff --git a/tests/integration/freeModelBenchmarkShared.ts b/tests/integration/freeModelBenchmarkShared.ts index 00c8c168d299..6e8600e213ca 100644 --- a/tests/integration/freeModelBenchmarkShared.ts +++ b/tests/integration/freeModelBenchmarkShared.ts @@ -56,8 +56,11 @@ export const FREE_MODELS: FreeModelSpec[] = [ displayName: "Gemini 3.1 Flash-Lite", }, { provider: "gemini", model: "gemini/gemma-4-31b-it", displayName: "Gemma 4 31B (Gemini)" }, - { provider: "nvidia", model: "nvidia/openai/gpt-oss-120b", displayName: "GPT OSS 120B (NVIDIA)" }, - { provider: "nvidia", model: "nvidia/moonshotai/kimi-k3", displayName: "Kimi K3 (NVIDIA)" }, + { + provider: "nvidia", + model: "nvidia/nvidia/nemotron-3.5-super-vl-120b-a12b", + displayName: "Nemotron 3.5 Super VL (NVIDIA)", + }, { provider: "nvidia", model: "nvidia/google/gemma-4-31b-it", diff --git a/tests/unit/api-key-budget-alias-auto-12341.test.ts b/tests/unit/api-key-budget-alias-auto-12341.test.ts index 43bdbe07a6d4..ff682e7ec4ea 100644 --- a/tests/unit/api-key-budget-alias-auto-12341.test.ts +++ b/tests/unit/api-key-budget-alias-auto-12341.test.ts @@ -12,6 +12,7 @@ const core = await import("../../src/lib/db/core.ts"); const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); const usageLimits = await import("../../src/lib/usage/apiKeyUsageLimits.ts"); +const { EMERGENCY_FALLBACK_CONFIG } = await import("../../open-sse/services/emergencyFallback.ts"); const NOW = Date.parse("2026-06-19T20:00:00.000Z"); @@ -112,6 +113,30 @@ test("control: a priced model routed at the same tokens does NOT trip fail-close assert.equal(status.dailyExceeded, false); }); +test("#18418: a served emergency fallback (free NVIDIA default) does not fail a USD-limited key closed", async () => { + const { created, metadata } = await makeMeteredKey(); + + await usageHistory.saveRequestUsage({ + provider: EMERGENCY_FALLBACK_CONFIG.provider, + model: EMERGENCY_FALLBACK_CONFIG.model, + apiKeyId: created.id, + apiKeyName: "Budget Alias Key", + tokens: { input: 1_000_000, output: 1_000_000 }, + success: true, + timestamp: "2026-06-19T12:00:00.000Z", + }); + + const status = await usageLimits.getApiKeyUsageLimitStatus( + { ...metadata, allowedConnections: null }, + { now: () => NOW } + ); + + assert.equal(status.dailySpentUsd, 0, "the free fallback model costs $0"); + assert.equal(status.dailyHasUnpricedUsage, false, "the fallback model must have a pricing row"); + assert.equal(status.dailyExceeded, false); + assert.equal(status.weeklyExceeded, false); +}); + test("USAGE_LIMIT_IGNORE_UNPRICED=true: unpriced usage counts as $0 instead of failing the quota closed", async () => { const previous = process.env.USAGE_LIMIT_IGNORE_UNPRICED; process.env.USAGE_LIMIT_IGNORE_UNPRICED = "true"; diff --git a/tests/unit/catalog-updates-v3x.test.ts b/tests/unit/catalog-updates-v3x.test.ts index a2b54066c44f..5988ba134e9c 100644 --- a/tests/unit/catalog-updates-v3x.test.ts +++ b/tests/unit/catalog-updates-v3x.test.ts @@ -26,14 +26,19 @@ test("Pollinations catalog mirrors the current public text model lineup", () => test("NVIDIA catalog includes the current hosted models and GPT OSS 120B alias resolution", () => { const ids = new Set(getModelsByProviderId("nvidia").map((model) => model.id)); - assert.ok(ids.has("moonshotai/kimi-k3")); - assert.ok(ids.has("deepseek-ai/deepseek-v4-pro-0813")); - assert.ok(ids.has("deepseek-ai/deepseek-v4-flash-0731")); assert.ok(ids.has("nvidia/nemotron-3.5-lightning-30b-a3b")); assert.ok(ids.has("meta/muse-glimmer-30b")); assert.ok(ids.has("google/diffusiongemma-26b-a4b-it")); - assert.ok(ids.has("openai/gpt-oss-120b")); assert.ok(ids.has("nvidia/nemotron-3.5-super-vl-120b-a12b")); + // #18418: NVIDIA retired these free endpoints (410 end of life). + for (const retired of [ + "openai/gpt-oss-120b", + "moonshotai/kimi-k3", + "deepseek-ai/deepseek-v4-pro-0813", + "deepseek-ai/deepseek-v4-flash-0731", + ]) { + assert.equal(ids.has(retired), false, `${retired} must not stay in the NVIDIA catalog`); + } assert.deepEqual(resolveCanonicalProviderModel("nvidia", "gpt-oss-120b"), { provider: "nvidia", diff --git a/tests/unit/chat-route-coverage.test.ts b/tests/unit/chat-route-coverage.test.ts index a90ab12d3570..4a111867a4d8 100644 --- a/tests/unit/chat-route-coverage.test.ts +++ b/tests/unit/chat-route-coverage.test.ts @@ -534,7 +534,7 @@ test("handleChat uses the emergency fallback model on budget exhaustion", async }); } - return buildOpenAIResponse("Emergency fallback answered", "gpt-oss-120b"); + return buildOpenAIResponse("Emergency fallback answered", "nemotron-3.5-super-vl-120b-a12b"); }; const response = await handleChat( @@ -551,7 +551,7 @@ test("handleChat uses the emergency fallback model on budget exhaustion", async assert.equal(response.status, 200); assert.equal(seenBodies.length, 2); - assert.equal(seenBodies[1].model, "openai/gpt-oss-120b"); + assert.equal(seenBodies[1].model, "nvidia/nemotron-3.5-super-vl-120b-a12b"); assert.equal(seenBodies[1].max_tokens, 4096); // nvidia supports the legacy `max_tokens` field (#6912 symmetric normalization in // chatCore.ts), so the redundant `max_completion_tokens` is renamed away rather than @@ -600,7 +600,7 @@ test("handleChat returns the primary budget error when emergency fallback also f // tries the free model once. (The second hop used to come from the executor-level // fallback inside chatCore, which re-sent the OpenAI credentials to the nvidia // endpoint — removed as a cross-provider credential leak.) - assert.deepEqual(seenModels, ["gpt-4.1", "openai/gpt-oss-120b"]); + assert.deepEqual(seenModels, ["gpt-4.1", "nvidia/nemotron-3.5-super-vl-120b-a12b"]); assert.match(json.error.message, /quota exceeded/i); }); diff --git a/tests/unit/emergency-fallback-response-header.test.ts b/tests/unit/emergency-fallback-response-header.test.ts index 3ea489c789bb..23414c998e50 100644 --- a/tests/unit/emergency-fallback-response-header.test.ts +++ b/tests/unit/emergency-fallback-response-header.test.ts @@ -75,7 +75,7 @@ test("a response served by the emergency fallback names the swap in a header", a headers: { "Content-Type": "application/json" }, }); } - return buildOpenAIResponse("served by fallback", "gpt-oss-120b"); + return buildOpenAIResponse("served by fallback", "nvidia/nemotron-3.5-super-vl-120b-a12b"); }; const response = await handleChat( @@ -93,7 +93,7 @@ test("a response served by the emergency fallback names the swap in a header", a assert.equal(calls, 2); assert.equal( response.headers.get(EMERGENCY_HEADER), - "from=openai/gpt-4.1; to=nvidia/openai/gpt-oss-120b" + "from=openai/gpt-4.1; to=nvidia/nvidia/nemotron-3.5-super-vl-120b-a12b" ); }); diff --git a/tests/unit/free-models.test.ts b/tests/unit/free-models.test.ts index 0d4ae6a566b6..134a8e89381d 100644 --- a/tests/unit/free-models.test.ts +++ b/tests/unit/free-models.test.ts @@ -57,8 +57,9 @@ test("isFreeModel: a model id listed in the free catalog for that provider is fr assert.equal(isFreeModel(sample.provider, { id: sample.modelId }), true); }); -test("isFreeModel: NVIDIA GPT OSS 120B remains in the reviewed trial catalog", () => { - assert.equal(isFreeModel("nvidia", { id: "openai/gpt-oss-120b" }), true); +test("isFreeModel: NVIDIA's retired GPT OSS 120B left the trial catalog (#18418)", () => { + assert.equal(isFreeModel("nvidia", { id: "openai/gpt-oss-120b" }), false); + assert.equal(isFreeModel("nvidia", { id: "nvidia/nemotron-3.5-super-vl-120b-a12b" }), true); }); test("selectModelsForImport: passthrough when importFreeOnly is false", () => { diff --git a/tests/unit/model-capabilities-registry.test.ts b/tests/unit/model-capabilities-registry.test.ts index 2a4e711c0ac3..37eb22719822 100644 --- a/tests/unit/model-capabilities-registry.test.ts +++ b/tests/unit/model-capabilities-registry.test.ts @@ -203,7 +203,9 @@ test("GPT OSS and DeepSeek Reasoner models support tool calling", () => { // GPT OSS models should not be blocked by the heuristic assert.equal(modelCapabilities.supportsToolCalling("fake-provider/gpt-oss-120b"), true); assert.equal(modelCapabilities.supportsToolCalling("gpt-oss-120b"), true); - assert.equal(modelCapabilities.supportsToolCalling("nvidia/openai/gpt-oss-120b"), false); // in registry + // A registry entry that pins toolCalling: false still wins over the heuristic. The NVIDIA + // gpt-oss-120b entry that used to anchor this was retired (#18418). + assert.equal(modelCapabilities.supportsToolCalling("ddgw/tinfoil/gpt-oss-120b"), false); // in registry // DeepSeek Reasoner supports tool calling assert.equal(modelCapabilities.supportsToolCalling("deepseek-reasoner"), true); diff --git a/tests/unit/nvidia-eol-catalog.test.ts b/tests/unit/nvidia-eol-catalog.test.ts index 089d52221fca..e6aa2ee47585 100644 --- a/tests/unit/nvidia-eol-catalog.test.ts +++ b/tests/unit/nvidia-eol-catalog.test.ts @@ -4,6 +4,9 @@ import assert from "node:assert/strict"; import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.data.ts"; import reviewedLiveIds from "../../open-sse/config/nvidiaHostedModels.snapshot.json" with { type: "json" }; import { nvidiaProvider } from "../../open-sse/config/providers/registry/nvidia/index.ts"; +import { EMERGENCY_FALLBACK_CONFIG } from "../../open-sse/services/emergencyFallback.ts"; +import { calculateCostDetailed } from "../../src/lib/usage/costCalculator.ts"; +import { getDefaultPricing } from "../../src/shared/constants/pricing.ts"; const registryIds = new Set(nvidiaProvider.models.map((model) => model.id)); @@ -13,15 +16,22 @@ const documentedFreeIds = new Set( const reviewedIds = new Set(reviewedLiveIds); +const RETIRED_NVIDIA_MODEL_IDS = [ + "z-ai/glm-5.1", + "z-ai/glm-5.2", + "deepseek-ai/deepseek-v4-pro", + "deepseek-ai/deepseek-v4-flash", + "minimaxai/minimax-m2.7", + "nvidia/nemotron-3-super-120b-a12b", + // #18418: NVIDIA answers 410 "reached its end of life" for these four. + "openai/gpt-oss-120b", + "moonshotai/kimi-k3", + "deepseek-ai/deepseek-v4-pro-0813", + "deepseek-ai/deepseek-v4-flash-0731", +]; + test("NVIDIA static catalog metadata excludes superseded model ids", () => { - for (const modelId of [ - "z-ai/glm-5.1", - "z-ai/glm-5.2", - "deepseek-ai/deepseek-v4-pro", - "deepseek-ai/deepseek-v4-flash", - "minimaxai/minimax-m2.7", - "nvidia/nemotron-3-super-120b-a12b", - ]) { + for (const modelId of RETIRED_NVIDIA_MODEL_IDS) { assert.ok(!registryIds.has(modelId), `${modelId} must not remain in the NVIDIA registry`); assert.ok( !reviewedIds.has(modelId), @@ -41,3 +51,32 @@ test("NVIDIA reviewed snapshot matches the registry and trial entries remain val assert.ok(registryIds.has(modelId), `${modelId} must exist in the NVIDIA hosted catalog`); } }); + +test("emergency budget fallback targets a live NVIDIA free-catalog model (#18418)", () => { + assert.equal(EMERGENCY_FALLBACK_CONFIG.provider, "nvidia"); + const model = EMERGENCY_FALLBACK_CONFIG.model; + assert.ok( + !RETIRED_NVIDIA_MODEL_IDS.includes(model), + `emergency fallback default ${model} is a retired NVIDIA model` + ); + assert.ok(registryIds.has(model), `emergency fallback default ${model} must be in the registry`); + assert.ok( + documentedFreeIds.has(model), + `emergency fallback default ${model} must be listed as free for nvidia` + ); +}); + +test("emergency budget fallback default has a zero-price NVIDIA pricing row (#18418, #12341)", async () => { + const { provider, model } = EMERGENCY_FALLBACK_CONFIG; + const pricing = getDefaultPricing() as Record>>; + assert.deepEqual( + pricing[provider]?.[model], + { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 }, + `${provider}/${model} needs a free (all zero) pricing row` + ); + // Unpriced usage makes USD-limited API keys fail closed (#12341), so one served + // emergency fallback must not leave unpriced usage behind. + const result = await calculateCostDetailed(provider, model, { input: 1000, output: 1000 }); + assert.equal(result.priced, true); + assert.equal(result.costUsd, 0); +}); diff --git a/tests/unit/nvidia-nim-registry-6108.test.ts b/tests/unit/nvidia-nim-registry-6108.test.ts index 3c60b0fc18b3..9d3051df1d54 100644 --- a/tests/unit/nvidia-nim-registry-6108.test.ts +++ b/tests/unit/nvidia-nim-registry-6108.test.ts @@ -4,9 +4,6 @@ import assert from "node:assert/strict"; import { nvidiaProvider } from "../../open-sse/config/providers/registry/nvidia/index.ts"; const EXPECTED_MODEL_IDS = [ - "moonshotai/kimi-k3", - "deepseek-ai/deepseek-v4-pro-0813", - "deepseek-ai/deepseek-v4-flash-0731", "meta/muse-glimmer-30b", "poolside/laguna-xs-2.1", "google/gemma-4-31b-it", @@ -15,7 +12,6 @@ const EXPECTED_MODEL_IDS = [ "nvidia/nemotron-3.5-super-vl-120b-a12b", "nvidia/nemotron-3.5-lightning-30b-a3b", "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - "openai/gpt-oss-120b", ] as const; test("NVIDIA NIM registry exactly matches the current hosted-model catalog", () => { @@ -28,20 +24,17 @@ test("NVIDIA NIM registry exactly matches the current hosted-model catalog", () test("NVIDIA NIM registry preserves known model capabilities", () => { const byId = new Map(nvidiaProvider.models.map((model) => [model.id, model])); - for (const id of [ - "deepseek-ai/deepseek-v4-pro-0813", - "deepseek-ai/deepseek-v4-flash-0731", - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - ]) { + for (const id of ["nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"]) { assert.equal(byId.get(id)?.supportsReasoning, true, `${id} must support reasoning`); } const omni = byId.get("nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"); assert.equal(omni?.supportsVision, true, "Nemotron 3 Nano Omni must support vision"); + // The emergency fallback default since #18418 (it replaced openai/gpt-oss-120b). assert.equal( - byId.get("openai/gpt-oss-120b")?.toolCalling, - false, - "openai/gpt-oss-120b must keep tool calling disabled" + byId.get("nvidia/nemotron-3.5-super-vl-120b-a12b")?.supportsVision, + true, + "Nemotron 3.5 Super VL must support vision" ); }); diff --git a/tests/unit/nvidia-stale-synced-catalog-12849.test.ts b/tests/unit/nvidia-stale-synced-catalog-12849.test.ts index 008d2c3efc3d..49a59bfbb910 100644 --- a/tests/unit/nvidia-stale-synced-catalog-12849.test.ts +++ b/tests/unit/nvidia-stale-synced-catalog-12849.test.ts @@ -24,15 +24,14 @@ process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "nvidia-stale-12849-t const core = await import("../../src/lib/db/core.ts"); const { replaceSyncedAvailableModelsForConnection } = await import("../../src/lib/db/models.ts"); const { getModelInfo } = await import("../../src/sse/services/model.ts"); -const { nvidiaProvider } = await import( - "../../open-sse/config/providers/registry/nvidia/index.ts" -); +const { nvidiaProvider } = await import("../../open-sse/config/providers/registry/nvidia/index.ts"); const PROVIDER = "nvidia"; const CONNECTION_ID = "nvidia-stale-catalog-12849"; // Live upstream + present in the current static registry (asserted below), but // deliberately absent from the small "historical sync" catalog seeded here. -const LIVE_MODEL = "moonshotai/kimi-k3"; +// (#18418 retired moonshotai/kimi-k3 on NVIDIA, so this now uses Gemma 4 31B.) +const LIVE_MODEL = "google/gemma-4-31b-it"; const STALE_SYNC_ONLY_MODEL = "some-retired-model-that-no-longer-exists"; function connectionRow(): { syncedModelsAt: string | null } { @@ -87,7 +86,10 @@ test.after(() => { test("#12849: a fresh synced catalog still gates — a model missing from it is rejected", async () => { // Sanity: touchConnectionSyncedModelsAt stamped this sync as fresh already. const { syncedModelsAt } = connectionRow(); - assert.ok(syncedModelsAt, "replaceSyncedAvailableModelsForConnection must stamp synced_models_at"); + assert.ok( + syncedModelsAt, + "replaceSyncedAvailableModelsForConnection must stamp synced_models_at" + ); const resolved = await getModelInfo(`${PROVIDER}/${LIVE_MODEL}`); diff --git a/tests/unit/siliconflow-paid-free-17028.test.ts b/tests/unit/siliconflow-paid-free-17028.test.ts index 952cf6ac8a03..839efb71516f 100644 --- a/tests/unit/siliconflow-paid-free-17028.test.ts +++ b/tests/unit/siliconflow-paid-free-17028.test.ts @@ -33,7 +33,8 @@ describe("#17028 SiliconFlow pay-as-you-go model protection", () => { const totals = computeFreeModelTotals(); // 471 → 484: #16325 added 13 onomeo free-catalog rows (one uncapped pool, not steady-monthly). // 484 → 485: #16315 added the Token Harbor free route to its own uncapped pool. - assert.equal(totals.modelCount, 485); + // 485 → 484: #18418 drops NVIDIA's retired openai/gpt-oss-120b endpoint. + assert.equal(totals.modelCount, 484); assert.equal(totals.poolCount, 34); assert.equal(totals.uncappedProviders.includes("siliconflow"), false); });