@tyvm/knowhow 0.0.144 → 0.0.146

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 (253) hide show
  1. package/bin/knowhow.js +22 -20
  2. package/package.json +1 -1
  3. package/scripts/migrate-usage-json.js +147 -0
  4. package/src/agents/base/base.ts +65 -10
  5. package/src/agents/tools/patch.ts +84 -2
  6. package/src/agents/tools/textSearch.ts +98 -3
  7. package/src/chat/CliChatService.ts +16 -0
  8. package/src/chat/modules/AgentModule.ts +159 -29
  9. package/src/chat/modules/SessionsModule.ts +13 -5
  10. package/src/chat/types.ts +15 -0
  11. package/src/chat.ts +11 -0
  12. package/src/cli.ts +2 -0
  13. package/src/clients/anthropic.ts +80 -1
  14. package/src/clients/contextLimits.ts +40 -0
  15. package/src/clients/fireworks.ts +3 -0
  16. package/src/clients/gemini.ts +67 -5
  17. package/src/clients/http.ts +85 -4
  18. package/src/clients/index.ts +4 -0
  19. package/src/clients/openai.ts +186 -35
  20. package/src/clients/pricing/fireworks.ts +50 -1
  21. package/src/clients/pricing/index.ts +1 -1
  22. package/src/clients/pricing/models.ts +3 -1
  23. package/src/clients/pricing/openai.ts +2 -2
  24. package/src/clients/pricing/qwen.ts +80 -0
  25. package/src/clients/qwen.ts +16 -0
  26. package/src/clients/types.ts +99 -8
  27. package/src/clients/xai.ts +96 -8
  28. package/src/commands/agent.ts +29 -1
  29. package/src/commands/services.ts +49 -3
  30. package/src/index.ts +9 -0
  31. package/src/plugins/plugins.ts +16 -0
  32. package/src/processors/Base64ImageDetector.ts +46 -7
  33. package/src/processors/GemmaToolProcessor.ts +163 -0
  34. package/src/processors/TokenCompressor.ts +40 -8
  35. package/src/processors/ToolResponseCache.ts +17 -8
  36. package/src/processors/index.ts +1 -0
  37. package/src/processors/tools/boundedToolResponse.ts +49 -0
  38. package/src/processors/tools/grepToolResponse.ts +164 -38
  39. package/src/processors/tools/index.ts +2 -0
  40. package/src/processors/tools/jqToolResponse.ts +40 -10
  41. package/src/processors/tools/listStoredToolResponses.ts +51 -10
  42. package/src/processors/tools/tailToolResponse.ts +85 -17
  43. package/src/services/AgentSyncFs.ts +37 -0
  44. package/src/services/LazyToolsService.ts +7 -0
  45. package/src/services/Mcp.ts +9 -2
  46. package/src/services/RuntimeReloadService.ts +31 -0
  47. package/src/services/SessionManager.ts +14 -0
  48. package/src/services/Tools.ts +14 -0
  49. package/src/services/TracingService.ts +116 -0
  50. package/src/services/index.ts +8 -0
  51. package/src/services/modules/computerUse.ts +216 -0
  52. package/src/services/modules/index.ts +90 -9
  53. package/src/services/modules/types.ts +31 -1
  54. package/src/types.ts +3 -0
  55. package/src/util/Trace.ts +236 -0
  56. package/tests/clients/contextLimits.test.ts +4 -0
  57. package/tests/clients/fireworks.test.ts +65 -0
  58. package/tests/clients/openai.images.test.ts +105 -0
  59. package/tests/clients/pricing.test.ts +6 -0
  60. package/tests/clients/reasoningEffort.test.ts +79 -0
  61. package/tests/compressor/toolResponseCache.test.ts +2 -1
  62. package/tests/compressor/usageBase64.json +159 -0
  63. package/tests/compressor/usageBase64.test.ts +175 -0
  64. package/tests/manual/clients/completions.test.ts +27 -10
  65. package/tests/patching/fixtures/transcript-mangles/agent-sync-fs-nested-catch.json +9 -0
  66. package/tests/patching/fixtures/transcript-mangles/automation-functions-crossed.json +9 -0
  67. package/tests/patching/fixtures/transcript-mangles/fruit-ninja-confirmed-retirement.json +8 -0
  68. package/tests/patching/patchFileOutput.test.ts +112 -1
  69. package/tests/patching/transcriptMangleRegression.test.ts +125 -0
  70. package/tests/processors/Base64ImageDetector.test.ts +36 -1
  71. package/tests/processors/TokenCompressor.test.ts +17 -3
  72. package/tests/processors/tools/boundedRetrievalTools.test.ts +68 -0
  73. package/tests/processors/tools/grepToolResponse.test.ts +88 -1
  74. package/tests/unit/plugins/pluginLoading.test.ts +31 -0
  75. package/tests/unit/services/RuntimeReloadService.test.ts +29 -0
  76. package/ts_build/package.json +1 -1
  77. package/ts_build/src/agents/base/base.d.ts +8 -1
  78. package/ts_build/src/agents/base/base.js +37 -3
  79. package/ts_build/src/agents/base/base.js.map +1 -1
  80. package/ts_build/src/agents/tools/patch.d.ts +1 -0
  81. package/ts_build/src/agents/tools/patch.js +35 -2
  82. package/ts_build/src/agents/tools/patch.js.map +1 -1
  83. package/ts_build/src/agents/tools/textSearch.js +56 -2
  84. package/ts_build/src/agents/tools/textSearch.js.map +1 -1
  85. package/ts_build/src/chat/CliChatService.js +15 -0
  86. package/ts_build/src/chat/CliChatService.js.map +1 -1
  87. package/ts_build/src/chat/modules/AgentModule.d.ts +14 -1
  88. package/ts_build/src/chat/modules/AgentModule.js +86 -24
  89. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  90. package/ts_build/src/chat/modules/SessionsModule.js +5 -6
  91. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  92. package/ts_build/src/chat/types.d.ts +12 -0
  93. package/ts_build/src/chat.js +7 -0
  94. package/ts_build/src/chat.js.map +1 -1
  95. package/ts_build/src/cli.d.ts +2 -1
  96. package/ts_build/src/cli.js +1 -0
  97. package/ts_build/src/cli.js.map +1 -1
  98. package/ts_build/src/clients/anthropic.d.ts +1 -0
  99. package/ts_build/src/clients/anthropic.js +40 -1
  100. package/ts_build/src/clients/anthropic.js.map +1 -1
  101. package/ts_build/src/clients/contextLimits.js +25 -0
  102. package/ts_build/src/clients/contextLimits.js.map +1 -1
  103. package/ts_build/src/clients/fireworks.js +1 -0
  104. package/ts_build/src/clients/fireworks.js.map +1 -1
  105. package/ts_build/src/clients/gemini.js +56 -4
  106. package/ts_build/src/clients/gemini.js.map +1 -1
  107. package/ts_build/src/clients/http.d.ts +2 -0
  108. package/ts_build/src/clients/http.js +66 -9
  109. package/ts_build/src/clients/http.js.map +1 -1
  110. package/ts_build/src/clients/index.d.ts +1 -0
  111. package/ts_build/src/clients/index.js +4 -0
  112. package/ts_build/src/clients/index.js.map +1 -1
  113. package/ts_build/src/clients/openai.d.ts +6 -1
  114. package/ts_build/src/clients/openai.js +135 -35
  115. package/ts_build/src/clients/openai.js.map +1 -1
  116. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -1
  117. package/ts_build/src/clients/pricing/fireworks.js +33 -1
  118. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  119. package/ts_build/src/clients/pricing/index.d.ts +1 -1
  120. package/ts_build/src/clients/pricing/index.js +6 -2
  121. package/ts_build/src/clients/pricing/index.js.map +1 -1
  122. package/ts_build/src/clients/pricing/models.d.ts +2 -1
  123. package/ts_build/src/clients/pricing/models.js +4 -1
  124. package/ts_build/src/clients/pricing/models.js.map +1 -1
  125. package/ts_build/src/clients/pricing/openai.js +2 -2
  126. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  127. package/ts_build/src/clients/pricing/qwen.d.ts +5 -0
  128. package/ts_build/src/clients/pricing/qwen.js +30 -0
  129. package/ts_build/src/clients/pricing/qwen.js.map +1 -0
  130. package/ts_build/src/clients/qwen.d.ts +4 -0
  131. package/ts_build/src/clients/qwen.js +15 -0
  132. package/ts_build/src/clients/qwen.js.map +1 -0
  133. package/ts_build/src/clients/types.d.ts +16 -1
  134. package/ts_build/src/clients/types.js +14 -0
  135. package/ts_build/src/clients/types.js.map +1 -1
  136. package/ts_build/src/clients/xai.d.ts +2 -0
  137. package/ts_build/src/clients/xai.js +79 -13
  138. package/ts_build/src/clients/xai.js.map +1 -1
  139. package/ts_build/src/commands/agent.js +15 -1
  140. package/ts_build/src/commands/agent.js.map +1 -1
  141. package/ts_build/src/commands/services.d.ts +2 -0
  142. package/ts_build/src/commands/services.js +45 -3
  143. package/ts_build/src/commands/services.js.map +1 -1
  144. package/ts_build/src/index.d.ts +2 -0
  145. package/ts_build/src/index.js +1 -0
  146. package/ts_build/src/index.js.map +1 -1
  147. package/ts_build/src/plugins/plugins.d.ts +1 -0
  148. package/ts_build/src/plugins/plugins.js +14 -0
  149. package/ts_build/src/plugins/plugins.js.map +1 -1
  150. package/ts_build/src/processors/Base64ImageDetector.js +22 -3
  151. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  152. package/ts_build/src/processors/GemmaToolProcessor.d.ts +14 -0
  153. package/ts_build/src/processors/GemmaToolProcessor.js +97 -0
  154. package/ts_build/src/processors/GemmaToolProcessor.js.map +1 -0
  155. package/ts_build/src/processors/TokenCompressor.js +15 -6
  156. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  157. package/ts_build/src/processors/ToolResponseCache.d.ts +3 -3
  158. package/ts_build/src/processors/ToolResponseCache.js +8 -8
  159. package/ts_build/src/processors/ToolResponseCache.js.map +1 -1
  160. package/ts_build/src/processors/index.d.ts +1 -0
  161. package/ts_build/src/processors/index.js +3 -1
  162. package/ts_build/src/processors/index.js.map +1 -1
  163. package/ts_build/src/processors/tools/boundedToolResponse.d.ts +7 -0
  164. package/ts_build/src/processors/tools/boundedToolResponse.js +27 -0
  165. package/ts_build/src/processors/tools/boundedToolResponse.js.map +1 -0
  166. package/ts_build/src/processors/tools/grepToolResponse.d.ts +4 -0
  167. package/ts_build/src/processors/tools/grepToolResponse.js +101 -25
  168. package/ts_build/src/processors/tools/grepToolResponse.js.map +1 -1
  169. package/ts_build/src/processors/tools/index.d.ts +2 -2
  170. package/ts_build/src/processors/tools/index.js.map +1 -1
  171. package/ts_build/src/processors/tools/jqToolResponse.d.ts +5 -1
  172. package/ts_build/src/processors/tools/jqToolResponse.js +26 -9
  173. package/ts_build/src/processors/tools/jqToolResponse.js.map +1 -1
  174. package/ts_build/src/processors/tools/listStoredToolResponses.d.ts +7 -1
  175. package/ts_build/src/processors/tools/listStoredToolResponses.js +39 -9
  176. package/ts_build/src/processors/tools/listStoredToolResponses.js.map +1 -1
  177. package/ts_build/src/processors/tools/tailToolResponse.d.ts +5 -0
  178. package/ts_build/src/processors/tools/tailToolResponse.js +56 -13
  179. package/ts_build/src/processors/tools/tailToolResponse.js.map +1 -1
  180. package/ts_build/src/services/AgentSyncFs.js +17 -0
  181. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  182. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  183. package/ts_build/src/services/LazyToolsService.js +5 -0
  184. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  185. package/ts_build/src/services/Mcp.js +8 -2
  186. package/ts_build/src/services/Mcp.js.map +1 -1
  187. package/ts_build/src/services/RuntimeReloadService.d.ts +12 -0
  188. package/ts_build/src/services/RuntimeReloadService.js +26 -0
  189. package/ts_build/src/services/RuntimeReloadService.js.map +1 -0
  190. package/ts_build/src/services/SessionManager.js +12 -0
  191. package/ts_build/src/services/SessionManager.js.map +1 -1
  192. package/ts_build/src/services/Tools.d.ts +3 -0
  193. package/ts_build/src/services/Tools.js +5 -0
  194. package/ts_build/src/services/Tools.js.map +1 -1
  195. package/ts_build/src/services/TracingService.d.ts +16 -0
  196. package/ts_build/src/services/TracingService.js +35 -0
  197. package/ts_build/src/services/TracingService.js.map +1 -0
  198. package/ts_build/src/services/index.d.ts +6 -0
  199. package/ts_build/src/services/index.js +7 -1
  200. package/ts_build/src/services/index.js.map +1 -1
  201. package/ts_build/src/services/modules/computerUse.d.ts +163 -0
  202. package/ts_build/src/services/modules/computerUse.js +3 -0
  203. package/ts_build/src/services/modules/computerUse.js.map +1 -0
  204. package/ts_build/src/services/modules/index.d.ts +6 -0
  205. package/ts_build/src/services/modules/index.js +49 -6
  206. package/ts_build/src/services/modules/index.js.map +1 -1
  207. package/ts_build/src/services/modules/types.d.ts +7 -0
  208. package/ts_build/src/types.d.ts +3 -0
  209. package/ts_build/src/types.js.map +1 -1
  210. package/ts_build/src/util/Trace.d.ts +13 -0
  211. package/ts_build/src/util/Trace.js +107 -0
  212. package/ts_build/src/util/Trace.js.map +1 -0
  213. package/ts_build/src/workers/tools/index.d.ts +1 -1
  214. package/ts_build/tests/clients/contextLimits.test.js +2 -0
  215. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  216. package/ts_build/tests/clients/fireworks.test.d.ts +1 -0
  217. package/ts_build/tests/clients/fireworks.test.js +65 -0
  218. package/ts_build/tests/clients/fireworks.test.js.map +1 -0
  219. package/ts_build/tests/clients/openai.images.test.d.ts +1 -0
  220. package/ts_build/tests/clients/openai.images.test.js +94 -0
  221. package/ts_build/tests/clients/openai.images.test.js.map +1 -0
  222. package/ts_build/tests/clients/pricing.test.js +4 -0
  223. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  224. package/ts_build/tests/clients/reasoningEffort.test.d.ts +1 -0
  225. package/ts_build/tests/clients/reasoningEffort.test.js +68 -0
  226. package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -0
  227. package/ts_build/tests/compressor/toolResponseCache.test.js +1 -1
  228. package/ts_build/tests/compressor/toolResponseCache.test.js.map +1 -1
  229. package/ts_build/tests/compressor/usageBase64.test.d.ts +1 -0
  230. package/ts_build/tests/compressor/usageBase64.test.js +109 -0
  231. package/ts_build/tests/compressor/usageBase64.test.js.map +1 -0
  232. package/ts_build/tests/manual/clients/completions.test.js +19 -6
  233. package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
  234. package/ts_build/tests/patching/patchFileOutput.test.js +82 -0
  235. package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -1
  236. package/ts_build/tests/patching/transcriptMangleRegression.test.d.ts +1 -0
  237. package/ts_build/tests/patching/transcriptMangleRegression.test.js +104 -0
  238. package/ts_build/tests/patching/transcriptMangleRegression.test.js.map +1 -0
  239. package/ts_build/tests/processors/Base64ImageDetector.test.js +24 -0
  240. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  241. package/ts_build/tests/processors/TokenCompressor.test.js +11 -2
  242. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  243. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.d.ts +1 -0
  244. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js +57 -0
  245. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js.map +1 -0
  246. package/ts_build/tests/processors/tools/grepToolResponse.test.js +37 -0
  247. package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -1
  248. package/ts_build/tests/unit/plugins/pluginLoading.test.js +59 -0
  249. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  250. package/ts_build/tests/unit/services/RuntimeReloadService.test.d.ts +1 -0
  251. package/ts_build/tests/unit/services/RuntimeReloadService.test.js +26 -0
  252. package/ts_build/tests/unit/services/RuntimeReloadService.test.js.map +1 -0
  253. package/x-home.png +0 -0
