@pentoshi/clai 4.1.0 → 4.2.1

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 (266) hide show
  1. package/README.md +14 -0
  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/context-breakdown.js +2 -0
  6. package/dist/agent/context-breakdown.js.map +1 -1
  7. package/dist/agent/context-manager.d.ts +23 -9
  8. package/dist/agent/context-manager.js +72 -43
  9. package/dist/agent/context-manager.js.map +1 -1
  10. package/dist/agent/events.d.ts +3 -0
  11. package/dist/agent/loop-guard.d.ts +2 -0
  12. package/dist/agent/loop-guard.js +48 -3
  13. package/dist/agent/loop-guard.js.map +1 -1
  14. package/dist/agent/prompt-composer.js +6 -6
  15. package/dist/agent/prompt-composer.js.map +1 -1
  16. package/dist/agent/reliability-policy.d.ts +1 -0
  17. package/dist/agent/reliability-policy.js +6 -3
  18. package/dist/agent/reliability-policy.js.map +1 -1
  19. package/dist/agent/request-accounting.d.ts +97 -0
  20. package/dist/agent/request-accounting.js +207 -0
  21. package/dist/agent/request-accounting.js.map +1 -0
  22. package/dist/agent/request-budget.d.ts +2 -2
  23. package/dist/agent/request-budget.js +2 -4
  24. package/dist/agent/request-budget.js.map +1 -1
  25. package/dist/agent/runner.d.ts +8 -2
  26. package/dist/agent/runner.js +409 -178
  27. package/dist/agent/runner.js.map +1 -1
  28. package/dist/agent/session-title.d.ts +0 -26
  29. package/dist/agent/session-title.js +0 -80
  30. package/dist/agent/session-title.js.map +1 -1
  31. package/dist/agent/tool-history.d.ts +2 -2
  32. package/dist/agent/tool-history.js +19 -6
  33. package/dist/agent/tool-history.js.map +1 -1
  34. package/dist/app/adapters/agent-event-adapter.js +19 -0
  35. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  36. package/dist/app/adapters/current-agent-adapter.js +4 -0
  37. package/dist/app/adapters/current-agent-adapter.js.map +1 -1
  38. package/dist/app/commands/catalog.js +24 -0
  39. package/dist/app/commands/catalog.js.map +1 -1
  40. package/dist/app/controllers/cancel-coordinator.d.ts +51 -0
  41. package/dist/app/controllers/cancel-coordinator.js +48 -0
  42. package/dist/app/controllers/cancel-coordinator.js.map +1 -0
  43. package/dist/app/controllers/session-compact-helper.d.ts +19 -2
  44. package/dist/app/controllers/session-compact-helper.js +100 -127
  45. package/dist/app/controllers/session-compact-helper.js.map +1 -1
  46. package/dist/app/controllers/session-context-usage.d.ts +32 -15
  47. package/dist/app/controllers/session-context-usage.js +186 -51
  48. package/dist/app/controllers/session-context-usage.js.map +1 -1
  49. package/dist/app/controllers/session-controller.d.ts +20 -11
  50. package/dist/app/controllers/session-controller.js +111 -90
  51. package/dist/app/controllers/session-controller.js.map +1 -1
  52. package/dist/app/controllers/session-naming.d.ts +31 -0
  53. package/dist/app/controllers/session-naming.js +173 -0
  54. package/dist/app/controllers/session-naming.js.map +1 -0
  55. package/dist/app/controllers/session-persistence.d.ts +2 -2
  56. package/dist/app/controllers/session-persistence.js +3 -6
  57. package/dist/app/controllers/session-persistence.js.map +1 -1
  58. package/dist/app/controllers/session-turn-request.d.ts +2 -1
  59. package/dist/app/controllers/session-turn-request.js +3 -0
  60. package/dist/app/controllers/session-turn-request.js.map +1 -1
  61. package/dist/app/controllers/turn-controller.d.ts +2 -1
  62. package/dist/app/controllers/turn-controller.js +1 -0
  63. package/dist/app/controllers/turn-controller.js.map +1 -1
  64. package/dist/app/events/app-event.d.ts +9 -0
  65. package/dist/app/events/app-event.js.map +1 -1
  66. package/dist/app/ports/agent-port.d.ts +8 -1
  67. package/dist/classic/app/ClassicApp.js +2 -1
  68. package/dist/classic/app/ClassicApp.js.map +1 -1
  69. package/dist/classic/app/app-wiring.js +1 -2
  70. package/dist/classic/app/app-wiring.js.map +1 -1
  71. package/dist/classic/app/wiring-interactions.js +2 -7
  72. package/dist/classic/app/wiring-interactions.js.map +1 -1
  73. package/dist/classic/blocks/tool-lines.js +2 -1
  74. package/dist/classic/blocks/tool-lines.js.map +1 -1
  75. package/dist/classic/input/cancel-ladder.d.ts +2 -19
  76. package/dist/classic/input/cancel-ladder.js +13 -20
  77. package/dist/classic/input/cancel-ladder.js.map +1 -1
  78. package/dist/commands/providers.js +2 -0
  79. package/dist/commands/providers.js.map +1 -1
  80. package/dist/llm/adapters/anthropic-tools.d.ts +31 -14
  81. package/dist/llm/adapters/anthropic-tools.js +124 -29
  82. package/dist/llm/adapters/anthropic-tools.js.map +1 -1
  83. package/dist/llm/adapters/gemini-tools.d.ts +16 -4
  84. package/dist/llm/adapters/gemini-tools.js +117 -16
  85. package/dist/llm/adapters/gemini-tools.js.map +1 -1
  86. package/dist/llm/adapters/openai-tools.d.ts +13 -2
  87. package/dist/llm/adapters/openai-tools.js +58 -11
  88. package/dist/llm/adapters/openai-tools.js.map +1 -1
  89. package/dist/llm/agentrouter.js +68 -45
  90. package/dist/llm/agentrouter.js.map +1 -1
  91. package/dist/llm/anthropic.js +69 -36
  92. package/dist/llm/anthropic.js.map +1 -1
  93. package/dist/llm/aws-mantle.js +70 -32
  94. package/dist/llm/aws-mantle.js.map +1 -1
  95. package/dist/llm/bynara.js +26 -69
  96. package/dist/llm/bynara.js.map +1 -1
  97. package/dist/llm/capabilities.d.ts +2 -1
  98. package/dist/llm/capabilities.js +47 -0
  99. package/dist/llm/capabilities.js.map +1 -1
  100. package/dist/llm/context-snapshot.d.ts +100 -0
  101. package/dist/llm/context-snapshot.js +277 -0
  102. package/dist/llm/context-snapshot.js.map +1 -0
  103. package/dist/llm/context-windows.d.ts +10 -0
  104. package/dist/llm/context-windows.js +129 -0
  105. package/dist/llm/context-windows.js.map +1 -0
  106. package/dist/llm/custom-provider-profile.d.ts +75 -0
  107. package/dist/llm/custom-provider-profile.js +558 -0
  108. package/dist/llm/custom-provider-profile.js.map +1 -0
  109. package/dist/llm/custom-providers.d.ts +13 -1
  110. package/dist/llm/custom-providers.js +77 -25
  111. package/dist/llm/custom-providers.js.map +1 -1
  112. package/dist/llm/effort-fallback.d.ts +44 -0
  113. package/dist/llm/effort-fallback.js +101 -0
  114. package/dist/llm/effort-fallback.js.map +1 -0
  115. package/dist/llm/fireworks.js +4 -0
  116. package/dist/llm/fireworks.js.map +1 -1
  117. package/dist/llm/free.js +4 -0
  118. package/dist/llm/free.js.map +1 -1
  119. package/dist/llm/gemini.d.ts +1 -1
  120. package/dist/llm/gemini.js +89 -51
  121. package/dist/llm/gemini.js.map +1 -1
  122. package/dist/llm/groq.js +4 -0
  123. package/dist/llm/groq.js.map +1 -1
  124. package/dist/llm/hetzner.js +4 -0
  125. package/dist/llm/hetzner.js.map +1 -1
  126. package/dist/llm/http.d.ts +49 -10
  127. package/dist/llm/http.js +417 -153
  128. package/dist/llm/http.js.map +1 -1
  129. package/dist/llm/kimchi.js +4 -0
  130. package/dist/llm/kimchi.js.map +1 -1
  131. package/dist/llm/lightning.js +4 -0
  132. package/dist/llm/lightning.js.map +1 -1
  133. package/dist/llm/meta.d.ts +2 -0
  134. package/dist/llm/meta.js +811 -655
  135. package/dist/llm/meta.js.map +1 -1
  136. package/dist/llm/modal.js +4 -0
  137. package/dist/llm/modal.js.map +1 -1
  138. package/dist/llm/model-families.d.ts +9 -0
  139. package/dist/llm/model-families.js +39 -0
  140. package/dist/llm/model-families.js.map +1 -0
  141. package/dist/llm/nvidia.js +4 -0
  142. package/dist/llm/nvidia.js.map +1 -1
  143. package/dist/llm/ollama.js +44 -40
  144. package/dist/llm/ollama.js.map +1 -1
  145. package/dist/llm/openai.js +4 -0
  146. package/dist/llm/openai.js.map +1 -1
  147. package/dist/llm/openrouter.js +4 -0
  148. package/dist/llm/openrouter.js.map +1 -1
  149. package/dist/llm/operation-ledger.d.ts +60 -0
  150. package/dist/llm/operation-ledger.js +136 -0
  151. package/dist/llm/operation-ledger.js.map +1 -0
  152. package/dist/llm/operation-usage.d.ts +32 -0
  153. package/dist/llm/operation-usage.js +172 -0
  154. package/dist/llm/operation-usage.js.map +1 -0
  155. package/dist/llm/orcarouter.d.ts +7 -0
  156. package/dist/llm/orcarouter.js +166 -0
  157. package/dist/llm/orcarouter.js.map +1 -0
  158. package/dist/llm/provider-profile-layers.d.ts +7 -0
  159. package/dist/llm/provider-profile-layers.js +961 -0
  160. package/dist/llm/provider-profile-layers.js.map +1 -0
  161. package/dist/llm/provider-profile.d.ts +181 -0
  162. package/dist/llm/provider-profile.js +329 -0
  163. package/dist/llm/provider-profile.js.map +1 -0
  164. package/dist/llm/provider-profiles.d.ts +13 -0
  165. package/dist/llm/provider-profiles.js +86 -0
  166. package/dist/llm/provider-profiles.js.map +1 -0
  167. package/dist/llm/provider.d.ts +2 -0
  168. package/dist/llm/provider.js +67 -0
  169. package/dist/llm/provider.js.map +1 -1
  170. package/dist/llm/qwen-cloud.js +4 -0
  171. package/dist/llm/qwen-cloud.js.map +1 -1
  172. package/dist/llm/reasoning-artifacts.d.ts +101 -0
  173. package/dist/llm/reasoning-artifacts.js +440 -0
  174. package/dist/llm/reasoning-artifacts.js.map +1 -0
  175. package/dist/llm/request-fingerprint.d.ts +10 -0
  176. package/dist/llm/request-fingerprint.js +313 -0
  177. package/dist/llm/request-fingerprint.js.map +1 -0
  178. package/dist/llm/request-plan.d.ts +119 -0
  179. package/dist/llm/request-plan.js +279 -0
  180. package/dist/llm/request-plan.js.map +1 -0
  181. package/dist/llm/router.d.ts +19 -1
  182. package/dist/llm/router.js +447 -75
  183. package/dist/llm/router.js.map +1 -1
  184. package/dist/llm/stream-events.d.ts +47 -0
  185. package/dist/llm/stream-events.js +55 -0
  186. package/dist/llm/stream-events.js.map +1 -0
  187. package/dist/llm/stream-terminal.d.ts +30 -0
  188. package/dist/llm/stream-terminal.js +51 -0
  189. package/dist/llm/stream-terminal.js.map +1 -0
  190. package/dist/llm/token-estimate-calibration.d.ts +23 -0
  191. package/dist/llm/token-estimate-calibration.js +140 -0
  192. package/dist/llm/token-estimate-calibration.js.map +1 -0
  193. package/dist/llm/token-usage.d.ts +35 -5
  194. package/dist/llm/token-usage.js +212 -162
  195. package/dist/llm/token-usage.js.map +1 -1
  196. package/dist/llm/tokenrouter.js +8 -2
  197. package/dist/llm/tokenrouter.js.map +1 -1
  198. package/dist/modes/ask.js +11 -5
  199. package/dist/modes/ask.js.map +1 -1
  200. package/dist/noninteractive/start-noninteractive.js +31 -2
  201. package/dist/noninteractive/start-noninteractive.js.map +1 -1
  202. package/dist/noninteractive/stream-blocks.js +15 -4
  203. package/dist/noninteractive/stream-blocks.js.map +1 -1
  204. package/dist/prompts/embedded.js +1 -1
  205. package/dist/prompts/embedded.js.map +1 -1
  206. package/dist/prompts/system.agent.md +6 -0
  207. package/dist/store/config.js +5 -1
  208. package/dist/store/config.js.map +1 -1
  209. package/dist/store/history.d.ts +2 -0
  210. package/dist/store/history.js +19 -10
  211. package/dist/store/history.js.map +1 -1
  212. package/dist/store/keys.js +11 -1
  213. package/dist/store/keys.js.map +1 -1
  214. package/dist/store/plan.js +20 -14
  215. package/dist/store/plan.js.map +1 -1
  216. package/dist/tools/elevated-shell.d.ts +1 -1
  217. package/dist/tools/elevated-shell.js +24 -72
  218. package/dist/tools/elevated-shell.js.map +1 -1
  219. package/dist/tools/nmap-runner.js +30 -36
  220. package/dist/tools/nmap-runner.js.map +1 -1
  221. package/dist/tools/sudo-session.d.ts +83 -0
  222. package/dist/tools/sudo-session.js +124 -0
  223. package/dist/tools/sudo-session.js.map +1 -0
  224. package/dist/tui-v2/app/App.js +9 -24
  225. package/dist/tui-v2/app/App.js.map +1 -1
  226. package/dist/tui-v2/components/status/context-limit-chip.js +2 -2
  227. package/dist/tui-v2/components/status/context-limit-chip.js.map +1 -1
  228. package/dist/tui-v2/components/transcript/intro-card.js +2 -1
  229. package/dist/tui-v2/components/transcript/intro-card.js.map +1 -1
  230. package/dist/tui-v2/composer/composer-editor.js +2 -1
  231. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  232. package/dist/types.d.ts +156 -1
  233. package/dist/types.js +1 -0
  234. package/dist/types.js.map +1 -1
  235. package/dist/ui-core/bootstrap/composition-root.d.ts +2 -0
  236. package/dist/ui-core/bootstrap/composition-root.js +26 -3
  237. package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
  238. package/dist/ui-core/commands/picker-commands.js +11 -5
  239. package/dist/ui-core/commands/picker-commands.js.map +1 -1
  240. package/dist/ui-core/commands/session-commands.js +41 -5
  241. package/dist/ui-core/commands/session-commands.js.map +1 -1
  242. package/dist/ui-core/plan/plan-lifecycle.js +3 -2
  243. package/dist/ui-core/plan/plan-lifecycle.js.map +1 -1
  244. package/dist/ui-core/rendering/context-limit.js +4 -1
  245. package/dist/ui-core/rendering/context-limit.js.map +1 -1
  246. package/dist/ui-core/rendering/duration.d.ts +2 -1
  247. package/dist/ui-core/rendering/duration.js +13 -5
  248. package/dist/ui-core/rendering/duration.js.map +1 -1
  249. package/dist/ui-core/rendering/intro-header.js +2 -2
  250. package/dist/ui-core/rendering/intro-header.js.map +1 -1
  251. package/dist/ui-core/rendering/theme.d.ts +1 -0
  252. package/dist/ui-core/rendering/theme.js +2 -0
  253. package/dist/ui-core/rendering/theme.js.map +1 -1
  254. package/dist/ui-core/state/transcript-hydrate.js +3 -3
  255. package/dist/ui-core/state/transcript-hydrate.js.map +1 -1
  256. package/dist/ui-core/state/transcript-reducer.d.ts +5 -0
  257. package/dist/ui-core/state/transcript-reducer.js +76 -3
  258. package/dist/ui-core/state/transcript-reducer.js.map +1 -1
  259. package/dist/ui-core/state/transcript-store.d.ts +10 -1
  260. package/dist/ui-core/state/transcript-store.js +22 -1
  261. package/dist/ui-core/state/transcript-store.js.map +1 -1
  262. package/dist/ui-core/state/transcript-types.d.ts +2 -0
  263. package/dist/ui-core/state/transcript-types.js.map +1 -1
  264. package/dist/version.generated.d.ts +2 -2
  265. package/dist/version.generated.js +2 -2
  266. 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,8 +197,8 @@ 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 = "";
