fix(pi): narrow converter guards — trailing assistant, tool/image/surrogate, signed-thinking#32
fix(pi): narrow converter guards — trailing assistant, tool/image/surrogate, signed-thinking#32iceteaSA wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
1 issue found across 3 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
b659ab4 to
768e897
Compare
There was a problem hiding this comment.
No issues found across 3 files
Tip: cubic could auto-approve low-risk PRs like this, if it thinks it's safe to merge. Learn more
Re-trigger cubic
768e897 to
432f588
Compare
|
Reviewed this in context with OpenCode PR anomalyco/opencode#30182 and the related OpenCode-side healing PRs here (#49, #51). My read:
Recommendation: split this PR. Keep the tests and the narrow Pi conversion guards, including trailing assistant stripping. Drop or separately rework the historical signed-thinking downgrade into a much narrower fix backed by a concrete Pi repro. |
c3d0d62 to
0003da7
Compare
Dropped it |
0003da7 to
ae0626e
Compare
…r guards - Strip trailing assistant-role messages from converted requests to prevent Anthropic prefill 400 errors on some models - Add Unicode flag to surrogate regex for spec compliance - Simplify sanitizeToolId with cleaner length check - Improve error content guard to catch empty string case - Add comprehensive test coverage for all pi conversion logic
ae0626e to
dbf4f8e
Compare
Signed thinking blocks must be sent back byte-identical — the signature is computed over the original text, so sanitizing would alter it and Anthropic rejects the block as modified. Send signed thinking verbatim instead of sanitizing it. The one exception is a signed block containing a lone (unpaired) UTF-16 surrogate: the signature cannot survive sanitization and the raw surrogate is invalid UTF-8 (400). Detect lone surrogates with hasLoneSurrogate() and, only in that concrete case, drop the signature and downgrade to sanitized text. No broad historical-thinking healing — valid signed/redacted thinking is preserved. (Signed-thinking reordering is handled OpenCode-side in #30182.)
dbf4f8e to
3bcd3e9
Compare
Narrow, Pi-owned converter hardening for
packages/pi/src/convert.ts, beyond what was merged in #17. All changes are scoped to concrete invalid shapes the Pi converter can produce — no broad payload "healing".Commit 1: strip trailing assistant messages, improve sanitize and error guards
/guflag) for spec-compliant surrogate handling — valid surrogate pairs (emoji / astral characters) are preserved; only lone surrogates are replaced with\uFFFD.sanitizeToolIdwith a cleaner length check (removes a redundant guard).Errorplaceholder foris_error=truetool results with empty content (!content, or an empty array).Commit 2: preserve signed thinking verbatim, guard lone surrogates
hasLoneSurrogate()detects exactly that case, and only then is the signature dropped and the text sanitized.Scope note (per review)
This PR intentionally does not include the broad "downgrade all historical signed thinking to text" healing. That approach can avoid 400s but discards valid signed/redacted reasoning when the request shape is otherwise correct. The signed-thinking reordering problem is better fixed at the OpenCode layer in anomalyco/opencode#30182. For Pi, the converter should avoid broad destructive healing unless a concrete Pi-generated invalid shape requires it — none currently does, so it is omitted here.
Greptile Summary
This PR hardens the Pi converter (
packages/pi/src/convert.ts) with four targeted fixes: stripping trailing assistant messages to avoid Anthropic prefill 400s, preserving signed thinking blocks verbatim (previously they were sanitized, which broke the signature), downgrading signed blocks that contain lone surrogates, and tightening the empty-content guard foris_error=truetool results.convert.tslines 149-168): the old code calledsanitize()on signed thinking text, which mutated the content and invalidated the signature. The new code sends the text byte-for-byte identical, and only falls back to sanitized text whenhasLoneSurrogate()detects an unpaired surrogate that would cause a UTF-8 400 anyway.buildAnthropicRequest, lines 306-312): awhileloop removes any assistant messages left at the tail of the converted array before the request is sent, covering models that reject assistant-role prefill.convert.test.ts, 428 lines): covers all new guards including surrogate sanitisation, signed-thinking preservation, error tool-result placeholder injection, and the trailing-assistant strip.Confidence Score: 5/5
Safe to merge — all changes are narrowly scoped to concrete invalid shapes the Pi converter produces, and the new test suite exercises every guard added.
The signed-thinking verbatim pass-through correctly identifies that sanitizing a signed block breaks its signature, and the lone-surrogate fallback covers the only edge case where verbatim sending would itself cause a 400. The trailing-assistant strip is a simple while-loop applied after conversion, which cannot affect any mid-conversation message. The error-content guard correctly handles the full string | Array union returned by convertTextAndImages. Test coverage is thorough and the emoji/astral-character preservation regression test is present.
No files require special attention.
Important Files Changed
"test": "bun test src/tests"script; no other changes.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[thinking block] --> B{thinkingSignature set?} B -- No --> E[sanitize text → emit as text block] B -- Yes --> C{hasLoneSurrogate?} C -- No --> D[emit verbatim thinking block\nwith original text + signature] C -- Yes --> F[drop signature\nsanitize text → emit as text block] G[tool_result content] --> H{isError = true?} H -- No --> I[emit content as-is] H -- Yes --> J{content empty/blank?} J -- No --> I J -- Yes --> K[inject placeholder: Error] L[converted messages array] --> M{trailing message role = assistant?} M -- Yes --> N[pop and repeat] M -- No --> O[send to Anthropic]Reviews (2): Last reviewed commit: "fix(pi): preserve signed thinking verbat..." | Re-trigger Greptile