fix(api): resolve /v1/models/{id} case-insensitively (#5082) (#5135)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-27 01:12:17 -03:00
committed by GitHub
parent c4fc33480c
commit 454eb75b31
3 changed files with 35 additions and 2 deletions

View File

@@ -26,6 +26,7 @@ _In development — bullets added per PR; finalized at release._
### 🔧 Bug Fixes
- **fix(api): resolve `GET /v1/models/{id}` case-insensitively** — clients that normalise the model id (e.g. OpenCode requesting `minimax/minimax-m3` for the canonical catalog entry `minimax/MiniMax-M3`) missed the single-model lookup, which is case-sensitive, and fell back to advertising `context_length: 0`. `findModelById` now prefers an exact-case match and falls back to a case-insensitive match, so the real entry (and its context window) is returned regardless of casing. ([#5082](https://github.com/diegosouzapw/OmniRoute/issues/5082))
- **fix(services): embed WS proxy honours `LIVE_WS_HOST`; reject empty `messages` early** — two headless/Docker deployment fixes (#5110). The embed WebSocket proxy (`:20131`) only read `EMBED_WS_PROXY_HOST`, so behind a reverse proxy/tunnel it stayed bound to `127.0.0.1` even with `LIVE_WS_HOST=0.0.0.0` set and the Live dashboard showed "WebSocket disconnected"; it now falls back to `LIVE_WS_HOST` (default still loopback). Separately, a request with an explicitly empty `messages: []` array was forwarded upstream and bounced back as a confusing raw `400/502`; `handleChat` now rejects it up front with a clear `messages: at least one message is required` (Responses-API `input` requests are unaffected). ([#5110](https://github.com/diegosouzapw/OmniRoute/issues/5110))
- **fix(proxy): repair one-click Deno & Cloudflare relay deployments** — the `/api/settings/proxy/test` endpoint only recognized the `vercel` relay type, so testing a deployed Deno or Cloudflare relay returned `proxy.type must be http, https, or socks5` and never reached the relay; it now routes all relay types through `isRelayType()`. On installs with `STORAGE_ENCRYPTION_KEY` the relay-auth token is read via `extractRelayAuth` (encrypted `relayAuthEnc` form), fixing the silent `401` that left `publicIp` null. The Cloudflare Worker upload now sends the script part as `application/javascript` (the API rejects `application/javascript+module`; ES-module semantics come from `main_module`), and the proxy-registry schema accepts the `deno`/`cloudflare` types + `deno-relay`/`cloudflare-relay` sources so editing a deployed relay no longer 400s. ([#5128](https://github.com/diegosouzapw/OmniRoute/issues/5128))
- **fix(quota): hydrate the in-memory quota cache from snapshots + scope auto-combo candidates** — after a restart the quota cache was empty, so a known-exhausted connection looked healthy until re-queried; `isAccountQuotaExhausted` now lazily hydrates from persisted `quota_snapshots`. Auto-combo candidate expansion is also scoped to the connections each combo target actually allows, instead of pulling in every connection for the provider. ([#5015](https://github.com/diegosouzapw/OmniRoute/pull/5015) — thanks @JxnLexn)

View File

@@ -12,13 +12,23 @@ import { CORS_HEADERS } from "@/shared/utils/cors";
type CatalogModel = { id?: unknown } & Record<string, unknown>;
/** Find a model entry in the unified catalog `data` array by its exact id. */
/**
* Find a model entry in the unified catalog `data` array by id.
*
* Exact-case matches win; failing that we fall back to a case-insensitive match
* so clients that normalise the model id (#5082 — OpenCode requesting
* `minimax/minimax-m3` for the canonical `minimax/MiniMax-M3`) still resolve the
* real entry — and its `context_length` — instead of falling back to 0.
*/
export function findModelById(
data: CatalogModel[] | null | undefined,
requestedId: string
): CatalogModel | null {
if (!Array.isArray(data)) return null;
return data.find((m) => typeof m?.id === "string" && m.id === requestedId) ?? null;
const exact = data.find((m) => typeof m?.id === "string" && m.id === requestedId);
if (exact) return exact;
const lower = requestedId.toLowerCase();
return data.find((m) => typeof m?.id === "string" && m.id.toLowerCase() === lower) ?? null;
}
/**

View File

@@ -38,6 +38,28 @@ test("findModelById returns null for an unknown model", () => {
assert.equal(findModelById(CATALOG, "does-not-exist"), null);
});
// #5082 — OpenCode (and other @ai-sdk/openai-compatible clients) may request a
// model id with different casing than the canonical catalog entry
// (e.g. `minimax/minimax-m3` vs the registered `minimax/MiniMax-M3`). A
// case-sensitive lookup misses, the client falls back to `context_length: 0`.
// The single-model lookup must resolve case-insensitively so the real entry
// (with its context window) is returned.
test("findModelById resolves a differently-cased id (case-insensitive)", () => {
const found = findModelById(CATALOG, "CLAUDE/Claude-Sonnet-4-6");
assert.ok(found, "lowercase/uppercase variants must resolve to the canonical entry");
assert.equal(found.id, "claude/claude-sonnet-4-6");
});
test("findModelById prefers an exact-case match over a case-insensitive one", () => {
const data = [
{ id: "Model-X", object: "model", context_length: 111 },
{ id: "model-x", object: "model", context_length: 222 },
];
// Exact case must win when present, not the first case-insensitive hit.
assert.equal(findModelById(data, "model-x")?.context_length, 222);
assert.equal(findModelById(data, "Model-X")?.context_length, 111);
});
test("findModelById tolerates a non-array catalog", () => {
assert.equal(findModelById(undefined, "gpt-5"), null);
assert.equal(findModelById(null, "gpt-5"), null);