xAI OAuth: POST /v0/management/api-call forwards literal $TOKEN$ (upstream 401)

Author: anlostsheepCreated Sep 15, 2026Updated Sep 17, 2026
LabelsFixed

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

  1. $TOKEN$ in header / data is replaced with the xAI OAuth access_token for that authIndex.
  2. If the token cannot be resolved, api-call should fail with 400 (auth token not found / auth token refresh failed) and must not forward Bearer $TOKEN$ upstream.
  3. After substitution, GET https://cli-chat-proxy.grok.com/v1/billing?format=credits should return 200 with config.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):

  • APICall only replaces $TOKEN$ when resolveTokenForAuth returns a non-empty token. If authByIndex returns nil, resolveTokenForAuth returns ("", nil) and substitution is skipped with no error; the request is still sent.
  • If auth != nil and the token is empty, it correctly returns 400 auth token not found. This reproduction got outer 200 + upstream 401, not 400 — so either authByIndex is nil for listed xAI OAuth files, or a non-empty wrong value is substituted. The $TOKEN$ vs TOKEN comparison above points to skip/no-replace.
  • tokenValueForAuth only reads auth.Metadata (access_token / accessToken / token / …) and auth.Attributes["api_key"|"session_token"].
  • xAI auth JSON stores access_token on the typed xai.TokenStorage struct. Extra fields go to Metadata with json:"-". If the runtime Auth.Metadata does not copy access_token, tokenValueForAuth returns "".

Suggested direction (maintainers decide):

  1. Resolve xAI OAuth access_token for api-call the same way the xAI executor already does (inference works).
  2. If $TOKEN$ is present and cannot be resolved, return 400 and do not forward the placeholder.
  3. Confirm authByIndex matches the auth_index returned by GET /v0/management/auth-files for type=xai OAuth files.

Happy to provide a redacted api-call response dump or test a patch build.

Source: router-for-me/CLIProxyAPI