From 79009bb3d4da31a3d8de193c6683336695766512 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 4 Mar 2026 02:06:24 +0800 Subject: [PATCH 1/5] Fixed: #797 **test(auth): add test for preserving ModelStates during auth updates** --- sdk/cliproxy/auth/conductor.go | 11 +++-- sdk/cliproxy/auth/conductor_update_test.go | 49 ++++++++++++++++++++++ sdk/cliproxy/service.go | 3 ++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 sdk/cliproxy/auth/conductor_update_test.go diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 3434b7a7..ae5b745c 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -463,9 +463,14 @@ func (m *Manager) Update(ctx context.Context, auth *Auth) (*Auth, error) { return nil, nil } m.mu.Lock() - if existing, ok := m.auths[auth.ID]; ok && existing != nil && !auth.indexAssigned && auth.Index == "" { - auth.Index = existing.Index - auth.indexAssigned = existing.indexAssigned + if existing, ok := m.auths[auth.ID]; ok && existing != nil { + if !auth.indexAssigned && auth.Index == "" { + auth.Index = existing.Index + auth.indexAssigned = existing.indexAssigned + } + if len(auth.ModelStates) == 0 && len(existing.ModelStates) > 0 { + auth.ModelStates = existing.ModelStates + } } auth.EnsureIndex() m.auths[auth.ID] = auth.Clone() diff --git a/sdk/cliproxy/auth/conductor_update_test.go b/sdk/cliproxy/auth/conductor_update_test.go new file mode 100644 index 00000000..f058f517 --- /dev/null +++ b/sdk/cliproxy/auth/conductor_update_test.go @@ -0,0 +1,49 @@ +package auth + +import ( + "context" + "testing" +) + +func TestManager_Update_PreservesModelStates(t *testing.T) { + m := NewManager(nil, nil, nil) + + model := "test-model" + backoffLevel := 7 + + if _, errRegister := m.Register(context.Background(), &Auth{ + ID: "auth-1", + Provider: "claude", + Metadata: map[string]any{"k": "v"}, + ModelStates: map[string]*ModelState{ + model: { + Quota: QuotaState{BackoffLevel: backoffLevel}, + }, + }, + }); errRegister != nil { + t.Fatalf("register auth: %v", errRegister) + } + + if _, errUpdate := m.Update(context.Background(), &Auth{ + ID: "auth-1", + Provider: "claude", + Metadata: map[string]any{"k": "v2"}, + }); errUpdate != nil { + t.Fatalf("update auth: %v", errUpdate) + } + + updated, ok := m.GetByID("auth-1") + if !ok || updated == nil { + t.Fatalf("expected auth to be present") + } + if len(updated.ModelStates) == 0 { + t.Fatalf("expected ModelStates to be preserved") + } + state := updated.ModelStates[model] + if state == nil { + t.Fatalf("expected model state to be present") + } + if state.Quota.BackoffLevel != backoffLevel { + t.Fatalf("expected BackoffLevel to be %d, got %d", backoffLevel, state.Quota.BackoffLevel) + } +} diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index 4be83816..9952e7b2 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -290,6 +290,9 @@ func (s *Service) applyCoreAuthAddOrUpdate(ctx context.Context, auth *coreauth.A auth.CreatedAt = existing.CreatedAt auth.LastRefreshedAt = existing.LastRefreshedAt auth.NextRefreshAfter = existing.NextRefreshAfter + if len(auth.ModelStates) == 0 && len(existing.ModelStates) > 0 { + auth.ModelStates = existing.ModelStates + } op = "update" _, err = s.coreManager.Update(ctx, auth) } else { From b48485b42b854d91979d0d75980dad03049615b9 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 4 Mar 2026 02:31:20 +0800 Subject: [PATCH 2/5] Fixed: #822 **fix(auth): normalize ID casing on Windows to prevent duplicate entries due to case-insensitive paths** --- .../api/handlers/management/auth_files.go | 22 +++++++++++-------- internal/watcher/synthesizer/file.go | 5 +++++ sdk/auth/filestore.go | 16 +++++++++----- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index dcff98d7..e0a16377 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -13,6 +13,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "sort" "strconv" "strings" @@ -692,17 +693,20 @@ func (h *Handler) authIDForPath(path string) string { if path == "" { return "" } - if h == nil || h.cfg == nil { - return path + id := path + if h != nil && h.cfg != nil { + authDir := strings.TrimSpace(h.cfg.AuthDir) + if authDir != "" { + if rel, errRel := filepath.Rel(authDir, path); errRel == nil && rel != "" { + id = rel + } + } } - authDir := strings.TrimSpace(h.cfg.AuthDir) - if authDir == "" { - return path + // On Windows, normalize ID casing to avoid duplicate auth entries caused by case-insensitive paths. + if runtime.GOOS == "windows" { + id = strings.ToLower(id) } - if rel, err := filepath.Rel(authDir, path); err == nil && rel != "" { - return rel - } - return path + return id } func (h *Handler) registerAuthFromFile(ctx context.Context, path string, data []byte) error { diff --git a/internal/watcher/synthesizer/file.go b/internal/watcher/synthesizer/file.go index 4e053117..ea96118b 100644 --- a/internal/watcher/synthesizer/file.go +++ b/internal/watcher/synthesizer/file.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strconv" "strings" "time" @@ -72,6 +73,10 @@ func (s *FileSynthesizer) Synthesize(ctx *SynthesisContext) ([]*coreauth.Auth, e if rel, errRel := filepath.Rel(ctx.AuthDir, full); errRel == nil && rel != "" { id = rel } + // On Windows, normalize ID casing to avoid duplicate auth entries caused by case-insensitive paths. + if runtime.GOOS == "windows" { + id = strings.ToLower(id) + } proxyURL := "" if p, ok := metadata["proxy_url"].(string); ok { diff --git a/sdk/auth/filestore.go b/sdk/auth/filestore.go index c424a89b..987d305e 100644 --- a/sdk/auth/filestore.go +++ b/sdk/auth/filestore.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "path/filepath" + "runtime" "strings" "sync" "time" @@ -257,14 +258,17 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth, } func (s *FileTokenStore) idFor(path, baseDir string) string { - if baseDir == "" { - return path + id := path + if baseDir != "" { + if rel, errRel := filepath.Rel(baseDir, path); errRel == nil && rel != "" { + id = rel + } } - rel, err := filepath.Rel(baseDir, path) - if err != nil { - return path + // On Windows, normalize ID casing to avoid duplicate auth entries caused by case-insensitive paths. + if runtime.GOOS == "windows" { + id = strings.ToLower(id) } - return rel + return id } func (s *FileTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error) { From 5c84d69d42bd5bf76a946ac740de26de6a74d9ad Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:11:07 +0800 Subject: [PATCH 3/5] feat(translator): map output_config.effort to adaptive thinking level in antigravity --- .../claude/antigravity_claude_request.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index c4e07b6a..35387488 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -441,9 +441,19 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) } case "adaptive", "auto": - // Keep adaptive/auto as a high level sentinel; ApplyThinking resolves it - // to model-specific max capability. - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") + // Adaptive/auto thinking: + // - If output_config.effort is present, pass it through as thinkingLevel. + // - Otherwise, default to "high". + // ApplyThinking later normalizes/clamps and may convert level → budget per target model. + effort := "" + if v := gjson.GetBytes(rawJSON, "output_config.effort"); v.Exists() && v.Type == gjson.String { + effort = strings.ToLower(strings.TrimSpace(v.String())) + } + if effort != "" { + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort) + } else { + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") + } out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) } } From b680c146c1b25a5e45437cdb2065aa91f2e6aea7 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 4 Mar 2026 18:29:23 +0800 Subject: [PATCH 4/5] chore(docs): update sponsor image links in README files --- README.md | 2 +- README_CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 80f6fbd0..8491b97c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ So you can use local or multi-account CLI access with OpenAI(include Responses)/ ## Sponsor -[![z.ai](https://assets.router-for.me/english-5.png)](https://z.ai/subscribe?ic=8JVLJQFSKB) +[![z.ai](https://assets.router-for.me/english-5-0.jpg)](https://z.ai/subscribe?ic=8JVLJQFSKB) This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN. diff --git a/README_CN.md b/README_CN.md index add9c5cf..6e987fdf 100644 --- a/README_CN.md +++ b/README_CN.md @@ -10,7 +10,7 @@ ## 赞助商 -[![bigmodel.cn](https://assets.router-for.me/chinese-5.png)](https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII) +[![bigmodel.cn](https://assets.router-for.me/chinese-5-0.jpg)](https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII) 本项目由 Z智谱 提供赞助, 他们通过 GLM CODING PLAN 对本项目提供技术支持。 From 48ffc4dee745bf291e8d40cf709091655f1e3e7b Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:47:42 +0800 Subject: [PATCH 5/5] feat(config): support excluded vertex models in config --- config.example.yaml | 3 +++ .../api/handlers/management/config_lists.go | 17 +++++++++++------ internal/config/vertex_compat.go | 4 ++++ internal/watcher/diff/config_diff.go | 5 +++++ internal/watcher/synthesizer/config.go | 2 +- sdk/cliproxy/service.go | 7 +++++-- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 7a3265b4..40bb8721 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -201,6 +201,9 @@ nonstream-keepalive-interval: 0 # alias: "vertex-flash" # client-visible alias # - name: "gemini-2.5-pro" # alias: "vertex-pro" +# excluded-models: # optional: models to exclude from listing +# - "imagen-3.0-generate-002" +# - "imagen-*" # Amp Integration # ampcode: diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 66e89992..503179c1 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -516,12 +516,13 @@ func (h *Handler) PutVertexCompatKeys(c *gin.Context) { } func (h *Handler) PatchVertexCompatKey(c *gin.Context) { type vertexCompatPatch struct { - APIKey *string `json:"api-key"` - Prefix *string `json:"prefix"` - BaseURL *string `json:"base-url"` - ProxyURL *string `json:"proxy-url"` - Headers *map[string]string `json:"headers"` - Models *[]config.VertexCompatModel `json:"models"` + APIKey *string `json:"api-key"` + Prefix *string `json:"prefix"` + BaseURL *string `json:"base-url"` + ProxyURL *string `json:"proxy-url"` + Headers *map[string]string `json:"headers"` + Models *[]config.VertexCompatModel `json:"models"` + ExcludedModels *[]string `json:"excluded-models"` } var body struct { Index *int `json:"index"` @@ -585,6 +586,9 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) { if body.Value.Models != nil { entry.Models = append([]config.VertexCompatModel(nil), (*body.Value.Models)...) } + if body.Value.ExcludedModels != nil { + entry.ExcludedModels = config.NormalizeExcludedModels(*body.Value.ExcludedModels) + } normalizeVertexCompatKey(&entry) h.cfg.VertexCompatAPIKey[targetIndex] = entry h.cfg.SanitizeVertexCompatKeys() @@ -1025,6 +1029,7 @@ func normalizeVertexCompatKey(entry *config.VertexCompatKey) { entry.BaseURL = strings.TrimSpace(entry.BaseURL) entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) entry.Headers = config.NormalizeHeaders(entry.Headers) + entry.ExcludedModels = config.NormalizeExcludedModels(entry.ExcludedModels) if len(entry.Models) == 0 { return } diff --git a/internal/config/vertex_compat.go b/internal/config/vertex_compat.go index 786c5318..5f6c7c88 100644 --- a/internal/config/vertex_compat.go +++ b/internal/config/vertex_compat.go @@ -34,6 +34,9 @@ type VertexCompatKey struct { // Models defines the model configurations including aliases for routing. Models []VertexCompatModel `yaml:"models,omitempty" json:"models,omitempty"` + + // ExcludedModels lists model IDs that should be excluded for this provider. + ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` } func (k VertexCompatKey) GetAPIKey() string { return k.APIKey } @@ -74,6 +77,7 @@ func (cfg *Config) SanitizeVertexCompatKeys() { } entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) entry.Headers = NormalizeHeaders(entry.Headers) + entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels) // Sanitize models: remove entries without valid alias sanitizedModels := make([]VertexCompatModel, 0, len(entry.Models)) diff --git a/internal/watcher/diff/config_diff.go b/internal/watcher/diff/config_diff.go index b7d537da..7997f04e 100644 --- a/internal/watcher/diff/config_diff.go +++ b/internal/watcher/diff/config_diff.go @@ -304,6 +304,11 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string { if oldModels.hash != newModels.hash { changes = append(changes, fmt.Sprintf("vertex[%d].models: updated (%d -> %d entries)", i, oldModels.count, newModels.count)) } + oldExcluded := SummarizeExcludedModels(o.ExcludedModels) + newExcluded := SummarizeExcludedModels(n.ExcludedModels) + if oldExcluded.hash != newExcluded.hash { + changes = append(changes, fmt.Sprintf("vertex[%d].excluded-models: updated (%d -> %d entries)", i, oldExcluded.count, newExcluded.count)) + } if !equalStringMap(o.Headers, n.Headers) { changes = append(changes, fmt.Sprintf("vertex[%d].headers: updated", i)) } diff --git a/internal/watcher/synthesizer/config.go b/internal/watcher/synthesizer/config.go index 69194efc..52ae9a48 100644 --- a/internal/watcher/synthesizer/config.go +++ b/internal/watcher/synthesizer/config.go @@ -315,7 +315,7 @@ func (s *ConfigSynthesizer) synthesizeVertexCompat(ctx *SynthesisContext) []*cor CreatedAt: now, UpdatedAt: now, } - ApplyAuthExcludedModelsMeta(a, cfg, nil, "apikey") + ApplyAuthExcludedModelsMeta(a, cfg, compat.ExcludedModels, "apikey") out = append(out, a) } return out diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index 9952e7b2..6124f8b1 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -791,10 +791,13 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) { case "vertex": // Vertex AI Gemini supports the same model identifiers as Gemini. models = registry.GetGeminiVertexModels() - if authKind == "apikey" { - if entry := s.resolveConfigVertexCompatKey(a); entry != nil && len(entry.Models) > 0 { + if entry := s.resolveConfigVertexCompatKey(a); entry != nil { + if len(entry.Models) > 0 { models = buildVertexCompatConfigModels(entry) } + if authKind == "apikey" { + excluded = entry.ExcludedModels + } } models = applyExcludedModels(models, excluded) case "gemini-cli":