@pentoshi/clai 4.2.2 → 4.4.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 (204) hide show
  1. package/README.md +15 -17
  2. package/dist/agent/compaction-summary.d.ts +1 -1
  3. package/dist/agent/compaction-summary.js +1 -1
  4. package/dist/agent/compaction-summary.js.map +1 -1
  5. package/dist/agent/runner.js +12 -13
  6. package/dist/agent/runner.js.map +1 -1
  7. package/dist/agent/tool-output-formatting.d.ts +6 -0
  8. package/dist/agent/tool-output-formatting.js +126 -40
  9. package/dist/agent/tool-output-formatting.js.map +1 -1
  10. package/dist/app/adapters/agent-event-adapter.d.ts +4 -0
  11. package/dist/app/adapters/agent-event-adapter.js +28 -3
  12. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  13. package/dist/app/commands/catalog.js +8 -26
  14. package/dist/app/commands/catalog.js.map +1 -1
  15. package/dist/app/commands/registry.js +0 -1
  16. package/dist/app/commands/registry.js.map +1 -1
  17. package/dist/app/controllers/session-controller.d.ts +3 -0
  18. package/dist/app/controllers/session-controller.js +18 -25
  19. package/dist/app/controllers/session-controller.js.map +1 -1
  20. package/dist/app/controllers/session-persistence.d.ts +4 -1
  21. package/dist/app/controllers/session-persistence.js +23 -5
  22. package/dist/app/controllers/session-persistence.js.map +1 -1
  23. package/dist/app/controllers/session-responder.d.ts +1 -0
  24. package/dist/app/controllers/session-responder.js +8 -0
  25. package/dist/app/controllers/session-responder.js.map +1 -1
  26. package/dist/app/controllers/session-usage-ledger.d.ts +63 -0
  27. package/dist/app/controllers/session-usage-ledger.js +232 -0
  28. package/dist/app/controllers/session-usage-ledger.js.map +1 -0
  29. package/dist/app/events/app-event.d.ts +2 -0
  30. package/dist/app/events/app-event.js.map +1 -1
  31. package/dist/classic/app/ClassicApp.js +2 -1
  32. package/dist/classic/app/ClassicApp.js.map +1 -1
  33. package/dist/classic/app/use-feed.js +2 -1
  34. package/dist/classic/app/use-feed.js.map +1 -1
  35. package/dist/classic/chrome/toast-rows.d.ts +1 -0
  36. package/dist/classic/chrome/toast-rows.js +18 -0
  37. package/dist/classic/chrome/toast-rows.js.map +1 -1
  38. package/dist/commands/providers.js +0 -4
  39. package/dist/commands/providers.js.map +1 -1
  40. package/dist/index.js +6 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/llm/adapters/openai-tools.d.ts +1 -0
  43. package/dist/llm/adapters/openai-tools.js +4 -1
  44. package/dist/llm/adapters/openai-tools.js.map +1 -1
  45. package/dist/llm/agentrouter.js +2 -0
  46. package/dist/llm/agentrouter.js.map +1 -1
  47. package/dist/llm/aws-mantle.js +2 -0
  48. package/dist/llm/aws-mantle.js.map +1 -1
  49. package/dist/llm/bynara.js +2 -0
  50. package/dist/llm/bynara.js.map +1 -1
  51. package/dist/llm/capabilities.d.ts +19 -0
  52. package/dist/llm/capabilities.js +165 -79
  53. package/dist/llm/capabilities.js.map +1 -1
  54. package/dist/llm/catalog-facts.d.ts +23 -0
  55. package/dist/llm/catalog-facts.js +253 -0
  56. package/dist/llm/catalog-facts.js.map +1 -0
  57. package/dist/llm/catalog-prefetch.d.ts +5 -0
  58. package/dist/llm/catalog-prefetch.js +38 -0
  59. package/dist/llm/catalog-prefetch.js.map +1 -0
  60. package/dist/llm/context-windows.d.ts +2 -0
  61. package/dist/llm/context-windows.js +20 -6
  62. package/dist/llm/context-windows.js.map +1 -1
  63. package/dist/llm/custom-profile-resolver.d.ts +5 -0
  64. package/dist/llm/custom-profile-resolver.js +8 -0
  65. package/dist/llm/custom-profile-resolver.js.map +1 -0
  66. package/dist/llm/custom-provider-profile.d.ts +7 -2
  67. package/dist/llm/custom-provider-profile.js +40 -10
  68. package/dist/llm/custom-provider-profile.js.map +1 -1
  69. package/dist/llm/custom-providers.js +2 -0
  70. package/dist/llm/custom-providers.js.map +1 -1
  71. package/dist/llm/effort-fallback.js +3 -0
  72. package/dist/llm/effort-fallback.js.map +1 -1
  73. package/dist/llm/fireworks.js +2 -0
  74. package/dist/llm/fireworks.js.map +1 -1
  75. package/dist/llm/free.js +2 -0
  76. package/dist/llm/free.js.map +1 -1
  77. package/dist/llm/hetzner.js +2 -0
  78. package/dist/llm/hetzner.js.map +1 -1
  79. package/dist/llm/http.d.ts +21 -2
  80. package/dist/llm/http.js +133 -141
  81. package/dist/llm/http.js.map +1 -1
  82. package/dist/llm/key-rotation.d.ts +1 -0
  83. package/dist/llm/key-rotation.js +8 -0
  84. package/dist/llm/key-rotation.js.map +1 -1
  85. package/dist/llm/learned-capabilities.d.ts +22 -0
  86. package/dist/llm/learned-capabilities.js +147 -0
  87. package/dist/llm/learned-capabilities.js.map +1 -0
  88. package/dist/llm/lightning.js +2 -0
  89. package/dist/llm/lightning.js.map +1 -1
  90. package/dist/llm/modal.js +2 -0
  91. package/dist/llm/modal.js.map +1 -1
  92. package/dist/llm/model-families.d.ts +20 -0
  93. package/dist/llm/model-families.js +227 -19
  94. package/dist/llm/model-families.js.map +1 -1
  95. package/dist/llm/nvidia.js +2 -0
  96. package/dist/llm/nvidia.js.map +1 -1
  97. package/dist/llm/openai.js +2 -0
  98. package/dist/llm/openai.js.map +1 -1
  99. package/dist/llm/openrouter.js +2 -0
  100. package/dist/llm/openrouter.js.map +1 -1
  101. package/dist/llm/orcarouter.js +2 -0
  102. package/dist/llm/orcarouter.js.map +1 -1
  103. package/dist/llm/provider-model-layers.d.ts +3 -0
  104. package/dist/llm/provider-model-layers.js +522 -0
  105. package/dist/llm/provider-model-layers.js.map +1 -0
  106. package/dist/llm/provider-profile-layers.d.ts +4 -1
  107. package/dist/llm/provider-profile-layers.js +22 -536
  108. package/dist/llm/provider-profile-layers.js.map +1 -1
  109. package/dist/llm/provider-profile.d.ts +14 -1
  110. package/dist/llm/provider-profile.js +23 -0
  111. package/dist/llm/provider-profile.js.map +1 -1
  112. package/dist/llm/provider-profiles.d.ts +5 -1
  113. package/dist/llm/provider-profiles.js +178 -5
  114. package/dist/llm/provider-profiles.js.map +1 -1
  115. package/dist/llm/provider.js +0 -15
  116. package/dist/llm/provider.js.map +1 -1
  117. package/dist/llm/qwen-cloud.js +2 -0
  118. package/dist/llm/qwen-cloud.js.map +1 -1
  119. package/dist/llm/reasoning-artifacts.d.ts +1 -0
  120. package/dist/llm/reasoning-artifacts.js +7 -5
  121. package/dist/llm/reasoning-artifacts.js.map +1 -1
  122. package/dist/llm/reasoning-controls.d.ts +16 -0
  123. package/dist/llm/reasoning-controls.js +210 -0
  124. package/dist/llm/reasoning-controls.js.map +1 -0
  125. package/dist/llm/reasoning-errors.d.ts +4 -0
  126. package/dist/llm/reasoning-errors.js +45 -0
  127. package/dist/llm/reasoning-errors.js.map +1 -0
  128. package/dist/llm/request-plan.d.ts +8 -6
  129. package/dist/llm/request-plan.js +47 -13
  130. package/dist/llm/request-plan.js.map +1 -1
  131. package/dist/llm/route-dialect-registry.d.ts +8 -0
  132. package/dist/llm/route-dialect-registry.js +33 -0
  133. package/dist/llm/route-dialect-registry.js.map +1 -0
  134. package/dist/llm/route-vocabulary.d.ts +2 -0
  135. package/dist/llm/route-vocabulary.js +23 -0
  136. package/dist/llm/route-vocabulary.js.map +1 -0
  137. package/dist/llm/router.d.ts +2 -0
  138. package/dist/llm/router.js +70 -20
  139. package/dist/llm/router.js.map +1 -1
  140. package/dist/llm/stream-terminal.js +2 -1
  141. package/dist/llm/stream-terminal.js.map +1 -1
  142. package/dist/llm/tokenrouter.js +2 -0
  143. package/dist/llm/tokenrouter.js.map +1 -1
  144. package/dist/store/config.d.ts +11 -0
  145. package/dist/store/config.js +21 -3
  146. package/dist/store/config.js.map +1 -1
  147. package/dist/store/history.d.ts +1 -0
  148. package/dist/store/history.js +7 -0
  149. package/dist/store/history.js.map +1 -1
  150. package/dist/store/keys.d.ts +1 -1
  151. package/dist/store/keys.js +1 -1
  152. package/dist/store/scope.d.ts +8 -0
  153. package/dist/store/scope.js +138 -1
  154. package/dist/store/scope.js.map +1 -1
  155. package/dist/store/session-workspace.d.ts +1 -0
  156. package/dist/store/session-workspace.js +21 -1
  157. package/dist/store/session-workspace.js.map +1 -1
  158. package/dist/tools/definitions.js +5 -10
  159. package/dist/tools/definitions.js.map +1 -1
  160. package/dist/tools/interactive-session-tools.js +5 -5
  161. package/dist/tools/interactive-session-tools.js.map +1 -1
  162. package/dist/tools/output-selection.js +47 -3
  163. package/dist/tools/output-selection.js.map +1 -1
  164. package/dist/tools/registry.js +2 -2
  165. package/dist/tools/registry.js.map +1 -1
  166. package/dist/tools/web/content-encoding.d.ts +6 -0
  167. package/dist/tools/web/content-encoding.js +57 -0
  168. package/dist/tools/web/content-encoding.js.map +1 -0
  169. package/dist/tools/web/fetch-core.js +23 -1
  170. package/dist/tools/web/fetch-core.js.map +1 -1
  171. package/dist/tui-v2/components/picker/picker.js +1 -1
  172. package/dist/tui-v2/components/picker/picker.js.map +1 -1
  173. package/dist/types.d.ts +2 -1
  174. package/dist/types.js +0 -2
  175. package/dist/types.js.map +1 -1
  176. package/dist/ui-core/bootstrap/composition-root.js +8 -2
  177. package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
  178. package/dist/ui-core/commands/command-handlers.js +3 -3
  179. package/dist/ui-core/commands/command-handlers.js.map +1 -1
  180. package/dist/ui-core/commands/config-commands.js +17 -14
  181. package/dist/ui-core/commands/config-commands.js.map +1 -1
  182. package/dist/ui-core/commands/picker-commands.js +34 -11
  183. package/dist/ui-core/commands/picker-commands.js.map +1 -1
  184. package/dist/ui-core/commands/session-commands.d.ts +3 -3
  185. package/dist/ui-core/commands/session-commands.js +64 -17
  186. package/dist/ui-core/commands/session-commands.js.map +1 -1
  187. package/dist/ui-core/rendering/format-help.js +2 -2
  188. package/dist/ui-core/rendering/format-help.js.map +1 -1
  189. package/dist/ui-core/rendering/format-usage.d.ts +6 -0
  190. package/dist/ui-core/rendering/format-usage.js +116 -0
  191. package/dist/ui-core/rendering/format-usage.js.map +1 -0
  192. package/dist/ui-core/state/transcript-reducer.js +78 -19
  193. package/dist/ui-core/state/transcript-reducer.js.map +1 -1
  194. package/dist/ui-core/state/transcript-types.d.ts +1 -0
  195. package/dist/ui-core/state/transcript-types.js.map +1 -1
  196. package/dist/version.generated.d.ts +2 -2
  197. package/dist/version.generated.js +2 -2
  198. package/package.json +1 -2
  199. package/dist/llm/groq.d.ts +0 -24
  200. package/dist/llm/groq.js +0 -155
  201. package/dist/llm/groq.js.map +0 -1
  202. package/dist/llm/kimchi.d.ts +0 -2
  203. package/dist/llm/kimchi.js +0 -96
  204. package/dist/llm/kimchi.js.map +0 -1
