mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-01 15:36:08 +00:00
The plan backend 400s on max_output_tokens/temperature/top_p — strip them in the provider; autotitle failures now log at warning, not invisible debug.
586 lines
21 KiB
Python
586 lines
21 KiB
Python
"""ChatGPT-subscription provider (`openai-codex`): PKCE flow, token storage +
|
|
refresh, backend request shape, failure modes, and the REST surface. No live
|
|
network — the token endpoint, the SDK client, and the verify probe are all faked;
|
|
the loopback callback server is exercised for real on its fixed port."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
import json
|
|
import socket
|
|
import time
|
|
from types import SimpleNamespace
|
|
from urllib.parse import parse_qs, urlsplit
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from coworker.providers import codex_auth
|
|
from coworker.providers.codex_auth import (
|
|
CODEX_BASE_URL,
|
|
CodexAuthError,
|
|
CodexSignInRequired,
|
|
CodexTokenStore,
|
|
account_id_from,
|
|
build_authorize_url,
|
|
create_pkce,
|
|
)
|
|
from coworker.providers.codex_provider import CodexProvider
|
|
from coworker.secrets import SecretStore
|
|
from coworker.server.app import create_app
|
|
from coworker.server.manager import SessionManager
|
|
|
|
ACCOUNT_CLAIM = "https://api.openai.com/auth"
|
|
|
|
|
|
def _jwt(claims: dict) -> str:
|
|
def b64(obj) -> str:
|
|
raw = json.dumps(obj).encode()
|
|
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
|
|
|
return f"{b64({'alg': 'none'})}.{b64(claims)}.sig"
|
|
|
|
|
|
def _access(exp_offset: float = 3600, account: str = "acct_1") -> str:
|
|
return _jwt(
|
|
{"exp": time.time() + exp_offset, ACCOUNT_CLAIM: {"chatgpt_account_id": account}}
|
|
)
|
|
|
|
|
|
def _id_token(email: str = "user@example.com", account: str = "acct_1") -> str:
|
|
return _jwt({"email": email, ACCOUNT_CLAIM: {"chatgpt_account_id": account}})
|
|
|
|
|
|
def _token_response(status: int = 200, body: dict | None = None):
|
|
return SimpleNamespace(status_code=status, json=lambda: body or {})
|
|
|
|
|
|
def _seed(secrets, exp_offset: float = 3600) -> None:
|
|
CodexTokenStore(secrets).save(
|
|
{
|
|
"access_token": _access(exp_offset),
|
|
"refresh_token": "rt-1",
|
|
"id_token": _id_token(),
|
|
}
|
|
)
|
|
|
|
|
|
# -- PKCE / authorize URL -----------------------------------------------------------
|
|
|
|
|
|
def test_pkce_challenge_is_s256_of_verifier():
|
|
import hashlib
|
|
|
|
verifier, challenge = create_pkce()
|
|
expected = (
|
|
base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest())
|
|
.rstrip(b"=")
|
|
.decode()
|
|
)
|
|
assert challenge == expected
|
|
assert create_pkce()[0] != verifier # fresh randomness per flow
|
|
|
|
|
|
def test_authorize_url_shape():
|
|
url = build_authorize_url("st4te", "ch4llenge")
|
|
parts = urlsplit(url)
|
|
assert f"{parts.scheme}://{parts.netloc}{parts.path}" == codex_auth.AUTHORIZE_URL
|
|
q = {k: v[0] for k, v in parse_qs(parts.query).items()}
|
|
assert q["response_type"] == "code"
|
|
assert q["client_id"] == codex_auth.CLIENT_ID
|
|
assert q["redirect_uri"] == "http://localhost:1455/auth/callback"
|
|
assert q["state"] == "st4te"
|
|
assert q["code_challenge"] == "ch4llenge"
|
|
assert q["code_challenge_method"] == "S256"
|
|
assert q["codex_cli_simplified_flow"] == "true"
|
|
assert q["originator"] == codex_auth.ORIGINATOR
|
|
|
|
|
|
def test_account_id_from_token_claim():
|
|
tokens = {"id_token": _id_token(account="acct_9")}
|
|
assert account_id_from(tokens) == "acct_9"
|
|
# Falls back to the access token, and to the plain account_id key.
|
|
tokens = {"access_token": _jwt({ACCOUNT_CLAIM: {"account_id": "acct_x"}})}
|
|
assert account_id_from(tokens) == "acct_x"
|
|
assert account_id_from({"access_token": "not-a-jwt"}) == ""
|
|
|
|
|
|
# -- sign-in flow (loopback callback → exchange → storage) --------------------------
|
|
|
|
|
|
async def _hit_callback(query: str) -> str:
|
|
reader, writer = await asyncio.open_connection("127.0.0.1", codex_auth.CALLBACK_PORT)
|
|
writer.write(
|
|
f"GET /auth/callback?{query} HTTP/1.1\r\nHost: localhost\r\n\r\n".encode()
|
|
)
|
|
await writer.drain()
|
|
data = await reader.read(-1)
|
|
writer.close()
|
|
return data.decode()
|
|
|
|
|
|
async def test_sign_in_full_flow(tmp_path, monkeypatch):
|
|
secrets = SecretStore(tmp_path / "s.json")
|
|
opened: dict = {}
|
|
exchanged: dict = {}
|
|
|
|
monkeypatch.setattr("webbrowser.open", lambda url: opened.update(url=url))
|
|
|
|
def fake_token_post(data, timeout=30.0):
|
|
exchanged.update(data)
|
|
return _token_response(
|
|
body={
|
|
"access_token": _access(),
|
|
"refresh_token": "rt-1",
|
|
"id_token": _id_token("user@example.com"),
|
|
}
|
|
)
|
|
|
|
monkeypatch.setattr(codex_auth, "_token_post", fake_token_post)
|
|
|
|
task = asyncio.create_task(codex_auth.sign_in(secrets))
|
|
while not opened: # wait for the flow to bind the port and "open" the browser
|
|
await asyncio.sleep(0.01)
|
|
state = parse_qs(urlsplit(opened["url"]).query)["state"][0]
|
|
|
|
# A forged local hit with the wrong state is rejected and does NOT consume the flow.
|
|
resp = await _hit_callback("code=evil&state=wrong")
|
|
assert resp.startswith("HTTP/1.1 400")
|
|
assert not task.done()
|
|
|
|
resp = await _hit_callback(f"code=c0de&state={state}")
|
|
assert resp.startswith("HTTP/1.1 200") and "close this tab" in resp.lower()
|
|
result = await task
|
|
|
|
assert result == {"ok": True, "account": "user@example.com"}
|
|
assert exchanged["grant_type"] == "authorization_code"
|
|
assert exchanged["code"] == "c0de"
|
|
assert exchanged["redirect_uri"] == "http://localhost:1455/auth/callback"
|
|
assert exchanged["code_verifier"]
|
|
profile = secrets.get("provider:openai-codex")
|
|
assert profile["tokens"]["refresh_token"] == "rt-1"
|
|
assert profile["account_id"] == "acct_1"
|
|
assert profile["account_email"] == "user@example.com"
|
|
assert isinstance(profile["tokens_issued_at"], int)
|
|
|
|
|
|
async def test_sign_in_provider_error_from_callback(tmp_path, monkeypatch):
|
|
secrets = SecretStore(tmp_path / "s.json")
|
|
opened: dict = {}
|
|
monkeypatch.setattr("webbrowser.open", lambda url: opened.update(url=url))
|
|
|
|
task = asyncio.create_task(codex_auth.sign_in(secrets))
|
|
while not opened:
|
|
await asyncio.sleep(0.01)
|
|
resp = await _hit_callback("error=access_denied")
|
|
assert resp.startswith("HTTP/1.1 400")
|
|
with pytest.raises(CodexAuthError, match="access_denied"):
|
|
await task
|
|
assert not CodexTokenStore(secrets).signed_in()
|
|
|
|
|
|
async def test_sign_in_port_busy_names_the_usual_holder(tmp_path):
|
|
secrets = SecretStore(tmp_path / "s.json")
|
|
blocker = socket.socket()
|
|
blocker.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
try:
|
|
blocker.bind(("127.0.0.1", codex_auth.CALLBACK_PORT))
|
|
blocker.listen(1)
|
|
with pytest.raises(CodexAuthError, match="1455"):
|
|
await codex_auth.sign_in(secrets, open_browser=False)
|
|
finally:
|
|
blocker.close()
|
|
|
|
|
|
# -- token store: refresh ------------------------------------------------------------
|
|
|
|
|
|
def test_access_token_fresh_needs_no_refresh(tmp_path, monkeypatch):
|
|
secrets = SecretStore(tmp_path / "s.json")
|
|
_seed(secrets)
|
|
monkeypatch.setattr(
|
|
codex_auth, "_token_post", lambda *a, **k: pytest.fail("refresh not needed")
|
|
)
|
|
token, account = CodexTokenStore(secrets).access_token()
|
|
assert token == secrets.get("provider:openai-codex")["tokens"]["access_token"]
|
|
assert account == "acct_1"
|
|
|
|
|
|
def test_access_token_refreshes_near_expiry(tmp_path, monkeypatch):
|
|
secrets = SecretStore(tmp_path / "s.json")
|
|
_seed(secrets, exp_offset=30) # inside the refresh margin
|
|
sent: dict = {}
|
|
fresh = _access(7200)
|
|
|
|
def fake_token_post(data, timeout=30.0):
|
|
sent.update(data)
|
|
return _token_response(body={"access_token": fresh})
|
|
|
|
monkeypatch.setattr(codex_auth, "_token_post", fake_token_post)
|
|
token, account = CodexTokenStore(secrets).access_token()
|
|
assert token == fresh and account == "acct_1"
|
|
assert sent == {
|
|
"grant_type": "refresh_token",
|
|
"refresh_token": "rt-1",
|
|
"client_id": codex_auth.CLIENT_ID,
|
|
}
|
|
profile = secrets.get("provider:openai-codex")
|
|
# The refresh response omitted refresh/id tokens — prior values survive.
|
|
assert profile["tokens"]["access_token"] == fresh
|
|
assert profile["tokens"]["refresh_token"] == "rt-1"
|
|
|
|
|
|
def test_rejected_refresh_blanks_tokens_to_signed_out(tmp_path, monkeypatch):
|
|
secrets = SecretStore(tmp_path / "s.json")
|
|
_seed(secrets, exp_offset=-10)
|
|
monkeypatch.setattr(
|
|
codex_auth, "_token_post", lambda *a, **k: _token_response(status=400)
|
|
)
|
|
store = CodexTokenStore(secrets)
|
|
with pytest.raises(CodexSignInRequired):
|
|
store.access_token()
|
|
assert not store.signed_in() # clean signed-out state, not a crash loop
|
|
|
|
|
|
def test_access_token_signed_out_raises_typed_error(tmp_path):
|
|
store = CodexTokenStore(SecretStore(tmp_path / "s.json"))
|
|
with pytest.raises(CodexSignInRequired, match="Not signed in"):
|
|
store.access_token()
|
|
|
|
|
|
# -- provider: request shape to the backend -----------------------------------------
|
|
|
|
|
|
class _FakeSDKClient:
|
|
def __init__(self, events=None, errors=None):
|
|
self.kwargs: dict = {}
|
|
errors = list(errors or [])
|
|
|
|
def create(**kwargs):
|
|
self.kwargs = kwargs
|
|
if errors:
|
|
raise errors.pop(0)
|
|
return iter(events or [])
|
|
|
|
self.responses = SimpleNamespace(create=create)
|
|
|
|
|
|
def _completed_event(text="hello"):
|
|
return SimpleNamespace(
|
|
type="response.completed",
|
|
response=SimpleNamespace(
|
|
output=[
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": text}],
|
|
}
|
|
],
|
|
status="completed",
|
|
incomplete_details=None,
|
|
),
|
|
)
|
|
|
|
|
|
def _status_error(status: int, message: str = ""):
|
|
exc = Exception(message or f"HTTP {status}")
|
|
exc.status_code = status
|
|
return exc
|
|
|
|
|
|
def _provider(tmp_path, monkeypatch, clients):
|
|
"""A CodexProvider over seeded tokens whose SDK clients come from `clients`
|
|
(each OpenAI() construction pops the next one and records its init kwargs)."""
|
|
secrets = SecretStore(tmp_path / "s.json")
|
|
_seed(secrets)
|
|
built: list[dict] = []
|
|
|
|
def fake_openai(**kwargs):
|
|
built.append(kwargs)
|
|
return clients.pop(0)
|
|
|
|
monkeypatch.setattr("openai.OpenAI", fake_openai)
|
|
return CodexProvider(secrets=secrets), secrets, built
|
|
|
|
|
|
def test_stream_request_headers_and_body(tmp_path, monkeypatch):
|
|
fake = _FakeSDKClient(events=[_completed_event()])
|
|
provider, secrets, built = _provider(tmp_path, monkeypatch, [fake])
|
|
|
|
out = list(
|
|
provider.stream(
|
|
model="gpt-5.2-codex",
|
|
messages=[
|
|
{"role": "system", "content": "sys"},
|
|
{"role": "user", "content": "hi"},
|
|
],
|
|
tools=[{"type": "function", "function": {"name": "f"}}],
|
|
)
|
|
)
|
|
|
|
assert built[0]["api_key"] == secrets.get("provider:openai-codex")["tokens"][
|
|
"access_token"
|
|
]
|
|
assert built[0]["base_url"] == CODEX_BASE_URL
|
|
headers = built[0]["default_headers"]
|
|
assert headers["chatgpt-account-id"] == "acct_1"
|
|
assert headers["originator"] == codex_auth.ORIGINATOR
|
|
assert headers["OpenAI-Beta"] == "responses=experimental"
|
|
assert headers["session-id"] # per-conversation uuid
|
|
assert fake.kwargs["model"] == "gpt-5.2-codex"
|
|
assert fake.kwargs["stream"] is True
|
|
assert fake.kwargs["store"] is False
|
|
assert fake.kwargs["include"] == ["reasoning.encrypted_content"]
|
|
assert fake.kwargs["instructions"] == "sys"
|
|
assert fake.kwargs["tools"] == [{"type": "function", "name": "f"}]
|
|
assert out[-1].turn.text == "hello"
|
|
|
|
|
|
def test_complete_goes_through_stream_and_reasoning_effort_rides(tmp_path, monkeypatch):
|
|
fake = _FakeSDKClient(events=[_completed_event("done")])
|
|
provider, _, _ = _provider(tmp_path, monkeypatch, [fake])
|
|
turn = provider.complete(
|
|
model="gpt-5.2-codex",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
reasoning_effort="high",
|
|
)
|
|
assert turn.text == "done"
|
|
assert fake.kwargs["stream"] is True # the backend only serves streams
|
|
assert fake.kwargs["reasoning"] == {"summary": "auto", "effort": "high"}
|
|
assert fake.kwargs["instructions"] # bare-call fallback instructions present
|
|
|
|
|
|
def test_401_refreshes_once_and_retries_with_new_bearer(tmp_path, monkeypatch):
|
|
first = _FakeSDKClient(errors=[_status_error(401, "Unauthorized")])
|
|
second = _FakeSDKClient(events=[_completed_event("after refresh")])
|
|
provider, secrets, built = _provider(tmp_path, monkeypatch, [first, second])
|
|
fresh = _access(7200)
|
|
monkeypatch.setattr(
|
|
codex_auth,
|
|
"_token_post",
|
|
lambda *a, **k: _token_response(body={"access_token": fresh}),
|
|
)
|
|
turn = provider.complete(
|
|
model="gpt-5.2-codex", messages=[{"role": "user", "content": "hi"}]
|
|
)
|
|
assert turn.text == "after refresh"
|
|
assert len(built) == 2 and built[1]["api_key"] == fresh
|
|
|
|
|
|
def test_429_surfaces_plan_limit_message(tmp_path, monkeypatch):
|
|
fake = _FakeSDKClient(errors=[_status_error(429, "Too Many Requests")])
|
|
provider, _, _ = _provider(tmp_path, monkeypatch, [fake])
|
|
with pytest.raises(RuntimeError, match="plan limit"):
|
|
provider.complete(
|
|
model="gpt-5.2-codex", messages=[{"role": "user", "content": "hi"}]
|
|
)
|
|
|
|
|
|
def test_signed_out_provider_raises_typed_error(tmp_path):
|
|
provider = CodexProvider(secrets=SecretStore(tmp_path / "s.json"))
|
|
with pytest.raises(CodexSignInRequired, match="Not signed in"):
|
|
provider.complete(
|
|
model="gpt-5.2-codex", messages=[{"role": "user", "content": "hi"}]
|
|
)
|
|
|
|
|
|
# -- registry / matrix ---------------------------------------------------------------
|
|
|
|
|
|
def test_registry_builds_codex_provider():
|
|
from coworker.providers.registry import build_provider_client, get_descriptor
|
|
|
|
assert isinstance(build_provider_client("openai-codex", {}, None), CodexProvider)
|
|
d = get_descriptor("openai-codex")
|
|
assert d.auth == "oauth" and d.fields == []
|
|
assert d.to_dict()["auth"] == "oauth"
|
|
|
|
|
|
def test_descriptor_configured_means_tokens_present():
|
|
from coworker.providers.registry import descriptor_configured, get_descriptor
|
|
|
|
d = get_descriptor("openai-codex")
|
|
assert not descriptor_configured(d, {})
|
|
assert descriptor_configured(d, {"tokens": {"access_token": "a"}})
|
|
|
|
|
|
def test_matrix_curates_subscription_models():
|
|
from coworker.providers.capabilities import capabilities_for
|
|
from coworker.providers.matrix import models_for_provider
|
|
|
|
assert models_for_provider("openai-codex") == [
|
|
"gpt-5.6-sol",
|
|
"gpt-5.6-terra",
|
|
"gpt-5.6-luna",
|
|
"gpt-5.2-codex",
|
|
"gpt-5.2",
|
|
"gpt-5.1-codex",
|
|
"gpt-5.1-codex-mini",
|
|
]
|
|
caps = capabilities_for("openai-codex:gpt-5.6-sol")
|
|
assert caps.tools and caps.vision and caps.streaming
|
|
|
|
|
|
# -- verify probe --------------------------------------------------------------------
|
|
|
|
|
|
def _verify_with_backend(tmp_path, monkeypatch, status: int):
|
|
secrets = SecretStore(tmp_path / "s.json")
|
|
_seed(secrets)
|
|
probes: dict = {}
|
|
|
|
def fake_post(url, headers=None, json=None, timeout=None):
|
|
probes.update(url=url, headers=headers, body=json)
|
|
return SimpleNamespace(status_code=status)
|
|
|
|
monkeypatch.setattr("httpx.post", fake_post)
|
|
return codex_auth.verify(secrets), probes
|
|
|
|
|
|
def test_verify_signed_out(tmp_path):
|
|
result = codex_auth.verify(SecretStore(tmp_path / "s.json"))
|
|
assert result["ok"] is False and result["state"] == "signed_out"
|
|
|
|
|
|
def test_verify_ok_probes_backend(tmp_path, monkeypatch):
|
|
result, probes = _verify_with_backend(tmp_path, monkeypatch, 200)
|
|
assert result == {"ok": True, "account": "user@example.com"}
|
|
assert probes["url"] == CODEX_BASE_URL + "/responses"
|
|
assert probes["headers"]["Authorization"].startswith("Bearer ")
|
|
assert probes["headers"]["chatgpt-account-id"] == "acct_1"
|
|
assert probes["body"]["store"] is False and probes["body"]["stream"] is True
|
|
|
|
|
|
def test_verify_expired_and_plan_limited(tmp_path, monkeypatch):
|
|
result, _ = _verify_with_backend(tmp_path, monkeypatch, 401)
|
|
assert result["ok"] is False and result["state"] == "expired"
|
|
result, _ = _verify_with_backend(tmp_path, monkeypatch, 429)
|
|
assert result["ok"] is True and "note" in result # auth fine, window used up
|
|
|
|
|
|
# -- REST surface --------------------------------------------------------------------
|
|
|
|
|
|
def _rest(tmp_path):
|
|
manager = SessionManager(data_dir=tmp_path / "data")
|
|
return manager, TestClient(create_app(manager))
|
|
|
|
|
|
def test_providers_list_shows_oauth_state(tmp_path):
|
|
manager, client = _rest(tmp_path)
|
|
rows = {p["name"]: p for p in client.get("/v1/providers").json()}
|
|
row = rows["openai-codex"]
|
|
assert row["auth"] == "oauth"
|
|
assert row["signed_in"] is False and row["configured"] is False
|
|
assert "gpt-5.6-sol" in row["suggested_models"]
|
|
|
|
manager.secrets.put(
|
|
"provider:openai-codex",
|
|
{"tokens": {"access_token": "a", "refresh_token": "r"}, "account_email": "u@x.com"},
|
|
)
|
|
row = {p["name"]: p for p in client.get("/v1/providers").json()}["openai-codex"]
|
|
assert row["signed_in"] is True and row["configured"] is True
|
|
assert row["account"] == "u@x.com"
|
|
assert "tokens" not in row.get("values", {}) # secrets never leave the store
|
|
|
|
|
|
def test_signin_route_starts_background_flow(tmp_path, monkeypatch):
|
|
manager, _ = _rest(tmp_path)
|
|
seen = {}
|
|
|
|
async def fake_signin():
|
|
seen["called"] = True
|
|
return {"ok": True}
|
|
|
|
monkeypatch.setattr(manager, "codex_signin", fake_signin)
|
|
client = TestClient(create_app(manager))
|
|
assert client.post("/v1/providers/openai-codex/signin").json() == {
|
|
"ok": True,
|
|
"started": True,
|
|
}
|
|
assert seen["called"]
|
|
# begin_codex_signin flagged before the task ran, so the very first status
|
|
# poll after the button press already shows authorizing.
|
|
assert manager._codex_authorizing is True
|
|
|
|
|
|
def test_status_and_signout_routes(tmp_path):
|
|
manager, client = _rest(tmp_path)
|
|
status = client.get("/v1/providers/openai-codex/status").json()
|
|
assert status["signed_in"] is False and status["authorizing"] is False
|
|
|
|
manager.secrets.put(
|
|
"provider:openai-codex",
|
|
{"tokens": {"access_token": "a"}, "account_email": "u@x.com"},
|
|
)
|
|
status = client.get("/v1/providers/openai-codex/status").json()
|
|
assert status["signed_in"] is True and status["account"] == "u@x.com"
|
|
|
|
assert client.post("/v1/providers/openai-codex/signout").json() == {
|
|
"ok": True,
|
|
"had_tokens": True,
|
|
}
|
|
assert client.get("/v1/providers/openai-codex/status").json()["signed_in"] is False
|
|
|
|
|
|
def test_verify_route_reports_signed_out(tmp_path):
|
|
_, client = _rest(tmp_path)
|
|
result = client.post("/v1/providers/verify", json={"name": "openai-codex"}).json()
|
|
assert result["ok"] is False and result["state"] == "signed_out"
|
|
|
|
|
|
async def test_manager_signin_stores_and_promotes_model(tmp_path, monkeypatch):
|
|
manager, _ = _rest(tmp_path)
|
|
|
|
async def fake_sign_in(secrets, **kwargs):
|
|
CodexTokenStore(secrets).save(
|
|
{
|
|
"access_token": _access(),
|
|
"refresh_token": "rt-1",
|
|
"id_token": _id_token(),
|
|
}
|
|
)
|
|
return {"ok": True, "account": "user@example.com"}
|
|
|
|
monkeypatch.setattr(codex_auth, "sign_in", fake_sign_in)
|
|
result = await manager.codex_signin()
|
|
assert result["ok"] is True
|
|
assert manager._codex_authorizing is False
|
|
settings = manager.get_settings()
|
|
assert "openai-codex:gpt-5.6-sol" in settings["models"]
|
|
|
|
|
|
async def test_manager_signin_failure_lands_in_status(tmp_path, monkeypatch):
|
|
manager, client = _rest(tmp_path)
|
|
|
|
async def fake_sign_in(secrets, **kwargs):
|
|
raise CodexAuthError("Port 1455 is already in use — quit the other holder.")
|
|
|
|
monkeypatch.setattr(codex_auth, "sign_in", fake_sign_in)
|
|
result = await manager.codex_signin()
|
|
assert result["ok"] is False
|
|
status = client.get("/v1/providers/openai-codex/status").json()
|
|
assert "1455" in status["last_error"]
|
|
assert status["authorizing"] is False
|
|
|
|
|
|
def test_request_strips_backend_unsupported_params(tmp_path):
|
|
# The plan backend 400s on standard sampling/cap knobs ("Unsupported parameter:
|
|
# max_output_tokens" / "temperature") — which silently killed every autotitle attempt
|
|
# on plan sessions (owner catch 2026-08-24). The provider strips them; the reasoning
|
|
# effort knob still rides.
|
|
from coworker.secrets import SecretStore
|
|
|
|
provider = CodexProvider(secrets=SecretStore(tmp_path / "s.json"))
|
|
kwargs = provider._request_kwargs(
|
|
model="gpt-5.6-sol",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
tools=None,
|
|
settings={"max_tokens": 64, "temperature": 0.2, "top_p": 0.9, "reasoning_effort": "none"},
|
|
)
|
|
assert "max_output_tokens" not in kwargs
|
|
assert "temperature" not in kwargs
|
|
assert "top_p" not in kwargs
|
|
assert kwargs["reasoning"]["effort"] == "none"
|