Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> 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))
Expand Down
18 changes: 13 additions & 5 deletions src/app/api/providers/[id]/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,13 +520,21 @@ const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {
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",
Expand Down
5 changes: 5 additions & 0 deletions src/shared/constants/providers/apikey/gateways.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/provider-models-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>).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");

Expand Down
Loading