fix(dashboard): qualify vendor-namespaced Playground models with provider prefix (#3050) (#3102)

The provider Playground (LlmChatCard) only added the providerId/ prefix to models without a slash, so vendor-namespaced ids (moonshotai/kimi-k2.6, nvidia/zyphra/...) were sent bare and rejected with 'Ambiguous model' when the same id exists under multiple providers. Extracted qualifyPlaygroundModel() which always prefixes with the provider unless already qualified. Bug 1 ('unhashable type: dict') is an upstream NVIDIA NIM server error, not OmniRoute. Tests: 4 cases for the qualifier.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-03 07:56:16 -03:00
committed by GitHub
parent 5f3b1e8cde
commit 5f7f74dc6a
3 changed files with 56 additions and 7 deletions

View File

@@ -22,6 +22,7 @@ _Development cycle in progress — entries are added as work merges into `releas
### 🔧 Bug Fixes
- **dashboard:** fix "Ambiguous model" error in the provider Playground for vendor-namespaced models — the Playground only prefixed models without a `/`, so ids like `moonshotai/kimi-k2.6` or `nvidia/zyphra/zamba2-7b-instruct` (NVIDIA NIM) were sent bare and rejected when the same id exists under multiple providers. The Playground now always qualifies the selected model with its `providerId/` prefix (without double-prefixing). ([#3050](https://github.com/diegosouzapw/OmniRoute/issues/3050))
- **db:** stop accepting duplicate API keys for the same provider — `createProviderConnection` now dedups by the decrypted key value (not just by name), so re-adding the same key under a different/blank name updates the existing connection instead of inserting a second row. Whitespace-only differences also dedup. ([#3023](https://github.com/diegosouzapw/OmniRoute/issues/3023))
- **dashboard:** "Import from /models" now works for no-auth providers (e.g. OpenCode Free) — the button used to silently no-op because no-auth providers have no connection row, so `handleImportModels` returned early and the models route 404'd. The route now serves the provider's model catalog when called with a no-auth provider id, and the dashboard falls back to the provider id when there is no connection. ([#3047](https://github.com/diegosouzapw/OmniRoute/issues/3047))
- **providers:** forward Grok's paired `sso-rw` cookie for grok-web — both the executor and the connection validator now send `sso=…; sso-rw=…` (via the new `buildGrokCookieHeader` helper) when the pasted blob carries `sso-rw`, fixing the `403` _"Request rejected by anti-bot rules"_ that Grok returns for `sso` alone. The add-account hint now asks for the full cookie line. ([#3063](https://github.com/diegosouzapw/OmniRoute/issues/3063))

View File

@@ -15,6 +15,25 @@ import { useProviderModels } from "../../providers/hooks/useProviderModels";
const ENDPOINT = "/api/v1/chat/completions";
/**
* Qualify a provider-scoped playground model with its `providerId/` prefix so
* OmniRoute can resolve it unambiguously. The previous heuristic only prefixed
* models without a `/`, which skipped vendor-namespaced ids like
* `moonshotai/kimi-k2.6` or `nvidia/zyphra/zamba2-7b-instruct` — those already
* contain a slash, so they were sent bare and rejected with
* "Ambiguous model ... Use provider/model prefix" when the same id exists under
* several providers (#3050). Always prefix unless the id is already qualified
* with this provider.
*/
export function qualifyPlaygroundModel(
model: string | null | undefined,
providerId: string | null | undefined
): string {
const m = (model ?? "").trim();
if (!m || !providerId) return m;
return m === providerId || m.startsWith(`${providerId}/`) ? m : `${providerId}/${m}`;
}
interface Message {
role: "user" | "assistant";
content: string;
@@ -125,13 +144,11 @@ export function LlmChatCard({
const firstModel = models[0]?.id ?? "";
const effectiveModel = model || firstModel || initialModel || "";
// Auto-prefix model with providerId when no provider/model prefix present, to avoid
// OmniRoute "Ambiguous model" rejection when same alias is registered under multiple providers.
const qualifiedModel = effectiveModel.includes("/")
? effectiveModel
: providerId
? `${providerId}/${effectiveModel}`
: effectiveModel;
// Auto-prefix model with providerId to avoid OmniRoute "Ambiguous model"
// rejection when the same id is registered under multiple providers. This
// also covers vendor-namespaced ids (e.g. `moonshotai/kimi-k2.6`) that already
// contain a slash but still need the provider prefix (#3050).
const qualifiedModel = qualifyPlaygroundModel(effectiveModel, providerId);
// Autofocus textarea in embedded mode
useEffect(() => {

View File

@@ -0,0 +1,31 @@
import test from "node:test";
import assert from "node:assert/strict";
const { qualifyPlaygroundModel } = await import(
"../../src/app/(dashboard)/dashboard/media-providers/components/LlmChatCard.tsx"
);
// #3050 — vendor-namespaced model ids already contain a "/", so the old
// `.includes("/")` heuristic skipped the provider prefix and the request was
// rejected with "Ambiguous model 'moonshotai/kimi-k2.6'".
test("qualifyPlaygroundModel prefixes a vendor-namespaced model with providerId (#3050)", () => {
assert.equal(qualifyPlaygroundModel("moonshotai/kimi-k2.6", "nim"), "nim/moonshotai/kimi-k2.6");
assert.equal(
qualifyPlaygroundModel("nvidia/zyphra/zamba2-7b-instruct", "nim"),
"nim/nvidia/zyphra/zamba2-7b-instruct"
);
});
test("qualifyPlaygroundModel prefixes a bare model", () => {
assert.equal(qualifyPlaygroundModel("gpt-4o", "openai"), "openai/gpt-4o");
});
test("qualifyPlaygroundModel does not double-prefix an already-qualified model", () => {
assert.equal(qualifyPlaygroundModel("nim/moonshotai/kimi-k2.6", "nim"), "nim/moonshotai/kimi-k2.6");
assert.equal(qualifyPlaygroundModel("nim", "nim"), "nim");
});
test("qualifyPlaygroundModel returns the model unchanged without a providerId", () => {
assert.equal(qualifyPlaygroundModel("moonshotai/kimi-k2.6", ""), "moonshotai/kimi-k2.6");
assert.equal(qualifyPlaygroundModel("", "nim"), "");
});