@@ -225,17 +225,44 @@ export class GenericGeminiClient implements GenericClient {
225
225
  msg.tool_calls &&
226
226
  msg.tool_calls.length > 0
227
227
  ) {
228
+ // Look up any captured thoughtSignatures for this assistant message so
229
+ // we can re-attach them to the matching functionCall part (required by
230
+ // Gemini 3 to preserve reasoning continuity across tool-use turns).
231
+ const details = (msg as any)._reasoning_details;
232
+ const sigByCallId: { [id: string]: string } = {};
233
+ const standaloneThoughtSigs: string[] = [];
234
+ if (details && details.provider === "gemini" && Array.isArray(details.items)) {
235
+ for (const item of details.items) {
236
+ if (item?.kind === "functionCall" && item.callId) {
237
+ sigByCallId[item.callId] = item.thoughtSignature;
238
+ } else if (item?.kind === "thoughtSignature" && item.thoughtSignature) {
239
+ standaloneThoughtSigs.push(item.thoughtSignature);
240
+ }
241
+ }
242
+ }
228
243
  for (const toolCall of msg.tool_calls) {
229
- parts.push({
244
+ const callPart: any = {
230
245
  functionCall: {
231
246
  name: toolCall.function.name,
232
247
  // Google expects arguments as a parsed object, not a string
233
248
  args: JSON.parse(toolCall.function.arguments || "{}"),
234
249
  },
235
- });
250
+ };
251
+ if (sigByCallId[toolCall.id]) {
252
+ callPart.thoughtSignature = sigByCallId[toolCall.id];
253
+ }
254
+ parts.push(callPart);
236
255
  // Store the tool call to potentially link with a future tool response message
237
256
  assistantToolCalls[toolCall.id] = toolCall;
238
257
  }
258
+ // Re-attach any standalone thought-part signatures onto the leading
259
+ // text part(s) so reasoning-only thoughts are also round-tripped.
260
+ for (let s = 0; s < standaloneThoughtSigs.length; s++) {
261
+ const p: any = parts[s];
262
+ if (p && "text" in p && !p.thoughtSignature) {
263
+ p.thoughtSignature = standaloneThoughtSigs[s];
264
+ }
265
+ }
239
266
  }
