From 22ea08997e5b470e1bea58905dffb5211c8a1fc1 Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:07:29 -0700 Subject: [PATCH] fix: The one-line picker change hides unauthenticated catalog rows in (#74530) Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com> --- src/agents/model-auth.ts | 25 ++++++ src/agents/model-provider-auth.test.ts | 80 ++++++++++++++++++ src/agents/model-provider-auth.ts | 17 ++-- src/commands/model-picker.test.ts | 110 +++++++++++++++++++++++-- 4 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 src/agents/model-provider-auth.test.ts diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index f39f4c36466..6817ff91e3f 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -304,6 +304,31 @@ export function hasSyntheticLocalProviderAuthConfig(params: { return Boolean(providerConfig.baseUrl && isLocalBaseUrl(providerConfig.baseUrl)); } +export function hasRuntimeAvailableProviderAuth(params: { + provider: string; + cfg?: OpenClawConfig; + env?: NodeJS.ProcessEnv; +}): boolean { + const provider = normalizeProviderId(params.provider); + const authOverride = resolveProviderAuthOverride(params.cfg, provider); + if (authOverride === "aws-sdk") { + return true; + } + if (resolveEnvApiKey(provider, params.env)) { + return true; + } + if (resolveUsableCustomProviderApiKey({ cfg: params.cfg, provider, env: params.env })) { + return true; + } + if (resolveSyntheticLocalProviderAuth({ cfg: params.cfg, provider })) { + return true; + } + if (authOverride === undefined && provider === "amazon-bedrock") { + return true; + } + return false; +} + type SyntheticProviderAuthResolution = { auth?: ResolvedProviderAuth; blockedOnManagedSecretRef?: boolean; diff --git a/src/agents/model-provider-auth.test.ts b/src/agents/model-provider-auth.test.ts new file mode 100644 index 00000000000..05c4aa05492 --- /dev/null +++ b/src/agents/model-provider-auth.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import type { AuthProfileStore } from "./auth-profiles.js"; +import { hasAuthForModelProvider } from "./model-provider-auth.js"; + +const emptyStore: AuthProfileStore = { + version: 1, + profiles: {}, +}; + +function modelDefinition(id: string) { + return { + id, + name: id, + reasoning: false, + input: ["text" as const], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 8192, + }; +} + +describe("model provider auth availability", () => { + it("accepts implicit Bedrock AWS SDK auth without an API key", () => { + expect( + hasAuthForModelProvider({ + provider: "amazon-bedrock", + cfg: {} as OpenClawConfig, + env: {}, + store: emptyStore, + }), + ).toBe(true); + }); + + it("accepts local no-key custom providers", () => { + const cfg = { + models: { + providers: { + vllm: { + api: "openai-completions", + baseUrl: "http://127.0.0.1:8000/v1", + models: [modelDefinition("meta-llama/Meta-Llama-3-8B-Instruct")], + }, + }, + }, + } as OpenClawConfig; + + expect( + hasAuthForModelProvider({ + provider: "vllm", + cfg, + env: {}, + store: emptyStore, + }), + ).toBe(true); + }); + + it("keeps remote no-key custom providers unavailable", () => { + const cfg = { + models: { + providers: { + remote: { + api: "openai-completions", + baseUrl: "https://remote.example.com/v1", + models: [modelDefinition("remote-model")], + }, + }, + }, + } as OpenClawConfig; + + expect( + hasAuthForModelProvider({ + provider: "remote", + cfg, + env: {}, + store: emptyStore, + }), + ).toBe(false); + }); +}); diff --git a/src/agents/model-provider-auth.ts b/src/agents/model-provider-auth.ts index 4b45924a121..c76a17d3163 100644 --- a/src/agents/model-provider-auth.ts +++ b/src/agents/model-provider-auth.ts @@ -4,7 +4,7 @@ import { listProfilesForProvider, type AuthProfileStore, } from "./auth-profiles.js"; -import { hasUsableCustomProviderApiKey, resolveEnvApiKey } from "./model-auth.js"; +import { hasRuntimeAvailableProviderAuth } from "./model-auth.js"; import { normalizeProviderId } from "./model-selection.js"; export function hasAuthForModelProvider(params: { @@ -15,6 +15,15 @@ export function hasAuthForModelProvider(params: { store?: AuthProfileStore; }): boolean { const provider = normalizeProviderId(params.provider); + if ( + hasRuntimeAvailableProviderAuth({ + provider, + cfg: params.cfg, + env: params.env, + }) + ) { + return true; + } const store = params.store ?? ensureAuthProfileStore(params.agentDir, { @@ -23,12 +32,6 @@ export function hasAuthForModelProvider(params: { if (listProfilesForProvider(store, provider).length > 0) { return true; } - if (resolveEnvApiKey(provider, params.env)?.apiKey) { - return true; - } - if (hasUsableCustomProviderApiKey(params.cfg, provider, params.env)) { - return true; - } return false; } diff --git a/src/commands/model-picker.test.ts b/src/commands/model-picker.test.ts index 30d594ad338..93ac81c3f96 100644 --- a/src/commands/model-picker.test.ts +++ b/src/commands/model-picker.test.ts @@ -36,15 +36,53 @@ vi.mock("../agents/auth-profiles.js", () => ({ })); const resolveEnvApiKey = vi.hoisted(() => - vi.fn<(_provider: string) => { apiKey: string; source: string } | null>((_provider: string) => ({ - apiKey: "test-key", - source: "test", - })), + vi.fn<(_provider: string, _env?: NodeJS.ProcessEnv) => { apiKey: string; source: string } | null>( + (_provider: string) => ({ + apiKey: "test-key", + source: "test", + }), + ), +); +const hasUsableCustomProviderApiKey = vi.hoisted(() => + vi.fn<(_cfg?: OpenClawConfig, _provider?: string, _env?: NodeJS.ProcessEnv) => boolean>( + () => false, + ), +); +const hasRuntimeAvailableProviderAuth = vi.hoisted(() => + vi.fn( + ({ + provider, + cfg, + env, + }: { + provider: string; + cfg?: OpenClawConfig; + env?: NodeJS.ProcessEnv; + }) => { + if (provider === "amazon-bedrock") { + const auth = cfg?.models?.providers?.["amazon-bedrock"]?.auth; + return auth === undefined || auth === "aws-sdk"; + } + if (resolveEnvApiKey(provider, env)?.apiKey) { + return true; + } + if (hasUsableCustomProviderApiKey(cfg, provider, env)) { + return true; + } + const providerConfig = cfg?.models?.providers?.[provider]; + return Boolean( + providerConfig?.baseUrl?.startsWith("http://127.0.0.1") && + providerConfig.api && + providerConfig.models?.length && + !providerConfig.apiKey, + ); + }, + ), ); -const hasUsableCustomProviderApiKey = vi.hoisted(() => vi.fn(() => false)); vi.mock("../agents/model-auth.js", () => ({ resolveEnvApiKey, hasUsableCustomProviderApiKey, + hasRuntimeAvailableProviderAuth, })); const resolveOwningPluginIdsForProvider = vi.hoisted(() => @@ -208,6 +246,30 @@ describe("promptDefaultModel", () => { expect(values).toEqual(["anthropic/claude-sonnet-4-6"]); }); + it("keeps implicit Bedrock AWS SDK models visible without API-key auth", async () => { + resolveEnvApiKey.mockReturnValue(null); + loadModelCatalog.mockResolvedValue([ + { provider: "amazon-bedrock", id: "us.anthropic.claude-sonnet-4-5", name: "Claude Sonnet" }, + { provider: "openai", id: "gpt-5.5", name: "GPT-5.5" }, + ]); + + const select = vi.fn(async (params) => params.initialValue as never); + const prompter = makePrompter({ select }); + + await promptDefaultModel({ + config: { agents: { defaults: {} } } as OpenClawConfig, + prompter, + allowKeep: false, + includeManual: false, + ignoreAllowlist: true, + }); + + const values = (select.mock.calls[0]?.[0]?.options ?? []).map( + (option: { value: string }) => option.value, + ); + expect(values).toEqual(["amazon-bedrock/us.anthropic.claude-sonnet-4-5"]); + }); + it("hides legacy runtime providers from default model choices", async () => { loadModelCatalog.mockResolvedValue([ { provider: "codex", id: "gpt-5.5", name: "GPT-5.5" }, @@ -899,6 +961,44 @@ describe("promptModelAllowlist", () => { }); }); + it("keeps local no-key provider models visible in allowlist choices", async () => { + resolveEnvApiKey.mockReturnValue(null); + loadModelCatalog.mockResolvedValue([ + { + provider: "vllm", + id: "meta-llama/Meta-Llama-3-8B-Instruct", + name: "Meta Llama", + }, + { + provider: "openai", + id: "gpt-5.5", + name: "GPT-5.5", + }, + ]); + + const multiselect = createSelectAllMultiselect(); + const prompter = makePrompter({ multiselect }); + const config = { + models: { + providers: { + vllm: { + api: "openai-completions", + baseUrl: "http://127.0.0.1:8000/v1", + models: [configuredTextModel("meta-llama/Meta-Llama-3-8B-Instruct", "Meta Llama")], + }, + }, + }, + agents: { defaults: {} }, + } as OpenClawConfig; + + const result = await promptModelAllowlist({ config, prompter }); + + expect( + multiselect.mock.calls[0]?.[0]?.options.map((option: { value: string }) => option.value), + ).toEqual(["vllm/meta-llama/Meta-Llama-3-8B-Instruct"]); + expect(result.models).toEqual(["vllm/meta-llama/Meta-Llama-3-8B-Instruct"]); + }); + it("seeds existing model fallbacks into unscoped allowlist selections", async () => { loadModelCatalog.mockResolvedValue([ {