@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0-preview.20260912
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-D_t6sCWs.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
|
@@ -1,295 +1,11 @@
|
|
|
1
|
-
import type { OcxAccountPoolRotationStrategy } from "../types";
|
|
2
|
-
import type { GenerationContext } from "../lib/state-store-sweeper";
|
|
3
|
-
|
|
4
|
-
export const POOL_KEY_CODEX = "codex";
|
|
5
|
-
export const POOL_KEY_ANTHROPIC = "anthropic";
|
|
6
|
-
|
|
7
|
-
interface SelectionState {
|
|
8
|
-
activeKey?: string;
|
|
9
|
-
successes: number;
|
|
10
|
-
currentWeights: Map<string, number>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const selectionState = new Map<string, SelectionState>();
|
|
14
|
-
let lastReconciledGeneration = 0;
|
|
15
|
-
|
|
16
|
-
const DEFAULT_STICKY_LIMIT = 1;
|
|
17
|
-
const MIN_STICKY_LIMIT = 1;
|
|
18
|
-
const MAX_STICKY_LIMIT = 100;
|
|
19
|
-
const DEFAULT_STRATEGY: OcxAccountPoolRotationStrategy = "quota";
|
|
20
|
-
const VALID_STRATEGIES = new Set<OcxAccountPoolRotationStrategy>(["quota", "round-robin", "fill-first"]);
|
|
21
|
-
|
|
22
|
-
/** Selection order for an account with no stored preference: one flat tier. */
|
|
23
|
-
export const DEFAULT_ACCOUNT_PRIORITY = 0;
|
|
24
|
-
export const MIN_ACCOUNT_PRIORITY = -100;
|
|
25
|
-
export const MAX_ACCOUNT_PRIORITY = 100;
|
|
26
|
-
|
|
27
|
-
/** Strict parse for management APIs — returns null instead of defaulting. */
|
|
28
|
-
export function parseAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | null {
|
|
29
|
-
if (typeof raw === "string" && VALID_STRATEGIES.has(raw as OcxAccountPoolRotationStrategy)) {
|
|
30
|
-
return raw as OcxAccountPoolRotationStrategy;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Strict parse for management APIs — returns null instead of defaulting. */
|
|
36
|
-
export function parseAccountPoolStickyLimit(raw: unknown): number | null {
|
|
37
|
-
if (typeof raw === "number" && Number.isInteger(raw) && raw >= MIN_STICKY_LIMIT && raw <= MAX_STICKY_LIMIT) {
|
|
38
|
-
return raw;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function normalizeAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy {
|
|
44
|
-
return parseAccountPoolStrategy(raw) ?? DEFAULT_STRATEGY;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function normalizeAccountPoolStickyLimit(raw: unknown): number {
|
|
48
|
-
return parseAccountPoolStickyLimit(raw) ?? DEFAULT_STICKY_LIMIT;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Strict parse for management APIs — returns null instead of defaulting. */
|
|
52
|
-
export function parseAccountPriority(raw: unknown): number | null {
|
|
53
|
-
if (
|
|
54
|
-
typeof raw === "number"
|
|
55
|
-
&& Number.isInteger(raw)
|
|
56
|
-
&& raw >= MIN_ACCOUNT_PRIORITY
|
|
57
|
-
&& raw <= MAX_ACCOUNT_PRIORITY
|
|
58
|
-
) {
|
|
59
|
-
return raw;
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function normalizeAccountPriority(raw: unknown): number {
|
|
65
|
-
return parseAccountPriority(raw) ?? DEFAULT_ACCOUNT_PRIORITY;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
1
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* eligibility: it never admits an account the caller already filtered out, and it
|
|
72
|
-
* never keeps the pool on a tier whose every member is drained.
|
|
2
|
+
* Compatibility shim. The rotation primitives moved to `src/oauth/pool-kernel.ts`
|
|
3
|
+
* so every credential kind can share them, not only Codex and Anthropic.
|
|
73
4
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* -
|
|
78
|
-
*
|
|
79
|
-
* - every tier drained returns `ids` unchanged, reproducing today's
|
|
80
|
-
* stay-put-until-429 behavior rather than inventing a pick;
|
|
81
|
-
* - a `pinnedId` that is present *and* has headroom lowers the ceiling to its own
|
|
82
|
-
* tier, which is what makes a manual "use this now" survive round-robin and
|
|
83
|
-
* fill-first without any mutable selection state. A drained or absent pin is
|
|
84
|
-
* ignored, so the pin expires on its own once the account crosses the threshold.
|
|
5
|
+
* This file stays because the move is behaviour-preserving and its importers are
|
|
6
|
+
* spread across files that other work owns right now. Re-exporting keeps
|
|
7
|
+
* `routing.ts`, `auth-api.ts`, `account-priority.ts` and
|
|
8
|
+
* `state-store-registrations.ts` on their existing import path, so the extraction
|
|
9
|
+
* lands without editing any of them.
|
|
85
10
|
*/
|
|
86
|
-
export
|
|
87
|
-
ids: readonly string[],
|
|
88
|
-
priorityOf: (id: string) => number,
|
|
89
|
-
hasHeadroom: (id: string) => boolean,
|
|
90
|
-
pinnedId?: string,
|
|
91
|
-
): readonly string[] {
|
|
92
|
-
// Readonly out as well as in: the no-change cases return the caller's own array, so a
|
|
93
|
-
// mutating caller would corrupt its input in exactly the cases that must not change.
|
|
94
|
-
const list = ids;
|
|
95
|
-
if (list.length <= 1) return list;
|
|
96
|
-
|
|
97
|
-
const priorities = list.map(priorityOf);
|
|
98
|
-
const firstPriority = priorities[0]!;
|
|
99
|
-
if (priorities.every(priority => priority === firstPriority)) return list;
|
|
100
|
-
|
|
101
|
-
let ceiling = Number.POSITIVE_INFINITY;
|
|
102
|
-
if (pinnedId !== undefined) {
|
|
103
|
-
const pinnedIndex = list.indexOf(pinnedId);
|
|
104
|
-
if (pinnedIndex >= 0 && hasHeadroom(pinnedId)) ceiling = priorities[pinnedIndex]!;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const tiers = [...new Set(priorities)].sort((a, b) => b - a);
|
|
108
|
-
for (const tier of tiers) {
|
|
109
|
-
if (tier > ceiling) continue;
|
|
110
|
-
const members = list.filter((_, index) => priorities[index] === tier);
|
|
111
|
-
if (members.some(hasHeadroom)) return members;
|
|
112
|
-
}
|
|
113
|
-
return list;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getOrCreateState(poolKey: string): SelectionState {
|
|
117
|
-
let state = selectionState.get(poolKey);
|
|
118
|
-
if (!state) {
|
|
119
|
-
state = { successes: 0, currentWeights: new Map() };
|
|
120
|
-
selectionState.set(poolKey, state);
|
|
121
|
-
}
|
|
122
|
-
return state;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function cloneSelectionState(state: SelectionState): SelectionState {
|
|
126
|
-
return {
|
|
127
|
-
activeKey: state.activeKey,
|
|
128
|
-
successes: state.successes,
|
|
129
|
-
currentWeights: new Map(state.currentWeights),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function smoothWeightedIndex(ids: readonly string[], state: SelectionState): number {
|
|
134
|
-
let best = -1;
|
|
135
|
-
let bestScore = Number.NEGATIVE_INFINITY;
|
|
136
|
-
let total = 0;
|
|
137
|
-
const weight = 1;
|
|
138
|
-
for (let i = 0; i < ids.length; i++) {
|
|
139
|
-
const id = ids[i]!;
|
|
140
|
-
const score = (state.currentWeights.get(id) ?? 0) + weight;
|
|
141
|
-
state.currentWeights.set(id, score);
|
|
142
|
-
total += weight;
|
|
143
|
-
if (score > bestScore) {
|
|
144
|
-
best = i;
|
|
145
|
-
bestScore = score;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (best >= 0) {
|
|
149
|
-
const key = ids[best]!;
|
|
150
|
-
state.currentWeights.set(key, (state.currentWeights.get(key) ?? 0) - total);
|
|
151
|
-
}
|
|
152
|
-
return best;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Shared pick core. Mutates `state` the same way live resolve does; callers pass
|
|
157
|
-
* either the live map entry or a scratch/clone for dry-run peek.
|
|
158
|
-
*/
|
|
159
|
-
function pickRoundRobinFromState(
|
|
160
|
-
eligibleIds: readonly string[],
|
|
161
|
-
stickyLimit: number,
|
|
162
|
-
state: SelectionState,
|
|
163
|
-
commitSticky: boolean,
|
|
164
|
-
): string | null {
|
|
165
|
-
if (eligibleIds.length === 0) return null;
|
|
166
|
-
|
|
167
|
-
const limit = normalizeAccountPoolStickyLimit(stickyLimit);
|
|
168
|
-
|
|
169
|
-
if (state.activeKey && eligibleIds.includes(state.activeKey)) {
|
|
170
|
-
return state.activeKey;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (state.activeKey) {
|
|
174
|
-
delete state.activeKey;
|
|
175
|
-
state.successes = 0;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const index = smoothWeightedIndex(eligibleIds, state);
|
|
179
|
-
if (index < 0) return null;
|
|
180
|
-
|
|
181
|
-
const picked = eligibleIds[index]!;
|
|
182
|
-
if (commitSticky && limit > 1) {
|
|
183
|
-
state.activeKey = picked;
|
|
184
|
-
state.successes = 0;
|
|
185
|
-
}
|
|
186
|
-
return picked;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function pickRoundRobinAccount(
|
|
190
|
-
poolKey: string,
|
|
191
|
-
eligibleIds: readonly string[],
|
|
192
|
-
stickyLimit: number,
|
|
193
|
-
): string | null {
|
|
194
|
-
return pickRoundRobinFromState(eligibleIds, stickyLimit, getOrCreateState(poolKey), true);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Dry-run of {@link pickRoundRobinAccount}: returns the same account resolve would
|
|
199
|
-
* pick without advancing ring weights, activeKey, or successes.
|
|
200
|
-
*/
|
|
201
|
-
export function peekRoundRobinAccount(
|
|
202
|
-
poolKey: string,
|
|
203
|
-
eligibleIds: readonly string[],
|
|
204
|
-
stickyLimit: number,
|
|
205
|
-
): string | null {
|
|
206
|
-
const live = selectionState.get(poolKey);
|
|
207
|
-
const scratch = live
|
|
208
|
-
? cloneSelectionState(live)
|
|
209
|
-
: { successes: 0, currentWeights: new Map<string, number>() };
|
|
210
|
-
return pickRoundRobinFromState(eligibleIds, stickyLimit, scratch, false);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function notePoolRotationSuccess(
|
|
214
|
-
poolKey: string,
|
|
215
|
-
accountId: string,
|
|
216
|
-
stickyLimit: number,
|
|
217
|
-
): void {
|
|
218
|
-
const limit = normalizeAccountPoolStickyLimit(stickyLimit);
|
|
219
|
-
const state = selectionState.get(poolKey);
|
|
220
|
-
if (!state) return;
|
|
221
|
-
if (state.activeKey !== accountId) {
|
|
222
|
-
state.activeKey = accountId;
|
|
223
|
-
state.successes = 0;
|
|
224
|
-
}
|
|
225
|
-
state.successes += 1;
|
|
226
|
-
if (state.successes >= limit) {
|
|
227
|
-
delete state.activeKey;
|
|
228
|
-
state.successes = 0;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function notePoolRotationFailure(poolKey: string, accountId: string): void {
|
|
233
|
-
const state = selectionState.get(poolKey);
|
|
234
|
-
if (state?.activeKey === accountId) {
|
|
235
|
-
delete state.activeKey;
|
|
236
|
-
state.successes = 0;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Force the next sticky/RR pick onto `accountId` (manual dashboard selection).
|
|
242
|
-
* Clears sticky success counters and ring weights so the seeded account is held
|
|
243
|
-
* for the next new-session pick before ordinary rotation resumes.
|
|
244
|
-
*/
|
|
245
|
-
export function seedPoolRotationAccount(poolKey: string, accountId: string): void {
|
|
246
|
-
const state = getOrCreateState(poolKey);
|
|
247
|
-
state.activeKey = accountId;
|
|
248
|
-
state.successes = 0;
|
|
249
|
-
state.currentWeights.clear();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function clearPoolRotationState(poolKey?: string): void {
|
|
253
|
-
if (poolKey === undefined) {
|
|
254
|
-
selectionState.clear();
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
selectionState.delete(poolKey);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function reconcilePoolRotationState(context: GenerationContext): number {
|
|
261
|
-
if (context.generation <= lastReconciledGeneration) return 0;
|
|
262
|
-
const anthropicIds = new Set<string>();
|
|
263
|
-
for (const key of context.oauthAccountKeys) {
|
|
264
|
-
const separator = key.indexOf("\0");
|
|
265
|
-
if (separator > 0 && key.slice(0, separator) === "anthropic") {
|
|
266
|
-
anthropicIds.add(key.slice(separator + 1));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
let removed = 0;
|
|
270
|
-
for (const [poolKey, state] of selectionState) {
|
|
271
|
-
const valid = poolKey === POOL_KEY_ANTHROPIC
|
|
272
|
-
? anthropicIds
|
|
273
|
-
: poolKey === POOL_KEY_CODEX || poolKey.startsWith(`${POOL_KEY_CODEX}:`)
|
|
274
|
-
? context.codexAccountIds
|
|
275
|
-
: null;
|
|
276
|
-
if (!valid) continue;
|
|
277
|
-
if (valid.size === 0) {
|
|
278
|
-
selectionState.delete(poolKey);
|
|
279
|
-
removed += 1;
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (state.activeKey && !valid.has(state.activeKey)) {
|
|
283
|
-
delete state.activeKey;
|
|
284
|
-
state.successes = 0;
|
|
285
|
-
removed += 1;
|
|
286
|
-
}
|
|
287
|
-
for (const accountId of state.currentWeights.keys()) {
|
|
288
|
-
if (valid.has(accountId)) continue;
|
|
289
|
-
state.currentWeights.delete(accountId);
|
|
290
|
-
removed += 1;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
lastReconciledGeneration = context.generation;
|
|
294
|
-
return removed;
|
|
295
|
-
}
|
|
11
|
+
export * from "../oauth/pool-kernel";
|
package/src/codex/quota.ts
CHANGED
|
@@ -44,6 +44,21 @@ const MONTHLY_WINDOW_MIN_MINUTES = MONTHLY_WINDOW_MIN_SECONDS / 60;
|
|
|
44
44
|
// Derived, never written as a literal: the header parser and the WHAM parser must not be able
|
|
45
45
|
// to drift to different thresholds, which is the class of defect this pair exists to prevent.
|
|
46
46
|
const WEEKLY_WINDOW_MIN_MINUTES = WEEKLY_WINDOW_MIN_SECONDS / 60;
|
|
47
|
+
/**
|
|
48
|
+
* Seconds/milliseconds split for a stored reset instant: below it the value is Unix seconds,
|
|
49
|
+
* at or above it milliseconds.
|
|
50
|
+
*
|
|
51
|
+
* Both units reach storage — `normalizeResetAt` does not scale, and the GUI disambiguates by
|
|
52
|
+
* magnitude at read time — so a comparison written against one assumption is off by 1000x
|
|
53
|
+
* against the other. In the seconds-read-as-milliseconds direction every reading looks like it
|
|
54
|
+
* elapsed in 1970, which is a check that passes its own test and does nothing. Exported so
|
|
55
|
+
* `isTerminalShortWindow` in routing.ts shares this one split instead of repeating the literal.
|
|
56
|
+
*/
|
|
57
|
+
const RESET_AT_SECONDS_MAX = 10_000_000_000;
|
|
58
|
+
|
|
59
|
+
export function resetAtToMs(resetAt: number): number {
|
|
60
|
+
return resetAt < RESET_AT_SECONDS_MAX ? resetAt * 1000 : resetAt;
|
|
61
|
+
}
|
|
47
62
|
|
|
48
63
|
const accountQuota = new Map<string, StoredAccountQuota>();
|
|
49
64
|
let lastReconciledGeneration = 0;
|
|
@@ -221,6 +236,25 @@ function snapshotHasShort(quota: Omit<StoredAccountQuota, "updatedAt">): boolean
|
|
|
221
236
|
|| quota.shortWindowSeconds !== undefined;
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
function shortResetHasElapsed(resetAt: number | undefined, now: number): boolean {
|
|
240
|
+
if (typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt <= 0) return false;
|
|
241
|
+
return resetAtToMs(resetAt) <= now;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Display/rotation carry expires; a reset clock cannot retract hard-lock evidence. */
|
|
245
|
+
function assignCarriedShort(
|
|
246
|
+
next: StoredAccountQuota,
|
|
247
|
+
existing: StoredAccountQuota | undefined,
|
|
248
|
+
now: number,
|
|
249
|
+
policyEvidence = false,
|
|
250
|
+
): void {
|
|
251
|
+
if (!existing || (!policyEvidence && shortResetHasElapsed(existing.shortResetAt, now))) return;
|
|
252
|
+
if (existing.shortPercent !== undefined) next.shortPercent = existing.shortPercent;
|
|
253
|
+
if (existing.shortObservedAt !== undefined) next.shortObservedAt = existing.shortObservedAt;
|
|
254
|
+
if (existing.shortResetAt !== undefined) next.shortResetAt = existing.shortResetAt;
|
|
255
|
+
if (existing.shortWindowSeconds !== undefined) next.shortWindowSeconds = existing.shortWindowSeconds;
|
|
256
|
+
}
|
|
257
|
+
|
|
224
258
|
function snapshotHasCustom(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
225
259
|
return quota.customWindows !== undefined;
|
|
226
260
|
}
|
|
@@ -282,10 +316,7 @@ function mergeAccountQuota(
|
|
|
282
316
|
if (existing?.monthlyPercent !== undefined) next.monthlyPercent = existing.monthlyPercent;
|
|
283
317
|
if (existing?.monthlyResetAt !== undefined) next.monthlyResetAt = existing.monthlyResetAt;
|
|
284
318
|
if (existing?.monthlyIsPrimaryWindow === true) next.monthlyIsPrimaryWindow = true;
|
|
285
|
-
|
|
286
|
-
if (existing?.shortObservedAt !== undefined) next.shortObservedAt = existing.shortObservedAt;
|
|
287
|
-
if (existing?.shortResetAt !== undefined) next.shortResetAt = existing.shortResetAt;
|
|
288
|
-
if (existing?.shortWindowSeconds !== undefined) next.shortWindowSeconds = existing.shortWindowSeconds;
|
|
319
|
+
assignCarriedShort(next, existing, updatedAt, policyEvidence);
|
|
289
320
|
if (existing?.customWindows !== undefined) next.customWindows = existing.customWindows;
|
|
290
321
|
next.resetCredits = quota.resetCredits;
|
|
291
322
|
return next;
|
|
@@ -329,10 +360,10 @@ function mergeAccountQuota(
|
|
|
329
360
|
} else {
|
|
330
361
|
// Unknown usage is not a lower reading. Retain the entire known tuple: pairing
|
|
331
362
|
// its percentage with new metadata would silently extend or shorten its reset.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
363
|
+
// An elapsed reset is the exception. It describes a window that has already rolled over,
|
|
364
|
+
// and carrying it republishes updatedAt, which is exactly what kept a Spark-polluted Pro
|
|
365
|
+
// row alive past the six-hour disk TTL that #4122 expected to expire it.
|
|
366
|
+
assignCarriedShort(next, existing, updatedAt, policyEvidence);
|
|
336
367
|
}
|
|
337
368
|
|
|
338
369
|
if (snapshotHasCustom(quota)) next.customWindows = quota.customWindows;
|
|
@@ -394,6 +425,7 @@ function notifyCodexQuotaSnapshot(accountId: string, next: StoredAccountQuota):
|
|
|
394
425
|
scope: "codex",
|
|
395
426
|
accountKey: accountId,
|
|
396
427
|
windows: codexWindowObservations(snapshot),
|
|
428
|
+
retainAbsentShortWindow: true,
|
|
397
429
|
});
|
|
398
430
|
})
|
|
399
431
|
.catch(() => {
|
|
@@ -561,14 +593,11 @@ export function updateAccountQuota(
|
|
|
561
593
|
: {}),
|
|
562
594
|
...(existing?.weeklyResetAt !== undefined ? { weeklyResetAt: existing.weeklyResetAt } : {}),
|
|
563
595
|
...(existing?.monthlyResetAt !== undefined ? { monthlyResetAt: existing.monthlyResetAt } : {}),
|
|
564
|
-
...(existing?.shortPercent !== undefined ? { shortPercent: existing.shortPercent } : {}),
|
|
565
|
-
...(existing?.shortObservedAt !== undefined ? { shortObservedAt: existing.shortObservedAt } : {}),
|
|
566
|
-
...(existing?.shortResetAt !== undefined ? { shortResetAt: existing.shortResetAt } : {}),
|
|
567
|
-
...(existing?.shortWindowSeconds !== undefined ? { shortWindowSeconds: existing.shortWindowSeconds } : {}),
|
|
568
596
|
...(existing?.customWindows !== undefined ? { customWindows: existing.customWindows } : {}),
|
|
569
597
|
...(existing?.resetCredits !== undefined ? { resetCredits: existing.resetCredits } : {}),
|
|
570
598
|
updatedAt: Date.now(),
|
|
571
599
|
};
|
|
600
|
+
assignCarriedShort(quota, existing, quota.updatedAt);
|
|
572
601
|
|
|
573
602
|
const nextWeeklyResetAt = normalizeResetAt(weeklyResetAt);
|
|
574
603
|
const nextMonthlyResetAt = normalizeResetAt(monthlyResetAt);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time migration off Codex-login models that upstream has retired.
|
|
3
|
+
*
|
|
4
|
+
* Originally this only moved the two sidecar defaults, and it was written when
|
|
5
|
+
* `gpt-5.4-mini` was merely superseded rather than withdrawn. Once OpenAI retired the
|
|
6
|
+
* model the same stored value stopped being a stale preference and became a guaranteed
|
|
7
|
+
* 404 on every web-search, vision and pool-warmup call, so the pool warmup slug joined
|
|
8
|
+
* the set.
|
|
9
|
+
*
|
|
10
|
+
* The match is exact equality, which means an explicitly chosen `gpt-5.4-mini` is
|
|
11
|
+
* rewritten too. That is deliberate: there is no configuration in which continuing to
|
|
12
|
+
* send a withdrawn model is what the operator wanted. Any other value is left alone.
|
|
13
|
+
*/
|
|
14
|
+
import type { OcxConfig } from "../types";
|
|
15
|
+
|
|
16
|
+
/** July 9 21:00 UTC = KST July 10 06:00. */
|
|
17
|
+
export const RETIRED_MODEL_MIGRATION_CUTOFF = Date.UTC(2026, 6, 9, 21, 0);
|
|
18
|
+
|
|
19
|
+
const RETIRED_SIDECAR_MODEL = "gpt-5.4-mini";
|
|
20
|
+
const REPLACEMENT_MODEL = "gpt-5.6-luna";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @returns true when the config changed and the caller should persist it.
|
|
24
|
+
*/
|
|
25
|
+
export function runRetiredCodexModelMigration(config: OcxConfig, now = Date.now()): boolean {
|
|
26
|
+
if (now < RETIRED_MODEL_MIGRATION_CUTOFF) return false;
|
|
27
|
+
let migrated = false;
|
|
28
|
+
if (config.webSearchSidecar?.model === RETIRED_SIDECAR_MODEL) {
|
|
29
|
+
config.webSearchSidecar = { ...config.webSearchSidecar, model: REPLACEMENT_MODEL };
|
|
30
|
+
migrated = true;
|
|
31
|
+
}
|
|
32
|
+
if (config.visionSidecar?.model === RETIRED_SIDECAR_MODEL) {
|
|
33
|
+
config.visionSidecar = { ...config.visionSidecar, model: REPLACEMENT_MODEL };
|
|
34
|
+
migrated = true;
|
|
35
|
+
}
|
|
36
|
+
if (config.tokenGuardian?.codexWarmupModel === RETIRED_SIDECAR_MODEL) {
|
|
37
|
+
config.tokenGuardian = { ...config.tokenGuardian, codexWarmupModel: REPLACEMENT_MODEL };
|
|
38
|
+
migrated = true;
|
|
39
|
+
}
|
|
40
|
+
return migrated;
|
|
41
|
+
}
|