Skip to content

Commit 5224992

Browse files
committed
fix(ollama): skip localhost discovery for remote providers
1 parent b94ad7c commit 5224992

4 files changed

Lines changed: 117 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
4848
- Providers/Ollama: strip the active custom Ollama provider prefix before native chat and embedding requests, so custom provider ids like `ollama-spark/qwen3:32b` reach Ollama as the real model name. Fixes #72353. Thanks @maximus-dss and @hclsys.
4949
- Providers/Ollama: parse stringified native tool-call arguments before dispatch, preserving unsafe integer values so Ollama tool use receives structured parameters. Fixes #69735; supersedes #69910. Thanks @rongshuzhao and @yfge.
5050
- Providers/Ollama: skip ambient localhost discovery unless Ollama auth or meaningful config opts in, preventing unexpected probes to `127.0.0.1:11434` for users who are not using Ollama. Fixes #56939; supersedes #57116. Thanks @IanxDev and @tsukhani.
51+
- Providers/Ollama: skip implicit localhost discovery when a custom remote `api: "ollama"` provider is configured, while still treating `127/8` loopback hosts as local. Carries forward #43224. Thanks @issacthekaylon.
5152
- Providers/Ollama: move memory embeddings to Ollama's current `/api/embed` endpoint with batched `input` requests while preserving vector normalization and custom provider auth/header overrides. Fixes #39983. Thanks @sskkcc and @LiudengZhang.
5253
- Providers/Ollama: route local web search through Ollama's signed `/api/experimental/web_search` daemon proxy, use hosted `/api/web_search` directly for `ollama.com`, and keep `OLLAMA_API_KEY` scoped to cloud fallback auth. Fixes #69132. Thanks @yoon1012 and @hyspacex.
5354
- Providers/Ollama: accept OpenAI SDK-style `baseURL` as an alias for `baseUrl` across discovery, streaming, setup pulls, embeddings, and web search so remote Ollama hosts are not silently ignored. Fixes #62533; supersedes #62549. Thanks @Julien-BKK and @Linux2010.

docs/providers/ollama.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ Choose your preferred setup method and mode.
174174

175175
## Model discovery (implicit provider)
176176

177-
When you set `OLLAMA_API_KEY` (or an auth profile) and **do not** define `models.providers.ollama`, OpenClaw discovers models from the local Ollama instance at `http://127.0.0.1:11434`.
177+
When you set `OLLAMA_API_KEY` (or an auth profile) and **do not** define `models.providers.ollama` or another custom remote provider with `api: "ollama"`, OpenClaw discovers models from the local Ollama instance at `http://127.0.0.1:11434`.
178178

179179
| Behavior | Detail |
180180
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -202,7 +202,7 @@ ollama pull mistral
202202
The new model will be automatically discovered and available to use.
203203

204204
<Note>
205-
If you set `models.providers.ollama` explicitly, auto-discovery is skipped and you must define models manually. See the explicit config section below.
205+
If you set `models.providers.ollama` explicitly, or configure a custom remote provider such as `models.providers.ollama-cloud` with `api: "ollama"`, auto-discovery is skipped and you must define models manually. Loopback custom providers such as `http://127.0.0.2:11434` are still treated as local. See the explicit config section below.
206206
</Note>
207207

208208
## Vision and image description

extensions/ollama/index.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,64 @@ describe("ollama plugin", () => {
369369
});
370370
});
371371

