diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index f5723c53b0ce..6253b2d2d4f7 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -111,13 +111,31 @@ interface OllamaTagsResponse { models: OllamaModel[]; } -async function discoverOllamaModels(): Promise { +/** + * Derive the Ollama native API base URL from a configured base URL. + * + * Users typically configure `baseUrl` with a `/v1` suffix (e.g. + * `http://192.168.20.14:11434/v1`) for the OpenAI-compatible endpoint. + * The native Ollama API lives at the root (e.g. `/api/tags`), so we + * strip the `/v1` suffix when present. + */ +function resolveOllamaApiBase(configuredBaseUrl?: string): string { + if (!configuredBaseUrl) { + return OLLAMA_API_BASE_URL; + } + // Strip trailing slash, then strip /v1 suffix if present + const trimmed = configuredBaseUrl.replace(/\/+$/, ""); + return trimmed.replace(/\/v1$/i, ""); +} + +async function discoverOllamaModels(baseUrl?: string): Promise { // Skip Ollama discovery in test environments if (process.env.VITEST || process.env.NODE_ENV === "test") { return []; } try { - const response = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, { + const apiBase = resolveOllamaApiBase(baseUrl); + const response = await fetch(`${apiBase}/api/tags`, { signal: AbortSignal.timeout(5000), }); if (!response.ok) { @@ -410,10 +428,10 @@ async function buildVeniceProvider(): Promise { }; } -async function buildOllamaProvider(): Promise { - const models = await discoverOllamaModels(); +async function buildOllamaProvider(configuredBaseUrl?: string): Promise { + const models = await discoverOllamaModels(configuredBaseUrl); return { - baseUrl: OLLAMA_BASE_URL, + baseUrl: configuredBaseUrl ?? OLLAMA_BASE_URL, api: "openai-completions", models, }; @@ -456,6 +474,7 @@ export function buildQianfanProvider(): ProviderConfig { export async function resolveImplicitProviders(params: { agentDir: string; + explicitProviders?: Record | null; }): Promise { const providers: Record = {}; const authStore = ensureAuthProfileStore(params.agentDir, { @@ -541,12 +560,15 @@ export async function resolveImplicitProviders(params: { break; } - // Ollama provider - only add if explicitly configured + // Ollama provider - only add if explicitly configured. + // Use the user's configured baseUrl (from explicit providers) for model + // discovery so that remote / non-default Ollama instances are reachable. const ollamaKey = resolveEnvApiKeyVarName("ollama") ?? resolveApiKeyFromProfiles({ provider: "ollama", store: authStore }); if (ollamaKey) { - providers.ollama = { ...(await buildOllamaProvider()), apiKey: ollamaKey }; + const ollamaBaseUrl = params.explicitProviders?.ollama?.baseUrl; + providers.ollama = { ...(await buildOllamaProvider(ollamaBaseUrl)), apiKey: ollamaKey }; } const togetherKey = diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 6664905ff4bf..b44c0d60b604 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -86,7 +86,7 @@ export async function ensureOpenClawModelsJson( const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveOpenClawAgentDir(); const explicitProviders = cfg.models?.providers ?? {}; - const implicitProviders = await resolveImplicitProviders({ agentDir }); + const implicitProviders = await resolveImplicitProviders({ agentDir, explicitProviders }); const providers: Record = mergeProviders({ implicit: implicitProviders, explicit: explicitProviders,