Skip to content

Commit f0b5d78

Browse files
fix(ollama): preserve configured API during discovery (#93729)
* fix(ollama): preserve configured API during discovery * fix(ollama): keep compatible discovery base URL * fix(ollama): route compatible APIs through configured transport --------- Co-authored-by: Vincent Koc <[email protected]>
1 parent f3982d6 commit f0b5d78

4 files changed

Lines changed: 188 additions & 15 deletions

File tree

extensions/ollama/index.test.ts

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,68 @@ describe("ollama plugin", () => {
467467
}
468468
});
469469

470+
it("preserves explicit api for configured dynamic Ollama models", async () => {
471+
const provider = registerProvider();
472+
const previous = process.env.OLLAMA_API_KEY;
473+
process.env.OLLAMA_API_KEY = "ollama-live";
474+
buildOllamaProviderMock.mockResolvedValueOnce({
475+
baseUrl: "https://ollama.example.com",
476+
api: "ollama",
477+
models: [
478+
{
479+
id: "qwen3-coder:cloud",
480+
name: "qwen3-coder:cloud",
481+
reasoning: false,
482+
input: ["text"],
483+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
484+
contextWindow: 8192,
485+
maxTokens: 2048,
486+
},
487+
],
488+
});
489+
490+
try {
491+
const config = {
492+
models: {
493+
providers: {
494+
ollama: {
495+
baseUrl: "https://ollama.example.com/v1",
496+
api: "openai-completions",
497+
models: [],
498+
},
499+
},
500+
},
501+
};
502+
503+
await provider.prepareDynamicModel?.({
504+
config,
505+
provider: "ollama",
506+
modelId: "qwen3-coder:cloud",
507+
modelRegistry: { find: vi.fn(() => null) },
508+
} as never);
509+
510+
const resolved = provider.resolveDynamicModel?.({
511+
config,
512+
provider: "ollama",
513+
modelId: "qwen3-coder:cloud",
514+
modelRegistry: { find: vi.fn(() => null) },
515+
} as never);
516+
expect(resolved?.provider).toBe("ollama");
517+
expect(resolved?.id).toBe("qwen3-coder:cloud");
518+
expect(resolved?.api).toBe("openai-completions");
519+
expect(resolved?.baseUrl).toBe("https://ollama.example.com/v1");
520+
expect(buildOllamaProviderMock).toHaveBeenCalledWith("https://ollama.example.com/v1", {
521+
quiet: true,
522+
});
523+
} finally {
524+
if (previous === undefined) {
525+
delete process.env.OLLAMA_API_KEY;
526+
} else {
527+
process.env.OLLAMA_API_KEY = previous;
528+
}
529+
}
530+
});
531+
470532
it("resolves requested Ollama cloud models that are omitted from tags but confirmed by show", async () => {
471533
const provider = registerProvider();
472534
const previous = process.env.OLLAMA_API_KEY;
@@ -1335,7 +1397,7 @@ describe("ollama plugin", () => {
13351397

13361398
provider.createStreamFn?.({
13371399
config: {},
1338-
model: { id: "kimi-k2.5:cloud" },
1400+
model: { api: "ollama", id: "kimi-k2.5:cloud" },
13391401
provider: "ollama-cloud",
13401402
} as never);
13411403
expect(requireConfiguredStreamParams().providerBaseUrl).toBe("https://ollama.com");
@@ -1434,6 +1496,55 @@ describe("ollama plugin", () => {
14341496
expect(nativePolicy?.validateAnthropicTurns).toBe(true);
14351497
});
14361498

1499+
it.each([
1500+
{
1501+
providerId: "ollama",
1502+
register: registerProvider,
1503+
nativeBaseUrl: "http://127.0.0.1:11434",
1504+
},
1505+
{
1506+
providerId: "ollama-cloud",
1507+
register: registerOllamaCloudProvider,
1508+
nativeBaseUrl: "https://ollama.com",
1509+
},
1510+
])(
1511+
"$providerId selects native /api/chat transport only for api=ollama",
1512+
({ providerId, register, nativeBaseUrl }) => {
1513+
const provider = register();
1514+
const createStream = (api: "ollama" | "openai-completions", baseUrl: string) =>
1515+
provider.createStreamFn?.({
1516+
config: {
1517+
models: {
1518+
providers: {
1519+
[providerId]: {
1520+
api,
1521+
baseUrl,
1522+
models: [],
1523+
},
1524+
},
1525+
},
1526+
},
1527+
model: {
1528+
api,
1529+
id: "qwen3:32b",
1530+
provider: providerId,
1531+
},
1532+
provider: providerId,
1533+
} as never);
1534+
1535+
const compatibleStream = createStream("openai-completions", `${nativeBaseUrl}/v1`);
1536+
1537+
expect(compatibleStream).toBeUndefined();
1538+
expect(createConfiguredOllamaStreamFnMock).not.toHaveBeenCalled();
1539+
1540+
const nativeStream = createStream("ollama", nativeBaseUrl);
1541+
1542+
expect(nativeStream).toBeDefined();
1543+
expect(createConfiguredOllamaStreamFnMock).toHaveBeenCalledOnce();
1544+
expect(requireConfiguredStreamParams().providerBaseUrl).toBe(nativeBaseUrl);
1545+
},
1546+
);
1547+
14371548
it("routes createStreamFn to the correct provider baseUrl for ollama2", () => {
14381549
const provider = registerProvider();
14391550
const config = {
@@ -1452,7 +1563,7 @@ describe("ollama plugin", () => {
14521563
},
14531564
},
14541565
};
1455-
const model = { id: "llama3.2", provider: "ollama2", baseUrl: undefined };
1566+
const model = { id: "llama3.2", provider: "ollama2", api: "ollama", baseUrl: undefined };
14561567

14571568
provider.createStreamFn?.({ config, model, provider: "ollama2" } as never);
14581569

@@ -1472,7 +1583,7 @@ describe("ollama plugin", () => {
14721583
},
14731584
},
14741585
};
1475-
const model = { id: "llama3.2", provider: "ollama2", baseUrl: undefined };
1586+
const model = { id: "llama3.2", provider: "ollama2", api: "ollama", baseUrl: undefined };
14761587

14771588
provider.createStreamFn?.({ config, model, provider: "ollama2" } as never);
14781589

@@ -1497,7 +1608,7 @@ describe("ollama plugin", () => {
14971608
},
14981609
},
14991610
};
1500-
const model = { id: "llama3.2", provider: "ollama", baseUrl: undefined };
1611+
const model = { id: "llama3.2", provider: "ollama", api: "ollama", baseUrl: undefined };
15011612

15021613
provider.createStreamFn?.({ config, model, provider: "ollama" } as never);
15031614

extensions/ollama/index.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
OLLAMA_PROVIDER_ID,
4848
isLocalOllamaBaseUrl,
4949
resolveOllamaDiscoveryResult,
50+
resolveOllamaRuntimeBaseUrl,
5051
shouldUseSyntheticOllamaAuth,
5152
type OllamaPluginConfig,
5253
} from "./src/discovery-shared.js";
@@ -104,7 +105,7 @@ function toDynamicOllamaModel(params: {
104105
id: params.model.id,
105106
name: params.model.name ?? params.model.id,
106107
provider: params.provider,
107-
api: "ollama",
108+
api: params.providerConfig.api ?? "ollama",
108109
baseUrl: readProviderBaseUrl(params.providerConfig) ?? "",
109110
reasoning: params.model.reasoning ?? false,
110111
input: input.length > 0 ? input : ["text"],
@@ -475,6 +476,9 @@ export default definePluginEntry({
475476
}),
476477
},
477478
createStreamFn: ({ config, model, provider }) => {
479+
if (model.api !== "ollama") {
480+
return undefined;
481+
}
478482
return createConfiguredOllamaStreamFn({
479483
model,
480484
providerBaseUrl:
@@ -597,6 +601,9 @@ export default definePluginEntry({
597601
await ensureOllamaModelPulled({ config, model, prompter });
598602
},
599603
createStreamFn: ({ config, model, provider }) => {
604+
if (model.api !== "ollama") {
605+
return undefined;
606+
}
600607
return createConfiguredOllamaStreamFn({
601608
model,
602609
providerBaseUrl: readProviderBaseUrl(
@@ -658,17 +665,27 @@ export default definePluginEntry({
658665
}
659666
const baseUrl = readProviderBaseUrl(providerConfig);
660667
const provider = await buildOllamaProvider(baseUrl, { quiet: true });
661-
const dynamicModels = (provider.models ?? []).map((model) =>
668+
const dynamicApi = providerConfig?.api ?? provider.api;
669+
const dynamicProvider = {
670+
...provider,
671+
baseUrl: resolveOllamaRuntimeBaseUrl({
672+
api: dynamicApi,
673+
configuredBaseUrl: baseUrl,
674+
discoveredBaseUrl: provider.baseUrl,
675+
}),
676+
api: dynamicApi,
677+
};
678+
const dynamicModels = (dynamicProvider.models ?? []).map((model) =>
662679
toDynamicOllamaModel({
663680
provider: ctx.provider,
664-
providerConfig: provider,
681+
providerConfig: dynamicProvider,
665682
model,
666683
}),
667684
);
668685
if (!dynamicModels.some((model) => model.id === ctx.modelId)) {
669686
const requestedModel = await resolveRequestedDynamicOllamaModel({
670687
provider: ctx.provider,
671-
providerConfig: provider,
688+
providerConfig: dynamicProvider,
672689
modelId: ctx.modelId,
673690
});
674691
if (requestedModel) {

extensions/ollama/provider-discovery.test.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ describe("Ollama provider", () => {
150150
});
151151
});
152152

153-
it("should preserve explicit ollama baseUrl on implicit provider injection", async () => {
153+
it("should preserve explicit ollama baseUrl and api on implicit provider injection", async () => {
154154
const fetchMock = stubTagsFetch();
155155

156156
await withOllamaApiKey(async () => {
@@ -171,8 +171,33 @@ describe("Ollama provider", () => {
171171

172172
expect(countFetchCallUrls(fetchMock, "/api/tags")).toBe(1);
173173

174-
// Native API strips /v1 suffix via resolveOllamaApiBase()
174+
expect(provider?.baseUrl).toBe("http://192.168.20.14:11434/v1");
175+
expect(provider?.api).toBe("openai-completions");
176+
});
177+
});
178+
179+
it("should normalize explicit native ollama baseUrl on implicit provider injection", async () => {
180+
const fetchMock = stubTagsFetch();
181+
182+
await withOllamaApiKey(async () => {
183+
const provider = await runOllamaCatalog({
184+
config: {
185+
models: {
186+
providers: {
187+
ollama: {
188+
baseUrl: "http://192.168.20.14:11434/v1",
189+
api: "ollama",
190+
models: [],
191+
},
192+
},
193+
},
194+
},
195+
env: { OLLAMA_API_KEY: "test-key" },
196+
});
197+
198+
expect(countFetchCallUrls(fetchMock, "/api/tags")).toBe(1);
175199
expect(provider?.baseUrl).toBe("http://192.168.20.14:11434");
200+
expect(provider?.api).toBe("ollama");
176201
});
177202
});
178203

@@ -650,7 +675,7 @@ describe("Ollama provider", () => {
650675
});
651676

652677
expect(provider?.apiKey).toBe("config-ollama-key");
653-
expect(provider?.baseUrl).toBe("http://remote-ollama:11434");
678+
expect(provider?.baseUrl).toBe("http://remote-ollama:11434/v1");
654679
expect(provider?.api).toBe("openai-completions");
655680
expect(fetchMock).not.toHaveBeenCalled();
656681
});

extensions/ollama/src/discovery-shared.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ function isOllamaApiKeyMarker(value: string): boolean {
4242
return value === "OLLAMA_API_KEY" || value === OLLAMA_DEFAULT_API_KEY;
4343
}
4444

45+
export function resolveOllamaRuntimeBaseUrl(params: {
46+
api?: ModelProviderConfig["api"];
47+
configuredBaseUrl?: string;
48+
discoveredBaseUrl: string;
49+
}): string {
50+
if (params.configuredBaseUrl && params.api && params.api !== "ollama") {
51+
return params.configuredBaseUrl;
52+
}
53+
return params.discoveredBaseUrl;
54+
}
55+
4556
function resolveOllamaDiscoveryApiKey(params: {
4657
env: NodeJS.ProcessEnv;
4758
baseUrl?: string;
@@ -251,19 +262,21 @@ export async function resolveOllamaDiscoveryResult(params: {
251262
ollamaKey.trim() !== OLLAMA_DEFAULT_API_KEY;
252263
const explicitApiKey = readStringValue(explicit?.apiKey);
253264
if (hasExplicitModels && explicit) {
254-
const baseUrl = resolveOllamaApiBase(readProviderBaseUrl(explicit) ?? OLLAMA_DEFAULT_BASE_URL);
265+
const configuredBaseUrl = readProviderBaseUrl(explicit) ?? OLLAMA_DEFAULT_BASE_URL;
266+
const discoveredBaseUrl = resolveOllamaApiBase(configuredBaseUrl);
267+
const api = explicit.api ?? "ollama";
255268
const apiKey = resolveOllamaDiscoveryApiKey({
256269
env: params.ctx.env,
257-
baseUrl,
270+
baseUrl: discoveredBaseUrl,
258271
explicitApiKey,
259272
resolvedApiKey: ollamaKey,
260273
resolvedDiscoveryApiKey: ollamaDiscoveryKey,
261274
});
262275
return {
263276
provider: {
264277
...explicit,
265-
baseUrl,
266-
api: explicit.api ?? "ollama",
278+
baseUrl: resolveOllamaRuntimeBaseUrl({ api, configuredBaseUrl, discoveredBaseUrl }),
279+
api,
267280
...(apiKey ? { apiKey } : {}),
268281
},
269282
};
@@ -307,9 +320,16 @@ export async function resolveOllamaDiscoveryResult(params: {
307320
resolvedApiKey: ollamaKey,
308321
resolvedDiscoveryApiKey: ollamaDiscoveryKey,
309322
});
323+
const api = explicit?.api ?? provider.api;
310324
return {
311325
provider: {
312326
...provider,
327+
baseUrl: resolveOllamaRuntimeBaseUrl({
328+
api,
329+
configuredBaseUrl,
330+
discoveredBaseUrl: provider.baseUrl,
331+
}),
332+
api,
313333
...(apiKey ? { apiKey } : {}),
314334
},
315335
};

0 commit comments

Comments
 (0)