diff --git a/strix/llm/codex_oauth.py b/strix/llm/codex_oauth.py
index 2ecb0528..80e24e72 100644
--- a/strix/llm/codex_oauth.py
+++ b/strix/llm/codex_oauth.py
@@ -206,7 +206,7 @@ def _post_codex_responses(
"Authorization": f"Bearer {credentials.access_token}",
"Accept": "text/event-stream",
"Content-Type": "application/json",
- "version": "strix-codex-oauth",
+ "User-Agent": "strix-codex-oauth",
}
if credentials.account_id:
headers["ChatGPT-Account-ID"] = credentials.account_id
diff --git a/strix/llm/llm.py b/strix/llm/llm.py
index ec35621e..4f70c013 100644
--- a/strix/llm/llm.py
+++ b/strix/llm/llm.py
@@ -10,7 +10,7 @@ from litellm import acompletion, completion_cost, stream_chunk_builder, supports
from litellm.utils import supports_prompt_caching, supports_vision
from strix.config import Config
-from strix.llm.codex_oauth import complete_codex_oauth
+from strix.llm.codex_oauth import CodexOAuthError, complete_codex_oauth
from strix.llm.config import LLMConfig
from strix.llm.memory_compressor import MemoryCompressor, get_message_tokens
from strix.llm.utils import (
@@ -314,9 +314,6 @@ class LLM:
self._total_stats.input_tokens += usage.get("input_tokens", 0)
self._total_stats.output_tokens += usage.get("output_tokens", 0)
- if content:
- yield LLMResponse(content=content)
-
content = _THINKING_BLOCK_RE.sub("", content)
content = normalize_tool_format(content)
content = fix_incomplete_tool_call(_truncate_to_first_function(content))
@@ -384,6 +381,8 @@ class LLM:
return 0.0
def _should_retry(self, e: Exception) -> bool:
+ if isinstance(e, CodexOAuthError):
+ return False
code = getattr(e, "status_code", None) or getattr(
getattr(e, "response", None), "status_code", None
)
diff --git a/tests/llm/test_codex_oauth.py b/tests/llm/test_codex_oauth.py
index 234247cb..c848f22b 100644
--- a/tests/llm/test_codex_oauth.py
+++ b/tests/llm/test_codex_oauth.py
@@ -4,13 +4,16 @@ from pathlib import Path
import pytest
from strix.llm.codex_oauth import (
+ CodexOAuthCredentials,
CodexOAuthError,
+ _post_codex_responses,
build_codex_responses_payload,
load_codex_oauth_credentials,
parse_responses_sse_events,
refresh_codex_oauth_credentials,
)
from strix.llm.config import LLMConfig
+from strix.llm.llm import LLM
def test_llm_config_detects_codex_oauth_provider(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -22,6 +25,41 @@ def test_llm_config_detects_codex_oauth_provider(monkeypatch: pytest.MonkeyPatch
assert config.codex_model == "gpt-5.5"
+def test_llm_does_not_retry_codex_oauth_errors(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("STRIX_LLM", "codex/gpt-5.5")
+
+ llm = LLM(LLMConfig())
+
+ assert llm._should_retry(CodexOAuthError("Run `codex login` first.")) is False
+
+
+@pytest.mark.asyncio
+async def test_codex_oauth_stream_yields_single_processed_response(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.setenv("STRIX_LLM", "codex/gpt-5.5")
+
+ def fake_complete_codex_oauth(*args, **kwargs):
+ return (
+ "hiddendone",
+ {"input_tokens": 1, "output_tokens": 2},
+ )
+
+ monkeypatch.setattr("strix.llm.llm.complete_codex_oauth", fake_complete_codex_oauth)
+
+ llm = LLM(LLMConfig())
+ responses = [
+ response
+ async for response in llm._stream_codex_oauth([{"role": "user", "content": "Hi"}])
+ ]
+
+ assert len(responses) == 1
+ assert "" not in responses[0].content
+ assert responses[0].tool_invocations
+ assert llm._total_stats.input_tokens == 1
+ assert llm._total_stats.output_tokens == 2
+
+
def test_load_codex_oauth_credentials_reads_codex_auth_json(tmp_path: Path) -> None:
auth_file = tmp_path / "auth.json"
auth_file.write_text(
@@ -95,6 +133,33 @@ def test_refresh_codex_oauth_credentials_persists_new_tokens(tmp_path: Path) ->
assert calls[0][1]["json"]["grant_type"] == "refresh_token"
+def test_post_codex_responses_uses_user_agent_header(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ calls = []
+
+ class FakeResponse:
+ pass
+
+ def fake_post(url: str, **kwargs):
+ calls.append((url, kwargs))
+ return FakeResponse()
+
+ monkeypatch.setattr("strix.llm.codex_oauth.requests.post", fake_post)
+
+ response = _post_codex_responses(
+ "https://example.test/codex",
+ CodexOAuthCredentials(access_token="access-token", account_id="account-id"),
+ {"model": "gpt-5.5"},
+ 60,
+ )
+
+ assert isinstance(response, FakeResponse)
+ headers = calls[0][1]["headers"]
+ assert headers["User-Agent"] == "strix-codex-oauth"
+ assert "version" not in headers
+
+
def test_build_codex_responses_payload_converts_chat_messages() -> None:
payload = build_codex_responses_payload(
model="gpt-5.5",