Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1832,14 +1832,23 @@ def _try_custom_endpoint() -> Tuple[Optional[Any], Optional[str]]:


def _build_xai_oauth_aux_client(model: str) -> Tuple[Optional[Any], Optional[str]]:
"""Build a CodexAuxiliaryClient for an xAI Grok OAuth-authenticated session.
"""Build a plain OpenAI auxiliary client for an xAI Grok OAuth session.

xAI's ``/v1/responses`` endpoint speaks the OpenAI Responses API, so we
wrap a plain ``OpenAI`` client in ``CodexAuxiliaryClient`` to translate
``chat.completions.create()`` calls into ``responses.stream()`` requests.
xAI OAuth tokens are scoped to the standard Chat Completions endpoint
(``/v1/chat/completions``) only — they are NOT authorized for the
Responses API. Wrapping the client in ``CodexAuxiliaryClient`` (which
translates chat.completions.create() calls into responses.stream()
requests) causes every auxiliary call to fail with HTTP 403:

'The OAuth2 access token could not be validated.
[WKE=unauthenticated:bad-credentials]'

Direct chat completions calls succeed with the same token. Fix: return
the plain ``OpenAI`` client — the upstream chat-completions code path
is what every other auxiliary task already expects. See #34171.

The caller must pass an explicit model — pinning a default for Grok
would silently rot when xAI's allowlist drifts. Returns ``(None, None)``
would silently rot when xAI's allowlist drifts. Returns ``(None, None)``
when the user has not authenticated with xAI Grok OAuth.
"""
if not model:
Expand All @@ -1852,9 +1861,11 @@ def _build_xai_oauth_aux_client(model: str) -> Tuple[Optional[Any], Optional[str
if resolved is None:
return None, None
api_key, base_url = resolved
logger.debug("Auxiliary client: xAI OAuth (%s via Responses API)", model)
real_client = OpenAI(api_key=api_key, base_url=base_url)
return CodexAuxiliaryClient(real_client, model), model
logger.debug("Auxiliary client: xAI OAuth (%s via chat.completions)", model)
# #34171: do NOT wrap in CodexAuxiliaryClient. The OAuth token isn't
# authorized for /v1/responses; let the plain OpenAI client hit
# /v1/chat/completions like every other auxiliary path does.
return OpenAI(api_key=api_key, base_url=base_url), model


def _build_codex_client(model: str) -> Tuple[Optional[Any], Optional[str]]:
Expand Down
81 changes: 81 additions & 0 deletions tests/agent/test_xai_oauth_aux_client_chat_completions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Regression tests for #34171 — xai-oauth auxiliary client must use
chat.completions, NOT the Responses API.

xAI OAuth tokens are scoped to /v1/chat/completions only. Wrapping the
OpenAI client in CodexAuxiliaryClient (which translates calls to
/v1/responses) caused every auxiliary task (compression, vision,
web_extract) to fail with HTTP 403.
"""

from __future__ import annotations

from unittest.mock import patch, MagicMock

import pytest


def test_xai_oauth_aux_returns_plain_openai_client_not_codex_wrapper():
"""The fix: _build_xai_oauth_aux_client returns a plain OpenAI client
that hits /v1/chat/completions, NOT a CodexAuxiliaryClient that would
route to /v1/responses (where xAI OAuth tokens are not authorized)."""
from agent.auxiliary_client import (
_build_xai_oauth_aux_client,
CodexAuxiliaryClient,
)
from openai import OpenAI

with patch(
"agent.auxiliary_client._resolve_xai_oauth_for_aux",
return_value=("xai-oauth-token-fake", "https://api.x.ai/v1"),
):
client, model = _build_xai_oauth_aux_client("grok-4.3")

assert client is not None
assert model == "grok-4.3"
# NOT wrapped in CodexAuxiliaryClient — that's the #34171 bug.
assert not isinstance(client, CodexAuxiliaryClient), (
"xai-oauth aux client must be a plain OpenAI client (chat.completions), "
"not a CodexAuxiliaryClient (responses). See #34171."
)
# IS an OpenAI client.
assert isinstance(client, OpenAI)


def test_xai_oauth_aux_returns_none_when_no_model():
"""Defensive: missing model returns (None, None) with a warning."""
from agent.auxiliary_client import _build_xai_oauth_aux_client

client, model = _build_xai_oauth_aux_client("")
assert client is None
assert model is None


def test_xai_oauth_aux_returns_none_when_unauthenticated():
"""When xAI OAuth resolution fails, return (None, None) cleanly."""
from agent.auxiliary_client import _build_xai_oauth_aux_client

with patch(
"agent.auxiliary_client._resolve_xai_oauth_for_aux",
return_value=None,
):
client, model = _build_xai_oauth_aux_client("grok-4.3")
assert client is None
assert model is None


def test_xai_oauth_aux_uses_correct_base_url():
"""The plain OpenAI client must hit the xAI base URL, not OpenAI's."""
from agent.auxiliary_client import _build_xai_oauth_aux_client

expected_base = "https://api.x.ai/v1"
with patch(
"agent.auxiliary_client._resolve_xai_oauth_for_aux",
return_value=("xai-token", expected_base),
):
client, _ = _build_xai_oauth_aux_client("grok-4.3")
# OpenAI 1.x stores base_url on the client. The exact attribute name has
# varied across SDK versions; check the most likely candidates.
base = getattr(client, "base_url", None) or getattr(client, "_base_url", None)
assert base is not None
# OpenAI SDK normalizes base_url to URL object → str() it.
assert "x.ai" in str(base)
Loading