fix(routing): fall through to compatible node when built-in alias has no credentials
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
getModelAliases,
|
||||
getComboByName,
|
||||
getProviderNodes,
|
||||
getProviderConnections,
|
||||
} from "@/lib/localDb";
|
||||
import {
|
||||
parseModel as parseModelCore,
|
||||
@@ -44,6 +45,52 @@ export async function resolveModelAlias(alias) {
|
||||
return resolveModelAliasFromMap(alias, aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a user-defined compatible node (openai/anthropic/custom-embedding) for a
|
||||
* model string.
|
||||
*
|
||||
* Built-in provider ids/aliases (e.g. `cf`, `cloudflare-ai`, `tr`) are reserved:
|
||||
* a compatible node must not shadow them. But when the built-in provider has no
|
||||
* active credentials, a matching compatible node is the user's explicit intent
|
||||
* (e.g. prefix `tr` -> a tokenrouter.com gateway), so fall through to it instead
|
||||
* of failing with "No active credentials for provider".
|
||||
*/
|
||||
async function matchCompatibleNode(providerAlias, model, builtinProviderId) {
|
||||
const reserved = RESERVED_PROVIDER_PREFIXES.has(providerAlias);
|
||||
if (reserved) {
|
||||
const builtinConns = await getProviderConnections({
|
||||
provider: builtinProviderId,
|
||||
isActive: true,
|
||||
});
|
||||
// Built-in route wins as long as it has credentials.
|
||||
if (builtinConns.length > 0) return null;
|
||||
}
|
||||
|
||||
const openaiNodes = await getProviderNodes({ type: "openai-compatible" });
|
||||
const matchedOpenAI = openaiNodes.find(
|
||||
(node) => node.prefix === providerAlias,
|
||||
);
|
||||
if (matchedOpenAI) return { provider: matchedOpenAI.id, model };
|
||||
|
||||
const anthropicNodes = await getProviderNodes({
|
||||
type: "anthropic-compatible",
|
||||
});
|
||||
const matchedAnthropic = anthropicNodes.find(
|
||||
(node) => node.prefix === providerAlias,
|
||||
);
|
||||
if (matchedAnthropic) return { provider: matchedAnthropic.id, model };
|
||||
|
||||
const embeddingNodes = await getProviderNodes({
|
||||
type: "custom-embedding",
|
||||
});
|
||||
const matchedEmbedding = embeddingNodes.find(
|
||||
(node) => node.prefix === providerAlias,
|
||||
);
|
||||
if (matchedEmbedding) return { provider: matchedEmbedding.id, model };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full model info (parse or resolve)
|
||||
*/
|
||||
@@ -51,37 +98,12 @@ 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 };
|
||||
}
|
||||
|
||||
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 matched = await matchCompatibleNode(
|
||||
parsed.providerAlias,
|
||||
parsed.model,
|
||||
parsed.provider,
|
||||
);
|
||||
if (matched) return matched;
|
||||
return {
|
||||
provider: parsed.provider,
|
||||
model: parsed.model,
|
||||
|
||||
Reference in New Issue
Block a user