Studio: pass codex_bin to AppServerConfig so PATH-only codex installs work
Reproduces with pip install openai-codex --no-deps (lightweight install
that skips the pinned openai-codex-cli-bin runtime) or any host where
the codex CLI is installed via npm i -g @openai/codex / Homebrew /
manual download. Studio constructs AsyncCodex(config=AppServerConfig(
env=...)) without codex_bin, so the SDK runs _installed_codex_path
which 'from codex_cli_bin import bundled_codex_path' and raises
FileNotFoundError: Unable to locate the pinned Codex runtime. Install
the published SDK build with its openai-codex-cli-bin dependency, or
set AppServerConfig.codex_bin explicitly. -- even though a perfectly
good codex is on PATH and was the binary the availability probe
already verified.
Fix: resolve shutil.which("codex") and pass it as
AppServerConfig(codex_bin=...). The PR's availability probe already
returns that exact path in /api/codex/status.cli_path, so we are
giving the SDK back the binary the user can see in the Connections
form. Falls back to AppServerConfig(env=...) (no codex_bin) when the
SDK build does not accept the kwarg yet, and falls back to PATH lookup
returning None on hosts without codex on PATH (in which case the
availability probe would have reported installed=false and Studio
never gets here).
Test fixture: also inject the fake module under openai_codex (the
canonical name the production importer prefers) so the test does not
silently exercise the real SDK on developer venvs that have
pip install openai-codex already done.
End-to-end verified live: pip install -e openai/codex sdk/python plus
Studio with codex CLI on PATH yielded ROUND_TRIP_OK streaming for
gpt-5.4-mini through the OpenAI-compat completions route.
This commit is contained in:
parent
2874abbfec
commit
2eaf1bbd31
2 changed files with 73 additions and 4 deletions
|
|
@ -40,6 +40,7 @@ import asyncio
|
|||
import importlib
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, AsyncGenerator, Optional
|
||||
|
|
@ -186,12 +187,52 @@ class _ScrubbedEnvAsyncCodex:
|
|||
self._restored_via_us.clear()
|
||||
|
||||
|
||||
def _resolve_codex_bin() -> Optional[str]:
|
||||
"""Best-effort PATH lookup for the codex CLI used as ``codex_bin``.
|
||||
|
||||
The upstream Python SDK normally locates its pinned codex binary
|
||||
via the ``openai-codex-cli-bin`` runtime package, installed
|
||||
automatically as a dependency of ``pip install openai-codex``.
|
||||
Users who installed the SDK with ``--no-deps`` (lightweight
|
||||
setups), users whose platform is not yet on the
|
||||
``openai-codex-cli-bin`` wheel matrix, and users whose codex CLI
|
||||
was installed via ``npm i -g @openai/codex`` / Homebrew never
|
||||
have the pinned runtime package on import path. Without an
|
||||
explicit ``codex_bin`` the SDK then raises
|
||||
``FileNotFoundError("Unable to locate the pinned Codex runtime")``
|
||||
even though a perfectly good ``codex`` is on PATH and was the
|
||||
binary Studio's availability probe already verified.
|
||||
|
||||
Returning ``shutil.which("codex")`` here turns that hard failure
|
||||
into a working session: Studio passes the resolved path through
|
||||
``AppServerConfig(codex_bin=...)`` and the SDK uses it directly.
|
||||
Returning ``None`` keeps the pinned-runtime path intact when the
|
||||
CLI is not on PATH (which only happens on hosts where the SDK is
|
||||
importable but the CLI is missing -- ``codex_availability`` would
|
||||
already report ``installed=false`` there, so callers never reach
|
||||
this).
|
||||
"""
|
||||
try:
|
||||
return shutil.which("codex")
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"codex_provider.codex_bin_lookup_failed",
|
||||
exc_type = type(exc).__name__,
|
||||
error = str(exc),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _open_async_codex(async_codex_cls: Any) -> Any:
|
||||
"""Construct an AsyncCodex whose spawned app-server cannot see
|
||||
Studio's secrets.
|
||||
|
||||
Preferred path: `AsyncCodex(config=AppServerConfig(env=...))`
|
||||
which scopes the override to the spawned subprocess only.
|
||||
Preferred path: `AsyncCodex(config=AppServerConfig(env=...,
|
||||
codex_bin=...))` which scopes the env override to the spawned
|
||||
subprocess only and explicitly pins the codex binary so the SDK
|
||||
does not need its pinned ``openai-codex-cli-bin`` runtime to be
|
||||
installed.
|
||||
|
||||
Fail-closed fallback: `_ScrubbedEnvAsyncCodex` swaps `os.environ`
|
||||
for the lifetime of the session so the SDK's internal
|
||||
`os.environ.copy()` spawn never sees HF_TOKEN / GH_TOKEN /
|
||||
|
|
@ -203,8 +244,27 @@ def _open_async_codex(async_codex_cls: Any) -> Any:
|
|||
if sdk_mod is not None:
|
||||
app_server_config = getattr(sdk_mod, "AppServerConfig", None)
|
||||
if app_server_config is not None:
|
||||
# Try the modern signature: AppServerConfig(env=..., codex_bin=...).
|
||||
# codex_bin keeps PATH-installed codex working without the
|
||||
# SDK's pinned openai-codex-cli-bin runtime package. We try
|
||||
# the full signature first, then degrade gracefully if the
|
||||
# installed SDK build does not accept codex_bin yet.
|
||||
codex_bin = _resolve_codex_bin()
|
||||
env_override = _codex_sdk_env_override()
|
||||
if codex_bin is not None:
|
||||
try:
|
||||
return async_codex_cls(
|
||||
config = app_server_config(
|
||||
env = env_override,
|
||||
codex_bin = codex_bin,
|
||||
),
|
||||
)
|
||||
except TypeError:
|
||||
# Older SDK build: codex_bin kwarg unknown. Fall
|
||||
# through to env-only construction below.
|
||||
pass
|
||||
return async_codex_cls(
|
||||
config = app_server_config(env = _codex_sdk_env_override()),
|
||||
config = app_server_config(env = env_override),
|
||||
)
|
||||
except TypeError:
|
||||
# Older SDK: AppServerConfig may not accept the env kwarg yet.
|
||||
|
|
|
|||
|
|
@ -141,7 +141,16 @@ def _install_fake_codex_sdk(monkeypatch, async_codex_cls, *, with_safety_enums =
|
|||
workspace_write = "WORKSPACE_WRITE",
|
||||
danger_full_access = "DANGER_FULL_ACCESS",
|
||||
)
|
||||
# Inject the fake under BOTH module names the production importer
|
||||
# checks. ``openai_codex`` is the canonical upstream name and is
|
||||
# preferred by the lazy-import gate; ``codex_app_server`` is the
|
||||
# legacy / Rust-crate alias. Hosts that have ``openai_codex``
|
||||
# actually installed (developer venvs, CI runners after the PR's
|
||||
# `pip install openai-codex`) would otherwise bypass the fake and
|
||||
# exercise the real SDK -- the same fake must be reachable under
|
||||
# both names for the test to be deterministic.
|
||||
monkeypatch.setitem(sys.modules, "codex_app_server", fake_mod)
|
||||
monkeypatch.setitem(sys.modules, "openai_codex", fake_mod)
|
||||
# importlib.util.find_spec walks finders, not sys.modules; patch
|
||||
# it directly so the lazy-import gate accepts the fake.
|
||||
import importlib.util as _iu
|
||||
|
|
@ -149,7 +158,7 @@ def _install_fake_codex_sdk(monkeypatch, async_codex_cls, *, with_safety_enums =
|
|||
real_find_spec = _iu.find_spec
|
||||
|
||||
def _shim(name: str, *args, **kwargs):
|
||||
if name == "codex_app_server":
|
||||
if name in ("codex_app_server", "openai_codex"):
|
||||
return types.SimpleNamespace()
|
||||
return real_find_spec(name, *args, **kwargs)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue