diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 4ea60a24ea..2f0482eeef 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -386,6 +386,75 @@ describe("DeepSeekHandler", () => { expect(usageChunks[0].cacheWriteTokens).toBe(8) expect(usageChunks[0].cacheReadTokens).toBe(2) }) + + it("streams reasoning chunks from delta.reasoning_content", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] } + yield { choices: [{ delta: { content: "answer" }, index: 0 }] } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("falls back to delta.reasoning when reasoning_content is absent", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) + }) + + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { + reasoning_content: "primary thought", + reasoning: "fallback thought", + }, + index: 0, + }, + ], + } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") + expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }]) + }) }) describe("processUsageMetrics", () => { diff --git a/src/api/providers/__tests__/mimo.spec.ts b/src/api/providers/__tests__/mimo.spec.ts index 9e7ec97d28..7da1c84463 100644 --- a/src/api/providers/__tests__/mimo.spec.ts +++ b/src/api/providers/__tests__/mimo.spec.ts @@ -470,21 +470,70 @@ describe("MimoHandler", () => { expect(usageChunks[0].outputTokens).toBe(5) }) - it("should handle reasoning_content in stream", async () => { - // Override mock to return reasoning_content + it("streams reasoning chunks from delta.reasoning_content", async () => { mockCreate.mockImplementationOnce(async () => ({ [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] } + yield { choices: [{ delta: { content: "answer" }, index: 0 }] } yield { - choices: [{ delta: { reasoning_content: "Thinking..." }, index: 0 }], - usage: null, + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, } + }, + })) + + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: [{ type: "text", text: "Hello" }] }, + ] + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("System prompt", messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("falls back to delta.reasoning when reasoning_content is absent", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] } yield { - choices: [{ delta: { content: "Done" }, index: 0 }], - usage: null, + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, } + }, + })) + + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: [{ type: "text", text: "Hello" }] }, + ] + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("System prompt", messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) + }) + + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { yield { - choices: [{ delta: {}, index: 0, finish_reason: "stop" }], - usage: { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, + choices: [ + { + delta: { + reasoning_content: "primary thought", + reasoning: "fallback thought", + }, + index: 0, + }, + ], + } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, } }, })) @@ -494,14 +543,12 @@ describe("MimoHandler", () => { ] const chunks: any[] = [] - const stream = handler.createMessage("System prompt", messages) - for await (const chunk of stream) { + for await (const chunk of handler.createMessage("System prompt", messages)) { chunks.push(chunk) } - const reasoningChunks = chunks.filter((c) => c.type === "reasoning") - expect(reasoningChunks).toHaveLength(1) - expect(reasoningChunks[0].text).toBe("Thinking...") + const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") + expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }]) }) it("should yield tool_call_partial chunks from stream", async () => { diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index ed5e82496e..07fa41786a 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -221,6 +221,77 @@ describe("OpenAiHandler", () => { expect(textChunks[0].text).toBe("Test response") }) + it("streams reasoning chunks from delta.reasoning_content", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] } + yield { choices: [{ delta: { content: "answer" }, index: 0 }] } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("falls back to delta.reasoning when reasoning_content is absent", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) + }) + + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { + reasoning_content: "primary thought", + reasoning: "fallback thought", + }, + index: 0, + }, + ], + } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const chunks: any[] = [] + + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") + + expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }]) + }) + it("should handle tool calls in streaming responses", async () => { mockCreate.mockImplementation(async (options) => { return { diff --git a/src/api/providers/__tests__/opencode-go.spec.ts b/src/api/providers/__tests__/opencode-go.spec.ts index 886b56d0e5..2877abd36b 100644 --- a/src/api/providers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/__tests__/opencode-go.spec.ts @@ -158,6 +158,84 @@ describe("OpencodeGoHandler", () => { }), ) }) + + it("streams reasoning chunks from delta.reasoning_content", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] } + yield { choices: [{ delta: { content: "answer" }, index: 0 }] } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const handler = new OpencodeGoHandler(mockOptions) + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("sys", messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("falls back to delta.reasoning when reasoning_content is absent", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const handler = new OpencodeGoHandler(mockOptions) + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("sys", messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) + }) + + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { + reasoning_content: "primary thought", + reasoning: "fallback thought", + }, + index: 0, + }, + ], + } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const handler = new OpencodeGoHandler(mockOptions) + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("sys", messages)) { + chunks.push(chunk) + } + + const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") + expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }]) + }) }) describe("completePrompt", () => { diff --git a/src/api/providers/__tests__/qwen-code-native-tools.spec.ts b/src/api/providers/__tests__/qwen-code-native-tools.spec.ts index b2e0e69e50..3615c0f92d 100644 --- a/src/api/providers/__tests__/qwen-code-native-tools.spec.ts +++ b/src/api/providers/__tests__/qwen-code-native-tools.spec.ts @@ -302,6 +302,78 @@ describe("QwenCodeHandler Native Tools", () => { expect(endChunks[0].id).toBe("call_qwen_test") }) + it("streams reasoning chunks from delta.reasoning_content", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] } + yield { choices: [{ delta: { content: "answer" }, index: 0 }] } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const stream = handler.createMessage("test prompt", []) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("falls back to delta.reasoning when reasoning_content is absent", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const stream = handler.createMessage("test prompt", []) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) + }) + + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { + reasoning_content: "primary thought", + reasoning: "fallback thought", + }, + index: 0, + }, + ], + } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } + }, + })) + + const stream = handler.createMessage("test prompt", []) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") + expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }]) + }) + it("should preserve thinking block handling alongside tool calls", async () => { mockCreate.mockImplementationOnce(() => ({ [Symbol.asyncIterator]: async function* () { diff --git a/src/api/providers/__tests__/requesty.spec.ts b/src/api/providers/__tests__/requesty.spec.ts index 7556aa58f6..1565011df0 100644 --- a/src/api/providers/__tests__/requesty.spec.ts +++ b/src/api/providers/__tests__/requesty.spec.ts @@ -255,6 +255,84 @@ describe("RequestyHandler", () => { await expect(generator.next()).rejects.toThrow("API Error") }) + it("streams reasoning chunks from delta.reasoning_content", async () => { + const handler = new RequestyHandler(mockOptions) + mockCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { id: "1", choices: [{ delta: { reasoning_content: "thinking..." } }] } + yield { id: "1", choices: [{ delta: { content: "answer" } }] } + yield { + id: "1", + choices: [{ delta: {} }], + usage: { prompt_tokens: 1, completion_tokens: 1 }, + } + }, + }) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "hi" }])) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("falls back to delta.reasoning when reasoning_content is absent", async () => { + const handler = new RequestyHandler(mockOptions) + mockCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { id: "1", choices: [{ delta: { reasoning: "router-style thought" } }] } + yield { + id: "1", + choices: [{ delta: {} }], + usage: { prompt_tokens: 1, completion_tokens: 1 }, + } + }, + }) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "hi" }])) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) + }) + + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { + const handler = new RequestyHandler(mockOptions) + + mockCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { + id: "1", + choices: [ + { + delta: { + reasoning_content: "primary thought", + reasoning: "fallback thought", + }, + }, + ], + } + yield { + id: "1", + choices: [{ delta: {} }], + usage: { prompt_tokens: 1, completion_tokens: 1 }, + } + }, + }) + + const chunks: any[] = [] + + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "hi" }])) { + chunks.push(chunk) + } + + const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") + + expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }]) + }) + describe("native tool support", () => { const systemPrompt = "test system prompt" const messages: Anthropic.Messages.MessageParam[] = [ diff --git a/src/api/providers/__tests__/unbound.spec.ts b/src/api/providers/__tests__/unbound.spec.ts index d741f2a371..a85d670ec1 100644 --- a/src/api/providers/__tests__/unbound.spec.ts +++ b/src/api/providers/__tests__/unbound.spec.ts @@ -52,6 +52,95 @@ describe("UnboundHandler", () => { ) }) + it("streams reasoning chunks from delta.reasoning_content", async () => { + const mockCreate = (OpenAI as unknown as any)().chat.completions.create + mockCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { choices: [{ delta: { reasoning_content: "thinking..." } }] } + yield { choices: [{ delta: { content: "answer" } }] } + yield { choices: [{ delta: {} }], usage: { prompt_tokens: 1, completion_tokens: 1 } } + }, + }) + + const handler = new UnboundHandler({ + unboundApiKey: "test-key", + unboundModelId: "openai/gpt-4o", + }) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }], { + taskId: "t", + tools: [], + })) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("falls back to delta.reasoning when reasoning_content is absent", async () => { + const mockCreate = (OpenAI as unknown as any)().chat.completions.create + mockCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { choices: [{ delta: { reasoning: "router-style thought" } }] } + yield { choices: [{ delta: {} }], usage: { prompt_tokens: 1, completion_tokens: 1 } } + }, + }) + + const handler = new UnboundHandler({ + unboundApiKey: "test-key", + unboundModelId: "openai/gpt-4o", + }) + + const chunks: any[] = [] + for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }], { + taskId: "t", + tools: [], + })) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) + }) + + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { + const mockCreate = (OpenAI as unknown as any)().chat.completions.create + + mockCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { + choices: [ + { + delta: { + reasoning_content: "primary thought", + reasoning: "fallback thought", + }, + }, + ], + } + yield { choices: [{ delta: {} }], usage: { prompt_tokens: 1, completion_tokens: 1 } } + }, + }) + + const handler = new UnboundHandler({ + unboundApiKey: "test-key", + unboundModelId: "openai/gpt-4o", + }) + + const chunks: any[] = [] + + for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }], { + taskId: "t", + tools: [], + })) { + chunks.push(chunk) + } + + const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") + + expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }]) + }) + it("identifies itself as Zoo Code in per-request Unbound metadata", async () => { const mockCreate = (OpenAI as unknown as any)().chat.completions.create mockCreate.mockResolvedValue({ diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index e2ffd29169..819fe6c7bc 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -16,6 +16,7 @@ import { getModelParams } from "../transform/model-params" import { convertToR1Format } from "../transform/r1-format" import { OpenAiHandler } from "./openai" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" import type { ApiHandlerCreateMessageMetadata } from "../index" // Custom interface for DeepSeek params to support thinking mode @@ -155,11 +156,9 @@ export class DeepSeekHandler extends OpenAiHandler { // Handle reasoning_content from DeepSeek's interleaved thinking // This is the proper way DeepSeek sends thinking content in streaming - if ("reasoning_content" in delta && delta.reasoning_content) { - yield { - type: "reasoning", - text: (delta.reasoning_content as string) || "", - } + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } } // Handle tool calls diff --git a/src/api/providers/mimo.ts b/src/api/providers/mimo.ts index f842926b60..2901c2e926 100644 --- a/src/api/providers/mimo.ts +++ b/src/api/providers/mimo.ts @@ -9,6 +9,7 @@ import { convertToR1Format } from "../transform/r1-format" import { getModelParams } from "../transform/model-params" import { calculateApiCostOpenAI } from "../../shared/cost" import { handleProviderError } from "./utils/error-handler" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" import { OpenAiHandler } from "./openai" import type { ApiHandlerCreateMessageMetadata } from "../index" @@ -127,11 +128,9 @@ export class MimoHandler extends OpenAiHandler { } } - if ("reasoning_content" in delta && delta.reasoning_content) { - yield { - type: "reasoning", - text: (delta.reasoning_content as string) || "", - } + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } } yield* this.processToolCalls(sanitizedDelta, finishReason, activeToolCallIds) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 532ed38ba2..f80544b409 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -24,6 +24,7 @@ import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getApiRequestTimeout } from "./utils/timeout-config" import { handleOpenAIError } from "./utils/openai-error-handler" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" // TODO: Rename this to OpenAICompatibleHandler. Also, I think the // `OpenAINativeHandler` can subclass from this, since it's obviously @@ -207,11 +208,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl } } - if ("reasoning_content" in delta && delta.reasoning_content) { - yield { - type: "reasoning", - text: (delta.reasoning_content as string | undefined) || "", - } + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } } yield* this.processToolCalls(delta, finishReason, activeToolCallIds) diff --git a/src/api/providers/opencode-go.ts b/src/api/providers/opencode-go.ts index 6b66aa6846..43d32e6192 100644 --- a/src/api/providers/opencode-go.ts +++ b/src/api/providers/opencode-go.ts @@ -10,6 +10,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { RouterProvider } from "./router-provider" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" /** * API handler for the Opencode "Go" subscription plan. @@ -80,8 +81,9 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio } // Several Go-plan models (GLM, DeepSeek) stream reasoning via this field. - if (delta && "reasoning_content" in delta && delta.reasoning_content) { - yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" } + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } } // Emit raw tool call chunks - NativeToolCallParser handles state management. diff --git a/src/api/providers/qwen-code.ts b/src/api/providers/qwen-code.ts index f2a207051e..0b7d7598af 100644 --- a/src/api/providers/qwen-code.ts +++ b/src/api/providers/qwen-code.ts @@ -14,6 +14,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai" @@ -283,11 +284,9 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan } } - if ("reasoning_content" in delta && delta.reasoning_content) { - yield { - type: "reasoning", - text: (delta.reasoning_content as string | undefined) || "", - } + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } } // Handle tool calls in stream - emit partial chunks for NativeToolCallParser diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 3e50adf9cc..df3dc35af5 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -18,6 +18,7 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ". import { toRequestyServiceUrl } from "../../shared/utils/requesty" import { handleOpenAIError } from "./utils/openai-error-handler" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" // Requesty usage includes an extra field for Anthropic use cases. // Safely cast the prompt token details section to the appropriate structure. @@ -174,8 +175,9 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan yield { type: "text", text: delta.content } } - if (delta && "reasoning_content" in delta && delta.reasoning_content) { - yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" } + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } } // Handle native tool calls diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts index a1de7dfa14..f0c6fe7582 100644 --- a/src/api/providers/unbound.ts +++ b/src/api/providers/unbound.ts @@ -17,6 +17,7 @@ import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { handleOpenAIError } from "./utils/openai-error-handler" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" // Unbound usage includes extra fields for Anthropic cache tokens. interface UnboundUsage extends OpenAI.CompletionUsage { @@ -162,8 +163,9 @@ export class UnboundHandler extends BaseProvider implements SingleCompletionHand yield { type: "text", text: delta.content } } - if (delta && "reasoning_content" in delta && delta.reasoning_content) { - yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" } + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } } // Handle native tool calls