Skip to content
Open
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
48 changes: 22 additions & 26 deletions src/sse/services/model.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
// Re-export from open-sse with localDb integration
import { getModelAliases, getComboByName, getProviderNodes } from "@/lib/localDb";
import { parseModel as parseModelCore, resolveModelAliasFromMap, getModelInfoCore } from "open-sse/services/model.js";
import REGISTRY from "open-sse/providers/registry/index.js";

// Local provider alias overrides (HMR-friendly, applied on top of open-sse map)
const LOCAL_PROVIDER_ALIASES = {
xmtp: "xiaomi-tokenplan",
"xiaomi-tokenplan": "xiaomi-tokenplan",
};

const RESERVED_PROVIDER_PREFIXES = new Set(Object.keys(LOCAL_PROVIDER_ALIASES));
for (const entry of REGISTRY) {
RESERVED_PROVIDER_PREFIXES.add(entry.id);
if (entry.alias) RESERVED_PROVIDER_PREFIXES.add(entry.alias);
for (const alias of entry.aliases || []) RESERVED_PROVIDER_PREFIXES.add(alias);
}

export function parseModel(modelStr) {
const parsed = parseModelCore(modelStr);
if (parsed?.providerAlias && LOCAL_PROVIDER_ALIASES[parsed.providerAlias]) {
Expand All @@ -39,27 +31,31 @@ export async function getModelInfo(modelStr) {
const parsed = parseModel(modelStr);

if (!parsed.isAlias) {
// Provider-node prefixes are user-defined. They must not override built-in
// provider ids/aliases such as `cf`, `cloudflare-ai`, `openai`, or `hf`.
if (!RESERVED_PROVIDER_PREFIXES.has(parsed.providerAlias)) {
const openaiNodes = await getProviderNodes({ type: "openai-compatible" });
const matchedOpenAI = openaiNodes.find((node) => node.prefix === parsed.providerAlias);
if (matchedOpenAI) {
return { provider: matchedOpenAI.id, model: parsed.model };
}
// Provider-node prefixes are user-defined. Check custom nodes first — if the
// user explicitly created a node with a given prefix, route to it even when
// the prefix collides with a built-in provider id/alias (e.g. a custom
// "tokenrouter" node with prefix "tr"). The user's credentials are stored
// under the node ID; routing to the built-in provider instead would fail
// with "No credentials for provider". When no custom node matches, fall
// through to the built-in provider resolution below.
const openaiNodes = await getProviderNodes({ type: "openai-compatible" });
const matchedOpenAI = openaiNodes.find((node) => node.prefix === parsed.providerAlias);
if (matchedOpenAI) {
return { provider: matchedOpenAI.id, model: parsed.model };
}

const anthropicNodes = await getProviderNodes({ type: "anthropic-compatible" });
const matchedAnthropic = anthropicNodes.find((node) => node.prefix === parsed.providerAlias);
if (matchedAnthropic) {
return { provider: matchedAnthropic.id, model: parsed.model };
}
const anthropicNodes = await getProviderNodes({ type: "anthropic-compatible" });
const matchedAnthropic = anthropicNodes.find((node) => node.prefix === parsed.providerAlias);
if (matchedAnthropic) {
return { provider: matchedAnthropic.id, model: parsed.model };
}

const embeddingNodes = await getProviderNodes({ type: "custom-embedding" });
const matchedEmbedding = embeddingNodes.find((node) => node.prefix === parsed.providerAlias);
if (matchedEmbedding) {
return { provider: matchedEmbedding.id, model: parsed.model };
}
const embeddingNodes = await getProviderNodes({ type: "custom-embedding" });
const matchedEmbedding = embeddingNodes.find((node) => node.prefix === parsed.providerAlias);
if (matchedEmbedding) {
return { provider: matchedEmbedding.id, model: parsed.model };
}

return {
provider: parsed.provider,
model: parsed.model
Expand Down
32 changes: 30 additions & 2 deletions tests/unit/model-routing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ describe("model routing", () => {
else process.env.DATA_DIR = originalDataDir;
});

it("keeps built-in provider aliases ahead of compatible node prefixes", async () => {
it("routes to custom node when its prefix collides with a built-in provider alias", async () => {
const ctx = await setupDb();
cleanup = ctx.cleanup;

// User explicitly creates a custom node with prefix "cf" (cloudflare-ai alias).
// Credentials are stored under the node ID, so routing must go to the node,
// not the built-in provider (which would have no credentials).
await ctx.createProviderNode({
id: "openai-compatible-chat-test",
type: "openai-compatible",
Expand All @@ -53,11 +56,36 @@ describe("model routing", () => {

await expect(ctx.getModelInfo("cf/@cf/black-forest-labs/flux-2-klein-9b"))
.resolves.toEqual({
provider: "cloudflare-ai",
provider: "openai-compatible-chat-test",
model: "@cf/black-forest-labs/flux-2-klein-9b",
});
});

it("routes to custom node when prefix matches built-in tokenrouter alias (tr)", async () => {
const ctx = await setupDb();
cleanup = ctx.cleanup;

// Reproduces: user creates a custom openai-compatible node named "tokenrouter"
// with prefix "tr". The built-in provider "tokenrouter" also has alias "tr".
// Previously, the reserved-prefix guard skipped the custom node lookup and
// routed to the built-in provider, which had no credentials →
// "No active credentials for provider: tokenrouter".
await ctx.createProviderNode({
id: "openai-compatible-chat-tr-node",
type: "openai-compatible",
name: "tokenrouter",
prefix: "tr",
apiType: "chat",
baseUrl: "https://api.tokenrouter.com/v1",
});

await expect(ctx.getModelInfo("tr/qwen/qwen3.8-max-free"))
.resolves.toEqual({
provider: "openai-compatible-chat-tr-node",
model: "qwen/qwen3.8-max-free",
});
});

it("still routes non-reserved compatible node prefixes", async () => {
const ctx = await setupDb();
cleanup = ctx.cleanup;
Expand Down