diff --git a/extensions/ollama/ollama.live.test.ts b/extensions/ollama/ollama.live.test.ts index a13e8c0f6613..d1d3b15d3610 100644 --- a/extensions/ollama/ollama.live.test.ts +++ b/extensions/ollama/ollama.live.test.ts @@ -258,7 +258,7 @@ describe.skipIf(!LIVE)("ollama live", () => { expect(events.map((event) => (event as { type?: string }).type)).toContain("done"); expect(payload?.model).toBe(CHAT_MODEL); expect(payload?.options?.num_ctx).toBe(4096); - expect(payload?.options?.top_p).toBe(0.9); + expect(payload?.options?.top_p).toBe(1); expect(payload?.think).toBe(false); expect(payload?.keep_alive).toBe("5m"); const properties = payload?.tools?.[0]?.function?.parameters?.properties; diff --git a/extensions/ollama/src/stream-runtime.test.ts b/extensions/ollama/src/stream-runtime.test.ts index 459c471ea80b..e6bbe537fa5a 100644 --- a/extensions/ollama/src/stream-runtime.test.ts +++ b/extensions/ollama/src/stream-runtime.test.ts @@ -2319,6 +2319,117 @@ describe("createOllamaStreamFn", () => { ); }); + it("sets top_p=1 for native Ollama greedy sampling requests", async () => { + await withMockNdjsonFetch( + [ + '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}', + '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}', + ], + async (fetchMock) => { + const stream = await createOllamaTestStream({ + baseUrl: "http://ollama-host:11434", + model: { + params: { + num_ctx: 4096, + top_p: 0.9, + thinking: false, + }, + }, + options: { temperature: 0 }, + }); + + const events = await collectStreamEvents(stream); + expect(events.at(-1)?.type).toBe("done"); + + const requestInit = getGuardedFetchCall(fetchMock).init ?? {}; + if (typeof requestInit.body !== "string") { + throw new Error("Expected string request body"); + } + const requestBody = JSON.parse(requestInit.body) as { + options: { + temperature?: number; + top_p?: number; + }; + }; + expect(requestBody.options.temperature).toBe(0); + expect(requestBody.options.top_p).toBe(1); + }, + ); + }); + + it("sets top_p=1 for native Ollama greedy requests without configured top_p", async () => { + await withMockNdjsonFetch( + [ + '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}', + '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}', + ], + async (fetchMock) => { + const stream = await createOllamaTestStream({ + baseUrl: "http://ollama-host:11434", + model: { + params: { + num_ctx: 4096, + thinking: false, + }, + }, + options: { temperature: 0 }, + }); + + const events = await collectStreamEvents(stream); + expect(events.at(-1)?.type).toBe("done"); + + const requestInit = getGuardedFetchCall(fetchMock).init ?? {}; + if (typeof requestInit.body !== "string") { + throw new Error("Expected string request body"); + } + const requestBody = JSON.parse(requestInit.body) as { + options: { + temperature?: number; + top_p?: number; + }; + }; + expect(requestBody.options.temperature).toBe(0); + expect(requestBody.options.top_p).toBe(1); + }, + ); + }); + + it("preserves configured top_p for native Ollama non-greedy sampling requests", async () => { + await withMockNdjsonFetch( + [ + '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}', + '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}', + ], + async (fetchMock) => { + const stream = await createOllamaTestStream({ + baseUrl: "http://ollama-host:11434", + model: { + params: { + top_p: 0.9, + }, + }, + options: { temperature: 0.2 }, + }); + + const events = await collectStreamEvents(stream); + expect(events.at(-1)?.type).toBe("done"); + + const requestInit = getGuardedFetchCall(fetchMock).init ?? {}; + if (typeof requestInit.body !== "string") { + throw new Error("Expected string request body"); + } + const requestBody = JSON.parse(requestInit.body) as { + options: { + temperature?: number; + top_p?: number; + }; + }; + expect(requestBody.options.temperature).toBe(0.2); + expect(requestBody.options.top_p).toBe(0.9); + }, + ); + }); + it("omits num_ctx when the model has no params.num_ctx and no catalog window", async () => { await withMockNdjsonFetch( [ diff --git a/extensions/ollama/src/stream.ts b/extensions/ollama/src/stream.ts index ca03e9bf207d..d52b4260e980 100644 --- a/extensions/ollama/src/stream.ts +++ b/extensions/ollama/src/stream.ts @@ -343,6 +343,18 @@ function resolveOllamaModelOptions(model: ProviderRuntimeModel): Record): void { + if (options.temperature !== 0) { + return; + } + if ( + options.top_p === undefined || + (typeof options.top_p === "number" && Number.isFinite(options.top_p) && options.top_p !== 1) + ) { + options.top_p = 1; + } +} + function resolveOllamaTopLevelParams( model: ProviderRuntimeModel, ): Record | undefined { @@ -1098,6 +1110,7 @@ export function createOllamaStreamFn( if (typeof options?.maxTokens === "number") { ollamaOptions.num_predict = options.maxTokens; } + normalizeOllamaGreedySamplingOptions(ollamaOptions); const body = buildOllamaChatRequest({ modelId: model.id,