@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
@@ -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,
@@ -140,6 +144,75 @@ export class GenericOpenAiClient implements GenericClient {
140
144
  return options.reasoning_effort === "none";
141
145
  }
142
146
 
147
+ /**
148
+ * OpenAI only permits image parts on user messages. A Chat Completions
149
+ * `tool` message may contain text parts, but not `image_url` parts. Keep the
150
+ * required tool result and attach images in a following user message.
151
+ */
152
+ toChatCompletionMessages(
153
+ messages: CompletionOptions["messages"]
154
+ ): ChatCompletionMessageParam[] {
155
+ return messages.flatMap((msg): ChatCompletionMessageParam[] => {
156
+ if (msg.role !== "tool") return [msg as ChatCompletionMessageParam];
157
+
158
+ const parts = Array.isArray(msg.content) ? msg.content : [];
159
+ const unsupportedPart = parts.find(
160
+ (part) => part.type !== "text" && part.type !== "image_url"
161
+ );
162
+ if (unsupportedPart) {
163
+ throw new Error(
164
+ `OpenAI Chat Completions cannot attach ${unsupportedPart.type} content to a tool result.`
165
+ );
166
+ }
167
+ const imageParts = parts.filter((part) => part.type === "image_url");
168
+ const textParts = parts.filter((part) => part.type === "text");
169
+ const toolMessage: ChatCompletionToolMessageParam = {
170
+ role: "tool",
171
+ tool_call_id: msg.tool_call_id!,
172
+ content: Array.isArray(msg.content)
173
+ ? textParts.length
174
+ ? textParts
175
+ : [{
176
+ type: "text",
177
+ text: "Image output attached in the following user message.",
178
+ }]
179
+ : msg.content || "",
180
+ };
181
+
182
+ if (!imageParts.length) return [toolMessage];
183
+ return [
184
+ toolMessage,
185
+ { role: "user", content: imageParts } as ChatCompletionMessageParam,
186
+ ];
187
+ });
188
+ }
189
+
190
+ /** Convert the CLI's Chat Completions-style parts to Responses API parts. */
191
+ toResponseContent(
192
+ content: Message["content"]
193
+ ): string | ResponseInputMessageContentList {
194
+ if (!Array.isArray(content)) return content || "";
195
+ const result: ResponseInputMessageContentList = [];
196
+ for (const part of content) {
197
+ if (part.type === "text") {
198
+ result.push({ type: "input_text", text: part.text });
199
+ }
200
+ else if (part.type === "image_url") {
201
+ result.push({
202
+ type: "input_image",
203
+ image_url: part.image_url.url,
204
+ detail: part.image_url.detail || "auto",
205
+ });
206
+ }
207
+ else {
208
+ throw new Error(
209
+ `OpenAI Responses API conversion does not support ${part.type} content.`
210
+ );
211
+ }
212
+ }
213
+ return result;
214
+ }
215
+
143
216
  async createChatCompletion(
144
217
  options: CompletionOptions
145
218
  ): Promise<CompletionResponse> {
@@ -148,17 +221,7 @@ export class GenericOpenAiClient implements GenericClient {
148
221
  return this.createChatResponse(options);
149
222
  }
150
223
 
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
- });
224
+ const openaiMessages = this.toChatCompletionMessages(options.messages);
162
225
 
163
226
  const response = await this.client.chat.completions.create({
164
227
  model: options.model,
@@ -222,17 +285,7 @@ export class GenericOpenAiClient implements GenericClient {
222
285
  return;
223
286
  }
224
287
 
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
- });
288
+ const openaiMessages = this.toChatCompletionMessages(options.messages);
236
289
 
