Skip to content

Review only: HOUSE-79 upstream v8.0.44, part 9 of 13: openai and claude translators - #21

Closed
ericlitman wants to merge 1 commit into
review/house79/base-09from
review/house79/chunk-09
Closed

ericlitman wants to merge 1 commit into
review/house79/base-09from
review/house79/chunk-09

Conversation

@ericlitman

@ericlitman ericlitman commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review only. Do not merge. This PR is one part of the review of #13, the merge of upstream v8.0.44 into the fork. That PR's diff (966 files) is too large for one review round.

  • This part covers: openai and claude translators.
  • The head tree is identical to Merge upstream v8.0.44 into the fork (HOUSE-79) #13 head tree (c88d701f7bc0894cde517ca7f931f5c05749cd74). The base is that tree with only this part's files restored to main (c265779f), so the diff is this part's files alone and the surrounding code is the real final code.
  • The 13 parts together cover every production file the sync changes (410 paths). Tests, golden data, generated model catalogs, images, docs and go.sum are in no part's diff.
  • The code is upstream's, except where our fork's soonest-reset selector, quota observations, quota reserves, usage reads and Terra injection touch it. Review it as code we are about to deploy to the proxy every one of our agents uses: report defects that would affect our deployment.

Linear: HOUSE-79

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@mergify

mergify Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

@unfret

unfret Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

🛑 5 blocking findings

🔴 High · Emit document URLs under the Interactions `uri` field

🟡 Medium · Reuse existing call IDs for sparse indexless deltas

🟡 Medium · Final finish chunks can lose Anthropic terminal events

🟡 Medium · Reserve historical identities when naming the shell bridge

🟡 Medium · Preserve the model role before applying drop validation

Walkthrough

This review-only slice of the upstream v8.0.44 merge updates the OpenAI and Claude request/response translators. Request translators now return an error alongside the body, so a user turn whose only attachment cannot be carried by the target format is refused rather than silently emptied. It also adds Codex client tool_search support for Claude (gated by upstream.claude.tool-search and pinned per request via context), a local shell tool bridge for OpenAI Responses-to-Chat, and many smaller fidelity fixes: pause_turn and refusal handling, reasoning-token and thought-signature passthrough, model-specific max_tokens defaults, whitespace-only text filtering, and request-echo fields extracted once per stream instead of re-parsed.

Area What changes Review notes
Dropped user attachments Gemini, Interactions, Chat and Responses translators to Claude, OpenAI and Interactions track per-turn sendable parts via UserTurnDrops and return an error when an unsupported file, audio or media part leaves a user turn empty; assistant content keeps placeholders. Preserve the model role before applying drop validation
Emit document URLs under the Interactions `uri` field
Claude Responses tool search and web search Client tool_search declarations, calls and outputs map to Claude tool_use/tool_result with deferred tools and a constant deferred anchor; paused web searches replay without results, and continuations and pause_turn complete with end_turn=false.
OpenAI Responses shell bridge Local shell tools and shell_call history map to a synthetic __cpa_local_shell function, and shell_call items are emitted back with validated actions. Reserve historical identities when naming the shell bridge
Streaming terminal and tool-call handling OpenAI-to-Claude streaming defers message_delta until a trailing usage chunk or [DONE] and reassigns indexless tool-call deltas with conflicting IDs. OpenAI-to-Gemini emits tool calls in index order with thought signatures, and Responses streams can finalize at EOF once items are complete. Reuse existing call IDs for sparse indexless deltas
Final finish chunks can lose Anthropic terminal events
Since the last review
  • New: Reuse existing call IDs for sparse indexless deltas
  • New: Final finish chunks can lose Anthropic terminal events
  • New: Emit document URLs under the Interactions `uri` field
  • New: Reserve historical identities when naming the shell bridge
  • New: Preserve the model role before applying drop validation
Repair prompt

Read the Unfret check run for mobilyze-llc/CLIProxyAPI#21 at head 32da31450215ad88870f254094c917c75e6512fb (run panel:32da31450215ad88870f254094c917c75e6512fb:gQ0qH9liNa-:uy2k7c-SBhQ, check). Use the check's App id and GET /repos/mobilyze-llc/CLIProxyAPI/check-runs/122348239872; its output.text contains the structured result. Compare it with the current code and head, verify that each listed mandatory finding still applies, repair only those that do, honor the repository constraints, and validate the changes. Leave advisories and accepted risks alone. A manually resolved thread is not a waiver.

