@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.0
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-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DO8liQVL.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
- package/gui/dist/assets/index-DX0RGj3e.js +0 -112
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-level helpers shared by every provider quota reader: cache lifetimes, number and
|
|
3
|
+
* timestamp normalisation, and the bounded JSON reader.
|
|
4
|
+
*
|
|
5
|
+
* These lived inside `quota.ts` as module-private functions. A second quota module cannot
|
|
6
|
+
* import them from there without creating a cycle, and copying a TTL constant into a
|
|
7
|
+
* second file is how two copies of the same number drift apart. Everything here is pure or
|
|
8
|
+
* depends only on the bounded-body reader.
|
|
9
|
+
*/
|
|
10
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
11
|
+
|
|
12
|
+
/** Provider-level quota response cache lifetime (the dashboard/display path). */
|
|
13
|
+
export const CACHE_TTL_MS = 5 * 60_000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-account quota cache lifetime.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately longer than the provider-level TTL: this path multiplies by account count,
|
|
19
|
+
* and at least one upstream (Anthropic) rate-limits its usage endpoint under repeated
|
|
20
|
+
* probing.
|
|
21
|
+
*/
|
|
22
|
+
export const ACCOUNT_QUOTA_TTL_MS = 10 * 60_000;
|
|
23
|
+
|
|
24
|
+
export const REQUEST_TIMEOUT_MS = 8_000;
|
|
25
|
+
|
|
26
|
+
export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024;
|
|
27
|
+
|
|
28
|
+
export const QUOTA_JSON_READ_FAILURE = Symbol("quota-json-read-failure");
|
|
29
|
+
|
|
30
|
+
/** Unix 0 / negative values are sentinels, not reset clocks (Command Code fiveHour.resetAt: 0). */
|
|
31
|
+
export function epochMillis(value: number): number | undefined {
|
|
32
|
+
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
33
|
+
const milliseconds = value > 10_000_000_000 ? value : value * 1000;
|
|
34
|
+
// A finite number is not necessarily a representable date. ECMAScript caps time values at
|
|
35
|
+
// ±8.64e15 ms, and `Intl.DateTimeFormat.format()` throws a RangeError past that instead of
|
|
36
|
+
// rendering something wrong. A provider that reports a bogus expiry must not become a
|
|
37
|
+
// rendering fault in every consumer that formats it.
|
|
38
|
+
return Number.isFinite(new Date(milliseconds).getTime()) ? milliseconds : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeResetAt(value: unknown): number | undefined {
|
|
42
|
+
if (typeof value === "number" && Number.isFinite(value)) return epochMillis(value);
|
|
43
|
+
if (typeof value === "string" && value.trim()) {
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
// Cursor Connect RPC returns billingCycleEnd as a unix-ms decimal string ("1771077734000").
|
|
46
|
+
// Date.parse treats that as invalid; numeric epoch strings must be handled explicitly.
|
|
47
|
+
if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
|
|
48
|
+
const numeric = Number(trimmed);
|
|
49
|
+
return epochMillis(numeric);
|
|
50
|
+
}
|
|
51
|
+
const parsed = Date.parse(trimmed);
|
|
52
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function toFiniteNumber(value: unknown): number | undefined {
|
|
58
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
59
|
+
if (typeof value === "string" && value.trim()) {
|
|
60
|
+
const parsed = Number(value);
|
|
61
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function normalizePercent(value: unknown): number | undefined {
|
|
67
|
+
const numeric = toFiniteNumber(value);
|
|
68
|
+
return numeric === undefined ? undefined : Math.max(0, Math.min(100, numeric));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
72
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function readQuotaJson(
|
|
76
|
+
response: Response,
|
|
77
|
+
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
78
|
+
): Promise<unknown | typeof QUOTA_JSON_READ_FAILURE> {
|
|
79
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
80
|
+
if (Number.isFinite(declaredLength) && declaredLength > QUOTA_RESPONSE_MAX_BYTES) {
|
|
81
|
+
try {
|
|
82
|
+
void response.body?.cancel(
|
|
83
|
+
new DOMException("Provider quota response is too large", "QuotaExceededError"),
|
|
84
|
+
).catch(() => undefined);
|
|
85
|
+
} catch {
|
|
86
|
+
// Best-effort cancellation only.
|
|
87
|
+
}
|
|
88
|
+
return QUOTA_JSON_READ_FAILURE;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const bounded = await readBoundedResponseBody(response, {
|
|
93
|
+
maxBytes: QUOTA_RESPONSE_MAX_BYTES,
|
|
94
|
+
totalTimeoutMs: timeoutMs,
|
|
95
|
+
inactivityTimeoutMs: timeoutMs,
|
|
96
|
+
});
|
|
97
|
+
if (bounded.oversized || bounded.truncated || !bounded.displaySafe) return QUOTA_JSON_READ_FAILURE;
|
|
98
|
+
return JSON.parse(bounded.text) as unknown;
|
|
99
|
+
} catch {
|
|
100
|
+
return QUOTA_JSON_READ_FAILURE;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/providers/quota.ts
CHANGED
|
@@ -23,21 +23,52 @@ import {
|
|
|
23
23
|
sweepExpiredOnWrite,
|
|
24
24
|
type GenerationContext,
|
|
25
25
|
} from "../lib/state-store-sweeper";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
ACCOUNT_QUOTA_TTL_MS,
|
|
28
|
+
asRecord,
|
|
29
|
+
CACHE_TTL_MS,
|
|
30
|
+
normalizePercent,
|
|
31
|
+
normalizeResetAt,
|
|
32
|
+
QUOTA_JSON_READ_FAILURE,
|
|
33
|
+
readQuotaJson,
|
|
34
|
+
REQUEST_TIMEOUT_MS,
|
|
35
|
+
toFiniteNumber,
|
|
36
|
+
} from "./quota-wire";
|
|
37
|
+
import {
|
|
38
|
+
clearCachedProviderQuotas,
|
|
39
|
+
replaceCachedProviderQuotas,
|
|
40
|
+
} from "./quota-routing-cache";
|
|
27
41
|
import {
|
|
28
42
|
aggregateCodexPoolCapacity,
|
|
29
43
|
CODEX_CAPACITY_MAX_QUOTA_AGE_MS,
|
|
30
44
|
type CodexCapacityAggregation,
|
|
31
45
|
type CodexCapacityQuota,
|
|
32
46
|
} from "./codex-capacity";
|
|
47
|
+
import type {
|
|
48
|
+
ProviderQuota,
|
|
49
|
+
ProviderQuotaCreditsUsd,
|
|
50
|
+
ProviderQuotaWindow,
|
|
51
|
+
} from "./quota-types";
|
|
52
|
+
import {
|
|
53
|
+
clearKiroAccountUsageState,
|
|
54
|
+
commitKiroAccountUsageState,
|
|
55
|
+
fetchKiroUsageSnapshot,
|
|
56
|
+
type KiroUsageSnapshot,
|
|
57
|
+
kiroUsageContextForAccount,
|
|
58
|
+
reconcileKiroAccountUsageState,
|
|
59
|
+
} from "./kiro-usage";
|
|
60
|
+
import {
|
|
61
|
+
cancelPendingAccountQuotaPersist,
|
|
62
|
+
readPersistedAccountQuotas,
|
|
63
|
+
schedulePersistAccountQuotas,
|
|
64
|
+
} from "./account-quota-disk";
|
|
65
|
+
|
|
66
|
+
export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types";
|
|
33
67
|
|
|
34
68
|
/** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
|
|
35
69
|
const ACCOUNT_TOKEN_SKEW_MS = 60_000;
|
|
36
|
-
|
|
37
|
-
const CACHE_TTL_MS = 5 * 60_000;
|
|
38
|
-
const REQUEST_TIMEOUT_MS = 8_000;
|
|
39
70
|
/** Successful provider quota payloads are small; reject oversized or stalled JSON before parsing. */
|
|
40
|
-
export
|
|
71
|
+
export { QUOTA_RESPONSE_MAX_BYTES } from "./quota-wire";
|
|
41
72
|
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
42
73
|
const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
|
|
43
74
|
const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
|
|
@@ -73,34 +104,23 @@ export function setProviderQuotaBeforePublishForTests(
|
|
|
73
104
|
providerQuotaBeforePublishForTests = hook;
|
|
74
105
|
}
|
|
75
106
|
const TERMINAL_QUOTA_FAILURE = Symbol("terminal-quota-failure");
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
export interface ProviderQuota {
|
|
94
|
-
fiveHourPercent?: number;
|
|
95
|
-
fiveHourResetAt?: number;
|
|
96
|
-
weeklyPercent?: number;
|
|
97
|
-
weeklyResetAt?: number;
|
|
98
|
-
monthlyPercent?: number;
|
|
99
|
-
monthlyResetAt?: number;
|
|
100
|
-
customWindows?: ProviderQuotaWindow[];
|
|
101
|
-
creditsUsd?: ProviderQuotaCreditsUsd;
|
|
102
|
-
updatedAt: number;
|
|
103
|
-
}
|
|
107
|
+
/**
|
|
108
|
+
* The probe succeeded and the upstream authoritatively reported NO model-quota windows.
|
|
109
|
+
*
|
|
110
|
+
* Distinct from `null`, which means "this probe told us nothing" and deliberately preserves
|
|
111
|
+
* the last-good row for up to 30 minutes. Collapsing the two would let a stale report outlive
|
|
112
|
+
* the authoritative answer that replaced it: a GLM plan whose payload carries only MCP
|
|
113
|
+
* `TIME_LIMIT` rows has no model windows, and the dashboard and quota-aware routing must stop
|
|
114
|
+
* showing the previous token windows rather than keep them for another half hour.
|
|
115
|
+
*
|
|
116
|
+
* Suppression is shared with `TERMINAL_QUOTA_FAILURE`; only the reason differs.
|
|
117
|
+
*/
|
|
118
|
+
const AUTHORITATIVE_EMPTY_QUOTA = Symbol("authoritative-empty-quota");
|
|
119
|
+
type ProviderQuotaProbeResult =
|
|
120
|
+
| ProviderQuotaReport
|
|
121
|
+
| null
|
|
122
|
+
| typeof TERMINAL_QUOTA_FAILURE
|
|
123
|
+
| typeof AUTHORITATIVE_EMPTY_QUOTA;
|
|
104
124
|
|
|
105
125
|
export interface ProviderQuotaReport {
|
|
106
126
|
provider: string;
|
|
@@ -125,6 +145,7 @@ let invalidationEpoch = 0;
|
|
|
125
145
|
/** Invalidate the report cache (e.g. after switching a provider's active account). */
|
|
126
146
|
export function clearProviderQuotaCache(): void {
|
|
127
147
|
cache = null;
|
|
148
|
+
clearCachedProviderQuotas();
|
|
128
149
|
invalidationEpoch += 1;
|
|
129
150
|
}
|
|
130
151
|
|
|
@@ -260,77 +281,6 @@ function providerLabel(providerId: string): string {
|
|
|
260
281
|
return getProviderRegistryEntry(providerId)?.label ?? providerId;
|
|
261
282
|
}
|
|
262
283
|
|
|
263
|
-
function normalizeResetAt(value: unknown): number | undefined {
|
|
264
|
-
if (typeof value === "number" && Number.isFinite(value)) return epochMillis(value);
|
|
265
|
-
if (typeof value === "string" && value.trim()) {
|
|
266
|
-
const trimmed = value.trim();
|
|
267
|
-
// Cursor Connect RPC returns billingCycleEnd as a unix-ms decimal string ("1771077734000").
|
|
268
|
-
// Date.parse treats that as invalid; numeric epoch strings must be handled explicitly.
|
|
269
|
-
if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
|
|
270
|
-
const numeric = Number(trimmed);
|
|
271
|
-
return epochMillis(numeric);
|
|
272
|
-
}
|
|
273
|
-
const parsed = Date.parse(trimmed);
|
|
274
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
275
|
-
}
|
|
276
|
-
return undefined;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Unix 0 / negative values are sentinels, not reset clocks (Command Code fiveHour.resetAt: 0). */
|
|
280
|
-
function epochMillis(value: number): number | undefined {
|
|
281
|
-
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
282
|
-
return value > 10_000_000_000 ? value : value * 1000;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function toFiniteNumber(value: unknown): number | undefined {
|
|
286
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
287
|
-
if (typeof value === "string" && value.trim()) {
|
|
288
|
-
const parsed = Number(value);
|
|
289
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
290
|
-
}
|
|
291
|
-
return undefined;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function normalizePercent(value: unknown): number | undefined {
|
|
295
|
-
const numeric = toFiniteNumber(value);
|
|
296
|
-
return numeric === undefined ? undefined : Math.max(0, Math.min(100, numeric));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
300
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const QUOTA_JSON_READ_FAILURE = Symbol("quota-json-read-failure");
|
|
304
|
-
|
|
305
|
-
async function readQuotaJson(
|
|
306
|
-
response: Response,
|
|
307
|
-
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
308
|
-
): Promise<unknown | typeof QUOTA_JSON_READ_FAILURE> {
|
|
309
|
-
const declaredLength = Number(response.headers.get("content-length"));
|
|
310
|
-
if (Number.isFinite(declaredLength) && declaredLength > QUOTA_RESPONSE_MAX_BYTES) {
|
|
311
|
-
try {
|
|
312
|
-
void response.body?.cancel(
|
|
313
|
-
new DOMException("Provider quota response is too large", "QuotaExceededError"),
|
|
314
|
-
).catch(() => undefined);
|
|
315
|
-
} catch {
|
|
316
|
-
// Best-effort cancellation only.
|
|
317
|
-
}
|
|
318
|
-
return QUOTA_JSON_READ_FAILURE;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
const bounded = await readBoundedResponseBody(response, {
|
|
323
|
-
maxBytes: QUOTA_RESPONSE_MAX_BYTES,
|
|
324
|
-
totalTimeoutMs: timeoutMs,
|
|
325
|
-
inactivityTimeoutMs: timeoutMs,
|
|
326
|
-
});
|
|
327
|
-
if (bounded.oversized || bounded.truncated || !bounded.displaySafe) return QUOTA_JSON_READ_FAILURE;
|
|
328
|
-
return JSON.parse(bounded.text) as unknown;
|
|
329
|
-
} catch {
|
|
330
|
-
return QUOTA_JSON_READ_FAILURE;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
284
|
/** Test-only access to the quota reader's deadline and cancellation contract. */
|
|
335
285
|
export async function readProviderQuotaJsonForTests(response: Response, timeoutMs: number): Promise<unknown> {
|
|
336
286
|
const result = await readQuotaJson(response, timeoutMs);
|
|
@@ -702,10 +652,19 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
|
|
|
702
652
|
* same rows `CREDIT_LIMIT`) and `TIME_LIMIT` rows. `TOKENS_LIMIT`/`CREDIT_LIMIT`
|
|
703
653
|
* rows carry the window length as `unit`/`number`: unit 3 is hours (number 5 →
|
|
704
654
|
* the rolling five-hour window), unit 6 is weeks (number 1 → the weekly
|
|
705
|
-
* window). `
|
|
706
|
-
* Reader / Zread). Every row's `percentage` is the consumed share (falling
|
|
655
|
+
* window). Every row's `percentage` is the consumed share (falling
|
|
707
656
|
* back to `currentValue`/`usage` when absent) and `nextResetTime` (unix ms)
|
|
708
657
|
* the window reset.
|
|
658
|
+
*
|
|
659
|
+
* `TIME_LIMIT` rows are deliberately ignored (issue #1168). They are the shared
|
|
660
|
+
* monthly MCP *call* allowance for Web Search / Web Reader / Zread — not a
|
|
661
|
+
* model-token budget — and `ProviderQuota.monthlyPercent` is consumed as a
|
|
662
|
+
* model-capacity signal: `headroomOf()` in `src/oauth/account-quota-rank.ts`
|
|
663
|
+
* takes the MAX across every window, so a user who spent their MCP search
|
|
664
|
+
* allowance would be ranked as having no model capacity left, and the dashboard
|
|
665
|
+
* would draw a full monthly bar for a plan whose model tokens are untouched.
|
|
666
|
+
* A payload carrying only `TIME_LIMIT` rows therefore reports no quota at all,
|
|
667
|
+
* which is the honest answer rather than a fabricated one.
|
|
709
668
|
*/
|
|
710
669
|
export function parseZaiQuotaLimits(data: Record<string, unknown> | null): ProviderQuota | null {
|
|
711
670
|
const limits = Array.isArray(data?.limits) ? data.limits as unknown[] : null;
|
|
@@ -715,6 +674,9 @@ export function parseZaiQuotaLimits(data: Record<string, unknown> | null): Provi
|
|
|
715
674
|
for (const raw of limits) {
|
|
716
675
|
const row = asRecord(raw);
|
|
717
676
|
if (!row) continue;
|
|
677
|
+
// Gate on row type before deriving a percentage: an MCP row must not even
|
|
678
|
+
// contribute a parsed value to a model-quota report.
|
|
679
|
+
if (row.type !== "TOKENS_LIMIT" && row.type !== "CREDIT_LIMIT") continue;
|
|
718
680
|
const resetAt = normalizeResetAt(row.nextResetTime);
|
|
719
681
|
let percent = normalizePercent(row.percentage);
|
|
720
682
|
if (percent === undefined) {
|
|
@@ -725,21 +687,15 @@ export function parseZaiQuotaLimits(data: Record<string, unknown> | null): Provi
|
|
|
725
687
|
}
|
|
726
688
|
}
|
|
727
689
|
if (percent === undefined) continue;
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
|
|
738
|
-
windows += 1;
|
|
739
|
-
}
|
|
740
|
-
} else if (row.type === "TIME_LIMIT") {
|
|
741
|
-
quota.monthlyPercent = percent;
|
|
742
|
-
if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
|
|
690
|
+
const unit = toFiniteNumber(row.unit);
|
|
691
|
+
const number = toFiniteNumber(row.number);
|
|
692
|
+
if (unit === 3 && number === 5) {
|
|
693
|
+
quota.fiveHourPercent = percent;
|
|
694
|
+
if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
|
|
695
|
+
windows += 1;
|
|
696
|
+
} else if (unit === 6 && number === 1) {
|
|
697
|
+
quota.weeklyPercent = percent;
|
|
698
|
+
if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
|
|
743
699
|
windows += 1;
|
|
744
700
|
}
|
|
745
701
|
}
|
|
@@ -781,9 +737,16 @@ function parseZaiQuotaLegacyFields(data: Record<string, unknown> | null): Provid
|
|
|
781
737
|
|
|
782
738
|
/**
|
|
783
739
|
* Fetches the Z.AI GLM Coding Plan quota — on whichever region the provider
|
|
784
|
-
* points at (api.z.ai or open.bigmodel.cn).
|
|
785
|
-
* a Bearer token per Z.AI's API reference. The `limits` array shape is
|
|
740
|
+
* points at (api.z.ai or open.bigmodel.cn). The `limits` array shape is
|
|
786
741
|
* preferred; older field-name payloads fall back to the legacy parser.
|
|
742
|
+
*
|
|
743
|
+
* Authentication differs by host (issue #1168). `api.z.ai` takes the API key as
|
|
744
|
+
* a Bearer token per Z.AI's API reference; `open.bigmodel.cn` expects the key
|
|
745
|
+
* directly in `Authorization` with no scheme prefix and answers a Bearer header
|
|
746
|
+
* with an auth error, which is why BigModel Coding Plan quota never rendered.
|
|
747
|
+
* The host is already canonicalized by `isCanonicalZaiBaseUrl` above and
|
|
748
|
+
* `redirect: "error"` stays set, so the bare key cannot travel to a lookalike
|
|
749
|
+
* host or follow a redirect off-origin.
|
|
787
750
|
*/
|
|
788
751
|
async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
789
752
|
if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null;
|
|
@@ -793,8 +756,9 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
|
|
|
793
756
|
const monitorHost = normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
|
|
794
757
|
? ZAI_BASE_URL
|
|
795
758
|
: ZAI_CN_BASE_URL;
|
|
759
|
+
const authorization = monitorHost === ZAI_CN_BASE_URL ? apiKey : `Bearer ${apiKey}`;
|
|
796
760
|
const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
|
|
797
|
-
headers: { Accept: "application/json", Authorization:
|
|
761
|
+
headers: { Accept: "application/json", Authorization: authorization },
|
|
798
762
|
redirect: "error",
|
|
799
763
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
800
764
|
});
|
|
@@ -806,10 +770,16 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
|
|
|
806
770
|
const body = asRecord(await readQuotaJson(response));
|
|
807
771
|
if (!body || body.success === false) return null;
|
|
808
772
|
const data = asRecord(body.data) ?? body;
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
773
|
+
if (Array.isArray(data?.limits)) {
|
|
774
|
+
const quota = parseZaiQuotaLimits(data);
|
|
775
|
+
// A well-formed `limits[]` we fully understood is authoritative even when it yields no
|
|
776
|
+
// model window — for example a plan reporting only the monthly MCP `TIME_LIMIT` row.
|
|
777
|
+
// Returning `null` here would preserve the previous token windows for up to 30 minutes
|
|
778
|
+
// and keep quota-aware routing acting on a report the provider has already superseded.
|
|
779
|
+
return quota ? report(provider, "zai:quota-limit", quota) : AUTHORITATIVE_EMPTY_QUOTA;
|
|
780
|
+
}
|
|
781
|
+
const legacy = parseZaiQuotaLegacyFields(data);
|
|
782
|
+
return legacy ? report(provider, "zai:quota-limit", legacy) : null;
|
|
813
783
|
}
|
|
814
784
|
|
|
815
785
|
/**
|
|
@@ -1219,14 +1189,17 @@ export function parseXaiCreditsResponse(value: unknown): { percent: number; rese
|
|
|
1219
1189
|
if (!config) return null;
|
|
1220
1190
|
const period = asRecord(config.currentPeriod);
|
|
1221
1191
|
if (!period || period.type !== "USAGE_PERIOD_TYPE_WEEKLY") return null;
|
|
1222
|
-
|
|
1223
|
-
if (resetAt === undefined) return null;
|
|
1192
|
+
let percent = 0;
|
|
1224
1193
|
if (config.creditUsagePercent !== undefined) {
|
|
1225
|
-
const
|
|
1226
|
-
if (
|
|
1227
|
-
|
|
1194
|
+
const normalized = normalizePercent(config.creditUsagePercent);
|
|
1195
|
+
if (normalized === undefined) return null;
|
|
1196
|
+
percent = normalized;
|
|
1228
1197
|
}
|
|
1229
|
-
|
|
1198
|
+
const resetAt = normalizeResetAt(period.end);
|
|
1199
|
+
return {
|
|
1200
|
+
percent,
|
|
1201
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
1202
|
+
};
|
|
1230
1203
|
}
|
|
1231
1204
|
|
|
1232
1205
|
async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise<ProviderQuota | null> {
|
|
@@ -1409,20 +1382,44 @@ async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaRepor
|
|
|
1409
1382
|
return report(provider, "anthropic:oauth-usage", quota);
|
|
1410
1383
|
}
|
|
1411
1384
|
|
|
1385
|
+
/**
|
|
1386
|
+
* Provider-level Kiro row: the active account's usage, shown on the Providers page.
|
|
1387
|
+
*
|
|
1388
|
+
* The per-account cache is seeded from the same probe so opening that page does not read
|
|
1389
|
+
* the active account twice, and the account id is captured before the await so a
|
|
1390
|
+
* concurrent account switch cannot file this answer under the wrong account.
|
|
1391
|
+
*/
|
|
1392
|
+
async function fetchKiroQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
1393
|
+
const probedAccountId = getAccountSet("kiro")?.activeAccountId;
|
|
1394
|
+
if (!probedAccountId) return null;
|
|
1395
|
+
const probedAccountKey = accountCacheKey("kiro", probedAccountId);
|
|
1396
|
+
const writerGeneration = captureConfigGeneration();
|
|
1397
|
+
let snapshot: KiroUsageSnapshot | null;
|
|
1398
|
+
try {
|
|
1399
|
+
snapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(probedAccountId));
|
|
1400
|
+
} catch {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
if (!snapshot) return null;
|
|
1404
|
+
if (mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
|
|
1405
|
+
accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota: snapshot.quota });
|
|
1406
|
+
commitKiroAccountUsageState(probedAccountKey, snapshot);
|
|
1407
|
+
}
|
|
1408
|
+
return report(provider, "kiro:usage-limits", snapshot.quota);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1412
1411
|
// ---------------------------------------------------------------------------
|
|
1413
1412
|
// Per-account quota (multiauth)
|
|
1414
1413
|
// ---------------------------------------------------------------------------
|
|
1415
1414
|
|
|
1416
1415
|
/**
|
|
1417
|
-
* Anthropic
|
|
1418
|
-
* own bearer token — the active-account selection and the local usage log
|
|
1419
|
-
* Mirrors the Codex pool behaviour
|
|
1420
|
-
* per-account TTL so N accounts cost
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
* count, and Anthropic rate-limits the usage endpoint (observed 429 under repeated probing).
|
|
1416
|
+
* Anthropic and Kiro both report usage per CREDENTIAL, so every logged-in account can be
|
|
1417
|
+
* probed with its own bearer token — the active-account selection and the local usage log
|
|
1418
|
+
* are irrelevant here. Mirrors the Codex pool behaviour
|
|
1419
|
+
* (codex/auth-api.ts:fetchPoolAccountQuota), including a per-account TTL so N accounts cost
|
|
1420
|
+
* at most N upstream calls per window. `ACCOUNT_QUOTA_TTL_MS` lives in `quota-wire.ts`
|
|
1421
|
+
* because the Kiro exhaustion reader applies the same staleness bound.
|
|
1424
1422
|
*/
|
|
1425
|
-
const ACCOUNT_QUOTA_TTL_MS = 10 * 60_000;
|
|
1426
1423
|
type AccountQuotaCacheEntry = {
|
|
1427
1424
|
ts: number;
|
|
1428
1425
|
quota: ProviderQuota | null;
|
|
@@ -1430,6 +1427,31 @@ type AccountQuotaCacheEntry = {
|
|
|
1430
1427
|
unavailable?: true;
|
|
1431
1428
|
};
|
|
1432
1429
|
const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Seed the cache from the last run, once.
|
|
1433
|
+
*
|
|
1434
|
+
* Without this a restart forgets every measurement, so the pool opens its next turn with
|
|
1435
|
+
* no idea which account has room — the exact blindness pre-dispatch selection exists to
|
|
1436
|
+
* remove. A hydrated row is still subject to the ordinary TTL, so it orders the first
|
|
1437
|
+
* request and is replaced by a live probe immediately after.
|
|
1438
|
+
*/
|
|
1439
|
+
let diskHydrated = false;
|
|
1440
|
+
function hydrateAccountQuotaCache(): void {
|
|
1441
|
+
if (diskHydrated) return;
|
|
1442
|
+
diskHydrated = true;
|
|
1443
|
+
for (const [key, quota] of readPersistedAccountQuotas()) {
|
|
1444
|
+
if (!accountQuotaCache.has(key)) accountQuotaCache.set(key, { ts: quota.updatedAt, quota });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function persistAccountQuotaCache(): void {
|
|
1449
|
+
schedulePersistAccountQuotas(function* () {
|
|
1450
|
+
for (const [key, entry] of accountQuotaCache) {
|
|
1451
|
+
if (entry.quota) yield [key, entry.quota] as [string, ProviderQuota];
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1433
1455
|
const accountQuotaInflight = new Map<string, Promise<AccountQuotaCacheEntry>>();
|
|
1434
1456
|
let lastReconciledGeneration = 0;
|
|
1435
1457
|
let liveAccountQuotaKeys = new Set<string>();
|
|
@@ -1452,7 +1474,7 @@ export interface ProviderAccountQuota {
|
|
|
1452
1474
|
|
|
1453
1475
|
/** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
|
|
1454
1476
|
export function supportsPerAccountQuota(provider: string): boolean {
|
|
1455
|
-
return provider === "anthropic";
|
|
1477
|
+
return provider === "anthropic" || provider === "kiro";
|
|
1456
1478
|
}
|
|
1457
1479
|
|
|
1458
1480
|
function accountCacheKey(provider: string, accountId: string): string {
|
|
@@ -1500,10 +1522,14 @@ export function reconcileProviderAccountQuotaRows(context: GenerationContext): n
|
|
|
1500
1522
|
accountQuotaCache.delete(key);
|
|
1501
1523
|
removed += 1;
|
|
1502
1524
|
}
|
|
1525
|
+
// Kiro exhaustion rows are keyed identically, so they retire with their quota row; a
|
|
1526
|
+
// verdict outliving its account would hand the replacement a cooldown it never earned.
|
|
1527
|
+
removed += reconcileKiroAccountUsageState(context.oauthAccountKeys);
|
|
1503
1528
|
if (cache) {
|
|
1504
1529
|
const reports = cache.response.reports.filter(report => context.providerNames.has(report.provider));
|
|
1505
1530
|
removed += cache.response.reports.length - reports.length;
|
|
1506
1531
|
cache = { ...cache, response: { ...cache.response, reports } };
|
|
1532
|
+
replaceCachedProviderQuotas(reports);
|
|
1507
1533
|
}
|
|
1508
1534
|
liveAccountQuotaKeys = new Set(context.oauthAccountKeys);
|
|
1509
1535
|
liveProviderQuotaKeys = new Set(context.providerNames);
|
|
@@ -1523,16 +1549,23 @@ export function clearAccountQuotaCache(provider?: string): void {
|
|
|
1523
1549
|
if (!provider) {
|
|
1524
1550
|
accountQuotaCache.clear();
|
|
1525
1551
|
accountQuotaInflight.clear();
|
|
1552
|
+
clearKiroAccountUsageState();
|
|
1553
|
+
// A cleared cache must not be re-seeded from the file it was just cleared of, and any
|
|
1554
|
+
// pending write of the old rows is abandoned.
|
|
1555
|
+
diskHydrated = false;
|
|
1556
|
+
cancelPendingAccountQuotaPersist();
|
|
1526
1557
|
return;
|
|
1527
1558
|
}
|
|
1528
1559
|
const prefix = `${provider}\u0000`;
|
|
1529
1560
|
for (const key of [...accountQuotaCache.keys()]) {
|
|
1530
1561
|
if (key.startsWith(prefix)) accountQuotaCache.delete(key);
|
|
1531
1562
|
}
|
|
1563
|
+
clearKiroAccountUsageState(prefix);
|
|
1532
1564
|
// Drop in-flight probes too so a late resolve cannot repopulate after logout/remove.
|
|
1533
1565
|
for (const key of [...accountQuotaInflight.keys()]) {
|
|
1534
1566
|
if (key.startsWith(prefix)) accountQuotaInflight.delete(key);
|
|
1535
1567
|
}
|
|
1568
|
+
persistAccountQuotaCache();
|
|
1536
1569
|
}
|
|
1537
1570
|
|
|
1538
1571
|
/**
|
|
@@ -1571,8 +1604,21 @@ async function fetchAccountQuota(
|
|
|
1571
1604
|
|
|
1572
1605
|
const probe = (async (): Promise<AccountQuotaCacheEntry> => {
|
|
1573
1606
|
try {
|
|
1574
|
-
|
|
1575
|
-
|
|
1607
|
+
let quota: ProviderQuota | null;
|
|
1608
|
+
let kiroSnapshot: KiroUsageSnapshot | null = null;
|
|
1609
|
+
if (provider === "kiro") {
|
|
1610
|
+
// Kiro resolves the bearer and its routing metadata from ONE account-scoped
|
|
1611
|
+
// snapshot. It deliberately does not use getTokenForAccountQuotaProbe: that
|
|
1612
|
+
// helper refuses to refresh a background `local-cli` slot because Anthropic's
|
|
1613
|
+
// lock can adopt a mismatched Claude CLI identity, but Kiro marks every
|
|
1614
|
+
// CLI-imported credential `local-cli`, so the same rule would blank the quota of
|
|
1615
|
+
// every inactive pool account the moment its token expired.
|
|
1616
|
+
kiroSnapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(accountId));
|
|
1617
|
+
quota = kiroSnapshot?.quota ?? null;
|
|
1618
|
+
} else {
|
|
1619
|
+
const token = await getTokenForAccountQuotaProbe(provider, accountId);
|
|
1620
|
+
quota = await fetchAnthropicUsageQuota(token);
|
|
1621
|
+
}
|
|
1576
1622
|
if (!quota) {
|
|
1577
1623
|
// Preserve last-good bars and mark unavailable; advance TTL so failures
|
|
1578
1624
|
// negative-cache instead of re-probing on every GUI poll.
|
|
@@ -1583,6 +1629,7 @@ async function fetchAccountQuota(
|
|
|
1583
1629
|
};
|
|
1584
1630
|
if (mayCommitAccountQuotaKey(key, writerGeneration)) {
|
|
1585
1631
|
accountQuotaCache.set(key, entry);
|
|
1632
|
+
if (provider === "kiro") commitKiroAccountUsageState(key, null);
|
|
1586
1633
|
sweepExpiredOnWrite(entry.ts);
|
|
1587
1634
|
}
|
|
1588
1635
|
return entry;
|
|
@@ -1590,6 +1637,9 @@ async function fetchAccountQuota(
|
|
|
1590
1637
|
const entry: AccountQuotaCacheEntry = { ts: Date.now(), quota };
|
|
1591
1638
|
if (mayCommitAccountQuotaKey(key, writerGeneration)) {
|
|
1592
1639
|
accountQuotaCache.set(key, entry);
|
|
1640
|
+
// Exhaustion state rides the SAME commit guard as the quota row: a probe from a
|
|
1641
|
+
// superseded config generation must not publish either half.
|
|
1642
|
+
if (provider === "kiro") commitKiroAccountUsageState(key, kiroSnapshot);
|
|
1593
1643
|
sweepExpiredOnWrite(entry.ts);
|
|
1594
1644
|
}
|
|
1595
1645
|
return entry;
|
|
@@ -2199,6 +2249,7 @@ async function maybeFetchProviderQuota(
|
|
|
2199
2249
|
if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
|
|
2200
2250
|
if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
|
|
2201
2251
|
if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
|
|
2252
|
+
if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name);
|
|
2202
2253
|
// Kimi Code `/usages` accepts OAuth or coding-plan API keys, but only on the canonical
|
|
2203
2254
|
// host and only for real key auth — forward/local modes carry no credential of ours.
|
|
2204
2255
|
if (provider.authMode === "oauth" && name === "kimi") return fetchKimiQuota(name, provider);
|
|
@@ -2293,9 +2344,18 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
|
|
|
2293
2344
|
maybeFetchProviderQuota(name, provider, config, forceRefresh, prefetchedCodexSnapshot)
|
|
2294
2345
|
)),
|
|
2295
2346
|
);
|
|
2296
|
-
const fresh = probeResults.filter((item): item is ProviderQuotaReport =>
|
|
2347
|
+
const fresh = probeResults.filter((item): item is ProviderQuotaReport => (
|
|
2348
|
+
item !== null && item !== TERMINAL_QUOTA_FAILURE && item !== AUTHORITATIVE_EMPTY_QUOTA
|
|
2349
|
+
));
|
|
2350
|
+
// Both sentinels suppress the previous row. A terminal failure means the response was
|
|
2351
|
+
// invalid; an authoritative empty means the response was valid and said there are no
|
|
2352
|
+
// model windows. Either way the old row is no longer true, which is what separates them
|
|
2353
|
+
// from `null` (told us nothing — keep the last-good row).
|
|
2297
2354
|
const terminalFailures = new Set(
|
|
2298
|
-
Object.keys(config.providers).filter((_, index) =>
|
|
2355
|
+
Object.keys(config.providers).filter((_, index) => (
|
|
2356
|
+
probeResults[index] === TERMINAL_QUOTA_FAILURE
|
|
2357
|
+
|| probeResults[index] === AUTHORITATIVE_EMPTY_QUOTA
|
|
2358
|
+
)),
|
|
2299
2359
|
);
|
|
2300
2360
|
await providerQuotaBeforePublishForTests?.();
|
|
2301
2361
|
let commitKey: string | null = null;
|
|
@@ -2341,6 +2401,7 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
|
|
|
2341
2401
|
) {
|
|
2342
2402
|
const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
|
|
2343
2403
|
cache = { key, ts: Date.now(), response: { ...response, reports } };
|
|
2404
|
+
replaceCachedProviderQuotas(reports);
|
|
2344
2405
|
}
|
|
2345
2406
|
return response;
|
|
2346
2407
|
})();
|