@tyvm/knowhow 0.0.151 → 0.0.154

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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +31 -5
  3. package/src/agents/tools/list.ts +31 -28
  4. package/src/auth/keyManager.ts +168 -3
  5. package/src/auth/registerIdentity.ts +1 -1
  6. package/src/clients/anthropic.ts +3 -1
  7. package/src/clients/contextLimits.ts +3 -1
  8. package/src/clients/gemini.ts +16 -4
  9. package/src/clients/http.ts +8 -5
  10. package/src/clients/knowhowMcp.ts +2 -2
  11. package/src/clients/openai.ts +7 -4
  12. package/src/clients/pricing/google.ts +11 -2
  13. package/src/clients/pricing/xai.ts +5 -1
  14. package/src/clients/xai.ts +6 -3
  15. package/src/commands/misc.ts +25 -2
  16. package/tests/clients/reasoningEffort.test.ts +5 -1
  17. package/tests/unit/auth/keyManager.test.ts +63 -0
  18. package/tests/unit/commands/misc.test.ts +85 -0
  19. package/ts_build/package.json +1 -1
  20. package/ts_build/src/agents/base/base.d.ts +1 -1
  21. package/ts_build/src/agents/base/base.js +13 -5
  22. package/ts_build/src/agents/base/base.js.map +1 -1
  23. package/ts_build/src/agents/tools/list.js +0 -26
  24. package/ts_build/src/agents/tools/list.js.map +1 -1
  25. package/ts_build/src/auth/keyManager.d.ts +4 -0
  26. package/ts_build/src/auth/keyManager.js +138 -3
  27. package/ts_build/src/auth/keyManager.js.map +1 -1
  28. package/ts_build/src/auth/registerIdentity.js +1 -1
  29. package/ts_build/src/auth/registerIdentity.js.map +1 -1
  30. package/ts_build/src/clients/anthropic.js +3 -1
  31. package/ts_build/src/clients/anthropic.js.map +1 -1
  32. package/ts_build/src/clients/contextLimits.js +2 -0
  33. package/ts_build/src/clients/contextLimits.js.map +1 -1
  34. package/ts_build/src/clients/gemini.js +14 -3
  35. package/ts_build/src/clients/gemini.js.map +1 -1
  36. package/ts_build/src/clients/http.js +8 -5
  37. package/ts_build/src/clients/http.js.map +1 -1
  38. package/ts_build/src/clients/knowhowMcp.js +2 -2
  39. package/ts_build/src/clients/knowhowMcp.js.map +1 -1
  40. package/ts_build/src/clients/openai.js +7 -4
  41. package/ts_build/src/clients/openai.js.map +1 -1
  42. package/ts_build/src/clients/pricing/google.d.ts +1 -0
  43. package/ts_build/src/clients/pricing/google.js +7 -0
  44. package/ts_build/src/clients/pricing/google.js.map +1 -1
  45. package/ts_build/src/clients/pricing/xai.d.ts +1 -0
  46. package/ts_build/src/clients/pricing/xai.js +4 -0
  47. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  48. package/ts_build/src/clients/xai.js +6 -3
  49. package/ts_build/src/clients/xai.js.map +1 -1
  50. package/ts_build/src/commands/misc.js +21 -3
  51. package/ts_build/src/commands/misc.js.map +1 -1
  52. package/ts_build/src/types.d.ts +2 -0
  53. package/ts_build/tests/clients/reasoningEffort.test.js +2 -1
  54. package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -1
  55. package/ts_build/tests/unit/auth/keyManager.test.js +44 -0
  56. package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -1
  57. package/ts_build/tests/unit/commands/misc.test.d.ts +1 -0
  58. package/ts_build/tests/unit/commands/misc.test.js +70 -0
  59. package/ts_build/tests/unit/commands/misc.test.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.151",
