mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-08-31 01:13:06 +08:00
Merge pull request #6150 from Wei-Shaw/fix/grok-upstream-user-agent
fix(grok): use official CLI user agent
This commit is contained in:
@@ -19,7 +19,7 @@ const (
|
||||
// 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.114"
|
||||
CLIClientVersion = "0.2.120"
|
||||
// billingCLIUserAgent is the legacy pager/shell UA used by billing probes.
|
||||
// Distinct from CLIUserAgent() in cli_identity.go (workspace-style UA).
|
||||
billingCLIUserAgent = "grok-pager/" + CLIClientVersion + " grok-shell/" + CLIClientVersion + " (macos; aarch64)"
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestApplyCLIProxyHeaders(t *testing.T) {
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("User-Agent", "sub2api-grok/1.0")
|
||||
req.Header.Set("User-Agent", "legacy-client/1.0")
|
||||
|
||||
ApplyCLIProxyHeaders(req)
|
||||
|
||||
@@ -56,12 +56,12 @@ func TestApplyCLIProxyHeadersLeavesAPIHostUnchanged(t *testing.T) {
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://api.x.ai/v1/responses", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("User-Agent", "sub2api-grok/1.0")
|
||||
req.Header.Set("User-Agent", "direct-api-client/1.0")
|
||||
|
||||
ApplyCLIProxyHeaders(req)
|
||||
|
||||
require.Empty(t, req.Header.Get("x-grok-client-version"))
|
||||
require.Empty(t, req.Header.Get("x-grok-client-identifier"))
|
||||
require.Empty(t, req.Header.Get("X-XAI-Token-Auth"))
|
||||
require.Equal(t, "sub2api-grok/1.0", req.Header.Get("User-Agent"))
|
||||
require.Equal(t, "direct-api-client/1.0", req.Header.Get("User-Agent"))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"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/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@@ -228,16 +229,16 @@ func TestHTTPUpstreamDoAppliesGrokCLIIdentityBeforeOAuthRoundTrip(t *testing.T)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/"+endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("User-Agent", "sub2api-grok/1.0")
|
||||
req.Header.Set("User-Agent", "legacy-client/1.0")
|
||||
|
||||
resp, err := svc.Do(req, "", accountID, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
require.Equal(t, "0.2.114", capturedHeaders.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIClientVersion, 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.114", capturedHeaders.Get("User-Agent"))
|
||||
require.Equal(t, xai.CLIUserAgent(xai.CLIClientVersion), capturedHeaders.Get("User-Agent"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -454,67 +455,67 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "")
|
||||
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("User-Agent", "sub2api-grok/1.0")
|
||||
req.Header.Set("User-Agent", "legacy-client/1.0")
|
||||
|
||||
applyGrokCLIProxyHeaders(req)
|
||||
|
||||
require.Equal(t, "0.2.114", req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIClientVersion, 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.114", req.Header.Get("User-Agent"))
|
||||
require.Equal(t, xai.CLIUserAgent(xai.CLIClientVersion), req.Header.Get("User-Agent"))
|
||||
})
|
||||
|
||||
t.Run("accepts a valid operator override", func(t *testing.T) {
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.115-alpha.1")
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.121-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.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"))
|
||||
require.Equal(t, "0.2.121-alpha.1", req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIUserAgent("0.2.121-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.115\r\nX-Injected: true")
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.121\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.114", req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIClientVersion, 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.113")
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.119")
|
||||
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.114", req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, "xai-grok-workspace/0.2.114", req.Header.Get("User-Agent"))
|
||||
require.Equal(t, xai.CLIClientVersion, req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIUserAgent(xai.CLIClientVersion), 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.114-beta.1")
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.120-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.114", req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, "xai-grok-workspace/0.2.114", req.Header.Get("User-Agent"))
|
||||
require.Equal(t, xai.CLIClientVersion, req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIUserAgent(xai.CLIClientVersion), 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.0115",
|
||||
"0.2.115-alpha..1",
|
||||
"0.2.0121",
|
||||
"0.2.121-alpha..1",
|
||||
"0.3",
|
||||
"1",
|
||||
"0.2.115+build.1",
|
||||
"0.2.121+build.1",
|
||||
} {
|
||||
t.Run("rejects invalid semver "+version, func(t *testing.T) {
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", version)
|
||||
@@ -523,8 +524,8 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
|
||||
|
||||
applyGrokCLIProxyHeaders(req)
|
||||
|
||||
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"))
|
||||
require.Equal(t, xai.CLIClientVersion, req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIUserAgent(xai.CLIClientVersion), req.Header.Get("User-Agent"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -532,13 +533,13 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.95")
|
||||
req, err := http.NewRequest(http.MethodPost, "https://api.x.ai/v1/responses", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("User-Agent", "sub2api-grok/1.0")
|
||||
req.Header.Set("User-Agent", "direct-api-client/1.0")
|
||||
|
||||
applyGrokCLIProxyHeaders(req)
|
||||
|
||||
require.Empty(t, req.Header.Get("x-grok-client-version"))
|
||||
require.Empty(t, req.Header.Get("X-XAI-Token-Auth"))
|
||||
require.Equal(t, "sub2api-grok/1.0", req.Header.Get("User-Agent"))
|
||||
require.Equal(t, "direct-api-client/1.0", req.Header.Get("User-Agent"))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ func (s *GrokQuotaService) syncGrokObservedModels(ctx context.Context, account *
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", grokUpstreamUserAgent)
|
||||
req.Header.Set("User-Agent", defaultGrokUpstreamUserAgent())
|
||||
if account.IsGrokOAuth() {
|
||||
applyGrokCLIHeaders(req.Header)
|
||||
if isGrokCLIProxyTarget(req.URL.String()) {
|
||||
|
||||
@@ -133,8 +133,9 @@ func TestSyncGrokObservedModelsUsesCLIIdentityAndAccountHeaders(t *testing.T) {
|
||||
|
||||
require.NoError(t, svc.syncGrokObservedModels(context.Background(), account))
|
||||
require.Equal(t, xai.DefaultCLIBaseURL+"/models", upstream.lastReq.URL.String())
|
||||
require.NotEmpty(t, upstream.lastReq.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIClientVersion, upstream.lastReq.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, xai.CLIClientIdentifier, upstream.lastReq.Header.Get("x-grok-client-identifier"))
|
||||
require.Equal(t, xai.CLIUserAgent(xai.CLIClientVersion), upstream.lastReq.Header.Get("User-Agent"))
|
||||
require.Equal(t, "interactive", upstream.lastReq.Header.Get("X-Grok-Client-Mode"))
|
||||
require.Equal(t, "user-902", upstream.lastReq.Header.Get("X-UserID"))
|
||||
require.Equal(t, "user902@example.test", upstream.lastReq.Header.Get("X-Email"))
|
||||
|
||||
@@ -10,10 +10,6 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
||||
)
|
||||
|
||||
// grokUpstreamUserAgent is kept for compatibility with older Grok request
|
||||
// tests. Current requests use the pinned default UA from this package.
|
||||
const grokUpstreamUserAgent = "sub2api-grok/1.0"
|
||||
|
||||
// Fixed CLI identity aliases — single source of truth is internal/pkg/xai.
|
||||
const (
|
||||
grokClientVersionHeader = xai.CLIStableVersion
|
||||
|
||||
@@ -175,7 +175,7 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
|
||||
SetActualOpenAIUpstreamEndpoint(c, grokChatRawEndpoint)
|
||||
customUA := account.GetOpenAIUserAgent()
|
||||
if customUA == "" && account.IsGrokOAuth() {
|
||||
customUA = "sub2api-grok/1.0"
|
||||
customUA = defaultGrokUpstreamUserAgent()
|
||||
}
|
||||
resp, err := s.sendCCUpstreamRequest(ctx, c, account, targetURL, upstreamBody, clientStream, token, customUA, grokCacheIdentity)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,14 +22,13 @@ import (
|
||||
const (
|
||||
grokComposerImageBridgeVisionModel = "grok-build-0.1"
|
||||
grokComposerImageBridgeMaxOutputTokens = 512
|
||||
// grokUpstreamUserAgent lives in grok_upstream_headers.go (shared with TLS header helpers).
|
||||
grokCLIVersion = xai.CLIClientVersion
|
||||
grokDefaultResponsesModel = "grok-4.5"
|
||||
grokRateLimitFallbackCooldown = 2 * time.Minute
|
||||
grokRateLimitRepeatCooldown = 10 * time.Minute
|
||||
grokRateLimitSustainedCooldown = 30 * time.Minute
|
||||
grokRateLimitMaxAdaptiveCooldown = time.Hour
|
||||
grokRateLimitBackoffQuietPeriod = time.Hour
|
||||
grokCLIVersion = xai.CLIClientVersion
|
||||
grokDefaultResponsesModel = "grok-4.5"
|
||||
grokRateLimitFallbackCooldown = 2 * time.Minute
|
||||
grokRateLimitRepeatCooldown = 10 * time.Minute
|
||||
grokRateLimitSustainedCooldown = 30 * time.Minute
|
||||
grokRateLimitMaxAdaptiveCooldown = time.Hour
|
||||
grokRateLimitBackoffQuietPeriod = time.Hour
|
||||
)
|
||||
|
||||
func (s *OpenAIGatewayService) forwardGrokResponses(
|
||||
|
||||
@@ -828,7 +828,7 @@ func TestBuildGrokResponsesRequestAllowsPublicAPIKeyBaseURLByDefault(t *testing.
|
||||
require.Equal(t, "https://grok.example.test/v1/responses", req.URL.String())
|
||||
require.Equal(t, "Bearer api-key", req.Header.Get("Authorization"))
|
||||
require.Empty(t, req.Header.Get("X-Grok-Client-Version"))
|
||||
require.NotEqual(t, grokUpstreamUserAgent, req.Header.Get("User-Agent"))
|
||||
require.NotEqual(t, defaultGrokUpstreamUserAgent(), req.Header.Get("User-Agent"))
|
||||
}
|
||||
|
||||
func TestBuildGrokResponsesRequestHonorsOAuthOfficialEndpointSwitch(t *testing.T) {
|
||||
@@ -1132,7 +1132,7 @@ func TestForwardGrokMediaImagesGenerationNormalizesImagineAlias(t *testing.T) {
|
||||
require.Equal(t, "Bearer api-key", upstream.lastReq.Header.Get("Authorization"))
|
||||
require.Equal(t, "application/json", upstream.lastReq.Header.Get("Content-Type"))
|
||||
require.Empty(t, upstream.lastReq.Header.Get("X-Grok-Client-Version"))
|
||||
require.NotEqual(t, grokUpstreamUserAgent, upstream.lastReq.Header.Get("User-Agent"))
|
||||
require.NotEqual(t, defaultGrokUpstreamUserAgent(), upstream.lastReq.Header.Get("User-Agent"))
|
||||
require.JSONEq(t, `{"model":"grok-imagine-image-quality","prompt":"draw a cat"}`, string(upstream.lastBody))
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.JSONEq(t, `{"data":[{"url":"https://images.test/cat.png"}]}`, recorder.Body.String())
|
||||
@@ -1646,7 +1646,7 @@ func TestForwardGrokMediaVideoStatusUsesGETWithoutBody(t *testing.T) {
|
||||
require.Equal(t, http.MethodGet, upstream.lastReq.Method)
|
||||
require.Equal(t, "Bearer api-key", upstream.lastReq.Header.Get("Authorization"))
|
||||
require.Empty(t, upstream.lastReq.Header.Get("X-Grok-Client-Version"))
|
||||
require.NotEqual(t, grokUpstreamUserAgent, upstream.lastReq.Header.Get("User-Agent"))
|
||||
require.NotEqual(t, defaultGrokUpstreamUserAgent(), upstream.lastReq.Header.Get("User-Agent"))
|
||||
require.Empty(t, upstream.lastReq.Header.Get("Content-Type"))
|
||||
require.Empty(t, upstream.lastBody)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
@@ -1982,7 +1982,7 @@ func TestForwardGrokResponsesAPIKeyUsesXAIResponses(t *testing.T) {
|
||||
require.Equal(t, "https://api.x.ai/v1/responses", upstream.lastReq.URL.String())
|
||||
require.Equal(t, "Bearer xai-test-key", upstream.lastReq.Header.Get("Authorization"))
|
||||
require.Empty(t, upstream.lastReq.Header.Get("X-Grok-Client-Version"))
|
||||
require.NotEqual(t, grokUpstreamUserAgent, upstream.lastReq.Header.Get("User-Agent"))
|
||||
require.NotEqual(t, defaultGrokUpstreamUserAgent(), upstream.lastReq.Header.Get("User-Agent"))
|
||||
require.Equal(t, "grok-4.6", gjson.GetBytes(upstream.lastBody, "model").String())
|
||||
require.Equal(t, "resp_grok_api_key", result.ResponseID)
|
||||
require.Equal(t, 2, result.Usage.InputTokens)
|
||||
@@ -2259,7 +2259,7 @@ func TestForwardAsChatCompletionsForGrokAPIKeyUsesConfiguredRawEndpointWithoutOA
|
||||
require.Equal(t, "https://grok.example.test/v1/chat/completions", upstream.lastReq.URL.String())
|
||||
require.Equal(t, "Bearer third-party-key", upstream.lastReq.Header.Get("Authorization"))
|
||||
require.Empty(t, upstream.lastReq.Header.Get("X-Grok-Client-Version"))
|
||||
require.NotEqual(t, grokUpstreamUserAgent, upstream.lastReq.Header.Get("User-Agent"))
|
||||
require.NotEqual(t, defaultGrokUpstreamUserAgent(), upstream.lastReq.Header.Get("User-Agent"))
|
||||
}
|
||||
|
||||
func TestForwardAsChatCompletionsForGrokAPIKeyRejectsNonStreamingResponseWithoutUsage(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user