formagent-sdk 0.2.0 → 0.3.3
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/README.md +2 -0
- package/dist/cli/index.js +1546 -134
- package/dist/index.js +1195 -34
- package/docs/README.md +126 -0
- package/docs/api-reference.md +677 -0
- package/docs/getting-started.md +273 -0
- package/docs/mcp-servers.md +465 -0
- package/docs/session-storage.md +320 -0
- package/docs/tools.md +501 -0
- package/package.json +3 -1
package/dist/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as readline from "node:readline";
|
|
|
7
7
|
import { homedir as homedir4 } from "node:os";
|
|
8
8
|
import { join as join8 } from "node:path";
|
|
9
9
|
import { existsSync as existsSync10 } from "node:fs";
|
|
10
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
10
11
|
|
|
11
12
|
// src/utils/id.ts
|
|
12
13
|
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -1678,8 +1679,18 @@ class SessionImpl {
|
|
|
1678
1679
|
async executeToolCall(block, abortSignal) {
|
|
1679
1680
|
let toolInput = block.input;
|
|
1680
1681
|
let systemMessage;
|
|
1682
|
+
let tool = this.tools.get(block.name);
|
|
1683
|
+
let effectiveToolName = block.name;
|
|
1684
|
+
if (!tool && this.enableToolRepair) {
|
|
1685
|
+
const lowerName = block.name.toLowerCase();
|
|
1686
|
+
const originalName = this.toolNameLookup.get(lowerName);
|
|
1687
|
+
if (originalName) {
|
|
1688
|
+
tool = this.tools.get(originalName);
|
|
1689
|
+
effectiveToolName = originalName;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1681
1692
|
if (this.hooksManager) {
|
|
1682
|
-
const preResult = await this.hooksManager.runPreToolUse(
|
|
1693
|
+
const preResult = await this.hooksManager.runPreToolUse(effectiveToolName, block.input, block.id, abortSignal);
|
|
1683
1694
|
if (!preResult.continue) {
|
|
1684
1695
|
return {
|
|
1685
1696
|
type: "tool_result",
|
|
@@ -1693,7 +1704,7 @@ class SessionImpl {
|
|
|
1693
1704
|
return {
|
|
1694
1705
|
type: "tool_result",
|
|
1695
1706
|
tool_use_id: block.id,
|
|
1696
|
-
content: preResult.reason ?? `Tool "${
|
|
1707
|
+
content: preResult.reason ?? `Tool "${effectiveToolName}" was denied by hook`,
|
|
1697
1708
|
is_error: true,
|
|
1698
1709
|
_hookSystemMessage: preResult.systemMessage
|
|
1699
1710
|
};
|
|
@@ -1703,16 +1714,6 @@ class SessionImpl {
|
|
|
1703
1714
|
}
|
|
1704
1715
|
systemMessage = preResult.systemMessage;
|
|
1705
1716
|
}
|
|
1706
|
-
let tool = this.tools.get(block.name);
|
|
1707
|
-
let effectiveToolName = block.name;
|
|
1708
|
-
if (!tool && this.enableToolRepair) {
|
|
1709
|
-
const lowerName = block.name.toLowerCase();
|
|
1710
|
-
const originalName = this.toolNameLookup.get(lowerName);
|
|
1711
|
-
if (originalName) {
|
|
1712
|
-
tool = this.tools.get(originalName);
|
|
1713
|
-
effectiveToolName = originalName;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
1717
|
if (!tool) {
|
|
1717
1718
|
const availableTools = Array.from(this.tools.keys()).slice(0, 10);
|
|
1718
1719
|
const suffix = this.tools.size > 10 ? ` (and ${this.tools.size - 10} more)` : "";
|
|
@@ -1734,7 +1735,8 @@ class SessionImpl {
|
|
|
1734
1735
|
const toolResult = await tool.execute(toolInput, context);
|
|
1735
1736
|
let content = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content);
|
|
1736
1737
|
if (needsTruncation(content)) {
|
|
1737
|
-
|
|
1738
|
+
const truncationConfig = this.config.tempDir ? { tempDir: this.config.tempDir } : undefined;
|
|
1739
|
+
content = await truncateToolOutput(content, truncationConfig);
|
|
1738
1740
|
}
|
|
1739
1741
|
toolResponse = toolResult;
|
|
1740
1742
|
result = {
|
|
@@ -1753,7 +1755,7 @@ class SessionImpl {
|
|
|
1753
1755
|
};
|
|
1754
1756
|
}
|
|
1755
1757
|
if (this.hooksManager) {
|
|
1756
|
-
const postResult = await this.hooksManager.runPostToolUse(
|
|
1758
|
+
const postResult = await this.hooksManager.runPostToolUse(effectiveToolName, toolInput, toolResponse, block.id, abortSignal);
|
|
1757
1759
|
if (postResult.systemMessage) {
|
|
1758
1760
|
systemMessage = postResult.systemMessage;
|
|
1759
1761
|
}
|
|
@@ -2395,7 +2397,7 @@ class OpenAIProvider {
|
|
|
2395
2397
|
}
|
|
2396
2398
|
this.config = {
|
|
2397
2399
|
apiKey,
|
|
2398
|
-
baseUrl: config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
|
|
2400
|
+
baseUrl: this.normalizeBaseUrl(config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"),
|
|
2399
2401
|
organization: config.organization,
|
|
2400
2402
|
defaultMaxTokens: config.defaultMaxTokens ?? 4096
|
|
2401
2403
|
};
|
|
@@ -2404,8 +2406,23 @@ class OpenAIProvider {
|
|
|
2404
2406
|
return this.supportedModels.some((pattern) => pattern.test(model));
|
|
2405
2407
|
}
|
|
2406
2408
|
async complete(request) {
|
|
2409
|
+
if (this.usesResponsesApi(request.config.model)) {
|
|
2410
|
+
const openaiRequest2 = this.buildResponsesRequest(request, false);
|
|
2411
|
+
const response2 = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2412
|
+
method: "POST",
|
|
2413
|
+
headers: this.getHeaders(),
|
|
2414
|
+
body: JSON.stringify(openaiRequest2),
|
|
2415
|
+
signal: request.abortSignal
|
|
2416
|
+
});
|
|
2417
|
+
if (!response2.ok) {
|
|
2418
|
+
const error = await response2.text();
|
|
2419
|
+
throw new Error(`OpenAI API error: ${response2.status} ${error}`);
|
|
2420
|
+
}
|
|
2421
|
+
const data2 = await response2.json();
|
|
2422
|
+
return this.convertResponsesResponse(data2);
|
|
2423
|
+
}
|
|
2407
2424
|
const openaiRequest = this.buildRequest(request, false);
|
|
2408
|
-
|
|
2425
|
+
let response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
2409
2426
|
method: "POST",
|
|
2410
2427
|
headers: this.getHeaders(),
|
|
2411
2428
|
body: JSON.stringify(openaiRequest),
|
|
@@ -2413,14 +2430,43 @@ class OpenAIProvider {
|
|
|
2413
2430
|
});
|
|
2414
2431
|
if (!response.ok) {
|
|
2415
2432
|
const error = await response.text();
|
|
2433
|
+
if (this.shouldFallbackToResponses(response.status, error)) {
|
|
2434
|
+
const fallbackRequest = this.buildResponsesRequest(request, false);
|
|
2435
|
+
response = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2436
|
+
method: "POST",
|
|
2437
|
+
headers: this.getHeaders(),
|
|
2438
|
+
body: JSON.stringify(fallbackRequest),
|
|
2439
|
+
signal: request.abortSignal
|
|
2440
|
+
});
|
|
2441
|
+
if (!response.ok) {
|
|
2442
|
+
const fallbackError = await response.text();
|
|
2443
|
+
throw new Error(`OpenAI API error: ${response.status} ${fallbackError}`);
|
|
2444
|
+
}
|
|
2445
|
+
const data2 = await response.json();
|
|
2446
|
+
return this.convertResponsesResponse(data2);
|
|
2447
|
+
}
|
|
2416
2448
|
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
2417
2449
|
}
|
|
2418
2450
|
const data = await response.json();
|
|
2419
2451
|
return this.convertResponse(data);
|
|
2420
2452
|
}
|
|
2421
2453
|
async stream(request, options) {
|
|
2454
|
+
if (this.usesResponsesApi(request.config.model)) {
|
|
2455
|
+
const openaiRequest2 = this.buildResponsesRequest(request, true);
|
|
2456
|
+
const response2 = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2457
|
+
method: "POST",
|
|
2458
|
+
headers: this.getHeaders(),
|
|
2459
|
+
body: JSON.stringify(openaiRequest2),
|
|
2460
|
+
signal: request.abortSignal
|
|
2461
|
+
});
|
|
2462
|
+
if (!response2.ok) {
|
|
2463
|
+
const error = await response2.text();
|
|
2464
|
+
throw new Error(`OpenAI API error: ${response2.status} ${error}`);
|
|
2465
|
+
}
|
|
2466
|
+
return this.createResponsesStreamIterator(response2.body, options);
|
|
2467
|
+
}
|
|
2422
2468
|
const openaiRequest = this.buildRequest(request, true);
|
|
2423
|
-
|
|
2469
|
+
let response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
2424
2470
|
method: "POST",
|
|
2425
2471
|
headers: this.getHeaders(),
|
|
2426
2472
|
body: JSON.stringify(openaiRequest),
|
|
@@ -2428,6 +2474,20 @@ class OpenAIProvider {
|
|
|
2428
2474
|
});
|
|
2429
2475
|
if (!response.ok) {
|
|
2430
2476
|
const error = await response.text();
|
|
2477
|
+
if (this.shouldFallbackToResponses(response.status, error)) {
|
|
2478
|
+
const fallbackRequest = this.buildResponsesRequest(request, true);
|
|
2479
|
+
response = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2480
|
+
method: "POST",
|
|
2481
|
+
headers: this.getHeaders(),
|
|
2482
|
+
body: JSON.stringify(fallbackRequest),
|
|
2483
|
+
signal: request.abortSignal
|
|
2484
|
+
});
|
|
2485
|
+
if (!response.ok) {
|
|
2486
|
+
const fallbackError = await response.text();
|
|
2487
|
+
throw new Error(`OpenAI API error: ${response.status} ${fallbackError}`);
|
|
2488
|
+
}
|
|
2489
|
+
return this.createResponsesStreamIterator(response.body, options);
|
|
2490
|
+
}
|
|
2431
2491
|
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
2432
2492
|
}
|
|
2433
2493
|
return this.createStreamIterator(response.body, options);
|
|
@@ -2435,10 +2495,10 @@ class OpenAIProvider {
|
|
|
2435
2495
|
buildRequest(request, stream) {
|
|
2436
2496
|
const messages = this.convertMessages(request.messages, request.systemPrompt);
|
|
2437
2497
|
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
2438
|
-
|
|
2498
|
+
const maxTokens = request.config.maxTokens ?? this.config.defaultMaxTokens;
|
|
2499
|
+
const openaiRequest = {
|
|
2439
2500
|
model: request.config.model,
|
|
2440
2501
|
messages,
|
|
2441
|
-
max_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
2442
2502
|
temperature: request.config.temperature,
|
|
2443
2503
|
top_p: request.config.topP,
|
|
2444
2504
|
stop: request.config.stopSequences,
|
|
@@ -2446,6 +2506,162 @@ class OpenAIProvider {
|
|
|
2446
2506
|
stream_options: stream ? { include_usage: true } : undefined,
|
|
2447
2507
|
tools
|
|
2448
2508
|
};
|
|
2509
|
+
if (this.usesMaxCompletionTokens(request.config.model)) {
|
|
2510
|
+
openaiRequest.max_completion_tokens = maxTokens;
|
|
2511
|
+
} else {
|
|
2512
|
+
openaiRequest.max_tokens = maxTokens;
|
|
2513
|
+
}
|
|
2514
|
+
return openaiRequest;
|
|
2515
|
+
}
|
|
2516
|
+
buildResponsesRequest(request, stream) {
|
|
2517
|
+
const input = this.convertResponsesInput(request.messages, request.systemPrompt);
|
|
2518
|
+
const tools = request.tools ? this.convertResponsesTools(request.tools) : undefined;
|
|
2519
|
+
return {
|
|
2520
|
+
model: request.config.model,
|
|
2521
|
+
input,
|
|
2522
|
+
max_output_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
2523
|
+
temperature: request.config.temperature,
|
|
2524
|
+
top_p: request.config.topP,
|
|
2525
|
+
stop: request.config.stopSequences,
|
|
2526
|
+
stream,
|
|
2527
|
+
tools
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
usesMaxCompletionTokens(model) {
|
|
2531
|
+
return /^gpt-5/.test(model) || /^o1/.test(model);
|
|
2532
|
+
}
|
|
2533
|
+
usesResponsesApi(model) {
|
|
2534
|
+
return /^gpt-5/.test(model) || /^o1/.test(model);
|
|
2535
|
+
}
|
|
2536
|
+
shouldFallbackToResponses(status, errorText) {
|
|
2537
|
+
if (status !== 404) {
|
|
2538
|
+
return false;
|
|
2539
|
+
}
|
|
2540
|
+
const normalized = errorText.toLowerCase();
|
|
2541
|
+
return normalized.includes("/chat/completions") && normalized.includes("not found");
|
|
2542
|
+
}
|
|
2543
|
+
convertResponsesInput(messages, systemPrompt) {
|
|
2544
|
+
const input = [];
|
|
2545
|
+
if (systemPrompt) {
|
|
2546
|
+
input.push({ role: "system", content: systemPrompt });
|
|
2547
|
+
}
|
|
2548
|
+
for (const msg of messages) {
|
|
2549
|
+
if (msg.role === "system") {
|
|
2550
|
+
input.push({
|
|
2551
|
+
role: "system",
|
|
2552
|
+
content: typeof msg.content === "string" ? msg.content : ""
|
|
2553
|
+
});
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
if (typeof msg.content === "string") {
|
|
2557
|
+
if (msg.role === "user") {
|
|
2558
|
+
input.push({
|
|
2559
|
+
role: "user",
|
|
2560
|
+
content: [{ type: "input_text", text: msg.content }]
|
|
2561
|
+
});
|
|
2562
|
+
} else {
|
|
2563
|
+
input.push({
|
|
2564
|
+
role: "assistant",
|
|
2565
|
+
content: [{ type: "output_text", text: msg.content }]
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
const userContent = [];
|
|
2571
|
+
const assistantContent = [];
|
|
2572
|
+
for (const block of msg.content) {
|
|
2573
|
+
if (block.type === "text") {
|
|
2574
|
+
if (msg.role === "user") {
|
|
2575
|
+
userContent.push({ type: "input_text", text: block.text });
|
|
2576
|
+
} else if (msg.role === "assistant") {
|
|
2577
|
+
assistantContent.push({ type: "output_text", text: block.text });
|
|
2578
|
+
}
|
|
2579
|
+
} else if (block.type === "image" && msg.role === "user") {
|
|
2580
|
+
if (block.source.type === "base64") {
|
|
2581
|
+
userContent.push({
|
|
2582
|
+
type: "input_image",
|
|
2583
|
+
image_url: `data:${block.source.media_type};base64,${block.source.data}`
|
|
2584
|
+
});
|
|
2585
|
+
} else if (block.source.type === "url") {
|
|
2586
|
+
userContent.push({
|
|
2587
|
+
type: "input_image",
|
|
2588
|
+
image_url: block.source.url
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
} else if (block.type === "tool_use") {
|
|
2592
|
+
input.push({
|
|
2593
|
+
type: "function_call",
|
|
2594
|
+
call_id: block.id,
|
|
2595
|
+
name: block.name,
|
|
2596
|
+
arguments: JSON.stringify(block.input)
|
|
2597
|
+
});
|
|
2598
|
+
} else if (block.type === "tool_result") {
|
|
2599
|
+
input.push({
|
|
2600
|
+
type: "function_call_output",
|
|
2601
|
+
call_id: block.tool_use_id,
|
|
2602
|
+
output: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
if (msg.role === "user" && userContent.length > 0) {
|
|
2607
|
+
input.push({ role: "user", content: userContent });
|
|
2608
|
+
} else if (msg.role === "assistant" && assistantContent.length > 0) {
|
|
2609
|
+
input.push({ role: "assistant", content: assistantContent });
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return input;
|
|
2613
|
+
}
|
|
2614
|
+
convertResponsesResponse(data) {
|
|
2615
|
+
const content = [];
|
|
2616
|
+
for (const item of data.output ?? []) {
|
|
2617
|
+
if (item.type === "message" && item.content) {
|
|
2618
|
+
for (const part of item.content) {
|
|
2619
|
+
if (part.type === "output_text") {
|
|
2620
|
+
content.push({ type: "text", text: part.text });
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
} else if (item.type === "function_call" && item.call_id && item.name) {
|
|
2624
|
+
content.push({
|
|
2625
|
+
type: "tool_use",
|
|
2626
|
+
id: item.call_id,
|
|
2627
|
+
name: item.name,
|
|
2628
|
+
input: item.arguments ? JSON.parse(item.arguments) : {}
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return {
|
|
2633
|
+
id: data.id,
|
|
2634
|
+
model: data.model,
|
|
2635
|
+
content,
|
|
2636
|
+
stopReason: "end_turn",
|
|
2637
|
+
stopSequence: null,
|
|
2638
|
+
usage: {
|
|
2639
|
+
input_tokens: data.usage?.input_tokens ?? 0,
|
|
2640
|
+
output_tokens: data.usage?.output_tokens ?? 0
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
normalizeBaseUrl(baseUrl) {
|
|
2645
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
2646
|
+
try {
|
|
2647
|
+
const url = new URL(trimmed);
|
|
2648
|
+
const path2 = url.pathname.replace(/\/+$/, "");
|
|
2649
|
+
if (path2 === "" || path2 === "/") {
|
|
2650
|
+
url.pathname = "/v1";
|
|
2651
|
+
return url.toString().replace(/\/+$/, "");
|
|
2652
|
+
}
|
|
2653
|
+
if (path2.endsWith("/openai")) {
|
|
2654
|
+
url.pathname = `${path2}/v1`;
|
|
2655
|
+
return url.toString().replace(/\/+$/, "");
|
|
2656
|
+
}
|
|
2657
|
+
if (!/\/v\d/.test(path2)) {
|
|
2658
|
+
url.pathname = `${path2}/v1`;
|
|
2659
|
+
return url.toString().replace(/\/+$/, "");
|
|
2660
|
+
}
|
|
2661
|
+
return url.toString().replace(/\/+$/, "");
|
|
2662
|
+
} catch {
|
|
2663
|
+
return trimmed;
|
|
2664
|
+
}
|
|
2449
2665
|
}
|
|
2450
2666
|
convertMessages(messages, systemPrompt) {
|
|
2451
2667
|
const result = [];
|
|
@@ -2528,6 +2744,14 @@ class OpenAIProvider {
|
|
|
2528
2744
|
}
|
|
2529
2745
|
}));
|
|
2530
2746
|
}
|
|
2747
|
+
convertResponsesTools(tools) {
|
|
2748
|
+
return tools.map((tool) => ({
|
|
2749
|
+
type: "function",
|
|
2750
|
+
name: tool.name,
|
|
2751
|
+
description: tool.description,
|
|
2752
|
+
parameters: tool.inputSchema
|
|
2753
|
+
}));
|
|
2754
|
+
}
|
|
2531
2755
|
convertResponse(data) {
|
|
2532
2756
|
const choice = data.choices[0];
|
|
2533
2757
|
const content = [];
|
|
@@ -2640,115 +2864,924 @@ class OpenAIProvider {
|
|
|
2640
2864
|
const textEvent = {
|
|
2641
2865
|
type: "content_block_delta",
|
|
2642
2866
|
index: 0,
|
|
2643
|
-
delta: {
|
|
2644
|
-
type: "text_delta",
|
|
2645
|
-
text: delta.content
|
|
2646
|
-
}
|
|
2867
|
+
delta: {
|
|
2868
|
+
type: "text_delta",
|
|
2869
|
+
text: delta.content
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
options?.onText?.(delta.content);
|
|
2873
|
+
options?.onEvent?.(textEvent);
|
|
2874
|
+
yield textEvent;
|
|
2875
|
+
}
|
|
2876
|
+
if (delta?.tool_calls) {
|
|
2877
|
+
for (const tc of delta.tool_calls) {
|
|
2878
|
+
const index = tc.index;
|
|
2879
|
+
const blockIndex = 1 + index;
|
|
2880
|
+
if (!toolCalls.has(index)) {
|
|
2881
|
+
toolCalls.set(index, {
|
|
2882
|
+
id: tc.id || "",
|
|
2883
|
+
name: tc.function?.name || "",
|
|
2884
|
+
arguments: tc.function?.arguments || ""
|
|
2885
|
+
});
|
|
2886
|
+
const startEvent = {
|
|
2887
|
+
type: "content_block_start",
|
|
2888
|
+
index: blockIndex,
|
|
2889
|
+
content_block: {
|
|
2890
|
+
type: "tool_use",
|
|
2891
|
+
id: tc.id || "",
|
|
2892
|
+
name: tc.function?.name || "",
|
|
2893
|
+
input: {}
|
|
2894
|
+
}
|
|
2895
|
+
};
|
|
2896
|
+
options?.onEvent?.(startEvent);
|
|
2897
|
+
yield startEvent;
|
|
2898
|
+
} else {
|
|
2899
|
+
const existing = toolCalls.get(index);
|
|
2900
|
+
if (tc.id)
|
|
2901
|
+
existing.id = tc.id;
|
|
2902
|
+
if (tc.function?.name)
|
|
2903
|
+
existing.name = tc.function.name;
|
|
2904
|
+
if (tc.function?.arguments)
|
|
2905
|
+
existing.arguments += tc.function.arguments;
|
|
2906
|
+
}
|
|
2907
|
+
if (tc.function?.arguments) {
|
|
2908
|
+
const deltaEvent = {
|
|
2909
|
+
type: "content_block_delta",
|
|
2910
|
+
index: blockIndex,
|
|
2911
|
+
delta: {
|
|
2912
|
+
type: "input_json_delta",
|
|
2913
|
+
partial_json: tc.function.arguments
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
options?.onEvent?.(deltaEvent);
|
|
2917
|
+
yield deltaEvent;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
if (finishReason) {
|
|
2922
|
+
finished = true;
|
|
2923
|
+
if (textBlockStarted) {
|
|
2924
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
2925
|
+
options?.onEvent?.(stopText);
|
|
2926
|
+
yield stopText;
|
|
2927
|
+
}
|
|
2928
|
+
for (const [index, tc] of toolCalls) {
|
|
2929
|
+
const stopEvent2 = {
|
|
2930
|
+
type: "content_block_stop",
|
|
2931
|
+
index: 1 + index
|
|
2932
|
+
};
|
|
2933
|
+
options?.onEvent?.(stopEvent2);
|
|
2934
|
+
yield stopEvent2;
|
|
2935
|
+
try {
|
|
2936
|
+
const input = JSON.parse(tc.arguments);
|
|
2937
|
+
options?.onToolUse?.({ id: tc.id, name: tc.name, input });
|
|
2938
|
+
} catch {}
|
|
2939
|
+
}
|
|
2940
|
+
const messageDelta = {
|
|
2941
|
+
type: "message_delta",
|
|
2942
|
+
delta: {
|
|
2943
|
+
stop_reason: self.convertStopReason(finishReason),
|
|
2944
|
+
stop_sequence: null
|
|
2945
|
+
},
|
|
2946
|
+
usage: {
|
|
2947
|
+
output_tokens: json.usage?.completion_tokens ?? 0,
|
|
2948
|
+
input_tokens: json.usage?.prompt_tokens ?? 0
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
options?.onEvent?.(messageDelta);
|
|
2952
|
+
yield messageDelta;
|
|
2953
|
+
const stopEvent = { type: "message_stop" };
|
|
2954
|
+
options?.onEvent?.(stopEvent);
|
|
2955
|
+
yield stopEvent;
|
|
2956
|
+
}
|
|
2957
|
+
} catch {}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
} finally {
|
|
2962
|
+
reader.releaseLock();
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
createResponsesStreamIterator(body, options) {
|
|
2968
|
+
const self = this;
|
|
2969
|
+
return {
|
|
2970
|
+
async* [Symbol.asyncIterator]() {
|
|
2971
|
+
const reader = body.getReader();
|
|
2972
|
+
const decoder = new TextDecoder;
|
|
2973
|
+
let buffer = "";
|
|
2974
|
+
let emittedMessageStart = false;
|
|
2975
|
+
let textBlockStarted = false;
|
|
2976
|
+
let finished = false;
|
|
2977
|
+
const toolCalls = new Map;
|
|
2978
|
+
let nextToolBlockIndex = 1;
|
|
2979
|
+
const ensureMessageStart = (id, model) => {
|
|
2980
|
+
if (emittedMessageStart)
|
|
2981
|
+
return;
|
|
2982
|
+
emittedMessageStart = true;
|
|
2983
|
+
const startEvent = {
|
|
2984
|
+
type: "message_start",
|
|
2985
|
+
message: {
|
|
2986
|
+
id: id ?? "",
|
|
2987
|
+
type: "message",
|
|
2988
|
+
role: "assistant",
|
|
2989
|
+
content: [],
|
|
2990
|
+
model: model ?? "",
|
|
2991
|
+
stop_reason: null,
|
|
2992
|
+
stop_sequence: null,
|
|
2993
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2996
|
+
options?.onEvent?.(startEvent);
|
|
2997
|
+
return startEvent;
|
|
2998
|
+
};
|
|
2999
|
+
try {
|
|
3000
|
+
while (true) {
|
|
3001
|
+
const { done, value } = await reader.read();
|
|
3002
|
+
if (done)
|
|
3003
|
+
break;
|
|
3004
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3005
|
+
const lines = buffer.split(`
|
|
3006
|
+
`);
|
|
3007
|
+
buffer = lines.pop() || "";
|
|
3008
|
+
for (const line of lines) {
|
|
3009
|
+
if (!line.startsWith("data: "))
|
|
3010
|
+
continue;
|
|
3011
|
+
const data = line.slice(6).trim();
|
|
3012
|
+
if (!data)
|
|
3013
|
+
continue;
|
|
3014
|
+
if (data === "[DONE]") {
|
|
3015
|
+
if (!finished) {
|
|
3016
|
+
const stopEvent = { type: "message_stop" };
|
|
3017
|
+
options?.onEvent?.(stopEvent);
|
|
3018
|
+
yield stopEvent;
|
|
3019
|
+
}
|
|
3020
|
+
finished = true;
|
|
3021
|
+
continue;
|
|
3022
|
+
}
|
|
3023
|
+
let payload;
|
|
3024
|
+
try {
|
|
3025
|
+
payload = JSON.parse(data);
|
|
3026
|
+
} catch {
|
|
3027
|
+
continue;
|
|
3028
|
+
}
|
|
3029
|
+
const type = payload?.type;
|
|
3030
|
+
if (type === "response.created") {
|
|
3031
|
+
const startEvent = ensureMessageStart(payload.response?.id, payload.response?.model);
|
|
3032
|
+
if (startEvent)
|
|
3033
|
+
yield startEvent;
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
if (!emittedMessageStart) {
|
|
3037
|
+
const startEvent = ensureMessageStart(payload?.response?.id, payload?.response?.model);
|
|
3038
|
+
if (startEvent)
|
|
3039
|
+
yield startEvent;
|
|
3040
|
+
}
|
|
3041
|
+
if (type === "response.output_text.delta") {
|
|
3042
|
+
if (!textBlockStarted) {
|
|
3043
|
+
textBlockStarted = true;
|
|
3044
|
+
const startText = {
|
|
3045
|
+
type: "content_block_start",
|
|
3046
|
+
index: 0,
|
|
3047
|
+
content_block: { type: "text", text: "" }
|
|
3048
|
+
};
|
|
3049
|
+
options?.onEvent?.(startText);
|
|
3050
|
+
yield startText;
|
|
3051
|
+
}
|
|
3052
|
+
const textDelta = payload.delta ?? "";
|
|
3053
|
+
if (textDelta) {
|
|
3054
|
+
const textEvent = {
|
|
3055
|
+
type: "content_block_delta",
|
|
3056
|
+
index: 0,
|
|
3057
|
+
delta: { type: "text_delta", text: textDelta }
|
|
3058
|
+
};
|
|
3059
|
+
options?.onText?.(textDelta);
|
|
3060
|
+
options?.onEvent?.(textEvent);
|
|
3061
|
+
yield textEvent;
|
|
3062
|
+
}
|
|
3063
|
+
} else if (type === "response.output_item.added") {
|
|
3064
|
+
const item = payload.item;
|
|
3065
|
+
if (item?.type === "function_call") {
|
|
3066
|
+
const blockIndex = nextToolBlockIndex++;
|
|
3067
|
+
const callId = item.call_id ?? item.id ?? "";
|
|
3068
|
+
toolCalls.set(item.id, {
|
|
3069
|
+
callId,
|
|
3070
|
+
name: item.name ?? "",
|
|
3071
|
+
arguments: item.arguments ?? "",
|
|
3072
|
+
blockIndex,
|
|
3073
|
+
done: false
|
|
3074
|
+
});
|
|
3075
|
+
const startEvent = {
|
|
3076
|
+
type: "content_block_start",
|
|
3077
|
+
index: blockIndex,
|
|
3078
|
+
content_block: {
|
|
3079
|
+
type: "tool_use",
|
|
3080
|
+
id: callId,
|
|
3081
|
+
name: item.name ?? "",
|
|
3082
|
+
input: {}
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
3085
|
+
options?.onEvent?.(startEvent);
|
|
3086
|
+
yield startEvent;
|
|
3087
|
+
if (item.arguments) {
|
|
3088
|
+
const deltaEvent = {
|
|
3089
|
+
type: "content_block_delta",
|
|
3090
|
+
index: blockIndex,
|
|
3091
|
+
delta: {
|
|
3092
|
+
type: "input_json_delta",
|
|
3093
|
+
partial_json: item.arguments
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
options?.onEvent?.(deltaEvent);
|
|
3097
|
+
yield deltaEvent;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
} else if (type === "response.function_call_arguments.delta") {
|
|
3101
|
+
const entry = toolCalls.get(payload.item_id);
|
|
3102
|
+
if (entry && payload.delta) {
|
|
3103
|
+
entry.arguments += payload.delta;
|
|
3104
|
+
const deltaEvent = {
|
|
3105
|
+
type: "content_block_delta",
|
|
3106
|
+
index: entry.blockIndex,
|
|
3107
|
+
delta: { type: "input_json_delta", partial_json: payload.delta }
|
|
3108
|
+
};
|
|
3109
|
+
options?.onEvent?.(deltaEvent);
|
|
3110
|
+
yield deltaEvent;
|
|
3111
|
+
}
|
|
3112
|
+
} else if (type === "response.output_item.done") {
|
|
3113
|
+
const item = payload.item;
|
|
3114
|
+
if (item?.type === "function_call") {
|
|
3115
|
+
const entry = toolCalls.get(item.id);
|
|
3116
|
+
if (entry && !entry.done) {
|
|
3117
|
+
entry.done = true;
|
|
3118
|
+
const stopEvent = {
|
|
3119
|
+
type: "content_block_stop",
|
|
3120
|
+
index: entry.blockIndex
|
|
3121
|
+
};
|
|
3122
|
+
options?.onEvent?.(stopEvent);
|
|
3123
|
+
yield stopEvent;
|
|
3124
|
+
try {
|
|
3125
|
+
const input = entry.arguments ? JSON.parse(entry.arguments) : {};
|
|
3126
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input });
|
|
3127
|
+
} catch {
|
|
3128
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input: {} });
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
} else if (type === "response.completed" || type === "response.incomplete") {
|
|
3133
|
+
finished = true;
|
|
3134
|
+
if (textBlockStarted) {
|
|
3135
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3136
|
+
options?.onEvent?.(stopText);
|
|
3137
|
+
yield stopText;
|
|
3138
|
+
}
|
|
3139
|
+
for (const entry of toolCalls.values()) {
|
|
3140
|
+
if (entry.done)
|
|
3141
|
+
continue;
|
|
3142
|
+
entry.done = true;
|
|
3143
|
+
const stopEvent2 = {
|
|
3144
|
+
type: "content_block_stop",
|
|
3145
|
+
index: entry.blockIndex
|
|
3146
|
+
};
|
|
3147
|
+
options?.onEvent?.(stopEvent2);
|
|
3148
|
+
yield stopEvent2;
|
|
3149
|
+
try {
|
|
3150
|
+
const input = entry.arguments ? JSON.parse(entry.arguments) : {};
|
|
3151
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input });
|
|
3152
|
+
} catch {
|
|
3153
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input: {} });
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
const finishReason = payload.response?.incomplete_details?.reason;
|
|
3157
|
+
const messageDelta = {
|
|
3158
|
+
type: "message_delta",
|
|
3159
|
+
delta: {
|
|
3160
|
+
stop_reason: self.convertResponsesStopReason(finishReason),
|
|
3161
|
+
stop_sequence: null
|
|
3162
|
+
},
|
|
3163
|
+
usage: {
|
|
3164
|
+
output_tokens: payload.response?.usage?.output_tokens ?? 0,
|
|
3165
|
+
input_tokens: payload.response?.usage?.input_tokens ?? 0
|
|
3166
|
+
}
|
|
3167
|
+
};
|
|
3168
|
+
options?.onEvent?.(messageDelta);
|
|
3169
|
+
yield messageDelta;
|
|
3170
|
+
const stopEvent = { type: "message_stop" };
|
|
3171
|
+
options?.onEvent?.(stopEvent);
|
|
3172
|
+
yield stopEvent;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
} finally {
|
|
3177
|
+
reader.releaseLock();
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
getHeaders() {
|
|
3183
|
+
const headers = {
|
|
3184
|
+
"Content-Type": "application/json",
|
|
3185
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
3186
|
+
};
|
|
3187
|
+
if (this.config.organization) {
|
|
3188
|
+
headers["OpenAI-Organization"] = this.config.organization;
|
|
3189
|
+
}
|
|
3190
|
+
return headers;
|
|
3191
|
+
}
|
|
3192
|
+
convertResponsesStopReason(reason) {
|
|
3193
|
+
if (reason === "max_output_tokens") {
|
|
3194
|
+
return "max_tokens";
|
|
3195
|
+
}
|
|
3196
|
+
return "end_turn";
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// src/llm/gemini.ts
|
|
3201
|
+
class GeminiProvider {
|
|
3202
|
+
id = "gemini";
|
|
3203
|
+
name = "Gemini";
|
|
3204
|
+
supportedModels = [/^gemini-/, /^models\/gemini-/];
|
|
3205
|
+
config;
|
|
3206
|
+
constructor(config = {}) {
|
|
3207
|
+
const apiKey = config.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
3208
|
+
if (!apiKey) {
|
|
3209
|
+
throw new Error("Gemini API key is required. Set GEMINI_API_KEY/GOOGLE_API_KEY or pass apiKey in config.");
|
|
3210
|
+
}
|
|
3211
|
+
this.config = {
|
|
3212
|
+
apiKey,
|
|
3213
|
+
baseUrl: config.baseUrl ?? process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta",
|
|
3214
|
+
defaultMaxTokens: config.defaultMaxTokens ?? 4096
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
supportsModel(model) {
|
|
3218
|
+
return this.supportedModels.some((pattern) => pattern.test(model));
|
|
3219
|
+
}
|
|
3220
|
+
async complete(request) {
|
|
3221
|
+
const geminiRequest = this.buildRequest(request);
|
|
3222
|
+
const url = this.buildUrl(this.getModelPath(request.config.model) + ":generateContent");
|
|
3223
|
+
const response = await fetch(url, {
|
|
3224
|
+
method: "POST",
|
|
3225
|
+
headers: this.getHeaders(),
|
|
3226
|
+
body: JSON.stringify(geminiRequest),
|
|
3227
|
+
signal: request.abortSignal
|
|
3228
|
+
});
|
|
3229
|
+
if (!response.ok) {
|
|
3230
|
+
const error = await response.text();
|
|
3231
|
+
throw new Error(`Gemini API error: ${response.status} ${error}`);
|
|
3232
|
+
}
|
|
3233
|
+
const data = await response.json();
|
|
3234
|
+
return this.convertResponse(data, request.config.model);
|
|
3235
|
+
}
|
|
3236
|
+
async stream(request, options) {
|
|
3237
|
+
const geminiRequest = this.buildRequest(request);
|
|
3238
|
+
const url = this.buildUrl(this.getModelPath(request.config.model) + ":streamGenerateContent", {
|
|
3239
|
+
alt: "sse"
|
|
3240
|
+
});
|
|
3241
|
+
const response = await fetch(url, {
|
|
3242
|
+
method: "POST",
|
|
3243
|
+
headers: this.getHeaders(),
|
|
3244
|
+
body: JSON.stringify(geminiRequest),
|
|
3245
|
+
signal: request.abortSignal
|
|
3246
|
+
});
|
|
3247
|
+
if (!response.ok) {
|
|
3248
|
+
const error = await response.text();
|
|
3249
|
+
throw new Error(`Gemini API error: ${response.status} ${error}`);
|
|
3250
|
+
}
|
|
3251
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3252
|
+
if (!contentType.includes("text/event-stream")) {
|
|
3253
|
+
const data = await response.json();
|
|
3254
|
+
return this.createResponseIterator(data, options, request.config.model);
|
|
3255
|
+
}
|
|
3256
|
+
return this.createStreamIterator(response.body, options, request.config.model);
|
|
3257
|
+
}
|
|
3258
|
+
buildRequest(request) {
|
|
3259
|
+
const { contents, systemInstruction } = this.convertMessages(request.messages, request.systemPrompt);
|
|
3260
|
+
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
3261
|
+
return {
|
|
3262
|
+
contents,
|
|
3263
|
+
systemInstruction,
|
|
3264
|
+
generationConfig: {
|
|
3265
|
+
temperature: request.config.temperature,
|
|
3266
|
+
topP: request.config.topP,
|
|
3267
|
+
maxOutputTokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
3268
|
+
stopSequences: request.config.stopSequences
|
|
3269
|
+
},
|
|
3270
|
+
tools,
|
|
3271
|
+
toolConfig: tools ? { functionCallingConfig: { mode: "AUTO" } } : undefined
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
convertMessages(messages, systemPrompt) {
|
|
3275
|
+
const contents = [];
|
|
3276
|
+
const systemTexts = [];
|
|
3277
|
+
const toolNameById = new Map;
|
|
3278
|
+
for (const msg of messages) {
|
|
3279
|
+
if (typeof msg.content !== "string") {
|
|
3280
|
+
for (const block of msg.content) {
|
|
3281
|
+
if (block.type === "tool_use") {
|
|
3282
|
+
toolNameById.set(block.id, block.name);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
if (systemPrompt) {
|
|
3288
|
+
systemTexts.push(systemPrompt);
|
|
3289
|
+
}
|
|
3290
|
+
for (const msg of messages) {
|
|
3291
|
+
if (msg.role === "system") {
|
|
3292
|
+
if (typeof msg.content === "string") {
|
|
3293
|
+
systemTexts.push(msg.content);
|
|
3294
|
+
} else {
|
|
3295
|
+
for (const block of msg.content) {
|
|
3296
|
+
if (block.type === "text") {
|
|
3297
|
+
systemTexts.push(block.text);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
if (typeof msg.content === "string") {
|
|
3304
|
+
contents.push({
|
|
3305
|
+
role: msg.role === "assistant" ? "model" : "user",
|
|
3306
|
+
parts: [{ text: msg.content }]
|
|
3307
|
+
});
|
|
3308
|
+
continue;
|
|
3309
|
+
}
|
|
3310
|
+
const parts = [];
|
|
3311
|
+
const toolResponses = [];
|
|
3312
|
+
for (const block of msg.content) {
|
|
3313
|
+
if (block.type === "text") {
|
|
3314
|
+
parts.push({ text: block.text });
|
|
3315
|
+
} else if (block.type === "image") {
|
|
3316
|
+
if (block.source.type === "base64") {
|
|
3317
|
+
parts.push({
|
|
3318
|
+
inlineData: {
|
|
3319
|
+
mimeType: block.source.media_type ?? "image/jpeg",
|
|
3320
|
+
data: block.source.data ?? ""
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
3323
|
+
} else if (block.source.type === "url") {
|
|
3324
|
+
parts.push({
|
|
3325
|
+
fileData: {
|
|
3326
|
+
mimeType: block.source.media_type ?? "image/jpeg",
|
|
3327
|
+
fileUri: block.source.url ?? ""
|
|
3328
|
+
}
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
} else if (block.type === "tool_result") {
|
|
3332
|
+
const toolName = toolNameById.get(block.tool_use_id) ?? "tool";
|
|
3333
|
+
const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
3334
|
+
toolResponses.push({
|
|
3335
|
+
functionResponse: {
|
|
3336
|
+
name: toolName,
|
|
3337
|
+
response: { output }
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
if (parts.length > 0) {
|
|
3343
|
+
contents.push({
|
|
3344
|
+
role: msg.role === "assistant" ? "model" : "user",
|
|
3345
|
+
parts
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
if (toolResponses.length > 0) {
|
|
3349
|
+
contents.push({
|
|
3350
|
+
role: "user",
|
|
3351
|
+
parts: toolResponses
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
const systemInstruction = systemTexts.length > 0 ? { parts: [{ text: systemTexts.join(`
|
|
3356
|
+
|
|
3357
|
+
`) }] } : undefined;
|
|
3358
|
+
return { contents, systemInstruction };
|
|
3359
|
+
}
|
|
3360
|
+
convertTools(tools) {
|
|
3361
|
+
return [
|
|
3362
|
+
{
|
|
3363
|
+
functionDeclarations: tools.map((tool) => ({
|
|
3364
|
+
name: tool.name,
|
|
3365
|
+
description: tool.description,
|
|
3366
|
+
parameters: this.sanitizeSchema(tool.inputSchema)
|
|
3367
|
+
}))
|
|
3368
|
+
}
|
|
3369
|
+
];
|
|
3370
|
+
}
|
|
3371
|
+
sanitizeSchema(schema) {
|
|
3372
|
+
const visited = new WeakMap;
|
|
3373
|
+
const scrub = (value) => {
|
|
3374
|
+
if (value === null || typeof value !== "object") {
|
|
3375
|
+
return value;
|
|
3376
|
+
}
|
|
3377
|
+
if (Array.isArray(value)) {
|
|
3378
|
+
return value.map((item) => scrub(item));
|
|
3379
|
+
}
|
|
3380
|
+
const existing = visited.get(value);
|
|
3381
|
+
if (existing) {
|
|
3382
|
+
return existing;
|
|
3383
|
+
}
|
|
3384
|
+
const result = {};
|
|
3385
|
+
visited.set(value, result);
|
|
3386
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
3387
|
+
if (key === "additionalProperties") {
|
|
3388
|
+
continue;
|
|
3389
|
+
}
|
|
3390
|
+
result[key] = scrub(inner);
|
|
3391
|
+
}
|
|
3392
|
+
return result;
|
|
3393
|
+
};
|
|
3394
|
+
return scrub(schema);
|
|
3395
|
+
}
|
|
3396
|
+
convertResponse(data, model) {
|
|
3397
|
+
const candidate = data.candidates?.[0];
|
|
3398
|
+
const content = [];
|
|
3399
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3400
|
+
let toolIndex = 0;
|
|
3401
|
+
for (const part of parts) {
|
|
3402
|
+
if ("text" in part && part.text) {
|
|
3403
|
+
content.push({ type: "text", text: part.text });
|
|
3404
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3405
|
+
const callId = `${part.functionCall.name}_${toolIndex++}`;
|
|
3406
|
+
content.push({
|
|
3407
|
+
type: "tool_use",
|
|
3408
|
+
id: callId,
|
|
3409
|
+
name: part.functionCall.name,
|
|
3410
|
+
input: part.functionCall.args ?? {}
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
return {
|
|
3415
|
+
id: "",
|
|
3416
|
+
model: data.model ?? model,
|
|
3417
|
+
content,
|
|
3418
|
+
stopReason: this.convertStopReason(candidate?.finishReason),
|
|
3419
|
+
stopSequence: null,
|
|
3420
|
+
usage: this.convertUsage(data.usageMetadata)
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
convertUsage(usage) {
|
|
3424
|
+
return {
|
|
3425
|
+
input_tokens: usage?.promptTokenCount ?? 0,
|
|
3426
|
+
output_tokens: usage?.candidatesTokenCount ?? 0
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
convertStopReason(reason) {
|
|
3430
|
+
switch (reason) {
|
|
3431
|
+
case "MAX_TOKENS":
|
|
3432
|
+
return "max_tokens";
|
|
3433
|
+
case "STOP":
|
|
3434
|
+
return "end_turn";
|
|
3435
|
+
default:
|
|
3436
|
+
return "end_turn";
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
createStreamIterator(body, options, model) {
|
|
3440
|
+
const self = this;
|
|
3441
|
+
return {
|
|
3442
|
+
async* [Symbol.asyncIterator]() {
|
|
3443
|
+
const reader = body.getReader();
|
|
3444
|
+
const decoder = new TextDecoder;
|
|
3445
|
+
let buffer = "";
|
|
3446
|
+
let emittedMessageStart = false;
|
|
3447
|
+
let textBlockStarted = false;
|
|
3448
|
+
let finished = false;
|
|
3449
|
+
let toolIndex = 0;
|
|
3450
|
+
let emittedAny = false;
|
|
3451
|
+
const emitMessageStart = (modelId) => {
|
|
3452
|
+
if (emittedMessageStart)
|
|
3453
|
+
return;
|
|
3454
|
+
emittedMessageStart = true;
|
|
3455
|
+
const startEvent = {
|
|
3456
|
+
type: "message_start",
|
|
3457
|
+
message: {
|
|
3458
|
+
id: "",
|
|
3459
|
+
type: "message",
|
|
3460
|
+
role: "assistant",
|
|
3461
|
+
content: [],
|
|
3462
|
+
model: modelId,
|
|
3463
|
+
stop_reason: null,
|
|
3464
|
+
stop_sequence: null,
|
|
3465
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
options?.onEvent?.(startEvent);
|
|
3469
|
+
return startEvent;
|
|
3470
|
+
};
|
|
3471
|
+
try {
|
|
3472
|
+
while (true) {
|
|
3473
|
+
const { done, value } = await reader.read();
|
|
3474
|
+
if (done)
|
|
3475
|
+
break;
|
|
3476
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3477
|
+
const lines = buffer.split(`
|
|
3478
|
+
`);
|
|
3479
|
+
buffer = lines.pop() || "";
|
|
3480
|
+
for (const line of lines) {
|
|
3481
|
+
const trimmed = line.trim();
|
|
3482
|
+
if (!trimmed)
|
|
3483
|
+
continue;
|
|
3484
|
+
let jsonText = trimmed;
|
|
3485
|
+
if (trimmed.startsWith("data:")) {
|
|
3486
|
+
jsonText = trimmed.slice(5).trim();
|
|
3487
|
+
}
|
|
3488
|
+
let payload;
|
|
3489
|
+
try {
|
|
3490
|
+
payload = JSON.parse(jsonText);
|
|
3491
|
+
} catch {
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3494
|
+
const startEvent = emitMessageStart(payload.model ?? model);
|
|
3495
|
+
if (startEvent) {
|
|
3496
|
+
yield startEvent;
|
|
3497
|
+
emittedAny = true;
|
|
3498
|
+
}
|
|
3499
|
+
const candidate = payload.candidates?.[0];
|
|
3500
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3501
|
+
for (const part of parts) {
|
|
3502
|
+
if ("text" in part && part.text) {
|
|
3503
|
+
if (!textBlockStarted) {
|
|
3504
|
+
textBlockStarted = true;
|
|
3505
|
+
const startText = {
|
|
3506
|
+
type: "content_block_start",
|
|
3507
|
+
index: 0,
|
|
3508
|
+
content_block: { type: "text", text: "" }
|
|
3509
|
+
};
|
|
3510
|
+
options?.onEvent?.(startText);
|
|
3511
|
+
yield startText;
|
|
3512
|
+
emittedAny = true;
|
|
3513
|
+
}
|
|
3514
|
+
const textEvent = {
|
|
3515
|
+
type: "content_block_delta",
|
|
3516
|
+
index: 0,
|
|
3517
|
+
delta: { type: "text_delta", text: part.text }
|
|
3518
|
+
};
|
|
3519
|
+
options?.onText?.(part.text);
|
|
3520
|
+
options?.onEvent?.(textEvent);
|
|
3521
|
+
yield textEvent;
|
|
3522
|
+
emittedAny = true;
|
|
3523
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3524
|
+
const callId = `${part.functionCall.name}_${toolIndex}`;
|
|
3525
|
+
const blockIndex = 1 + toolIndex;
|
|
3526
|
+
toolIndex += 1;
|
|
3527
|
+
const startTool = {
|
|
3528
|
+
type: "content_block_start",
|
|
3529
|
+
index: blockIndex,
|
|
3530
|
+
content_block: {
|
|
3531
|
+
type: "tool_use",
|
|
3532
|
+
id: callId,
|
|
3533
|
+
name: part.functionCall.name,
|
|
3534
|
+
input: {}
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
options?.onEvent?.(startTool);
|
|
3538
|
+
yield startTool;
|
|
3539
|
+
emittedAny = true;
|
|
3540
|
+
const args = JSON.stringify(part.functionCall.args ?? {});
|
|
3541
|
+
if (args) {
|
|
3542
|
+
const deltaEvent = {
|
|
3543
|
+
type: "content_block_delta",
|
|
3544
|
+
index: blockIndex,
|
|
3545
|
+
delta: { type: "input_json_delta", partial_json: args }
|
|
3546
|
+
};
|
|
3547
|
+
options?.onEvent?.(deltaEvent);
|
|
3548
|
+
yield deltaEvent;
|
|
3549
|
+
emittedAny = true;
|
|
3550
|
+
}
|
|
3551
|
+
const stopTool = {
|
|
3552
|
+
type: "content_block_stop",
|
|
3553
|
+
index: blockIndex
|
|
3554
|
+
};
|
|
3555
|
+
options?.onEvent?.(stopTool);
|
|
3556
|
+
yield stopTool;
|
|
3557
|
+
emittedAny = true;
|
|
3558
|
+
options?.onToolUse?.({
|
|
3559
|
+
id: callId,
|
|
3560
|
+
name: part.functionCall.name,
|
|
3561
|
+
input: part.functionCall.args ?? {}
|
|
3562
|
+
});
|
|
3563
|
+
emittedAny = true;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
if (candidate?.finishReason && !finished) {
|
|
3567
|
+
finished = true;
|
|
3568
|
+
if (textBlockStarted) {
|
|
3569
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3570
|
+
options?.onEvent?.(stopText);
|
|
3571
|
+
yield stopText;
|
|
3572
|
+
}
|
|
3573
|
+
const messageDelta = {
|
|
3574
|
+
type: "message_delta",
|
|
3575
|
+
delta: {
|
|
3576
|
+
stop_reason: self.convertStopReason(candidate.finishReason),
|
|
3577
|
+
stop_sequence: null
|
|
3578
|
+
},
|
|
3579
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3580
|
+
};
|
|
3581
|
+
options?.onEvent?.(messageDelta);
|
|
3582
|
+
yield messageDelta;
|
|
3583
|
+
emittedAny = true;
|
|
3584
|
+
const stopEvent = { type: "message_stop" };
|
|
3585
|
+
options?.onEvent?.(stopEvent);
|
|
3586
|
+
yield stopEvent;
|
|
3587
|
+
emittedAny = true;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
} finally {
|
|
3592
|
+
reader.releaseLock();
|
|
3593
|
+
}
|
|
3594
|
+
if (!emittedAny) {
|
|
3595
|
+
const trimmed = buffer.trim();
|
|
3596
|
+
if (trimmed) {
|
|
3597
|
+
try {
|
|
3598
|
+
const parsed = JSON.parse(trimmed);
|
|
3599
|
+
const responses = Array.isArray(parsed) ? parsed : [parsed];
|
|
3600
|
+
for (const payload of responses) {
|
|
3601
|
+
const startEvent = emitMessageStart(payload.model ?? model);
|
|
3602
|
+
if (startEvent) {
|
|
3603
|
+
yield startEvent;
|
|
3604
|
+
}
|
|
3605
|
+
const candidate = payload.candidates?.[0];
|
|
3606
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3607
|
+
for (const part of parts) {
|
|
3608
|
+
if ("text" in part && part.text) {
|
|
3609
|
+
if (!textBlockStarted) {
|
|
3610
|
+
textBlockStarted = true;
|
|
3611
|
+
const startText = {
|
|
3612
|
+
type: "content_block_start",
|
|
3613
|
+
index: 0,
|
|
3614
|
+
content_block: { type: "text", text: "" }
|
|
3615
|
+
};
|
|
3616
|
+
options?.onEvent?.(startText);
|
|
3617
|
+
yield startText;
|
|
3618
|
+
}
|
|
3619
|
+
const textEvent = {
|
|
3620
|
+
type: "content_block_delta",
|
|
3621
|
+
index: 0,
|
|
3622
|
+
delta: { type: "text_delta", text: part.text }
|
|
2647
3623
|
};
|
|
2648
|
-
options?.onText?.(
|
|
3624
|
+
options?.onText?.(part.text);
|
|
2649
3625
|
options?.onEvent?.(textEvent);
|
|
2650
3626
|
yield textEvent;
|
|
2651
3627
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
3628
|
+
}
|
|
3629
|
+
if (textBlockStarted) {
|
|
3630
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3631
|
+
options?.onEvent?.(stopText);
|
|
3632
|
+
yield stopText;
|
|
3633
|
+
}
|
|
3634
|
+
const messageDelta = {
|
|
3635
|
+
type: "message_delta",
|
|
3636
|
+
delta: {
|
|
3637
|
+
stop_reason: self.convertStopReason(candidate?.finishReason),
|
|
3638
|
+
stop_sequence: null
|
|
3639
|
+
},
|
|
3640
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3641
|
+
};
|
|
3642
|
+
options?.onEvent?.(messageDelta);
|
|
3643
|
+
yield messageDelta;
|
|
3644
|
+
const stopEvent = { type: "message_stop" };
|
|
3645
|
+
options?.onEvent?.(stopEvent);
|
|
3646
|
+
yield stopEvent;
|
|
3647
|
+
}
|
|
3648
|
+
return;
|
|
3649
|
+
} catch {}
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
if (!finished) {
|
|
3653
|
+
if (textBlockStarted) {
|
|
3654
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3655
|
+
options?.onEvent?.(stopText);
|
|
3656
|
+
yield stopText;
|
|
3657
|
+
}
|
|
3658
|
+
const stopEvent = { type: "message_stop" };
|
|
3659
|
+
options?.onEvent?.(stopEvent);
|
|
3660
|
+
yield stopEvent;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3665
|
+
createResponseIterator(data, options, model) {
|
|
3666
|
+
const responses = Array.isArray(data) ? data : [data];
|
|
3667
|
+
const self = this;
|
|
3668
|
+
return {
|
|
3669
|
+
async* [Symbol.asyncIterator]() {
|
|
3670
|
+
for (const payload of responses) {
|
|
3671
|
+
const candidate = payload.candidates?.[0];
|
|
3672
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3673
|
+
let textIndex = 0;
|
|
3674
|
+
let toolIndex = 0;
|
|
3675
|
+
const startEvent = {
|
|
3676
|
+
type: "message_start",
|
|
3677
|
+
message: {
|
|
3678
|
+
id: "",
|
|
3679
|
+
type: "message",
|
|
3680
|
+
role: "assistant",
|
|
3681
|
+
content: [],
|
|
3682
|
+
model: payload.model ?? model,
|
|
3683
|
+
stop_reason: null,
|
|
3684
|
+
stop_sequence: null,
|
|
3685
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
3686
|
+
}
|
|
3687
|
+
};
|
|
3688
|
+
options?.onEvent?.(startEvent);
|
|
3689
|
+
yield startEvent;
|
|
3690
|
+
for (const part of parts) {
|
|
3691
|
+
if ("text" in part && part.text) {
|
|
3692
|
+
const startText = {
|
|
3693
|
+
type: "content_block_start",
|
|
3694
|
+
index: textIndex,
|
|
3695
|
+
content_block: { type: "text", text: "" }
|
|
3696
|
+
};
|
|
3697
|
+
options?.onEvent?.(startText);
|
|
3698
|
+
yield startText;
|
|
3699
|
+
const textEvent = {
|
|
3700
|
+
type: "content_block_delta",
|
|
3701
|
+
index: textIndex,
|
|
3702
|
+
delta: { type: "text_delta", text: part.text }
|
|
3703
|
+
};
|
|
3704
|
+
options?.onText?.(part.text);
|
|
3705
|
+
options?.onEvent?.(textEvent);
|
|
3706
|
+
yield textEvent;
|
|
3707
|
+
const stopText = { type: "content_block_stop", index: textIndex };
|
|
3708
|
+
options?.onEvent?.(stopText);
|
|
3709
|
+
yield stopText;
|
|
3710
|
+
textIndex += 1;
|
|
3711
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3712
|
+
const callId = `${part.functionCall.name}_${toolIndex}`;
|
|
3713
|
+
const blockIndex = textIndex + toolIndex + 1;
|
|
3714
|
+
toolIndex += 1;
|
|
3715
|
+
const startTool = {
|
|
3716
|
+
type: "content_block_start",
|
|
3717
|
+
index: blockIndex,
|
|
3718
|
+
content_block: {
|
|
3719
|
+
type: "tool_use",
|
|
3720
|
+
id: callId,
|
|
3721
|
+
name: part.functionCall.name,
|
|
3722
|
+
input: {}
|
|
3723
|
+
}
|
|
3724
|
+
};
|
|
3725
|
+
options?.onEvent?.(startTool);
|
|
3726
|
+
yield startTool;
|
|
3727
|
+
const args = JSON.stringify(part.functionCall.args ?? {});
|
|
3728
|
+
if (args) {
|
|
3729
|
+
const deltaEvent = {
|
|
3730
|
+
type: "content_block_delta",
|
|
3731
|
+
index: blockIndex,
|
|
3732
|
+
delta: { type: "input_json_delta", partial_json: args }
|
|
3733
|
+
};
|
|
3734
|
+
options?.onEvent?.(deltaEvent);
|
|
3735
|
+
yield deltaEvent;
|
|
2734
3736
|
}
|
|
3737
|
+
const stopTool = { type: "content_block_stop", index: blockIndex };
|
|
3738
|
+
options?.onEvent?.(stopTool);
|
|
3739
|
+
yield stopTool;
|
|
3740
|
+
options?.onToolUse?.({
|
|
3741
|
+
id: callId,
|
|
3742
|
+
name: part.functionCall.name,
|
|
3743
|
+
input: part.functionCall.args ?? {}
|
|
3744
|
+
});
|
|
2735
3745
|
}
|
|
2736
3746
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
3747
|
+
const messageDelta = {
|
|
3748
|
+
type: "message_delta",
|
|
3749
|
+
delta: {
|
|
3750
|
+
stop_reason: self.convertStopReason(candidate?.finishReason),
|
|
3751
|
+
stop_sequence: null
|
|
3752
|
+
},
|
|
3753
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3754
|
+
};
|
|
3755
|
+
options?.onEvent?.(messageDelta);
|
|
3756
|
+
yield messageDelta;
|
|
3757
|
+
const stopEvent = { type: "message_stop" };
|
|
3758
|
+
options?.onEvent?.(stopEvent);
|
|
3759
|
+
yield stopEvent;
|
|
2739
3760
|
}
|
|
2740
3761
|
}
|
|
2741
3762
|
};
|
|
2742
3763
|
}
|
|
2743
3764
|
getHeaders() {
|
|
2744
|
-
|
|
3765
|
+
return {
|
|
2745
3766
|
"Content-Type": "application/json",
|
|
2746
|
-
|
|
3767
|
+
"x-goog-api-key": this.config.apiKey
|
|
2747
3768
|
};
|
|
2748
|
-
|
|
2749
|
-
|
|
3769
|
+
}
|
|
3770
|
+
buildUrl(path2, params) {
|
|
3771
|
+
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
3772
|
+
const url = new URL(`${base}/${path2.replace(/^\/+/, "")}`);
|
|
3773
|
+
if (!url.searchParams.has("key")) {
|
|
3774
|
+
url.searchParams.set("key", this.config.apiKey);
|
|
2750
3775
|
}
|
|
2751
|
-
|
|
3776
|
+
if (params) {
|
|
3777
|
+
for (const [key, value] of Object.entries(params)) {
|
|
3778
|
+
url.searchParams.set(key, value);
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
return url.toString();
|
|
3782
|
+
}
|
|
3783
|
+
getModelPath(model) {
|
|
3784
|
+
return model.startsWith("models/") ? model : `models/${model}`;
|
|
2752
3785
|
}
|
|
2753
3786
|
}
|
|
2754
3787
|
|
|
@@ -2766,8 +3799,13 @@ function getGlobalManager() {
|
|
|
2766
3799
|
apiKey: process.env.OPENAI_API_KEY,
|
|
2767
3800
|
baseUrl: process.env.OPENAI_BASE_URL
|
|
2768
3801
|
});
|
|
3802
|
+
} else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
3803
|
+
defaultProvider = new GeminiProvider({
|
|
3804
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
3805
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
3806
|
+
});
|
|
2769
3807
|
} else {
|
|
2770
|
-
throw new Error("No default provider set. Set ANTHROPIC_API_KEY or
|
|
3808
|
+
throw new Error("No default provider set. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
|
|
2771
3809
|
}
|
|
2772
3810
|
}
|
|
2773
3811
|
globalManager = new SessionManagerImpl({
|
|
@@ -2788,8 +3826,13 @@ async function createSession(options) {
|
|
|
2788
3826
|
apiKey: process.env.OPENAI_API_KEY,
|
|
2789
3827
|
baseUrl: process.env.OPENAI_BASE_URL
|
|
2790
3828
|
});
|
|
3829
|
+
} else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
3830
|
+
provider = new GeminiProvider({
|
|
3831
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
3832
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
3833
|
+
});
|
|
2791
3834
|
} else {
|
|
2792
|
-
throw new Error("No provider available. Set ANTHROPIC_API_KEY or
|
|
3835
|
+
throw new Error("No provider available. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
|
|
2793
3836
|
}
|
|
2794
3837
|
}
|
|
2795
3838
|
const customManager = new SessionManagerImpl({
|
|
@@ -3220,6 +4263,7 @@ function checkDirAccess(dirPath, options) {
|
|
|
3220
4263
|
|
|
3221
4264
|
// src/tools/builtin/bash.ts
|
|
3222
4265
|
var DEFAULT_TIMEOUT = 120000;
|
|
4266
|
+
var DEFAULT_IDLE_TIMEOUT = 30000;
|
|
3223
4267
|
var MAX_OUTPUT_LENGTH = 1e5;
|
|
3224
4268
|
var DEFAULT_BLOCKED_PATTERNS = [
|
|
3225
4269
|
"\\bsudo\\b",
|
|
@@ -3286,21 +4330,39 @@ function createBashTool(options = {}) {
|
|
|
3286
4330
|
}
|
|
3287
4331
|
}
|
|
3288
4332
|
const actualTimeout = Math.min(timeout, 600000);
|
|
4333
|
+
const idleTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT;
|
|
3289
4334
|
return new Promise((resolve2) => {
|
|
3290
4335
|
let stdout = "";
|
|
3291
4336
|
let stderr = "";
|
|
3292
4337
|
let killed = false;
|
|
4338
|
+
let killedReason = null;
|
|
4339
|
+
let lastOutputTime = Date.now();
|
|
3293
4340
|
const proc = spawn("bash", ["-c", command], {
|
|
3294
4341
|
cwd: cwdAccess.resolved,
|
|
3295
4342
|
env: process.env,
|
|
3296
|
-
shell: false
|
|
4343
|
+
shell: false,
|
|
4344
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3297
4345
|
});
|
|
3298
4346
|
const timer = setTimeout(() => {
|
|
3299
4347
|
killed = true;
|
|
4348
|
+
killedReason = "timeout";
|
|
3300
4349
|
proc.kill("SIGTERM");
|
|
3301
4350
|
setTimeout(() => proc.kill("SIGKILL"), 1000);
|
|
3302
4351
|
}, actualTimeout);
|
|
4352
|
+
const idleChecker = setInterval(() => {
|
|
4353
|
+
const idleTime = Date.now() - lastOutputTime;
|
|
4354
|
+
if (idleTime >= idleTimeout && !killed) {
|
|
4355
|
+
killed = true;
|
|
4356
|
+
killedReason = "idle";
|
|
4357
|
+
proc.kill("SIGTERM");
|
|
4358
|
+
setTimeout(() => proc.kill("SIGKILL"), 1000);
|
|
4359
|
+
}
|
|
4360
|
+
}, 1000);
|
|
4361
|
+
const updateLastOutputTime = () => {
|
|
4362
|
+
lastOutputTime = Date.now();
|
|
4363
|
+
};
|
|
3303
4364
|
proc.stdout?.on("data", (data) => {
|
|
4365
|
+
updateLastOutputTime();
|
|
3304
4366
|
stdout += data.toString();
|
|
3305
4367
|
if (stdout.length > MAX_OUTPUT_LENGTH) {
|
|
3306
4368
|
stdout = stdout.slice(0, MAX_OUTPUT_LENGTH) + `
|
|
@@ -3309,6 +4371,7 @@ function createBashTool(options = {}) {
|
|
|
3309
4371
|
}
|
|
3310
4372
|
});
|
|
3311
4373
|
proc.stderr?.on("data", (data) => {
|
|
4374
|
+
updateLastOutputTime();
|
|
3312
4375
|
stderr += data.toString();
|
|
3313
4376
|
if (stderr.length > MAX_OUTPUT_LENGTH) {
|
|
3314
4377
|
stderr = stderr.slice(0, MAX_OUTPUT_LENGTH) + `
|
|
@@ -3317,17 +4380,31 @@ function createBashTool(options = {}) {
|
|
|
3317
4380
|
});
|
|
3318
4381
|
proc.on("close", (code) => {
|
|
3319
4382
|
clearTimeout(timer);
|
|
4383
|
+
clearInterval(idleChecker);
|
|
3320
4384
|
if (killed) {
|
|
3321
|
-
|
|
3322
|
-
|
|
4385
|
+
if (killedReason === "idle") {
|
|
4386
|
+
resolve2({
|
|
4387
|
+
content: `Command terminated: no output for ${idleTimeout / 1000} seconds (likely waiting for input)
|
|
3323
4388
|
|
|
3324
4389
|
Partial output:
|
|
3325
4390
|
${stdout}
|
|
3326
4391
|
|
|
3327
4392
|
Stderr:
|
|
3328
4393
|
${stderr}`,
|
|
3329
|
-
|
|
3330
|
-
|
|
4394
|
+
isError: true
|
|
4395
|
+
});
|
|
4396
|
+
} else {
|
|
4397
|
+
resolve2({
|
|
4398
|
+
content: `Command timed out after ${actualTimeout}ms
|
|
4399
|
+
|
|
4400
|
+
Partial output:
|
|
4401
|
+
${stdout}
|
|
4402
|
+
|
|
4403
|
+
Stderr:
|
|
4404
|
+
${stderr}`,
|
|
4405
|
+
isError: true
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
3331
4408
|
return;
|
|
3332
4409
|
}
|
|
3333
4410
|
const output = stdout + (stderr ? `
|
|
@@ -3348,6 +4425,7 @@ ${output}`,
|
|
|
3348
4425
|
});
|
|
3349
4426
|
proc.on("error", (error) => {
|
|
3350
4427
|
clearTimeout(timer);
|
|
4428
|
+
clearInterval(idleChecker);
|
|
3351
4429
|
resolve2({
|
|
3352
4430
|
content: `Failed to execute command: ${error.message}`,
|
|
3353
4431
|
isError: true
|
|
@@ -4987,7 +6065,7 @@ ${responseText}`;
|
|
|
4987
6065
|
metadata: {
|
|
4988
6066
|
status: response.status,
|
|
4989
6067
|
statusText: response.statusText,
|
|
4990
|
-
headers: Object.fromEntries(response.headers
|
|
6068
|
+
headers: Object.fromEntries(response.headers),
|
|
4991
6069
|
body: responseBody
|
|
4992
6070
|
}
|
|
4993
6071
|
};
|
|
@@ -5063,7 +6141,17 @@ function loadEnvOverride(cwd) {
|
|
|
5063
6141
|
|
|
5064
6142
|
// src/cli/cli.ts
|
|
5065
6143
|
loadEnvOverride();
|
|
5066
|
-
|
|
6144
|
+
function getCliVersion() {
|
|
6145
|
+
try {
|
|
6146
|
+
const pkgUrl = new URL("../../package.json", import.meta.url);
|
|
6147
|
+
const raw = readFileSync2(pkgUrl, "utf-8");
|
|
6148
|
+
const parsed = JSON.parse(raw);
|
|
6149
|
+
return parsed.version ?? "0.0.0";
|
|
6150
|
+
} catch {
|
|
6151
|
+
return "0.0.0";
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
var VERSION = getCliVersion();
|
|
5067
6155
|
var SKILLS_PATH = join8(homedir4(), ".claude");
|
|
5068
6156
|
var colors = {
|
|
5069
6157
|
reset: "\x1B[0m",
|
|
@@ -5092,6 +6180,8 @@ var session = null;
|
|
|
5092
6180
|
var totalInputTokens = 0;
|
|
5093
6181
|
var totalOutputTokens = 0;
|
|
5094
6182
|
var messageCount = 0;
|
|
6183
|
+
var currentProviderId = null;
|
|
6184
|
+
var currentModelOverride = null;
|
|
5095
6185
|
function isGitRepo(dir) {
|
|
5096
6186
|
return existsSync10(join8(dir, ".git"));
|
|
5097
6187
|
}
|
|
@@ -5139,6 +6229,7 @@ ${c.bold("Interactive Commands:")}
|
|
|
5139
6229
|
${c.cyan("/clear")} Clear conversation history
|
|
5140
6230
|
${c.cyan("/tools")} List available tools
|
|
5141
6231
|
${c.cyan("/skills")} List available skills
|
|
6232
|
+
${c.cyan("/models")} Show or switch provider/model
|
|
5142
6233
|
${c.cyan("/todos")} Show current todo list
|
|
5143
6234
|
${c.cyan("/usage")} Show token usage statistics
|
|
5144
6235
|
${c.cyan("/debug")} Show debug info (prompt, model, env)
|
|
@@ -5147,8 +6238,11 @@ ${c.bold("Interactive Commands:")}
|
|
|
5147
6238
|
${c.bold("Environment:")}
|
|
5148
6239
|
${c.cyan("ANTHROPIC_API_KEY")} Anthropic API key (for Claude models)
|
|
5149
6240
|
${c.cyan("ANTHROPIC_MODEL")} Optional. Claude model (default: claude-sonnet-4-20250514)
|
|
6241
|
+
${c.cyan("GEMINI_API_KEY")} Gemini API key (for Gemini models)
|
|
6242
|
+
${c.cyan("GEMINI_MODEL")} Optional. Gemini model (default: gemini-1.5-pro)
|
|
6243
|
+
${c.cyan("GEMINI_BASE_URL")} Optional. Custom Gemini API base URL
|
|
5150
6244
|
${c.cyan("OPENAI_API_KEY")} OpenAI API key (for GPT models)
|
|
5151
|
-
${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-
|
|
6245
|
+
${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-5.2)
|
|
5152
6246
|
${c.cyan("OPENAI_BASE_URL")} Optional. Custom OpenAI-compatible API URL
|
|
5153
6247
|
|
|
5154
6248
|
${c.bold("Examples:")}
|
|
@@ -5166,7 +6260,7 @@ function printVersion() {
|
|
|
5166
6260
|
console.log(`formagent-sdk v${VERSION}`);
|
|
5167
6261
|
}
|
|
5168
6262
|
function printBanner() {
|
|
5169
|
-
const model =
|
|
6263
|
+
const model = getActiveModel();
|
|
5170
6264
|
console.log();
|
|
5171
6265
|
console.log(c.cyan("╔═══════════════════════════════════════════════════════════╗"));
|
|
5172
6266
|
console.log(c.cyan("║") + c.bold(" FormAgent CLI v" + VERSION + " ") + c.cyan("║"));
|
|
@@ -5174,6 +6268,7 @@ function printBanner() {
|
|
|
5174
6268
|
console.log(c.cyan("╚═══════════════════════════════════════════════════════════╝"));
|
|
5175
6269
|
console.log();
|
|
5176
6270
|
console.log(c.dim(" Model: ") + c.green(model));
|
|
6271
|
+
console.log(c.dim(" Provider: ") + c.green(getActiveProviderId() ?? "auto"));
|
|
5177
6272
|
console.log(c.dim(" Type your message and press Enter to chat."));
|
|
5178
6273
|
console.log(c.dim(" Use /help for commands, /exit to quit."));
|
|
5179
6274
|
console.log();
|
|
@@ -5186,6 +6281,7 @@ function printInteractiveHelp() {
|
|
|
5186
6281
|
console.log(` ${c.cyan("/clear")} Clear conversation history`);
|
|
5187
6282
|
console.log(` ${c.cyan("/tools")} List available tools`);
|
|
5188
6283
|
console.log(` ${c.cyan("/skills")} List available skills`);
|
|
6284
|
+
console.log(` ${c.cyan("/models")} Show or switch provider/model`);
|
|
5189
6285
|
console.log(` ${c.cyan("/todos")} Show current todo list`);
|
|
5190
6286
|
console.log(` ${c.cyan("/usage")} Show token usage statistics`);
|
|
5191
6287
|
console.log(` ${c.cyan("/debug")} Show debug info (prompt, model, env)`);
|
|
@@ -5259,8 +6355,238 @@ function printUsage() {
|
|
|
5259
6355
|
console.log(` ${c.cyan("Est. cost:")} $${(inputCost + outputCost).toFixed(4)}`);
|
|
5260
6356
|
console.log();
|
|
5261
6357
|
}
|
|
6358
|
+
async function resetSessionForModelChange() {
|
|
6359
|
+
if (session) {
|
|
6360
|
+
await session.close();
|
|
6361
|
+
session = null;
|
|
6362
|
+
}
|
|
6363
|
+
totalInputTokens = 0;
|
|
6364
|
+
totalOutputTokens = 0;
|
|
6365
|
+
messageCount = 0;
|
|
6366
|
+
}
|
|
6367
|
+
function printModelsHelp() {
|
|
6368
|
+
const provider = getActiveProviderId() ?? "auto";
|
|
6369
|
+
const model = getActiveModel();
|
|
6370
|
+
console.log();
|
|
6371
|
+
console.log(c.bold("Model Selection:"));
|
|
6372
|
+
console.log();
|
|
6373
|
+
console.log(` ${c.cyan("Current provider:")} ${provider}`);
|
|
6374
|
+
console.log(` ${c.cyan("Current model:")} ${model}`);
|
|
6375
|
+
console.log();
|
|
6376
|
+
console.log(c.bold("Usage:"));
|
|
6377
|
+
console.log(` ${c.cyan("/models")}`);
|
|
6378
|
+
console.log(c.dim(" List models for the active provider"));
|
|
6379
|
+
console.log(` ${c.cyan("/models")} openai gpt-5-mini`);
|
|
6380
|
+
console.log(` ${c.cyan("/models")} anthropic claude-sonnet-4-20250514`);
|
|
6381
|
+
console.log(` ${c.cyan("/models")} gemini gemini-1.5-pro`);
|
|
6382
|
+
console.log(` ${c.cyan("/models")} gpt-5.2`);
|
|
6383
|
+
console.log(` ${c.cyan("/models")} reset`);
|
|
6384
|
+
console.log();
|
|
6385
|
+
}
|
|
6386
|
+
async function handleModelsCommand(args) {
|
|
6387
|
+
if (args.length === 0) {
|
|
6388
|
+
await listModelsSummary();
|
|
6389
|
+
return;
|
|
6390
|
+
}
|
|
6391
|
+
if (args[0].toLowerCase() === "reset") {
|
|
6392
|
+
currentProviderId = null;
|
|
6393
|
+
currentModelOverride = null;
|
|
6394
|
+
await resetSessionForModelChange();
|
|
6395
|
+
console.log(c.green(`
|
|
6396
|
+
✓ Model selection reset to environment defaults.
|
|
6397
|
+
`));
|
|
6398
|
+
return;
|
|
6399
|
+
}
|
|
6400
|
+
if (args.length === 1) {
|
|
6401
|
+
const provider2 = parseProvider(args[0]);
|
|
6402
|
+
if (provider2) {
|
|
6403
|
+
currentProviderId = provider2;
|
|
6404
|
+
currentModelOverride = null;
|
|
6405
|
+
await resetSessionForModelChange();
|
|
6406
|
+
console.log(c.green(`
|
|
6407
|
+
✓ Provider set to ${provider2}. Model: ${getActiveModel()}.
|
|
6408
|
+
`));
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
currentModelOverride = args[0];
|
|
6412
|
+
currentProviderId = inferProviderFromModel(args[0]) ?? currentProviderId;
|
|
6413
|
+
await resetSessionForModelChange();
|
|
6414
|
+
console.log(c.green(`
|
|
6415
|
+
✓ Model set to ${currentModelOverride} (provider: ${getActiveProviderId() ?? "auto"}).
|
|
6416
|
+
`));
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6419
|
+
const provider = parseProvider(args[0]);
|
|
6420
|
+
if (!provider) {
|
|
6421
|
+
console.log(c.yellow(`
|
|
6422
|
+
Unknown provider: ${args[0]}. Use "openai", "anthropic", or "gemini".
|
|
6423
|
+
`));
|
|
6424
|
+
return;
|
|
6425
|
+
}
|
|
6426
|
+
const model = args.slice(1).join(" ");
|
|
6427
|
+
if (!model) {
|
|
6428
|
+
console.log(c.yellow(`
|
|
6429
|
+
Missing model name. Example: /models openai gpt-5-mini
|
|
6430
|
+
`));
|
|
6431
|
+
return;
|
|
6432
|
+
}
|
|
6433
|
+
currentProviderId = provider;
|
|
6434
|
+
currentModelOverride = model;
|
|
6435
|
+
await resetSessionForModelChange();
|
|
6436
|
+
console.log(c.green(`
|
|
6437
|
+
✓ Provider set to ${provider}, model set to ${model}.
|
|
6438
|
+
`));
|
|
6439
|
+
}
|
|
6440
|
+
function normalizeOpenAIBaseUrl(baseUrl) {
|
|
6441
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
6442
|
+
if (trimmed.endsWith("/v1")) {
|
|
6443
|
+
return trimmed;
|
|
6444
|
+
}
|
|
6445
|
+
return `${trimmed}/v1`;
|
|
6446
|
+
}
|
|
6447
|
+
function getOpenAIApiType(baseUrl) {
|
|
6448
|
+
const normalized = baseUrl.toLowerCase();
|
|
6449
|
+
return normalized.includes("api.openai.com") ? "openai" : "openai-compatible";
|
|
6450
|
+
}
|
|
6451
|
+
function isGoogleGeminiBaseUrl(baseUrl) {
|
|
6452
|
+
const normalized = baseUrl.toLowerCase();
|
|
6453
|
+
return normalized.includes("generativelanguage.googleapis.com") || normalized.includes("/v1beta");
|
|
6454
|
+
}
|
|
6455
|
+
async function listAnthropicModels() {
|
|
6456
|
+
const baseUrlRaw = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\/+$/, "");
|
|
6457
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
6458
|
+
const baseUrl = baseUrlRaw.endsWith("/v1") ? baseUrlRaw : `${baseUrlRaw}/v1`;
|
|
6459
|
+
console.log(c.bold("Anthropic Models:"));
|
|
6460
|
+
console.log(c.dim(" API Type: anthropic (official)"));
|
|
6461
|
+
console.log(c.dim(` Base URL: ${baseUrl}`));
|
|
6462
|
+
if (!apiKey) {
|
|
6463
|
+
console.log(c.red(" ✗ ANTHROPIC_API_KEY not set"));
|
|
6464
|
+
console.log();
|
|
6465
|
+
return;
|
|
6466
|
+
}
|
|
6467
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
6468
|
+
headers: {
|
|
6469
|
+
"x-api-key": apiKey,
|
|
6470
|
+
"anthropic-version": "2023-06-01"
|
|
6471
|
+
}
|
|
6472
|
+
});
|
|
6473
|
+
if (!res.ok) {
|
|
6474
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6475
|
+
console.log(c.dim(` URL: ${baseUrl}/models`));
|
|
6476
|
+
console.log();
|
|
6477
|
+
return;
|
|
6478
|
+
}
|
|
6479
|
+
const payload = await res.json();
|
|
6480
|
+
const items = payload.data ?? [];
|
|
6481
|
+
for (const item of items) {
|
|
6482
|
+
const name = item.display_name ? ` (${item.display_name})` : "";
|
|
6483
|
+
console.log(` ${c.green("●")} ${item.id}${name}`);
|
|
6484
|
+
}
|
|
6485
|
+
console.log();
|
|
6486
|
+
}
|
|
6487
|
+
async function listOpenAIModels() {
|
|
6488
|
+
const baseUrl = normalizeOpenAIBaseUrl(process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1");
|
|
6489
|
+
const apiFlavor = getOpenAIApiType(baseUrl);
|
|
6490
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
6491
|
+
console.log(c.bold("OpenAI Models:"));
|
|
6492
|
+
console.log(c.dim(` API Type: ${apiFlavor}`));
|
|
6493
|
+
console.log(c.dim(` Base URL: ${baseUrl}`));
|
|
6494
|
+
if (!apiKey) {
|
|
6495
|
+
console.log(c.red(" ✗ OPENAI_API_KEY not set"));
|
|
6496
|
+
console.log();
|
|
6497
|
+
return;
|
|
6498
|
+
}
|
|
6499
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
6500
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
6501
|
+
});
|
|
6502
|
+
if (!res.ok) {
|
|
6503
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6504
|
+
console.log(c.dim(` URL: ${baseUrl}/models`));
|
|
6505
|
+
console.log();
|
|
6506
|
+
return;
|
|
6507
|
+
}
|
|
6508
|
+
const payload = await res.json();
|
|
6509
|
+
const items = payload.data ?? [];
|
|
6510
|
+
for (const item of items) {
|
|
6511
|
+
const owner = item.owned_by ? ` (${item.owned_by})` : "";
|
|
6512
|
+
console.log(` ${c.green("●")} ${item.id}${owner}`);
|
|
6513
|
+
}
|
|
6514
|
+
console.log();
|
|
6515
|
+
}
|
|
6516
|
+
async function listGeminiModels() {
|
|
6517
|
+
const baseUrlRaw = (process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/+$/, "");
|
|
6518
|
+
const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
6519
|
+
console.log(c.bold("Gemini Models:"));
|
|
6520
|
+
console.log(c.dim(` Base URL: ${baseUrlRaw}`));
|
|
6521
|
+
if (!apiKey) {
|
|
6522
|
+
console.log(c.red(" ✗ GEMINI_API_KEY not set"));
|
|
6523
|
+
console.log();
|
|
6524
|
+
return;
|
|
6525
|
+
}
|
|
6526
|
+
if (isGoogleGeminiBaseUrl(baseUrlRaw)) {
|
|
6527
|
+
console.log(c.dim(" API Type: gemini"));
|
|
6528
|
+
const url = `${baseUrlRaw}/models`;
|
|
6529
|
+
const res2 = await fetch(url, {
|
|
6530
|
+
headers: { "x-goog-api-key": apiKey }
|
|
6531
|
+
});
|
|
6532
|
+
if (!res2.ok) {
|
|
6533
|
+
console.log(c.red(` ✗ Failed to fetch models (${res2.status})`));
|
|
6534
|
+
console.log(c.dim(` URL: ${url}`));
|
|
6535
|
+
console.log();
|
|
6536
|
+
return;
|
|
6537
|
+
}
|
|
6538
|
+
const payload2 = await res2.json();
|
|
6539
|
+
const items2 = payload2.models ?? [];
|
|
6540
|
+
for (const item of items2) {
|
|
6541
|
+
console.log(` ${c.green("●")} ${item.name}`);
|
|
6542
|
+
}
|
|
6543
|
+
console.log();
|
|
6544
|
+
return;
|
|
6545
|
+
}
|
|
6546
|
+
const openaiBase = normalizeOpenAIBaseUrl(baseUrlRaw);
|
|
6547
|
+
console.log(c.dim(" API Type: openai-compatible"));
|
|
6548
|
+
console.log(c.dim(" Auth: Bearer (GEMINI_API_KEY)"));
|
|
6549
|
+
const res = await fetch(`${openaiBase}/models`, {
|
|
6550
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
6551
|
+
});
|
|
6552
|
+
if (!res.ok) {
|
|
6553
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6554
|
+
console.log(c.dim(` URL: ${openaiBase}/models`));
|
|
6555
|
+
console.log();
|
|
6556
|
+
return;
|
|
6557
|
+
}
|
|
6558
|
+
const payload = await res.json();
|
|
6559
|
+
const items = payload.data ?? [];
|
|
6560
|
+
for (const item of items) {
|
|
6561
|
+
const owner = item.owned_by ? ` (${item.owned_by})` : "";
|
|
6562
|
+
console.log(` ${c.green("●")} ${item.id}${owner}`);
|
|
6563
|
+
}
|
|
6564
|
+
console.log();
|
|
6565
|
+
}
|
|
6566
|
+
async function listModelsSummary() {
|
|
6567
|
+
const provider = getActiveProviderId();
|
|
6568
|
+
const apiType = provider ?? "auto";
|
|
6569
|
+
console.log();
|
|
6570
|
+
console.log(c.bold("Available Models:"));
|
|
6571
|
+
console.log(c.dim(` Active Provider: ${apiType}`));
|
|
6572
|
+
console.log();
|
|
6573
|
+
printModelsHelp();
|
|
6574
|
+
try {
|
|
6575
|
+
await listOpenAIModels();
|
|
6576
|
+
} catch (error) {
|
|
6577
|
+
console.log(c.red(` ✗ OpenAI: ${error instanceof Error ? error.message : String(error)}`));
|
|
6578
|
+
console.log();
|
|
6579
|
+
}
|
|
6580
|
+
try {
|
|
6581
|
+
await listGeminiModels();
|
|
6582
|
+
} catch (error) {
|
|
6583
|
+
console.log(c.red(` ✗ Gemini: ${error instanceof Error ? error.message : String(error)}`));
|
|
6584
|
+
console.log();
|
|
6585
|
+
}
|
|
6586
|
+
await listAnthropicModels();
|
|
6587
|
+
}
|
|
5262
6588
|
function printDebug() {
|
|
5263
|
-
const model =
|
|
6589
|
+
const model = getActiveModel();
|
|
5264
6590
|
const tools = getAllTools();
|
|
5265
6591
|
const systemPrompt = buildSystemPrompt();
|
|
5266
6592
|
const cwd = process.cwd();
|
|
@@ -5271,14 +6597,20 @@ function printDebug() {
|
|
|
5271
6597
|
console.log();
|
|
5272
6598
|
console.log(c.bold("Model:"));
|
|
5273
6599
|
console.log(` ${c.cyan("Current:")} ${model}`);
|
|
6600
|
+
console.log(` ${c.cyan("Provider:")} ${getActiveProviderId() ?? "auto"}`);
|
|
6601
|
+
console.log(` ${c.cyan("Override:")} ${currentModelOverride ?? c.dim("(not set)")}`);
|
|
5274
6602
|
console.log(` ${c.cyan("ANTHROPIC_MODEL:")} ${process.env.ANTHROPIC_MODEL || c.dim("(not set)")}`);
|
|
6603
|
+
console.log(` ${c.cyan("GEMINI_MODEL:")} ${process.env.GEMINI_MODEL || c.dim("(not set)")}`);
|
|
6604
|
+
console.log(` ${c.cyan("GEMINI_BASE_URL:")} ${process.env.GEMINI_BASE_URL || c.dim("(not set)")}`);
|
|
5275
6605
|
console.log(` ${c.cyan("OPENAI_MODEL:")} ${process.env.OPENAI_MODEL || c.dim("(not set)")}`);
|
|
5276
6606
|
console.log(` ${c.cyan("OPENAI_BASE_URL:")} ${process.env.OPENAI_BASE_URL || c.dim("(not set)")}`);
|
|
5277
6607
|
console.log();
|
|
5278
6608
|
console.log(c.bold("API Keys:"));
|
|
5279
6609
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
6610
|
+
const geminiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
5280
6611
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
5281
6612
|
console.log(` ${c.cyan("ANTHROPIC_API_KEY:")} ${anthropicKey ? c.green("✓ set") + c.dim(` (${anthropicKey.slice(0, 8)}...${anthropicKey.slice(-4)})`) : c.red("✗ not set")}`);
|
|
6613
|
+
console.log(` ${c.cyan("GEMINI_API_KEY:")} ${geminiKey ? c.green("✓ set") + c.dim(` (${geminiKey.slice(0, 8)}...${geminiKey.slice(-4)})`) : c.red("✗ not set")}`);
|
|
5282
6614
|
console.log(` ${c.cyan("OPENAI_API_KEY:")} ${openaiKey ? c.green("✓ set") + c.dim(` (${openaiKey.slice(0, 8)}...${openaiKey.slice(-4)})`) : c.red("✗ not set")}`);
|
|
5283
6615
|
console.log();
|
|
5284
6616
|
console.log(c.bold("Environment:"));
|
|
@@ -5342,19 +6674,94 @@ function formatToolInput(name, input) {
|
|
|
5342
6674
|
return JSON.stringify(input).slice(0, 50);
|
|
5343
6675
|
}
|
|
5344
6676
|
}
|
|
5345
|
-
function
|
|
6677
|
+
function getDefaultProviderFromEnv() {
|
|
5346
6678
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
5347
|
-
return
|
|
6679
|
+
return "anthropic";
|
|
5348
6680
|
}
|
|
5349
6681
|
if (process.env.OPENAI_API_KEY) {
|
|
5350
|
-
return
|
|
6682
|
+
return "openai";
|
|
6683
|
+
}
|
|
6684
|
+
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
6685
|
+
return "gemini";
|
|
6686
|
+
}
|
|
6687
|
+
return null;
|
|
6688
|
+
}
|
|
6689
|
+
function inferProviderFromModel(model) {
|
|
6690
|
+
const normalized = model.toLowerCase();
|
|
6691
|
+
if (normalized.startsWith("claude")) {
|
|
6692
|
+
return "anthropic";
|
|
6693
|
+
}
|
|
6694
|
+
if (normalized.startsWith("gpt") || normalized.startsWith("o1") || normalized.startsWith("chatgpt")) {
|
|
6695
|
+
return "openai";
|
|
6696
|
+
}
|
|
6697
|
+
if (normalized.startsWith("gemini") || normalized.startsWith("models/gemini")) {
|
|
6698
|
+
return "gemini";
|
|
6699
|
+
}
|
|
6700
|
+
return null;
|
|
6701
|
+
}
|
|
6702
|
+
function getDefaultModelForProvider(providerId) {
|
|
6703
|
+
if (providerId === "anthropic") {
|
|
6704
|
+
return process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
|
|
6705
|
+
}
|
|
6706
|
+
if (providerId === "gemini") {
|
|
6707
|
+
return process.env.GEMINI_MODEL || "gemini-1.5-pro";
|
|
6708
|
+
}
|
|
6709
|
+
return process.env.OPENAI_MODEL || "gpt-5.2";
|
|
6710
|
+
}
|
|
6711
|
+
function getActiveProviderId() {
|
|
6712
|
+
if (currentProviderId) {
|
|
6713
|
+
return currentProviderId;
|
|
6714
|
+
}
|
|
6715
|
+
if (currentModelOverride) {
|
|
6716
|
+
return inferProviderFromModel(currentModelOverride);
|
|
6717
|
+
}
|
|
6718
|
+
return getDefaultProviderFromEnv();
|
|
6719
|
+
}
|
|
6720
|
+
function getActiveModel() {
|
|
6721
|
+
if (currentModelOverride) {
|
|
6722
|
+
return currentModelOverride;
|
|
6723
|
+
}
|
|
6724
|
+
const provider = getActiveProviderId();
|
|
6725
|
+
if (provider) {
|
|
6726
|
+
return getDefaultModelForProvider(provider);
|
|
5351
6727
|
}
|
|
5352
6728
|
return "claude-sonnet-4-20250514";
|
|
5353
6729
|
}
|
|
6730
|
+
function parseProvider(arg) {
|
|
6731
|
+
const normalized = arg.toLowerCase();
|
|
6732
|
+
if (normalized === "anthropic" || normalized === "claude") {
|
|
6733
|
+
return "anthropic";
|
|
6734
|
+
}
|
|
6735
|
+
if (normalized === "openai" || normalized === "gpt") {
|
|
6736
|
+
return "openai";
|
|
6737
|
+
}
|
|
6738
|
+
if (normalized === "gemini" || normalized === "google") {
|
|
6739
|
+
return "gemini";
|
|
6740
|
+
}
|
|
6741
|
+
return null;
|
|
6742
|
+
}
|
|
6743
|
+
function createProvider(providerId) {
|
|
6744
|
+
if (providerId === "anthropic") {
|
|
6745
|
+
return new AnthropicProvider;
|
|
6746
|
+
}
|
|
6747
|
+
if (providerId === "gemini") {
|
|
6748
|
+
return new GeminiProvider({
|
|
6749
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
6750
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
6751
|
+
});
|
|
6752
|
+
}
|
|
6753
|
+
return new OpenAIProvider({
|
|
6754
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
6755
|
+
baseUrl: process.env.OPENAI_BASE_URL
|
|
6756
|
+
});
|
|
6757
|
+
}
|
|
5354
6758
|
async function getSession() {
|
|
5355
6759
|
if (!session) {
|
|
6760
|
+
const providerId = getActiveProviderId();
|
|
6761
|
+
const provider = providerId ? createProvider(providerId) : undefined;
|
|
5356
6762
|
session = await createSession({
|
|
5357
|
-
model:
|
|
6763
|
+
model: getActiveModel(),
|
|
6764
|
+
provider,
|
|
5358
6765
|
tools: getAllTools(),
|
|
5359
6766
|
systemPrompt: buildSystemPrompt()
|
|
5360
6767
|
});
|
|
@@ -5412,7 +6819,9 @@ async function handleInput(input) {
|
|
|
5412
6819
|
return true;
|
|
5413
6820
|
}
|
|
5414
6821
|
if (trimmed.startsWith("/")) {
|
|
5415
|
-
const
|
|
6822
|
+
const parts = trimmed.split(/\s+/);
|
|
6823
|
+
const cmd = parts[0].toLowerCase();
|
|
6824
|
+
const args = parts.slice(1);
|
|
5416
6825
|
switch (cmd) {
|
|
5417
6826
|
case "/help":
|
|
5418
6827
|
printInteractiveHelp();
|
|
@@ -5436,6 +6845,9 @@ async function handleInput(input) {
|
|
|
5436
6845
|
case "/skills":
|
|
5437
6846
|
await printSkills();
|
|
5438
6847
|
return true;
|
|
6848
|
+
case "/models":
|
|
6849
|
+
await handleModelsCommand(args);
|
|
6850
|
+
return true;
|
|
5439
6851
|
case "/todos":
|
|
5440
6852
|
printTodos();
|
|
5441
6853
|
return true;
|
|
@@ -5468,9 +6880,9 @@ Error: ${error instanceof Error ? error.message : String(error)}
|
|
|
5468
6880
|
return true;
|
|
5469
6881
|
}
|
|
5470
6882
|
async function runQuickQuery(query) {
|
|
5471
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
6883
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
|
|
5472
6884
|
console.error(c.red("Error: No API key found"));
|
|
5473
|
-
console.error(c.dim("Set ANTHROPIC_API_KEY or
|
|
6885
|
+
console.error(c.dim("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable"));
|
|
5474
6886
|
process.exit(1);
|
|
5475
6887
|
}
|
|
5476
6888
|
try {
|
|
@@ -5484,9 +6896,9 @@ async function runQuickQuery(query) {
|
|
|
5484
6896
|
}
|
|
5485
6897
|
}
|
|
5486
6898
|
async function runInteractive() {
|
|
5487
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
6899
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
|
|
5488
6900
|
console.error(c.red("Error: No API key found"));
|
|
5489
|
-
console.error(c.dim("Set ANTHROPIC_API_KEY or
|
|
6901
|
+
console.error(c.dim("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable"));
|
|
5490
6902
|
process.exit(1);
|
|
5491
6903
|
}
|
|
5492
6904
|
setTodoChangeCallback(() => {});
|