@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
package/src/providers/quota.ts
CHANGED
|
@@ -9,10 +9,12 @@ import type { StoredAccountQuota } from "../codex/quota";
|
|
|
9
9
|
import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
|
|
10
10
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
11
11
|
import { codexPlanKey } from "../codex/plan";
|
|
12
|
+
import { resolveEnvValue } from "../config";
|
|
12
13
|
import { resolveProviderApiKey } from "./key-store";
|
|
13
14
|
import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
|
|
14
15
|
import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
|
|
15
16
|
import { antigravityUserAgent } from "../adapters/client-fingerprint";
|
|
17
|
+
import { isCanonicalOllamaCloudUrl } from "../adapters/ollama-native-url";
|
|
16
18
|
import { providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound";
|
|
17
19
|
import { apiKeyPoolEntryId } from "./api-keys";
|
|
18
20
|
import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
type CodexCapacityQuota,
|
|
47
49
|
} from "./codex-capacity";
|
|
48
50
|
import type {
|
|
51
|
+
AccountQuotaMode,
|
|
49
52
|
ProviderQuota,
|
|
50
53
|
ProviderQuotaCreditsUsd,
|
|
51
54
|
ProviderQuotaWindow,
|
|
@@ -63,6 +66,7 @@ import {
|
|
|
63
66
|
readPersistedAccountQuotas,
|
|
64
67
|
schedulePersistAccountQuotas,
|
|
65
68
|
} from "./account-quota-disk";
|
|
69
|
+
import { clearProviderApiKeyQuotaCache, mapQuotaRoster, readProviderApiKeyQuotas, type ProviderApiKeyQuota } from "./quota-key-accounts";
|
|
66
70
|
|
|
67
71
|
export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types";
|
|
68
72
|
|
|
@@ -83,6 +87,8 @@ const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
|
|
|
83
87
|
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
84
88
|
const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
|
|
85
89
|
const CLINE_BASE_URL = "https://api.cline.bot";
|
|
90
|
+
const OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
|
|
91
|
+
const OLLAMA_CLOUD_USAGE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/usage`;
|
|
86
92
|
const ZAI_BASE_URL = "https://api.z.ai";
|
|
87
93
|
const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
|
|
88
94
|
const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
|
|
@@ -96,6 +102,7 @@ const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
|
|
|
96
102
|
/** Keep a failed probe's previous row at most this long before dropping it. */
|
|
97
103
|
const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
98
104
|
const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
|
|
105
|
+
const accountReportCurrent = new WeakMap<ProviderQuotaReport, () => boolean>();
|
|
99
106
|
let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
|
|
100
107
|
|
|
101
108
|
/** Test-only seam for identity/config invalidation after probes but before publication. */
|
|
@@ -130,6 +137,18 @@ export interface ProviderQuotaReport {
|
|
|
130
137
|
quota: ProviderQuota;
|
|
131
138
|
updatedAt: number;
|
|
132
139
|
reverseEngineered?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* The row was OBSERVED in-band on a streaming turn rather than probed.
|
|
142
|
+
*
|
|
143
|
+
* Age means something different for these. A probed provider re-reads on its own TTL,
|
|
144
|
+
* so a row older than the last-good bound means the probe is failing and showing it
|
|
145
|
+
* would misrepresent a live number. A passive provider publishes no endpoint at all
|
|
146
|
+
* (`hasPassiveAccountQuota`), so its last observation is not a stale reading of
|
|
147
|
+
* something fresher — it is the only measurement that exists, and dropping it leaves
|
|
148
|
+
* the operator with nothing. Consumers that enforce a freshness bound must exempt
|
|
149
|
+
* these and state the observation age instead.
|
|
150
|
+
*/
|
|
151
|
+
observed?: boolean;
|
|
133
152
|
aggregation?: CodexCapacityAggregation;
|
|
134
153
|
}
|
|
135
154
|
|
|
@@ -147,6 +166,7 @@ let invalidationEpoch = 0;
|
|
|
147
166
|
export function clearProviderQuotaCache(): void {
|
|
148
167
|
cache = null;
|
|
149
168
|
clearCachedProviderQuotas();
|
|
169
|
+
clearProviderApiKeyQuotaCache();
|
|
150
170
|
invalidationEpoch += 1;
|
|
151
171
|
}
|
|
152
172
|
|
|
@@ -316,6 +336,15 @@ function isCanonicalClineBaseUrl(baseUrl: string): boolean {
|
|
|
316
336
|
return normalized === CLINE_BASE_URL || normalized === `${CLINE_BASE_URL}/api/v1`;
|
|
317
337
|
}
|
|
318
338
|
|
|
339
|
+
function isCanonicalOllamaCloudBaseUrl(baseUrl?: string): boolean {
|
|
340
|
+
if (!baseUrl) return false;
|
|
341
|
+
try {
|
|
342
|
+
return isCanonicalOllamaCloudUrl(baseUrl);
|
|
343
|
+
} catch {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
319
348
|
function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
|
|
320
349
|
const normalized = normalizedBaseUrl(baseUrl);
|
|
321
350
|
return normalized === ZAI_BASE_URL
|
|
@@ -647,6 +676,78 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
|
|
|
647
676
|
return windows > 0 ? report(provider, "cline:plan-usage-limits", quota) : null;
|
|
648
677
|
}
|
|
649
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Ollama Cloud `GET https://ollama.com/api/usage` — returns account usage.
|
|
681
|
+
* Legacy plans report rolling 5-hour `limits.session.usage` and 7-day
|
|
682
|
+
* `limits.weekly.usage`. Migrated monthly-credit plans report
|
|
683
|
+
* `limits.monthly.usage`. `usage` values are normalized fractions (0..1).
|
|
684
|
+
*/
|
|
685
|
+
function parseOllamaPercent(usageValue: unknown): number | undefined {
|
|
686
|
+
const usage = toFiniteNumber(usageValue);
|
|
687
|
+
if (usage === undefined || usage < 0) return undefined;
|
|
688
|
+
const percent = Math.round(usage * 10000) / 100;
|
|
689
|
+
return normalizePercent(percent);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function parseOllamaCloudQuota(body: Record<string, unknown> | null): ProviderQuota | null {
|
|
693
|
+
if (!body) return null;
|
|
694
|
+
const limits = asRecord(body.limits);
|
|
695
|
+
if (!limits) return null;
|
|
696
|
+
|
|
697
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
698
|
+
let windows = 0;
|
|
699
|
+
|
|
700
|
+
const session = asRecord(limits.session);
|
|
701
|
+
if (session) {
|
|
702
|
+
const percent = parseOllamaPercent(session.usage);
|
|
703
|
+
if (percent !== undefined) {
|
|
704
|
+
quota.fiveHourPercent = percent;
|
|
705
|
+
windows += 1;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const weekly = asRecord(limits.weekly);
|
|
710
|
+
if (weekly) {
|
|
711
|
+
const percent = parseOllamaPercent(weekly.usage);
|
|
712
|
+
if (percent !== undefined) {
|
|
713
|
+
quota.weeklyPercent = percent;
|
|
714
|
+
windows += 1;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const monthly = asRecord(limits.monthly);
|
|
719
|
+
if (monthly) {
|
|
720
|
+
const percent = parseOllamaPercent(monthly.usage);
|
|
721
|
+
if (percent !== undefined) {
|
|
722
|
+
quota.monthlyPercent = percent;
|
|
723
|
+
windows += 1;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return windows > 0 ? quota : null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
731
|
+
const effectiveBaseUrl = config.baseUrl ?? getProviderRegistryEntry(provider)?.baseUrl ?? "";
|
|
732
|
+
if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
|
|
733
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
734
|
+
if (!apiKey) return null;
|
|
735
|
+
const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
|
|
736
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
737
|
+
redirect: "error",
|
|
738
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
739
|
+
});
|
|
740
|
+
if (!response.ok) {
|
|
741
|
+
if (response.status === 404) return null;
|
|
742
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
743
|
+
? TERMINAL_QUOTA_FAILURE
|
|
744
|
+
: null;
|
|
745
|
+
}
|
|
746
|
+
const body = asRecord(await readQuotaJson(response));
|
|
747
|
+
const quota = parseOllamaCloudQuota(body);
|
|
748
|
+
return quota ? report(provider, "ollama-cloud:usage", quota) : null;
|
|
749
|
+
}
|
|
750
|
+
|
|
650
751
|
/**
|
|
651
752
|
* Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
|
|
652
753
|
* limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
|
|
@@ -1085,7 +1186,8 @@ function tagNativeMainReport(
|
|
|
1085
1186
|
|
|
1086
1187
|
function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolean {
|
|
1087
1188
|
const generation = nativeMainReportGenerations.get(value);
|
|
1088
|
-
return generation === undefined || isMainAccountIdentityGenerationLive(generation)
|
|
1189
|
+
return (generation === undefined || isMainAccountIdentityGenerationLive(generation))
|
|
1190
|
+
&& (accountReportCurrent.get(value)?.() ?? true);
|
|
1089
1191
|
}
|
|
1090
1192
|
|
|
1091
1193
|
async function fetchChatGptForwardQuota(
|
|
@@ -1206,6 +1308,7 @@ export function parseXaiCreditsResponse(value: unknown): { percent: number; rese
|
|
|
1206
1308
|
async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise<ProviderQuota | null> {
|
|
1207
1309
|
try {
|
|
1208
1310
|
const response = await fetch(XAI_CREDITS_URL, {
|
|
1311
|
+
redirect: "error",
|
|
1209
1312
|
headers: {
|
|
1210
1313
|
Accept: "application/json",
|
|
1211
1314
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -1229,16 +1332,11 @@ async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promi
|
|
|
1229
1332
|
}
|
|
1230
1333
|
}
|
|
1231
1334
|
|
|
1232
|
-
async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
1233
|
-
|
|
1234
|
-
try {
|
|
1235
|
-
accessToken = await getValidAccessToken("xai");
|
|
1236
|
-
} catch {
|
|
1237
|
-
return null;
|
|
1238
|
-
}
|
|
1335
|
+
async function fetchXaiQuota(provider: string, context: { accessToken: string; upstreamAccountId?: string }): Promise<ProviderQuotaReport | null> {
|
|
1336
|
+
const { accessToken } = context;
|
|
1239
1337
|
|
|
1240
1338
|
// Prefer the SuperGrok weekly credits window that actually gates prompting (#1283).
|
|
1241
|
-
const userId =
|
|
1339
|
+
const userId = context.upstreamAccountId?.trim() || xaiUserIdFromAccessToken(accessToken);
|
|
1242
1340
|
if (userId) {
|
|
1243
1341
|
const weekly = await fetchXaiWeeklyCredits(accessToken, userId);
|
|
1244
1342
|
if (weekly) return report(provider, "xai:grok-billing-credits", weekly);
|
|
@@ -1247,6 +1345,7 @@ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | nu
|
|
|
1247
1345
|
// Legacy monthly dollar pool — retained when weekly is unavailable.
|
|
1248
1346
|
try {
|
|
1249
1347
|
const response = await fetch(XAI_BILLING_URL, {
|
|
1348
|
+
redirect: "error",
|
|
1250
1349
|
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
|
|
1251
1350
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1252
1351
|
});
|
|
@@ -1427,7 +1526,9 @@ async function fetchPassiveProviderQuota(provider: string): Promise<ProviderQuot
|
|
|
1427
1526
|
hydrateAccountQuotaCache();
|
|
1428
1527
|
const entry = accountQuotaCache.get(accountCacheKey(provider, activeId));
|
|
1429
1528
|
if (!entry?.quota) return null;
|
|
1430
|
-
|
|
1529
|
+
const built = report(provider, `${provider}:subscription-observation`, entry.quota);
|
|
1530
|
+
// Tagged here rather than inside report(), which every probed path shares.
|
|
1531
|
+
return built ? { ...built, observed: true } : null;
|
|
1431
1532
|
}
|
|
1432
1533
|
|
|
1433
1534
|
// ---------------------------------------------------------------------------
|
|
@@ -1447,8 +1548,12 @@ type AccountQuotaCacheEntry = {
|
|
|
1447
1548
|
quota: ProviderQuota | null;
|
|
1448
1549
|
/** Last probe failed (429 / network / expired login); still may hold last-good quota. */
|
|
1449
1550
|
unavailable?: true;
|
|
1551
|
+
/** Private new-reader identity; never persisted or serialized. */
|
|
1552
|
+
identity?: string;
|
|
1553
|
+
isCurrent?: () => boolean;
|
|
1450
1554
|
};
|
|
1451
1555
|
const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
|
|
1556
|
+
let explicitAccountEpoch = 0;
|
|
1452
1557
|
|
|
1453
1558
|
/**
|
|
1454
1559
|
* Seed the cache from the last run, once.
|
|
@@ -1492,11 +1597,21 @@ export interface ProviderAccountQuota {
|
|
|
1492
1597
|
quota: ProviderQuota | null;
|
|
1493
1598
|
/** Set when the probe could not reach upstream (expired login, 429, network). */
|
|
1494
1599
|
unavailable?: true;
|
|
1600
|
+
isCurrent?: () => boolean;
|
|
1495
1601
|
}
|
|
1496
1602
|
|
|
1497
1603
|
/** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
|
|
1498
1604
|
export function supportsPerAccountQuota(provider: string): boolean {
|
|
1499
|
-
return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity"
|
|
1605
|
+
return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity"
|
|
1606
|
+
|| explicitAccountReader(provider);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function explicitAccountReader(provider: string): boolean {
|
|
1610
|
+
return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code";
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode {
|
|
1614
|
+
return hasPassiveAccountQuota(provider) ? "passive" : supportsPerAccountQuota(provider) ? "probe" : "unsupported";
|
|
1500
1615
|
}
|
|
1501
1616
|
|
|
1502
1617
|
function accountCacheKey(provider: string, accountId: string): string {
|
|
@@ -1509,6 +1624,7 @@ function accountCacheKey(provider: string, accountId: string): string {
|
|
|
1509
1624
|
*/
|
|
1510
1625
|
export function getCachedProviderAccountQuota(provider: string, accountId: string): ProviderQuota | null {
|
|
1511
1626
|
const entry = accountQuotaCache.get(accountCacheKey(provider, accountId));
|
|
1627
|
+
if (entry?.isCurrent && !entry.isCurrent()) return null;
|
|
1512
1628
|
return entry?.quota ?? null;
|
|
1513
1629
|
}
|
|
1514
1630
|
|
|
@@ -1530,11 +1646,8 @@ export function setCachedProviderAccountQuotaForTests(
|
|
|
1530
1646
|
* Providers whose per-account quota is OBSERVED in-band, never probed.
|
|
1531
1647
|
*
|
|
1532
1648
|
* Deliberately separate from `supportsPerAccountQuota` rather than folded into it. That
|
|
1533
|
-
* predicate gates
|
|
1534
|
-
*
|
|
1535
|
-
* there without a dedicated branch would ship a Meta credential to Anthropic. And even
|
|
1536
|
-
* with a branch it would be the wrong predicate: it means "this provider can be probed",
|
|
1537
|
-
* and Meta publishes no quota endpoint to probe.
|
|
1649
|
+
* predicate gates explicit upstream readers. Meta publishes no quota endpoint, so it
|
|
1650
|
+
* remains a cache-only observation even when every probe reader is account-scoped.
|
|
1538
1651
|
*/
|
|
1539
1652
|
export function hasPassiveAccountQuota(provider: string): boolean {
|
|
1540
1653
|
return provider === "meta-muse";
|
|
@@ -1640,6 +1753,7 @@ export function resetProviderQuotaReconcileStateForTests(): void {
|
|
|
1640
1753
|
|
|
1641
1754
|
/** Drop cached per-account rows (all, or just one provider's). */
|
|
1642
1755
|
export function clearAccountQuotaCache(provider?: string): void {
|
|
1756
|
+
explicitAccountEpoch += 1;
|
|
1643
1757
|
if (!provider) {
|
|
1644
1758
|
accountQuotaCache.clear();
|
|
1645
1759
|
accountQuotaInflight.clear();
|
|
@@ -1684,11 +1798,115 @@ async function getTokenForAccountQuotaProbe(provider: string, accountId: string)
|
|
|
1684
1798
|
return getValidAccessTokenForAccount(provider, accountId);
|
|
1685
1799
|
}
|
|
1686
1800
|
|
|
1801
|
+
function explicitQuotaConfig(provider: string, configured?: OcxProviderConfig): OcxProviderConfig | undefined {
|
|
1802
|
+
if (configured) return configured;
|
|
1803
|
+
const entry = getProviderRegistryEntry(provider);
|
|
1804
|
+
return entry ? { adapter: entry.adapter, baseUrl: entry.baseUrl, authMode: "oauth" } : undefined;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function explicitQuotaIdentity(provider: string, accountId: string, configured?: OcxProviderConfig): string | undefined {
|
|
1808
|
+
const credential = getAccountCredential(provider, accountId);
|
|
1809
|
+
const target = explicitQuotaConfig(provider, configured);
|
|
1810
|
+
if (!credential || !target) return undefined;
|
|
1811
|
+
return createHash("sha256").update(JSON.stringify([
|
|
1812
|
+
provider, accountId, credential.access, credential.refresh, credential.expires,
|
|
1813
|
+
credential.accountId, credential.projectId, credential.source,
|
|
1814
|
+
target.adapter, target.baseUrl, target.authMode, target.disabled === true,
|
|
1815
|
+
])).digest("hex");
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function explicitQuotaDestination(provider: string, config: OcxProviderConfig): boolean {
|
|
1819
|
+
if (config.disabled === true || config.authMode !== "oauth") return false;
|
|
1820
|
+
if (provider === "kimi") return isCanonicalKimiCodeBaseUrl(config.baseUrl);
|
|
1821
|
+
if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl);
|
|
1822
|
+
// These readers use fixed canonical billing origins, never config.baseUrl.
|
|
1823
|
+
return provider === "xai" || provider === "cursor";
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
async function readExplicitAccountQuota(provider: string, accountId: string, configured?: OcxProviderConfig): Promise<{
|
|
1827
|
+
result: ProviderQuotaProbeResult;
|
|
1828
|
+
identity: string | undefined;
|
|
1829
|
+
isCurrent: () => boolean;
|
|
1830
|
+
} | null> {
|
|
1831
|
+
const target = explicitQuotaConfig(provider, configured);
|
|
1832
|
+
if (!target || !explicitQuotaDestination(provider, target)) return null;
|
|
1833
|
+
const config = { ...target };
|
|
1834
|
+
const epoch = explicitAccountEpoch;
|
|
1835
|
+
const accessToken = await getTokenForAccountQuotaProbe(provider, accountId);
|
|
1836
|
+
const credential = getAccountCredential(provider, accountId);
|
|
1837
|
+
if (!credential || credential.access !== accessToken) return null;
|
|
1838
|
+
// Pair the post-renewal credential with the destination captured before renewal.
|
|
1839
|
+
const identity = explicitQuotaIdentity(provider, accountId, config);
|
|
1840
|
+
const isCurrent = () => epoch === explicitAccountEpoch
|
|
1841
|
+
&& identity === explicitQuotaIdentity(provider, accountId, configured);
|
|
1842
|
+
if (!isCurrent()) return null;
|
|
1843
|
+
let result: ProviderQuotaProbeResult;
|
|
1844
|
+
switch (provider) {
|
|
1845
|
+
case "xai": result = await fetchXaiQuota(provider, { accessToken, upstreamAccountId: credential.accountId }); break;
|
|
1846
|
+
case "cursor": result = await fetchCursorQuota(provider, accessToken); break;
|
|
1847
|
+
case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break;
|
|
1848
|
+
case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); break;
|
|
1849
|
+
default: return null;
|
|
1850
|
+
}
|
|
1851
|
+
return { result, identity, isCurrent };
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
async function fetchExplicitAccountQuota(provider: string, accountId: string, force: boolean, configured?: OcxProviderConfig): Promise<AccountQuotaCacheEntry> {
|
|
1855
|
+
const key = accountCacheKey(provider, accountId);
|
|
1856
|
+
const identity = explicitQuotaIdentity(provider, accountId, configured);
|
|
1857
|
+
const previous = accountQuotaCache.get(key);
|
|
1858
|
+
const cached = identity && previous?.identity === identity && previous.isCurrent?.() ? previous : undefined;
|
|
1859
|
+
if (!force && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS
|
|
1860
|
+
&& (!cached.quota || Date.now() - cached.quota.updatedAt < LAST_GOOD_MAX_AGE_MS)) return cached;
|
|
1861
|
+
const flightKey = `${key}\u0000${identity ?? "missing"}`;
|
|
1862
|
+
const running = accountQuotaInflight.get(flightKey);
|
|
1863
|
+
if (running) return running;
|
|
1864
|
+
const epoch = explicitAccountEpoch;
|
|
1865
|
+
const lastGood = cached?.quota && Date.now() - cached.quota.updatedAt < LAST_GOOD_MAX_AGE_MS ? cached.quota : null;
|
|
1866
|
+
const flight = (async (): Promise<AccountQuotaCacheEntry> => {
|
|
1867
|
+
let read: Awaited<ReturnType<typeof readExplicitAccountQuota>> = null;
|
|
1868
|
+
try { read = await readExplicitAccountQuota(provider, accountId, configured); } catch { /* unavailable */ }
|
|
1869
|
+
const isCurrent = read?.isCurrent ?? (() => epoch === explicitAccountEpoch && !!identity
|
|
1870
|
+
&& identity === explicitQuotaIdentity(provider, accountId, configured));
|
|
1871
|
+
const result = read?.result;
|
|
1872
|
+
const current = epoch === explicitAccountEpoch && isCurrent();
|
|
1873
|
+
const quota = current && result && typeof result !== "symbol" ? result.quota : null;
|
|
1874
|
+
const empty = result === AUTHORITATIVE_EMPTY_QUOTA;
|
|
1875
|
+
const entry: AccountQuotaCacheEntry = {
|
|
1876
|
+
ts: Date.now(),
|
|
1877
|
+
quota: quota ?? (current && result !== TERMINAL_QUOTA_FAILURE && !empty
|
|
1878
|
+
&& lastGood && Date.now() - lastGood.updatedAt < LAST_GOOD_MAX_AGE_MS ? lastGood : null),
|
|
1879
|
+
...(!current || (!quota && !empty) ? { unavailable: true as const } : {}),
|
|
1880
|
+
identity: read?.identity ?? identity,
|
|
1881
|
+
isCurrent: () => epoch === explicitAccountEpoch && isCurrent(),
|
|
1882
|
+
};
|
|
1883
|
+
if (entry.isCurrent?.()) accountQuotaCache.set(key, entry);
|
|
1884
|
+
return entry;
|
|
1885
|
+
})().finally(() => { if (accountQuotaInflight.get(flightKey) === flight) accountQuotaInflight.delete(flightKey); });
|
|
1886
|
+
accountQuotaInflight.set(flightKey, flight);
|
|
1887
|
+
return flight;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function fetchExplicitCurrentQuota(provider: string, config: OcxProviderConfig, liveConfig: OcxConfig): Promise<ProviderQuotaProbeResult> {
|
|
1891
|
+
const id = getAccountSet(provider)?.activeAccountId;
|
|
1892
|
+
if (!id) return null;
|
|
1893
|
+
const read = await readExplicitAccountQuota(provider, id, config);
|
|
1894
|
+
if (!read) return null;
|
|
1895
|
+
const isCurrent = () => liveConfig.providers[provider] === config
|
|
1896
|
+
&& read.isCurrent() && getAccountSet(provider)?.activeAccountId === id;
|
|
1897
|
+
if (!isCurrent()) return TERMINAL_QUOTA_FAILURE;
|
|
1898
|
+
if (read.result && typeof read.result !== "symbol") accountReportCurrent.set(read.result, isCurrent);
|
|
1899
|
+
return read.result;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1687
1902
|
async function fetchAccountQuota(
|
|
1688
1903
|
provider: string,
|
|
1689
1904
|
accountId: string,
|
|
1690
1905
|
forceRefresh: boolean,
|
|
1906
|
+
providerConfig?: OcxProviderConfig,
|
|
1691
1907
|
): Promise<AccountQuotaCacheEntry> {
|
|
1908
|
+
if (!supportsPerAccountQuota(provider)) return { ts: Date.now(), quota: null, unavailable: true };
|
|
1909
|
+
if (explicitAccountReader(provider)) return fetchExplicitAccountQuota(provider, accountId, forceRefresh, providerConfig);
|
|
1692
1910
|
const key = accountCacheKey(provider, accountId);
|
|
1693
1911
|
const writerGeneration = captureConfigGeneration();
|
|
1694
1912
|
const cached = accountQuotaCache.get(key);
|
|
@@ -1718,8 +1936,10 @@ async function fetchAccountQuota(
|
|
|
1718
1936
|
const projectId = getAccountCredential(provider, accountId)?.projectId;
|
|
1719
1937
|
if (!projectId) throw new Error("antigravity account has no project id");
|
|
1720
1938
|
quota = await fetchAntigravityUsageQuota(token, projectId);
|
|
1721
|
-
} else {
|
|
1939
|
+
} else if (provider === "anthropic") {
|
|
1722
1940
|
quota = await fetchAnthropicUsageQuota(token);
|
|
1941
|
+
} else {
|
|
1942
|
+
return { ts: Date.now(), quota: null, unavailable: true };
|
|
1723
1943
|
}
|
|
1724
1944
|
}
|
|
1725
1945
|
if (!quota) {
|
|
@@ -1772,18 +1992,27 @@ async function fetchAccountQuota(
|
|
|
1772
1992
|
export async function fetchProviderAccountQuotas(
|
|
1773
1993
|
provider: string,
|
|
1774
1994
|
forceRefresh = false,
|
|
1995
|
+
providerConfig?: OcxProviderConfig,
|
|
1775
1996
|
): Promise<ProviderAccountQuota[]> {
|
|
1776
1997
|
if (!supportsPerAccountQuota(provider)) return [];
|
|
1777
1998
|
const set = getAccountSet(provider);
|
|
1778
1999
|
if (!set) return [];
|
|
1779
|
-
return
|
|
1780
|
-
const entry = await fetchAccountQuota(provider, account.id, forceRefresh);
|
|
1781
|
-
|
|
2000
|
+
return mapQuotaRoster(set.accounts, async account => {
|
|
2001
|
+
const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig);
|
|
2002
|
+
const result: ProviderAccountQuota = {
|
|
1782
2003
|
accountId: account.id,
|
|
1783
2004
|
quota: entry.quota,
|
|
1784
2005
|
...(entry.unavailable ? { unavailable: true as const } : {}),
|
|
1785
2006
|
};
|
|
1786
|
-
|
|
2007
|
+
if (!explicitAccountReader(provider)) return result;
|
|
2008
|
+
const identity = entry.identity;
|
|
2009
|
+
Object.defineProperty(result, "isCurrent", { value: () => {
|
|
2010
|
+
if (entry.isCurrent) return entry.isCurrent();
|
|
2011
|
+
const credential = getAccountCredential(provider, account.id);
|
|
2012
|
+
return !!credential && (!identity || explicitQuotaIdentity(provider, account.id, providerConfig) === identity);
|
|
2013
|
+
} });
|
|
2014
|
+
return result;
|
|
2015
|
+
});
|
|
1787
2016
|
}
|
|
1788
2017
|
|
|
1789
2018
|
function normalizedBaseUrl(value: string): string | null {
|
|
@@ -1902,10 +2131,10 @@ function parseKimiQuotaPayload(value: unknown): ProviderQuota | null {
|
|
|
1902
2131
|
return hasQuotaRows(quota) ? quota : null;
|
|
1903
2132
|
}
|
|
1904
2133
|
|
|
1905
|
-
async function resolveKimiQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
|
|
2134
|
+
async function resolveKimiQuotaBearer(config: OcxProviderConfig, accountId?: string): Promise<string | null> {
|
|
1906
2135
|
if (config.authMode === "oauth") {
|
|
1907
2136
|
try {
|
|
1908
|
-
return await
|
|
2137
|
+
return accountId ? await getTokenForAccountQuotaProbe("kimi", accountId) : null;
|
|
1909
2138
|
} catch {
|
|
1910
2139
|
return null;
|
|
1911
2140
|
}
|
|
@@ -1917,13 +2146,13 @@ async function resolveKimiQuotaBearer(config: OcxProviderConfig): Promise<string
|
|
|
1917
2146
|
return primary || null;
|
|
1918
2147
|
}
|
|
1919
2148
|
|
|
1920
|
-
async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
|
|
2149
|
+
async function fetchKimiQuota(provider: string, config: OcxProviderConfig, accessToken: string): Promise<ProviderQuotaReport | null> {
|
|
1921
2150
|
// Never release credentials to a user-edited or lookalike provider host.
|
|
1922
2151
|
if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
|
|
1923
|
-
const accessToken = await resolveKimiQuotaBearer(config);
|
|
1924
2152
|
if (!accessToken) return null;
|
|
1925
2153
|
const response = await fetch(KIMI_CODE_USAGE_URL, {
|
|
1926
2154
|
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
|
|
2155
|
+
redirect: "error",
|
|
1927
2156
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1928
2157
|
});
|
|
1929
2158
|
if (!response.ok) return null;
|
|
@@ -2010,10 +2239,10 @@ async function fetchCommandCodeSpend(
|
|
|
2010
2239
|
}
|
|
2011
2240
|
|
|
2012
2241
|
/** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */
|
|
2013
|
-
async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
|
|
2242
|
+
async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig, accountId?: string): Promise<string | null> {
|
|
2014
2243
|
if (config.authMode === "oauth") {
|
|
2015
2244
|
try {
|
|
2016
|
-
return await
|
|
2245
|
+
return accountId ? await getTokenForAccountQuotaProbe("command-code", accountId) : null;
|
|
2017
2246
|
} catch {
|
|
2018
2247
|
return null;
|
|
2019
2248
|
}
|
|
@@ -2028,10 +2257,9 @@ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise
|
|
|
2028
2257
|
* usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
|
|
2029
2258
|
* whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
|
|
2030
2259
|
*/
|
|
2031
|
-
async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
2260
|
+
async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig, bearer: string): Promise<ProviderQuotaProbeResult> {
|
|
2032
2261
|
// Never release credentials to a user-edited or lookalike provider host.
|
|
2033
2262
|
if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
|
|
2034
|
-
const bearer = await resolveCommandCodeQuotaBearer(config);
|
|
2035
2263
|
if (!bearer) return null;
|
|
2036
2264
|
const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
|
|
2037
2265
|
const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
|
|
@@ -2071,13 +2299,7 @@ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig
|
|
|
2071
2299
|
}
|
|
2072
2300
|
|
|
2073
2301
|
/** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
|
|
2074
|
-
async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
2075
|
-
let accessToken: string;
|
|
2076
|
-
try {
|
|
2077
|
-
accessToken = await getValidAccessToken("cursor");
|
|
2078
|
-
} catch {
|
|
2079
|
-
return null;
|
|
2080
|
-
}
|
|
2302
|
+
async function fetchCursorQuota(provider: string, accessToken: string): Promise<ProviderQuotaReport | null> {
|
|
2081
2303
|
|
|
2082
2304
|
const authHeaders = {
|
|
2083
2305
|
Accept: "application/json",
|
|
@@ -2090,6 +2312,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
2090
2312
|
try {
|
|
2091
2313
|
const periodRes = await fetch("https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
|
|
2092
2314
|
method: "POST",
|
|
2315
|
+
redirect: "error",
|
|
2093
2316
|
headers: {
|
|
2094
2317
|
...authHeaders,
|
|
2095
2318
|
"Content-Type": "application/json",
|
|
@@ -2158,6 +2381,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
2158
2381
|
try {
|
|
2159
2382
|
const summaryRes = await fetch("https://api2.cursor.sh/api/usage/summary", {
|
|
2160
2383
|
headers: authHeaders,
|
|
2384
|
+
redirect: "error",
|
|
2161
2385
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2162
2386
|
});
|
|
2163
2387
|
if (summaryRes.ok) {
|
|
@@ -2187,6 +2411,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
2187
2411
|
|
|
2188
2412
|
const response = await fetch("https://api2.cursor.sh/auth/usage", {
|
|
2189
2413
|
headers: authHeaders,
|
|
2414
|
+
redirect: "error",
|
|
2190
2415
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2191
2416
|
});
|
|
2192
2417
|
if (!response.ok) return null;
|
|
@@ -2273,10 +2498,11 @@ function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, un
|
|
|
2273
2498
|
}
|
|
2274
2499
|
|
|
2275
2500
|
function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | undefined {
|
|
2276
|
-
const
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2501
|
+
const target = asRecord(quotaInfo.remaining) ?? quotaInfo;
|
|
2502
|
+
const remaining = normalizePercent(toFiniteNumber(target.remainingFraction) !== undefined
|
|
2503
|
+
? toFiniteNumber(target.remainingFraction)! * 100
|
|
2504
|
+
: toFiniteNumber(target.remainingPercentage) !== undefined
|
|
2505
|
+
? toFiniteNumber(target.remainingPercentage)! * 100
|
|
2280
2506
|
: undefined);
|
|
2281
2507
|
if (remaining === undefined) return undefined;
|
|
2282
2508
|
return normalizePercent(100 - remaining);
|
|
@@ -2311,6 +2537,75 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
|
|
|
2311
2537
|
return customWindows;
|
|
2312
2538
|
}
|
|
2313
2539
|
|
|
2540
|
+
/**
|
|
2541
|
+
* Parse Google Antigravity quota from `v1internal:retrieveUserQuotaSummary`.
|
|
2542
|
+
* Groups contain Gemini models and Claude/3P models, each with 5h and weekly limit buckets.
|
|
2543
|
+
*/
|
|
2544
|
+
function parseAntigravityQuotaSummary(body: Record<string, unknown> | null): ProviderQuota | null {
|
|
2545
|
+
const groups = Array.isArray(body?.groups) ? (body.groups as unknown[]) : [];
|
|
2546
|
+
if (groups.length === 0) return null;
|
|
2547
|
+
|
|
2548
|
+
const customWindowsMap = new Map<string, ProviderQuotaWindow>();
|
|
2549
|
+
|
|
2550
|
+
for (const rawGroup of groups) {
|
|
2551
|
+
const group = asRecord(rawGroup);
|
|
2552
|
+
if (!group) continue;
|
|
2553
|
+
const groupName = `${typeof group.displayName === "string" ? group.displayName : ""} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase();
|
|
2554
|
+
const isGemini = groupName.includes("gemini");
|
|
2555
|
+
const isClaude = groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt");
|
|
2556
|
+
|
|
2557
|
+
const buckets = Array.isArray(group.buckets) ? (group.buckets as unknown[]) : [];
|
|
2558
|
+
for (const rawBucket of buckets) {
|
|
2559
|
+
const bucket = asRecord(rawBucket);
|
|
2560
|
+
if (!bucket) continue;
|
|
2561
|
+
const windowStr = `${typeof bucket.window === "string" ? bucket.window : ""} ${typeof bucket.bucketId === "string" ? bucket.bucketId : ""} ${typeof bucket.displayName === "string" ? bucket.displayName : ""}`.toLowerCase();
|
|
2562
|
+
const percent = antigravityUsedPercent(bucket);
|
|
2563
|
+
if (percent === undefined) continue;
|
|
2564
|
+
const resetAt = normalizeResetAt(bucket.resetTime);
|
|
2565
|
+
|
|
2566
|
+
const isWeekly = windowStr.includes("week");
|
|
2567
|
+
const is5h = windowStr.includes("5h") || windowStr.includes("five");
|
|
2568
|
+
|
|
2569
|
+
if (isGemini) {
|
|
2570
|
+
const label = is5h ? "Gem" : isWeekly ? "Gem (Weekly)" : "";
|
|
2571
|
+
if (label && !customWindowsMap.has(label)) {
|
|
2572
|
+
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
|
|
2573
|
+
}
|
|
2574
|
+
} else if (isClaude) {
|
|
2575
|
+
const label = is5h ? "Cla" : isWeekly ? "Cla (Weekly)" : "";
|
|
2576
|
+
if (label && !customWindowsMap.has(label)) {
|
|
2577
|
+
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
|
|
2578
|
+
}
|
|
2579
|
+
} else {
|
|
2580
|
+
const baseLabel = typeof group.displayName === "string" ? group.displayName : "Other";
|
|
2581
|
+
const label = isWeekly ? `${baseLabel} (Weekly)` : baseLabel;
|
|
2582
|
+
if (!customWindowsMap.has(label)) {
|
|
2583
|
+
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
const PREFERRED_ORDER = ["Gem", "Gem (Weekly)", "Cla", "Cla (Weekly)"];
|
|
2590
|
+
const customWindows = Array.from(customWindowsMap.values()).sort((a, b) => {
|
|
2591
|
+
const ia = PREFERRED_ORDER.indexOf(a.label);
|
|
2592
|
+
const ib = PREFERRED_ORDER.indexOf(b.label);
|
|
2593
|
+
if (ia !== -1 && ib !== -1) return ia - ib;
|
|
2594
|
+
if (ia !== -1) return -1;
|
|
2595
|
+
if (ib !== -1) return 1;
|
|
2596
|
+
return a.label.localeCompare(b.label);
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
if (customWindows.length === 0) {
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
return {
|
|
2604
|
+
customWindows,
|
|
2605
|
+
updatedAt: Date.now(),
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2314
2609
|
const ANTIGRAVITY_ACCOUNT_QUOTA_BASE = "https://daily-cloudcode-pa.googleapis.com";
|
|
2315
2610
|
let antigravityOutboundDependencies: ProviderOutboundDependencies = {};
|
|
2316
2611
|
|
|
@@ -2327,6 +2622,28 @@ export function setAntigravityAccountQuotaTransportForTests(dependencies: Provid
|
|
|
2327
2622
|
* A redirect or non-2xx yields null (unavailable), never a partial row.
|
|
2328
2623
|
*/
|
|
2329
2624
|
export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> {
|
|
2625
|
+
const summaryUrl = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:retrieveUserQuotaSummary`;
|
|
2626
|
+
try {
|
|
2627
|
+
const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, {
|
|
2628
|
+
headers: {
|
|
2629
|
+
Accept: "application/json",
|
|
2630
|
+
"Content-Type": "application/json",
|
|
2631
|
+
"User-Agent": antigravityUserAgent(),
|
|
2632
|
+
Authorization: `Bearer ${accessToken}`,
|
|
2633
|
+
},
|
|
2634
|
+
body: JSON.stringify({ project: projectId }),
|
|
2635
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2636
|
+
}, antigravityOutboundDependencies);
|
|
2637
|
+
if (await providerRedirectError(summaryResponse, summaryUrl)) return null;
|
|
2638
|
+
if (summaryResponse.status === 401 || summaryResponse.status === 403) return null;
|
|
2639
|
+
if (summaryResponse.ok) {
|
|
2640
|
+
const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(summaryResponse)));
|
|
2641
|
+
if (quota) return quota;
|
|
2642
|
+
}
|
|
2643
|
+
} catch {
|
|
2644
|
+
// Fallback to fetchAvailableModels on error
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2330
2647
|
const url = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:fetchAvailableModels`;
|
|
2331
2648
|
const response = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
|
|
2332
2649
|
headers: {
|
|
@@ -2355,6 +2672,35 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
|
|
|
2355
2672
|
return null;
|
|
2356
2673
|
}
|
|
2357
2674
|
const baseUrl = (config.baseUrl || ANTIGRAVITY_ACCOUNT_QUOTA_BASE).replace(/\/+$/, "");
|
|
2675
|
+
|
|
2676
|
+
// The summary probe is pinned to Google's own host through the provider-outbound
|
|
2677
|
+
// transport, mirroring `fetchAntigravityUsageQuota` above: a configured `baseUrl` is a
|
|
2678
|
+
// routing choice for requests, not a second source of Google's accounting, and this
|
|
2679
|
+
// request carries the account bearer.
|
|
2680
|
+
const summaryUrl = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:retrieveUserQuotaSummary`;
|
|
2681
|
+
try {
|
|
2682
|
+
const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, {
|
|
2683
|
+
headers: {
|
|
2684
|
+
Accept: "application/json",
|
|
2685
|
+
"Content-Type": "application/json",
|
|
2686
|
+
"User-Agent": antigravityUserAgent(),
|
|
2687
|
+
Authorization: `Bearer ${accessToken}`,
|
|
2688
|
+
},
|
|
2689
|
+
body: JSON.stringify({ project: credential.projectId }),
|
|
2690
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2691
|
+
}, antigravityOutboundDependencies);
|
|
2692
|
+
if (await providerRedirectError(summaryResponse, summaryUrl)) return null;
|
|
2693
|
+
if (summaryResponse.status === 401 || summaryResponse.status === 403) return null;
|
|
2694
|
+
if (summaryResponse.ok) {
|
|
2695
|
+
const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(summaryResponse)));
|
|
2696
|
+
if (quota) {
|
|
2697
|
+
return report(provider, "google-antigravity:retrieveUserQuotaSummary", quota);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
} catch {
|
|
2701
|
+
// Fallback on network/fetch error
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2358
2704
|
const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, {
|
|
2359
2705
|
method: "POST",
|
|
2360
2706
|
headers: {
|
|
@@ -2375,6 +2721,54 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
|
|
|
2375
2721
|
});
|
|
2376
2722
|
}
|
|
2377
2723
|
|
|
2724
|
+
type KeyQuotaReader = (name: string, provider: OcxProviderConfig) => Promise<ProviderQuotaProbeResult>;
|
|
2725
|
+
|
|
2726
|
+
/** Same selector drives cheap capabilities and uncached reads; never resolves credentials. */
|
|
2727
|
+
function keyQuotaReaderForProvider(name: string, provider: OcxProviderConfig): KeyQuotaReader | null {
|
|
2728
|
+
if (provider.disabled === true || (provider.authMode ?? "key") !== "key") return null;
|
|
2729
|
+
if (isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
|
|
2730
|
+
return async (id, config) => {
|
|
2731
|
+
const bearer = await resolveKimiQuotaBearer(config);
|
|
2732
|
+
return bearer ? fetchKimiQuota(id, config, bearer) : null;
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
if (name === "commandcode" && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
|
|
2736
|
+
return async (id, config) => {
|
|
2737
|
+
const bearer = await resolveCommandCodeQuotaBearer(config);
|
|
2738
|
+
return bearer ? fetchCommandCodeQuota(id, config, bearer) : null;
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
if (registryEntryForProviderDestination(provider)?.id === "opencode-go") return fetchOpenCodeGoQuota;
|
|
2742
|
+
if (isCanonicalA6apiBaseUrl(provider.baseUrl)) return fetchA6apiQuota;
|
|
2743
|
+
if (name === "openrouter" && isCanonicalOpenRouterBaseUrl(provider.baseUrl)) return fetchOpenRouterQuota;
|
|
2744
|
+
if (name === "deepseek" && isCanonicalDeepSeekBaseUrl(provider.baseUrl)) return fetchDeepSeekQuota;
|
|
2745
|
+
if (name === "cline-pass" && isCanonicalClineBaseUrl(provider.baseUrl)) return fetchClineQuota;
|
|
2746
|
+
if (isCanonicalOllamaCloudBaseUrl(provider.baseUrl ?? getProviderRegistryEntry(name)?.baseUrl)) return fetchOllamaCloudQuota;
|
|
2747
|
+
if (["zai", "glm", "glm-cn", "zhipu-bigmodel-coding"].includes(name) && isCanonicalZaiBaseUrl(provider.baseUrl)) return fetchZaiQuota;
|
|
2748
|
+
if (["minimax", "minimax-cn"].includes(name) && isCanonicalMinimaxBaseUrl(provider.baseUrl)) return fetchMinimaxQuota;
|
|
2749
|
+
if (name === "moonshot" && isCanonicalMoonshotBaseUrl(provider.baseUrl)) return fetchMoonshotQuota;
|
|
2750
|
+
if (name === "venice" && isCanonicalVeniceBaseUrl(provider.baseUrl)) return fetchVeniceQuota;
|
|
2751
|
+
if (name === "synthetic" && isCanonicalSyntheticBaseUrl(provider.baseUrl)) return fetchSyntheticQuota;
|
|
2752
|
+
if (name === "deepinfra" && isCanonicalDeepInfraBaseUrl(provider.baseUrl)) return fetchDeepInfraQuota;
|
|
2753
|
+
if (name === "neuralwatt" && isCanonicalNeuralwattBaseUrl(provider.baseUrl)) return fetchNeuralwattQuota;
|
|
2754
|
+
return null;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
export function providerApiKeyQuotaMode(name: string, provider: OcxProviderConfig): AccountQuotaMode {
|
|
2758
|
+
return keyQuotaReaderForProvider(name, provider) ? "probe" : "unsupported";
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
export async function fetchProviderApiKeyQuotas(config: OcxConfig, name: string, forceRefresh = false): Promise<ProviderApiKeyQuota[]> {
|
|
2762
|
+
const provider = config.providers[name];
|
|
2763
|
+
if (!provider || !keyQuotaReaderForProvider(name, provider)) return [];
|
|
2764
|
+
return readProviderApiKeyQuotas(config, name, forceRefresh, async (isolatedProvider, isolatedConfig) => {
|
|
2765
|
+
const result = await maybeFetchProviderQuota(name, isolatedProvider, isolatedConfig, false);
|
|
2766
|
+
if (result === TERMINAL_QUOTA_FAILURE) return { kind: "terminal" };
|
|
2767
|
+
if (result === AUTHORITATIVE_EMPTY_QUOTA) return { kind: "empty" };
|
|
2768
|
+
return result ? { kind: "quota", quota: result.quota } : { kind: "unavailable" };
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2378
2772
|
async function maybeFetchProviderQuota(
|
|
2379
2773
|
name: string,
|
|
2380
2774
|
provider: OcxProviderConfig,
|
|
@@ -2387,79 +2781,108 @@ async function maybeFetchProviderQuota(
|
|
|
2387
2781
|
if (isBuiltInChatGptForwardProvider(name, provider)) {
|
|
2388
2782
|
return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexSnapshot);
|
|
2389
2783
|
}
|
|
2390
|
-
if (provider.authMode === "oauth" && name
|
|
2784
|
+
if (provider.authMode === "oauth" && explicitAccountReader(name)) return await fetchExplicitCurrentQuota(name, provider, config);
|
|
2391
2785
|
if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
|
|
2392
|
-
if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
|
|
2393
2786
|
if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
|
|
2394
2787
|
if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name);
|
|
2395
2788
|
// Passive providers (meta-muse): Meta publishes no quota endpoint, so there is no
|
|
2396
2789
|
// probe to run — the row is the active account's last in-band observation.
|
|
2397
2790
|
if (provider.authMode === "oauth" && hasPassiveAccountQuota(name)) return fetchPassiveProviderQuota(name);
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
if (provider.authMode === "oauth" && name === "kimi") return fetchKimiQuota(name, provider);
|
|
2401
|
-
if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
|
|
2402
|
-
return fetchKimiQuota(name, provider);
|
|
2403
|
-
}
|
|
2404
|
-
// OAuth account login or Provider-API key only; forward/local modes carry no
|
|
2405
|
-
// credential of ours on the canonical host.
|
|
2406
|
-
if (provider.authMode === "oauth" && name === "command-code") {
|
|
2407
|
-
return fetchCommandCodeQuota(name, provider);
|
|
2408
|
-
}
|
|
2409
|
-
if ((provider.authMode ?? "key") === "key" && name === "commandcode"
|
|
2410
|
-
&& isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
|
|
2411
|
-
return fetchCommandCodeQuota(name, provider);
|
|
2412
|
-
}
|
|
2413
|
-
// Identify OpenCode Go by where it routes, not by what the row is called. Multi-account
|
|
2414
|
-
// setups keep the same destination under names like `opencode-go-2` (#1924), and those rows
|
|
2415
|
-
// silently had no quota panel and no `ocx provider quota --json` report while the literal
|
|
2416
|
-
// name was the gate. `registryEntryForProviderDestination` is the existing predicate for
|
|
2417
|
-
// exactly this question: normalized endpoint + adapter + key auth, so a canonical URL behind
|
|
2418
|
-
// a different adapter is still not OpenCode Go. The defensive URL check inside
|
|
2419
|
-
// `fetchOpenCodeGoQuota` stays — sending a key anywhere must not depend on this gate.
|
|
2420
|
-
if ((provider.authMode ?? "key") === "key" && registryEntryForProviderDestination(provider)?.id === "opencode-go") {
|
|
2421
|
-
return fetchOpenCodeGoQuota(name, provider);
|
|
2422
|
-
}
|
|
2423
|
-
if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {
|
|
2424
|
-
return fetchA6apiQuota(name, provider);
|
|
2425
|
-
}
|
|
2426
|
-
if ((provider.authMode ?? "key") === "key" && name === "openrouter") {
|
|
2427
|
-
return fetchOpenRouterQuota(name, provider);
|
|
2428
|
-
}
|
|
2429
|
-
if ((provider.authMode ?? "key") === "key" && name === "deepseek") {
|
|
2430
|
-
return fetchDeepSeekQuota(name, provider);
|
|
2431
|
-
}
|
|
2432
|
-
if ((provider.authMode ?? "key") === "key" && name === "cline-pass") {
|
|
2433
|
-
return fetchClineQuota(name, provider);
|
|
2434
|
-
}
|
|
2435
|
-
if ((provider.authMode ?? "key") === "key"
|
|
2436
|
-
&& (name === "zai" || name === "glm" || name === "glm-cn" || name === "zhipu-bigmodel-coding")) {
|
|
2437
|
-
return fetchZaiQuota(name, provider);
|
|
2438
|
-
}
|
|
2439
|
-
if ((provider.authMode ?? "key") === "key" && (name === "minimax" || name === "minimax-cn")) {
|
|
2440
|
-
return fetchMinimaxQuota(name, provider);
|
|
2441
|
-
}
|
|
2442
|
-
if ((provider.authMode ?? "key") === "key" && name === "moonshot") {
|
|
2443
|
-
return fetchMoonshotQuota(name, provider);
|
|
2444
|
-
}
|
|
2445
|
-
if ((provider.authMode ?? "key") === "key" && name === "venice") {
|
|
2446
|
-
return fetchVeniceQuota(name, provider);
|
|
2447
|
-
}
|
|
2448
|
-
if ((provider.authMode ?? "key") === "key" && name === "synthetic") {
|
|
2449
|
-
return fetchSyntheticQuota(name, provider);
|
|
2450
|
-
}
|
|
2451
|
-
if ((provider.authMode ?? "key") === "key" && name === "deepinfra") {
|
|
2452
|
-
return fetchDeepInfraQuota(name, provider);
|
|
2453
|
-
}
|
|
2454
|
-
if ((provider.authMode ?? "key") === "key" && name === "neuralwatt") {
|
|
2455
|
-
return fetchNeuralwattQuota(name, provider);
|
|
2456
|
-
}
|
|
2457
|
-
return null;
|
|
2791
|
+
const reader = keyQuotaReaderForProvider(name, provider);
|
|
2792
|
+
return reader ? reader(name, provider) : null;
|
|
2458
2793
|
} catch {
|
|
2459
2794
|
return null;
|
|
2460
2795
|
}
|
|
2461
2796
|
}
|
|
2462
2797
|
|
|
2798
|
+
/**
|
|
2799
|
+
* Hand freshly committed provider reports to the optional quota-reset observer.
|
|
2800
|
+
*
|
|
2801
|
+
* Lazy import on purpose: this module is statically reachable from
|
|
2802
|
+
* src/server/responses/core.ts (via oauth/anthropic-routing.ts), so a static edge would put
|
|
2803
|
+
* the observer and its sink registry on every install's request path.
|
|
2804
|
+
*
|
|
2805
|
+
* No previous snapshot is passed. The observer keeps its own persisted last-seen map,
|
|
2806
|
+
* because `previous` here is bound only when `cache.key === key` and the key digest at
|
|
2807
|
+
* cacheKeyWithAggregationState includes quota values and updatedAt — so it is empty exactly
|
|
2808
|
+
* when a reset happened.
|
|
2809
|
+
*
|
|
2810
|
+
* Account identity is resolved SYNCHRONOUSLY, before any await. Two reasons, both observed:
|
|
2811
|
+
* pool failover rewrites `activeAccountId` during request routing
|
|
2812
|
+
* (promoteAnthropicActiveAccount -> setActiveAccount), so a 429 between this commit and a
|
|
2813
|
+
* later async read would attribute this report to a different account and overwrite that
|
|
2814
|
+
* account's baseline with these numbers. fetchAnthropicQuota already captures
|
|
2815
|
+
* `probedAccountId` before awaiting for exactly this reason; the observer must not be
|
|
2816
|
+
* sloppier than the cache it observes.
|
|
2817
|
+
*
|
|
2818
|
+
* Observations are serialized through a module-level promise chain for the same reason as
|
|
2819
|
+
* the codex seam: Bun does not resolve concurrent import() calls in call order, and an
|
|
2820
|
+
* out-of-order baseline swap manufactures false resets that then burn the durable
|
|
2821
|
+
* idempotence key.
|
|
2822
|
+
*/
|
|
2823
|
+
let pendingProviderObservation: Promise<void> = Promise.resolve();
|
|
2824
|
+
|
|
2825
|
+
/**
|
|
2826
|
+
* Stable per-account observation key for one provider report.
|
|
2827
|
+
*
|
|
2828
|
+
* OAuth providers key by active account id. Key-auth providers have NO account set, so
|
|
2829
|
+
* every key in `apiKeyPool` would collapse onto "default" and rotating from a spent key to
|
|
2830
|
+
* a fresh one would read as a reset (measured: 97% -> 12% fired a false surprise). The
|
|
2831
|
+
* cache key already discriminates these at cacheKey() via apiKeyPoolEntryId, so this
|
|
2832
|
+
* mirrors that discriminator instead of inventing a second notion of identity.
|
|
2833
|
+
*/
|
|
2834
|
+
function providerObservationAccountKey(provider: string, config: OcxConfig): string {
|
|
2835
|
+
const oauthAccountId = getAccountSet(provider)?.activeAccountId;
|
|
2836
|
+
if (oauthAccountId !== undefined) return `${provider}\u0000${oauthAccountId}`;
|
|
2837
|
+
const providerConfig = config.providers[provider];
|
|
2838
|
+
const resolvedKey = typeof providerConfig?.apiKey === "string"
|
|
2839
|
+
? resolveEnvValue(providerConfig.apiKey)?.trim()
|
|
2840
|
+
: undefined;
|
|
2841
|
+
const keyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "default";
|
|
2842
|
+
return `${provider}\u0000key:${keyId}`;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
/** Test-only view of the observation account key. */
|
|
2846
|
+
export function providerObservationAccountKeyForTests(provider: string, config: OcxConfig): string {
|
|
2847
|
+
return providerObservationAccountKey(provider, config);
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
function notifyProviderQuotaSnapshot(
|
|
2851
|
+
reports: ReadonlyArray<ProviderQuotaReport>,
|
|
2852
|
+
config: OcxConfig,
|
|
2853
|
+
): void {
|
|
2854
|
+
if (reports.length === 0) return;
|
|
2855
|
+
// Resolved here, synchronously, while the identity is still the one that produced these
|
|
2856
|
+
// reports.
|
|
2857
|
+
const observations = reports.map(report => ({
|
|
2858
|
+
scope: report.provider,
|
|
2859
|
+
accountKey: providerObservationAccountKey(report.provider, config),
|
|
2860
|
+
quota: report.quota,
|
|
2861
|
+
}));
|
|
2862
|
+
pendingProviderObservation = pendingProviderObservation
|
|
2863
|
+
.then(async () => {
|
|
2864
|
+
const observer = await import("../quota/reset-observer");
|
|
2865
|
+
if (!observer.hasQuotaResetSink()) return;
|
|
2866
|
+
const { providerWindowObservations } = await import("../quota/window-mapping");
|
|
2867
|
+
for (const observation of observations) {
|
|
2868
|
+
observer.observeQuotaSnapshot({
|
|
2869
|
+
scope: observation.scope,
|
|
2870
|
+
accountKey: observation.accountKey,
|
|
2871
|
+
windows: providerWindowObservations(observation.quota),
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
})
|
|
2875
|
+
.catch(() => {
|
|
2876
|
+
// Detection is best-effort: a quota refresh must never fail because of it. Swallowing
|
|
2877
|
+
// here also keeps the chain alive — a rejected link would poison every later one.
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
/** Await the observation chain. Tests only: production never needs to join it. */
|
|
2882
|
+
export function flushProviderQuotaObservationsForTests(): Promise<void> {
|
|
2883
|
+
return pendingProviderObservation;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2463
2886
|
export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
|
|
2464
2887
|
// A Pool report's cache signature and provider fetch must share one account snapshot.
|
|
2465
2888
|
// Preserve force semantics when deciding whether that snapshot refreshes upstream data.
|
|
@@ -2472,9 +2895,14 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
|
|
|
2472
2895
|
const now = Date.now();
|
|
2473
2896
|
// The cache fast path must not extend a preserved last-good row past its 30-minute bound:
|
|
2474
2897
|
// a row preserved at age 29:59 plus a full 5-minute TTL would otherwise serve until ~35min.
|
|
2898
|
+
// An OBSERVED row is exempt: it carries the observation time, which is older than the bound
|
|
2899
|
+
// by construction and never becomes fresher on its own. Without the exemption a single
|
|
2900
|
+
// configured passive provider makes this predicate permanently false, so every dashboard
|
|
2901
|
+
// poll re-probes every OTHER provider upstream instead of serving the 5-minute cache.
|
|
2475
2902
|
const cacheFresh = cache && cache.key === key && now - cache.ts < CACHE_TTL_MS
|
|
2476
2903
|
&& cache.response.reports.every(item =>
|
|
2477
|
-
now - item.updatedAt < LAST_GOOD_MAX_AGE_MS
|
|
2904
|
+
(item.observed === true || now - item.updatedAt < LAST_GOOD_MAX_AGE_MS)
|
|
2905
|
+
&& isProviderQuotaReportCurrent(item));
|
|
2478
2906
|
if (!forceRefresh && cacheFresh) return cache!.response;
|
|
2479
2907
|
const joinable = inflight.get(key);
|
|
2480
2908
|
if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) return joinable.promise;
|
|
@@ -2518,7 +2946,9 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
|
|
|
2518
2946
|
const byProvider = new Map<string, ProviderQuotaReport>();
|
|
2519
2947
|
const generationMismatchedProviders = new Set<string>();
|
|
2520
2948
|
for (const item of previous) {
|
|
2521
|
-
|
|
2949
|
+
// Same exemption as the fast path. A passive row reaching `previous` is not a probe
|
|
2950
|
+
// that went quiet — there is no probe — so age cannot condemn it.
|
|
2951
|
+
if (item.observed !== true && item.updatedAt < cutoff) continue;
|
|
2522
2952
|
if (isProviderQuotaReportCurrent(item)) byProvider.set(item.provider, item);
|
|
2523
2953
|
else generationMismatchedProviders.add(item.provider);
|
|
2524
2954
|
}
|
|
@@ -2547,6 +2977,7 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
|
|
|
2547
2977
|
const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
|
|
2548
2978
|
cache = { key, ts: Date.now(), response: { ...response, reports } };
|
|
2549
2979
|
replaceCachedProviderQuotas(reports);
|
|
2980
|
+
notifyProviderQuotaSnapshot(reports, config);
|
|
2550
2981
|
}
|
|
2551
2982
|
return response;
|
|
2552
2983
|
})();
|