@tyvm/knowhow 0.0.105 → 0.0.107

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 (219) hide show
  1. package/CONFIG.md +8 -5
  2. package/package.json +3 -2
  3. package/scripts/check-model-pricing.ts +509 -0
  4. package/scripts/compare-openrouter-coverage.ts +576 -0
  5. package/src/agents/base/base.ts +169 -5
  6. package/src/agents/tools/execCommand.ts +4 -0
  7. package/src/agents/tools/executeScript/definition.ts +1 -1
  8. package/src/agents/tools/index.ts +0 -1
  9. package/src/agents/tools/list.ts +3 -43
  10. package/src/agents/tools/writeFile.ts +1 -1
  11. package/src/auth/browserLogin.ts +9 -4
  12. package/src/chat/modules/RemoteSyncModule.ts +3 -0
  13. package/src/cli.ts +31 -1
  14. package/src/clients/anthropic.ts +8 -2
  15. package/src/clients/cerebras.ts +10 -0
  16. package/src/clients/contextLimits.ts +7 -2
  17. package/src/clients/copilot.ts +23 -0
  18. package/src/clients/deepseek.ts +16 -0
  19. package/src/clients/fireworks.ts +15 -0
  20. package/src/clients/gemini.ts +59 -4
  21. package/src/clients/github.ts +16 -0
  22. package/src/clients/groq.ts +15 -0
  23. package/src/clients/http.ts +194 -6
  24. package/src/clients/index.ts +116 -4
  25. package/src/clients/llama.ts +16 -0
  26. package/src/clients/mistral.ts +16 -0
  27. package/src/clients/nvidia.ts +16 -0
  28. package/src/clients/openai.ts +53 -12
  29. package/src/clients/openrouter.ts +17 -0
  30. package/src/clients/pricing/anthropic.ts +105 -78
  31. package/src/clients/pricing/cerebras.ts +11 -0
  32. package/src/clients/pricing/copilot.ts +60 -0
  33. package/src/clients/pricing/deepseek.ts +15 -0
  34. package/src/clients/pricing/fireworks.ts +32 -0
  35. package/src/clients/pricing/github.ts +69 -0
  36. package/src/clients/pricing/google.ts +245 -206
  37. package/src/clients/pricing/groq.ts +56 -0
  38. package/src/clients/pricing/index.ts +42 -5
  39. package/src/clients/pricing/llama.ts +18 -0
  40. package/src/clients/pricing/mistral.ts +34 -0
  41. package/src/clients/pricing/models.ts +7 -236
  42. package/src/clients/pricing/nvidia.ts +102 -0
  43. package/src/clients/pricing/openai.ts +348 -171
  44. package/src/clients/pricing/openrouter.ts +36 -0
  45. package/src/clients/pricing/types.ts +83 -2
  46. package/src/clients/pricing/xai.ts +121 -65
  47. package/src/clients/types.ts +28 -1
  48. package/src/clients/xai.ts +161 -1
  49. package/src/fileSync.ts +8 -2
  50. package/src/login.ts +11 -3
  51. package/src/services/AgentSyncFs.ts +36 -12
  52. package/src/services/KnowhowClient.ts +11 -0
  53. package/src/services/LazyToolsService.ts +6 -0
  54. package/src/services/S3.ts +0 -7
  55. package/src/services/modules/index.ts +11 -2
  56. package/src/types.ts +56 -279
  57. package/src/worker.ts +174 -0
  58. package/tests/clients/AIClient.test.ts +1 -1
  59. package/tests/clients/anthropic.test.ts +202 -0
  60. package/tests/clients/pricing.test.ts +37 -0
  61. package/tests/manual/clients/completions.json +838 -226
  62. package/tests/manual/clients/completions.test.ts +46 -31
  63. package/ts_build/package.json +3 -2
  64. package/ts_build/src/agents/base/base.d.ts +18 -1
  65. package/ts_build/src/agents/base/base.js +111 -4
  66. package/ts_build/src/agents/base/base.js.map +1 -1
  67. package/ts_build/src/agents/tools/execCommand.js +3 -0
  68. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  69. package/ts_build/src/agents/tools/executeScript/definition.js +1 -1
  70. package/ts_build/src/agents/tools/executeScript/definition.js.map +1 -1
  71. package/ts_build/src/agents/tools/index.d.ts +0 -1
  72. package/ts_build/src/agents/tools/index.js +0 -1
  73. package/ts_build/src/agents/tools/index.js.map +1 -1
  74. package/ts_build/src/agents/tools/list.js +3 -38
  75. package/ts_build/src/agents/tools/list.js.map +1 -1
  76. package/ts_build/src/agents/tools/visionTool.d.ts +1 -1
  77. package/ts_build/src/agents/tools/writeFile.js +1 -1
  78. package/ts_build/src/agents/tools/writeFile.js.map +1 -1
  79. package/ts_build/src/ai.d.ts +1 -1
  80. package/ts_build/src/auth/browserLogin.d.ts +2 -1
  81. package/ts_build/src/auth/browserLogin.js +10 -3
  82. package/ts_build/src/auth/browserLogin.js.map +1 -1
  83. package/ts_build/src/chat/modules/RemoteSyncModule.js +1 -0
  84. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  85. package/ts_build/src/cli.js +19 -0
  86. package/ts_build/src/cli.js.map +1 -1
  87. package/ts_build/src/clients/anthropic.d.ts +1 -82
  88. package/ts_build/src/clients/anthropic.js +8 -2
  89. package/ts_build/src/clients/anthropic.js.map +1 -1
  90. package/ts_build/src/clients/cerebras.d.ts +4 -0
  91. package/ts_build/src/clients/cerebras.js +14 -0
  92. package/ts_build/src/clients/cerebras.js.map +1 -0
  93. package/ts_build/src/clients/contextLimits.js +7 -2
  94. package/ts_build/src/clients/contextLimits.js.map +1 -1
  95. package/ts_build/src/clients/copilot.d.ts +4 -0
  96. package/ts_build/src/clients/copilot.js +15 -0
  97. package/ts_build/src/clients/copilot.js.map +1 -0
  98. package/ts_build/src/clients/deepseek.d.ts +4 -0
  99. package/ts_build/src/clients/deepseek.js +15 -0
  100. package/ts_build/src/clients/deepseek.js.map +1 -0
  101. package/ts_build/src/clients/fireworks.d.ts +4 -0
  102. package/ts_build/src/clients/fireworks.js +15 -0
  103. package/ts_build/src/clients/fireworks.js.map +1 -0
  104. package/ts_build/src/clients/gemini.d.ts +1 -0
  105. package/ts_build/src/clients/gemini.js +38 -2
  106. package/ts_build/src/clients/gemini.js.map +1 -1
  107. package/ts_build/src/clients/github.d.ts +4 -0
  108. package/ts_build/src/clients/github.js +15 -0
  109. package/ts_build/src/clients/github.js.map +1 -0
  110. package/ts_build/src/clients/groq.d.ts +4 -0
  111. package/ts_build/src/clients/groq.js +15 -0
  112. package/ts_build/src/clients/groq.js.map +1 -0
  113. package/ts_build/src/clients/http.d.ts +22 -1
  114. package/ts_build/src/clients/http.js +135 -7
  115. package/ts_build/src/clients/http.js.map +1 -1
  116. package/ts_build/src/clients/index.d.ts +14 -0
  117. package/ts_build/src/clients/index.js +94 -4
  118. package/ts_build/src/clients/index.js.map +1 -1
  119. package/ts_build/src/clients/llama.d.ts +4 -0
  120. package/ts_build/src/clients/llama.js +15 -0
  121. package/ts_build/src/clients/llama.js.map +1 -0
  122. package/ts_build/src/clients/mistral.d.ts +4 -0
  123. package/ts_build/src/clients/mistral.js +15 -0
  124. package/ts_build/src/clients/mistral.js.map +1 -0
  125. package/ts_build/src/clients/nvidia.d.ts +4 -0
  126. package/ts_build/src/clients/nvidia.js +15 -0
  127. package/ts_build/src/clients/nvidia.js.map +1 -0
  128. package/ts_build/src/clients/openai.d.ts +4 -206
  129. package/ts_build/src/clients/openai.js +38 -10
  130. package/ts_build/src/clients/openai.js.map +1 -1
  131. package/ts_build/src/clients/openrouter.d.ts +4 -0
  132. package/ts_build/src/clients/openrouter.js +15 -0
  133. package/ts_build/src/clients/openrouter.js.map +1 -0
  134. package/ts_build/src/clients/pricing/anthropic.d.ts +26 -78
  135. package/ts_build/src/clients/pricing/anthropic.js +75 -78
  136. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  137. package/ts_build/src/clients/pricing/cerebras.d.ts +4 -0
  138. package/ts_build/src/clients/pricing/cerebras.js +11 -0
  139. package/ts_build/src/clients/pricing/cerebras.js.map +1 -0
  140. package/ts_build/src/clients/pricing/copilot.d.ts +5 -0
  141. package/ts_build/src/clients/pricing/copilot.js +35 -0
  142. package/ts_build/src/clients/pricing/copilot.js.map +1 -0
  143. package/ts_build/src/clients/pricing/deepseek.d.ts +5 -0
  144. package/ts_build/src/clients/pricing/deepseek.js +10 -0
  145. package/ts_build/src/clients/pricing/deepseek.js.map +1 -0
  146. package/ts_build/src/clients/pricing/fireworks.d.ts +5 -0
  147. package/ts_build/src/clients/pricing/fireworks.js +21 -0
  148. package/ts_build/src/clients/pricing/fireworks.js.map +1 -0
  149. package/ts_build/src/clients/pricing/github.d.ts +4 -0
  150. package/ts_build/src/clients/pricing/github.js +58 -0
  151. package/ts_build/src/clients/pricing/github.js.map +1 -0
  152. package/ts_build/src/clients/pricing/google.d.ts +59 -6
  153. package/ts_build/src/clients/pricing/google.js +214 -167
  154. package/ts_build/src/clients/pricing/google.js.map +1 -1
  155. package/ts_build/src/clients/pricing/groq.d.ts +5 -0
  156. package/ts_build/src/clients/pricing/groq.js +41 -0
  157. package/ts_build/src/clients/pricing/groq.js.map +1 -0
  158. package/ts_build/src/clients/pricing/index.d.ts +16 -5
  159. package/ts_build/src/clients/pricing/index.js +62 -7
  160. package/ts_build/src/clients/pricing/index.js.map +1 -1
  161. package/ts_build/src/clients/pricing/llama.d.ts +4 -0
  162. package/ts_build/src/clients/pricing/llama.js +14 -0
  163. package/ts_build/src/clients/pricing/llama.js.map +1 -0
  164. package/ts_build/src/clients/pricing/mistral.d.ts +5 -0
  165. package/ts_build/src/clients/pricing/mistral.js +23 -0
  166. package/ts_build/src/clients/pricing/mistral.js.map +1 -0
  167. package/ts_build/src/clients/pricing/models.d.ts +5 -4
  168. package/ts_build/src/clients/pricing/models.js +8 -162
  169. package/ts_build/src/clients/pricing/models.js.map +1 -1
  170. package/ts_build/src/clients/pricing/nvidia.d.ts +8 -0
  171. package/ts_build/src/clients/pricing/nvidia.js +96 -0
  172. package/ts_build/src/clients/pricing/nvidia.js.map +1 -0
  173. package/ts_build/src/clients/pricing/openai.d.ts +86 -197
  174. package/ts_build/src/clients/pricing/openai.js +295 -168
  175. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  176. package/ts_build/src/clients/pricing/openrouter.d.ts +4 -0
  177. package/ts_build/src/clients/pricing/openrouter.js +29 -0
  178. package/ts_build/src/clients/pricing/openrouter.js.map +1 -0
  179. package/ts_build/src/clients/pricing/types.d.ts +27 -2
  180. package/ts_build/src/clients/pricing/types.js +46 -0
  181. package/ts_build/src/clients/pricing/types.js.map +1 -1
  182. package/ts_build/src/clients/pricing/xai.d.ts +37 -57
  183. package/ts_build/src/clients/pricing/xai.js +92 -59
  184. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  185. package/ts_build/src/clients/types.d.ts +12 -1
  186. package/ts_build/src/clients/xai.d.ts +2 -62
  187. package/ts_build/src/clients/xai.js +132 -1
  188. package/ts_build/src/clients/xai.js.map +1 -1
  189. package/ts_build/src/fileSync.js +7 -2
  190. package/ts_build/src/fileSync.js.map +1 -1
  191. package/ts_build/src/login.js +8 -2
  192. package/ts_build/src/login.js.map +1 -1
  193. package/ts_build/src/services/AgentSyncFs.js +1 -0
  194. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  195. package/ts_build/src/services/KnowhowClient.d.ts +1 -0
  196. package/ts_build/src/services/KnowhowClient.js +7 -0
  197. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  198. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  199. package/ts_build/src/services/LazyToolsService.js +3 -0
  200. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  201. package/ts_build/src/services/S3.js +0 -7
  202. package/ts_build/src/services/S3.js.map +1 -1
  203. package/ts_build/src/services/modules/index.js +41 -1
  204. package/ts_build/src/services/modules/index.js.map +1 -1
  205. package/ts_build/src/types.d.ts +163 -124
  206. package/ts_build/src/types.js +33 -213
  207. package/ts_build/src/types.js.map +1 -1
  208. package/ts_build/src/worker.d.ts +4 -0
  209. package/ts_build/src/worker.js +140 -0
  210. package/ts_build/src/worker.js.map +1 -1
  211. package/ts_build/tests/clients/AIClient.test.js +1 -1
  212. package/ts_build/tests/clients/AIClient.test.js.map +1 -1
  213. package/ts_build/tests/clients/anthropic.test.d.ts +1 -0
  214. package/ts_build/tests/clients/anthropic.test.js +159 -0
  215. package/ts_build/tests/clients/anthropic.test.js.map +1 -0
  216. package/ts_build/tests/clients/pricing.test.js +21 -0
  217. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  218. package/ts_build/tests/manual/clients/completions.test.js +27 -24
  219. package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
