diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 91885de58db..f39f4c36466 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -274,6 +274,36 @@ function isManagedSecretRefApiKeyMarker(apiKey: string | undefined): boolean { return apiKey?.trim() === NON_ENV_SECRETREF_MARKER; } +export function hasSyntheticLocalProviderAuthConfig(params: { + cfg: OpenClawConfig | undefined; + provider: string; +}): boolean { + const providerConfig = resolveProviderConfig(params.cfg, params.provider); + if (!providerConfig) { + return false; + } + + const hasApiConfig = + Boolean(providerConfig.api?.trim()) || + Boolean(providerConfig.baseUrl?.trim()) || + (Array.isArray(providerConfig.models) && providerConfig.models.length > 0); + if (!hasApiConfig) { + return false; + } + + const authOverride = resolveProviderAuthOverride(params.cfg, params.provider); + if (authOverride && authOverride !== "api-key") { + return false; + } + if (!isCustomLocalProviderConfig(providerConfig)) { + return false; + } + if (hasExplicitProviderApiKeyConfig(providerConfig)) { + return false; + } + return Boolean(providerConfig.baseUrl && isLocalBaseUrl(providerConfig.baseUrl)); +} + type SyntheticProviderAuthResolution = { auth?: ResolvedProviderAuth; blockedOnManagedSecretRef?: boolean; @@ -340,29 +370,10 @@ function resolveSyntheticLocalProviderAuth(params: { return null; } - const hasApiConfig = - Boolean(providerConfig.api?.trim()) || - Boolean(providerConfig.baseUrl?.trim()) || - (Array.isArray(providerConfig.models) && providerConfig.models.length > 0); - if (!hasApiConfig) { - return null; - } - - const authOverride = resolveProviderAuthOverride(params.cfg, params.provider); - if (authOverride && authOverride !== "api-key") { - return null; - } - if (!isCustomLocalProviderConfig(providerConfig)) { - return null; - } - if (hasExplicitProviderApiKeyConfig(providerConfig)) { - return null; - } - // Custom providers pointing at a local server (e.g. llama.cpp, vLLM, LocalAI) // typically don't require auth. Synthesize a local key so the auth resolver // doesn't reject them when the user left the API key blank during setup. - if (providerConfig.baseUrl && isLocalBaseUrl(providerConfig.baseUrl)) { + if (hasSyntheticLocalProviderAuthConfig(params)) { return { apiKey: CUSTOM_LOCAL_AUTH_MARKER, source: `models.providers.${params.provider} (synthetic local key)`, diff --git a/src/commands/models.list.e2e.test.ts b/src/commands/models.list.e2e.test.ts index a31db341b88..00136e04cd7 100644 --- a/src/commands/models.list.e2e.test.ts +++ b/src/commands/models.list.e2e.test.ts @@ -16,6 +16,7 @@ const listProfilesForProvider = vi.fn().mockReturnValue([]); const resolveEnvApiKey = vi.fn().mockReturnValue(undefined); const resolveAwsSdkEnvVarName = vi.fn().mockReturnValue(undefined); const hasUsableCustomProviderApiKey = vi.fn().mockReturnValue(false); +const hasSyntheticLocalProviderAuthConfig = vi.fn().mockReturnValue(false); const loadModelCatalog = vi.fn(async () => []); const loadProviderCatalogModelsForList = vi.fn<() => Promise>>>( async () => [], @@ -64,6 +65,7 @@ vi.mock("../agents/auth-profiles/store.js", () => ({ vi.mock("../agents/model-auth.js", () => ({ hasUsableCustomProviderApiKey, + hasSyntheticLocalProviderAuthConfig, resolveAwsSdkEnvVarName, resolveEnvApiKey, })); diff --git a/src/commands/models/list.auth-index.test.ts b/src/commands/models/list.auth-index.test.ts index 98227837592..797fe99591b 100644 --- a/src/commands/models/list.auth-index.test.ts +++ b/src/commands/models/list.auth-index.test.ts @@ -66,6 +66,26 @@ describe("createModelListAuthIndex", () => { expect(index.hasProviderAuth("custom-openai")).toBe(true); }); + it("records configured local custom provider markers", () => { + const index = createModelListAuthIndex({ + cfg: { + models: { + providers: { + "local-openai": { + api: "openai-completions", + baseUrl: "http://127.0.0.1:8080/v1", + models: [{ id: "local-model" }], + }, + }, + }, + }, + authStore: emptyStore, + env: {}, + }); + + expect(index.hasProviderAuth("local-openai")).toBe(true); + }); + it("uses injected synthetic auth refs without loading provider runtime", () => { const index = createModelListAuthIndex({ cfg: {}, diff --git a/src/commands/models/list.auth-index.ts b/src/commands/models/list.auth-index.ts index 26284a9563f..fab861a31a8 100644 --- a/src/commands/models/list.auth-index.ts +++ b/src/commands/models/list.auth-index.ts @@ -2,7 +2,10 @@ import type { AuthProfileStore } from "../../agents/auth-profiles/types.js"; import { resolveProviderEnvApiKeyCandidates } from "../../agents/model-auth-env-vars.js"; import { resolveEnvApiKey } from "../../agents/model-auth-env.js"; import { resolveAwsSdkEnvVarName } from "../../agents/model-auth-runtime-shared.js"; -import { hasUsableCustomProviderApiKey } from "../../agents/model-auth.js"; +import { + hasSyntheticLocalProviderAuthConfig, + hasUsableCustomProviderApiKey, +} from "../../agents/model-auth.js"; import { resolveProviderAuthAliasMap } from "../../agents/provider-auth-aliases.js"; import { normalizeProviderIdForAuth } from "../../agents/provider-id.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -65,7 +68,10 @@ export function createModelListAuthIndex( } for (const provider of Object.keys(params.cfg.models?.providers ?? {})) { - if (hasUsableCustomProviderApiKey(params.cfg, provider, env)) { + if ( + hasUsableCustomProviderApiKey(params.cfg, provider, env) || + hasSyntheticLocalProviderAuthConfig({ cfg: params.cfg, provider }) + ) { addProvider(provider); } } diff --git a/src/commands/models/list.list-command.forward-compat.test.ts b/src/commands/models/list.list-command.forward-compat.test.ts index 05717fa9d8b..27f320cbfeb 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -209,6 +209,7 @@ function installModelsListCommandForwardCompatMocks() { vi.doMock("../../agents/model-auth.js", () => ({ hasUsableCustomProviderApiKey: vi.fn().mockReturnValue(false), + hasSyntheticLocalProviderAuthConfig: vi.fn().mockReturnValue(false), })); vi.doMock("../../plugins/installed-plugin-index-store.js", () => ({