From 271f9bfe0aee00c8531ed6b08c73059294714ec9 Mon Sep 17 00:00:00 2001 From: sfun <15905889+ssfun@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:23:39 +0800 Subject: [PATCH] refactor: remove low-value maintenance hardcodes --- .github/workflows/ci.yml | 4 +- .github/workflows/release-core.yml | 4 +- .github/workflows/release-management.yml | 4 +- .../patches/account_inspection_scheduler.go | 89 +++++++------------ .../account_inspection_scheduler_test.go | 12 +-- .../patches/plugin_gemini_cli_quota_legacy.go | 51 +++++------ .../plugin_gemini_cli_quota_legacy_test.go | 6 +- cliproxyapi-pro-management/README.md | 4 +- cliproxyapi-pro-management/README_EN.md | 4 +- .../overlay-replacements.json | 4 - .../extensions/quota/geminiCliQuotaConfig.ts | 82 +++++++---------- .../tests/geminiCliQuotaGroups.test.ts | 48 ++++++++++ compatibility/upstream.env | 1 - 13 files changed, 153 insertions(+), 160 deletions(-) create mode 100644 cliproxyapi-pro-management/overlay/tests/geminiCliQuotaGroups.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5bd6c7..4fd18d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,6 @@ jobs: management_repo: ${{ steps.resolve.outputs.management_repo }} management_tag: ${{ steps.resolve.outputs.management_tag }} management_sha: ${{ steps.resolve.outputs.management_sha }} - bun_version: ${{ steps.resolve.outputs.bun_version }} mode: ${{ steps.resolve.outputs.mode }} steps: @@ -73,7 +72,6 @@ jobs: echo "management_repo=${MANAGEMENT_UPSTREAM_REPO}" >> "${GITHUB_OUTPUT}" echo "management_tag=${management_tag}" >> "${GITHUB_OUTPUT}" echo "management_sha=${management_sha}" >> "${GITHUB_OUTPUT}" - echo "bun_version=${BUN_VERSION}" >> "${GITHUB_OUTPUT}" repo-tests: runs-on: ubuntu-latest @@ -185,7 +183,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: ${{ needs.resolve-upstream.outputs.bun_version }} + bun-version-file: upstream-management/package.json - name: Cache Bun dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index b3ac081..bad0dab 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -209,7 +209,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: 1.3.14 + bun-version-file: upstream-management/package.json - name: Cache Bun dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 @@ -767,7 +767,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: 1.3.14 + bun-version-file: upstream-management/package.json - name: Cache Bun dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 diff --git a/.github/workflows/release-management.yml b/.github/workflows/release-management.yml index 041c377..dfb78a8 100644 --- a/.github/workflows/release-management.yml +++ b/.github/workflows/release-management.yml @@ -163,7 +163,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: 1.3.14 + bun-version-file: upstream/package.json - name: Cache Bun dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 @@ -212,7 +212,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: 1.3.14 + bun-version-file: upstream/package.json - name: Cache Bun dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 diff --git a/cliproxyapi-pro-core/patches/account_inspection_scheduler.go b/cliproxyapi-pro-core/patches/account_inspection_scheduler.go index f1079e2..6c64303 100644 --- a/cliproxyapi-pro-core/patches/account_inspection_scheduler.go +++ b/cliproxyapi-pro-core/patches/account_inspection_scheduler.go @@ -4691,17 +4691,13 @@ func buildGeminiCLIQuotaBuckets(body string) ([]map[string]any, *float64, error) return nil, nil, fmt.Errorf("empty Gemini CLI quota buckets") } type geminiBucketGroup struct { - ID string - Label string - TokenType string - ModelIDs []string - PreferredModelID string - PreferredRemaining *float64 - PreferredRemainingAmount any - PreferredResetTime string - FallbackRemaining *float64 - FallbackRemainingAmount any - FallbackResetTime string + ID string + Label string + TokenType string + ModelIDs []string + Remaining *float64 + RemainingAmount *float64 + ResetTime string } groups := make(map[string]*geminiBucketGroup) for _, rawBucket := range rawBuckets { @@ -4714,47 +4710,35 @@ func buildGeminiCLIQuotaBuckets(body string) ([]map[string]any, *float64, error) continue } tokenType := stringFromAny(firstAny(bucket, "tokenType", "token_type")) - groupID, label, preferredModelID := geminiCLIQuotaGroupMeta(modelID) + groupID, label := geminiCLIQuotaGroupMeta(modelID) mapKey := groupID + "::" + tokenType group := groups[mapKey] if group == nil { - group = &geminiBucketGroup{ID: groupID, Label: label, TokenType: tokenType, PreferredModelID: preferredModelID} + group = &geminiBucketGroup{ID: groupID, Label: label, TokenType: tokenType} groups[mapKey] = group } group.ModelIDs = append(group.ModelIDs, modelID) remaining := geminiCLIRemainingFraction(bucket) - remainingAmount := firstAny(bucket, "remainingAmount", "remaining_amount") - resetTime := stringFromAny(firstAny(bucket, "resetTime", "reset_time")) - group.FallbackRemaining = minFloatPtr(group.FallbackRemaining, remaining) - group.FallbackResetTime = pickEarlierResetTime(group.FallbackResetTime, resetTime) - if group.FallbackRemainingAmount == nil { - group.FallbackRemainingAmount = remainingAmount - } - if preferredModelID != "" && modelID == preferredModelID { - group.PreferredRemaining = remaining - group.PreferredRemainingAmount = remainingAmount - group.PreferredResetTime = resetTime + var remainingAmount *float64 + if value, ok := floatFromAny(firstAny(bucket, "remainingAmount", "remaining_amount")); ok { + remainingAmount = &value } + resetTime := stringFromAny(firstAny(bucket, "resetTime", "reset_time")) + group.Remaining = minFloatPtr(group.Remaining, remaining) + group.RemainingAmount = minFloatPtr(group.RemainingAmount, remainingAmount) + group.ResetTime = pickEarlierResetTime(group.ResetTime, resetTime) } if len(groups) == 0 { return nil, nil, fmt.Errorf("empty Gemini CLI quota buckets") } out := make([]map[string]any, 0, len(groups)) for _, group := range groups { - remaining := group.FallbackRemaining - remainingAmount := group.FallbackRemainingAmount - resetTime := group.FallbackResetTime - if group.PreferredRemaining != nil { - remaining = group.PreferredRemaining - remainingAmount = group.PreferredRemainingAmount - resetTime = group.PreferredResetTime - } item := map[string]any{ "id": geminiCLIQuotaBucketID(group.ID, group.TokenType), "label": group.Label, - "remainingFraction": floatPtrAny(remaining), - "remainingAmount": normalizeGeminiCLIRemainingAmount(remainingAmount), - "resetTime": emptyStringAsNil(resetTime), + "remainingFraction": floatPtrAny(group.Remaining), + "remainingAmount": floatPtrAny(group.RemainingAmount), + "resetTime": emptyStringAsNil(group.ResetTime), "tokenType": emptyStringAsNil(group.TokenType), "modelIds": uniqueStrings(group.ModelIDs), } @@ -4786,17 +4770,19 @@ func isIgnoredGeminiCLIModel(modelID string) bool { return modelID == "gemini-2.0-flash" || strings.HasPrefix(modelID, "gemini-2.0-flash-") } -func geminiCLIQuotaGroupMeta(modelID string) (string, string, string) { - switch modelID { - case "gemini-2.5-flash-lite": - return "gemini-flash-lite-series", "Gemini Flash Lite Series", "gemini-2.5-flash-lite" - case "gemini-3-flash-preview", "gemini-2.5-flash": - return "gemini-flash-series", "Gemini Flash Series", "gemini-3-flash-preview" - case "gemini-3.1-pro-preview", "gemini-3-pro-preview", "gemini-2.5-pro": - return "gemini-pro-series", "Gemini Pro Series", "gemini-3.1-pro-preview" - default: - return modelID, modelID, modelID +func geminiCLIQuotaGroupMeta(modelID string) (string, string) { + normalized := strings.ToLower(strings.TrimSpace(modelID)) + if strings.HasPrefix(normalized, "gemini-") { + switch { + case strings.Contains(normalized, "-flash-lite"): + return "gemini-flash-lite-series", "Gemini Flash Lite Series" + case strings.Contains(normalized, "-flash"): + return "gemini-flash-series", "Gemini Flash Series" + case strings.Contains(normalized, "-pro"): + return "gemini-pro-series", "Gemini Pro Series" + } } + return modelID, modelID } func geminiCLIQuotaBucketID(groupID string, tokenType string) string { @@ -4836,19 +4822,6 @@ func geminiCLIRemainingFraction(bucket map[string]any) *float64 { return nil } -func normalizeGeminiCLIRemainingAmount(value any) any { - if value == nil { - return nil - } - if number, ok := floatFromAny(value); ok { - return number - } - if text := stringFromAny(value); text != "" { - return text - } - return nil -} - func buildGeminiCLISubscription(payload map[string]any) map[string]any { if payload == nil { return nil diff --git a/cliproxyapi-pro-core/patches/account_inspection_scheduler_test.go b/cliproxyapi-pro-core/patches/account_inspection_scheduler_test.go index 29fe7b7..e5c6b39 100644 --- a/cliproxyapi-pro-core/patches/account_inspection_scheduler_test.go +++ b/cliproxyapi-pro-core/patches/account_inspection_scheduler_test.go @@ -1053,13 +1053,13 @@ func TestBuildAntigravityGroupsCanonicalizesLatestGroups(t *testing.T) { } } -func TestBuildGeminiCLIQuotaBucketsGroupsLatestModels(t *testing.T) { +func TestBuildGeminiCLIQuotaBucketsGroupsModelFamiliesConservatively(t *testing.T) { body := `{ "buckets": [ {"modelId": "gemini-2.5-flash-lite_vertex", "tokenType": "input", "remainingFraction": 0.8, "remainingAmount": "80", "resetTime": "2026-06-22T01:00:00Z"}, - {"model_id": "gemini-3-flash-preview", "token_type": "input", "remaining_fraction": 0.6, "remaining_amount": 60, "reset_time": "2026-06-22T02:00:00Z"}, + {"model_id": "gemini-4-flash-preview", "token_type": "input", "remaining_fraction": 0.6, "remaining_amount": 60, "reset_time": "2026-06-22T02:00:00Z"}, {"modelId": "gemini-2.5-flash", "tokenType": "input", "remainingFraction": 0.4, "remainingAmount": 40, "resetTime": "2026-06-22T03:00:00Z"}, - {"modelId": "gemini-3.1-pro-preview", "tokenType": "output", "remainingFraction": 0.2, "remainingAmount": 20, "resetTime": "2026-06-22T04:00:00Z"}, + {"modelId": "gemini-4-pro-preview", "tokenType": "output", "remainingFraction": 0.2, "remainingAmount": 20, "resetTime": "2026-06-22T04:00:00Z"}, {"modelId": "gemini-2.0-flash", "tokenType": "input", "remainingFraction": 0} ] }` @@ -1074,11 +1074,11 @@ func TestBuildGeminiCLIQuotaBucketsGroupsLatestModels(t *testing.T) { if buckets[0]["id"] != "gemini-flash-lite-series-input" || buckets[1]["id"] != "gemini-flash-series-input" || buckets[2]["id"] != "gemini-pro-series-output" { t.Fatalf("bucket order/ids = %#v", buckets) } - if buckets[1]["remainingFraction"] != 0.6 { - t.Fatalf("flash series remaining = %#v, want preferred 0.6", buckets[1]["remainingFraction"]) + if buckets[1]["remainingFraction"] != 0.4 || buckets[1]["remainingAmount"] != 40.0 { + t.Fatalf("flash series remaining = %#v/%#v, want conservative 0.4/40", buckets[1]["remainingFraction"], buckets[1]["remainingAmount"]) } modelIDs, ok := buckets[1]["modelIds"].([]string) - if !ok || len(modelIDs) != 2 || modelIDs[0] != "gemini-3-flash-preview" || modelIDs[1] != "gemini-2.5-flash" { + if !ok || len(modelIDs) != 2 || modelIDs[0] != "gemini-4-flash-preview" || modelIDs[1] != "gemini-2.5-flash" { t.Fatalf("flash series modelIds = %#v", buckets[1]["modelIds"]) } if used == nil || math.Abs(*used-80) > 0.000001 { diff --git a/cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy.go b/cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy.go index 52b7971..971aa92 100644 --- a/cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy.go +++ b/cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy.go @@ -153,24 +153,29 @@ type legacyGeminiCLIBucket struct { } type legacyGeminiCLIGroup struct { - id, label, preferred string - models []string + id, label string } -var legacyGeminiCLIGroups = []legacyGeminiCLIGroup{ - {id: "gemini-flash-lite-series", label: "Gemini Flash Lite Series", preferred: "gemini-2.5-flash-lite", models: []string{"gemini-2.5-flash-lite"}}, - {id: "gemini-flash-series", label: "Gemini Flash Series", preferred: "gemini-3-flash-preview", models: []string{"gemini-3-flash-preview", "gemini-2.5-flash"}}, - {id: "gemini-pro-series", label: "Gemini Pro Series", preferred: "gemini-3.1-pro-preview", models: []string{"gemini-3.1-pro-preview", "gemini-3-pro-preview", "gemini-2.5-pro"}}, +func legacyGeminiCLIGroupForModel(modelID string) legacyGeminiCLIGroup { + normalized := strings.ToLower(strings.TrimSpace(modelID)) + if strings.HasPrefix(normalized, "gemini-") { + switch { + case strings.Contains(normalized, "-flash-lite"): + return legacyGeminiCLIGroup{id: "gemini-flash-lite-series", label: "Gemini Flash Lite Series"} + case strings.Contains(normalized, "-flash"): + return legacyGeminiCLIGroup{id: "gemini-flash-series", label: "Gemini Flash Series"} + case strings.Contains(normalized, "-pro"): + return legacyGeminiCLIGroup{id: "gemini-pro-series", label: "Gemini Pro Series"} + } + } + return legacyGeminiCLIGroup{id: modelID, label: modelID} } func legacyGeminiCLIQuotaItems(payload map[string]any) []pluginapi.QuotaItem { - lookup := make(map[string]legacyGeminiCLIGroup) - order := make(map[string]int) - for index, group := range legacyGeminiCLIGroups { - order[group.id] = index - for _, model := range group.models { - lookup[model] = group - } + order := map[string]int{ + "gemini-flash-lite-series": 0, + "gemini-flash-series": 1, + "gemini-pro-series": 2, } type aggregate struct { group legacyGeminiCLIGroup @@ -182,10 +187,7 @@ func legacyGeminiCLIQuotaItems(payload map[string]any) []pluginapi.QuotaItem { if bucket.modelID == "gemini-2.0-flash" || strings.HasPrefix(bucket.modelID, "gemini-2.0-flash-") { continue } - group, okGroup := lookup[bucket.modelID] - if !okGroup { - group = legacyGeminiCLIGroup{id: bucket.modelID, label: bucket.modelID, preferred: bucket.modelID, models: []string{bucket.modelID}} - } + group := legacyGeminiCLIGroupForModel(bucket.modelID) key := group.id + "\x00" + bucket.tokenType if aggregates[key] == nil { aggregates[key] = &aggregate{group: group, token: bucket.tokenType} @@ -196,18 +198,9 @@ func legacyGeminiCLIQuotaItems(payload map[string]any) []pluginapi.QuotaItem { items := make([]pluginapi.QuotaItem, 0, len(aggregates)) for _, aggregate := range aggregates { chosen := aggregate.buckets[0] - foundPreferred := false - for _, bucket := range aggregate.buckets { - if bucket.modelID == aggregate.group.preferred { - chosen, foundPreferred = bucket, true - break - } - } - if !foundPreferred { - for _, bucket := range aggregate.buckets[1:] { - if legacyGeminiCLILessNullable(bucket.remainingFraction, chosen.remainingFraction) { - chosen = bucket - } + for _, bucket := range aggregate.buckets[1:] { + if legacyGeminiCLILessNullable(bucket.remainingFraction, chosen.remainingFraction) { + chosen = bucket } } modelIDs := make([]string, 0, len(aggregate.buckets)) diff --git a/cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy_test.go b/cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy_test.go index ead108f..522ce46 100644 --- a/cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy_test.go +++ b/cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy_test.go @@ -40,7 +40,8 @@ func (e *legacyGeminiCLIQuotaTestExecutor) HttpRequest(_ context.Context, _ *cor Header: make(http.Header), Body: io.NopCloser(bytes.NewBufferString(`{"buckets":[ {"modelId":"gemini-2.0-flash","remainingFraction":0.9}, - {"modelId":"gemini-3.1-pro-preview","remainingFraction":0.75,"remainingAmount":42} + {"modelId":"gemini-4-pro-preview","remainingFraction":0.75,"remainingAmount":42}, + {"modelId":"gemini-3.1-pro-preview","remainingFraction":0.25,"remainingAmount":21} ]}`)), }, nil } @@ -87,6 +88,9 @@ func TestLegacyGeminiCLIQuotaAdapterUsesRegisteredExecutorAndRetainsPlan(t *test if len(result.Snapshot.Items) != 1 || result.Snapshot.Items[0].ID != "gemini-pro-series" { t.Fatalf("quota items = %#v", result.Snapshot.Items) } + if remaining := result.Snapshot.Items[0].RemainingFraction; remaining == nil || *remaining != 0.25 { + t.Fatalf("remaining fraction = %v, want conservative 0.25", remaining) + } if result.Snapshot.Plan == nil || result.Snapshot.Plan.ID != "g1-pro-tier" || !result.Snapshot.Plan.Stale { t.Fatalf("retained plan = %#v", result.Snapshot.Plan) } diff --git a/cliproxyapi-pro-management/README.md b/cliproxyapi-pro-management/README.md index 36bd7cb..3d51dba 100644 --- a/cliproxyapi-pro-management/README.md +++ b/cliproxyapi-pro-management/README.md @@ -157,13 +157,13 @@ UI 会在主布局中启动 `QuotaPersistenceBootstrap`,把已保存的配额 - `overlay/src/features/monitoring/` — 监控与巡检逻辑。 - `overlay/src/extensions/quota/` — SQLite 配额持久化集成。 - `overlay/src/services/api/` — 新增 API clients。 -- `overlay-replacements.json` — 对有意覆盖 upstream 同路径文件的 full-file replacements,同时记录已审阅的 upstream 与 overlay SHA-256。 +- `overlay-replacements.json` — 对有意覆盖 upstream 同路径文件的 full-file replacements,记录已审阅的 upstream SHA-256 与替换原因。 - `monitoring-locales.json` — 合并进 upstream locale 文件的多语言文案。 - `apply_customizations.py` — 将全部定制应用到目标 upstream checkout。 - `apply.sh` — `apply_customizations.py` 的 shell 包装脚本。 - `quota-persistence.patch` — 历史补丁文件,保留用于参考;当前构建使用 `apply_customizations.py`。 -Overlay collision 预检会同时校验每个已审阅替换的两侧内容。upstream 文件或本地替换文件发生变化时,都必须显式更新 `overlay-replacements.json`;新的未审阅路径冲突会在复制任何 overlay 文件前被拒绝。 +Overlay collision 预检会校验每个已审阅替换的 upstream 内容。upstream 文件变化时必须显式更新 `overlay-replacements.json`;本地替换通过正常 PR diff 和行为测试审查,新的未审阅路径冲突会在复制任何 overlay 文件前被拒绝。 ## 本地应用 diff --git a/cliproxyapi-pro-management/README_EN.md b/cliproxyapi-pro-management/README_EN.md index 361dc4c..64e768c 100644 --- a/cliproxyapi-pro-management/README_EN.md +++ b/cliproxyapi-pro-management/README_EN.md @@ -134,13 +134,13 @@ Request Monitoring uses an initial snapshot plus SSE increments and cursor catch - `overlay/src/features/monitoring/` — monitoring and inspection logic. - `overlay/src/extensions/quota/` — SQLite quota persistence integration. - `overlay/src/services/api/` — added API clients. -- `overlay-replacements.json` — reviewed upstream and overlay SHA-256 pairs for the full-file replacements that intentionally collide with upstream paths. +- `overlay-replacements.json` — reviewed upstream SHA-256 values and reasons for full-file replacements that intentionally collide with upstream paths. - `monitoring-locales.json` — locale additions merged into upstream locale files. - `apply_customizations.py` — applies all customizations to a target upstream checkout. - `apply.sh` — shell wrapper around `apply_customizations.py`. - `quota-persistence.patch` — legacy patch artifact kept for reference; current builds use `apply_customizations.py`. -Overlay collision preflight validates both sides of every reviewed replacement. An upstream file change or a local replacement change must update `overlay-replacements.json` explicitly; new unreviewed path collisions are rejected before any overlay file is copied. +Overlay collision preflight validates the upstream side of every reviewed replacement. Upstream file changes must update `overlay-replacements.json` explicitly; local replacements are reviewed through normal PR diffs and behavior tests, and new unreviewed path collisions are rejected before any overlay file is copied. ## Applying locally diff --git a/cliproxyapi-pro-management/overlay-replacements.json b/cliproxyapi-pro-management/overlay-replacements.json index 9c77f17..068469b 100644 --- a/cliproxyapi-pro-management/overlay-replacements.json +++ b/cliproxyapi-pro-management/overlay-replacements.json @@ -1,9 +1,5 @@ { "schemaVersion": 1, - "upstream": { - "repository": "router-for-me/Cli-Proxy-API-Management-Center", - "tag": "v1.18.5" - }, "replacements": [ { "path": "src/services/api/apiCall.ts", diff --git a/cliproxyapi-pro-management/overlay/src/extensions/quota/geminiCliQuotaConfig.ts b/cliproxyapi-pro-management/overlay/src/extensions/quota/geminiCliQuotaConfig.ts index 6bbf5cb..066716f 100644 --- a/cliproxyapi-pro-management/overlay/src/extensions/quota/geminiCliQuotaConfig.ts +++ b/cliproxyapi-pro-management/overlay/src/extensions/quota/geminiCliQuotaConfig.ts @@ -63,8 +63,6 @@ type GeminiCliQuotaData = { type GeminiCliQuotaGroupDefinition = { id: string; label: string; - preferredModelId?: string; - modelIds: string[]; }; const resolveGeminiCliTierDisplay = ( @@ -106,20 +104,14 @@ const GEMINI_CLI_QUOTA_GROUPS: GeminiCliQuotaGroupDefinition[] = [ { id: 'gemini-flash-lite-series', label: 'Gemini Flash Lite Series', - preferredModelId: 'gemini-2.5-flash-lite', - modelIds: ['gemini-2.5-flash-lite'], }, { id: 'gemini-flash-series', label: 'Gemini Flash Series', - preferredModelId: 'gemini-3-flash-preview', - modelIds: ['gemini-3-flash-preview', 'gemini-2.5-flash'], }, { id: 'gemini-pro-series', label: 'Gemini Pro Series', - preferredModelId: 'gemini-3.1-pro-preview', - modelIds: ['gemini-3.1-pro-preview', 'gemini-3-pro-preview', 'gemini-2.5-pro'], }, ]; @@ -127,11 +119,16 @@ const GEMINI_CLI_GROUP_ORDER = new Map( GEMINI_CLI_QUOTA_GROUPS.map((group, index) => [group.id, index] as const) ); -const GEMINI_CLI_GROUP_LOOKUP = new Map( - GEMINI_CLI_QUOTA_GROUPS.flatMap((group) => - group.modelIds.map((modelId) => [modelId, group] as const) - ) -); +const resolveGeminiCliQuotaGroup = ( + modelId: string +): GeminiCliQuotaGroupDefinition | undefined => { + const normalized = modelId.toLowerCase(); + if (!normalized.startsWith('gemini-')) return undefined; + if (normalized.includes('-flash-lite')) return GEMINI_CLI_QUOTA_GROUPS[0]; + if (normalized.includes('-flash')) return GEMINI_CLI_QUOTA_GROUPS[1]; + if (normalized.includes('-pro')) return GEMINI_CLI_QUOTA_GROUPS[2]; + return undefined; +}; const PREMIUM_GEMINI_CLI_TIER_IDS = new Set(['g1-pro-tier', 'g1-ultra-tier']); @@ -207,7 +204,7 @@ const minNullableNumber = (current: number | null, next: number | null): number return Math.min(current, next); }; -const buildGeminiCliQuotaBuckets = ( +export const buildGeminiCliQuotaBuckets = ( buckets: GeminiCliParsedBucket[] ): GeminiCliQuotaBucketState[] => { if (buckets.length === 0) return []; @@ -217,18 +214,16 @@ const buildGeminiCliQuotaBuckets = ( label: string; tokenType: string | null; modelIds: string[]; - preferredModelId?: string; - preferredBucket?: GeminiCliParsedBucket; - fallbackRemainingFraction: number | null; - fallbackRemainingAmount: number | null; - fallbackResetTime: string | undefined; + remainingFraction: number | null; + remainingAmount: number | null; + resetTime: string | undefined; }; const grouped = new Map(); buckets.forEach((bucket) => { if (isIgnoredGeminiCliModel(bucket.modelId)) return; - const group = GEMINI_CLI_GROUP_LOOKUP.get(bucket.modelId); + const group = resolveGeminiCliQuotaGroup(bucket.modelId); const groupId = group?.id ?? bucket.modelId; const label = group?.label ?? bucket.modelId; const tokenKey = bucket.tokenType ?? ''; @@ -236,36 +231,28 @@ const buildGeminiCliQuotaBuckets = ( const existing = grouped.get(mapKey); if (!existing) { - const preferredModelId = group?.preferredModelId; grouped.set(mapKey, { id: `${groupId}${tokenKey ? `-${tokenKey}` : ''}`, label, tokenType: bucket.tokenType, modelIds: [bucket.modelId], - preferredModelId, - preferredBucket: - preferredModelId && bucket.modelId === preferredModelId ? bucket : undefined, - fallbackRemainingFraction: bucket.remainingFraction, - fallbackRemainingAmount: bucket.remainingAmount, - fallbackResetTime: bucket.resetTime, + remainingFraction: bucket.remainingFraction, + remainingAmount: bucket.remainingAmount, + resetTime: bucket.resetTime, }); return; } - existing.fallbackRemainingFraction = minNullableNumber( - existing.fallbackRemainingFraction, + existing.remainingFraction = minNullableNumber( + existing.remainingFraction, bucket.remainingFraction ); - existing.fallbackRemainingAmount = minNullableNumber( - existing.fallbackRemainingAmount, + existing.remainingAmount = minNullableNumber( + existing.remainingAmount, bucket.remainingAmount ); - existing.fallbackResetTime = pickEarlierResetTime(existing.fallbackResetTime, bucket.resetTime); + existing.resetTime = pickEarlierResetTime(existing.resetTime, bucket.resetTime); existing.modelIds.push(bucket.modelId); - - if (existing.preferredModelId && bucket.modelId === existing.preferredModelId) { - existing.preferredBucket = bucket; - } }); const toGroupOrder = (bucket: BucketGroup): number => { @@ -282,20 +269,15 @@ const buildGeminiCliQuotaBuckets = ( if (orderDiff !== 0) return orderDiff; return (a.tokenType ?? '').localeCompare(b.tokenType ?? ''); }) - .map((bucket) => { - const preferred = bucket.preferredBucket; - return { - id: bucket.id, - label: bucket.label, - remainingFraction: preferred - ? preferred.remainingFraction - : bucket.fallbackRemainingFraction, - remainingAmount: preferred ? preferred.remainingAmount : bucket.fallbackRemainingAmount, - resetTime: preferred ? preferred.resetTime : bucket.fallbackResetTime, - tokenType: bucket.tokenType, - modelIds: Array.from(new Set(bucket.modelIds)), - }; - }); + .map((bucket) => ({ + id: bucket.id, + label: bucket.label, + remainingFraction: bucket.remainingFraction, + remainingAmount: bucket.remainingAmount, + resetTime: bucket.resetTime, + tokenType: bucket.tokenType, + modelIds: Array.from(new Set(bucket.modelIds)), + })); }; const resolveGeminiCliRemainingFraction = (bucket: GeminiCliQuotaBucket): number | null => { diff --git a/cliproxyapi-pro-management/overlay/tests/geminiCliQuotaGroups.test.ts b/cliproxyapi-pro-management/overlay/tests/geminiCliQuotaGroups.test.ts new file mode 100644 index 0000000..27f3191 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/tests/geminiCliQuotaGroups.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from 'bun:test'; +import { buildGeminiCliQuotaBuckets } from '../src/extensions/quota/geminiCliQuotaConfig'; + +describe('Gemini CLI quota families', () => { + test('groups future models conservatively and preserves unknown models', () => { + const buckets = buildGeminiCliQuotaBuckets([ + { + modelId: 'gemini-4-flash-preview', + tokenType: 'input', + remainingFraction: 0.6, + remainingAmount: 60, + resetTime: '2026-06-22T02:00:00Z', + }, + { + modelId: 'gemini-2.5-flash', + tokenType: 'input', + remainingFraction: 0.4, + remainingAmount: 40, + resetTime: '2026-06-22T03:00:00Z', + }, + { + modelId: 'gemini-4-pro-preview', + tokenType: 'output', + remainingFraction: 0.2, + remainingAmount: 20, + resetTime: '2026-06-22T04:00:00Z', + }, + { + modelId: 'gemini-experimental', + tokenType: null, + remainingFraction: 0.9, + remainingAmount: null, + resetTime: undefined, + }, + ]); + + expect(buckets.map((bucket) => bucket.id)).toEqual([ + 'gemini-flash-series-input', + 'gemini-pro-series-output', + 'gemini-experimental', + ]); + expect(buckets[0]).toMatchObject({ + remainingFraction: 0.4, + remainingAmount: 40, + modelIds: ['gemini-4-flash-preview', 'gemini-2.5-flash'], + }); + }); +}); diff --git a/compatibility/upstream.env b/compatibility/upstream.env index d404109..5602264 100644 --- a/compatibility/upstream.env +++ b/compatibility/upstream.env @@ -2,4 +2,3 @@ CORE_UPSTREAM_REPO=router-for-me/CLIProxyAPI CORE_UPSTREAM_TAG=v7.2.94 MANAGEMENT_UPSTREAM_REPO=router-for-me/Cli-Proxy-API-Management-Center MANAGEMENT_UPSTREAM_TAG=v1.18.5 -BUN_VERSION=1.3.14