Skip to content

Review only: HOUSE-79 upstream v8.0.44, part 7 of 13: sdk handlers and api server - #19

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

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

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: sdk handlers and api server.
  • 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

🛑 4 blocking findings

🔴 Critical · Bound decompressed WebSocket messages before enabling compression

🟡 Medium · Preserve registered speech-model prefixes when routing

🟡 Medium · Match local interrupts to the active response

🟡 Medium · Propagate the Responses conversion refusal

Walkthrough

This review-only slice of the upstream v8.0.44 merge brings the API server and SDK request handlers up to the new upstream behavior. It adds a configurable request-body size limit, response compression (gzip/zstd), and optional keep-alives sent before the first token on streaming endpoints. It also adds response.interrupt and per-turn re-authentication for Responses WebSockets, and new speech/TTS endpoints routed to xAI. Most of the remaining changes move config and plugin-host reads behind locked snapshots so hot reloads no longer race with in-flight requests.

Area What changes Review notes
HTTP server and middleware Configs are cloned at startup and on reload, and access goes through getConfig. New middleware enforces max-request-body-bytes (413) and negotiates gzip/zstd response compression. CORS now lists allowed headers explicitly and exposes ETag. The Redis RESP reader gets size limits and panic recovery, and the connection multiplexer recovers from panics.
Streaming handlers (OpenAI, Claude, Gemini, Responses) Streams can send keep-alives before the first token while waiting on bootstrap. A Claude stream that ends without message_stop, or an OpenAI chat stream that ends without finish_reason, is now reported as a 502 stream error. Responses SSE surfaces write and flush errors and uses delivery tracking to decide whether completion succeeded. Body-read errors keep their HTTP status, e.g. 413. Propagate the Responses conversion refusal
Responses WebSocket A single reader goroutine now owns the client socket. response.interrupt is forwarded to the upstream Codex socket, or used to cancel an in-flight HTTP turn locally. Each turn is re-authenticated through the access manager, and permessage compression can be toggled in config. Match local interrupts to the active response
Bound decompressed WebSocket messages before enabling compression
New endpoints and model catalog Adds /v1/audio/speech and /v1/tts, which map OpenAI-style speech requests to xAI Grok TTS; speech-only models are rejected on other endpoints. Adds /v1/models/{id} detail lookup and xAI image-model prefix routing. Codex and Home catalogs report a tool_search capability, and SIWC OAuth routes are added under v8 management. Preserve registered speech-model prefixes when routing
Advisories (2)
  • 🔵 Low · Identity quality is ignored during compression negotiation · internal/api/response_compression.go:104
    The selection loop ranks only `zstd` and `gzip`; `identityAllowed` records identity as a boolean but its quality is never compared. With compression enabled, `Accept-Encoding: identity;q=1, gzip;q=0.5` therefore returns a gzip-encoded response even though the client explicitly ranks identity higher, violating the negotiated output preference.
    Not blocking because: The confirmed consequence is choosing a lower-ranked but explicitly acceptable encoding. The response content and status remain intact, and the example does not require the client to decode an unsupported encoding.
  • 🔵 Low · Preserve CORS headers on body-limit rejections · internal/api/server.go:176
    The body-limit middleware is registered before `corsMiddleware`. When a cross-origin POST exceeds the configured limit, its early 413 response omits `Access-Control-Allow-Origin`, so browser clients receive a failed fetch instead of being able to inspect the status and error body. Chunked requests rejected by request logging have the same ordering problem. CORS needs to run before these rejecting readers.
    Not blocking because: Only browser visibility of an already-rejected oversized request is lost. Body-limit enforcement remains effective and valid requests are unaffected; the confirmed consequence is reduced error diagnostics, not failed supported generation or weakened access control.
Since the last review
  • New: Preserve registered speech-model prefixes when routing
  • New: Match local interrupts to the active response
  • New: Bound decompressed WebSocket messages before enabling compression
  • New: Propagate the Responses conversion refusal
Repair prompt

Read the Unfret check run for mobilyze-llc/CLIProxyAPI#19 at head 180c8bfb2c84abb65b0a6e6ba61510cb46e14c6f (run panel:180c8bfb2c84abb65b0a6e6ba61510cb46e14c6f:V_Myt5rHGAa:h7CbMdx1cMU, check). Use the check's App id and GET /repos/mobilyze-llc/CLIProxyAPI/check-runs/122333309132; 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 8 min 29 s
broad-opus-5.5-max / review Not recorded Not recorded failed Not recorded
focal-gpt-5.6-sol-xhigh copy / review openai/gpt-6.1-sol → gpt-6.1-sol · primary xhigh succeeded 19 min 11 s
focal-gpt-5.6-sol-xhigh copy 2 / review openai/gpt-6.1-sol → gpt-6.1-sol · primary xhigh succeeded 16 min 25 s
judge / judge openai/gpt-6-astra → gpt-6-astra · primary high succeeded 2 min 23 s

