271 lines
9.3 KiB
Python
271 lines
9.3 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
|
|
"""Local-only Codex SDK spoof for credit-free dev / CI runs.
|
|
|
|
Activated by ``UNSLOTH_CODEX_SPOOF=1`` -- the gate in ``codex_provider``
|
|
swaps in this module's symbols for ``openai_codex`` so the rest of the
|
|
provider can run end-to-end (thread_start, turn().stream(), run_streaming,
|
|
run(), AppServerConfig, ApprovalMode.deny_all, SandboxMode.read_only)
|
|
without ever touching the real CLI or upstream API.
|
|
|
|
The fake stream emits one ``message.delta`` per visible token plus a
|
|
trailing ``ItemCompletedNotification(item=agentMessage)`` so both the
|
|
delta path and the completion-only fallback in
|
|
``_stream_thread_run`` exercise their real branches.
|
|
|
|
The replies are deterministic and tagged with the model + tab index so
|
|
the parallel-calls fan-out shows visibly distinct text per tab, which
|
|
is the point of the tab UI demo. The spoof intentionally does NOT
|
|
emit command / file / tool deltas -- those would be denylisted by
|
|
``_coerce_text`` and never reach the user, and we want the demo to
|
|
show the same shape Codex normally streams: pure agent text.
|
|
|
|
This file is import-safe: it has no side effects on import. It MUST
|
|
never be selected unless the env flag is set explicitly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any, AsyncIterator, Optional
|
|
|
|
|
|
SPOOF_ENV_VAR = "UNSLOTH_CODEX_SPOOF"
|
|
|
|
|
|
def is_spoof_enabled() -> bool:
|
|
"""Return True when the env flag is set to an explicit truthy value."""
|
|
return os.environ.get(SPOOF_ENV_VAR, "").strip().lower() in (
|
|
"1",
|
|
"true",
|
|
"yes",
|
|
"on",
|
|
)
|
|
|
|
|
|
class ApprovalMode(str, Enum):
|
|
deny_all = "deny_all"
|
|
auto_review = "auto_review"
|
|
|
|
|
|
class SandboxMode(str, Enum):
|
|
read_only = "read_only"
|
|
workspace_write = "workspace_write"
|
|
|
|
|
|
@dataclass
|
|
class AppServerConfig:
|
|
env: Optional[dict[str, str]] = None
|
|
codex_bin: Optional[str] = None
|
|
extra: dict[str, Any] = field(default_factory = dict)
|
|
|
|
|
|
@dataclass
|
|
class _AgentMessage:
|
|
type: str = "agentMessage"
|
|
text: str = ""
|
|
|
|
|
|
@dataclass
|
|
class _ItemRoot:
|
|
root: _AgentMessage
|
|
|
|
|
|
@dataclass
|
|
class _ItemCompletedNotification:
|
|
"""Mirrors openai_codex.api.ItemCompletedNotification shape.
|
|
|
|
The class name is matched verbatim by ``_completed_agent_message_text``
|
|
so the completion-only fallback in ``_stream_thread_run`` recognises
|
|
these payloads.
|
|
"""
|
|
|
|
item: _ItemRoot
|
|
type: str = "ItemCompletedNotification"
|
|
|
|
|
|
def _tab_id_from_system(system: Optional[str]) -> int:
|
|
"""Pull the synthetic ``[tab N]`` marker the provider prepends to
|
|
each parallel worker's system prompt (when present), else 0."""
|
|
if not system:
|
|
return 0
|
|
for line in system.splitlines():
|
|
line = line.strip()
|
|
if line.startswith("[tab ") and line.endswith("]"):
|
|
try:
|
|
return int(line[len("[tab ") : -1].split("/")[0])
|
|
except ValueError:
|
|
pass
|
|
return 0
|
|
|
|
|
|
def _spoof_response_text(model: str, prompt: str, tab_id: int) -> str:
|
|
"""Deterministic but visibly per-tab response.
|
|
|
|
Format keeps each parallel worker's reply distinct so when the user
|
|
clicks between tabs they see different text -- the whole point of
|
|
the tab UI demo.
|
|
"""
|
|
prompt_clean = (prompt or "").strip().replace("\n", " ")
|
|
if len(prompt_clean) > 120:
|
|
prompt_clean = prompt_clean[:117] + "..."
|
|
tab_suffix = f" (worker {tab_id})" if tab_id else ""
|
|
return (
|
|
f"[spoof reply from {model}{tab_suffix}] "
|
|
f"You said: {prompt_clean!r}. "
|
|
f"This response is generated by the local Codex spoof "
|
|
f"(UNSLOTH_CODEX_SPOOF=1) -- no upstream tokens were used."
|
|
)
|
|
|
|
|
|
class _TurnStream:
|
|
"""Async iterator returned by ``Turn.stream()``.
|
|
|
|
Emits a sequence of dict-shaped ``message.delta`` events (one word at
|
|
a time, so the chat-adapter's streaming surface gets exercised) and
|
|
closes with an ``ItemCompletedNotification`` carrying the same final
|
|
text. Matches the dual delta + completion shape the real upstream
|
|
SDK emits.
|
|
"""
|
|
|
|
def __init__(self, text: str, delay_s: float = 0.01) -> None:
|
|
self._text = text
|
|
self._delay_s = delay_s
|
|
self._iter: Optional[AsyncIterator[Any]] = None
|
|
|
|
def __aiter__(self) -> "_TurnStream":
|
|
return self
|
|
|
|
async def _generate(self) -> AsyncIterator[Any]:
|
|
# One word at a time gives a visible streaming effect in the UI
|
|
# without flooding the SSE channel.
|
|
words = self._text.split(" ")
|
|
for i, word in enumerate(words):
|
|
chunk = (" " + word) if i > 0 else word
|
|
yield {"type": "message.delta", "delta": chunk}
|
|
if self._delay_s > 0:
|
|
await asyncio.sleep(self._delay_s)
|
|
# Final completion event -- the canonical SDK always emits this,
|
|
# and ``_stream_thread_run`` uses it as its fallback when no
|
|
# deltas arrived (so worth keeping even when deltas did stream).
|
|
yield _ItemCompletedNotification(
|
|
item = _ItemRoot(root = _AgentMessage(text = self._text))
|
|
)
|
|
|
|
async def __anext__(self) -> Any:
|
|
if self._iter is None:
|
|
self._iter = self._generate()
|
|
return await self._iter.__anext__()
|
|
|
|
# Some SDK revs let callers ``async with stream:``. Treat as a no-op.
|
|
async def __aenter__(self) -> "_TurnStream":
|
|
return self
|
|
|
|
async def __aexit__(self, *_exc: Any) -> None:
|
|
return None
|
|
|
|
|
|
class _Turn:
|
|
def __init__(self, text: str) -> None:
|
|
self._text = text
|
|
|
|
def stream(self) -> _TurnStream:
|
|
return _TurnStream(self._text)
|
|
|
|
|
|
class _Thread:
|
|
def __init__(self, model: str, system: Optional[str]) -> None:
|
|
self._model = model
|
|
self._system = system
|
|
self._tab_id = _tab_id_from_system(system)
|
|
|
|
# Canonical path: ``thread.turn(prompt).stream()``.
|
|
def turn(self, prompt: str) -> _Turn:
|
|
text = _spoof_response_text(self._model, prompt, self._tab_id)
|
|
return _Turn(text)
|
|
|
|
# Legacy path: ``async for event in thread.run_streaming(prompt)``.
|
|
def run_streaming(self, prompt: str) -> _TurnStream:
|
|
text = _spoof_response_text(self._model, prompt, self._tab_id)
|
|
return _TurnStream(text)
|
|
|
|
# Buffered fallback: ``await thread.run(prompt)`` returning a result
|
|
# whose ``.text`` (or ``.final_response``) is the answer.
|
|
async def run(self, prompt: str) -> Any:
|
|
from types import SimpleNamespace
|
|
|
|
text = _spoof_response_text(self._model, prompt, self._tab_id)
|
|
await asyncio.sleep(0)
|
|
return SimpleNamespace(text = text, final_response = text)
|
|
|
|
|
|
class AsyncCodex:
|
|
"""Spoof drop-in for ``openai_codex.AsyncCodex``.
|
|
|
|
Accepts the same ``config=AppServerConfig(...)`` constructor signature
|
|
Studio passes through. ``thread_start`` returns a ``_Thread`` whose
|
|
turn / run / run_streaming methods emit deterministic streams.
|
|
"""
|
|
|
|
def __init__(self, config: Optional[AppServerConfig] = None, **_kw: Any) -> None:
|
|
self._config = config or AppServerConfig()
|
|
self._started_at = time.time()
|
|
|
|
async def thread_start(
|
|
self,
|
|
*,
|
|
model: str,
|
|
base_instructions: Optional[str] = None,
|
|
system: Optional[str] = None,
|
|
approval_mode: Optional[ApprovalMode] = None,
|
|
sandbox: Optional[SandboxMode] = None,
|
|
**_extra: Any,
|
|
) -> _Thread:
|
|
await asyncio.sleep(0)
|
|
# Either kwarg path is accepted -- the real provider tries
|
|
# ``base_instructions`` first then falls back to ``system``.
|
|
sys_text = base_instructions if base_instructions is not None else system
|
|
return _Thread(model = model, system = sys_text)
|
|
|
|
|
|
def install_as_openai_codex() -> None:
|
|
"""Insert this module into ``sys.modules`` under the names the real
|
|
SDK would use, so ``importlib.util.find_spec`` succeeds and the
|
|
provider's existing import path picks it up unchanged.
|
|
|
|
Idempotent: a second call is a no-op. Called from ``codex_provider``
|
|
inside ``_import_codex`` when the env flag is set.
|
|
"""
|
|
import sys
|
|
|
|
for name in ("openai_codex", "codex_app_server"):
|
|
if name in sys.modules:
|
|
continue
|
|
sys.modules[name] = _build_module_alias(name)
|
|
|
|
|
|
def _build_module_alias(name: str) -> Any:
|
|
"""Build a module-like object exposing the same public symbols as
|
|
this file, under the requested import name. Using a fresh module
|
|
object (rather than aliasing ``codex_spoof`` directly) means the
|
|
SDK's ``__name__`` lookups (e.g. for ``ImportError`` messages) get
|
|
the real upstream-style name.
|
|
"""
|
|
import types
|
|
import importlib.machinery
|
|
|
|
mod = types.ModuleType(name)
|
|
# ``importlib.util.find_spec(name)`` walks ``sys.modules[name].__spec__``
|
|
# first, so an empty spec is required for the provider's existing
|
|
# availability probe to recognise the spoof.
|
|
mod.__spec__ = importlib.machinery.ModuleSpec(name, loader = None)
|
|
mod.AsyncCodex = AsyncCodex # type: ignore[attr-defined]
|
|
mod.AppServerConfig = AppServerConfig # type: ignore[attr-defined]
|
|
mod.ApprovalMode = ApprovalMode # type: ignore[attr-defined]
|
|
mod.SandboxMode = SandboxMode # type: ignore[attr-defined]
|
|
mod.__spoof__ = True # marker -- tests can assert this
|
|
return mod
|