Skip to content

Commit 23fa618

Browse files
fix: The one-line picker change hides unauthenticated catalog rows in
1 parent 2a7d83b commit 23fa618

4 files changed

Lines changed: 220 additions & 12 deletions

File tree

src/agents/model-auth.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,31 @@ export function hasSyntheticLocalProviderAuthConfig(params: {
304304
return Boolean(providerConfig.baseUrl && isLocalBaseUrl(providerConfig.baseUrl));
305305
}
306306

307+
export function hasRuntimeAvailableProviderAuth(params: {
308+
provider: string;
309+
cfg?: OpenClawConfig;
310+
env?: NodeJS.ProcessEnv;
311+
}): boolean {
312+
const provider = normalizeProviderId(params.provider);
313+
const authOverride = resolveProviderAuthOverride(params.cfg, provider);
314+
if (authOverride === "aws-sdk") {
315+
return true;
316+
}
317+
if (resolveEnvApiKey(provider, params.env)) {
318+
return true;
319+
}
320+
if (resolveUsableCustomProviderApiKey({ cfg: params.cfg, provider, env: params.env })) {
321+
return true;
322+
}
323+
if (resolveSyntheticLocalProviderAuth({ cfg: params.cfg, provider })) {
324+
return true;
325+
}
326+
if (authOverride === undefined && provider === "amazon-bedrock") {
327+
return true;
328+
}
329+
return false;
330+
}
331+
307332
type SyntheticProviderAuthResolution = {
308333
auth?: ResolvedProviderAuth;
309334
blockedOnManagedSecretRef?: boolean;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { OpenClawConfig } from "../config/types.openclaw.js";
3+
import type { AuthProfileStore } from "./auth-profiles.js";
4+
import { hasAuthForModelProvider } from "./model-provider-auth.js";
5+
6+
const emptyStore: AuthProfileStore = {
7+
version: 1,
8+
profiles: {},
9+
};
10+
11+
function modelDefinition(id: string) {
12+
return {
13+
id,
14+
name: id,
15+
reasoning: false,
16+
input: ["text" as const],
17+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
18+
contextWindow: 128_000,
19+
maxTokens: 8192,
20+
};
21+
}
22+
23+
describe("model provider auth availability", () => {
24+
it("accepts implicit Bedrock AWS SDK auth without an API key", () => {
25+
expect(
26+
hasAuthForModelProvider({
27+
provider: "amazon-bedrock",
28+
cfg: {} as OpenClawConfig,
29+
env: {},
30+
store: emptyStore,
31+
}),
32+
).toBe(true);
33+
});
34+
35+
it("accepts local no-key custom providers", () => {
36+
const cfg = {
37+
models: {
38+
providers: {
39+
vllm: {
40+
api: "openai-completions",
41+
baseUrl: "http://127.0.0.1:8000/v1",
42+
models: [modelDefinition("meta-llama/Meta-Llama-3-8B-Instruct")],
43+
},
44+
},
45+
},
46+
} as OpenClawConfig;
47+
48+
expect(
49+
hasAuthForModelProvider({
50+
provider: "vllm",
51+
cfg,
52+
env: {},
53+
store: emptyStore,
54+
}),
55+
).toBe(true);
56+
});
57+
58+
it("keeps remote no-key custom providers unavailable", () => {
59+
const cfg = {
60+
models: {
61+
providers: {
62+
remote: {
63+
api: "openai-completions",
64+
baseUrl: "https://remote.example.com/v1",
65+
models: [modelDefinition("remote-model")],
66+
},
67+
},
68+
},
69+
} as OpenClawConfig;
70+
71+
expect(
72+
hasAuthForModelProvider({
73+
provider: "remote",
74+
cfg,
75+
env: {},
76+
store: emptyStore,
77+
}),
78+
).toBe(false);
79+
});
80+
});

src/agents/model-provider-auth.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
listProfilesForProvider,
55
type AuthProfileStore,
66
} from "./auth-profiles.js";
7-
import { hasUsableCustomProviderApiKey, resolveEnvApiKey } from "./model-auth.js";
7+
import { hasRuntimeAvailableProviderAuth } from "./model-auth.js";
88
import { normalizeProviderId } from "./model-selection.js";
99

1010
export function hasAuthForModelProvider(params: {
@@ -15,6 +15,15 @@ export function hasAuthForModelProvider(params: {
1515
store?: AuthProfileStore;
1616
}): boolean {
1717
const provider = normalizeProviderId(params.provider);
18+
if (
19+
hasRuntimeAvailableProviderAuth({
20+
provider,
21+
cfg: params.cfg,
22+
env: params.env,
23+
})
24+
) {
25+
return true;
26+
}
1827
const store =
1928
params.store ??
2029
ensureAuthProfileStore(params.agentDir, {
@@ -23,12 +32,6 @@ export function hasAuthForModelProvider(params: {
2332
if (listProfilesForProvider(store, provider).length > 0) {
2433
return true;
2534
}
26-
if (resolveEnvApiKey(provider, params.env)?.apiKey) {
27-
return true;
28-
}
29-
if (hasUsableCustomProviderApiKey(params.cfg, provider, params.env)) {
30-
return true;
31-
}
3235
return false;
3336
}
3437

src/commands/model-picker.test.ts

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,53 @@ vi.mock("../agents/auth-profiles.js", () => ({
3636
}));
3737

3838
const resolveEnvApiKey = vi.hoisted(() =>
39-
vi.fn<(_provider: string) => { apiKey: string; source: string } | null>((_provider: string) => ({
40-
apiKey: "test-key",
41-
source: "test",
42-
})),
39+
vi.fn<(_provider: string, _env?: NodeJS.ProcessEnv) => { apiKey: string; source: string } | null>(
40+
(_provider: string) => ({
41+
apiKey: "test-key",
42+
source: "test",
43+
}),
44+
),
45+
);
46+
const hasUsableCustomProviderApiKey = vi.hoisted(() =>
47+
vi.fn<(_cfg?: OpenClawConfig, _provider?: string, _env?: NodeJS.ProcessEnv) => boolean>(
48+
() => false,
49+
),
50+
);
51+
const hasRuntimeAvailableProviderAuth = vi.hoisted(() =>
52+
vi.fn(
53+
({
54+
provider,
55+
cfg,
56+
env,
57+
}: {
58+
provider: string;
59+
cfg?: OpenClawConfig;
60+
env?: NodeJS.ProcessEnv;
61+
}) => {
62+
if (provider === "amazon-bedrock") {
63+
const auth = cfg?.models?.providers?.["amazon-bedrock"]?.auth;
64+
return auth === undefined || auth === "aws-sdk";
65+
}
66+
if (resolveEnvApiKey(provider, env)?.apiKey) {
67+
return true;
68+
}
69+
if (hasUsableCustomProviderApiKey(cfg, provider, env)) {
70+
return true;
71+
}
72+
const providerConfig = cfg?.models?.providers?.[provider];
73+
return Boolean(
74+
providerConfig?.baseUrl?.startsWith("http://127.0.0.1") &&
75+
providerConfig.api &&
76+
providerConfig.models?.length &&
77+
!providerConfig.apiKey,
78+
);
79+
},
80+
),
4381
);
44-
const hasUsableCustomProviderApiKey = vi.hoisted(() => vi.fn(() => false));
4582
vi.mock("../agents/model-auth.js", () => ({
4683
resolveEnvApiKey,
4784
hasUsableCustomProviderApiKey,
85+
hasRuntimeAvailableProviderAuth,
4886
}));
4987

5088
const resolveOwningPluginIdsForProvider = vi.hoisted(() =>
@@ -208,6 +246,30 @@ describe("promptDefaultModel", () => {
208246
expect(values).toEqual(["anthropic/claude-sonnet-4-6"]);
209247
});
210248

249+
it("keeps implicit Bedrock AWS SDK models visible without API-key auth", async () => {
250+
resolveEnvApiKey.mockReturnValue(null);
251+
loadModelCatalog.mockResolvedValue([
252+
{ provider: "amazon-bedrock", id: "us.anthropic.claude-sonnet-4-5", name: "Claude Sonnet" },
253+
{ provider: "openai", id: "gpt-5.5", name: "GPT-5.5" },
254+
]);
255+
256+
const select = vi.fn(async (params) => params.initialValue as never);
257+
const prompter = makePrompter({ select });
258+
259+
await promptDefaultModel({
260+
config: { agents: { defaults: {} } } as OpenClawConfig,
261+
prompter,
262+
allowKeep: false,
263+
includeManual: false,
264+
ignoreAllowlist: true,
265+
});
266+
267+
const values = (select.mock.calls[0]?.[0]?.options ?? []).map(
268+
(option: { value: string }) => option.value,
269+
);
270+
expect(values).toEqual(["amazon-bedrock/us.anthropic.claude-sonnet-4-5"]);
271+
});
272+
211273
it("hides legacy runtime providers from default model choices", async () => {
212274
loadModelCatalog.mockResolvedValue([
213275
{ provider: "codex", id: "gpt-5.5", name: "GPT-5.5" },
@@ -899,6 +961,44 @@ describe("promptModelAllowlist", () => {
899961
});
900962
});
901963

964+
it("keeps local no-key provider models visible in allowlist choices", async () => {
965+
resolveEnvApiKey.mockReturnValue(null);
966+
loadModelCatalog.mockResolvedValue([
967+
{
968+
provider: "vllm",
969+
id: "meta-llama/Meta-Llama-3-8B-Instruct",
970+
name: "Meta Llama",
971+
},
972+
{
973+
provider: "openai",
974+
id: "gpt-5.5",
975+
name: "GPT-5.5",
976+
},
977+
]);
978+
979+
const multiselect = createSelectAllMultiselect();
980+
const prompter = makePrompter({ multiselect });
981+
const config = {
982+
models: {
983+
providers: {
984+
vllm: {
985+
api: "openai-completions",
986+
baseUrl: "http://127.0.0.1:8000/v1",
987+
models: [configuredTextModel("meta-llama/Meta-Llama-3-8B-Instruct", "Meta Llama")],
988+
},
989+
},
990+
},
991+
agents: { defaults: {} },
992+
} as OpenClawConfig;
993+
994+
const result = await promptModelAllowlist({ config, prompter });
995+
996+
expect(
997+
multiselect.mock.calls[0]?.[0]?.options.map((option: { value: string }) => option.value),
998+
).toEqual(["vllm/meta-llama/Meta-Llama-3-8B-Instruct"]);
999+
expect(result.models).toEqual(["vllm/meta-llama/Meta-Llama-3-8B-Instruct"]);
1000+
});
1001+
9021002
it("seeds existing model fallbacks into unscoped allowlist selections", async () => {
9031003
loadModelCatalog.mockResolvedValue([
9041004
{

0 commit comments

Comments
 (0)