240
267
 
241
268
  if (parts.length > 0) {
@@ -407,9 +434,13 @@ export class GenericGeminiClient implements GenericClient {
407
434
  // Gemini 3.x — use thinkingLevel
408
435
  if (GoogleThinkingLevelModels.includes(model)) {
409
436
  const levelMap: Record<string, string> = {
437
+ none: "minimal",
438
+ minimal: "minimal",
410
439
  low: "low",
411
440
  medium: "medium",
412
441
  high: "high",
442
+ xhigh: "high",
443
+ max: "high",
413
444
  };
414
445
  return {
415
446
  thinkingLevel: levelMap[effort] ?? "low",
@@ -420,9 +451,13 @@ export class GenericGeminiClient implements GenericClient {
420
451
  if (GoogleThinkingBudgetModels.includes(model)) {
421
452
  // Map effort to token budget
422
453
  const budgetMap: Record<string, number> = {
454
+ none: 0,
455
+ minimal: 512,
423
456
  low: 1024,
424
457
  medium: 8192,
425
458
  high: -1, // dynamic
459
+ xhigh: -1,
460
+ max: -1,
426
461
  };
427
462
  return {
428
463
  thinkingBudget: budgetMap[effort] ?? 1024,
@@ -499,24 +534,51 @@ export class GenericGeminiClient implements GenericClient {
499
534
  return { message };
500
535
  }
501
536
 
537
+ // Capture Gemini thoughtSignatures so they can be round-tripped on the
538
+ // next turn. Gemini 3 REQUIRES these to be echoed back on the matching
539
+ // functionCall part or it returns a 400 "Thought signature is not
540
+ // valid" on the 2nd+ tool-calling turn. We key them by tool-call id
541
+ // (and also keep any standalone thought-part signatures in order).
542
+ const reasoningItems: any[] = [];
543
+
502
544
  candidate?.content?.parts?.forEach((part) => {
503
545
  if ("text" in part && typeof part.text === "string") {
504
546
  textContent += part.text; // Concatenate text parts
547
+ if ((part as any).thoughtSignature) {
548
+ reasoningItems.push({
549
+ kind: "thoughtSignature",
550
+ thoughtSignature: (part as any).thoughtSignature,
551
+ });
552
+ }
505
553
  } else if ("functionCall" in part && part.functionCall) {
554
+ const callId =
555
+ part.functionCall.id ||
556
+ `fc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
506
557
  message.tool_calls.push({
507
- id:
508
- part.functionCall.id ||
509
- `fc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
558
+ id: callId,
510
559
  type: "function",
511
560
  function: {
512
561
  name: part.functionCall.name,
513
562
  arguments: JSON.stringify(part.functionCall.args || {}),
514
563
  },
515
564
  });
565
+ if ((part as any).thoughtSignature) {
566
+ reasoningItems.push({
567
+ kind: "functionCall",
568
+ callId,
569
+ thoughtSignature: (part as any).thoughtSignature,
570
+ });
571
+ }
516
572
  }
517
573
  });
518
574
 
519
575
  message.content = textContent || null;
576
+ if (reasoningItems.length > 0) {
577
+ (message as any)._reasoning_details = {
578
+ provider: "gemini",
579
+ items: reasoningItems,
580
+ };
581
+ }
520
582
 
521
583
  return { message };
522
584
  }) || []; // Handle case with no candidates
@@ -92,6 +92,33 @@ export class HttpClient implements GenericClient {
92
92
  this.pricingMap = pricingMap;
93
93
  }
94
94
 
95
+ /**
96
+ * A stable provider id used to tag/read the provider-agnostic
97
+ * `_reasoning_details` slot. Reasoning items are opaque and only meaningful
98
+ * to the provider that produced them, so we key on the client's baseUrl.
99
+ */
100
+ protected reasoningProviderId(): string {
101
+ return `http:${this.baseUrl}`;
102
+ }
103
+
104
+ /**
105
+ * Resolve the provider-native reasoning items (Responses API `reasoning`
106
+ * items with encrypted_content) to re-inject for an assistant message,
107
+ * reading the provider-agnostic `_reasoning_details` slot only when it was
108
+ * produced by this same client.
109
+ */
110
+ protected getReasoningItems(msg: any): any[] {
111
+ const details = msg?._reasoning_details;
112
+ if (
113
+ details &&
114
+ details.provider === this.reasoningProviderId() &&
115
+ Array.isArray(details.items)
116
+ ) {
117
+ return details.items;
118
+ }
119
+ return [];
120
+ }
121
+
95
122
  /**
96
123
  * Returns the pricing entry for a specific model, or all pricing entries if no model is given.
97
124
  * Returns undefined for a specific model if no pricing is known.
@@ -299,22 +326,44 @@ export class HttpClient implements GenericClient {
299
326
  .trim() || undefined;
300
327
 
301
328
  // Convert messages to Responses API input format
329
+ // Responses API hard limit: 10,485,760 chars per function_call_output.output
330
+ const MAX_TOOL_OUTPUT = 10_485_000;
331
+ const truncateToolOutput = (s: string) =>
332
+ s.length > MAX_TOOL_OUTPUT
333
+ ? s.slice(0, MAX_TOOL_OUTPUT) + "\n\n[TRUNCATED: output exceeded 10MB API limit]"
334
+ : s;
302
335
  const input: any[] = nonSystemMessages.map((msg) => {
303
336
  if (msg.role === "tool") {
304
337
  return {
305
338
  type: "function_call_output",
306
339
  call_id: msg.tool_call_id,
307
- output: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
340
+ output: truncateToolOutput(typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)),
308
341
  };
309
342
  }
310
343
  if (msg.role === "assistant" && msg.tool_calls?.length) {
311
- return (msg.tool_calls as any[]).map((tc: any) => ({
344
+ // Re-inject any captured reasoning items (encrypted_content) BEFORE
345
+ // the function_call items so the model preserves its chain-of-thought
346
+ // across tool-use turns instead of re-deriving it each turn.
347
+ const reasoningItems = this.getReasoningItems(msg);
348
+ return [
349
+ ...reasoningItems,
350
+ ...(msg.tool_calls as any[]).map((tc: any) => ({
312
351
  type: "function_call",
313
352
  id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
314
353
  call_id: tc.id,
315
354
  name: tc.function.name,
316
355
  arguments: tc.function.arguments,
317
- }));
356
+ })),
357
+ ];
358
+ }
359
+ if (msg.role === "assistant" && this.getReasoningItems(msg).length) {
360
+ return [
361
+ ...this.getReasoningItems(msg),
362
+ {
363
+ role: msg.role,
364
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
365
+ },
366
+ ];
318
367
  }
319
368
  return {
320
369
  role: msg.role,
@@ -336,7 +385,18 @@ export class HttpClient implements GenericClient {
336
385
  ...(instructions && { instructions }),
337
386
  ...(options.max_tokens && { max_output_tokens: options.max_tokens }),
338
387
  ...(tools?.length && { tools, tool_choice: "auto" }),
339
- store,
388
+ // Reasoning persistence: store server-side when requested, otherwise
389
+ // ask the provider to return encrypted reasoning content so we can
390
+ // thread the reasoning items back into the next request ourselves.
391
+ store: options.store === true ? true : store,
392
+ ...(options.previous_response_id && {
393
+ previous_response_id: options.previous_response_id,
394
+ }),
395
+ ...(options.store !== true &&
396
+ options.reasoning_effort &&
397
+ options.reasoning_effort !== "none" && {
398
+ include: ["reasoning.encrypted_content"],
399
+ }),
340
400
  ...this.extra_body,
341
401
  };
342
402
 
@@ -368,6 +428,10 @@ export class HttpClient implements GenericClient {
368
428
  // Collect text content and tool calls from output items
369
429
  let textContent: string | null = null;
370
430
  const toolCalls: any[] = [];
431
+ // Reasoning items (encrypted_content) to thread back into the next turn,
432
+ // plus a human-readable summary of the model's thinking (if present).
433
+ const reasoningItems: any[] = [];
434
+ let reasoningSummary: string | null = null;
371
435
 
372
436
  for (const item of data.output ?? []) {
373
437
  if (item.type === "message") {
@@ -376,6 +440,15 @@ export class HttpClient implements GenericClient {
376
440
  textContent = (textContent ?? "") + part.text;
377
441
  }
378
442
  }
443
+ } else if (item.type === "reasoning") {
444
+ reasoningItems.push(item);
445
+ const summaryParts = item.summary ?? [];
446
+ for (const part of summaryParts) {
447
+ const text = typeof part === "string" ? part : part?.text;
448
+ if (text) {
449
+ reasoningSummary = (reasoningSummary ?? "") + text;
450
+ }
451
+ }
379
452
  } else if (item.type === "function_call") {
380
453
  toolCalls.push({
381
454
  id: item.call_id,
@@ -392,12 +465,20 @@ export class HttpClient implements GenericClient {
392
465
  role: "assistant",
393
466
  content: textContent,
394
467
  ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
468
+ ...(reasoningSummary && { reasoning_summary: reasoningSummary }),
469
+ ...(reasoningItems.length > 0 && {
470
+ _reasoning_details: {
471
+ provider: this.reasoningProviderId(),
472
+ items: reasoningItems,
473
+ },
474
+ }),
395
475
  },
396
476
  },
397
477
  ],
398
478
  model: data.model ?? options.model,
399
479
  usage,
400
480
  usd_cost: data.usd_cost ?? this.calculateCost(options.model, usage),
481
+ response_id: data.id,
401
482
  };
402
483
  });
403
484
  }
@@ -58,6 +58,7 @@ import { GitHubCopilotClient } from "./copilot";
58
58
  import { GenericLlamaClient } from "./llama";
59
59
  import { GenericFireworksClient } from "./fireworks";
60
60
  import { GenericMetaClient } from "./meta";
61
+ import { GenericQwenClient } from "./qwen";
61
62
  export {
62
63
  OpenAiTextPricing,
63
64
  AnthropicTextPricing,
@@ -103,6 +104,7 @@ const BUILT_IN_PROVIDER_REGISTRY: Record<string, ProviderRegistryEntry> = {
103
104
  llama: { clientClass: GenericLlamaClient },
104
105
  fireworks: { clientClass: GenericFireworksClient },
105
106
  meta: { clientClass: GenericMetaClient },
107
+ qwen: { clientClass: GenericQwenClient },
106
108
  knowhow: {
107
109
  createClient: (entry: ModelProvider) => {
108
110
  const jwt = loadKnowhowJwt();
@@ -134,6 +136,7 @@ const DEFAULT_PROVIDERS: ModelProvider[] = [
134
136
  { provider: "llama", envKey: "LLAMA_API_KEY" },
135
137
  { provider: "fireworks", envKey: "FIREWORKS_API_KEY" },
136
138
  { provider: "meta", envKey: "META_API_KEY" },
139
+ { provider: "qwen", envKey: "QWEN_CLOUD_API_KEY" },
137
140
  ];
138
141
 
139
142
  export class AIClient {
@@ -1075,3 +1078,4 @@ export * from "./llama";
1075
1078
  export * from "./copilot";
1076
1079
  export * from "./fireworks";
1077
1080
  export * from "./meta";
1081
+ export * from "./qwen";
@@ -22,13 +22,17 @@ import {
22
22
  FileUploadResponse,
23
23
  FileDownloadOptions,
24
24
  FileDownloadResponse,
25
+ Message,
25
26
  } from "./types";
26
27
  import {
27
28
  ChatCompletionMessageParam,
28
29
  ChatCompletionToolMessageParam,
29
30
  ChatCompletionMessageToolCall,
30
31
  } from "openai/resources/chat";
31
- import { ResponseFunctionToolCall } from "openai/resources/responses/responses";
32
+ import {
33
+ ResponseFunctionToolCall,
34
+ ResponseInputMessageContentList,
35
+ } from "openai/resources/responses/responses";
32
36
 
33
37
  import {
34
38
  EmbeddingModels,
@@ -108,9 +112,10 @@ export class GenericOpenAiClient implements GenericClient {
108
112
 
109
113
  resolveReasoningEffort(options: CompletionOptions): "low" | "medium" | "high" {
110
114
  const effort = options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
111
- // "none" is not a valid SDK ReasoningEffort — callers that need "none" should
112
- // check isReasoningDisabled() separately and skip the reasoning params.
113
- if (effort === "none") return "low";
115
+ // Chat Completions only accepts low/medium/high. Preserve newer values on
116
+ // the Responses API, but map them to the nearest legacy level here.
117
+ if (effort === "none" || effort === "minimal") return "low";
118
+ if (effort === "xhigh" || effort === "max") return "high";
114
119
  return effort;
115
120
  }
116
121
 
@@ -121,7 +126,9 @@ export class GenericOpenAiClient implements GenericClient {
121
126
  */
122
127
  resolveReasoningEffortForModel(options: CompletionOptions): string {
123
128
  const raw = options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
124
- const requested = raw === "none" ? "none" : raw;
129
+ // Anthropic calls its highest level `max`; OpenAI calls it `xhigh`.
130
+ // Accept both provider-neutral CLI spellings.
131
+ const requested = raw === "max" ? "xhigh" : raw;
125
132
  const pricing = OpenAiTextPricing[options.model];
126
133
  const supportedLevels = pricing?.reasoningLevels;
127
134
  if (!supportedLevels || supportedLevels.length === 0) {
@@ -140,6 +147,75 @@ export class GenericOpenAiClient implements GenericClient {
140
147
  return options.reasoning_effort === "none";
141
148
  }
142
149
 
150
+ /**
151
+ * OpenAI only permits image parts on user messages. A Chat Completions
152
+ * `tool` message may contain text parts, but not `image_url` parts. Keep the
153
+ * required tool result and attach images in a following user message.
154
+ */
155
+ toChatCompletionMessages(
156
+ messages: CompletionOptions["messages"]
157
+ ): ChatCompletionMessageParam[] {
158
+ return messages.flatMap((msg): ChatCompletionMessageParam[] => {
159
+ if (msg.role !== "tool") return [msg as ChatCompletionMessageParam];
160
+
161
+ const parts = Array.isArray(msg.content) ? msg.content : [];
162
+ const unsupportedPart = parts.find(
163
+ (part) => part.type !== "text" && part.type !== "image_url"
164
+ );
165
+ if (unsupportedPart) {
166
+ throw new Error(
167
+ `OpenAI Chat Completions cannot attach ${unsupportedPart.type} content to a tool result.`
168
+ );
169
+ }
170
+ const imageParts = parts.filter((part) => part.type === "image_url");
171
+ const textParts = parts.filter((part) => part.type === "text");
172
+ const toolMessage: ChatCompletionToolMessageParam = {
173
+ role: "tool",
174
+ tool_call_id: msg.tool_call_id!,
175
+ content: Array.isArray(msg.content)
176
+ ? textParts.length
177
+ ? textParts
178
+ : [{
179
+ type: "text",
180
+ text: "Image output attached in the following user message.",
181
+ }]
182
+ : msg.content || "",
183
+ };
184
+
185
+ if (!imageParts.length) return [toolMessage];
186
+ return [
187
+ toolMessage,
188
+ { role: "user", content: imageParts } as ChatCompletionMessageParam,
189
+ ];
190
+ });
191
+ }
192
+
193
+ /** Convert the CLI's Chat Completions-style parts to Responses API parts. */
194
+ toResponseContent(
195
+ content: Message["content"]
196
+ ): string | ResponseInputMessageContentList {
197
+ if (!Array.isArray(content)) return content || "";
198
+ const result: ResponseInputMessageContentList = [];
199
+ for (const part of content) {
200
+ if (part.type === "text") {
201
+ result.push({ type: "input_text", text: part.text });
202
+ }
203
+ else if (part.type === "image_url") {
204
+ result.push({
205
+ type: "input_image",
206
+ image_url: part.image_url.url,
207
+ detail: part.image_url.detail || "auto",
208
+ });
209
+ }
210
+ else {
211
+ throw new Error(
212
+ `OpenAI Responses API conversion does not support ${part.type} content.`
213
+ );
214
+ }
215
+ }
216
+ return result;
217
+ }
218
+
143
219
  async createChatCompletion(
144
220
  options: CompletionOptions
145
221
  ): Promise<CompletionResponse> {
@@ -148,17 +224,7 @@ export class GenericOpenAiClient implements GenericClient {
148
224
  return this.createChatResponse(options);
149
225
  }
150
226
 
151
- const openaiMessages = options.messages.map((msg) => {
152
- if (msg.role === "tool") {
153
- return {
154
- ...msg,
155
- content: msg.content || "",
156
- role: "tool",
157
- tool_call_id: msg.tool_call_id,
158
- } as ChatCompletionToolMessageParam;
159
- }
160
- return msg as ChatCompletionMessageParam;
161
- });
227
+ const openaiMessages = this.toChatCompletionMessages(options.messages);
162
228
 
163
229
  const response = await this.client.chat.completions.create({
164
230
  model: options.model,
@@ -222,17 +288,7 @@ export class GenericOpenAiClient implements GenericClient {
222
288
  return;
223
289
  }
224
290
 
225
- const openaiMessages = options.messages.map((msg) => {
226
- if (msg.role === "tool") {
227
- return {
228
- ...msg,
229
- content: msg.content || "",
230
- role: "tool",
231
- tool_call_id: msg.tool_call_id,
232
- } as ChatCompletionToolMessageParam;
233
- }
234
- return msg as ChatCompletionMessageParam;
235
- });
291
+ const openaiMessages = this.toChatCompletionMessages(options.messages);
236
292
 
237
293
  const stream = await this.client.chat.completions.create({
238
294
  model: options.model,
@@ -307,6 +363,20 @@ export class GenericOpenAiClient implements GenericClient {
307
363
  }
308
364
  }
309
365
 
366
+ /**
367
+ * Resolve the OpenAI-native reasoning items to re-inject for an assistant
368
+ * message. Reads the provider-agnostic `_reasoning_details` slot (only when
369
+ * it was produced by this provider) and falls back to the deprecated
370
+ * `_reasoning_items` for backward compatibility.
371
+ */
372
+ private getReasoningItems(msg: any): any[] {
373
+ const details = (msg as any)._reasoning_details;
374
+ if (details && details.provider === "openai" && Array.isArray(details.items)) {
375
+ return details.items;
376
+ }
377
+ return (msg as any)._reasoning_items ?? [];
378
+ }
379
+
310
380
  async createChatResponse(
311
381
  options: CompletionOptions
312
382
  ): Promise<CompletionResponse> {
@@ -324,30 +394,68 @@ export class GenericOpenAiClient implements GenericClient {
324
394
 
325
395
  // Convert chat messages to Responses API input items
326
396
  // The Responses API accepts: user/assistant/system messages and function_call_output items
397
+ // OpenAI Responses API hard limit: 10,485,760 chars per function_call_output.output
398
+ const MAX_TOOL_OUTPUT = 10_485_000;
399
+ const truncateToolOutput = (s: string) =>
400
+ s.length > MAX_TOOL_OUTPUT
401
+ ? s.slice(0, MAX_TOOL_OUTPUT) + "\n\n[TRUNCATED: output exceeded 10MB API limit]"
402
+ : s;
327
403
  const input: any[] = nonSystemMessages.map((msg) => {
328
404
  if (msg.role === "tool") {
329
- // tool result → function_call_output
330
- return {
405
+ // OpenAI function outputs cannot directly contain image content. Send
406
+ // the required function output, then attach images as a user message.
407
+ const parts = Array.isArray(msg.content) ? msg.content : [];
408
+ const imageParts = parts.filter((part) => part.type === "image_url");
409
+ const text = parts
410
+ .filter((part) => part.type === "text")
411
+ .map((part: any) => part.text)
412
+ .join("\n");
413
+ const toolOutput = {
331
414
  type: "function_call_output",
332
415
  call_id: msg.tool_call_id,
333
- output: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
416
+ output: truncateToolOutput(typeof msg.content === "string"
417
+ ? msg.content
418
+ : text || "Image output attached in the following user message."),
334
419
  };
420
+ if (!imageParts.length) return toolOutput;
421
+ return [
422
+ toolOutput,
423
+ { role: "user", content: this.toResponseContent(imageParts) },
424
+ ];
335
425
  }
336
426
  if (msg.role === "assistant" && msg.tool_calls?.length) {
337
- // assistant message with tool calls → function_call items
338
- return msg.tool_calls.map((tc) => ({
427
+ // assistant message with tool calls → function_call items.
428
+ // If the prior turn captured reasoning items (encrypted_content path),
429
+ // re-inject them *before* the function_call items so the model keeps its
430
+ // chain-of-thought instead of re-deriving it from scratch.
431
+ const reasoningItems = this.getReasoningItems(msg);
432
+ return [
433
+ ...reasoningItems,
434
+ ...msg.tool_calls.map((tc) => ({
339
435
  type: "function_call",
340
436
  // id must start with 'fc_'; call_id is the original call_ ID used for function_call_output matching
341
437
  id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
342
438
  call_id: tc.id,
343
439
  name: tc.function.name,
344
440
  arguments: tc.function.arguments,
345
- }));
441
+ })),
442
+ ];
443
+ }
444
+ if (msg.role === "assistant" && this.getReasoningItems(msg).length) {
445
+ // assistant message (no tool calls) that carried reasoning items →
446
+ // re-inject reasoning items, then the visible message content.
447
+ return [
448
+ ...this.getReasoningItems(msg),
449
+ {
450
+ role: msg.role,
451
+ content: this.toResponseContent(msg.content),
452
+ },
453
+ ];
346
454
  }
347
455
  // Regular user/assistant message
348
456
  return {
349
457
  role: msg.role,
350
- content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
458
+ content: this.toResponseContent(msg.content),
351
459
  };
352
460
  }).flat();
353
461
 
@@ -360,10 +468,29 @@ export class GenericOpenAiClient implements GenericClient {
360
468
  strict: false,
361
469
  }));
362
470
 
471
+ // Reasoning persistence:
472
+ // - store === true → OpenAI persists the response server-side; the next
473
+ // turn passes previous_response_id to continue the reasoning chain.
474
+ // - store !== true → stateless; ask OpenAI to return encrypted reasoning
475
+ // content so we can thread it back into the next request's input ourselves.
476
+ const store = options.store === true;
477
+ const wantsReasoning =
478
+ OpenAiReasoningModels.includes(options.model) &&
479
+ !this.isReasoningDisabled(options);
480
+
363
481
  const response = await this.client.responses.create({
364
482
  model: options.model as any,
365
483
  input,
366
484
  ...(instructions && { instructions }),
485
+ ...(store && { store: true }),
486
+ ...(!store && { store: false }),
487
+ ...(options.previous_response_id && {
488
+ previous_response_id: options.previous_response_id,
489
+ }),
490
+ // When stateless, request encrypted reasoning so we can re-inject it next turn.
491
+ ...(!store && wantsReasoning && {
492
+ include: ["reasoning.encrypted_content"],
493
+ }),
367
494
  // Don't limit max_output_tokens for Responses API - codex truncates tool call arguments when limited
368
495
  ...(OpenAiReasoningModels.includes(options.model) && !this.isReasoningDisabled(options) && {
369
496
  max_output_tokens: Math.max(options.max_tokens || 0, 16000),
@@ -376,7 +503,6 @@ export class GenericOpenAiClient implements GenericClient {
376
503
  tools,
377
504
  tool_choice: "auto",
378
505
  }),
379
- store: false,
380
506
  } as any);
381
507
 
382
508
  // Map Responses API usage to Chat Completions usage format
@@ -405,6 +531,10 @@ export class GenericOpenAiClient implements GenericClient {
405
531
  // Collect text content and tool calls from the output items
406
532
  let textContent: string | null = null;
407
533
  const toolCalls: ChatCompletionMessageToolCall[] = [];
534
+ // Reasoning items (with encrypted_content) to thread back into the next turn,
535
+ // and a human-readable summary of the model's thinking (when requested).
536
+ const reasoningItems: any[] = [];
537
+ let reasoningSummary: string | null = null;
408
538
 
409
539
  for (const item of response.output) {
410
540
  if (item.type === "message") {
@@ -415,6 +545,18 @@ export class GenericOpenAiClient implements GenericClient {
415
545
  textContent = (textContent ?? "") + part.text;
416
546
  }
417
547
  }
548
+ } else if (item.type === "reasoning") {
549
+ // ResponseReasoningItem — keep the raw item so we can re-inject it
550
+ // (it carries encrypted_content when include was requested), and pull
551
+ // out any human-readable summary text.
552
+ reasoningItems.push(item);
553
+ const summaryParts = (item as any).summary ?? [];
554
+ for (const part of summaryParts) {
555
+ const text = typeof part === "string" ? part : part?.text;
556
+ if (text) {
557
+ reasoningSummary = (reasoningSummary ?? "") + text;
558
+ }
559
+ }
418
560
  } else if (item.type === "function_call") {
419
561
  // ResponseFunctionToolCall
420
562
  const fc = item as ResponseFunctionToolCall;
@@ -444,12 +586,21 @@ export class GenericOpenAiClient implements GenericClient {
444
586
  role: "assistant",
445
587
  content: textContent,
446
588
  ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
589
+ ...(reasoningSummary && { reasoning_summary: reasoningSummary }),
590
+ // Stash the raw reasoning items (encrypted_content) so the next call
591
+ // to createChatResponse can re-inject them into the input. We populate
592
+ // both the provider-agnostic slot and the deprecated legacy field.
593
+ ...(reasoningItems.length > 0 && {
594
+ _reasoning_items: reasoningItems,
595
+ _reasoning_details: { provider: "openai", items: reasoningItems },
596
+ }),
447
597
  },
448
598
  },
449
599
  ],
450
600
  model: options.model,
451
601
  usage,
452
602
  usd_cost: usdCost,
603
+ response_id: (response as any).id,
453
604
  };
454
605
  }
455
606