@bitkyc08/opencodex 2.67.0 → 2.68.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 (257) hide show
  1. package/bin/ocx.mjs +3 -0
  2. package/gui/dist/assets/App-DCBismRi.css +1 -0
  3. package/gui/dist/assets/App-mpsibsFj.js +51 -0
  4. package/gui/dist/assets/Tray-Bn8WhKBH.js +1 -0
  5. package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
  6. package/gui/dist/assets/index-kUE8tuAK.js +86 -0
  7. package/gui/dist/assets/quota-summary-DL-bNuNL.js +1 -0
  8. package/gui/dist/index.html +21 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-image-guard.ts +13 -1
  11. package/src/adapters/anthropic-image-normalize.ts +3 -2
  12. package/src/adapters/anthropic-output-schema.ts +36 -0
  13. package/src/adapters/anthropic.ts +10 -2
  14. package/src/adapters/base.ts +7 -0
  15. package/src/adapters/codebuddy/live-models.ts +180 -0
  16. package/src/adapters/coding-agent/protocol.ts +91 -12
  17. package/src/adapters/coding-agent/turn.ts +32 -27
  18. package/src/adapters/devin/cloud-direct/index.ts +1 -0
  19. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
  20. package/src/adapters/devin.ts +9 -6
  21. package/src/adapters/google-http.ts +4 -4
  22. package/src/adapters/google.ts +93 -3
  23. package/src/adapters/kiro/adapter.ts +6 -2
  24. package/src/adapters/kiro/stream.ts +12 -2
  25. package/src/adapters/kiro/usage.ts +3 -1
  26. package/src/adapters/kiro-errors.ts +11 -1
  27. package/src/adapters/kiro-events.ts +27 -1
  28. package/src/adapters/kiro-refusal.ts +30 -0
  29. package/src/adapters/kiro-retry.ts +71 -31
  30. package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
  31. package/src/adapters/openai-chat/messages.ts +31 -4
  32. package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
  33. package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
  34. package/src/adapters/openai-chat/tool-schema.ts +6 -2
  35. package/src/adapters/openai-chat.ts +2 -2
  36. package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
  37. package/src/adapters/openai-responses/passthrough.ts +9 -3
  38. package/src/adapters/opencode-go-additional-tools.ts +19 -10
  39. package/src/adapters/physical-send.ts +10 -3
  40. package/src/adapters/registry.ts +2 -1
  41. package/src/adapters/responses-tool-schema.ts +31 -3
  42. package/src/adapters/run-turn-queue.ts +63 -23
  43. package/src/adapters/unique-tool-call-ids.ts +63 -0
  44. package/src/adapters/xai-web-search.ts +32 -2
  45. package/src/bridge/sse.ts +14 -2
  46. package/src/chat/inbound.ts +20 -6
  47. package/src/claude/agents-inject.ts +4 -0
  48. package/src/claude/desktop-3p.ts +53 -13
  49. package/src/claude/desktop-profile.ts +41 -4
  50. package/src/claude/inbound-model-options.ts +14 -2
  51. package/src/claude/inbound.ts +1 -1
  52. package/src/claude/intercept/connect-proxy.ts +17 -1
  53. package/src/claude/intercept/local-ca.ts +7 -1
  54. package/src/claude/message-threads.ts +28 -0
  55. package/src/cli/account-api.ts +13 -0
  56. package/src/cli/account-auth.ts +49 -2
  57. package/src/cli/account-extended.ts +4 -2
  58. package/src/cli/account.ts +4 -2
  59. package/src/cli/capabilities.ts +17 -0
  60. package/src/cli/claude.ts +3 -1
  61. package/src/cli/dispatch.ts +62 -5
  62. package/src/cli/index.ts +84 -79
  63. package/src/cli/minimax.ts +4 -2
  64. package/src/cli/opencode.ts +6 -1
  65. package/src/cli/restart-handoff.ts +110 -0
  66. package/src/cli/status.ts +51 -0
  67. package/src/client/connect.ts +34 -15
  68. package/src/client/link-ingress.ts +102 -0
  69. package/src/client/link-join.ts +37 -16
  70. package/src/client/link-relay.ts +228 -45
  71. package/src/client/link-state.ts +54 -0
  72. package/src/client/link-status.ts +39 -0
  73. package/src/client/link-teardown.ts +2 -2
  74. package/src/client/link-tunnel.ts +615 -86
  75. package/src/client/machine-api.ts +10 -4
  76. package/src/client/machine-listener.ts +57 -10
  77. package/src/client/runtime.ts +210 -35
  78. package/src/clients/config-export/constants.ts +1 -12
  79. package/src/clients/config-export/contracts.ts +2 -0
  80. package/src/clients/config-export/model-metadata.ts +9 -3
  81. package/src/clients/config-export/omp.ts +1 -1
  82. package/src/clients/config-export/zcode-store.ts +2 -2
  83. package/src/clients/config-export.ts +36 -3
  84. package/src/codex/app-server-processes.ts +32 -0
  85. package/src/codex/app-server-restart-service.ts +20 -3
  86. package/src/codex/catalog/access-programs.ts +50 -0
  87. package/src/codex/catalog/build-entries.ts +13 -0
  88. package/src/codex/catalog/derive-entry.ts +1 -0
  89. package/src/codex/catalog/model-hints.ts +11 -0
  90. package/src/codex/catalog/parsing.ts +6 -2
  91. package/src/codex/catalog/provider-models.ts +81 -0
  92. package/src/codex/catalog/retained-sync.ts +3 -0
  93. package/src/codex/codex-write-lock.ts +2 -2
  94. package/src/codex/convergence.ts +2 -0
  95. package/src/codex/desired-state.ts +14 -4
  96. package/src/codex/home.ts +21 -3
  97. package/src/codex/inject/restore.ts +17 -0
  98. package/src/codex/inject/routing-classify.ts +3 -2
  99. package/src/codex/inject-coordination.ts +4 -1
  100. package/src/codex/inject.ts +8 -1
  101. package/src/codex/journal.ts +6 -2
  102. package/src/codex/management-convergence.ts +9 -0
  103. package/src/codex/model-entitlements.ts +28 -5
  104. package/src/codex/routing/idle-window.ts +58 -0
  105. package/src/codex/routing-drift.ts +134 -0
  106. package/src/codex/routing-healer.ts +419 -0
  107. package/src/codex/routing.ts +10 -0
  108. package/src/codex/runtime.ts +188 -57
  109. package/src/codex/sibling-handoff.ts +71 -0
  110. package/src/codex/sibling-start.ts +139 -0
  111. package/src/codex/sync.ts +5 -3
  112. package/src/combos/failover.ts +32 -5
  113. package/src/combos/request.ts +11 -3
  114. package/src/combos/reset-window.ts +10 -5
  115. package/src/combos/resolve.ts +36 -6
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/load-degrade.ts +2 -0
  118. package/src/config/paths.ts +7 -1
  119. package/src/config/process-state.ts +10 -1
  120. package/src/config/schema/compaction-recovery.ts +15 -0
  121. package/src/config/schema/config-schema.ts +2 -0
  122. package/src/config/schema/leaf-validators.ts +9 -3
  123. package/src/generated/compatibility-version.json +360 -204
  124. package/src/images/loop.ts +47 -32
  125. package/src/integrations/catalog-refresh.ts +4 -0
  126. package/src/integrations/omp-yaml-source.ts +1 -0
  127. package/src/lib/claude-request-projection.ts +101 -0
  128. package/src/lib/local-account-switch-capability.ts +48 -0
  129. package/src/lib/local-upstream.ts +146 -0
  130. package/src/lib/package-tree-integrity.ts +25 -3
  131. package/src/lib/package-tree-retarget.ts +130 -0
  132. package/src/lib/process-control.ts +4 -1
  133. package/src/lib/system-restart-contract.ts +106 -0
  134. package/src/lib/upstream-retry.ts +57 -1
  135. package/src/link/ports.ts +9 -0
  136. package/src/link/ssh-argv.ts +16 -0
  137. package/src/link/ssh-runner.ts +70 -2
  138. package/src/link/tunnel-state.ts +58 -9
  139. package/src/oauth/account-quota-rank.ts +29 -18
  140. package/src/oauth/generic-account-failover.ts +179 -22
  141. package/src/oauth/index.ts +17 -4
  142. package/src/oauth/kiro-account-load.ts +107 -0
  143. package/src/oauth/kiro-device-login.ts +312 -0
  144. package/src/oauth/kiro-terminal-failover.ts +28 -0
  145. package/src/oauth/login-flow-state.ts +7 -1
  146. package/src/oauth/pool-settings-capability.ts +23 -14
  147. package/src/oauth/store.ts +56 -3
  148. package/src/oauth/types.ts +4 -0
  149. package/src/plugins/loader.ts +350 -0
  150. package/src/plugins/upstream-hooks.ts +119 -0
  151. package/src/protocols/encoders/adapter-events.ts +9 -2
  152. package/src/providers/account-quota-disk.ts +42 -4
  153. package/src/providers/codebuddy-models.ts +6 -2
  154. package/src/providers/command-code-efforts.ts +33 -10
  155. package/src/providers/kiro-account-state-disk.ts +44 -0
  156. package/src/providers/kiro-model-catalog.ts +162 -0
  157. package/src/providers/kiro-models.ts +5 -4
  158. package/src/providers/kiro-quota-metrics.ts +35 -0
  159. package/src/providers/kiro-usage.ts +118 -14
  160. package/src/providers/quota/account-cache.ts +43 -7
  161. package/src/providers/quota/antigravity.ts +8 -3
  162. package/src/providers/quota/kiro-account-probe.ts +12 -0
  163. package/src/providers/quota/vendor-probes-key.ts +31 -26
  164. package/src/providers/quota/vendor-probes-oauth.ts +16 -7
  165. package/src/providers/quota-types.ts +14 -0
  166. package/src/providers/quota.ts +21 -21
  167. package/src/providers/registry/entries-extended.ts +5 -3
  168. package/src/providers/request-pacing.ts +410 -12
  169. package/src/remote-control/workspace-codex-runtime.ts +4 -3
  170. package/src/responses/citation-markers.ts +132 -65
  171. package/src/responses/hosted-tool-policy.ts +14 -3
  172. package/src/responses/parser-content.ts +3 -2
  173. package/src/responses/parser.ts +4 -1
  174. package/src/responses/schema.ts +4 -0
  175. package/src/responses/visualization-directives.ts +182 -0
  176. package/src/server/chat-native-sse.ts +24 -12
  177. package/src/server/chat-native.ts +137 -68
  178. package/src/server/claude-messages.ts +147 -12
  179. package/src/server/index/link-listener.ts +7 -0
  180. package/src/server/index/optional-listeners.ts +4 -1
  181. package/src/server/index/package-tree-guard.ts +35 -16
  182. package/src/server/index/serve-options.ts +4 -2
  183. package/src/server/index/startup-warnings.ts +6 -0
  184. package/src/server/index.ts +7 -7
  185. package/src/server/inference/client-encoder-delivery.ts +3 -0
  186. package/src/server/inference/context.ts +30 -2
  187. package/src/server/local-account-switch-auth.ts +79 -0
  188. package/src/server/management/agent-settings-routes.ts +11 -3
  189. package/src/server/management/config-routes.ts +28 -11
  190. package/src/server/management/context.ts +2 -0
  191. package/src/server/management/link-routes.ts +123 -39
  192. package/src/server/management/logs-usage-routes.ts +9 -42
  193. package/src/server/management/model-rows.ts +1 -0
  194. package/src/server/management/oauth-account-routes.ts +67 -12
  195. package/src/server/management/route-registry.ts +5 -5
  196. package/src/server/management/sibling-guard.ts +60 -0
  197. package/src/server/management/storage-log-guard-routes.ts +72 -6
  198. package/src/server/management/system-restart.ts +92 -135
  199. package/src/server/management/system-routes.ts +1 -1
  200. package/src/server/management-api.ts +31 -3
  201. package/src/server/management-auth.ts +3 -0
  202. package/src/server/port-reclaim.ts +111 -1
  203. package/src/server/proxy-liveness.ts +16 -0
  204. package/src/server/relay.ts +16 -15
  205. package/src/server/request-log-filter.ts +28 -27
  206. package/src/server/request-log.ts +7 -3
  207. package/src/server/request-metrics.ts +28 -0
  208. package/src/server/responses/adapter-continuation.ts +151 -21
  209. package/src/server/responses/adapter-delivery.ts +21 -7
  210. package/src/server/responses/adapter-dispatch.ts +239 -51
  211. package/src/server/responses/codex-ws-pool.ts +5 -4
  212. package/src/server/responses/codex-ws-request.ts +4 -1
  213. package/src/server/responses/compact.ts +8 -4
  214. package/src/server/responses/compaction-recovery-policy.ts +116 -0
  215. package/src/server/responses/compaction-recovery.ts +330 -0
  216. package/src/server/responses/core-combo-failure.ts +1 -0
  217. package/src/server/responses/core-combo.ts +5 -1
  218. package/src/server/responses/core-lifetime.ts +22 -0
  219. package/src/server/responses/core-options.ts +16 -1
  220. package/src/server/responses/core.ts +14 -4
  221. package/src/server/responses/empty-completion-guard.ts +2 -0
  222. package/src/server/responses/fetch-helpers.ts +82 -31
  223. package/src/server/responses/passthrough-delivery.ts +3 -2
  224. package/src/server/responses/passthrough-dispatch.ts +12 -2
  225. package/src/server/responses/passthrough-execution.ts +2 -2
  226. package/src/server/responses/request-send-budget.ts +21 -5
  227. package/src/server/responses/request-transport.ts +116 -18
  228. package/src/server/responses/reset-replay.ts +32 -0
  229. package/src/server/responses/run-turn-execution.ts +136 -19
  230. package/src/server/responses/sidecar-execution.ts +41 -8
  231. package/src/server/responses/terminal-guard.ts +2 -0
  232. package/src/server/responses/ws-upstream.ts +44 -4
  233. package/src/server/restart-replacement.ts +382 -0
  234. package/src/server/startup-health-cache.ts +23 -4
  235. package/src/server/stop-teardown.ts +35 -1
  236. package/src/server/system-env.ts +3 -0
  237. package/src/service/windows-taskxml.ts +11 -7
  238. package/src/service/windows-wrapper-exit.ts +8 -0
  239. package/src/stall-timeout.ts +35 -10
  240. package/src/storage/policy-job.ts +4 -0
  241. package/src/storage/scanner.ts +177 -29
  242. package/src/storage/storage-mutation-coordinator.ts +17 -0
  243. package/src/tray/windows-tray.ps1 +50 -23
  244. package/src/types/config.ts +4 -0
  245. package/src/types/provider.ts +5 -1
  246. package/src/types/request.ts +13 -2
  247. package/src/update/index.ts +3 -1
  248. package/src/update/job.ts +6 -2
  249. package/src/update/mise-launcher-target.ts +100 -0
  250. package/src/update/worker-launch.ts +46 -0
  251. package/src/usage/log.ts +2 -0
  252. package/src/web-search/loop.ts +5 -2
  253. package/gui/dist/assets/App-BJsT8Icc.css +0 -1
  254. package/gui/dist/assets/App-D3pNGiN4.js +0 -51
  255. package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
  256. package/gui/dist/assets/index-CouvvtMV.js +0 -86
  257. package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
