Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
cbc3c43
Studio: add Codex SDK as a chat provider with parallel-calls fan-out
danielhanchen May 22, 2026
ea7ae85
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2026
4188f91
wip: anthropic citation helper
danielhanchen May 22, 2026
0a4309f
Studio: add codex_router to test_desktop_auth router stub
danielhanchen May 22, 2026
b8cd677
Studio: harden Codex provider against upstream CLI/SDK shape
danielhanchen May 24, 2026
861da31
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
b6577a6
Studio: name the actual PyPI package in the Codex install hint
danielhanchen May 24, 2026
0d904d6
Merge branch 'main' into feat/codex-provider
danielhanchen May 24, 2026
d6c47f6
Studio: wire Codex provider through the UI end-to-end
danielhanchen May 24, 2026
e2ac490
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
028b7b7
Studio: finish Codex UI flow (test, sign-in, parallel tabs)
danielhanchen May 24, 2026
1f7cad4
Merge branch 'feat/codex-provider' of https://github.com/unslothai/un…
danielhanchen May 24, 2026
fd8f25f
Studio: scrub Codex subprocess env, guard partial-stream replay, abor…
danielhanchen May 24, 2026
4b4c855
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
4be807b
Studio: round 4 hardening for the Codex provider
danielhanchen May 24, 2026
67b837e
Merge branch 'feat/codex-provider' of https://github.com/unslothai/un…
danielhanchen May 24, 2026
593fc9e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
1a10765
Studio: round 5 hardening for the Codex provider
danielhanchen May 24, 2026
c5289f0
Studio: pin Codex thread approvals + sandbox to safe defaults
danielhanchen May 24, 2026
8c1c63a
Studio: surface Codex final agent message when no deltas stream
danielhanchen May 24, 2026
9b4bd11
Studio: surface Codex parallel-calls in the connections dialog
danielhanchen May 24, 2026
f97a800
Studio: round 5e Codex hardening (4 follow-ups)
danielhanchen May 24, 2026
cb0680e
Studio: per-tab buffer for Codex fan-out so chunks cannot interleave
danielhanchen May 24, 2026
aa258b9
Studio: account for every Codex turn in fan-out usage chunk
danielhanchen May 24, 2026
8ee6001
Studio: round 6 Codex hardening (5 follow-ups)
danielhanchen May 24, 2026
2874abb
Studio: fail closed when Codex SDK cannot enforce safety pins
danielhanchen May 24, 2026
2eaf1bb
Studio: pass codex_bin to AppServerConfig so PATH-only codex installs…
danielhanchen May 25, 2026
c755d00
Studio: cross-platform CI matrix for Codex provider tests
danielhanchen May 25, 2026
b786238
Studio: render Codex parallel-calls synthesis AFTER the per-tab blocks
danielhanchen May 25, 2026
26799d9
Studio: pre-check Codex default models when creating the connection
danielhanchen May 25, 2026
f05517a
Studio: round 7 Codex hardening (cross-wrapper env scrub + device URL…
danielhanchen May 25, 2026
8cef479
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
dee1b68
Studio: round 7b -- tighten device-login log filter + harden timeout …
danielhanchen May 25, 2026
be15c57
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
b17765b
Studio: round 8 -- replay guard on non-visible events + Add-flow Code…
danielhanchen May 25, 2026
5185032
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
f29faef
Merge branch 'main' into feat/codex-provider
danielhanchen May 27, 2026
3bbbd41
Merge branch 'feat/codex-provider' of https://github.com/unslothai/un…
danielhanchen May 27, 2026
e2b7f59
Studio: round 9 -- three P2 fixes from latest Codex bot review
danielhanchen May 27, 2026
dd0aeec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
ecf8bc7
Merge remote-tracking branch 'origin/main' into feat/codex-provider
danielhanchen May 27, 2026
3d1075f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
f01011e
Studio: real Codex parallel-call tab UI + in-process SDK spoof
danielhanchen May 27, 2026
10d6b69
Merge branch 'feat/codex-provider' of https://github.com/unslothai/un…
danielhanchen May 27, 2026
cd8284d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
6867cfb
Merge remote-tracking branch 'origin/main' into feat/codex-provider
danielhanchen May 27, 2026
6403846
Studio: spoof-aware Codex availability probe + autouse fixture
danielhanchen May 27, 2026
c716af4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions studio/backend/core/inference/codex_availability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""
Codex CLI / SDK availability probe.

This module never imports `codex_app_server` at module top level. The
SDK is optional and not pinned in pyproject.toml -- if it's installed
locally, we use it; if it isn't, the probe simply returns
``installed=False`` and the provider stays hidden in the frontend.

The frontend calls ``GET /api/codex/status`` at startup to decide
whether to surface the "codex" entry in the provider picker. Three
states matter:

* ``installed=False`` -- either the CLI is missing OR the SDK
(``codex_app_server``) is not importable. The picker hides the
entry entirely.
* ``installed=True, logged_in=False`` -- everything resolves on the
Python side but ``codex auth status`` (or equivalent) reports no
active credentials. The provider config dialog shows a
``Sign in to Codex`` button instead of the regular API-key field.
* ``installed=True, logged_in=True`` -- ready to use; the picker
shows the regular model dropdown.

Detection is best-effort and cheap: we shell out to ``which codex``
plus ``codex --version`` for the CLI and use ``importlib.util.find_spec``
for the SDK. No long-running CLI commands are invoked here so the
status endpoint is safe to poll on every page load.
"""

from __future__ import annotations

import asyncio
import importlib.util
import os
import shutil
from typing import Any, Optional

import structlog

logger = structlog.get_logger(__name__)


# Default catalog of models surfaced in the picker when the CLI is
# present but doesn't advertise a list. The SDK accepts arbitrary model
# ids; this is purely a sensible default.
_DEFAULT_SUPPORTED_MODELS: tuple[str, ...] = (
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.5",
"o3",
)


def _which_codex() -> Optional[str]:
"""Return absolute path to the ``codex`` CLI, or None if missing.

Uses :func:`shutil.which` so the lookup honours ``PATH`` exactly
the way the user's shell would. Returns ``None`` on any failure
so callers can treat "missing" and "broken probe" the same way.
"""
try:
return shutil.which("codex")
except Exception as exc:
# shutil.which itself is documented as raising only on
# genuinely unusual conditions, but a hardened wrapper costs
# nothing and keeps the status endpoint from 500'ing.
logger.warning("codex_availability.which_failed", error = str(exc))
return None


def _sdk_importable() -> bool:
"""True iff ``codex_app_server`` is importable in this interpreter.

We deliberately use :func:`importlib.util.find_spec` instead of an
``import codex_app_server`` so the import never actually runs --
that keeps the cost negligible and avoids the SDK's own side
effects (which include reaching out to the CLI subprocess for an
RPC ping) during a simple availability check.
"""
try:
return importlib.util.find_spec("codex_app_server") is not None
except Exception as exc:
logger.warning("codex_availability.find_spec_failed", error = str(exc))
return False


async def _run_cli(args: list[str], *, timeout: float = 4.0) -> tuple[int, str, str]:
"""Run a short ``codex`` CLI command and return (rc, stdout, stderr).

The probe uses 4s as the wall-clock cap because ``codex --version``
and ``codex auth status`` both return in well under a second on a
healthy install. A longer probe would block the
``/api/codex/status`` route -- and that route fires on every chat
page load, so a tight cap matters.
"""
try:
proc = await asyncio.create_subprocess_exec(
"codex",
*args,
stdout = asyncio.subprocess.PIPE,
stderr = asyncio.subprocess.PIPE,
env = os.environ.copy(),
)
except FileNotFoundError:
return -1, "", "codex binary not on PATH"
except Exception as exc:
logger.warning(
"codex_availability.spawn_failed",
args = args,
error = str(exc),
)
return -1, "", str(exc)

try:
stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout = timeout)
except asyncio.TimeoutError:
proc.kill()
try:
await proc.wait()
except Exception:
pass
return -1, "", f"codex {' '.join(args)} timed out after {timeout:.1f}s"

return (
proc.returncode if proc.returncode is not None else -1,
stdout_b.decode("utf-8", errors = "replace").strip(),
stderr_b.decode("utf-8", errors = "replace").strip(),
)


async def _detect_version() -> Optional[str]:
rc, stdout, stderr = await _run_cli(["--version"])
if rc != 0:
return None
# ``codex --version`` prints something like "codex-cli 0.133.0".
# Surface the whole line so the UI can show the exact build the
# user has installed -- it's useful when troubleshooting.
text = stdout or stderr
return text.split("\n", 1)[0].strip() if text else None


async def _detect_logged_in() -> bool:
"""Best-effort: assume a non-zero ``codex auth status`` rc means
"not logged in". The CLI surface has shifted over releases (some
versions print to stderr, some to stdout, some use "Logged in as
..." vs "Not authenticated"). The return code is the most stable
signal we have, so we lean on it and fall back to substring
inspection only when the rc itself is ambiguous (rc=0 but no
output, or rc=-1 from a timeout / crash).
"""
rc, stdout, stderr = await _run_cli(["auth", "status"])
combined = f"{stdout}\n{stderr}".lower()
if rc == 0:
# Some 0.x releases exit 0 even when the user is logged out.
# If we got any output, look for the obvious "not logged in"
# signals; if there's nothing on either pipe at all, treat
# rc=0 as authenticated (the optimistic default).
if not combined.strip():
return True
if "not authenticated" in combined or "not logged in" in combined:
return False
if "logged in" in combined or "authenticated" in combined:
return True
return True
# rc != 0: lean on substring matching one more time before
# defaulting to False, in case a future CLI uses rc=2 for the
# logged-out state instead of "no auth command exists".
if "logged in" in combined or "authenticated as" in combined:
return True
return False


async def probe_codex_availability() -> dict[str, Any]:
"""Return the full status payload consumed by ``GET /api/codex/status``.

Returns a dict with keys:

* ``installed`` (bool) -- True iff both CLI and SDK are present.
Note this is the gate the frontend uses to surface the provider
at all, so if EITHER is missing the picker hides the entry.
* ``cli_path`` (str | None) -- absolute path to the CLI, or None.
* ``sdk_importable`` (bool) -- the Python SDK is importable.
* ``logged_in`` (bool) -- best-effort auth check; meaningless when
``installed`` is False.
* ``version`` (str | None) -- the ``codex --version`` first line.
* ``supported_models`` (list[str]) -- default model id catalog.
"""
cli_path = _which_codex()
sdk_ok = _sdk_importable()

payload: dict[str, Any] = {
"installed": bool(cli_path) and sdk_ok,
"cli_path": cli_path,
"sdk_importable": sdk_ok,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark unsupported Codex SDK variants as unavailable

The status probe reports installed=true whenever CLI+SDK are present, but the request path later hard-fails with CodexUnavailableError if the SDK cannot expose ApprovalMode/SandboxMode (unless an unsafe override env var is set). On those SDK builds, the UI will surface Codex as available and logged in, yet every chat request fails at runtime; the availability check should include this capability gate so installed matches actual usability.

Useful? React with 👍 / 👎.

"logged_in": False,
"version": None,
"supported_models": list(_DEFAULT_SUPPORTED_MODELS),
}

if cli_path:
# version + login probes only matter when the CLI is present;
# they would otherwise just churn subprocess errors. Run them
# in parallel because both are independent CLI invocations.
version, logged_in = await asyncio.gather(
_detect_version(),
_detect_logged_in(),
)
payload["version"] = version
payload["logged_in"] = bool(logged_in)

logger.info(
"codex_availability.probed",
installed = payload["installed"],
sdk_importable = payload["sdk_importable"],
cli_path = payload["cli_path"],
version = payload["version"],
logged_in = payload["logged_in"],
)
return payload
Loading
Loading