refactor(headers): streamline User-Agent handling and introduce GeminiCLI versioning
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user