@pentoshi/clai 4.0.3 → 4.2.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 (314) hide show
  1. package/README.md +45 -9
  2. package/dist/agent/compaction-executor.d.ts +68 -0
  3. package/dist/agent/compaction-executor.js +343 -0
  4. package/dist/agent/compaction-executor.js.map +1 -0
  5. package/dist/agent/confirm-port.js +6 -3
  6. package/dist/agent/confirm-port.js.map +1 -1
  7. package/dist/agent/context-breakdown.js +2 -0
  8. package/dist/agent/context-breakdown.js.map +1 -1
  9. package/dist/agent/context-manager.d.ts +23 -9
  10. package/dist/agent/context-manager.js +72 -43
  11. package/dist/agent/context-manager.js.map +1 -1
  12. package/dist/agent/events.d.ts +3 -0
  13. package/dist/agent/loop-guard.d.ts +2 -0
  14. package/dist/agent/loop-guard.js +48 -3
  15. package/dist/agent/loop-guard.js.map +1 -1
  16. package/dist/agent/prompt-composer.js +6 -6
  17. package/dist/agent/prompt-composer.js.map +1 -1
  18. package/dist/agent/reliability-policy.d.ts +1 -0
  19. package/dist/agent/reliability-policy.js +6 -3
  20. package/dist/agent/reliability-policy.js.map +1 -1
  21. package/dist/agent/request-accounting.d.ts +97 -0
  22. package/dist/agent/request-accounting.js +207 -0
  23. package/dist/agent/request-accounting.js.map +1 -0
  24. package/dist/agent/request-budget.d.ts +2 -2
  25. package/dist/agent/request-budget.js +2 -4
  26. package/dist/agent/request-budget.js.map +1 -1
  27. package/dist/agent/runner.d.ts +8 -2
  28. package/dist/agent/runner.js +412 -181
  29. package/dist/agent/runner.js.map +1 -1
  30. package/dist/agent/session-title.d.ts +0 -26
  31. package/dist/agent/session-title.js +0 -80
  32. package/dist/agent/session-title.js.map +1 -1
  33. package/dist/agent/tool-history.d.ts +2 -2
  34. package/dist/agent/tool-history.js +19 -6
  35. package/dist/agent/tool-history.js.map +1 -1
  36. package/dist/agent/turn-outcome.d.ts +1 -1
  37. package/dist/agent/turn-outcome.js +2 -12
  38. package/dist/agent/turn-outcome.js.map +1 -1
  39. package/dist/app/adapters/agent-event-adapter.js +19 -0
  40. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  41. package/dist/app/adapters/current-agent-adapter.js +4 -0
  42. package/dist/app/adapters/current-agent-adapter.js.map +1 -1
  43. package/dist/app/commands/catalog.js +23 -16
  44. package/dist/app/commands/catalog.js.map +1 -1
  45. package/dist/app/controllers/cancel-coordinator.d.ts +51 -0
  46. package/dist/app/controllers/cancel-coordinator.js +48 -0
  47. package/dist/app/controllers/cancel-coordinator.js.map +1 -0
  48. package/dist/app/controllers/session-compact-helper.d.ts +19 -2
  49. package/dist/app/controllers/session-compact-helper.js +100 -127
  50. package/dist/app/controllers/session-compact-helper.js.map +1 -1
  51. package/dist/app/controllers/session-context-usage.d.ts +32 -15
  52. package/dist/app/controllers/session-context-usage.js +186 -51
  53. package/dist/app/controllers/session-context-usage.js.map +1 -1
  54. package/dist/app/controllers/session-controller.d.ts +22 -11
  55. package/dist/app/controllers/session-controller.js +137 -92
  56. package/dist/app/controllers/session-controller.js.map +1 -1
  57. package/dist/app/controllers/session-naming.d.ts +31 -0
  58. package/dist/app/controllers/session-naming.js +173 -0
  59. package/dist/app/controllers/session-naming.js.map +1 -0
  60. package/dist/app/controllers/session-persistence.d.ts +2 -2
  61. package/dist/app/controllers/session-persistence.js +3 -6
  62. package/dist/app/controllers/session-persistence.js.map +1 -1
  63. package/dist/app/controllers/session-turn-request.d.ts +2 -1
  64. package/dist/app/controllers/session-turn-request.js +3 -0
  65. package/dist/app/controllers/session-turn-request.js.map +1 -1
  66. package/dist/app/controllers/turn-continuation.d.ts +0 -6
  67. package/dist/app/controllers/turn-continuation.js +4 -21
  68. package/dist/app/controllers/turn-continuation.js.map +1 -1
  69. package/dist/app/controllers/turn-controller.d.ts +2 -1
  70. package/dist/app/controllers/turn-controller.js +1 -0
  71. package/dist/app/controllers/turn-controller.js.map +1 -1
  72. package/dist/app/events/app-event.d.ts +9 -0
  73. package/dist/app/events/app-event.js.map +1 -1
  74. package/dist/app/ports/agent-port.d.ts +8 -1
  75. package/dist/app/ports/transcript-item.d.ts +9 -1
  76. package/dist/classic/app/ClassicApp.js +3 -3
  77. package/dist/classic/app/ClassicApp.js.map +1 -1
  78. package/dist/classic/app/app-wiring.js +1 -2
  79. package/dist/classic/app/app-wiring.js.map +1 -1
  80. package/dist/classic/app/wiring-interactions.js +4 -9
  81. package/dist/classic/app/wiring-interactions.js.map +1 -1
  82. package/dist/classic/blocks/TurnSummaryBlock.d.ts +3 -0
  83. package/dist/classic/blocks/TurnSummaryBlock.js +6 -0
  84. package/dist/classic/blocks/TurnSummaryBlock.js.map +1 -0
  85. package/dist/classic/blocks/tool-lines.js +2 -1
  86. package/dist/classic/blocks/tool-lines.js.map +1 -1
  87. package/dist/classic/blocks/turn-summary-lines.d.ts +3 -0
  88. package/dist/classic/blocks/turn-summary-lines.js +7 -0
  89. package/dist/classic/blocks/turn-summary-lines.js.map +1 -0
  90. package/dist/classic/chrome/status-rows.d.ts +0 -1
  91. package/dist/classic/chrome/status-rows.js +0 -12
  92. package/dist/classic/chrome/status-rows.js.map +1 -1
  93. package/dist/classic/feed/Feed.js +2 -0
  94. package/dist/classic/feed/Feed.js.map +1 -1
  95. package/dist/classic/feed/feed-blocks.d.ts +1 -1
  96. package/dist/classic/feed/feed-blocks.js +3 -0
  97. package/dist/classic/feed/feed-blocks.js.map +1 -1
  98. package/dist/classic/input/cancel-ladder.d.ts +2 -19
  99. package/dist/classic/input/cancel-ladder.js +13 -20
  100. package/dist/classic/input/cancel-ladder.js.map +1 -1
  101. package/dist/commands/providers.js +2 -0
  102. package/dist/commands/providers.js.map +1 -1
  103. package/dist/llm/adapters/anthropic-tools.d.ts +31 -14
  104. package/dist/llm/adapters/anthropic-tools.js +124 -29
  105. package/dist/llm/adapters/anthropic-tools.js.map +1 -1
  106. package/dist/llm/adapters/gemini-tools.d.ts +16 -4
  107. package/dist/llm/adapters/gemini-tools.js +117 -16
  108. package/dist/llm/adapters/gemini-tools.js.map +1 -1
  109. package/dist/llm/adapters/openai-tools.d.ts +13 -2
  110. package/dist/llm/adapters/openai-tools.js +58 -11
  111. package/dist/llm/adapters/openai-tools.js.map +1 -1
  112. package/dist/llm/agentrouter.js +68 -45
  113. package/dist/llm/agentrouter.js.map +1 -1
  114. package/dist/llm/anthropic.js +69 -36
  115. package/dist/llm/anthropic.js.map +1 -1
  116. package/dist/llm/aws-mantle.js +70 -32
  117. package/dist/llm/aws-mantle.js.map +1 -1
  118. package/dist/llm/bynara.js +26 -69
  119. package/dist/llm/bynara.js.map +1 -1
  120. package/dist/llm/capabilities.d.ts +2 -1
  121. package/dist/llm/capabilities.js +56 -1
  122. package/dist/llm/capabilities.js.map +1 -1
  123. package/dist/llm/context-snapshot.d.ts +100 -0
  124. package/dist/llm/context-snapshot.js +277 -0
  125. package/dist/llm/context-snapshot.js.map +1 -0
  126. package/dist/llm/context-windows.d.ts +10 -0
  127. package/dist/llm/context-windows.js +129 -0
  128. package/dist/llm/context-windows.js.map +1 -0
  129. package/dist/llm/custom-provider-profile.d.ts +75 -0
  130. package/dist/llm/custom-provider-profile.js +558 -0
  131. package/dist/llm/custom-provider-profile.js.map +1 -0
  132. package/dist/llm/custom-providers.d.ts +13 -1
  133. package/dist/llm/custom-providers.js +77 -25
  134. package/dist/llm/custom-providers.js.map +1 -1
  135. package/dist/llm/effort-fallback.d.ts +44 -0
  136. package/dist/llm/effort-fallback.js +101 -0
  137. package/dist/llm/effort-fallback.js.map +1 -0
  138. package/dist/llm/fireworks.d.ts +3 -0
  139. package/dist/llm/fireworks.js +103 -0
  140. package/dist/llm/fireworks.js.map +1 -0
  141. package/dist/llm/free.js +4 -0
  142. package/dist/llm/free.js.map +1 -1
  143. package/dist/llm/gemini.d.ts +1 -1
  144. package/dist/llm/gemini.js +89 -51
  145. package/dist/llm/gemini.js.map +1 -1
  146. package/dist/llm/groq.js +4 -0
  147. package/dist/llm/groq.js.map +1 -1
  148. package/dist/llm/hetzner.d.ts +3 -0
  149. package/dist/llm/hetzner.js +98 -0
  150. package/dist/llm/hetzner.js.map +1 -0
  151. package/dist/llm/http.d.ts +60 -10
  152. package/dist/llm/http.js +465 -156
  153. package/dist/llm/http.js.map +1 -1
  154. package/dist/llm/kimchi.js +4 -0
  155. package/dist/llm/kimchi.js.map +1 -1
  156. package/dist/llm/lightning.js +4 -0
  157. package/dist/llm/lightning.js.map +1 -1
  158. package/dist/llm/meta.d.ts +2 -0
  159. package/dist/llm/meta.js +839 -624
  160. package/dist/llm/meta.js.map +1 -1
  161. package/dist/llm/modal.js +4 -0
  162. package/dist/llm/modal.js.map +1 -1
  163. package/dist/llm/model-families.d.ts +9 -0
  164. package/dist/llm/model-families.js +39 -0
  165. package/dist/llm/model-families.js.map +1 -0
  166. package/dist/llm/nvidia.js +4 -0
  167. package/dist/llm/nvidia.js.map +1 -1
  168. package/dist/llm/ollama.js +44 -40
  169. package/dist/llm/ollama.js.map +1 -1
  170. package/dist/llm/openai.js +4 -0
  171. package/dist/llm/openai.js.map +1 -1
  172. package/dist/llm/openrouter.js +4 -0
  173. package/dist/llm/openrouter.js.map +1 -1
  174. package/dist/llm/operation-ledger.d.ts +60 -0
  175. package/dist/llm/operation-ledger.js +136 -0
  176. package/dist/llm/operation-ledger.js.map +1 -0
  177. package/dist/llm/operation-usage.d.ts +32 -0
  178. package/dist/llm/operation-usage.js +172 -0
  179. package/dist/llm/operation-usage.js.map +1 -0
  180. package/dist/llm/orcarouter.d.ts +7 -0
  181. package/dist/llm/orcarouter.js +166 -0
  182. package/dist/llm/orcarouter.js.map +1 -0
  183. package/dist/llm/provider-profile-layers.d.ts +7 -0
  184. package/dist/llm/provider-profile-layers.js +961 -0
  185. package/dist/llm/provider-profile-layers.js.map +1 -0
  186. package/dist/llm/provider-profile.d.ts +181 -0
  187. package/dist/llm/provider-profile.js +329 -0
  188. package/dist/llm/provider-profile.js.map +1 -0
  189. package/dist/llm/provider-profiles.d.ts +13 -0
  190. package/dist/llm/provider-profiles.js +86 -0
  191. package/dist/llm/provider-profiles.js.map +1 -0
  192. package/dist/llm/provider.d.ts +2 -0
  193. package/dist/llm/provider.js +198 -21
  194. package/dist/llm/provider.js.map +1 -1
  195. package/dist/llm/qwen-cloud.js +4 -0
  196. package/dist/llm/qwen-cloud.js.map +1 -1
  197. package/dist/llm/reasoning-artifacts.d.ts +101 -0
  198. package/dist/llm/reasoning-artifacts.js +440 -0
  199. package/dist/llm/reasoning-artifacts.js.map +1 -0
  200. package/dist/llm/request-fingerprint.d.ts +10 -0
  201. package/dist/llm/request-fingerprint.js +313 -0
  202. package/dist/llm/request-fingerprint.js.map +1 -0
  203. package/dist/llm/request-plan.d.ts +119 -0
  204. package/dist/llm/request-plan.js +279 -0
  205. package/dist/llm/request-plan.js.map +1 -0
  206. package/dist/llm/router.d.ts +19 -1
  207. package/dist/llm/router.js +504 -102
  208. package/dist/llm/router.js.map +1 -1
  209. package/dist/llm/stream-events.d.ts +47 -0
  210. package/dist/llm/stream-events.js +55 -0
  211. package/dist/llm/stream-events.js.map +1 -0
  212. package/dist/llm/stream-terminal.d.ts +30 -0
  213. package/dist/llm/stream-terminal.js +51 -0
  214. package/dist/llm/stream-terminal.js.map +1 -0
  215. package/dist/llm/token-estimate-calibration.d.ts +23 -0
  216. package/dist/llm/token-estimate-calibration.js +140 -0
  217. package/dist/llm/token-estimate-calibration.js.map +1 -0
  218. package/dist/llm/token-usage.d.ts +35 -5
  219. package/dist/llm/token-usage.js +212 -162
  220. package/dist/llm/token-usage.js.map +1 -1
  221. package/dist/llm/tokenrouter.js +13 -6
  222. package/dist/llm/tokenrouter.js.map +1 -1
  223. package/dist/modes/ask.js +11 -5
  224. package/dist/modes/ask.js.map +1 -1
  225. package/dist/noninteractive/start-noninteractive.js +31 -2
  226. package/dist/noninteractive/start-noninteractive.js.map +1 -1
  227. package/dist/noninteractive/stream-blocks.js +15 -4
  228. package/dist/noninteractive/stream-blocks.js.map +1 -1
  229. package/dist/prompts/embedded.js +1 -1
  230. package/dist/prompts/embedded.js.map +1 -1
  231. package/dist/prompts/system.agent.md +6 -0
  232. package/dist/store/config.js +7 -1
  233. package/dist/store/config.js.map +1 -1
  234. package/dist/store/history.d.ts +2 -0
  235. package/dist/store/history.js +19 -10
  236. package/dist/store/history.js.map +1 -1
  237. package/dist/store/plan.js +20 -14
  238. package/dist/store/plan.js.map +1 -1
  239. package/dist/tools/elevated-shell.d.ts +1 -1
  240. package/dist/tools/elevated-shell.js +24 -72
  241. package/dist/tools/elevated-shell.js.map +1 -1
  242. package/dist/tools/fs.d.ts +5 -3
  243. package/dist/tools/fs.js +8 -5
  244. package/dist/tools/fs.js.map +1 -1
  245. package/dist/tools/nmap-runner.js +30 -36
  246. package/dist/tools/nmap-runner.js.map +1 -1
  247. package/dist/tools/sudo-session.d.ts +83 -0
  248. package/dist/tools/sudo-session.js +124 -0
  249. package/dist/tools/sudo-session.js.map +1 -0
  250. package/dist/tui-v2/app/App.js +9 -24
  251. package/dist/tui-v2/app/App.js.map +1 -1
  252. package/dist/tui-v2/components/plan/plan-view.js +5 -3
  253. package/dist/tui-v2/components/plan/plan-view.js.map +1 -1
  254. package/dist/tui-v2/components/status/context-limit-chip.js +2 -2
  255. package/dist/tui-v2/components/status/context-limit-chip.js.map +1 -1
  256. package/dist/tui-v2/components/status/status-line.js +1 -14
  257. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  258. package/dist/tui-v2/components/transcript/intro-card.js +2 -1
  259. package/dist/tui-v2/components/transcript/intro-card.js.map +1 -1
  260. package/dist/tui-v2/components/transcript/notice-row.d.ts +3 -1
  261. package/dist/tui-v2/components/transcript/notice-row.js +28 -3
  262. package/dist/tui-v2/components/transcript/notice-row.js.map +1 -1
  263. package/dist/tui-v2/components/transcript/thinking-block.d.ts +0 -5
  264. package/dist/tui-v2/components/transcript/thinking-block.js +3 -8
  265. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
  266. package/dist/tui-v2/components/transcript/tool-card.js +16 -7
  267. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
  268. package/dist/tui-v2/components/transcript/transcript-row.d.ts +0 -2
  269. package/dist/tui-v2/components/transcript/transcript-row.js +7 -3
  270. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
  271. package/dist/tui-v2/components/transcript/transcript-view.js +1 -3
  272. package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -1
  273. package/dist/tui-v2/composer/composer-editor.js +2 -1
  274. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  275. package/dist/types.d.ts +156 -1
  276. package/dist/types.js +3 -0
  277. package/dist/types.js.map +1 -1
  278. package/dist/ui-core/bootstrap/composition-root.d.ts +2 -0
  279. package/dist/ui-core/bootstrap/composition-root.js +26 -3
  280. package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
  281. package/dist/ui-core/commands/picker-commands.js +11 -5
  282. package/dist/ui-core/commands/picker-commands.js.map +1 -1
  283. package/dist/ui-core/commands/session-commands.js +41 -5
  284. package/dist/ui-core/commands/session-commands.js.map +1 -1
  285. package/dist/ui-core/plan/plan-lifecycle.js +3 -2
  286. package/dist/ui-core/plan/plan-lifecycle.js.map +1 -1
  287. package/dist/ui-core/rendering/context-limit.js +4 -1
  288. package/dist/ui-core/rendering/context-limit.js.map +1 -1
  289. package/dist/ui-core/rendering/duration.d.ts +5 -0
  290. package/dist/ui-core/rendering/duration.js +43 -0
  291. package/dist/ui-core/rendering/duration.js.map +1 -0
  292. package/dist/ui-core/rendering/intro-header.js +2 -2
  293. package/dist/ui-core/rendering/intro-header.js.map +1 -1
  294. package/dist/ui-core/rendering/theme.d.ts +1 -0
  295. package/dist/ui-core/rendering/theme.js +2 -0
  296. package/dist/ui-core/rendering/theme.js.map +1 -1
  297. package/dist/ui-core/rendering/transcript-semantic.js +2 -0
  298. package/dist/ui-core/rendering/transcript-semantic.js.map +1 -1
  299. package/dist/ui-core/state/transcript-compaction.js +2 -0
  300. package/dist/ui-core/state/transcript-compaction.js.map +1 -1
  301. package/dist/ui-core/state/transcript-hydrate.js +34 -4
  302. package/dist/ui-core/state/transcript-hydrate.js.map +1 -1
  303. package/dist/ui-core/state/transcript-reducer.d.ts +5 -0
  304. package/dist/ui-core/state/transcript-reducer.js +96 -8
  305. package/dist/ui-core/state/transcript-reducer.js.map +1 -1
  306. package/dist/ui-core/state/transcript-store.d.ts +10 -1
  307. package/dist/ui-core/state/transcript-store.js +22 -1
  308. package/dist/ui-core/state/transcript-store.js.map +1 -1
  309. package/dist/ui-core/state/transcript-types.d.ts +9 -1
  310. package/dist/ui-core/state/transcript-types.js +3 -1
  311. package/dist/ui-core/state/transcript-types.js.map +1 -1
  312. package/dist/version.generated.d.ts +2 -2
  313. package/dist/version.generated.js +2 -2
  314. package/package.json +6 -1