First review of this pull request.
3 reviewers finished.
Commit 180c8bf.
Check run.
Run: GET /unfret/run/panel:180c8bfb2c84abb65b0a6e6ba61510cb46e14c6f:V_Myt5rHGAa:h7CbMdx1cMU.

@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.

🛑 4 blocking findings
🔴 Critical · Bound decompressed WebSocket messages before enabling compression
🟡 Medium · Preserve registered speech-model prefixes when routing
🟡 Medium · Match local interrupts to the active response
🟡 Medium · Propagate the Responses conversion refusal
Check

Comment on lines +107 to +109
return defaultXAISpeechModel, true
case xaiSpeechVoiceModel:
return xaiSpeechVoiceModel, 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 · Preserve registered speech-model prefixes when routing

With an xAI credential prefix of xai and force-model-prefix: true, model registration publishes xai/grok-tts without the bare model. Requesting that advertised ID passes validation, but this return replaces it with grok-tts, causing a 400 model_not_found without executing the credential. The voice-model branch has the same problem. Preserve the registered routing ID when normalizing speech aliases. Also reported for this defect: - Preserve registered speech routing prefixes (sdk/api/handlers/openai/openai_speech_handlers.go:107-109): With force-model-prefix enabled and an xAI credential prefix of xai, the registry publishes xai/grok-tts rather than grok-tts. speechModelBase strips the prefix, and these returns route the request using the unregistered bare ID. Posting the advertised xai/grok-tts model therefore returns 400 'unknown provider for model grok-tts' without executing speech synthesis. The same problem affects grok-voice-tts-1.0. Preserve the registered routing ID separately from upstream model normalization.

Comment on lines +66 to +69
case s.frames <- bytes.Clone(payload):
default:
}
return 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 · Match local interrupts to the active response

The local interrupt queue checks only whether a turn is active, not whether response_id identifies that turn. During an HTTP-backed response r-current, an interrupt naming an unrelated or previous HTTP response is accepted here. completeResponsesWebsocketLocalInterrupt then cancels r-current and emits response.incomplete under the supplied, incorrect ID, which also becomes the continuation ID. This is reproducible with a mismatched response_id; local cancellation needs response-scoped identity checks. Also reported for this defect: - Match local interrupts to the active response ID (sdk/api/handlers/openai/openai_responses_websocket_input.go:66): During an HTTP fallback turn streaming response r-http, an interrupt naming a different response is accepted because deliver checks only active. The forwarder then cancels r-http and emits response.incomplete using the unrelated supplied ID. Consequently, a delayed interrupt from an earlier turn can terminate the current generation. Validate the target response ID before enqueueing the interrupt.

Also affects:

Comment on lines +350 to +351
upgrader.EnableCompression = responsesWebsocketCompressionEnabled.Load()
conn, err := upgrader.Upgrade(c.Writer, c.Request, websocketUpgradeHeaders(c.Request))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Critical · Bound decompressed WebSocket messages before enabling compression

Enabling permessage-deflate while the reader still uses unbounded conn.ReadMessage() introduces a decompression-amplified memory-exhaustion path. An authenticated client can send a small compressed message that allocates arbitrarily large buffers before turn authorization or validation. A reproduction expanded roughly 17 KB on the wire into 16 MiB despite a configured 1 MiB request-body limit; HTTP MaxBytesReader does not cover messages after upgrade. Larger or repeated messages can exhaust the heap and terminate the proxy.

if shouldTreatAsResponsesFormat(rawJSON) {
modelName := gjson.GetBytes(rawJSON, "model").String()
rawJSON = responsesconverter.ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName, rawJSON, stream)
rawJSON, _ = responsesconverter.ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName, rawJSON, stream)

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 · Propagate the Responses conversion refusal

The converter now returns UnsupportedPartError (HTTP 400) when translation would empty a user turn, but this call discards that error. For example, a Responses-format request containing instructions and a user turn consisting solely of input_file.file_url reaches the executor with content:[] instead of being rejected. A reproduction invoked the upstream and returned 200 despite the conversion refusal. Handle the error before entering either execution path.

@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-07 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