@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
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
* This rewrites one thing: a saved `models` list that is still byte-for-byte the roster
|
|
11
11
|
* this file names as `from`, on a provider that still carries the registry adapter, and
|
|
12
12
|
* only while the registry currently seeds `to`. A list the user changed does not match and
|
|
13
|
-
* is left alone. For each added id it also fills a per-model context window
|
|
14
|
-
* effort, but only inside a container the row already has: enrichment fills
|
|
15
|
-
* all-or-nothing, so creating one here would hide the rest of the registry seed.
|
|
13
|
+
* is left alone. For each added id it also fills a per-model context window, output limit
|
|
14
|
+
* and default effort, but only inside a container the row already has: enrichment fills
|
|
15
|
+
* those records all-or-nothing, so creating one here would hide the rest of the registry seed.
|
|
16
16
|
* Same shape and restraint as `stale-context-window-migration`.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { ALIBABA_INTL_TOKEN_PLAN_BASE_URL } from "./base-url-choices";
|
|
19
|
+
import { PROVIDER_REGISTRY, normalizedProviderEndpoint } from "./registry";
|
|
19
20
|
import {
|
|
21
|
+
ALIBABA_TOKEN_PLAN_MODELS,
|
|
22
|
+
ALIBABA_INTL_TOKEN_PLAN_MODELS,
|
|
23
|
+
ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
|
|
24
|
+
ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
|
|
20
25
|
MINIMAX_M31_DEFAULT_REASONING_EFFORT,
|
|
21
26
|
MINIMAX_M31_FLASH_PREVIEW,
|
|
22
27
|
MINIMAX_MODELS,
|
|
@@ -33,7 +38,18 @@ export interface StaleModelRoster {
|
|
|
33
38
|
to: readonly string[];
|
|
34
39
|
/** Per-model seed values for the added ids, written only into containers that exist. */
|
|
35
40
|
contextWindows?: Readonly<Record<string, number>>;
|
|
41
|
+
/**
|
|
42
|
+
* Custom catalog rows read the saved map directly, so an added id missing from a
|
|
43
|
+
* partial map would publish without the registry output limit.
|
|
44
|
+
*/
|
|
45
|
+
maxOutputTokens?: Readonly<Record<string, number>>;
|
|
36
46
|
defaultReasoningEfforts?: Readonly<Record<string, string>>;
|
|
47
|
+
/**
|
|
48
|
+
* For a provider whose saved URL may point elsewhere (pay-as-you-go or Custom), the
|
|
49
|
+
* destinations whose roster `to` describes. A row on any other URL keeps its list; a row
|
|
50
|
+
* with no saved URL uses the registry default and is refreshed.
|
|
51
|
+
*/
|
|
52
|
+
destinationBaseUrls?: readonly string[];
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
export interface StaleModelRosterProjection {
|
|
@@ -42,17 +58,45 @@ export interface StaleModelRosterProjection {
|
|
|
42
58
|
warnings: string[];
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
const ALIBABA_TOKEN_PLAN_MODELS_BEFORE_1017 = [
|
|
62
|
+
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
|
|
63
|
+
"deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3",
|
|
64
|
+
];
|
|
65
|
+
const ALIBABA_INTL_TOKEN_PLAN_MODELS_BEFORE_1017 = [
|
|
66
|
+
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
67
|
+
"deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v3.2",
|
|
68
|
+
"kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5",
|
|
69
|
+
"glm-5.2", "glm-5.3", "glm-5.1", "glm-5",
|
|
70
|
+
"MiniMax-M2.5",
|
|
71
|
+
];
|
|
72
|
+
|
|
45
73
|
/**
|
|
46
74
|
* MiniMax-M3.1-Flash-Preview (2026-09-30). MiniMax's /v1/models roster does not list the
|
|
47
75
|
* preview, so live discovery cannot add it; the catalog keeps it only when it is configured.
|
|
48
76
|
*/
|
|
49
|
-
export const STALE_MODEL_ROSTERS: readonly StaleModelRoster[] = [
|
|
50
|
-
provider
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
77
|
+
export const STALE_MODEL_ROSTERS: readonly StaleModelRoster[] = [
|
|
78
|
+
...["minimax", "minimax-cn"].map(provider => ({
|
|
79
|
+
provider,
|
|
80
|
+
from: MINIMAX_MODELS_BEFORE_M31,
|
|
81
|
+
to: MINIMAX_MODELS,
|
|
82
|
+
contextWindows: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_MODEL_CONTEXT_WINDOWS[MINIMAX_M31_FLASH_PREVIEW]! },
|
|
83
|
+
defaultReasoningEfforts: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
|
|
84
|
+
})),
|
|
85
|
+
...[
|
|
86
|
+
{ provider: "alibaba-token-plan", from: ALIBABA_TOKEN_PLAN_MODELS_BEFORE_1017, to: ALIBABA_TOKEN_PLAN_MODELS },
|
|
87
|
+
// International honors pay-as-you-go and Custom URLs, which may still serve the retired ids.
|
|
88
|
+
// Beijing needs no guard: the router pins it to the registry Token Plan host.
|
|
89
|
+
{
|
|
90
|
+
provider: "alibaba-token-plan-intl", from: ALIBABA_INTL_TOKEN_PLAN_MODELS_BEFORE_1017, to: ALIBABA_INTL_TOKEN_PLAN_MODELS,
|
|
91
|
+
destinationBaseUrls: [ALIBABA_INTL_TOKEN_PLAN_BASE_URL],
|
|
92
|
+
},
|
|
93
|
+
].map(entry => ({
|
|
94
|
+
...entry,
|
|
95
|
+
contextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
|
|
96
|
+
maxOutputTokens: ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
|
|
97
|
+
defaultReasoningEfforts: { "deepseek-v4.1-flash-1017": "high" },
|
|
98
|
+
})),
|
|
99
|
+
];
|
|
56
100
|
|
|
57
101
|
function sameList(value: unknown, expected: readonly string[]): boolean {
|
|
58
102
|
return Array.isArray(value)
|
|
@@ -65,12 +109,24 @@ function registrySeeds(provider: string, adapter: unknown, roster: readonly stri
|
|
|
65
109
|
return entry !== undefined && entry.adapter === adapter && sameList(entry.models, roster);
|
|
66
110
|
}
|
|
67
111
|
|
|
68
|
-
function
|
|
69
|
-
if (!
|
|
112
|
+
function servesRoster(baseUrl: unknown, destinations: readonly string[] | undefined): boolean {
|
|
113
|
+
if (!destinations || baseUrl === undefined || baseUrl === "") return true;
|
|
114
|
+
if (typeof baseUrl !== "string") return false;
|
|
115
|
+
const saved = normalizedProviderEndpoint(baseUrl);
|
|
116
|
+
return destinations.some(url => normalizedProviderEndpoint(url) === saved);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function fillExisting<T>(container: Record<string, T> | undefined, values: Readonly<Record<string, T>> | undefined, ids: readonly string[]): boolean {
|
|
120
|
+
if (!container || !values) return false;
|
|
121
|
+
let filled = false;
|
|
70
122
|
for (const id of ids) {
|
|
71
123
|
const value = values[id];
|
|
72
|
-
if (value !== undefined && !Object.prototype.hasOwnProperty.call(container, id))
|
|
124
|
+
if (value !== undefined && !Object.prototype.hasOwnProperty.call(container, id)) {
|
|
125
|
+
container[id] = value;
|
|
126
|
+
filled = true;
|
|
127
|
+
}
|
|
73
128
|
}
|
|
129
|
+
return filled;
|
|
74
130
|
}
|
|
75
131
|
|
|
76
132
|
/** Pure projection. The caller decides whether to persist. */
|
|
@@ -85,10 +141,22 @@ export function projectStaleModelRosters(
|
|
|
85
141
|
const prov = config.providers?.[entry.provider];
|
|
86
142
|
if (!prov) continue;
|
|
87
143
|
if (!registrySeeds(entry.provider, prov.adapter, entry.to)) continue;
|
|
88
|
-
if (!
|
|
144
|
+
if (!servesRoster(prov.baseUrl, entry.destinationBaseUrls)) continue;
|
|
89
145
|
const added = entry.to.filter(id => !entry.from.includes(id));
|
|
146
|
+
if (sameList(prov.models, entry.to)) {
|
|
147
|
+
// A row refreshed before output limits joined the refresh still has a partial map.
|
|
148
|
+
// Only that record is revisited: the earlier pass already filled windows and efforts,
|
|
149
|
+
// so refilling them could restore a key the operator has since removed.
|
|
150
|
+
if (fillExisting(prov.modelMaxOutputTokens, entry.maxOutputTokens, added)) {
|
|
151
|
+
changed = true;
|
|
152
|
+
warnings.push(`filled output limits for ${added.join(", ")} on the saved "${entry.provider}" row.`);
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!sameList(prov.models, entry.from)) continue;
|
|
90
157
|
prov.models = [...entry.to];
|
|
91
158
|
fillExisting(prov.modelContextWindows, entry.contextWindows, added);
|
|
159
|
+
fillExisting(prov.modelMaxOutputTokens, entry.maxOutputTokens, added);
|
|
92
160
|
fillExisting(prov.modelDefaultReasoningEfforts, entry.defaultReasoningEfforts, added);
|
|
93
161
|
changed = true;
|
|
94
162
|
warnings.push(
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface WorkBuddyCreditAmount {
|
|
2
|
+
total: number;
|
|
3
|
+
used: number;
|
|
4
|
+
remaining: number;
|
|
5
|
+
frozen: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const WORKBUDDY_CREDIT_KINDS = ["plan_base", "plan_bonus", "addon", "benefit", "other"] as const;
|
|
9
|
+
export type WorkBuddyCreditKind = typeof WORKBUDDY_CREDIT_KINDS[number];
|
|
10
|
+
|
|
11
|
+
/** Display-only credits; they are neither USD nor provider-wide model eligibility. */
|
|
12
|
+
export interface WorkBuddyCreditBalance extends WorkBuddyCreditAmount {
|
|
13
|
+
groups: Array<WorkBuddyCreditAmount & { kind: WorkBuddyCreditKind }>;
|
|
14
|
+
expiring?: { amount: number; at: number };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function workBuddyRecord(value: unknown): Record<string, unknown> {
|
|
18
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function workBuddyCapacity(value: unknown): number | undefined {
|
|
22
|
+
if (typeof value !== "number" && !(typeof value === "string" && /^\d+(?:\.\d+)?$/.test(value))) return undefined;
|
|
23
|
+
const number = Number(value);
|
|
24
|
+
return Number.isFinite(number) && number >= 0 && number <= Number.MAX_SAFE_INTEGER ? number : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function amount(value: unknown): WorkBuddyCreditAmount | undefined {
|
|
28
|
+
const row = workBuddyRecord(value);
|
|
29
|
+
const total = workBuddyCapacity(row.total), used = workBuddyCapacity(row.used);
|
|
30
|
+
const remaining = workBuddyCapacity(row.remaining), frozen = workBuddyCapacity(row.frozen);
|
|
31
|
+
if (total === undefined || used === undefined || remaining === undefined || frozen === undefined) return undefined;
|
|
32
|
+
return { total, used, remaining, frozen };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Narrow wire/cache input before rendering, without admitting account or package identifiers. */
|
|
36
|
+
export function normalizeWorkBuddyCredits(value: unknown): WorkBuddyCreditBalance | undefined {
|
|
37
|
+
const row = workBuddyRecord(value), balance = amount(value);
|
|
38
|
+
if (!balance || !Array.isArray(row.groups) || row.groups.length > WORKBUDDY_CREDIT_KINDS.length) return undefined;
|
|
39
|
+
const groups: WorkBuddyCreditBalance["groups"] = [];
|
|
40
|
+
const seen = new Set<string>();
|
|
41
|
+
for (const raw of row.groups) {
|
|
42
|
+
const group = workBuddyRecord(raw), quantities = amount(group);
|
|
43
|
+
const kind = WORKBUDDY_CREDIT_KINDS.find(kind => kind === group.kind);
|
|
44
|
+
if (!quantities || !kind || seen.has(kind)) return undefined;
|
|
45
|
+
seen.add(kind); groups.push({ kind, ...quantities });
|
|
46
|
+
}
|
|
47
|
+
const expiring = workBuddyRecord(row.expiring), quantity = workBuddyCapacity(expiring.amount);
|
|
48
|
+
const at = typeof expiring.at === "number" && expiring.at > 0 && Number.isFinite(new Date(expiring.at).getTime()) ? expiring.at : undefined;
|
|
49
|
+
return { ...balance, groups, ...(quantity !== undefined && quantity > 0 && at !== undefined ? { expiring: { amount: quantity, at } } : {}) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function packageAmount(row: Record<string, unknown>): WorkBuddyCreditAmount | undefined {
|
|
53
|
+
const total = workBuddyCapacity(row.CycleTotalCapacity), used = workBuddyCapacity(row.CycleUsedCapacity);
|
|
54
|
+
const frozen = row.CycleFrozenCapacity === undefined ? 0 : workBuddyCapacity(row.CycleFrozenCapacity);
|
|
55
|
+
if (total === undefined || used === undefined || frozen === undefined) return undefined;
|
|
56
|
+
const remaining = row.CycleRemainCapacity === undefined ? Math.max(0, total - used - frozen) : workBuddyCapacity(row.CycleRemainCapacity);
|
|
57
|
+
return remaining === undefined ? undefined : { total, used, remaining, frozen };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseWorkBuddyCredits(value: unknown): WorkBuddyCreditBalance | undefined {
|
|
61
|
+
const summary = workBuddyRecord(value);
|
|
62
|
+
if (!Array.isArray(summary.Packages) || summary.Packages.length > 512) return undefined;
|
|
63
|
+
const balance: WorkBuddyCreditBalance = { total: 0, used: 0, remaining: 0, frozen: 0, groups: [] };
|
|
64
|
+
for (const raw of summary.Packages) {
|
|
65
|
+
const quantities = packageAmount(workBuddyRecord(raw));
|
|
66
|
+
if (!quantities) return undefined;
|
|
67
|
+
for (const key of ["total", "used", "remaining", "frozen"] as const) balance[key] += quantities[key];
|
|
68
|
+
}
|
|
69
|
+
// CreditGroups is a breakdown of Packages, not another balance to add to it.
|
|
70
|
+
if (Array.isArray(summary.CreditGroups) && summary.CreditGroups.length <= 16) {
|
|
71
|
+
for (const raw of summary.CreditGroups) {
|
|
72
|
+
const row = workBuddyRecord(raw), quantities = packageAmount(row);
|
|
73
|
+
if (!quantities) continue;
|
|
74
|
+
const kind = WORKBUDDY_CREDIT_KINDS.find(kind => kind === row.Kind) ?? "other";
|
|
75
|
+
const existing = balance.groups.find(group => group.kind === kind);
|
|
76
|
+
if (existing) for (const key of ["total", "used", "remaining", "frozen"] as const) existing[key] += quantities[key];
|
|
77
|
+
else balance.groups.push({ kind, ...quantities });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return normalizeWorkBuddyCredits(balance);
|
|
81
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isValidModelDiscoveryModelId } from "./model-discovery-limits";
|
|
2
|
+
import type { ProviderModelItemsResult, ProviderModelsApiItem } from "./model-discovery";
|
|
3
|
+
|
|
4
|
+
function record(value: unknown): Record<string, unknown> {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The product catalog contains non-chat models too; only the CLI agent's roster is routable. */
|
|
9
|
+
export function extractWorkBuddyModels(value: unknown, limit: number): ProviderModelItemsResult {
|
|
10
|
+
const envelope = record(value), data = record(envelope.data);
|
|
11
|
+
if ((envelope.code !== 0 && envelope.code !== "0") || !Array.isArray(data.agents) || !Array.isArray(data.models)) return { ok: false, reason: "invalid_shape" };
|
|
12
|
+
if (data.models.length > limit || data.agents.length > limit) return { ok: false, reason: "too_many_models" };
|
|
13
|
+
const agent = data.agents.map(record).find(a => a.name === "cli");
|
|
14
|
+
if (!Array.isArray(agent?.models)) return { ok: false, reason: "invalid_shape" };
|
|
15
|
+
if (agent.models.length > limit) return { ok: false, reason: "too_many_models" };
|
|
16
|
+
const definitions = new Map(data.models.map(raw => { const row = record(raw); return [row.id, row] as const; }));
|
|
17
|
+
const items: ProviderModelsApiItem[] = [], seen = new Set<string>();
|
|
18
|
+
for (const id of agent.models) {
|
|
19
|
+
if (!isValidModelDiscoveryModelId(id)) return { ok: false, reason: "invalid_shape" };
|
|
20
|
+
if (seen.has(id)) continue;
|
|
21
|
+
seen.add(id);
|
|
22
|
+
const row = definitions.get(id) ?? {}, reasoning = record(row.reasoning);
|
|
23
|
+
const rawEfforts = Array.isArray(reasoning.supportedEfforts) ? reasoning.supportedEfforts
|
|
24
|
+
: row.supportsReasoning === true ? ["low", "high", "max"] : [];
|
|
25
|
+
const efforts = rawEfforts.filter(e => typeof e === "string" && ["low", "medium", "high", "xhigh", "max"].includes(e));
|
|
26
|
+
if (efforts.length && row.onlyReasoning !== true && reasoning.canDisableThinking !== false) efforts.unshift("none");
|
|
27
|
+
const positive = (v: unknown) => typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : undefined;
|
|
28
|
+
items.push({
|
|
29
|
+
id, ...(typeof row.name === "string" && row.name.length <= 256 ? { name: row.name } : {}),
|
|
30
|
+
context_length: positive(row.maxInputTokens), max_input_tokens: positive(row.maxInputTokens),
|
|
31
|
+
max_output_tokens: positive(row.maxOutputTokens), reasoning_efforts: efforts,
|
|
32
|
+
// WorkBuddy AI filters images from GLM requests despite advertising supportsImages.
|
|
33
|
+
input_modalities: row.supportsImages === true && !id.startsWith("glm-") ? ["text", "image"] : ["text"],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, items, rawCount: agent.models.length };
|
|
37
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { WorkBuddyOAuthMetadata } from "../oauth/types";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
export const WORKBUDDY_ORIGIN = "https://www.workbuddy.ai";
|
|
5
|
+
export const WORKBUDDY_BASE_URL = `${WORKBUDDY_ORIGIN}/v2`;
|
|
6
|
+
export const WORKBUDDY_CONFIG_URL = `${WORKBUDDY_ORIGIN}/v3/config`;
|
|
7
|
+
export const WORKBUDDY_USER_AGENT = "WorkBuddy/5.5.6";
|
|
8
|
+
|
|
9
|
+
export function workBuddyCatalogIdentity(access: string, metadata: WorkBuddyOAuthMetadata | undefined): string {
|
|
10
|
+
return createHash("sha256").update(JSON.stringify([access, metadata])).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function workBuddyHeaderValue(value: unknown, max = 1024): value is string {
|
|
14
|
+
return typeof value === "string" && value.length > 0 && value.length <= max && /^[\x21-\x7e]+$/.test(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeWorkBuddyMetadata(value: unknown): WorkBuddyOAuthMetadata | undefined {
|
|
18
|
+
if (!value || typeof value !== "object") return undefined;
|
|
19
|
+
const v = value as Partial<WorkBuddyOAuthMetadata>;
|
|
20
|
+
if (!workBuddyHeaderValue(v.uid) || !workBuddyHeaderValue(v.domain)) return undefined;
|
|
21
|
+
if (v.enterpriseId !== undefined && !workBuddyHeaderValue(v.enterpriseId)) return undefined;
|
|
22
|
+
return {
|
|
23
|
+
uid: v.uid, domain: v.domain,
|
|
24
|
+
...(v.enterpriseId ? { enterpriseId: v.enterpriseId } : {}),
|
|
25
|
+
...(typeof v.refreshExpiresAt === "number" && Number.isFinite(v.refreshExpiresAt) && v.refreshExpiresAt > 0
|
|
26
|
+
? { refreshExpiresAt: v.refreshExpiresAt } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function assertWorkBuddyDestination(baseUrl: string): void {
|
|
31
|
+
const u = new URL(baseUrl);
|
|
32
|
+
if (u.origin !== WORKBUDDY_ORIGIN || u.username || u.password || u.search || u.hash
|
|
33
|
+
|| !["", "/v2", "/v2/chat/completions"].includes(u.pathname.replace(/\/+$/, ""))) {
|
|
34
|
+
throw new Error("WorkBuddy AI credentials require https://www.workbuddy.ai/v2");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function workBuddyHeaders(access: string | undefined, metadata?: WorkBuddyOAuthMetadata): Record<string, string> {
|
|
39
|
+
const headers: Record<string, string> = {
|
|
40
|
+
"User-Agent": WORKBUDDY_USER_AGENT,
|
|
41
|
+
"X-Product": "SaaS", "X-IDE-Type": "WorkBuddy", "X-IDE-Name": "WorkBuddy",
|
|
42
|
+
"X-IDE-Version": "5.5.6", "X-Requested-With": "XMLHttpRequest",
|
|
43
|
+
"X-Agent-Intent": "craft", "X-Agent-Purpose": "conversation", "X-Agent-Type": "main",
|
|
44
|
+
};
|
|
45
|
+
if (access) {
|
|
46
|
+
if (!workBuddyHeaderValue(access, 32768)) throw new Error("Invalid WorkBuddy AI access token");
|
|
47
|
+
headers.Authorization = `Bearer ${access}`;
|
|
48
|
+
}
|
|
49
|
+
if (metadata) {
|
|
50
|
+
const valid = normalizeWorkBuddyMetadata(metadata);
|
|
51
|
+
if (!valid) throw new Error("WorkBuddy AI account identity unavailable; sign in again");
|
|
52
|
+
headers["X-User-Id"] = valid.uid;
|
|
53
|
+
headers["X-Domain"] = valid.domain;
|
|
54
|
+
if (valid.enterpriseId) {
|
|
55
|
+
headers["X-Enterprise-Id"] = valid.enterpriseId;
|
|
56
|
+
headers["X-Tenant-Id"] = valid.enterpriseId;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return headers;
|
|
60
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble `ocx.quota-pool/1` (`GET /api/quota-pool`) from what this Home already holds.
|
|
3
|
+
*
|
|
4
|
+
* Strictly passive. Every source below is a cache another path filled; this module starts no
|
|
5
|
+
* probe, token refresh, quota refresh or upstream call and writes nothing. The whole assembly runs
|
|
6
|
+
* inside `runPassiveRead`, so even the shared readers it calls (the Codex account list, the OAuth
|
|
7
|
+
* store) take their passive branch: no WHAM probe, no native-main claim, no file hardening or
|
|
8
|
+
* malformed-store backup. Sources:
|
|
9
|
+
*
|
|
10
|
+
* - Codex pool accounts: the passive `listCodexAuthAccountsSnapshot` (stored `StoredAccountQuota`
|
|
11
|
+
* per account plus the cached main-account reading);
|
|
12
|
+
* - provider-level reports: `heldProviderQuotaReports`, the reports the Home last published
|
|
13
|
+
* (for the Codex pool provider this is OpenCodex's own pool aggregate, reported as `pooled`);
|
|
14
|
+
* - per-account OAuth quota (Anthropic instances, Kiro, Antigravity, xAI, Cursor, Kimi, Command
|
|
15
|
+
* Code, Devin, DSH, Meta): the last-good per-account cache;
|
|
16
|
+
* - Claude CLI: the usage-admission snapshot (5-hour, weekly and family windows, plus a parsed
|
|
17
|
+
* CLI limit refusal).
|
|
18
|
+
*
|
|
19
|
+
* An unknown window is omitted, never emitted as 0%. A window whose reset has passed is unknown
|
|
20
|
+
* again and is omitted too. Optional fields are left out instead of set to null.
|
|
21
|
+
*/
|
|
22
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
23
|
+
import type { ProviderQuota, ProviderQuotaWindow } from "../providers/quota-types";
|
|
24
|
+
import type { StoredAccountQuota } from "../codex/quota-types";
|
|
25
|
+
import { isTerminalShortWindow, resetAtToMs } from "../codex/quota-types";
|
|
26
|
+
import { runPassiveRead } from "../lib/passive-read";
|
|
27
|
+
import { packageVersion } from "../lib/package-version";
|
|
28
|
+
import { listCodexAuthAccountsSnapshot } from "../codex/auth-api";
|
|
29
|
+
import { latestCodexQuotaObservationSource } from "../codex/quota";
|
|
30
|
+
import { ACCOUNT_LOG_LABEL_RE, oauthAccountLogLabel } from "../codex/account-label";
|
|
31
|
+
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers-destination";
|
|
32
|
+
import { hasCodexPoolProvider, heldProviderQuotaReports, type ProviderQuotaReport } from "../providers/quota/report-cache";
|
|
33
|
+
import {
|
|
34
|
+
accountCacheKey,
|
|
35
|
+
accountQuotaCache,
|
|
36
|
+
getCachedProviderAccountQuota,
|
|
37
|
+
hasPassiveAccountQuota,
|
|
38
|
+
supportsPerAccountQuota,
|
|
39
|
+
} from "../providers/quota/account-cache";
|
|
40
|
+
import { parseQuotaFailureCode } from "../providers/quota-types";
|
|
41
|
+
import { getAccountSet } from "../oauth/store";
|
|
42
|
+
import { peekClaudeUsageObservation } from "../adapters/claude-cli/usage-admission";
|
|
43
|
+
import {
|
|
44
|
+
QUOTA_POOL_SCHEMA,
|
|
45
|
+
type QuotaPool,
|
|
46
|
+
type QuotaPoolAccount,
|
|
47
|
+
type QuotaPoolAccountState,
|
|
48
|
+
type QuotaPoolProvider,
|
|
49
|
+
type QuotaPoolRole,
|
|
50
|
+
type QuotaPoolSource,
|
|
51
|
+
type QuotaPoolWindow,
|
|
52
|
+
} from "./contract";
|
|
53
|
+
import { quotaPoolAccountRef, readQuotaPoolIdentity, type QuotaPoolIdentity } from "./identity";
|
|
54
|
+
|
|
55
|
+
const VERSION = packageVersion("0.0.0");
|
|
56
|
+
const SHORT_BURST_MAX_SECONDS = 6 * 3600;
|
|
57
|
+
const SHORT_DAILY_MAX_SECONDS = 36 * 3600;
|
|
58
|
+
|
|
59
|
+
export interface QuotaPoolDeps {
|
|
60
|
+
now?: () => number;
|
|
61
|
+
/** Test seam for the Claude CLI admission file. */
|
|
62
|
+
claudeUsageStatePath?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type QuotaPoolResult = { ok: true; pool: QuotaPool } | { ok: false; error: "quota_pool_identity_unavailable" };
|
|
66
|
+
|
|
67
|
+
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
68
|
+
|
|
69
|
+
function iso(ms: number): string | undefined {
|
|
70
|
+
if (!finite(ms) || ms <= 0) return undefined;
|
|
71
|
+
const date = new Date(ms);
|
|
72
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A used percent inside the contract's 0..100, or undefined when there is no reading. */
|
|
76
|
+
function percent(value: unknown): number | undefined {
|
|
77
|
+
if (!finite(value) || value < 0) return undefined;
|
|
78
|
+
return Math.min(100, value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type WindowDraft = Omit<QuotaPoolWindow, "observedAt" | "source"> & { resetMs?: number };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Finish a window, or drop it when it carries nothing (no percent, no counters) or its reset has
|
|
85
|
+
* already passed: an elapsed window is unknown now, not empty.
|
|
86
|
+
*/
|
|
87
|
+
function finish(draft: WindowDraft, source: QuotaPoolSource, observedMs: number, now: number): QuotaPoolWindow | null {
|
|
88
|
+
const observedAt = iso(observedMs);
|
|
89
|
+
if (!observedAt) return null;
|
|
90
|
+
const { resetMs, ...rest } = draft;
|
|
91
|
+
if (resetMs !== undefined && resetMs <= now) return null;
|
|
92
|
+
if (rest.usedPercent === undefined && rest.used === undefined && rest.limit === undefined && rest.exhausted === undefined) return null;
|
|
93
|
+
const resetsAt = resetMs !== undefined ? iso(resetMs) : undefined;
|
|
94
|
+
const exhausted = rest.exhausted ?? (rest.usedPercent !== undefined ? rest.usedPercent >= 100 : undefined);
|
|
95
|
+
const window: QuotaPoolWindow = { ...rest, source, observedAt };
|
|
96
|
+
if (resetsAt) window.resetsAt = resetsAt;
|
|
97
|
+
if (exhausted !== undefined) window.exhausted = exhausted;
|
|
98
|
+
return window;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resetMs(value: unknown): number | undefined {
|
|
102
|
+
return finite(value) && value > 0 ? resetAtToMs(value) : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function shortKind(seconds: number | undefined): Pick<QuotaPoolWindow, "id" | "kind" | "label"> {
|
|
106
|
+
if (seconds === undefined || seconds <= SHORT_BURST_MAX_SECONDS) return { id: "5h", kind: "burst_5h", label: "5-hour" };
|
|
107
|
+
if (seconds <= SHORT_DAILY_MAX_SECONDS) return { id: "daily", kind: "daily", label: "Daily" };
|
|
108
|
+
return { id: "short", kind: "custom", label: "Short window" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Codex `StoredAccountQuota`, mapped as the header and WHAM parsers stored it. */
|
|
112
|
+
export function codexQuotaWindows(quota: StoredAccountQuota, source: QuotaPoolSource, now: number): QuotaPoolWindow[] {
|
|
113
|
+
const out: QuotaPoolWindow[] = [];
|
|
114
|
+
const push = (window: QuotaPoolWindow | null) => { if (window) out.push(window); };
|
|
115
|
+
const shortPercent = percent(quota.shortPercent);
|
|
116
|
+
const shortSeconds = finite(quota.shortWindowSeconds) && quota.shortWindowSeconds > 0 ? quota.shortWindowSeconds : undefined;
|
|
117
|
+
if (shortPercent !== undefined) {
|
|
118
|
+
push(finish({
|
|
119
|
+
...shortKind(shortSeconds),
|
|
120
|
+
usedPercent: shortPercent,
|
|
121
|
+
...(shortSeconds !== undefined ? { windowSeconds: shortSeconds } : {}),
|
|
122
|
+
resetMs: resetMs(quota.shortResetAt),
|
|
123
|
+
exhausted: isTerminalShortWindow(quota, now),
|
|
124
|
+
}, source, quota.shortObservedAt ?? quota.updatedAt, now));
|
|
125
|
+
}
|
|
126
|
+
const weekly = percent(quota.weeklyPercent);
|
|
127
|
+
if (weekly !== undefined) {
|
|
128
|
+
push(finish({ id: "weekly", kind: "weekly", label: "Weekly", usedPercent: weekly, resetMs: resetMs(quota.weeklyResetAt) }, source, quota.updatedAt, now));
|
|
129
|
+
}
|
|
130
|
+
const monthly = percent(quota.monthlyPercent);
|
|
131
|
+
if (monthly !== undefined) {
|
|
132
|
+
push(finish({ id: "monthly", kind: "monthly", label: "Monthly", usedPercent: monthly, resetMs: resetMs(quota.monthlyResetAt) }, source, quota.updatedAt, now));
|
|
133
|
+
}
|
|
134
|
+
for (const custom of quota.customWindows ?? []) {
|
|
135
|
+
const used = percent(custom?.percent);
|
|
136
|
+
if (typeof custom?.label !== "string" || !custom.label.trim() || used === undefined) continue;
|
|
137
|
+
push(finish({ id: `custom:${custom.label}`, kind: "custom", label: custom.label, usedPercent: used, resetMs: resetMs(custom.resetAt) }, source, quota.updatedAt, now));
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function customWindow(window: ProviderQuotaWindow, source: QuotaPoolSource, updatedAt: number, now: number): QuotaPoolWindow | null {
|
|
143
|
+
if (typeof window?.label !== "string" || !window.label.trim()) return null;
|
|
144
|
+
// An exact-model row (Antigravity diagnostics, CCA fallback) restates the pool its family
|
|
145
|
+
// window already reports and carries no proven scope; reporting it unscoped would let it gate
|
|
146
|
+
// every model.
|
|
147
|
+
if (window.modelId !== undefined && window.scope !== "model") return null;
|
|
148
|
+
const scoped = window.scope === "model";
|
|
149
|
+
const passive = finite(window.passiveObservedAt);
|
|
150
|
+
return finish({
|
|
151
|
+
id: scoped ? `family:${window.label.toLowerCase()}` : `custom:${window.label}`,
|
|
152
|
+
kind: "custom",
|
|
153
|
+
label: window.label,
|
|
154
|
+
...(percent(window.percent) !== undefined ? { usedPercent: percent(window.percent) } : {}),
|
|
155
|
+
...(scoped ? { scopeModel: window.label } : {}),
|
|
156
|
+
...(window.rejected === true ? { exhausted: true } : {}),
|
|
157
|
+
resetMs: resetMs(window.resetAt),
|
|
158
|
+
}, passive ? "header" : source, passive ? window.passiveObservedAt! : updatedAt, now);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A `ProviderQuota` (provider report or per-account cache row). */
|
|
162
|
+
export function providerQuotaWindows(quota: ProviderQuota, source: QuotaPoolSource, now: number): QuotaPoolWindow[] {
|
|
163
|
+
const out: QuotaPoolWindow[] = [];
|
|
164
|
+
const push = (window: QuotaPoolWindow | null) => { if (window) out.push(window); };
|
|
165
|
+
const at = quota.updatedAt;
|
|
166
|
+
const fixed = [
|
|
167
|
+
["5h", "burst_5h", "5-hour", quota.fiveHourPercent, quota.fiveHourResetAt],
|
|
168
|
+
["weekly", "weekly", "Weekly", quota.weeklyPercent, quota.weeklyResetAt],
|
|
169
|
+
["monthly", "monthly", "Monthly", quota.monthlyPercent, quota.monthlyResetAt],
|
|
170
|
+
] as const;
|
|
171
|
+
for (const [id, kind, label, value, reset] of fixed) {
|
|
172
|
+
const used = percent(value);
|
|
173
|
+
if (used === undefined) continue;
|
|
174
|
+
push(finish({ id, kind, label, usedPercent: used, resetMs: resetMs(reset) }, source, at, now));
|
|
175
|
+
}
|
|
176
|
+
const kiroUsed = finite(quota.kiroCreditsUsed) && quota.kiroCreditsUsed >= 0 ? quota.kiroCreditsUsed : undefined;
|
|
177
|
+
const kiroLimit = finite(quota.kiroCreditsLimit) && quota.kiroCreditsLimit > 0 ? quota.kiroCreditsLimit : undefined;
|
|
178
|
+
if (kiroUsed !== undefined || kiroLimit !== undefined) {
|
|
179
|
+
push(finish({
|
|
180
|
+
id: "credits:kiro", kind: "credits", label: "Kiro credits", unit: "credits",
|
|
181
|
+
...(kiroUsed !== undefined ? { used: kiroUsed } : {}),
|
|
182
|
+
...(kiroLimit !== undefined ? { limit: kiroLimit } : {}),
|
|
183
|
+
...(kiroUsed !== undefined && kiroLimit !== undefined ? { usedPercent: percent((kiroUsed / kiroLimit) * 100) } : {}),
|
|
184
|
+
}, source, at, now));
|
|
185
|
+
}
|
|
186
|
+
const usd = quota.creditsUsd;
|
|
187
|
+
if (usd && finite(usd.used) && usd.used >= 0) {
|
|
188
|
+
const unlimited = usd.unlimited === true;
|
|
189
|
+
push(finish({
|
|
190
|
+
id: "credits:usd", kind: "credits", label: "USD credits", unit: "usd", used: usd.used,
|
|
191
|
+
...(!unlimited && finite(usd.limit) && usd.limit > 0 ? { limit: usd.limit } : {}),
|
|
192
|
+
...(!unlimited && percent(usd.percent) !== undefined ? { usedPercent: percent(usd.percent) } : {}),
|
|
193
|
+
...(unlimited ? { exhausted: false } : finite(usd.remaining) ? { exhausted: usd.remaining <= 0 } : {}),
|
|
194
|
+
}, source, at, now));
|
|
195
|
+
}
|
|
196
|
+
for (const custom of quota.customWindows ?? []) push(customWindow(custom, source, at, now));
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function accountState(input: { reauth: boolean; paused: boolean; windows: readonly QuotaPoolWindow[] }): QuotaPoolAccountState {
|
|
201
|
+
if (input.reauth) return "reauth";
|
|
202
|
+
if (input.paused) return "paused";
|
|
203
|
+
// A model-scoped window never gates the account as a whole.
|
|
204
|
+
if (input.windows.some(window => window.exhausted === true && window.scopeModel === undefined)) return "exhausted";
|
|
205
|
+
return input.windows.length === 0 ? "unknown" : "available";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function nextResetAt(windows: readonly QuotaPoolWindow[]): string | undefined {
|
|
209
|
+
const resets = windows.filter(window => window.scopeModel === undefined && window.resetsAt).map(window => window.resetsAt!).sort();
|
|
210
|
+
return resets[0];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reportSource(report: ProviderQuotaReport): QuotaPoolSource {
|
|
214
|
+
if (report.aggregation) return "estimated";
|
|
215
|
+
return report.observed === true ? "header" : "measured";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function codexAccounts(identity: QuotaPoolIdentity, accounts: Awaited<ReturnType<typeof listCodexAuthAccountsSnapshot>>["accounts"], now: number): QuotaPoolAccount[] {
|
|
219
|
+
const provider = OPENAI_CODEX_PROVIDER_ID;
|
|
220
|
+
return accounts.map(dto => {
|
|
221
|
+
const source: QuotaPoolSource = latestCodexQuotaObservationSource(dto.id) === "response-header" ? "header" : "measured";
|
|
222
|
+
const windows = dto.quota ? codexQuotaWindows(dto.quota as StoredAccountQuota, source, now) : [];
|
|
223
|
+
const account: QuotaPoolAccount = {
|
|
224
|
+
provider,
|
|
225
|
+
accountRef: quotaPoolAccountRef(identity, provider, dto.id),
|
|
226
|
+
// needsReauth only: a passive read leaves hasCredential false when it did not check, and an
|
|
227
|
+
// account without a credential already carries needsReauth.
|
|
228
|
+
state: accountState({ reauth: dto.needsReauth === true, paused: dto.paused, windows }),
|
|
229
|
+
windows,
|
|
230
|
+
};
|
|
231
|
+
if (typeof dto.logLabel === "string" && ACCOUNT_LOG_LABEL_RE.test(dto.logLabel)) account.label = dto.logLabel;
|
|
232
|
+
if (typeof dto.plan === "string" && dto.plan.trim()) account.plan = dto.plan.trim();
|
|
233
|
+
return account;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function oauthAccounts(identity: QuotaPoolIdentity, provider: string, now: number): QuotaPoolAccount[] {
|
|
238
|
+
const set = getAccountSet(provider);
|
|
239
|
+
if (!set) return [];
|
|
240
|
+
return set.accounts.map(row => {
|
|
241
|
+
const quota = getCachedProviderAccountQuota(provider, row.id);
|
|
242
|
+
const entry = accountQuotaCache.get(accountCacheKey(provider, row.id));
|
|
243
|
+
const windows = quota ? providerQuotaWindows(quota, "measured", now) : [];
|
|
244
|
+
const account: QuotaPoolAccount = {
|
|
245
|
+
provider,
|
|
246
|
+
accountRef: quotaPoolAccountRef(identity, provider, row.id),
|
|
247
|
+
label: oauthAccountLogLabel(row.id, provider),
|
|
248
|
+
state: accountState({ reauth: row.needsReauth === true, paused: row.paused === true, windows }),
|
|
249
|
+
windows,
|
|
250
|
+
};
|
|
251
|
+
const failure = entry?.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? parseQuotaFailureCode(entry.quotaFailure) : undefined;
|
|
252
|
+
if (failure) account.failure = failure;
|
|
253
|
+
return account;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function claudeCliAccounts(identity: QuotaPoolIdentity, provider: string, deps: QuotaPoolDeps, now: number): QuotaPoolAccount[] {
|
|
258
|
+
const observation = peekClaudeUsageObservation(deps.claudeUsageStatePath ? { statePath: deps.claudeUsageStatePath } : {});
|
|
259
|
+
if (!observation) return [];
|
|
260
|
+
const windows = observation.quota ? providerQuotaWindows(observation.quota, "measured", now) : [];
|
|
261
|
+
if (finite(observation.refusalUntil) && observation.refusalUntil > now) {
|
|
262
|
+
const refusal = finish({ id: "refusal", kind: "custom", label: "Claude CLI limit", exhausted: true, resetMs: observation.refusalUntil },
|
|
263
|
+
"estimated", observation.checkedAt, now);
|
|
264
|
+
if (refusal) windows.push(refusal);
|
|
265
|
+
}
|
|
266
|
+
return [{
|
|
267
|
+
provider,
|
|
268
|
+
accountRef: quotaPoolAccountRef(identity, provider, `claude-cli:${observation.identity}`),
|
|
269
|
+
state: accountState({ reauth: false, paused: false, windows }),
|
|
270
|
+
windows,
|
|
271
|
+
}];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function role(config: OcxConfig): QuotaPoolRole {
|
|
275
|
+
return config.runtimeRole === "hub" || config.runtimeRole === "client" ? config.runtimeRole : "standalone";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function enabled(provider: OcxProviderConfig | undefined): provider is OcxProviderConfig {
|
|
279
|
+
return !!provider && provider.disabled !== true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function assemble(config: OcxConfig, identity: QuotaPoolIdentity, deps: QuotaPoolDeps): Promise<QuotaPool> {
|
|
283
|
+
const now = (deps.now ?? Date.now)();
|
|
284
|
+
const names = Object.keys(config.providers).filter(name => enabled(config.providers[name])).sort();
|
|
285
|
+
const reports = new Map(heldProviderQuotaReports(names).reports.map(report => [report.provider, report]));
|
|
286
|
+
const codexPool = hasCodexPoolProvider(config);
|
|
287
|
+
const codex = codexPool ? (await listCodexAuthAccountsSnapshot(config)).accounts : [];
|
|
288
|
+
const providers: QuotaPoolProvider[] = [];
|
|
289
|
+
for (const name of names) {
|
|
290
|
+
const provider = config.providers[name]!;
|
|
291
|
+
let accounts: QuotaPoolAccount[] = [];
|
|
292
|
+
let tracked = false;
|
|
293
|
+
if (codexPool && name === OPENAI_CODEX_PROVIDER_ID) {
|
|
294
|
+
tracked = true;
|
|
295
|
+
accounts = codexAccounts(identity, codex, now);
|
|
296
|
+
} else if (provider.adapter === "claude-cli") {
|
|
297
|
+
tracked = true;
|
|
298
|
+
accounts = claudeCliAccounts(identity, name, deps, now);
|
|
299
|
+
} else if (supportsPerAccountQuota(name) || hasPassiveAccountQuota(name)) {
|
|
300
|
+
tracked = true;
|
|
301
|
+
accounts = oauthAccounts(identity, name, now);
|
|
302
|
+
}
|
|
303
|
+
const report = reports.get(name);
|
|
304
|
+
const pooledWindows = report ? providerQuotaWindows(report.quota, reportSource(report), now) : [];
|
|
305
|
+
if (!tracked && !report) continue;
|
|
306
|
+
const pooled: QuotaPoolProvider["pooled"] = { windows: pooledWindows };
|
|
307
|
+
const next = nextResetAt(pooledWindows);
|
|
308
|
+
if (next) pooled.nextResetAt = next;
|
|
309
|
+
providers.push({ provider: name, accounts, pooled });
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
schema: QUOTA_POOL_SCHEMA,
|
|
313
|
+
producer: { version: VERSION, role: role(config), homeId: identity.homeId },
|
|
314
|
+
generatedAt: iso(now) ?? new Date().toISOString(),
|
|
315
|
+
providers,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The quota pool, or an identity error. Always a passive read, whoever asks. */
|
|
320
|
+
export function readQuotaPool(config: OcxConfig, deps: QuotaPoolDeps = {}): Promise<QuotaPoolResult> {
|
|
321
|
+
return runPassiveRead(async () => {
|
|
322
|
+
const identity = readQuotaPoolIdentity();
|
|
323
|
+
if (!identity) return { ok: false, error: "quota_pool_identity_unavailable" } as const;
|
|
324
|
+
return { ok: true, pool: await assemble(config, identity, deps) } as const;
|
|
325
|
+
});
|
|
326
|
+
}
|