@@ -14,6 +14,7 @@ import {
14
14
  retryBackoffDelayMs,
15
15
  sleepWithAbort,
16
16
  } from "../lib/upstream-retry";
17
+ import { releaseProviderRequestSlot, sendTrackingRequestSlot, type ProviderRequestSlot } from "../providers/request-pacing";
17
18
 
18
19
  const RESET_ATTEMPTS = 3;
19
20
  const RESET_RETRY_BASE_MS = 150;
@@ -29,6 +30,7 @@ const ENDPOINT_ERROR_MARKERS = [
29
30
  "unsupported endpoint",
30
31
  ];
31
32
  const CONNECT_ERROR_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH"]);
33
+ type KiroFetchContext = AdapterFetchContext & { allowGatewayRotation?: boolean };
32
34
 
33
35
  interface KiroThrottleProbe {
34
36
  token: symbol;
@@ -165,33 +167,54 @@ async function fetchWithResetRecovery(
165
167
  let lastError: unknown;
166
168
  for (let attempt = 0; attempt < RESET_ATTEMPTS; attempt++) {
167
169
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
168
- // Every physical send is admitted, not just the adapter entry. Kiro nests a throttle loop
169
- // over this ladder and can run the ladder twice per throttle round, so counting one entry
170
- // as one send hid up to eighteen upstream requests from the per-request cap (#4546).
171
- const decision = ctx.sendBudget?.reserveDispatch({ sendClass: "transient", targetKey: url });
172
- if (decision && (!decision.allowed || !decision.permit.use())) {
173
- throw new SendBudgetExhaustedError(url);
174
- }
175
- // Reported after admission and before dispatch, so a refused send is never counted and an
176
- // admitted one is counted exactly once whichever way the fetch below settles.
177
- notePhysicalSend(attempt > 0);
170
+ const executor = (ctx.executor ?? globalThis.fetch) as typeof globalThis.fetch & {
171
+ waitForPacing?: (signal?: AbortSignal) => Promise<ProviderRequestSlot | void>;
172
+ unpacedFetch?: typeof globalThis.fetch;
173
+ };
174
+ const slot = (await executor.waitForPacing?.(ctx.abortSignal)) || undefined;
178
175
  try {
179
- const headers = new Headers(request.headers);
180
- const recovered = attempt > 0;
181
- if (recovered) headers.set("connection", "close");
182
- return await fetchWithAttemptDeadline(url, {
183
- method: request.method,
184
- headers,
185
- body: request.body,
186
- ...(recovered ? { keepalive: false } : {}),
187
- }, timeoutMs, ctx.abortSignal, ctx.stream);
188
- } catch (error) {
189
- if (ctx.abortSignal?.aborted || !isConnectionResetError(error) || attempt === RESET_ATTEMPTS - 1) throw error;
190
- lastError = error;
191
- await sleepWithAbort(retryBackoffDelayMs(attempt, {
192
- baseDelayMs: RESET_RETRY_BASE_MS,
193
- maxDelayMs: RESET_RETRY_MAX_MS,
194
- }), ctx.abortSignal);
176
+ if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
177
+ // Every physical send is admitted, not just the adapter entry. Kiro nests a throttle loop
178
+ // over this ladder and can run the ladder twice per throttle round, so counting one entry
179
+ // as one send hid up to eighteen upstream requests from the per-request cap (#4546).
180
+ const decision = ctx.sendBudget?.reserveDispatch({ sendClass: "transient", targetKey: url });
181
+ if (decision && (!decision.allowed || !decision.permit.use())) {
182
+ throw new SendBudgetExhaustedError(url);
183
+ }
184
+ // Reported after admission and before dispatch, so a refused send is never counted and an
185
+ // admitted one is counted exactly once whichever way the fetch below settles.
186
+ notePhysicalSend(attempt > 0);
187
+ try {
188
+ const headers = new Headers(request.headers);
189
+ const recovered = attempt > 0;
190
+ if (recovered) headers.set("connection", "close");
191
+ return await sendTrackingRequestSlot(slot, () => fetchWithAttemptDeadline(url, {
192
+ method: request.method,
193
+ headers,
194
+ body: request.body,
195
+ ...(recovered ? { keepalive: false } : {}),
196
+ }, timeoutMs, ctx.abortSignal, ctx.stream, (async (input, init) => {
197
+ try {
198
+ return await (executor.unpacedFetch ?? executor)(input, init);
199
+ } catch (error) {
200
+ if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
201
+ const signal = init?.signal;
202
+ if (signal?.aborted && signal.reason instanceof Error && signal.reason.name === "TimeoutError") {
203
+ throw signal.reason;
204
+ }
205
+ throw error;
206
+ }
207
+ }) as typeof globalThis.fetch));
208
+ } catch (error) {
209
+ if (ctx.abortSignal?.aborted || !isConnectionResetError(error) || attempt === RESET_ATTEMPTS - 1) throw error;
210
+ lastError = error;
211
+ await sleepWithAbort(retryBackoffDelayMs(attempt, {
212
+ baseDelayMs: RESET_RETRY_BASE_MS,
213
+ maxDelayMs: RESET_RETRY_MAX_MS,
214
+ }), ctx.abortSignal);
215
+ }
216
+ } finally {
217
+ releaseProviderRequestSlot(slot);
195
218
  }
196
219
  }
197
220
  throw lastError ?? new Error("Kiro fetch failed");
@@ -217,7 +240,7 @@ async function inspectEndpointHttpFailure(
217
240
  return { response: rebuilt, fallback: ENDPOINT_ERROR_MARKERS.some(marker => text.includes(marker)) };
218
241
  }
219
242
 
220
- async function normalizeFinalKiroHttpError(res: Response, signal?: AbortSignal): Promise<Response> {
243
+ export async function normalizeFinalKiroHttpError(res: Response, signal?: AbortSignal): Promise<Response> {
221
244
  return normalizeUpstreamHttpErrorResponse(res, {
222
245
  signal,
223
246
  formatMessage: payloadText => safeKiroHttpErrorMessage(res.status, res.headers, payloadText),
@@ -255,19 +278,28 @@ async function inspectKiroThrottle(
255
278
 
256
279
  async function fetchKiroAttempt(
257
280
  request: AdapterRequest,
258
- ctx: AdapterFetchContext,
281
+ ctx: KiroFetchContext,
259
282
  timeoutMs: number,
260
283
  notePhysicalSend: (reset: boolean) => void,
261
284
  ): Promise<Response> {
262
- const legacy = legacyUrl(request.url);
285
+ const plannedUrl = request.url;
286
+ const legacy = legacyUrl(plannedUrl);
263
287
  let response: Response;
264
288
  try {
265
- response = await fetchWithResetRecovery(request, request.url, ctx, timeoutMs, notePhysicalSend);
289
+ response = await fetchWithResetRecovery(request, plannedUrl, ctx, timeoutMs, notePhysicalSend);
266
290
  } catch (error) {
267
291
  if (!legacy || !endpointConnectFailure(error)) throw error;
268
292
  return fetchWithResetRecovery(request, legacy, ctx, timeoutMs, notePhysicalSend);
269
293
  }
270
294
 
295
+ if (!response.ok && (response.status === 502 || response.status === 503 || response.status === 504)) {
296
+ const baseUrl = response.url || request.url || plannedUrl;
297
+ const alternate = ctx.allowGatewayRotation === false ? undefined : legacyUrl(baseUrl);
298
+ if (alternate) {
299
+ cancelResponseBodyBestEffort(response);
300
+ return fetchWithResetRecovery(request, alternate, ctx, timeoutMs, notePhysicalSend);
301
+ }
302
+ }
271
303
  if (legacy && !response.ok) {
272
304
  const inspected = await inspectEndpointHttpFailure(response, ctx.abortSignal);
273
305
  response = inspected.response;
@@ -284,7 +316,7 @@ async function fetchKiroAttempt(
284
316
  * throttle recovery. The shared probe starts only after a 429, so healthy parallel traffic remains
285
317
  * parallel while a throttled account cannot burn every caller's independent retry budget (#532).
286
318
  */
287
- export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
319
+ export async function fetchKiroWithRetry(request: AdapterRequest, ctx: KiroFetchContext = {}): Promise<Response> {
288
320
  const timeoutMs = ctx.timeoutMs ?? 200_000;
289
321
  let probeToken: symbol | undefined;
290
322
  // One ordinal sequence for the whole call, across the throttle loop, the endpoint fallback
@@ -307,6 +339,10 @@ export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFe
307
339
 
308
340
  const response = await fetchKiroAttempt(request, ctx, timeoutMs, notePhysicalSend);
309
341
  const throttle = await inspectKiroThrottle(response, ctx.abortSignal);
342
+ if (throttle && ctx.kiroPreferAccountFailover) {
343
+ releaseKiroThrottleProbe(probeToken);
344
+ return throttle.response;
345
+ }
310
346
  if (!throttle || !throttle.transient) {
311
347
  releaseKiroThrottleProbe(probeToken);
312
348
  const finalResponse = throttle?.response ?? response;
@@ -334,6 +370,10 @@ export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFe
334
370
  throw new Error("Kiro throttle retry loop exhausted without a response");
335
371
  } catch (error) {
336
372
  releaseKiroThrottleProbe(probeToken);
373
+ if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
374
+ if (error instanceof Error && error.name === "TimeoutError") {
375
+ return new Response("Kiro upstream gateway timeout", { status: 504 });
376
+ }
337
377
  throw error;
338
378
  }
339
379
  }
@@ -0,0 +1,45 @@
1
+ import type { OcxProviderConfig } from "../../types";
2
+
3
+ export function isDeepseekArtifactTarget(provider: OcxProviderConfig, name: string, namespace?: string): boolean {
4
+ if (name !== "Artifact" || namespace) return false;
5
+ try {
6
+ return new URL(provider.baseUrl).hostname === "api.deepseek.com";
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ const NAME_BAGS = new Set(["properties", "$defs", "definitions", "patternProperties", "dependentSchemas"]);
13
+ const SCHEMA_CHILDREN = new Set(["items", "additionalProperties", "additionalItems", "allOf", "oneOf", "not", "if", "then", "else", "contains", "propertyNames", "prefixItems", "unevaluatedItems", "unevaluatedProperties"]);
14
+
15
+ /** DeepSeek rejects Artifact regex/union constraints. Relax them only on this built-in tool.
16
+ * Walk schema positions, preserving property names and literal defaults/examples verbatim.
17
+ * An explicit stack avoids recursion on caller-controlled schema nesting.
18
+ */
19
+ export function relaxDeepseekArtifactSchema(schema: unknown): unknown {
20
+ let result: unknown;
21
+ const pending: Array<{ value: unknown; names?: boolean; assign: (value: unknown) => void }> = [
22
+ { value: schema, assign: value => { result = value; } },
23
+ ];
24
+ while (pending.length) {
25
+ const { value, names, assign } = pending.pop()!;
26
+ if (!value || typeof value !== "object") { assign(value); continue; }
27
+ if (Array.isArray(value)) {
28
+ const out: unknown[] = new Array(value.length);
29
+ assign(out);
30
+ value.forEach((child, index) => pending.push({ value: child, assign: next => { out[index] = next; } }));
31
+ continue;
32
+ }
33
+ const out: Record<string, unknown> = Object.create(null);
34
+ assign(out);
35
+ for (const [key, child] of Object.entries(value)) {
36
+ if (!names && (key === "pattern" || key === "anyOf")) continue;
37
+ if (names || NAME_BAGS.has(key) || SCHEMA_CHILDREN.has(key)) {
38
+ pending.push({ value: child, names: !names && NAME_BAGS.has(key), assign: next => { out[key] = next; } });
39
+ } else {
40
+ out[key] = child;
41
+ }
42
+ }
43
+ }
44
+ return result;
45
+ }
@@ -7,6 +7,7 @@ import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../
7
7
  import { identifyRoutedModel } from "../identity";
8
8
  import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "../tool-catalog-nudge";
9
9
  import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
10
+ import type { ClaudeThinkingProjection } from "../../lib/claude-request-projection";
10
11
  import { inlineDocumentDataUrl } from "../../responses/inline-document";
11
12
  import type { OcxAssistantMessage, OcxContentPart, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall } from "../../types";
12
13
  import { modelInList, namespacedToolName } from "../../types";
@@ -61,10 +62,36 @@ export function toolResultImageChatParts(content: string | OcxContentPart[]): un
61
62
  return parts;
62
63
  }
63
64
 
65
+ /**
66
+ * Whether this wire serializes a replayed thinking block for the given model.
67
+ *
68
+ * The single source of truth for that question on the Chat wire: the assistant branch below uses
69
+ * it to decide `reasoning_content`, and the Messages ingress uses it to decide how much of a
70
+ * prompt is worth counting. Two copies of this rule would drift, and the count would then
71
+ * describe a body this adapter does not send.
72
+ */
73
+ export function openAIChatSerializesThinking(
74
+ provider: OcxProviderConfig,
75
+ modelId: string,
76
+ ): ClaudeThinkingProjection {
77
+ return {
78
+ text: modelInList(provider.preserveReasoningContentModels, modelId),
79
+ // The wire has no `signature` field — base64 replay tokens are Anthropic-wire-only.
80
+ signature: false,
81
+ // A `redacted_thinking` block is opaque provider data with no Chat representation: the
82
+ // assistant branch below reads only `type: "thinking"` parts, so the encrypted form is
83
+ // dropped whatever the preserve list says.
84
+ redacted: false,
85
+ };
86
+ }
87
+
64
88
  export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
65
89
  const out: unknown[] = [];
66
90
  const { context, options } = parsed;
67
91
  const replayCacheScope = parsed._reasoningReplayScope;
92
+ // One question, one answer for the whole conversion: does this wire carry replayed thinking
93
+ // back for this model? Asked once so a long history cannot pay for it per message.
94
+ const wireSerializesThinking = openAIChatSerializesThinking(provider, parsed.modelId).text;
68
95
 
69
96
  interface PendingToolCall { id: string; name: string }
70
97
  let pendingToolCalls: PendingToolCall[] = [];
@@ -225,7 +252,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
225
252
  if (
226
253
  reasoningContent.length === 0
227
254
  && (toolCalls.length > 0 || thinkingParts.length > 0)
228
- && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
255
+ && wireSerializesThinking
229
256
  ) {
230
257
  const cached = toolCalls
231
258
  .map(tc => (tc.id ? peekReasoningForCall(tc.id, replayCacheScope) : undefined))
@@ -248,7 +275,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
248
275
  reasoningContent = " ";
249
276
  }
250
277
  }
251
- if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
278
+ if (reasoningContent.length > 0 && wireSerializesThinking) {
252
279
  // MiniMax's interleaved-thinking contract requires the structured
253
280
  // reasoning_details array back on the next turn; a reasoning_content
254
281
  // string is the native-format pass-back the docs mark unsupported.
@@ -302,7 +329,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
302
329
  flushPendingToolCalls();
303
330
  const name = safeToolName(msg.toolName);
304
331
  const cachedReasoning =
305
- toolCallId && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
332
+ toolCallId && wireSerializesThinking
306
333
  ? peekReasoningForCall(toolCallId, replayCacheScope)
307
334
  : undefined;
308
335
  // Same fallback as the main-assistant path: never emit a bare orphan
@@ -316,7 +343,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
316
343
  // falsy hit as a miss so the placeholder still fires.
317
344
  const orphanReasoning =
318
345
  cachedReasoning
319
- || (modelInList(provider.preserveReasoningContentModels, parsed.modelId)
346
+ || (wireSerializesThinking
320
347
  && modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)
321
348
  ? " "
322
349
  : undefined);
@@ -46,6 +46,7 @@ export interface StructuredToolCallReference {
46
46
  names: ReadonlySet<string>;
47
47
  argumentsText: string;
48
48
  freeformTool?: FreeformToolIdentity;
49
+ recoveredParameterWrapper?: boolean;
49
50
  }
50
51
 
51
52
  const BLOCK_HEADER = /<tool_call>\s*<function=([^>\r\n]+)>/y;
@@ -222,6 +223,7 @@ function deferredContinuationBreak(text: string, context: TextContext): RegExp |
222
223
  * original order.
223
224
  */
224
225
  export class SerializedToolCallContentBuffer {
226
+ releasedAnswerText = false;
225
227
  private text = "";
226
228
  private bytes = 0;
227
229
  private hasOpenTag = false;
@@ -235,6 +237,12 @@ export class SerializedToolCallContentBuffer {
235
237
 
236
238
  constructor(private readonly budget: TranslatorBudget) {}
237
239
 
240
+ /** Remembers visible text released before a later structured call can be reconciled. */
241
+ private observeText(events: AdapterEvent[]): AdapterEvent[] {
242
+ if (events.some(event => event.type === "text_delta" && event.text.trim() !== "")) this.releasedAnswerText = true;
243
+ return events;
244
+ }
245
+
238
246
  /** Measures an append exactly even when the runtime prices isolated UTF-16 surrogates differently. */
239
247
  private appendedByteLength(delta: string): number {
240
248
  let bytes = Buffer.byteLength(delta);
@@ -324,16 +332,16 @@ export class SerializedToolCallContentBuffer {
324
332
  // A delta that alone passes the bound is delivered as text rather than retained.
325
333
  if (Buffer.byteLength(delta) > MAX_HELD_BYTES) {
326
334
  this.context = contextAfter(delta, this.context);
327
- return [...released, ...textEvents(delta)];
335
+ return this.observeText([...released, ...textEvents(delta)]);
328
336
  }
329
- return [...released, ...textEvents(this.ingest(delta))];
337
+ return this.observeText([...released, ...textEvents(this.ingest(delta))]);
330
338
  }
331
339
  const text = this.ingest(delta);
332
340
  // Checked after ingest too: one delta can open a block and already carry more than a bound.
333
341
  if (this.hasOpenTag && (this.bytes > MAX_HELD_BYTES || (this.sawCloser && !this.openAfterCloser && this.trailingProseChars > MAX_TRAILING_CHARS))) {
334
- return [...textEvents(text), ...this.drain([])];
342
+ return this.observeText([...textEvents(text), ...this.drain([])]);
335
343
  }
336
- return textEvents(text);
344
+ return this.observeText(textEvents(text));
337
345
  }
338
346
 
339
347
  /** Exposes held text as evidence for narrowly repairing duplicated argument prefixes. */
@@ -386,7 +394,7 @@ export class SerializedToolCallContentBuffer {
386
394
  this.context = contextAfter(this.text, this.context);
387
395
  this.queued = [];
388
396
  this.replace("", false);
389
- return out;
397
+ return this.observeText(out);
390
398
  }
391
399
 
392
400
  /** Text-only drain for callers that never queued an event. */
@@ -475,7 +483,10 @@ function duplicatedSerializedToolCallRanges(
475
483
  const body = freeformBody(call.body);
476
484
  return structuredCalls.some(structured => {
477
485
  const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined;
478
- return input !== undefined && freeformBody(input) === body;
486
+ if (input === undefined) return false;
487
+ const normalized = freeformBody(input);
488
+ return normalized === body || (structured.recoveredParameterWrapper === true
489
+ && body.startsWith("<parameter=") && normalized === freeformBody(body.slice("<parameter=".length)));
479
490
  });
480
491
  });
481
492
  }
@@ -578,14 +589,48 @@ export interface StructuredToolCallInput {
578
589
  export function reconcileStructuredToolCalls(
579
590
  calls: readonly StructuredToolCallInput[],
580
591
  serializedText: string,
592
+ recoverEmptyMiMoFreeformInput = false,
581
593
  ): StructuredToolCallReference[] {
582
- const references = calls.map(call => {
594
+ const references: StructuredToolCallReference[] = calls.map(call => {
583
595
  const names = new Set([call.wireName, call.restoredName]);
584
596
  return { names, freeformTool: call.freeformTool, argumentsText: repairArgumentsDuplicatedBesideSerializedCall(call.argumentsText, names, serializedText) };
585
597
  });
598
+ if (recoverEmptyMiMoFreeformInput && calls.length === 1 && calls[0]!.freeformTool
599
+ && emptyObjectArguments(references[0]!.argumentsText)) {
600
+ // A standalone MiMo block and one empty call to the exact wire tool provide the only
601
+ // unambiguous recovery shape. Prose before/after it or another call keeps text inert.
602
+ const blocks = callsIn(serializedText);
603
+ if (blocks.length === 1 && serializedText.slice(0, blocks[0]!.start).trim() === ""
604
+ && serializedText.slice(blocks[0]!.end).trim() === "") {
605
+ const block = blocks[0]!;
606
+ const wrappedBody = freeformBody(block.body);
607
+ // MiMo can put the freeform input after a malformed `<parameter=` opener.
608
+ const hasParameterOpener = wrappedBody.startsWith("<parameter=");
609
+ const body = hasParameterOpener
610
+ ? freeformBody(wrappedBody.slice("<parameter=".length)) : wrappedBody;
611
+ if (block.name === calls[0]!.wireName && body.trim() !== ""
612
+ && !/(?:^|\n)<tool_call>\s*<function=[^>\r\n]+>/.test(block.body)
613
+ && !(hasParameterOpener && /^<parameter=[A-Za-z_$][\w$.-]*>/.test(wrappedBody))
614
+ && !body.includes("<parameter=") && !body.includes("</parameter>")) {
615
+ references[0] = { ...references[0]!, argumentsText: JSON.stringify({ input: body }),
616
+ recoveredParameterWrapper: hasParameterOpener };
617
+ }
618
+ }
619
+ }
586
620
  return reduceUnambiguousDoubledInput(references, serializedText);
587
621
  }
588
622
 
623
+ /** An empty JSON object means the structured freeform call supplied no input. */
624
+ function emptyObjectArguments(text: string): boolean {
625
+ try {
626
+ const parsed: unknown = JSON.parse(text);
627
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
628
+ && Object.keys(parsed).length === 0;
629
+ } catch {
630
+ return false;
631
+ }
632
+ }
633
+
589
634
  /**
590
635
  * Applies the doubled-input reduction across the batch. The doubled shape is valid JSON, so the
591
636
  * prefix repair returns it untouched, and the reduction only ever rewrites a call the repair left
@@ -0,0 +1,65 @@
1
+ import { isConformingToolCallId, MAX_TOOL_CALL_ID_LENGTH } from "../tool-call-id";
2
+ import type { OcxMessage } from "../../types";
3
+
4
+ /**
5
+ * Remint a tool-call id the client's history already carries.
6
+ *
7
+ * Some upstreams mint tool-call ids deterministically per RESPONSE rather than per call: the same
8
+ * `call-0-0` on every turn of a conversation. Anthropic's wire requires `tool_use.id` to identify
9
+ * one call, and a client that has already stored that id for an earlier call cannot pair the new
10
+ * result to the new call — it drops one of them, the turn becomes an assistant message whose
11
+ * tool call has no result, and the model re-issues the same call forever.
12
+ *
13
+ * The first occurrence of an id is emitted byte-identical, so prompt-cache keys, reasoning-replay
14
+ * lookups, and every upstream that already mints unique ids stay untouched. Only a repeat — against
15
+ * the history the caller seeded, or against a call already emitted in this response — is rewritten,
16
+ * to the smallest unused suffix that still fits Anthropic's id bound.
17
+ */
18
+ export function createToolCallIdReminter(reservedIds: Iterable<string>): (rawId: string) => string {
19
+ const occupied = new Set(reservedIds);
20
+ return rawId => {
21
+ if (!occupied.has(rawId)) {
22
+ occupied.add(rawId);
23
+ return rawId;
24
+ }
25
+ // A non-conforming source is sanitized, never dropped: the wire still needs an id, and the
26
+ // occupied check below covers a sanitized form that now equals some other call's id.
27
+ const base = isConformingToolCallId(rawId) ? rawId : rawId.replace(/[^a-zA-Z0-9_-]/g, "_");
28
+ for (let n = 2; ; n++) {
29
+ // Hyphen, not underscore: an id that extends another id as `<earlier>_<digits>` is parsed by
30
+ // at least one client as a batch sub-call of `<earlier>`, which pairs the second call's
31
+ // result to the first call. A `-<n>` suffix is in the same id family without that reading.
32
+ const suffix = `-${n}`;
33
+ const candidate = base.slice(0, Math.max(1, MAX_TOOL_CALL_ID_LENGTH - suffix.length)) + suffix;
34
+ if (!occupied.has(candidate)) {
35
+ occupied.add(candidate);
36
+ return candidate;
37
+ }
38
+ }
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Tool-call ids the client's own history has already fixed: every assistant tool call it kept, plus
44
+ * every tool result that answered one. A response repeating any of them is the collision this
45
+ * module exists for.
46
+ *
47
+ * Read from the client's history rather than from earlier responses because the client is the
48
+ * authority on uniqueness here: it is the side that drops duplicates, so the proxy cannot observe
49
+ * the ids it discarded — a dropped id only ever exists as the absence it caused. A Messages client
50
+ * sends the whole conversation on every turn, which is why one turn's history is a complete picture
51
+ * of the ids that may not be reused.
52
+ */
53
+ export function reservedToolCallIdsFromHistory(messages: readonly OcxMessage[]): Set<string> {
54
+ const ids = new Set<string>();
55
+ for (const message of messages) {
56
+ if (message.role === "assistant") {
57
+ for (const part of message.content) {
58
+ if (part.type === "toolCall") ids.add(part.id);
59
+ }
60
+ continue;
61
+ }
62
+ if (message.role === "toolResult") ids.add(message.toolCallId);
63
+ }
64
+ return ids;
65
+ }
@@ -1,3 +1,4 @@
1
+ import { isDeepseekArtifactTarget, relaxDeepseekArtifactSchema } from "./deepseek-artifact-schema";
1
2
  import { isNativeOpenAIChatTarget } from "./wire";
2
3
  import { createOpenAIChatToolNameRegistry, type OpenAIChatToolNameRegistry } from "./tool-name-registry";
3
4
  import { isXaiSchemaTarget, lookupLocalJsonPointer, normalizeXaiToolParameters } from "../xai-tool-schema";
@@ -548,7 +549,10 @@ export function toolsToChatFormat(
548
549
  : moonshotTarget
549
550
  ? normalizeMoonshotToolParameters(t.parameters, moonshotInlineByteBudget)
550
551
  : ensureRootObjectType(t.parameters);
551
- const parameters = stripUnicodePropertyPatterns(stripResponsesOnlyEncryptedMarker(normalized));
552
+ const deepseekArtifact = isDeepseekArtifactTarget(provider, t.name, t.namespace);
553
+ const parameters = stripUnicodePropertyPatterns(stripResponsesOnlyEncryptedMarker(
554
+ deepseekArtifact ? relaxDeepseekArtifactSchema(normalized) : normalized,
555
+ ));
552
556
 
553
557
  if (parameters === undefined) return [];
554
558
  return [{
@@ -557,7 +561,7 @@ export function toolsToChatFormat(
557
561
  name: registry.alias(t),
558
562
  ...(t.description ? { description: t.description } : {}),
559
563
  parameters,
560
- ...(t.strict !== undefined ? { strict: t.strict } : {}),
564
+ ...(t.strict !== undefined && !deepseekArtifact ? { strict: t.strict } : {}),
561
565
  },
562
566
  }];
563
567
  });
@@ -340,7 +340,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
340
340
  }
341
341
  }
342
342
  // Held markup is released only now, as one batch per response: the doubled-input repair needs every call.
343
- const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current());
343
+ const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current(), !toolCallContent.releasedAnswerText && /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? ""));
344
344
  calls.forEach((call, index) => { call.args = references[index]!.argumentsText; });
345
345
  yield* toolCallContent.drain(references);
346
346
  for (const call of calls) {
@@ -803,7 +803,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
803
803
  events.push({ type: "tool_call_start", id, name: toolNames.restore(name) }, delta, { type: "tool_call_end" });
804
804
  }
805
805
  }
806
- const references = reconcileStructuredToolCalls(structuredCalls, answerText);
806
+ const references = reconcileStructuredToolCalls(structuredCalls, answerText, /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? ""));
807
807
  structuredCalls.forEach((call, index) => { call.delta.arguments = references[index]!.argumentsText; });
808
808
  reconcileSerializedToolCallEvents(events, contentStart, contentEnd, references, budget);
809
809
  const stopReason = stopReasonFor(choice.finish_reason);
@@ -0,0 +1,31 @@
1
+ import { isPlainObject } from "./internal";
2
+
3
+ export class MuseToolChoiceCompatibilityError extends Error {
4
+ constructor() {
5
+ super("This tool_choice cannot be preserved for Meta Responses; use auto or none.");
6
+ this.name = "MuseToolChoiceCompatibilityError";
7
+ }
8
+ }
9
+
10
+ function selectorKind(choice: unknown): "auto" | "none" {
11
+ if (choice === undefined || choice === "auto") return "auto";
12
+ if (choice === "none") return "none";
13
+ throw new MuseToolChoiceCompatibilityError();
14
+ }
15
+
16
+ export function normalizeMuseToolChoice(body: unknown, originalChoice: unknown): unknown {
17
+ const originalKind = selectorKind(originalChoice);
18
+ const effectiveKind = selectorKind(isPlainObject(body) ? body.tool_choice : undefined);
19
+ if (originalKind !== "none" && effectiveKind !== "none") return body;
20
+ if (!isPlainObject(body)) return body;
21
+
22
+ const input = Array.isArray(body.input)
23
+ ? body.input.filter(item => !isPlainObject(item) || item.type !== "additional_tools")
24
+ : body.input;
25
+ const {
26
+ tool_choice: _toolChoice,
27
+ parallel_tool_calls: _parallelToolCalls,
28
+ ...rest
29
+ } = body;
30
+ return { ...rest, tools: [], ...(input !== body.input ? { input } : {}) };
31
+ }
@@ -44,6 +44,7 @@ import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuation
44
44
  import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
45
45
  import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
46
46
  import { observeOutbound } from "../../usage/cache-diagnostic";
47
+ import { normalizeMuseToolChoice } from "./muse-tool-choice";
47
48
 
48
49
  /**
49
50
  * Identifies DeepSeek's strict Responses replay contract: tool-bearing continuations need
@@ -411,8 +412,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
411
412
  outBody = rewritten.body;
412
413
  convertedRoutedNamespaceToolAliases = rewritten.aliases;
413
414
  // Preserve xAI's cached-only fail-closed semantics and image-search mapping before the
414
- // generic capability fallback removes the private OpenAI fields.
415
- outBody = normalizeXaiResponsesWebSearch(outBody, provider);
415
+ // generic capability fallback removes the private OpenAI fields. Grok relayed through
416
+ // OpenCode Go speaks the same dialect, which the final model id and URL identify.
417
+ outBody = normalizeXaiResponsesWebSearch(outBody, provider, { modelId: parsed.modelId, responseUrl: url });
416
418
  outBody = injectXaiResponsesXSearch(outBody, provider, parsed._replayPrefixLen);
417
419
  // xAI and explicitly classified compatible gateways reject these OpenAI web_search
418
420
  // extensions. Keep them for OpenAI API-key traffic and unclassified gateways.
@@ -494,7 +496,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
494
496
  parsed.modelId,
495
497
  );
496
498
  // Normalize the wire model before deriving model-dependent transport metadata.
497
- const finalBody =
499
+ let finalBody =
498
500
  provider.modelSuffixBracketStrip
499
501
  && unnormalizedBody !== null
500
502
  && typeof unnormalizedBody === "object"
@@ -502,6 +504,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
502
504
  && typeof (unnormalizedBody as { model?: unknown }).model === "string"
503
505
  ? { ...(unnormalizedBody as Record<string, unknown>), model: stripBracketedModelSuffix((unnormalizedBody as { model: string }).model) }
504
506
  : unnormalizedBody;
507
+ if (isMetaAiResponsesDestination(url)) {
508
+ const originalChoice = isPlainObject(parsed._rawBody) ? parsed._rawBody.tool_choice : undefined;
509
+ finalBody = normalizeMuseToolChoice(finalBody, originalChoice);
510
+ }
505
511
  if (isCanonicalOpenAiForwardProvider(provider)) {
506
512
  const routingHeaders = new Headers(headers);
507
513
  applyCodexRoutingHint(routingHeaders, finalBody);
@@ -2,22 +2,31 @@ function isRecord(value: unknown): value is Record<string, unknown> {
2
2
  return !!value && typeof value === "object" && !Array.isArray(value);
3
3
  }
4
4
 
5
+ /**
6
+ * True only for the exact OpenCode Go Responses resource. Lookalike hosts, other Zen paths,
7
+ * credentials, queries, fragments and unparsable URLs are all excluded, so a custom provider
8
+ * that merely resembles Go never inherits a Go-specific body rewrite.
9
+ */
10
+ export function isOpenCodeGoResponsesUrl(responseUrl: string): boolean {
11
+ let destination: URL;
12
+ try {
13
+ destination = new URL(responseUrl);
14
+ } catch {
15
+ return false;
16
+ }
17
+ return destination.origin === "https://opencode.ai"
18
+ && destination.pathname === "/zen/go/v1/responses"
19
+ && !destination.username && !destination.password
20
+ && !destination.href.includes("?") && !destination.href.includes("#");
21
+ }
22
+
5
23
  /** Console Go accepts public tools but rejects the private additional_tools input wrapper. */
6
24
  export function normalizeOpenCodeGoAdditionalTools(
7
25
  body: unknown,
8
26
  responseUrl: string,
9
27
  replayPrefixLength = 0,
10
28
  ): unknown {
11
- let destination: URL;
12
- try {
13
- destination = new URL(responseUrl);
14
- } catch {
15
- return body;
16
- }
17
- if (destination.origin !== "https://opencode.ai"
18
- || destination.pathname !== "/zen/go/v1/responses"
19
- || destination.username || destination.password
20
- || destination.href.includes("?") || destination.href.includes("#")) return body;
29
+ if (!isOpenCodeGoResponsesUrl(responseUrl)) return body;
21
30
  if (!isRecord(body) || !Array.isArray(body.input)) return body;
22
31
  // Do not replace a malformed top-level catalog with a partial promoted one.
23
32
  if (body.tools !== undefined && !Array.isArray(body.tools)) return body;