@bitkyc08/opencodex 2.91.0 → 2.92.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.
Files changed (148) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/{App-nZ34cR5G.js → App-sUf_8afJ.js} +18 -18
  3. package/gui/dist/assets/{Tray-BuLy7hL1.js → Tray-BSsxznDm.js} +1 -1
  4. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  5. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  8. package/package.json +4 -3
  9. package/src/adapters/google-vertex-endpoint.ts +9 -1
  10. package/src/adapters/google-vertex-key-project.ts +223 -0
  11. package/src/adapters/google.ts +13 -3
  12. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  13. package/src/adapters/openai-chat.ts +2 -2
  14. package/src/adapters/openai-responses/request-strips.ts +60 -0
  15. package/src/adapters/registry.ts +5 -0
  16. package/src/adapters/workbuddy-schema.ts +61 -0
  17. package/src/adapters/workbuddy.ts +68 -0
  18. package/src/bridge/response-json.ts +1 -1
  19. package/src/bridge/sse.ts +1 -1
  20. package/src/chat/outbound.ts +11 -4
  21. package/src/cli/capabilities-agents-routing.ts +3 -3
  22. package/src/cli/codex-cli-update.ts +8 -0
  23. package/src/cli/combo-result.ts +6 -0
  24. package/src/cli/combo-stats.ts +3 -0
  25. package/src/cli/combo.ts +66 -42
  26. package/src/cli/launcher-context.ts +49 -2
  27. package/src/cli/link.ts +45 -32
  28. package/src/cli/provider.ts +2 -1
  29. package/src/cli/usage-compare.ts +68 -12
  30. package/src/client/link-ingress.ts +1 -1
  31. package/src/client/link-readiness.ts +97 -0
  32. package/src/client/link-relay-transport.ts +79 -11
  33. package/src/client/link-relay.ts +74 -15
  34. package/src/client/link-tunnel.ts +45 -18
  35. package/src/client/machine-listener.ts +20 -2
  36. package/src/client/runtime.ts +20 -2
  37. package/src/codex/account-store.ts +17 -3
  38. package/src/codex/app-server-restart-service.ts +6 -5
  39. package/src/codex/catalog/effort.ts +9 -2
  40. package/src/codex/catalog/gather-capture.ts +4 -2
  41. package/src/codex/catalog/provider-models.ts +14 -8
  42. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  43. package/src/codex/cli-install-provenance.ts +418 -0
  44. package/src/codex/cli-installation-targets.ts +5 -1
  45. package/src/codex/cli-update-plan.ts +43 -16
  46. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  47. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  48. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  49. package/src/codex/pool-refresh-backoff.ts +3 -1
  50. package/src/codex/user-identity.ts +35 -6
  51. package/src/codex/windows-current-sid.ts +162 -0
  52. package/src/combos/jev-quota-activation.ts +5 -1
  53. package/src/combos/jev-quota-route.ts +1 -1
  54. package/src/combos/jev-quota.ts +9 -1
  55. package/src/combos/jev-session-pin-config.ts +51 -0
  56. package/src/combos/jev.ts +6 -0
  57. package/src/combos/types.ts +19 -0
  58. package/src/config/provider-validation.ts +7 -0
  59. package/src/config/schema/leaf-validators.ts +1 -0
  60. package/src/generated/compatibility-version.json +194 -122
  61. package/src/lib/decision-quota-sink.ts +10 -0
  62. package/src/lib/translator-budget.ts +4 -4
  63. package/src/lib/upstream-retry.ts +4 -1
  64. package/src/link/diagnostics.ts +193 -0
  65. package/src/link/relay-auth.ts +6 -0
  66. package/src/link/status-projection.ts +39 -0
  67. package/src/link/supervisor.ts +45 -12
  68. package/src/link/tunnel-history.ts +109 -0
  69. package/src/link/tunnel-state.ts +80 -17
  70. package/src/messaging/remote-files.ts +24 -1
  71. package/src/oauth/generic-account-failover.ts +91 -7
  72. package/src/oauth/index.ts +15 -0
  73. package/src/oauth/model-discovery-auth.ts +5 -0
  74. package/src/oauth/store.ts +4 -1
  75. package/src/oauth/token-guardian.ts +7 -0
  76. package/src/oauth/types.ts +9 -0
  77. package/src/oauth/workbuddy.ts +139 -0
  78. package/src/providers/antigravity-quota-identity.ts +37 -0
  79. package/src/providers/base-url-choices.ts +8 -5
  80. package/src/providers/derive.ts +23 -3
  81. package/src/providers/model-discovery.ts +2 -0
  82. package/src/providers/model-rename-fields.ts +1 -0
  83. package/src/providers/quota/account-cache.ts +3 -2
  84. package/src/providers/quota/antigravity.ts +25 -14
  85. package/src/providers/quota/report-cache.ts +13 -4
  86. package/src/providers/quota/vendor-probes-key.ts +60 -0
  87. package/src/providers/quota/workbuddy.ts +83 -0
  88. package/src/providers/quota-decision-antigravity.ts +64 -0
  89. package/src/providers/quota-decision-publication.ts +11 -0
  90. package/src/providers/quota-decision-snapshot.ts +21 -4
  91. package/src/providers/quota-types.ts +2 -0
  92. package/src/providers/quota.ts +3 -1
  93. package/src/providers/registry/entries-core.ts +9 -0
  94. package/src/providers/registry/entries-extended.ts +20 -7
  95. package/src/providers/registry/model-ids.ts +3 -0
  96. package/src/providers/registry/model-seeds.ts +27 -28
  97. package/src/providers/registry/types.ts +19 -1
  98. package/src/providers/resolved-model-policy.ts +4 -4
  99. package/src/providers/routed-config.ts +15 -0
  100. package/src/providers/stale-model-roster-migration.ts +83 -15
  101. package/src/providers/workbuddy-credit-balance.ts +81 -0
  102. package/src/providers/workbuddy-models.ts +37 -0
  103. package/src/providers/workbuddy.ts +60 -0
  104. package/src/responses/reasoning-replay-cache.ts +18 -14
  105. package/src/router.ts +2 -0
  106. package/src/routing/analytics.ts +2 -3
  107. package/src/routing/fallback-policy.ts +3 -3
  108. package/src/server/auth-cors.ts +1 -0
  109. package/src/server/chat-completions.ts +14 -1
  110. package/src/server/chat-native-reasoning.ts +10 -3
  111. package/src/server/chat-native.ts +13 -2
  112. package/src/server/claude-messages.ts +3 -0
  113. package/src/server/index/link-relay-sessions.ts +9 -5
  114. package/src/server/index/optional-listeners.ts +4 -0
  115. package/src/server/local-management-read-client.ts +3 -1
  116. package/src/server/management/combo-routes.ts +10 -4
  117. package/src/server/management/link-routes.ts +5 -2
  118. package/src/server/management/provider-routes.ts +1 -1
  119. package/src/server/port-reclaim.ts +51 -9
  120. package/src/server/request-log-conversation.ts +49 -0
  121. package/src/server/request-log.ts +2 -0
  122. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  123. package/src/server/responses/compact.ts +10 -2
  124. package/src/server/responses/core-combo.ts +53 -31
  125. package/src/server/responses/core-lifetime.ts +2 -2
  126. package/src/server/responses/core-normalize.ts +6 -3
  127. package/src/server/responses/core-opaque-recovery.ts +1 -0
  128. package/src/server/responses/core-options.ts +5 -0
  129. package/src/server/responses/core-replay.ts +2 -1
  130. package/src/server/responses/jev-combo-choice.ts +39 -0
  131. package/src/server/responses/jev-session-pin.ts +203 -0
  132. package/src/server/responses/passthrough-delivery.ts +4 -0
  133. package/src/server/responses/request-prepare.ts +10 -5
  134. package/src/server/responses/request-transport.ts +25 -6
  135. package/src/server/responses/run-turn-execution.ts +2 -4
  136. package/src/server/responses/sidecar-execution.ts +2 -4
  137. package/src/server/responses-phase-inference.ts +267 -0
  138. package/src/server/sse-payload-rewrite.ts +14 -1
  139. package/src/service/definition.ts +20 -8
  140. package/src/types/config.ts +6 -0
  141. package/src/types/provider.ts +9 -1
  142. package/src/types/request.ts +1 -0
  143. package/src/types/wire.ts +1 -0
  144. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  145. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  146. package/src/usage/jev-stats.ts +54 -14
  147. package/gui/dist/assets/index-DGXS51sp.js +0 -94
  148. package/gui/dist/assets/quota-summary-DO1Sx5TM.js +0 -1