237
290
  const stream = await this.client.chat.completions.create({
238
291
  model: options.model,
@@ -307,6 +360,20 @@ export class GenericOpenAiClient implements GenericClient {
307
360
  }
308
361
  }
309
362
 
363
+ /**
364
+ * Resolve the OpenAI-native reasoning items to re-inject for an assistant
365
+ * message. Reads the provider-agnostic `_reasoning_details` slot (only when
366
+ * it was produced by this provider) and falls back to the deprecated
367
+ * `_reasoning_items` for backward compatibility.
368
+ */
369
+ private getReasoningItems(msg: any): any[] {
370
+ const details = (msg as any)._reasoning_details;
371
+ if (details && details.provider === "openai" && Array.isArray(details.items)) {
372
+ return details.items;
373
+ }
374
+ return (msg as any)._reasoning_items ?? [];
375
+ }
376
+
310
377
  async createChatResponse(
311
378
  options: CompletionOptions
312
379
  ): Promise<CompletionResponse> {
@@ -324,30 +391,68 @@ export class GenericOpenAiClient implements GenericClient {
324
391
 
325
392
  // Convert chat messages to Responses API input items
326
393
  // The Responses API accepts: user/assistant/system messages and function_call_output items
394
+ // OpenAI Responses API hard limit: 10,485,760 chars per function_call_output.output
395
+ const MAX_TOOL_OUTPUT = 10_485_000;
396
+ const truncateToolOutput = (s: string) =>
397
+ s.length > MAX_TOOL_OUTPUT
398
+ ? s.slice(0, MAX_TOOL_OUTPUT) + "\n\n[TRUNCATED: output exceeded 10MB API limit]"
399
+ : s;
327
400
  const input: any[] = nonSystemMessages.map((msg) => {
328
401
  if (msg.role === "tool") {
329
- // tool result → function_call_output
330
- return {
402
+ // OpenAI function outputs cannot directly contain image content. Send
403
+ // the required function output, then attach images as a user message.
404
+ const parts = Array.isArray(msg.content) ? msg.content : [];
405
+ const imageParts = parts.filter((part) => part.type === "image_url");
406
+ const text = parts
407
+ .filter((part) => part.type === "text")
408
+ .map((part: any) => part.text)
409
+ .join("\n");
410
+ const toolOutput = {
331
411
  type: "function_call_output",
332
412
  call_id: msg.tool_call_id,
333
- output: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
413
+ output: truncateToolOutput(typeof msg.content === "string"
414
+ ? msg.content
415
+ : text || "Image output attached in the following user message."),
334
416
  };
417
+ if (!imageParts.length) return toolOutput;
418
+ return [
419
+ toolOutput,
420
+ { role: "user", content: this.toResponseContent(imageParts) },
421
+ ];
335
422
  }
336
423
  if (msg.role === "assistant" && msg.tool_calls?.length) {
337
- // assistant message with tool calls → function_call items
338
- return msg.tool_calls.map((tc) => ({
424
+ // assistant message with tool calls → function_call items.
425
+ // If the prior turn captured reasoning items (encrypted_content path),
426
+ // re-inject them *before* the function_call items so the model keeps its
427
+ // chain-of-thought instead of re-deriving it from scratch.
428
+ const reasoningItems = this.getReasoningItems(msg);
429
+ return [
430
+ ...reasoningItems,
431
+ ...msg.tool_calls.map((tc) => ({
339
432
  type: "function_call",
340
433
  // id must start with 'fc_'; call_id is the original call_ ID used for function_call_output matching
341
434
  id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
342
435
  call_id: tc.id,
343
436
  name: tc.function.name,
344
437
  arguments: tc.function.arguments,
345
- }));
438
+ })),
439
+ ];
440
+ }
441
+ if (msg.role === "assistant" && this.getReasoningItems(msg).length) {
442
+ // assistant message (no tool calls) that carried reasoning items →
443
+ // re-inject reasoning items, then the visible message content.
444
+ return [
445
+ ...this.getReasoningItems(msg),
446
+ {
447
+ role: msg.role,
448
+ content: this.toResponseContent(msg.content),
449
+ },
450
+ ];
346
451
  }
347
452
  // Regular user/assistant message
348
453
  return {
349
454
  role: msg.role,
350
- content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
455
+ content: this.toResponseContent(msg.content),
351
456
  };
352
457
  }).flat();
353
458
 
@@ -360,10 +465,29 @@ export class GenericOpenAiClient implements GenericClient {
360
465
  strict: false,
361
466
  }));
362
467
 
468
+ // Reasoning persistence:
469
+ // - store === true → OpenAI persists the response server-side; the next
470
+ // turn passes previous_response_id to continue the reasoning chain.
471
+ // - store !== true → stateless; ask OpenAI to return encrypted reasoning
472
+ // content so we can thread it back into the next request's input ourselves.
473
+ const store = options.store === true;
474
+ const wantsReasoning =
475
+ OpenAiReasoningModels.includes(options.model) &&
476
+ !this.isReasoningDisabled(options);
477
+
363
478
  const response = await this.client.responses.create({
364
479
  model: options.model as any,
365
480
  input,
366
481
  ...(instructions && { instructions }),
482
+ ...(store && { store: true }),
483
+ ...(!store && { store: false }),
484
+ ...(options.previous_response_id && {
485
+ previous_response_id: options.previous_response_id,
486
+ }),
487
+ // When stateless, request encrypted reasoning so we can re-inject it next turn.
488
+ ...(!store && wantsReasoning && {
489
+ include: ["reasoning.encrypted_content"],
490
+ }),
367
491
  // Don't limit max_output_tokens for Responses API - codex truncates tool call arguments when limited
368
492
  ...(OpenAiReasoningModels.includes(options.model) && !this.isReasoningDisabled(options) && {
369
493
  max_output_tokens: Math.max(options.max_tokens || 0, 16000),
@@ -376,7 +500,6 @@ export class GenericOpenAiClient implements GenericClient {
376
500
  tools,
377
501
  tool_choice: "auto",
378
502
  }),
379
- store: false,
380
503
  } as any);
381
504
 
382
505
  // Map Responses API usage to Chat Completions usage format
@@ -405,6 +528,10 @@ export class GenericOpenAiClient implements GenericClient {
405
528
  // Collect text content and tool calls from the output items
406
529
  let textContent: string | null = null;
407
530
  const toolCalls: ChatCompletionMessageToolCall[] = [];
531
+ // Reasoning items (with encrypted_content) to thread back into the next turn,
532
+ // and a human-readable summary of the model's thinking (when requested).
533
+ const reasoningItems: any[] = [];
534
+ let reasoningSummary: string | null = null;
408
535
 
409
536
  for (const item of response.output) {
410
537
  if (item.type === "message") {
@@ -415,6 +542,18 @@ export class GenericOpenAiClient implements GenericClient {
415
542
  textContent = (textContent ?? "") + part.text;
416
543
  }
417
544
  }
545
+ } else if (item.type === "reasoning") {
546
+ // ResponseReasoningItem — keep the raw item so we can re-inject it
547
+ // (it carries encrypted_content when include was requested), and pull
548
+ // out any human-readable summary text.
549
+ reasoningItems.push(item);
550
+ const summaryParts = (item as any).summary ?? [];
551
+ for (const part of summaryParts) {
552
+ const text = typeof part === "string" ? part : part?.text;
553
+ if (text) {
554
+ reasoningSummary = (reasoningSummary ?? "") + text;
555
+ }
556
+ }
418
557
  } else if (item.type === "function_call") {
419
558
  // ResponseFunctionToolCall
420
559
  const fc = item as ResponseFunctionToolCall;
@@ -444,12 +583,21 @@ export class GenericOpenAiClient implements GenericClient {
444
583
  role: "assistant",
445
584
  content: textContent,
446
585
  ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
586
+ ...(reasoningSummary && { reasoning_summary: reasoningSummary }),
587
+ // Stash the raw reasoning items (encrypted_content) so the next call
588
+ // to createChatResponse can re-inject them into the input. We populate
589
+ // both the provider-agnostic slot and the deprecated legacy field.
590
+ ...(reasoningItems.length > 0 && {
591
+ _reasoning_items: reasoningItems,
592
+ _reasoning_details: { provider: "openai", items: reasoningItems },
593
+ }),
447
594
  },
448
595
  },
449
596
  ],
450
597
  model: options.model,
451
598
  usage,
452
599
  usd_cost: usdCost,
600
+ response_id: (response as any).id,
453
601
  };
454
602
  }
455
603
 
@@ -1,4 +1,4 @@
1
- import { ModelPricing } from "./types";
1
+ import { ModelPricing, ModelCatalogEntry, completions, embeddings } from "./types";
2
2
 
3
3
  /**
4
4
  * Fireworks AI model IDs, pricing, and catalog.
@@ -101,3 +101,52 @@ export const FireworksEmbeddingPricing: Record<string, ModelPricing> = {
101
101
  // Qwen3 Reranker 8B — $0.20/M tokens, context 40k
102
102
  [FireworksModels.Qwen3Reranker8b]: { input: 0.20, output: 0.20 },
103
103
  };
104
+
105
+
106
+ // ─── Active text models (non-deprecated) ─────────────────────────────────────
107
+
108
+ export const FireworksActiveTextModels: string[] = [
109
+ // Moonshot AI
110
+ FireworksModels.KimiK3,
111
+ FireworksModels.KimiK2p7Code,
112
+ FireworksModels.KimiK2_6,
113
+ FireworksModels.KimiK2_5,
114
+ // MiniMax
115
+ FireworksModels.MinimaxM3,
116
+ FireworksModels.MinimaxM2_7,
117
+ FireworksModels.MinimaxM2_5,
118
+ // Qwen
119
+ FireworksModels.Qwen3p7Plus,
120
+ // Z.ai
121
+ FireworksModels.Glm5p2,
122
+ FireworksModels.Glm5p1,
123
+ // DeepSeek AI
124
+ FireworksModels.DeepseekV4Pro,
125
+ FireworksModels.DeepseekV4Flash,
126
+ // OpenAI OSS
127
+ FireworksModels.GptOss120b,
128
+ ];
129
+
130
+ export const FireworksDeprecatedTextModels: string[] = [
131
+ FireworksModels.Qwen3_6Plus,
132
+ FireworksModels.Qwen3p6Plus,
133
+ FireworksModels.Qwen3Vl30bA3bThinking,
134
+ FireworksModels.Qwen3Vl30bA3bInstruct,
135
+ FireworksModels.Qwen3_8b,
136
+ FireworksModels.Glm5,
137
+ FireworksModels.Glm4_7,
138
+ FireworksModels.DeepseekV3_2,
139
+ FireworksModels.DeepseekV3_1,
140
+ ];
141
+
142
+ // ─── Catalog ──────────────────────────────────────────────────────────────────
143
+
144
+ export const FIREWORKS_MODEL_CATALOG: ModelCatalogEntry[] = [
145
+ ...completions(FireworksActiveTextModels, "fireworks", FireworksTextPricing),
146
+ ...completions(FireworksDeprecatedTextModels, "fireworks", FireworksTextPricing),
147
+ ...embeddings(
148
+ [FireworksModels.Qwen3Embedding8b, FireworksModels.Qwen3Reranker8b],
149
+ "fireworks",
150
+ FireworksEmbeddingPricing,
151
+ ),
152
+ ];
@@ -35,7 +35,7 @@ export { GitHubModelsTextPricing } from "./github";
35
35
  export { OpenRouterTextPricing } from "./openrouter";
36
36
  export { LlamaTextPricing } from "./llama";
37
37
  export { CopilotTextPricing, CopilotModelMultipliers } from "./copilot";
38
- export { FireworksTextPricing } from "./fireworks";
38
+ export { FireworksTextPricing, FireworksEmbeddingPricing, FIREWORKS_MODEL_CATALOG, FireworksActiveTextModels, FireworksDeprecatedTextModels } from "./fireworks";
39
39
  export { MetaTextPricing, MetaWebSearchPricingPer1000Queries } from "./meta";
40
40
 
41
41
  // ─── Combined catalog ─────────────────────────────────────────────────────────
@@ -7,17 +7,19 @@ import { OPENAI_MODEL_CATALOG } from "./openai";
7
7
  import { ANTHROPIC_MODEL_CATALOG } from "./anthropic";
8
8
  import { GOOGLE_MODEL_CATALOG } from "./google";
9
9
  import { XAI_MODEL_CATALOG } from "./xai";
10
+ import { FIREWORKS_MODEL_CATALOG } from "./fireworks";
10
11
 
11
12
  export { ModelPricing, ModelType, ModelCatalogEntry };
12
13
 
13
14
  /** 2.5% platform markup applied on top of all provider base rates */
14
15
  export const USAGE_MARKUP_PERCENT = 2.5 / 100;
15
16
 
16
- export { OPENAI_MODEL_CATALOG, ANTHROPIC_MODEL_CATALOG, GOOGLE_MODEL_CATALOG, XAI_MODEL_CATALOG };
17
+ export { OPENAI_MODEL_CATALOG, ANTHROPIC_MODEL_CATALOG, GOOGLE_MODEL_CATALOG, XAI_MODEL_CATALOG, FIREWORKS_MODEL_CATALOG };
17
18
 
18
19
  export const ALL_MODEL_CATALOG: ModelCatalogEntry[] = [
19
20
  ...OPENAI_MODEL_CATALOG,
20
21
  ...ANTHROPIC_MODEL_CATALOG,
21
22
  ...GOOGLE_MODEL_CATALOG,
22
23
  ...XAI_MODEL_CATALOG,
24
+ ...FIREWORKS_MODEL_CATALOG,
23
25
  ];
@@ -232,8 +232,8 @@ export const OpenAiSearchModels: string[] = [
232
232
  export const OpenAiTextPricing: Record<string, ModelPricing> = {
233
233
  // gpt-5.6 series — short context / long context pricing
234
234
  [OpenAiModels.GPT_56_Sol]: { input: 5.0, cached_input: 0.5, cache_write: 6.25, output: 30.0, input_gt_200k: 10.0, output_gt_200k: 45.0 },
235
- [OpenAiModels.GPT_56_Terra]: { input: 2.5, cached_input: 0.25, cache_write: 3.125, output: 15.0, input_gt_200k: 5.0, output_gt_200k: 22.5 },
236
- [OpenAiModels.GPT_56_Luna]: { input: 1.0, cached_input: 0.10, cache_write: 1.25, output: 6.0, input_gt_200k: 2.0, output_gt_200k: 9.0 },
235
+ [OpenAiModels.GPT_56_Terra]: { input: 2.0, cached_input: 0.20, cache_write: 2.5, output: 12.0, input_gt_200k: 4.0, output_gt_200k: 18.0 },
236
+ [OpenAiModels.GPT_56_Luna]: { input: 0.2, cached_input: 0.02, cache_write: 0.25, output: 1.2, input_gt_200k: 0.4, output_gt_200k: 1.8 },
237
237
  [OpenAiModels.GPT_55]: { input: 5.0, cached_input: 0.5, output: 30.0 },
238
238
  [OpenAiModels.GPT_55_Pro]: { input: 30.0, cached_input: 0, output: 180.0, reasoningLevels: ["medium", "high", "xhigh"] },
239
239
  [OpenAiModels.GPT_54]: { input: 2.5, cached_input: 0.25, output: 15.0 },
@@ -0,0 +1,80 @@
1
+ /**
2
+ * QWEN Cloud pricing (USD per 1M tokens)
3
+ * Source: https://home.qwencloud.com/data/api.json?product=AliyunDeliveryService&action=ListModelSeries
4
+ * International DashScope endpoint: https://dashscope-intl.aliyuncs.com/compatible-mode/v1
5
+ * Last updated: 2026-08-03
6
+ *
7
+ * Notes:
8
+ * - Several Qwen models use tiered pricing based on input context length.
9
+ * We use the standard tier (≤256k) as the base price here.
10
+ * - DeepSeek and GLM models are also available via QwenCloud's DashScope endpoint.
11
+ * - Video/image/audio generation models have per-second/per-image pricing (not per-token)
12
+ * and are NOT included in QwenTextPricing.
13
+ */
14
+
15
+ export const QwenTextPricing: Record<string, { input: number; output: number; cached_input?: number }> = {
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Qwen3.8 series (newest flagship, released 2026-08)
19
+ // ---------------------------------------------------------------------------
20
+ // qwen3.8-max: $2/$6, implicit cache: $0.25, explicit cache creation: $2.5, explicit cache read: $0.17
21
+ "qwen3.8-max": { input: 2.00, output: 6.00, cached_input: 0.25 },
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Qwen3.7 series
25
+ // ---------------------------------------------------------------------------
26
+ // qwen3.7-plus: tiered — ≤256k: $0.4/$1.6, 256k-1M: $1.2/$4.8
27
+ // implicit cache: $0.08 (≤256k), explicit cache creation: $0.5, explicit cache read: $0.04
28
+ "qwen3.7-plus": { input: 0.40, output: 1.60, cached_input: 0.08 },
29
+ "qwen3.7-plus-2026-05-26": { input: 0.40, output: 1.60, cached_input: 0.08 },
30
+
31
+ // qwen3.7-flash: tiered — ≤32k: $0.03/$0.13, 32k-256k: $0.10/$0.40, 256k-1M: $0.20/$0.80
32
+ "qwen3.7-flash": { input: 0.03, output: 0.13, cached_input: 0.006 },
33
+ "qwen3.7-flash-2026-07-15": { input: 0.03, output: 0.13, cached_input: 0.006 },
34
+
35
+ // qwen3.7-max: $2.50/$7.50, cache: $0.50
36
+ "qwen3.7-max": { input: 2.50, output: 7.50, cached_input: 0.50 },
37
+ "qwen3.7-max-2026-06-08": { input: 2.50, output: 7.50, cached_input: 0.50 },
38
+ "qwen3.7-max-2026-05-20": { input: 2.50, output: 7.50 },
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Qwen3.6 series
42
+ // ---------------------------------------------------------------------------
43
+ // qwen3.6-flash: tiered — ≤256k: $0.25/$1.50, 256k-1M: $1.00/$4.00
44
+ "qwen3.6-flash": { input: 0.25, output: 1.50, cached_input: 0.025 },
45
+ "qwen3.6-flash-2026-04-16": { input: 0.25, output: 1.50 },
46
+ // qwen3.6-plus: tiered — ≤256k: $0.50/$3.00, 256k-1M: $2.00/$6.00
47
+ "qwen3.6-plus": { input: 0.50, output: 3.00, cached_input: 0.05 },
48
+ "qwen3.6-plus-2026-04-02": { input: 0.50, output: 3.00 },
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Legacy alias models (qwen-turbo, qwen-plus, qwen-max, qwen-long)
52
+ // These are rolling aliases that point to the latest generation.
53
+ // ---------------------------------------------------------------------------
54
+ "qwen-turbo": { input: 0.05, output: 0.20, cached_input: 0.005 },
55
+ "qwen-turbo-latest": { input: 0.05, output: 0.20, cached_input: 0.005 },
56
+ "qwen-plus": { input: 0.40, output: 1.20, cached_input: 0.04 },
57
+ "qwen-plus-latest": { input: 0.40, output: 1.20, cached_input: 0.04 },
58
+ "qwen-max": { input: 0.861, output: 3.44, cached_input: 0.086 },
59
+ "qwen-max-latest": { input: 0.861, output: 3.44, cached_input: 0.086 },
60
+ // qwen-long: extended context up to 10M tokens
61
+ "qwen-long": { input: 0.05, output: 0.20, cached_input: 0.005 },
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // DeepSeek models (available via QwenCloud/DashScope endpoint)
65
+ // ---------------------------------------------------------------------------
66
+ // DeepSeek-V4-Flash: lightweight MoE, 284B params, 13B activated, 1M context
67
+ "deepseek-v4-flash": { input: 0.20, output: 0.40, cached_input: 0.04 },
68
+ "deepseek-v4-flash-0731": { input: 0.20, output: 0.40, cached_input: 0.04 },
69
+ // DeepSeek-V4-Pro: flagship MoE, 1.6T params, 49B activated, 1M context
70
+ "deepseek-v4-pro": { input: 2.40, output: 4.80, cached_input: 0.20 },
71
+ // DeepSeek-V3.2: sparse attention, 128k context
72
+ // explicit cache creation: $0.713, explicit cache read: $0.057
73
+ "deepseek-v3.2": { input: 0.57, output: 1.71, cached_input: 0.114 },
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // GLM models (Zhipu AI, available via QwenCloud/DashScope endpoint)
77
+ // ---------------------------------------------------------------------------
78
+ // GLM-5.2-Fast-Preview: 1M context, high-speed inference
79
+ "glm-5.2-fast-preview": { input: 2.80, output: 8.80, cached_input: 0.56 },
80
+ };