[Fix] DeepSeek/OpenAI requests fail permanently when task history contains a lone UTF-16 surrogate - #1831
[Fix] DeepSeek/OpenAI requests fail permanently when task history contains a lone UTF-16 surrogate#1831zoomote[bot] wants to merge 1 commit into
Conversation
DeepSeek rejects the entire request body when any string contains a lone UTF-16 surrogate (e.g. after history text was sliced through an astral-plane character), permanently breaking the affected task. Apply the same sanitization pattern PR #1605 added for the VS Code LM provider to the shared OpenAI-chat transforms, covering the DeepSeek and OpenAI providers (and other providers on the same shared path): - Move sanitizeSurrogates/sanitizeIdentifierSurrogates/ sanitizeSurrogatesDeep into a shared transform module; vscode-lm-format re-exports them. - convertToOpenAiMessages/convertToR1Format: sanitize message text, tool_result content, tool_use ids (injective, composed with normalizeToolCallId so call/result pairing survives), tool names, nested tool argument strings, and reasoning_content. - OpenAiHandler: sanitize the system/developer prompt. - BaseProvider.convertToolsForOpenAI: sanitize tool definition name/description/parameters. completePrompt is intentionally out of scope, matching #1605. Fixes #461
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository: Zoo-Code-Org/Zoo-Code/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Review statusThis PR was opened by an automated account. A human maintainer must verify the change intent, provenance, and validation before merging. Current step: Mark the PR ready. Required CI must pass before CodeRabbit starts. Review-state labels are managed by this workflow; do not edit them manually. |
Related GitHub Issue
Fixes #461
Description
DeepSeek rejects the entire request body with
400 Failed to parse the request body as JSON: messages[N].content: lone leading surrogate in hex escapewhen any string in the payload contains a lone UTF-16 surrogate — which can happen when task history text is sliced through an astral-plane character (emoji, CJK extension, etc.). Once a task's history contains such a string, every retry of that task fails the same way while new tasks work fine.This applies the same sanitization pattern that #1605 added for the VS Code LM provider to the shared OpenAI-chat request path, covering the DeepSeek and OpenAI providers (and every other provider built on the same shared transforms):
sanitizeSurrogates/sanitizeIdentifierSurrogates/sanitizeSurrogatesDeephelpers move fromvscode-lm-format.tsinto a shared module,src/api/transform/sanitize-surrogates.ts(vscode-lm-format.tsre-exports them, so existing consumers and tests are unchanged). Valid surrogate pairs are untouched; lone surrogates become U+FFFD. The Copilot-specific tool-name alphabet encoding from fix(vscode-lm): sanitize lone UTF-16 surrogates in text and tool input #1605 stays vscode-lm-only — DeepSeek/OpenAI have no such constraint.convertToOpenAiMessages(openai-format.ts) andconvertToR1Format(r1-format.ts, used by DeepSeek) sanitize: message text (simple strings and content blocks),tool_resultcontent (string and blocks), tool_use names, nested strings inside tool arguments (JSON.stringify(sanitizeSurrogatesDeep(input))), andreasoning_contentpass-through.sanitizeIdentifierSurrogates(escape existing U+FFFD, then encode each lone surrogate as U+FFFD + hex), composed after any caller-providednormalizeToolCallId, so ids differing only in a lone surrogate never collapse and a tool_use stays paired with its tool_result.OpenAiHandlersanitizes the system prompt (including the o3-family developer message; DeepSeek's system prompt flows throughconvertToR1Format).BaseProvider.convertToolsForOpenAIsanitizes tool definition name, description, and parameter schemas.Reviewer notes / deliberate scope limits (mirroring #1605):
completePromptis unchanged;reasoning_detailspass-through (OpenRouter/Gemini-specific) is unchanged.sanitizeSurrogatesDeepkeeps #1605's documented lossy-key limitation (keys differing only in a lone surrogate collapse, last value wins) — accepted there and here because a request that reaches the provider beats one rejected outright.Test Procedure
src/api/transform/__tests__/sanitize-surrogates.spec.ts: lone high/low surrogate, valid pair untouched, trailing lone surrogate, reversed pair, nested deep sanitization, id injectivity and U+FFFD escape ordering.#461suites inopenai-format.spec.tsandr1-format.spec.ts: every serialization point above, id injectivity + tool call/result pairing, composition withnormalizeToolCallId, and a whole-bodyJSON.stringifyassertion that no lone surrogate remains.base-provider.spec.ts: tool definition name/description/parameters sanitization.deepseek.spec.tsandopenai.spec.ts: a history carrying lone surrogates in the system prompt, message text, tool_use id/name/input, and tool_result content produces a request body free of lone surrogates with tool call/result ids still paired.pnpm testfrom the repo root (13/13 turbo tasks successful),tsc --noEmit, and eslint with--max-warnings=0on all touched files. Reviewers can reproduce withcd src && npx vitest run api/transform/__tests__/sanitize-surrogates.spec.ts api/transform/__tests__/openai-format.spec.ts api/transform/__tests__/r1-format.spec.ts api/providers/__tests__/deepseek.spec.ts api/providers/__tests__/openai.spec.ts.Pre-Submission Checklist
Visual Snapshots
Not applicable — internal request-serialization fix with no rendered surface.
Videos (interaction / animation only)
Not applicable.
Documentation Updates
Zoo-Code-Docs documents provider setup (API keys, model lists) only; nothing there documents request serialization or this error mode, so no paired docs PR is needed for this internal robustness fix.
Additional Notes
The same fix pattern automatically covers the other OpenAI-compatible providers that share these transforms (OpenRouter, xAI, Requesty, etc. via
convertToOpenAiMessages/convertToolsForOpenAI), since the sanitization lives in the shared path rather than in the DeepSeek provider alone.Get in Touch
Available via the linked Roomote task or the Discord thread on #461 for reviewer questions.