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 <25068+vincentkoc@users.noreply.github.com>
This commit is contained in:
zhang-guiping
2026-06-17 03:29:46 +08:00
committed by GitHub
parent f3982d6442
commit f0b5d78ff9
4 changed files with 188 additions and 15 deletions

View File

@@ -467,6 +467,68 @@ describe("ollama plugin", () => {
}
});
it("preserves explicit api for configured dynamic Ollama models", async () => {
const provider = registerProvider();
const previous = process.env.OLLAMA_API_KEY;
process.env.OLLAMA_API_KEY = "ollama-live";
buildOllamaProviderMock.mockResolvedValueOnce({
baseUrl: "https://ollama.example.com",
api: "ollama",
models: [
{
id: "qwen3-coder:cloud",
name: "qwen3-coder:cloud",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 8192,
maxTokens: 2048,
},
],
});
try {
const config = {
models: {
providers: {
ollama: {
baseUrl: "https://ollama.example.com/v1",
api: "openai-completions",
models: [],
},
},
},
};
await provider.prepareDynamicModel?.({
config,
provider: "ollama",
modelId: "qwen3-coder:cloud",
modelRegistry: { find: vi.fn(() => null) },
} as never);
const resolved = provider.resolveDynamicModel?.({
config,
provider: "ollama",
modelId: "qwen3-coder:cloud",
modelRegistry: { find: vi.fn(() => null) },
} as never);
expect(resolved?.provider).toBe("ollama");
expect(resolved?.id).toBe("qwen3-coder:cloud");
expect(resolved?.api).toBe("openai-completions");
expect(resolved?.baseUrl).toBe("https://ollama.example.com/v1");
expect(buildOllamaProviderMock).toHaveBeenCalledWith("https://ollama.example.com/v1", {
quiet: true,
});
} finally {
if (previous === undefined) {
delete process.env.OLLAMA_API_KEY;
} else {
process.env.OLLAMA_API_KEY = previous;
}
}
});
it("resolves requested Ollama cloud models that are omitted from tags but confirmed by show", async () => {
const provider = registerProvider();
const previous = process.env.OLLAMA_API_KEY;
@@ -1335,7 +1397,7 @@ describe("ollama plugin", () => {
provider.createStreamFn?.({
config: {},
model: { id: "kimi-k2.5:cloud" },
model: { api: "ollama", id: "kimi-k2.5:cloud" },
provider: "ollama-cloud",
} as never);
expect(requireConfiguredStreamParams().providerBaseUrl).toBe("https://ollama.com");
@@ -1434,6 +1496,55 @@ describe("ollama plugin", () => {
expect(nativePolicy?.validateAnthropicTurns).toBe(true);
});
it.each([
{
providerId: "ollama",
register: registerProvider,
nativeBaseUrl: "http://127.0.0.1:11434",
},
{
providerId: "ollama-cloud",
register: registerOllamaCloudProvider,
nativeBaseUrl: "https://ollama.com",
},
])(
"$providerId selects native /api/chat transport only for api=ollama",
({ providerId, register, nativeBaseUrl }) => {
const provider = register();
const createStream = (api: "ollama" | "openai-completions", baseUrl: string) =>
provider.createStreamFn?.({
config: {
models: {
providers: {
[providerId]: {
api,
baseUrl,
models: [],
},
},
},
},
model: {
api,
id: "qwen3:32b",
provider: providerId,
},
provider: providerId,
} as never);
const compatibleStream = createStream("openai-completions", `${nativeBaseUrl}/v1`);
expect(compatibleStream).toBeUndefined();
expect(createConfiguredOllamaStreamFnMock).not.toHaveBeenCalled();
const nativeStream = createStream("ollama", nativeBaseUrl);
expect(nativeStream).toBeDefined();
expect(createConfiguredOllamaStreamFnMock).toHaveBeenCalledOnce();
expect(requireConfiguredStreamParams().providerBaseUrl).toBe(nativeBaseUrl);
},
);
it("routes createStreamFn to the correct provider baseUrl for ollama2", () => {
const provider = registerProvider();
const config = {
@@ -1452,7 +1563,7 @@ describe("ollama plugin", () => {
},
},
};
const model = { id: "llama3.2", provider: "ollama2", baseUrl: undefined };
const model = { id: "llama3.2", provider: "ollama2", api: "ollama", baseUrl: undefined };
provider.createStreamFn?.({ config, model, provider: "ollama2" } as never);
@@ -1472,7 +1583,7 @@ describe("ollama plugin", () => {
},
},
};
const model = { id: "llama3.2", provider: "ollama2", baseUrl: undefined };
const model = { id: "llama3.2", provider: "ollama2", api: "ollama", baseUrl: undefined };
provider.createStreamFn?.({ config, model, provider: "ollama2" } as never);
@@ -1497,7 +1608,7 @@ describe("ollama plugin", () => {
},
},
};
const model = { id: "llama3.2", provider: "ollama", baseUrl: undefined };
const model = { id: "llama3.2", provider: "ollama", api: "ollama", baseUrl: undefined };
provider.createStreamFn?.({ config, model, provider: "ollama" } as never);

View File

@@ -47,6 +47,7 @@ import {
OLLAMA_PROVIDER_ID,
isLocalOllamaBaseUrl,
resolveOllamaDiscoveryResult,
resolveOllamaRuntimeBaseUrl,
shouldUseSyntheticOllamaAuth,
type OllamaPluginConfig,
} from "./src/discovery-shared.js";
@@ -104,7 +105,7 @@ function toDynamicOllamaModel(params: {
id: params.model.id,
name: params.model.name ?? params.model.id,
provider: params.provider,
api: "ollama",
api: params.providerConfig.api ?? "ollama",
baseUrl: readProviderBaseUrl(params.providerConfig) ?? "",
reasoning: params.model.reasoning ?? false,
input: input.length > 0 ? input : ["text"],
@@ -475,6 +476,9 @@ export default definePluginEntry({
}),
},
createStreamFn: ({ config, model, provider }) => {
if (model.api !== "ollama") {
return undefined;
}
return createConfiguredOllamaStreamFn({
model,
providerBaseUrl:
@@ -597,6 +601,9 @@ export default definePluginEntry({
await ensureOllamaModelPulled({ config, model, prompter });
},
createStreamFn: ({ config, model, provider }) => {
if (model.api !== "ollama") {
return undefined;
}
return createConfiguredOllamaStreamFn({
model,
providerBaseUrl: readProviderBaseUrl(
@@ -658,17 +665,27 @@ export default definePluginEntry({
}
const baseUrl = readProviderBaseUrl(providerConfig);
const provider = await buildOllamaProvider(baseUrl, { quiet: true });
const dynamicModels = (provider.models ?? []).map((model) =>
const dynamicApi = providerConfig?.api ?? provider.api;
const dynamicProvider = {
...provider,
baseUrl: resolveOllamaRuntimeBaseUrl({
api: dynamicApi,
configuredBaseUrl: baseUrl,
discoveredBaseUrl: provider.baseUrl,
}),
api: dynamicApi,
};
const dynamicModels = (dynamicProvider.models ?? []).map((model) =>
toDynamicOllamaModel({
provider: ctx.provider,
providerConfig: provider,
providerConfig: dynamicProvider,
model,
}),
);
if (!dynamicModels.some((model) => model.id === ctx.modelId)) {
const requestedModel = await resolveRequestedDynamicOllamaModel({
provider: ctx.provider,
providerConfig: provider,
providerConfig: dynamicProvider,
modelId: ctx.modelId,
});
if (requestedModel) {

View File

@@ -150,7 +150,7 @@ describe("Ollama provider", () => {
});
});
it("should preserve explicit ollama baseUrl on implicit provider injection", async () => {
it("should preserve explicit ollama baseUrl and api on implicit provider injection", async () => {
const fetchMock = stubTagsFetch();
await withOllamaApiKey(async () => {
@@ -171,8 +171,33 @@ describe("Ollama provider", () => {
expect(countFetchCallUrls(fetchMock, "/api/tags")).toBe(1);
// Native API strips /v1 suffix via resolveOllamaApiBase()
expect(provider?.baseUrl).toBe("http://192.168.20.14:11434/v1");
expect(provider?.api).toBe("openai-completions");
});
});
it("should normalize explicit native ollama baseUrl on implicit provider injection", async () => {
const fetchMock = stubTagsFetch();
await withOllamaApiKey(async () => {
const provider = await runOllamaCatalog({
config: {
models: {
providers: {
ollama: {
baseUrl: "http://192.168.20.14:11434/v1",
api: "ollama",
models: [],
},
},
},
},
env: { OLLAMA_API_KEY: "test-key" },
});
expect(countFetchCallUrls(fetchMock, "/api/tags")).toBe(1);
expect(provider?.baseUrl).toBe("http://192.168.20.14:11434");
expect(provider?.api).toBe("ollama");
});
});
@@ -650,7 +675,7 @@ describe("Ollama provider", () => {
});
expect(provider?.apiKey).toBe("config-ollama-key");
expect(provider?.baseUrl).toBe("http://remote-ollama:11434");
expect(provider?.baseUrl).toBe("http://remote-ollama:11434/v1");
expect(provider?.api).toBe("openai-completions");
expect(fetchMock).not.toHaveBeenCalled();
});

View File

@@ -42,6 +42,17 @@ function isOllamaApiKeyMarker(value: string): boolean {
return value === "OLLAMA_API_KEY" || value === OLLAMA_DEFAULT_API_KEY;
}
export function resolveOllamaRuntimeBaseUrl(params: {
api?: ModelProviderConfig["api"];
configuredBaseUrl?: string;
discoveredBaseUrl: string;
}): string {
if (params.configuredBaseUrl && params.api && params.api !== "ollama") {
return params.configuredBaseUrl;
}
return params.discoveredBaseUrl;
}
function resolveOllamaDiscoveryApiKey(params: {
env: NodeJS.ProcessEnv;
baseUrl?: string;
@@ -251,10 +262,12 @@ export async function resolveOllamaDiscoveryResult(params: {
ollamaKey.trim() !== OLLAMA_DEFAULT_API_KEY;
const explicitApiKey = readStringValue(explicit?.apiKey);
if (hasExplicitModels && explicit) {
const baseUrl = resolveOllamaApiBase(readProviderBaseUrl(explicit) ?? OLLAMA_DEFAULT_BASE_URL);
const configuredBaseUrl = readProviderBaseUrl(explicit) ?? OLLAMA_DEFAULT_BASE_URL;
const discoveredBaseUrl = resolveOllamaApiBase(configuredBaseUrl);
const api = explicit.api ?? "ollama";
const apiKey = resolveOllamaDiscoveryApiKey({
env: params.ctx.env,
baseUrl,
baseUrl: discoveredBaseUrl,
explicitApiKey,
resolvedApiKey: ollamaKey,
resolvedDiscoveryApiKey: ollamaDiscoveryKey,
@@ -262,8 +275,8 @@ export async function resolveOllamaDiscoveryResult(params: {
return {
provider: {
...explicit,
baseUrl,
api: explicit.api ?? "ollama",
baseUrl: resolveOllamaRuntimeBaseUrl({ api, configuredBaseUrl, discoveredBaseUrl }),
api,
...(apiKey ? { apiKey } : {}),
},
};
@@ -307,9 +320,16 @@ export async function resolveOllamaDiscoveryResult(params: {
resolvedApiKey: ollamaKey,
resolvedDiscoveryApiKey: ollamaDiscoveryKey,
});
const api = explicit?.api ?? provider.api;
return {
provider: {
...provider,
baseUrl: resolveOllamaRuntimeBaseUrl({
api,
configuredBaseUrl,
discoveredBaseUrl: provider.baseUrl,
}),
api,
...(apiKey ? { apiKey } : {}),
},
};