@tyvm/knowhow 0.0.144 → 0.0.145

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 (151) 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/modules/AgentModule.ts +129 -6
  8. package/src/chat/modules/SessionsModule.ts +6 -0
  9. package/src/chat/types.ts +14 -0
  10. package/src/chat.ts +11 -0
  11. package/src/cli.ts +2 -0
  12. package/src/clients/anthropic.ts +56 -1
  13. package/src/clients/contextLimits.ts +40 -0
  14. package/src/clients/gemini.ts +59 -5
  15. package/src/clients/http.ts +85 -4
  16. package/src/clients/index.ts +4 -0
  17. package/src/clients/openai.ts +179 -31
  18. package/src/clients/pricing/fireworks.ts +50 -1
  19. package/src/clients/pricing/index.ts +1 -1
  20. package/src/clients/pricing/models.ts +3 -1
  21. package/src/clients/pricing/openai.ts +2 -2
  22. package/src/clients/pricing/qwen.ts +80 -0
  23. package/src/clients/qwen.ts +16 -0
  24. package/src/clients/types.ts +69 -1
  25. package/src/clients/xai.ts +73 -4
  26. package/src/commands/agent.ts +20 -1
  27. package/src/index.ts +9 -0
  28. package/src/processors/Base64ImageDetector.ts +46 -7
  29. package/src/processors/GemmaToolProcessor.ts +163 -0
  30. package/src/processors/index.ts +1 -0
  31. package/src/services/AgentSyncFs.ts +37 -0
  32. package/src/services/SessionManager.ts +14 -0
  33. package/src/services/Tools.ts +3 -0
  34. package/src/services/modules/computerUse.ts +216 -0
  35. package/src/services/modules/index.ts +47 -9
  36. package/src/services/modules/types.ts +23 -1
  37. package/src/types.ts +3 -0
  38. package/tests/clients/contextLimits.test.ts +4 -0
  39. package/tests/clients/openai.images.test.ts +105 -0
  40. package/tests/clients/pricing.test.ts +6 -0
  41. package/tests/compressor/usageBase64.json +159 -0
  42. package/tests/compressor/usageBase64.test.ts +175 -0
  43. package/tests/patching/fixtures/transcript-mangles/agent-sync-fs-nested-catch.json +9 -0
  44. package/tests/patching/fixtures/transcript-mangles/automation-functions-crossed.json +9 -0
  45. package/tests/patching/fixtures/transcript-mangles/fruit-ninja-confirmed-retirement.json +8 -0
  46. package/tests/patching/patchFileOutput.test.ts +112 -1
  47. package/tests/patching/transcriptMangleRegression.test.ts +125 -0
  48. package/tests/processors/Base64ImageDetector.test.ts +36 -1
  49. package/ts_build/package.json +1 -1
  50. package/ts_build/src/agents/base/base.d.ts +8 -1
  51. package/ts_build/src/agents/base/base.js +37 -3
  52. package/ts_build/src/agents/base/base.js.map +1 -1
  53. package/ts_build/src/agents/tools/patch.d.ts +1 -0
  54. package/ts_build/src/agents/tools/patch.js +35 -2
  55. package/ts_build/src/agents/tools/patch.js.map +1 -1
  56. package/ts_build/src/agents/tools/textSearch.js +56 -2
  57. package/ts_build/src/agents/tools/textSearch.js.map +1 -1
  58. package/ts_build/src/chat/modules/AgentModule.d.ts +14 -1
  59. package/ts_build/src/chat/modules/AgentModule.js +73 -5
  60. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  61. package/ts_build/src/chat/modules/SessionsModule.js +4 -0
  62. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  63. package/ts_build/src/chat/types.d.ts +11 -0
  64. package/ts_build/src/chat.js +7 -0
  65. package/ts_build/src/chat.js.map +1 -1
  66. package/ts_build/src/cli.d.ts +2 -1
  67. package/ts_build/src/cli.js +1 -0
  68. package/ts_build/src/cli.js.map +1 -1
  69. package/ts_build/src/clients/anthropic.js +28 -1
  70. package/ts_build/src/clients/anthropic.js.map +1 -1
  71. package/ts_build/src/clients/contextLimits.js +25 -0
  72. package/ts_build/src/clients/contextLimits.js.map +1 -1
  73. package/ts_build/src/clients/gemini.js +48 -4
  74. package/ts_build/src/clients/gemini.js.map +1 -1
  75. package/ts_build/src/clients/http.d.ts +2 -0
  76. package/ts_build/src/clients/http.js +66 -9
  77. package/ts_build/src/clients/http.js.map +1 -1
  78. package/ts_build/src/clients/index.d.ts +1 -0
  79. package/ts_build/src/clients/index.js +4 -0
  80. package/ts_build/src/clients/index.js.map +1 -1
  81. package/ts_build/src/clients/openai.d.ts +6 -1
  82. package/ts_build/src/clients/openai.js +131 -33
  83. package/ts_build/src/clients/openai.js.map +1 -1
  84. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -1
  85. package/ts_build/src/clients/pricing/fireworks.js +33 -1
  86. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  87. package/ts_build/src/clients/pricing/index.d.ts +1 -1
  88. package/ts_build/src/clients/pricing/index.js +6 -2
  89. package/ts_build/src/clients/pricing/index.js.map +1 -1
  90. package/ts_build/src/clients/pricing/models.d.ts +2 -1
  91. package/ts_build/src/clients/pricing/models.js +4 -1
  92. package/ts_build/src/clients/pricing/models.js.map +1 -1
  93. package/ts_build/src/clients/pricing/openai.js +2 -2
  94. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  95. package/ts_build/src/clients/pricing/qwen.d.ts +5 -0
  96. package/ts_build/src/clients/pricing/qwen.js +30 -0
  97. package/ts_build/src/clients/pricing/qwen.js.map +1 -0
  98. package/ts_build/src/clients/qwen.d.ts +4 -0
  99. package/ts_build/src/clients/qwen.js +15 -0
  100. package/ts_build/src/clients/qwen.js.map +1 -0
  101. package/ts_build/src/clients/types.d.ts +12 -0
  102. package/ts_build/src/clients/xai.d.ts +1 -0
  103. package/ts_build/src/clients/xai.js +56 -9
  104. package/ts_build/src/clients/xai.js.map +1 -1
  105. package/ts_build/src/commands/agent.js +10 -1
  106. package/ts_build/src/commands/agent.js.map +1 -1
  107. package/ts_build/src/index.d.ts +2 -0
  108. package/ts_build/src/index.js +1 -0
  109. package/ts_build/src/index.js.map +1 -1
  110. package/ts_build/src/processors/Base64ImageDetector.js +22 -3
  111. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  112. package/ts_build/src/processors/GemmaToolProcessor.d.ts +14 -0
  113. package/ts_build/src/processors/GemmaToolProcessor.js +97 -0
  114. package/ts_build/src/processors/GemmaToolProcessor.js.map +1 -0
  115. package/ts_build/src/processors/index.d.ts +1 -0
  116. package/ts_build/src/processors/index.js +3 -1
  117. package/ts_build/src/processors/index.js.map +1 -1
  118. package/ts_build/src/services/AgentSyncFs.js +17 -0
  119. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  120. package/ts_build/src/services/SessionManager.js +12 -0
  121. package/ts_build/src/services/SessionManager.js.map +1 -1
  122. package/ts_build/src/services/Tools.d.ts +2 -0
  123. package/ts_build/src/services/Tools.js.map +1 -1
  124. package/ts_build/src/services/modules/computerUse.d.ts +163 -0
  125. package/ts_build/src/services/modules/computerUse.js +3 -0
  126. package/ts_build/src/services/modules/computerUse.js.map +1 -0
  127. package/ts_build/src/services/modules/index.d.ts +1 -0
  128. package/ts_build/src/services/modules/index.js +23 -6
  129. package/ts_build/src/services/modules/index.js.map +1 -1
  130. package/ts_build/src/services/modules/types.d.ts +4 -0
  131. package/ts_build/src/types.d.ts +3 -0
  132. package/ts_build/src/types.js.map +1 -1
  133. package/ts_build/src/workers/tools/index.d.ts +1 -1
  134. package/ts_build/tests/clients/contextLimits.test.js +2 -0
  135. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  136. package/ts_build/tests/clients/openai.images.test.d.ts +1 -0
  137. package/ts_build/tests/clients/openai.images.test.js +94 -0
  138. package/ts_build/tests/clients/openai.images.test.js.map +1 -0
  139. package/ts_build/tests/clients/pricing.test.js +4 -0
  140. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  141. package/ts_build/tests/compressor/usageBase64.test.d.ts +1 -0
  142. package/ts_build/tests/compressor/usageBase64.test.js +109 -0
  143. package/ts_build/tests/compressor/usageBase64.test.js.map +1 -0
  144. package/ts_build/tests/patching/patchFileOutput.test.js +82 -0
  145. package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -1
  146. package/ts_build/tests/patching/transcriptMangleRegression.test.d.ts +1 -0
  147. package/ts_build/tests/patching/transcriptMangleRegression.test.js +104 -0
  148. package/ts_build/tests/patching/transcriptMangleRegression.test.js.map +1 -0
  149. package/ts_build/tests/processors/Base64ImageDetector.test.js +24 -0
  150. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  151. package/x-home.png +0 -0