package/src/worker.ts CHANGED
@@ -578,3 +578,177 @@ export async function worker(options?: {
578
578
  await wait(5000);
579
579
  }
580
580
  }
581
+
582
+ /**
583
+ * Run tunnel-only mode: connects to the Knowhow tunnel WebSocket without
584
+ * registering any MCP tools. Useful for users who only want the web tunnel
585
+ * feature to expose local ports to the cloud.
586
+ */
587
+ export async function tunnel(options?: {
588
+ share?: boolean;
589
+ unshare?: boolean;
590
+ }) {
591
+ const config = await getConfig();
592
+
593
+ const isInsideDocker = process.env.KNOWHOW_DOCKER === "true";
594
+
595
+ // Determine localHost based on environment
596
+ let tunnelLocalHost = config.worker?.tunnel?.localHost;
597
+ if (!tunnelLocalHost) {
598
+ if (isInsideDocker) {
599
+ tunnelLocalHost = "host.docker.internal";
600
+ console.log(
601
+ "🐳 Docker detected: tunnel will use host.docker.internal to reach host services"
602
+ );
603
+ } else {
604
+ tunnelLocalHost = "127.0.0.1";
605
+ }
606
+ }
607
+
608
+ // Check for port mapping configuration
609
+ const portMapping = config.worker?.tunnel?.portMapping || {};
610
+ if (Object.keys(portMapping).length > 0) {
611
+ console.log("🔀 Port mapping configured:");
612
+ for (const [containerPort, hostPort] of Object.entries(portMapping)) {
613
+ console.log(` Container port ${containerPort} → Host port ${hostPort}`);
614
+ }
615
+ }
616
+
617
+ const tunnelPorts = config.worker?.tunnel?.allowedPorts || [];
618
+ if (tunnelPorts.length === 0) {
619
+ console.warn(
620
+ "⚠️ No allowedPorts configured. Add worker.tunnel.allowedPorts to knowhow.json"
621
+ );
622
+ } else {
623
+ console.log(`🌐 Tunnel mode for ports: ${tunnelPorts.join(", ")}`);
624
+ }
625
+
626
+ // Extract tunnel domain from API_URL
627
+ function extractTunnelDomain(apiUrl: string): {
628
+ domain: string;
629
+ useHttps: boolean;
630
+ } {
631
+ try {
632
+ const url = new URL(apiUrl);
633
+ const useHttps = url.protocol === "https:";
634
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
635
+ return {
636
+ domain: `worker.${url.hostname}:${url.port || "80"}`,
637
+ useHttps,
638
+ };
639
+ }
640
+ return { domain: `worker.${url.hostname}`, useHttps };
641
+ } catch (err) {
642
+ console.error("Failed to parse API_URL for tunnel domain:", err);
643
+ return { domain: "worker.localhost:4000", useHttps: false };
644
+ }
645
+ }
646
+
647
+ let connected = false;
648
+ let tunnelHandler: TunnelHandler | null = null;
649
+ let lastJwt: string | null = null;
650
+ let unauthorizedJwt: string | null = null;
651
+
652
+ async function connectTunnel() {
653
+ const jwt = await loadJwt();
654
+ lastJwt = jwt;
655
+ console.log(`Connecting tunnel to ${API_URL}`);
656
+
657
+ const dir = process.cwd();
658
+ const homedir = os.homedir();
659
+ const hostname = process.env.WORKER_HOSTNAME || os.hostname();
660
+ const root =
661
+ process.env.WORKER_ROOT ||
662
+ (dir === homedir ? "~" : dir.replace(homedir, "~"));
663
+
664
+ const headers: Record<string, string> = {
665
+ Authorization: `Bearer ${jwt}`,
666
+ "User-Agent": `knowhow-tunnel/1.0.0/${hostname}`,
667
+ Root: root,
668
+ };
669
+
670
+ if (options?.share) {
671
+ headers.Shared = "true";
672
+ console.log("🔓 Tunnel shared with organization");
673
+ } else if (options?.unshare) {
674
+ headers.Shared = "false";
675
+ console.log("🔒 Tunnel is now private (unshared)");
676
+ } else {
677
+ console.log("🔒 Tunnel is private (only you can use it)");
678
+ }
679
+
680
+ const { domain: tunnelDomain, useHttps: tunnelUseHttps } =
681
+ extractTunnelDomain(API_URL);
682
+
683
+ const tunnelConnection = new WebSocket(`${API_URL}/ws/tunnel`, { headers });
684
+
685
+ tunnelConnection.on("open", () => {
686
+ console.log("🌐 Tunnel WebSocket connected");
687
+ connected = true;
688
+
689
+ const allowedPorts = config.worker?.tunnel?.allowedPorts || [];
690
+ const urlRewriter = (port: number, metadata?: any) => {
691
+ const workerId = metadata?.workerId;
692
+ const secret = metadata?.secret;
693
+ const subdomain = secret ? `${secret}-p${port}` : `${workerId}-p${port}`;
694
+ return `${subdomain}.${tunnelDomain}`;
695
+ };
696
+
697
+ const tunnelConfig = {
698
+ allowedPorts,
699
+ maxConcurrentStreams: config.worker?.tunnel?.maxConcurrentStreams || 50,
700
+ tunnelUseHttps,
701
+ localHost: tunnelLocalHost,
702
+ urlRewriter,
703
+ enableUrlRewriting: config.worker?.tunnel?.enableUrlRewriting !== false,
704
+ portMapping,
705
+ logLevel: "debug" as const,
706
+ };
707
+
708
+ tunnelHandler = createTunnelHandler(tunnelConnection, tunnelConfig);
709
+ console.log("🌐 Tunnel handler initialized");
710
+ console.log(tunnelConfig);
711
+ });
712
+
713
+ tunnelConnection.on("close", (code, reason) => {
714
+ console.log(`Tunnel WebSocket closed. Code: ${code}, Reason: ${reason.toString()}`);
715
+ if (code === 1008) {
716
+ unauthorizedJwt = lastJwt;
717
+ console.error("❌ Tunnel received Unauthorized (1008). The JWT may be expired.");
718
+ console.error(" Run 'knowhow login' to refresh your token, then restart.");
719
+ console.error(" Pausing reconnection until JWT changes...");
720
+ } else {
721
+ console.log("Tunnel connection will reconnect on next cycle...");
722
+ }
723
+ if (tunnelHandler) {
724
+ tunnelHandler.cleanup();
725
+ tunnelHandler = null;
726
+ }
727
+ connected = false;
728
+ });
729
+
730
+ tunnelConnection.on("error", (error) => {
731
+ console.error("Tunnel WebSocket error:", error);
732
+ connected = false;
733
+ });
734
+
735
+ return tunnelConnection;
736
+ }
737
+
738
+ while (true) {
739
+ if (!connected) {
740
+ if (unauthorizedJwt !== null) {
741
+ const currentJwt = await loadJwt().catch(() => null);
742
+ if (currentJwt === unauthorizedJwt) {
743
+ await wait(5000);
744
+ continue;
745
+ }
746
+ console.log("🔄 JWT has changed, attempting to reconnect tunnel...");
747
+ unauthorizedJwt = null;
748
+ }
749
+ console.log("Attempting to connect tunnel...");
750
+ await connectTunnel();
751
+ }
752
+ await wait(5000);
753
+ }
754
+ }
@@ -42,7 +42,7 @@ class FakeClient implements GenericClient {
42
42
  },