@@ -0,0 +1,37 @@
1
+ import { createHash } from "node:crypto";
2
+ import { effectiveGoogleMode, getProviderRegistryEntry } from "./registry";
3
+ import type { OAuthCredentials } from "../oauth/types";
4
+ import type { OcxProviderConfig } from "../types";
5
+
6
+ const ARGON_TOKEN = /(?:^|[^a-z0-9])argon(?:[^a-z0-9]|$)/i;
7
+ /** Argon has an independent quota pool, including its native effort-suffixed identities. */
8
+ export function isAntigravityArgonIdentity(identity: string): boolean { return ARGON_TOKEN.test(identity); }
9
+
10
+ /**
11
+ * The OAuth store key whose account both inference and the quota probe resolve. Inference reads
12
+ * `store[route.providerName]` and refreshes through the OAuth definition registered under that same
13
+ * name, and `ocx login` only accepts registered names, so a renamed provider row never authenticates
14
+ * as this account. Evidence is bound to this key alone; a renamed row reads unknown.
15
+ */
16
+ export const ANTIGRAVITY_OAUTH_STORE_KEY = "google-antigravity";
17
+
18
+ /** Accounting and inference must refer to the same OAuth store entry and the supported Antigravity destination. */
19
+ export function antigravityQuotaEligible(name: string, provider: OcxProviderConfig): boolean {
20
+ if (name !== ANTIGRAVITY_OAUTH_STORE_KEY || provider.disabled || provider.authMode !== "oauth"
21
+ || effectiveGoogleMode(name, provider) !== "cloud-code-assist") return false;
22
+ try {
23
+ const url = new URL(provider.baseUrl);
24
+ const expected = new URL(getProviderRegistryEntry(ANTIGRAVITY_OAUTH_STORE_KEY)!.baseUrl);
25
+ return url.href.replace(/\/$/, "") === expected.href.replace(/\/$/, "");
26
+ } catch { return false; }
27
+ }
28
+
29
+ /** Private generation includes the accounting project, not just the rotating bearer. */
30
+ export function antigravityQuotaCredentialGeneration(credential: OAuthCredentials): string {
31
+ return createHash("sha256").update(JSON.stringify([credential.refresh, credential.access, credential.expires, credential.projectId])).digest("hex");
32
+ }
33
+
34
+ /** Immutable instance/destination policy, never returned to decision services. */
35
+ export function antigravityQuotaPolicy(name: string, provider: OcxProviderConfig): string {
36
+ return createHash("sha256").update(JSON.stringify([name, provider])).digest("hex");
37
+ }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Qwen Cloud OpenAI-compatible endpoint presets.
3
3
  * Token plan is the GUI default; pay-as-you-go and custom are selectable.
4
+ * Token plan host per https://docs.qwencloud.com/token-plan/personal/token-plan-personal-quickstart
5
+ * (checked 2026-10-23); a saved older host stays usable as a custom URL.
4
6
  */
5
7
  export const QWEN_CLOUD_TOKEN_PLAN_BASE_URL =
6
- "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
8
+ "https://token-plan.maas.qwencloudapi.com/compatible-mode/v1";
7
9
  export const QWEN_CLOUD_PAYG_BASE_URL =
8
10
  "https://dashscope.aliyuncs.com/compatible-mode/v1";
9
11
 
@@ -23,10 +25,11 @@ export const QWEN_CLOUD_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
23
25
 
24
26
  /**
25
27
  * Alibaba Token Plan International (ap-southeast-1) endpoint presets.
26
- * Same product as the Beijing Token Plan but for international accounts.
27
- * Note: ALIBABA_INTL_TOKEN_PLAN_BASE_URL intentionally duplicates
28
- * QWEN_CLOUD_TOKEN_PLAN_BASE_URL — same host, different product branding
29
- * and model lineup. Kept as a separate constant for clarity.
28
+ * Separate regional account and billing destination; both regions offer both tiers.
29
+ * Alibaba Cloud's own quick start still documents this host (checked 2026-10-23):
30
+ * https://www.alibabacloud.com/help/en/model-studio/token-plan-personal-quick-start
31
+ * Qwen Cloud documents a different Token Plan host, so the two constants are
32
+ * independent and must not be assumed equal.
30
33
  */
