@bitkyc08/opencodex 2.87.0 → 2.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/index-uchD-ngf.js +94 -0
  7. package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  13. package/src/adapters/anthropic-replay.ts +135 -0
  14. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  15. package/src/adapters/anthropic.ts +86 -48
  16. package/src/adapters/coding-agent/protocol.ts +6 -0
  17. package/src/adapters/google-http.ts +3 -2
  18. package/src/adapters/google-progress-history.ts +53 -0
  19. package/src/adapters/google-vertex-endpoint.ts +67 -0
  20. package/src/adapters/google.ts +52 -30
  21. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  22. package/src/adapters/openai-responses/passthrough.ts +17 -12
  23. package/src/adapters/openai-responses/reasoning.ts +68 -1
  24. package/src/adapters/registry.ts +1 -1
  25. package/src/adapters/upstream-http-error.ts +3 -2
  26. package/src/adapters/zed.ts +7 -4
  27. package/src/bridge/internal.ts +17 -3
  28. package/src/bridge/response-json.ts +4 -4
  29. package/src/bridge/sse.ts +17 -10
  30. package/src/claude/inbound-content-options.ts +66 -0
  31. package/src/claude/inbound.ts +12 -26
  32. package/src/claude/outbound.ts +18 -5
  33. package/src/cli/account-api.ts +13 -1
  34. package/src/cli/account-extended.ts +17 -5
  35. package/src/cli/account-key-quota.ts +4 -1
  36. package/src/cli/account.ts +1 -1
  37. package/src/cli/capabilities-agents-routing.ts +45 -8
  38. package/src/cli/capabilities-base.ts +5 -5
  39. package/src/cli/capabilities-provider-models.ts +5 -4
  40. package/src/cli/combo-result.ts +8 -2
  41. package/src/cli/combo.ts +17 -0
  42. package/src/cli/cross-home-owner.ts +16 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/loopback-family.ts +31 -0
  45. package/src/cli/message-command.ts +4 -1
  46. package/src/cli/message-remote-args.ts +69 -0
  47. package/src/cli/message-remote-command.ts +77 -0
  48. package/src/cli/provider-runtime.ts +69 -1
  49. package/src/cli/registry.ts +3 -3
  50. package/src/codex/account-pause.ts +6 -1
  51. package/src/codex/account-runtime-state.ts +5 -0
  52. package/src/codex/account-store.ts +30 -3
  53. package/src/codex/auth-api/account-list.ts +3 -0
  54. package/src/codex/auth-api/main-account-probe.ts +3 -2
  55. package/src/codex/auth-context.ts +9 -4
  56. package/src/codex/catalog/aggregation.ts +15 -0
  57. package/src/codex/catalog/auto-review.ts +2 -1
  58. package/src/codex/catalog/build-entries.ts +17 -7
  59. package/src/codex/catalog/derive-entry.ts +3 -1
  60. package/src/codex/catalog/metadata.ts +2 -0
  61. package/src/codex/catalog/model-visibility.ts +0 -2
  62. package/src/codex/catalog/ownership.ts +71 -0
  63. package/src/codex/catalog/parsing.ts +12 -3
  64. package/src/codex/catalog/retained-sync.ts +2 -1
  65. package/src/codex/catalog/routed-gather.ts +3 -1
  66. package/src/codex/catalog/routed-removal.ts +4 -4
  67. package/src/codex/convergence.ts +2 -1
  68. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  69. package/src/codex/low-quota-protection.ts +57 -31
  70. package/src/codex/main-account-cache.ts +77 -3
  71. package/src/codex/main-account.ts +6 -2
  72. package/src/codex/native-residue.ts +2 -3
  73. package/src/codex/quota.ts +26 -2
  74. package/src/codex/websocket-activity.ts +161 -0
  75. package/src/codex/websocket-registry.ts +25 -1
  76. package/src/combos/failover.ts +2 -1
  77. package/src/combos/jev-model-backend.ts +30 -7
  78. package/src/combos/jev-model-request.ts +20 -0
  79. package/src/combos/jev-quota-activation.ts +153 -0
  80. package/src/combos/jev-quota-config.ts +62 -0
  81. package/src/combos/jev-quota-route.ts +26 -0
  82. package/src/combos/jev-quota.ts +88 -0
  83. package/src/combos/jev.ts +25 -12
  84. package/src/combos/types.ts +27 -0
  85. package/src/compatibility/index.ts +3 -0
  86. package/src/compatibility/openai-responses.ts +8 -1
  87. package/src/compatibility/opengateway-responses.ts +61 -0
  88. package/src/config/account-auto-pause-revisions.ts +136 -0
  89. package/src/config/diagnostics.ts +13 -0
  90. package/src/config/live-reconcile.ts +9 -0
  91. package/src/config/opencode-zen-upgrade.ts +9 -8
  92. package/src/config/persist-unlocked.ts +10 -1
  93. package/src/config/removed-opencode-free.ts +135 -0
  94. package/src/config/schema/account-auto-pause.ts +14 -0
  95. package/src/config/schema/config-schema.ts +15 -0
  96. package/src/config/schema/leaf-validators.ts +7 -0
  97. package/src/config.ts +10 -3
  98. package/src/generated/compatibility-version.json +411 -175
  99. package/src/images/loop.ts +101 -27
  100. package/src/lib/account-auto-pause-response.ts +16 -0
  101. package/src/lib/bun-runtime.ts +18 -1
  102. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  103. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  104. package/src/lib/decision-quota-sink.ts +76 -0
  105. package/src/lib/request-failure-model.ts +2 -0
  106. package/src/lib/state-store-registrations.ts +25 -0
  107. package/src/link/ssh-argv.ts +3 -0
  108. package/src/messaging/envelope.ts +6 -3
  109. package/src/messaging/remote-auth.ts +83 -0
  110. package/src/messaging/remote-bridge.ts +270 -0
  111. package/src/messaging/remote-contract.ts +108 -0
  112. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  113. package/src/messaging/remote-enrollment.ts +244 -0
  114. package/src/messaging/remote-envelope.ts +40 -0
  115. package/src/messaging/remote-files.ts +117 -0
  116. package/src/messaging/remote-owner.ts +169 -0
  117. package/src/messaging/remote-ports.ts +56 -0
  118. package/src/messaging/remote-process.ts +131 -0
  119. package/src/messaging/remote-replies.ts +98 -0
  120. package/src/messaging/remote-rpc-admission.ts +32 -0
  121. package/src/messaging/remote-send.ts +72 -0
  122. package/src/messaging/remote-store.ts +111 -0
  123. package/src/messaging/remote-tunnels.ts +65 -0
  124. package/src/messaging/rpc.ts +13 -1
  125. package/src/oauth/account-auto-pause-policy.ts +66 -0
  126. package/src/oauth/account-auto-pause.ts +171 -0
  127. package/src/oauth/account-quota-rank.ts +2 -1
  128. package/src/oauth/anthropic-account-refusal.ts +6 -0
  129. package/src/oauth/anthropic-model-family.ts +8 -0
  130. package/src/oauth/anthropic-model-quota.ts +2 -6
  131. package/src/oauth/api-key-pool-preserve.ts +37 -0
  132. package/src/oauth/generic-account-failover.ts +7 -4
  133. package/src/oauth/index.ts +5 -35
  134. package/src/oauth/store.ts +35 -8
  135. package/src/oauth/types.ts +2 -0
  136. package/src/plugins/upstream-hooks.ts +14 -1
  137. package/src/protocols/encoders/adapter-events.ts +5 -5
  138. package/src/protocols/encoders/messages.ts +2 -2
  139. package/src/providers/api-key-pause.ts +32 -0
  140. package/src/providers/api-key-resolve.ts +10 -4
  141. package/src/providers/api-key-selection.ts +6 -1
  142. package/src/providers/api-keys.ts +8 -4
  143. package/src/providers/claude-cli-usage.ts +5 -0
  144. package/src/providers/key-failover.ts +11 -11
  145. package/src/providers/model-rename-fields.ts +1 -0
  146. package/src/providers/opencode-zen-rate-limit.ts +12 -73
  147. package/src/providers/quota/account-cache.ts +15 -1
  148. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  150. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  151. package/src/providers/quota/antigravity.ts +39 -6
  152. package/src/providers/quota/report-cache.ts +14 -0
  153. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  154. package/src/providers/quota-decision-publication.ts +77 -0
  155. package/src/providers/quota-decision-snapshot.ts +145 -0
  156. package/src/providers/quota-routing-cache.ts +3 -0
  157. package/src/providers/quota-types.ts +6 -1
  158. package/src/providers/quota.ts +3 -2
  159. package/src/providers/registry/entries-extended.ts +8 -52
  160. package/src/providers/registry/model-seeds.ts +1 -1
  161. package/src/providers/registry.ts +1 -2
  162. package/src/providers/stale-vision-classification-migration.ts +1 -1
  163. package/src/responses/parser.ts +18 -10
  164. package/src/responses/reasoning-replay-cache.ts +50 -4
  165. package/src/responses/web-search-replay.ts +138 -0
  166. package/src/routing/analytics.ts +7 -15
  167. package/src/routing/compatibility/behavior.ts +4 -1
  168. package/src/server/auth-cors.ts +7 -0
  169. package/src/server/background-lifecycle.ts +2 -1
  170. package/src/server/chat-native.ts +28 -13
  171. package/src/server/claude-messages.ts +4 -2
  172. package/src/server/index/live-sideband.ts +3 -2
  173. package/src/server/index/optional-listeners.ts +6 -0
  174. package/src/server/index/serve-options.ts +11 -5
  175. package/src/server/index/websocket-handler.ts +18 -4
  176. package/src/server/index.ts +7 -7
  177. package/src/server/lifecycle.ts +13 -1
  178. package/src/server/management/combo-routes.ts +17 -2
  179. package/src/server/management/oauth-account-routes.ts +10 -1
  180. package/src/server/management/provider-completion-test.ts +289 -0
  181. package/src/server/management/provider-routes.ts +9 -1
  182. package/src/server/management/route-registry.ts +2 -0
  183. package/src/server/messages-native.ts +34 -7
  184. package/src/server/messages-shared.ts +4 -1
  185. package/src/server/request-log.ts +7 -2
  186. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  187. package/src/server/responses/adapter-continuation.ts +34 -18
  188. package/src/server/responses/adapter-delivery.ts +53 -6
  189. package/src/server/responses/adapter-dispatch.ts +104 -29
  190. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  191. package/src/server/responses/combo-session-recall.ts +25 -0
  192. package/src/server/responses/combo-stream-preflight.ts +6 -3
  193. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  194. package/src/server/responses/compact.ts +8 -5
  195. package/src/server/responses/compaction-recovery.ts +2 -0
  196. package/src/server/responses/context-overflow.ts +141 -2
  197. package/src/server/responses/core-codex-account.ts +9 -3
  198. package/src/server/responses/core-combo.ts +34 -7
  199. package/src/server/responses/core-lifetime.ts +3 -2
  200. package/src/server/responses/core-options.ts +2 -0
  201. package/src/server/responses/core-replay.ts +5 -1
  202. package/src/server/responses/core.ts +12 -9
  203. package/src/server/responses/empty-completion-guard.ts +2 -0
  204. package/src/server/responses/fetch-helpers.ts +35 -4
  205. package/src/server/responses/jev-model-invoke.ts +13 -14
  206. package/src/server/responses/native-steering-log.ts +2 -2
  207. package/src/server/responses/passthrough-delivery.ts +3 -2
  208. package/src/server/responses/passthrough-dispatch.ts +30 -33
  209. package/src/server/responses/passthrough-execution.ts +3 -2
  210. package/src/server/responses/passthrough-observations.ts +38 -0
  211. package/src/server/responses/request-compression.ts +98 -0
  212. package/src/server/responses/request-prepare.ts +1 -0
  213. package/src/server/responses/request-transport.ts +32 -3
  214. package/src/server/responses/response-effects.ts +1 -0
  215. package/src/server/responses/response-tool-ownership.ts +21 -0
  216. package/src/server/responses/run-turn-execution.ts +2 -0
  217. package/src/server/responses/sidecar-execution.ts +59 -10
  218. package/src/server/responses/sidecar-send-budget.ts +2 -2
  219. package/src/server/responses/terminal-guard.ts +2 -0
  220. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  221. package/src/server/startup-action-control.ts +2 -2
  222. package/src/server/startup-health-cache.ts +2 -2
  223. package/src/server/windows-tray-control.ts +2 -2
  224. package/src/server/ws-bridge.ts +2 -0
  225. package/src/service/definition.ts +9 -1
  226. package/src/types/account-auto-pause.ts +15 -0
  227. package/src/types/config.ts +23 -1
  228. package/src/types/opencode-zen.ts +9 -3
  229. package/src/types/provider.ts +9 -5
  230. package/src/types/request.ts +30 -4
  231. package/src/types/wire.ts +4 -2
  232. package/src/types.ts +2 -0
  233. package/src/usage/cost.ts +17 -1
  234. package/src/usage/expected-prices.ts +3 -1
  235. package/src/usage/jev-stats.ts +4 -0
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/summary.ts +16 -3
  238. package/src/usage/telemetry-contract.ts +1 -0
  239. package/src/web-search/anthropic-history.ts +59 -0
  240. package/src/web-search/loop.ts +212 -73
  241. package/src/web-search/progress-stream.ts +14 -1
  242. package/src/web-search/run-turn-loop.ts +9 -3
  243. package/src/web-search/sidecar-search-replay.ts +96 -0
  244. package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
  245. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
  246. package/gui/dist/assets/index-CfYDMj34.js +0 -94
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Activation owner for JEV advisory quota evidence.
3
+ *
4
+ * The decision snapshot (`src/providers/quota-decision-snapshot.ts`) is imported here and by
5
+ * nothing on the core path. Core credential stores and quota producers call the slot in
6
+ * `src/lib/decision-quota-sink.ts`; this module registers the snapshot-backed sink only while the
7
+ * live config has a JEV combo with `decisionQuotaSignals: true`, and withdraws everything when the
8
+ * last such combo is turned off, deleted or the config root changes.
9
+ *
10
+ * Activation replays the current stores through observe-only reads (`peekAuthStore`,
11
+ * `peekCodexAccountRecordStore`, `peekMainDecisionRow`): no hardening, backup, write, credential
12
+ * resolution or upstream probe, and never on a request's decision path. Stores keep no in-memory
13
+ * cache, so this one bounded read at activation is what "publish once from the loaded stores" costs,
14
+ * paid only by installs that opted in.
15
+ */
16
+ import {
17
+ clearDecisionAccountQuotas, invalidateDecisionKeyQuotas, observeDecisionKeyCredential, publishDecisionAccountQuota,
18
+ publishDecisionKeyQuota, publishDecisionQuotaRoster, readLoadedDecisionKeyQuota, readLoadedDecisionQuotaPool,
19
+ setDecisionAccountUsable, withdrawDecisionQuotaRoster,
20
+ } from "../providers/quota-decision-snapshot";
21
+ import { registerDecisionQuotaSink, type DecisionQuotaSink, type MainDecisionRow } from "../lib/decision-quota-sink";
22
+ import { registerLiveConfigObserver } from "../lib/state-store-registrations";
23
+ import { getConfigDir } from "../config/paths";
24
+ import { peekAuthStore } from "../oauth/store";
25
+ import { peekCodexAccountRecordStore } from "../codex/account-store";
26
+ import { peekMainDecisionRow } from "../codex/main-account-cache";
27
+ import { createHash } from "node:crypto";
28
+ import type { AuthStore } from "../oauth/store";
29
+ import type { CodexAccountCredentialRecord, OcxConfig } from "../types";
30
+
31
+ const POOL_PROVIDERS = ["anthropic", "codex", "codex-main"] as const;
32
+
33
+ /** Project an auth store the owner already read into the secret-free Anthropic roster (primary pool only); a failure withdraws so the pool reads as unknown. The generation is a one-way digest of the credential, never the credential itself. */
34
+ function publishAuthDecisionQuotaRoster(store: AuthStore): void {
35
+ try {
36
+ publishDecisionQuotaRoster("anthropic", (store.anthropic?.accounts ?? []).map(row => ({
37
+ id: row.id,
38
+ generation: createHash("sha256").update(JSON.stringify([row.credential.refresh, row.credential.access, row.credential.expires])).digest("hex"),
39
+ usable: row.paused !== true && row.needsReauth !== true && Boolean(row.credential.access || row.credential.refresh),
40
+ })));
41
+ } catch {
42
+ try { withdrawDecisionQuotaRoster("anthropic"); } catch { /* unknown */ }
43
+ }
44
+ }
45
+
46
+ /** Project a Codex account store the owner already read into the secret-free roster (id, credential generation, usability); a failure withdraws. */
47
+ function publishCodexDecisionQuotaRoster(store: Readonly<Record<string, CodexAccountCredentialRecord>>): void {
48
+ try {
49
+ publishDecisionQuotaRoster("codex", Object.entries(store).map(([id, row]) => ({
50
+ id, generation: row.generation,
51
+ usable: !!row.credential && row.deletedAt == null && !row.codexValidationPending && !row.lastCodexValidationTerminal,
52
+ })));
53
+ } catch {
54
+ try { withdrawDecisionQuotaRoster("codex"); } catch { /* unknown */ }
55
+ }
56
+ }
57
+
58
+ /** Whether a config enables advisory quota signals on at least one JEV combo. */
59
+ export function jevQuotaSignalsWanted(config: Pick<OcxConfig, "combos"> | null | undefined): boolean {
60
+ return Object.values(config?.combos ?? {}).some(combo => combo?.strategy === "jev" && combo.decisionQuotaSignals === true);
61
+ }
62
+
63
+ /** Publish the native-main row, or withdraw it; a failed publication withdraws so a previous row cannot linger (same guard as the auth/Codex projections). */
64
+ function publishMainRow(row: MainDecisionRow | undefined): void {
65
+ try {
66
+ if (row) publishDecisionQuotaRoster("codex-main", [{ id: "__main__", generation: row.generation, usable: row.usable }]);
67
+ else withdrawDecisionQuotaRoster("codex-main");
68
+ } catch {
69
+ try { withdrawDecisionQuotaRoster("codex-main"); } catch { /* best-effort; the pool reads as unknown */ }
70
+ }
71
+ }
72
+
73
+ const snapshotSink: DecisionQuotaSink = {
74
+ observeKeyCredential: observeDecisionKeyCredential,
75
+ invalidateKeyQuotas: invalidateDecisionKeyQuotas,
76
+ publishKeyQuota: publishDecisionKeyQuota,
77
+ publishAccountQuota: (provider, id, generation, windows, partial) => publishDecisionAccountQuota(provider, id, generation, windows, partial),
78
+ setAccountUsable: setDecisionAccountUsable,
79
+ clearAccountQuotas: clearDecisionAccountQuotas,
80
+ authStoreLoaded: publishAuthDecisionQuotaRoster,
81
+ codexStoreLoaded: publishCodexDecisionQuotaRoster,
82
+ mainRosterChanged: row => publishMainRow(row),
83
+ readKeyQuota: readLoadedDecisionKeyQuota,
84
+ readPool: readLoadedDecisionQuotaPool,
85
+ };
86
+
87
+ let active: { detach: () => void; root: string } | undefined;
88
+
89
+ function withdrawAll(): void {
90
+ for (const provider of POOL_PROVIDERS) withdrawDecisionQuotaRoster(provider);
91
+ clearDecisionAccountQuotas();
92
+ invalidateDecisionKeyQuotas();
93
+ }
94
+
95
+ /**
96
+ * Replay the current stores into the snapshot through observe-only reads. Each pool fails closed to
97
+ * unknown. Ownership is rechecked after every read: a reentrant notification that deactivates (or
98
+ * restarts) during a read must not be followed by a publication from this stale replay.
99
+ */
100
+ function replayCurrentStores(owner: object): void {
101
+ const owns = () => active === owner;
102
+ try { const store = peekAuthStore(); if (owns()) publishAuthDecisionQuotaRoster(store); } catch { if (owns()) withdrawDecisionQuotaRoster("anthropic"); }
103
+ try { const store = peekCodexAccountRecordStore(); if (owns()) publishCodexDecisionQuotaRoster(store); } catch { if (owns()) withdrawDecisionQuotaRoster("codex"); }
104
+ try { const row = peekMainDecisionRow(); if (owns()) publishMainRow(row); } catch { if (owns()) { try { withdrawDecisionQuotaRoster("codex-main"); } catch { /* unknown */ } } }
105
+ }
106
+
107
+ /** Detach the sink first, so no producer can write while the snapshot is withdrawn. */
108
+ export function deactivateJevQuotaActivation(): void {
109
+ const current = active;
110
+ if (!current) return;
111
+ active = undefined;
112
+ current.detach();
113
+ try { withdrawAll(); } catch { /* the snapshot is unreachable once detached */ }
114
+ }
115
+
116
+ /** Converge activation with a live config: register on the first enabling combo, withdraw after the last, and restart on a config root change. Never throws. */
117
+ export function reconcileJevQuotaActivation(config: OcxConfig): void {
118
+ try {
119
+ const wanted = jevQuotaSignalsWanted(config);
120
+ const root = getConfigDir();
121
+ if (active && (!wanted || active.root !== root)) deactivateJevQuotaActivation();
122
+ if (!wanted || active) return;
123
+ const owner = { detach: registerDecisionQuotaSink(snapshotSink), root };
124
+ active = owner;
125
+ replayCurrentStores(owner);
126
+ } catch {
127
+ deactivateJevQuotaActivation();
128
+ }
129
+ }
130
+
131
+ /** Whether the snapshot-backed sink is currently registered by this owner. */
132
+ export function jevQuotaActivationActive(): boolean { return active !== undefined; }
133
+
134
+ let stopCurrent: (() => void) | undefined;
135
+
136
+ /**
137
+ * Live-owner lifecycle, started by the server's optional-subsystem set once it is serving.
138
+ * Registers the live-config observer and reconciles once with the start-time config; the returned
139
+ * stop unregisters and deactivates. A second start replaces the first.
140
+ */
141
+ export function startJevQuotaActivation(config: OcxConfig): () => void {
142
+ stopCurrent?.();
143
+ const unregister = registerLiveConfigObserver("jev-quota-activation", reconcileJevQuotaActivation);
144
+ const stop = () => {
145
+ if (stopCurrent !== stop) return;
146
+ stopCurrent = undefined;
147
+ unregister();
148
+ deactivateJevQuotaActivation();
149
+ };
150
+ stopCurrent = stop;
151
+ reconcileJevQuotaActivation(config);
152
+ return stop;
153
+ }
@@ -0,0 +1,62 @@
1
+ /** Import-free quota policy shared by config, CLI and telemetry. */
2
+ export interface JevQuotaTiers {
3
+ moderate?: number;
4
+ limited?: number;
5
+ nearlyExhausted?: number;
6
+ }
7
+ export interface ResolvedJevQuotaTiers {
8
+ moderate?: number;
9
+ limited: number;
10
+ nearlyExhausted: number;
11
+ }
12
+ export const JEV_QUOTA_TIERS = ["unknown", "healthy", "moderate", "limited", "nearly_exhausted"] as const;
13
+ export type JevQuotaTier = typeof JEV_QUOTA_TIERS[number];
14
+ export const JEV_QUOTA_DEFAULT_TIERS: ResolvedJevQuotaTiers = { limited: 70, nearlyExhausted: 90 };
15
+ /** Validate quota tier overrides and merge them with the defaults; return undefined for unknown keys, non-finite or out-of-range values, or thresholds that are not strictly ascending. */
16
+ export function normalizeJevQuotaTiers(value: unknown): ResolvedJevQuotaTiers | undefined {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
18
+ const raw = value as Record<string, unknown>;
19
+ if (Object.keys(raw).some(key => !["moderate", "limited", "nearlyExhausted"].includes(key))) return undefined;
20
+ if (Object.keys(raw).some(key => raw[key] === undefined)) return undefined;
21
+ const result = { ...JEV_QUOTA_DEFAULT_TIERS, ...raw };
22
+ const values = [result.moderate, result.limited, result.nearlyExhausted].filter(value => value !== undefined);
23
+ if (values.some(value => typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100)) return undefined;
24
+ if (Object.hasOwn(raw, "moderate") && raw.moderate === undefined) return undefined;
25
+ if (values.some((value, i) => i > 0 && value! <= values[i - 1]!)) return undefined;
26
+ return result as ResolvedJevQuotaTiers;
27
+ }
28
+ /** Map a used percentage to a quota tier using the thresholds; a non-finite or out-of-range percentage is unknown. */
29
+ export function jevQuotaTier(percent: number, tiers: ResolvedJevQuotaTiers = JEV_QUOTA_DEFAULT_TIERS): JevQuotaTier {
30
+ if (!Number.isFinite(percent) || percent < 0 || percent > 100) return "unknown";
31
+ if (percent >= tiers.nearlyExhausted) return "nearly_exhausted";
32
+ if (percent >= tiers.limited) return "limited";
33
+ if (tiers.moderate !== undefined && percent >= tiers.moderate) return "moderate";
34
+ return "healthy";
35
+ }
36
+ /** Rank a quota tier for ordering; unknown ties healthy so missing evidence is never treated as exhausted. */
37
+ export function jevQuotaRank(tier: JevQuotaTier): number {
38
+ return tier === "unknown" || tier === "healthy" ? 0 : tier === "moderate" ? 1 : tier === "limited" ? 2 : 3;
39
+ }
40
+ export interface JevQuotaDecisionSummary {
41
+ unknown: number;
42
+ healthy: number;
43
+ moderate: number;
44
+ limited: number;
45
+ nearly_exhausted: number;
46
+ selected?: JevQuotaTier;
47
+ }
48
+ /** Validate a persisted decision quota summary (bounded per-tier counts and an optional selected tier), returning undefined when it is malformed. */
49
+ export function normalizeJevQuotaSummary(value: unknown): JevQuotaDecisionSummary | undefined {
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
51
+ const raw = value as Record<string, unknown>;
52
+ const result = { unknown: 0, healthy: 0, moderate: 0, limited: 0, nearly_exhausted: 0 };
53
+ let total = 0;
54
+ for (const tier of JEV_QUOTA_TIERS) {
55
+ const count = raw[tier];
56
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 0 || count > 64) return undefined;
57
+ result[tier] = count; total += count;
58
+ }
59
+ if (total > 64) return undefined;
60
+ const selected = JEV_QUOTA_TIERS.find(tier => tier === raw.selected);
61
+ return { ...result, ...(selected ? { selected } : {}) };
62
+ }
@@ -0,0 +1,26 @@
1
+ import { jevQuotaDecisionSummary, jevQuotaSignalForTarget } from "./jev-quota";
2
+ import { JEV_QUOTA_DEFAULT_TIERS, normalizeJevQuotaTiers } from "./jev-quota-config";
3
+ import type { JevCandidate, JevDecision, ResolveJevDecisionOptions } from "./jev";
4
+ /** Attach a loaded-only quota signal to each candidate when the combo opted in; otherwise return the candidates unchanged. */
5
+ export function jevQuotaCandidates(options: ResolveJevDecisionOptions): readonly JevCandidate[] {
6
+ if (options.decisionQuotaSignals !== true) return options.candidates;
7
+ const tiers = normalizeJevQuotaTiers(options.decisionQuotaTiers) ?? JEV_QUOTA_DEFAULT_TIERS;
8
+ const now = (options.now ?? Date.now)();
9
+ return options.candidates.map(candidate => ({ ...candidate,
10
+ quota: jevQuotaSignalForTarget(options.config, candidate.provider, candidate.model, now, tiers) }));
11
+ }
12
+ /** Attach the per-tier quota summary to a decision when any candidate carried quota evidence. */
13
+ export function withJevQuotaSummary(decision: JevDecision, candidates: readonly JevCandidate[]): JevDecision {
14
+ const quota = jevQuotaDecisionSummary(candidates, decision.targetKey);
15
+ return quota ? { ...decision, quota } : decision;
16
+ }
17
+ /** Retries serialization without quota, never truncates ordinary evidence or the allowlist. */
18
+ export function boundedJevQuotaPayload(candidates: readonly JevCandidate[], build: (rows: readonly JevCandidate[]) => string, limit: number): { body: string; candidates: readonly JevCandidate[] } {
19
+ let rows = candidates;
20
+ let body = build(rows);
21
+ if (new TextEncoder().encode(body).byteLength > limit && rows.some(row => row.quota)) {
22
+ rows = rows.map(({ quota: _quota, ...row }) => row);
23
+ body = build(rows);
24
+ }
25
+ return { body, candidates: rows };
26
+ }
@@ -0,0 +1,88 @@
1
+ import { anthropicModelFamily } from "../oauth/anthropic-model-family";
2
+ /** Advisory quota only: loaded snapshots, never serving-account selection or hard eligibility. */
3
+ import { resolveAnthropicModelRoute, routeCandidates } from "../oauth/anthropic-model-routes";
4
+ import type { OcxConfig } from "../types";
5
+ import type { DecisionQuotaWindow } from "../providers/quota-decision-snapshot";
6
+ import { readLoadedDecisionKeyQuota, readLoadedDecisionQuotaPool } from "../lib/decision-quota-sink";
7
+ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination";
8
+ import { providerCodexAccountMode } from "../providers/registry";
9
+ import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
10
+ import { isSelectableCodexPoolAccount, MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
11
+ import { JEV_QUOTA_DEFAULT_TIERS, jevQuotaRank, jevQuotaTier, type JevQuotaTier, type ResolvedJevQuotaTiers, type JevQuotaDecisionSummary } from "./jev-quota-config";
12
+ export const JEV_QUOTA_SIGNAL_MAX_AGE_MS = 30 * 60_000;
13
+ export interface JevQuotaSignal {
14
+ tier: JevQuotaTier;
15
+ usedPercent?: number;
16
+ window?: DecisionQuotaWindow["window"];
17
+ resetsInSeconds?: number;
18
+ }
19
+ /** Build the signal used when evidence is missing, stale or invalid; it ties healthy. */
20
+ const unknown = (): JevQuotaSignal => ({ tier: "unknown" });
21
+ /** Reduce decision quota windows to the worst applicable window for a model, ignoring cold, stale, future, invalid or reset-expired rows. */
22
+ export function jevQuotaSignalFromWindows(windows: readonly DecisionQuotaWindow[] | undefined, model: string, now: number, tiers = JEV_QUOTA_DEFAULT_TIERS): JevQuotaSignal {
23
+ let worst: DecisionQuotaWindow | undefined;
24
+ for (const row of windows ?? []) {
25
+ if (!["5h", "weekly", "monthly"].includes(row.window) && anthropicModelFamily(model)?.toLowerCase() !== row.window) continue;
26
+ if (!Number.isFinite(now) || now < 0 || !Number.isFinite(row.observedAt) || row.observedAt < 0 || row.observedAt > now
27
+ || now - row.observedAt > JEV_QUOTA_SIGNAL_MAX_AGE_MS || !Number.isFinite(row.percent) || row.percent < 0 || row.percent > 100
28
+ || row.resetAt !== undefined && (!Number.isFinite(row.resetAt) || row.resetAt < 0 || row.resetAt <= now)) continue;
29
+ if (!worst || row.percent > worst.percent) worst = row;
30
+ }
31
+ if (!worst) return unknown();
32
+ return { tier: jevQuotaTier(worst.percent, tiers), usedPercent: worst.percent, window: worst.window,
33
+ ...(worst.resetAt !== undefined ? { resetsInSeconds: Math.min(31_536_000, Math.ceil((worst.resetAt - now) / 1000)) } : {}) };
34
+ }
35
+ /** Unknown ties healthy; stable order within tiers. Exhaustion requires EVERY usable row known. */
36
+ export function jevQuotaPoolSignal(signals: readonly JevQuotaSignal[]): JevQuotaSignal {
37
+ return signals.reduce<JevQuotaSignal | undefined>((best, signal) => !best || jevQuotaRank(signal.tier) < jevQuotaRank(best.tier) ? signal : best, undefined) ?? unknown();
38
+ }
39
+ /** Read only already-loaded evidence for a target provider and model and return its quota signal; never resolves credentials or probes upstream. */
40
+ export function jevQuotaSignalForTarget(config: OcxConfig, provider: string, model: string, now = Date.now(), tiers: ResolvedJevQuotaTiers = JEV_QUOTA_DEFAULT_TIERS): JevQuotaSignal {
41
+ const row = config.providers[provider];
42
+ if (!row || row.disabled) return unknown();
43
+ if (provider === "anthropic" && row.authMode === "oauth" && config.anthropicAccountPool?.enabled === true) {
44
+ const route = resolveAnthropicModelRoute(config, model);
45
+ if (route.error) return unknown();
46
+ const usable = (readLoadedDecisionQuotaPool("anthropic") ?? []).filter(account => account.usable);
47
+ const ids = new Set(routeCandidates(usable.map(account => account.id), route.decision));
48
+ return jevQuotaPoolSignal(usable.filter(account => ids.has(account.id)).map(account => jevQuotaSignalFromWindows(account.windows, model, now, tiers)));
49
+ }
50
+ if (isCanonicalOpenAiForwardProvider(row) && providerCodexAccountMode(provider, row) === "pool") {
51
+ if (model.toLowerCase().split("/").at(-1) === NATIVE_RESERVE_MODEL) return unknown();
52
+ const roster = readLoadedDecisionQuotaPool("codex");
53
+ if (!roster) return unknown();
54
+ const paused = new Set(config.pausedCodexAccountIds ?? []);
55
+ const ids = new Set((config.codexAccounts ?? []).filter(isSelectableCodexPoolAccount).map(account => account.id));
56
+ const loadedIds = new Set(roster.map(account => account.id));
57
+ const signals = roster.filter(account => account.usable && ids.has(account.id) && !paused.has(account.id))
58
+ .map(account => jevQuotaSignalFromWindows(account.windows, model, now, tiers));
59
+ for (const id of ids) if (!loadedIds.has(id) && !paused.has(id)) signals.push(unknown());
60
+ if (!paused.has(MAIN_CODEX_ACCOUNT_ID)) {
61
+ const main = readLoadedDecisionQuotaPool("codex-main")?.[0];
62
+ if (!main || main.usable) signals.push(jevQuotaSignalFromWindows(main?.windows, model, now, tiers));
63
+ }
64
+ return jevQuotaPoolSignal(signals);
65
+ }
66
+ return jevQuotaSignalFromWindows(readLoadedDecisionKeyQuota(provider, row), model, now, tiers);
67
+ }
68
+ /** Render the short natural-language quota clause appended to an option description. */
69
+ export function jevQuotaClause(signal: JevQuotaSignal): string {
70
+ return ` Quota ${signal.tier}${signal.usedPercent !== undefined ? ` (${signal.usedPercent}% of ${signal.window} used)` : ""}.`;
71
+ }
72
+ /** Project a quota signal onto the outbound criterion object: tier, used percentage, window and reset delay only. */
73
+ export function jevQuotaCriterion(signal: JevQuotaSignal): Record<string, unknown> {
74
+ return { tier: signal.tier, ...(signal.usedPercent !== undefined ? { used_percent: signal.usedPercent, window: signal.window } : {}),
75
+ ...(signal.resetsInSeconds !== undefined ? { resets_in_seconds: signal.resetsInSeconds } : {}) };
76
+ }
77
+ export const JEV_QUOTA_INSTRUCTION = "Remaining subscription quota is advisory: among adequate options prefer healthier quota; unknown is not exhausted. Never change the target or effort allowlist.";
78
+ /** Count candidate quota tiers and record the selected candidate's tier; undefined when no candidate carried quota evidence. */
79
+ export function jevQuotaDecisionSummary(candidates: readonly { key: string; quota?: JevQuotaSignal }[], selectedKey: string): JevQuotaDecisionSummary | undefined {
80
+ if (!candidates.some(candidate => candidate.quota)) return undefined;
81
+ const summary: JevQuotaDecisionSummary = { unknown: 0, healthy: 0, moderate: 0, limited: 0, nearly_exhausted: 0 };
82
+ for (const candidate of candidates) {
83
+ const tier = candidate.quota?.tier ?? "unknown";
84
+ summary[tier]++;
85
+ if (candidate.key === selectedKey) summary.selected = tier;
86
+ }
87
+ return summary;
88
+ }
package/src/combos/jev.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { jevQuotaClause, jevQuotaCriterion, JEV_QUOTA_INSTRUCTION, type JevQuotaSignal } from "./jev-quota";
2
+ import type { JevQuotaTiers, JevQuotaDecisionSummary } from "./jev-quota-config";
3
+ import { boundedJevQuotaPayload, jevQuotaCandidates, withJevQuotaSummary } from "./jev-quota-route";
4
+ import { JEV_MAX_REQUEST_BYTES as REQUEST_LIMIT } from "./jev-service-exchange";
1
5
  import type { providerOutboundPost } from "../lib/provider-outbound";
2
6
  import type { OcxComboDefaultEffort, OcxConfig } from "../types";
3
7
  import {
@@ -75,6 +79,7 @@ export interface JevCandidate {
75
79
  reasoningEfforts: readonly OcxComboDefaultEffort[];
76
80
  /** Optional operator note sent as decision evidence for this target only. */
77
81
  modelProfile?: string;
82
+ quota?: JevQuotaSignal;
78
83
  }
79
84
 
80
85
  /**
@@ -98,6 +103,7 @@ export interface JevDecision {
98
103
  effort: OcxComboDefaultEffort | null;
99
104
  gate: "apply" | "missing_key" | "no_choices" | "no_state" | "timeout" | "network" | "redirect" | "http" | "malformed" | "invalid";
100
105
  latencyMs: number;
106
+ quota?: JevQuotaDecisionSummary;
101
107
  confidence?: number;
102
108
  chosenProbability?: number;
103
109
  usage?: Record<string, number>;
@@ -113,6 +119,8 @@ export interface ResolveJevDecisionOptions {
113
119
  * any other id names a configured `jev-decision` row (for example a self-hosted Ollama `tev1`).
114
120
  */
115
121
  decisionProvider?: string;
122
+ decisionQuotaSignals?: boolean;
123
+ decisionQuotaTiers?: JevQuotaTiers;
116
124
  /** Request-local authorization of the concrete decision destination, before credential access. */
117
125
  isDestinationAllowed?: (providerName: string, modelId: string) => boolean;
118
126
  /** Decision deadline; values outside 1000..120000 ms keep the four-second default. */
@@ -477,11 +485,12 @@ function modelProfile(candidate: JevCandidate): string {
477
485
  ?? "Configured target with capability unspecified by JEV; judge it only from the supplied request evidence.";
478
486
  }
479
487
 
480
- function criterionDescription(criterion: JevRouteOption["criterion"]): string {
488
+ /** Describe one allowlisted target and effort option for a decision backend, including the optional advisory quota clause. */
489
+ function criterionDescription(criterion: JevRouteOption["criterion"], quota?: JevQuotaSignal): string {
481
490
  const effort = criterion.reasoning_effort
482
491
  ? `${criterion.reasoning_effort} reasoning effort`
483
492
  : "no reasoning-effort control";
484
- return `Target ${criterion.target} (provider ${criterion.provider}, model ${criterion.model}) with ${effort}.`;
493
+ return `Target ${criterion.target} (provider ${criterion.provider}, model ${criterion.model}) with ${effort}.${quota ? jevQuotaClause(quota) : ""}`;
485
494
  }
486
495
 
487
496
  /** One allowlisted target/effort option, shared by every decision backend. */
@@ -498,7 +507,7 @@ export function jevRouteOptions(candidates: readonly JevCandidate[]): JevRouteOp
498
507
  key,
499
508
  targetKey: option.targetKey,
500
509
  effort: option.effort,
501
- description: criterionDescription(option.criterion),
510
+ description: criterionDescription(option.criterion, candidates.find(candidate => candidate.key === option.targetKey)?.quota),
502
511
  }));