Details
Lane / stage Model Effort Outcome Duration
broad-gpt-5.6-sol-xhigh / review openai/gpt-5.6-sol → gpt-5.6-sol · primary xhigh succeeded 10 min 2 s
broad-opus-5.5-max / review anthropic/claude-opus-5-5 → claude-opus-5-5 · primary max succeeded 19 min 29 s
focal-gpt-5.6-sol-xhigh copy / review openai/gpt-6.1-sol → gpt-6.1-sol · primary xhigh succeeded 17 min 3 s
focal-gpt-5.6-sol-xhigh copy 2 / review openai/gpt-6.1-sol → gpt-6.1-sol · primary xhigh succeeded 15 min 56 s
judge / judge openai/gpt-6-astra → gpt-6-astra · primary high succeeded 2 min 58 s
confirmation / confirmation anthropic/claude-opus-5-5 → claude-opus-5-5 · primary max succeeded 9 min 41 s

First review of this pull request.
4 reviewers finished.
Commit 32da314.
Check run.
Run: GET /unfret/run/panel:32da31450215ad88870f254094c917c75e6512fb:gQ0qH9liNa-:uy2k7c-SBhQ.

@unfret review · @unfret status · @unfret help

@unfret unfret Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 5 blocking findings
🔴 High · Emit document URLs under the Interactions `uri` field
🟡 Medium · Reuse existing call IDs for sparse indexless deltas
🟡 Medium · Final finish chunks can lose Anthropic terminal events
🟡 Medium · Reserve historical identities when naming the shell bridge
🟡 Medium · Preserve the model role before applying drop validation
Check

