@coseung2/opencodex 2.8.0-cs.12 → 2.8.0-cs.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-MUpaVatk.js +67 -0
- package/gui/dist/index.html +1 -1
- package/package.json +3 -3
- package/packages/ocx-notch/README.md +3 -2
- package/src/adapters/cursor/discovery.ts +6 -2
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/google-antigravity-replay.ts +24 -0
- package/src/adapters/google.ts +16 -11
- package/src/chat/inbound.ts +5 -11
- package/src/cli/account-api.ts +9 -1
- package/src/cli/account-extended.ts +4 -1
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +12 -1
- package/src/codex/account-namespaces.ts +21 -0
- package/src/codex/account-priority.ts +49 -0
- package/src/codex/account-store.ts +73 -10
- package/src/codex/auth-api.ts +312 -45
- package/src/codex/auth-context.ts +61 -16
- package/src/codex/catalog/metadata.ts +34 -12
- package/src/codex/catalog/parsing.ts +8 -1
- package/src/codex/catalog/provider-fetch.ts +24 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/pool-rotation.ts +51 -4
- package/src/codex/quota.ts +154 -35
- package/src/codex/routing.ts +133 -33
- package/src/codex/warmup.ts +204 -82
- package/src/config.ts +84 -1
- package/src/lib/bounded-body.ts +13 -6
- package/src/lib/bun-stream-caps.ts +5 -6
- package/src/lib/redact.ts +13 -0
- package/src/oauth/index.ts +79 -12
- package/src/oauth/log.ts +3 -1
- package/src/oauth/store.ts +31 -8
- package/src/oauth/token-guardian.ts +130 -9
- package/src/providers/antigravity-models.ts +53 -24
- package/src/providers/codex-capacity.ts +303 -0
- package/src/providers/model-rename-migration.ts +147 -0
- package/src/providers/model-rename-startup.ts +29 -0
- package/src/providers/quota.ts +126 -16
- package/src/providers/registry.ts +262 -38
- package/src/responses/parser.ts +19 -12
- package/src/responses/spill-store.ts +14 -1
- package/src/responses/state.ts +108 -14
- package/src/router.ts +6 -0
- package/src/server/index.ts +9 -1
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +8 -1
- package/src/server/relay.ts +10 -42
- package/src/server/request-log.ts +42 -1
- package/src/server/responses/compact.ts +16 -4
- package/src/server/responses/core.ts +217 -59
- package/src/server/responses/empty-completion-guard.ts +275 -0
- package/src/server/responses/encrypted-payload.ts +54 -39
- package/src/server/responses/fetch-helpers.ts +24 -3
- package/src/server/responses/ws-upstream.ts +318 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/ws-bridge.ts +17 -11
- package/src/types.ts +13 -1
- package/src/usage/log.ts +24 -0
- package/src/usage/summary.ts +152 -2
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
- package/gui/dist/assets/index-GgF1IpQa.js +0 -67
|
@@ -6,11 +6,22 @@
|
|
|
6
6
|
// receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
|
|
7
7
|
// picker exposes collapsed base models with reasoning-effort routing.
|
|
8
8
|
|
|
9
|
+
const GEMINI_FLASH_CURRENT = "gemini-3.7-flash";
|
|
10
|
+
const RETIRED_FLASH_TIERS: Record<string, string> = {
|
|
11
|
+
"gemini-3.6-flash": "medium",
|
|
12
|
+
"gemini-3.6-flash-low": "low",
|
|
13
|
+
"gemini-3.6-flash-medium": "medium",
|
|
14
|
+
"gemini-3.6-flash-high": "high",
|
|
15
|
+
"gemini-3.5-flash-extra-low": "low",
|
|
16
|
+
"gemini-3.5-flash-low": "medium",
|
|
17
|
+
"gemini-3.5-flash-mid": "medium",
|
|
18
|
+
"gemini-3.5-flash-high": "high",
|
|
19
|
+
"gemini-3-flash-agent": "high",
|
|
20
|
+
};
|
|
21
|
+
|
|
9
22
|
// ── Wire IDs (what CCA :fetchAvailableModels returns) ──
|
|
10
23
|
const ANTIGRAVITY_WIRE_MODELS = [
|
|
11
|
-
|
|
12
|
-
"gemini-3.6-flash-medium",
|
|
13
|
-
"gemini-3.6-flash-high",
|
|
24
|
+
GEMINI_FLASH_CURRENT,
|
|
14
25
|
"gemini-3.1-pro-low",
|
|
15
26
|
"gemini-pro-agent",
|
|
16
27
|
"gemini-3.1-flash-image",
|
|
@@ -23,7 +34,7 @@ const ANTIGRAVITY_WIRE_MODELS = [
|
|
|
23
34
|
// Gemini models: effort → wire model suffix (official agy UI pattern).
|
|
24
35
|
// Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
|
|
25
36
|
export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
|
|
26
|
-
|
|
37
|
+
[GEMINI_FLASH_CURRENT]: ["low", "medium", "high"],
|
|
27
38
|
"gemini-3.1-pro": ["low", "high"],
|
|
28
39
|
"claude-sonnet-4-6": ["low", "medium", "high", "max"],
|
|
29
40
|
"claude-opus-4-6-thinking": ["low", "medium", "high", "max"],
|
|
@@ -31,11 +42,6 @@ export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
|
|
|
31
42
|
|
|
32
43
|
// ── Effort → wire model map for Gemini base models ──
|
|
33
44
|
const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
|
|
34
|
-
"gemini-3.6-flash": {
|
|
35
|
-
low: "gemini-3.6-flash-low",
|
|
36
|
-
medium: "gemini-3.6-flash-medium",
|
|
37
|
-
high: "gemini-3.6-flash-high",
|
|
38
|
-
},
|
|
39
45
|
"gemini-3.1-pro": {
|
|
40
46
|
low: "gemini-3.1-pro-low",
|
|
41
47
|
high: "gemini-pro-agent",
|
|
@@ -44,11 +50,13 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
|
|
|
44
50
|
|
|
45
51
|
// ── Default effort per Gemini base model ──
|
|
46
52
|
const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
|
|
47
|
-
"gemini-3.6-flash": "medium",
|
|
48
53
|
"gemini-3.1-pro": "high",
|
|
49
54
|
};
|
|
50
55
|
|
|
51
|
-
const
|
|
56
|
+
const ANTIGRAVITY_THINKING_LEVEL_MODELS: Record<string, string> = {
|
|
57
|
+
[GEMINI_FLASH_CURRENT]: "medium",
|
|
58
|
+
};
|
|
59
|
+
const ANTIGRAVITY_THINKING_LEVELS = new Set(["low", "medium", "high"]);
|
|
52
60
|
|
|
53
61
|
function resolveAntigravityThinkingLevel(effort: string): string | undefined {
|
|
54
62
|
if (effort === "xhigh" || effort === "max" || effort === "ultra") return "high";
|
|
@@ -65,16 +73,9 @@ const ANTIGRAVITY_VISIBLE_MODEL_ALIASES: Record<string, string> = {
|
|
|
65
73
|
// Wire suffix IDs are identity aliases — they resolve to themselves so saved configs
|
|
66
74
|
// with explicit suffixes (e.g. gemini-3.6-flash-low) continue to work.
|
|
67
75
|
const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record<string, string> = {
|
|
68
|
-
"gemini-3.6-flash-low": "gemini-3.6-flash-low",
|
|
69
|
-
"gemini-3.6-flash-medium": "gemini-3.6-flash-medium",
|
|
70
|
-
"gemini-3.6-flash-high": "gemini-3.6-flash-high",
|
|
71
76
|
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
|
|
72
77
|
"gemini-pro-agent": "gemini-pro-agent",
|
|
73
|
-
|
|
74
|
-
"gemini-3.5-flash-low": "gemini-3.6-flash-medium",
|
|
75
|
-
"gemini-3.5-flash-mid": "gemini-3.6-flash-medium",
|
|
76
|
-
"gemini-3.5-flash-high": "gemini-3.6-flash-high",
|
|
77
|
-
"gemini-3-flash-agent": "gemini-3.6-flash-high",
|
|
78
|
+
...Object.fromEntries(Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired, GEMINI_FLASH_CURRENT])),
|
|
78
79
|
};
|
|
79
80
|
|
|
80
81
|
export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
|
|
@@ -84,7 +85,7 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
|
|
|
84
85
|
|
|
85
86
|
// Picker-visible: collapsed base models only.
|
|
86
87
|
export const ANTIGRAVITY_MODELS = [
|
|
87
|
-
|
|
88
|
+
GEMINI_FLASH_CURRENT,
|
|
88
89
|
"gemini-3.1-pro",
|
|
89
90
|
"gemini-3.1-flash-image",
|
|
90
91
|
"claude-sonnet-4-6",
|
|
@@ -94,9 +95,7 @@ export const ANTIGRAVITY_MODELS = [
|
|
|
94
95
|
|
|
95
96
|
// Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
|
|
96
97
|
const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
97
|
-
|
|
98
|
-
"gemini-3.6-flash-medium": 1_048_576,
|
|
99
|
-
"gemini-3.6-flash-high": 1_048_576,
|
|
98
|
+
[GEMINI_FLASH_CURRENT]: 1_048_576,
|
|
100
99
|
"gemini-3.1-pro-low": 1_048_576,
|
|
101
100
|
"gemini-pro-agent": 1_048_576,
|
|
102
101
|
"gemini-3.1-flash-image": 1_048_576,
|
|
@@ -107,7 +106,7 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
107
106
|
|
|
108
107
|
export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
109
108
|
// Collapsed base IDs — explicit entries for the picker.
|
|
110
|
-
|
|
109
|
+
[GEMINI_FLASH_CURRENT]: 1_048_576,
|
|
111
110
|
"gemini-3.1-pro": 1_048_576,
|
|
112
111
|
// Wire IDs and aliases via derivation.
|
|
113
112
|
...ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS,
|
|
@@ -119,6 +118,15 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
119
118
|
),
|
|
120
119
|
};
|
|
121
120
|
|
|
121
|
+
export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
|
|
122
|
+
[GEMINI_FLASH_CURRENT]: ["text", "image"],
|
|
123
|
+
"gemini-3.1-pro": ["text", "image"],
|
|
124
|
+
"gemini-3.1-flash-image": ["text", "image"],
|
|
125
|
+
"claude-sonnet-4-6": ["text", "image"],
|
|
126
|
+
"claude-opus-4-6-thinking": ["text", "image"],
|
|
127
|
+
"gpt-oss-120b-medium": ["text"],
|
|
128
|
+
};
|
|
129
|
+
|
|
122
130
|
export function resolveAntigravityWireModelId(modelId: string): string {
|
|
123
131
|
return ANTIGRAVITY_MODEL_ALIASES[modelId] ?? modelId;
|
|
124
132
|
}
|
|
@@ -132,6 +140,10 @@ export function isAntigravitySuffixModelId(modelId: string): boolean {
|
|
|
132
140
|
return !(ANTIGRAVITY_MODELS as string[]).includes(modelId);
|
|
133
141
|
}
|
|
134
142
|
|
|
143
|
+
export function retiredAntigravityFlashTier(modelId: string): string | undefined {
|
|
144
|
+
return RETIRED_FLASH_TIERS[modelId];
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
/**
|
|
136
148
|
* Resolve a picker-visible base model + optional reasoning effort to the CCA wire model ID.
|
|
137
149
|
*
|
|
@@ -146,11 +158,27 @@ export function resolveAntigravityEffortWireModel(
|
|
|
146
158
|
modelId: string,
|
|
147
159
|
effort?: string,
|
|
148
160
|
): { wireModelId: string; thinkingLevel?: string } {
|
|
161
|
+
const retiredTier = RETIRED_FLASH_TIERS[modelId];
|
|
162
|
+
if (retiredTier) {
|
|
163
|
+
return {
|
|
164
|
+
wireModelId: GEMINI_FLASH_CURRENT,
|
|
165
|
+
thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? retiredTier : retiredTier,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
149
169
|
// Rule 1: suffix/compat alias — suffix IS the effort.
|
|
150
170
|
if (isAntigravitySuffixModelId(modelId)) {
|
|
151
171
|
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
152
172
|
}
|
|
153
173
|
|
|
174
|
+
const defaultLevel = ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
|
|
175
|
+
if (defaultLevel) {
|
|
176
|
+
return {
|
|
177
|
+
wireModelId: modelId,
|
|
178
|
+
thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? defaultLevel : defaultLevel,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
154
182
|
// Rule 2/3: mapped Gemini base model.
|
|
155
183
|
const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId];
|
|
156
184
|
if (effortMap) {
|
|
@@ -189,6 +217,7 @@ const ANTIGRAVITY_USAGE_BASE_BY_ID: Record<string, string> = (() => {
|
|
|
189
217
|
// If alias is itself a base/wire already mapped, keep that mapping.
|
|
190
218
|
else if (rev[wire]) rev[alias] = rev[wire]!;
|
|
191
219
|
}
|
|
220
|
+
for (const retired of Object.keys(RETIRED_FLASH_TIERS)) rev[retired] = retired;
|
|
192
221
|
// Visible aliases that only appear in ANTIGRAVITY_VISIBLE_MODEL_ALIASES are already
|
|
193
222
|
// included via ANTIGRAVITY_MODEL_ALIASES. Identity bases without effort maps remain.
|
|
194
223
|
return rev;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
export const CODEX_CONFIGURED_CAPACITY_WEIGHTS = {
|
|
2
|
+
plus: 1,
|
|
3
|
+
team: 1,
|
|
4
|
+
business: 1,
|
|
5
|
+
prolite: 5,
|
|
6
|
+
pro: 20,
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export const CODEX_CAPACITY_MAX_QUOTA_AGE_MS = 30 * 60_000;
|
|
10
|
+
|
|
11
|
+
export type CodexCapacityQuota = {
|
|
12
|
+
fiveHourPercent?: number;
|
|
13
|
+
fiveHourResetAt?: number;
|
|
14
|
+
weeklyPercent?: number;
|
|
15
|
+
weeklyResetAt?: number;
|
|
16
|
+
monthlyPercent?: number;
|
|
17
|
+
monthlyResetAt?: number;
|
|
18
|
+
customWindows?: Array<{ label: string; percent: number; resetAt?: number }>;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface CodexCapacityAccount {
|
|
23
|
+
isMain: boolean;
|
|
24
|
+
active?: boolean;
|
|
25
|
+
plan?: unknown;
|
|
26
|
+
paused: boolean;
|
|
27
|
+
needsReauth?: boolean;
|
|
28
|
+
quota: CodexCapacityQuota | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CodexCapacityWindowAggregation {
|
|
32
|
+
usedPercent: number;
|
|
33
|
+
includedAccounts: number;
|
|
34
|
+
excludedAccounts: number;
|
|
35
|
+
incomplete: boolean;
|
|
36
|
+
/** Internal calculation evidence; management projections should omit it. */
|
|
37
|
+
totalWeight?: number;
|
|
38
|
+
consumedWeight?: number;
|
|
39
|
+
remainingWeight?: number;
|
|
40
|
+
updatedAt: number;
|
|
41
|
+
nextRecoveryAt?: number;
|
|
42
|
+
nextRecoveryPercent?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CodexCapacityAggregation {
|
|
46
|
+
kind: "capacity-weighted-v1";
|
|
47
|
+
scope: "routable-known";
|
|
48
|
+
includedAccounts: number;
|
|
49
|
+
excludedAccounts: number;
|
|
50
|
+
unknownPlanAccounts: number;
|
|
51
|
+
missingQuotaAccounts: number;
|
|
52
|
+
pausedAccounts: number;
|
|
53
|
+
reauthAccounts: number;
|
|
54
|
+
staleQuotaAccounts: number;
|
|
55
|
+
partialWindowAccounts: number;
|
|
56
|
+
incomplete: boolean;
|
|
57
|
+
presentation?: "aggregate" | "effective-account-fallback" | "coverage-only";
|
|
58
|
+
fiveHour?: CodexCapacityWindowAggregation;
|
|
59
|
+
weekly?: CodexCapacityWindowAggregation;
|
|
60
|
+
monthly?: CodexCapacityWindowAggregation;
|
|
61
|
+
customWindows?: Array<CodexCapacityWindowAggregation & { label: string }>;
|
|
62
|
+
currentAccount?: {
|
|
63
|
+
isMain: boolean;
|
|
64
|
+
plan?: string | null;
|
|
65
|
+
quota: CodexCapacityQuota | null;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CodexCapacityResult {
|
|
70
|
+
quota: CodexCapacityQuota | null;
|
|
71
|
+
aggregation: CodexCapacityAggregation | null;
|
|
72
|
+
currentAccount?: CodexCapacityAggregation["currentAccount"];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type MutableWindow = {
|
|
76
|
+
totalWeight: number;
|
|
77
|
+
consumedWeight: number;
|
|
78
|
+
includedAccounts: number;
|
|
79
|
+
recoveries: Map<number, number>;
|
|
80
|
+
oldestUpdatedAt: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function planValue(plan: unknown): string | undefined {
|
|
84
|
+
return typeof plan === "string" && plan.trim() ? plan.trim() : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizedPlan(plan: unknown): string | undefined {
|
|
88
|
+
return planValue(plan)?.toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function configuredWeight(plan: unknown): number | undefined {
|
|
92
|
+
const normalized = normalizedPlan(plan);
|
|
93
|
+
return normalized && Object.hasOwn(CODEX_CONFIGURED_CAPACITY_WEIGHTS, normalized)
|
|
94
|
+
? CODEX_CONFIGURED_CAPACITY_WEIGHTS[normalized as keyof typeof CODEX_CONFIGURED_CAPACITY_WEIGHTS]
|
|
95
|
+
: undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizedPercent(value: unknown): number | undefined {
|
|
99
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
100
|
+
? Math.max(0, Math.min(100, value))
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function futureResetMs(value: unknown, now: number): number | undefined {
|
|
105
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
106
|
+
const milliseconds = value > 10_000_000_000 ? value : value * 1000;
|
|
107
|
+
return milliseconds > now ? milliseconds : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasKnownQuotaWindow(quota: CodexCapacityQuota | null): quota is CodexCapacityQuota {
|
|
111
|
+
if (!quota) return false;
|
|
112
|
+
return normalizedPercent(quota.fiveHourPercent) !== undefined
|
|
113
|
+
|| normalizedPercent(quota.weeklyPercent) !== undefined
|
|
114
|
+
|| normalizedPercent(quota.monthlyPercent) !== undefined
|
|
115
|
+
|| !!quota.customWindows?.some(window => normalizedPercent(window.percent) !== undefined);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function currentQuotaForDisplay(account: CodexCapacityAccount, now: number): CodexCapacityQuota | null {
|
|
119
|
+
const quota = account.quota;
|
|
120
|
+
const fresh = !!quota
|
|
121
|
+
&& Number.isFinite(quota.updatedAt)
|
|
122
|
+
&& now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
123
|
+
return !account.paused && !account.needsReauth && fresh && hasKnownQuotaWindow(quota) ? quota : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function addWindow(
|
|
127
|
+
windows: Map<string, MutableWindow>,
|
|
128
|
+
key: string,
|
|
129
|
+
weight: number,
|
|
130
|
+
percent: number,
|
|
131
|
+
resetAt: number | undefined,
|
|
132
|
+
updatedAt: number,
|
|
133
|
+
): void {
|
|
134
|
+
const window = windows.get(key) ?? {
|
|
135
|
+
totalWeight: 0,
|
|
136
|
+
consumedWeight: 0,
|
|
137
|
+
includedAccounts: 0,
|
|
138
|
+
recoveries: new Map<number, number>(),
|
|
139
|
+
oldestUpdatedAt: updatedAt,
|
|
140
|
+
};
|
|
141
|
+
const consumed = weight * percent / 100;
|
|
142
|
+
window.totalWeight += weight;
|
|
143
|
+
window.consumedWeight += consumed;
|
|
144
|
+
window.includedAccounts += 1;
|
|
145
|
+
window.oldestUpdatedAt = Math.min(window.oldestUpdatedAt, updatedAt);
|
|
146
|
+
if (resetAt !== undefined && consumed > 0) {
|
|
147
|
+
window.recoveries.set(resetAt, (window.recoveries.get(resetAt) ?? 0) + consumed);
|
|
148
|
+
}
|
|
149
|
+
windows.set(key, window);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function finalizeWindow(window: MutableWindow, totalAccounts: number): CodexCapacityWindowAggregation {
|
|
153
|
+
const nextRecoveryAt = [...window.recoveries.keys()].sort((a, b) => a - b)[0];
|
|
154
|
+
const recovered = nextRecoveryAt === undefined ? undefined : window.recoveries.get(nextRecoveryAt);
|
|
155
|
+
return {
|
|
156
|
+
usedPercent: window.consumedWeight / window.totalWeight * 100,
|
|
157
|
+
includedAccounts: window.includedAccounts,
|
|
158
|
+
excludedAccounts: totalAccounts - window.includedAccounts,
|
|
159
|
+
incomplete: window.includedAccounts < totalAccounts,
|
|
160
|
+
totalWeight: window.totalWeight,
|
|
161
|
+
consumedWeight: window.consumedWeight,
|
|
162
|
+
remainingWeight: window.totalWeight - window.consumedWeight,
|
|
163
|
+
updatedAt: window.oldestUpdatedAt,
|
|
164
|
+
...(nextRecoveryAt !== undefined ? { nextRecoveryAt } : {}),
|
|
165
|
+
...(recovered !== undefined ? { nextRecoveryPercent: recovered / window.totalWeight * 100 } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Display-only configured-weight estimate; never used by routing or quota admission. */
|
|
170
|
+
export function aggregateCodexPoolCapacity(
|
|
171
|
+
accounts: readonly CodexCapacityAccount[],
|
|
172
|
+
now = Date.now(),
|
|
173
|
+
): CodexCapacityResult {
|
|
174
|
+
const current = accounts.find(account => account.active)
|
|
175
|
+
?? accounts.find(account => account.isMain)
|
|
176
|
+
?? accounts[0];
|
|
177
|
+
const currentPlan = planValue(current?.plan);
|
|
178
|
+
const currentAccount = current ? {
|
|
179
|
+
isMain: current.isMain,
|
|
180
|
+
...(currentPlan !== undefined ? { plan: currentPlan } : {}),
|
|
181
|
+
quota: currentQuotaForDisplay(current, now),
|
|
182
|
+
} : undefined;
|
|
183
|
+
const windows = new Map<string, MutableWindow>();
|
|
184
|
+
const included = new Set<CodexCapacityAccount>();
|
|
185
|
+
const contributions = new Map<CodexCapacityAccount, Set<string>>();
|
|
186
|
+
let unknownPlanAccounts = 0;
|
|
187
|
+
let missingQuotaAccounts = 0;
|
|
188
|
+
let pausedAccounts = 0;
|
|
189
|
+
let reauthAccounts = 0;
|
|
190
|
+
let staleQuotaAccounts = 0;
|
|
191
|
+
|
|
192
|
+
for (const account of accounts) {
|
|
193
|
+
const weight = configuredWeight(account.plan);
|
|
194
|
+
if (weight === undefined) unknownPlanAccounts += 1;
|
|
195
|
+
if (account.paused) pausedAccounts += 1;
|
|
196
|
+
if (account.needsReauth) reauthAccounts += 1;
|
|
197
|
+
const quota = account.quota;
|
|
198
|
+
const quotaFresh = !!quota
|
|
199
|
+
&& Number.isFinite(quota.updatedAt)
|
|
200
|
+
&& now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
201
|
+
if (quota && !quotaFresh) staleQuotaAccounts += 1;
|
|
202
|
+
const standard = quota ? [
|
|
203
|
+
["fiveHour", quota.fiveHourPercent, quota.fiveHourResetAt],
|
|
204
|
+
["weekly", quota.weeklyPercent, quota.weeklyResetAt],
|
|
205
|
+
["monthly", quota.monthlyPercent, quota.monthlyResetAt],
|
|
206
|
+
] as const : [];
|
|
207
|
+
const custom = quota?.customWindows ?? [];
|
|
208
|
+
const hasQuota = hasKnownQuotaWindow(quota);
|
|
209
|
+
if (!hasQuota) missingQuotaAccounts += 1;
|
|
210
|
+
if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota || !quotaFresh) continue;
|
|
211
|
+
|
|
212
|
+
const contributionKeys = new Set<string>();
|
|
213
|
+
for (const [key, rawPercent, rawReset] of standard) {
|
|
214
|
+
const percent = normalizedPercent(rawPercent);
|
|
215
|
+
if (percent === undefined) continue;
|
|
216
|
+
addWindow(windows, key, weight, percent, futureResetMs(rawReset, now), quota.updatedAt);
|
|
217
|
+
contributionKeys.add(key);
|
|
218
|
+
}
|
|
219
|
+
for (const customWindow of custom) {
|
|
220
|
+
const percent = normalizedPercent(customWindow.percent);
|
|
221
|
+
if (percent === undefined) continue;
|
|
222
|
+
const key = `custom:${customWindow.label}`;
|
|
223
|
+
addWindow(windows, key, weight, percent, futureResetMs(customWindow.resetAt, now), quota.updatedAt);
|
|
224
|
+
contributionKeys.add(key);
|
|
225
|
+
}
|
|
226
|
+
if (contributionKeys.size > 0) {
|
|
227
|
+
included.add(account);
|
|
228
|
+
contributions.set(account, contributionKeys);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (windows.size === 0) {
|
|
233
|
+
if (accounts.length === 0) return { quota: null, aggregation: null };
|
|
234
|
+
const aggregation: CodexCapacityAggregation = {
|
|
235
|
+
kind: "capacity-weighted-v1",
|
|
236
|
+
scope: "routable-known",
|
|
237
|
+
includedAccounts: 0,
|
|
238
|
+
excludedAccounts: accounts.length,
|
|
239
|
+
unknownPlanAccounts,
|
|
240
|
+
missingQuotaAccounts,
|
|
241
|
+
pausedAccounts,
|
|
242
|
+
reauthAccounts,
|
|
243
|
+
staleQuotaAccounts,
|
|
244
|
+
partialWindowAccounts: 0,
|
|
245
|
+
incomplete: true,
|
|
246
|
+
...(currentAccount ? { currentAccount } : {}),
|
|
247
|
+
};
|
|
248
|
+
return { quota: null, aggregation, ...(currentAccount ? { currentAccount } : {}) };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const fiveHour = windows.get("fiveHour") ? finalizeWindow(windows.get("fiveHour")!, accounts.length) : undefined;
|
|
252
|
+
const weekly = windows.get("weekly") ? finalizeWindow(windows.get("weekly")!, accounts.length) : undefined;
|
|
253
|
+
const monthly = windows.get("monthly") ? finalizeWindow(windows.get("monthly")!, accounts.length) : undefined;
|
|
254
|
+
const customWindows = [...windows.entries()].flatMap(([key, window]) => key.startsWith("custom:")
|
|
255
|
+
? [{ label: key.slice("custom:".length), ...finalizeWindow(window, accounts.length) }]
|
|
256
|
+
: []);
|
|
257
|
+
const quota: CodexCapacityQuota = {
|
|
258
|
+
...(fiveHour ? { fiveHourPercent: fiveHour.usedPercent } : {}),
|
|
259
|
+
...(fiveHour && currentAccount?.quota?.fiveHourResetAt !== undefined
|
|
260
|
+
? { fiveHourResetAt: currentAccount.quota.fiveHourResetAt }
|
|
261
|
+
: {}),
|
|
262
|
+
...(weekly ? { weeklyPercent: weekly.usedPercent } : {}),
|
|
263
|
+
...(weekly && currentAccount?.quota?.weeklyResetAt !== undefined
|
|
264
|
+
? { weeklyResetAt: currentAccount.quota.weeklyResetAt }
|
|
265
|
+
: {}),
|
|
266
|
+
...(monthly ? { monthlyPercent: monthly.usedPercent } : {}),
|
|
267
|
+
...(monthly && currentAccount?.quota?.monthlyResetAt !== undefined
|
|
268
|
+
? { monthlyResetAt: currentAccount.quota.monthlyResetAt }
|
|
269
|
+
: {}),
|
|
270
|
+
...(customWindows.length > 0 ? {
|
|
271
|
+
customWindows: customWindows.map(window => ({ label: window.label, percent: window.usedPercent })),
|
|
272
|
+
} : {}),
|
|
273
|
+
updatedAt: Math.min(
|
|
274
|
+
...[fiveHour, weekly, monthly, ...customWindows]
|
|
275
|
+
.flatMap(window => window ? [window.updatedAt] : []),
|
|
276
|
+
),
|
|
277
|
+
};
|
|
278
|
+
const visibleWindowKeys = [...windows.keys()];
|
|
279
|
+
const partialWindowAccounts = accounts.filter(account => {
|
|
280
|
+
const keys = contributions.get(account);
|
|
281
|
+
return !!keys && visibleWindowKeys.some(key => !keys.has(key));
|
|
282
|
+
}).length;
|
|
283
|
+
const excludedAccounts = accounts.length - included.size;
|
|
284
|
+
const aggregation: CodexCapacityAggregation = {
|
|
285
|
+
kind: "capacity-weighted-v1",
|
|
286
|
+
scope: "routable-known",
|
|
287
|
+
includedAccounts: included.size,
|
|
288
|
+
excludedAccounts,
|
|
289
|
+
unknownPlanAccounts,
|
|
290
|
+
missingQuotaAccounts,
|
|
291
|
+
pausedAccounts,
|
|
292
|
+
reauthAccounts,
|
|
293
|
+
staleQuotaAccounts,
|
|
294
|
+
partialWindowAccounts,
|
|
295
|
+
incomplete: excludedAccounts > 0 || partialWindowAccounts > 0,
|
|
296
|
+
...(fiveHour ? { fiveHour } : {}),
|
|
297
|
+
...(weekly ? { weekly } : {}),
|
|
298
|
+
...(monthly ? { monthly } : {}),
|
|
299
|
+
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
300
|
+
...(currentAccount ? { currentAccount } : {}),
|
|
301
|
+
};
|
|
302
|
+
return { quota, aggregation, ...(currentAccount ? { currentAccount } : {}) };
|
|
303
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
2
|
+
import { PROVIDER_REGISTRY } from "./registry";
|
|
3
|
+
|
|
4
|
+
export interface ModelRename {
|
|
5
|
+
provider: string;
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
reason: string;
|
|
9
|
+
dropReasoningEffortMap?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const RETIRED_ANTIGRAVITY_FLASH_MODELS = [
|
|
13
|
+
"gemini-3.6-flash",
|
|
14
|
+
"gemini-3.6-flash-low",
|
|
15
|
+
"gemini-3.6-flash-medium",
|
|
16
|
+
"gemini-3.6-flash-high",
|
|
17
|
+
"gemini-3.5-flash-extra-low",
|
|
18
|
+
"gemini-3.5-flash-low",
|
|
19
|
+
"gemini-3.5-flash-mid",
|
|
20
|
+
"gemini-3.5-flash-high",
|
|
21
|
+
"gemini-3-flash-agent",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export const MODEL_RENAMES: readonly ModelRename[] = RETIRED_ANTIGRAVITY_FLASH_MODELS.map(from => ({
|
|
25
|
+
provider: "google-antigravity",
|
|
26
|
+
from,
|
|
27
|
+
to: "gemini-3.7-flash",
|
|
28
|
+
reason: "Google retires the previous Antigravity Flash generation from Cloud Code Assist when its successor ships",
|
|
29
|
+
dropReasoningEffortMap: true,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const MODEL_KEYED_RECORDS = [
|
|
33
|
+
"modelContextWindows",
|
|
34
|
+
"modelMaxOutputTokens",
|
|
35
|
+
"modelInputModalities",
|
|
36
|
+
"modelReasoningEfforts",
|
|
37
|
+
"modelDefaultReasoningEfforts",
|
|
38
|
+
"modelReasoningEffortMap",
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
const MODEL_ID_LISTS = [
|
|
42
|
+
"models",
|
|
43
|
+
"selectedModels",
|
|
44
|
+
"noVisionModels",
|
|
45
|
+
"noReasoningModels",
|
|
46
|
+
"noTemperatureModels",
|
|
47
|
+
"noTopPModels",
|
|
48
|
+
"noPenaltyModels",
|
|
49
|
+
"autoToolChoiceOnlyModels",
|
|
50
|
+
"preserveReasoningContentModels",
|
|
51
|
+
"thinkingBudgetModels",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
function renameInList(value: unknown, from: string, to: string): string[] | null {
|
|
55
|
+
if (!Array.isArray(value) || !value.includes(from)) return null;
|
|
56
|
+
const next: string[] = [];
|
|
57
|
+
const seen = new Set<string>();
|
|
58
|
+
for (const entry of value) {
|
|
59
|
+
if (typeof entry !== "string") continue;
|
|
60
|
+
const mapped = entry === from ? to : entry;
|
|
61
|
+
if (seen.has(mapped)) continue;
|
|
62
|
+
seen.add(mapped);
|
|
63
|
+
next.push(mapped);
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renameInRecord(value: unknown, from: string, to: string): Record<string, unknown> | null {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
70
|
+
const record = value as Record<string, unknown>;
|
|
71
|
+
if (!(from in record)) return null;
|
|
72
|
+
const next: Record<string, unknown> = {};
|
|
73
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
74
|
+
const mapped = key === from ? to : key;
|
|
75
|
+
if (mapped in next) continue;
|
|
76
|
+
next[mapped] = key === from && to in record ? record[to] : entry;
|
|
77
|
+
}
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function dropFromRecord(value: unknown, from: string): Record<string, unknown> | null {
|
|
82
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
83
|
+
const record = value as Record<string, unknown>;
|
|
84
|
+
if (!(from in record)) return null;
|
|
85
|
+
return Object.fromEntries(Object.entries(record).filter(([key]) => key !== from));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function providerStillMatchesRegistry(name: string, provider: OcxProviderConfig): boolean {
|
|
89
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
|
|
90
|
+
if (!entry) return false;
|
|
91
|
+
if (!provider.baseUrl || !entry.baseUrl) return true;
|
|
92
|
+
const known = [entry.baseUrl, ...(entry.baseUrlChoices?.map(choice => choice.baseUrl).filter(Boolean) ?? [])]
|
|
93
|
+
.map(url => url!.replace(/\/+$/, ""));
|
|
94
|
+
return known.includes(provider.baseUrl.replace(/\/+$/, ""));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ModelRenameProjection {
|
|
98
|
+
config: OcxConfig;
|
|
99
|
+
changed: boolean;
|
|
100
|
+
warnings: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function projectModelRenames(
|
|
104
|
+
config: OcxConfig,
|
|
105
|
+
renames: readonly ModelRename[] = MODEL_RENAMES,
|
|
106
|
+
): ModelRenameProjection {
|
|
107
|
+
let changed = false;
|
|
108
|
+
const warnings: string[] = [];
|
|
109
|
+
|
|
110
|
+
for (const rename of renames) {
|
|
111
|
+
const provider = config.providers?.[rename.provider];
|
|
112
|
+
const registry = PROVIDER_REGISTRY.find(row => row.id === rename.provider);
|
|
113
|
+
if (!provider || !registry?.models?.includes(rename.to) || !providerStillMatchesRegistry(rename.provider, provider)) continue;
|
|
114
|
+
|
|
115
|
+
const row = provider as unknown as Record<string, unknown>;
|
|
116
|
+
let touched = false;
|
|
117
|
+
for (const field of MODEL_ID_LISTS) {
|
|
118
|
+
const next = renameInList(row[field], rename.from, rename.to);
|
|
119
|
+
if (!next) continue;
|
|
120
|
+
row[field] = next;
|
|
121
|
+
touched = true;
|
|
122
|
+
}
|
|
123
|
+
for (const field of MODEL_KEYED_RECORDS) {
|
|
124
|
+
const next = rename.dropReasoningEffortMap && field === "modelReasoningEffortMap"
|
|
125
|
+
? dropFromRecord(row[field], rename.from)
|
|
126
|
+
: renameInRecord(row[field], rename.from, rename.to);
|
|
127
|
+
if (!next) continue;
|
|
128
|
+
row[field] = next;
|
|
129
|
+
touched = true;
|
|
130
|
+
}
|
|
131
|
+
if (provider.defaultModel === rename.from) {
|
|
132
|
+
provider.defaultModel = rename.to;
|
|
133
|
+
touched = true;
|
|
134
|
+
}
|
|
135
|
+
const disabled = `${rename.provider}/${rename.from}`;
|
|
136
|
+
if (config.disabledModels?.includes(disabled)) {
|
|
137
|
+
config.disabledModels = config.disabledModels.filter(model => model !== disabled);
|
|
138
|
+
touched = true;
|
|
139
|
+
}
|
|
140
|
+
if (touched) {
|
|
141
|
+
changed = true;
|
|
142
|
+
warnings.push(`renamed "${rename.provider}/${rename.from}" to "${rename.to}": ${rename.reason}.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { config, changed, warnings };
|
|
147
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import { saveConfig } from "../config";
|
|
3
|
+
import { projectModelRenames } from "./model-rename-migration";
|
|
4
|
+
import type { OcxConfig } from "../types";
|
|
5
|
+
|
|
6
|
+
export interface ModelRenameStartupDeps {
|
|
7
|
+
project: typeof projectModelRenames;
|
|
8
|
+
save: (config: OcxConfig) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Apply registry model renames to the saved config at startup (issue #1610).
|
|
13
|
+
*
|
|
14
|
+
* No backup is taken, unlike the OpenAI tier and Alibaba region migrations: those
|
|
15
|
+
* rewrite credentials and provider identity, where a bad projection is not
|
|
16
|
+
* recoverable from the config alone. This one only rewrites model ids that the
|
|
17
|
+
* registry itself no longer seeds, and the pre-migration value is a string this
|
|
18
|
+
* file still names, so the change is reversible by hand.
|
|
19
|
+
*/
|
|
20
|
+
export function runModelRenameStartupMigration(
|
|
21
|
+
config: OcxConfig,
|
|
22
|
+
deps: ModelRenameStartupDeps = { project: projectModelRenames, save: saveConfig },
|
|
23
|
+
): OcxConfig {
|
|
24
|
+
const projection = deps.project(config);
|
|
25
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
26
|
+
if (!projection.changed) return projection.config;
|
|
27
|
+
deps.save(projection.config);
|
|
28
|
+
return projection.config;
|
|
29
|
+
}
|