503
512
  }
504
513
 
@@ -516,7 +525,8 @@ export function buildJevRouteQuestion(
516
525
  const routeOptions = candidateOptions(candidates);
517
526
  const criteria: Record<string, unknown> = {};
518
527
  for (const [choice, option] of routeOptions) {
519
- criteria[choice] = options.descriptiveCriteria ? criterionDescription(option.criterion) : option.criterion;
528
+ const quota = candidates.find(candidate => candidate.key === option.targetKey)?.quota;
529
+ criteria[choice] = options.descriptiveCriteria ? criterionDescription(option.criterion, quota) : { ...option.criterion, ...(quota ? { quota: jevQuotaCriterion(quota) } : {}) };
520
530
  }
521
531
  const modelProfiles: Record<string, string> = {};
522
532
  for (const candidate of candidates) modelProfiles[candidate.key] = modelProfile(candidate);
@@ -531,6 +541,7 @@ export function buildJevRouteQuestion(
531
541
  model_profiles: modelProfiles,
532
542
  effort_profiles: EFFORT_PROFILES,
533
543
  speed: "Every option uses standard speed. Fast mode is unavailable.",
544
+ ...(candidates.some(candidate => candidate.quota) ? { quota: JEV_QUOTA_INSTRUCTION } : {}),
534
545
  },
535
546
  criteria,
536
547
  },
