@bitkyc08/opencodex 2.55.0 → 2.57.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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,202 @@
1
+ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
2
+ import { stripItemIdsWhenUnstored } from "./request-strips";
3
+ import { isPlainObject } from "./internal";
4
+ import { stripPromptCacheBreakpoints } from "./prompt-cache";
5
+
6
+ /**
7
+ * Remove `previous_response_id` before forwarding. Two triggers:
8
+ * - the proxy expanded the request into a full input replay (the id is now redundant), or
9
+ * - the target is the ChatGPT backend (`authMode: "forward"`), whose Codex REST endpoint
10
+ * categorically rejects the parameter with `{"detail":"Unsupported parameter:
11
+ * previous_response_id"}` (strict allowlist; it also rejects `metadata` and
12
+ * `max_output_tokens`). Codex only sends the id on WS turns, and ocx converts those to
13
+ * internal HTTP requests, so forwarding it upstream is a guaranteed 400 — stripping is
14
+ * strictly better even when the local replay state missed. API-key mode keeps the field on
15
+ * unexpanded requests: the platform `/v1/responses` supports real server-side storage.
16
+ */
17
+ export function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
18
+ if (!strip || !isPlainObject(body) || !Object.prototype.hasOwnProperty.call(body, "previous_response_id")) return body;
19
+ const { previous_response_id: _previousResponseId, ...rest } = body;
20
+ return rest;
21
+ }
22
+
23
+ /** Apply the settled tier only to a fresh outbound object; `_rawBody` remains caller-owned. */
24
+ export function applyTierDecisionToResponsesBody(body: unknown, decision: TierDecision | undefined): unknown {
25
+ if (!decision || decision.kind === "forward-caller" || !isPlainObject(body)) return body;
26
+ const next: Record<string, unknown> = { ...body };
27
+ if (decision.kind === "set") next.service_tier = decision.value;
28
+ else delete next.service_tier;
29
+ return next;
30
+ }
31
+
32
+ /**
33
+ * Drop request parameters a stateless Responses upstream cannot implement, and pin
34
+ * `store` false.
35
+ *
36
+ * `previous_response_id` is listed here as well as in `stripPreviousResponseId`
37
+ * because that helper's strip is conditional on replay expansion, and it keeps the
38
+ * field for API-key providers on the premise that the platform offers real
39
+ * server-side storage. DeepSeek documents the opposite: "the API is stateless:
40
+ * responses and conversations are not stored on the server", so the field can never
41
+ * be honoured regardless of expansion state.
42
+ *
43
+ * `prompt` is a reference to a server-stored prompt template — the most stateful
44
+ * field in the accepted schema.
45
+ *
46
+ * `service_tier` is deliberately NOT dropped: the final TierDecision is applied to a
47
+ * detached outbound body before this sanitizer chain, and silently deleting a configured knob is
48
+ * worse than forwarding a parameter the upstream ignores.
49
+ *
50
+ * MUST run before the composed sanitize chain below: `stripItemIdsWhenUnstored` keys
51
+ * off `store === false`, and a stateless upstream cannot resolve a stored item id.
52
+ * Returns a copy, so `parsed._rawBody` keeps the client's original `store` value and
53
+ * the local replay cache still records the turn.
54
+ */
55
+ export function stripStatefulResponsesParams(body: unknown): unknown {
56
+ if (!isPlainObject(body)) return body;
57
+ const drop = ["previous_response_id", "conversation", "background", "metadata", "prompt"] as const;
58
+ const present = drop.some(key => Object.prototype.hasOwnProperty.call(body, key));
59
+ if (!present && body.store === false) return body;
60
+ const next: Record<string, unknown> = { ...body };
61
+ for (const key of drop) delete next[key];
62
+ next.store = false;
63
+ return next;
64
+ }
65
+
66
+ /**
67
+ * Remove top-level parameters the ChatGPT backend (`authMode: "forward"`) rejects
68
+ * with `{"detail":"Unsupported parameter: …"}` (strict allowlist). Codex CLI never
69
+ * sends these — it controls output length via `reasoning.effort` — but third-party
70
+ * Responses API clients (GJC, SDK wrappers) include `max_output_tokens` per the
71
+ * public spec. `metadata` is likewise absent from the allowlist. No-op when the
72
+ * body carries neither field, keeping the common Codex path allocation-free.
73
+ */
74
+ export function stripUnsupportedForwardParams(body: unknown): unknown {
75
+ if (!isPlainObject(body)) return body;
76
+ const hasMot = Object.prototype.hasOwnProperty.call(body, "max_output_tokens");
77
+ const hasMeta = Object.prototype.hasOwnProperty.call(body, "metadata");
78
+ if (!hasMot && !hasMeta) return body;
79
+ const { max_output_tokens: _mot, metadata: _meta, ...rest } = body;
80
+ return rest;
81
+ }
82
+
83
+ /** Sampling controls the canonical ChatGPT backend rejects; other forward gateways accept them. */
84
+ const CANONICAL_FORWARD_UNSUPPORTED_SAMPLING = ["temperature", "top_p", "stop", "user"] as const;
85
+
86
+ /**
87
+ * Remove sampling controls only the canonical ChatGPT backend rejects.
88
+ *
89
+ * A translated Chat turn used to lose these at the Chat ingress for every provider on
90
+ * the `openai-responses` adapter, which silently discarded caller intent on generic
91
+ * key gateways that accept them. Deciding at the ingress was also unsound for combo
92
+ * and policy routes, whose concrete child is chosen later — so the decision belongs
93
+ * here, on the provider that actually receives the body.
94
+ *
95
+ * Returns a copy and never mutates, so `parsed._rawBody` stays caller-owned, and
96
+ * no-ops when the body carries none of these keys.
97
+ */
98
+ export function stripCanonicalForwardSamplingParams(body: unknown): unknown {
99
+ if (!isPlainObject(body)) return body;
100
+ if (!CANONICAL_FORWARD_UNSUPPORTED_SAMPLING.some(key => Object.prototype.hasOwnProperty.call(body, key))) {
101
+ return body;
102
+ }
103
+ const next: Record<string, unknown> = { ...body };
104
+ for (const key of CANONICAL_FORWARD_UNSUPPORTED_SAMPLING) delete next[key];
105
+ return next;
106
+ }
107
+
108
+ /** Return the lossless text represented by one system message, or null when it is multimodal. */
109
+ function canonicalForwardSystemText(item: Record<string, unknown>): string | null {
110
+ const content = item.content;
111
+ if (content === undefined) return "";
112
+ if (typeof content === "string") return content;
113
+ if (!Array.isArray(content)) return null;
114
+ let text = "";
115
+ for (const block of content) {
116
+ if (!isPlainObject(block)) return null;
117
+ if (block.type !== "input_text" && block.type !== "text") return null;
118
+ if (typeof block.text !== "string") return null;
119
+ text += block.text;
120
+ }
121
+ return text;
122
+ }
123
+
124
+ /** Only message items may carry privileged system instructions. */
125
+ function isCanonicalForwardSystemMessage(item: unknown): item is Record<string, unknown> {
126
+ return isPlainObject(item)
127
+ && (item.type === undefined || item.type === "message")
128
+ && item.role === "system";
129
+ }
130
+
131
+ /**
132
+ * The public Responses API accepts input system messages and `truncation`, but the canonical
133
+ * ChatGPT Codex forward endpoint rejects both. Fold only fully textual system messages into the
134
+ * existing top-level instructions and remove the unsupported flag at this destination boundary.
135
+ *
136
+ * The fold is atomic: if any system message contains a non-text block, keep every message in
137
+ * place so the proxy never silently drops multimodal content. The backend may still reject that
138
+ * unsupported shape, but it will not receive a partially rewritten prompt.
139
+ */
140
+ export function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown {
141
+ if (!isPlainObject(body)) return body;
142
+ const stripTruncation = Object.hasOwn(body, "truncation");
143
+ const input = Array.isArray(body.input) ? body.input : undefined;
144
+ if (!input) {
145
+ if (!stripTruncation) return body;
146
+ const { truncation: _truncation, ...rest } = body;
147
+ return rest;
148
+ }
149
+
150
+ const foldedText: string[] = [];
151
+ let sawSystemMessage = false;
152
+ let canFoldAllSystemMessages = true;
153
+ for (const item of input) {
154
+ if (!isCanonicalForwardSystemMessage(item)) continue;
155
+ sawSystemMessage = true;
156
+ const text = canonicalForwardSystemText(item);
157
+ if (text === null) {
158
+ canFoldAllSystemMessages = false;
159
+ break;
160
+ }
161
+ foldedText.push(text);
162
+ }
163
+ if (!stripTruncation && (!sawSystemMessage || !canFoldAllSystemMessages)) return body;
164
+
165
+ const next: Record<string, unknown> = { ...body };
166
+ if (stripTruncation) delete next.truncation;
167
+ if (sawSystemMessage && canFoldAllSystemMessages) {
168
+ next.input = input.filter(item => !isCanonicalForwardSystemMessage(item));
169
+ const folded = foldedText.join("\n\n");
170
+ if (folded !== "") {
171
+ const existing = typeof body.instructions === "string" ? body.instructions : "";
172
+ next.instructions = existing !== "" ? `${existing}\n\n${folded}` : folded;
173
+ }
174
+ }
175
+ return next;
176
+ }
177
+
178
+ /**
179
+ * Posit Assistant can replay client-only cache markers and stored-item references on a
180
+ * `store: false` continuation. The canonical ChatGPT Codex backend rejects both. Remove the
181
+ * markers recursively and drop only `item_reference` rows that cannot name persisted state;
182
+ * ordinary item ids are handled later by stripItemIdsWhenUnstored and tool call_id pairs remain.
183
+ */
184
+ export function normalizeCanonicalForwardContinuationEnvelope(body: unknown): unknown {
185
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
186
+ let input: unknown[] = body.input;
187
+ let changed = false;
188
+ if (body.store === false) {
189
+ const withoutReferences = input.filter(item => !isPlainObject(item) || item.type !== "item_reference");
190
+ if (withoutReferences.length !== input.length) {
191
+ input = withoutReferences;
192
+ changed = true;
193
+ }
194
+ }
195
+
196
+ const markerRewrite = stripPromptCacheBreakpoints(input, { nodes: 0 });
197
+ if (markerRewrite.complete && markerRewrite.changed) {
198
+ input = markerRewrite.value as unknown[];
199
+ changed = true;
200
+ }
201
+ return changed ? { ...body, input } : body;
202
+ }
@@ -0,0 +1,406 @@
1
+ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
2
+ import { collectResponsesToolGroups } from "../../responses/tool-groups";
3
+ import { isPlainObject } from "./internal";
4
+
5
+ const IMAGE_GEN_NAMESPACE = "image_gen";
6
+ const HOSTED_IMAGE_GENERATION_TOOL = "image_generation";
7
+ const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
8
+ const IMAGE_GEN_WIRE_PREFIX = `${IMAGE_GEN_NAMESPACE}__`;
9
+
10
+ /** Remove a supported client prefix before constructing the canonical image-gen wire alias. */
11
+ function imageGenLocalName(name: string): string {
12
+ if (name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) return name.slice(IMAGE_GEN_DOTTED_PREFIX.length);
13
+ if (name.startsWith(IMAGE_GEN_WIRE_PREFIX)) return name.slice(IMAGE_GEN_WIRE_PREFIX.length);
14
+ return name;
15
+ }
16
+
17
+ /** Build the flat public-Responses name used only on the upstream wire. */
18
+ function imageGenWireName(name: string): string {
19
+ return namespacedToolName(IMAGE_GEN_NAMESPACE, imageGenLocalName(name));
20
+ }
21
+
22
+ /** Match client image-gen declarations across namespace, legacy dotted, and canonical wire forms. */
23
+ function isImageGenClientName(name: string): boolean {
24
+ return name === IMAGE_GEN_NAMESPACE
25
+ || name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
26
+ || name.startsWith(IMAGE_GEN_WIRE_PREFIX);
27
+ }
28
+
29
+ /** Identify declarations that should activate image-gen request normalization. */
30
+ function declaresImageGenClientTool(tool: unknown): boolean {
31
+ if (!isPlainObject(tool) || typeof tool.name !== "string") return false;
32
+ if (tool.type === "namespace") return tool.name === IMAGE_GEN_NAMESPACE;
33
+ return isImageGenClientName(tool.name);
34
+ }
35
+
36
+ /** Rewrite client image-gen selectors to the hosted tool without widening caller restrictions. */
37
+ function preferHostedImageGenToolChoice(toolChoice: unknown): unknown {
38
+ if (!isPlainObject(toolChoice)) return toolChoice;
39
+ if ((toolChoice.type === "function" || toolChoice.type === "custom") && typeof toolChoice.name === "string") {
40
+ return isImageGenClientName(toolChoice.name) ? { type: HOSTED_IMAGE_GENERATION_TOOL } : toolChoice;
41
+ }
42
+ if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
43
+ const hasHostedImageTool = toolChoice.tools.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
44
+ let changed = false;
45
+ let addedHostedImageTool = false;
46
+ const tools: unknown[] = [];
47
+ for (const tool of toolChoice.tools) {
48
+ const isClientImageTool = isPlainObject(tool)
49
+ && (tool.type === "function" || tool.type === "custom")
50
+ && typeof tool.name === "string"
51
+ && isImageGenClientName(tool.name);
52
+ if (!isClientImageTool) {
53
+ tools.push(tool);
54
+ continue;
55
+ }
56
+ changed = true;
57
+ if (!hasHostedImageTool && !addedHostedImageTool) {
58
+ tools.push({ type: HOSTED_IMAGE_GENERATION_TOOL });
59
+ addedHostedImageTool = true;
60
+ }
61
+ }
62
+ return changed ? { ...toolChoice, tools } : toolChoice;
63
+ }
64
+
65
+ /**
66
+ * Some Responses-compatible gateways reserve the hosted image namespace even when the request
67
+ * does not explicitly declare `image_generation`. For an explicitly configured model, remove only
68
+ * colliding client declarations so the gateway's hosted tool can take precedence.
69
+ */
70
+ export function preferConfiguredHostedTools(
71
+ body: unknown,
72
+ provider: OcxProviderConfig,
73
+ modelId: string,
74
+ selectedModelId?: string,
75
+ ): unknown {
76
+ // A virtual model's advertised id takes precedence over its resolved wire-model id.
77
+ // Read own properties only: a routed model id of `constructor`/`toString` would
78
+ // otherwise resolve to an inherited Object.prototype function and throw on the
79
+ // membership test below, failing the request before it is dispatched.
80
+ const preferenceMap = provider.modelPreferHostedTools;
81
+ const ownPreference = (key: string | undefined): string[] | undefined => {
82
+ if (!key || !preferenceMap || !Object.prototype.hasOwnProperty.call(preferenceMap, key)) return undefined;
83
+ const entry = preferenceMap[key];
84
+ return Array.isArray(entry) ? entry : undefined;
85
+ };
86
+ const preferredTools = ownPreference(selectedModelId) ?? ownPreference(modelId);
87
+ if (!preferredTools?.includes(HOSTED_IMAGE_GENERATION_TOOL) || !isPlainObject(body)) return body;
88
+
89
+ const stripGroup = (tools: unknown[]): unknown[] => {
90
+ const filtered = tools.filter(tool => !declaresImageGenClientTool(tool));
91
+ return filtered.length === tools.length ? tools : filtered;
92
+ };
93
+
94
+ let changed = false;
95
+ let tools = body.tools;
96
+ let strippedTopLevelImageGenTool = false;
97
+ if (Array.isArray(body.tools)) {
98
+ tools = stripGroup(body.tools);
99
+ strippedTopLevelImageGenTool = tools !== body.tools;
100
+ changed ||= strippedTopLevelImageGenTool;
101
+ }
102
+
103
+ let input = body.input;
104
+ const strippedAdditionalToolsIndices = new Set<number>();
105
+ if (Array.isArray(body.input)) {
106
+ let nestedChanged = false;
107
+ const mappedInput = body.input.map((item, index) => {
108
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
109
+ const nestedTools = stripGroup(item.tools);
110
+ if (nestedTools === item.tools) return item;
111
+ strippedAdditionalToolsIndices.add(index);
112
+ nestedChanged = true;
113
+ return { ...item, tools: nestedTools };
114
+ });
115
+ if (nestedChanged) {
116
+ input = mappedInput;
117
+ changed = true;
118
+ }
119
+ }
120
+
121
+ const hasToolChoice = Object.hasOwn(body, "tool_choice");
122
+ const toolChoice = hasToolChoice ? preferHostedImageGenToolChoice(body.tool_choice) : body.tool_choice;
123
+ const toolChoiceChanged = hasToolChoice && toolChoice !== body.tool_choice;
124
+ const hasHostedImageGenTool = (toolGroup: unknown): boolean => Array.isArray(toolGroup)
125
+ && toolGroup.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
126
+ const hasHostedImageGenDeclaration = hasHostedImageGenTool(tools)
127
+ || (Array.isArray(input) && input.some(item => isPlainObject(item)
128
+ && item.type === "additional_tools"
129
+ && hasHostedImageGenTool(item.tools)));
130
+ if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndices.size > 0) && !hasHostedImageGenDeclaration) {
131
+ if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
132
+ tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
133
+ } else if (strippedAdditionalToolsIndices.size > 0 && Array.isArray(input)) {
134
+ // Restore into the FIRST stripped container only. Tool declarations are
135
+ // request-scoped, not container-scoped — the containers are separate carriers for
136
+ // one tool set, so a single hosted declaration covers the request. An earlier
137
+ // revision restored into every stripped container and put `image_generation` on
138
+ // the wire twice; review caught it.
139
+ const firstStripped = Math.min(...strippedAdditionalToolsIndices);
140
+ input = input.map((item, index) => index === firstStripped
141
+ && isPlainObject(item)
142
+ && Array.isArray(item.tools)
143
+ ? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
144
+ : item);
145
+ }
146
+ }
147
+ changed ||= toolChoiceChanged;
148
+ if (!changed) return body;
149
+ const next: Record<string, unknown> = {
150
+ ...body,
151
+ ...(Array.isArray(body.tools) ? { tools } : {}),
152
+ ...(Array.isArray(body.input) ? { input } : {}),
153
+ };
154
+ if (toolChoiceChanged) next.tool_choice = toolChoice;
155
+ return next;
156
+ }
157
+
158
+ /**
159
+ * Lower one complete Codex image-gen namespace to public Responses function tools.
160
+ *
161
+ * The public API reserves the `image_gen` namespace and restricts function names to a flat safe
162
+ * alphabet. `image_gen__<tool>` is therefore an upstream-only alias; client-facing responses are
163
+ * restored to explicit `{ namespace: "image_gen", name: "<tool>" }` calls by the server. Only a
164
+ * non-empty namespace containing named function tools is safe to lower. Malformed, empty, and
165
+ * future namespace shapes stay untouched instead of silently losing client capabilities.
166
+ */
167
+ function flattenImageGenNamespace(tool: unknown): Record<string, unknown>[] | undefined {
168
+ if (
169
+ !isPlainObject(tool)
170
+ || tool.type !== "namespace"
171
+ || tool.name !== IMAGE_GEN_NAMESPACE
172
+ || !Array.isArray(tool.tools)
173
+ || tool.tools.length === 0
174
+ ) return undefined;
175
+
176
+ for (const innerTool of tool.tools) {
177
+ if (
178
+ !isPlainObject(innerTool)
179
+ || innerTool.type !== "function"
180
+ || typeof innerTool.name !== "string"
181
+ || innerTool.name.length === 0
182
+ ) return undefined;
183
+ }
184
+
185
+ return tool.tools.map(innerTool => {
186
+ const functionTool = innerTool as Record<string, unknown> & { name: string };
187
+ return {
188
+ ...functionTool,
189
+ name: imageGenWireName(functionTool.name),
190
+ };
191
+ });
192
+ }
193
+
194
+ /** Convert a legacy dotted function declaration while preserving all other function metadata. */
195
+ function normalizeFlatImageGenFunction(tool: unknown): unknown {
196
+ if (
197
+ !isPlainObject(tool)
198
+ || tool.type !== "function"
199
+ || typeof tool.name !== "string"
200
+ || !tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
201
+ ) return tool;
202
+ return { ...tool, name: imageGenWireName(tool.name) };
203
+ }
204
+
205
+ /** Return the image-gen function name used for stable cross-container deduplication. */
206
+ function imageGenFunctionName(tool: unknown): string | undefined {
207
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
208
+ return undefined;
209
+ }
210
+ return isImageGenClientName(tool.name) ? tool.name : undefined;
211
+ }
212
+
213
+ /** True only when a declaration can yield a callable upstream-safe image-gen function alias. */
214
+ function declaresUsableImageGenAlias(tool: unknown): boolean {
215
+ if (flattenImageGenNamespace(tool)) return true;
216
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
217
+ return false;
218
+ }
219
+ if (tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
220
+ return tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length;
221
+ }
222
+ return tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
223
+ && tool.name.length > IMAGE_GEN_WIRE_PREFIX.length;
224
+ }
225
+
226
+ /** Collect client tool-choice names and the exact upstream aliases declared for them. */
227
+ function imageGenToolChoiceAliases(toolGroups: unknown[][]): Map<string, string> {
228
+ const aliases = new Map<string, string>();
229
+
230
+ for (const group of toolGroups) {
231
+ for (const tool of group) {
232
+ const flattened = flattenImageGenNamespace(tool);
233
+ if (flattened) {
234
+ for (const candidate of flattened) {
235
+ const wireName = candidate.name as string;
236
+ aliases.set(`${IMAGE_GEN_DOTTED_PREFIX}${imageGenLocalName(wireName)}`, wireName);
237
+ aliases.set(wireName, wireName);
238
+ }
239
+ continue;
240
+ }
241
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
242
+ continue;
243
+ }
244
+ if (
245
+ tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
246
+ && tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length
247
+ ) {
248
+ aliases.set(tool.name, imageGenWireName(tool.name));
249
+ } else if (
250
+ tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
251
+ && tool.name.length > IMAGE_GEN_WIRE_PREFIX.length
252
+ ) {
253
+ aliases.set(tool.name, tool.name);
254
+ }
255
+ }
256
+ }
257
+
258
+ return aliases;
259
+ }
260
+
261
+ /** Rewrite function selectors only when their corresponding declaration receives a wire alias. */
262
+ function normalizeImageGenToolChoice(
263
+ toolChoice: unknown,
264
+ aliases: ReadonlyMap<string, string>,
265
+ ): unknown {
266
+ if (!isPlainObject(toolChoice)) return toolChoice;
267
+
268
+ if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
269
+ const alias = aliases.get(toolChoice.name);
270
+ return alias && alias !== toolChoice.name ? { ...toolChoice, name: alias } : toolChoice;
271
+ }
272
+
273
+ if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
274
+ let changed = false;
275
+ const tools = toolChoice.tools.map(tool => {
276
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
277
+ return tool;
278
+ }
279
+ const alias = aliases.get(tool.name);
280
+ if (!alias || alias === tool.name) return tool;
281
+ changed = true;
282
+ return { ...tool, name: alias };
283
+ });
284
+ return changed ? { ...toolChoice, tools } : toolChoice;
285
+ }
286
+
287
+ /** Identify replayed image-gen calls that require upstream wire encoding. */
288
+ function declaresImageGenFunctionCall(item: unknown): boolean {
289
+ if (!isPlainObject(item) || item.type !== "function_call" || typeof item.name !== "string") {
290
+ return false;
291
+ }
292
+ return item.namespace === IMAGE_GEN_NAMESPACE || isImageGenClientName(item.name);
293
+ }
294
+
295
+ /** Encode native or legacy replay calls to the same flat name used by tool declarations. */
296
+ function normalizeImageGenFunctionCall(item: unknown): unknown {
297
+ if (!declaresImageGenFunctionCall(item) || !isPlainObject(item) || typeof item.name !== "string") {
298
+ return item;
299
+ }
300
+ if (item.namespace === IMAGE_GEN_NAMESPACE) {
301
+ const { namespace: _namespace, ...rest } = item;
302
+ return { ...rest, name: imageGenWireName(item.name) };
303
+ }
304
+ if (item.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
305
+ return { ...item, name: imageGenWireName(item.name) };
306
+ }
307
+ return item;
308
+ }
309
+
310
+ /**
311
+ * Normalize Codex's private image-gen tool declaration for API-key Responses providers.
312
+ *
313
+ * A complete `image_gen` namespace is flattened to safe `image_gen__<tool>` aliases even when it is
314
+ * the only image tool in the request. Replayed client calls are encoded to the same alias, including
315
+ * legacy dotted calls from older compatibility attempts. When a usable alias replaces a client
316
+ * image-gen declaration, the duplicate hosted `image_generation` entry is removed. Duplicate aliases
317
+ * are resolved in stable container order: top-level tools first, then Responses Lite
318
+ * `additional_tools` entries.
319
+ *
320
+ * This function is called only on the API-key path. ChatGPT forward mode understands the private
321
+ * namespace and must keep it. Copy-on-write preserves the original request reference when no
322
+ * namespace is flattened, hosted tool removed, or duplicate function discarded.
323
+ */
324
+ export function normalizeImageGenClientTools(body: unknown): unknown {
325
+ if (!isPlainObject(body)) return body;
326
+
327
+ const toolGroups = collectResponsesToolGroups(body);
328
+ const hasImageGenClientTool = toolGroups.some(group => group.some(declaresImageGenClientTool))
329
+ || (Array.isArray(body.input) && body.input.some(declaresImageGenFunctionCall));
330
+ if (!hasImageGenClientTool) return body;
331
+ const hasUsableImageGenAlias = toolGroups.some(group => group.some(declaresUsableImageGenAlias));
332
+ const toolChoiceAliases = imageGenToolChoiceAliases(toolGroups);
333
+
334
+ const seenFunctionNames = new Set<string>();
335
+ const normalizeGroup = (tools: unknown[]): unknown[] => {
336
+ const normalized: unknown[] = [];
337
+ let groupChanged = false;
338
+
339
+ for (const tool of tools) {
340
+ if (
341
+ hasUsableImageGenAlias
342
+ && isPlainObject(tool)
343
+ && tool.type === HOSTED_IMAGE_GENERATION_TOOL
344
+ ) {
345
+ groupChanged = true;
346
+ continue;
347
+ }
348
+
349
+ const flattened = flattenImageGenNamespace(tool);
350
+ const candidates = flattened ?? [tool];
351
+ if (flattened) groupChanged = true;
352
+
353
+ for (const candidate of candidates) {
354
+ const normalizedCandidate = normalizeFlatImageGenFunction(candidate);
355
+ if (normalizedCandidate !== candidate) groupChanged = true;
356
+ const functionName = imageGenFunctionName(normalizedCandidate);
357
+ if (functionName && seenFunctionNames.has(functionName)) {
358
+ groupChanged = true;
359
+ continue;
360
+ }
361
+ if (functionName) seenFunctionNames.add(functionName);
362
+ normalized.push(normalizedCandidate);
363
+ }
364
+ }
365
+
366
+ return groupChanged ? normalized : tools;
367
+ };
368
+
369
+ let changed = false;
370
+ let tools = body.tools;
371
+ if (Array.isArray(body.tools)) {
372
+ tools = normalizeGroup(body.tools);
373
+ changed ||= tools !== body.tools;
374
+ }
375
+
376
+ let input = body.input;
377
+ if (Array.isArray(body.input)) {
378
+ let nestedChanged = false;
379
+ const mappedInput = body.input.map(item => {
380
+ if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
381
+ const nestedTools = normalizeGroup(item.tools);
382
+ if (nestedTools === item.tools) return item;
383
+ nestedChanged = true;
384
+ return { ...item, tools: nestedTools };
385
+ }
386
+ const normalizedCall = normalizeImageGenFunctionCall(item);
387
+ if (normalizedCall !== item) nestedChanged = true;
388
+ return normalizedCall;
389
+ });
390
+ if (nestedChanged) {
391
+ input = mappedInput;
392
+ changed = true;
393
+ }
394
+ }
395
+
396
+ const toolChoice = normalizeImageGenToolChoice(body.tool_choice, toolChoiceAliases);
397
+ changed ||= toolChoice !== body.tool_choice;
398
+
399
+ if (!changed) return body;
400
+ return {
401
+ ...body,
402
+ ...(Array.isArray(body.tools) ? { tools } : {}),
403
+ ...(Array.isArray(body.input) ? { input } : {}),
404
+ ...(Object.prototype.hasOwnProperty.call(body, "tool_choice") ? { tool_choice: toolChoice } : {}),
405
+ };
406
+ }
@@ -0,0 +1,3 @@
1
+ export function isPlainObject(v: unknown): v is Record<string, unknown> {
2
+ return !!v && typeof v === "object" && !Array.isArray(v);
3
+ }