43
43
  ],
44
44
  model: options.model,
45
- usage: { total_tokens: 100 },
45
+ usage: { prompt_tokens: 50, completion_tokens: 50, total_tokens: 100 },
46
46
  };
47
47
  }
48
48
 
@@ -0,0 +1,202 @@
1
+ import { GenericAnthropicClient } from "../../src/clients/anthropic";
2
+
3
+ // We only need to test transformMessages, which doesn't require an API key
4
+ function createClient() {
5
+ return new GenericAnthropicClient("fake-key");
6
+ }
7
+
8
+ describe("GenericAnthropicClient.transformMessages", () => {
9
+ let client: GenericAnthropicClient;
10
+
11
+ beforeEach(() => {
12
+ client = createClient();
13
+ });
14
+
15
+ it("should handle a simple user message", () => {
16
+ const messages = [
17
+ { role: "user" as const, content: "Hello" },
18
+ ];
19
+ const result = client.transformMessages(messages);
20
+ expect(result).toHaveLength(1);
21
+ expect(result[0].role).toBe("user");
22
+ expect(result[0].content).toBe("Hello");
23
+ });
24
+
25
+ it("should filter out system messages", () => {
26
+ const messages = [
27
+ { role: "system" as const, content: "You are helpful" },
28
+ { role: "user" as const, content: "Hello" },
29
+ ];
30
+ const result = client.transformMessages(messages);
31
+ expect(result).toHaveLength(1);
32
+ expect(result[0].role).toBe("user");
33
+ });
34
+
35
+ it("should inject tool_use assistant block when processing tool result", () => {
36
+ // Simulates: assistant responds with tool_call (content: ""), then tool result comes back
37
+ const messages = [
38
+ { role: "user" as const, content: "Use a tool" },
39
+ {
40
+ role: "assistant" as const,
41
+ content: "",
42
+ tool_calls: [
43
+ {
44
+ id: "toolu_abc123",
45
+ type: "function" as const,
46
+ function: {
47
+ name: "listAvailableTools",
48
+ arguments: "{}",
49
+ },
50
+ },
51
+ ],
52
+ },
53
+ {
54
+ role: "tool" as const,
55
+ tool_call_id: "toolu_abc123",
56
+ name: "listAvailableTools",
57
+ content: '{"enabled": ["finalAnswer"], "disabled": []}',
58
+ },
59
+ ];
60
+
61
+ const result = client.transformMessages(messages);
62
+
63
+ // Should have: user msg, assistant tool_use block, user tool_result block
64
+ expect(result.length).toBeGreaterThanOrEqual(2);
65
+
66
+ // Find the assistant message with tool_use
67
+ const assistantMsg = result.find(
68
+ (m) =>
69
+ m.role === "assistant" &&
70
+ Array.isArray(m.content) &&
71
+ (m.content as any[]).some((c) => c.type === "tool_use")
72
+ );
73
+ expect(assistantMsg).toBeDefined();
74
+ const toolUseBlock = (assistantMsg!.content as any[]).find(
75
+ (c) => c.type === "tool_use"
76
+ );
77
+ expect(toolUseBlock.id).toBe("toolu_abc123");
78
+ expect(toolUseBlock.name).toBe("listAvailableTools");
79
+
80
+ // Find the user message with tool_result
81
+ const userToolResult = result.find(
82
+ (m) =>
83
+ m.role === "user" &&
84
+ Array.isArray(m.content) &&
85
+ (m.content as any[]).some((c) => c.type === "tool_result")
86
+ );
87
+ expect(userToolResult).toBeDefined();
88
+ const toolResultBlock = (userToolResult!.content as any[]).find(
89
+ (c) => c.type === "tool_result"
90
+ );
91
+ expect(toolResultBlock.tool_use_id).toBe("toolu_abc123");
92
+ });
93
+
94
+ it("should not have undefined tool_use_id when assistant message has empty content with tool_calls", () => {
95
+ // This is the failing scenario: assistant has content: "" (falsy) but has tool_calls
96
+ const messages = [
97
+ { role: "user" as const, content: "Use a tool" },
98
+ {
99
+ role: "assistant" as const,
100
+ content: "", // empty string - would be filtered by `msg.content` check
101
+ tool_calls: [
102
+ {
103
+ id: "toolu_abc123",
104
+ type: "function" as const,
105
+ function: {
106
+ name: "listAvailableTools",
107
+ arguments: "{}",
108
+ },
109
+ },
110
+ ],
111
+ },
112
+ {
113
+ role: "tool" as const,
114
+ tool_call_id: "toolu_abc123",
115
+ name: "listAvailableTools",
116
+ content: '{"enabled": ["finalAnswer"]}',
117
+ },
118
+ ];
119
+
120
+ const result = client.transformMessages(messages);
121
+
122
+ // Find the user message with tool_result - tool_use_id must NOT be undefined
123
+ const userToolResult = result.find(
124
+ (m) =>
125
+ m.role === "user" &&
126
+ Array.isArray(m.content) &&
127
+ (m.content as any[]).some((c) => c.type === "tool_result")
128
+ );
129
+ expect(userToolResult).toBeDefined();
130
+ const toolResultBlock = (userToolResult!.content as any[]).find(
131
+ (c) => c.type === "tool_result"
132
+ );
133
+ // This should be "toolu_abc123", NOT undefined
134
+ expect(toolResultBlock.tool_use_id).toBe("toolu_abc123");
135
+ expect(toolResultBlock.tool_use_id).not.toBeUndefined();
136
+ });
137
+
138
+ it("should handle multiple sequential tool calls", () => {
139
+ const messages = [
140
+ { role: "user" as const, content: "Do two things" },
141
+ {
142
+ role: "assistant" as const,
143
+ content: "",
144
+ tool_calls: [
145
+ {
146
+ id: "toolu_111",
147
+ type: "function" as const,
148
+ function: { name: "toolOne", arguments: "{}" },
149
+ },
150
+ ],
151
+ },
152
+ {
153
+ role: "tool" as const,
154
+ tool_call_id: "toolu_111",
155
+ name: "toolOne",
156
+ content: "result one",
157
+ },
158
+ {
159
+ role: "assistant" as const,
160
+ content: "",
161
+ tool_calls: [
162
+ {
163
+ id: "toolu_222",
164
+ type: "function" as const,
165
+ function: { name: "toolTwo", arguments: "{}" },
166
+ },
167
+ ],
168
+ },
169
+ {
170
+ role: "tool" as const,
171
+ tool_call_id: "toolu_222",
172
+ name: "toolTwo",
173
+ content: "result two",
174
+ },
175
+ ];
176
+
177
+ const result = client.transformMessages(messages);
178
+
179
+ // Both tool results should have correct tool_use_ids
180
+ const toolResults = result
181
+ .filter((m) => m.role === "user" && Array.isArray(m.content))
182
+ .flatMap((m) => (m.content as any[]).filter((c) => c.type === "tool_result"));
183
+
184
+ expect(toolResults).toHaveLength(2);
185
+ const ids = toolResults.map((r) => r.tool_use_id);
186
+ expect(ids).toContain("toolu_111");
187
+ expect(ids).toContain("toolu_222");
188
+ expect(ids).not.toContain(undefined);
189
+ });
190
+
191
+ it("should not crash when response is undefined (Cannot use in operator bug)", () => {
192
+ // Test that the base agent undefined response check doesn't throw
193
+ // This tests the guard we added to base.ts
194
+ const undefinedLike = undefined as any;
195
+ // Should not throw "Cannot use 'in' operator to search for 'response' in undefined"
196
+ expect(() => {
197
+ if (undefinedLike != null && "response" in undefinedLike) {
198
+ // This should not be reached
199
+ }
200
+ }).not.toThrow();
201
+ });
202
+ });
@@ -5,6 +5,9 @@
5
5
  * Models that are image-only, video-only, TTS, transcription, realtime, or live streaming
