From 550440f65f55a2b2aafcc7f1651a2fc885db5f39 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:25:35 -0300 Subject: [PATCH] fix(providers): Cloudflare Workers AI discovery uses model names, not UUIDs (#4259) (#4282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cloudflare's /ai/models/search returns { id: "", name: "@cf/..." } where name is the callable slug and id is an internal UUID. The cloudflare-ai discovery config passed the raw objects through (parseResponse: data.result), so buildResponse used id (the UUID) as the model id — the dashboard/import listed UUIDs instead of @cf/... model names. Map each result's name -> id (mirrors the gemini/huggingface/ clarifai parseResponse normalizers in the same map); falls through to the local catalog on error so import never breaks. TDD: tests/unit/cloudflare-models-uuid-4259.test.ts (RED on UUID ids -> GREEN on slugs). Closes #4259 --- CHANGELOG.md | 4 + config/quality/file-size-baseline.json | 3 +- src/app/api/providers/[id]/models/route.ts | 18 ++- .../unit/cloudflare-models-uuid-4259.test.ts | 112 ++++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 tests/unit/cloudflare-models-uuid-4259.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a098f75312..b253218492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ _In development — bullets added per PR; finalized at release._ +### 🐛 Fixed + +- **fix(providers): Cloudflare Workers AI model discovery shows model names, not UUIDs** — importing a Cloudflare Workers AI key listed models with internal UUID identifiers (e.g. `429b9e8b-d99e-…`) instead of their usable slugs (`@cf/meta/llama-3.1-8b-instruct`). Cloudflare's `/ai/models/search` returns `{ id: "", name: "@cf/…" }`, and discovery was passing the raw objects through — so the UUID `id` became the callable model id. The `cloudflare-ai` discovery now maps each result's `name` → id, surfacing the real `@cf/…` model ids. ([#4259](https://github.com/diegosouzapw/OmniRoute/issues/4259) — thanks @FerLuisxd) + --- ## [3.8.29] — 2026-06-19 diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 50817b1cd0..b3ed4c43b0 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,5 +1,6 @@ { "_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", + "_rebaseline_2026_06_19_4259_cloudflare_uuid_models": "Issue #4259 own growth: providers/[id]/models/route.ts 2538->2554 (+16 = the cloudflare-ai PROVIDER_MODELS_CONFIG `parseResponse` now maps each Cloudflare `/ai/models/search` result `name`->id + a 3-line comment, replacing the one-line `data.result || []` passthrough). Cloudflare Workers AI returns `{ id: \"\", name: \"@cf/...\" }` — `name` is the callable slug, `id` is an internal UUID. The old passthrough fed the raw objects to buildResponse (id: m.id), so the dashboard/import surfaced UUIDs instead of `@cf/...` model ids. The map+filter is cohesive normalization at the config entry (mirrors the gemini/huggingface/clarifai parseResponse normalizers in the same map); not extractable. Structural shrink of this route tracked in #3789.", "_rebaseline_2026_06_19_4249_vercel_gateway_live_models": "Issue #4249 own growth: providers/[id]/models/route.ts 2534->2538 (+4 = one NAMED_OPENAI_STYLE_PROVIDERS Set entry `vercel-ai-gateway` + a 3-line comment). Same fix shape as #4202 (zenmux) / #3976 (llm7/byteplus): vercel-ai-gateway carries a real baseUrl (https://ai-gateway.vercel.sh/v1/chat/completions, format openai) but was unclassified by every live-fetch branch, so import served the 5-entry hardcoded registry catalog instead of the upstream list — the import button looked broken (loaded nothing usable) while manual add worked. The `/models` probe (after stripping /chat/completions) resolves to https://ai-gateway.vercel.sh/v1/models; falls back to the local catalog on upstream error so import never breaks. Pure additive Set membership; not extractable.", "_rebaseline_2026_06_19_4227_cursor_cloud_ui": "PR #4250 (#4227 Cursor Cloud Agent) cross-layer UI wiring own growth: cloud-agents/page.tsx 913->922 (+9 = one CLOUD_AGENTS dropdown entry for the new `cursor-cloud` provider, mirroring the existing jules/devin/codex-cloud entries). The backend agent (cursor.ts) + registry/types/credentials-route landed in the PR, but the dashboard CLOUD_AGENTS list, health PROVIDER_NAMES, and lobeProviderIcons maps are hardcoded (client/server boundary — the registry can't be imported into the client page), so the agent was API-usable but not selectable in the UI and rendered with the Jules fallback name/icon. The +9 is the single presentational data entry at the existing hardcoded list; not extractable (it IS the list). The health-name + icon adds land in non-frozen files.", "_rebaseline_2026_06_19_cost_telemetry_combo_prettier": "NOT this PR's logic change: combo.ts 2601->2605 (+4) is a pure Prettier reflow applied by the cost-telemetry-parity merge commit's lint-staged hook. The DEFAULT_WEIGHTS/ProviderCandidate/ScoringWeights import from ./autoCombo/scoring.ts is 102 chars (> printWidth 100) on release (it landed via a GitHub merge that never ran the local Prettier hook), so the hook wrapped it to the canonical 4-line form. Zero logic change (git diff -w empty); cost-telemetry-parity never edits combo.ts. Reverting is futile — Prettier re-wraps the 102-char import on any later commit. Bumped here so the merge surfacing the reflow carries its own baseline.", @@ -135,7 +136,7 @@ "src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148, "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1069, "src/app/api/oauth/[provider]/[action]/route.ts": 918, - "src/app/api/providers/[id]/models/route.ts": 2538, + "src/app/api/providers/[id]/models/route.ts": 2554, "src/app/api/providers/[id]/test/route.ts": 842, "src/app/api/usage/analytics/route.ts": 941, "src/app/api/v1/models/catalog.ts": 1465, diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 7639e9d50b..02787d4d98 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -664,7 +664,23 @@ const PROVIDER_MODELS_CONFIG: Record = { headers: { "Content-Type": "application/json" }, authHeader: "Authorization", authPrefix: "Bearer ", - parseResponse: (data) => data.result || [], + // #4259: Cloudflare's `/ai/models/search` returns `{ id: "", name: "@cf/..." }`. + // `name` is the usable model slug; `id` is an internal UUID. Map `name`→id so the + // dashboard/import surfaces callable model ids (`@cf/...`) instead of UUIDs. + parseResponse: (data) => + (data.result || []) + .map((model: any) => { + const slug = typeof model?.name === "string" ? model.name : ""; + if (!slug) return null; + return { + id: slug, + name: slug, + ...(typeof model?.description === "string" && model.description + ? { description: model.description } + : {}), + }; + }) + .filter(Boolean), }, synthetic: { url: "https://api.synthetic.new/openai/v1/models", diff --git a/tests/unit/cloudflare-models-uuid-4259.test.ts b/tests/unit/cloudflare-models-uuid-4259.test.ts new file mode 100644 index 0000000000..e14f4bc57b --- /dev/null +++ b/tests/unit/cloudflare-models-uuid-4259.test.ts @@ -0,0 +1,112 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// #4259: Cloudflare Workers AI `/ai/models/search` returns objects shaped like +// `{ id: "", name: "@cf/meta/llama-3.1-8b-instruct" }` — the human-usable +// model identifier is `name`, while `id` is an internal UUID. Discovery must use +// `name` as the callable model id; otherwise the dashboard/import shows UUIDs. + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cf-models-4259-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const providerModelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts"); + +const originalFetch = globalThis.fetch; + +async function resetStorage() { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +async function seedConnection(provider: string, overrides: Record = {}) { + return providersDb.createProviderConnection({ + provider, + authType: overrides.authType || "apikey", + name: overrides.name || `${provider}-${Math.random().toString(16).slice(2, 8)}`, + apiKey: overrides.apiKey, + isActive: overrides.isActive ?? true, + testStatus: overrides.testStatus || "active", + providerSpecificData: overrides.providerSpecificData || {}, + }); +} + +async function callRoute(connectionId: string, search = "") { + return providerModelsRoute.GET( + new Request(`http://localhost/api/providers/${connectionId}/models${search}`), + { params: { id: connectionId } } + ); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(async () => { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("#4259 cloudflare-ai discovery uses the model name (slug) as id, not the UUID", async () => { + const connection = await seedConnection("cloudflare-ai", { + apiKey: "cf-token", + providerSpecificData: { accountId: "acc-123" }, + }); + + const LLAMA_UUID = "429b9e8b-d99e-44de-91ad-706cf8183658"; + const QWEN_UUID = "f8703a00-ed54-4c83-bcd9-706cf8183999"; + + let calledUrl = ""; + globalThis.fetch = (async (url: any) => { + calledUrl = String(url); + return Response.json({ + result: [ + { + id: LLAMA_UUID, + name: "@cf/meta/llama-3.1-8b-instruct", + description: "Llama 3.1 8B Instruct", + task: { name: "Text Generation" }, + }, + { + id: QWEN_UUID, + name: "@cf/qwen/qwen1.5-0.5b-chat", + description: "Qwen 1.5 0.5B Chat", + task: { name: "Text Generation" }, + }, + ], + }); + }) as typeof fetch; + + const response = await callRoute(connection.id, "?refresh=true"); + assert.equal(response.status, 200); + + // Sanity: hit the Cloudflare models search endpoint with the configured account. + assert.ok( + calledUrl.includes("/accounts/acc-123/ai/models/search"), + `unexpected discovery URL: ${calledUrl}` + ); + + const body = await response.json(); + assert.equal(body.source, "api"); + const ids: string[] = body.models.map((m: any) => m.id); + + // The human-usable slug must be the id (RED before the fix — id was the UUID). + assert.ok( + ids.includes("@cf/meta/llama-3.1-8b-instruct"), + `expected slug id, got ${JSON.stringify(ids)}` + ); + assert.ok( + ids.includes("@cf/qwen/qwen1.5-0.5b-chat"), + `expected slug id, got ${JSON.stringify(ids)}` + ); + // The internal UUID must never be exposed as a callable model id. + assert.ok(!ids.includes(LLAMA_UUID), "UUID must not be used as a model id"); + assert.ok(!ids.includes(QWEN_UUID), "UUID must not be used as a model id"); +});