@coseung2/opencodex 2.8.0-cs.13 → 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 +2 -1
- 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 +2 -1
- package/src/codex/auth-api.ts +108 -17
- 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 +193 -85
- 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/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 +258 -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/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 +8 -0
- 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-BucjyD4I.js +0 -67
|
@@ -58,7 +58,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
|
|
|
58
58
|
|
|
59
59
|
import { JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
|
|
60
60
|
import type { CatalogModel } from "./parsing";
|
|
61
|
-
import { disabledNativeSlugs, hasComboTargets, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
61
|
+
import { disabledNativeSlugs, hasComboTargets, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
62
62
|
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
|
|
63
63
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
64
64
|
|
|
@@ -274,10 +274,15 @@ export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds:
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
export function isGlm52ModelId(id: string): boolean {
|
|
277
|
-
const normalized = id.toLowerCase();
|
|
277
|
+
const normalized = id.trim().toLowerCase();
|
|
278
278
|
return normalized === "glm-5.2" || normalized === "glm-5.2[1m]";
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
export function isGlm53ModelId(id: string): boolean {
|
|
282
|
+
const normalized = id.trim().toLowerCase();
|
|
283
|
+
return normalized === "glm-5.3" || normalized === "glm-5.3[1m]";
|
|
284
|
+
}
|
|
285
|
+
|
|
281
286
|
function plainRecord(value: unknown): Record<string, unknown> | undefined {
|
|
282
287
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
283
288
|
? value as Record<string, unknown>
|
|
@@ -358,6 +363,17 @@ function modelInputModalities(
|
|
|
358
363
|
value === "text" || value === "image" || value === "audio"
|
|
359
364
|
));
|
|
360
365
|
if (explicit && explicit.length > 0) return explicit;
|
|
366
|
+
const architecture = plainRecord(item.architecture);
|
|
367
|
+
const architectureModality = typeof architecture?.modality === "string"
|
|
368
|
+
? normalizedMetadataString(architecture.modality, 64)
|
|
369
|
+
: undefined;
|
|
370
|
+
if (architectureModality?.includes("->")) {
|
|
371
|
+
const [rawInput = ""] = architectureModality.split("->");
|
|
372
|
+
const inferred = rawInput
|
|
373
|
+
.split("+")
|
|
374
|
+
.filter(value => value === "text" || value === "image" || value === "audio");
|
|
375
|
+
if (inferred.length > 0) return [...new Set(inferred)];
|
|
376
|
+
}
|
|
361
377
|
if (capabilityRecord?.vision === false) return ["text"];
|
|
362
378
|
if (capabilityRecord?.vision === true || capabilities?.some(value => value === "vision" || value === "image-input")) {
|
|
363
379
|
return ["text", "image"];
|
|
@@ -385,9 +401,11 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
385
401
|
? sanitizeCodexReasoningEfforts(listedReasoningEfforts)
|
|
386
402
|
: typeof rawReasoningEfforts === "boolean"
|
|
387
403
|
? (rawReasoningEfforts
|
|
388
|
-
? ((providerName === "neuralwatt" || providerName === "zai") &&
|
|
389
|
-
? ["low", "
|
|
390
|
-
:
|
|
404
|
+
? ((providerName === "neuralwatt" || providerName === "zai") && isGlm53ModelId(item.id)
|
|
405
|
+
? ["low", "high", "max"]
|
|
406
|
+
: (providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
|
|
407
|
+
? ["low", "medium", "high", "xhigh", "max"]
|
|
408
|
+
: ["low", "medium", "high", "xhigh"])
|
|
391
409
|
: [])
|
|
392
410
|
: undefined;
|
|
393
411
|
const capabilities = modelCapabilities(item);
|
|
@@ -756,7 +774,7 @@ async function gatherRoutedModelsUncached(
|
|
|
756
774
|
id: slug,
|
|
757
775
|
owned_by: "openai",
|
|
758
776
|
contextWindow,
|
|
759
|
-
maxInputTokens: contextWindow,
|
|
777
|
+
maxInputTokens: nativeOpenAiMaxInputTokens(slug) ?? contextWindow,
|
|
760
778
|
inputModalities: nativeInputModalities(slug),
|
|
761
779
|
reasoningEfforts: nativeReasoningEfforts(slug),
|
|
762
780
|
...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
|
package/src/codex/catalog.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Public surface preserved exactly; importers keep using "src/codex/catalog".
|
|
3
3
|
export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
|
|
4
4
|
export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
|
|
5
|
-
export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort } from "./catalog/metadata";
|
|
5
|
+
export { NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_CONTEXT_WINDOW, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort } from "./catalog/metadata";
|
|
6
6
|
export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
|
|
7
7
|
export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
|
|
8
8
|
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch";
|
|
@@ -19,6 +19,10 @@ const MAX_STICKY_LIMIT = 100;
|
|
|
19
19
|
const DEFAULT_STRATEGY: OcxAccountPoolRotationStrategy = "quota";
|
|
20
20
|
const VALID_STRATEGIES = new Set<OcxAccountPoolRotationStrategy>(["quota", "round-robin", "fill-first"]);
|
|
21
21
|
|
|
22
|
+
export const DEFAULT_ACCOUNT_PRIORITY = 0;
|
|
23
|
+
export const MIN_ACCOUNT_PRIORITY = -100;
|
|
24
|
+
export const MAX_ACCOUNT_PRIORITY = 100;
|
|
25
|
+
|
|
22
26
|
/** Strict parse for management APIs — returns null instead of defaulting. */
|
|
23
27
|
export function parseAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | null {
|
|
24
28
|
if (typeof raw === "string" && VALID_STRATEGIES.has(raw as OcxAccountPoolRotationStrategy)) {
|
|
@@ -43,6 +47,49 @@ export function normalizeAccountPoolStickyLimit(raw: unknown): number {
|
|
|
43
47
|
return parseAccountPoolStickyLimit(raw) ?? DEFAULT_STICKY_LIMIT;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
export function parseAccountPriority(raw: unknown): number | null {
|
|
51
|
+
return typeof raw === "number"
|
|
52
|
+
&& Number.isInteger(raw)
|
|
53
|
+
&& raw >= MIN_ACCOUNT_PRIORITY
|
|
54
|
+
&& raw <= MAX_ACCOUNT_PRIORITY
|
|
55
|
+
? raw
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizeAccountPriority(raw: unknown): number {
|
|
60
|
+
return parseAccountPriority(raw) ?? DEFAULT_ACCOUNT_PRIORITY;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Narrow eligible ids to the highest configured tier that still has quota headroom.
|
|
65
|
+
* Equal-priority pools and fully drained pools preserve the caller's exact list/order.
|
|
66
|
+
*/
|
|
67
|
+
export function selectPriorityTier(
|
|
68
|
+
ids: readonly string[],
|
|
69
|
+
priorityOf: (id: string) => number,
|
|
70
|
+
hasHeadroom: (id: string) => boolean,
|
|
71
|
+
pinnedId?: string,
|
|
72
|
+
): readonly string[] {
|
|
73
|
+
if (ids.length <= 1) return ids;
|
|
74
|
+
const priorities = ids.map(priorityOf);
|
|
75
|
+
const firstPriority = priorities[0]!;
|
|
76
|
+
if (priorities.every(priority => priority === firstPriority)) return ids;
|
|
77
|
+
|
|
78
|
+
let ceiling = Number.POSITIVE_INFINITY;
|
|
79
|
+
if (pinnedId !== undefined) {
|
|
80
|
+
const pinnedIndex = ids.indexOf(pinnedId);
|
|
81
|
+
if (pinnedIndex >= 0 && hasHeadroom(pinnedId)) ceiling = priorities[pinnedIndex]!;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tiers = [...new Set(priorities)].sort((a, b) => b - a);
|
|
85
|
+
for (const tier of tiers) {
|
|
86
|
+
if (tier > ceiling) continue;
|
|
87
|
+
const members = ids.filter((_, index) => priorities[index] === tier);
|
|
88
|
+
if (members.some(hasHeadroom)) return members;
|
|
89
|
+
}
|
|
90
|
+
return ids;
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
function getOrCreateState(poolKey: string): SelectionState {
|
|
47
94
|
let state = selectionState.get(poolKey);
|
|
48
95
|
if (!state) {
|
|
@@ -60,7 +107,7 @@ function cloneSelectionState(state: SelectionState): SelectionState {
|
|
|
60
107
|
};
|
|
61
108
|
}
|
|
62
109
|
|
|
63
|
-
function smoothWeightedIndex(ids: string[], state: SelectionState): number {
|
|
110
|
+
function smoothWeightedIndex(ids: readonly string[], state: SelectionState): number {
|
|
64
111
|
let best = -1;
|
|
65
112
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
66
113
|
let total = 0;
|
|
@@ -87,7 +134,7 @@ function smoothWeightedIndex(ids: string[], state: SelectionState): number {
|
|
|
87
134
|
* either the live map entry or a scratch/clone for dry-run peek.
|
|
88
135
|
*/
|
|
89
136
|
function pickRoundRobinFromState(
|
|
90
|
-
eligibleIds: string[],
|
|
137
|
+
eligibleIds: readonly string[],
|
|
91
138
|
stickyLimit: number,
|
|
92
139
|
state: SelectionState,
|
|
93
140
|
commitSticky: boolean,
|
|
@@ -118,7 +165,7 @@ function pickRoundRobinFromState(
|
|
|
118
165
|
|
|
119
166
|
export function pickRoundRobinAccount(
|
|
120
167
|
poolKey: string,
|
|
121
|
-
eligibleIds: string[],
|
|
168
|
+
eligibleIds: readonly string[],
|
|
122
169
|
stickyLimit: number,
|
|
123
170
|
): string | null {
|
|
124
171
|
return pickRoundRobinFromState(eligibleIds, stickyLimit, getOrCreateState(poolKey), true);
|
|
@@ -130,7 +177,7 @@ export function pickRoundRobinAccount(
|
|
|
130
177
|
*/
|
|
131
178
|
export function peekRoundRobinAccount(
|
|
132
179
|
poolKey: string,
|
|
133
|
-
eligibleIds: string[],
|
|
180
|
+
eligibleIds: readonly string[],
|
|
134
181
|
stickyLimit: number,
|
|
135
182
|
): string | null {
|
|
136
183
|
const live = selectionState.get(poolKey);
|
package/src/codex/quota.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { atomicWriteFile, getConfigDir } from "../config";
|
|
|
4
4
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
5
5
|
|
|
6
6
|
export type StoredAccountQuota = {
|
|
7
|
+
fiveHourPercent?: number;
|
|
7
8
|
weeklyPercent?: number;
|
|
9
|
+
fiveHourResetAt?: number;
|
|
8
10
|
monthlyPercent?: number;
|
|
9
11
|
weeklyResetAt?: number;
|
|
10
12
|
monthlyResetAt?: number;
|
|
@@ -41,13 +43,15 @@ export type WhamUsageResponse = {
|
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
type WhamUsageWindow = {
|
|
44
|
-
used_percent?: number;
|
|
45
|
-
reset_at?: number;
|
|
46
|
-
limit_window_seconds?: number;
|
|
46
|
+
used_percent?: number | string | null;
|
|
47
|
+
reset_at?: number | string | null;
|
|
48
|
+
limit_window_seconds?: number | string | null;
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
const MONTHLY_WINDOW_MIN_SECONDS = 28 * 24 * 60 * 60;
|
|
50
52
|
const MONTHLY_WINDOW_MIN_MINUTES = MONTHLY_WINDOW_MIN_SECONDS / 60;
|
|
53
|
+
const FIVE_HOUR_WINDOW_MIN_SECONDS = 2 * 60 * 60;
|
|
54
|
+
const FIVE_HOUR_WINDOW_MAX_SECONDS = 12 * 60 * 60;
|
|
51
55
|
|
|
52
56
|
const accountQuota = new Map<string, StoredAccountQuota>();
|
|
53
57
|
let lastReconciledGeneration = 0;
|
|
@@ -62,15 +66,26 @@ function mayCommitAccountQuota(accountId: string, writerGeneration: number): boo
|
|
|
62
66
|
export const CODEX_UNKNOWN_USAGE_SCORE = 101;
|
|
63
67
|
export const CODEX_EXHAUSTED_USAGE_PERCENT = 100;
|
|
64
68
|
|
|
69
|
+
/** Plans whose WHAM payloads expose a separate rolling five-hour window. */
|
|
70
|
+
export function isCodexFiveHourQuotaPlan(plan: string | null | undefined): boolean {
|
|
71
|
+
const normalized = plan?.trim().toLowerCase();
|
|
72
|
+
// Business is the current name for the older Team workspace plan.
|
|
73
|
+
return normalized === "plus" || normalized === "team" || normalized === "business";
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
export function isCodexQuotaExhausted(
|
|
66
|
-
quota: Pick<StoredAccountQuota, "weeklyPercent" | "monthlyPercent"> | null,
|
|
77
|
+
quota: Pick<StoredAccountQuota, "fiveHourPercent" | "weeklyPercent" | "monthlyPercent"> | null,
|
|
67
78
|
plan?: string | null,
|
|
68
79
|
): boolean {
|
|
69
80
|
if (!quota) return false;
|
|
70
81
|
const normalizedPlan = plan?.trim().toLowerCase();
|
|
71
82
|
const values = normalizedPlan === "go" || normalizedPlan === "free"
|
|
72
83
|
? [quota.monthlyPercent]
|
|
73
|
-
: [
|
|
84
|
+
: [
|
|
85
|
+
...(isCodexFiveHourQuotaPlan(plan) ? [quota.fiveHourPercent] : []),
|
|
86
|
+
quota.weeklyPercent,
|
|
87
|
+
quota.monthlyPercent,
|
|
88
|
+
];
|
|
74
89
|
return values.some(value => typeof value === "number"
|
|
75
90
|
&& Number.isFinite(value)
|
|
76
91
|
&& value >= CODEX_EXHAUSTED_USAGE_PERCENT);
|
|
@@ -97,15 +112,23 @@ function normalizeResetAt(value: unknown): number | undefined {
|
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
function hasKnownQuotaValue(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
100
|
-
return [quota.weeklyPercent, quota.monthlyPercent]
|
|
115
|
+
return [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent]
|
|
101
116
|
.some(value => typeof value === "number" && Number.isFinite(value));
|
|
102
117
|
}
|
|
103
118
|
|
|
119
|
+
function windowDurationSeconds(window: WhamUsageWindow | null | undefined): number | undefined {
|
|
120
|
+
const raw: unknown = window?.limit_window_seconds;
|
|
121
|
+
const seconds = typeof raw === "number"
|
|
122
|
+
? raw
|
|
123
|
+
: typeof raw === "string" && raw.trim() !== ""
|
|
124
|
+
? Number(raw)
|
|
125
|
+
: undefined;
|
|
126
|
+
return typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 ? seconds : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
104
129
|
function isExplicitMonthlyWindow(window: WhamUsageWindow | null | undefined): boolean {
|
|
105
|
-
const seconds = window
|
|
106
|
-
return
|
|
107
|
-
&& Number.isFinite(seconds)
|
|
108
|
-
&& seconds >= MONTHLY_WINDOW_MIN_SECONDS;
|
|
130
|
+
const seconds = windowDurationSeconds(window);
|
|
131
|
+
return seconds !== undefined && seconds >= MONTHLY_WINDOW_MIN_SECONDS;
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
function isExplicitMonthlyWindowMinutes(windowMinutes: unknown): boolean {
|
|
@@ -119,17 +142,39 @@ function isExplicitMonthlyWindowMinutes(windowMinutes: unknown): boolean {
|
|
|
119
142
|
&& minutes >= MONTHLY_WINDOW_MIN_MINUTES;
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
function isExplicitFiveHourWindow(window: WhamUsageWindow | null | undefined): boolean {
|
|
146
|
+
const seconds = windowDurationSeconds(window);
|
|
147
|
+
return seconds !== undefined
|
|
148
|
+
&& seconds >= FIVE_HOUR_WINDOW_MIN_SECONDS
|
|
149
|
+
&& seconds <= FIVE_HOUR_WINDOW_MAX_SECONDS;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isExplicitFiveHourWindowMinutes(windowMinutes: unknown): boolean {
|
|
153
|
+
const minutes = typeof windowMinutes === "number"
|
|
154
|
+
? windowMinutes
|
|
155
|
+
: typeof windowMinutes === "string" && windowMinutes.trim() !== ""
|
|
156
|
+
? Number(windowMinutes)
|
|
157
|
+
: undefined;
|
|
158
|
+
return typeof minutes === "number"
|
|
159
|
+
&& Number.isFinite(minutes)
|
|
160
|
+
&& minutes >= FIVE_HOUR_WINDOW_MIN_SECONDS / 60
|
|
161
|
+
&& minutes <= FIVE_HOUR_WINDOW_MAX_SECONDS / 60;
|
|
162
|
+
}
|
|
122
163
|
|
|
123
164
|
function snapshotHasWeekly(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
124
165
|
return quota.weeklyPercent !== undefined || quota.weeklyResetAt !== undefined;
|
|
125
166
|
}
|
|
126
167
|
|
|
168
|
+
function snapshotHasFiveHour(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
169
|
+
return quota.fiveHourPercent !== undefined || quota.fiveHourResetAt !== undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
127
172
|
function snapshotHasMonthly(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
128
173
|
return quota.monthlyPercent !== undefined || quota.monthlyResetAt !== undefined;
|
|
129
174
|
}
|
|
130
175
|
|
|
131
176
|
function snapshotHasUsage(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
132
|
-
return snapshotHasWeekly(quota) || snapshotHasMonthly(quota);
|
|
177
|
+
return snapshotHasFiveHour(quota) || snapshotHasWeekly(quota) || snapshotHasMonthly(quota);
|
|
133
178
|
}
|
|
134
179
|
export function setAccountQuotaFromParsed(
|
|
135
180
|
accountId: string,
|
|
@@ -143,6 +188,8 @@ export function setAccountQuotaFromParsed(
|
|
|
143
188
|
const creditsOnly = quota.resetCredits !== undefined && !snapshotHasUsage(quota);
|
|
144
189
|
|
|
145
190
|
if (creditsOnly) {
|
|
191
|
+
if (existing?.fiveHourPercent !== undefined) next.fiveHourPercent = existing.fiveHourPercent;
|
|
192
|
+
if (existing?.fiveHourResetAt !== undefined) next.fiveHourResetAt = existing.fiveHourResetAt;
|
|
146
193
|
if (existing?.weeklyPercent !== undefined) next.weeklyPercent = existing.weeklyPercent;
|
|
147
194
|
if (existing?.weeklyResetAt !== undefined) next.weeklyResetAt = existing.weeklyResetAt;
|
|
148
195
|
if (existing?.monthlyPercent !== undefined) next.monthlyPercent = existing.monthlyPercent;
|
|
@@ -153,20 +200,32 @@ export function setAccountQuotaFromParsed(
|
|
|
153
200
|
return;
|
|
154
201
|
}
|
|
155
202
|
|
|
156
|
-
|
|
203
|
+
const hasFiveHour = snapshotHasFiveHour(quota);
|
|
204
|
+
const hasWeekly = snapshotHasWeekly(quota);
|
|
205
|
+
const hasMonthly = snapshotHasMonthly(quota);
|
|
206
|
+
|
|
207
|
+
if (hasFiveHour) {
|
|
208
|
+
if (quota.fiveHourPercent !== undefined) next.fiveHourPercent = quota.fiveHourPercent;
|
|
209
|
+
if (quota.fiveHourResetAt !== undefined) next.fiveHourResetAt = quota.fiveHourResetAt;
|
|
210
|
+
// A five-hour-only response can be a partial snapshot; retain a known
|
|
211
|
+
// weekly value until a later response supplies that window.
|
|
212
|
+
if (!hasWeekly && existing?.weeklyPercent !== undefined) {
|
|
213
|
+
next.weeklyPercent = existing.weeklyPercent;
|
|
214
|
+
if (existing.weeklyResetAt !== undefined) next.weeklyResetAt = existing.weeklyResetAt;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (hasWeekly) {
|
|
157
219
|
if (quota.weeklyPercent !== undefined) next.weeklyPercent = quota.weeklyPercent;
|
|
158
220
|
if (quota.weeklyResetAt !== undefined) next.weeklyResetAt = quota.weeklyResetAt;
|
|
159
|
-
} else if (
|
|
221
|
+
} else if (hasMonthly && !hasFiveHour) {
|
|
160
222
|
// Monthly-only snapshots intentionally clear stale weekly values (issue #382).
|
|
161
|
-
} else if (existing?.weeklyPercent !== undefined) {
|
|
162
|
-
next.weeklyPercent = existing.weeklyPercent;
|
|
163
|
-
if (existing.weeklyResetAt !== undefined) next.weeklyResetAt = existing.weeklyResetAt;
|
|
164
223
|
}
|
|
165
224
|
|
|
166
|
-
if (
|
|
225
|
+
if (hasMonthly) {
|
|
167
226
|
if (quota.monthlyPercent !== undefined) next.monthlyPercent = quota.monthlyPercent;
|
|
168
227
|
if (quota.monthlyResetAt !== undefined) next.monthlyResetAt = quota.monthlyResetAt;
|
|
169
|
-
} else if (
|
|
228
|
+
} else if ((hasWeekly || hasFiveHour) && existing?.monthlyPercent !== undefined) {
|
|
170
229
|
next.monthlyPercent = existing.monthlyPercent;
|
|
171
230
|
if (existing.monthlyResetAt !== undefined) next.monthlyResetAt = existing.monthlyResetAt;
|
|
172
231
|
}
|
|
@@ -178,7 +237,10 @@ export function setAccountQuotaFromParsed(
|
|
|
178
237
|
schedulePersistAccountQuotas();
|
|
179
238
|
}
|
|
180
239
|
|
|
181
|
-
export function parseUpstreamQuotaHeaders(
|
|
240
|
+
export function parseUpstreamQuotaHeaders(
|
|
241
|
+
headers: Headers,
|
|
242
|
+
plan?: string | null,
|
|
243
|
+
): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
182
244
|
const primaryRaw = headers.get("x-codex-primary-used-percent");
|
|
183
245
|
const secondaryRaw = headers.get("x-codex-secondary-used-percent");
|
|
184
246
|
const tertiaryRaw = headers.get("x-codex-tertiary-used-percent");
|
|
@@ -196,25 +258,52 @@ export function parseUpstreamQuotaHeaders(headers: Headers): Omit<StoredAccountQ
|
|
|
196
258
|
const secondaryResetAt = normalizeResetAt(secondaryResetRaw);
|
|
197
259
|
const tertiaryResetAt = normalizeResetAt(tertiaryResetRaw);
|
|
198
260
|
const primaryIsMonthly = primaryRaw !== null && isExplicitMonthlyWindowMinutes(primaryWindowMinutes);
|
|
261
|
+
const trackFiveHour = plan == null || isCodexFiveHourQuotaPlan(plan);
|
|
262
|
+
const primaryIsFiveHour = trackFiveHour && primaryRaw !== null && isExplicitFiveHourWindowMinutes(primaryWindowMinutes);
|
|
263
|
+
const secondaryIsFiveHour = trackFiveHour && secondaryRaw !== null && isExplicitFiveHourWindowMinutes(secondaryWindowMinutes);
|
|
199
264
|
|
|
200
265
|
if (primaryIsMonthly) {
|
|
201
266
|
if (primaryPercent !== undefined) {
|
|
202
267
|
quota.monthlyPercent = primaryPercent;
|
|
203
268
|
if (primaryResetAt !== undefined) quota.monthlyResetAt = primaryResetAt;
|
|
204
269
|
}
|
|
205
|
-
if (secondaryPercent !== undefined) {
|
|
270
|
+
if (secondaryIsFiveHour && secondaryPercent !== undefined) {
|
|
271
|
+
quota.fiveHourPercent = secondaryPercent;
|
|
272
|
+
if (secondaryResetAt !== undefined) quota.fiveHourResetAt = secondaryResetAt;
|
|
273
|
+
} else if (secondaryPercent !== undefined) {
|
|
206
274
|
quota.weeklyPercent = secondaryPercent;
|
|
207
275
|
if (secondaryResetAt !== undefined) quota.weeklyResetAt = secondaryResetAt;
|
|
208
276
|
}
|
|
209
277
|
} else {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
278
|
+
if (primaryIsFiveHour && primaryPercent !== undefined) {
|
|
279
|
+
quota.fiveHourPercent = primaryPercent;
|
|
280
|
+
if (primaryResetAt !== undefined) quota.fiveHourResetAt = primaryResetAt;
|
|
281
|
+
}
|
|
282
|
+
if (secondaryIsFiveHour && secondaryPercent !== undefined) {
|
|
283
|
+
quota.fiveHourPercent ??= secondaryPercent;
|
|
284
|
+
if (quota.fiveHourResetAt === undefined && secondaryResetAt !== undefined) {
|
|
285
|
+
quota.fiveHourResetAt = secondaryResetAt;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const weeklyPercent = primaryIsFiveHour ? secondaryPercent : primaryPercent ?? secondaryPercent;
|
|
289
|
+
const weeklyResetAt = primaryIsFiveHour
|
|
290
|
+
? secondaryResetAt
|
|
291
|
+
: primaryPercent !== undefined ? primaryResetAt : secondaryResetAt;
|
|
214
292
|
if (weeklyPercent !== undefined) {
|
|
215
293
|
quota.weeklyPercent = weeklyPercent;
|
|
216
294
|
if (weeklyResetAt !== undefined) quota.weeklyResetAt = weeklyResetAt;
|
|
217
295
|
}
|
|
296
|
+
if (trackFiveHour
|
|
297
|
+
&& !primaryIsFiveHour
|
|
298
|
+
&& primaryPercent !== undefined
|
|
299
|
+
&& secondaryPercent !== undefined
|
|
300
|
+
&& primaryWindowMinutes === null
|
|
301
|
+
&& secondaryWindowMinutes === null) {
|
|
302
|
+
quota.fiveHourPercent = primaryPercent;
|
|
303
|
+
if (primaryResetAt !== undefined) quota.fiveHourResetAt = primaryResetAt;
|
|
304
|
+
quota.weeklyPercent = secondaryPercent;
|
|
305
|
+
if (secondaryResetAt !== undefined) quota.weeklyResetAt = secondaryResetAt;
|
|
306
|
+
}
|
|
218
307
|
}
|
|
219
308
|
|
|
220
309
|
if (tertiaryPercent !== undefined && quota.monthlyPercent === undefined) {
|
|
@@ -229,8 +318,9 @@ export function applyAccountQuotaFromUpstreamHeaders(
|
|
|
229
318
|
accountId: string,
|
|
230
319
|
headers: Headers,
|
|
231
320
|
writerGeneration = captureConfigGeneration(),
|
|
321
|
+
plan?: string | null,
|
|
232
322
|
): void {
|
|
233
|
-
const quota = parseUpstreamQuotaHeaders(headers);
|
|
323
|
+
const quota = parseUpstreamQuotaHeaders(headers, plan);
|
|
234
324
|
if (!quota) return;
|
|
235
325
|
setAccountQuotaFromParsed(accountId, quota, writerGeneration);
|
|
236
326
|
}
|
|
@@ -251,7 +341,9 @@ export function updateAccountQuota(
|
|
|
251
341
|
if (nextWeekly === undefined && nextMonthly === undefined && resetCredits === undefined) return;
|
|
252
342
|
|
|
253
343
|
const quota: StoredAccountQuota = {
|
|
344
|
+
...(existing?.fiveHourPercent !== undefined ? { fiveHourPercent: existing.fiveHourPercent } : {}),
|
|
254
345
|
...(existing?.weeklyPercent !== undefined ? { weeklyPercent: existing.weeklyPercent } : {}),
|
|
346
|
+
...(existing?.fiveHourResetAt !== undefined ? { fiveHourResetAt: existing.fiveHourResetAt } : {}),
|
|
255
347
|
...(existing?.monthlyPercent !== undefined ? { monthlyPercent: existing.monthlyPercent } : {}),
|
|
256
348
|
...(existing?.weeklyResetAt !== undefined ? { weeklyResetAt: existing.weeklyResetAt } : {}),
|
|
257
349
|
...(existing?.monthlyResetAt !== undefined ? { monthlyResetAt: existing.monthlyResetAt } : {}),
|
|
@@ -378,20 +470,43 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
378
470
|
const secondaryResetAt = normalizeResetAt(secondaryWindow?.reset_at);
|
|
379
471
|
const tertiaryResetAt = normalizeResetAt(tertiaryWindow?.reset_at);
|
|
380
472
|
const primaryIsMonthly = isExplicitMonthlyWindow(primaryWindow);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const weeklyPercent = primaryIsMonthly ? secondaryPercent : primaryPercent ?? secondaryPercent;
|
|
390
|
-
const weeklyResetAt = primaryIsMonthly
|
|
473
|
+
const primaryIsFiveHour = isExplicitFiveHourWindow(primaryWindow);
|
|
474
|
+
const secondaryIsFiveHour = isExplicitFiveHourWindow(secondaryWindow);
|
|
475
|
+
|
|
476
|
+
// Explicit durations win over positional assumptions. This matters because
|
|
477
|
+
// WHAM has shipped both a legacy primary=weekly shape and a dual-window
|
|
478
|
+
// primary=5h, secondary=weekly shape over time.
|
|
479
|
+
let weeklyPercent = primaryIsMonthly ? secondaryPercent : primaryPercent ?? secondaryPercent;
|
|
480
|
+
let weeklyResetAt = primaryIsMonthly
|
|
391
481
|
? secondaryResetAt
|
|
392
482
|
: primaryPercent !== undefined ? primaryResetAt : secondaryResetAt;
|
|
393
483
|
const monthlyPercent = primaryIsMonthly ? primaryPercent ?? tertiaryPercent : tertiaryPercent;
|
|
394
484
|
const monthlyResetAt = primaryIsMonthly && primaryPercent !== undefined ? primaryResetAt : tertiaryResetAt;
|
|
485
|
+
const trackFiveHour = !thirtyDayOnly
|
|
486
|
+
&& (isCodexFiveHourQuotaPlan(data.plan_type)
|
|
487
|
+
// If WHAM omitted plan_type, an explicit 5h duration is still
|
|
488
|
+
// unambiguous and should not be discarded from the display contract.
|
|
489
|
+
|| (data.plan_type == null && (primaryIsFiveHour || secondaryIsFiveHour)));
|
|
490
|
+
let fiveHourPercent = trackFiveHour && primaryIsFiveHour ? primaryPercent : undefined;
|
|
491
|
+
let fiveHourResetAt = trackFiveHour && primaryIsFiveHour ? primaryResetAt : undefined;
|
|
492
|
+
if (trackFiveHour && secondaryIsFiveHour && fiveHourPercent === undefined) {
|
|
493
|
+
fiveHourPercent = secondaryPercent;
|
|
494
|
+
fiveHourResetAt = secondaryResetAt;
|
|
495
|
+
}
|
|
496
|
+
// Older dual-window payloads omitted durations. For Plus/Team/Business,
|
|
497
|
+
// the primary+secondary pair is the legacy positional 5h+weekly shape.
|
|
498
|
+
if (trackFiveHour && !primaryIsMonthly && !primaryIsFiveHour
|
|
499
|
+
&& primaryPercent !== undefined && secondaryPercent !== undefined
|
|
500
|
+
&& windowDurationSeconds(primaryWindow) === undefined
|
|
501
|
+
&& windowDurationSeconds(secondaryWindow) === undefined) {
|
|
502
|
+
fiveHourPercent = primaryPercent;
|
|
503
|
+
fiveHourResetAt = primaryResetAt;
|
|
504
|
+
weeklyPercent = secondaryPercent;
|
|
505
|
+
weeklyResetAt = secondaryResetAt;
|
|
506
|
+
} else if (trackFiveHour && primaryIsFiveHour) {
|
|
507
|
+
weeklyPercent = secondaryPercent;
|
|
508
|
+
weeklyResetAt = secondaryResetAt;
|
|
509
|
+
}
|
|
395
510
|
if (thirtyDayOnly) {
|
|
396
511
|
if (monthlyPercent !== undefined) {
|
|
397
512
|
quota.monthlyPercent = monthlyPercent;
|
|
@@ -401,6 +516,10 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
401
516
|
quota.weeklyPercent = weeklyPercent;
|
|
402
517
|
if (weeklyResetAt !== undefined) quota.weeklyResetAt = weeklyResetAt;
|
|
403
518
|
}
|
|
519
|
+
if (trackFiveHour && fiveHourPercent !== undefined) {
|
|
520
|
+
quota.fiveHourPercent = fiveHourPercent;
|
|
521
|
+
if (fiveHourResetAt !== undefined) quota.fiveHourResetAt = fiveHourResetAt;
|
|
522
|
+
}
|
|
404
523
|
if (!thirtyDayOnly && monthlyPercent !== undefined) {
|
|
405
524
|
quota.monthlyPercent = monthlyPercent;
|
|
406
525
|
if (monthlyResetAt !== undefined) quota.monthlyResetAt = monthlyResetAt;
|