package/dist/llm/http.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { modelAcceptsImages, modelSupportsThinking, isReasoningUnsupported, modelReasoningEfforts, learnModelEmitsReasoning, registerModelCatalog, } from "./capabilities.js";
2
+ import { catalogEffortList, catalogEntriesFromPayload, parseCatalogFacts, } from "./catalog-facts.js";
2
3
  import { resolveSampling } from "./sampling.js";
3
4
  import { accumulateOpenAiToolCallDelta, finalizeOpenAiToolCalls, fromWireName, parseOpenAiMessageToolCalls, } from "./tool-protocol.js";
4
5
  import { openAiToolBodyFields, toOpenAiToolMessages, } from "./adapters/openai-tools.js";
@@ -7,6 +8,9 @@ import { generationFetch } from "./operation-usage.js";
7
8
  import { isOperationPolicyError } from "./operation-ledger.js";
8
9
  import { emitStreamReasoningArtifacts, emitStreamReasoningDelta, } from "./stream-events.js";
9
10
  import { CHAT_COMPLETIONS_STREAM_TERMINAL, requireTerminalProof, } from "./stream-terminal.js";
11
+ import { modelMaxOutputTokens } from "./context-windows.js";
12
+ import { inBandBadRequestStatus, isMissingReasoningContentError, } from "./reasoning-errors.js";
13
+ import { emitReasoningControls, } from "./reasoning-controls.js";
10
14
  import { createReasoningArtifact, createReasoningArtifactProvenance, } from "./reasoning-artifacts.js";
