@bitkyc08/opencodex 2.84.0 → 2.86.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 +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
  5. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  6. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/package.json +1 -2
  9. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  10. package/src/adapters/anthropic/client-identity.ts +1 -1
  11. package/src/adapters/anthropic/passthrough.ts +33 -4
  12. package/src/adapters/azure.ts +19 -1
  13. package/src/adapters/client-fingerprint.ts +24 -0
  14. package/src/adapters/coding-agent/turn.ts +22 -9
  15. package/src/adapters/cursor/arg-codec.ts +17 -5
  16. package/src/adapters/cursor/catalog.ts +8 -0
  17. package/src/adapters/cursor/effort-map.ts +3 -0
  18. package/src/adapters/cursor/freeform-args.ts +75 -0
  19. package/src/adapters/cursor/protobuf-events.ts +36 -23
  20. package/src/adapters/google-antigravity-wire.ts +22 -1
  21. package/src/adapters/google.ts +2 -2
  22. package/src/adapters/openai-chat/response-events.ts +59 -1
  23. package/src/adapters/openai-chat.ts +8 -5
  24. package/src/adapters/qoder/adapter.ts +40 -1
  25. package/src/advisor/consult.ts +220 -0
  26. package/src/advisor/context.ts +226 -0
  27. package/src/advisor/disclosure.ts +20 -0
  28. package/src/advisor/runtime.ts +305 -0
  29. package/src/advisor/settings.ts +135 -0
  30. package/src/advisor/state.ts +369 -0
  31. package/src/advisor/synthetic-tool.ts +33 -0
  32. package/src/bridge/errors.ts +5 -0
  33. package/src/chat/inbound.ts +30 -6
  34. package/src/chat/outbound.ts +5 -0
  35. package/src/claude/desktop-3p-library.ts +43 -3
  36. package/src/claude/desktop-3p.ts +22 -8
  37. package/src/claude/desktop-picker-profile.ts +11 -4
  38. package/src/claude/desktop-profile.ts +10 -1
  39. package/src/claude/desktop-remote-store-io.ts +5 -3
  40. package/src/claude/inbound.ts +121 -50
  41. package/src/claude/intercept/local-ca.ts +8 -1
  42. package/src/claude/model-default-settings.ts +154 -0
  43. package/src/claude/model-info.ts +7 -7
  44. package/src/claude/outbound.ts +13 -6
  45. package/src/cli/advisor.ts +151 -0
  46. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  47. package/src/cli/capabilities-agents-routing.ts +18 -0
  48. package/src/cli/capabilities-base.ts +1 -1
  49. package/src/cli/chatgpt-command.ts +35 -14
  50. package/src/cli/claude.ts +46 -6
  51. package/src/cli/dispatch.ts +5 -1
  52. package/src/cli/doctor.ts +9 -0
  53. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  54. package/src/cli/export-command.ts +1 -1
  55. package/src/cli/help.ts +2 -1
  56. package/src/cli/index.ts +13 -4
  57. package/src/cli/integration-plan-dto.ts +2 -0
  58. package/src/cli/registry.ts +15 -3
  59. package/src/cli/restart-scope.ts +3 -1
  60. package/src/clients/config-export/contracts.ts +1 -0
  61. package/src/clients/config-export/model-metadata.ts +3 -2
  62. package/src/clients/config-export/qoder.ts +93 -0
  63. package/src/clients/config-export.ts +17 -4
  64. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  65. package/src/codex/auth-context.ts +79 -5
  66. package/src/codex/autostart-health.ts +4 -0
  67. package/src/codex/catalog/build-entries.ts +11 -2
  68. package/src/codex/catalog/configured-tiers.ts +50 -0
  69. package/src/codex/catalog/effort.ts +4 -1
  70. package/src/codex/catalog/model-hints.ts +21 -3
  71. package/src/codex/catalog/native-ultrafast.ts +106 -0
  72. package/src/codex/catalog/parsing.ts +5 -0
  73. package/src/codex/catalog/provider-models.ts +4 -0
  74. package/src/codex/catalog/retained-sync.ts +2 -0
  75. package/src/codex/catalog/routed-gather.ts +22 -4
  76. package/src/codex/catalog/vendor-context.ts +82 -0
  77. package/src/codex/config-write-lock.ts +40 -2
  78. package/src/codex/convergence.ts +2 -0
  79. package/src/codex/desktop-app/types.ts +2 -0
  80. package/src/codex/desktop-app/windows.ts +29 -16
  81. package/src/codex/desktop-app-restart.ts +4 -2
  82. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  83. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  84. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  85. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  86. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  87. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  88. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  89. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  90. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  91. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  92. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  93. package/src/codex/desktop-compatibility/service.ts +30 -0
  94. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  95. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  96. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  97. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  98. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  99. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  100. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  101. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  102. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  103. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  104. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  105. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  106. package/src/codex/history-provider.ts +1 -1
  107. package/src/codex/inject/config-write-section.ts +4 -1
  108. package/src/codex/inject/remove.ts +18 -6
  109. package/src/codex/journal.ts +16 -1
  110. package/src/codex/main-account.ts +99 -15
  111. package/src/codex/native-main-refresh-mode.ts +217 -0
  112. package/src/codex/routing/health-store.ts +1 -1
  113. package/src/codex/routing/pool-recovery.ts +79 -0
  114. package/src/codex/shim-diagnostics.ts +6 -0
  115. package/src/codex/shim-overlay.ts +7 -2
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/live-reconcile.ts +13 -0
  118. package/src/config/load-degrade.ts +6 -1
  119. package/src/config/paths.ts +10 -3
  120. package/src/config/schema/config-schema.ts +17 -0
  121. package/src/config/schema/desktop-compatibility.ts +9 -0
  122. package/src/config/schema/leaf-validators.ts +1 -0
  123. package/src/generated/compatibility-version.json +451 -163
  124. package/src/generated/model-metadata.ts +1 -1
  125. package/src/integrations/catalog-refresh.ts +1 -1
  126. package/src/integrations/config-io.ts +20 -2
  127. package/src/integrations/journal.ts +15 -1
  128. package/src/integrations/mutation-plan.ts +1 -0
  129. package/src/integrations/registry.ts +7 -0
  130. package/src/integrations/store.ts +3 -0
  131. package/src/integrations/writer.ts +10 -1
  132. package/src/lib/advisor-activation.ts +16 -0
  133. package/src/lib/background-work.ts +65 -0
  134. package/src/lib/bounded-file-read.ts +58 -0
  135. package/src/lib/bun-path-runtime.d.mts +10 -1
  136. package/src/lib/bun-path-runtime.mjs +62 -3
  137. package/src/lib/desktop-proxy-route.ts +30 -0
  138. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  139. package/src/lib/errors.ts +7 -0
  140. package/src/lib/local-internal-call-capability.ts +55 -0
  141. package/src/lib/package-tree-integrity.ts +2 -2
  142. package/src/lib/socks5-fetch.ts +48 -71
  143. package/src/lib/socks5-handshake.ts +112 -0
  144. package/src/lib/translator-budget.ts +7 -1
  145. package/src/oauth/generic-account-failover.ts +29 -5
  146. package/src/oauth/index.ts +4 -2
  147. package/src/oauth/kiro-device-login.ts +2 -1
  148. package/src/oauth/kiro.ts +7 -1
  149. package/src/protocols/codecs/chat-ir.ts +467 -0
  150. package/src/protocols/codecs/chat.ts +10 -1
  151. package/src/protocols/codecs/messages-ir.ts +500 -0
  152. package/src/protocols/features.ts +10 -2
  153. package/src/protocols/path.ts +11 -1
  154. package/src/protocols/plan-snapshot.ts +39 -16
  155. package/src/protocols/plan.ts +9 -1
  156. package/src/protocols/settings.ts +8 -0
  157. package/src/providers/antigravity-account-roster.ts +102 -0
  158. package/src/providers/antigravity-models.ts +85 -7
  159. package/src/providers/model-rename-fields.ts +2 -0
  160. package/src/providers/model-rename-migration.ts +5 -3
  161. package/src/providers/registry/entries-core.ts +7 -6
  162. package/src/providers/registry/entries-extended.ts +19 -0
  163. package/src/responses/parser.ts +30 -19
  164. package/src/responses/state.ts +32 -29
  165. package/src/routing/history/build-file.ts +92 -0
  166. package/src/routing/history/indexer.ts +248 -216
  167. package/src/routing/history/ingest.ts +238 -0
  168. package/src/routing/history/rebuild-job.ts +111 -0
  169. package/src/routing/history/rebuild-protocol.ts +15 -0
  170. package/src/routing/history/rebuild-worker.ts +34 -0
  171. package/src/server/admission-model-scope.ts +4 -1
  172. package/src/server/antigravity-direct-eligibility.ts +208 -0
  173. package/src/server/auth-cors.ts +4 -0
  174. package/src/server/chat-completions.ts +75 -9
  175. package/src/server/claude-messages.ts +110 -20
  176. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  177. package/src/server/index/serve-options.ts +2 -2
  178. package/src/server/index/startup-warnings.ts +9 -0
  179. package/src/server/index/websocket-handler.ts +9 -0
  180. package/src/server/index.ts +13 -10
  181. package/src/server/inference/client-encoder-delivery.ts +6 -5
  182. package/src/server/login-shell-path.ts +146 -0
  183. package/src/server/management/advisor-routes.ts +191 -0
  184. package/src/server/management/agent-settings-routes.ts +22 -4
  185. package/src/server/management/companion-routes.ts +4 -0
  186. package/src/server/management/config-routes.ts +2 -2
  187. package/src/server/management/context.ts +4 -0
  188. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  189. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  190. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  191. package/src/server/management/model-routes.ts +2 -2
  192. package/src/server/management/protocol-settings-patch.ts +1 -0
  193. package/src/server/management/route-registry.ts +11 -0
  194. package/src/server/management/sibling-guard.ts +1 -0
  195. package/src/server/management-api.ts +6 -0
  196. package/src/server/messages-antigravity-direct.ts +195 -0
  197. package/src/server/messages-native.ts +5 -1
  198. package/src/server/request-log.ts +7 -0
  199. package/src/server/responses/adapter-continuation.ts +35 -18
  200. package/src/server/responses/adapter-delivery.ts +24 -2
  201. package/src/server/responses/adapter-dispatch.ts +29 -2
  202. package/src/server/responses/advisor-plan-slot.ts +27 -0
  203. package/src/server/responses/advisor-slot.ts +336 -0
  204. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  205. package/src/server/responses/core-combo-failure.ts +10 -1
  206. package/src/server/responses/core-combo.ts +21 -0
  207. package/src/server/responses/core-lifetime.ts +2 -0
  208. package/src/server/responses/core-normalize.ts +11 -0
  209. package/src/server/responses/core-options.ts +46 -0
  210. package/src/server/responses/core-replay.ts +7 -4
  211. package/src/server/responses/core.ts +1 -1
  212. package/src/server/responses/passthrough-delivery.ts +16 -10
  213. package/src/server/responses/passthrough-dispatch.ts +14 -3
  214. package/src/server/responses/request-prepare.ts +68 -5
  215. package/src/server/responses/request-transport.ts +16 -0
  216. package/src/server/responses/run-turn-execution.ts +70 -8
  217. package/src/server/responses/sidecar-execution.ts +78 -3
  218. package/src/server/responses/terminal-guard.ts +2 -1
  219. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  220. package/src/server/startup-action-control.ts +16 -2
  221. package/src/server/startup-health-cache.ts +1 -0
  222. package/src/service/desktop-startup.ts +14 -0
  223. package/src/service/health-sentinel-main.ts +23 -4
  224. package/src/service/health-sentinel-probe.ts +24 -7
  225. package/src/service/logging.ts +72 -0
  226. package/src/tray/windows-tray.ps1 +43 -24
  227. package/src/types/config.ts +63 -6
  228. package/src/types/provider.ts +7 -0
  229. package/src/types/request.ts +15 -0
  230. package/src/types/tools.ts +2 -0
  231. package/src/update/async-check.ts +1 -1
  232. package/src/update/badge.ts +2 -2
  233. package/src/update/index.ts +88 -15
  234. package/src/update/job-supervised-restart.ts +107 -0
  235. package/src/update/job.ts +32 -32
  236. package/src/update/notify.ts +1 -1
  237. package/src/update/refresh-scheduler.ts +2 -2
  238. package/src/update/supervised-recovery.d.mts +27 -0
  239. package/src/update/supervised-recovery.mjs +157 -0
  240. package/src/usage/cache-diagnostic.ts +2 -0
  241. package/src/usage/expected-prices.ts +12 -0
  242. package/src/web-search/gemini-executor.ts +2 -2
  243. package/assets/sponsors/packycode.png +0 -0
  244. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  245. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  246. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -30,6 +30,7 @@ export const SIBLING_REFUSED_MANAGEMENT_PATHS: readonly { readonly path: string;
30
30
  { path: "/api/client-integrations", children: true },
31
31
  { path: "/api/native-integrations", children: true },
32
32
  { path: "/api/claude-desktop", children: true },
33
+ { path: "/api/codex/desktop-compatibility", children: true },
33
34
  { path: "/api/claude-code", children: false },
34
35
  { path: "/api/grok/apply", children: false },
35
36
  { path: "/api/grok/selection", children: false },
@@ -79,6 +79,9 @@ import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
79
79
  import { handleIntegrationRoutes } from "./management/integration-routes";
80
80
  import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
81
81
  import { handleClaudeDesktopPickerRoutes } from "./management/claude-desktop-picker-routes";
82
+ import { handleDesktopCompatibilityRoutes } from "./management/desktop-compatibility-routes";
83
+ import { handleDesktopCompatibilityRuntimeRoutes } from "./management/desktop-compatibility-runtime-routes";
84
+ import { handleDesktopCompatibilitySettingsRoutes } from "./management/desktop-compatibility-settings-routes";
82
85
  import { handleCursorIntegrationRoutes } from "./management/cursor-integration-routes";
83
86
  import type { ManagementContext } from "./management/context";
84
87
  import type { ManagementPrincipal, ManagementSessionControl } from "./management-auth";
@@ -360,6 +363,9 @@ export async function handleManagementAPI(
360
363
  ?? (await handleNativeIntegrationRoutes(ctx))
361
364
  ?? (await handleCursorIntegrationRoutes(ctx))
362
365
  ?? (await handleClaudeDesktopPickerRoutes(ctx))
366
+ ?? (await handleDesktopCompatibilityRoutes(ctx))
367
+ ?? (await handleDesktopCompatibilityRuntimeRoutes(ctx))
368
+ ?? (await handleDesktopCompatibilitySettingsRoutes(ctx))
363
369
  ?? (await handleAgentSettingsRoutes(ctx))
364
370
  ?? (await handleCodexPromptRoutes(ctx))
365
371
  ?? (await handleOauthAccountRoutes(ctx))
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The Antigravity direct lane for the Messages ingress (`protocols.rollout.antigravityDirect`).
3
+ *
4
+ * The Messages ingress translates before it routes: the route is decided on the translated body's
5
+ * resolved model. So the lane is planned ahead of translation on the settings-only body
6
+ * (`anthropicSettingsTranslation`), whose model is that same resolved model, against a preview of
7
+ * the route; the conversation is decoded straight into the IR only when that preview is an
8
+ * eligible Antigravity route. The ingress then routes the settings-only body as usual and keeps
9
+ * the lane only if the settled route is still the one the plan was made for; otherwise it builds
10
+ * the full bridge translation, as it would have without the switch.
11
+ *
12
+ * Refusals stay the bridge's by construction: a settings error, or an undecodable proxy envelope
13
+ * (which the bridge retries through the native projection), returns no plan and the ingress takes
14
+ * its ordinary translation path, which answers exactly as before. Any other decode error is one
15
+ * the translator's walk raises with the same message at the same point
16
+ * (`tests/responses/protocol-messages-ir-parity.test.ts`).
17
+ */
18
+ import { anthropicSettingsTranslation, AnthropicRequestError, type ClaudeCacheKeySource } from "../claude/inbound";
19
+ import { messagesToResponsesTranslation } from "../protocols/codecs/messages";
20
+ import {
21
+ MALFORMED_REASONING_SIGNATURE,
22
+ messagesConversationToIr,
23
+ messagesRouteEvidence,
24
+ withDirectMessagesConversation,
25
+ type MessagesIrConversation,
26
+ } from "../protocols/codecs/messages-ir";
27
+ import { resolveProtocolSettings } from "../protocols/settings";
28
+ import type { TranslatorBudget, TranslatorBufferKind } from "../lib/translator-budget";
29
+ import type { PolicyRequestEvidence } from "../routing/evaluator";
30
+ import { previewRouteModel, type RouteResult } from "../router";
31
+ import type { OcxClaudeCodeConfig, OcxConfig } from "../types";
32
+ import { antigravityMessagesDeclineReason, type AntigravityDirectDeclineReason } from "./antigravity-direct-eligibility";
33
+ import { cacheDiagnosticsObserving } from "./request-log";
34
+ import type { DirectRequestOption } from "./responses/core-options";
35
+
36
+ type Rec = Record<string, unknown>;
37
+
38
+ export interface MessagesDirectPlan {
39
+ /** The concrete route the plan was made for. */
40
+ providerName: string;
41
+ modelId: string;
42
+ /** The settings-only Responses body the core receives, and its prompt-cache key provenance. */
43
+ settingsBody: Rec;
44
+ cacheKeySource: ClaudeCacheKeySource;
45
+ conversation: MessagesIrConversation;
46
+ /** What the core would have read from the bridge body for policy routing. */
47
+ evidence: PolicyRequestEvidence;
48
+ /**
49
+ * Release the retained charges the plan's decode made. The ingress calls it when it abandons
50
+ * the plan for the full translation, which charges the same reasoning copies again.
51
+ */
52
+ releaseDecodeCharges(): void;
53
+ }
54
+
55
+ /**
56
+ * The request budget, with the retained charges made through it recorded so a decode the ingress
57
+ * abandons can hand them back. Without that, the bridge translation that follows would be held to
58
+ * a budget already spent on a copy nobody keeps, and could answer 413 where the bridge alone fits.
59
+ */
60
+ function recordingBudget(budget: TranslatorBudget): { budget: TranslatorBudget; release(): void } {
61
+ const charged = new Map<TranslatorBufferKind, number>();
62
+ const recording: TranslatorBudget = {
63
+ openCall: id => budget.openCall(id),
64
+ closeCall: id => budget.closeCall(id),
65
+ reserveTransient: (bytes, scope) => budget.reserveTransient(bytes, scope),
66
+ chargeRetained: (delta, scope) => {
67
+ budget.chargeRetained(delta, scope);
68
+ charged.set(scope.kind, (charged.get(scope.kind) ?? 0) + delta);
69
+ },
70
+ releaseRetained: (bytes, scope) => {
71
+ budget.releaseRetained(bytes, scope);
72
+ charged.set(scope.kind, (charged.get(scope.kind) ?? 0) - bytes);
73
+ },
74
+ observeAcceptedRequestCopy: bytes => budget.observeAcceptedRequestCopy(bytes),
75
+ observeExternallyCapped: (kind, bytes) => budget.observeExternallyCapped(kind, bytes),
76
+ snapshot: () => budget.snapshot(),
77
+ dispose: () => budget.dispose(),
78
+ };
79
+ return {
80
+ budget: recording,
81
+ release: () => {
82
+ for (const [kind, bytes] of charged) if (bytes > 0) budget.releaseRetained(bytes, { kind });
83
+ charged.clear();
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Plan the direct lane for a Messages body, or return `undefined` to translate as before. Throws
90
+ * only what the translator's conversation walk throws for the same body.
91
+ */
92
+ export function planMessagesAntigravityDirect(
93
+ body: unknown,
94
+ cc: OcxClaudeCodeConfig | undefined,
95
+ config: OcxConfig,
96
+ budget: TranslatorBudget,
97
+ ): MessagesDirectPlan | undefined {
98
+ if (!resolveProtocolSettings(config).rollout.antigravityDirect) return undefined;
99
+ if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
100
+ let settings: ReturnType<typeof anthropicSettingsTranslation>;
101
+ let route: RouteResult;
102
+ try {
103
+ settings = anthropicSettingsTranslation(body, cc);
104
+ route = previewRouteModel(config, settings.body.model as string);
105
+ } catch {
106
+ return undefined;
107
+ }
108
+ const model = settings.body.model as string;
109
+ if (!messagesDirectRouteHolds(route, body as Rec, model, config)) return undefined;
110
+ const recorded = recordingBudget(budget);
111
+ let conversation: MessagesIrConversation;
112
+ try {
113
+ conversation = messagesConversationToIr(body, cc, recorded.budget, model);
114
+ } catch (err) {
115
+ if (err instanceof AnthropicRequestError && err.message === MALFORMED_REASONING_SIGNATURE) {
116
+ // The bridge retries this body through the native projection; give it the whole budget.
117
+ recorded.release();
118
+ return undefined;
119
+ }
120
+ throw err;
121
+ }
122
+ return {
123
+ providerName: route.providerName,
124
+ modelId: route.modelId,
125
+ settingsBody: settings.body,
126
+ cacheKeySource: settings.cacheKeySource,
127
+ conversation,
128
+ evidence: messagesRouteEvidence(body),
129
+ releaseDecodeCharges: recorded.release,
130
+ };
131
+ }
132
+
133
+ /** Whether a route may carry this body on the direct lane, judged as the ingress judges it. */
134
+ export function messagesDirectRouteHolds(
135
+ route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "combo" | "routeKind">,
136
+ body: Rec,
137
+ model: string,
138
+ config: OcxConfig,
139
+ ): boolean {
140
+ return antigravityMessagesDeclineReason(route, body, model, config, { cacheDiagnostics: cacheDiagnosticsObserving() }) === undefined;
141
+ }
142
+
143
+ /** Whether the route the ingress settled is still the one `plan` was made for, and still eligible. */
144
+ export function messagesDirectPlanHolds(plan: MessagesDirectPlan, route: RouteResult, body: Rec, config: OcxConfig): boolean {
145
+ return route.providerName === plan.providerName && route.modelId === plan.modelId
146
+ && messagesDirectRouteHolds(route, body, plan.settingsBody.model as string, config);
147
+ }
148
+
149
+ /**
150
+ * The lane's decline for a bridged request's trace, when it says something about this route: with
151
+ * the switch off or a non-Antigravity target the path already explains the bridge.
152
+ */
153
+ export function messagesDirectTraceDecline(
154
+ route: RouteResult,
155
+ body: unknown,
156
+ model: string,
157
+ config: OcxConfig,
158
+ ): AntigravityDirectDeclineReason | undefined {
159
+ if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
160
+ const reason = antigravityMessagesDeclineReason(route, body as Rec, model, config, { cacheDiagnostics: cacheDiagnosticsObserving() });
161
+ return reason === "rollout-disabled" || reason === "not-migrated" ? undefined : reason;
162
+ }
163
+
164
+ /**
165
+ * The core option for a planned request. The bridge conversation is translated at most once, and
166
+ * only if the core asks for it (a reroute, or an ambiguous-reset judgment).
167
+ */
168
+ export function messagesDirectRequestOption(
169
+ plan: MessagesDirectPlan,
170
+ body: unknown,
171
+ cc: OcxClaudeCodeConfig | undefined,
172
+ budget: TranslatorBudget,
173
+ ): DirectRequestOption {
174
+ let bridge: Rec | undefined;
175
+ return {
176
+ protocol: "messages",
177
+ providerName: plan.providerName,
178
+ modelId: plan.modelId,
179
+ evidence: plan.evidence,
180
+ decode: base => withDirectMessagesConversation(
181
+ base,
182
+ plan.conversation,
183
+ modelId => messagesConversationToIr(body, cc, budget, modelId),
184
+ ),
185
+ bridgeConversation: () => {
186
+ bridge ??= messagesToResponsesTranslation(body, cc, budget).body;
187
+ return {
188
+ input: bridge.input,
189
+ ...(bridge.instructions !== undefined ? { instructions: bridge.instructions } : {}),
190
+ ...(bridge.tools !== undefined ? { tools: bridge.tools } : {}),
191
+ };
192
+ },
193
+ };
194
+ }
195
+
@@ -82,6 +82,7 @@ import { preservesPhysicalComboProvider, resolveComboId } from "../combos";
82
82
  import { captureRouteStaticPolicy, routedProviderConfig, routeModel, type RouteResult } from "../router";
83
83
  import { POLICY_NAMESPACE, resolvePolicyProfileId } from "../routing/profile";
84
84
  import type { AnthropicClientIdentity } from "../adapters/anthropic/client-identity";
85
+ import { logDroppedAnthropicBetas } from "../adapters/anthropic/beta-allowlist";
85
86
  import type { OcxConfig, OcxProviderConfig, OcxUsage } from "../types";
86
87
  import { resolveWireProtocolOverride } from "./adapter-resolve";
87
88
  import {
@@ -469,7 +470,10 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
469
470
  clientIdentity: options.clientIdentity,
470
471
  });
471
472
  if (built.strippedOpaqueState && rejectUnrepresentable) throw new NativeOpaqueStateRefusal();
472
- if (built.droppedBetas) addProtocolEntryReason(logCtx, "anthropic-beta-dropped");
473
+ if (built.droppedBetas) {
474
+ addProtocolEntryReason(logCtx, "anthropic-beta-dropped");
475
+ logDroppedAnthropicBetas({ droppedNames: built.droppedBetaNames, droppedUnrecognized: built.droppedUnrecognizedBetas });
476
+ }
473
477
  if (built.strippedOpaqueState) addProtocolEntryReason(logCtx, "opaque-state-stripped");
474
478
  return built;
475
479
  };
@@ -96,6 +96,8 @@ import type {
96
96
 
97
97
  const CACHE_DIAGNOSTIC_HOOK = Symbol.for("opencodex.cache-diagnostic.v1");
98
98
  interface CacheDiagnosticHooks {
99
+ /** Whether inbound bodies are actually recorded; the hook is registered at import either way. */
100
+ enabled(): boolean;
99
101
  observeInbound(body: unknown, headers: Headers, source: PromptCacheKeySource): CacheDiagnosticDraft;
100
102
  rebind(body: unknown, draft: CacheDiagnosticDraft | undefined): void;
101
103
  finalize(facts: CacheDiagnosticFinalFacts): void;
@@ -104,6 +106,11 @@ function cacheDiagnosticHooks(): CacheDiagnosticHooks | undefined {
104
106
  return (globalThis as Record<symbol, CacheDiagnosticHooks | undefined>)[CACHE_DIAGNOSTIC_HOOK];
105
107
  }
106
108
 
109
+ /** Whether the cache diagnostic recorder will record inbound bodies (`OPENCODEX_CACHE_DEBUG=1`). */
110
+ export function cacheDiagnosticsObserving(): boolean {
111
+ return cacheDiagnosticHooks()?.enabled() === true;
112
+ }
113
+
107
114
  export interface RequestLogContext extends UpstreamErrorDiagnostics {
108
115
  model: string;
109
116
  provider: string;
@@ -30,6 +30,8 @@ import {
30
30
  isNonReplayableResponse,
31
31
  prepareSameTarget429Wait,
32
32
  } from "../../lib/upstream-retry";
33
+ import { unwrapUpstreamRetryEvidenceError } from "../../codex/auth-context";
34
+ import { AdmissionModelDeniedError, MODEL_NOT_ALLOWED_FOR_KEY } from "../admission-model-scope";
33
35
  import { redactSecretString } from "../../lib/redact";
34
36
  import { resolveWireProtocolOverride } from "../adapter-resolve";
35
37
  import { bindRouteReasoningReplayScope } from "./core-replay";
@@ -146,6 +148,22 @@ export function createAdapterContinuations(
146
148
  sendBudgetExhausted,
147
149
  } = sendBudgetState;
148
150
 
151
+ const continuationFailureEvent = (error: unknown): Extract<AdapterEvent, { type: "error" }> => {
152
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
153
+ return { type: "error", message: "client closed request during terminal continuation", status: 499 };
154
+ }
155
+ const refusal = unwrapUpstreamRetryEvidenceError(error);
156
+ if (refusal instanceof AdmissionModelDeniedError) {
157
+ return {
158
+ type: "error", message: refusal.message, status: 403,
159
+ errorType: MODEL_NOT_ALLOWED_FOR_KEY, code: MODEL_NOT_ALLOWED_FOR_KEY, retryable: false,
160
+ };
161
+ }
162
+ return {
163
+ type: "error",
164
+ message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}`,
165
+ };
166
+ };
149
167
 
150
168
  // One bounded internal continuation re-ask for clean end_turn turns that announced an edit
151
169
  // without emitting a tool call. Anthropic gets this by default; openai-chat providers opt in
@@ -304,12 +322,7 @@ export function createAdapterContinuations(
304
322
  kiroRefusalPendingReplay = undefined;
305
323
  kiroReplaySetupFailed = true;
306
324
  } else {
307
- if (options.abortSignal?.aborted || upstream.signal.aborted) {
308
- yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
309
- } else {
310
- yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
311
- }
312
- return;
325
+ yield continuationFailureEvent(error); return;
313
326
  }
314
327
  }
315
328
 
@@ -360,12 +373,7 @@ export function createAdapterContinuations(
360
373
  try {
361
374
  response = await fetchContinuation("rate-limit-429");
362
375
  } catch (error) {
363
- if (options.abortSignal?.aborted || upstream.signal.aborted) {
364
- yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
365
- } else {
366
- yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
367
- }
368
- return;
376
+ yield continuationFailureEvent(error); return;
369
377
  }
370
378
  }
371
379
 
@@ -381,9 +389,7 @@ export function createAdapterContinuations(
381
389
  route.provider = rotated;
382
390
  try { await transportState.resolveCopilotSelection(nextParsed); }
383
391
  catch (error) {
384
- yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}`,
385
- ...(options.abortSignal?.aborted || upstream.signal.aborted ? { status: 499 } : {}) };
386
- return;
392
+ yield continuationFailureEvent(error); return;
387
393
  }
388
394
  invalidateSameTargetRequest();
389
395
  transportState.activeAdapter = resolveSelectionAdapter(
@@ -630,13 +636,17 @@ export function createAdapterContinuations(
630
636
  nextContinuationRecoveryKind = "oauth-account-429";
631
637
  continue;
632
638
  }
633
- } catch {
639
+ } catch (error) {
634
640
  // Everything in this try runs before the replay: the send happens on the next
635
641
  // iteration, after `continue`. A throw here therefore leaves a reservation that
636
642
  // never dispatched, and holding it would refuse a later recovery in this same
637
643
  // request for a send that never left the process.
638
644
  hop.permit?.release();
639
- // fall through to emit continuation error below
645
+ if (unwrapUpstreamRetryEvidenceError(error) instanceof AdmissionModelDeniedError) {
646
+ yield continuationFailureEvent(error);
647
+ return;
648
+ }
649
+ // Other negotiation failures preserve the original provider refusal.
640
650
  }
641
651
  }
642
652
  }
@@ -717,7 +727,7 @@ export function createAdapterContinuations(
717
727
 
718
728
  const fetchGuardedEmptyCompletionRetry = (): AsyncIterable<AdapterEvent> => {
719
729
  const retryEvents = fetchTerminalGuardContinuation(parsed, "empty-completion", true);
720
- return terminalGuardEnabled
730
+ const terminalGuardedRetry = terminalGuardEnabled
721
731
  ? guardTerminalEventStream({
722
732
  parsed,
723
733
  firstEvents: retryEvents,
@@ -726,6 +736,13 @@ export function createAdapterContinuations(
726
736
  continuation: next => fetchTerminalGuardContinuation(next, undefined, !parsed.stream),
727
737
  })
728
738
  : retryEvents;
739
+ return parsed._advisorGuard
740
+ ? parsed._advisorGuard({
741
+ parsed,
742
+ firstEvents: terminalGuardedRetry,
743
+ continuation: fetchTerminalGuardContinuation,
744
+ })
745
+ : terminalGuardedRetry;
729
746
  };
730
747
 
731
748
  return {
@@ -113,16 +113,28 @@ export async function deliverAdapterResponse(
113
113
  continuation: next => fetchTerminalGuardContinuation(next, undefined, !parsed.stream),
114
114
  })
115
115
  : initialEventStream;
116
+ // Optional advisor guard (registered per request by the sidecar planner; absent unless the
117
+ // user enabled the advisor): holds synthetic `advisor` tool calls, consults the configured
118
+ // expert model, and re-dispatches the worker. Sits inside the empty-completion guard so an
119
+ // advisor-only turn never reads as an empty completion, and outside the bridge so the
120
+ // synthetic call never reaches the client.
121
+ const advisorStream = parsed._advisorGuard
122
+ ? parsed._advisorGuard({
123
+ parsed,
124
+ firstEvents: eventStream,
125
+ continuation: fetchTerminalGuardContinuation,
126
+ })
127
+ : eventStream;
116
128
  // The empty-completion guard sits OUTSIDE the terminal guard: a completed
117
129
  // turn with no text and no tool call is retried with the IDENTICAL request
118
130
  // (fetchTerminalGuardContinuation(parsed) replays the cached byte-identical
119
131
  // request — same body, same headers, same signal).
120
132
  const guardedEventStream = emptyCompletionGuardEnabled
121
133
  ? guardEmptyCompletionEventStream({
122
- firstEvents: eventStream,
134
+ firstEvents: advisorStream,
123
135
  continuation: fetchGuardedEmptyCompletionRetry,
124
136
  })
125
- : eventStream;
137
+ : advisorStream;
126
138
  const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
127
139
  // One completion owner for both deliveries: the bridge calls it from its terminal, the
128
140
  // direct client encoder from the fold of the same events.
@@ -240,6 +252,16 @@ export async function deliverAdapterResponse(
240
252
  } else {
241
253
  guardedEvents = initialEvents;
242
254
  }
255
+ // Optional advisor guard — same semantics as the streaming branch above.
256
+ if (parsed._advisorGuard) {
257
+ const advisorEvents: AdapterEvent[] = [];
258
+ for await (const event of parsed._advisorGuard({
259
+ parsed,
260
+ firstEvents: (async function* () { yield* guardedEvents; })(),
261
+ continuation: fetchTerminalGuardContinuation,
262
+ })) advisorEvents.push(event);
263
+ guardedEvents = advisorEvents;
264
+ }
243
265
  if (emptyCompletionGuardEnabled) {
244
266
  events = [];
245
267
  for await (const event of guardEmptyCompletionEventStream({
@@ -112,6 +112,7 @@ import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
112
112
  import { chargeWorkflowSends } from "../../lib/workflow-budget";
113
113
  import { isAntigravityValidationRefusal } from "./antigravity-validation-refusal";
114
114
  import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
115
+ import { AdmissionModelDeniedError, admissionModelDeniedResponse } from "../admission-model-scope";
115
116
 
116
117
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
117
118
  export async function prepareAdapterExchange(
@@ -225,8 +226,13 @@ export async function prepareAdapterExchange(
225
226
 
226
227
 
227
228
  let selfContainedJudgment: boolean | undefined;
229
+ // On the Antigravity direct lane the raw body carries only settings; judge the body the bridge
230
+ // would have sent, built only if an ambiguous reset actually asks.
231
+ const directRequest = options.directRequest;
228
232
  const requestIsSelfContained = (): boolean =>
229
- selfContainedJudgment ??= selfContainedResponsesBody(parsed._rawBody);
233
+ selfContainedJudgment ??= selfContainedResponsesBody(directRequest
234
+ ? { ...(parsed._rawBody as Record<string, unknown>), ...directRequest.bridgeConversation() }
235
+ : parsed._rawBody);
230
236
  const claimPreHeaderResend = (): boolean => authorizeResendForRecovery(
231
237
  "pre-header", "connection-reset",
232
238
  ambiguousResendAllowanceFor(route.provider, requestIsSelfContained, claimAmbiguousResend),
@@ -541,6 +547,10 @@ export async function prepareAdapterExchange(
541
547
  return storageRefusal;
542
548
  }
543
549
  const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
550
+ if (refusal instanceof AdmissionModelDeniedError) {
551
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
552
+ return admissionModelDeniedResponse(refusal);
553
+ }
544
554
  const codexRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
545
555
  now: Date.now(), accountSelector: route.codexAccountNamespace,
546
556
  });
@@ -810,6 +820,13 @@ export async function prepareAdapterExchange(
810
820
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
811
821
  return { failed: storageRefusal };
812
822
  }
823
+ const scopeRefusal = unwrapUpstreamRetryEvidenceError(err);
824
+ if (!options.abortSignal?.aborted && scopeRefusal instanceof AdmissionModelDeniedError) {
825
+ cleanupUpstreamAbort();
826
+ upstream.abort();
827
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
828
+ return { failed: admissionModelDeniedResponse(scopeRefusal) };
829
+ }
813
830
  const codexRefusal = !options.abortSignal?.aborted && mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
814
831
  now: Date.now(), accountSelector: route.codexAccountNamespace,
815
832
  });
@@ -1539,7 +1556,17 @@ export async function prepareAdapterExchange(
1539
1556
  // The hop's permit is already settled by the dispatch boundary above; continuing
1540
1557
  // only skips the remaining arms, it does not abandon a reservation.
1541
1558
  if (isNonReplayableResponse(upstreamResponse)) continue recovery;
1542
- } catch {
1559
+ } catch (err) {
1560
+ // A local admission denial is terminal for the whole request: rotating to another
1561
+ // account cannot re-scope the key, so the refusal answers in place of the 429.
1562
+ const scopeRefusal = unwrapUpstreamRetryEvidenceError(err);
1563
+ if (!options.abortSignal?.aborted && scopeRefusal instanceof AdmissionModelDeniedError) {
1564
+ hop.permit?.release();
1565
+ cleanupUpstreamAbort();
1566
+ upstream.abort();
1567
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
1568
+ return admissionModelDeniedResponse(scopeRefusal);
1569
+ }
1543
1570
  // A throw before the send — snapshot fetch, credential application, adapter
1544
1571
  // resolution — must hand the reservation back. Without this the ladder charges the
1545
1572
  // request for a send it never made, and a later recovery in the same request is
@@ -0,0 +1,27 @@
1
+ /** Core-owned, config-scoped registration. An inactive install has no Advisor factory. */
2
+ import type { OcxConfig, OcxParsedRequest, OcxTool } from "../../types";
3
+
4
+ export interface AdvisorPlanInput {
5
+ config: OcxConfig;
6
+ workerIdentity: string;
7
+ workerModelId: string;
8
+ abortSignal?: AbortSignal;
9
+ }
10
+
11
+ export interface RegisteredAdvisorPlan {
12
+ tool: OcxTool;
13
+ preflightInject(parsed: OcxParsedRequest): Promise<boolean>;
14
+ attachGuard(parsed: OcxParsedRequest): void;
15
+ }
16
+
17
+ type AdvisorPlanFactory = (input: AdvisorPlanInput) => Promise<RegisteredAdvisorPlan | null>;
18
+ const factories = new WeakMap<OcxConfig, AdvisorPlanFactory>();
19
+
20
+ export function setAdvisorPlanFactory(config: OcxConfig, factory: AdvisorPlanFactory | null): void {
21
+ if (factory) factories.set(config, factory);
22
+ else factories.delete(config);
23
+ }
24
+
25
+ export function createRegisteredAdvisorPlan(input: AdvisorPlanInput): Promise<RegisteredAdvisorPlan | null> | null {
26
+ return factories.get(input.config)?.(input) ?? null;
27
+ }