mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
Cloudflare's /ai/models/search returns { id: "<uuid>", 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
This commit is contained in:
committed by
GitHub
parent
98b0d5e51e
commit
550440f65f
@@ -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: "<uuid>", 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
|
||||
|
||||
@@ -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: \"<uuid>\", 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 `<baseUrl>/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,
|
||||
|
||||
@@ -664,7 +664,23 @@ const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.result || [],
|
||||
// #4259: Cloudflare's `/ai/models/search` returns `{ id: "<uuid>", 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",
|
||||
|
||||
112
tests/unit/cloudflare-models-uuid-4259.test.ts
Normal file
112
tests/unit/cloudflare-models-uuid-4259.test.ts
Normal file
@@ -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: "<uuid>", 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<string, any> = {}) {
|
||||
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");
|
||||
});
|
||||
Reference in New Issue
Block a user