11
15
  import { classifyBynaraModel, classifyNvidiaModel, } from "./model-families.js";
12
16
  import { compileRequestPlan, } from "./request-plan.js";
@@ -23,6 +27,31 @@ export class ProviderError extends Error {
23
27
  this.name = "ProviderError";
24
28
  }
25
29
  }
30
+ const USAGE_INPUT_KEYS = [
31
+ "prompt_tokens",
32
+ "promptTokens",
33
+ "input_tokens",
34
+ "inputTokens",
35
+ ];
36
+ const USAGE_OUTPUT_KEYS = [
37
+ "completion_tokens",
38
+ "completionTokens",
39
+ "output_tokens",
40
+ "outputTokens",
41
+ ];
42
+ function isFinalUsageFrame(parsed) {
43
+ if (Array.isArray(parsed.choices) && parsed.choices.length > 0)
44
+ return false;
45
+ const usage = parsed.usage;
46
+ if (!usage || typeof usage !== "object" || Array.isArray(usage))
47
+ return false;
48
+ const raw = usage;
49
+ const reported = (keys) => keys.some((key) => {
50
+ const value = raw[key];
51
+ return typeof value === "number" && Number.isFinite(value);
52
+ });
53
+ return reported(USAGE_INPUT_KEYS) && reported(USAGE_OUTPUT_KEYS);
54
+ }
26
55
  function parseRetryAfterHeader(value) {
27
56
  if (!value)
28
57
  return undefined;
@@ -68,116 +97,26 @@ function statusCodeHint(status) {
68
97
  }
69
98
  return "";
70
99
  }