Comment on lines +348 to +355
if existing, exists := param.ToolCallsAccumulator[index]; exists && existing != nil && incomingID != "" && existing.ID != "" && existing.ID != incomingID {
nextIndex := index + 1
for k := range param.ToolCallsAccumulator {
if k >= nextIndex {
nextIndex = k + 1
}
}
index = nextIndex

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Reuse existing call IDs for sparse indexless deltas

When an indexless chunk initially announces calls A and B, a later indexless chunk containing only B's arguments lands at array position zero. This branch compares B against A and allocates a third accumulator instead of finding B's existing accumulator. Reproducing this with two no-argument calls followed by B's arguments="{}" emits three tool_use blocks, including a duplicate B ID with the synthesized name tool_2; the base emits only A and B.

(param.FinishReason != "" || param.SawToolCall || param.TextContentBlockStarted || param.ThinkingContentBlockStarted || param.ContentAccumulator.Len() > 0 || len(param.InterleavedContentChunks) > 0)

if !param.MessageDeltaSent && (param.FinishReason != "" || isTrailingUsageChunk) && hasUsage {
if !param.MessageDeltaSent && isTrailingUsageChunk {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Final finish chunks can lose Anthropic terminal events

The deleted param.FinishReason != "" branch terminated streams whose final choice chunk contained both finish_reason and usage. This replacement only terminates on a separate usage-only chunk, so an OpenAI-compatible upstream that closes after that final choice chunk without [DONE] now emits neither message_delta nor message_stop, leaving Anthropic clients with an unterminated response; no EOF finalizer for this state is visible here. Also reported for this defect: - message_stop now depends on [DONE] when usage rides on the finish chunk (internal/translator/openai/claude/openai_claude_response.go:454): Before this diff, message_delta and message_stop were emitted on the finish_reason chunk whenever that chunk carried usage ((FinishReason != "" || isTrailingUsageChunk) && hasUsage). The new condition at this line fires only for a usage-only chunk with no choices. Otherwise emission waits for convertOpenAIDoneToAnthropic. Failure scenario: an OpenAI-compatible upstream sends its last chunk with finish_reason and usage, then closes the stream without data: [DONE]. The Claude client now receives content blocks but no message_delta (no stop_reason, no output usage) and no message_stop. Claude Code then sees a null stop_reason and does not execute the emitted tool_use calls, so the agent turn stalls. Nothing in the visible diff synthesizes a [DONE] at EOF for ConvertOpenAIResponseToAnthropicParams. Such synthesis may exist in the executor outside this diff. - Keep EOF finalization compatible with bare terminators (internal/translator/openai/claude/openai_claude_response.go:454): Removing the param.FinishReason != "" completion case breaks Kimi execution with SourceFormat=OpenAI and ResponseFormat=Claude when upstream ends after a finish-reason-plus-usage chunk. Kimi's EOF finalizer supplies bare [DONE], which this translator rejects at its data: prefix check. Execution now closes successfully without either message_delta or message_stop; the same sequence emitted both terminal events in the base.

Comment on lines +428 to +429
out, _ = sjson.SetBytes(out, "file_url", fileURL)
hasContent = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 High · Emit document URLs under the Interactions uri field

For an input_file containing only file_url, this mapper emits a document with file_url and reports it as sendable. Native Interactions DocumentContent uses uri, not file_url; the Gemini Interactions executors forward this payload without remapping that field. The resulting document has neither inline data nor a recognized URI, so a valid URL attachment produces an unusable upstream request. Also reported for this defect: - Translate document URLs to the Interactions uri field (internal/translator/openai/interactions/responses/interactions_openai_responses_request.go:428): An input_file containing only file_url becomes a document part with file_url, and this helper reports it as sendable. Native Gemini Interactions expects the remote location in uri, not file_url. No downstream normalization rewrites this key, leaving the document without either data or uri and causing an upstream validation failure. This also breaks otherwise valid requests containing text alongside the URL attachment.

Also affects:

Comment on lines +101 to +105
for _, d := range declarations {
if !d.shell {
reserved[d.localName] = true
reserved[d.chatName] = true
reserved[rawResponsesNamespaceQualifiedName(d.namespace, d.localName)] = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Reserve historical identities when naming the shell bridge

This reservation scans current declarations only. If history contains a now-withdrawn user function named __cpa_local_shell and the current request adds a local shell tool, the bridge takes that same name. Request translation then emits both the historical non-shell call and rewritten shell calls with identical function.name values, attributing unrelated calls and arguments to one tool identity. The replay-only naming path already reserves historical calls, but this declared-shell path does not.

}
wrapped, _ = sjson.SetRawBytes(wrapped, "content", []byte(step.Get("parts").Raw))
appendInteractionsStepToClaude(accumulator, gjson.ParseBytes(wrapped), "user")
appendInteractionsStepToClaude(accumulator, gjson.ParseBytes(wrapped), "user", translatorcommon.IsInteractionsInstructionStep(step, false), run)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Preserve the model role before applying drop validation

Native history items shaped as {role:"model",parts:[...]} are wrapped as model_output but passed here with defaultRole="user"; the wrapped object has no role to override it. The newly added drop tracking therefore classifies assistant audio as an emptied user turn. For example, model audio followed by another model_output and a normal user question now produces UnsupportedPartError/400. The equivalent model_output/content shape correctly retains the assistant-media placeholder. Also reported for this defect: - Preserve the model role when tracking dropped media (internal/translator/claude/interactions/interactions_claude_request.go:199): A {role:"model",parts:[audio]} history item is wrapped as model_output but still passed here with default role user. The new drop accounting therefore treats assistant media as an emptied user turn. After a preceding model_output has closed the user run, this item causes UnsupportedPartError at the next model step or input end, rejecting the request with HTTP 400. The base preserved a placeholder instead of rejecting this assistant history.

@ericlitman

Copy link
Copy Markdown
Contributor Author

Closing: the review scope changed. Eric (2026-10-27): "i don't want to fix anything that's an issue in upstream - our only goal here is to validate our changes". Upstream code is out of scope. Our fork's own changes on v8.0.44 get one review PR instead. Findings from this part are recorded in the HOUSE-79 adjudication record. None was a defect in our code.

@ericlitman ericlitman closed this Oct 27, 2026
@ericlitman
ericlitman deleted the review/house79/chunk-09 branch October 27, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant