diff --git a/README.md b/README.md index b6a8548fc7..5e3c617314 100644 --- a/README.md +++ b/README.md @@ -747,7 +747,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 48a2df326d..4d367c9aeb 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