71
- function modalitiesDeclareImage(value) {
72
- if (Array.isArray(value)) {
73
- const items = value.filter((item) => typeof item === "string");
74
- if (items.length === 0)
75
- return undefined;
76
- return items.some((item) => /image|vision/i.test(item));
77
- }
78
- if (typeof value === "string") {
79
- if (!/text|image|audio|video/i.test(value))
80
- return undefined;
81
- return /image|vision/i.test(value);
82
- }
83
- if (value && typeof value === "object") {
84
- const nested = value;
85
- if (typeof nested.vision === "boolean")
86
- return nested.vision;
87
- if (typeof nested.image === "boolean")
88
- return nested.image;
89
- if (nested.input !== undefined)
90
- return modalitiesDeclareImage(nested.input);
91
- }
92
- return undefined;
93
- }
94
100
  export function catalogEntryVision(entry) {
95
- if (!entry || typeof entry !== "object")
96
- return undefined;
97
- const raw = entry;
98
- for (const flag of [raw.vision, raw.supports_vision, raw.multimodal]) {
99
- if (typeof flag === "boolean")
100
- return flag;
101
- }
102
- for (const candidate of [
103
- raw.architecture?.input_modalities,
104
- raw.architecture?.modality,
105
- raw.input_modalities,
106
- raw.modalities,
107
- raw.capabilities,
108
- raw.features,
109
- ]) {
110
- const declared = modalitiesDeclareImage(candidate);
111
- if (declared !== undefined)
112
- return declared;
113
- }
114
- return undefined;
115
- }
116
- const REASONING_FEATURE_RE = /^(?:reasoning|reasoning_effort|include_reasoning|thinking|reasoning_content)$/i;
117
- function catalogEntryReasoning(entry) {
118
- const raw = entry;
119
- if (typeof raw?.reasoning === "boolean")
120
- return raw.reasoning;
121
- if (Array.isArray(raw?.reasoning_options) && raw.reasoning_options.length > 0) {
122
- return true;
123
- }
124
- for (const container of [
125
- raw?.supported_features,
126
- raw?.supported_parameters,
127
- raw?.features,
128
- raw?.capabilities,
129
- ]) {
130
- if (!Array.isArray(container))
131
- continue;
132
- const names = container.filter((value) => typeof value === "string");
133
- if (names.length === 0)
134
- continue;
135
- return names.some((name) => REASONING_FEATURE_RE.test(name.trim()));
136
- }
137
- return undefined;
138
- }
139
- function catalogEntryReasoningEfforts(entry) {
140
- const raw = entry;
141
- const collected = [];
142
- const pushValues = (values) => {
143
- if (!Array.isArray(values))
144
- return;
145
- for (const value of values) {
146
- if (typeof value === "string" && value.trim())
147
- collected.push(value.trim());
148
- }
149
- };
150
- if (Array.isArray(raw?.reasoning_options)) {
151
- for (const option of raw.reasoning_options) {
152
- const shaped = option;
153
- if (shaped?.type !== undefined && shaped.type !== "effort")
154
- continue;
155
- pushValues(shaped?.values);
156
- }
157
- }
158
- pushValues(raw?.supported_reasoning_efforts);
159
- if (raw?.reasoning_effort && typeof raw.reasoning_effort === "object") {
160
- pushValues(raw.reasoning_effort.values);
161
- }
162
- return collected.length > 0 ? collected : undefined;
101
+ return parseCatalogFacts(entry)?.vision;
163
102
  }
