Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- **fix(catalog):** PublicAI is no longer miscatalogued as keyless/free — it requires an API key (registry `authType:"apikey"`; signup grants a one-time credit, then it bills). The three PublicAI models moved from `freeType:"keyless"` (which could pick them into the no-auth pool and dispatch with no `Authorization` header) to `"one-time-initial"`, and the provider's `hasFree` flag is now `false` — matching `freeTierCatalog.ts`, which already excluded publicai. ([#3558](https://github.com/diegosouzapw/OmniRoute/issues/3558))
- **fix(gemini-web):** a missing Playwright Chromium browser no longer loops and trips the provider breaker — when the browser binary is not installed, `chromium.launch()` threw an error surfaced as a retryable **500**, so accountFallback marked the account unavailable and retry-looped. It is now classified as a host/config problem and returns **503** with an actionable message (`npx playwright install chromium`) and the `X-Omni-Fallback-Hint: connection_cooldown` header, which skips the provider circuit breaker and applies a short non-exponential cooldown. ([#3516](https://github.com/diegosouzapw/OmniRoute/issues/3516))
- **fix(proxy):** the SOCKS5 proxy option now follows the runtime `ENABLE_SOCKS5_PROXY` env instead of the build-time `NEXT_PUBLIC_ENABLE_SOCKS5_PROXY` — Next.js inlines `NEXT_PUBLIC_*` at build time, so a prebuilt Docker image ignored a runtime setting and the SOCKS5 type stayed hidden. The proxy modal now reads `socks5Enabled` from `GET /api/settings/proxies` (server-side `ENABLE_SOCKS5_PROXY`), with the build-time value kept only as a static-deploy fallback. ([#3508](https://github.com/diegosouzapw/OmniRoute/issues/3508))
- **fix(playground):** the playground model selector now lists models from custom-endpoint (OpenAI/Anthropic-compatible) providers — it filtered `/v1/models` by the provider's connection id, but the catalog emits compatible-provider models under the node's custom prefix (`prefix/model`), so the list came up empty ("None"/"-"). The selector now filters by the node prefix (exposed additively as `modelPrefix` on provider options; the connection id is unchanged, so translator send/translate and connection lookups are unaffected). ([#3505](https://github.com/diegosouzapw/OmniRoute/issues/3505))
- **fix(security):** route raw `err.message` through `sanitizeErrorMessage()` in five web executors (`adapta-web`, `deepseek-web`, `perplexity-web`, `qoder`, `veoaifree-web`) and the embeddings + search handlers (Hard Rule #12) — these built error response bodies from the raw upstream/exception message, which could leak internal detail. ([#3494](https://github.com/diegosouzapw/OmniRoute/issues/3494), [#3495](https://github.com/diegosouzapw/OmniRoute/issues/3495))
- **fix(dashboard):** correct two dashboard fetches that hit non-existent routes (404) — `CustomHostsManager` called `/api/tools/traffic-inspector/custom-hosts` (the real route is `/hosts`), and `FeatureFlagsGrid`'s post-restart liveness probe called `/api/health` (the real lightweight endpoint is `/api/health/ping`). ([#3486](https://github.com/diegosouzapw/OmniRoute/issues/3486), [#3487](https://github.com/diegosouzapw/OmniRoute/issues/3487))
- **chore(providers):** remove the dead `krutrim` registry entry — it was half-registered (present in `providerRegistry.ts` with a baseUrl + one model, but absent from `providers.ts`, with no executor/translator/OAuth), so it was never selectable. Dropped its `ProviderIcon` entry and the `KNOWN_REGISTRY_ONLY` exception. ([#3483](https://github.com/diegosouzapw/OmniRoute/issues/3483))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ export default function StudioConfigPane({ configState, setConfigState }: Studio
const { provider, setProvider, providerOptions, loading: loadingProviders } = useProviderOptions(
configState.provider ?? ""
);
const { availableModels, loading: loadingModels } = useAvailableModels(provider || undefined);
// #3505: filter models by the selected provider's catalog namespace. Compatible providers
// emit models under their node prefix (e.g. "myprefix/gpt-4o"), not under the connection id,
// so use the option's modelPrefix when present; fall back to the id for built-in providers.
const selectedProviderOption = providerOptions.find(
(opt: { value: string; modelPrefix?: string }) => opt.value === provider
);
Comment on lines +58 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use optional chaining (providerOptions?.find) to safely handle cases where providerOptions might be null or undefined (e.g., during initial load or error states). This aligns with defensive programming practices to prevent runtime TypeError crashes.

Suggested change
const selectedProviderOption = providerOptions.find(
(opt: { value: string; modelPrefix?: string }) => opt.value === provider
);
const selectedProviderOption = providerOptions?.find(
(opt: { value: string; modelPrefix?: string }) => opt.value === provider
);

const modelFilterKey = selectedProviderOption?.modelPrefix || provider || undefined;
const { availableModels, loading: loadingModels } = useAvailableModels(modelFilterKey);

function update<K extends keyof ConfigState>(key: K, value: ConfigState[K]) {
setConfigState({ ...configState, [key]: value });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ const FORMAT_MODEL_PREFIXES = {
* pickModelForFormat: (format: string) => string
* }}
*/
/**
* Filter the /v1/models id list to a provider's models. The `provider` key must be the model
* NAMESPACE used in the catalog: built-in providers use their id (e.g. "openai"), while
* compatible providers use the node's custom PREFIX (e.g. "myprefix"), NOT the node id — see
* #3505. Pure + exported for testing.
*/
export function filterModelsByProvider(allModels: string[], provider?: string): string[] {
return provider
? allModels.filter((m) => m.startsWith(`${provider}/`) || m === provider)
: allModels;
}

export function useAvailableModels(provider?: string) {
const [model, setModel] = useState("");
const [allModels, setAllModels] = useState<string[]>([]);
Expand All @@ -46,11 +58,10 @@ export function useAvailableModels(provider?: string) {
fetchModels();
}, []);

const availableModels = useMemo(() => {
return provider
? allModels.filter((m) => m.startsWith(`${provider}/`) || m === provider)
: allModels;
}, [allModels, provider]);
const availableModels = useMemo(
() => filterModelsByProvider(allModels, provider),
[allModels, provider]
);

/**
* Pick the best model for a given format from the available models.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export function useProviderOptions(initialProvider = "openai") {
label = node?.name || t("openaiCompatibleLabel");
if (!info && (pid as string).startsWith(ANTHROPIC_COMPATIBLE_PREFIX))
label = node?.name || t("anthropicCompatibleLabel");
return { value: pid, label };
// #3505: compatible providers emit catalog models under the node's custom prefix
// (e.g. "myprefix/gpt-4o"), not under the connection id (`value`). Expose the prefix
// so model-filter consumers (playground) can match; `value` stays the id for
// connection lookups (translator send/translate, ApiTab).
const modelPrefix = !info && node?.prefix ? String(node.prefix) : undefined;
return { value: pid, label, modelPrefix };
})
.sort((a, b) => compareTr(a.label, b.label));

Expand Down
30 changes: 30 additions & 0 deletions tests/unit/playground-model-filter-3505.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import test from "node:test";
import assert from "node:assert/strict";
import { filterModelsByProvider } from "../../src/app/(dashboard)/dashboard/translator/hooks/useAvailableModels.tsx";

// Regression for #3505: the playground model selector filtered the /v1/models list by the
// provider OPTION VALUE (a compatible provider's node id, e.g. "openai-compatible-<uuid>"),
// but the catalog emits compatible-provider models under the node's custom PREFIX
// (e.g. "myprefix/gpt-4o"). So `startsWith("openai-compatible-<uuid>/")` matched nothing and
// the selector showed "None"/"-". The fix passes the node prefix as the filter key. This locks
// the filter behaviour: given the right key (the prefix), the models surface.

test("#3505 filters models by a custom node prefix (the catalog's model namespace)", () => {
const all = ["myprefix/gpt-4o", "myprefix/llama-3", "openai/gpt-4o", "anthropic/claude-opus-4-8"];
assert.deepEqual(filterModelsByProvider(all, "myprefix"), ["myprefix/gpt-4o", "myprefix/llama-3"]);
});

test("#3505 a UUID-style node id (the old wrong key) matches nothing → empty (the bug)", () => {
const all = ["myprefix/gpt-4o", "openai/gpt-4o"];
assert.deepEqual(filterModelsByProvider(all, "openai-compatible-1234-uuid"), []);
});

test("#3505 built-in provider filtering still works", () => {
const all = ["openai/gpt-4o", "openai/gpt-4o-mini", "anthropic/claude-opus-4-8"];
assert.deepEqual(filterModelsByProvider(all, "openai"), ["openai/gpt-4o", "openai/gpt-4o-mini"]);
});

test("#3505 an exact bare match is included; no provider returns all", () => {
assert.deepEqual(filterModelsByProvider(["auto", "openai/gpt-4o"], "auto"), ["auto"]);
assert.deepEqual(filterModelsByProvider(["a", "b"], undefined), ["a", "b"]);
});
Comment on lines +1 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the project standardizes on Vitest (as indicated by vitest.config.ts), writing tests using Node's native node:test and node:assert introduces inconsistency. Migrating this test file to use Vitest's native describe, it, and expect APIs will improve consistency and integration with the test suite. Additionally, importing with a .tsx extension should be avoided to prevent potential TypeScript compilation or module resolution issues.

import { describe, it, expect } from "vitest";
import { filterModelsByProvider } from "../../src/app/(dashboard)/dashboard/translator/hooks/useAvailableModels";

// Regression for #3505: the playground model selector filtered the /v1/models list by the
// provider OPTION VALUE (a compatible provider's node id, e.g. "openai-compatible-<uuid>"),
// but the catalog emits compatible-provider models under the node's custom PREFIX
// (e.g. "myprefix/gpt-4o"). So `startsWith("openai-compatible-<uuid>/")` matched nothing and
// the selector showed "None"/"-". The fix passes the node prefix as the filter key. This locks
// the filter behaviour: given the right key (the prefix), the models surface.

describe("#3505 playground model filter", () => {
  it("filters models by a custom node prefix (the catalog's model namespace)", () => {
    const all = ["myprefix/gpt-4o", "myprefix/llama-3", "openai/gpt-4o", "anthropic/claude-opus-4-8"];
    expect(filterModelsByProvider(all, "myprefix")).toEqual(["myprefix/gpt-4o", "myprefix/llama-3"]);
  });

  it("a UUID-style node id (the old wrong key) matches nothing → empty (the bug)", () => {
    const all = ["myprefix/gpt-4o", "openai/gpt-4o"];
    expect(filterModelsByProvider(all, "openai-compatible-1234-uuid")).toEqual([]);
  });

  it("built-in provider filtering still works", () => {
    const all = ["openai/gpt-4o", "openai/gpt-4o-mini", "anthropic/claude-opus-4-8"];
    expect(filterModelsByProvider(all, "openai")).toEqual(["openai/gpt-4o", "openai/gpt-4o-mini"]);
  });

  it("an exact bare match is included; no provider returns all", () => {
    expect(filterModelsByProvider(["auto", "openai/gpt-4o"], "auto")).toEqual(["auto"]);
    expect(filterModelsByProvider(["a", "b"], undefined)).toEqual(["a", "b"]);
  });
});