@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.
- package/CONFIG.md +8 -5
- package/package.json +3 -2
- package/scripts/check-model-pricing.ts +509 -0
- package/scripts/compare-openrouter-coverage.ts +576 -0
- package/src/agents/base/base.ts +169 -5
- package/src/agents/tools/execCommand.ts +4 -0
- package/src/agents/tools/executeScript/definition.ts +1 -1
- package/src/agents/tools/index.ts +0 -1
- package/src/agents/tools/list.ts +3 -43
- package/src/agents/tools/writeFile.ts +1 -1
- package/src/auth/browserLogin.ts +9 -4
- package/src/chat/modules/RemoteSyncModule.ts +3 -0
- package/src/cli.ts +31 -1
- package/src/clients/anthropic.ts +8 -2
- package/src/clients/cerebras.ts +10 -0
- package/src/clients/contextLimits.ts +7 -2
- package/src/clients/copilot.ts +23 -0
- package/src/clients/deepseek.ts +16 -0
- package/src/clients/fireworks.ts +15 -0
- package/src/clients/gemini.ts +59 -4
- package/src/clients/github.ts +16 -0
- package/src/clients/groq.ts +15 -0
- package/src/clients/http.ts +194 -6
- package/src/clients/index.ts +116 -4
- package/src/clients/llama.ts +16 -0
- package/src/clients/mistral.ts +16 -0
- package/src/clients/nvidia.ts +16 -0
- package/src/clients/openai.ts +53 -12
- package/src/clients/openrouter.ts +17 -0
- package/src/clients/pricing/anthropic.ts +105 -78
- package/src/clients/pricing/cerebras.ts +11 -0
- package/src/clients/pricing/copilot.ts +60 -0
- package/src/clients/pricing/deepseek.ts +15 -0
- package/src/clients/pricing/fireworks.ts +32 -0
- package/src/clients/pricing/github.ts +69 -0
- package/src/clients/pricing/google.ts +245 -206
- package/src/clients/pricing/groq.ts +56 -0
- package/src/clients/pricing/index.ts +42 -5
- package/src/clients/pricing/llama.ts +18 -0
- package/src/clients/pricing/mistral.ts +34 -0
- package/src/clients/pricing/models.ts +7 -236
- package/src/clients/pricing/nvidia.ts +102 -0
- package/src/clients/pricing/openai.ts +348 -171
- package/src/clients/pricing/openrouter.ts +36 -0
- package/src/clients/pricing/types.ts +83 -2
- package/src/clients/pricing/xai.ts +121 -65
- package/src/clients/types.ts +28 -1
- package/src/clients/xai.ts +161 -1
- package/src/fileSync.ts +8 -2
- package/src/login.ts +11 -3
- package/src/services/AgentSyncFs.ts +36 -12
- package/src/services/KnowhowClient.ts +11 -0
- package/src/services/LazyToolsService.ts +6 -0
- package/src/services/S3.ts +0 -7
- package/src/services/modules/index.ts +11 -2
- package/src/types.ts +56 -279
- package/src/worker.ts +174 -0
- package/tests/clients/AIClient.test.ts +1 -1
- package/tests/clients/anthropic.test.ts +202 -0
- package/tests/clients/pricing.test.ts +37 -0
- package/tests/manual/clients/completions.json +838 -226
- package/tests/manual/clients/completions.test.ts +46 -31
- package/ts_build/package.json +3 -2
- package/ts_build/src/agents/base/base.d.ts +18 -1
- package/ts_build/src/agents/base/base.js +111 -4
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.js +3 -0
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/executeScript/definition.js +1 -1
- package/ts_build/src/agents/tools/executeScript/definition.js.map +1 -1
- package/ts_build/src/agents/tools/index.d.ts +0 -1
- package/ts_build/src/agents/tools/index.js +0 -1
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +3 -38
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/visionTool.d.ts +1 -1
- package/ts_build/src/agents/tools/writeFile.js +1 -1
- package/ts_build/src/agents/tools/writeFile.js.map +1 -1
- package/ts_build/src/ai.d.ts +1 -1
- package/ts_build/src/auth/browserLogin.d.ts +2 -1
- package/ts_build/src/auth/browserLogin.js +10 -3
- package/ts_build/src/auth/browserLogin.js.map +1 -1
- package/ts_build/src/chat/modules/RemoteSyncModule.js +1 -0
- package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
- package/ts_build/src/cli.js +19 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +1 -82
- package/ts_build/src/clients/anthropic.js +8 -2
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/cerebras.d.ts +4 -0
- package/ts_build/src/clients/cerebras.js +14 -0
- package/ts_build/src/clients/cerebras.js.map +1 -0
- package/ts_build/src/clients/contextLimits.js +7 -2
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/copilot.d.ts +4 -0
- package/ts_build/src/clients/copilot.js +15 -0
- package/ts_build/src/clients/copilot.js.map +1 -0
- package/ts_build/src/clients/deepseek.d.ts +4 -0
- package/ts_build/src/clients/deepseek.js +15 -0
- package/ts_build/src/clients/deepseek.js.map +1 -0
- package/ts_build/src/clients/fireworks.d.ts +4 -0
- package/ts_build/src/clients/fireworks.js +15 -0
- package/ts_build/src/clients/fireworks.js.map +1 -0
- package/ts_build/src/clients/gemini.d.ts +1 -0
- package/ts_build/src/clients/gemini.js +38 -2
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/github.d.ts +4 -0
- package/ts_build/src/clients/github.js +15 -0
- package/ts_build/src/clients/github.js.map +1 -0
- package/ts_build/src/clients/groq.d.ts +4 -0
- package/ts_build/src/clients/groq.js +15 -0
- package/ts_build/src/clients/groq.js.map +1 -0
- package/ts_build/src/clients/http.d.ts +22 -1
- package/ts_build/src/clients/http.js +135 -7
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +14 -0
- package/ts_build/src/clients/index.js +94 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/llama.d.ts +4 -0
- package/ts_build/src/clients/llama.js +15 -0
- package/ts_build/src/clients/llama.js.map +1 -0
- package/ts_build/src/clients/mistral.d.ts +4 -0
- package/ts_build/src/clients/mistral.js +15 -0
- package/ts_build/src/clients/mistral.js.map +1 -0
- package/ts_build/src/clients/nvidia.d.ts +4 -0
- package/ts_build/src/clients/nvidia.js +15 -0
- package/ts_build/src/clients/nvidia.js.map +1 -0
- package/ts_build/src/clients/openai.d.ts +4 -206
- package/ts_build/src/clients/openai.js +38 -10
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/openrouter.d.ts +4 -0
- package/ts_build/src/clients/openrouter.js +15 -0
- package/ts_build/src/clients/openrouter.js.map +1 -0
- package/ts_build/src/clients/pricing/anthropic.d.ts +26 -78
- package/ts_build/src/clients/pricing/anthropic.js +75 -78
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/clients/pricing/cerebras.d.ts +4 -0
- package/ts_build/src/clients/pricing/cerebras.js +11 -0
- package/ts_build/src/clients/pricing/cerebras.js.map +1 -0
- package/ts_build/src/clients/pricing/copilot.d.ts +5 -0
- package/ts_build/src/clients/pricing/copilot.js +35 -0
- package/ts_build/src/clients/pricing/copilot.js.map +1 -0
- package/ts_build/src/clients/pricing/deepseek.d.ts +5 -0
- package/ts_build/src/clients/pricing/deepseek.js +10 -0
- package/ts_build/src/clients/pricing/deepseek.js.map +1 -0
- package/ts_build/src/clients/pricing/fireworks.d.ts +5 -0
- package/ts_build/src/clients/pricing/fireworks.js +21 -0
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -0
- package/ts_build/src/clients/pricing/github.d.ts +4 -0
- package/ts_build/src/clients/pricing/github.js +58 -0
- package/ts_build/src/clients/pricing/github.js.map +1 -0
- package/ts_build/src/clients/pricing/google.d.ts +59 -6
- package/ts_build/src/clients/pricing/google.js +214 -167
- package/ts_build/src/clients/pricing/google.js.map +1 -1
- package/ts_build/src/clients/pricing/groq.d.ts +5 -0
- package/ts_build/src/clients/pricing/groq.js +41 -0
- package/ts_build/src/clients/pricing/groq.js.map +1 -0
- package/ts_build/src/clients/pricing/index.d.ts +16 -5
- package/ts_build/src/clients/pricing/index.js +62 -7
- package/ts_build/src/clients/pricing/index.js.map +1 -1
- package/ts_build/src/clients/pricing/llama.d.ts +4 -0
- package/ts_build/src/clients/pricing/llama.js +14 -0
- package/ts_build/src/clients/pricing/llama.js.map +1 -0
- package/ts_build/src/clients/pricing/mistral.d.ts +5 -0
- package/ts_build/src/clients/pricing/mistral.js +23 -0
- package/ts_build/src/clients/pricing/mistral.js.map +1 -0
- package/ts_build/src/clients/pricing/models.d.ts +5 -4
- package/ts_build/src/clients/pricing/models.js +8 -162
- package/ts_build/src/clients/pricing/models.js.map +1 -1
- package/ts_build/src/clients/pricing/nvidia.d.ts +8 -0
- package/ts_build/src/clients/pricing/nvidia.js +96 -0
- package/ts_build/src/clients/pricing/nvidia.js.map +1 -0
- package/ts_build/src/clients/pricing/openai.d.ts +86 -197
- package/ts_build/src/clients/pricing/openai.js +295 -168
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/openrouter.d.ts +4 -0
- package/ts_build/src/clients/pricing/openrouter.js +29 -0
- package/ts_build/src/clients/pricing/openrouter.js.map +1 -0
- package/ts_build/src/clients/pricing/types.d.ts +27 -2
- package/ts_build/src/clients/pricing/types.js +46 -0
- package/ts_build/src/clients/pricing/types.js.map +1 -1
- package/ts_build/src/clients/pricing/xai.d.ts +37 -57
- package/ts_build/src/clients/pricing/xai.js +92 -59
- package/ts_build/src/clients/pricing/xai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +12 -1
- package/ts_build/src/clients/xai.d.ts +2 -62
- package/ts_build/src/clients/xai.js +132 -1
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/fileSync.js +7 -2
- package/ts_build/src/fileSync.js.map +1 -1
- package/ts_build/src/login.js +8 -2
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.js +1 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -0
- package/ts_build/src/services/KnowhowClient.js +7 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +1 -0
- package/ts_build/src/services/LazyToolsService.js +3 -0
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/S3.js +0 -7
- package/ts_build/src/services/S3.js.map +1 -1
- package/ts_build/src/services/modules/index.js +41 -1
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +163 -124
- package/ts_build/src/types.js +33 -213
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/worker.d.ts +4 -0
- package/ts_build/src/worker.js +140 -0
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/clients/AIClient.test.js +1 -1
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/clients/anthropic.test.d.ts +1 -0
- package/ts_build/tests/clients/anthropic.test.js +159 -0
- package/ts_build/tests/clients/anthropic.test.js.map +1 -0
- package/ts_build/tests/clients/pricing.test.js +21 -0
- package/ts_build/tests/clients/pricing.test.js.map +1 -1
- package/ts_build/tests/manual/clients/completions.test.js +27 -24
- 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
|
+
}
|
|
@@ -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
|
});
|