@@ -27,6 +27,7 @@ import {
27
27
  CustomVariables,
28
28
  XmlToolCallProcessor,
29
29
  HarmonyToolProcessor,
30
+ GemmaToolProcessor,
30
31
  Base64ImageProcessor,
31
32
  MinimalToolsMessageProcessor,
32
33
  } from "../../processors/index";
@@ -339,7 +340,34 @@ export class AgentModule extends BaseChatModule {
339
340
  }
340
341
  );
341
342
 
342
- await this.handleAgentCommand(["Patcher"]);
343
+ // Determine the default agent from context (set by chat.ts from config.chat.defaultAgent)
344
+ // falling back to "Patcher" if not configured.
345
+ const context = this.chatService?.getContext();
346
+ const defaultAgentName = context?.defaultAgent ?? "Patcher";
347
+ await this.handleAgentCommand([defaultAgentName]);
348
+
349
+ // Apply defaultModel and defaultProvider if configured
350
+ const defaultModel = context?.defaultModel;
351
+ const defaultProvider = context?.defaultProvider;
352
+ if (defaultModel || defaultProvider) {
353
+ const selectedAgent = context?.selectedAgent;
354
+ if (selectedAgent) {
355
+ if (defaultModel) {
356
+ selectedAgent.setModel(defaultModel);
357
+ selectedAgent.setModelPreferences([
358
+ { model: defaultModel, provider: defaultProvider as any },
359
+ ]);
360
+ }
361
+ if (defaultProvider) {
362
+ selectedAgent.setProvider(defaultProvider as any);
363
+ }
364
+ // Refresh context model/provider display
365
+ if (context) {
366
+ if (defaultModel) context.currentModel = defaultModel;
367
+ if (defaultProvider) context.currentProvider = defaultProvider;
368
+ }
369
+ }
370
+ }
343
371
  }
