diff options
| author | Adam Malczewski <[email protected]> | 2026-05-19 20:44:09 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-19 20:44:09 +0900 |
| commit | b3d16aa62a1b8724fe57ab3846a83f03a4a3fbc0 (patch) | |
| tree | 480a1fa2c95bf87ab214f709b1b4414aa64e8929 /packages/core/src | |
| parent | f78a91c20f658dd404277919a0b872b352c99bb6 (diff) | |
| download | dispatch-b3d16aa62a1b8724fe57ab3846a83f03a4a3fbc0.tar.gz dispatch-b3d16aa62a1b8724fe57ab3846a83f03a4a3fbc0.zip | |
fix: DeepSeek reasoning_content dropped on multi-step tool calls
- Base URL corrected: zen/v1 -> zen/go/v1 (opencode-go provider)
- Model changed: deepseek-v4-flash-free -> deepseek-v4-flash
- Added wrapLanguageModel middleware to inject reasoning_content via
providerMetadata.openaiCompatible before each stream call
- Fixed test mocks: removed vi.importActual (unsupported in Bun), added
tool factory mocks, preserved real tool export in ai mock
- Added 11 tests for the normalizeMessages middleware
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/llm/provider.ts | 67 |
1 files changed, 66 insertions, 1 deletions
diff --git a/packages/core/src/llm/provider.ts b/packages/core/src/llm/provider.ts index 06f7b72..2a917b2 100644 --- a/packages/core/src/llm/provider.ts +++ b/packages/core/src/llm/provider.ts @@ -1,9 +1,74 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; +import { wrapLanguageModel } from "ai"; +import type { LanguageModelV1Middleware, LanguageModelV1Prompt } from "ai"; + +/** + * Normalize messages for interleaved reasoning providers (DeepSeek). + * Extracts { type: "reasoning" } / { type: "redacted-reasoning" } parts from + * assistant message content arrays and moves them into + * providerMetadata.openaiCompatible.reasoning_content so + * @ai-sdk/openai-compatible serializes them correctly for the API. + * + * IMPORTANT: The messages in params.prompt are already in LanguageModelV1Prompt + * format (post-conversion by the AI SDK). They use `providerMetadata`, NOT + * `providerOptions` — that conversion happens before the middleware runs. + */ +function normalizeMessages(msgs: unknown[]): unknown[] { + return msgs.map((msg: unknown) => { + const message = msg as Record<string, unknown>; + if (message.role !== "assistant" || !Array.isArray(message.content)) return message; + + const content = message.content as Array<Record<string, unknown>>; + const reasoningParts = content.filter( + (p) => p.type === "reasoning" || p.type === "redacted-reasoning", + ); + const reasoningText = reasoningParts.map((p) => p.text ?? "").join(""); + + const filteredContent = content.filter( + (p) => p.type !== "reasoning" && p.type !== "redacted-reasoning", + ); + + const existingMetadata = (message.providerMetadata ?? {}) as Record<string, unknown>; + const existingOpenAICompat = (existingMetadata.openaiCompatible ?? {}) as Record<string, unknown>; + + return { + ...message, + content: filteredContent, + providerMetadata: { + ...existingMetadata, + openaiCompatible: { + ...existingOpenAICompat, + reasoning_content: reasoningText, + }, + }, + }; + }); +} export function createProvider(config: { apiKey: string; baseURL: string }) { - return createOpenAICompatible({ + const provider = createOpenAICompatible({ name: "opencode-zen", apiKey: config.apiKey, baseURL: config.baseURL, }); + + return (modelId: string) => { + const middleware: LanguageModelV1Middleware = { + middlewareVersion: "v1" as const, + transformParams: async ({ type, params }) => { + if (type === "stream" && params.prompt) { + return { + ...params, + prompt: normalizeMessages(params.prompt as unknown[]) as LanguageModelV1Prompt, + }; + } + return params; + }, + }; + + return wrapLanguageModel({ + model: provider(modelId), + middleware: [middleware], + }); + }; } |