372+
it("skips implicit localhost discovery when a custom remote Ollama provider is configured", async () => {
373+
const provider = registerProvider();
374+
375+
const result = await provider.discovery.run({
376+
config: {
377+
models: {
378+
providers: {
379+
"ollama-cloud": {
380+
api: "ollama",
381+
baseUrl: "https://ollama.com",
382+
models: [{ id: "kimi-k2.5", name: "Kimi K2.5" }],
383+
},
384+
},
385+
},
386+
},
387+
env: { NODE_ENV: "development", OLLAMA_API_KEY: "ollama-live" },
388+
resolveProviderApiKey: () => ({ apiKey: "ollama-live" }),
389+
} as never);
390+
391+
expect(result).toBeNull();
392+
expect(buildOllamaProviderMock).not.toHaveBeenCalled();
393+
});
394+
395+
it("treats custom 127/8 Ollama providers as loopback for implicit discovery", async () => {
396+
const provider = registerProvider();
397+
buildOllamaProviderMock.mockResolvedValueOnce({
398+
baseUrl: "http://127.0.0.1:11434",
399+
api: "ollama",
400+
models: [],
401+
});
402+
403+
const result = await provider.discovery.run({
404+
config: {
405+
models: {
406+
providers: {
407+
"ollama-alt-local": {
408+
api: "ollama",
409+
baseUrl: "http://127.0.0.2:11434",
410+
models: [{ id: "llama3.2", name: "Llama 3.2" }],
411+
},
412+
},
413+
},
414+
},
415+
env: { NODE_ENV: "development", OLLAMA_API_KEY: "ollama-live" },
416+
resolveProviderApiKey: () => ({ apiKey: "ollama-live" }),
417+
} as never);
418+
419+
expect(result).toMatchObject({
420+
provider: {
421+
baseUrl: "http://127.0.0.1:11434",
422+
api: "ollama",
423+
},
424+
});
425+
expect(buildOllamaProviderMock).toHaveBeenCalledWith(undefined, {
426+
quiet: false,
427+
});
428+
});
429+
372430
it("does not mint synthetic auth for empty default-ish provider stubs", () => {
373431
const provider = registerProvider();
374432

extensions/ollama/src/discovery-shared.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ export type OllamaPluginConfig = {
1515
type OllamaDiscoveryContext = {
1616
config: {
1717
models?: {
18-
providers?: {
19-
ollama?: ModelProviderConfig;
20-
};
18+
providers?: Record<string, ModelProviderConfig | undefined>;
2119
ollamaDiscovery?: {
2220
enabled?: boolean;
2321
};
@@ -73,6 +71,17 @@ function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean {
7371

7472
const LOCAL_OLLAMA_HOSTNAMES = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "::"]);
7573

74+
function isIpv4Loopback(host: string): boolean {
75+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
76+
return false;
77+
}
78+
const octets = host.split(".").map((part) => Number.parseInt(part, 10));
79+
if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
80+
return false;
81+
}
82+
return octets[0] === 127;
83+
}
84+
7685
function isIpv4PrivateRange(host: string): boolean {
7786
if (!/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
7887
return false;
@@ -113,6 +122,44 @@ export function isLocalOllamaBaseUrl(baseUrl: string | undefined | null): boolea
113122
);
114123
}
115124

125+
function isLoopbackOllamaBaseUrl(baseUrl: string | undefined | null): boolean {
126+
if (!baseUrl) {
127+
return true;
128+
}
129+
let parsed: URL;
130+
try {
131+
parsed = new URL(baseUrl);
132+
} catch {
133+
return false;
134+
}
135+
let host = parsed.hostname.toLowerCase();
136+
if (host.startsWith("[") && host.endsWith("]")) {
137+
host = host.slice(1, -1);
138+
}
139+
return LOCAL_OLLAMA_HOSTNAMES.has(host) || isIpv4Loopback(host);
140+
}
141+
142+
function hasExplicitRemoteOllamaApiProvider(
143+
providers: Record<string, ModelProviderConfig | undefined> | undefined,
144+
): boolean {
145+
if (!providers) {
146+
return false;
147+
}
148+
for (const [providerId, provider] of Object.entries(providers)) {
149+
if (providerId === OLLAMA_PROVIDER_ID || !provider) {
150+
continue;
151+
}
152+
if (normalizeOptionalString(provider.api)?.toLowerCase() !== "ollama") {
153+
continue;
154+
}
155+
const baseUrl = readProviderBaseUrl(provider);
156+
if (baseUrl && !isLoopbackOllamaBaseUrl(baseUrl)) {
157+
return true;
158+
}
159+
}
160+
return false;
161+
}
162+
116163
export function shouldUseSyntheticOllamaAuth(
117164
providerConfig: ModelProviderConfig | undefined,
118165
): boolean {
@@ -171,6 +218,9 @@ export async function resolveOllamaDiscoveryResult(params: {
171218
const explicit = params.ctx.config.models?.providers?.ollama;
172219
const hasExplicitModels = Array.isArray(explicit?.models) && explicit.models.length > 0;
173220
const hasMeaningfulExplicitConfig = hasMeaningfulExplicitOllamaConfig(explicit);
221+
const hasRemoteOllamaApiProvider = hasExplicitRemoteOllamaApiProvider(
222+
params.ctx.config.models?.providers,
223+
);
174224
const discoveryEnabled =
175225
params.pluginConfig.discovery?.enabled ?? params.ctx.config.models?.ollamaDiscovery?.enabled;
176226
if (!hasExplicitModels && discoveryEnabled === false) {
@@ -202,6 +252,9 @@ export async function resolveOllamaDiscoveryResult(params: {
202252
},
203253
};
204254
}
255+
if (!hasMeaningfulExplicitConfig && hasRemoteOllamaApiProvider) {
256+
return null;
257+
}
205258
if (!hasOllamaDiscoveryOptIn && !hasMeaningfulExplicitConfig) {
206259
return null;
207260
}

0 commit comments

Comments
 (0)