From f699ea55a80b0df298247c53d52b514445d1f36d Mon Sep 17 00:00:00 2001 From: CreatorGhost Date: Sun, 12 Jul 2026 02:05:30 +0530 Subject: [PATCH] fix(llm): accept Ollama reasoning field in OpenAI Chat deltas Ported from upstream anomalyco/opencode#36068. --- packages/llm/src/protocols/openai-chat.ts | 15 ++++++++--- .../llm/test/provider/openai-chat.test.ts | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index 9ac85b07b139..514671e83000 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -75,6 +75,7 @@ const OpenAIChatMessage = Schema.Union([ content: Schema.NullOr(Schema.String), tool_calls: optionalArray(OpenAIChatAssistantToolCall), reasoning_content: Schema.optional(Schema.String), + reasoning: Schema.optional(Schema.String), }), Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }), ]).pipe(Schema.toTaggedUnion("role")) @@ -145,6 +146,7 @@ type OpenAIChatToolCallDelta = Schema.Schema.Type - isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined + isRecord(native) + ? typeof native.reasoning_content === "string" + ? native.reasoning_content + : typeof native.reasoning === "string" + ? native.reasoning + : undefined + : undefined const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) { const content: Array> = [] @@ -416,8 +424,9 @@ const step = (state: ParserState, event: OpenAIChatEvent) => let lifecycle = state.lifecycle - if (delta?.reasoning_content) - lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content) + const reasoningText = delta ? openAICompatibleReasoningContent(delta) : undefined + if (reasoningText) + lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", reasoningText) if (delta?.content) { lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0") diff --git a/packages/llm/test/provider/openai-chat.test.ts b/packages/llm/test/provider/openai-chat.test.ts index b736dc9dd33c..bf16adb08f9a 100644 --- a/packages/llm/test/provider/openai-chat.test.ts +++ b/packages/llm/test/provider/openai-chat.test.ts @@ -552,6 +552,32 @@ describe("OpenAI Chat route", () => { }), ) + it.effect("parses Ollama reasoning field deltas (reasoning alias for reasoning_content)", () => + Effect.gen(function* () { + const body = sseEvents( + { choices: [{ delta: { reasoning: "thinking" } }] }, + { choices: [{ delta: { content: "Hello" } }] }, + { choices: [{ delta: {}, finish_reason: "stop" }] }, + ) + + const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body))) + + expect(response.reasoning).toBe("thinking") + expect(response.text).toBe("Hello") + expect(response.events).toMatchObject([ + { type: "step-start", index: 0 }, + { type: "reasoning-start", id: "reasoning-0" }, + { type: "reasoning-delta", id: "reasoning-0", text: "thinking" }, + { type: "reasoning-end", id: "reasoning-0" }, + { type: "text-start", id: "text-0" }, + { type: "text-delta", id: "text-0", text: "Hello" }, + { type: "text-end", id: "text-0" }, + { type: "step-finish", index: 0, reason: "stop" }, + { type: "finish", reason: "stop" }, + ]) + }), + ) + it.effect("assembles streamed tool call input", () => Effect.gen(function* () { const body = sseEvents(