Merge remote-tracking branch 'upstream/main'

This commit is contained in:
781456868@qq.com
2026-01-20 21:57:16 +08:00
37 changed files with 1489 additions and 426 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ func resolveEndpointOverride(modelName, requestedEndpoint string) (string, bool)
if modelName == "" {
return "", false
}
info := registry.GetGlobalRegistry().GetModelInfo(modelName)
info := registry.GetGlobalRegistry().GetModelInfo(modelName, "")
if info == nil || len(info.SupportedEndpoints) == 0 {
return "", false
}
@@ -34,4 +34,4 @@ func endpointListContains(items []string, value string) bool {
}
}
return false
}
}
+15 -1
View File
@@ -2,6 +2,8 @@ package auth
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
@@ -191,7 +193,19 @@ waitForCallback:
return nil, fmt.Errorf("codex token storage missing account information")
}
fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email)
planType := ""
hashAccountID := ""
if tokenStorage.IDToken != "" {
if claims, errParse := codex.ParseJWTToken(tokenStorage.IDToken); errParse == nil && claims != nil {
planType = strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType)
accountID := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID)
if accountID != "" {
digest := sha256.Sum256([]byte(accountID))
hashAccountID = hex.EncodeToString(digest[:])[:8]
}
}
}
fileName := codex.CredentialFileName(tokenStorage.Email, planType, hashAccountID, true)
metadata := map[string]any{
"email": tokenStorage.Email,
}
+90 -18
View File
@@ -8,7 +8,6 @@ import (
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"time"
@@ -76,7 +75,7 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str
if existing, errRead := os.ReadFile(path); errRead == nil {
// Use metadataEqualIgnoringTimestamps to skip writes when only timestamp fields change.
// This prevents the token refresh loop caused by timestamp/expired/expires_in changes.
if metadataEqualIgnoringTimestamps(existing, raw) {
if metadataEqualIgnoringTimestamps(existing, raw, auth.Provider) {
return path, nil
}
file, errOpen := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0o600)
@@ -309,28 +308,101 @@ func (s *FileTokenStore) baseDirSnapshot() string {
return s.baseDir
}
// metadataEqualIgnoringTimestamps compares two metadata JSON blobs, ignoring volatile fields that
// change on every refresh but don't affect authentication logic.
func metadataEqualIgnoringTimestamps(a, b []byte) bool {
var objA map[string]any
var objB map[string]any
if errUnmarshalA := json.Unmarshal(a, &objA); errUnmarshalA != nil {
// DEPRECATED: Use metadataEqualIgnoringTimestamps for comparing auth metadata.
// This function is kept for backward compatibility but can cause refresh loops.
func jsonEqual(a, b []byte) bool {
var objA any
var objB any
if err := json.Unmarshal(a, &objA); err != nil {
return false
}
if errUnmarshalB := json.Unmarshal(b, &objB); errUnmarshalB != nil {
if err := json.Unmarshal(b, &objB); err != nil {
return false
}
stripVolatileMetadataFields(objA)
stripVolatileMetadataFields(objB)
return reflect.DeepEqual(objA, objB)
return deepEqualJSON(objA, objB)
}
func stripVolatileMetadataFields(metadata map[string]any) {
if metadata == nil {
return
// metadataEqualIgnoringTimestamps compares two metadata JSON blobs,
// ignoring fields that change on every refresh but don't affect functionality.
// This prevents unnecessary file writes that would trigger watcher events and
// create refresh loops.
// The provider parameter controls whether access_token is ignored: providers like
// Google OAuth (gemini, gemini-cli) can re-fetch tokens when needed, while others
// like iFlow require the refreshed token to be persisted.
func metadataEqualIgnoringTimestamps(a, b []byte, provider string) bool {
var objA, objB map[string]any
if err := json.Unmarshal(a, &objA); err != nil {
return false
}
// These fields change on refresh and would otherwise trigger watcher reload loops.
for _, field := range []string{"timestamp", "expired", "expires_in", "last_refresh", "access_token"} {
delete(metadata, field)
if err := json.Unmarshal(b, &objB); err != nil {
return false
}
// Fields to ignore: these change on every refresh but don't affect authentication logic.
// - timestamp, expired, expires_in, last_refresh: time-related fields that change on refresh
ignoredFields := []string{"timestamp", "expired", "expires_in", "last_refresh"}
// For providers that can re-fetch tokens when needed (e.g., Google OAuth),
// we ignore access_token to avoid unnecessary file writes.
switch provider {
case "gemini", "gemini-cli", "antigravity":
ignoredFields = append(ignoredFields, "access_token")
}
for _, field := range ignoredFields {
delete(objA, field)
delete(objB, field)
}
return deepEqualJSON(objA, objB)
}
func deepEqualJSON(a, b any) bool {
switch valA := a.(type) {
case map[string]any:
valB, ok := b.(map[string]any)
if !ok || len(valA) != len(valB) {
return false
}
for key, subA := range valA {
subB, ok1 := valB[key]
if !ok1 || !deepEqualJSON(subA, subB) {
return false
}
}
return true
case []any:
sliceB, ok := b.([]any)
if !ok || len(valA) != len(sliceB) {
return false
}
for i := range valA {
if !deepEqualJSON(valA[i], sliceB[i]) {
return false
}
}
return true
case float64:
valB, ok := b.(float64)
if !ok {
return false
}
return valA == valB
case string:
valB, ok := b.(string)
if !ok {
return false
}
return valA == valB
case bool:
valB, ok := b.(bool)
if !ok {
return false
}
return valA == valB
case nil:
return b == nil
default:
return false
}
}