Followups on the post-merge review pass for the Codex SDK chat
provider. Verified against codex-cli 0.133.0 + the upstream
`openai/codex` Rust + Python sources, then pinned each fix with
a regression test in `test_codex_provider.py` (24/24 passing).
* Probe both `openai_codex` (canonical upstream Python package at
`openai/codex/sdk/python`) and the legacy `codex_app_server`
alias. Without this the availability probe always reported
`sdk_importable: false` even when the SDK was installed, so the
provider was permanently hidden.
* Switch the device-auth and login-status invocations from
`codex auth login --device-auth` / `codex auth status` to the
real upstream subcommands `codex login --device-auth` and
`codex login status`. The former path returns
`unrecognized subcommand 'auth'` on a real CLI.
* Strip ANSI control sequences before extracting the device URL
(upstream wraps the URL in `\x1b[34m...\x1b[0m`) and tighten the
pattern to the canonical `.../codex/device` shape. Also surface
the one-time code as a `device_code` SSE event so the UI can
show it alongside the URL.
* Fix `_detect_logged_in` substring footgun: `"logged in" in
combined` matched inside `"not logged in"`, flipping logged-out
users to logged-in. Anchor on word boundaries with negative
prefixes winning regardless of return code.
* Cancel in-flight fan-out workers on SSE disconnect. Previously
every parallel Codex turn ran to completion against a
disconnected client and burned quota; now `_stream_codex_parallel`
cancels its worker + drain tasks in a try/finally on
`CancelledError`/`GeneratorExit`.
* Tear down the device-login subprocess on disconnect via
`start_new_session=True` + `os.killpg(SIGTERM)` (Unix) or
`CREATE_NEW_PROCESS_GROUP` + `CTRL_BREAK_EVENT` (Windows), with
a bounded `proc.wait()` and `proc.kill()` fallback. Previously
`finally: await proc.wait()` blocked the SSE close path because
`codex login --device-auth` only exits on user action.
* Render the full conversation transcript in `_last_user_prompt`
instead of returning only the most recent user message. The PR
opens a fresh thread per request so prior assistant turns were
dropped, degrading multi-turn chats to single-shot prompts.
Single-turn input is unchanged.
* Make `ChatCompletionRequest.parallel_calls` default to 1 (`int`
with `ge=1, le=20`) instead of `Optional[int] = None`. The
runtime already coerced `None` -> 1, but the schema now matches
the documented `[1, 20]` range.
* Replace the registry's hardcoded `default_models` (which
contained `o3`, not in the upstream catalog) with the current
`gpt-5.5 / 5.4 / 5.4-mini / 5.3-codex / 5.2` set from
`codex-rs/models-manager/models.json`.
* Stop echoing `str(exc)` in SSE error frames in both
`routes/inference.py` and `routes/codex.py`. The Codex SDK can
raise with local paths, env-var content, or traceback fragments
(CodeQL `py/information-exposure-through-exception`). Surface a
generic message + `exception_type` discriminator; log the full
reason server-side via `logger.error(..., exc_type=..., error=...)`.
Doc / comment updates throughout to refer to `codex login` /
`openai_codex` rather than the older incorrect strings.
Tested: pytest 24 cases in `test_codex_provider.py` (the original
14 + 10 new `TestCodexHardenedRegressions`) plus the rest of the
Studio-backend test suite the PR touches (209 passing). Also
verified live against Studio launched from this branch on a
Blackwell B200 via `UNSLOTH_STUDIO_HOME=$WORKSPACE/temp/...
./install.sh --local` then a Playwright probe.
Wires the OpenAI Codex CLI / Python SDK (codex_app_server) into Studio
as a new chat provider type. Hosts that don't have the CLI or the SDK
installed never see the entry; on logged-out hosts the provider config
dialog renders a device-auth Sign-in button that surfaces the
verification URL and streams CLI progress back over SSE.
Backend
- new core/inference/codex_availability.py probes the CLI + SDK and
reports {installed, logged_in, version, supported_models}; it never
imports codex_app_server at module top level so the rest of the
backend keeps starting cleanly on hosts that don't have the SDK.
- new core/inference/codex_provider.py wraps AsyncCodex and translates
Codex events into OpenAI chat-completion chunks. Supports the
thread.run_streaming path with a non-streaming fallback for older
SDK revs.
- parallel_calls > 1 fans the turn out across N tasks (capped at 20)
via asyncio.gather and emits codex_tab_open / codex_tab_chunk /
codex_tab_close tool-events per attempt plus a final codex_gather
synthesis event. A separate standalone Codex call produces the
unified answer.
- new routes/codex.py exposes GET /api/codex/status and POST
/api/codex/login. The login route shells out to
codex auth login --device-auth and streams events; the first event
carries the verification URL so the frontend can window.open it.
- ChatCompletionRequest gains a parallel_calls field bounded [1, 20]
by pydantic. The codex registry entry stays hidden by default; the
/api/codex/status probe is the authoritative gate.
- routes/inference.py dispatches provider_type=codex through the
local CLI/SDK pipeline instead of the standard HTTP client, with
graceful error surfacing for CodexUnavailableError.
Frontend
- new api/codex-api.ts exposes fetchCodexStatus() and an async
generator streamCodexDeviceLogin() that drives the SSE stream and
yields parsed events.
- new components/codex-parallel-tabs.tsx renders the tabbed parallel-
calls UI with a Synthesis tab highlighted once the codex_gather
event arrives. Pure reducer keeps the state transitions unit-
testable.
- new components/codex-login-button.tsx posts to /api/codex/login,
opens the verification URL in a new tab via window.open, and shows
the streamed CLI log as it lands.
- external-providers.ts exports CODEX_PROVIDER_TYPE,
CODEX_MAX_PARALLEL_CALLS, isCodexProviderType, and
clampCodexParallelCalls. Codex is marked text-only so the composer
hides image-attach affordances when selected.
Tests
- tests/test_codex_provider.py (14 cases) covers the availability
probe across the four install / login states, the streaming +
parallel-calls translation against a fake codex_app_server module
injected into sys.modules, the [1, 20] pydantic clamp, the
CodexUnavailableError surfacing path, and the parallel_calls=1
single-call shape (no tab tool-events).