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).
92 lines
3.2 KiB
Python
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",
|
|
},
|
|
)
|