xAI OAuth: POST /v0/management/api-call forwards literal $TOKEN$ (upstream 401)
Is it a request payload issue? [x] Yes, this is a request payload issue. I am using a client/cURL to send a request payload, but I received an unexpected error. [ ] No, it's another issue.
Describe the bug
On CLIProxyAPI v7.3.3, POST /v0/management/api-call does not substitute $TOKEN$ for xAI OAuth auth files (type=xai, auth_kind=oauth).
The management API still returns HTTP 200 and forwards the upstream request. The upstream then sees a literal Bearer $TOKEN$ and returns 401.
The same xAI OAuth credential works for normal inference (POST /v1/responses via the xAI executor / cli-chat-proxy). The same api-call succeeds if Authorization is set to the real access_token instead of $TOKEN$.
This started immediately after the automatic upgrade v7.3.1 → v7.3.3 (v7.3.2 is in that jump; v7.3.2 changelog mentions APICall $TOKEN$ body replacement / escaping). Quota probes that succeeded on v7.3.1 now persist empty results.
CLI Type
xAI / Grok Build OAuth (type=xai, auth_kind=oauth, auth JSON in auth-dir)
Model Name
grok-4.6 (inference still works; this bug is on management api-call, not chat)
LLM Client
CPA Manager Plus quota refresh (it sends Authorization: Bearer $TOKEN$ and authIndex from GET /v0/management/auth-files). Reproduced with a direct api-call as well.
Request Information
Auth file shape (secrets omitted):
{
"type": "xai",
"auth_kind": "oauth",
"base_url": "https://api.x.ai/v1",
"access_token": "<oauth access token>",
"refresh_token": "<refresh token>",
"id_token": "<id token>",
"email": "<redacted>",
"disabled": false
}
GET /v0/management/auth-files lists the file as type=xai, account_type=oauth, with a non-empty auth_index.
Failing call (placeholder as sent by CMP / as reproduced):
POST /v0/management/api-call
Content-Type: application/json
{
"authIndex": "<auth_index from GET /v0/management/auth-files>",
"method": "GET",
"url": "https://cli-chat-proxy.grok.com/v1/billing?format=credits",
"header": {
"Authorization": "Bearer $TOKEN$",
"x-xai-token-auth": "xai-grok-cli",
"x-grok-client-version": "0.2.101",
"accept": "*/*",
"user-agent": "grok-pager/0.2.101 grok-shell/0.2.101"
}
}
Result:
Authorization |
api-call outer |
upstream status_code |
upstream body |
|---|---|---|---|
Bearer $TOKEN$ |
200 | 401 | Invalid or expired credentials (auth_kind=bearer, x_xai_token_auth=xai-grok-cli, upstream=PermissionDenied, reason=no auth context) |
Bearer TOKEN (no $) |
200 | 401 | same no auth context |
Bearer <real oauth access_token> |
200 | 200 | billing config with creditUsagePercent / productUsage |
GET https://api.x.ai/v1/me with the same pattern:
Authorization |
upstream |
|---|---|
Bearer $TOKEN$ |
400 Incorrect API key provided. You can obtain an API key from https://console.x.ai. |
Bearer <real oauth access_token> |
200 { user_id, team_id, oauth, ... } |
The identical 401 for $TOKEN$ and for the obviously invalid literal TOKEN is why this looks like no substitution (the placeholder is forwarded), not like an expired OAuth token.
Expected behavior
$TOKEN$inheader/datais replaced with the xAI OAuthaccess_tokenfor thatauthIndex.- If the token cannot be resolved,
api-callshould fail with 400 (auth token not found/auth token refresh failed) and must not forwardBearer $TOKEN$upstream. - After substitution,
GET https://cli-chat-proxy.grok.com/v1/billing?format=creditsshould return 200 withconfig.creditUsagePercent(confirmed when sending the real access token).
OS Type
- OS: Linux (Ubuntu 24.04, amd64)
- CLIProxyAPI: 7.3.3 (
Commit: 7bbfeaf8,BuiltAt: 2026-09-14T19:44:00Z) - Previous working: 7.3.1 (same host, same xAI OAuth files; quota probe still wrote windows)
Additional context
Code on v7.3.3 (internal/api/handlers/management/api_tools.go):
APICallonly replaces$TOKEN$whenresolveTokenForAuthreturns a non-empty token. IfauthByIndexreturns nil,resolveTokenForAuthreturns("", nil)and substitution is skipped with no error; the request is still sent.- If
auth != niland the token is empty, it correctly returns 400auth token not found. This reproduction got outer 200 + upstream 401, not 400 — so eitherauthByIndexis nil for listed xAI OAuth files, or a non-empty wrong value is substituted. The$TOKEN$vsTOKENcomparison above points to skip/no-replace. tokenValueForAuthonly readsauth.Metadata(access_token/accessToken/token/ …) andauth.Attributes["api_key"|"session_token"].- xAI auth JSON stores
access_tokenon the typedxai.TokenStoragestruct. Extra fields go toMetadatawithjson:"-". If the runtimeAuth.Metadatadoes not copyaccess_token,tokenValueForAuthreturns "".
Suggested direction (maintainers decide):
- Resolve xAI OAuth
access_tokenforapi-callthe same way the xAI executor already does (inference works). - If
$TOKEN$is present and cannot be resolved, return 400 and do not forward the placeholder. - Confirm
authByIndexmatches theauth_indexreturned byGET /v0/management/auth-filesfortype=xaiOAuth files.
Happy to provide a redacted api-call response dump or test a patch build.
Source: router-for-me/CLIProxyAPI