3
+ "version": "0.0.154",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -1107,6 +1107,14 @@ export abstract class BaseAgent implements IAgent {
1107
1107
 
1108
1108
  // Typically, there's only one choice in the array, but you could have many
1109
1109
  // If you set `n` to more than 1, you will get multiple choices
1110
+ // Collect all tool calls across all choices up front.
1111
+ // This is used by detectTruncatedToolCalls so that the size heuristic
1112
+ // compares the full completion_tokens against ALL argument content,
1113
+ // rather than just the current choice's single tool call (Anthropic maps
1114
+ // each parallel tool_use block to a separate choice).
1115
+ const allResponseToolCalls = response.choices.flatMap(
1116
+ (c) => c.message.tool_calls ?? []
1117
+ );
1110
1118
  for (const choice of response.choices) {
1111
1119
  messages.push(choice.message);
1112
1120
 
@@ -1136,7 +1144,8 @@ export abstract class BaseAgent implements IAgent {
1136
1144
 
1137
1145
  const truncationWarning = this.detectTruncatedToolCalls(
1138
1146
  toolCalls,
1139
- response
1147
+ response,
1148
+ allResponseToolCalls
1140
1149
  );
1141
1150
  if (truncationWarning) {
1142
1151
  messages.push(truncationWarning as Message);
@@ -1589,11 +1598,14 @@ export abstract class BaseAgent implements IAgent {
1589
1598
  * 2. The model reported many output tokens but the total argument content received is tiny
1590
1599
  * relative to what those tokens should represent (soft/silent truncation).
1591
1600
  *
1601
+ * @param toolCalls - The tool calls from the CURRENT choice being checked.
1602
+ * @param allToolCalls - ALL tool calls across ALL choices in the response (used for the size heuristic).
1592
1603
  * Returns a warning system message if truncation is detected, or null otherwise.
1593
1604
  */
1594
1605
  detectTruncatedToolCalls(
1595
1606
  toolCalls: ToolCall[],
1596
- response: CompletionResponse
1607
+ response: CompletionResponse,
1608
+ allToolCalls?: ToolCall[]
1597
1609
  ): { role: string; content: string } | null {
1598
1610
  // Subtract thinking/reasoning tokens — they're billed as output tokens but
1599
1611
  // don't produce visible argument content, so including them inflates the heuristic.
@@ -1602,7 +1614,13 @@ export abstract class BaseAgent implements IAgent {
1602
1614
  (response?.usage?.output_tokens_details?.thinking_tokens ?? 0) +
1603
1615
  (response?.usage?.output_tokens_details?.reasoning_tokens ?? 0);
1604
1616
  const outputTokens = Math.max(0, rawOutputTokens - thinkingTokens);
1605
- const totalArgLength = toolCalls.reduce(
1617
+ // Use allToolCalls for the size heuristic so that parallel tool calls from
1618
+ // Anthropic (each mapped to a separate choice) are counted together. Without
1619
+ // this, a response with N parallel tool calls would compare the full
1620
+ // completion_tokens count against only 1/N of the actual argument content,
1621
+ // causing false-positive truncation warnings.
1622
+ const callsForSizeCheck = allToolCalls ?? toolCalls;
1623
+ const totalArgLength = callsForSizeCheck.reduce(
1606
1624
  (sum, tc) => sum + (tc.function?.arguments?.length || 0),
1607
1625
  0
1608
1626
  );
@@ -1688,9 +1706,13 @@ export abstract class BaseAgent implements IAgent {
1688
1706
  .filter((s): s is string => typeof s === "string" && s.trim().length > 0)
1689
1707
  .join("\n\n");
1690
1708
 
1691
- this.taskBreakdown = breakdownContent || "";
1709
+ if (!breakdownContent) {
1710
+ throw new Error("Compaction task-breakdown request returned no text content");
1711
+ }
1712
+
1713
+ this.taskBreakdown = breakdownContent;
1692
1714
  this.log(`task breakdown cost: ${response.usd_cost}`);
1693
- return this.taskBreakdown || undefined;
1715
+ return this.taskBreakdown;
1694
1716
  }
1695
1717
 
1696
1718
  async compressMessages(messages: Message[]) {
@@ -1742,6 +1764,10 @@ export abstract class BaseAgent implements IAgent {
1742
1764
  const summaries = response.choices
1743
1765
  .map((c) => c.message.content)
1744
1766
  .filter((s): s is string => typeof s === "string" && s.trim().length > 0);
1767
+ if (summaries.length === 0) {
1768
+ throw new Error("Compaction summary request returned no text content");
1769
+ }
1770
+
1745
1771
  this.summaries.push(...summaries);
1746
1772
 
1747
1773
  const startMessages = [
@@ -31,34 +31,37 @@ function getPluginNames(): string {
31
31
  }
32
32
 
33
33
  export const includedTools = [
34
- {
35
- type: "function",
36
- function: {
37
- name: "agentCall",
38
- description:
39
- "Run another registered agent in-process as a blocking call and get its final answer back as a string. Unlike a plain llm() call, the target agent can use tools, loop, and maintain its own context. Useful as a graph node that delegates to a specialized agent (e.g. Researcher, Patcher). For long-running / parallel / detached work prefer startAgentTask.",
40
- parameters: {
41
- type: "object",
42
- positional: true,
43
- properties: {
44
- agentName: {
45
- type: "string",
46
- description:
47
- "The name of the agent to call (e.g. Researcher, Developer, Patcher).",
48
- },
49
- query: {
50
- type: "string",
51
- description: "The query / task to send to the agent.",
52
- },
53
- },
54
- required: ["agentName", "query"],
55
- },
56
- returns: {
57
- type: "string",
58
- description: "The agent's final answer text.",
59
- },
60
- },
61
- },
34
+ /*
35
+ * This doesn't use messageProcessors etc, needs rewrite
36
+ *{
37
+ * type: "function",
38
+ * function: {
39
+ * name: "agentCall",
40
+ * description:
41
+ * "Run another registered agent in-process as a blocking call and get its final answer back as a string. Unlike a plain llm() call, the target agent can use tools, loop, and maintain its own context. Useful as a graph node that delegates to a specialized agent (e.g. Researcher, Patcher). For long-running / parallel / detached work prefer startAgentTask.",
42
+ * parameters: {
43
+ * type: "object",
44
+ * positional: true,
45
+ * properties: {
46
+ * agentName: {
47
+ * type: "string",
48
+ * description:
49
+ * "The name of the agent to call (e.g. Researcher, Developer, Patcher).",
50
+ * },
51
+ * query: {
52
+ * type: "string",
53
+ * description: "The query / task to send to the agent.",
54
+ * },
55
+ * },
56
+ * required: ["agentName", "query"],
57
+ * },
58
+ * returns: {
59
+ * type: "string",
60
+ * description: "The agent's final answer text.",
61
+ * },
62
+ * },
63
+ *},
64
+ */
62
65
  {
63
66
  type: "function",
64
67
  function: {
@@ -6,6 +6,13 @@ import path from "path";
6
6
  const ED25519_KEY_TYPE = "ssh-ed25519";
7
7
  const ED25519_RAW_KEY_LENGTH = 32;
8
8
  const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
9
+ const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
10
+
11
+ interface OpenSshMetadata {
12
+ encrypted: boolean;
13
+ algorithm?: string;
14
+ seed?: Buffer;
15
+ }
9
16
 
10
17
  export interface KeyPair {
11
18
  privateKeyPath: string;
@@ -90,6 +97,14 @@ export function getOrCreatePublicKey(privateKeyPath?: string): KeyPair {
90
97
  : generateKeyPair(privateKeyPath);
91
98
  }
92
99
 
100
+ export function ensureIdentityKeyPair(privateKeyPath?: string): {
101
+ keyPair: KeyPair;
102
+ created: boolean;
103
+ } {
104
+ if (keyPairExists(privateKeyPath)) return { keyPair: loadKeyPair(privateKeyPath), created: false };
105
+ return { keyPair: generateKeyPair(privateKeyPath), created: true };
106
+ }
107
+
93
108
  export function signMessage(message: string, privateKeyPath?: string): string {
94
109
  const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
95
110
  if (!fs.existsSync(privatePath)) throw new Error(`Private key not found at: ${privatePath}`);
@@ -98,14 +113,50 @@ export function signMessage(message: string, privateKeyPath?: string): string {
98
113
 
99
114
  function readEd25519PrivateKey(privatePath: string): crypto.KeyObject {
100
115
  let key: crypto.KeyObject;
116
+ const privateKeyContents = fs.readFileSync(privatePath, "utf8");
117
+ const openSshMetadata = parseOpenSshMetadata(privateKeyContents);
118
+
119
+ if (openSshMetadata?.encrypted) {
120
+ throw new Error(
121
+ `Identity at ${privatePath} is an encrypted OpenSSH key. Use an unencrypted Ed25519 key for CLI auth (example: ssh-keygen -t ed25519 -N '' -f ${privatePath})`
122
+ );
123
+ }
124
+
101
125
  try {
102
- key = crypto.createPrivateKey(fs.readFileSync(privatePath));
126
+ key = crypto.createPrivateKey({ key: privateKeyContents, format: "pem" });
103
127
  } catch {
104
- throw new Error(`Identity at ${privatePath} is not a supported PEM Ed25519 private key`);
128
+ if (openSshMetadata) {
129
+ if (openSshMetadata.algorithm && openSshMetadata.algorithm !== ED25519_KEY_TYPE) {
130
+ throw new Error(
131
+ `Identity at ${privatePath} must be an OpenSSH Ed25519 key (found ${openSshMetadata.algorithm})`
132
+ );
133
+ }
134
+
135
+ if (openSshMetadata.seed) {
136
+ key = createEd25519PrivateKeyFromSeed(openSshMetadata.seed, privatePath);
137
+ } else {
138
+ throw new Error(
139
+ `Identity at ${privatePath} must be a PKCS#8 Ed25519 private key or an unencrypted OpenSSH Ed25519 private key`
140
+ );
141
+ }
142
+ return key;
143
+ }
144
+
145
+ throw new Error(`Identity at ${privatePath} must be a PKCS#8 Ed25519 private key or an unencrypted OpenSSH Ed25519 private key`);
105
146
  }
147
+
148
+ const detectedAlgorithm = key.asymmetricKeyType === "ed25519" ? openSshMetadata?.algorithm ?? "ed25519" : key.asymmetricKeyType;
149
+
106
150
  if (key.asymmetricKeyType !== "ed25519") {
107
- throw new Error(`Identity at ${privatePath} must be an Ed25519 private key`);
151
+ throw new Error(
152
+ `Identity at ${privatePath} must be an Ed25519 private key (found ${detectedAlgorithm ?? "unknown"})`
153
+ );
154
+ }
155
+
156
+ if (openSshMetadata?.algorithm && openSshMetadata.algorithm !== ED25519_KEY_TYPE) {
157
+ throw new Error(`Identity at ${privatePath} must be an OpenSSH Ed25519 key (found ${openSshMetadata.algorithm})`);
108
158
  }
159
+
109
160
  return key;
110
161
  }
111
162
 
@@ -164,3 +215,117 @@ function keyPairMetadata(privatePath: string, rawPublicKey: Buffer): KeyPair {
164
215
  fingerprint: `SHA256:${hash}`,
165
216
  };
166
217
  }
218
+
219
+ function createEd25519PrivateKeyFromSeed(seed: Buffer, privatePath: string): crypto.KeyObject {
220
+ if (seed.length !== ED25519_RAW_KEY_LENGTH) {
221
+ throw new Error(`Identity at ${privatePath} has an invalid OpenSSH Ed25519 private key payload`);
222
+ }
223
+
224
+ try {
225
+ const der = Buffer.concat([ED25519_PKCS8_PREFIX, seed]);
226
+ return crypto.createPrivateKey({ key: der, format: "der", type: "pkcs8" });
227
+ } catch {
228
+ throw new Error(
229
+ `Identity at ${privatePath} must be a PKCS#8 Ed25519 private key or an unencrypted OpenSSH Ed25519 private key`
230
+ );
231
+ }
232
+ }
233
+
234
+ function parseOpenSshMetadata(privateKeyContents: string): OpenSshMetadata | undefined {
235
+ const trimmed = privateKeyContents.trim();
236
+ if (!trimmed.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----")) {
237
+ return undefined;
238
+ }
239
+
240
+ const body = trimmed
241
+ .replace("-----BEGIN OPENSSH PRIVATE KEY-----", "")
242
+ .replace("-----END OPENSSH PRIVATE KEY-----", "")
243
+ .replace(/\s+/g, "");
244
+
245
+ let buffer: Buffer;
246
+ try {
247
+ buffer = Buffer.from(body, "base64");
248
+ } catch {
249
+ return undefined;
250
+ }
251
+
252
+ let offset = 0;
253
+
254
+ const magic = Buffer.from("openssh-key-v1\0", "utf8");
255
+ if (buffer.length < magic.length || !buffer.subarray(0, magic.length).equals(magic)) {
256
+ return undefined;
257
+ }
258
+ offset += magic.length;
259
+
260
+ const readString = (): string | undefined => {
261
+ if (offset + 4 > buffer.length) return undefined;
262
+ const length = buffer.readUInt32BE(offset);
263
+ offset += 4;
264
+ if (offset + length > buffer.length) return undefined;
265
+ const value = buffer.subarray(offset, offset + length).toString("utf8");
266
+ offset += length;
267
+ return value;
268
+ };
269
+
270
+ const readBuffer = (): Buffer | undefined => {
271
+ if (offset + 4 > buffer.length) return undefined;
272
+ const length = buffer.readUInt32BE(offset);
273
+ offset += 4;
274
+ if (offset + length > buffer.length) return undefined;
275
+ const value = buffer.subarray(offset, offset + length);
276
+ offset += length;
277
+ return value;
278
+ };
279
+
280
+ const ciphername = readString();
281
+ const kdfname = readString();
282
+ const kdfoptions = readBuffer();
283
+ if (!ciphername || !kdfname || !kdfoptions) {
284
+ return undefined;
285
+ }
286
+
287
+ if (offset + 4 > buffer.length) return { encrypted: ciphername !== "none" || kdfname !== "none" };
288
+ const keyCount = buffer.readUInt32BE(offset);
289
+ offset += 4;
290
+
291
+ for (let i = 0; i < keyCount; i++) {
292
+ const skippedPublic = readBuffer();
293
+ if (!skippedPublic) return { encrypted: ciphername !== "none" || kdfname !== "none" };
294
+ }
295
+
296
+ const privateBlock = readBuffer();
297
+ if (!privateBlock) return { encrypted: ciphername !== "none" || kdfname !== "none" };
298
+
299
+ let privateOffset = 0;
300
+ const readPrivateString = (): string | undefined => {
301
+ if (privateOffset + 4 > privateBlock.length) return undefined;
302
+ const length = privateBlock.readUInt32BE(privateOffset);
303
+ privateOffset += 4;
304
+ if (privateOffset + length > privateBlock.length) return undefined;
305
+ const value = privateBlock.subarray(privateOffset, privateOffset + length).toString("utf8");
306
+ privateOffset += length;
307
+ return value;
308
+ };
309
+
310
+ const readPrivateBuffer = (): Buffer | undefined => {
311
+ if (privateOffset + 4 > privateBlock.length) return undefined;
312
+ const length = privateBlock.readUInt32BE(privateOffset);
313
+ privateOffset += 4;
314
+ if (privateOffset + length > privateBlock.length) return undefined;
315
+ const value = privateBlock.subarray(privateOffset, privateOffset + length);
316
+ privateOffset += length;
317
+ return value;
318
+ };
319
+
320
+ privateOffset += 8; // checkints
321
+
322
+ const algorithm = readPrivateString();
323
+ let seed: Buffer | undefined;
324
+ if (algorithm === ED25519_KEY_TYPE) {
325
+ readPrivateBuffer(); // public key (32 bytes)
326
+ const privateConcat = readPrivateBuffer(); // private seed (32) + public key (32)
327
+ seed = privateConcat?.subarray(0, ED25519_RAW_KEY_LENGTH);
328
+ }
329
+
330
+ return { encrypted: ciphername !== "none" || kdfname !== "none", algorithm, seed };
331
+ }
@@ -28,7 +28,7 @@ export function decodeJwtPayload(jwt: string): Record<string, unknown> {
28
28
 
29
29
  export function extractOrgIdFromJwt(jwt: string): string {
30
30
  const payload = decodeJwtPayload(jwt);
31
- const orgId = payload["org"] ?? payload["orgId"] ?? payload["org_id"] ?? payload["organizationId"];
31
+ const orgId = payload.org ?? payload.orgId ?? payload.org_id ?? payload.organizationId;
32
32
  if (typeof orgId !== "string" || !orgId) {
33
33
  throw new Error("JWT does not contain an organization claim (expected org, orgId, org_id, or organizationId)");
34
34
  }
@@ -472,7 +472,9 @@ export class GenericAnthropicClient implements GenericClient {
472
472
  ...(thinkingParam && { thinking: thinkingParam }),
473
473
  ...(effortOutputConfig && { output_config: effortOutputConfig }),
474
474
  ...(tools.length && {
475
- tool_choice: { type: "auto" },
475
+ tool_choice: {
476
+ type: options.tool_choice ?? "auto",
477
+ },
476
478
  tools,
477
479
  }),
478
480
  }, { signal: options.signal });
@@ -123,7 +123,8 @@ export const ContextLimits: Record<string, number> = {
123
123
  [Models.anthropic.Sonnet4_0]: 200_000,
124
124
 
125
125
  // ─── Google ───────────────────────────────────────────────────────────────
126
- // Gemini 3.6 / 3.5 (text)
126
+ // Gemini 3.7 / 3.6 / 3.5 (text)
127
+ [Models.google.Gemini_37_Flash]: 1_048_576,
127
128
  [Models.google.Gemini_36_Flash]: 1_000_000,
128
129
  [Models.google.Gemini_35_Flash]: 1_000_000,
129
130
  [Models.google.Gemini_35_Flash_Lite]: 1_000_000,
@@ -178,6 +179,7 @@ export const ContextLimits: Record<string, number> = {
178
179
  [Models.google.Gemini_15_Pro]: 2_000_000,
179
180
 
180
181
  // ─── xAI ──────────────────────────────────────────────────────────────────
182
+ [Models.xai.Grok4_6]: 500_000,
181
183
  [Models.xai.Grok4_1_Fast_Reasoning]: 2_000_000,
182
184
  [Models.xai.Grok4_1_Fast_NonReasoning]: 2_000_000,
183
185
  [Models.xai.Grok_4_20_Reasoning]: 131_072,
@@ -420,7 +420,7 @@ export class GenericGeminiClient implements GenericClient {
420
420
 
421
421
  /**
422
422
  * Builds the thinkingConfig for Gemini models that support it.
423
- * - Gemini 3.x models use thinkingLevel: "minimal" | "low" | "medium" | "high"
423
+ * - Gemini 3.x models use thinkingLevel (supported levels vary by model)
424
424
  * - Gemini 2.5 models use thinkingBudget: number (0 = off, -1 = dynamic)
425
425
  *
426
426
  * Maps CompletionOptions.reasoning_effort to provider-specific values.
@@ -433,9 +433,11 @@ export class GenericGeminiClient implements GenericClient {
433
433
 
434
434
  // Gemini 3.x — use thinkingLevel
435
435
  if (GoogleThinkingLevelModels.includes(model)) {
436
+ // Gemini 3.7 Flash rejects "minimal"; low is its least intensive level.
437
+ const minimumLevel = model === Models.google.Gemini_37_Flash ? "low" : "minimal";
436
438
  const levelMap: Record<string, string> = {
437
- none: "minimal",
438
- minimal: "minimal",
439
+ none: minimumLevel,
440
+ minimal: minimumLevel,
439
441
  low: "low",
440
442
  medium: "medium",
441
443
  high: "high",
@@ -474,6 +476,7 @@ export class GenericGeminiClient implements GenericClient {
474
476
  const { systemInstruction, contents } = this.transformMessages(
475
477
  options.messages
476
478
  );
479
+ const tools = this.transformTools(options.tools);
477
480
 
478
481
  try {
479
482
  await wait(2000);
@@ -483,7 +486,16 @@ export class GenericGeminiClient implements GenericClient {
483
486
  config: {
484
487
  systemInstruction,
485
488
  thinkingConfig,
486
- tools: this.transformTools(options.tools),
489
+ tools,
490
+ ...(tools.length && {
491
+ toolConfig: {
492
+ functionCallingConfig: {
493
+ mode: options.tool_choice === "none"
494
+ ? FunctionCallingConfigMode.NONE
495
+ : FunctionCallingConfigMode.AUTO,
496
+ },
497
+ },
498
+ }),
487
499
  maxOutputTokens: options.max_tokens,
488
500
  },
489
501
  });
@@ -247,9 +247,9 @@ export class HttpClient implements GenericClient {
247
247
  max_tokens: options.max_tokens || 4000,
248
248
  ...this.extra_body,
249
249
 
250
- ...(options.tools && {
250
+ ...(options.tools?.length && {
251
251
  tools: options.tools,
252
- tool_choice: "auto",
252
+ tool_choice: options.tool_choice ?? "auto",
253
253
  }),
254
254
  };
255
255
 
@@ -297,9 +297,9 @@ export class HttpClient implements GenericClient {
297
297
  ...this.extra_body,
298
298
  stream: true,
299
299
  stream_options: { include_usage: true },
300
- ...(options.tools && {
300
+ ...(options.tools?.length && {
301
301
  tools: options.tools,
302
- tool_choice: "auto",
302
+ tool_choice: options.tool_choice ?? "auto",
303
303
  }),
304
304
  };
305
305
 
@@ -442,7 +442,10 @@ export class HttpClient implements GenericClient {
442
442
  input,
443
443
  ...(instructions && { instructions }),
444
444
  ...(options.max_tokens && { max_output_tokens: options.max_tokens }),
445
- ...(tools?.length && { tools, tool_choice: "auto" }),
445
+ ...(tools?.length && {
446
+ tools,
447
+ tool_choice: options.tool_choice ?? "auto",
448
+ }),
446
449
  // Reasoning persistence: store server-side when requested, otherwise
447
450
  // ask the provider to return encrypted reasoning content so we can
448
451
  // thread the reasoning items back into the next request ourselves.
@@ -62,9 +62,9 @@ export class KnowhowMcpClient implements GenericClient {
62
62
  ...options,
63
63
  max_tokens: options.max_tokens || 3000,
64
64
 
65
- ...(options.tools && {
65
+ ...(options.tools?.length && {
66
66
  tools: options.tools,
67
- tool_choice: "auto",
67
+ tool_choice: options.tool_choice ?? "auto",
68
68
  }),
69
69
  },
70
70
  }
@@ -235,9 +235,9 @@ export class GenericOpenAiClient implements GenericClient {
235
235
  max_completion_tokens: Math.max(options.max_tokens ?? 0, 16_000),
236
236
  reasoning_effort: this.resolveReasoningEffort(options),
237
237
  }),
238
- ...(options.tools && {
238
+ ...(options.tools?.length && {
239
239
  tools: options.tools,
240
- tool_choice: "auto",
240
+ tool_choice: options.tool_choice ?? "auto",
241
241
  }),
242
242
  }, { signal: options.signal });
243
243
 
@@ -296,7 +296,10 @@ export class GenericOpenAiClient implements GenericClient {
296
296
  max_tokens: options.max_tokens,
297
297
  stream: true,
298
298
  stream_options: { include_usage: true },
299
- ...(options.tools && { tools: options.tools, tool_choice: "auto" }),
299
+ ...(options.tools?.length && {
300
+ tools: options.tools,
301
+ tool_choice: options.tool_choice ?? "auto",
302
+ }),
300
303
  }, { signal: options.signal });
301
304
 
302
305
  let usage: CompletionResponse['usage'] | undefined;
@@ -501,7 +504,7 @@ export class GenericOpenAiClient implements GenericClient {
501
504
  }),
502
505
  ...(tools?.length && {
503
506
  tools,
504
- tool_choice: "auto",
507
+ tool_choice: options.tool_choice ?? "auto",
505
508
  }),
506
509
  } as any);
507
510
 
@@ -7,7 +7,8 @@ import { completions, embeddings, images, videos, audios, liveApi, ModelCatalogE
7
7
  // ─── Model IDs ────────────────────────────────────────────────────────────────
8
8
 
9
9
  export const GoogleModels = {
10
- // Gemini 3.6 / 3.5
10
+ // Gemini 3.7 / 3.6 / 3.5
11
+ Gemini_37_Flash: "gemini-3.7-flash",
11
12
  Gemini_36_Flash: "gemini-3.6-flash",
12
13
  Gemini_35_Flash: "gemini-3.5-flash",
13
14
  Gemini_35_Flash_Lite: "gemini-3.5-flash-lite",
@@ -86,6 +87,7 @@ export const GoogleEmbeddingModels: Record<string, string> = {
86
87
  // ─── Modality arrays ──────────────────────────────────────────────────────────
87
88
 
88
89
  export const GoogleTextModels: string[] = [
90
+ GoogleModels.Gemini_37_Flash,
89
91
  GoogleModels.Gemini_36_Flash,
90
92
  GoogleModels.Gemini_35_Flash,
91
93
  GoogleModels.Gemini_35_Flash_Lite,
@@ -101,6 +103,7 @@ export const GoogleTextModels: string[] = [
101
103
 
102
104
  // Models that support thinkingLevel (Gemini 3.x series)
103
105
  export const GoogleThinkingLevelModels: string[] = [
106
+ GoogleModels.Gemini_37_Flash,
104
107
  GoogleModels.Gemini_36_Flash,
105
108
  GoogleModels.Gemini_35_Flash,
106
109
  GoogleModels.Gemini_35_Flash_Lite,
@@ -214,7 +217,13 @@ export interface GeminiModelPricing extends ModelPricing {
214
217
  }
215
218
 
216
219
  export const GeminiPricing: Record<string, GeminiModelPricing> = {
217
- // ── Gemini 3.6 / 3.5 ──────────────────────────────────────────────────────
220
+ // ── Gemini 3.7 / 3.6 / 3.5 ────────────────────────────────────────────────
221
+ // Introductory pricing through December 31, 2026. Standard pricing beginning
222
+ // January 1, 2027 is $1.50 input, $7.50 output, and $0.15 cached input.
223
+ [GoogleModels.Gemini_37_Flash]: {
224
+ input: 0.75, output: 3.75,
225
+ context_caching: 0.075, context_caching_storage: 0.5,
226
+ },
218
227
  [GoogleModels.Gemini_36_Flash]: {
219
228
  input: 1.5, output: 7.5,
220
229
  context_caching: 0.15, context_caching_storage: 1.0,
@@ -7,6 +7,7 @@ import { completions, images, videos, ModelCatalogEntry, ModelPricing } from "./
7
7
  // ─── Model IDs ────────────────────────────────────────────────────────────────
8
8
 
9
9
  export const XaiModels = {
10
+ Grok4_6: "grok-4.6",
10
11
  Grok_4_20_Reasoning: "grok-4.20-0309-reasoning",
11
12
  Grok_4_20_NonReasoning: "grok-4.20-0309-non-reasoning",
12
13
  Grok_4_20_MultiAgent: "grok-4.20-multi-agent-0309",
@@ -42,6 +43,7 @@ export const XaiModels = {
42
43
  // ─── Modality arrays ──────────────────────────────────────────────────────────
43
44
 
44
45
  export const XaiTextModels: string[] = [
46
+ XaiModels.Grok4_6,
45
47
  XaiModels.Grok_4_20_Reasoning, XaiModels.Grok_4_20_NonReasoning,
46
48
  XaiModels.Grok_4_20_MultiAgent,
47
49
  XaiModels.Grok4_1_Fast_Reasoning, XaiModels.Grok4_1_Fast_NonReasoning,
@@ -61,8 +63,9 @@ export const XaiResponsesOnlyModels: string[] = [
61
63
  ];
62
64
 
63
65
  // Models that support the reasoning_effort parameter
64
- // grok-3-mini variants support reasoning_effort; grok-3-beta, grok-4 etc. do NOT
66
+ // Grok 4.6, Grok 4.20 multi-agent, and grok-3-mini variants support reasoning_effort
65
67
  export const XaiReasoningModels: string[] = [
68
+ XaiModels.Grok4_6,
66
69
  XaiModels.Grok_4_20_MultiAgent,
67
70
  XaiModels.Grok3MiniBeta,
68
71
  XaiModels.Grok3MiniFastBeta,
@@ -84,6 +87,7 @@ export const XaiVideoModels: string[] = [XaiModels.GrokImagineVideo];
84
87
  // ─── Pricing (USD per 1M tokens / per-image / per-second) ────────────────────
85
88
 
86
89
  export const XaiTextPricing: Record<string, ModelPricing> = {
90
+ [XaiModels.Grok4_6]: { input: 2.0, input_gt_200k: 4.0, cache_hit: 0.50, output: 6.0, output_gt_200k: 12.0, reasoningLevels: ["low", "medium", "high", "xhigh"] },
87
91
  [XaiModels.Grok_4_20_Reasoning]: { input: 2.0, cache_hit: 0.20, output: 6.0, useResponsesApi: true },
88
92
  [XaiModels.Grok_4_20_NonReasoning]: { input: 2.0, cache_hit: 0.20, output: 6.0, useResponsesApi: true },
89
93
  [XaiModels.Grok_4_20_MultiAgent]: { input: 2.0, cache_hit: 0.20, output: 6.0, reasoningLevels: ["low", "medium", "high", "xhigh"], useResponsesApi: true },
@@ -90,9 +90,9 @@ export class GenericXAIClient implements GenericClient {
90
90
  // grok-3-mini models support reasoning_effort: "low" | "medium" | "high"
91
91
  reasoning_effort: reasoningEffort,
92
92
  }),
93
- ...(options.tools && {
93
+ ...(options.tools?.length && {
94
94
  tools: options.tools,
95
- tool_choice: "auto",
95
+ tool_choice: options.tool_choice ?? "auto",
96
96
  }),
97
97
  });
98
98
 
@@ -238,7 +238,10 @@ export class GenericXAIClient implements GenericClient {
238
238
  ...(instructions && { instructions }),
239
239
  ...(options.max_tokens && { max_output_tokens: Math.max(options.max_tokens, 16_000) }),
240
240
  ...(reasoningEffort && { reasoning: { effort: reasoningEffort } }),
241
- ...(tools?.length && { tools, tool_choice: "auto" }),
241
+ ...(tools?.length && {
242
+ tools,
243
+ tool_choice: options.tool_choice ?? "auto",
244
+ }),
242
245
  // Reasoning persistence (same construct as OpenAI's Responses API):
243
246
  // - store === true → xAI persists the response; next turn passes
244
247
  // previous_response_id to continue the reasoning chain server-side.