refactor(headers): streamline User-Agent handling and introduce GeminiCLI versioning
Some checks failed
docker-image / docker_amd64 (push) Has been cancelled
docker-image / docker_arm64 (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
docker-image / docker_manifest (push) Has been cancelled

This commit is contained in:
hkfires
2026-03-02 13:04:30 +08:00
parent 660bd7eff5
commit 914db94e79
6 changed files with 58 additions and 41 deletions

View File

@@ -13,7 +13,6 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -43,17 +42,13 @@ import (
var lastRefreshKeys = []string{"last_refresh", "lastRefresh", "last_refreshed_at", "lastRefreshedAt"} var lastRefreshKeys = []string{"last_refresh", "lastRefresh", "last_refreshed_at", "lastRefreshedAt"}
const ( const (
anthropicCallbackPort = 54545 anthropicCallbackPort = 54545
geminiCallbackPort = 8085 geminiCallbackPort = 8085
codexCallbackPort = 1455 codexCallbackPort = 1455
geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com"
geminiCLIVersion = "v1internal" geminiCLIVersion = "v1internal"
) )
func getGeminiCLIUserAgent() string {
return fmt.Sprintf("GeminiCLI/1.0.0/unknown (%s; %s)", runtime.GOOS, runtime.GOARCH)
}
type callbackForwarder struct { type callbackForwarder struct {
provider string provider string
server *http.Server server *http.Server
@@ -2287,7 +2282,7 @@ func callGeminiCLI(ctx context.Context, httpClient *http.Client, endpoint string
return fmt.Errorf("create request: %w", errRequest) return fmt.Errorf("create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", getGeminiCLIUserAgent()) req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))
resp, errDo := httpClient.Do(req) resp, errDo := httpClient.Do(req)
if errDo != nil { if errDo != nil {
@@ -2357,7 +2352,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec
return false, fmt.Errorf("failed to create request: %w", errRequest) return false, fmt.Errorf("failed to create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", getGeminiCLIUserAgent()) req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))
resp, errDo := httpClient.Do(req) resp, errDo := httpClient.Do(req)
if errDo != nil { if errDo != nil {
return false, fmt.Errorf("failed to execute request: %w", errDo) return false, fmt.Errorf("failed to execute request: %w", errDo)
@@ -2378,7 +2373,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec
return false, fmt.Errorf("failed to create request: %w", errRequest) return false, fmt.Errorf("failed to create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", getGeminiCLIUserAgent()) req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))
resp, errDo = httpClient.Do(req) resp, errDo = httpClient.Do(req)
if errDo != nil { if errDo != nil {
return false, fmt.Errorf("failed to execute request: %w", errDo) return false, fmt.Errorf("failed to execute request: %w", errDo)

View File

@@ -28,14 +28,10 @@ import (
) )
const ( const (
geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com"
geminiCLIVersion = "v1internal" geminiCLIVersion = "v1internal"
) )
func getGeminiCLIUserAgent() string {
return misc.GeminiCLIUserAgent("")
}
type projectSelectionRequiredError struct{} type projectSelectionRequiredError struct{}
func (e *projectSelectionRequiredError) Error() string { func (e *projectSelectionRequiredError) Error() string {
@@ -411,7 +407,7 @@ func callGeminiCLI(ctx context.Context, httpClient *http.Client, endpoint string
return fmt.Errorf("create request: %w", errRequest) return fmt.Errorf("create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", getGeminiCLIUserAgent()) req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))
resp, errDo := httpClient.Do(req) resp, errDo := httpClient.Do(req)
if errDo != nil { if errDo != nil {
@@ -630,7 +626,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec
return false, fmt.Errorf("failed to create request: %w", errRequest) return false, fmt.Errorf("failed to create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", getGeminiCLIUserAgent()) req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))
resp, errDo := httpClient.Do(req) resp, errDo := httpClient.Do(req)
if errDo != nil { if errDo != nil {
return false, fmt.Errorf("failed to execute request: %w", errDo) return false, fmt.Errorf("failed to execute request: %w", errDo)
@@ -651,7 +647,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec
return false, fmt.Errorf("failed to create request: %w", errRequest) return false, fmt.Errorf("failed to create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", getGeminiCLIUserAgent()) req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))
resp, errDo = httpClient.Do(req) resp, errDo = httpClient.Do(req)
if errDo != nil { if errDo != nil {
return false, fmt.Errorf("failed to execute request: %w", errDo) return false, fmt.Errorf("failed to execute request: %w", errDo)

View File

@@ -10,13 +10,43 @@ import (
"strings" "strings"
) )
const (
// GeminiCLIVersion is the version string reported in the User-Agent for upstream requests.
GeminiCLIVersion = "0.31.0"
// GeminiCLIApiClientHeader is the value for the X-Goog-Api-Client header sent to the Gemini CLI upstream.
GeminiCLIApiClientHeader = "google-genai-sdk/1.41.0 gl-node/v22.19.0"
)
// geminiCLIOS maps Go runtime OS names to the Node.js-style platform strings used by Gemini CLI.
func geminiCLIOS() string {
switch runtime.GOOS {
case "windows":
return "win32"
default:
return runtime.GOOS
}
}
// geminiCLIArch maps Go runtime architecture names to the Node.js-style arch strings used by Gemini CLI.
func geminiCLIArch() string {
switch runtime.GOARCH {
case "amd64":
return "x64"
case "386":
return "x86"
default:
return runtime.GOARCH
}
}
// GeminiCLIUserAgent returns a User-Agent string that matches the Gemini CLI format. // GeminiCLIUserAgent returns a User-Agent string that matches the Gemini CLI format.
// The model parameter is included in the UA; pass "" or "unknown" when the model is not applicable. // The model parameter is included in the UA; pass "" or "unknown" when the model is not applicable.
func GeminiCLIUserAgent(model string) string { func GeminiCLIUserAgent(model string) string {
if model == "" { if model == "" {
model = "unknown" model = "unknown"
} }
return fmt.Sprintf("GeminiCLI/1.0.0/%s (%s; %s)", model, runtime.GOOS, runtime.GOARCH) return fmt.Sprintf("GeminiCLI/%s/%s (%s; %s)", GeminiCLIVersion, model, geminiCLIOS(), geminiCLIArch())
} }
// ScrubProxyAndFingerprintHeaders removes all headers that could reveal // ScrubProxyAndFingerprintHeaders removes all headers that could reveal
@@ -93,4 +123,3 @@ func EnsureHeader(target http.Header, source http.Header, key, defaultValue stri
target.Set(key, val) target.Set(key, val)
} }
} }

View File

@@ -16,7 +16,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli"
@@ -738,13 +737,11 @@ func stringValue(m map[string]any, key string) string {
} }
// applyGeminiCLIHeaders sets required headers for the Gemini CLI upstream. // applyGeminiCLIHeaders sets required headers for the Gemini CLI upstream.
// User-Agent is always forced to the GeminiCLI format regardless of the client's value,
// so that upstream identifies the request as a native GeminiCLI client.
func applyGeminiCLIHeaders(r *http.Request, model string) { func applyGeminiCLIHeaders(r *http.Request, model string) {
var ginHeaders http.Header r.Header.Set("User-Agent", misc.GeminiCLIUserAgent(model))
if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { r.Header.Set("X-Goog-Api-Client", misc.GeminiCLIApiClientHeader)
ginHeaders = ginCtx.Request.Header
}
misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", misc.GeminiCLIUserAgent(model))
} }
// cliPreviewFallbackOrder returns preview model candidates for a base model. // cliPreviewFallbackOrder returns preview model candidates for a base model.

View File

@@ -22,8 +22,8 @@ var (
// ConvertCodexResponseToClaudeParams holds parameters for response conversion. // ConvertCodexResponseToClaudeParams holds parameters for response conversion.
type ConvertCodexResponseToClaudeParams struct { type ConvertCodexResponseToClaudeParams struct {
HasToolCall bool HasToolCall bool
BlockIndex int BlockIndex int
HasReceivedArgumentsDelta bool HasReceivedArgumentsDelta bool
} }

View File

@@ -264,18 +264,18 @@ func TestConvertSystemRoleToDeveloper_AssistantRole(t *testing.T) {
} }
} }
func TestUserFieldDeletion(t *testing.T) { func TestUserFieldDeletion(t *testing.T) {
inputJSON := []byte(`{ inputJSON := []byte(`{
"model": "gpt-5.2", "model": "gpt-5.2",
"user": "test-user", "user": "test-user",
"input": [{"role": "user", "content": "Hello"}] "input": [{"role": "user", "content": "Hello"}]
}`) }`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false) output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output) outputStr := string(output)
// Verify user field is deleted // Verify user field is deleted
userField := gjson.Get(outputStr, "user") userField := gjson.Get(outputStr, "user")
if userField.Exists() { if userField.Exists() {
t.Errorf("user field should be deleted, but it was found with value: %s", userField.Raw) t.Errorf("user field should be deleted, but it was found with value: %s", userField.Raw)
} }