@bitkyc08/opencodex 2.90.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-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.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/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- 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/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +92 -0
- package/src/adapters/openai-responses/transport.ts +6 -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/inbound.ts +20 -2
- package/src/chat/outbound.ts +47 -8
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +53 -3
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/codex-shim-autorestore.ts +5 -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/dispatch.ts +30 -0
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +123 -35
- package/src/cli/provider.ts +46 -3
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +84 -9
- package/src/client/link-relay.ts +98 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +103 -3
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +155 -25
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/parsing.ts +4 -0
- 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/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/index.ts +1 -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/resolve.ts +12 -0
- package/src/combos/types.ts +19 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +375 -195
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/diagnostics.ts +193 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +63 -2
- 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 +16 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +6 -2
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- 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/mistral-transport.ts +37 -4
- 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 +27 -22
- 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/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -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 +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +7 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +22 -2
- 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/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +20 -7
- package/src/server/index/optional-listeners.ts +11 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +16 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -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/agent-task-recovery.ts +1 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +12 -4
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +125 -32
- 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/encrypted-payload.ts +3 -3
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +19 -6
- 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/restart-replacement.ts +50 -2
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/definition.ts +20 -8
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +13 -0
- package/src/types/fallback.ts +38 -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/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/index-TmaHa54u.js +0 -94
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
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> {
|
|
@@ -8,6 +8,14 @@ import { RequestPacingQueueOverloadError } from "./request-pacing";
|
|
|
8
8
|
import type { OcxProviderTransport } from "./xai-transport";
|
|
9
9
|
|
|
10
10
|
const MAX_IDENTITIES = 128;
|
|
11
|
+
/** Queued waiters per credential+model identity; one hot identity never refuses another. */
|
|
12
|
+
const MAX_WAITERS_PER_IDENTITY = 128;
|
|
13
|
+
/**
|
|
14
|
+
* Process-wide waiter bound. Model strings come from clients, so identities are not bounded by
|
|
15
|
+
* configuration; this keeps memory and pending requests finite. It is a multiple of the
|
|
16
|
+
* per-identity cap, so no single identity can fill it alone.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_WAITERS_TOTAL = 4 * MAX_WAITERS_PER_IDENTITY;
|
|
11
19
|
const DEFAULT_MAX_WAIT_MS = 60_000; // Existing retry wait budget, not an inferred rate window.
|
|
12
20
|
const MINUTE_MS = 60 * 1000; // Unit named by the observed headers, never an account quota.
|
|
13
21
|
type RecoveryState = { generation: number; probe?: { done: Promise<void>; finish(): void } };
|
|
@@ -61,7 +69,14 @@ export class MistralBudgetPacer {
|
|
|
61
69
|
private readonly recoveries = new Map<string, RecoveryState>();
|
|
62
70
|
/** Latest live deadline evicted at capacity; unknown identities wait it out instead of sending early. */
|
|
63
71
|
private evictedUntil = 0;
|
|
64
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Parked waiters keyed by identity. A request counts only once it is about to wait, so a held
|
|
74
|
+
* credential or model fills only its own queue and a request that can send now is never answered
|
|
75
|
+
* queue_full (HTTP 429). The map holds identities with live waiters only; the total is capped by
|
|
76
|
+
* MAX_WAITERS_TOTAL.
|
|
77
|
+
*/
|
|
78
|
+
private readonly waiting = new Map<string, number>();
|
|
79
|
+
private waitingTotal = 0;
|
|
65
80
|
|
|
66
81
|
constructor(
|
|
67
82
|
private readonly now: () => number = Date.now,
|
|
@@ -103,9 +118,18 @@ export class MistralBudgetPacer {
|
|
|
103
118
|
/** Wait within the admission budget and return a release callback for one recovery probe. */
|
|
104
119
|
async acquire(identity: string, signal?: AbortSignal, maxWaitMs = DEFAULT_MAX_WAIT_MS): Promise<(success?: boolean) => void> {
|
|
105
120
|
signal?.throwIfAborted();
|
|
106
|
-
if (this.waiting >= MAX_IDENTITIES) throw new RequestPacingQueueOverloadError("mistral", "queue_full", 60);
|
|
107
|
-
this.waiting++;
|
|
108
121
|
const deadline = this.now() + maxWaitMs;
|
|
122
|
+
let parked = false;
|
|
123
|
+
const park = (): void => {
|
|
124
|
+
if (parked) return;
|
|
125
|
+
const queued = this.waiting.get(identity) ?? 0;
|
|
126
|
+
if (queued >= MAX_WAITERS_PER_IDENTITY || this.waitingTotal >= MAX_WAITERS_TOTAL) {
|
|
127
|
+
throw new RequestPacingQueueOverloadError("mistral", "queue_full", 60);
|
|
128
|
+
}
|
|
129
|
+
this.waiting.set(identity, queued + 1);
|
|
130
|
+
this.waitingTotal++;
|
|
131
|
+
parked = true;
|
|
132
|
+
};
|
|
109
133
|
try {
|
|
110
134
|
for (;;) {
|
|
111
135
|
signal?.throwIfAborted();
|
|
@@ -118,6 +142,7 @@ export class MistralBudgetPacer {
|
|
|
118
142
|
throw new RequestPacingQueueOverloadError("mistral", "queue_expired", Math.ceil(delay / 1000));
|
|
119
143
|
}
|
|
120
144
|
if (readyAt !== undefined && readyAt > this.now()) {
|
|
145
|
+
park();
|
|
121
146
|
await this.sleep(readyAt - this.now(), signal);
|
|
122
147
|
continue;
|
|
123
148
|
}
|
|
@@ -126,6 +151,7 @@ export class MistralBudgetPacer {
|
|
|
126
151
|
if (state.probe) {
|
|
127
152
|
const remaining = deadline - this.now();
|
|
128
153
|
if (remaining <= 0) throw new RequestPacingQueueOverloadError("mistral", "queue_expired", 1);
|
|
154
|
+
park();
|
|
129
155
|
const local = new AbortController();
|
|
130
156
|
try {
|
|
131
157
|
await Promise.race([state.probe.done, this.sleep(remaining, AbortSignal.any([local.signal, ...(signal ? [signal] : [])]))]);
|
|
@@ -150,7 +176,14 @@ export class MistralBudgetPacer {
|
|
|
150
176
|
probe.finish();
|
|
151
177
|
};
|
|
152
178
|
}
|
|
153
|
-
} finally {
|
|
179
|
+
} finally {
|
|
180
|
+
if (parked) {
|
|
181
|
+
this.waitingTotal--;
|
|
182
|
+
const left = (this.waiting.get(identity) ?? 1) - 1;
|
|
183
|
+
if (left > 0) this.waiting.set(identity, left);
|
|
184
|
+
else this.waiting.delete(identity);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
154
187
|
}
|
|
155
188
|
}
|
|
156
189
|
|
|
@@ -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,11 +3,16 @@ 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";
|
|
15
|
+
import { httpQuotaFailure } from "./failure-codes";
|
|
11
16
|
|
|
12
17
|
export function antigravityQuotaDiagnosticIdentity(accountId: string, credential = getAccountCredential("google-antigravity", accountId)): string | undefined {
|
|
13
18
|
return credential ? quotaCredentialIdentity("google-antigravity", accountId, credential, {
|
|
@@ -45,11 +50,6 @@ function quotaInfoEntries(modelInfo: Record<string, unknown>): Record<string, un
|
|
|
45
50
|
return entries;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
/**
|
|
49
|
-
* Gemini 4 Argon has its own quota pool. Its names also contain "gemini", so every family
|
|
50
|
-
* decision below checks Argon first; otherwise Argon readings fill or replace the Gem family.
|
|
51
|
-
*/
|
|
52
|
-
const ARGON_TOKEN = /(?:^|[^a-z0-9])argon(?:[^a-z0-9]|$)/i;
|
|
53
53
|
const GEMINI_TOKEN = /(?:^|[^a-z0-9])gemini(?:[^a-z0-9]|$)/i;
|
|
54
54
|
const CLAUDE_FAMILY_TOKEN = /(?:^|[^a-z0-9])(?:claude|3p|gpt|opus|sonnet)(?:[^a-z0-9]|$)/i;
|
|
55
55
|
/** Canonical model of the Argon summary pool; tagging keeps the window out of routing. */
|
|
@@ -60,7 +60,7 @@ type AntigravitySummaryFamily = "argon" | "gem" | "cla";
|
|
|
60
60
|
/** A bucket's own id and name decide its family when they name one; the group decides otherwise. */
|
|
61
61
|
function antigravitySummaryBucketFamily(bucket: Record<string, unknown>): AntigravitySummaryFamily | undefined {
|
|
62
62
|
const identity = `${typeof bucket.bucketId === "string" ? bucket.bucketId : ""} ${typeof bucket.displayName === "string" ? bucket.displayName : ""}`;
|
|
63
|
-
if (
|
|
63
|
+
if (isAntigravityArgonIdentity(identity)) return "argon";
|
|
64
64
|
if (GEMINI_TOKEN.test(identity)) return "gem";
|
|
65
65
|
if (CLAUDE_FAMILY_TOKEN.test(identity)) return "cla";
|
|
66
66
|
return undefined;
|
|
@@ -69,7 +69,7 @@ function antigravitySummaryBucketFamily(bucket: Record<string, unknown>): Antigr
|
|
|
69
69
|
function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, unknown>, quotaInfo: Record<string, unknown>): "Gem" | "Cla" | null {
|
|
70
70
|
const displayName = typeof modelInfo.displayName === "string" ? modelInfo.displayName : "";
|
|
71
71
|
// Argon models keep their exact model rows but never seed the shared Gem family bar.
|
|
72
|
-
if (
|
|
72
|
+
if (isAntigravityArgonIdentity(`${modelId} ${displayName}`)) return null;
|
|
73
73
|
const tier = typeof quotaInfo.tier === "string" ? quotaInfo.tier : "";
|
|
74
74
|
const haystack = `${modelId} ${displayName} ${tier}`.toLowerCase();
|
|
75
75
|
if (haystack.includes("gemini")) return "Gem";
|
|
@@ -154,7 +154,7 @@ function parseAntigravityQuotaSummary(body: Record<string, unknown> | null): Pro
|
|
|
154
154
|
const groupDisplayName = typeof group.displayName === "string" ? group.displayName : "";
|
|
155
155
|
const groupName = `${groupDisplayName} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase();
|
|
156
156
|
// A description that merely mentions Argon must not move a whole Gemini group.
|
|
157
|
-
const groupFamily: AntigravitySummaryFamily | undefined =
|
|
157
|
+
const groupFamily: AntigravitySummaryFamily | undefined = isAntigravityArgonIdentity(groupDisplayName) ? "argon"
|
|
158
158
|
: groupName.includes("gemini") ? "gem"
|
|
159
159
|
: groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt") ? "cla"
|
|
160
160
|
: undefined;
|
|
@@ -275,13 +275,6 @@ function quotaTransportFailure(error: unknown): QuotaFailureCode {
|
|
|
275
275
|
return "transport_error";
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
function quotaHttpFailure(status: number): QuotaFailureCode {
|
|
279
|
-
if (status >= 300 && status < 400) return "redirect_blocked";
|
|
280
|
-
if (status === 401 || status === 403) return "access_denied";
|
|
281
|
-
if (status === 429) return "rate_limited";
|
|
282
|
-
return "upstream_error";
|
|
283
|
-
}
|
|
284
|
-
|
|
285
278
|
function unavailableAntigravityQuota(failure: QuotaFailureCode): AntigravityQuotaProbeResult {
|
|
286
279
|
return { kind: "unavailable", failure, legacy: { kind: "null" } };
|
|
287
280
|
}
|
|
@@ -342,7 +335,7 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId:
|
|
|
342
335
|
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "redirect_blocked"));
|
|
343
336
|
}
|
|
344
337
|
if (!response.ok) {
|
|
345
|
-
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure,
|
|
338
|
+
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, httpQuotaFailure(response.status)));
|
|
346
339
|
}
|
|
347
340
|
const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
|
|
348
341
|
if (!customWindows.length) {
|
|
@@ -366,13 +359,25 @@ export async function fetchAntigravityUsageQuota(accessToken: string, projectId:
|
|
|
366
359
|
return null;
|
|
367
360
|
}
|
|
368
361
|
|
|
369
|
-
|
|
370
|
-
|
|
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);
|
|
371
365
|
if (!credential?.projectId) return null;
|
|
372
366
|
let accessToken: string;
|
|
373
|
-
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;
|
|
374
372
|
const result = await probeAntigravityUsageQuota(accessToken, credential.projectId);
|
|
375
|
-
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
|
+
}
|
|
376
381
|
if (result.legacy.kind === "throw") throw result.legacy.error;
|
|
377
382
|
return null;
|
|
378
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;
|