unsloth/studio/backend/routes/codex.py
Daniel Han cbc3c43655 Studio: add Codex SDK as a chat provider with parallel-calls fan-out
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).
2026-05-23 14:00:31 +00:00

92 lines
3.2 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""
API routes for the Codex SDK chat provider.
Two endpoints live here:
* ``GET /api/codex/status`` -- the availability probe. The frontend
hits this at chat-page load time and uses ``installed`` to decide
whether to surface the "codex" entry in the provider picker. When
``installed=True`` but ``logged_in=False``, the provider config
dialog shows the "Sign in to Codex" affordance instead of the
regular API-key field.
* ``POST /api/codex/login`` -- the device-auth helper. Spawns the
``codex auth login --device-auth`` CLI command, captures the
verification URL from its output, and streams the rest of the auth
exchange back as SSE so the UI can show progress. The URL appears
in the first SSE event so the frontend can ``window.open`` it before
the user wanders off.
"""
from __future__ import annotations
import json
from typing import AsyncGenerator
import structlog
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from auth.authentication import get_current_subject
from core.inference.codex_availability import probe_codex_availability
from core.inference.codex_provider import stream_codex_device_login
logger = structlog.get_logger(__name__)
router = APIRouter()
@router.get("/status")
async def get_codex_status(
current_subject: str = Depends(get_current_subject),
) -> dict:
"""Return the Codex CLI / SDK availability snapshot.
The frontend gates the provider entry on ``installed`` and gates
the "Sign in to Codex" button on ``logged_in``. Both are
best-effort and cheap to recompute; the route does not cache the
probe because the user can install the CLI / SDK or run
``codex auth login`` between page loads and the picker should pick
that up on the next refresh.
"""
return await probe_codex_availability()
@router.post("/login")
async def codex_device_login(
current_subject: str = Depends(get_current_subject),
) -> StreamingResponse:
"""Stream the ``codex auth login --device-auth`` exchange.
Returns an SSE stream of events:
``data: {"type": "device_url", "url": "https://..."}``
``data: {"type": "log", "line": "..."}`` (zero or more)
``data: {"type": "done", "ok": true}``
The frontend opens the device URL in a new tab via
``window.open(url, "_blank", "noopener,noreferrer")`` as soon as
the first event arrives, then renders the streamed log lines so
the user can see the CLI making progress while they're at the
verification page.
"""
async def _to_sse() -> AsyncGenerator[str, None]:
async for event in stream_codex_device_login():
yield f"data: {json.dumps(event)}\n\n"
# Frontend treats the trailing [DONE] the same way it does for
# chat streams, so we emit it for parity.
yield "data: [DONE]\n\n"
return StreamingResponse(
_to_sse(),
media_type = "text/event-stream",
headers = {
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)