fix(models): bound model browse discovery (#92247)

Bounds default model browsing to configured/read-only discovery while preserving explicit full-catalog browsing. Reuses prepared plugin metadata and auth state without triggering external CLI discovery on the picker hot path, while retaining provider normalization and canonical runtime aliases.

Verified with focused model tests, official OpenAI and Anthropic transport suites, fresh live tool calls for both providers, a full build, AWS check:changed, remote Docker OpenAI tools E2E, and green PR CI.

Fixes #91809.

Co-authored-by: samson1357924 <98934496+samson1357924@users.noreply.github.com>
This commit is contained in:
samson910022
2026-06-13 17:04:55 +08:00
committed by GitHub
parent 1c655008cd
commit 44f45d8729
11 changed files with 264 additions and 60 deletions
+19 -3
View File
@@ -68,19 +68,35 @@ describe("loadModelCatalogForBrowse", () => {
expect(loadCatalog).toHaveBeenCalledExactlyOnceWith({ readOnly: false });
});
it("uses the full catalog when configured visibility has provider wildcards", async () => {
it("uses the read-only catalog when configured visibility has provider wildcards", async () => {
const loadCatalog = vi.fn(async ({ readOnly }: { readOnly: boolean }) =>
readOnly ? readOnlyCatalog : fullCatalog,
);
await expect(
loadModelCatalogForBrowse({ cfg: config({ providerWildcard: true }), loadCatalog }),
).resolves.toBe(readOnlyCatalog);
expect(loadCatalog).toHaveBeenCalledExactlyOnceWith({ readOnly: true });
});
it("uses the full catalog for configured views with provider wildcards", async () => {
const loadCatalog = vi.fn(async ({ readOnly }: { readOnly: boolean }) =>
readOnly ? readOnlyCatalog : fullCatalog,
);
await expect(
loadModelCatalogForBrowse({
cfg: config({ providerWildcard: true }),
view: "configured",
loadCatalog,
}),
).resolves.toBe(fullCatalog);
expect(loadCatalog).toHaveBeenCalledExactlyOnceWith({ readOnly: false });
});
it("returns an empty catalog when read-only catalog loading times out", async () => {
it("returns an empty catalog when read-only catalog loading times out with provider wildcards", async () => {
const onTimeout = vi.fn();
const timeoutHandle = { unref: vi.fn() } as unknown as NodeJS.Timeout;
const clearTimeout = vi.fn();
@@ -94,7 +110,7 @@ describe("loadModelCatalogForBrowse", () => {
const loadCatalog = vi.fn(() => new Promise<ModelCatalogEntry[]>(() => {}));
const resultPromise = loadModelCatalogForBrowse({
cfg: config(),
cfg: config({ providerWildcard: true }),
loadCatalog,
timeoutMs: 5,
onTimeout,
+9 -9
View File
@@ -36,13 +36,6 @@ export function restoreModelCatalogBrowseTestDeps(): void {
modelCatalogBrowseDeps.clearTimeout = globalThis.clearTimeout;
}
function resolveModelCatalogBrowseTimeoutMs(value: number | undefined): number {
return (
clampTimerTimeoutMs(value, 1) ??
resolveTimerTimeoutMs(DEFAULT_MODEL_CATALOG_BROWSE_TIMEOUT_MS, 1)
);
}
/** True when a browse view cannot be answered from read-only cached catalog entries. */
export function modelCatalogBrowseRequiresFullDiscovery(params: {
cfg: OpenClawConfig;
@@ -51,7 +44,15 @@ export function modelCatalogBrowseRequiresFullDiscovery(params: {
const view = params.view ?? "default";
return (
view === "all" ||
parseConfiguredModelVisibilityEntries({ cfg: params.cfg }).providerWildcards.size > 0
(view === "configured" &&
parseConfiguredModelVisibilityEntries({ cfg: params.cfg }).providerWildcards.size > 0)
);
}
function resolveModelCatalogBrowseTimeoutMs(value: number | undefined): number {
return (
clampTimerTimeoutMs(value, 1) ??
resolveTimerTimeoutMs(DEFAULT_MODEL_CATALOG_BROWSE_TIMEOUT_MS, 1)
);
}
@@ -65,7 +66,6 @@ export async function loadModelCatalogForBrowse(params: {
}): Promise<ModelCatalogEntry[]> {
const view = params.view ?? "default";
if (modelCatalogBrowseRequiresFullDiscovery({ cfg: params.cfg, view })) {
// Wildcards depend on provider discovery; read-only cached entries can hide matching models.
return await params.loadCatalog({ readOnly: false });
}
+7 -2
View File
@@ -10,6 +10,11 @@ import { isCliRuntimeProvider } from "./model-runtime-aliases.js";
// model picker choices. Hide them while keeping real provider/model refs visible.
const RETIRED_MODEL_PICKER_PROVIDERS = new Set(["codex", "codex-cli"]);
/** True for retired provider ids that should stay out of model selection surfaces. */
export function isRetiredModelPickerProvider(provider: string): boolean {
return RETIRED_MODEL_PICKER_PROVIDERS.has(normalizeProviderId(provider));
}
/** Creates a provider visibility predicate for model picker rendering. */
export function createModelPickerVisibleProviderPredicate(
params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; includeSetupRegistry?: boolean } = {},
@@ -23,7 +28,7 @@ export function createModelPickerVisibleProviderPredicate(
);
return (provider: string): boolean => {
const normalized = normalizeProviderId(provider);
return !RETIRED_MODEL_PICKER_PROVIDERS.has(normalized) && !cliRuntimeProviders.has(normalized);
return !isRetiredModelPickerProvider(normalized) && !cliRuntimeProviders.has(normalized);
};
}
@@ -31,7 +36,7 @@ export function createModelPickerVisibleProviderPredicate(
export function isModelPickerVisibleProvider(provider: string): boolean {
const normalized = normalizeProviderId(provider);
return (
!RETIRED_MODEL_PICKER_PROVIDERS.has(normalized) &&
!isRetiredModelPickerProvider(normalized) &&
!isCliRuntimeProvider(normalized, { includeSetupRegistry: true })
);
}
+13
View File
@@ -234,6 +234,19 @@ describe("prepared provider auth state", () => {
).resolves.toBe(false);
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(2);
// Bounded browse callers may explicitly consume the prepared broad answer
// while keeping slow fallback discovery disabled.
await expect(
hasAuthForModelProvider({
provider: "openai",
cfg,
discoverExternalCliAuth: false,
allowPluginSyntheticAuth: false,
allowPreparedRuntimeAuth: true,
}),
).resolves.toBe(true);
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(2);
// Broad-scope caller (default flags) still hits the prepared map.
await expect(hasAuthForModelProvider({ provider: "openai", cfg })).resolves.toBe(true);
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(2);
+5 -2
View File
@@ -127,6 +127,7 @@ export async function hasAuthForModelProvider(params: {
store?: AuthProfileStore;
allowPluginSyntheticAuth?: boolean;
discoverExternalCliAuth?: boolean;
allowPreparedRuntimeAuth?: boolean;
runtimeAuthLookup?: RuntimeProviderAuthLookup;
resolveRuntimeAuthLookup?: () => RuntimeProviderAuthLookup;
}): Promise<boolean> {
@@ -162,8 +163,8 @@ export async function hasAuthForModelProvider(params: {
configFingerprint === preparedState.configFingerprint &&
workspaceDir === expectedWorkspaceDir &&
(params.agentDir === undefined || params.agentDir === expectedAgentDir) &&
params.discoverExternalCliAuth !== false &&
params.allowPluginSyntheticAuth !== false &&
(params.allowPreparedRuntimeAuth === true ||
(params.discoverExternalCliAuth !== false && params.allowPluginSyntheticAuth !== false)) &&
params.env === undefined &&
params.store === undefined &&
params.modelApi === undefined;
@@ -227,6 +228,7 @@ export function createProviderAuthChecker(params: {
env?: NodeJS.ProcessEnv;
allowPluginSyntheticAuth?: boolean;
discoverExternalCliAuth?: boolean;
allowPreparedRuntimeAuth?: boolean;
}): (provider: string, modelApi?: string) => Promise<boolean> {
const authCache = new Map<string, boolean>();
let runtimeAuthLookup: RuntimeProviderAuthLookup | undefined;
@@ -247,6 +249,7 @@ export function createProviderAuthChecker(params: {
env: params.env,
allowPluginSyntheticAuth: params.allowPluginSyntheticAuth,
discoverExternalCliAuth: params.discoverExternalCliAuth,
allowPreparedRuntimeAuth: params.allowPreparedRuntimeAuth,
resolveRuntimeAuthLookup: () =>
(runtimeAuthLookup ??= createRuntimeProviderAuthLookup({
cfg: params.cfg,
+18 -1
View File
@@ -2,7 +2,10 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { testing as cliBackendsTesting } from "./cli-backends.js";
import { createModelPickerVisibleProviderPredicate } from "./model-picker-visibility.js";
import {
createModelPickerVisibleProviderPredicate,
isRetiredModelPickerProvider,
} from "./model-picker-visibility.js";
import {
areRuntimeModelRefsEquivalent,
isCliRuntimeProvider,
@@ -169,6 +172,20 @@ describe("resolveCliRuntimeExecutionProvider", () => {
expect(isCliRuntimeProvider("acme-cli")).toBe(false);
expect(isVisibleProvider("acme-cli")).toBe(true);
});
it("recognizes retired picker providers without loading CLI backend metadata", () => {
cliBackendsTesting.setDepsForTest({
resolvePluginSetupRegistry: () => {
throw new Error("retired provider checks should not load setup metadata");
},
resolveRuntimeCliBackends: () => {
throw new Error("retired provider checks should not load runtime metadata");
},
});
expect(isRetiredModelPickerProvider("CODEX-CLI")).toBe(true);
expect(isRetiredModelPickerProvider("anthropic")).toBe(false);
});
});
describe("areRuntimeModelRefsEquivalent", () => {
+88 -3
View File
@@ -29,6 +29,16 @@ const modelProviderAuthMocks = vi.hoisted(() => {
return state;
});
const normalizeProviderModelIdWithRuntimeMock = vi.hoisted(() => vi.fn());
const pluginMetadataMocks = vi.hoisted(() => ({
snapshot: undefined as
| {
plugins: unknown[];
owners: {
cliBackends: Map<string, string>;
};
}
| undefined,
}));
const MODELS_ADD_DEPRECATED_TEXT =
"⚠️ /models add is deprecated. Use /models to browse providers and /model to switch models.";
@@ -83,6 +93,10 @@ vi.mock("../../agents/provider-model-normalization.runtime.js", () => ({
normalizeProviderModelIdWithRuntimeMock(params),
}));
vi.mock("../../plugins/current-plugin-metadata-snapshot.js", () => ({
getCurrentPluginMetadataSnapshot: () => pluginMetadataMocks.snapshot,
}));
const telegramModelsTestPlugin: ChannelPlugin = {
...createChannelTestPluginBase({
id: "telegram",
@@ -160,6 +174,7 @@ beforeEach(() => {
modelAuthLabelMocks.resolveModelAuthLabel.mockReset();
modelAuthLabelMocks.resolveModelAuthLabel.mockReturnValue(undefined);
normalizeProviderModelIdWithRuntimeMock.mockReset();
pluginMetadataMocks.snapshot = undefined;
modelProviderAuthMocks.authenticatedProviders = new Set(["anthropic", "google", "openai"]);
modelProviderAuthMocks.createProviderAuthChecker.mockClear();
const registry = createTestRegistry([
@@ -252,6 +267,12 @@ function firstAuthCheckerParams() {
return modelProviderAuthMocks.createProviderAuthChecker.mock.calls[0]?.[0];
}
function preparedAuthCheckerParams() {
return modelProviderAuthMocks.createProviderAuthChecker.mock.calls
.map(([params]) => params)
.find((params) => params.allowPreparedRuntimeAuth === true);
}
describe("handleModelsCommand", () => {
it("shows a simple providers menu on text surfaces", async () => {
const result = await handleModelsCommand(buildParams("/models"), true);
@@ -264,7 +285,7 @@ describe("handleModelsCommand", () => {
expect(result?.reply?.text).toContain("Use: /models <provider>");
expect(result?.reply?.text).toContain("Switch: /model <provider/model>");
expect(result?.reply?.text).not.toContain("Add: /models add");
const authCheckerParams = firstAuthCheckerParams();
const authCheckerParams = preparedAuthCheckerParams();
expect(authCheckerParams?.workspaceDir).toBe("/tmp");
});
@@ -272,9 +293,10 @@ describe("handleModelsCommand", () => {
await handleModelsCommand(buildParams("/models"), true);
expect(modelCatalogMocks.loadModelCatalog.mock.calls[0]?.[0]?.readOnly).toBe(true);
const authCheckerParams = firstAuthCheckerParams();
const authCheckerParams = preparedAuthCheckerParams();
expect(authCheckerParams?.allowPluginSyntheticAuth).toBe(false);
expect(authCheckerParams?.discoverExternalCliAuth).toBe(false);
expect(authCheckerParams?.allowPreparedRuntimeAuth).toBe(true);
});
it("does not block default browse when read-only catalog loading is slow", async () => {
@@ -302,6 +324,25 @@ describe("handleModelsCommand", () => {
expect(modelCatalogMocks.loadModelCatalog.mock.calls[0]?.[0]?.readOnly).toBe(false);
});
it("reuses the current plugin metadata snapshot for read-only catalog loading", async () => {
const metadataSnapshot = {
plugins: [],
owners: {
cliBackends: new Map<string, string>(),
},
};
pluginMetadataMocks.snapshot = metadataSnapshot;
await handleModelsCommand(buildParams("/models"), true);
expect(modelCatalogMocks.loadModelCatalog).toHaveBeenCalledWith(
expect.objectContaining({
readOnly: true,
metadataSnapshot,
}),
);
});
it("hides unauthenticated providers by default and keeps all as explicit browse", async () => {
modelProviderAuthMocks.authenticatedProviders = new Set(["anthropic"]);
@@ -375,7 +416,7 @@ describe("handleModelsCommand", () => {
true,
);
expect(modelCatalogMocks.loadModelCatalog.mock.calls[0]?.[0]?.readOnly).toBe(false);
expect(modelCatalogMocks.loadModelCatalog.mock.calls[0]?.[0]?.readOnly).toBe(true);
expect(result?.reply?.text).toContain("- openai (2)");
expect(result?.reply?.text).toContain("- vllm (2)");
expect(result?.reply?.text).not.toContain("- anthropic");
@@ -449,6 +490,50 @@ describe("handleModelsCommand", () => {
]);
});
it("does not treat standalone CLI backends as canonical provider aliases", async () => {
cliBackendsTesting.setDepsForTest({
resolvePluginSetupRegistry: () => ({
providers: [],
cliBackends: [],
configMigrations: [],
autoEnableProbes: [],
diagnostics: [],
}),
resolveRuntimeCliBackends: () => [
{
id: "acme-cli",
pluginId: "acme",
config: { command: "acme" },
bundleMcp: false,
},
],
});
pluginMetadataMocks.snapshot = {
plugins: [],
owners: {
cliBackends: new Map([["acme-cli", "acme"]]),
},
};
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
{ provider: "anthropic", id: "claude-opus-4-7", name: "Claude Opus 4.7" },
{ provider: "acme-cli", id: "acme-model", name: "Acme Model" },
]);
modelProviderAuthMocks.authenticatedProviders = new Set(["anthropic", "acme-cli"]);
const data = await buildModelsProviderData({
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-7" },
models: {
"anthropic/*": {},
},
},
},
} as OpenClawConfig);
expect(data.byProvider.has("acme-cli")).toBe(false);
});
it("keeps non-CLI configured provider model lists scoped to user config", async () => {
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
{ provider: "claude-cli", id: "claude-opus-4-7", name: "Claude Opus 4.7" },
+40 -28
View File
@@ -15,9 +15,8 @@ import { resolveModelAuthLabel } from "../../agents/model-auth-label.js";
import { loadModelCatalogForBrowse } from "../../agents/model-catalog-browse.js";
import { resolveVisibleModelCatalog } from "../../agents/model-catalog-visibility.js";
import { loadModelCatalog } from "../../agents/model-catalog.js";
import { isModelPickerVisibleProvider } from "../../agents/model-picker-visibility.js";
import { isRetiredModelPickerProvider } from "../../agents/model-picker-visibility.js";
import { createProviderAuthChecker } from "../../agents/model-provider-auth.js";
import { isCliRuntimeProvider } from "../../agents/model-runtime-aliases.js";
import {
buildModelAliasIndex,
normalizeProviderId,
@@ -34,6 +33,7 @@ import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
import { getChannelPlugin } from "../../channels/plugins/index.js";
import type { SessionEntry } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
import { resolveAgentRuntimeLabel } from "../../status/agent-runtime-label.js";
import type { ReplyPayload } from "../types.js";
import { rejectUnauthorizedCommand } from "./command-gates.js";
@@ -78,15 +78,14 @@ type ParsedModelsCommand =
};
function isModelsBrowseVisibleProvider(provider: string): boolean {
const normalized = normalizeProviderId(provider);
return (
isCliRuntimeProvider(normalized, { includeSetupRegistry: true }) ||
isModelPickerVisibleProvider(normalized)
);
return !isRetiredModelPickerProvider(provider);
}
function usesUnfilteredCatalogModels(provider: string): boolean {
return isCliRuntimeProvider(provider, { includeSetupRegistry: true });
function usesUnfilteredCatalogModels(
provider: string,
cliRuntimeProviders: ReadonlySet<string>,
): boolean {
return cliRuntimeProviders.has(normalizeProviderId(provider));
}
function normalizeRuntimeChoiceId(runtime: string | undefined): string {
@@ -155,11 +154,24 @@ export async function buildModelsProviderData(
cfg,
agentId,
});
const workspaceDir =
options.workspaceDir ??
(agentId ? resolveAgentWorkspaceDir(cfg, agentId) : undefined) ??
resolveDefaultAgentWorkspaceDir();
const metadataSnapshot = getCurrentPluginMetadataSnapshot({
config: cfg,
workspaceDir,
env: process.env,
allowScopedSnapshot: true,
});
const cliRuntimeProviders = new Set(
listCliRuntimeModelBackendBindings().map((binding) => normalizeProviderId(binding.runtime)),
);
const catalog = await loadModelCatalogForBrowse({
cfg,
view: options.view ?? "default",
loadCatalog: ({ readOnly }) => loadModelCatalog({ config: cfg, readOnly }),
loadCatalog: ({ readOnly }) => loadModelCatalog({ config: cfg, readOnly, metadataSnapshot }),
});
const visibilityPolicy = createModelVisibilityPolicy({
cfg,
@@ -169,18 +181,27 @@ export async function buildModelsProviderData(
agentId,
...RUNTIME_MODEL_VISIBILITY_NORMALIZATION,
});
const hasAuth: (provider: string) => Promise<boolean> =
options.view === "all"
? async () => true
: createProviderAuthChecker({
cfg,
workspaceDir,
agentId,
allowPluginSyntheticAuth: false,
discoverExternalCliAuth: false,
allowPreparedRuntimeAuth: true,
});
const visibleCatalog = await resolveVisibleModelCatalog({
cfg,
catalog,
defaultProvider: resolvedDefault.provider,
defaultModel: resolvedDefault.model,
agentId,
workspaceDir:
options.workspaceDir ??
(agentId ? resolveAgentWorkspaceDir(cfg, agentId) : undefined) ??
resolveDefaultAgentWorkspaceDir(),
workspaceDir,
view: options.view,
runtimeAuthDiscovery: false,
providerAuthChecker: hasAuth,
});
const aliasIndex = buildModelAliasIndex({
@@ -198,7 +219,7 @@ export async function buildModelsProviderData(
}
if (
restrictToProviderWildcards &&
!usesUnfilteredCatalogModels(key) &&
!usesUnfilteredCatalogModels(key, cliRuntimeProviders) &&
!visibilityPolicy.allows({ provider: key, model: m })
) {
return;
@@ -258,20 +279,11 @@ export async function buildModelsProviderData(
add(entry.provider, entry.id);
}
const hasAuth: (provider: string) => Promise<boolean> =
options.view === "all"
? async () => true
: createProviderAuthChecker({
cfg,
workspaceDir:
options.workspaceDir ??
(agentId ? resolveAgentWorkspaceDir(cfg, agentId) : undefined) ??
resolveDefaultAgentWorkspaceDir(),
agentId,
});
for (const entry of catalog) {
if (usesUnfilteredCatalogModels(entry.provider) && (await hasAuth(entry.provider))) {
if (
usesUnfilteredCatalogModels(entry.provider, cliRuntimeProviders) &&
(await hasAuth(entry.provider))
) {
add(entry.provider, entry.id);
}
}
+52 -1
View File
@@ -19,7 +19,7 @@ vi.mock("../../plugins/provider-runtime.js", () => ({
normalizeProviderResolvedModelWithPlugin: mocks.normalizeProviderResolvedModelWithPlugin,
}));
import { appendProviderCatalogRows } from "./list.rows.js";
import { appendConfiguredProviderRows, appendProviderCatalogRows } from "./list.rows.js";
const authIndex = {
hasProviderAuth: (provider: string) => provider === "codex",
@@ -79,6 +79,7 @@ describe("appendProviderCatalogRows", () => {
models: { providers: {} },
},
});
expect(mocks.normalizeProviderResolvedModelWithPlugin).not.toHaveBeenCalled();
const row = requireOnlyRow(rows);
expect(row.key).toBe("codex/gpt-5.5");
expect(row.available).toBe(true);
@@ -189,3 +190,53 @@ describe("appendProviderCatalogRows", () => {
expect(row.tags).toEqual(["configured"]);
});
});
describe("appendConfiguredProviderRows", () => {
it("keeps provider normalization for configured provider models", async () => {
mocks.normalizeProviderResolvedModelWithPlugin.mockReturnValueOnce({
provider: "anthropic",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
input: ["text", "image"],
contextWindow: 200_000,
} as never);
const rows: ModelRow[] = [];
await appendConfiguredProviderRows({
rows,
seenKeys: new Set(),
context: {
cfg: {
models: {
providers: {
anthropic: {
api: "anthropic-messages",
baseUrl: "https://api.anthropic.com",
models: [
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 8192,
},
],
},
},
},
},
agentDir: "/tmp/openclaw-agent",
authIndex,
configuredByKey: new Map(),
discoveredKeys: new Set(),
filter: { provider: "anthropic", local: false },
skipRuntimeModelSuppression: true,
},
});
expect(mocks.normalizeProviderResolvedModelWithPlugin).toHaveBeenCalledOnce();
expect(requireOnlyRow(rows).input).toBe("text+image");
});
});
+11 -11
View File
@@ -145,6 +145,7 @@ function normalizeListRowWithProviderPlugin(params: {
provider: params.model.provider,
config: params.context.cfg,
workspaceDir: params.context.workspaceDir,
pluginMetadataSnapshot: params.context.metadataSnapshot,
context: {
config: params.context.cfg,
agentDir: params.context.agentDir,
@@ -177,6 +178,7 @@ async function appendVisibleRow(params: {
seenKeys?: Set<string>;
allowProviderAvailabilityFallback?: boolean;
skipSuppression?: boolean;
normalizeWithProviderPlugin?: boolean;
}): Promise<boolean> {
if (params.seenKeys?.has(params.key)) {
return false;
@@ -184,21 +186,18 @@ async function appendVisibleRow(params: {
if (!matchesRowFilter(params.context, params.model)) {
return false;
}
const normalizedModel = normalizeListRowWithProviderPlugin({
model: params.model,
context: params.context,
});
// Normalize provider-owned runtime model ids before suppression/filtering so
// list output matches the model ids users can actually select.
if (
!params.skipSuppression &&
shouldSuppressListModel({ model: normalizedModel, context: params.context })
) {
const model = params.normalizeWithProviderPlugin
? normalizeListRowWithProviderPlugin({
model: params.model,
context: params.context,
})
: params.model;
if (!params.skipSuppression && shouldSuppressListModel({ model, context: params.context })) {
return false;
}
params.rows.push(
await buildRow({
model: normalizedModel,
model,
key: params.key,
context: params.context,
allowProviderAvailabilityFallback: params.allowProviderAvailabilityFallback,
@@ -375,6 +374,7 @@ export async function appendConfiguredProviderRows(params: {
context: params.context,
seenKeys: params.seenKeys,
allowProviderAvailabilityFallback: !params.context.discoveredKeys.has(key),
normalizeWithProviderPlugin: true,
});
}
}
+2
View File
@@ -21,6 +21,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js";
import { resolvePluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js";
import { resolvePluginDiscoveryProvidersRuntime } from "./provider-discovery.runtime.js";
import {
clearProviderRuntimePluginCacheForTest,
@@ -327,6 +328,7 @@ export function normalizeProviderResolvedModelWithPlugin(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
pluginMetadataSnapshot?: PluginMetadataRegistryView;
context: {
config?: OpenClawConfig;
agentDir?: string;