diff --git a/packages/ai/src/providers/ollama.ts b/packages/ai/src/providers/ollama.ts index fb9f72092..4234a62f4 100644 --- a/packages/ai/src/providers/ollama.ts +++ b/packages/ai/src/providers/ollama.ts @@ -199,7 +199,18 @@ function convertMessages(model: Model<"ollama-chat">, context: Context): OllamaM }); } messages.push(...context.messages); - return transformMessages(messages, model).map(convertMessage); + const isCloud = model.provider === "ollama-cloud"; + return transformMessages(messages, model).map(msg => { + const converted = convertMessage(msg); + // Ollama cloud rejects requests when assistant history messages contain the `thinking` + // field — it's valid in model responses but not accepted as a history input. Strip it + // to prevent HTTP 400 errors. Local Ollama instances are unaffected. + if (isCloud && converted.role === "assistant" && converted.thinking) { + const { thinking: _t, ...rest } = converted; + return rest; + } + return converted; + }); } function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefined { diff --git a/packages/ai/test/ollama-cloud-provider.test.ts b/packages/ai/test/ollama-cloud-provider.test.ts index 4c07d567e..cf5c6c383 100644 --- a/packages/ai/test/ollama-cloud-provider.test.ts +++ b/packages/ai/test/ollama-cloud-provider.test.ts @@ -415,6 +415,65 @@ describe("ollama-cloud provider support", () => { }); }); + test("strips `thinking` from assistant history messages on ollama-cloud", async () => { + let requestBody: Record | undefined; + global.fetch = vi.fn(async (_input, init) => { + requestBody = JSON.parse(String(init?.body ?? "{}")) as Record; + return createNdjsonResponse([ + { model: "gpt-oss:120b", message: { role: "assistant", content: "ok" }, done: false }, + { model: "gpt-oss:120b", done: true, done_reason: "stop", prompt_eval_count: 1, eval_count: 1 }, + ]); + }) as unknown as typeof fetch; + + const context: Context = { + messages: [ + { role: "user", content: "kick off", timestamp: Date.now() }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "internal reasoning" }, + { type: "toolCall", id: "tool-1", name: "read_file", arguments: { path: "README.md" } }, + ], + api: "ollama-chat", + provider: "ollama-cloud", + model: "gpt-oss:120b", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "toolUse", + timestamp: Date.now(), + }, + { + role: "toolResult", + toolCallId: "tool-1", + toolName: "read_file", + content: [{ type: "text", text: "README contents" }], + isError: false, + timestamp: Date.now(), + }, + ], + tools: [readFileTool], + }; + + await stream(cloudModel, context, { apiKey: "cloud-test-key" }).result(); + + const messages = requestBody?.messages as Array> | undefined; + const assistant = messages?.find(message => message.role === "assistant"); + expect(assistant).toBeDefined(); + expect(assistant).not.toHaveProperty("thinking"); + expect(assistant?.tool_calls).toEqual([ + { + type: "function", + function: { name: "read_file", arguments: { path: "README.md" } }, + }, + ]); + }); + test("emits one Ollama system message per ordered system prompt entry", async () => { let requestBody: Record | undefined; global.fetch = vi.fn(async (_input, init) => {