197
204
  let extractedMessage = "";
@@ -285,18 +292,30 @@ export function bodyAddsInformation(bodyText, extracted) {
285
292
  .trim();
286
293
  return rest.length > 0;
287
294
  }
288
- async function readBodyCapped(response, maxBytes) {
295
+ async function readBodyCapped(response, maxBytes, signal) {
289
296
  const reader = response.body?.getReader();
290
297
  if (!reader) {
298
+ if (signal?.aborted) {
299
+ throw signal.reason ?? new Error("Response body read aborted");
300
+ }
291
301
  const text = await response.text();
292
302
  return text.length > maxBytes ? text.slice(0, maxBytes) : text;
293
303
  }
294
304
  const decoder = new TextDecoder("utf-8", { fatal: false });
295
305
  let collected = "";
296
306
  let bytesRead = 0;
307
+ const cancelOnAbort = () => {
308
+ void reader.cancel(signal?.reason).catch(() => undefined);
309
+ };
310
+ signal?.addEventListener("abort", cancelOnAbort, { once: true });
297
311
  try {
298
312
  while (bytesRead < maxBytes) {
299
- 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
+ }
300
319
  if (done)
301
320
  break;
302
321
  if (!value)
@@ -321,6 +340,7 @@ async function readBodyCapped(response, maxBytes) {
321
340
  collected += decoder.decode();
322
341
  }
323
342
  finally {
343
+ signal?.removeEventListener("abort", cancelOnAbort);
324
344
  try {
325
345
  reader.releaseLock();
326
346
  }
@@ -343,8 +363,8 @@ async function readBodyCapped(response, maxBytes) {
343
363
  * re-generated the same prefix before one happened to finish inside the window.
344
364
  */
345
365
  export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 240_000;
346
- export const THINKING_STREAM_IDLE_TIMEOUT_MS = 600_000;
347
- 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;
348
368
  export function streamIdleBudgets(reasoningEnabled) {
349
369
  const idleTimeoutMs = reasoningEnabled
350
370
  ? THINKING_STREAM_IDLE_TIMEOUT_MS
@@ -528,6 +548,79 @@ export async function* readStreamLines(response, options = {}) {
528
548
  }
529
549
  }
530
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
+ }
531
624
  /** Map shared OpenAI-compatible payload → CompletionResult (includes usage). */
532
625
  export function toCompletionResult(provider, model, payload) {
533
626
  return {
@@ -537,6 +630,10 @@ export function toCompletionResult(provider, model, payload) {
537
630
  ...(payload.toolCalls?.length ? { toolCalls: payload.toolCalls } : {}),
538
631
  ...(payload.finishReason ? { finishReason: payload.finishReason } : {}),
539
632
  ...(payload.usage ? { usage: payload.usage } : {}),
633
+ ...(payload.reasoningBlock ? { reasoningBlock: payload.reasoningBlock } : {}),
634
+ ...(payload.reasoningArtifacts
635
+ ? { reasoningArtifacts: payload.reasoningArtifacts }
636
+ : {}),
540
637
  };
541
638
  }
542
639
  /**
@@ -589,7 +686,7 @@ export function createSseFrameAssembler(options) {
589
686
  },
590
687
  };
591
688
  }
592
- export function toOpenAiMessages(messages, supportsVision = true) {
689
+ export function toOpenAiMessages(messages, supportsVision = true, replay) {
593
690
  return toOpenAiToolMessages(messages, (message) => {
594
691
  if (supportsVision &&
595
692
  message.images &&
@@ -609,44 +706,12 @@ export function toOpenAiMessages(messages, supportsVision = true) {
609
706
  return parts;
610
707
  }
611
708
  return message.content;
612
- });
613
- }
614
- export function classifyNvidiaModel(model) {
615
- const m = model.toLowerCase();
616
- if (/kimi-k2(?:\.6|-thinking|-instruct)?/.test(m))
617
- return "kimi-thinking";
618
- if (/deepseek-v4/.test(m))
619
- return "deepseek-v4";
620
- // Match newer Nemotron-3 (uses enable_thinking + reasoning_budget) before
621
- // the legacy Nemotron pattern below — the older `nemotron` bucket would
622
- // otherwise swallow these too.
623
- if (/nemotron-3/.test(m))
624
- return "nemotron-3";
625
- if (/glm-?[345]/.test(m))
626
- return "glm-thinking";
627
- if (/gemma-?[34]/.test(m))
628
- return "enable-thinking";
629
- if (/deepseek-(?:v3|r1)|nemotron/.test(m))
630
- return "thinking";
631
- if (/gpt-oss|qwen3|mistral-(?:medium|small|large)-(?:[3-9]|\d{2,})/.test(m))
632
- return "effort-only";
633
- return "none";
709
+ }, replay);
634
710
  }
711
+ export { classifyBynaraModel, classifyNvidiaModel, } from "./model-families.js";
635
712
  function supportsOpenRouterReasoning(model) {
636
713
  return /:thinking|deepseek-r1|qwen3|kimi-k2|claude-(?:opus|sonnet|haiku)-4|gpt-5|(?:^|\/)o[134]|grok.*reasoner/i.test(model);
637
714
  }
638
- export function classifyBynaraModel(model) {
639
- const m = model.toLowerCase();
640
- if (/kimi/.test(m))
641
- return "kimi";
642
- if (/deepseek/.test(m))
643
- return "deepseek";
644
- if (/agnes/.test(m))
645
- return "agnes";
646
- if (/stepfun|step-3/.test(m))
647
- return "stepfun";
648
- return "none";
649
- }
650
715
  const MODAL_DEFAULT_EFFORTS = ["low", "high", "max"];
651
716
  const MODAL_EFFORT_PREFERENCE = {
652
717
  minimal: ["minimal", "low", "medium", "high", "max"],
@@ -978,7 +1043,7 @@ export function isOpenAiReasoningModel(model) {
978
1043
  /(?:^|\/)o[134](?:\.|-|$)/.test(m) ||
979
1044
  /muse-spark/.test(m));
980
1045
  }
981
- export function buildChatBody(options) {
1046
+ function emitChatCompletionsBody(options) {
982
1047
  // Skip reasoning knobs entirely for models observed to reject them this
983
1048
  // session (see isReasoningUnsupportedError). This is how thinking degrades
984
1049
  // gracefully: the request still runs, just without the unsupported option.
@@ -1017,8 +1082,16 @@ export function buildChatBody(options) {
1017
1082
  : (options.maxTokens ?? defaultMaxTokens);
1018
1083
  const body = {
1019
1084
  model: options.model,
1020
- 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),
1021
1091
  stream: options.stream,
1092
+ ...(options.providerId === "fireworks"
1093
+ ? { perf_metrics_in_response: true }
1094
+ : {}),
1022
1095
  ...(reasoningModel
1023
1096
  ? { max_completion_tokens: effectiveMaxTokens }
1024
1097
  : { max_tokens: effectiveMaxTokens }),
@@ -1043,6 +1116,35 @@ export function buildChatBody(options) {
1043
1116
  }
1044
1117
  return JSON.stringify(body);
1045
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
+ }
1046
1148
  function sentReasoningEffort(requestBody) {
1047
1149
  try {
1048
1150
  const body = JSON.parse(requestBody);
@@ -1066,35 +1168,27 @@ function privateReasoningNote(provider, requestBody, reasoningTokens) {
1066
1168
  const effortText = effort ? ` at ${effort} effort` : "";
1067
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.`;
1068
1170
  }
1069
- function foldReasoning(provider, requestBody, text, reasoning, usage) {
1070
- if (reasoning && reasoning.trim()) {
1071
- return `${wrapReasoning(reasoning)}${text}`;
1072
- }
1073
- const tokens = usage?.reasoningTokens ?? 0;
1074
- if (tokens > 0) {
1075
- return `${wrapReasoning(privateReasoningNote(provider, requestBody, tokens))}${text}`;
1076
- }
1077
- return text;
1078
- }
1079
1171
  export async function openAiCompatibleComplete(options) {
1080
- const supportsVision = modelAcceptsImages(options.providerId, options.model);
1081
- const requestBody = buildChatBody({
1172
+ const plan = compileRequestPlan({
1173
+ provider: options.providerId,
1082
1174
  model: options.model,
1083
- providerId: options.providerId,
1084
1175
  messages: options.messages,
1085
- maxTokens: options.maxTokens,
1086
- temperature: options.temperature,
1087
1176
  stream: false,
1177
+ endpoint: options.baseUrl,
1088
1178
  reasoning: options.reasoning,
1089
- reasoningStyle: options.reasoningStyle,
1090
- supportsVision,
1091
1179
  tools: options.tools,
1092
1180
  toolChoice: options.toolChoice,
1093
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,
1094
1188
  });
1095
1189
  let response;
1096
1190
  try {
1097
- response = await fetch(`${options.baseUrl}/chat/completions`, {
1191
+ response = await generationFetch(`${options.baseUrl}/chat/completions`, {
1098
1192
  method: "POST",
1099
1193
  signal: options.signal ?? null,
1100
1194
  headers: {
@@ -1112,6 +1206,8 @@ export async function openAiCompatibleComplete(options) {
1112
1206
  catch (error) {
1113
1207
  if (error instanceof Error && error.name === "AbortError")
1114
1208
  throw error;
1209
+ if (isOperationPolicyError(error))
1210
+ throw error;
1115
1211
  const msg = error instanceof Error ? error.message : String(error);
1116
1212
  throw new ProviderError(`${options.provider} request could not be sent (${msg}). Check connectivity to ${options.baseUrl}.`);
1117
1213
  }
@@ -1134,14 +1230,42 @@ export async function openAiCompatibleComplete(options) {
1134
1230
  }
1135
1231
  // If the API returns reasoning separately, prepend it inside <think>
1136
1232
  // tags so the existing thinking parser can pick it up uniformly.
1137
- 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);
1138
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
+ });
1139
1264
  if (typeof reasoning === "string" && reasoning.trim()) {
1140
1265
  learnModelEmitsReasoning(options.providerId, options.model);
1141
1266
  }
1142
- const full = foldReasoning(options.provider, requestBody, text, reasoning, usage);
1143
1267
  return {
1144
- text: full,
1268
+ text,
1145
1269
  ...(toolCalls.length ? { toolCalls } : {}),
1146
1270
  ...(choice?.finish_reason
1147
1271
  ? { finishReason: choice.finish_reason }
@@ -1149,6 +1273,10 @@ export async function openAiCompatibleComplete(options) {
1149
1273
  ? { finishReason: "tool_calls" }
1150
1274
  : {}),
1151
1275
  ...(usage ? { usage } : {}),
1276
+ ...(typeof reasoning === "string" && reasoning
1277
+ ? { reasoningBlock: { text: reasoning } }
1278
+ : {}),
1279
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1152
1280
  };
1153
1281
  }
1154
1282
  export async function openAiCompatibleStream(options) {
@@ -1229,25 +1357,27 @@ export async function openAiCompatibleStream(options) {
1229
1357
  };
1230
1358
  const onCallerAbort = () => idleController.abort(options.signal?.reason);
1231
1359
  options.signal?.addEventListener("abort", onCallerAbort, { once: true });
1232
- const supportsVision = modelAcceptsImages(options.providerId, options.model);
1233
- const requestBody = buildChatBody({
1360
+ const plan = compileRequestPlan({
1361
+ provider: options.providerId,
1234
1362
  model: options.model,
1235
- providerId: options.providerId,
1236
1363
  messages: options.messages,
1237
- maxTokens: options.maxTokens,
1238
- temperature: options.temperature,
1239
1364
  stream: true,
1240
- includeStreamUsage: options.includeStreamUsage,
1365
+ endpoint: options.baseUrl,
1241
1366
  reasoning: options.reasoning,
1242
- reasoningStyle: options.reasoningStyle,
1243
- supportsVision,
1244
1367
  tools: options.tools,
1245
1368
  toolChoice: options.toolChoice,
1246
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,
1247
1377
  });
1248
1378
  let response;
1249
1379
  try {
1250
- response = await fetch(`${options.baseUrl}/chat/completions`, {
1380
+ response = await generationFetch(`${options.baseUrl}/chat/completions`, {
1251
1381
  method: "POST",
1252
1382
  signal: idleController.signal,
1253
1383
  headers: {
@@ -1290,35 +1420,84 @@ export async function openAiCompatibleStream(options) {
1290
1420
  }
1291
1421
  const contentType = response.headers.get("content-type") ?? "";
1292
1422
  if (response.status === 202 || /\bapplication\/json\b/i.test(contentType)) {
1293
- clearIdleTimers();
1294
- options.signal?.removeEventListener("abort", onCallerAbort);
1295
- const data = await readJson(response);
1296
- if (response.status === 202) {
1297
- const requestId = data.requestId ?? data.id;
1298
- 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));
1299
- }
1300
- const choice = data.choices?.[0];
1301
- const message = choice?.message;
1302
- const toolCalls = parseOpenAiMessageToolCalls(message?.tool_calls);
1303
- const text = message?.content ?? "";
1304
- const jsonUsage = parseOpenAiUsage(data.usage);
1305
- const reasoning = message?.reasoning_content ?? message?.reasoning;
1306
- const full = foldReasoning(options.provider, requestBody, text, reasoning, jsonUsage);
1307
- if (full.trim() || toolCalls.length > 0) {
1308
- if (full.trim())
1309
- options.onToken(full);
1310
- return {
1311
- text: full,
1312
- ...(toolCalls.length ? { toolCalls } : {}),
1313
- ...(choice?.finish_reason
1314
- ? { finishReason: choice.finish_reason }
1315
- : toolCalls.length
1316
- ? { 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 } }
1317
1478
  : {}),
1318
- ...(jsonUsage ? { usage: jsonUsage } : {}),
1319
- };
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);
1320
1500
  }
1321
- 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));
1322
1501
  }
1323
1502
  const decoder = new TextDecoder();
1324
1503
  const reader = response.body.getReader();
@@ -1328,30 +1507,63 @@ export async function openAiCompatibleStream(options) {
1328
1507
  let reasoningSeen = "";
1329
1508
  let contentWireSeen = "";
1330
1509
  let reasoningWireSeen = "";
1331
- let inReasoning = false;
1332
1510
  let finishReason;
1333
- 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;
1334
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
+ };
1335
1561
  const normalizeChannelDelta = (token, seen) => {
1336
1562
  if (seen.length >= 64 && token.length >= seen.length && token.startsWith(seen)) {
1337
1563
  return { delta: token.slice(seen.length), seen: token };
1338
1564
  }
1339
1565
  return { delta: token, seen: seen + token };
1340
1566
  };
1341
- const enterReasoning = () => {
1342
- if (inReasoning)
1343
- return;
1344
- inReasoning = true;
1345
- full += REASONING_OPEN;
1346
- options.onToken(REASONING_OPEN);
1347
- };
1348
- const exitReasoning = () => {
1349
- if (!inReasoning)
1350
- return;
1351
- inReasoning = false;
1352
- full += REASONING_CLOSE;
1353
- options.onToken(REASONING_CLOSE);
1354
- };
1355
1567
  const emitPrivateReasoningNote = (hasToolCalls) => {
1356
1568
  if (reasoningSeen.trim())
1357
1569
  return;
@@ -1361,10 +1573,7 @@ export async function openAiCompatibleStream(options) {
1361
1573
  if (tokens <= 0)
1362
1574
  return;
1363
1575
  const note = privateReasoningNote(options.provider, requestBody, tokens);
1364
- enterReasoning();
1365
- full += note;
1366
- options.onToken(note);
1367
- exitReasoning();
1576
+ emitStreamReasoningDelta(options.onStreamEvent, note);
1368
1577
  };
1369
1578
  /**
1370
1579
  * Reasoning-echo suppression.
@@ -1388,8 +1597,6 @@ export async function openAiCompatibleStream(options) {
1388
1597
  const emitVisible = (text) => {
1389
1598
  if (!text)
1390
1599
  return;
1391
- if (inReasoning)
1392
- exitReasoning();
1393
1600
  visible += text;
1394
1601
  full += text;
1395
1602
  options.onToken(text);
@@ -1402,9 +1609,7 @@ export async function openAiCompatibleStream(options) {
1402
1609
  const emitReasoningEcho = (text) => {
1403
1610
  if (!text)
1404
1611
  return;
1405
- enterReasoning();
1406
- full += text;
1407
- options.onToken(text);
1612
+ emitStreamReasoningDelta(options.onStreamEvent, text);
1408
1613
  };
1409
1614
  /** Release a still-undecided hold-back as the answer (stream ended early). */
1410
1615
  const flushEchoBuffer = () => {
@@ -1498,11 +1703,19 @@ export async function openAiCompatibleStream(options) {
1498
1703
  if (payload === undefined)
1499
1704
  continue;
1500
1705
  if (payload === "[DONE]") {
1706
+ terminalSignal = "done-sentinel";
1707
+ requireTerminalProof({
1708
+ provider: options.provider,
1709
+ policy: terminalPolicy,
1710
+ signal: terminalSignal,
1711
+ ...emittedByteCounts(),
1712
+ });
1501
1713
  flushEchoBuffer();
1502
- exitReasoning();
1503
1714
  cleanup();
1504
1715
  const toolCalls = finalizeOpenAiToolCalls(toolCallState);
1505
1716
  emitPrivateReasoningNote(toolCalls.length > 0);
1717
+ const reasoningArtifacts = finalReasoningArtifacts(toolCalls);
1718
+ emitStreamReasoningArtifacts(options.onStreamEvent, reasoningArtifacts);
1506
1719
  if (!visible.trim() && toolCalls.length === 0) {
1507
1720
  // Thinking models (Kimi/Moonshot via Mantle, etc.) often emit only
1508
1721
  // reasoning, sometimes with tool sentinels inside <think>. Returning
@@ -1513,6 +1726,8 @@ export async function openAiCompatibleStream(options) {
1513
1726
  text: full,
1514
1727
  ...(finishReason ? { finishReason } : { finishReason: "stop" }),
1515
1728
  ...(streamUsage ? { usage: streamUsage } : {}),
1729
+ ...(reasoningSeen ? { reasoningBlock: { text: reasoningSeen } } : {}),
1730
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1516
1731
  };
1517
1732
  }
1518
1733
  throw new ProviderError(`${options.provider} completed without a visible answer.`);
@@ -1526,6 +1741,8 @@ export async function openAiCompatibleStream(options) {
1526
1741
  ? { finishReason: "tool_calls" }
1527
1742
  : {}),
1528
1743
  ...(streamUsage ? { usage: streamUsage } : {}),
1744
+ ...(reasoningSeen ? { reasoningBlock: { text: reasoningSeen } } : {}),
1745
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1529
1746
  };
1530
1747
  }
1531
1748
  let parsed;
@@ -1549,13 +1766,18 @@ export async function openAiCompatibleStream(options) {
1549
1766
  throw new ProviderError(`${options.provider} stream error: ${detail}`, undefined, payload.slice(0, 500));
1550
1767
  }
1551
1768
  {
1552
- 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);
1553
1772
  if (chunkUsage)
1554
1773
  streamUsage = chunkUsage;
1555
1774
  const choice = parsed.choices?.[0];
1556
1775
  const delta = choice?.delta;
1557
1776
  const reasoningToken = delta?.reasoning_content ?? delta?.reasoning;
1558
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++;
1559
1781
  const toolProgress = delta?.tool_calls?.some((toolCall) => Boolean(toolCall.id ||
1560
1782
  toolCall.function?.name ||
1561
1783
  toolCall.function?.arguments));
@@ -1563,63 +1785,101 @@ export async function openAiCompatibleStream(options) {
1563
1785
  chunkUsage ||
1564
1786
  reasoningToken ||
1565
1787
  token ||
1788
+ detailRaw ||
1789
+ thoughtSignature ||
1566
1790
  toolProgress) {
1567
1791
  resetIdleTimer();
1568
1792
  }
1569
- if (choice?.finish_reason)
1793
+ if (choice?.finish_reason) {
1570
1794
  finishReason = choice.finish_reason;
1795
+ terminalSignal = "finish-reason";
1796
+ }
1571
1797
  if (reasoningToken) {
1572
1798
  const normalized = normalizeChannelDelta(reasoningToken, reasoningWireSeen);
1573
1799
  reasoningWireSeen = normalized.seen;
1574
1800
  if (normalized.delta) {
1801
+ reasoningArtifactSequence ??= artifactSequence;
1575
1802
  if (!reasoningSeen) {
1576
1803
  learnModelEmitsReasoning(options.providerId, options.model);
1577
1804
  }
1578
- enterReasoning();
1579
1805
  reasoningSeen += normalized.delta;
1580
- full += normalized.delta;
1581
- options.onToken(normalized.delta);
1806
+ emitStreamReasoningDelta(options.onStreamEvent, normalized.delta);
1582
1807
  }
1583
1808
  }
1809
+ if (detailRaw) {
1810
+ structuredDetails.push({ raw: detailRaw, sequence: artifactSequence });
1811
+ }
1584
1812
  if (token) {
1585
1813
  const normalized = normalizeChannelDelta(token, contentWireSeen);
1586
1814
  contentWireSeen = normalized.seen;
1587
1815
  if (normalized.delta)
1588
1816
  handleContentToken(normalized.delta);
1589
1817
  }
1818
+ const deltaToolCallIndices = [];
1590
1819
  if (delta?.tool_calls?.length) {
1591
1820
  for (const tc of delta.tool_calls) {
1592
1821
  const accInfo = accumulateOpenAiToolCallDelta(toolCallState, tc);
1593
- if (!options.onToolCallDelta)
1594
- continue;
1595
- const wire = accInfo.name;
1596
- const canonical = wire
1597
- ? (fromWireName(wire) ?? wire)
1598
- : undefined;
1599
- const largeArgTick = !accInfo.nameBecameKnown &&
1600
- accInfo.argumentsBytes > 0 &&
1601
- accInfo.argumentsBytes % 4096 <
1602
- (typeof tc.function?.arguments === "string"
1603
- ? tc.function.arguments.length
1604
- : 0);
1605
- if (accInfo.nameBecameKnown || largeArgTick) {
1606
- options.onToolCallDelta({
1607
- index: accInfo.index,
1608
- ...(accInfo.id !== undefined ? { id: accInfo.id } : {}),
1609
- ...(canonical !== undefined ? { name: canonical } : {}),
1610
- argumentsBytes: accInfo.argumentsBytes,
1611
- });
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
+ }
1612
1843
  }
1613
1844
  }
1614
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
+ }
1615
1868
  }
1616
1869
  }
1617
1870
  }
1618
1871
  flushEchoBuffer();
1619
- exitReasoning();
1620
1872
  cleanup();
1873
+ requireTerminalProof({
1874
+ provider: options.provider,
1875
+ policy: terminalPolicy,
1876
+ signal: terminalSignal,
1877
+ ...emittedByteCounts(),
1878
+ });
1621
1879
  const toolCalls = finalizeOpenAiToolCalls(toolCallState);
1622
1880
  emitPrivateReasoningNote(toolCalls.length > 0);
1881
+ const reasoningArtifacts = finalReasoningArtifacts(toolCalls);
1882
+ emitStreamReasoningArtifacts(options.onStreamEvent, reasoningArtifacts);
1623
1883
  if (!visible.trim() && toolCalls.length === 0) {
1624
1884
  // See [DONE] branch — hand thinking-only streams to the runner so it can
1625
1885
  // salvage sentinel tool blocks (or nudge) instead of hard-failing.
@@ -1628,6 +1888,8 @@ export async function openAiCompatibleStream(options) {
1628
1888
  text: full,
1629
1889
  ...(finishReason ? { finishReason } : { finishReason: "stop" }),
1630
1890
  ...(streamUsage ? { usage: streamUsage } : {}),
1891
+ ...(reasoningSeen ? { reasoningBlock: { text: reasoningSeen } } : {}),
1892
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1631
1893
  };
1632
1894
  }
1633
1895
  throw new ProviderError(`${options.provider} completed without a visible answer.`);
@@ -1641,6 +1903,8 @@ export async function openAiCompatibleStream(options) {
1641
1903
  ? { finishReason: "tool_calls" }
1642
1904
  : {}),
1643
1905
  ...(streamUsage ? { usage: streamUsage } : {}),
1906
+ ...(reasoningSeen ? { reasoningBlock: { text: reasoningSeen } } : {}),
1907
+ ...(reasoningArtifacts ? { reasoningArtifacts } : {}),
1644
1908
  };
1645
1909
  }
1646
1910
  catch (error) {