Daeseon Yoo
Back to project
·Troubleshoot·1 min

Sparring + AI-audit moved to gpt-5 — and the empty-response trap of reasoning models

Upgraded the two adversarial features (sparring, ai-audit) from gpt-4o to gpt-5. The first deploy 502'd with 'unparseable model response' because gpt-5 spent the entire token budget on hidden reasoning; fixed by raising max_completion_tokens and setting reasoning_effort low.

The two features that benefit most from stronger reasoning — /sparring (the relentless interviewer) and /aiaudit (proposes a design with a planted flaw to catch) — were running on gpt-4o. Changed the default model in both routes to gpt-5 (still overridable via OPENAI_SPARRING_MODEL / OPENAI_AIAUDIT_MODEL). The routes already branched on a /^(gpt-5|o[134])/ regex to send max_completion_tokens instead of max_tokens and drop the custom temperature, so the wiring was there.

First gpt-5 deploy returned 502:

{"error":"unparseable model response"}

This was not an access error — res.ok was true, so OpenAI had returned 200. The trap: gpt-5 is a reasoning model, and the request capped max_completion_tokens at 1600. The hidden reasoning tokens count against that budget, so the model spent all 1600 thinking and returned empty visible content. JSON.parse(content ?? "{}") then got an empty string (not null), threw, and the route fell into its "unparseable" branch.

Fix: for reasoning models, raise max_completion_tokens to 6000 (headroom for reasoning plus the JSON answer) and add reasoning_effort: "low", which caps how much the model thinks — that also holds down latency and cost, which matters for an interactive drill that fires per question. Verified live by POSTing to both endpoints: /api/aiaudit (propose) returned a real design proposal and /api/sparring returned real adversarial scoring, both 200.

One note on verifying access: the production OpenAI key lives only in the ECS task environment, not in the local secrets file, and reading it back out of the running task was (correctly) blocked as a production-secret read. So access was confirmed the safe way — by calling the app's own deployed endpoint, which uses the key server-side — rather than by extracting the key.