@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.
- package/bin/ocx.mjs +37 -3
- package/gui/dist/assets/{App-nZ34cR5G.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-BuLy7hL1.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/request-strips.ts +60 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/outbound.ts +11 -4
- package/src/cli/capabilities-agents-routing.ts +3 -3
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link.ts +45 -32
- package/src/cli/provider.ts +2 -1
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +79 -11
- package/src/client/link-relay.ts +74 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +20 -2
- package/src/client/runtime.ts +20 -2
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/types.ts +19 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +194 -122
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/diagnostics.ts +193 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/status-projection.ts +39 -0
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +15 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/store.ts +4 -1
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +25 -14
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +3 -3
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/index/link-relay-sessions.ts +9 -5
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/link-routes.ts +5 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +10 -2
- package/src/server/responses/core-combo.ts +53 -31
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/request-prepare.ts +10 -5
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/service/definition.ts +20 -8
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/index-DGXS51sp.js +0 -94
- 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.
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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";
|
package/src/providers/derive.ts
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
364
|
-
|
|
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(
|
|
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")
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
358
|
-
|
|
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;
|