fix(grok): bump pinned Grok CLI version to 0.2.114

`cli-chat-proxy.grok.com` turns away requests that do not name a
supported client version — the failure mode #3952 was opened for. The
pin has sat at 0.2.93 since 2026-07-17, while https://x.ai/cli/stable
and `npm view @xai-official/grok version` both report 0.2.114.

The version was written out in three places, which is how they came to
drift:

- `repository/http_upstream.go` (OAuth traffic through the CLI proxy)
- `pkg/xai/billing.go` (billing and quota probes)
- `service/openai_gateway_grok.go` (gateway request headers)

`xai.CLIClientVersion` is now the single source, and the other two build
their own client identity from it, so the next bump is one line.
`internal/pkg/xai` is a leaf package both layers already import, so this
adds no cycle. `CLIUserAgent` derives from the same constant.

Checked against the real 0.2.114 binary rather than the version feed
alone: the `x-grok-client-version` header name, the `xai-grok-cli`
token-auth value, and the version string all match what the CLI sends.

The pinned value is also the floor an operator override must clear, so
some fixtures carried a meaning relative to it rather than a fixed
number. Those were moved, not search-and-replaced:

- the "valid override" case now uses 0.2.115-alpha.1; at 0.2.95-alpha.1
  it would fall below the new floor, be dropped, and stop testing
  acceptance at all
- the prerelease-at-the-minimum case tracks the pin to 0.2.114-beta.1,
  or it becomes a copy of the below-minimum case
- three of the five malformed-semver entries sat below the new floor and
  would have been turned away for being old, not for being malformed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nick
