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>
This commit is contained in:
clawsweeper[bot]
2026-04-29 14:07:29 -07:00
committed by GitHub
parent e2ab5b98cc
commit 22ea08997e
4 changed files with 220 additions and 12 deletions

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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;
}

View File

@@ -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([
{