Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release-management.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
89 changes: 31 additions & 58 deletions cliproxyapi-pro-core/patches/account_inspection_scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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),
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
]
}`
Expand All @@ -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 {
Expand Down
51 changes: 22 additions & 29 deletions cliproxyapi-pro-core/patches/plugin_gemini_cli_quota_legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions cliproxyapi-pro-management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 文件前被拒绝。

## 本地应用

Expand Down
4 changes: 2 additions & 2 deletions cliproxyapi-pro-management/README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 0 additions & 4 deletions cliproxyapi-pro-management/overlay-replacements.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading
Loading