mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
This commit is contained in:
committed by
GitHub
parent
1079fc36f0
commit
5cb3cf8843
@@ -19,6 +19,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
|
||||
- **fix(providers): qwen-web model discovery now lists the live catalog instead of nothing** — the `qwen-web` cookie provider had no entry in `PROVIDER_MODELS_CONFIG`, so its model-discovery page returned an empty/stale local catalog (the OAuth fallback at the top of the route only fires for `provider === "qwen"`, leaving `qwen-web` to fall through to the no-config branch). Added a `qwen-web` entry that fetches the **public** `https://chat.qwen.ai/api/v2/models` endpoint (no auth header) and parses the `{ data: { data: [{ id, name, owned_by }] } }` shape (with a flatter `{ data: [] }` fallback). This is Problem #3 of #3931 (diagnosed by @thezukiru); Problem #1 — validator bare-token false-positive — shipped earlier in #3958, and Problem #2 — empty stream from Qwen WAF bot-detection on the streaming endpoint — remains a separate upstream/stealth concern. ([#3931](https://github.com/diegosouzapw/OmniRoute/issues/3931) — thanks @thezukiru)
|
||||
- **fix(providers): ZenMux model discovery now lists the live catalog (incl. the free models) instead of the stale 9-entry hardcoded list** — adding a ZenMux key validated fine, but the connection then showed `API unavailable — using local catalog` and was missing the free models ZenMux advertises (`z-ai/glm-5.2-free`, `moonshotai/kimi-k2.7-code-free`). Root cause: `zenmux` carries a correct `modelsUrl` in the registry, but — like `llm7`/`byteplus` before #3976 — it was not classified by any live-fetch branch of the model-import route (not `openai-compatible-*`, not self-hosted, not in `NAMED_OPENAI_STYLE_PROVIDERS`), so the route never probed the upstream `/models` and fell through to the registry's hardcoded `models[]`. Added `zenmux` to `NAMED_OPENAI_STYLE_PROVIDERS`, so the route probes `https://zenmux.ai/api/v1/models` (the `/chat/completions`-stripped `<baseUrl>/models` candidate) and serves the live list, falling back to the local catalog only when the upstream fetch fails — import never breaks. ([#4202](https://github.com/diegosouzapw/OmniRoute/issues/4202) — thanks @mikmaneggahommie)
|
||||
- **fix(providers): Vercel AI Gateway "import models" now loads the live catalog instead of nothing** — adding a Vercel AI Gateway key worked, but clicking **import** on the models page loaded nothing usable (manually adding the same models worked). Same class as #4202 (zenmux) / #3976 (llm7/byteplus): `vercel-ai-gateway` carries a real `baseUrl` (`https://ai-gateway.vercel.sh/v1/chat/completions`, format `openai`) in the registry, but was not classified by any live-fetch branch of the model-import route (not `openai-compatible-*`, not self-hosted, not in `NAMED_OPENAI_STYLE_PROVIDERS`), so the route never probed the upstream `/models` and fell through to the registry's tiny 5-entry hardcoded `models[]`. Added `vercel-ai-gateway` to `NAMED_OPENAI_STYLE_PROVIDERS`, so the route probes `https://ai-gateway.vercel.sh/v1/models` (the `/chat/completions`-stripped `<baseUrl>/models` candidate) and serves the live list, falling back to the local catalog only when the upstream fetch fails — import never breaks. ([#4249](https://github.com/diegosouzapw/OmniRoute/issues/4249) — thanks @FerLuisxd)
|
||||
- **fix(sse): clear error when the request queue drops a job (no more fake-upstream "This job timed out after Nms")** — under concurrent load, requests that exceed the per-connection rate-limit queue budget (`resilienceSettings.requestQueue.maxWaitMs`) were dropped by Bottleneck with its raw `This job timed out after <maxWaitMs> ms.` message. That string is indistinguishable from an upstream gateway timeout, so the 502 body and call-log `last_error` looked like a provider outage across unrelated providers (TI:0\|TO:0) — an operator spent ~3h misdiagnosing local queue saturation as upstream failures. `withRateLimit` now rewrites that specific Bottleneck error into a clear, OmniRoute-owned message that names the knob (`requestQueue.maxWaitMs`, tunable in Settings → Resilience), explicitly disclaims an upstream timeout, preserves the original as `cause`, and tags `code: "RATE_LIMIT_QUEUE_TIMEOUT"`. Behavior is unchanged — the job is still dropped so combo falls back to the next target. ([#4165](https://github.com/diegosouzapw/OmniRoute/issues/4165) — thanks @KooshaPari)
|
||||
- **fix(api): advertise the built-in `auto/*` combos in `/v1/models`** — OmniRoute ships a zero-setup `auto/*` catalog (`auto/best-coding`, `auto/pro-reasoning`, …, 16 variants) that the dashboard advertises and that resolve on demand, but the `/v1/models` listing only emitted persisted DB combos + provider models. Clients that build their model picker from `/v1/models` (e.g. Hermes Agent) never saw any `auto/*` option. The catalog now emits every `AUTO_TEMPLATE_VARIANTS` id (as `owned_by: "combo"`) at the top of the list, deduped against persisted combos. (Showing each `auto/*`'s dynamically-selected members is a separate enhancement.) ([#4164](https://github.com/diegosouzapw/OmniRoute/issues/4164) — thanks @MRDGH2821)
|
||||
- **fix(sse): restore MCP / third-party tool names on the native Claude path (MCP dispatch broken in Claude Code)** — since 3.8.27, every MCP tool call routed through OmniRoute to a native Claude OAuth provider failed client-side with `Error: No such tool available: <PascalCaseName>`: tool schemas arrived fine but the streamed `tool_use.name` reached Claude Code in its cloaked form (e.g. `McpN8nMcpSearchWorkflows` instead of the registered `mcp__n8n-mcp__search_workflows`). The native-Claude tool-name cloak stashes its per-request alias→original map as a **non-enumerable** `_toolNameMap` on the request body; the request-inspector capture added in 3.8.27 rebuilds the captured body from its serialized form (`JSON.parse(JSON.stringify(...))`), which drops non-enumerable properties, so `finalBody._toolNameMap` was empty and the response-side un-cloak silently fell back to the static built-in map — never restoring dynamic MCP / snake_case names. Built-in tools (Bash/Read/…) were unaffected (static map); cross-format paths were unaffected (they attach the map enumerably). The provider-request capture now re-attaches the per-request map (kept non-enumerable, so it still never re-serializes upstream) when the captured copy lost it, restoring MCP tool dispatch. ([#4091](https://github.com/diegosouzapw/OmniRoute/issues/4091) — thanks @pedrotecinf, @NakHalal)
|
||||
|
||||
@@ -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_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.",
|
||||
"_rebaseline_2026_06_18_cost_telemetry_parity": "Cost Telemetry Parity own growth: chatCore.ts 5095->5097 (+2 net at the existing non-streaming success return chokepoint, ~line 4629). The inline `headers: { ...buildOmniRouteResponseMetaHeaders({…}) }` spread was converted into an explicit `responseHeaders` Record + a single attachOmniRouteMetaHeaders(responseHeaders, {…, requestId: skillRequestId}) call, routing this return through the new choke-point helper and threading the new identity header X-OmniRoute-Request-Id (skillRequestId). The now-orphaned buildOmniRouteResponseMetaHeaders import was dropped (only attachOmniRouteMetaHeaders is used here), netting the construction to +2. The reusable helper attachOmniRouteMetaHeaders + the always-on X-OmniRoute-Version header live in the (non-frozen) src/domain/omnirouteResponseMeta.ts. Cohesive telemetry growth at the response chokepoint; the +2 is the headers-bag construction needed to mutate-in-place rather than spread, not extractable without hiding the return boundary. Structural shrink of chatCore.ts tracked in #3501.",
|
||||
@@ -134,7 +135,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": 2534,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2538,
|
||||
"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,
|
||||
|
||||
@@ -151,6 +151,10 @@ const NAMED_OPENAI_STYLE_PROVIDERS = new Set([
|
||||
// #4202: zenmux is the same case — its free models (e.g. z-ai/glm-5.2-free,
|
||||
// moonshotai/kimi-k2.7-code-free) live only on the upstream /models list.
|
||||
"zenmux",
|
||||
// #4249: vercel-ai-gateway carries a real baseUrl (.../v1/chat/completions) but
|
||||
// was unclassified, so import served the 5-entry hardcoded catalog instead of the
|
||||
// live `https://ai-gateway.vercel.sh/v1/models` list. Falls back to local on error.
|
||||
"vercel-ai-gateway",
|
||||
]);
|
||||
|
||||
function isNamedOpenAIStyleProvider(provider: string): boolean {
|
||||
|
||||
128
tests/unit/vercel-gateway-models-fetch-4249.test.ts
Normal file
128
tests/unit/vercel-gateway-models-fetch-4249.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* TDD regression for #4249: the Vercel AI Gateway "import models" button on
|
||||
* /models returned nothing usable — clicking import did not load the live
|
||||
* catalog, even though manually adding the same models works. (@FerLuisxd)
|
||||
*
|
||||
* Root cause: `vercel-ai-gateway` carries a real `baseUrl`
|
||||
* (`https://ai-gateway.vercel.sh/v1/chat/completions`, format "openai") in the
|
||||
* registry, but is not classified by any live-fetch branch of the import route —
|
||||
* it is not `openai-compatible-*`, not self-hosted, and not in
|
||||
* NAMED_OPENAI_STYLE_PROVIDERS. So the route never probes the upstream `/models`
|
||||
* and falls through to the registry's tiny hardcoded `models[]` (5 entries).
|
||||
*
|
||||
* Fix: add `vercel-ai-gateway` to NAMED_OPENAI_STYLE_PROVIDERS so the route does a
|
||||
* live `<baseUrl>/models` fetch. After stripping `/chat/completions`, the
|
||||
* `${base}/models` candidate resolves to `https://ai-gateway.vercel.sh/v1/models`
|
||||
* (the real Vercel AI Gateway models endpoint), normalized via
|
||||
* `normalizeOpenAiLikeModelsResponse`, falling back to the local catalog only when
|
||||
* the upstream fetch fails — import never breaks. Mirrors #4202 (zenmux) and
|
||||
* #3976 (llm7/byteplus).
|
||||
*/
|
||||
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";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-4249-"));
|
||||
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 modelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
interface ModelsBody {
|
||||
provider: string;
|
||||
connectionId: string;
|
||||
models: Array<{ id: string }>;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
test("#4249 Vercel AI Gateway import fetches the live /v1/models catalog", async () => {
|
||||
await resetStorage();
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "vercel-ai-gateway",
|
||||
authType: "apikey",
|
||||
name: "vag-live",
|
||||
apiKey: "vck_key",
|
||||
});
|
||||
|
||||
let fetched = false;
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url) => {
|
||||
// `${base}/models` after stripping `/chat/completions` → the gateway endpoint.
|
||||
if (String(url) === "https://ai-gateway.vercel.sh/v1/models") {
|
||||
fetched = true;
|
||||
return Response.json({
|
||||
object: "list",
|
||||
data: [
|
||||
{ id: "xai/grok-4" },
|
||||
{ id: "openai/gpt-5.1" },
|
||||
{ id: "anthropic/claude-opus-4.5" },
|
||||
],
|
||||
});
|
||||
}
|
||||
// Bogus probe variants (…/v1/v1/models, …/chat/completions/models) → 404
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await modelsRoute.GET(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as ModelsBody;
|
||||
assert.equal(body.provider, "vercel-ai-gateway");
|
||||
assert.equal(body.source, "api", "should serve the live upstream catalog, not local_catalog");
|
||||
assert.ok(fetched, "should have probed https://ai-gateway.vercel.sh/v1/models");
|
||||
const ids = body.models.map((m) => m.id);
|
||||
assert.ok(ids.includes("xai/grok-4"), `live models missing from catalog: ${ids.join(",")}`);
|
||||
assert.ok(ids.includes("openai/gpt-5.1"), `live models missing: ${ids.join(",")}`);
|
||||
// The stale hardcoded registry entry must not be what we serve.
|
||||
assert.ok(
|
||||
!ids.includes("vercel/v0-1.5-md"),
|
||||
"served the stale hardcoded registry catalog instead of the live list"
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("#4249 Vercel AI Gateway import falls back to the local catalog when the live fetch fails", async () => {
|
||||
await resetStorage();
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "vercel-ai-gateway",
|
||||
authType: "apikey",
|
||||
name: "vag-fallback",
|
||||
apiKey: "vck_key_2",
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => new Response("bad gateway", { status: 502 });
|
||||
|
||||
try {
|
||||
const response = await modelsRoute.GET(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as ModelsBody;
|
||||
assert.equal(body.provider, "vercel-ai-gateway");
|
||||
assert.equal(body.source, "local_catalog", "import must not break when upstream is down");
|
||||
assert.ok(body.models.length > 0, "fallback catalog should be non-empty");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user