@@ -628,6 +639,7 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
628
639
  if (options.candidates.length === 0) return failed("no_choices");
629
640
  if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
630
641
 
642
+ let candidates = options.candidates;
631
643
  const exchanged = await exchangeJevDecision(options, (endpoint) => {
632
644
  if (endpoint.descriptiveCriteria) {
633
645
  // Self-hosted choice questions accept 2..26 options; decide locally instead of spending a
@@ -638,19 +650,20 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
638
650
  }
639
651
  const state = buildJevState(options.body, options.candidates);
640
652
  if (!hasJevDecisionState(state)) return "no_state";
641
- return { body: JSON.stringify({
642
- model: endpoint.model,
643
- state,
644
- questions: buildJevRouteQuestion(options.candidates, { descriptiveCriteria: endpoint.descriptiveCriteria }),
645
- }) };
653
+ const payload = boundedJevQuotaPayload(jevQuotaCandidates(options), rows => JSON.stringify({
654
+ model: endpoint.model, state,
655
+ questions: buildJevRouteQuestion(rows, { descriptiveCriteria: endpoint.descriptiveCriteria }),
656
+ }), REQUEST_LIMIT);
657
+ candidates = payload.candidates;
658
+ return { body: payload.body };
646
659
  }, (payload) => parseJevDecision(payload, options.candidates));
