Skip to content

Commit 0fa3f63

Browse files
b3nwclaw-io
andauthored
fix(xai): set Grok Build User-Agent on all requests to fix billing classification
xAI classifies usage into "Grok Build" (subscription) vs "API" (pay-per-token) partly based on the User-Agent header. The openai-python SDK injects "OpenAI/Python ..." which gets bucketed as API usage. Now all xAI requests send "User-Agent: grok/<version>" via extra_headers, matching the Grok Build CLI and aligning with how 9router/CLIProxyAPI route OAuth traffic. Co-authored-by: claw-io <273482092+claw-io@users.noreply.github.com>
1 parent 5c5a3fc commit 0fa3f63

3 files changed

Lines changed: 59 additions & 5 deletions

File tree

.fork/features/xai.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,36 @@ Notes:
4747
`.bak` backup), preventing duplicate files for the same identity.
4848
- `set_app_ref(app)` added to `main.py` lifespan so background OAuth poll tasks
4949
(which lack a `Request` object) can access the `RotatingClient` for hot-loading.
50+
51+
## 2026-07-09 — Fix xAI usage classification: Grok Build vs API
52+
53+
Branch: `fix/xai-grok-build-useragent`
54+
Files:
55+
- `src/rotator_library/providers/x_ai_provider.py`
56+
57+
Problem:
58+
- xAI usage was reporting as "API" (pay-per-token) instead of "Grok Build"
59+
(subscription) on the xAI billing dashboard.
60+
- Root cause: the `openai` Python SDK (used via LiteLLM) sets
61+
`User-Agent: OpenAI/Python ...` on all requests. xAI uses the User-Agent
62+
to classify traffic into billing tracks.
63+
- 9router (decolua/9router PR #1286) and CLIProxyAPI both route OAuth chat
64+
completions to `api.x.ai/v1` with `User-Agent: grok/<version>`, which xAI
65+
recognizes as Grok Build subscription traffic.
66+
67+
Fix:
68+
- Added `_get_grok_build_headers()` returning `{"User-Agent": "grok/<version>"}`.
69+
- `acompletion()` now injects this header via `extra_headers` on **all** xAI
70+
requests (not just CLI proxy models). LiteLLM merges `extra_headers` into
71+
the outgoing HTTP request, overriding the openai SDK's default User-Agent.
72+
- `aembedding()` similarly injects the Grok Build User-Agent.
73+
- CLI proxy models continue to get the additional `x-xai-token-auth` and
74+
`x-grok-client-version` headers on top.
75+
76+
Verification:
77+
- `uv run python3 -m py_compile` and `uv run ruff check` — passed
78+
- Hot-patched to `llm-proxy-dev` on `docker-test`, restarted, verified healthy
79+
- Dry-run import: `XAiProvider()._get_grok_build_headers()` returns correct UA
80+
- Header capture test confirmed `user-agent: grok/0.1.202` reaches upstream
81+
(via openai SDK `extra_headers` → httpx request)
82+
- Test completions to `grok-4.3` and `grok-build-0.1` both succeeded

.fork/stack.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ rules:
2525
xai:
2626
- "feat(xai): add xAI Grok OAuth provider with PKCE and Device Code flows"
2727
- "feat(xai): enable xAI Grok device-code OAuth in admin WebUI"
28+
- "fix(xai): set Grok Build User-Agent on all requests to fix billing classification"
2829
umans:
2930
- "feat(umans): add Umans provider with request-based quota tracking"
3031
- "fix(umans): normalize API base and show quota without proxy requests"

src/rotator_library/providers/x_ai_provider.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,20 @@ def has_custom_logic(self) -> bool:
250250
"""
251251
return True
252252

253+
def _get_grok_build_headers(self) -> dict:
254+
"""Return User-Agent header identifying as a Grok Build client.
255+
256+
xAI classifies usage as "Grok Build" vs "API" partly based on the
257+
User-Agent. The openai-python SDK sets ``OpenAI/Python ...`` which
258+
gets bucketed as plain API usage. Overriding with the same UA the
259+
Grok Build CLI sends ensures OAuth-subscription traffic is attributed
260+
to the Grok Build billing track.
261+
"""
262+
return {"User-Agent": f"grok/{self._cli_version}"}
263+
253264
def _get_cli_proxy_headers(self) -> dict:
254265
"""Return extra headers required by the CLI chat proxy."""
255-
ver = XAI_CLI_VERSION
266+
ver = self._cli_version
256267
return {
257268
"User-Agent": f"grok/{ver}",
258269
"x-xai-token-auth": "xai-grok-cli",
@@ -301,15 +312,20 @@ async def acompletion(
301312
kwargs["api_base"] = api_base
302313
kwargs["custom_llm_provider"] = "xai"
303314

304-
# Inject CLI proxy headers if needed
315+
# Always inject Grok Build User-Agent so xAI attributes traffic to
316+
# the subscription billing track instead of the pay-per-token API.
317+
existing_headers = kwargs.get("extra_headers") or {}
318+
extra_headers = {**self._get_grok_build_headers(), **existing_headers}
319+
320+
# CLI proxy models need additional transport headers
305321
if use_cli_proxy:
306-
extra_headers = self._get_cli_proxy_headers()
307-
existing_headers = kwargs.get("extra_headers") or {}
308-
kwargs["extra_headers"] = {**existing_headers, **extra_headers}
322+
extra_headers.update(self._get_cli_proxy_headers())
309323
lib_logger.debug(
310324
f"xai: routing {model_bare} through CLI proxy with version header"
311325
)
312326

327+
kwargs["extra_headers"] = extra_headers
328+
313329
# Set up async OpenAI client for LiteLLM
314330
kwargs["client"] = openai.AsyncOpenAI(
315331
api_key=token,
@@ -355,6 +371,10 @@ async def aembedding(
355371
kwargs["api_key"] = token
356372
kwargs["api_base"] = self.api_base
357373
kwargs["custom_llm_provider"] = "xai"
374+
kwargs["extra_headers"] = {
375+
**self._get_grok_build_headers(),
376+
**(kwargs.get("extra_headers") or {}),
377+
}
358378

359379
kwargs["client"] = openai.AsyncOpenAI(
360380
api_key=token,

0 commit comments

Comments
 (0)