1. parallel_calls validator now clamps instead of 422-rejecting.
The Pydantic schema was `int Field(ge=1, le=20)`, which was a
regression from the pre-PR OpenAI-extra behaviour: a non-Codex
client that sent the field with a legacy value like 0 (or a
stray string from a misconfigured wrapper) now got a 422 even
though the route silently ignores the field on every non-Codex
provider. Replaced with a `field_validator(mode="before")` that
coerces any input to the [1, 20] range, keeping the schema docs
self-documenting while accepting legacy inputs.
2. Buffered Codex result with `final_response=None` no longer
leaks `TurnResult(...)` Python object repr into the chat. The
upstream SDK documents `TurnResult.final_response` as nullable
for turns that perform tool work without producing a final
assistant message; the previous `... or str(result)` fallback
would render the repr as visible assistant text. New
`_buffered_result_text` helper returns the empty string in that
case so the stream finishes cleanly with no extra content
chunk. Same fix applied to `_run_codex_synthesis`.
3. Device-login SSE no longer forwards arbitrary subprocess output
to the browser. The previous code yielded every CLI line under
`{type:"log"}`, which on a shimmed binary could leak refresh
tokens, auth JSON, or local config paths into the authenticated
stream. Filtered to a known-safe vocabulary ("Welcome to
Codex", "Initializing", "Successfully logged in", etc.).
`device_url` and `device_code` events still fire as before.
4. CodexLoginButton no longer calls `window.open` from inside an
awaited SSE handler. Browser popup blockers (Firefox, Safari,
Chrome strict) silently block popups triggered outside a fresh
user gesture, so the auto-open was unreliable. Replaced with a
prominent "Open verification page" button styled as an anchor;
the click handler is a real user gesture and is never blocked.
The URL string is still shown below the button for copy/paste.
Tests: 46 cases total (was 43). New regressions cover the
parallel_calls clamp path on three garbage inputs, the buffered
TurnResult-with-None-final repr leak guard, and the device-login
log filter (asserts refresh tokens / auth.json paths are dropped
while known-safe progress lines pass through).