647
- if ("gate" in exchanged) return failed(exchanged.gate);
648
- return {
660
+ if ("gate" in exchanged) return withJevQuotaSummary(failed(exchanged.gate), candidates);
661
+ return withJevQuotaSummary({
649
662
  backend,
650
663
  ...exchanged.value,
651
664
  gate: "apply",
652
665
  latencyMs: Math.max(0, now() - startedAt),
653
- };
666
+ }, candidates);
654
667
  }
655
668
 
656
669
  export type JevDecisionProbeResult =
@@ -1,3 +1,4 @@
1
+ import { normalizeJevQuotaTiers, type ResolvedJevQuotaTiers } from "./jev-quota-config";
1
2
  import { isCodexReasoningEffort, isDeclaredReasoningEffort } from "../reasoning-effort";
2
3
  import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
3
4
  import type { OcxComboConfig, OcxComboCooldownWaitPolicy, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
@@ -68,12 +69,16 @@ export interface NormalizedComboConfig {
68
69
  nativeAlias: boolean;
69
70
  /** Display-only label for the catalog row, or null when unset. */
70
71
  displayName: string | null;
72
+ /** Display-only catalog row description; absent keeps the generated text (#7229). */
73
+ description?: string;
71
74
  /** JEV decision service provider id; absent means the canonical `jev` service. */
72
75
  decisionProvider?: string;
73
76
  /** Ordinary inference route used for JEV decisions. */
74
77
  decisionModel?: string;
75
78
  /** JEV decision deadline override; absent keeps the default four-second deadline. */
76
79
  decisionTimeoutMs?: number;
80
+ decisionQuotaSignals?: boolean;
81
+ decisionQuotaTiers?: ResolvedJevQuotaTiers;
77
82
  targets: NormalizedComboTarget[];
78
83
  }
79
84
 
@@ -137,6 +142,7 @@ export interface ComboValidationOptions {
137
142
  excludeComboId?: string;
138
143
  }
139
144
 
145
+ /** Validate a raw combo definition against the configured providers and return every issue found, without mutating the input. */
140
146
  export function comboConfigIssues(
141
147
  id: string,
142
148
  raw: unknown,
@@ -277,6 +283,16 @@ export function comboConfigIssues(
277
283
  });
278
284
  }