package/dist/llm/http.js CHANGED
@@ -2,8 +2,15 @@ import { modelAcceptsImages, modelSupportsThinking, isReasoningUnsupported, mode
2
2
  import { resolveSampling } from "./sampling.js";
3
3
  import { accumulateOpenAiToolCallDelta, finalizeOpenAiToolCalls, fromWireName, parseOpenAiMessageToolCalls, } from "./tool-protocol.js";
4
4
  import { openAiToolBodyFields, toOpenAiToolMessages, } from "./adapters/openai-tools.js";
5
- import { parseOpenAiUsage } from "./token-usage.js";
6
- import { REASONING_CLOSE, REASONING_OPEN, wrapReasoning, } from "./reasoning-marker.js";
5
+ import { parseFireworksUsage, parseOpenAiUsage, } from "./token-usage.js";
6
+ import { generationFetch } from "./operation-usage.js";
7
+ import { isOperationPolicyError } from "./operation-ledger.js";
8
+ import { emitStreamReasoningArtifacts, emitStreamReasoningDelta, } from "./stream-events.js";
9
+ import { CHAT_COMPLETIONS_STREAM_TERMINAL, requireTerminalProof, } from "./stream-terminal.js";
10
+ import { createReasoningArtifact, createReasoningArtifactProvenance, } from "./reasoning-artifacts.js";
11
+ import { classifyBynaraModel, classifyNvidiaModel, } from "./model-families.js";
12
+ import { compileRequestPlan, } from "./request-plan.js";
13
+ import { singleLeadingSystemMessages } from "./system-messages.js";
7
14
  export class ProviderError extends Error {
8
15
  status;
9
16
  body;
@@ -190,10 +197,11 @@ export function ingestOpenAiModelCatalog(provider, payload) {
190
197
  : [];
191
198
  return ingestModelCatalogEntries(provider, entries);
192
199
  }
193
- export async function readJson(response) {
194
- const text = await readBodyCapped(response, MAX_JSON_RESPONSE_BYTES);
200
+ export async function readJson(response, signal) {
201
+ const text = await readBodyCapped(response, MAX_JSON_RESPONSE_BYTES, signal);
195
202
  if (!response.ok) {
196
203
  let detail = "";
204
+ let extractedMessage = "";
197
205
  try {
198
206
  const body = JSON.parse(text);
199
207
  const error = body.error;
@@ -223,10 +231,23 @@ export async function readJson(response) {
223
231
  detail = ` — ${msg}`;
224
232
  }
225
233
  }
234
+ extractedMessage = msg;
226
235
  }
227
236
  catch {
228
- if (text.length > 0)
229
- detail = ` ${text.slice(0, 200)}`;
237
+ // Non-JSON body — the raw text is surfaced via the full-response rule
238
+ // below, so nothing is lost here.
239
+ }
240
+ // Show the full error received from the request: the extracted
241
+ // `error.message` alone routinely hides the actionable part of a gateway
242
+ // error (upstream detail, codes, validation fields). Append the complete
243
+ // body whenever it carries anything beyond that extracted message.
244
+ const bodyText = text.slice(0, MAX_ERROR_BODY_CHARS);
245
+ if (bodyAddsInformation(bodyText, extractedMessage)) {
246
+ const shown = collapseWhitespace(bodyText);
247
+ const capped = shown.length > MAX_ERROR_BODY_IN_MESSAGE_CHARS
248
+ ? `${shown.slice(0, MAX_ERROR_BODY_IN_MESSAGE_CHARS)}…`
249
+ : shown;
250
+ detail = `${detail} — full response: ${capped}`;
230
251
  }
231
252
  const retryAfterSeconds = parseRetryAfterHeader(response.headers.get("retry-after")) ??
232
253
  parseRetryHintFromBody(text);
@@ -234,24 +255,67 @@ export async function readJson(response) {
234
255
  ? ` (retry after ${Math.ceil(retryAfterSeconds)}s)`
235
256
  : "";
236
257
  const codeHint = statusCodeHint(response.status);
237
- throw new ProviderError(`Provider request failed with HTTP ${response.status}${retryHint}${detail}${codeHint}`, response.status, text.slice(0, 1_000), retryAfterSeconds);
258
+ throw new ProviderError(`Provider request failed with HTTP ${response.status}${retryHint}${detail}${codeHint}`, response.status, bodyText, retryAfterSeconds);
238
259
  }
239
260
  return JSON.parse(text);
240
261
  }
241
262
  /** Hard cap on a JSON response body so a misbehaving provider can't OOM us. */
242
263
  const MAX_JSON_RESPONSE_BYTES = 4 * 1024 * 1024;
243
- async function readBodyCapped(response, maxBytes) {
264
+ /** How much of a provider error body we retain on the ProviderError. */
265
+ export const MAX_ERROR_BODY_CHARS = 8_000;
266
+ /** How much of that body is embedded in the user-visible error message. */
267
+ export const MAX_ERROR_BODY_IN_MESSAGE_CHARS = 2_000;
268
+ export function collapseWhitespace(text) {
269
+ return text.replace(/\s+/g, " ").trim();
270
+ }
271
+ /**
272
+ * True when the raw response body carries information beyond the message we
273
+ * already extracted from it (extra JSON fields, non-JSON payloads, …). Pure
274
+ * `{"error":{"message": …}}` envelopes are redundant and stay compact.
275
+ */
276
+ export function bodyAddsInformation(bodyText, extracted) {
277
+ const collapsed = collapseWhitespace(bodyText);
278
+ if (!collapsed)
279
+ return false;
280
+ if (!extracted)
281
+ return true;
282
+ const normalizedExtracted = collapseWhitespace(extracted);
283
+ // The user-facing text already carries the entire body verbatim.
284
+ if (normalizedExtracted.includes(collapsed))
285
+ return false;
286
+ if (!collapsed.includes(normalizedExtracted))
287
+ return true;
288
+ const rest = collapsed
289
+ .replace(normalizedExtracted, " ")
290
+ .replace(/\b(errors?|message|msg|detail|details|type|code|status)\b/gi, " ")
291
+ .replace(/[{}\[\]":,]/g, " ")
292
+ .trim();
293
+ return rest.length > 0;
294
+ }
295
+ async function readBodyCapped(response, maxBytes, signal) {
244
296
  const reader = response.body?.getReader();
245
297
  if (!reader) {
298
+ if (signal?.aborted) {
299
+ throw signal.reason ?? new Error("Response body read aborted");
300
+ }
246
301
  const text = await response.text();
247
302
  return text.length > maxBytes ? text.slice(0, maxBytes) : text;
248
303
  }
249
304
  const decoder = new TextDecoder("utf-8", { fatal: false });
250
305
  let collected = "";
251
306
  let bytesRead = 0;
307
+ const cancelOnAbort = () => {
308
+ void reader.cancel(signal?.reason).catch(() => undefined);
309
+ };
310
+ signal?.addEventListener("abort", cancelOnAbort, { once: true });
252
311
  try {
253
312
  while (bytesRead < maxBytes) {
254
- const { done, value } = await reader.read();
313
+ const { done, value } = signal
314
+ ? await readWithAbort(reader, signal)
315
+ : await reader.read();
316
+ if (signal?.aborted) {
317
+ throw signal.reason ?? new Error("Response body read aborted");
318
+ }
255
319
  if (done)
256
320
  break;
257
321
  if (!value)
@@ -276,6 +340,7 @@ async function readBodyCapped(response, maxBytes) {
276
340
  collected += decoder.decode();
277
341
  }
278
342
  finally {
343
+ signal?.removeEventListener("abort", cancelOnAbort);
279
344
  try {
280
345
  reader.releaseLock();
281
346
  }
@@ -298,8 +363,8 @@ async function readBodyCapped(response, maxBytes) {
298
363
  * re-generated the same prefix before one happened to finish inside the window.
299
364
  */
300
365
  export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 240_000;
301
- export const THINKING_STREAM_IDLE_TIMEOUT_MS = 600_000;
302
- export const THINKING_STREAM_INITIAL_IDLE_TIMEOUT_MS = 600_000;
366
+ export const THINKING_STREAM_IDLE_TIMEOUT_MS = 900_000;
367
+ export const THINKING_STREAM_INITIAL_IDLE_TIMEOUT_MS = 900_000;
303
368
  export function streamIdleBudgets(reasoningEnabled) {
304
369
  const idleTimeoutMs = reasoningEnabled
305
370
  ? THINKING_STREAM_IDLE_TIMEOUT_MS
@@ -483,6 +548,79 @@ export async function* readStreamLines(response, options = {}) {
483
548
  }
484
549
  }
485
550
  }
551
+ const DEFAULT_COMPATIBLE_REASONING_ARTIFACT_POLICY = {
552
+ scope: "all-history",
553
+ persistence: "tool-turn",
554
+ };
555
+ function artifactRaw(value) {
556
+ if (typeof value === "string")
557
+ return value;
558
+ if (Array.isArray(value))
559
+ return value;
560
+ if (value && typeof value === "object") {
561
+ return value;
562
+ }
563
+ return undefined;
564
+ }
565
+ function compatibleReasoningArtifacts(input) {
566
+ const policy = input.policy ?? DEFAULT_COMPATIBLE_REASONING_ARTIFACT_POLICY;
567
+ const provenance = createReasoningArtifactProvenance({
568
+ provider: input.providerId,
569
+ model: input.model,
570
+ dialect: "openai-compatible",
571
+ endpoint: input.baseUrl,
572
+ });
573
+ const artifacts = [];
574
+ const primaryToolPosition = input.toolCalls.length > 0 ? 0 : undefined;
575
+ if (input.reasoning?.text) {
576
+ artifacts.push(createReasoningArtifact({
577
+ kind: "plaintext",
578
+ raw: input.reasoning.text,
579
+ displaySummary: input.reasoning.text,
580
+ provenance,
581
+ replay: policy,
582
+ position: {
583
+ sequence: input.reasoning.sequence,
584
+ placement: primaryToolPosition === undefined ? "assistant" : "before-tool-call",
585
+ ...(primaryToolPosition === undefined
586
+ ? {}
587
+ : { toolCallIndex: primaryToolPosition }),
588
+ },
589
+ }));
590
+ }
591
+ for (const detail of input.details ?? []) {
592
+ artifacts.push(createReasoningArtifact({
593
+ kind: "structured-details",
594
+ raw: detail.raw,
595
+ provenance,
596
+ replay: policy,
597
+ position: {
598
+ sequence: detail.sequence,
599
+ placement: primaryToolPosition === undefined ? "assistant" : "before-tool-call",
600
+ ...(primaryToolPosition === undefined
601
+ ? {}
602
+ : { toolCallIndex: primaryToolPosition }),
603
+ },
604
+ }));
605
+ }
606
+ for (const signature of input.thoughtSignatures ?? []) {
607
+ const toolCallIndex = signature.toolCallIndex;
608
+ artifacts.push(createReasoningArtifact({
609
+ kind: "thought-signature",
610
+ raw: signature.raw,
611
+ provenance,
612
+ replay: toolCallIndex === undefined
613
+ ? { scope: "none", persistence: "never" }
614
+ : { scope: "tool-turn", persistence: "tool-turn" },
615
+ position: {
616
+ sequence: signature.sequence,
617
+ placement: toolCallIndex === undefined ? "assistant" : "on-tool-call",
618
+ ...(toolCallIndex === undefined ? {} : { toolCallIndex }),
619
+ },
620
+ }));
621
+ }
622
+ return artifacts.length ? artifacts : undefined;
623
+ }
486
624
  /** Map shared OpenAI-compatible payload → CompletionResult (includes usage). */
487
625
  export function toCompletionResult(provider, model, payload) {
488
626
  return {
@@ -492,6 +630,10 @@ export function toCompletionResult(provider, model, payload) {
492
630
  ...(payload.toolCalls?.length ? { toolCalls: payload.toolCalls } : {}),
493
631
  ...(payload.finishReason ? { finishReason: payload.finishReason } : {}),
494
632
  ...(payload.usage ? { usage: payload.usage } : {}),
633
+ ...(payload.reasoningBlock ? { reasoningBlock: payload.reasoningBlock } : {}),
634
+ ...(payload.reasoningArtifacts
635
+ ? { reasoningArtifacts: payload.reasoningArtifacts }
636
+ : {}),
495
637
  };
496
638
  }
497
639
  /**
@@ -544,7 +686,7 @@ export function createSseFrameAssembler(options) {
544
686
  },
545
687
  };
546
688
  }
547
- export function toOpenAiMessages(messages, supportsVision = true) {
689
+ export function toOpenAiMessages(messages, supportsVision = true, replay) {
548
690
  return toOpenAiToolMessages(messages, (message) => {
549
691
  if (supportsVision &&
550
692
  message.images &&
@@ -564,44 +706,12 @@ export function toOpenAiMessages(messages, supportsVision = true) {
564
706
  return parts;
565
707
  }
566
708
  return message.content;
567
- });
568
- }
569
- export function classifyNvidiaModel(model) {
570
- const m = model.toLowerCase();
571
- if (/kimi-k2(?:\.6|-thinking|-instruct)?/.test(m))
572
- return "kimi-thinking";
573
- if (/deepseek-v4/.test(m))
574
- return "deepseek-v4";
575
- // Match newer Nemotron-3 (uses enable_thinking + reasoning_budget) before
576
- // the legacy Nemotron pattern below — the older `nemotron` bucket would
577
- // otherwise swallow these too.
578
- if (/nemotron-3/.test(m))
579
- return "nemotron-3";
580
- if (/glm-?[345]/.test(m))
581
- return "glm-thinking";
582
- if (/gemma-?[34]/.test(m))
583
- return "enable-thinking";
584
- if (/deepseek-(?:v3|r1)|nemotron/.test(m))
585
- return "thinking";
586
- if (/gpt-oss|qwen3|mistral-(?:medium|small|large)-(?:[3-9]|\d{2,})/.test(m))
587
- return "effort-only";
588
- return "none";
709
+ }, replay);
589
710
  }
711
+ export { classifyBynaraModel, classifyNvidiaModel, } from "./model-families.js";
590
712
  function supportsOpenRouterReasoning(model) {
591
713
  return /:thinking|deepseek-r1|qwen3|kimi-k2|claude-(?:opus|sonnet|haiku)-4|gpt-5|(?:^|\/)o[134]|grok.*reasoner/i.test(model);
592
714
  }
593
- export function classifyBynaraModel(model) {
594
- const m = model.toLowerCase();
595
- if (/kimi/.test(m))
596
- return "kimi";
597
- if (/deepseek/.test(m))
598
- return "deepseek";
599
- if (/agnes/.test(m))
600
- return "agnes";
601
- if (/stepfun|step-3/.test(m))
602
- return "stepfun";
603
- return "none";
604
- }
605
715
  const MODAL_DEFAULT_EFFORTS = ["low", "high", "max"];
606
716
  const MODAL_EFFORT_PREFERENCE = {
607
717
  minimal: ["minimal", "low", "medium", "high", "max"],
@@ -933,7 +1043,7 @@ export function isOpenAiReasoningModel(model) {
933
1043
  /(?:^|\/)o[134](?:\.|-|$)/.test(m) ||
934
1044
  /muse-spark/.test(m));
935
1045
  }
936
- export function buildChatBody(options) {
1046
+ function emitChatCompletionsBody(options) {
937
1047
  // Skip reasoning knobs entirely for models observed to reject them this
938
1048
  // session (see isReasoningUnsupportedError). This is how thinking degrades
939
1049
  // gracefully: the request still runs, just without the unsupported option.
@@ -972,8 +1082,16 @@ export function buildChatBody(options) {
972
1082
  : (options.maxTokens ?? defaultMaxTokens);
973
1083
  const body = {
974
1084
  model: options.model,
975
- messages: toOpenAiMessages(options.messages, options.supportsVision),
1085
+ messages: toOpenAiMessages(singleLeadingSystemMessages(options.messages), options.supportsVision, options.replayTarget
1086
+ ? {
1087
+ target: options.replayTarget,
1088
+ observe: options.reasoningArtifactReplayObserver,
1089
+ }
1090
+ : undefined),
976
1091
  stream: options.stream,
1092
+ ...(options.providerId === "fireworks"
1093
+ ? { perf_metrics_in_response: true }
1094
+ : {}),
977
1095
  ...(reasoningModel
978
1096
  ? { max_completion_tokens: effectiveMaxTokens }
979
1097
  : { max_tokens: effectiveMaxTokens }),
@@ -998,6 +1116,35 @@ export function buildChatBody(options) {
998
1116
  }
999
1117
  return JSON.stringify(body);
1000
1118
  }
1119
+ export function buildChatBody(options) {
1120
+ return emitChatCompletionsBody(options);
1121
+ }
1122
+ /**
1123
+ * Serializes a compiled canonical plan onto the Chat Completions wire. Wire
1124
+ * dialect knobs that the plan intentionally does not model (gateway reasoning
1125
+ * style, stream-usage flag, replay observer) stay serializer-side extras.
1126
+ */
1127
+ export function chatCompletionsBodyFromPlan(plan, extras = {}) {
1128
+ return emitChatCompletionsBody({
1129
+ model: plan.route.model,
1130
+ providerId: plan.route.provider,
1131
+ messages: [...plan.timeline.messages],
1132
+ maxTokens: plan.controls.requestedMaxTokens,
1133
+ temperature: plan.controls.temperature,
1134
+ stream: plan.controls.stream,
1135
+ includeStreamUsage: extras.includeStreamUsage,
1136
+ reasoning: plan.controls.reasoning,
1137
+ reasoningStyle: extras.reasoningStyle,
1138
+ supportsVision: plan.images.visionAccepted,
1139
+ tools: plan.tools.definitions.length
1140
+ ? [...plan.tools.definitions]
1141
+ : undefined,
1142
+ toolChoice: plan.tools.choice,
1143
+ parallelToolCalls: plan.tools.parallelToolCalls,
1144
+ replayTarget: plan.replay.target,
1145
+ reasoningArtifactReplayObserver: extras.reasoningArtifactReplayObserver,
1146
+ });
1147
+ }
1001
1148
  function sentReasoningEffort(requestBody) {
1002
1149
  try {
1003
1150
  const body = JSON.parse(requestBody);
@@ -1021,35 +1168,27 @@ function privateReasoningNote(provider, requestBody, reasoningTokens) {
1021
1168
  const effortText = effort ? ` at ${effort} effort` : "";
1022
1169
  return `Reasoning is private on ${provider}: the model reasoned${effortText} and used ${reasoningTokens.toLocaleString("en-US")} reasoning tokens, but the API returns no reasoning text to display.`;
1023
1170
  }
1024
- function foldReasoning(provider, requestBody, text, reasoning, usage) {
1025
- if (reasoning && reasoning.trim()) {
1026
- return `${wrapReasoning(reasoning)}${text}`;
1027
- }
1028
- const tokens = usage?.reasoningTokens ?? 0;
1029
- if (tokens > 0) {
1030
- return `${wrapReasoning(privateReasoningNote(provider, requestBody, tokens))}${text}`;
1031
- }
1032
- return text;
1033
- }
1034
1171
  export async function openAiCompatibleComplete(options) {
1035
- const supportsVision = modelAcceptsImages(options.providerId, options.model);
1036
- const requestBody = buildChatBody({
1172
+ const plan = compileRequestPlan({
1173
+ provider: options.providerId,
1037
1174
  model: options.model,
1038
- providerId: options.providerId,
1039
1175
  messages: options.messages,
1040
- maxTokens: options.maxTokens,
1041
- temperature: options.temperature,
1042
1176
  stream: false,
1177
+ endpoint: options.baseUrl,
1043
1178
  reasoning: options.reasoning,
1044
- reasoningStyle: options.reasoningStyle,
1045
- supportsVision,
1046
1179
  tools: options.tools,
1047
1180
  toolChoice: options.toolChoice,
1048
1181
  parallelToolCalls: options.parallelToolCalls,
1182
+ temperature: options.temperature,
1183
+ maxTokens: options.maxTokens,
1184
+ });
1185
+ const requestBody = chatCompletionsBodyFromPlan(plan, {
1186
+ reasoningStyle: options.reasoningStyle,
1187
+ reasoningArtifactReplayObserver: options.reasoningArtifactReplayObserver,
1049
1188
  });
1050
1189
  let response;
1051
1190
  try {
1052
- response = await fetch(`${options.baseUrl}/chat/completions`, {
1191
+ response = await generationFetch(`${options.baseUrl}/chat/completions`, {
1053
1192
  method: "POST",
1054
1193
  signal: options.signal ?? null,
1055
1194
  headers: {
@@ -1067,6 +1206,8 @@ export async function openAiCompatibleComplete(options) {
1067
1206
  catch (error) {
1068
1207
  if (error instanceof Error && error.name === "AbortError")
1069
1208
  throw error;
1209
+ if (isOperationPolicyError(error))
1210
+ throw error;
1070
1211
  const msg = error instanceof Error ? error.message : String(error);
1071
1212
  throw new ProviderError(`${options.provider} request could not be sent (${msg}). Check connectivity to ${options.baseUrl}.`);
1072
1213
  }
@@ -1089,14 +1230,42 @@ export async function openAiCompatibleComplete(options) {
1089
1230
  }
1090
1231
  // If the API returns reasoning separately, prepend it inside <think>
1091
1232
  // tags so the existing thinking parser can pick it up uniformly.
1092
- const usage = parseOpenAiUsage(data.usage);
1233
+ // Fireworks exposes cache metrics in headers for complete calls and can also
1234
+ // include them in perf_metrics. Other compatible routes use their standard
1235
+ // usage object plus any explicitly configured aliases.
1236
+ const usage = options.providerId === "fireworks"
1237
+ ? parseFireworksUsage(data.usage, data.perf_metrics, response.headers)
1238
+ : parseOpenAiUsage(data.usage, options.usageAliases);
1093
1239
  const reasoning = message?.reasoning_content ?? message?.reasoning;
1240
+ const detailsRaw = artifactRaw(message?.reasoning_details);
1241
+ const thoughtSignature = message?.extra_content?.google?.thought_signature;
1242
+ const reasoningArtifacts = compatibleReasoningArtifacts({
1243
+ providerId: options.providerId,
1244
+ model: options.model,
1245
+ baseUrl: options.baseUrl,
1246
+ toolCalls,
1247
+ policy: options.reasoningArtifactPolicy,
1248
+ ...(typeof reasoning === "string" && reasoning
1249
+ ? { reasoning: { text: reasoning, sequence: 0 } }
1250
+ : {}),
1251
+ ...(detailsRaw ? { details: [{ raw: detailsRaw, sequence: 1 }] } : {}),
1252
+ ...(thoughtSignature
1253
+ ? {
1254
+ thoughtSignatures: [
1255
+ {
1256
+ raw: thoughtSignature,
1257
+ sequence: 2,
1258
+ ...(toolCalls.length ? { toolCallIndex: 0 } : {}),
1259
+ },
1260
+ ],
1261
+ }
1262
+ : {}),
1263
+ });
1094
1264
  if (typeof reasoning === "string" && reasoning.trim()) {
1095
1265
  learnModelEmitsReasoning(options.providerId, options.model);
1096
1266
  }
1097
- const full = foldReasoning(options.provider, requestBody, text, reasoning, usage);
1098
1267
  return {
1099
- text: full,
1268
+ text,
1100
1269
  ...(toolCalls.length ? { toolCalls } : {}),
1101
1270
  ...(choice?.finish_reason
1102
1271
  ? { finishReason: choice.finish_reason }
@@ -1104,6 +1273,10 @@ export async function openAiCompatibleComplete(options) {
1104
1273
  ? { finishReason: "tool_calls" }
1105
1274
  : {}),
1106
1275
  ...(usage ? { usage } : {}),
1276
+ ...(typeof reasoning === "string" && reasoning
1277
+ ? { reasoningBlock: { text: reasoning } }
1278
+ : {}),
1279
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1107
1280
  };
1108
1281
  }
1109
1282
  export async function openAiCompatibleStream(options) {
@@ -1184,25 +1357,27 @@ export async function openAiCompatibleStream(options) {
1184
1357
  };
1185
1358
  const onCallerAbort = () => idleController.abort(options.signal?.reason);
1186
1359
  options.signal?.addEventListener("abort", onCallerAbort, { once: true });
1187
- const supportsVision = modelAcceptsImages(options.providerId, options.model);
1188
- const requestBody = buildChatBody({
1360
+ const plan = compileRequestPlan({
1361
+ provider: options.providerId,
1189
1362
  model: options.model,
1190
- providerId: options.providerId,
1191
1363
  messages: options.messages,
1192
- maxTokens: options.maxTokens,
1193
- temperature: options.temperature,
1194
1364
  stream: true,
1195
- includeStreamUsage: options.includeStreamUsage,
1365
+ endpoint: options.baseUrl,
1196
1366
  reasoning: options.reasoning,
1197
- reasoningStyle: options.reasoningStyle,
1198
- supportsVision,
1199
1367
  tools: options.tools,
1200
1368
  toolChoice: options.toolChoice,
1201
1369
  parallelToolCalls: options.parallelToolCalls,
1370
+ temperature: options.temperature,
1371
+ maxTokens: options.maxTokens,
1372
+ });
1373
+ const requestBody = chatCompletionsBodyFromPlan(plan, {
1374
+ reasoningStyle: options.reasoningStyle,
1375
+ includeStreamUsage: options.includeStreamUsage,
1376
+ reasoningArtifactReplayObserver: options.reasoningArtifactReplayObserver,
1202
1377
  });
1203
1378
  let response;
1204
1379
  try {
1205
- response = await fetch(`${options.baseUrl}/chat/completions`, {
1380
+ response = await generationFetch(`${options.baseUrl}/chat/completions`, {
1206
1381
  method: "POST",
1207
1382
  signal: idleController.signal,
1208
1383
  headers: {
@@ -1245,35 +1420,84 @@ export async function openAiCompatibleStream(options) {
1245
1420
  }
1246
1421
  const contentType = response.headers.get("content-type") ?? "";
1247
1422
  if (response.status === 202 || /\bapplication\/json\b/i.test(contentType)) {
1248
- clearIdleTimers();
1249
- options.signal?.removeEventListener("abort", onCallerAbort);
1250
- const data = await readJson(response);
1251
- if (response.status === 202) {
1252
- const requestId = data.requestId ?? data.id;
1253
- throw new ProviderError(`${options.provider} returned a pending async response${requestId ? ` (${requestId})` : ""}; streaming did not start.`, response.status, JSON.stringify(data).slice(0, 1_000));
1254
- }
1255
- const choice = data.choices?.[0];
1256
- const message = choice?.message;
1257
- const toolCalls = parseOpenAiMessageToolCalls(message?.tool_calls);
1258
- const text = message?.content ?? "";
1259
- const jsonUsage = parseOpenAiUsage(data.usage);
1260
- const reasoning = message?.reasoning_content ?? message?.reasoning;
1261
- const full = foldReasoning(options.provider, requestBody, text, reasoning, jsonUsage);
1262
- if (full.trim() || toolCalls.length > 0) {
1263
- if (full.trim())
1264
- options.onToken(full);
1265
- return {
1266
- text: full,
1267
- ...(toolCalls.length ? { toolCalls } : {}),
1268
- ...(choice?.finish_reason
1269
- ? { finishReason: choice.finish_reason }
1270
- : toolCalls.length
1271
- ? { finishReason: "tool_calls" }
1423
+ try {
1424
+ const data = await readJson(response, idleController.signal);
1425
+ if (response.status === 202) {
1426
+ const requestId = data.requestId ?? data.id;
1427
+ throw new ProviderError(`${options.provider} returned a pending async response${requestId ? ` (${requestId})` : ""}; streaming did not start.`, response.status, JSON.stringify(data).slice(0, 1_000));
1428
+ }
1429
+ const choice = data.choices?.[0];
1430
+ const message = choice?.message;
1431
+ const toolCalls = parseOpenAiMessageToolCalls(message?.tool_calls);
1432
+ const text = message?.content ?? "";
1433
+ const jsonUsage = options.providerId === "fireworks"
1434
+ ? parseFireworksUsage(data.usage, data.perf_metrics, response.headers)
1435
+ : parseOpenAiUsage(data.usage, options.usageAliases);
1436
+ const reasoning = message?.reasoning_content ?? message?.reasoning;
1437
+ const detailsRaw = artifactRaw(message?.reasoning_details);
1438
+ const thoughtSignature = message?.extra_content?.google?.thought_signature;
1439
+ const reasoningArtifacts = compatibleReasoningArtifacts({
1440
+ providerId: options.providerId,
1441
+ model: options.model,
1442
+ baseUrl: options.baseUrl,
1443
+ toolCalls,
1444
+ policy: options.reasoningArtifactPolicy,
1445
+ ...(typeof reasoning === "string" && reasoning
1446
+ ? { reasoning: { text: reasoning, sequence: 0 } }
1447
+ : {}),
1448
+ ...(detailsRaw ? { details: [{ raw: detailsRaw, sequence: 1 }] } : {}),
1449
+ ...(thoughtSignature
1450
+ ? {
1451
+ thoughtSignatures: [
1452
+ {
1453
+ raw: thoughtSignature,
1454
+ sequence: 2,
1455
+ ...(toolCalls.length ? { toolCallIndex: 0 } : {}),
1456
+ },
1457
+ ],
1458
+ }
1459
+ : {}),
1460
+ });
1461
+ if (text.trim() ||
1462
+ toolCalls.length > 0 ||
1463
+ (typeof reasoning === "string" && reasoning.trim())) {
1464
+ emitStreamReasoningArtifacts(options.onStreamEvent, reasoningArtifacts);
1465
+ if (text)
1466
+ options.onToken(text);
1467
+ return {
1468
+ text,
1469
+ ...(toolCalls.length ? { toolCalls } : {}),
1470
+ ...(choice?.finish_reason
1471
+ ? { finishReason: choice.finish_reason }
1472
+ : toolCalls.length
1473
+ ? { finishReason: "tool_calls" }
1474
+ : {}),
1475
+ ...(jsonUsage ? { usage: jsonUsage } : {}),
1476
+ ...(typeof reasoning === "string" && reasoning
1477
+ ? { reasoningBlock: { text: reasoning } }
1272
1478
  : {}),
1273
- ...(jsonUsage ? { usage: jsonUsage } : {}),
1274
- };
1479
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1480
+ };
1481
+ }
1482
+ throw new ProviderError(`${options.provider} returned JSON instead of an SSE stream, but no completion text was present.`, response.status, JSON.stringify(data).slice(0, 1_000));
1483
+ }
1484
+ catch (error) {
1485
+ if (idleFired) {
1486
+ const seconds = Math.round(firedBudgetMs / 1000);
1487
+ if (firedWatchdog === "transport" || !sawTransportActivity) {
1488
+ if (!sawTransportActivity) {
1489
+ throw new ProviderError(`${options.provider} request timed out before any response (${seconds}s) — no data arrived on the connection.`);
1490
+ }
1491
+ throw new ProviderError(`${options.provider} stream transport timeout (${seconds}s) — no data arrived on the connection after it had started.`);
1492
+ }
1493
+ throw new ProviderError(`${options.provider} stream stalled — ${STREAM_STALL_MARKER} for ${seconds}s`);
1494
+ }
1495
+ throw error;
1496
+ }
1497
+ finally {
1498
+ clearIdleTimers();
1499
+ options.signal?.removeEventListener("abort", onCallerAbort);
1275
1500
  }
1276
- throw new ProviderError(`${options.provider} returned JSON instead of an SSE stream, but no completion text was present.`, response.status, JSON.stringify(data).slice(0, 1_000));
1277
1501
  }
1278
1502
  const decoder = new TextDecoder();
1279
1503
  const reader = response.body.getReader();
@@ -1283,30 +1507,63 @@ export async function openAiCompatibleStream(options) {
1283
1507
  let reasoningSeen = "";
1284
1508
  let contentWireSeen = "";
1285
1509
  let reasoningWireSeen = "";
1286
- let inReasoning = false;
1287
1510
  let finishReason;
1288
- let streamUsage;
1511
+ let terminalSignal;
1512
+ const terminalPolicy = options.streamTerminal ?? CHAT_COMPLETIONS_STREAM_TERMINAL;
1513
+ const emittedByteCounts = () => {
1514
+ let toolArgumentBytes = 0;
1515
+ for (const state of toolCallState.values()) {
1516
+ toolArgumentBytes += state.arguments.length;
1517
+ }
1518
+ return {
1519
+ answerBytes: visible.length,
1520
+ reasoningBytes: reasoningSeen.length,
1521
+ toolArgumentBytes,
1522
+ };
1523
+ };
1524
+ let streamUsage = options.providerId === "fireworks"
1525
+ ? parseFireworksUsage(undefined, undefined, response.headers)
1526
+ : undefined;
1289
1527
  const toolCallState = new Map();
1528
+ let reasoningArtifactSequence;
1529
+ let nextArtifactSequence = 0;
1530
+ let lastToolCallIndex;
1531
+ const structuredDetails = [];
1532
+ const thoughtSignatures = [];
1533
+ const pendingThoughtSignatures = [];
1534
+ const finalReasoningArtifacts = (toolCalls) => {
1535
+ if (pendingThoughtSignatures.length) {
1536
+ const toolCallIndex = toolCalls.length ? 0 : undefined;
1537
+ for (const capture of pendingThoughtSignatures.splice(0)) {
1538
+ thoughtSignatures.push(toolCallIndex === undefined
1539
+ ? capture
1540
+ : { ...capture, toolCallIndex });
1541
+ }
1542
+ }
1543
+ return compatibleReasoningArtifacts({
1544
+ providerId: options.providerId,
1545
+ model: options.model,
1546
+ baseUrl: options.baseUrl,
1547
+ toolCalls,
1548
+ policy: options.reasoningArtifactPolicy,
1549
+ ...(reasoningSeen
1550
+ ? {
1551
+ reasoning: {
1552
+ text: reasoningSeen,
1553
+ sequence: reasoningArtifactSequence ?? 0,
1554
+ },
1555
+ }
1556
+ : {}),
1557
+ ...(structuredDetails.length ? { details: structuredDetails } : {}),
1558
+ ...(thoughtSignatures.length ? { thoughtSignatures } : {}),
1559
+ });
1560
+ };
1290
1561
  const normalizeChannelDelta = (token, seen) => {
1291
1562
  if (seen.length >= 64 && token.length >= seen.length && token.startsWith(seen)) {
1292
1563
  return { delta: token.slice(seen.length), seen: token };
1293
1564
  }
1294
1565
  return { delta: token, seen: seen + token };
1295
1566
  };
1296
- const enterReasoning = () => {
1297
- if (inReasoning)
1298
- return;
1299
- inReasoning = true;
1300
- full += REASONING_OPEN;
1301
- options.onToken(REASONING_OPEN);
1302
- };
1303
- const exitReasoning = () => {
1304
- if (!inReasoning)
1305
- return;
1306
- inReasoning = false;
1307
- full += REASONING_CLOSE;
1308
- options.onToken(REASONING_CLOSE);
1309
- };
1310
1567
  const emitPrivateReasoningNote = (hasToolCalls) => {
1311
1568
  if (reasoningSeen.trim())
1312
1569
  return;
@@ -1316,10 +1573,7 @@ export async function openAiCompatibleStream(options) {
1316
1573
  if (tokens <= 0)
1317
1574
  return;
1318
1575
  const note = privateReasoningNote(options.provider, requestBody, tokens);
1319
- enterReasoning();
1320
- full += note;
1321
- options.onToken(note);
1322
- exitReasoning();
1576
+ emitStreamReasoningDelta(options.onStreamEvent, note);
1323
1577
  };
1324
1578
  /**
1325
1579
  * Reasoning-echo suppression.
@@ -1343,8 +1597,6 @@ export async function openAiCompatibleStream(options) {
1343
1597
  const emitVisible = (text) => {
1344
1598
  if (!text)
1345
1599
  return;
1346
- if (inReasoning)
1347
- exitReasoning();
1348
1600
  visible += text;
1349
1601
  full += text;
1350
1602
  options.onToken(text);
@@ -1357,9 +1609,7 @@ export async function openAiCompatibleStream(options) {
1357
1609
  const emitReasoningEcho = (text) => {
1358
1610
  if (!text)
1359
1611
  return;
1360
- enterReasoning();
1361
- full += text;
1362
- options.onToken(text);
1612
+ emitStreamReasoningDelta(options.onStreamEvent, text);
1363
1613
  };
1364
1614
  /** Release a still-undecided hold-back as the answer (stream ended early). */
1365
1615
  const flushEchoBuffer = () => {
@@ -1453,11 +1703,19 @@ export async function openAiCompatibleStream(options) {
1453
1703
  if (payload === undefined)
1454
1704
  continue;
1455
1705
  if (payload === "[DONE]") {
1706
+ terminalSignal = "done-sentinel";
1707
+ requireTerminalProof({
1708
+ provider: options.provider,
1709
+ policy: terminalPolicy,
1710
+ signal: terminalSignal,
1711
+ ...emittedByteCounts(),
1712
+ });
1456
1713
  flushEchoBuffer();
1457
- exitReasoning();
1458
1714
  cleanup();
1459
1715
  const toolCalls = finalizeOpenAiToolCalls(toolCallState);
1460
1716
  emitPrivateReasoningNote(toolCalls.length > 0);
1717
+ const reasoningArtifacts = finalReasoningArtifacts(toolCalls);
1718
+ emitStreamReasoningArtifacts(options.onStreamEvent, reasoningArtifacts);
1461
1719
  if (!visible.trim() && toolCalls.length === 0) {
1462
1720
  // Thinking models (Kimi/Moonshot via Mantle, etc.) often emit only
1463
1721
  // reasoning, sometimes with tool sentinels inside <think>. Returning
@@ -1468,6 +1726,8 @@ export async function openAiCompatibleStream(options) {
1468
1726
  text: full,
1469
1727
  ...(finishReason ? { finishReason } : { finishReason: "stop" }),
1470
1728
  ...(streamUsage ? { usage: streamUsage } : {}),
1729
+ ...(reasoningSeen ? { reasoningBlock: { text: reasoningSeen } } : {}),
1730
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1471
1731
  };
1472
1732
  }
1473
1733
  throw new ProviderError(`${options.provider} completed without a visible answer.`);
@@ -1481,6 +1741,8 @@ export async function openAiCompatibleStream(options) {
1481
1741
  ? { finishReason: "tool_calls" }
1482
1742
  : {}),
1483
1743
  ...(streamUsage ? { usage: streamUsage } : {}),
1744
+ ...(reasoningSeen ? { reasoningBlock: { text: reasoningSeen } } : {}),
1745
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1484
1746
  };
1485
1747
  }
1486
1748
  let parsed;
@@ -1504,13 +1766,18 @@ export async function openAiCompatibleStream(options) {
1504
1766
  throw new ProviderError(`${options.provider} stream error: ${detail}`, undefined, payload.slice(0, 500));
1505
1767
  }
1506
1768
  {
1507
- const chunkUsage = parseOpenAiUsage(parsed.usage);
1769
+ const chunkUsage = options.providerId === "fireworks"
1770
+ ? parseFireworksUsage(parsed.usage, parsed.perf_metrics, response.headers)
1771
+ : parseOpenAiUsage(parsed.usage, options.usageAliases);
1508
1772
  if (chunkUsage)
1509
1773
  streamUsage = chunkUsage;
1510
1774
  const choice = parsed.choices?.[0];
1511
1775
  const delta = choice?.delta;
1512
1776
  const reasoningToken = delta?.reasoning_content ?? delta?.reasoning;
1513
1777
  const token = delta?.content;
1778
+ const detailRaw = artifactRaw(delta?.reasoning_details);
1779
+ const thoughtSignature = delta?.extra_content?.google?.thought_signature;
1780
+ const artifactSequence = nextArtifactSequence++;
1514
1781
  const toolProgress = delta?.tool_calls?.some((toolCall) => Boolean(toolCall.id ||
1515
1782
  toolCall.function?.name ||
1516
1783
  toolCall.function?.arguments));
@@ -1518,63 +1785,101 @@ export async function openAiCompatibleStream(options) {
1518
1785
  chunkUsage ||
1519
1786
  reasoningToken ||
1520
1787
  token ||
1788
+ detailRaw ||
1789
+ thoughtSignature ||
1521
1790
  toolProgress) {
1522
1791
  resetIdleTimer();
1523
1792
  }
1524
- if (choice?.finish_reason)
1793
+ if (choice?.finish_reason) {
1525
1794
  finishReason = choice.finish_reason;
1795
+ terminalSignal = "finish-reason";
1796
+ }
1526
1797
  if (reasoningToken) {
1527
1798
  const normalized = normalizeChannelDelta(reasoningToken, reasoningWireSeen);
1528
1799
  reasoningWireSeen = normalized.seen;
1529
1800
  if (normalized.delta) {
1801
+ reasoningArtifactSequence ??= artifactSequence;
1530
1802
  if (!reasoningSeen) {
1531
1803
  learnModelEmitsReasoning(options.providerId, options.model);
1532
1804
  }
1533
- enterReasoning();
1534
1805
  reasoningSeen += normalized.delta;
1535
- full += normalized.delta;
1536
- options.onToken(normalized.delta);
1806
+ emitStreamReasoningDelta(options.onStreamEvent, normalized.delta);
1537
1807
  }
1538
1808
  }
1809
+ if (detailRaw) {
1810
+ structuredDetails.push({ raw: detailRaw, sequence: artifactSequence });
1811
+ }
1539
1812
  if (token) {
1540
1813
  const normalized = normalizeChannelDelta(token, contentWireSeen);
1541
1814
  contentWireSeen = normalized.seen;
1542
1815
  if (normalized.delta)
1543
1816
  handleContentToken(normalized.delta);
1544
1817
  }
1818
+ const deltaToolCallIndices = [];
1545
1819
  if (delta?.tool_calls?.length) {
1546
1820
  for (const tc of delta.tool_calls) {
1547
1821
  const accInfo = accumulateOpenAiToolCallDelta(toolCallState, tc);
1548
- if (!options.onToolCallDelta)
1549
- continue;
1550
- const wire = accInfo.name;
1551
- const canonical = wire
1552
- ? (fromWireName(wire) ?? wire)
1553
- : undefined;
1554
- const largeArgTick = !accInfo.nameBecameKnown &&
1555
- accInfo.argumentsBytes > 0 &&
1556
- accInfo.argumentsBytes % 4096 <
1557
- (typeof tc.function?.arguments === "string"
1558
- ? tc.function.arguments.length
1559
- : 0);
1560
- if (accInfo.nameBecameKnown || largeArgTick) {
1561
- options.onToolCallDelta({
1562
- index: accInfo.index,
1563
- ...(accInfo.id !== undefined ? { id: accInfo.id } : {}),
1564
- ...(canonical !== undefined ? { name: canonical } : {}),
1565
- argumentsBytes: accInfo.argumentsBytes,
1566
- });
1822
+ deltaToolCallIndices.push(accInfo.index);
1823
+ lastToolCallIndex = accInfo.index;
1824
+ if (options.onToolCallDelta) {
1825
+ const wire = accInfo.name;
1826
+ const canonical = wire
1827
+ ? (fromWireName(wire) ?? wire)
1828
+ : undefined;
1829
+ const largeArgTick = !accInfo.nameBecameKnown &&
1830
+ accInfo.argumentsBytes > 0 &&
1831
+ accInfo.argumentsBytes % 4096 <
1832
+ (typeof tc.function?.arguments === "string"
1833
+ ? tc.function.arguments.length
1834
+ : 0);
1835
+ if (accInfo.nameBecameKnown || largeArgTick) {
1836
+ options.onToolCallDelta({
1837
+ index: accInfo.index,
1838
+ ...(accInfo.id !== undefined ? { id: accInfo.id } : {}),
1839
+ ...(canonical !== undefined ? { name: canonical } : {}),
1840
+ argumentsBytes: accInfo.argumentsBytes,
1841
+ });
1842
+ }
1567
1843
  }
1568
1844
  }
1569
1845
  }
1846
+ const signatureToolCallIndex = deltaToolCallIndices[0] ?? lastToolCallIndex;
1847
+ if (thoughtSignature) {
1848
+ const capture = {
1849
+ raw: thoughtSignature,
1850
+ sequence: artifactSequence,
1851
+ ...(signatureToolCallIndex === undefined
1852
+ ? {}
1853
+ : { toolCallIndex: signatureToolCallIndex }),
1854
+ };
1855
+ if (signatureToolCallIndex === undefined) {
1856
+ pendingThoughtSignatures.push(capture);
1857
+ }
1858
+ else {
1859
+ thoughtSignatures.push(capture);
1860
+ }
1861
+ }
1862
+ if (deltaToolCallIndices.length && pendingThoughtSignatures.length) {
1863
+ const toolCallIndex = deltaToolCallIndices[0];
1864
+ for (const capture of pendingThoughtSignatures.splice(0)) {
1865
+ thoughtSignatures.push({ ...capture, toolCallIndex });
1866
+ }
1867
+ }
1570
1868
  }
1571
1869
  }
1572
1870
  }
1573
1871
  flushEchoBuffer();
1574
- exitReasoning();
1575
1872
  cleanup();
1873
+ requireTerminalProof({
1874
+ provider: options.provider,
1875
+ policy: terminalPolicy,
1876
+ signal: terminalSignal,
1877
+ ...emittedByteCounts(),
1878
+ });
1576
1879
  const toolCalls = finalizeOpenAiToolCalls(toolCallState);
1577
1880
  emitPrivateReasoningNote(toolCalls.length > 0);
1881
+ const reasoningArtifacts = finalReasoningArtifacts(toolCalls);
1882
+ emitStreamReasoningArtifacts(options.onStreamEvent, reasoningArtifacts);
1578
1883
  if (!visible.trim() && toolCalls.length === 0) {
1579
1884
  // See [DONE] branch — hand thinking-only streams to the runner so it can
1580
1885
  // salvage sentinel tool blocks (or nudge) instead of hard-failing.
@@ -1583,6 +1888,8 @@ export async function openAiCompatibleStream(options) {
1583
1888
  text: full,
1584
1889
  ...(finishReason ? { finishReason } : { finishReason: "stop" }),
1585
1890
  ...(streamUsage ? { usage: streamUsage } : {}),
1891
+ ...(reasoningSeen ? { reasoningBlock: { text: reasoningSeen } } : {}),
1892
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1586
1893
  };
1587
1894
  }
1588
1895
  throw new ProviderError(`${options.provider} completed without a visible answer.`);
@@ -1596,6 +1903,8 @@ export async function openAiCompatibleStream(options) {
1596
1903
  ? { finishReason: "tool_calls" }
1597
1904
  : {}),
1598
1905
  ...(streamUsage ? { usage: streamUsage } : {}),
1906
+ ...(reasoningSeen ? { reasoningBlock: { text: reasoningSeen } } : {}),
1907
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1599
1908
  };
1600
1909
  }
1601
1910
  catch (error) {