mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix: resolve bare fallback model providers
This commit is contained in:
@@ -114,6 +114,48 @@ export function inferUniqueProviderFromConfiguredModels(params: {
|
||||
return providers.values().next().value;
|
||||
}
|
||||
|
||||
export function inferUniqueProviderFromCatalog(params: {
|
||||
catalog: readonly ModelCatalogEntry[];
|
||||
model: string;
|
||||
}): string | undefined {
|
||||
const model = params.model.trim();
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(model);
|
||||
const providers = new Set<string>();
|
||||
for (const entry of params.catalog) {
|
||||
const entryId = entry.id.trim();
|
||||
if (!entryId) {
|
||||
continue;
|
||||
}
|
||||
if (entryId !== model && normalizeLowercaseStringOrEmpty(entryId) !== normalized) {
|
||||
continue;
|
||||
}
|
||||
const provider = normalizeProviderId(entry.provider);
|
||||
if (provider) {
|
||||
providers.add(provider);
|
||||
}
|
||||
if (providers.size > 1) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return providers.size === 1 ? providers.values().next().value : undefined;
|
||||
}
|
||||
|
||||
export function resolveBareModelDefaultProvider(params: {
|
||||
cfg: OpenClawConfig;
|
||||
catalog: readonly ModelCatalogEntry[];
|
||||
model: string;
|
||||
defaultProvider: string;
|
||||
}): string {
|
||||
return (
|
||||
inferUniqueProviderFromConfiguredModels({ cfg: params.cfg, model: params.model }) ??
|
||||
inferUniqueProviderFromCatalog({ catalog: params.catalog, model: params.model }) ??
|
||||
params.defaultProvider
|
||||
);
|
||||
}
|
||||
|
||||
function isConcreteOpenRouterFreeModelRef(ref: ModelRef): boolean {
|
||||
return ref.provider === "openrouter" && ref.model.includes("/") && ref.model.endsWith(":free");
|
||||
}
|
||||
@@ -496,10 +538,19 @@ export function buildAllowedModelSetWithFallbacks(params: {
|
||||
const allowedKeys = new Set<string>();
|
||||
const syntheticCatalogEntries = new Map<string, ModelCatalogEntry>();
|
||||
const addAllowedModelRef = (raw: string) => {
|
||||
const trimmed = raw.trim();
|
||||
const defaultProvider = !trimmed.includes("/")
|
||||
? resolveBareModelDefaultProvider({
|
||||
cfg: params.cfg,
|
||||
catalog,
|
||||
model: trimmed,
|
||||
defaultProvider: params.defaultProvider,
|
||||
})
|
||||
: params.defaultProvider;
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultProvider,
|
||||
});
|
||||
if (!parsed) {
|
||||
return;
|
||||
|
||||
@@ -29,8 +29,10 @@ import {
|
||||
buildConfiguredModelCatalog,
|
||||
buildModelAliasIndex,
|
||||
getModelRefStatusWithFallbackModels,
|
||||
inferUniqueProviderFromCatalog,
|
||||
inferUniqueProviderFromConfiguredModels,
|
||||
normalizeModelSelection,
|
||||
resolveBareModelDefaultProvider,
|
||||
resolveAllowedModelRefFromAliasIndex,
|
||||
resolveAllowlistModelKey as resolveAllowlistModelKeyFromShared,
|
||||
resolveConfiguredModelRef,
|
||||
@@ -60,6 +62,7 @@ export {
|
||||
findNormalizedProviderKey,
|
||||
findNormalizedProviderValue,
|
||||
inferUniqueProviderFromConfiguredModels,
|
||||
inferUniqueProviderFromCatalog,
|
||||
legacyModelKey,
|
||||
modelKey,
|
||||
normalizeModelRef,
|
||||
@@ -67,6 +70,7 @@ export {
|
||||
normalizeProviderId,
|
||||
normalizeProviderIdForAuth,
|
||||
parseModelRef,
|
||||
resolveBareModelDefaultProvider,
|
||||
resolveConfiguredModelRef,
|
||||
resolveHooksGmailModel,
|
||||
resolveModelRefFromString,
|
||||
|
||||
@@ -286,6 +286,44 @@ describe("handleModelsCommand", () => {
|
||||
expect(result?.reply?.text).toContain("Switch: /model <provider/model>");
|
||||
});
|
||||
|
||||
it("does not list bare fallback models under the default provider when catalog ownership is unique", async () => {
|
||||
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
|
||||
{ provider: "openai-codex", id: "gpt-5.4", name: "GPT-5.4" },
|
||||
{ provider: "deepseek", id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
|
||||
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
|
||||
]);
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai-codex/gpt-5.4",
|
||||
fallbacks: ["deepseek-v4-flash", "deepseek-v4-pro"],
|
||||
},
|
||||
models: {
|
||||
"openai-codex/gpt-5.4": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Partial<OpenClawConfig>;
|
||||
|
||||
const defaultProviderResult = await handleModelsCommand(
|
||||
buildParams("/models openai-codex", cfg),
|
||||
true,
|
||||
);
|
||||
const deepseekResult = await handleModelsCommand(buildParams("/models deepseek", cfg), true);
|
||||
|
||||
expect(defaultProviderResult?.reply?.text).toContain(
|
||||
"Models (openai-codex) — showing 1-1 of 1 (page 1/1)",
|
||||
);
|
||||
expect(defaultProviderResult?.reply?.text).toContain("- openai-codex/gpt-5.4");
|
||||
expect(defaultProviderResult?.reply?.text).not.toContain("openai-codex/deepseek-v4");
|
||||
expect(deepseekResult?.reply?.text).toContain(
|
||||
"Models (deepseek) — showing 1-2 of 2 (page 1/1)",
|
||||
);
|
||||
expect(deepseekResult?.reply?.text).toContain("- deepseek/deepseek-v4-flash");
|
||||
expect(deepseekResult?.reply?.text).toContain("- deepseek/deepseek-v4-pro");
|
||||
});
|
||||
|
||||
it("keeps /models list <provider> as an alias", async () => {
|
||||
const result = await handleModelsCommand(buildParams("/models list anthropic"), true);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
buildAllowedModelSet,
|
||||
buildModelAliasIndex,
|
||||
normalizeProviderId,
|
||||
resolveBareModelDefaultProvider,
|
||||
resolveDefaultModelForAgent,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
@@ -94,9 +95,17 @@ export async function buildModelsProviderData(
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
const defaultProvider = !trimmed.includes("/")
|
||||
? resolveBareModelDefaultProvider({
|
||||
cfg,
|
||||
catalog,
|
||||
model: trimmed,
|
||||
defaultProvider: resolvedDefault.provider,
|
||||
})
|
||||
: resolvedDefault.provider;
|
||||
const resolved = resolveModelRefFromString({
|
||||
raw: trimmed,
|
||||
defaultProvider: resolvedDefault.provider,
|
||||
defaultProvider,
|
||||
aliasIndex,
|
||||
});
|
||||
if (!resolved) {
|
||||
|
||||
Reference in New Issue
Block a user