From 7d3476216ac0a1959a8a0ac2c77484ea4cb86f82 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 29 May 2026 11:38:02 +0000 Subject: [PATCH] fix(ai): dropped nanogpt :tools route on deepseek The :tools model suffix engages NanoGPT's server-side tool-call parser, which 502s with code malformed_tool_call on complex DeepSeek payloads (observed reliably on todo_write). The default route forwards delta.content (including DSML envelope leaks) which our StreamMarkupHealing already heals into a structured tool call. The DSML allowlist still covers nanogpt and the parallel-index fix remains; only the :tools suffix is removed. Fixes #1488 --- packages/ai/CHANGELOG.md | 1 + .../ai/src/providers/openai-completions.ts | 20 ++++++++----------- .../ai/test/stream-markup-healing.test.ts | 5 ++++- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1d921ff91..23116d20b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed GLM-5.x coding-plan OpenAI-compatible streams to use a longer default watchdog window, avoiding spurious `OpenAI completions stream stalled while waiting for the next event` errors during slow `glm-5.1` thinking/output phases. ([#1494](https://github.com/can1357/oh-my-pi/issues/1494)) - Fixed `zhipu-coding-plan` model discovery and credential validation to use the dedicated GLM Coding Plan endpoint (`https://open.bigmodel.cn/api/coding/paas/v4`) instead of the general BigModel endpoint, preventing requests from consuming ordinary account balance. ([#1494](https://github.com/can1357/oh-my-pi/issues/1494)) - Fixed DeepSeek tool calls failing on NanoGPT (e.g. `nanogpt/deepseek/deepseek-v4-pro` with reasoning enabled) by routing tool-bearing DeepSeek requests through NanoGPT's `:tools` model route and adding `nanogpt` to the DSML leak allowlist so streamed `<|DSML|tool_calls>...` envelopes are healed into structured tool calls instead of being passed through as visible text. ([#1488](https://github.com/can1357/oh-my-pi/issues/1488)) +- Fixed DeepSeek tool calls failing on NanoGPT (e.g. `nanogpt/deepseek/deepseek-v4-pro` with reasoning enabled) by adding `nanogpt` to the DSML leak allowlist so streamed `<|DSML|tool_calls>...` envelopes are healed into structured tool calls instead of being passed through as visible text. The `:tools` model suffix is no longer appended on NanoGPT; that route triggered NanoGPT's server-side tool-call parser and 502'd with `code: "malformed_tool_call"` on complex tool schemas (`todo_write`) — the default route forwards `delta.content` (including DSML envelopes) which is healed client-side. ([#1488](https://github.com/can1357/oh-my-pi/issues/1488)) - Fixed OpenAI-compatible streamed parallel tool calls losing indexed argument deltas by tracking active tool-call blocks by the provider's `tool_calls[].index`; this keeps parallel NanoGPT `read` calls from merging or dropping their `path` arguments. ([#1488](https://github.com/can1357/oh-my-pi/issues/1488)) ## [15.5.11] - 2026-05-29 diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index f80d27eff..98b6e069a 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -92,24 +92,20 @@ function normalizeMistralToolId(id: string, isMistral: boolean): string { } return normalized; } - -// NanoGPT's default DeepSeek route can attempt server-side tool-call repair and -// fail before streaming. `:tools` selects its documented tools-capable route. -function shouldUseNanoGptToolsRoute(model: Model<"openai-completions">, context: Context): boolean { - return ( - model.provider === "nanogpt" && !!context.tools?.length && /deepseek/i.test(model.id) && !model.id.includes(":") - ); -} - +// Direct DeepSeek model ids on NanoGPT are routed via the default tools-capable +// path. We deliberately do NOT append `:tools` here: with `:tools`, NanoGPT +// performs server-side tool-call parsing on the upstream DeepSeek stream and +// 502s with `code: "malformed_tool_call"` on more complex tool schemas (issue +// #1488). The default route forwards `delta.content` (including any DSML +// envelope leaks) which `StreamMarkupHealing` heals into a structured call +// client-side. function resolveOpenAICompletionsModelId( model: Model<"openai-completions">, - context: Context, options: OpenAICompletionsOptions | undefined, ): string { if (model.provider === "firepass") return toFirepassWireModelId(model.id); if (model.provider === "fireworks") return toFireworksWireModelId(model.id); if (model.provider === "openrouter") return applyOpenRouterRoutingVariant(model.id, options?.openrouterVariant); - if (shouldUseNanoGptToolsRoute(model, context)) return `${model.id}:tools`; return model.id; } @@ -1140,7 +1136,7 @@ function buildParams( // Note: Direct kimi-code provider is handled by the dedicated Kimi provider in kimi.ts. const effectiveMaxTokens = options?.maxTokens ?? (isKimiModelId ? model.maxTokens : undefined); - const requestModelId = resolveOpenAICompletionsModelId(model, context, options); + const requestModelId = resolveOpenAICompletionsModelId(model, options); const params: OpenAICompletionsParams = { model: requestModelId, messages, diff --git a/packages/ai/test/stream-markup-healing.test.ts b/packages/ai/test/stream-markup-healing.test.ts index 97656ef05..935eb9481 100644 --- a/packages/ai/test/stream-markup-healing.test.ts +++ b/packages/ai/test/stream-markup-healing.test.ts @@ -659,7 +659,10 @@ describe("OpenAI completions provider DSML envelope healing", () => { }, ).result(); - expect(payload?.model).toBe("deepseek/deepseek-v4-pro:tools"); + // Issue #1488: `:tools` triggers NanoGPT's server-side tool-call parser + // which 502s on complex DeepSeek payloads. We route via the default + // path and rely on DSML healing instead. + expect(payload?.model).toBe("deepseek/deepseek-v4-pro"); expect(payload?.reasoning_effort).toBe("high"); expect(payload?.tools).toBeDefined(); const text = result.content