mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Python sdk
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from openai_codex_sdk.exec import CodexExecArgs
|
||||
|
||||
from .responses_proxy import FakeExec, ResponsesProxy
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CodexExecSpyResult:
|
||||
args: list[CodexExecArgs]
|
||||
restore: Callable[[], None]
|
||||
|
||||
|
||||
def install_codex_exec_spy(monkeypatch: MonkeyPatch, proxy: ResponsesProxy) -> CodexExecSpyResult:
|
||||
calls: list[CodexExecArgs] = []
|
||||
|
||||
def factory(path: str) -> FakeExec:
|
||||
return FakeExec(path, proxy, calls)
|
||||
|
||||
monkeypatch.setattr("openai_codex_sdk.codex.CodexExec", factory)
|
||||
|
||||
return CodexExecSpyResult(args=calls, restore=monkeypatch.undo)
|
||||
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from .codex_exec_spy import CodexExecSpyResult, install_codex_exec_spy
|
||||
from .responses_proxy import ResponsesProxy, ResponsesProxyOptions, start_responses_test_proxy
|
||||
|
||||
ProxyFactory = Callable[[ResponsesProxyOptions], Awaitable[ResponsesProxy]]
|
||||
SpyFactory = Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def make_responses_proxy() -> AsyncIterator[ProxyFactory]:
|
||||
proxies: list[ResponsesProxy] = []
|
||||
|
||||
async def _make(options: ResponsesProxyOptions) -> ResponsesProxy:
|
||||
proxy = await start_responses_test_proxy(options)
|
||||
proxies.append(proxy)
|
||||
return proxy
|
||||
|
||||
try:
|
||||
yield _make
|
||||
finally:
|
||||
for proxy in proxies:
|
||||
await proxy.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def codex_exec_spy(monkeypatch: MonkeyPatch) -> SpyFactory:
|
||||
def _install(proxy: ResponsesProxy) -> CodexExecSpyResult:
|
||||
return install_codex_exec_spy(monkeypatch, proxy)
|
||||
|
||||
return _install
|
||||
@@ -0,0 +1,210 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, AsyncGenerator, TypedDict
|
||||
|
||||
from openai_codex_sdk.exec import CodexExecArgs
|
||||
|
||||
DEFAULT_RESPONSE_ID = "resp_mock"
|
||||
DEFAULT_MESSAGE_ID = "msg_mock"
|
||||
|
||||
|
||||
class SseEvent(TypedDict, total=False):
|
||||
type: str
|
||||
item: dict[str, Any]
|
||||
response: dict[str, Any]
|
||||
|
||||
|
||||
class SseResponseBody(TypedDict):
|
||||
kind: str
|
||||
events: list[SseEvent]
|
||||
|
||||
|
||||
class ResponsesProxyOptions(TypedDict, total=False):
|
||||
response_bodies: list[SseResponseBody]
|
||||
status_code: int
|
||||
|
||||
|
||||
class RecordedRequest(TypedDict):
|
||||
body: str
|
||||
json: dict[str, Any]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ResponsesProxy:
|
||||
response_bodies: list[SseResponseBody]
|
||||
status_code: int
|
||||
requests: list[RecordedRequest]
|
||||
_response_index: int = field(init=False, default=0)
|
||||
_thread_counter: itertools.count = field(init=False, default_factory=lambda: itertools.count(1))
|
||||
_thread_histories: dict[str, list[str]] = field(init=False, default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.response_bodies:
|
||||
raise ValueError("response_bodies is required")
|
||||
|
||||
async def close(self) -> None:
|
||||
await asyncio.sleep(0)
|
||||
|
||||
def _next_thread_id(self) -> str:
|
||||
return f"thread_{next(self._thread_counter)}"
|
||||
|
||||
def _next_response(self) -> SseResponseBody:
|
||||
index = min(self._response_index, len(self.response_bodies) - 1)
|
||||
self._response_index += 1
|
||||
return self.response_bodies[index]
|
||||
|
||||
def _build_request(self, args: CodexExecArgs, thread_id: str) -> RecordedRequest:
|
||||
history = self._thread_histories.get(thread_id, [])
|
||||
input_entries: list[dict[str, Any]] = []
|
||||
for text in history:
|
||||
input_entries.append(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": text,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
input_entries.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": args.input,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
request_json: dict[str, Any] = {"input": input_entries}
|
||||
if args.model is not None:
|
||||
request_json["model"] = args.model
|
||||
|
||||
recorded = RecordedRequest(body=json.dumps(request_json), json=request_json)
|
||||
self.requests.append(recorded)
|
||||
return recorded
|
||||
|
||||
def record_run(self, args: CodexExecArgs) -> tuple[str, RecordedRequest, bool]:
|
||||
if args.thread_id:
|
||||
thread_id = args.thread_id
|
||||
new_thread = False
|
||||
else:
|
||||
thread_id = self._next_thread_id()
|
||||
new_thread = True
|
||||
request = self._build_request(args, thread_id)
|
||||
return thread_id, request, new_thread
|
||||
|
||||
def add_history(self, thread_id: str, text: str) -> None:
|
||||
self._thread_histories.setdefault(thread_id, []).append(text)
|
||||
|
||||
def _convert_events(
|
||||
self, response_body: SseResponseBody, thread_id: str, new_thread: bool
|
||||
) -> list[dict[str, Any]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
if new_thread:
|
||||
events.append({"type": "thread.started", "thread_id": thread_id})
|
||||
|
||||
for event in response_body["events"]:
|
||||
if event["type"] == "response.created":
|
||||
events.append({"type": "turn.started"})
|
||||
elif event["type"] == "response.output_item.done":
|
||||
item = event["item"]
|
||||
text = item["content"][0]["text"]
|
||||
events.append(
|
||||
{
|
||||
"type": "item.completed",
|
||||
"item": {
|
||||
"id": item["id"],
|
||||
"item_type": "assistant_message",
|
||||
"text": text,
|
||||
},
|
||||
}
|
||||
)
|
||||
elif event["type"] == "response.completed":
|
||||
events.append(
|
||||
{
|
||||
"type": "turn.completed",
|
||||
"usage": {
|
||||
"input_tokens": 0,
|
||||
"cached_input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
},
|
||||
}
|
||||
)
|
||||
return events
|
||||
|
||||
def next_events(self, thread_id: str, new_thread: bool) -> list[dict[str, Any]]:
|
||||
response_body = self._next_response()
|
||||
return self._convert_events(response_body, thread_id, new_thread)
|
||||
|
||||
|
||||
class FakeExec:
|
||||
def __init__(self, _path: str, proxy: ResponsesProxy, calls: list[CodexExecArgs]) -> None:
|
||||
self._proxy = proxy
|
||||
self.calls = calls
|
||||
|
||||
async def run(self, args: CodexExecArgs) -> AsyncGenerator[str, None]:
|
||||
self.calls.append(args)
|
||||
thread_id, _request, new_thread = self._proxy.record_run(args)
|
||||
events = self._proxy.next_events(thread_id, new_thread)
|
||||
|
||||
for event in events:
|
||||
if event["type"] == "item.completed":
|
||||
item = event["item"]
|
||||
text = item.get("text")
|
||||
if text:
|
||||
self._proxy.add_history(thread_id, text)
|
||||
await asyncio.sleep(0)
|
||||
yield json.dumps(event)
|
||||
|
||||
|
||||
async def start_responses_test_proxy(options: ResponsesProxyOptions) -> ResponsesProxy:
|
||||
response_bodies = options.get("response_bodies")
|
||||
if response_bodies is None:
|
||||
raise ValueError("response_bodies is required")
|
||||
status_code = options.get("status_code", 200)
|
||||
proxy = ResponsesProxy(response_bodies, status_code, requests=[])
|
||||
return proxy
|
||||
|
||||
|
||||
def sse(*events: SseEvent) -> SseResponseBody:
|
||||
return {"kind": "sse", "events": list(events)}
|
||||
|
||||
|
||||
def response_started(response_id: str = DEFAULT_RESPONSE_ID) -> SseEvent:
|
||||
return {
|
||||
"type": "response.created",
|
||||
"response": {"id": response_id},
|
||||
}
|
||||
|
||||
|
||||
def assistant_message(text: str, item_id: str = DEFAULT_MESSAGE_ID) -> SseEvent:
|
||||
return {
|
||||
"type": "response.output_item.done",
|
||||
"item": {
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"id": item_id,
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": text,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def response_completed(response_id: str = DEFAULT_RESPONSE_ID) -> SseEvent:
|
||||
return {
|
||||
"type": "response.completed",
|
||||
"response": {"id": response_id},
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator, Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from openai_codex_sdk import Codex, CodexOptions
|
||||
from openai_codex_sdk.events import ThreadEvent
|
||||
|
||||
from .codex_exec_spy import CodexExecSpyResult
|
||||
from .responses_proxy import (
|
||||
ResponsesProxy,
|
||||
assistant_message,
|
||||
response_completed,
|
||||
response_started,
|
||||
sse,
|
||||
)
|
||||
|
||||
CODEX_EXEC_PATH = Path(__file__).resolve().parents[2] / "codex-rs" / "target" / "debug" / "codex"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_thread_events(
|
||||
make_responses_proxy, codex_exec_spy: Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
) -> None:
|
||||
proxy = await make_responses_proxy(
|
||||
{
|
||||
"status_code": 200,
|
||||
"response_bodies": [
|
||||
sse(
|
||||
response_started(),
|
||||
assistant_message("Hi!"),
|
||||
response_completed(),
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
codex_exec_spy(proxy)
|
||||
|
||||
client = Codex(CodexOptions(executable_path=str(CODEX_EXEC_PATH), base_url="http://proxy", api_key="test"))
|
||||
|
||||
thread = client.start_thread()
|
||||
result = await thread.run_streamed("Hello, world!")
|
||||
|
||||
events: list[ThreadEvent] = []
|
||||
async for event in result.events:
|
||||
events.append(event)
|
||||
|
||||
assert events == [
|
||||
{
|
||||
"type": "thread.started",
|
||||
"thread_id": "thread_1",
|
||||
},
|
||||
{"type": "turn.started"},
|
||||
{
|
||||
"type": "item.completed",
|
||||
"item": {
|
||||
"id": "msg_mock",
|
||||
"item_type": "assistant_message",
|
||||
"text": "Hi!",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "turn.completed",
|
||||
"usage": {
|
||||
"input_tokens": 0,
|
||||
"cached_input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
assert thread.id == "thread_1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sends_previous_items_when_run_streamed_called_twice(
|
||||
make_responses_proxy, codex_exec_spy: Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
) -> None:
|
||||
proxy = await make_responses_proxy(
|
||||
{
|
||||
"status_code": 200,
|
||||
"response_bodies": [
|
||||
sse(
|
||||
response_started("response_1"),
|
||||
assistant_message("First response", "item_1"),
|
||||
response_completed("response_1"),
|
||||
),
|
||||
sse(
|
||||
response_started("response_2"),
|
||||
assistant_message("Second response", "item_2"),
|
||||
response_completed("response_2"),
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
codex_exec_spy(proxy)
|
||||
|
||||
client = Codex(CodexOptions(executable_path=str(CODEX_EXEC_PATH), base_url="http://proxy", api_key="test"))
|
||||
|
||||
thread = client.start_thread()
|
||||
first = await thread.run_streamed("first input")
|
||||
await _drain_events(first.events)
|
||||
|
||||
second = await thread.run_streamed("second input")
|
||||
await _drain_events(second.events)
|
||||
|
||||
assert len(proxy.requests) >= 2
|
||||
second_request = proxy.requests[1]
|
||||
payload = second_request["json"]
|
||||
assistant_entry = next((entry for entry in payload["input"] if entry["role"] == "assistant"), None)
|
||||
assert assistant_entry is not None
|
||||
assistant_text = next(
|
||||
(item["text"] for item in assistant_entry.get("content", []) if item.get("type") == "output_text"),
|
||||
None,
|
||||
)
|
||||
assert assistant_text == "First response"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resumes_thread_by_id_when_streaming(
|
||||
make_responses_proxy, codex_exec_spy: Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
) -> None:
|
||||
proxy = await make_responses_proxy(
|
||||
{
|
||||
"status_code": 200,
|
||||
"response_bodies": [
|
||||
sse(
|
||||
response_started("response_1"),
|
||||
assistant_message("First response", "item_1"),
|
||||
response_completed("response_1"),
|
||||
),
|
||||
sse(
|
||||
response_started("response_2"),
|
||||
assistant_message("Second response", "item_2"),
|
||||
response_completed("response_2"),
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
codex_exec_spy(proxy)
|
||||
|
||||
client = Codex(CodexOptions(executable_path=str(CODEX_EXEC_PATH), base_url="http://proxy", api_key="test"))
|
||||
|
||||
original_thread = client.start_thread()
|
||||
first = await original_thread.run_streamed("first input")
|
||||
await _drain_events(first.events)
|
||||
|
||||
resumed_thread = client.resume_thread(original_thread.id or "")
|
||||
second = await resumed_thread.run_streamed("second input")
|
||||
await _drain_events(second.events)
|
||||
|
||||
assert resumed_thread.id == original_thread.id
|
||||
|
||||
assert len(proxy.requests) >= 2
|
||||
second_request = proxy.requests[1]
|
||||
payload = second_request["json"]
|
||||
assistant_entry = next((entry for entry in payload["input"] if entry["role"] == "assistant"), None)
|
||||
assert assistant_entry is not None
|
||||
assistant_text = next(
|
||||
(item["text"] for item in assistant_entry.get("content", []) if item.get("type") == "output_text"),
|
||||
None,
|
||||
)
|
||||
assert assistant_text == "First response"
|
||||
|
||||
|
||||
async def _drain_events(events: AsyncGenerator[ThreadEvent, None]) -> None:
|
||||
async for _ in events:
|
||||
pass
|
||||
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from openai_codex_sdk import Codex, CodexOptions
|
||||
from openai_codex_sdk.turn_options import TurnOptions
|
||||
|
||||
from .codex_exec_spy import CodexExecSpyResult
|
||||
from .responses_proxy import (
|
||||
ResponsesProxy,
|
||||
assistant_message,
|
||||
response_completed,
|
||||
response_started,
|
||||
sse,
|
||||
)
|
||||
|
||||
CODEX_EXEC_PATH = Path(__file__).resolve().parents[2] / "codex-rs" / "target" / "debug" / "codex"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_thread_events(
|
||||
make_responses_proxy, codex_exec_spy: Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
) -> None:
|
||||
proxy = await make_responses_proxy(
|
||||
{
|
||||
"status_code": 200,
|
||||
"response_bodies": [
|
||||
sse(
|
||||
response_started(),
|
||||
assistant_message("Hi!"),
|
||||
response_completed(),
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
spy = codex_exec_spy(proxy)
|
||||
|
||||
client = Codex(CodexOptions(executable_path=str(CODEX_EXEC_PATH), base_url="http://proxy", api_key="test"))
|
||||
thread = client.start_thread()
|
||||
|
||||
result = await thread.run("Hello, world!")
|
||||
|
||||
expected_items = [
|
||||
{
|
||||
"id": "msg_mock",
|
||||
"item_type": "assistant_message",
|
||||
"text": "Hi!",
|
||||
}
|
||||
]
|
||||
assert result.items == expected_items
|
||||
assert thread.id is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sends_previous_items_when_run_called_twice(
|
||||
make_responses_proxy, codex_exec_spy: Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
) -> None:
|
||||
proxy = await make_responses_proxy(
|
||||
{
|
||||
"status_code": 200,
|
||||
"response_bodies": [
|
||||
sse(
|
||||
response_started("response_1"),
|
||||
assistant_message("First response", "item_1"),
|
||||
response_completed("response_1"),
|
||||
),
|
||||
sse(
|
||||
response_started("response_2"),
|
||||
assistant_message("Second response", "item_2"),
|
||||
response_completed("response_2"),
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
codex_exec_spy(proxy)
|
||||
|
||||
client = Codex(CodexOptions(executable_path=str(CODEX_EXEC_PATH), base_url="http://proxy", api_key="test"))
|
||||
|
||||
thread = client.start_thread()
|
||||
await thread.run("first input")
|
||||
await thread.run("second input")
|
||||
|
||||
assert len(proxy.requests) >= 2
|
||||
second_request = proxy.requests[1]
|
||||
payload = second_request["json"]
|
||||
assistant_entry = next((entry for entry in payload["input"] if entry["role"] == "assistant"), None)
|
||||
assert assistant_entry is not None
|
||||
assistant_text = next(
|
||||
(item["text"] for item in assistant_entry.get("content", []) if item.get("type") == "output_text"),
|
||||
None,
|
||||
)
|
||||
assert assistant_text == "First response"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_continues_thread_with_options(
|
||||
make_responses_proxy, codex_exec_spy: Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
) -> None:
|
||||
proxy = await make_responses_proxy(
|
||||
{
|
||||
"status_code": 200,
|
||||
"response_bodies": [
|
||||
sse(
|
||||
response_started("response_1"),
|
||||
assistant_message("First response", "item_1"),
|
||||
response_completed("response_1"),
|
||||
),
|
||||
sse(
|
||||
response_started("response_2"),
|
||||
assistant_message("Second response", "item_2"),
|
||||
response_completed("response_2"),
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
codex_exec_spy(proxy)
|
||||
|
||||
client = Codex(CodexOptions(executable_path=str(CODEX_EXEC_PATH), base_url="http://proxy", api_key="test"))
|
||||
|
||||
thread = client.start_thread()
|
||||
await thread.run("first input")
|
||||
await thread.run("second input", TurnOptions(model="gpt-test-1"))
|
||||
|
||||
assert len(proxy.requests) >= 2
|
||||
second_request = proxy.requests[1]
|
||||
payload = second_request["json"]
|
||||
assert payload.get("model") == "gpt-test-1"
|
||||
assistant_entry = next((entry for entry in payload["input"] if entry["role"] == "assistant"), None)
|
||||
assert assistant_entry is not None
|
||||
assistant_text = next(
|
||||
(item["text"] for item in assistant_entry.get("content", []) if item.get("type") == "output_text"),
|
||||
None,
|
||||
)
|
||||
assert assistant_text == "First response"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resumes_thread_by_id(
|
||||
make_responses_proxy, codex_exec_spy: Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
) -> None:
|
||||
proxy = await make_responses_proxy(
|
||||
{
|
||||
"status_code": 200,
|
||||
"response_bodies": [
|
||||
sse(
|
||||
response_started("response_1"),
|
||||
assistant_message("First response", "item_1"),
|
||||
response_completed("response_1"),
|
||||
),
|
||||
sse(
|
||||
response_started("response_2"),
|
||||
assistant_message("Second response", "item_2"),
|
||||
response_completed("response_2"),
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
codex_exec_spy(proxy)
|
||||
|
||||
client = Codex(CodexOptions(executable_path=str(CODEX_EXEC_PATH), base_url="http://proxy", api_key="test"))
|
||||
|
||||
original_thread = client.start_thread()
|
||||
await original_thread.run("first input")
|
||||
|
||||
resumed_thread = client.resume_thread(original_thread.id or "")
|
||||
result = await resumed_thread.run("second input")
|
||||
|
||||
assert resumed_thread.id == original_thread.id
|
||||
assert result.final_response == "Second response"
|
||||
assert len(proxy.requests) >= 2
|
||||
second_request = proxy.requests[1]
|
||||
payload = second_request["json"]
|
||||
assistant_entry = next((entry for entry in payload["input"] if entry["role"] == "assistant"), None)
|
||||
assert assistant_entry is not None
|
||||
assistant_text = next(
|
||||
(item["text"] for item in assistant_entry.get("content", []) if item.get("type") == "output_text"),
|
||||
None,
|
||||
)
|
||||
assert assistant_text == "First response"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passes_turn_options_to_exec(
|
||||
make_responses_proxy, codex_exec_spy: Callable[[ResponsesProxy], CodexExecSpyResult]
|
||||
) -> None:
|
||||
proxy = await make_responses_proxy(
|
||||
{
|
||||
"status_code": 200,
|
||||
"response_bodies": [
|
||||
sse(
|
||||
response_started("response_1"),
|
||||
assistant_message("Turn options applied", "item_1"),
|
||||
response_completed("response_1"),
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
spy = codex_exec_spy(proxy)
|
||||
|
||||
client = Codex(CodexOptions(executable_path=str(CODEX_EXEC_PATH), base_url="http://proxy", api_key="test"))
|
||||
|
||||
thread = client.start_thread()
|
||||
await thread.run(
|
||||
"apply options",
|
||||
TurnOptions(model="gpt-test-1", sandbox_mode="workspace-write"),
|
||||
)
|
||||
|
||||
assert proxy.requests
|
||||
payload = proxy.requests[0]["json"]
|
||||
assert payload.get("model") == "gpt-test-1"
|
||||
|
||||
assert spy.args
|
||||
command_args = spy.args[0]
|
||||
assert command_args.sandbox_mode == "workspace-write"
|
||||
assert command_args.model == "gpt-test-1"
|
||||
Reference in New Issue
Block a user