344
372
 
345
373
  async handleAgentCommand(args: string[]): Promise<void> {
@@ -682,6 +710,11 @@ export class AgentModule extends BaseChatModule {
682
710
  messageId: session.knowhowMessageId,
683
711
  chatHistory: [],
684
712
  run: false, // Don't run yet, we need to set up event listeners first
713
+ // Restore the exact model/provider/reasoning the original run used.
714
+ model: session.model,
715
+ provider: session.provider,
716
+ reasoningEffort: session.reasoningEffort,
717
+ summarizeReasoning: session.summarizeReasoning,
685
718
  });
686
719
 
687
720
  // After resume finishes, revert to normal chat (non-agent mode) so the
@@ -758,6 +791,10 @@ export class AgentModule extends BaseChatModule {
758
791
  run?: boolean; // whether to run immediately
759
792
  taskId?: string; // optional pre-generated taskId
760
793
  parentTaskId?: string; // taskId of the parent agent that spawned this task
794
+ // Reasoning settings to restore (e.g. on resume). When provided these take
795
+ // precedence over the module-level CLI defaults.
796
+ reasoningEffort?: CompletionOptions["reasoning_effort"];
797
+ summarizeReasoning?: boolean;
761
798
  }) {
762
799
  const { input, chatHistory = [], agentName } = options;
763
800
  const agentContext = services().Agents.getAgentContext();
@@ -841,8 +878,15 @@ export class AgentModule extends BaseChatModule {
841
878
 
842
879
  // Set up session update listener
843
880
  const threadUpdateHandler = async (threadState: any) => {
844
- this.updateSession(taskId, agent.getThreads());
845
881
  taskInfo.totalCost = agent.getTotalCostUsd();
882
+ // Capture the live run configuration so it's persisted for an exact
883
+ // resume (including mid-run model/provider fallbacks).
884
+ taskInfo.model = agent.getModel();
885
+ taskInfo.provider = agent.getProvider();
886
+ taskInfo.reasoningEffort = agent.getReasoningEffort?.();
887
+ taskInfo.summarizeReasoning = agent.getSummarizeReasoning?.();
888
+ // updateSession pulls the (mutated) taskInfo from the registry.
889
+ this.updateSession(taskId, agent.getThreads());
846
890
  };
847
891
  agent.agentEvents.on(agent.eventTypes.threadUpdate, threadUpdateHandler);
848
892
 
@@ -854,6 +898,13 @@ export class AgentModule extends BaseChatModule {
854
898
  // Initialize new task
855
899
  await agent.newTask(taskId);
856
900
 
901
+ // Record the parent task id on the agent so self-referential tools
902
+ // (e.g. replyToParent) can resolve who spawned this agent without
903
+ // relying solely on reading metadata.json off disk.
904
+ if (options.parentTaskId) {
905
+ agent.setParentTaskId(options.parentTaskId);
906
+ }
907
+
857
908
  if (options.model) {
858
909
  console.log("Setting model:", options.model);
859
910
  agent.setModel(options.model);
@@ -862,11 +913,28 @@ export class AgentModule extends BaseChatModule {
862
913
  ]);
863
914
  }
864
915
 
865
- // Apply module-level reasoning settings to the agent
866
- if (this.reasoningEffort !== undefined) {
867
- agent.setReasoningEffort(this.reasoningEffort);
916
+ // Restore the provider explicitly (e.g. on resume) so the agent talks to
917
+ // the same client the original run used, not the model's default provider.
918
+ if (options.provider) {
919
+ agent.setProvider(options.provider as any);
868
920
  }
869
- agent.setSummarizeReasoning(this.summarizeReasoning);
921
+
922
+ // Apply reasoning settings to the agent. Explicit per-task settings
923
+ // (e.g. restored from a resumed task's metadata) take precedence over
924
+ // the module-level CLI defaults so a /resume keeps the same reasoning
925
+ // effort the original run used.
926
+ const effectiveReasoningEffort =
927
+ options.reasoningEffort !== undefined
928
+ ? options.reasoningEffort
929
+ : this.reasoningEffort;
930
+ if (effectiveReasoningEffort !== undefined) {
931
+ agent.setReasoningEffort(effectiveReasoningEffort);
932
+ }
933
+ agent.setSummarizeReasoning(
934
+ options.summarizeReasoning !== undefined
935
+ ? options.summarizeReasoning
936
+ : this.summarizeReasoning
937
+ );
870
938
 
871
939
  // Set up message processors like in original startAgent
872
940
 
@@ -908,6 +976,7 @@ export class AgentModule extends BaseChatModule {
908
976
  agent.messageProcessor.setProcessors("post_call", [
909
977
  new XmlToolCallProcessor().createProcessor(),
910
978
  new HarmonyToolProcessor().createProcessor(),
979
+ new GemmaToolProcessor().createProcessor(),
911
980
  ]);
912
981
 
913
982
  agent.messageProcessor.setProcessors("pre_tools", [
@@ -1118,6 +1187,50 @@ export class AgentModule extends BaseChatModule {
1118
1187
  return threads;
1119
1188
  }
1120
1189
 
1190
+ /**
1191
+ * Load the persisted run settings (model / provider / reasoning) for a task
1192
+ * from local FS metadata, so a headless --resume can restore the exact
1193
+ * configuration the original run used instead of falling back to agent
1194
+ * defaults. Returns an empty object when no local metadata is found (e.g.
1195
+ * remote-only tasks).
1196
+ */
1197
+ public async loadTaskSettings(
1198
+ taskId: string,
1199
+ messageId?: string
1200
+ ): Promise<{
1201
+ model?: string;
1202
+ provider?: string;
1203
+ reasoningEffort?: CompletionOptions["reasoning_effort"];
1204
+ summarizeReasoning?: boolean;
1205
+ agentName?: string;
1206
+ }> {
1207
+ const localMetadataPath = path.join(
1208
+ ".knowhow",
1209
+ "processes",
1210
+ "agents",
1211
+ taskId,
1212
+ "metadata.json"
1213
+ );
1214
+
1215
+ if (!messageId && fs.existsSync(localMetadataPath)) {
1216
+ try {
1217
+ const raw = await fsPromises.readFile(localMetadataPath, "utf-8");
1218
+ const metadata = JSON.parse(raw);
1219
+ return {
1220
+ model: metadata.model,
1221
+ provider: metadata.provider,
1222
+ reasoningEffort: metadata.reasoningEffort,
1223
+ summarizeReasoning: metadata.summarizeReasoning,
1224
+ agentName: metadata.agentName,
1225
+ };
1226
+ } catch (e) {
1227
+ console.warn(`⚠️ Failed to parse local metadata for settings: ${e.message}`);
1228
+ }
1229
+ }
1230
+
1231
+ return {};
1232
+ }
1233
+
1121
1234
  /**
1122
1235
  * Resume an agent from a set of existing message threads
1123
1236
  * Used by the CLI --resume flag to continue crashed/failed tasks
@@ -1129,6 +1242,11 @@ export class AgentModule extends BaseChatModule {
1129
1242
  messageId?: string;
1130
1243
  taskId?: string;
1131
1244
  interactive?: boolean;
1245
+ // Restore the exact model/provider/reasoning the original run used.
1246
+ model?: string;
1247
+ provider?: string;
1248
+ reasoningEffort?: CompletionOptions["reasoning_effort"];
1249
+ summarizeReasoning?: boolean;
1132
1250
  }): Promise<{ taskCompleted: Promise<string> }> {
1133
1251
  const { agentName, input, threads, messageId, taskId } = options;
1134
1252
 
@@ -1189,6 +1307,11 @@ export class AgentModule extends BaseChatModule {
1189
1307
  // remote task identified by taskId.
1190
1308
  syncRemote: !messageId && !!taskId,
1191
1309
  run: false,
1310
+ // Restore the model/provider/reasoning captured from the task metadata.
1311
+ model: options.model,
1312
+ provider: options.provider,
1313
+ reasoningEffort: options.reasoningEffort,
1314
+ summarizeReasoning: options.summarizeReasoning,
1192
1315
  });
1193
1316
 
1194
1317
  // Interactive (chat REPL) resume: route through the attached chat loop so the
@@ -396,6 +396,12 @@ export class SessionsModule extends BaseChatModule {
396
396
  threads: normalizedThreads,
397
397
  input: additionalContext?.trim() || initialInput || "",
398
398
  interactive: true,
399
+ // Restore the exact model/provider/reasoning the run used so
400
+ // resuming doesn't silently drop back to agent defaults.
401
+ model: metadata.model,
402
+ provider: metadata.provider,
403
+ reasoningEffort: metadata.reasoningEffort,
404
+ summarizeReasoning: metadata.summarizeReasoning,
399
405
  });
400
406
  return;
401
407
  } catch (e: any) {
package/src/chat/types.ts CHANGED
@@ -10,6 +10,9 @@ export interface ChatContext {
10
10
  debugMode?: boolean;
11
11
  agentMode?: boolean;
12
12
  currentAgent?: string;
13
+ defaultAgent?: string;
14
+ defaultModel?: string;
15
+ defaultProvider?: string;
13
16
  promptText?: string;
14
17
  searchMode?: boolean;
15
18
  voiceMode?: boolean;
@@ -94,6 +97,12 @@ export interface TaskInfo {
94
97
  endTime?: number;
95
98
  totalCost: number;
96
99
  sessionFile?: string;
100
+ // Model/provider/reasoning actually used for this run, so a resume can
101
+ // restore the exact configuration instead of falling back to agent defaults.
102
+ model?: string;
103
+ provider?: string;
104
+ reasoningEffort?: "none" | "low" | "medium" | "high";
105
+ summarizeReasoning?: boolean;
97
106
  }
98
107
 
99
108
  export interface ChatSession {
@@ -111,6 +120,11 @@ export interface ChatSession {
111
120
  threads: any[][];
112
121
  currentThread: number;
113
122
  lastUpdated: number;
123
+ // Persisted run configuration for exact resume restoration.
124
+ model?: string;
125
+ provider?: string;
126
+ reasoningEffort?: "none" | "low" | "medium" | "high";
127
+ summarizeReasoning?: boolean;
114
128
  }
115
129
 
116
130
  export interface ChatHistory {
package/src/chat.ts CHANGED
@@ -54,6 +54,17 @@ async function main(initialCommand?: string) {
54
54
  // Create chat service with plugins
55
55
  const chatService = new CliChatService(config.plugins?.enabled ?? []);
56
56
 
57
+ // ── Default Agent / Model / Provider ──────────────────────────────────────
58
+ // If chat config specifies defaults, inject them into the context so
59
+ // AgentModule can pick them up during initialize().
60
+ if (config.chat?.defaultAgent || config.chat?.defaultModel || config.chat?.defaultProvider) {
61
+ chatService.setContext({
62
+ defaultAgent: config.chat?.defaultAgent,
63
+ defaultModel: config.chat?.defaultModel,
64
+ defaultProvider: config.chat?.defaultProvider,
65
+ });
66
+ }
67
+
57
68
  // ── Renderer ──────────────────────────────────────────────────────────────
58
69
  // Load a custom renderer if configured, otherwise use the default
59
70
  // ConsoleRenderer (already the default inside AgentModule).
package/src/cli.ts CHANGED
@@ -157,6 +157,8 @@ async function main() {
157
157
  await program.parseAsync(process.argv);
158
158
  }
159
159
 
160
+ export { main };
161
+
160
162
  if (require.main === module) {
161
163
  main()
162
164
  .catch((e) => {
@@ -245,6 +245,18 @@ export class GenericAnthropicClient implements GenericClient {
245
245
 
246
246
  transformMessages(messages: Message[], longTtl = false): MessageParam[] {
247
247
  const toolCalls = messages.flatMap((msg) => msg.tool_calls || []);
248
+ // Map each tool_call id → the thinking blocks captured on the assistant
249
+ // message that produced it, so we can re-inject them before the tool_use.
250
+ const thinkingByCallId: { [id: string]: any[] } = {};
251
+ for (const m of messages) {
252
+ if (m.role !== "assistant" || !m.tool_calls?.length) continue;
253
+ const details = (m as any)._reasoning_details;
254
+ if (details?.provider === "anthropic" && Array.isArray(details.items)) {
255
+ // Attach to the FIRST tool_call of this assistant turn (thinking blocks
256
+ // must precede the first tool_use in the turn).
257
+ thinkingByCallId[m.tool_calls[0].id] = details.items;
258
+ }
259
+ }
248
260
  const claudeMessages: MessageParam[] = messages
249
261
  .filter((msg) => msg.role !== "system")
250
262
  .filter((msg) => msg.content || msg.role === "tool")
@@ -258,9 +270,14 @@ export class GenericAnthropicClient implements GenericClient {
258
270
  JSON.stringify(msg, null, 2)
259
271
  );
260
272
  } else {
273
+ // Re-inject any captured thinking / redacted_thinking blocks
274
+ // (verbatim, with signature/data) BEFORE the tool_use block, as
275
+ // Anthropic requires for reasoning continuity across tool turns.
276
+ const thinkingBlocks = thinkingByCallId[msg.tool_call_id] ?? [];
261
277
  toolMessages.push({
262
278
  role: "assistant",
263
279
  content: [
280
+ ...thinkingBlocks,
264
281
  {
265
282
  type: "tool_use",
266
283
  id: msg.tool_call_id,
@@ -440,8 +457,42 @@ export class GenericAnthropicClient implements GenericClient {
440
457
  console.log("no content in Anthropic response", response);
441
458
  }
442
459
 
460
+ // Capture thinking / redacted_thinking blocks verbatim (with their
461
+ // signature / data) so they can be round-tripped unmodified on the next
462
+ // tool-use turn. Anthropic REQUIRES thinking blocks from the assistant
463
+ // message to be passed back, complete and unmodified, when returning tool
464
+ // results — otherwise reasoning continuity and prompt-cache hits are lost.
465
+ const thinkingBlocks = response.content.filter(
466
+ (c) => c.type === "thinking" || c.type === "redacted_thinking"
467
+ );
468
+ const reasoningDetails =
469
+ thinkingBlocks.length > 0
470
+ ? { provider: "anthropic", items: thinkingBlocks }
471
+ : undefined;
472
+ // Extract a human-readable summary from thinking blocks (when present).
473
+ const reasoningSummary = thinkingBlocks
474
+ .map((b) => ("thinking" in b ? (b as any).thinking : ""))
475
+ .filter(Boolean)
476
+ .join("\n") || undefined;
477
+
478
+ // Determine which resulting choice should carry the thinking blocks. They
479
+ // are re-injected before the first tool_use on the next turn, so prefer
480
+ // the first tool_use block; if there are no tool calls, fall back to the
481
+ // first (text) block. This keeps a single copy that transformMessages can
482
+ // reliably find via the assistant message's tool_calls.
483
+ const nonThinking = response.content.filter(
484
+ (c) => c.type !== "thinking" && c.type !== "redacted_thinking"
485
+ );
486
+ const firstToolUseIdx = nonThinking.findIndex((c) => c.type === "tool_use");
487
+ const carryIdx = firstToolUseIdx >= 0 ? firstToolUseIdx : 0;
488
+
443
489
  return {
444
- choices: response.content.map((c) => {
490
+ choices: nonThinking.map((c, idx) => {
491
+ // Only the FIRST resulting assistant message carries the thinking
492
+ // blocks (Anthropic requires a single thinking block sequence at the
493
+ // start of the assistant turn); attaching to every choice would
494
+ // duplicate them once the messages are re-transformed.
495
+ const carryReasoning = idx === carryIdx;
445
496
  if (c.type === "tool_use") {
446
497
  return {
447
498
  message: {
@@ -457,6 +508,8 @@ export class GenericAnthropicClient implements GenericClient {
457
508
  },
458
509
  },
459
510
  ],
511
+ ...(carryReasoning && reasoningDetails && { _reasoning_details: reasoningDetails }),
512
+ ...(carryReasoning && reasoningSummary && { reasoning_summary: reasoningSummary }),
460
513
  },
461
514
  };
462
515
  } else {
@@ -465,6 +518,8 @@ export class GenericAnthropicClient implements GenericClient {
465
518
  role: "assistant",
466
519
  content: "text" in c ? c.text : c.type,
467
520
  tool_calls: [],
521
+ ...(carryReasoning && reasoningDetails && { _reasoning_details: reasoningDetails }),
522
+ ...(carryReasoning && reasoningSummary && { reasoning_summary: reasoningSummary }),
468
523
  },
469
524
  };
470
525
  }
@@ -1,4 +1,5 @@
1
1
  import { Models, EmbeddingModels } from "../types";
2
+ import { FireworksModels } from "./pricing/fireworks";
2
3
 
3
4
  /**
4
5
  * Context window limits (in tokens) for all supported models.
@@ -207,6 +208,45 @@ export const ContextLimits: Record<string, number> = {
207
208
  [Models.xai.GrokImagineImage]: 0,
208
209
  [Models.xai.GrokImagineVideo]: 0,
209
210
  [Models.xai.Grok2Image1212]: 0,
211
+
212
+ // ─── Fireworks AI ──────────────────────────────────────────────────────────
213
+ // Moonshot AI (Kimi) — context windows per fireworks.ai/pricing
214
+ [FireworksModels.KimiK3]: 131_072,
215
+ [FireworksModels.KimiK2p7Code]: 131_072,
216
+ [FireworksModels.KimiK2_6]: 131_072,
217
+ [FireworksModels.KimiK2_5]: 131_072,
218
+
219
+ // MiniMax
220
+ [FireworksModels.MinimaxM3]: 1_000_000,
221
+ [FireworksModels.MinimaxM2_7]: 1_000_000,
222
+ [FireworksModels.MinimaxM2_5]: 1_000_000,
223
+
224
+ // Qwen
225
+ [FireworksModels.Qwen3p7Plus]: 131_072,
226
+ [FireworksModels.Qwen3_6Plus]: 131_072,
227
+ [FireworksModels.Qwen3p6Plus]: 131_072,
228
+ [FireworksModels.Qwen3Vl30bA3bThinking]: 32_768,
229
+ [FireworksModels.Qwen3Vl30bA3bInstruct]: 32_768,
230
+ [FireworksModels.Qwen3_8b]: 131_072,
231
+
232
+ // Z.ai (GLM)
233
+ [FireworksModels.Glm5p2]: 131_072,
234
+ [FireworksModels.Glm5p1]: 131_072,
235
+ [FireworksModels.Glm5]: 131_072,
236
+ [FireworksModels.Glm4_7]: 131_072,
237
+
238
+ // DeepSeek AI
239
+ [FireworksModels.DeepseekV4Pro]: 163_840,
240
+ [FireworksModels.DeepseekV4Flash]: 163_840,
241
+ [FireworksModels.DeepseekV3_2]: 163_840,
242
+ [FireworksModels.DeepseekV3_1]: 163_840,
243
+
244
+ // OpenAI OSS
245
+ [FireworksModels.GptOss120b]: 131_072,
246
+
247
+ // Fireworks embedding/reranker models — 40k context
248
+ [FireworksModels.Qwen3Embedding8b]: 40_960,
249
+ [FireworksModels.Qwen3Reranker8b]: 40_960,
210
250
  };
211
251
 
212
252
  /** Default fallback context window limit (tokens) used when a model is not found. */
@@ -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) {
@@ -499,24 +526,51 @@ export class GenericGeminiClient implements GenericClient {
499
526
  return { message };
500
527
  }
501
528
 
529
+ // Capture Gemini thoughtSignatures so they can be round-tripped on the
530
+ // next turn. Gemini 3 REQUIRES these to be echoed back on the matching
531
+ // functionCall part or it returns a 400 "Thought signature is not
532
+ // valid" on the 2nd+ tool-calling turn. We key them by tool-call id
533
+ // (and also keep any standalone thought-part signatures in order).
534
+ const reasoningItems: any[] = [];
535
+
502
536
  candidate?.content?.parts?.forEach((part) => {
503
537
  if ("text" in part && typeof part.text === "string") {
504
538
  textContent += part.text; // Concatenate text parts
539
+ if ((part as any).thoughtSignature) {
540
+ reasoningItems.push({
541
+ kind: "thoughtSignature",
542
+ thoughtSignature: (part as any).thoughtSignature,
543
+ });
544
+ }
505
545
  } else if ("functionCall" in part && part.functionCall) {
546
+ const callId =
547
+ part.functionCall.id ||
548
+ `fc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
506
549
  message.tool_calls.push({
507
- id:
508
- part.functionCall.id ||
509
- `fc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
550
+ id: callId,
510
551
  type: "function",
511
552
  function: {
512
553
  name: part.functionCall.name,
513
554
  arguments: JSON.stringify(part.functionCall.args || {}),
514
555
  },
515
556
  });
557
+ if ((part as any).thoughtSignature) {
558
+ reasoningItems.push({
559
+ kind: "functionCall",
560
+ callId,
561
+ thoughtSignature: (part as any).thoughtSignature,
562
+ });
563
+ }
516
564
  }
517
565
  });
518
566
 
519
567
  message.content = textContent || null;
568
+ if (reasoningItems.length > 0) {
569
+ (message as any)._reasoning_details = {
570
+ provider: "gemini",
571
+ items: reasoningItems,
572
+ };
573
+ }
520
574
 
521
575
  return { message };
522
576
  }) || []; // Handle case with no candidates