mixdog 1.2.1 → 1.2.2

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 (233) hide show
  1. package/package.json +1 -1
  2. package/scripts/gate-local.mjs +19 -3
  3. package/scripts/model-catalog-audit.mjs +1 -15
  4. package/src/agents/reviewer/AGENT.md +3 -3
  5. package/src/agents/reviewer/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +1 -0
  7. package/src/repl/slash-commands.mjs +16 -1
  8. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  10. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
  11. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
  12. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
  14. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  15. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  16. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  17. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  18. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  19. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  20. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  21. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
  23. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  25. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  27. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  29. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  38. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  43. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  45. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  47. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  48. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  50. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  52. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  54. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  55. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  56. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
  57. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  59. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  60. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  65. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  66. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  67. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  71. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  72. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
  89. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  90. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  92. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  93. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  96. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  97. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  98. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  99. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  100. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  101. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  103. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  104. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  105. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  106. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  107. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  108. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
  109. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  110. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  111. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  113. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  114. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  115. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  116. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  117. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  118. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  119. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
  120. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  121. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  123. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  124. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
  125. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
  126. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  130. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  131. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  132. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  133. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  134. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  140. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  141. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  142. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  143. package/src/runtime/office/README.md +1 -1
  144. package/src/runtime/office/shared/values.mjs +0 -5
  145. package/src/runtime/shared/background-tasks.mjs +92 -14
  146. package/src/runtime/shared/err-text.mjs +25 -0
  147. package/src/runtime/shared/error-presentation.mjs +159 -19
  148. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  149. package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
  150. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  151. package/src/runtime/shared/llm/model-catalog.mjs +53 -40
  152. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  153. package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
  154. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  155. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  156. package/src/runtime/shared/model-display.mjs +13 -0
  157. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  158. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  159. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  160. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  161. package/src/runtime/shared/turn-changes.mjs +447 -219
  162. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  163. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  164. package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
  165. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  166. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  167. package/src/session-runtime/boot/apis.mjs +6 -0
  168. package/src/session-runtime/boot/infrastructure.mjs +6 -0
  169. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  170. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  171. package/src/session-runtime/mcp-glue/connect.mjs +29 -9
  172. package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
  173. package/src/session-runtime/mcp-glue.mjs +3 -0
  174. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  175. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  176. package/src/session-runtime/model-route/set-route.mjs +83 -15
  177. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  178. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  179. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  180. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  181. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  182. package/src/session-runtime/resource-extension-sync.mjs +4 -0
  183. package/src/session-runtime/resource-mcp-api.mjs +98 -2
  184. package/src/session-runtime/runtime-review-api.mjs +13 -0
  185. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  186. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  187. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  188. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  189. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  190. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  191. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  192. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  193. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  194. package/src/standalone/daemon-drain-policy.mjs +8 -0
  195. package/src/standalone/daemon-telemetry.mjs +55 -0
  196. package/src/standalone/daemon.mjs +3 -7
  197. package/src/standalone/session-protocol.mjs +7 -0
  198. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  199. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  200. package/src/standalone/session-service/agent-tree.mjs +12 -0
  201. package/src/standalone/session-service/quota-resume.mjs +8 -0
  202. package/src/standalone/session-service/retention/busy.mjs +15 -1
  203. package/src/standalone/session-service-api.mjs +5 -0
  204. package/src/standalone/session-service.mjs +1 -0
  205. package/src/tui/app/app-view.jsx +1 -0
  206. package/src/tui/app/create-app-pickers.mjs +4 -0
  207. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  208. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  209. package/src/tui/app/extension-pickers.mjs +2 -1
  210. package/src/tui/app/model-options.mjs +11 -16
  211. package/src/tui/app/slash-commands.mjs +7 -2
  212. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  213. package/src/tui/components/StatusLine.jsx +2 -0
  214. package/src/tui/components/status-line/local-line.mjs +5 -2
  215. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  216. package/src/tui/dist/index.mjs +816 -43
  217. package/src/tui/session/quota-resume.mjs +46 -14
  218. package/src/tui/session/session-api/commands.mjs +33 -24
  219. package/src/tui/session/session-api/extensions.mjs +22 -0
  220. package/src/tui/session/session-api/integrations.mjs +7 -0
  221. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  222. package/src/tui/session/session-api/routes.mjs +71 -7
  223. package/src/tui/session/session-api/settings.mjs +5 -14
  224. package/src/tui/session/session-flow/drain.mjs +7 -1
  225. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  226. package/src/tui/session/turn.mjs +38 -15
  227. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  228. package/src/ui/statusline-agents.mjs +15 -4
  229. package/src/ui/statusline.mjs +7 -3
  230. package/src/ui/streaming-markdown-heal.mjs +34 -3
  231. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  232. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  233. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -2,6 +2,7 @@
2
2
  // wire event type maps to one handler over the shared stream `state`; `ctx`
3
3
  // carries the label, the caller callbacks and the leaked-text relay.
4
4
  import { typedStatusFrom } from './retry-classifier.mjs';
5
+ import { applyCompatErrorContract } from './openai-compat-error-contract.mjs';
5
6
  import { customToolCallFromResponseItem, nativeToolSearchCallFromArguments } from './custom-tool-wire.mjs';