6
6
  * are exempt from text pricing requirements — they should have their own pricing entries
7
7
  * in the appropriate pricing tables (image, video, audio, etc.).
8
+ *
9
+ * Also verifies that every model in Models.* and EmbeddingModels.* has a catalog entry,
10
+ * ensuring the catalog stays in sync with the model definitions.
8
11
  */
9
12
 
10
13
  import {
@@ -27,6 +30,7 @@ import {
27
30
  GeminiTextPricing,
28
31
  AnthropicTextPricing,
29
32
  XaiTextPricing,
33
+ ALL_MODEL_CATALOG,
30
34
  XaiImagePricing,
31
35
  XaiVideoPricing,
32
36
  } from "../../src/clients/pricing";
@@ -140,5 +144,38 @@ describe("Model Pricing Coverage", () => {
140
144
  expect(entry).toBeDefined();
141
145
  });
142
146
  }
147
+
148
+ describe("Model Catalog Coverage", () => {
149
+ /**
150
+ * Every model defined in Models.* and EmbeddingModels.* must have an entry
151
+ * in ALL_MODEL_CATALOG. This ensures the catalog stays in sync and is the
152
+ * single source of truth for model metadata and pricing.
153
+ */
154
+ const catalogIds = new Set(ALL_MODEL_CATALOG.map((m) => m.id));
155
+
156
+ describe("All Models.* entries are in the catalog", () => {
157
+ for (const [provider, providerModels] of Object.entries(Models)) {
158
+ for (const [modelKey, modelId] of Object.entries(
159
+ providerModels as Record<string, string>
160
+ )) {
161
+ it(`Models.${provider}.${modelKey} (${modelId}) is in ALL_MODEL_CATALOG`, () => {
162
+ expect(catalogIds.has(modelId)).toBe(true);
163
+ });
164
+ }
165
+ }
166
+ });
167
+
168
+ describe("All EmbeddingModels.* entries are in the catalog", () => {
169
+ for (const [provider, providerModels] of Object.entries(EmbeddingModels)) {
170
+ for (const [modelKey, modelId] of Object.entries(
171
+ providerModels as Record<string, string>
172
+ )) {
173
+ it(`EmbeddingModels.${provider}.${modelKey} (${modelId}) is in ALL_MODEL_CATALOG`, () => {
174
+ expect(catalogIds.has(modelId)).toBe(true);
175
+ });
176
+ }
177
+ }
178
+ });
179
+ });
143
180
  });
144
181
  });