diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b1fb80e2f7..d49a710b9f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ ### 🔧 Bug Fixes +- **providers (AI/ML API):** the model-import step now loads the **live** AI/ML API catalog (400+ models) instead of falling back to a stale 6-model seed. The registry had no `modelsUrl`, so the route silently used the bundled catalog with an "API unavailable — using local catalog" warning even when the key was valid. AI/ML API exposes its full catalog at the public, **auth-free** `https://api.aimlapi.com/models` endpoint (a bare array of `{ id, type, info }`, distinct from the OpenAI-compat `/v1/models`); it's now wired into the models route's discovery config, with the bundled catalog kept as the offline fallback. Regression guard: `tests/unit/provider-models-route.test.ts`. ([#5570](https://github.com/diegosouzapw/OmniRoute/issues/5570)) + +- **providers (CablyAI):** mark **CablyAI** deprecated — `cablyai.com` no longer resolves (DNS `NXDOMAIN`, verified 2026-06-30); the domain is gone. The provider is removed from the models-route discovery config so the import step returns a clean error instead of an **unhandled 500 crash** (the dead-domain fetch threw with no local-catalog fallback), and the registry entry now carries `deprecated: true` / `riskNoticeVariant: "deprecated"` so the dashboard flags existing connections (same treatment as the shut-down `glhf`/`kluster.ai` gateways). Regression guard: `tests/unit/provider-models-route.test.ts`. ([#5568](https://github.com/diegosouzapw/OmniRoute/issues/5568)) + - **dashboard (provider add):** non-LLM search/agent providers no longer fail the model-import step with a red `Provider does not support models listing`. **Jules** (Google Labs coding agent), **linkup-search** (Linkup web search), **ollama-search** (Ollama Cloud web search — distinct from the local Ollama LLM), and **searchapi-search** (SearchAPI SERP) have no `/v1/models` endpoint, so the import surfaced a failure for expected behavior. Each now ships a small static catalog of its selectable capability ids — Linkup's `fast`/`standard`/`deep` search depths, SearchAPI's `google`/`bing`/`youtube`/… engines, a single Jules/Ollama-web-search entry — so the import step returns a usable list (`source: local_catalog`) instead of an error. Regression guard: `tests/unit/provider-models-route.test.ts`. ([#5569](https://github.com/diegosouzapw/OmniRoute/issues/5569), [#5571](https://github.com/diegosouzapw/OmniRoute/issues/5571), [#5573](https://github.com/diegosouzapw/OmniRoute/issues/5573), [#5575](https://github.com/diegosouzapw/OmniRoute/issues/5575)) - **dashboard (provider add):** providers without a live key/cookie validator (e.g. **LMArena (Free)**, **PiAPI**) can now be saved. The Add-connection modal treated the backend's `"Provider validation not supported"` response as a hard **Invalid** state and blocked Save entirely, leaving those providers impossible to add. The validate route now returns `unsupported: true` alongside the message, and the modal treats that as a non-blocking warning — the "Check" badge still shows "validation not supported" (informational), but Save persists the credential as-is. Regression guards: `tests/unit/ui/add-api-key-modal-unsupported-save-5565.test.tsx` (Save proceeds) and `tests/unit/providers-validate-route.test.ts` (wire-format). ([#5565](https://github.com/diegosouzapw/OmniRoute/issues/5565), [#5567](https://github.com/diegosouzapw/OmniRoute/issues/5567)) diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 438f5463e0dc..a44aa399356a 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -520,13 +520,21 @@ const PROVIDER_MODELS_CONFIG: Record = { authPrefix: "Bearer ", parseResponse: (data) => data.data || data.models || [], }, - cablyai: { - url: "https://cablyai.com/v1/models", + aimlapi: { + // #5570: AI/ML API's live catalog (400+ models) lives at the public, + // auth-free /models database endpoint (NOT /v1/models). The registry has no + // modelsUrl, so without this entry the route fell back to a stale 6-model + // seed. Response is a bare array of { id, type, info: { name } }. + url: "https://api.aimlapi.com/models", method: "GET", headers: { "Content-Type": "application/json" }, - authHeader: "Authorization", - authPrefix: "Bearer ", - parseResponse: (data) => data.data || data.models || [], + parseResponse: (data) => { + const all = Array.isArray(data) ? data : []; + const chat = all.filter((m) => m?.type === "chat-completion"); + return (chat.length > 0 ? chat : all) + .map((m) => ({ id: m?.id, name: m?.info?.name || m?.id })) + .filter((m) => typeof m.id === "string" && m.id); + }, }, thebai: { url: "https://api.theb.ai/v1/models", diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 38c0151f9723..0fca742cb5ec 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -362,6 +362,11 @@ export const APIKEY_PROVIDERS_GATEWAYS = { website: "https://cablyai.com", authHint: "Bearer API key for the CablyAI OpenAI-compatible gateway.", passthroughModels: true, + subscriptionRisk: true, + riskNoticeVariant: "deprecated", + deprecated: true, + deprecationReason: + "cablyai.com no longer resolves (DNS NXDOMAIN, verified 2026-06-30) — the domain is gone and every request fails with a DNS error (#5568).", }, thebai: { id: "thebai", diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index cfe8f3ff9fe0..34e5c2b50e9e 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -80,6 +80,61 @@ test("provider models route returns a static local catalog for non-LLM search/ag } }); +test("provider models route fetches the live AI/ML API catalog from the auth-free /models endpoint (#5570)", async () => { + const connection = await seedConnection("aimlapi", { apiKey: "aiml-key" }); + let calledUrl = ""; + globalThis.fetch = async (url) => { + calledUrl = String(url); + return Response.json([ + { id: "openai/gpt-5.5", type: "chat-completion", info: { name: "GPT-5.5" } }, + { id: "zhipu/glm-5.2", type: "chat-completion", info: { name: "GLM 5.2" } }, + { id: "flux/flux-pro", type: "image", info: { name: "FLUX Pro" } }, + ]); + }; + + const response = await callRoute(connection.id); + const body = (await response.json()) as any; + + // RED before the fix: aimlapi had no PROVIDER_MODELS_CONFIG entry → stale + // 6-model local seed (source "local_catalog"), live endpoint never called. + assert.equal(response.status, 200); + assert.equal(body.source, "api"); + assert.equal(calledUrl, "https://api.aimlapi.com/models"); + const ids = body.models.map((m: any) => m.id); + assert.ok(ids.includes("openai/gpt-5.5") && ids.includes("zhipu/glm-5.2")); + assert.ok(!ids.includes("flux/flux-pro"), "non-chat model types are filtered out"); +}); + +test("provider models route falls back to the local AI/ML API catalog when the live fetch fails (#5570)", async () => { + const connection = await seedConnection("aimlapi", { apiKey: "aiml-key" }); + globalThis.fetch = async () => new Response("upstream down", { status: 500 }); + + const response = await callRoute(connection.id); + const body = (await response.json()) as any; + + assert.equal(response.status, 200); + assert.equal(body.source, "local_catalog"); + assert.ok(body.models.length > 0); +}); + +test("cablyai is flagged deprecated (domain NXDOMAIN) and no longer 500s on model import (#5568)", async () => { + const { APIKEY_PROVIDERS_GATEWAYS } = await import( + "../../src/shared/constants/providers/apikey/gateways.ts" + ); + const cablyai = (APIKEY_PROVIDERS_GATEWAYS as Record).cablyai; + assert.equal(cablyai?.deprecated, true, "cablyai must be marked deprecated (domain is NXDOMAIN)"); + assert.ok( + typeof cablyai?.deprecationReason === "string" && cablyai.deprecationReason.length > 0, + "cablyai must carry a deprecationReason" + ); + + // Removed from PROVIDER_MODELS_CONFIG → no live fetch to the dead domain → a + // controlled 400 instead of an unhandled 500 crash. + const connection = await seedConnection("cablyai", { apiKey: "dead-key" }); + const response = await callRoute(connection.id); + assert.notEqual(response.status, 500, "cablyai must not 500-crash on a dead domain"); +}); + test("provider models route returns 404 for unknown connections", async () => { const response = await callRoute("missing-connection");