From b74cb7891ccfac92ac9cbb9f340aa69244aa2153 Mon Sep 17 00:00:00 2001 From: Nick <126941599+coo1white@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:31:18 +0700 Subject: [PATCH] fix(grok): bump pinned Grok CLI version to 0.2.114 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- README.md | 2 +- backend/internal/pkg/xai/billing.go | 5 ++- backend/internal/repository/http_upstream.go | 3 +- .../internal/repository/http_upstream_test.go | 42 ++++++++++--------- .../internal/service/openai_gateway_grok.go | 2 +- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index eb0f4ba3f1..1cf3716f5e 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/backend/internal/pkg/xai/billing.go b/backend/internal/pkg/xai/billing.go index d8580ebab0..3c620a9884 100644 --- a/backend/internal/pkg/xai/billing.go +++ b/backend/internal/pkg/xai/billing.go @@ -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" diff --git a/backend/internal/repository/http_upstream.go b/backend/internal/repository/http_upstream.go index ec42e189f5..68a986648a 100644 --- a/backend/internal/repository/http_upstream.go +++ b/backend/internal/repository/http_upstream.go @@ -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 ) diff --git a/backend/internal/repository/http_upstream_test.go b/backend/internal/repository/http_upstream_test.go index 35df747992..ac0c064b20 100644 --- a/backend/internal/repository/http_upstream_test.go +++ b/backend/internal/repository/http_upstream_test.go @@ -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")) }) } diff --git a/backend/internal/service/openai_gateway_grok.go b/backend/internal/service/openai_gateway_grok.go index fdfd0bf321..e9048a770f 100644 --- a/backend/internal/service/openai_gateway_grok.go +++ b/backend/internal/service/openai_gateway_grok.go @@ -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