279
285
  }
286
+ if (body.description !== undefined) {
287
+ if (typeof body.description !== "string") {
288
+ issues.push({ path: ["description"], message: "description must be a string" });
289
+ } else if (body.description.trim().length > 128 || /\p{Cc}/u.test(body.description)) {
290
+ issues.push({
291
+ path: ["description"],
292
+ message: "description must be at most 128 characters and contain no control characters",
293
+ });
294
+ }
295
+ }
280
296
  const alias = typeof body.alias === "string" ? body.alias.trim() : "";
281
297
  const nativeAlias = body.nativeAlias === true;
282
298
  if (nativeAlias && !SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias)) {
@@ -288,6 +304,12 @@ export function comboConfigIssues(
288
304
  if (nativeAlias && (typeof body.displayName !== "string" || body.displayName.trim().length === 0)) {
289
305
  issues.push({ path: ["displayName"], message: "displayName is required for native aliases" });
290
306
  }
307
+ for (const field of ["decisionQuotaSignals", "decisionQuotaTiers"] as const) {
308
+ if (body[field] === undefined || body[field] === null) continue;
309
+ if (body.strategy !== "jev") issues.push({ path: [field], message: `${field} is only valid with strategy "jev"` });
310
+ if (field === "decisionQuotaSignals" ? typeof body[field] !== "boolean" : !normalizeJevQuotaTiers(body[field]))
311
+ issues.push({ path: [field], message: field === "decisionQuotaSignals" ? "decisionQuotaSignals must be a boolean" : "decisionQuotaTiers must contain finite 0..100 strictly ascending thresholds" });
312
+ }
291
313
  if (body.decisionProvider !== undefined && body.decisionProvider !== null) {
292
314
  const decisionProvider = typeof body.decisionProvider === "string" ? body.decisionProvider.trim() : "";
293
315
  if (!decisionProvider) {
@@ -480,9 +502,11 @@ export function comboConfigError(
480
502
  return comboConfigIssues(id, raw, providers, options)[0]?.message ?? null;
481
503
  }
482
504
 
505
+ /** Normalize a validated combo definition into its canonical form: trimmed strings, defaults and the opt-in quota signal fields. */
483
506
  export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig {
484
507
  const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
485
508
  const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
509
+ const description = typeof raw.description === "string" ? raw.description.trim() : "";
486
510
  const decisionProvider = typeof raw.decisionProvider === "string" ? raw.decisionProvider.trim() : "";
487
511
  const decisionModel = typeof raw.decisionModel === "string" ? raw.decisionModel.trim() : "";
488
512
  const defaultEffort = typeof raw.defaultEffort === "string" && isCodexReasoningEffort(raw.defaultEffort)
@@ -502,10 +526,13 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
502
526
  alias: alias || null,
503
527
  nativeAlias: raw.nativeAlias === true,
504
528
  displayName: displayName || null,
529
+ ...(description ? { description } : {}),
505
530
  // Explicit "jev" is the default and stays sparse.
506
531
  ...(decisionProvider && decisionProvider !== CANONICAL_JEV_DECISION_PROVIDER ? { decisionProvider } : {}),
507
532
  ...(decisionModel ? { decisionModel } : {}),
508
533
  ...(typeof raw.decisionTimeoutMs === "number" ? { decisionTimeoutMs: raw.decisionTimeoutMs } : {}),
534
+ ...(raw.strategy === "jev" && raw.decisionQuotaSignals === true ? { decisionQuotaSignals: true } : {}),
535
+ ...(raw.strategy === "jev" && normalizeJevQuotaTiers(raw.decisionQuotaTiers) ? { decisionQuotaTiers: normalizeJevQuotaTiers(raw.decisionQuotaTiers)! } : {}),
509
536
  targets: raw.targets.map(target => ({
510
537
  provider: target.provider.trim(),
511
538
  model: target.model.trim(),
@@ -1,4 +1,5 @@
1
1
  import { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
2
+ import { OPENGATEWAY_KEY_GLM53_RESPONSES_MANIFEST } from "./opengateway-responses";
2
3
  import type { CompatibilityManifestV1 } from "./manifest";
3
4
 
4
5
  export {
@@ -16,9 +17,11 @@ export type {
16
17
  CompatibilitySubjectV1,
17
18
  } from "./manifest";
18
19
  export { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
20
+ export { OPENGATEWAY_KEY_GLM53_RESPONSES_MANIFEST } from "./opengateway-responses";
19
21
 
20
22
  export const COMPATIBILITY_MANIFESTS: readonly CompatibilityManifestV1[] = Object.freeze([
21
23
  OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST,
24
+ OPENGATEWAY_KEY_GLM53_RESPONSES_MANIFEST,
22
25
  ]);
23
26
 
24
27
  export function getCompatibilityManifest(id: string): CompatibilityManifestV1 | undefined {
@@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
9
9
  export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
10
10
  schemaVersion: 1,
11
11
  id: "openai.codex-forward.gpt-5-6-sol.responses",
12
- version: "1.3.0",
12
+ version: "1.4.0",
13
13
  subject: {
14
14
  providerId: "openai",
15
15
  baseUrl: "https://chatgpt.com/backend-api/codex",
@@ -117,5 +117,12 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife
117
117
  limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
118
118
  evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-options-removed"] }],
119
119
  },
120
+ {
121
+ id: "custom-tool-grammar",
122
+ feature: "request.custom_tools.grammar",
123
+ disposition: "passthrough",
124
+ summary: "A custom tool's grammar format, including its definition, reaches the backend unchanged.",
125
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-grammar-preserved"] }],
126
+ },
120
127
  ],
121
128
  } as const);
@@ -0,0 +1,61 @@
1
+ import { defineCompatibilityManifest } from "./manifest";
2
+
3
+ const FIXTURE_ID = "opengateway-key-glm53-responses-v1";
4
+
5
+ /**
6
+ * A routed Responses subject whose destination rejects native custom tools, so OpenCodex lowers
7
+ * them to functions before dispatch. The claims describe that local wire transformation only;
8
+ * they do not certify how the upstream model treats the lowered declaration.
9
+ */
10
+ export const OPENGATEWAY_KEY_GLM53_RESPONSES_MANIFEST = defineCompatibilityManifest({
11
+ schemaVersion: 1,
12
+ id: "opengateway.key.glm-5-3-flash-ultrafast.responses",
13
+ version: "1.0.0",
14
+ subject: {
15
+ providerId: "opengateway",
16
+ baseUrl: "https://apis.opengateway.ai/v1",
17
+ adapterId: "openai-responses",
18
+ authMode: "key",
19
+ inboundProtocol: "responses",
20
+ upstreamProtocol: "openai-responses",
21
+ modelIds: ["z-ai/glm-5.3-flash-ultrafast"],
22
+ },
23
+ claims: [
24
+ {
25
+ id: "custom-tools",
26
+ feature: "request.custom_tools",
27
+ disposition: "translated",
28
+ summary: "Custom tool declarations are sent as functions that take one raw string input.",
29
+ limitation: "The destination rejects native custom tools, so each declaration is lowered to a function with a single required string parameter named input.",
30
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-lowered-to-function"] }],
31
+ },
32
+ {
33
+ id: "custom-tool-grammar",
34
+ feature: "request.custom_tools.grammar",
35
+ disposition: "degraded",
36
+ summary: "The tool and its description survive, but its grammar does not reach the destination.",
37
+ limitation: "The format, including any grammar definition, is removed during lowering, so the upstream model is not constrained by it; only the string input type remains.",
38
+ evidence: [{
39
+ kind: "fixture",
40
+ id: FIXTURE_ID,
41
+ assertionIds: ["custom-tool-grammar-removed", "custom-tool-lowered-to-function"],
42
+ }],
43
+ },
44
+ {
45
+ id: "custom-tool-call-history",
46
+ feature: "continuation.custom_tool_call",
47
+ disposition: "translated",
48
+ summary: "Replayed custom tool calls are sent as function calls with the same call_id and name.",
49
+ limitation: "The raw input is wrapped as the JSON arguments object {\"input\": <raw input>}.",
50
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-call-lowered"] }],
51
+ },
52
+ {
53
+ id: "custom-tool-output-history",
54
+ feature: "continuation.custom_tool_call_output",
55
+ disposition: "translated",
56
+ summary: "Replayed custom tool outputs are sent as function call outputs with the same call_id and output.",
57
+ limitation: "Only the item type changes from custom_tool_call_output to function_call_output.",
58
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-output-lowered"] }],
59
+ },
60
+ ],
61
+ } as const);