summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-19 20:44:09 +0900
committerAdam Malczewski <[email protected]>2026-05-19 20:44:09 +0900
commitb3d16aa62a1b8724fe57ab3846a83f03a4a3fbc0 (patch)
tree480a1fa2c95bf87ab214f709b1b4414aa64e8929 /packages/core/src
parentf78a91c20f658dd404277919a0b872b352c99bb6 (diff)
downloaddispatch-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.ts67
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],
+ });
+ };
}