164
103
  export function ingestModelCatalogEntries(provider, entries) {
165
104
  const models = [];
166
105
  const seen = new Set();
167
106
  for (const entry of entries) {
168
- const raw = typeof entry === "string" ? entry : entry?.id;
169
- if (typeof raw !== "string")
107
+ const facts = parseCatalogFacts(entry);
108
+ if (!facts)
170
109
  continue;
171
- const id = raw.trim();
172
- if (id.length === 0 || seen.has(id))
110
+ const id = facts.id;
111
+ if (seen.has(id))
173
112
  continue;
174
113
  seen.add(id);
175
- const vision = typeof entry === "string" ? undefined : catalogEntryVision(entry);
176
- const reasoning = typeof entry === "string" ? undefined : catalogEntryReasoning(entry);
177
- const reasoningEfforts = typeof entry === "string" ? undefined : catalogEntryReasoningEfforts(entry);
114
+ const reasoning = facts.reasoning?.supported;
115
+ const reasoningEfforts = catalogEffortList(facts.reasoning?.supportedEfforts);
178
116
  models.push({
179
117
  id,
180
- ...(vision === undefined ? {} : { vision }),
118
+ facts,
119
+ ...(facts.vision === undefined ? {} : { vision: facts.vision }),
181
120
  ...(reasoning === undefined ? {} : { reasoning }),
182
121
  ...(reasoningEfforts === undefined ? {} : { reasoningEfforts }),
183
122
  });
@@ -187,15 +126,7 @@ export function ingestModelCatalogEntries(provider, entries) {
187
126
  return models.map((model) => model.id).sort();
188
127
  }
189
128
  export function ingestOpenAiModelCatalog(provider, payload) {
190
- const container = payload;
191
- const entries = Array.isArray(payload)
192
- ? payload
193
- : Array.isArray(container?.data)
194
- ? container.data
195
- : Array.isArray(container?.models)
196
- ? container.models
197
- : [];
198
- return ingestModelCatalogEntries(provider, entries);
129
+ return ingestModelCatalogEntries(provider, catalogEntriesFromPayload(payload));
199
130
  }
200
131
  export async function readJson(response, signal) {
201
132
  const text = await readBodyCapped(response, MAX_JSON_RESPONSE_BYTES, signal);
@@ -552,6 +483,15 @@ const DEFAULT_COMPATIBLE_REASONING_ARTIFACT_POLICY = {
552
483
  scope: "all-history",
553
484
  persistence: "tool-turn",
554
485
  };
486
+ const PRESERVED_FINAL_TURN_ARTIFACT_POLICY = {
487
+ scope: "all-history",
488
+ persistence: "all-turns",
489
+ };
490
+ export function compatibleArtifactPolicyFor(preservation) {
491
+ return preservation === "required"
492
+ ? PRESERVED_FINAL_TURN_ARTIFACT_POLICY
493
+ : DEFAULT_COMPATIBLE_REASONING_ARTIFACT_POLICY;
494
+ }
555
495
  function artifactRaw(value) {
556
496
  if (typeof value === "string")
557
497
  return value;
@@ -726,7 +666,16 @@ function pickAdvertisedEffort(effort, advertised) {
726
666
  return (order.find((candidate) => advertised.includes(candidate)) ??
727
667
  advertised[advertised.length - 1]);
728
668
  }
729
- export function buildReasoningPayload(reasoning, style, model, providerId) {
669
+ export function buildReasoningPayload(reasoning, style, model, providerId, control) {
670
+ if (control) {
671
+ return {
672
+ ...emitReasoningControls({
673
+ profile: control.profile,
674
+ preference: reasoning,
675
+ willReplayReasoning: control.willReplayReasoning,
676
+ }),
677
+ };
678
+ }
730
679
  if (style === "none")
731
680
  return {};
732
681
  const enabled = Boolean(reasoning?.enabled);
@@ -866,18 +815,6 @@ export function buildReasoningPayload(reasoning, style, model, providerId) {
866
815
  // off, so compaction would still buy hidden reasoning tokens. vLLM's
867
816
  // StepFun template honours this explicit per-request switch.
868
817
  return { chat_template_kwargs: { enable_thinking: enabled } };
869
- case "groq": {
870
- const m = (model ?? "").toLowerCase();
871
- if (/qwen\/qwen3-32b/.test(m)) {
872
- return { reasoning_effort: enabled ? "default" : "none" };
873
- }
874
- if (/openai\/gpt-oss-(?:20b|120b)/.test(m)) {
875
- return enabled
876
- ? { reasoning_effort: clampEffort(effort), include_reasoning: true }
877
- : { reasoning_effort: "low", include_reasoning: false };
878
- }
879
- return {};
880
- }
881
818
  case "nvidia": {
882
819
  const kind = classifyNvidiaModel(model ?? "");
883
820
  switch (kind) {
@@ -964,6 +901,8 @@ export function buildReasoningPayload(reasoning, style, model, providerId) {
964
901
  * payload and retries so an unsupported option never fails the whole request.
965
902
  */
966
903
  export function isReasoningUnsupportedError(error) {
904
+ if (isMissingReasoningContentError(error))
905
+ return false;
967
906
  const status = error && typeof error === "object" && "status" in error
968
907
  ? Number(error.status)
969
908
  : undefined;
@@ -1043,30 +982,46 @@ export function isOpenAiReasoningModel(model) {
1043
982
  /(?:^|\/)o[134](?:\.|-|$)/.test(m) ||
1044
983
  /muse-spark/.test(m));
1045
984
  }
985
+ const DEFAULT_REASONING_OUTPUT_FLOOR = 16_384;
986
+ function outputBudgetWithReasoning(requested, options) {
987
+ const floor = options.control?.profile.reasoning.minOutputTokens ??
988
+ DEFAULT_REASONING_OUTPUT_FLOOR;
989
+ const ceiling = modelMaxOutputTokens(options.providerId, options.model, options.outputTokenLimit);
990
+ const floored = Math.max(requested, floor);
991
+ return ceiling !== undefined ? Math.min(floored, ceiling) : floored;
992
+ }
1046
993
  function emitChatCompletionsBody(options) {
1047
994
  // Skip reasoning knobs entirely for models observed to reject them this
1048
995
  // session (see isReasoningUnsupportedError). This is how thinking degrades
1049
996
  // gracefully: the request still runs, just without the unsupported option.
1050
- const capabilityDeniesThinking = options.providerId !== undefined &&
997
+ const capabilityDeniesThinking = options.control === undefined &&
998
+ options.providerId !== undefined &&
1051
999
  Boolean(options.reasoning?.enabled) &&
1052
1000
  !modelSupportsThinking(options.providerId, options.model);
1053
- const reasoning = (options.providerId !== undefined &&
1054
- isReasoningUnsupported(options.providerId, options.model)) ||
1055
- capabilityDeniesThinking
1001
+ const legacyControlDenied = options.control === undefined &&
1002
+ options.providerId !== undefined &&
1003
+ isReasoningUnsupported(options.providerId, options.model);
1004
+ const reasoning = legacyControlDenied ||
1005
+ capabilityDeniesThinking ||
1006
+ options.control?.suppressed === true
1056
1007
  ? {}
1057
- : buildReasoningPayload(options.reasoning, options.reasoningStyle ?? "none", options.model, options.providerId);
1008
+ : buildReasoningPayload(options.reasoning, options.reasoningStyle ?? "none", options.model, options.providerId, options.control);
1058
1009
  const reasoningOn = Boolean(options.reasoning?.enabled);
1059
1010
  // Kimchi exposes this model as `minimax-m3`; NVIDIA uses the longer
1060
1011
  // `minimaxai/minimax-m3` ID. Both need the larger default output budget.
1061
1012
  const isMinimaxM3 = /minimax-m3/i.test(options.model);
1062
1013
  const defaultMaxTokens = isMinimaxM3 ? 8_192 : reasoningOn ? 8_192 : 4_096;
1063
1014
  // One declarative sampling policy; explicit caller value wins.
1064
- const sampling = resolveSampling({
1065
- model: options.model,
1066
- reasoningEnabled: reasoningOn,
1067
- requestedTemperature: options.temperature,
1068
- });
1015
+ const sampling = options.resolvedSampling ?? {
1016
+ ...resolveSampling({
1017
+ model: options.model,
1018
+ reasoningEnabled: reasoningOn,
1019
+ requestedTemperature: options.temperature,
1020
+ }),
1021
+ };
1069
1022
  const reasoningModel = isOpenAiReasoningModel(options.model);
1023
+ const emitTemperature = sampling.temperature !== undefined &&
1024
+ (options.resolvedSampling !== undefined || !reasoningModel);
1070
1025
  // Claude extended thinking via AgentRouter maps reasoning_effort to an
1071
1026
  // Anthropic `thinking.budget_tokens`, and the gateway (Bedrock) rejects the
1072
1027
  // request with HTTP 400 unless `max_tokens > budget_tokens`. Some upstream
@@ -1077,15 +1032,20 @@ function emitChatCompletionsBody(options) {
1077
1032
  const claudeThinking = reasoningOn &&
1078
1033
  options.reasoningStyle === "agentrouter" &&
1079
1034
  /claude/i.test(options.model);
1080
- const effectiveMaxTokens = claudeThinking
1081
- ? Math.max(options.maxTokens ?? defaultMaxTokens, 32_000)
1082
- : (options.maxTokens ?? defaultMaxTokens);
1035
+ const requestedMaxTokens = options.maxTokens ?? defaultMaxTokens;
1036
+ const claudeFloored = claudeThinking
1037
+ ? Math.max(requestedMaxTokens, 32_000)
1038
+ : requestedMaxTokens;
1039
+ const effectiveMaxTokens = reasoningOn
1040
+ ? outputBudgetWithReasoning(claudeFloored, options)
1041
+ : claudeFloored;
1083
1042
  const body = {
1084
1043
  model: options.model,
1085
1044
  messages: toOpenAiMessages(singleLeadingSystemMessages(options.messages), options.supportsVision, options.replayTarget
1086
1045
  ? {
1087
1046
  target: options.replayTarget,
1088
1047
  observe: options.reasoningArtifactReplayObserver,
1048
+ ...(options.forceReasoningReplay ? { forceScope: true } : {}),
1089
1049
  }
1090
1050
  : undefined),
1091
1051
  stream: options.stream,
@@ -1098,7 +1058,7 @@ function emitChatCompletionsBody(options) {
1098
1058
  // gpt-5.x / o1 / o3 / o4 only accept the default temperature (1) and
1099
1059
  // reject any explicit value — omit the field entirely rather than send
1100
1060
  // our 0.2 default and get a 400.
1101
- ...(reasoningModel ? {} : { temperature: sampling.temperature }),
1061
+ ...(emitTemperature ? { temperature: sampling.temperature } : {}),
1102
1062
  ...reasoning,
1103
1063
  ...openAiToolBodyFields({
1104
1064
  tools: options.tools,
@@ -1106,7 +1066,7 @@ function emitChatCompletionsBody(options) {
1106
1066
  parallelToolCalls: options.parallelToolCalls,
1107
1067
  }),
1108
1068
  };
1109
- if (!reasoningModel && sampling.topP !== undefined) {
1069
+ if (emitTemperature && sampling.topP !== undefined) {
1110
1070
  body.top_p = sampling.topP;
1111
1071
  }
1112
1072
  // OpenAI + many OpenAI-compatible gateways attach usage on the final SSE
@@ -1136,6 +1096,21 @@ export function chatCompletionsBodyFromPlan(plan, extras = {}) {
1136
1096
  reasoning: plan.controls.reasoning,
1137
1097
  reasoningStyle: extras.reasoningStyle,
1138
1098
  supportsVision: plan.images.visionAccepted,
1099
+ control: {
1100
+ profile: {
1101
+ reasoning: plan.policy.reasoning,
1102
+ capabilities: { acceptedParameters: plan.policy.acceptedParameters },
1103
+ },
1104
+ willReplayReasoning: plan.replay.decisions.some((entry) => entry.decision.action === "replayed"),
1105
+ suppressed: plan.controls.controlSuppression !== undefined,
1106
+ },
1107
+ resolvedSampling: {
1108
+ temperature: plan.controls.temperature,
1109
+ topP: plan.controls.topP,
1110
+ },
1111
+ ...(plan.policy.limits.outputTokens !== undefined
1112
+ ? { outputTokenLimit: plan.policy.limits.outputTokens }
1113
+ : {}),
1139
1114
  tools: plan.tools.definitions.length
1140
1115
  ? [...plan.tools.definitions]
1141
1116
  : undefined,
@@ -1143,6 +1118,7 @@ export function chatCompletionsBodyFromPlan(plan, extras = {}) {
1143
1118
  parallelToolCalls: plan.tools.parallelToolCalls,
1144
1119
  replayTarget: plan.replay.target,
1145
1120
  reasoningArtifactReplayObserver: extras.reasoningArtifactReplayObserver,
1121
+ ...(extras.forceReasoningReplay ? { forceReasoningReplay: true } : {}),
1146
1122
  });
1147
1123
  }
1148
1124
  function sentReasoningEffort(requestBody) {
@@ -1185,6 +1161,7 @@ export async function openAiCompatibleComplete(options) {
1185
1161
  const requestBody = chatCompletionsBodyFromPlan(plan, {
1186
1162
  reasoningStyle: options.reasoningStyle,
1187
1163
  reasoningArtifactReplayObserver: options.reasoningArtifactReplayObserver,
1164
+ ...(options.forceReasoningReplay ? { forceReasoningReplay: true } : {}),
1188
1165
  });
1189
1166
  let response;
1190
1167
  try {
@@ -1244,7 +1221,8 @@ export async function openAiCompatibleComplete(options) {
1244
1221
  model: options.model,
1245
1222
  baseUrl: options.baseUrl,
1246
1223
  toolCalls,
1247
- policy: options.reasoningArtifactPolicy,
1224
+ policy: options.reasoningArtifactPolicy ??
1225
+ compatibleArtifactPolicyFor(plan.policy.reasoning.finalTurnPreservation),
1248
1226
  ...(typeof reasoning === "string" && reasoning
1249
1227
  ? { reasoning: { text: reasoning, sequence: 0 } }
1250
1228
  : {}),
@@ -1374,6 +1352,7 @@ export async function openAiCompatibleStream(options) {
1374
1352
  reasoningStyle: options.reasoningStyle,
1375
1353
  includeStreamUsage: options.includeStreamUsage,
1376
1354
  reasoningArtifactReplayObserver: options.reasoningArtifactReplayObserver,
1355
+ ...(options.forceReasoningReplay ? { forceReasoningReplay: true } : {}),
1377
1356
  });
1378
1357
  let response;
1379
1358
  try {
@@ -1441,7 +1420,8 @@ export async function openAiCompatibleStream(options) {
1441
1420
  model: options.model,
1442
1421
  baseUrl: options.baseUrl,
1443
1422
  toolCalls,
1444
- policy: options.reasoningArtifactPolicy,
1423
+ policy: options.reasoningArtifactPolicy ??
1424
+ compatibleArtifactPolicyFor(plan.policy.reasoning.finalTurnPreservation),
1445
1425
  ...(typeof reasoning === "string" && reasoning
1446
1426
  ? { reasoning: { text: reasoning, sequence: 0 } }
1447
1427
  : {}),
@@ -1509,7 +1489,10 @@ export async function openAiCompatibleStream(options) {
1509
1489
  let reasoningWireSeen = "";
1510
1490
  let finishReason;
1511
1491
  let terminalSignal;
1512
- const terminalPolicy = options.streamTerminal ?? CHAT_COMPLETIONS_STREAM_TERMINAL;
1492
+ const terminalPolicy = options.streamTerminal ??
1493
+ (plan.policy.terminal.proofs.length > 0
1494
+ ? plan.policy.terminal
1495
+ : CHAT_COMPLETIONS_STREAM_TERMINAL);
1513
1496
  const emittedByteCounts = () => {
1514
1497
  let toolArgumentBytes = 0;
1515
1498
  for (const state of toolCallState.values()) {
@@ -1545,7 +1528,8 @@ export async function openAiCompatibleStream(options) {
1545
1528
  model: options.model,
1546
1529
  baseUrl: options.baseUrl,
1547
1530
  toolCalls,
1548
- policy: options.reasoningArtifactPolicy,
1531
+ policy: options.reasoningArtifactPolicy ??
1532
+ compatibleArtifactPolicyFor(plan.policy.reasoning.finalTurnPreservation),
1549
1533
  ...(reasoningSeen
1550
1534
  ? {
1551
1535
  reasoning: {
@@ -1763,7 +1747,7 @@ export async function openAiCompatibleStream(options) {
1763
1747
  const detail = typeof parsed.error === "string"
1764
1748
  ? parsed.error
1765
1749
  : (parsed.error.message ?? parsed.error.type ?? "unknown error");
1766
- throw new ProviderError(`${options.provider} stream error: ${detail}`, undefined, payload.slice(0, 500));
1750
+ throw new ProviderError(`${options.provider} stream error: ${detail}`, inBandBadRequestStatus(parsed.error), payload.slice(0, 500));
1767
1751
  }
1768
1752
  {
1769
1753
  const chunkUsage = options.providerId === "fireworks"
@@ -1771,6 +1755,7 @@ export async function openAiCompatibleStream(options) {
1771
1755
  : parseOpenAiUsage(parsed.usage, options.usageAliases);
1772
1756
  if (chunkUsage)
1773
1757
  streamUsage = chunkUsage;
1758
+ const finalUsageFrame = isFinalUsageFrame(parsed);
1774
1759
  const choice = parsed.choices?.[0];
1775
1760
  const delta = choice?.delta;
1776
1761
  const reasoningToken = delta?.reasoning_content ?? delta?.reasoning;
@@ -1790,10 +1775,17 @@ export async function openAiCompatibleStream(options) {
1790
1775
  toolProgress) {
1791
1776
  resetIdleTimer();
1792
1777
  }
1778
+ if (terminalSignal === "usage-chunk" &&
1779
+ (token || reasoningToken || toolProgress)) {
1780
+ terminalSignal = undefined;
1781
+ }
1793
1782
  if (choice?.finish_reason) {
1794
1783
  finishReason = choice.finish_reason;
1795
1784
  terminalSignal = "finish-reason";
1796
1785
  }
1786
+ else if (finalUsageFrame && terminalSignal === undefined) {
1787
+ terminalSignal = "usage-chunk";
1788
+ }
1797
1789
  if (reasoningToken) {
1798
1790
  const normalized = normalizeChannelDelta(reasoningToken, reasoningWireSeen);
1799
1791
  reasoningWireSeen = normalized.seen;