diff --git a/backend/internal/pkg/geminicli/constants.go b/backend/internal/pkg/geminicli/constants.go index 9b204640..19a3ddd9 100644 --- a/backend/internal/pkg/geminicli/constants.go +++ b/backend/internal/pkg/geminicli/constants.go @@ -3,8 +3,8 @@ package geminicli import ( "fmt" + "os" "runtime" - "strings" "time" ) @@ -51,15 +51,49 @@ const ( SessionTTL = 30 * time.Minute - // GeminiCLIUserAgent mimics Gemini CLI to maximize compatibility with internal endpoints. - // Note: The real Gemini CLI uses OS-appropriate platform strings. - // Use GetGeminiCLIUserAgent() for runtime-aware User-Agent. - GeminiCLIUserAgent = "GeminiCLI/0.1.5" + // GeminiCLIUserAgent 静态回退值(不含 model) + // 优先使用 GetGeminiCLIUserAgent(model) 获取完整格式 + GeminiCLIUserAgent = "GeminiCLI/0.33.1" + + // FakeNodeVersion 模拟真实 Gemini CLI 的 Node.js 版本 + // 用于 x-goog-api-client 和 token exchange User-Agent + FakeNodeVersion = "24.13.1" + + // GoogleAuthLibraryUA 模拟 google-auth-library 的 User-Agent + // 真实 Gemini CLI token exchange 由 google-auth-library 发起 + GoogleAuthLibraryUA = "google-api-nodejs-client" ) -// GetGeminiCLIUserAgent 返回带有正确平台信息的 Gemini CLI User-Agent -func GetGeminiCLIUserAgent() string { - osName := strings.Title(runtime.GOOS) // Darwin, Linux, Windows - arch := strings.ToUpper(runtime.GOARCH) - return fmt.Sprintf("GeminiCLI/0.1.5 (%s; %s)", osName, arch) +// defaultGeminiCLIVersion 可通过环境变量 GEMINI_CLI_VERSION 覆盖 +var defaultGeminiCLIVersion = "0.33.1" + +func init() { + if v := os.Getenv("GEMINI_CLI_VERSION"); v != "" { + defaultGeminiCLIVersion = v + } +} + +// GetGeminiCLIUserAgent 返回匹配真实 Gemini CLI 格式的 User-Agent +// 真实格式: GeminiCLI/{version}/{model} ({platform}; {arch}) +// 示例: GeminiCLI/0.33.1/gemini-2.5-pro (darwin; arm64) +func GetGeminiCLIUserAgent(model ...string) string { + m := "unknown" + if len(model) > 0 && model[0] != "" { + m = model[0] + } + return fmt.Sprintf("GeminiCLI/%s/%s (%s; %s)", + defaultGeminiCLIVersion, m, runtime.GOOS, runtime.GOARCH) +} + +// GetGeminiCLIGoogAPIClient 返回 x-goog-api-client 头的值 +// 真实 Gemini CLI 通过 google-auth-library DefaultTransporter 自动注入: +// gl-node/{nodeVersion} +func GetGeminiCLIGoogAPIClient() string { + return fmt.Sprintf("gl-node/%s", FakeNodeVersion) +} + +// GetGeminiCLITokenExchangeUA 返回 token exchange/refresh 时的 User-Agent +// 真实 Gemini CLI 使用 google-auth-library 发起 token 交换 +func GetGeminiCLITokenExchangeUA() string { + return GoogleAuthLibraryUA } diff --git a/backend/internal/repository/gemini_oauth_client.go b/backend/internal/repository/gemini_oauth_client.go index eb14f313..3841dfe0 100644 --- a/backend/internal/repository/gemini_oauth_client.go +++ b/backend/internal/repository/gemini_oauth_client.go @@ -63,6 +63,8 @@ func (c *geminiOAuthClient) ExchangeCode(ctx context.Context, oauthType, code, c resp, err := client.R(). SetContext(ctx). SetFormDataFromValues(formData). + SetHeader("User-Agent", geminicli.GetGeminiCLITokenExchangeUA()). + SetHeader("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()). SetSuccessResult(&tokenResp). Post(c.tokenURL) if err != nil { @@ -106,6 +108,8 @@ func (c *geminiOAuthClient) RefreshToken(ctx context.Context, oauthType, refresh resp, err := client.R(). SetContext(ctx). SetFormDataFromValues(formData). + SetHeader("User-Agent", geminicli.GetGeminiCLITokenExchangeUA()). + SetHeader("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()). SetSuccessResult(&tokenResp). Post(c.tokenURL) if err != nil { diff --git a/backend/internal/repository/geminicli_codeassist_client.go b/backend/internal/repository/geminicli_codeassist_client.go index b5bc6497..7e0d85b3 100644 --- a/backend/internal/repository/geminicli_codeassist_client.go +++ b/backend/internal/repository/geminicli_codeassist_client.go @@ -34,7 +34,8 @@ func (c *geminiCliCodeAssistClient) LoadCodeAssist(ctx context.Context, accessTo SetContext(ctx). SetHeader("Authorization", "Bearer "+accessToken). SetHeader("Content-Type", "application/json"). - SetHeader("User-Agent", geminicli.GeminiCLIUserAgent). + SetHeader("User-Agent", geminicli.GetGeminiCLIUserAgent()). + SetHeader("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()). SetBody(reqBody). SetSuccessResult(&out). Post(c.baseURL + "/v1internal:loadCodeAssist") @@ -78,7 +79,8 @@ func (c *geminiCliCodeAssistClient) OnboardUser(ctx context.Context, accessToken SetContext(ctx). SetHeader("Authorization", "Bearer "+accessToken). SetHeader("Content-Type", "application/json"). - SetHeader("User-Agent", geminicli.GeminiCLIUserAgent). + SetHeader("User-Agent", geminicli.GetGeminiCLIUserAgent()). + SetHeader("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()). SetBody(reqBody). SetSuccessResult(&out). Post(c.baseURL + "/v1internal:onboardUser") @@ -116,7 +118,7 @@ func createGeminiCliReqClient(proxyURL string) (*req.Client, error) { func defaultLoadCodeAssistRequest() *geminicli.LoadCodeAssistRequest { return &geminicli.LoadCodeAssistRequest{ Metadata: geminicli.LoadCodeAssistMetadata{ - IDEType: "ANTIGRAVITY", + IDEType: "IDE_UNSPECIFIED", Platform: "PLATFORM_UNSPECIFIED", PluginType: "GEMINI", }, @@ -127,7 +129,7 @@ func defaultOnboardUserRequest() *geminicli.OnboardUserRequest { return &geminicli.OnboardUserRequest{ TierID: "LEGACY", Metadata: geminicli.LoadCodeAssistMetadata{ - IDEType: "ANTIGRAVITY", + IDEType: "IDE_UNSPECIFIED", Platform: "PLATFORM_UNSPECIFIED", PluginType: "GEMINI", }, diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 12617336..0ba0e9a3 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -1464,7 +1464,8 @@ func (s *AccountTestService) buildCodeAssistRequest(ctx context.Context, accessT req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) - req.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent) + req.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + req.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return req, nil } diff --git a/backend/internal/service/gemini_messages_compat_service.go b/backend/internal/service/gemini_messages_compat_service.go index 0e0898b6..490722ce 100644 --- a/backend/internal/service/gemini_messages_compat_service.go +++ b/backend/internal/service/gemini_messages_compat_service.go @@ -669,7 +669,8 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex } upstreamReq.Header.Set("Content-Type", "application/json") upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent(mappedModel)) + upstreamReq.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return upstreamReq, "x-request-id", nil } else { // Mode 2: AI Studio API with OAuth (like API key mode, but using Bearer token) @@ -690,7 +691,8 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex } upstreamReq.Header.Set("Content-Type", "application/json") upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent(mappedModel)) + upstreamReq.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return upstreamReq, "x-request-id", nil } } @@ -1171,7 +1173,8 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin. } upstreamReq.Header.Set("Content-Type", "application/json") upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent(mappedModel)) + upstreamReq.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return upstreamReq, "x-request-id", nil } else { // Mode 2: AI Studio API with OAuth (like API key mode, but using Bearer token) @@ -1192,7 +1195,8 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin. } upstreamReq.Header.Set("Content-Type", "application/json") upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent(mappedModel)) + upstreamReq.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return upstreamReq, "x-request-id", nil } } diff --git a/backend/internal/service/gemini_oauth_service.go b/backend/internal/service/gemini_oauth_service.go index 08a74a37..091f9aa3 100644 --- a/backend/internal/service/gemini_oauth_service.go +++ b/backend/internal/service/gemini_oauth_service.go @@ -1037,7 +1037,8 @@ func fetchProjectIDFromResourceManager(ctx context.Context, accessToken, proxyUR } req.Header.Set("Authorization", "Bearer "+accessToken) - req.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent) + req.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + req.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) client, err := httpclient.GetClient(httpclient.Options{ ProxyURL: strings.TrimSpace(proxyURL),