2026-07-29 13:31:18 +07:00
co-authored by Claude Opus 5
parent 5a6143097d
commit b74cb7891c
5 changed files with 30 additions and 24 deletions
+1 -1
View File
@@ -743,7 +743,7 @@ The Grok OAuth flow uses PKCE and does not require committing private secrets. T
| `XAI_OAUTH_AUTHORIZE_URL` | `https://auth.x.ai/oauth2/authorize` |
| `XAI_OAUTH_TOKEN_URL` | `https://auth.x.ai/oauth2/token` |
| `XAI_BASE_URL` | `https://api.x.ai/v1`; runtime-diagnostics override (account `base_url` controls request forwarding) |
| `XAI_GROK_CLI_VERSION` | `0.2.93`; optional override for the client identity sent to `cli-chat-proxy.grok.com` |
| `XAI_GROK_CLI_VERSION` | `0.2.114`; optional override for the client identity sent to `cli-chat-proxy.grok.com`. The pinned value is also the floor: an override below it is dropped |
Administrators can create Grok OAuth or API-key accounts from the dashboard. OAuth authorization and reauthorization are also available through the admin API:
+4 -1
View File
@@ -15,8 +15,11 @@ const (
CLITokenAuthHeader = "x-xai-token-auth"
CLITokenAuthValue = "xai-grok-cli"
CLIClientVersionHeader = "x-grok-client-version"
// CLIClientVersion is the one place the pinned Grok CLI version lives. The
// repository and service layers build their own client identity from it, so
// one bump here covers OAuth traffic and billing probes together.
// Keep in sync with https://x.ai/cli/stable.
CLIClientVersion = "0.2.93"
CLIClientVersion = "0.2.114"
CLIUserAgent = "grok-pager/" + CLIClientVersion + " grok-shell/" + CLIClientVersion + " (macos; aarch64)"
BillingWeeklyPath = "/billing?format=credits"
+2 -1
View File
@@ -30,6 +30,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyutil"
"github.com/Wei-Shaw/sub2api/internal/pkg/servertiming"
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
"golang.org/x/mod/semver"
@@ -76,7 +77,7 @@ const (
// allowing operators to bump it without waiting for a Sub2API release.
grokCLIProxyHost = "cli-chat-proxy.grok.com"
grokOfficialAPIHost = "api.x.ai"
grokCLIStableVersion = "0.2.93"
grokCLIStableVersion = xai.CLIClientVersion
grokCLIVersionOverride = "XAI_GROK_CLI_VERSION"
grokFallbackBodyLimit = 64 << 10
)
@@ -235,9 +235,9 @@ func TestHTTPUpstreamDoAppliesGrokCLIIdentityBeforeOAuthRoundTrip(t *testing.T)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.NoError(t, resp.Body.Close())
require.Equal(t, "0.2.93", capturedHeaders.Get("x-grok-client-version"))
require.Equal(t, "0.2.114", capturedHeaders.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-cli", capturedHeaders.Get("X-XAI-Token-Auth"))
require.Equal(t, "xai-grok-workspace/0.2.93", capturedHeaders.Get("User-Agent"))
require.Equal(t, "xai-grok-workspace/0.2.114", capturedHeaders.Get("User-Agent"))
})
}
}
@@ -458,61 +458,63 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
applyGrokCLIProxyHeaders(req)
require.Equal(t, "0.2.93", req.Header.Get("x-grok-client-version"))
require.Equal(t, "0.2.114", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-cli", req.Header.Get("X-XAI-Token-Auth"))
require.Equal(t, "xai-grok-workspace/0.2.93", req.Header.Get("User-Agent"))
require.Equal(t, "xai-grok-workspace/0.2.114", req.Header.Get("User-Agent"))
})
t.Run("accepts a valid operator override", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.95-alpha.1")
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.115-alpha.1")
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/chat/completions", nil)
require.NoError(t, err)
applyGrokCLIProxyHeaders(req)
require.Equal(t, "0.2.95-alpha.1", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-workspace/0.2.95-alpha.1", req.Header.Get("User-Agent"))
require.Equal(t, "0.2.115-alpha.1", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-workspace/0.2.115-alpha.1", req.Header.Get("User-Agent"))
})
t.Run("rejects an unsafe override", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.95\r\nX-Injected: true")
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.115\r\nX-Injected: true")
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
require.NoError(t, err)
applyGrokCLIProxyHeaders(req)
require.Equal(t, "0.2.93", req.Header.Get("x-grok-client-version"))
require.Equal(t, "0.2.114", req.Header.Get("x-grok-client-version"))
require.Empty(t, req.Header.Get("X-Injected"))
})
t.Run("rejects an override below the supported minimum", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.92")
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.113")
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
require.NoError(t, err)
applyGrokCLIProxyHeaders(req)
require.Equal(t, "0.2.93", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-workspace/0.2.93", req.Header.Get("User-Agent"))
require.Equal(t, "0.2.114", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-workspace/0.2.114", req.Header.Get("User-Agent"))
})
t.Run("rejects a prerelease override at the minimum version", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.93-beta.1")
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.114-beta.1")
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
require.NoError(t, err)
applyGrokCLIProxyHeaders(req)
require.Equal(t, "0.2.93", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-workspace/0.2.93", req.Header.Get("User-Agent"))
require.Equal(t, "0.2.114", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-workspace/0.2.114", req.Header.Get("User-Agent"))
})
// Every entry sits above the pinned minimum, so a rejection here can only be
// caused by the malformed semver and never by the version being too old.
for _, version := range []string{
"0.2.093",
"0.2.94-alpha..1",
"0.2.0115",
"0.2.115-alpha..1",
"0.3",
"1",
"0.2.95+build.1",
"0.2.115+build.1",
} {
t.Run("rejects invalid semver "+version, func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", version)
@@ -521,8 +523,8 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
applyGrokCLIProxyHeaders(req)
require.Equal(t, "0.2.93", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-workspace/0.2.93", req.Header.Get("User-Agent"))
require.Equal(t, "0.2.114", req.Header.Get("x-grok-client-version"))
require.Equal(t, "xai-grok-workspace/0.2.114", req.Header.Get("User-Agent"))
})
}
@@ -23,7 +23,7 @@ const (
grokComposerImageBridgeVisionModel = "grok-build-0.1"
grokComposerImageBridgeMaxOutputTokens = 512
grokUpstreamUserAgent = "sub2api-grok/1.0"
grokCLIVersion = "0.2.93"
grokCLIVersion = xai.CLIClientVersion
grokDefaultResponsesModel = "grok-4.5"
grokRateLimitFallbackCooldown = 2 * time.Minute
grokRateLimitRepeatCooldown = 10 * time.Minute