6
7
  import { emitCompatToolCallOnce } from './openai-compat-stream-common.mjs';
7
8
  import { truncatedCompatStreamError, parseCompletedToolCallArgumentsJson } from './lib/openai-tool-args.mjs';
@@ -41,9 +42,11 @@ function typedResponsesFailure(message, event) {
41
42
  if (code != null && code !== '') err.providerErrorCode = String(code);
42
43
  if (detail) err.providerError = detail;
43
44
  err.providerWireError = true;
44
- return err;
45
+ return applyCompatErrorContract(err, detail || (event?.code != null ? { code: event.code } : null));
45
46
  }
46
47
 
48
+ const streamLabel = (ctx) => ctx?.label || 'compat Responses';
49
+
47
50
  // Reconcile a COMPLETED function_call item into state.toolCalls: fill in the
48
51
  // id/name a streamed call may still be missing, or adopt the item as a new
49
52
  // call. `response.output_item.done` and the `response.completed` sweep see the
@@ -289,6 +292,7 @@ function onCompleted(event, state, ctx) {
289
292
  }
290
293
 
291
294
  function onDone(event, state, ctx) {
295
+ const label = streamLabel(ctx);
292
296
  if (!event.response || event.response.status === 'completed') {
293
297
  state.completed = true;
294
298
  // A stream that never emits response.completed: this frame carries usage.
@@ -298,7 +302,7 @@ function onDone(event, state, ctx) {
298
302
  if (event.response.status === 'failed') {
299
303
  const msg = event.response?.error?.message || 'response.done failed';
300
304
  throw withIncompleteUsage(
301
- typedResponsesFailure(`xAI Responses stream response.done failed: ${msg}`, event),
305
+ typedResponsesFailure(`${label} stream response.done failed: ${msg}`, event),
302
306
  event,
303
307
  state
304
308
  );
@@ -306,27 +310,31 @@ function onDone(event, state, ctx) {
306
310
  if (event.response.status === 'incomplete') {
307
311
  const reason = incompleteReasonFromEvent(event);
308
312
  if (isMaxOutputIncompleteReason(reason)) return settleMaxOutputIncomplete(event, state, ctx, reason);
309
- throw withIncompleteUsage(new Error(`xAI Responses stream response.done incomplete: ${reason}`), event, state);
313
+ throw withIncompleteUsage(new Error(`${label} stream response.done incomplete: ${reason}`), event, state);
310
314
  }
311
315
  }
312
316
 
313
- function onFailed(event, state) {
317
+ function onFailed(event, state, ctx) {
314
318
  const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
315
319
  // The wire event's OWN typed status/code is preserved verbatim. A
316
320
  // forbidden/unknown failure is never coerced into a synthetic 500:
317
321
  // without typed evidence it stays unclassified and is surfaced.
318
- throw withIncompleteUsage(typedResponsesFailure(`xAI Responses stream response.failed: ${msg}`, event), event, state);
322
+ throw withIncompleteUsage(
323
+ typedResponsesFailure(`${streamLabel(ctx)} stream response.failed: ${msg}`, event),
324
+ event,
325
+ state
326
+ );
319
327
  }
320
328
 
321
329
  function onIncomplete(event, state, ctx) {
322
330
  const reason = incompleteReasonFromEvent(event);
323
331
  if (isMaxOutputIncompleteReason(reason)) return settleMaxOutputIncomplete(event, state, ctx, reason);
324
- throw withIncompleteUsage(new Error(`xAI Responses stream response.incomplete: ${reason}`), event, state);
332
+ throw withIncompleteUsage(new Error(`${streamLabel(ctx)} stream response.incomplete: ${reason}`), event, state);
325
333
  }
326
334
 
327
- function onError(event) {
335
+ function onError(event, _state, ctx) {
328
336
  const msg = event.message || event.error?.message || 'unknown';
329
- throw typedResponsesFailure(`xAI Responses stream error: ${msg}`, event);
337
+ throw typedResponsesFailure(`${streamLabel(ctx)} stream error: ${msg}`, event);
330
338
  }
331
339
 
332
340
  const RESPONSES_EVENT_HANDLERS = new Map([
@@ -139,11 +139,11 @@ export function createResponsesOutcomeStamper(state, leakedCalls) {
139
139
  }
140
140
 
141
141
  /** A streamed function_call whose id/name never arrived cannot be dispatched. */
142
- export function unresolvedSalvageError(state) {
142
+ export function unresolvedSalvageError(state, label) {
143
143
  const unresolved = state.toolCalls.find((t) => t._pendingItemId);
144
144
  if (!unresolved) return null;
145
145
  return new Error(
146
- `xAI Responses stream function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`
146
+ `${label || 'compat Responses'} stream function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`
147
147
  );
148
148
  }
149
149
 
@@ -169,5 +169,6 @@ export function finalizeResponsesStream(state, { leakedCalls, parseResponsesTool
169
169
  model: state.model || response.model || null,
170
170
  responseId: state.responseId || response.id || null,
171
171
  stopReason: state.stopReason || null,
172
+ reasoningEmitted: state.emittedReasoning === true,
172
173
  };
173
174
  }
@@ -7,6 +7,7 @@
7
7
  // turn tool loops keep the model's reasoning chain without server storage.
8
8
 
9
9
  import { markProviderRecoveryExhausted, withRetry } from './retry-classifier.mjs';
10
+ import { compatRetryOptions, withCompatRetryProfile } from './openai-compat-retry-profile.mjs';
10
11
  import { consumeCompatResponsesStream } from './openai-compat-stream.mjs';
11
12
  import { getModelMetadataSync } from './model-catalog.mjs';
12
13
  import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, createPassthroughSignal } from '../stall-policy.mjs';
@@ -24,6 +25,8 @@ import {
24
25
  import { preparePdfTextForProvider } from './media-normalization.mjs';
25
26
  import { normalizeOpencodeGoReasoningEffort } from './openai-compat-xai.mjs';
26
27
  import { isCustomProviderName, resolveCustomReasoning } from './custom-reasoning.mjs';
28
+ import { projectEffortConfiguration, withEffortConfigurationFallback } from './effort-configuration.mjs';
29
+ import { normalizeCacheKey } from '../agent-runtime/cache-strategy.mjs';
27
30
  import { createProviderReplay } from './lib/provider-replay.mjs';
28
31
  import {
29
32
  applyCompatToolChoice,
@@ -53,9 +56,65 @@ function resolveReasoning(provider, useModel, opts) {
53
56
  return reasoningEffort ? { effort: reasoningEffort } : null;
54
57
  }
55
58
 
59
+ /**
60
+ * A custom route's mid-conversation effort projection (GPT-6+ on this wire)
61
+ * with every effort in the row's wire value, or null for the per-request
62
+ * effort. Consecutive efforts sharing one wire value collapse into none; an
63
+ * effort the row cannot serve falls back to the per-request effort.
64
+ */
65
+ function customEffortProjection(provider, useModel, messages, opts) {
66
+ if (!isCustomProviderName(provider.name)) return null;
67
+ const projection = projectEffortConfiguration(messages, provider.name, useModel, {
68
+ ...opts,
69
+ protocol: 'openai-responses',
70
+ });
71
+ if (!projection) return null;
72
+ const model = provider.getCachedModelInfo?.(useModel) || null;
73
+ const wire = (effort) =>
74
+ resolveCustomReasoning({ protocol: 'openai-responses', model, effort }).fields.reasoning?.effort || null;
75
+ const initialEffort = wire(projection.initialEffort);
76
+ if (!initialEffort) return null;
77
+ const updates = new Map();
78
+ let current = initialEffort;
79
+ for (const [message, effort] of projection.updates) {
80
+ const value = wire(effort);
81
+ if (!value) return null;
82
+ if (value !== current) {
83
+ current = value;
84
+ updates.set(message, value);
85
+ }
86
+ }
87
+ return { ...projection, initialEffort, effort: current, updates };
88
+ }
89
+
90
+ /**
91
+ * A custom route's session identity: its prompt_cache_key and session
92
+ * headers, '' without a session. The session wins over the provider-scoped
93
+ * cache key, so parallel sessions never evict each other; a one-shot role
94
+ * ('shared' scope, a fresh session per call) keys on the provider key instead.
95
+ * Never the effort, so an effort change keeps the cache routing.
96
+ */
97
+ function customSessionCacheKey(provider, opts) {
98
+ if (!isCustomProviderName(provider.name)) return '';
99
+ const explicit = opts.promptCacheKey || opts.providerCacheKey || opts.session?.promptCacheKey;
100
+ return normalizeCacheKey(
101
+ opts.promptCacheScope === 'shared' ? explicit || opts.sessionId : opts.sessionId || explicit
102
+ );
103
+ }
104
+
56
105
  /** The request body: lowered transcript plus the per-turn knobs (output cap,
57
- * tools and tool choice, reasoning effort). */
58
- function buildCompatResponsesParams({ provider, useModel, input, previousResponseId, tools, opts, instructions }) {
106
+ * tools and tool choice, reasoning effort, session cache key). */
107
+ function buildCompatResponsesParams({
108
+ provider,
109
+ useModel,
110
+ input,
111
+ previousResponseId,
112
+ tools,
113
+ opts,
114
+ instructions,
115
+ effortProjection,
116
+ sessionCacheKey,
117
+ }) {
59
118
  const params = {
60
119
  model: useModel,
61
120
  input,
@@ -71,14 +130,19 @@ function buildCompatResponsesParams({ provider, useModel, input, previousRespons
71
130
  params.parallel_tool_calls = true;
72
131
  }
73
132
  applyCompatToolChoice(params, opts);
74
- const reasoning = resolveReasoning(provider, useModel, opts);
133
+ // A projected conversation keeps its initial effort on top; changes ride as input items.
134
+ const reasoning = effortProjection
135
+ ? { effort: effortProjection.initialEffort }
136
+ : resolveReasoning(provider, useModel, opts);
75
137
  if (reasoning) params.reasoning = reasoning;
76
138
  // Opt-in system-as-instructions: set before the hook so the hook can override it.
77
139
  if (instructions) params.instructions = instructions;
140
+ if (sessionCacheKey) params.prompt_cache_key = sessionCacheKey;
78
141
  // Opt-in per-provider final shaping (a provider passes the hook in its config).
79
142
  const hook = provider.config?.responsesParamsHook;
80
143
  if (typeof hook === 'function') {
81
- const modelInfo = (typeof provider.getCachedModelInfo === 'function' && provider.getCachedModelInfo(useModel)) || null;
144
+ const modelInfo =
145
+ (typeof provider.getCachedModelInfo === 'function' && provider.getCachedModelInfo(useModel)) || null;
82
146
  hook(params, { useModel, opts, modelInfo });
83
147
  }
84
148
  return params;
@@ -90,16 +154,16 @@ function buildCompatResponsesParams({ provider, useModel, input, previousRespons
90
154
  * that re-opens the request. A content-idle stall is terminal for this
91
155
  * transport, so it is marked recovery-exhausted rather than retried.
92
156
  */
93
- async function streamCompatResponses({ provider, params, label, signal, opts }) {
157
+ async function streamCompatResponses({ provider, params, label, signal, opts, headers }) {
94
158
  const totalSignal = createPassthroughSignal(signal);
95
159
  try {
96
160
  return await withRetry(
97
- async ({ signal: attemptSignal }) => {
161
+ withCompatRetryProfile(provider.name, async ({ signal: attemptSignal }) => {
98
162
  const stream = await withRetry(
99
163
  ({ signal: openSignal }) =>
100
164
  provider.client.responses.create(params, {
101
165
  signal: openSignal,
102
- ...(opts.requestHeaders ? { headers: opts.requestHeaders } : {}),
166
+ ...(headers ? { headers } : {}),
103
167
  }),
104
168
  {
105
169
  signal: attemptSignal,
@@ -130,11 +194,12 @@ async function streamCompatResponses({ provider, params, label, signal, opts })
130
194
  }
131
195
  throw error;
132
196
  }
133
- },
197
+ }),
134
198
  {
135
199
  signal: totalSignal.signal,
136
200
  onRetry: compatStreamRetryReporter(label, opts),
137
201
  retry529: opts.retry529,
202
+ ...compatRetryOptions(provider.name),
138
203
  }
139
204
  );
140
205
  } finally {
@@ -145,9 +210,17 @@ async function streamCompatResponses({ provider, params, label, signal, opts })
145
210
  /**
146
211
  * Send one turn over `POST {baseURL}/responses` (streaming) using the
147
212
  * provider's existing OpenAI SDK client. Returns the same result shape as
148
- * the chat/completions path so the agent loop stays wire-agnostic.
213
+ * the chat/completions path so the agent loop stays wire-agnostic. A custom
214
+ * route whose model rejects the mid-conversation effort update is replayed
215
+ * once with the per-request effort.
149
216
  */
150
- export async function sendCompatResponses(provider, messages, useModel, tools, opts = {}) {
217
+ export function sendCompatResponses(provider, messages, useModel, tools, opts = {}) {
218
+ const send = (sendOpts) => sendCompatResponsesOnce(provider, messages, useModel, tools, sendOpts);
219
+ if (!isCustomProviderName(provider.name)) return send(opts);
220
+ return withEffortConfigurationFallback(provider.name, useModel, opts, send);
221
+ }
222
+
223
+ async function sendCompatResponsesOnce(provider, messages, useModel, tools, opts) {
151
224
  const replayProvider = compatResponsesReplayProvider(provider.name);
152
225
  const signal = opts.signal || null;
153
226
  if (signal?.aborted) {
@@ -156,7 +229,10 @@ export async function sendCompatResponses(provider, messages, useModel, tools, o
156
229
  }
157
230
  const label = `${provider.name}:responses`;
158
231
  messages = await preparePdfTextForProvider(messages, provider);
232
+ const effortProjection = customEffortProjection(provider, useModel, messages, opts);
233
+ const sessionCacheKey = customSessionCacheKey(provider, opts);
159
234
  const { input, previousResponseId, continuationResetReason } = toXaiResponsesInput(messages, opts.providerState, {
235
+ effortProjection,
160
236
  model: useModel,
161
237
  stateKey: COMPAT_RESPONSES_STATE_KEY,
162
238
  replayProvider,
@@ -165,8 +241,7 @@ export async function sendCompatResponses(provider, messages, useModel, tools, o
165
241
  // Opt-in: system prompts travel as top-level `instructions`, not input items.
166
242
  includeSystem: provider.config?.responsesSystemAsInstructions !== true,
167
243
  });
168
- const instructions =
169
- provider.config?.responsesSystemAsInstructions === true ? xaiSystemInstructions(messages) : '';
244
+ const instructions = provider.config?.responsesSystemAsInstructions === true ? xaiSystemInstructions(messages) : '';
170
245
  const params = buildCompatResponsesParams({
171
246
  provider,
172
247
  useModel,
@@ -175,14 +250,20 @@ export async function sendCompatResponses(provider, messages, useModel, tools, o
175
250
  tools,
176
251
  opts,
177
252
  instructions,
253
+ effortProjection,
254
+ sessionCacheKey,
178
255
  });
256
+ // The session identity also travels as headers; caller headers win.
257
+ const headers = sessionCacheKey
258
+ ? { 'session-id': sessionCacheKey, 'x-client-request-id': sessionCacheKey, ...opts.requestHeaders }
259
+ : opts.requestHeaders;
179
260
 
180
261
  try {
181
262
  opts.onStageChange?.('requesting');
182
263
  } catch {
183
264
  /* heartbeat best-effort */
184
265
  }
185
- const streamed = await streamCompatResponses({ provider, params, label, signal, opts });
266
+ const streamed = await streamCompatResponses({ provider, params, label, signal, opts, headers });
186
267
  const response = streamed.response;
187
268
  const usage = response?.usage || null;
188
269
  const inputTokens = Number(usage?.input_tokens ?? usage?.prompt_tokens ?? 0);
@@ -0,0 +1,193 @@
1
+ // Per-provider request retry profile for the OpenAI-compatible family. Each
2
+ // provider carries its own retry constants; the
3
+ // shared withRetry still owns classification, the exposure/replay veto,
4
+ // quota refusals, x-should-retry and Retry-After. Providers with no profile
5
+ // keep the shared defaults.
6
+ //
7
+ // `backoffMs[n]` is the base wait before retry n (index 0 is never read, so
8
+ // the first value is the duplicate the withRetry schedule expects).
9
+ // statusRetryCaps — retries allowed for one status (for example a
10
+ // 429 stops after a single retry, whatever the overall budget).
11
+ // terminalStatuses — statuses that are terminal although the
12
+ // shared classifier would retry them.
13
+ // retryOnlyStatuses — the complete retryable error set: any other 4xx/5xx
14
+ // status surfaces.
15
+ // maxRetryAfterMs — give up instead of waiting longer than this.
16
+ // maxFailedAttemptMs — a 5xx that took longer than this is not replayed.
17
+ // retryEmptyResponse — a completed turn with no text, tool call or
18
+ // reasoning is re-asked inside the ladder (nothing was exposed); a number
19
+ // bounds how many times per send, true leaves it to the ladder.
20
+ // adviceWindow - outside exceptStatuses, Retry-After is clamped to maxMs
21
+ // and jittered by jitterRatio (the advice of the excepted statuses stays raw).
22
+ // recycleDispatcherOn5xx — a retried 5xx rebuilds the shared HTTP pool.
23
+ // adviceAtLeastBackoff — the advice is a floor under the jittered backoff step.
24
+ // Every stop is provider-terminal: the profile's limits are final, so the
25
+ // agent loop never re-runs a stopped or exhausted request (see
26
+ // COMPAT_RETRY_POLICY).
27
+ import { markProviderRecoveryExhausted } from './retry-classifier.mjs';
28
+ import { retryAfterMsFromError } from './provider-error-kind.mjs';
29
+ import { STREAM_TRANSPORTS, stampStreamOutcome } from './lib/stream-outcome.mjs';
30
+ import { jitterDelayMs } from './retry-backoff.mjs';
31
+
32
+ const statusOf = (err) => Number(err?.httpStatus || err?.status || err?.response?.status || 0);
33
+
34
+ /** Loop retry policy of every profiled provider: no recovery cycles on top of the profile. */
35
+ export const COMPAT_RETRY_POLICY = Object.freeze({ recoveryCycles: false });
36
+
37
+ /** xAI additionally treats an in-band invalid-request error as retryable. */
38
+ export const XAI_COMPAT_RETRY_POLICY = Object.freeze({ ...COMPAT_RETRY_POLICY, inBandInvalidRequestRetryable: true });
39
+
40
+ export const COMPAT_RETRY_PROFILES = Object.freeze({
41
+ // A budget of 15 sends (the retry limit is reached when the next retry number
42
+ // equals it, so 14 waits), 2s doubling to 30s, ±20% jitter, one retry for a
43
+ // 429. Outside a 429, Retry-After is capped at 30s and jittered.
44
+ xai: Object.freeze({
45
+ maxAttempts: 15,
46
+ backoffMs: [2000, 2000, 4000, 8000, 16000, 30000],
47
+ retryJitterRatio: 0.2,
48
+ statusRetryCaps: { 429: 1 },
49
+ terminalStatuses: [408, 409, 425],
50
+ retryEmptyResponse: true,
51
+ adviceWindow: Object.freeze({ maxMs: 30_000, jitterRatio: 0.2, exceptStatuses: [429] }),
52
+ recycleDispatcherOn5xx: true,
53
+ }),
54
+ // 5 retries, 500ms doubling to 10s, ±10% jitter;
55
+ // a Retry-After beyond the 10s ceiling is not waited out.
56
+ deepseek: Object.freeze({
57
+ maxAttempts: 6,
58
+ backoffMs: [500, 500, 1000, 2000, 4000, 8000],
59
+ retryJitterRatio: 0.1,
60
+ maxRetryAfterMs: 10_000,
61
+ terminalStatuses: [408, 409, 425],
62
+ retryEmptyResponse: true,
63
+ }),
64
+ // The HTTP transport layer governs one request: 6 sends, 500ms doubling, no
65
+ // jitter, a Retry-After beyond 60s is not waited out; only 408, 429 and 5xx
66
+ // are transient, every other 4xx is terminal. A benign empty completion is
67
+ // re-asked at most twice.
68
+ 'muse-code-oauth': Object.freeze({
69
+ maxAttempts: 6,
70
+ backoffMs: [500, 500, 1000, 2000, 4000, 8000],
71
+ retryJitterRatio: 0,
72
+ terminalStatuses: [409, 425],
73
+ maxRetryAfterMs: 60_000,
74
+ retryEmptyResponse: 2,
75
+ }),
76
+ // server budget 8 retries (2s doubling to 30s,
77
+ // ±10%), rate-limit budget 5 retries; 408, 409, 501 and 505 are terminal
78
+ // (425 stays retryable); a Retry-After beyond 300s is not waited out.
79
+ 'mimo-oauth': Object.freeze({
80
+ maxAttempts: 9,
81
+ backoffMs: [2000, 2000, 4000, 8000, 16000, 30000],
82
+ retryJitterRatio: 0.1,
83
+ statusRetryCaps: { 429: 5 },
84
+ terminalStatuses: [408, 409, 501, 505],
85
+ maxRetryAfterMs: 300_000,
86
+ }),
87
+ // 3 sends (400ms doubling to 5s) for exactly
88
+ // 500/502/503/504/520/521/522 — every 4xx (429 included) and every other
89
+ // 5xx is terminal; a failed attempt slower than 15s is returned.
90
+ 'command-code-oauth': Object.freeze({
91
+ maxAttempts: 3,
92
+ backoffMs: [400, 400, 800],
93
+ retryOnlyStatuses: [500, 502, 503, 504, 520, 521, 522],
94
+ maxFailedAttemptMs: 15_000,
95
+ maxRetryAfterMs: 60_000,
96
+ adviceAtLeastBackoff: true,
97
+ }),
98
+ });
99
+
100
+ /** withRetry options (attempt budget, schedule, jitter) for a provider; {} for the shared defaults. */
101
+ export function compatRetryOptions(providerName) {
102
+ const profile = COMPAT_RETRY_PROFILES[providerName];
103
+ if (!profile) return {};
104
+ return {
105
+ maxAttempts: profile.maxAttempts,
106
+ backoffMs: profile.backoffMs,
107
+ retryJitterRatio: profile.retryJitterRatio,
108
+ finalOnExhaust: true,
109
+ ...(profile.adviceAtLeastBackoff ? { adviceAtLeastBackoff: true } : {}),
110
+ ...(profile.recycleDispatcherOn5xx ? { recycleDispatcherOn5xx: true } : {}),
111
+ ...(profile.retryJitterMode ? { retryJitterMode: profile.retryJitterMode } : {}),
112
+ };
113
+ }
114
+
115
+ function profileStops(profile, err, { attempt, elapsedMs }) {
116
+ const status = statusOf(err);
117
+ if (profile.terminalStatuses?.includes(status)) return true;
118
+ const cap = profile.statusRetryCaps?.[status];
119
+ if (cap !== undefined && attempt >= cap) return true;
120
+ if (profile.retryOnlyStatuses && status >= 400 && status < 600 && !profile.retryOnlyStatuses.includes(status)) {
121
+ return true;
122
+ }
123
+ if (profile.maxFailedAttemptMs && status >= 500 && status < 600 && elapsedMs > profile.maxFailedAttemptMs) {
124
+ return true;
125
+ }
126
+ const advice = retryAfterMsFromError(err);
127
+ return profile.maxRetryAfterMs !== undefined && advice != null && advice > profile.maxRetryAfterMs;
128
+ }
129
+
130
+ // Restamps the advice as a deadline (the highest-precedence source) so the
131
+ // shared loop waits the clamped, jittered value instead of the raw one.
132
+ function shapeAdvice(window, err) {
133
+ if (!window || !err || typeof err !== 'object' || window.exceptStatuses?.includes(statusOf(err))) return;
134
+ const advice = retryAfterMsFromError(err);
135
+ if (advice == null) return;
136
+ err.retryAfterAt = Date.now() + jitterDelayMs(Math.min(advice, window.maxMs), window.jitterRatio);
137
+ }
138
+
139
+ // A completed turn that produced nothing: no text, tool call or reasoning, and
140
+ // a normal finish (a Responses turn carries no stop reason when it completes).
141
+ function isEmptyNormalFinish(result) {
142
+ if (!result || typeof result !== 'object') return false;
143
+ if (result.stopReason != null && result.stopReason !== 'stop') return false;
144
+ return (
145
+ !String(result.content ?? '').trim() &&
146
+ !result.toolCalls?.length &&
147
+ !result.reasoningContent &&
148
+ !result.reasoningDetails?.length &&
149
+ result.reasoningEmitted !== true
150
+ );
151
+ }
152
+
153
+ function emptyResponseError(providerName) {
154
+ const err = Object.assign(new Error(`${providerName} returned an empty response`), {
155
+ code: 'server_error',
156
+ emptyResponse: true,
157
+ });
158
+ // The turn finished: a failed ladder must not reopen it as a non-streaming request.
159
+ stampStreamOutcome(err, { transport: STREAM_TRANSPORTS.SSE, terminalObserved: true });
160
+ return err;
161
+ }
162
+
163
+ /**
164
+ * Wrap one withRetry attempt with the provider's profile: a failure the
165
+ * profile treats as final is marked recovery-exhausted and provider-terminal
166
+ * (withRetry then surfaces it unchanged and no outer layer replays it), and a
167
+ * profiled empty response is raised as a retryable failure.
168
+ */
169
+ export function withCompatRetryProfile(providerName, attemptFn) {
170
+ const profile = COMPAT_RETRY_PROFILES[providerName];
171
+ if (!profile) return attemptFn;
172
+ let emptyRetries = 0;
173
+ return async (ctx) => {
174
+ const startedAt = Date.now();
175
+ try {
176
+ const result = await attemptFn(ctx);
177
+ if (profile.retryEmptyResponse && isEmptyNormalFinish(result)) {
178
+ const bound = profile.retryEmptyResponse === true ? Infinity : profile.retryEmptyResponse;
179
+ if (emptyRetries < bound) {
180
+ emptyRetries += 1;
181
+ throw emptyResponseError(providerName);
182
+ }
183
+ }
184
+ return result;
185
+ } catch (err) {
186
+ if (profileStops(profile, err, { attempt: ctx?.attempt | 0, elapsedMs: Date.now() - startedAt })) {
187
+ throw markProviderRecoveryExhausted(err, { owner: `${providerName}-retry-profile`, terminal: true });
188
+ }
189
+ shapeAdvice(profile.adviceWindow, err);
190
+ throw err;
191
+ }
192
+ };
193
+ }
@@ -1,6 +1,8 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, providerTimeoutError, streamStalledError } from '../stall-policy.mjs';
3
3
  import { typedStatusFrom } from './retry-classifier.mjs';
4
+ import { applyCompatErrorContract } from './openai-compat-error-contract.mjs';
5
+ import { truncatedCompatStreamError } from './lib/openai-tool-args.mjs';
4
6
 
5
7
  // Shared lifecycle and replay boundaries for both OpenAI-compatible protocols.
6
8
 
@@ -22,10 +24,68 @@ function markInBandWireError(err) {
22
24
  if (typed && !err.httpStatus) err.httpStatus = typed;
23
25
  const code = detail.code ?? detail.type ?? null;
24
26
  if (code != null && code !== '' && !err.providerErrorCode) err.providerErrorCode = String(code);
27
+ applyCompatErrorContract(err, detail);
25
28
  } catch {}
26
29
  return err;
27
30
  }
28
31
 
32
+ /**
33
+ * A frame the SDK failed to JSON-decode. Raised only at the wire-decoding
34
+ * boundary (the iterator's own next()); consumers stamp the exposure evidence
35
+ * (partial output, replay safety) on it in their failure path.
36
+ */
37
+ export function malformedCompatStreamError(label, cause) {
38
+ const err = new Error(`${label || 'compat SSE'} malformed stream frame: ${cause?.message || 'invalid JSON'}`, {
39
+ cause,
40
+ });
41
+ err.name = 'MalformedStreamError';
42
+ err.code = 'malformed_stream';
43
+ err.malformedStream = true;
44
+ return err;
45
+ }
46
+
47
+ /**
48
+ * A chat `finish_reason: network_error` is an interruption the gateway reports
49
+ * in-band, never a completion. Typed as a truncated stream so the shared
50
+ * classifier treats it as transient; exposure gates decide replay.
51
+ */
52
+ export function networkInterruptedCompatStreamError(label) {
53
+ const err = truncatedCompatStreamError(label, 'finish_reason=network_error');
54
+ err.name = 'StreamInterruptedError';
55
+ err.streamInterrupted = true;
56
+ err.finishReason = 'network_error';
57
+ return err;
58
+ }
59
+
60
+ export function isNetworkErrorFinishReason(finishReason) {
61
+ return typeof finishReason === 'string' && finishReason.trim().toLowerCase() === 'network_error';
62
+ }
63
+
64
+ /**
65
+ * A chat `finish_reason: error` means the upstream failed the generation
66
+ * (gateways surface it as a 502 upstream_error). Typed like the network interruption, with the gateway status.
67
+ */
68
+ export function upstreamErrorCompatStreamError(label) {
69
+ const err = truncatedCompatStreamError(label, 'finish_reason=error');
70
+ err.name = 'StreamInterruptedError';
71
+ err.streamInterrupted = true;
72
+ err.finishReason = 'error';
73
+ err.status = 502;
74
+ err.code = 'upstream_error';
75
+ return err;
76
+ }
77
+
78
+ export function isErrorFinishReason(finishReason) {
79
+ return typeof finishReason === 'string' && finishReason.trim().toLowerCase() === 'error';
80
+ }
81
+
82
+ /** The typed interruption for a failing chat finish_reason, or null for any other value. */
83
+ export function failingFinishReasonError(finishReason, label) {
84
+ if (isNetworkErrorFinishReason(finishReason)) return networkInterruptedCompatStreamError(label);
85
+ if (isErrorFinishReason(finishReason)) return upstreamErrorCompatStreamError(label);
86
+ return null;
87
+ }
88
+
29
89
  export function synthLeakedOpenAICall(recovered) {
30
90
  let args = recovered?.arguments;
31
91
  if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
@@ -59,7 +119,7 @@ export function closeCompatStream(stream, iterator) {
59
119
 
60
120
  export async function nextAsyncWithWatchdog(
61
121
  iterator,
62
- { signal, idleMs, idleDeadlineAt, idleEnabled, idleLabel, emittedToolCall } = {}
122
+ { signal, idleMs, idleDeadlineAt, idleEnabled, idleLabel, streamLabel, emittedToolCall } = {}
63
123
  ) {
64
124
  let idleTimer = null;
65
125
  let idleReject = null;
@@ -106,7 +166,11 @@ export async function nextAsyncWithWatchdog(
106
166
  };
107
167
  signal.addEventListener('abort', onAbort, { once: true });
108
168
  }
109
- iterator.next().then(resolve, reject);
169
+ // The SDK JSON-decodes each frame inside next(): a SyntaxError here is a
170
+ // malformed wire frame, typed at this boundary and nowhere else.
171
+ iterator.next().then(resolve, (error) =>
172
+ reject(error instanceof SyntaxError ? malformedCompatStreamError(streamLabel || idleLabel, error) : error)
173
+ );
110
174
  });
111
175
  } catch (err) {
112
176
  if (idleTimedOut)
@@ -99,6 +99,7 @@ export async function consumeCompatResponsesStream(
99
99
  idleDeadlineAt: state.semanticIdleDeadlineAt,
100
100
  idleEnabled: sawFirstEvent && idleEnabled && state.semanticIdleDeadlineAt > 0,
101
101
  idleLabel: `${label} SSE idle`,
102
+ streamLabel: label,
102
103
  // Unsafe-to-retry once any tool call (native or recovered-leaked)
103
104
  // has been emitted this stream — avoid a double side-effect.
104
105
  emittedToolCall: () => state.emittedToolCall || leakedCalls.length > 0,
@@ -144,7 +145,7 @@ export async function consumeCompatResponsesStream(
144
145
  }
145
146
  throw stampOutcome(err);
146
147
  }
147
- const unresolved = unresolvedSalvageError(state);
148
+ const unresolved = unresolvedSalvageError(state, label);
148
149
  if (unresolved) {
149
150
  attachPartialState(unresolved, state, leakedCalls);
150
151
  throw stampOutcome(unresolved);
@@ -18,6 +18,7 @@ import {
18
18
  import { customToolCallFromResponseItem, nativeToolSearchCallFromArguments } from './custom-tool-wire.mjs';
19
19
  import { providerReplayItems } from './lib/provider-replay.mjs';
20
20
  import { ensureResponsesCallOutputs } from './lib/wire-pairing.mjs';
21
+ import { responsesEffortUpdates } from './effort-configuration.mjs';
21
22
 
22
23
  function positiveTokenInt(value) {
23
24
  const n = Number(value);
@@ -423,10 +424,14 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
423
424
  if (!pendingToolMedia.length) return;
424
425
  input.push({ role: 'user', content: pendingToolMedia.splice(0) });
425
426
  };
427
+ // `options.effortProjection` (custom GPT-6+ routes) carries wire-ready effort updates.
428
+ const effortUpdates = responsesEffortUpdates(options.effortProjection);
426
429
  for (let messageIndex = startIndex; messageIndex < messages.length; messageIndex += 1) {
427
430
  const m = messages[messageIndex];
428
431
  if (!includeSystem && m.role === 'system') continue;
432
+ const updates = effortUpdates.before(m);
429
433
  if (m.role !== 'tool') flushToolMedia();
434
+ input.push(...updates);
430
435
  const orderedReplay = m.role === 'assistant' ? providerReplayItems(m, replayProvider) : undefined;
431
436
  if (orderedReplay?.length) {
432
437
  input.push(...orderedReplay);
@@ -444,6 +449,7 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
444
449
  flushToolMedia();
445
450
  const trailingReasoning = reasoningByMessageIndex.get(messages.length);
446
451
  if (trailingReasoning) input.push(...trailingReasoning);
452
+ input.push(...effortUpdates.end());
447
453
  // Wire-level pairing guard: replay envelopes can carry a call whose
448
454
  // result never committed (cancel/abort). The provider hard-rejects the
449
455
  // unpaired call, so synthesize the missing outputs here.
@@ -1,5 +1,6 @@
1
1
  import { resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
2
2
  import { markProviderRecoveryExhausted, withRetry } from './retry-classifier.mjs';
3
+ import { compatRetryOptions, withCompatRetryProfile } from './openai-compat-retry-profile.mjs';
3
4
  import { consumeCompatResponsesStream } from './openai-compat-stream.mjs';
4
5
  import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, createPassthroughSignal } from '../stall-policy.mjs';
5
6
  import {
@@ -91,7 +92,7 @@ export async function sendXaiResponses(provider, messages, useModel, tools, opts
91
92
  let streamed;
92
93
  try {
93
94
  streamed = await withRetry(
94
- async ({ signal: attemptSignal }) => {
95
+ withCompatRetryProfile(provider.name, async ({ signal: attemptSignal }) => {
95
96
  const stream = await withRetry(
96
97
  ({ signal: openSignal }) => provider.client.responses.create(params, { signal: openSignal }),
97
98
  {
@@ -127,11 +128,12 @@ export async function sendXaiResponses(provider, messages, useModel, tools, opts
127
128
  }
128
129
  throw error;
129
130
  }
130
- },
131
+ }),
131
132
  {
132
133
  signal: totalSignal.signal,
133
134
  onRetry: compatStreamRetryReporter('xai:responses', opts),
134
135
  retry529: opts.retry529,
136
+ ...compatRetryOptions(provider.name),
135
137
  }
136
138
  );
137
139
  } finally {