@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { antigravityUserAgent } from "../../adapters/client-fingerprint";
|
|
2
|
+
import { DestinationDnsResolutionError } from "../../lib/destination-policy";
|
|
3
|
+
import { PinnedHttpError } from "../../lib/pinned-http";
|
|
4
|
+
import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../../lib/provider-outbound";
|
|
5
|
+
import { getValidAccessToken } from "../../oauth";
|
|
6
|
+
import { getAccountCredential, getCredential } from "../../oauth/store";
|
|
7
|
+
import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
|
|
8
|
+
import { report, type ProviderQuotaReport } from "./report-cache";
|
|
9
|
+
import { quotaCredentialIdentity } from "./account-cache";
|
|
10
|
+
import type { ProviderQuota, ProviderQuotaWindow, QuotaFailureCode } from "../quota-types";
|
|
11
|
+
|
|
12
|
+
export function antigravityQuotaDiagnosticIdentity(accountId: string, credential = getAccountCredential("google-antigravity", accountId)): string | undefined {
|
|
13
|
+
return credential ? quotaCredentialIdentity("google-antigravity", accountId, credential, {
|
|
14
|
+
adapter: "google", baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE, authMode: "oauth",
|
|
15
|
+
}) : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
function quotaInfoEntries(modelInfo: Record<string, unknown>): Record<string, unknown>[] {
|
|
20
|
+
const entries: Record<string, unknown>[] = [];
|
|
21
|
+
const add = (value: unknown, tier?: string) => {
|
|
22
|
+
const rec = asRecord(value);
|
|
23
|
+
if (!rec) return;
|
|
24
|
+
entries.push(tier ? { ...rec, tier } : rec);
|
|
25
|
+
};
|
|
26
|
+
const addArray = (value: unknown) => {
|
|
27
|
+
if (!Array.isArray(value)) return;
|
|
28
|
+
for (const entry of value) add(entry);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(modelInfo.quotaInfo)) addArray(modelInfo.quotaInfo);
|
|
32
|
+
else add(modelInfo.quotaInfo);
|
|
33
|
+
addArray(modelInfo.quotaInfos);
|
|
34
|
+
|
|
35
|
+
const byTier = asRecord(modelInfo.quotaInfoByTier);
|
|
36
|
+
if (byTier) {
|
|
37
|
+
for (const [tier, value] of Object.entries(byTier)) {
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
for (const entry of value) add(entry, tier);
|
|
40
|
+
} else {
|
|
41
|
+
add(value, tier);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, unknown>, quotaInfo: Record<string, unknown>): "Gem" | "Cla" | null {
|
|
49
|
+
const displayName = typeof modelInfo.displayName === "string" ? modelInfo.displayName : "";
|
|
50
|
+
const tier = typeof quotaInfo.tier === "string" ? quotaInfo.tier : "";
|
|
51
|
+
const haystack = `${modelId} ${displayName} ${tier}`.toLowerCase();
|
|
52
|
+
if (haystack.includes("gemini")) return "Gem";
|
|
53
|
+
if (haystack.includes("claude") || haystack.includes("opus") || haystack.includes("sonnet") || haystack.includes("gpt-oss") || haystack.includes("gpt_oss")) return "Cla";
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | undefined {
|
|
58
|
+
const target = asRecord(quotaInfo.remaining) ?? quotaInfo;
|
|
59
|
+
const remaining = normalizePercent(toFiniteNumber(target.remainingFraction) !== undefined
|
|
60
|
+
? toFiniteNumber(target.remainingFraction)! * 100
|
|
61
|
+
: toFiniteNumber(target.remainingPercentage) !== undefined
|
|
62
|
+
? toFiniteNumber(target.remainingPercentage)! * 100
|
|
63
|
+
: undefined);
|
|
64
|
+
if (remaining === undefined) return undefined;
|
|
65
|
+
return normalizePercent(100 - remaining);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Gem/Cla windows from a `fetchAvailableModels` body; shared by the provider and account probes. */
|
|
69
|
+
function antigravityWindowsFromModels(body: Record<string, unknown> | null): ProviderQuotaWindow[] {
|
|
70
|
+
const models = asRecord(body?.models);
|
|
71
|
+
if (!models) return [];
|
|
72
|
+
|
|
73
|
+
const windows = new Map<string, ProviderQuotaWindow>();
|
|
74
|
+
for (const [modelId, rawModelInfo] of Object.entries(models)) {
|
|
75
|
+
const modelInfo = asRecord(rawModelInfo);
|
|
76
|
+
if (!modelInfo) continue;
|
|
77
|
+
for (const quotaInfo of quotaInfoEntries(modelInfo)) {
|
|
78
|
+
const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo);
|
|
79
|
+
if (!label || windows.has(label)) continue;
|
|
80
|
+
const percent = antigravityUsedPercent(quotaInfo);
|
|
81
|
+
if (percent === undefined) continue;
|
|
82
|
+
windows.set(label, {
|
|
83
|
+
label,
|
|
84
|
+
percent,
|
|
85
|
+
...(normalizeResetAt(quotaInfo.resetTime) !== undefined ? { resetAt: normalizeResetAt(quotaInfo.resetTime) } : {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const customWindows = ["Gem", "Cla"].flatMap(label => {
|
|
91
|
+
const window = windows.get(label);
|
|
92
|
+
return window ? [window] : [];
|
|
93
|
+
});
|
|
94
|
+
return customWindows;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse Google Antigravity quota from `v1internal:retrieveUserQuotaSummary`.
|
|
99
|
+
* Groups contain Gemini models and Claude/3P models, each with 5h and weekly limit buckets.
|
|
100
|
+
*/
|
|
101
|
+
function parseAntigravityQuotaSummary(body: Record<string, unknown> | null): ProviderQuota | null {
|
|
102
|
+
const groups = Array.isArray(body?.groups) ? (body.groups as unknown[]) : [];
|
|
103
|
+
if (groups.length === 0) return null;
|
|
104
|
+
|
|
105
|
+
const customWindowsMap = new Map<string, ProviderQuotaWindow>();
|
|
106
|
+
|
|
107
|
+
for (const rawGroup of groups) {
|
|
108
|
+
const group = asRecord(rawGroup);
|
|
109
|
+
if (!group) continue;
|
|
110
|
+
const groupName = `${typeof group.displayName === "string" ? group.displayName : ""} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase();
|
|
111
|
+
const isGemini = groupName.includes("gemini");
|
|
112
|
+
const isClaude = groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt");
|
|
113
|
+
|
|
114
|
+
const buckets = Array.isArray(group.buckets) ? (group.buckets as unknown[]) : [];
|
|
115
|
+
for (const rawBucket of buckets) {
|
|
116
|
+
const bucket = asRecord(rawBucket);
|
|
117
|
+
if (!bucket) continue;
|
|
118
|
+
const windowStr = `${typeof bucket.window === "string" ? bucket.window : ""} ${typeof bucket.bucketId === "string" ? bucket.bucketId : ""} ${typeof bucket.displayName === "string" ? bucket.displayName : ""}`.toLowerCase();
|
|
119
|
+
const percent = antigravityUsedPercent(bucket);
|
|
120
|
+
if (percent === undefined) continue;
|
|
121
|
+
const resetAt = normalizeResetAt(bucket.resetTime);
|
|
122
|
+
|
|
123
|
+
const isWeekly = windowStr.includes("week");
|
|
124
|
+
const is5h = windowStr.includes("5h") || windowStr.includes("five");
|
|
125
|
+
|
|
126
|
+
if (isGemini) {
|
|
127
|
+
const label = is5h ? "Gem" : isWeekly ? "Gem (Weekly)" : "";
|
|
128
|
+
if (label && !customWindowsMap.has(label)) {
|
|
129
|
+
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
|
|
130
|
+
}
|
|
131
|
+
} else if (isClaude) {
|
|
132
|
+
const label = is5h ? "Cla" : isWeekly ? "Cla (Weekly)" : "";
|
|
133
|
+
if (label && !customWindowsMap.has(label)) {
|
|
134
|
+
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
const baseLabel = typeof group.displayName === "string" ? group.displayName : "Other";
|
|
138
|
+
const label = isWeekly ? `${baseLabel} (Weekly)` : baseLabel;
|
|
139
|
+
if (!customWindowsMap.has(label)) {
|
|
140
|
+
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const PREFERRED_ORDER = ["Gem", "Gem (Weekly)", "Cla", "Cla (Weekly)"];
|
|
147
|
+
const customWindows = Array.from(customWindowsMap.values()).sort((a, b) => {
|
|
148
|
+
const ia = PREFERRED_ORDER.indexOf(a.label);
|
|
149
|
+
const ib = PREFERRED_ORDER.indexOf(b.label);
|
|
150
|
+
if (ia !== -1 && ib !== -1) return ia - ib;
|
|
151
|
+
if (ia !== -1) return -1;
|
|
152
|
+
if (ib !== -1) return 1;
|
|
153
|
+
return a.label.localeCompare(b.label);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (customWindows.length === 0) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
customWindows,
|
|
162
|
+
updatedAt: Date.now(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const ANTIGRAVITY_ACCOUNT_QUOTA_BASE = "https://daily-cloudcode-pa.googleapis.com";
|
|
167
|
+
const ANTIGRAVITY_QUOTA_SUMMARY_URL = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:retrieveUserQuotaSummary`;
|
|
168
|
+
const ANTIGRAVITY_QUOTA_MODELS_URL = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:fetchAvailableModels`;
|
|
169
|
+
|
|
170
|
+
/** Only these fixed accounting destinations may use transparent Fake-IP DNS. */
|
|
171
|
+
export function isCanonicalAntigravityQuotaUrl(name: string, url: string): boolean {
|
|
172
|
+
return name === "google-antigravity"
|
|
173
|
+
&& (url === ANTIGRAVITY_QUOTA_SUMMARY_URL || url === ANTIGRAVITY_QUOTA_MODELS_URL);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let antigravityOutboundDependencies: ProviderOutboundDependencies = {
|
|
177
|
+
isCanonicalUrl: isCanonicalAntigravityQuotaUrl,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** Test seam: inject resolver/pinned transport for provider and per-account probes. */
|
|
181
|
+
export function setAntigravityAccountQuotaTransportForTests(dependencies: ProviderOutboundDependencies | null): void {
|
|
182
|
+
antigravityOutboundDependencies = { ...dependencies, isCanonicalUrl: isCanonicalAntigravityQuotaUrl };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Per-account Antigravity quota (#1082). Always probes Google's own Cloud Code Assist host
|
|
187
|
+
* through the pinned provider-outbound transport: a configured `baseUrl` is a routing choice
|
|
188
|
+
* for requests, not a second source of Google's accounting for a stored credential, and fixing
|
|
189
|
+
* the destination keeps the `provider\0accountId` cache identity exact across config changes.
|
|
190
|
+
* A redirect or non-2xx yields null (unavailable), never a partial row.
|
|
191
|
+
*/
|
|
192
|
+
type AntigravityQuotaProbeResult =
|
|
193
|
+
| { kind: "available"; quota: ProviderQuota; source: "google-antigravity:retrieveUserQuotaSummary" | "google-antigravity:fetchAvailableModels" }
|
|
194
|
+
| { kind: "unavailable"; failure: QuotaFailureCode; legacy: { kind: "null" } | { kind: "throw"; error: unknown } };
|
|
195
|
+
|
|
196
|
+
function quotaTransportFailure(error: unknown): QuotaFailureCode {
|
|
197
|
+
if (error instanceof ProviderOutboundPolicyError) return "destination_blocked";
|
|
198
|
+
if (error instanceof DestinationDnsResolutionError) return "dns_failed";
|
|
199
|
+
if (error instanceof PinnedHttpError) return error.code === "output_byte_limit" ? "response_unusable" : "timeout";
|
|
200
|
+
if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
|
|
201
|
+
return "transport_error";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function quotaHttpFailure(status: number): QuotaFailureCode {
|
|
205
|
+
if (status >= 300 && status < 400) return "redirect_blocked";
|
|
206
|
+
if (status === 401 || status === 403) return "access_denied";
|
|
207
|
+
if (status === 429) return "rate_limited";
|
|
208
|
+
return "upstream_error";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function unavailableAntigravityQuota(failure: QuotaFailureCode): AntigravityQuotaProbeResult {
|
|
212
|
+
return { kind: "unavailable", failure, legacy: { kind: "null" } };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Prefer a summary network-policy diagnosis over a vaguer fallback. A blocked
|
|
217
|
+
* destination is an actionable local-network fact, while "upstream_error" tells
|
|
218
|
+
* the operator to go look at Google. A successful models probe still clears
|
|
219
|
+
* the first failure completely.
|
|
220
|
+
*/
|
|
221
|
+
function antigravityUnavailableFailure(
|
|
222
|
+
summaryFailure: QuotaFailureCode | undefined,
|
|
223
|
+
fallbackFailure: QuotaFailureCode,
|
|
224
|
+
): QuotaFailureCode {
|
|
225
|
+
if (
|
|
226
|
+
(summaryFailure === "destination_blocked" || summaryFailure === "dns_failed")
|
|
227
|
+
&& fallbackFailure !== "destination_blocked"
|
|
228
|
+
&& fallbackFailure !== "dns_failed"
|
|
229
|
+
) {
|
|
230
|
+
return summaryFailure;
|
|
231
|
+
}
|
|
232
|
+
return fallbackFailure;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function probeAntigravityUsageQuota(accessToken: string, projectId: string): Promise<AntigravityQuotaProbeResult> {
|
|
236
|
+
const fetchQuota = (url: string) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
|
|
237
|
+
headers: {
|
|
238
|
+
Accept: "application/json", "Content-Type": "application/json",
|
|
239
|
+
"User-Agent": antigravityUserAgent(), Authorization: `Bearer ${accessToken}`,
|
|
240
|
+
},
|
|
241
|
+
body: JSON.stringify({ project: projectId }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
242
|
+
}, antigravityOutboundDependencies);
|
|
243
|
+
let summaryFailure: QuotaFailureCode | undefined;
|
|
244
|
+
try {
|
|
245
|
+
const response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL);
|
|
246
|
+
if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_SUMMARY_URL)) return unavailableAntigravityQuota("redirect_blocked");
|
|
247
|
+
if (response.status === 401 || response.status === 403) return unavailableAntigravityQuota("access_denied");
|
|
248
|
+
if (response.ok) {
|
|
249
|
+
const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(response)));
|
|
250
|
+
if (quota) return { kind: "available", quota, source: "google-antigravity:retrieveUserQuotaSummary" };
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
// Existing behavior: summary transport/parse failure may recover through the models probe.
|
|
254
|
+
summaryFailure = quotaTransportFailure(error);
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const response = await fetchQuota(ANTIGRAVITY_QUOTA_MODELS_URL);
|
|
258
|
+
if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_MODELS_URL)) {
|
|
259
|
+
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "redirect_blocked"));
|
|
260
|
+
}
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, quotaHttpFailure(response.status)));
|
|
263
|
+
}
|
|
264
|
+
const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
|
|
265
|
+
if (!customWindows.length) {
|
|
266
|
+
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "response_unusable"));
|
|
267
|
+
}
|
|
268
|
+
return { kind: "available", quota: { customWindows, updatedAt: Date.now() }, source: "google-antigravity:fetchAvailableModels" };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// The public compatibility wrapper still rejects this exact fallback error; it never enters a DTO.
|
|
271
|
+
return {
|
|
272
|
+
kind: "unavailable",
|
|
273
|
+
failure: antigravityUnavailableFailure(summaryFailure, quotaTransportFailure(error)),
|
|
274
|
+
legacy: { kind: "throw", error },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> {
|
|
280
|
+
const result = await probeAntigravityUsageQuota(accessToken, projectId);
|
|
281
|
+
if (result.kind === "available") return result.quota;
|
|
282
|
+
if (result.legacy.kind === "throw") throw result.legacy.error;
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function fetchAntigravityQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
287
|
+
const credential = getCredential("google-antigravity");
|
|
288
|
+
if (!credential?.projectId) return null;
|
|
289
|
+
let accessToken: string;
|
|
290
|
+
try { accessToken = await getValidAccessToken("google-antigravity"); } catch { return null; }
|
|
291
|
+
const result = await probeAntigravityUsageQuota(accessToken, credential.projectId);
|
|
292
|
+
if (result.kind === "available") return report(provider, result.source, result.quota);
|
|
293
|
+
if (result.legacy.kind === "throw") throw result.legacy.error;
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { effectiveCodexAuthAccountId, listCodexAuthAccountsSnapshot } from "../../codex/auth-api";
|
|
3
|
+
import { withoutRetiredCodexQuota, type StoredAccountQuota } from "../../codex/quota";
|
|
4
|
+
import { isMainAccountIdentityGenerationLive } from "../../codex/main-account-cache";
|
|
5
|
+
import { codexPlanKey } from "../../codex/plan";
|
|
6
|
+
import { resolveProviderApiKey } from "../key-store";
|
|
7
|
+
import { apiKeyPoolEntryId } from "../api-keys";
|
|
8
|
+
import { getProviderRegistryEntry, providerCodexAccountMode } from "../registry";
|
|
9
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../openai-tiers";
|
|
10
|
+
import { CODEX_CAPACITY_MAX_QUOTA_AGE_MS, type CodexCapacityAggregation, type CodexCapacityQuota } from "../codex-capacity";
|
|
11
|
+
import { clearCachedProviderQuotas, providerQuotaRoutingBinding, type ProviderQuotaRoutingEvidence } from "../quota-routing-cache";
|
|
12
|
+
import { clearProviderApiKeyQuotaCache } from "../quota-key-accounts";
|
|
13
|
+
import { QUOTA_JSON_READ_FAILURE, readQuotaJson } from "../quota-wire";
|
|
14
|
+
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
15
|
+
import type { ProviderQuota, ProviderRoutingQuota } from "../quota-types";
|
|
16
|
+
|
|
17
|
+
/** Keep a failed probe's previous row at most this long before dropping it. */
|
|
18
|
+
export const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
19
|
+
const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
|
|
20
|
+
export const accountReportCurrent = new WeakMap<ProviderQuotaReport, () => boolean>();
|
|
21
|
+
export const routingEvidence = new WeakMap<ProviderQuotaReport, ProviderQuotaRoutingEvidence>();
|
|
22
|
+
export let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
|
|
23
|
+
|
|
24
|
+
/** Test-only seam for identity/config invalidation after probes but before publication. */
|
|
25
|
+
export function setProviderQuotaBeforePublishForTests(
|
|
26
|
+
hook: (() => void | Promise<void>) | null,
|
|
27
|
+
): void {
|
|
28
|
+
providerQuotaBeforePublishForTests = hook;
|
|
29
|
+
}
|
|
30
|
+
export const TERMINAL_QUOTA_FAILURE = Symbol("terminal-quota-failure");
|
|
31
|
+
/**
|
|
32
|
+
* The probe succeeded and the upstream authoritatively reported NO model-quota windows.
|
|
33
|
+
*
|
|
34
|
+
* Distinct from `null`, which means "this probe told us nothing" and deliberately preserves
|
|
35
|
+
* the last-good row for up to 30 minutes. Collapsing the two would let a stale report outlive
|
|
36
|
+
* the authoritative answer that replaced it: a GLM plan whose payload carries only MCP
|
|
37
|
+
* `TIME_LIMIT` rows has no model windows, and the dashboard and quota-aware routing must stop
|
|
38
|
+
* showing the previous token windows rather than keep them for another half hour.
|
|
39
|
+
*
|
|
40
|
+
* Suppression is shared with `TERMINAL_QUOTA_FAILURE`; only the reason differs.
|
|
41
|
+
*/
|
|
42
|
+
export const AUTHORITATIVE_EMPTY_QUOTA = Symbol("authoritative-empty-quota");
|
|
43
|
+
export type ProviderQuotaProbeResult =
|
|
44
|
+
| ProviderQuotaReport
|
|
45
|
+
| null
|
|
46
|
+
| typeof TERMINAL_QUOTA_FAILURE
|
|
47
|
+
| typeof AUTHORITATIVE_EMPTY_QUOTA;
|
|
48
|
+
|
|
49
|
+
export interface ProviderQuotaReport {
|
|
50
|
+
provider: string;
|
|
51
|
+
label: string;
|
|
52
|
+
source: string;
|
|
53
|
+
quota: ProviderQuota;
|
|
54
|
+
updatedAt: number;
|
|
55
|
+
/** Added by the management response projection, never stored on a cached report. */
|
|
56
|
+
routingQuota?: ProviderRoutingQuota;
|
|
57
|
+
reverseEngineered?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The row was OBSERVED in-band on a streaming turn rather than probed.
|
|
60
|
+
*
|
|
61
|
+
* Age means something different for these. A probed provider re-reads on its own TTL,
|
|
62
|
+
* so a row older than the last-good bound means the probe is failing and showing it
|
|
63
|
+
* would misrepresent a live number. A passive provider publishes no endpoint at all
|
|
64
|
+
* (`hasPassiveAccountQuota`), so its last observation is not a stale reading of
|
|
65
|
+
* something fresher — it is the only measurement that exists, and dropping it leaves
|
|
66
|
+
* the operator with nothing. Consumers that enforce a freshness bound must exempt
|
|
67
|
+
* these and state the observation age instead.
|
|
68
|
+
*/
|
|
69
|
+
observed?: boolean;
|
|
70
|
+
aggregation?: CodexCapacityAggregation;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ProviderQuotaResponse {
|
|
74
|
+
generatedAt: number;
|
|
75
|
+
reports: ProviderQuotaReport[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let cache: { key: string; ts: number; response: ProviderQuotaResponse } | null = null;
|
|
79
|
+
export const inflight = new Map<string, { epoch: number; promise: Promise<ProviderQuotaResponse> }>();
|
|
80
|
+
/** Bumped on cache clear and on force-refresh start; stale-epoch probes lose commit authority. */
|
|
81
|
+
export let invalidationEpoch = 0;
|
|
82
|
+
|
|
83
|
+
/** Owner-module accessors: cache reassignment stays inside this file. */
|
|
84
|
+
export function getProviderQuotaReportCache(): { key: string; ts: number; response: ProviderQuotaResponse } | null {
|
|
85
|
+
return cache;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function setProviderQuotaReportCache(next: { key: string; ts: number; response: ProviderQuotaResponse } | null): void {
|
|
89
|
+
cache = next;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function bumpProviderQuotaInvalidationEpoch(): void {
|
|
93
|
+
invalidationEpoch += 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Invalidate the report cache (e.g. after switching a provider's active account). */
|
|
97
|
+
export function clearProviderQuotaCache(): void {
|
|
98
|
+
cache = null;
|
|
99
|
+
clearCachedProviderQuotas();
|
|
100
|
+
clearProviderApiKeyQuotaCache();
|
|
101
|
+
invalidationEpoch += 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cacheKey(config: OcxConfig): string {
|
|
105
|
+
const providers = Object.entries(config.providers)
|
|
106
|
+
.map(([name, provider]) => {
|
|
107
|
+
const resolvedKey = typeof provider.apiKey === "string"
|
|
108
|
+
? resolveProviderApiKey(provider.apiKey)?.trim()
|
|
109
|
+
: undefined;
|
|
110
|
+
const activeKeyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "none";
|
|
111
|
+
return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}`;
|
|
112
|
+
})
|
|
113
|
+
.sort()
|
|
114
|
+
.join("|");
|
|
115
|
+
return `${config.defaultProvider}|${providers}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type CodexAuthAccountsSnapshotPromise = ReturnType<typeof listCodexAuthAccountsSnapshot>;
|
|
119
|
+
|
|
120
|
+
export function hasCodexPoolProvider(config: OcxConfig): boolean {
|
|
121
|
+
return Object.entries(config.providers).some(([name, provider]) => (
|
|
122
|
+
provider.disabled !== true
|
|
123
|
+
&& isBuiltInChatGptForwardProvider(name, provider)
|
|
124
|
+
&& providerCodexAccountMode(name, provider) !== "direct"
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
|
|
129
|
+
if (!quota) return null;
|
|
130
|
+
return {
|
|
131
|
+
fiveHourPercent: quota.fiveHourPercent,
|
|
132
|
+
fiveHourResetAt: quota.fiveHourResetAt,
|
|
133
|
+
weeklyPercent: quota.weeklyPercent,
|
|
134
|
+
weeklyResetAt: quota.weeklyResetAt,
|
|
135
|
+
monthlyPercent: quota.monthlyPercent,
|
|
136
|
+
monthlyResetAt: quota.monthlyResetAt,
|
|
137
|
+
updatedAt: quota.updatedAt,
|
|
138
|
+
customWindows: [...(quota.customWindows ?? [])]
|
|
139
|
+
.map(window => ({ label: window.label, percent: window.percent, resetAt: window.resetAt }))
|
|
140
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function providerQuotaFromCodexQuota(
|
|
145
|
+
quota: StoredAccountQuota | Omit<StoredAccountQuota, "updatedAt"> | null | undefined,
|
|
146
|
+
): CodexCapacityQuota | null {
|
|
147
|
+
if (!quota) return null;
|
|
148
|
+
// Direct snapshots bypass account DTOs; sanitize here as well as at ingestion.
|
|
149
|
+
quota = withoutRetiredCodexQuota(quota);
|
|
150
|
+
if (!quota) return null;
|
|
151
|
+
const projected: CodexCapacityQuota = {
|
|
152
|
+
...(quota.shortPercent !== undefined ? { fiveHourPercent: quota.shortPercent } : {}),
|
|
153
|
+
...(quota.shortResetAt !== undefined ? { fiveHourResetAt: quota.shortResetAt } : {}),
|
|
154
|
+
...(quota.weeklyPercent !== undefined ? { weeklyPercent: quota.weeklyPercent } : {}),
|
|
155
|
+
...(quota.weeklyResetAt !== undefined ? { weeklyResetAt: quota.weeklyResetAt } : {}),
|
|
156
|
+
...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
|
|
157
|
+
...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
|
|
158
|
+
...(quota.customWindows !== undefined ? { customWindows: quota.customWindows } : {}),
|
|
159
|
+
updatedAt: "updatedAt" in quota ? quota.updatedAt : Date.now(),
|
|
160
|
+
};
|
|
161
|
+
return hasQuotaRows(projected) ? projected : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
|
|
165
|
+
export function cacheKeyWithAggregationState(
|
|
166
|
+
config: OcxConfig,
|
|
167
|
+
prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
|
|
168
|
+
): string | Promise<string> {
|
|
169
|
+
const base = cacheKey(config);
|
|
170
|
+
if (!hasCodexPoolProvider(config)) return base;
|
|
171
|
+
return (async () => {
|
|
172
|
+
try {
|
|
173
|
+
const activeId = effectiveCodexAuthAccountId(config);
|
|
174
|
+
const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, false));
|
|
175
|
+
const rows = snapshot.accounts.map(account => ({
|
|
176
|
+
isMain: account.isMain,
|
|
177
|
+
active: account.id === activeId,
|
|
178
|
+
plan: codexPlanKey(account.plan) ?? null,
|
|
179
|
+
paused: account.paused,
|
|
180
|
+
needsReauth: account.needsReauth === true,
|
|
181
|
+
quota: quotaSignatureValue(providerQuotaFromCodexQuota(account.quota)),
|
|
182
|
+
}));
|
|
183
|
+
const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
|
|
184
|
+
const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
|
|
185
|
+
return `${base}|codex-pool:${digest}`;
|
|
186
|
+
} catch {
|
|
187
|
+
return `${base}|codex-pool:unavailable`;
|
|
188
|
+
}
|
|
189
|
+
})();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function publicCapacityWindow(window: import("../codex-capacity").CodexCapacityWindowAggregation) {
|
|
193
|
+
const { totalWeight: _totalWeight, consumedWeight: _consumedWeight, remainingWeight: _remainingWeight, ...safe } = window;
|
|
194
|
+
return safe;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Management API metadata intentionally omits configured/weighted unit counts. */
|
|
198
|
+
export function publicCapacityAggregation(
|
|
199
|
+
aggregation: CodexCapacityAggregation,
|
|
200
|
+
presentation: NonNullable<CodexCapacityAggregation["presentation"]>,
|
|
201
|
+
): CodexCapacityAggregation {
|
|
202
|
+
const safeCurrentAccount = presentation === "coverage-only" && aggregation.currentAccount
|
|
203
|
+
? { ...aggregation.currentAccount, quota: null }
|
|
204
|
+
: aggregation.currentAccount;
|
|
205
|
+
return {
|
|
206
|
+
...aggregation,
|
|
207
|
+
presentation,
|
|
208
|
+
...(safeCurrentAccount ? { currentAccount: safeCurrentAccount } : {}),
|
|
209
|
+
...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
|
|
210
|
+
...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
|
|
211
|
+
...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
|
|
212
|
+
...(aggregation.customWindows ? {
|
|
213
|
+
customWindows: aggregation.customWindows.map(window => ({
|
|
214
|
+
label: window.label,
|
|
215
|
+
...publicCapacityWindow(window),
|
|
216
|
+
})),
|
|
217
|
+
} : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
|
|
222
|
+
if (!quota) return false;
|
|
223
|
+
return typeof quota.fiveHourPercent === "number"
|
|
224
|
+
|| typeof quota.weeklyPercent === "number"
|
|
225
|
+
|| typeof quota.monthlyPercent === "number"
|
|
226
|
+
|| quota.creditsUsd?.unlimited === true
|
|
227
|
+
|| typeof quota.creditsUsd?.percent === "number"
|
|
228
|
+
|| !!quota.customWindows?.some(window => typeof window.percent === "number");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function providerLabel(providerId: string): string {
|
|
232
|
+
return getProviderRegistryEntry(providerId)?.label ?? providerId;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Test-only access to the quota reader's deadline and cancellation contract. */
|
|
236
|
+
export async function readProviderQuotaJsonForTests(response: Response, timeoutMs: number): Promise<unknown> {
|
|
237
|
+
const result = await readQuotaJson(response, timeoutMs);
|
|
238
|
+
return result === QUOTA_JSON_READ_FAILURE ? null : result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConfig): boolean {
|
|
242
|
+
return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function report(
|
|
246
|
+
provider: string,
|
|
247
|
+
source: string,
|
|
248
|
+
quota: ProviderQuota,
|
|
249
|
+
aggregation?: CodexCapacityAggregation,
|
|
250
|
+
): ProviderQuotaReport | null {
|
|
251
|
+
if (!hasQuotaRows(quota)) return null;
|
|
252
|
+
return {
|
|
253
|
+
provider,
|
|
254
|
+
label: providerLabel(provider),
|
|
255
|
+
source,
|
|
256
|
+
quota,
|
|
257
|
+
updatedAt: quota.updatedAt,
|
|
258
|
+
...(aggregation ? { aggregation } : {}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Publish a credential-bound report, and routing evidence only when the producer
|
|
264
|
+
* hands over its inference-only projection.
|
|
265
|
+
*
|
|
266
|
+
* The projection is deliberately not defaulted to the display quota. A producer must
|
|
267
|
+
* decide that its rows really do constrain inference on the probed credential; omitting
|
|
268
|
+
* the argument leaves the report display-only, so a new producer cannot inherit
|
|
269
|
+
* provider-veto authority merely by calling this helper. Ownership alone is not the
|
|
270
|
+
* scope decision: providerQuotaRoutingBinding resolving is necessary, never sufficient.
|
|
271
|
+
*/
|
|
272
|
+
export function keyReport(
|
|
273
|
+
provider: string,
|
|
274
|
+
source: string,
|
|
275
|
+
quota: ProviderQuota,
|
|
276
|
+
config: OcxProviderConfig,
|
|
277
|
+
probedCredential: string,
|
|
278
|
+
inferenceQuota?: ProviderQuota,
|
|
279
|
+
): ProviderQuotaReport | null {
|
|
280
|
+
const result = report(provider, source, quota);
|
|
281
|
+
if (!result || !inferenceQuota) return result;
|
|
282
|
+
const binding = providerQuotaRoutingBinding(provider, config, probedCredential);
|
|
283
|
+
if (binding) routingEvidence.set(result, { quota: inferenceQuota, binding });
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function tagNativeMainReport(
|
|
288
|
+
value: ProviderQuotaReport | null,
|
|
289
|
+
generation: number,
|
|
290
|
+
): ProviderQuotaReport | null {
|
|
291
|
+
if (value) nativeMainReportGenerations.set(value, generation);
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Test-only seam: publish exactly as a credential-bound producer does, and hand back the
|
|
297
|
+
* routing evidence the publication actually attached.
|
|
298
|
+
*
|
|
299
|
+
* Live producers all pass a projection today, so no probe fixture can prove the OTHER half
|
|
300
|
+
* of the contract: that omitting it stays display-only. Routing an omitted argument through
|
|
301
|
+
* the real helper keeps that provable, and a re-introduced `= quota` default would be
|
|
302
|
+
* observed here (a defaulted parameter also fires for an explicitly undefined argument).
|
|
303
|
+
*/
|
|
304
|
+
export function publishKeyReportForTests(
|
|
305
|
+
provider: string,
|
|
306
|
+
source: string,
|
|
307
|
+
quota: ProviderQuota,
|
|
308
|
+
config: OcxProviderConfig,
|
|
309
|
+
probedCredential: string,
|
|
310
|
+
inferenceQuota?: ProviderQuota,
|
|
311
|
+
): { report: ProviderQuotaReport | null; routing: ProviderQuotaRoutingEvidence | undefined } {
|
|
312
|
+
const result = keyReport(provider, source, quota, config, probedCredential, inferenceQuota);
|
|
313
|
+
return { report: result, routing: result ? routingEvidence.get(result) : undefined };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolean {
|
|
317
|
+
const generation = nativeMainReportGenerations.get(value);
|
|
318
|
+
return (generation === undefined || isMainAccountIdentityGenerationLive(generation))
|
|
319
|
+
&& (accountReportCurrent.get(value)?.() ?? true);
|
|
320
|
+
}
|