-
Notifications
You must be signed in to change notification settings - Fork 882
feat(gui): surface per-target quota state in the combo workspace #2454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,12 @@ export interface ComboTarget { | |
| clientKey?: string; | ||
| } | ||
|
|
||
| export type ComboQuotaState = "available" | "exhausted" | "unknown"; | ||
| export type ProviderQuotaStates = Readonly<Record<string, ComboQuotaState>>; | ||
|
|
||
| /** Matches the management endpoint's bounded last-good quota lifetime. */ | ||
| export const COMBO_QUOTA_MAX_AGE_MS = 30 * 60_000; | ||
|
|
||
|
Comment on lines
+52
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Find the server-side "last-good" quota lifetime constant that the comment claims this matches.
rg -n -i 'last-good|lastGood|quota.*(ttl|max.*age|lifetime)' --type=ts -g '!gui/**' -C5Repository: lidge-jun/opencodex Length of output: 157 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(gui|server|src|backend|api)/|provider|quota|combo-workspace-data' | head -200
printf '%s\n' '--- provider quota references ---'
rg -n -i 'provider[-_ ]quotas|providerQuotas|quotaTimestampIsFresh|COMBO_QUOTA_MAX_AGE_MS|last[-_ ]good|quota.*(ttl|max.?age|lifetime)|max.?age.*quota' . -g '!gui/**' -g '!node_modules/**' -C3 || trueRepository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- quota implementation outline ---'
ast-grep outline src/providers/quota.ts
printf '%s\n' '--- management route outline ---'
ast-grep outline src/server/management/provider-routes.ts
printf '%s\n' '--- GUI data outline ---'
ast-grep outline gui/src/combo-workspace-data.ts
printf '%s\n' '--- backend quota age-related definitions and uses ---'
rg -n -i 'MAX_AGE|MAXAGE|AGE_MS|last.good|lastGood|updatedAt|quota.*(30|minute|window)|window.*quota' src/providers/quota.ts src/server/management/provider-routes.ts src/codex/quota.ts gui/src/combo-workspace-data.ts tests/provider-quota.test.ts tests/combo-workspace-data.test.ts -C2Repository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- shared backend constant ---'
rg -n 'CODEX_CAPACITY_MAX_QUOTA_AGE_MS|LAST_GOOD_MAX_AGE_MS' src gui tests -C4
printf '%s\n' '--- quota cache and expiry path ---'
sed -n '1,75p' src/providers/quota.ts
sed -n '2208,2270p' src/providers/quota.ts
printf '%s\n' '--- capacity module imports and definition ---'
rg -n 'CODEX_CAPACITY_MAX_QUOTA_AGE_MS' src -g '*.ts' -C8
printf '%s\n' '--- relevant GUI quota logic ---'
sed -n '225,390p' gui/src/combo-workspace-data.tsRepository: lidge-jun/opencodex Length of output: 30523 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- management quota response construction ---'
sed -n '360,430p' src/server/management/provider-routes.ts
printf '%s\n' '--- API client quota response types/usages ---'
rg -n 'ProviderQuotaResponse|provider-quotas|generatedAt|reports' gui/src src -g '*.ts' -g '*.tsx' -C3 | head -180
printf '%s\n' '--- project package and TypeScript boundaries ---'
git ls-files | rg '(^|/)(package.json|tsconfig[^/]*\.json|vite\.config\.[^/]+)$' | xargs -r -n1 sh -c 'echo --- $0; sed -n "1,180p" "$0"'Repository: lidge-jun/opencodex Length of output: 26535 🏁 Script executed: #!/bin/bash
python3 - <<'PY'
MAX_AGE_MS = 30 * 60_000
now = 1_800_000_000_000
updated_at = now - MAX_AGE_MS
cutoff = now - MAX_AGE_MS
backend_keeps_last_good = updated_at >= cutoff # src/providers/quota.ts:2258 drops only `< cutoff`
gui_marks_fresh = now - updated_at < MAX_AGE_MS # gui/src/combo-workspace-data.ts:233
print({
"max_age_ms": MAX_AGE_MS,
"age_ms": now - updated_at,
"backend_keeps_last_good_at_exact_bound": backend_keeps_last_good,
"gui_marks_exact_bound_fresh": gui_marks_fresh,
})
PYRepository: lidge-jun/opencodex Length of output: 286 Align the quota freshness boundary. The backend uses the same 30-minute bound, but it retains rows exactly 30 minutes old while 🤖 Prompt for AI Agents |
||
| let comboTargetKeySeq = 0; | ||
|
|
||
| export function newComboTarget(partial: Partial<ComboTarget> = {}): ComboTarget { | ||
|
|
@@ -90,7 +96,7 @@ export interface ComboSections { | |
| export interface ComboAttentionItem { | ||
| id: string; | ||
| model: string; | ||
| reason: "few-targets" | "empty-targets" | "catalog-omitted"; | ||
| reason: "few-targets" | "empty-targets" | "catalog-omitted" | "all-targets-exhausted"; | ||
| } | ||
|
|
||
| export const COMBO_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/; | ||
|
|
@@ -212,9 +218,197 @@ export function filterCombos(items: ComboItem[], query: string): ComboItem[] { | |
| }); | ||
| } | ||
|
|
||
| function recordFromUnknown(value: unknown): Record<string, unknown> | null { | ||
| return value && typeof value === "object" && !Array.isArray(value) | ||
| ? value as Record<string, unknown> | ||
| : null; | ||
| } | ||
|
|
||
| function finiteNumber(value: unknown): number | null { | ||
| return typeof value === "number" && Number.isFinite(value) ? value : null; | ||
| } | ||
|
|
||
| function quotaTimestampIsFresh(value: unknown, now: number): boolean { | ||
| const timestamp = finiteNumber(value); | ||
| return timestamp !== null && now - timestamp < COMBO_QUOTA_MAX_AGE_MS; | ||
| } | ||
|
|
||
| function nonNegativeInteger(value: unknown): number | null { | ||
| const number = finiteNumber(value); | ||
| return number !== null && Number.isInteger(number) && number >= 0 ? number : null; | ||
| } | ||
|
|
||
| function aggregateWindowIsComplete(value: unknown, now: number): boolean { | ||
| const window = recordFromUnknown(value); | ||
| const usedPercent = finiteNumber(window?.usedPercent); | ||
| return !!window | ||
| && usedPercent !== null | ||
| && usedPercent >= 0 | ||
| && nonNegativeInteger(window.includedAccounts) !== null | ||
| && (nonNegativeInteger(window.includedAccounts) ?? 0) > 0 | ||
| && nonNegativeInteger(window.excludedAccounts) === 0 | ||
| && window.incomplete === false | ||
| && quotaTimestampIsFresh(window.updatedAt, now); | ||
| } | ||
|
|
||
| function aggregateEvidenceIsComplete(value: unknown, now: number): boolean { | ||
| const aggregation = recordFromUnknown(value); | ||
| if ( | ||
| !aggregation | ||
| || aggregation.kind !== "capacity-weighted-v1" | ||
| || aggregation.scope !== "routable-known" | ||
| || aggregation.presentation !== "aggregate" | ||
| || aggregation.incomplete !== false | ||
| ) return false; | ||
|
|
||
| for (const key of [ | ||
| "includedAccounts", | ||
| "excludedAccounts", | ||
| "unknownPlanAccounts", | ||
| "missingQuotaAccounts", | ||
| "pausedAccounts", | ||
| "reauthAccounts", | ||
| "staleQuotaAccounts", | ||
| "partialWindowAccounts", | ||
| ] as const) { | ||
| if (nonNegativeInteger(aggregation[key]) === null) return false; | ||
| } | ||
| if ((nonNegativeInteger(aggregation.includedAccounts) ?? 0) === 0) return false; | ||
| for (const key of [ | ||
| "excludedAccounts", | ||
| "unknownPlanAccounts", | ||
| "missingQuotaAccounts", | ||
| "pausedAccounts", | ||
| "reauthAccounts", | ||
| "staleQuotaAccounts", | ||
| "partialWindowAccounts", | ||
| ] as const) { | ||
| if (aggregation[key] !== 0) return false; | ||
| } | ||
|
|
||
| let hasWindow = false; | ||
| for (const key of ["fiveHour", "weekly", "monthly"] as const) { | ||
| if (!Object.hasOwn(aggregation, key)) continue; | ||
| if (!aggregateWindowIsComplete(aggregation[key], now)) return false; | ||
| hasWindow = true; | ||
| } | ||
| if (Object.hasOwn(aggregation, "customWindows")) { | ||
| if (!Array.isArray(aggregation.customWindows)) return false; | ||
| for (const value of aggregation.customWindows) { | ||
| const custom = recordFromUnknown(value); | ||
| if (!custom || typeof custom.label !== "string" || !custom.label.trim()) return false; | ||
| if (!aggregateWindowIsComplete(custom, now)) return false; | ||
| hasWindow = true; | ||
| } | ||
| } | ||
| return hasWindow; | ||
| } | ||
|
|
||
| function quotaStateFromReport(raw: Record<string, unknown>, now: number): ComboQuotaState { | ||
| if (!quotaTimestampIsFresh(raw.updatedAt, now)) return "unknown"; | ||
| const quota = recordFromUnknown(raw.quota); | ||
| if (!quota || !quotaTimestampIsFresh(quota.updatedAt, now)) return "unknown"; | ||
| if (raw.aggregation !== undefined && !aggregateEvidenceIsComplete(raw.aggregation, now)) return "unknown"; | ||
|
|
||
| let hasEvidence = false; | ||
| let exhausted = false; | ||
| for (const key of ["fiveHourPercent", "weeklyPercent", "monthlyPercent"] as const) { | ||
| if (!Object.hasOwn(quota, key)) continue; | ||
| const percent = finiteNumber(quota[key]); | ||
| if (percent === null || percent < 0) return "unknown"; | ||
| hasEvidence = true; | ||
| if (percent >= 100) exhausted = true; | ||
| } | ||
| for (const key of ["fiveHourResetAt", "weeklyResetAt", "monthlyResetAt"] as const) { | ||
| if (Object.hasOwn(quota, key) && finiteNumber(quota[key]) === null) return "unknown"; | ||
| } | ||
|
|
||
| if (Object.hasOwn(quota, "customWindows")) { | ||
| if (!Array.isArray(quota.customWindows)) return "unknown"; | ||
| for (const value of quota.customWindows) { | ||
| const window = recordFromUnknown(value); | ||
| const percent = finiteNumber(window?.percent); | ||
| if (!window || typeof window.label !== "string" || !window.label.trim() || percent === null || percent < 0) { | ||
| return "unknown"; | ||
| } | ||
| if (Object.hasOwn(window, "resetAt") && finiteNumber(window.resetAt) === null) return "unknown"; | ||
| hasEvidence = true; | ||
| if (percent >= 100) exhausted = true; | ||
| } | ||
| } | ||
|
|
||
| if (Object.hasOwn(quota, "creditsUsd")) { | ||
| const credits = recordFromUnknown(quota.creditsUsd); | ||
| if (!credits) return "unknown"; | ||
| const used = finiteNumber(credits.used); | ||
| const limit = finiteNumber(credits.limit); | ||
| const remaining = finiteNumber(credits.remaining); | ||
| const percent = finiteNumber(credits.percent); | ||
| if (used === null || used < 0 || limit === null || limit < 0 || remaining === null || percent === null || percent < 0) { | ||
| return "unknown"; | ||
| } | ||
| if (credits.unlimited !== undefined && typeof credits.unlimited !== "boolean") return "unknown"; | ||
| if (Object.hasOwn(credits, "expiresAt") && finiteNumber(credits.expiresAt) === null) return "unknown"; | ||
| hasEvidence = true; | ||
| if (credits.unlimited !== true && remaining <= 0) exhausted = true; | ||
| } | ||
|
|
||
| if (!hasEvidence) return "unknown"; | ||
| return exhausted ? "exhausted" : "available"; | ||
| } | ||
|
|
||
| /** Fail-unknown parser for the live `/api/provider-quotas` report array. */ | ||
| export function providerQuotaStatesFromReports( | ||
| reports: unknown, | ||
| now = Date.now(), | ||
| ): Record<string, ComboQuotaState> { | ||
| if (!Array.isArray(reports)) return {}; | ||
| const states: Record<string, ComboQuotaState> = {}; | ||
| for (const value of reports) { | ||
| const report = recordFromUnknown(value); | ||
| const provider = typeof report?.provider === "string" ? report.provider.trim() : ""; | ||
| if (!report || !provider) continue; | ||
| const next = quotaStateFromReport(report, now); | ||
| states[provider] = Object.hasOwn(states, provider) && states[provider] !== next | ||
| ? "unknown" | ||
| : next; | ||
| } | ||
| return states; | ||
| } | ||
|
|
||
| /** | ||
| * A combo is exhausted only when it has at least one configured, enabled, | ||
| * complete target and every such target has known exhausted quota evidence. | ||
| */ | ||
| export function comboQuotaState( | ||
| targets: readonly ComboTarget[], | ||
| providerQuotaStates: ProviderQuotaStates, | ||
| providers: Readonly<Record<string, { disabled?: boolean }>>, | ||
| ): ComboQuotaState { | ||
| const usableProviders = targets.flatMap((target) => { | ||
| const provider = target.provider.trim(); | ||
| if (!provider || !target.model.trim()) return []; | ||
| if (!Object.hasOwn(providers, provider) || providers[provider]?.disabled === true) return []; | ||
| return [provider]; | ||
| }); | ||
| if (usableProviders.length === 0) return "unknown"; | ||
|
|
||
| let sawUnknown = false; | ||
| for (const provider of usableProviders) { | ||
| const state = providerQuotaStates[provider] ?? "unknown"; | ||
| if (state === "available") return "available"; | ||
| if (state === "unknown") sawUnknown = true; | ||
| } | ||
| return sawUnknown ? "unknown" : "exhausted"; | ||
| } | ||
|
|
||
| export function buildComboAttention( | ||
| items: ComboItem[], | ||
| options: { cataloguedComboIds?: ReadonlySet<string> } = {}, | ||
| options: { | ||
| cataloguedComboIds?: ReadonlySet<string>; | ||
| providerQuotaStates?: ProviderQuotaStates; | ||
| providers?: Readonly<Record<string, { disabled?: boolean }>>; | ||
| } = {}, | ||
| ): ComboAttentionItem[] { | ||
| const out: ComboAttentionItem[] = []; | ||
| const catalogued = options.cataloguedComboIds; | ||
|
|
@@ -230,6 +424,13 @@ export function buildComboAttention( | |
| if (catalogued && item.targets.length > 0 && !catalogued.has(item.id)) { | ||
| out.push({ id: item.id, model: item.model, reason: "catalog-omitted" }); | ||
| } | ||
| if ( | ||
| options.providerQuotaStates | ||
| && options.providers | ||
| && comboQuotaState(item.targets, options.providerQuotaStates, options.providers) === "exhausted" | ||
| ) { | ||
| out.push({ id: item.id, model: item.model, reason: "all-targets-exhausted" }); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use “usable targets” consistently across the combo guides.
All eight quota paragraphs describe the action gate using “enabled targets”, but the implementation evaluates usable providers and excludes invalid targets and disabled providers.
docs-site/src/content/docs/guides/combos.md#L271-L274: replace “every enabled target” with “every usable target”.docs-site/src/content/docs/fr/guides/combos.md#L262-L265: replacechaque cible activéewith the French equivalent of “every usable target”.docs-site/src/content/docs/ja/guides/combos.md#L170-L172: replace有効な全ターゲットwith the Japanese equivalent of “all usable targets”.docs-site/src/content/docs/ko/guides/combos.md#L170-L172: replace활성화된 모든 대상with the Korean equivalent of “all usable targets”.docs-site/src/content/docs/ru/guides/combos.md#L216-L219: replaceдля всех включённых целейwith the Russian equivalent of “all usable targets”.docs-site/src/content/docs/tr/guides/combos.md#L289-L292: replaceetkin hedeflerin tamamıwith the Turkish equivalent of “all usable targets”.docs-site/src/content/docs/zh-cn/guides/combos.md#L200-L202: replace所有已启用目标with the Simplified Chinese equivalent of “all usable targets”.docs-site/src/content/docs/zh-tw/guides/combos.md#L208-L210: replace所有啟用目標with the Traditional Chinese equivalent of “all usable targets”.Based on
gui/src/combo-workspace-data.ts, Lines 383-403, and the docs-site requirement to keep translated pages synchronized with actual GUI behavior.📍 Affects 8 files
docs-site/src/content/docs/guides/combos.md#L271-L274(this comment)docs-site/src/content/docs/fr/guides/combos.md#L262-L265docs-site/src/content/docs/ja/guides/combos.md#L170-L172docs-site/src/content/docs/ko/guides/combos.md#L170-L172docs-site/src/content/docs/ru/guides/combos.md#L216-L219docs-site/src/content/docs/tr/guides/combos.md#L289-L292docs-site/src/content/docs/zh-cn/guides/combos.md#L200-L202docs-site/src/content/docs/zh-tw/guides/combos.md#L208-L210🤖 Prompt for AI Agents
Source: Path instructions