31
34
  export const ALIBABA_INTL_TOKEN_PLAN_BASE_URL =
32
35
  "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
@@ -3,6 +3,7 @@ import { COST_VENDOR_PRIORITY, getModelMetadata, getModelMetadataCaseInsensitive
3
3
  import { cloneFastWire } from "./fastwire";
4
4
  import { isAzureModelMetadataDestination, publishedAzureModelMetadata, type AzureModelMetadata } from "./azure-model-metadata";
5
5
  import { resolveModelPolicy } from "./resolved-model-policy";
6
+ import { mapFill, nestedMapFill, stableUnion } from "./resolved-model-policy-merge";
6
7
  import {
7
8
  PROVIDER_REGISTRY,
8
9
  registryEntryForProviderDestination,
@@ -56,6 +57,7 @@ export interface DerivedKeyLoginProvider {
56
57
  thinkingBudgetModels?: string[];
57
58
  escapeBuiltinToolNames?: boolean;
58
59
  openaiChatEofTolerance?: boolean;
60
+ foldDeveloperRoleToSystem?: boolean;
59
61
  googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
60
62
  project?: string;
61
63
  location?: string;
@@ -280,6 +282,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
280
282
  ...(entry.promptCacheKey !== undefined ? { promptCacheKey: entry.promptCacheKey } : {}),
281
283
  ...(entry.chatServiceTier !== undefined ? { chatServiceTier: entry.chatServiceTier } : {}),
282
284
  ...(entry.openaiChatEofTolerance !== undefined ? { openaiChatEofTolerance: entry.openaiChatEofTolerance } : {}),
285
+ ...(entry.foldDeveloperRoleToSystem !== undefined ? { foldDeveloperRoleToSystem: entry.foldDeveloperRoleToSystem } : {}),
283
286
  ...(entry.responsesPath !== undefined ? { responsesPath: entry.responsesPath } : {}),
284
287
  ...(entry.chatCompletionsPath !== undefined ? { chatCompletionsPath: entry.chatCompletionsPath } : {}),
285
288
  ...(entry.statelessResponses !== undefined ? { statelessResponses: entry.statelessResponses } : {}),
@@ -312,6 +315,9 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
312
315
  const out: Record<string, DerivedKeyLoginProvider> = {};
313
316
  for (const entry of PROVIDER_REGISTRY) {
314
317
  if (entry.authKind !== "key") continue;
318
+ // A deprecated route cannot complete a login, but its entry must stay resolvable for
319
+ // configs saved while it still worked.
320
+ if (entry.deprecated) continue;
315
321
  if (!entry.dashboardUrl) throw new Error(`Registry key provider missing dashboardUrl: ${entry.id}`);
316
322
  const liveModels = registryEntrySupportsLiveModelDiscovery(entry) ? entry.liveModels : false;
317
323
  out[entry.id] = {
@@ -355,6 +361,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
355
361
  ...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
356
362
  ...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
357
363
  ...(entry.openaiChatEofTolerance !== undefined ? { openaiChatEofTolerance: entry.openaiChatEofTolerance } : {}),
364
+ ...(entry.foldDeveloperRoleToSystem !== undefined ? { foldDeveloperRoleToSystem: entry.foldDeveloperRoleToSystem } : {}),
358
365
  ...(entry.googleMode ? { googleMode: entry.googleMode } : {}),
359
366
  ...(entry.project ? { project: entry.project } : {}),
360
367
  ...(entry.location ? { location: entry.location } : {}),
@@ -364,7 +371,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
364
371
  }
365
372
 
366
373
  export function deriveInitProviders(): DerivedInitProvider[] {
367
- return PROVIDER_REGISTRY.map(entry => ({
374
+ return PROVIDER_REGISTRY.filter(entry => !entry.deprecated).map(entry => ({
368
375
  id: entry.id,
369
376
  label: formatInitLabel(entry),
370
377
  adapter: entry.adapter,
@@ -391,6 +398,7 @@ export function deriveOAuthIds(): string[] {
391
398
 
392
399
  export function deriveProviderPresets(): DerivedProviderPreset[] {
393
400
  const presets = PROVIDER_REGISTRY
401
+ .filter(entry => !entry.deprecated)
394
402
  .filter(entry => entry.featured || entry.authKind === "key" || entry.dashboardPreset)
395
403
  .map(entryToPreset);
396
404
  return [...dedupePresets(presets), customPreset()];
@@ -470,13 +478,22 @@ function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistry
470
478
  * was skipped and the reasoning ladder was advertised without summary support — exactly the
471
479
  * inconsistency that makes Codex drop the inbound reasoning object.
472
480
  *
473
- * Deliberately narrow: reasoning-summary, effort, and replay-compatibility metadata only, via
481
+ * Deliberately narrow: reasoning-summary, effort, structured-output, and replay-compatibility metadata only, via
474
482
  * `registryEntryForProviderDestination`, which matches fixed key destinations and refuses
475
483
  * templated or overridable base URLs. A custom row keeps its own identity for everything else.
476
484
  */
477
485
  function enrichReasoningMetadataByDestination(prov: OcxProviderConfig): void {
478
486
  const destination = registryEntryForProviderDestination(prov);
479
487
  applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries);
488
+ if (destination?.modelDefaultReasoningEfforts) {
489
+ prov.modelDefaultReasoningEfforts = mapFill(destination.modelDefaultReasoningEfforts, prov.modelDefaultReasoningEfforts)[0];
490
+ }
491
+ if (destination?.modelReasoningEffortMap) {
492
+ prov.modelReasoningEffortMap = nestedMapFill(destination.modelReasoningEffortMap, prov.modelReasoningEffortMap)[0];
493
+ }
494
+ if (destination?.noStructuredOutputModels?.length) {
495
+ prov.noStructuredOutputModels = stableUnion(destination.noStructuredOutputModels, prov.noStructuredOutputModels)[0];
496
+ }
480
497
  if (prov.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined) {
481
498
  prov.dropResponsesReasoningItems = destination.dropResponsesReasoningItems;
482
499
  }
@@ -567,6 +584,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
567
584
  if (prov.openaiChatEofTolerance === undefined && seed.openaiChatEofTolerance !== undefined) {
568
585
  prov.openaiChatEofTolerance = seed.openaiChatEofTolerance;
569
586
  }
587
+ if (prov.foldDeveloperRoleToSystem === undefined && seed.foldDeveloperRoleToSystem !== undefined) {
588
+ prov.foldDeveloperRoleToSystem = seed.foldDeveloperRoleToSystem;
589
+ }
570
590
  // Fill-only: a hand-edited path must survive, and a config saved before the registry
571
591
  // learned this route still gets backfilled.
572
592
  if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
@@ -633,7 +653,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
633
653
  }
634
654
 
635
655
  export function deriveFeaturedProviderIds(): string[] {
636
- return PROVIDER_REGISTRY.filter(entry => entry.featured).map(entry => entry.id);
656
+ return PROVIDER_REGISTRY.filter(entry => entry.featured && !entry.deprecated).map(entry => entry.id);
637
657
  }
638
658
 
639
659
  export function deriveJawcodeAliases(): Record<string, string> {
@@ -1,3 +1,4 @@
1
+ import { extractWorkBuddyModels } from "./workbuddy-models";
1
2
  import type { OcxProviderConfig } from "../types";
2
3
  import {
3
4
  isValidModelDiscoveryModelId,
@@ -513,6 +514,7 @@ export function extractProviderModelItems(
513
514
  discovery: ResolvedProviderModelDiscovery,
514
515
  ): ProviderModelItemsResult {
515
516
  const limit = positiveIntegerAtMost(discovery.maxModels, MODEL_DISCOVERY_MAX_MODELS);
517
+ if (discovery.spec?.format === "workbuddy") return extractWorkBuddyModels(value, limit);
516
518
  let data: unknown[];
517
519
  let siblings: SiblingIndex | null = null;
518
520
  if (Array.isArray(value)) {
@@ -153,6 +153,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
153
153
  desktopExecutor: "none",
154
154
  unsafeAllowNativeLocalExec: "none",
155
155
  nativeLocalExec: "none",
156
+ messagePhaseInference: "none",
156
157
  tlsProfile: "none",
157
158
  } as const satisfies Record<keyof OcxProviderConfig, ModelRenameRole>;
158
159
 
@@ -203,7 +203,7 @@ export function supportsPerAccountQuota(provider: string): boolean {
203
203
 
204
204
  export function explicitAccountReader(provider: string): boolean {
205
205
  return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code"
206
- || provider === "devin" || provider === "dsh-account";
206
+ || provider === "devin" || provider === "workbuddy-ai" || provider === "dsh-account";
207
207
  }
208
208
 
209
209
  export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode {
@@ -558,6 +558,7 @@ export function quotaCredentialIdentity(provider: string, accountId: string, cre
558
558
  // Only credentials that carry their own endpoint (Devin tenants) extend the identity, so
559
559
  // every other provider's existing cache keys stay valid.
560
560
  ...(provider === "devin" && credential.apiBaseUrl ? [credential.apiBaseUrl] : []),
561
+ ...(provider === "workbuddy-ai" ? [credential.workbuddy] : []),
561
562
  ])).digest("hex");
562
563
  }
563
564
 
@@ -567,5 +568,5 @@ export function explicitQuotaDestination(provider: string, config: OcxProviderCo
567
568
  if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl);
568
569
  // These readers use fixed canonical billing origins, never config.baseUrl. Devin reads
569
570
  // the credential's own allowlisted api-server host instead (fetchDevinQuota revalidates it).
570
- return provider === "xai" || provider === "cursor" || provider === "devin" || provider === "dsh-account";
571
+ return provider === "xai" || provider === "cursor" || provider === "devin" || provider === "workbuddy-ai" || provider === "dsh-account";
571
572
  }
@@ -3,9 +3,13 @@ import { DestinationDnsResolutionError } from "../../lib/destination-policy";
3
3
  import { PinnedHttpError, type PinnedHttpErrorCode } from "../../lib/pinned-http";
4
4
  import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../../lib/provider-outbound";
5
5
  import { getValidAccessToken } from "../../oauth";
6
- import { getAccountCredential, getCredential } from "../../oauth/store";
6
+ import { getAccountCredential, getAccountSet, getCredential } from "../../oauth/store";
7
7
  import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
8
- import { report, type ProviderQuotaReport } from "./report-cache";
8
+ import { antigravityEvidence, report, type ProviderQuotaReport } from "./report-cache";
9
+ import { captureAntigravityQuotaReceipt, decisionQuotaSinkActive } from "../../lib/decision-quota-sink";
10
+ import { antigravityDecisionWindows } from "../quota-decision-publication";
11
+ import { antigravityQuotaCredentialGeneration, isAntigravityArgonIdentity } from "../antigravity-quota-identity";
12
+ import type { OcxProviderConfig } from "../../types";
9
13
  import { quotaCredentialIdentity } from "./account-cache";
10
14
  import type { ProviderQuota, ProviderQuotaWindow, QuotaFailureCode } from "../quota-types";
11
15
  import { httpQuotaFailure } from "./failure-codes";
@@ -46,11 +50,6 @@ function quotaInfoEntries(modelInfo: Record<string, unknown>): Record<string, un
46
50
  return entries;
47
51
  }
48
52
 
49
- /**
50
- * Gemini 4 Argon has its own quota pool. Its names also contain "gemini", so every family
51
- * decision below checks Argon first; otherwise Argon readings fill or replace the Gem family.
52
- */
53
- const ARGON_TOKEN = /(?:^|[^a-z0-9])argon(?:[^a-z0-9]|$)/i;
54
53
  const GEMINI_TOKEN = /(?:^|[^a-z0-9])gemini(?:[^a-z0-9]|$)/i;
55
54
  const CLAUDE_FAMILY_TOKEN = /(?:^|[^a-z0-9])(?:claude|3p|gpt|opus|sonnet)(?:[^a-z0-9]|$)/i;
56
55
  /** Canonical model of the Argon summary pool; tagging keeps the window out of routing. */
@@ -61,7 +60,7 @@ type AntigravitySummaryFamily = "argon" | "gem" | "cla";
61
60
  /** A bucket's own id and name decide its family when they name one; the group decides otherwise. */
62
61
  function antigravitySummaryBucketFamily(bucket: Record<string, unknown>): AntigravitySummaryFamily | undefined {
63
62
  const identity = `${typeof bucket.bucketId === "string" ? bucket.bucketId : ""} ${typeof bucket.displayName === "string" ? bucket.displayName : ""}`;
64
- if (ARGON_TOKEN.test(identity)) return "argon";
63
+ if (isAntigravityArgonIdentity(identity)) return "argon";
65
64
  if (GEMINI_TOKEN.test(identity)) return "gem";
66
65
  if (CLAUDE_FAMILY_TOKEN.test(identity)) return "cla";
67
66
  return undefined;
@@ -70,7 +69,7 @@ function antigravitySummaryBucketFamily(bucket: Record<string, unknown>): Antigr
70
69
  function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, unknown>, quotaInfo: Record<string, unknown>): "Gem" | "Cla" | null {
71
70
  const displayName = typeof modelInfo.displayName === "string" ? modelInfo.displayName : "";
72
71
  // Argon models keep their exact model rows but never seed the shared Gem family bar.
73
- if (ARGON_TOKEN.test(`${modelId} ${displayName}`)) return null;
72
+ if (isAntigravityArgonIdentity(`${modelId} ${displayName}`)) return null;
74
73
  const tier = typeof quotaInfo.tier === "string" ? quotaInfo.tier : "";
75
74
  const haystack = `${modelId} ${displayName} ${tier}`.toLowerCase();
76
75
  if (haystack.includes("gemini")) return "Gem";
@@ -155,7 +154,7 @@ function parseAntigravityQuotaSummary(body: Record<string, unknown> | null): Pro
155
154
  const groupDisplayName = typeof group.displayName === "string" ? group.displayName : "";
156
155
  const groupName = `${groupDisplayName} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase();
157
156
  // A description that merely mentions Argon must not move a whole Gemini group.
158
- const groupFamily: AntigravitySummaryFamily | undefined = ARGON_TOKEN.test(groupDisplayName) ? "argon"
157
+ const groupFamily: AntigravitySummaryFamily | undefined = isAntigravityArgonIdentity(groupDisplayName) ? "argon"
159
158
  : groupName.includes("gemini") ? "gem"
160
159
  : groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt") ? "cla"
161
160
  : undefined;
@@ -360,13 +359,25 @@ export async function fetchAntigravityUsageQuota(accessToken: string, projectId:
360
359
  return null;
361
360
  }
362
361
 
363
- export async function fetchAntigravityQuota(provider: string): Promise<ProviderQuotaReport | null> {
364
- const credential = getCredential("google-antigravity");
362
+ /** Every store read uses `provider`, the same key inference resolves, so a row never probes another row's account. */
363
+ export async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
364
+ const credential = getCredential(provider);
365
365
  if (!credential?.projectId) return null;
366
366
  let accessToken: string;
367
- try { accessToken = await getValidAccessToken("google-antigravity"); } catch { return null; }
367
+ try { accessToken = await getValidAccessToken(provider); } catch { return null; }
368
+ const set = decisionQuotaSinkActive() ? getAccountSet(provider) : undefined;
369
+ const account = set?.accounts.find(row => row.id === set.activeAccountId);
370
+ const probed = account?.credential.access === accessToken && account.credential.projectId === credential.projectId ? account : undefined;
371
+ const receipt = probed ? captureAntigravityQuotaReceipt(provider, config, probed.id, antigravityQuotaCredentialGeneration(probed.credential)) : undefined;
368
372
  const result = await probeAntigravityUsageQuota(accessToken, credential.projectId);
369
- if (result.kind === "available") return report(provider, result.source, result.quota);
373
+ if (result.kind === "available") {
374
+ const value = report(provider, result.source, result.quota);
375
+ // Only the summary endpoint reports real family buckets. The models fallback labels one model's
376
+ // counter Gem/Cla, so it stays display-only: no windows, and retained summary clocks survive.
377
+ const windows = result.source === "google-antigravity:retrieveUserQuotaSummary" ? antigravityDecisionWindows(result.quota) : [];
378
+ if (value && receipt) antigravityEvidence.set(value, { receipt, windows: Object.freeze(windows.map(row => Object.freeze(row))) });
379
+ return value;
380
+ }
370
381
  if (result.legacy.kind === "throw") throw result.legacy.error;
371
382
  return null;
372
383
  }
@@ -1,5 +1,6 @@
1
- import { decisionQuotaSinkActive, invalidateDecisionKeyQuotas, publishDecisionKeyQuota } from "../../lib/decision-quota-sink";
1
+ import { decisionQuotaSinkActive, invalidateDecisionKeyQuotas, replaceCollectedDecisionQuotas, type AntigravityQuotaObservation, type AntigravityQuotaReceipt, type DecisionKeyObservation } from "../../lib/decision-quota-sink";
2
2
  import { providerDecisionWindows } from "../quota-decision-publication";
3
+ import type { DecisionQuotaWindow } from "../quota-decision-snapshot";
3
4
  import { createHash } from "node:crypto";
4
5
  import { effectiveCodexAuthAccountId, listCodexAuthAccountsSnapshot } from "../../codex/auth-api";
5
6
  import { withoutRetiredCodexQuota, type StoredAccountQuota } from "../../codex/quota";
@@ -21,6 +22,8 @@ export const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
21
22
  const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
22
23
  export const accountReportCurrent = new WeakMap<ProviderQuotaReport, () => boolean>();
23
24
  export const routingEvidence = new WeakMap<ProviderQuotaReport, ProviderQuotaRoutingEvidence>();
25
+ /** Only the live Antigravity probe binds this projection; display/last-good rows cannot create it. */
26
+ export const antigravityEvidence = new WeakMap<ProviderQuotaReport, { receipt: AntigravityQuotaReceipt; windows: readonly DecisionQuotaWindow[] }>();
24
27
  export let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
25
28
 
26
29
  /** Test-only seam for identity/config invalidation after probes but before publication. */
@@ -100,6 +103,7 @@ export function clearProviderQuotaCache(): void {
100
103
  cache = null;
101
104
  clearCachedProviderQuotas();
102
105
  clearProviderApiKeyQuotaCache();
106
+ invalidateDecisionKeyQuotas();
103
107
  invalidationEpoch += 1;
104
108
  }
105
109
 
@@ -350,13 +354,18 @@ export function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolea
350
354
  /** Called only by an accepted collector/cache publication, never the JEV read path. */
351
355
  export function publishCollectedDecisionQuota(reports: ProviderQuotaReport[], config: OcxConfig): void {
352
356
  if (!decisionQuotaSinkActive()) return;
353
- invalidateDecisionKeyQuotas();
357
+ const keys: DecisionKeyObservation[] = [];
358
+ const oauth: AntigravityQuotaObservation[] = [];
354
359
  for (const report of reports) {
355
360
  const provider = config.providers[report.provider];
361
+ if (!provider || !isProviderQuotaReportCurrent(report)) continue;
362
+ const observation = antigravityEvidence.get(report);
363
+ if (observation) oauth.push({ name: report.provider, provider, ...observation });
356
364
  const evidence = routingEvidence.get(report);
357
- if (provider && evidence && evidence.binding === providerQuotaRoutingBinding(report.provider, provider))
358
- publishDecisionKeyQuota(report.provider, provider, providerDecisionWindows(evidence.quota));
365
+ if (evidence && evidence.binding === providerQuotaRoutingBinding(report.provider, provider))
366
+ keys.push({ name: report.provider, provider, windows: providerDecisionWindows(evidence.quota) });
359
367
  }
368
+ replaceCollectedDecisionQuotas(keys, oauth);
360
369
  }
361
370
 
362
371
  /**
@@ -47,6 +47,8 @@ const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
47
47
  const SYNTHETIC_BASE_URL = "https://api.synthetic.new/v2";
48
48
  const DEEPINFRA_BASE_URL = "https://api.deepinfra.com";
49
49
  const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1";
50
+ const ABLITERATION_BASE_URL = "https://api.abliteration.ai";
51
+ const ABLITERATION_CREDITS_URL = `${ABLITERATION_BASE_URL}/v1/credits`;
50
52
 
51
53
 
52
54
  function isCanonicalA6apiBaseUrl(baseUrl: string): boolean {
@@ -132,6 +134,11 @@ function isCanonicalNeuralwattBaseUrl(baseUrl: string): boolean {
132
134
  return normalizedBaseUrl(baseUrl) === NEURALWATT_BASE_URL;
133
135
  }
134
136
 
137
+ function isCanonicalAbliterationBaseUrl(baseUrl: string): boolean {
138
+ const normalized = normalizedBaseUrl(baseUrl);
139
+ return normalized === ABLITERATION_BASE_URL || normalized === `${ABLITERATION_BASE_URL}/v1`;
140
+ }
141
+
135
142
  function a6apiPayload(value: unknown): Record<string, unknown> | null {
136
143
  const body = asRecord(value);
137
144
  return asRecord(body?.data) ?? body;
@@ -1043,6 +1050,56 @@ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig)
1043
1050
  return windows > 0 ? report(provider, "neuralwatt:quota", quota) : null;
1044
1051
  }
1045
1052
 
1053
+ /**
1054
+ * Abliteration `GET /v1/credits` body: `{ data: { total_credits, total_usage } }`, both in USD.
1055
+ * `total_credits` is everything granted or purchased and `total_usage` is finalized spend, so the
1056
+ * balance is their difference. A well-formed reply with zero total credits is authoritative: it has
1057
+ * no ceiling to meter against, so it clears any previous row instead of fabricating a bar. A
1058
+ * malformed reply returns null and leaves the last-good row in place.
1059
+ */
1060
+ export function parseAbliterationCredits(
1061
+ body: Record<string, unknown> | null,
1062
+ ): ProviderQuota | typeof AUTHORITATIVE_EMPTY_QUOTA | null {
1063
+ const data = asRecord(body?.data);
1064
+ const total = toFiniteNumber(data?.total_credits);
1065
+ const used = toFiniteNumber(data?.total_usage);
1066
+ if (total === undefined || used === undefined || total < 0 || used < 0) return null;
1067
+ if (total === 0) return AUTHORITATIVE_EMPTY_QUOTA;
1068
+ const percent = normalizePercent((used / total) * 100);
1069
+ if (percent === undefined) return null;
1070
+ return {
1071
+ creditsUsd: { used, limit: total, remaining: Math.max(0, total - used), percent },
1072
+ updatedAt: Date.now(),
1073
+ };
1074
+ }
1075
+
1076
+ /**
1077
+ * Abliteration `GET /v1/credits` — the organization credit balance for the active project key.
1078
+ * The balance funds every inference request on this key, so display and routing scope agree.
1079
+ * `503 credit_balance_unavailable`, 408 and 429 are transient and keep the last-good row.
1080
+ */
1081
+ async function fetchAbliterationQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1082
+ // Never send a configured API key to a lookalike host or through a redirect.
1083
+ if (!isCanonicalAbliterationBaseUrl(config.baseUrl)) return null;
1084
+ const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
1085
+ if (!apiKey) return null;
1086
+ const response = await quotaFetch(provider, config, ABLITERATION_CREDITS_URL, {
1087
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1088
+ redirect: "error",
1089
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1090
+ });
1091
+ if (!response.ok) {
1092
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1093
+ ? TERMINAL_QUOTA_FAILURE
1094
+ : null;
1095
+ }
1096
+ const raw = await readQuotaJson(response);
1097
+ if (raw === QUOTA_JSON_READ_FAILURE) return null;
1098
+ const quota = parseAbliterationCredits(asRecord(raw));
1099
+ if (quota === null || quota === AUTHORITATIVE_EMPTY_QUOTA) return quota;
1100
+ return keyReport(provider, "abliteration:credits", quota, config, apiKey, quota);
1101
+ }
1102
+
1046
1103
 
1047
1104
  export function normalizedBaseUrl(value: string): string | null {
1048
1105
  try {
@@ -1371,6 +1428,9 @@ export function keyQuotaReaderForProvider(name: string, provider: OcxProviderCon
1371
1428
  if (name === "synthetic" && isCanonicalSyntheticBaseUrl(provider.baseUrl)) return fetchSyntheticQuota;
1372
1429
  if (name === "deepinfra" && isCanonicalDeepInfraBaseUrl(provider.baseUrl)) return fetchDeepInfraQuota;
1373
1430
  if (name === "neuralwatt" && isCanonicalNeuralwattBaseUrl(provider.baseUrl)) return fetchNeuralwattQuota;
1431
+ // Abliteration has no registry preset, so it is usually a custom provider under any name; the
1432
+ // canonical-URL guard alone decides eligibility and keeps the key off lookalike hosts.
1433
+ if (isCanonicalAbliterationBaseUrl(provider.baseUrl)) return fetchAbliterationQuota;
1374
1434
  return null;
1375
1435
  }
1376
1436
 
@@ -0,0 +1,83 @@
1
+ import type { OAuthCredentials } from "../../oauth/types";
2
+ import { workBuddyApi, WorkBuddyApiError } from "../../oauth/workbuddy";
3
+ import { workBuddyHeaders } from "../workbuddy";
4
+ import { parseWorkBuddyCredits, workBuddyCapacity, workBuddyRecord, type WorkBuddyCreditBalance } from "../workbuddy-credit-balance";
5
+ import { REQUEST_TIMEOUT_MS } from "../quota-wire";
6
+ import { TERMINAL_QUOTA_FAILURE, type ProviderQuotaProbeResult } from "./report-cache";
7
+
8
+ function packageEnd(row: Record<string, unknown>): number | undefined {
9
+ const deduction = workBuddyCapacity(row.DeductionEndTime);
10
+ if (deduction && Number.isFinite(new Date(deduction).getTime())) return deduction;
11
+ for (const value of [row.ExpiredTime, row.CycleEndTime]) {
12
+ if (typeof value !== "string") continue;
13
+ const date = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)
14
+ ? `${value.replace(" ", "T")}+08:00` : value;
15
+ const parsed = Date.parse(date);
16
+ if (parsed > 0) return parsed;
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ export function workBuddyExpiringCredits(packages: unknown[], codes: ReadonlySet<string>, now: number): WorkBuddyCreditBalance["expiring"] {
22
+ let at = Infinity, amount = 0;
23
+ for (const raw of packages) {
24
+ const row = workBuddyRecord(raw);
25
+ if (typeof row.PackageCode !== "string" || !codes.has(row.PackageCode) || String(row.CapacityType) === "4") continue;
26
+ const remaining = workBuddyCapacity(row.CycleCapacityRemainPrecise || row.CycleCapacityRemain);
27
+ const end = packageEnd(row);
28
+ if (!remaining || !end || end <= now) continue;
29
+ if (end < at) { at = end; amount = remaining; }
30
+ else if (end === at) amount += remaining;
31
+ }
32
+ return Number.isFinite(at) && Number.isFinite(amount) ? { amount, at } : undefined;
33
+ }
34
+
35
+ async function expiringCredits(summary: Record<string, unknown>, headers: Record<string, string>, signal: AbortSignal, now: number) {
36
+ const packages = Array.isArray(summary.Packages) ? summary.Packages : [];
37
+ const codes = [...new Set(packages.map(raw => workBuddyRecord(raw).PackageCode)
38
+ .filter((code): code is string => typeof code === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(code)))];
39
+ if (!codes.length) return undefined;
40
+ const day = new Date(now + 8 * 3600_000).toISOString().slice(0, 10);
41
+ const lists = await Promise.all(["paid", "free"].map(async kind => {
42
+ const accounts: unknown[] = [];
43
+ for (let page = 1; page <= 10; page++) {
44
+ const data = await workBuddyApi(`/billing/meter/get-user-resource-${kind}-packages`, headers, signal, true, {
45
+ PageNumber: page, PageSize: 200, Status: [0, 3], PackageCodes: codes,
46
+ ...(kind === "paid" ? { NeedRenewInfo: true }
47
+ : { SlicePeriodStartTime: `${day} 00:00:00`, SlicePeriodEndTime: `${day} 23:59:59` }),
48
+ });
49
+ if (!Array.isArray(data.Accounts) || data.Accounts.length > 200) throw new Error("Invalid WorkBuddy package list");
50
+ accounts.push(...data.Accounts);
51
+ const total = workBuddyCapacity(data.TotalCount);
52
+ if (data.Accounts.length < 200 || total !== undefined && accounts.length >= total) return accounts;
53
+ }
54
+ throw new Error("WorkBuddy package list exceeded page limit");
55
+ }));
56
+ return workBuddyExpiringCredits(lists.flat(), new Set(codes), now);
57
+ }
58
+
59
+ export async function fetchWorkBuddyQuota(provider: string, credential: OAuthCredentials): Promise<ProviderQuotaProbeResult> {
60
+ if (!credential.workbuddy) return null;
61
+ const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS), now = Date.now();
62
+ try {
63
+ const headers = workBuddyHeaders(credential.access, credential.workbuddy);
64
+ const summary = await workBuddyApi("/billing/meter/get-user-resource-summary", headers, signal, true);
65
+ let credits = parseWorkBuddyCredits(summary);
66
+ if (credential.workbuddy.enterpriseId && (!credits || credits.total === 0)) {
67
+ const team = await workBuddyApi("/billing/meter/get-enterprise-user-usage", headers, signal, true);
68
+ const total = workBuddyCapacity(team.limitNum), used = workBuddyCapacity(team.credit);
69
+ if (total === undefined || used === undefined) return null;
70
+ credits = { total, used, remaining: Math.max(0, total - used), frozen: 0, groups: [] };
71
+ }
72
+ if (!credits) return null;
73
+ if (credits.remaining > 0) {
74
+ // Optional expiry failures must not erase a successful balance reading.
75
+ credits.expiring = await expiringCredits(summary, headers, signal, now).catch(() => undefined);
76
+ }
77
+ const updatedAt = Date.now();
78
+ return { provider, label: "WorkBuddy AI", source: "WorkBuddy credits", updatedAt,
79
+ reverseEngineered: true, quota: { updatedAt, workbuddyCredits: credits } };
80
+ } catch (error) {
81
+ return error instanceof WorkBuddyApiError && error.status === 401 ? TERMINAL_QUOTA_FAILURE : null;
82
+ }
83
+ }
@@ -0,0 +1,64 @@
1
+ /** Optional snapshot owner. No store reads: roster input comes only from loaded-store notifications. */
2
+ import { getConfigDir } from "../config/paths";
3
+ import { captureConfigGeneration } from "../lib/state-store-sweeper";
4
+ import { ANTIGRAVITY_OAUTH_STORE_KEY, antigravityQuotaCredentialGeneration, antigravityQuotaEligible, antigravityQuotaPolicy } from "./antigravity-quota-identity";
5
+ import type { AuthStore } from "../oauth/store";
6
+ import type { OcxProviderConfig } from "../types";
7
+ import type { AntigravityQuotaObservation, AntigravityQuotaReceipt } from "../lib/decision-quota-sink";
8
+ import type { DecisionQuotaWindow } from "./quota-decision-snapshot";
9
+
10
+ type Owner = { root: string; id: string; generation: string; revision: string };
11
+ type Receipt = Owner & { owner: Owner; name: string; policy: string; configGeneration: number; epoch: number };
12
+ let owner: Owner | undefined;
13
+ let epoch = 0;
14
+ const receipts = new WeakMap<AntigravityQuotaReceipt, Receipt>();
15
+ let observations = new Map<string, { receipt: Receipt; windows: readonly DecisionQuotaWindow[] }>();
16
+
17
+ /** Any roster transition retires old receipts, including switching away and back. */
18
+ export function observeAntigravityQuotaStore(store: AuthStore): void {
19
+ const set = store[ANTIGRAVITY_OAUTH_STORE_KEY];
20
+ const account = set?.accounts.length === 1 ? set.accounts[0] : undefined;
21
+ const next = account && set?.activeAccountId === account.id && !account.paused && !account.needsReauth
22
+ && account.credential.access && account.credential.projectId
23
+ ? { root: getConfigDir(), id: account.id, generation: antigravityQuotaCredentialGeneration(account.credential), revision: JSON.stringify([account.loginId, account.pauseRevision, set.selectionRevision]) } : undefined;
24
+ if (owner?.root === next?.root && owner?.id === next?.id && owner?.generation === next?.generation && owner?.revision === next?.revision) return;
25
+ owner = next;
26
+ invalidateAntigravityQuota();
27
+ }
28
+
29
+ /** Epoch fences also retire outstanding probes on explicit cache/evidence invalidation. */
30
+ export function invalidateAntigravityQuota(): void { epoch++; observations.clear(); }
31
+
32
+ export function captureAntigravityReceipt(name: string, provider: OcxProviderConfig, id: string, generation: string): AntigravityQuotaReceipt | undefined {
33
+ if (!owner || owner.root !== getConfigDir() || owner.id !== id || owner.generation !== generation || !antigravityQuotaEligible(name, provider)) return undefined;
34
+ const token: AntigravityQuotaReceipt = Object.freeze({ kind: "antigravity" });
35
+ receipts.set(token, { ...owner, owner, name, policy: antigravityQuotaPolicy(name, provider), configGeneration: captureConfigGeneration(), epoch });
36
+ return token;
37
+ }
38
+
39
+ function current(receipt: Receipt): boolean {
40
+ return receipt.owner === owner && receipt.root === getConfigDir() && receipt.epoch === epoch && receipt.configGeneration === captureConfigGeneration();
41
+ }
42
+
43
+ /** Only accepted, live immutable receipts can retain omitted family rows; clocks never change. */
44
+ export function replaceAntigravityQuotas(rows: readonly AntigravityQuotaObservation[]): void {
45
+ const next = new Map<string, { receipt: Receipt; windows: readonly DecisionQuotaWindow[] }>();
46
+ for (const row of rows.slice(0, 256)) {
47
+ const receipt = receipts.get(row.receipt);
48
+ if (!receipt) continue;
49
+ if (!current(receipt) || receipt.name !== row.name || !antigravityQuotaEligible(row.name, row.provider)
50
+ || receipt.policy !== antigravityQuotaPolicy(row.name, row.provider)) { receipts.delete(row.receipt); continue; }
51
+ const previous = observations.get(receipt.name);
52
+ const fresh = row.windows.length > 64 ? [] : row.windows.filter(window => window.family === "gem" || window.family === "cla");
53
+ const retained = previous && current(previous.receipt) && previous.receipt.owner === receipt.owner && previous.receipt.policy === receipt.policy
54
+ ? previous.windows.filter(window => !fresh.some(value => value.family === window.family && value.window === window.window)) : [];
55
+ next.set(receipt.name, { receipt, windows: [...retained, ...fresh].map(window => ({ ...window })) });
56
+ }
57
+ observations = next;
58
+ }
59
+
60
+ export function readAntigravityQuota(name: string, provider: OcxProviderConfig): readonly DecisionQuotaWindow[] | undefined {
61
+ const row = observations.get(name);
62
+ if (!row || !current(row.receipt) || !antigravityQuotaEligible(name, provider) || row.receipt.policy !== antigravityQuotaPolicy(name, provider)) return undefined;
63
+ return row.windows.map(window => ({ ...window }));
64
+ }
@@ -35,6 +35,17 @@ export function providerDecisionWindows(quota: ProviderQuota): DecisionQuotaWind
35
35
  }
36
36
  return windows;
37
37
  }
38
+ /** Antigravity family buckets: Gem/Cla are 5h counters, "(Weekly)" their weekly pair; per-model rows and other groups are display-only. */
39
+ export function antigravityDecisionWindows(quota: ProviderQuota): DecisionQuotaWindow[] {
40
+ const windows: DecisionQuotaWindow[] = [];
41
+ for (const row of Array.isArray(quota.customWindows) ? quota.customWindows : []) {
42
+ const match = typeof row?.label === "string" && row.modelId === undefined ? /^(Gem|Cla)( \(Weekly\))?$/.exec(row.label) : null;
43
+ if (!match || !Number.isFinite(row.percent)) continue;
44
+ windows.push({ window: match[2] ? "weekly" : "5h", family: match[1] === "Gem" ? "gem" : "cla", percent: row.percent,
45
+ observedAt: quota.updatedAt, ...(row.resetAt !== undefined ? { resetAt: row.resetAt } : {}) });
46
+ }
47
+ return windows;
48
+ }
38
49
  /** Publish decision windows for one Anthropic pool account, bound to its credential generation; a null quota publishes no windows. */
39
50
  export function publishAnthropicDecisionQuota(id: string, generation: string, quota: ProviderQuota | null, partial = false): void {
40
51
  if (!decisionQuotaSinkActive()) return;