formagent-sdk 0.2.0 → 0.3.0
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/dist/cli/index.js +1506 -130
- package/dist/index.js +1152 -27
- package/package.json +1 -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)` : "";
|
|
@@ -1753,7 +1754,7 @@ class SessionImpl {
|
|
|
1753
1754
|
};
|
|
1754
1755
|
}
|
|
1755
1756
|
if (this.hooksManager) {
|
|
1756
|
-
const postResult = await this.hooksManager.runPostToolUse(
|
|
1757
|
+
const postResult = await this.hooksManager.runPostToolUse(effectiveToolName, toolInput, toolResponse, block.id, abortSignal);
|
|
1757
1758
|
if (postResult.systemMessage) {
|
|
1758
1759
|
systemMessage = postResult.systemMessage;
|
|
1759
1760
|
}
|
|
@@ -2395,7 +2396,7 @@ class OpenAIProvider {
|
|
|
2395
2396
|
}
|
|
2396
2397
|
this.config = {
|
|
2397
2398
|
apiKey,
|
|
2398
|
-
baseUrl: config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
|
|
2399
|
+
baseUrl: this.normalizeBaseUrl(config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"),
|
|
2399
2400
|
organization: config.organization,
|
|
2400
2401
|
defaultMaxTokens: config.defaultMaxTokens ?? 4096
|
|
2401
2402
|
};
|
|
@@ -2404,8 +2405,23 @@ class OpenAIProvider {
|
|
|
2404
2405
|
return this.supportedModels.some((pattern) => pattern.test(model));
|
|
2405
2406
|
}
|
|
2406
2407
|
async complete(request) {
|
|
2408
|
+
if (this.usesResponsesApi(request.config.model)) {
|
|
2409
|
+
const openaiRequest2 = this.buildResponsesRequest(request, false);
|
|
2410
|
+
const response2 = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2411
|
+
method: "POST",
|
|
2412
|
+
headers: this.getHeaders(),
|
|
2413
|
+
body: JSON.stringify(openaiRequest2),
|
|
2414
|
+
signal: request.abortSignal
|
|
2415
|
+
});
|
|
2416
|
+
if (!response2.ok) {
|
|
2417
|
+
const error = await response2.text();
|
|
2418
|
+
throw new Error(`OpenAI API error: ${response2.status} ${error}`);
|
|
2419
|
+
}
|
|
2420
|
+
const data2 = await response2.json();
|
|
2421
|
+
return this.convertResponsesResponse(data2);
|
|
2422
|
+
}
|
|
2407
2423
|
const openaiRequest = this.buildRequest(request, false);
|
|
2408
|
-
|
|
2424
|
+
let response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
2409
2425
|
method: "POST",
|
|
2410
2426
|
headers: this.getHeaders(),
|
|
2411
2427
|
body: JSON.stringify(openaiRequest),
|
|
@@ -2413,14 +2429,43 @@ class OpenAIProvider {
|
|
|
2413
2429
|
});
|
|
2414
2430
|
if (!response.ok) {
|
|
2415
2431
|
const error = await response.text();
|
|
2432
|
+
if (this.shouldFallbackToResponses(response.status, error)) {
|
|
2433
|
+
const fallbackRequest = this.buildResponsesRequest(request, false);
|
|
2434
|
+
response = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2435
|
+
method: "POST",
|
|
2436
|
+
headers: this.getHeaders(),
|
|
2437
|
+
body: JSON.stringify(fallbackRequest),
|
|
2438
|
+
signal: request.abortSignal
|
|
2439
|
+
});
|
|
2440
|
+
if (!response.ok) {
|
|
2441
|
+
const fallbackError = await response.text();
|
|
2442
|
+
throw new Error(`OpenAI API error: ${response.status} ${fallbackError}`);
|
|
2443
|
+
}
|
|
2444
|
+
const data2 = await response.json();
|
|
2445
|
+
return this.convertResponsesResponse(data2);
|
|
2446
|
+
}
|
|
2416
2447
|
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
2417
2448
|
}
|
|
2418
2449
|
const data = await response.json();
|
|
2419
2450
|
return this.convertResponse(data);
|
|
2420
2451
|
}
|
|
2421
2452
|
async stream(request, options) {
|
|
2453
|
+
if (this.usesResponsesApi(request.config.model)) {
|
|
2454
|
+
const openaiRequest2 = this.buildResponsesRequest(request, true);
|
|
2455
|
+
const response2 = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2456
|
+
method: "POST",
|
|
2457
|
+
headers: this.getHeaders(),
|
|
2458
|
+
body: JSON.stringify(openaiRequest2),
|
|
2459
|
+
signal: request.abortSignal
|
|
2460
|
+
});
|
|
2461
|
+
if (!response2.ok) {
|
|
2462
|
+
const error = await response2.text();
|
|
2463
|
+
throw new Error(`OpenAI API error: ${response2.status} ${error}`);
|
|
2464
|
+
}
|
|
2465
|
+
return this.createResponsesStreamIterator(response2.body, options);
|
|
2466
|
+
}
|
|
2422
2467
|
const openaiRequest = this.buildRequest(request, true);
|
|
2423
|
-
|
|
2468
|
+
let response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
2424
2469
|
method: "POST",
|
|
2425
2470
|
headers: this.getHeaders(),
|
|
2426
2471
|
body: JSON.stringify(openaiRequest),
|
|
@@ -2428,6 +2473,20 @@ class OpenAIProvider {
|
|
|
2428
2473
|
});
|
|
2429
2474
|
if (!response.ok) {
|
|
2430
2475
|
const error = await response.text();
|
|
2476
|
+
if (this.shouldFallbackToResponses(response.status, error)) {
|
|
2477
|
+
const fallbackRequest = this.buildResponsesRequest(request, true);
|
|
2478
|
+
response = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2479
|
+
method: "POST",
|
|
2480
|
+
headers: this.getHeaders(),
|
|
2481
|
+
body: JSON.stringify(fallbackRequest),
|
|
2482
|
+
signal: request.abortSignal
|
|
2483
|
+
});
|
|
2484
|
+
if (!response.ok) {
|
|
2485
|
+
const fallbackError = await response.text();
|
|
2486
|
+
throw new Error(`OpenAI API error: ${response.status} ${fallbackError}`);
|
|
2487
|
+
}
|
|
2488
|
+
return this.createResponsesStreamIterator(response.body, options);
|
|
2489
|
+
}
|
|
2431
2490
|
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
2432
2491
|
}
|
|
2433
2492
|
return this.createStreamIterator(response.body, options);
|
|
@@ -2435,10 +2494,10 @@ class OpenAIProvider {
|
|
|
2435
2494
|
buildRequest(request, stream) {
|
|
2436
2495
|
const messages = this.convertMessages(request.messages, request.systemPrompt);
|
|
2437
2496
|
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
2438
|
-
|
|
2497
|
+
const maxTokens = request.config.maxTokens ?? this.config.defaultMaxTokens;
|
|
2498
|
+
const openaiRequest = {
|
|
2439
2499
|
model: request.config.model,
|
|
2440
2500
|
messages,
|
|
2441
|
-
max_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
2442
2501
|
temperature: request.config.temperature,
|
|
2443
2502
|
top_p: request.config.topP,
|
|
2444
2503
|
stop: request.config.stopSequences,
|
|
@@ -2446,6 +2505,162 @@ class OpenAIProvider {
|
|
|
2446
2505
|
stream_options: stream ? { include_usage: true } : undefined,
|
|
2447
2506
|
tools
|
|
2448
2507
|
};
|
|
2508
|
+
if (this.usesMaxCompletionTokens(request.config.model)) {
|
|
2509
|
+
openaiRequest.max_completion_tokens = maxTokens;
|
|
2510
|
+
} else {
|
|
2511
|
+
openaiRequest.max_tokens = maxTokens;
|
|
2512
|
+
}
|
|
2513
|
+
return openaiRequest;
|
|
2514
|
+
}
|
|
2515
|
+
buildResponsesRequest(request, stream) {
|
|
2516
|
+
const input = this.convertResponsesInput(request.messages, request.systemPrompt);
|
|
2517
|
+
const tools = request.tools ? this.convertResponsesTools(request.tools) : undefined;
|
|
2518
|
+
return {
|
|
2519
|
+
model: request.config.model,
|
|
2520
|
+
input,
|
|
2521
|
+
max_output_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
2522
|
+
temperature: request.config.temperature,
|
|
2523
|
+
top_p: request.config.topP,
|
|
2524
|
+
stop: request.config.stopSequences,
|
|
2525
|
+
stream,
|
|
2526
|
+
tools
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
usesMaxCompletionTokens(model) {
|
|
2530
|
+
return /^gpt-5/.test(model) || /^o1/.test(model);
|
|
2531
|
+
}
|
|
2532
|
+
usesResponsesApi(model) {
|
|
2533
|
+
return /^gpt-5/.test(model) || /^o1/.test(model);
|
|
2534
|
+
}
|
|
2535
|
+
shouldFallbackToResponses(status, errorText) {
|
|
2536
|
+
if (status !== 404) {
|
|
2537
|
+
return false;
|
|
2538
|
+
}
|
|
2539
|
+
const normalized = errorText.toLowerCase();
|
|
2540
|
+
return normalized.includes("/chat/completions") && normalized.includes("not found");
|
|
2541
|
+
}
|
|
2542
|
+
convertResponsesInput(messages, systemPrompt) {
|
|
2543
|
+
const input = [];
|
|
2544
|
+
if (systemPrompt) {
|
|
2545
|
+
input.push({ role: "system", content: systemPrompt });
|
|
2546
|
+
}
|
|
2547
|
+
for (const msg of messages) {
|
|
2548
|
+
if (msg.role === "system") {
|
|
2549
|
+
input.push({
|
|
2550
|
+
role: "system",
|
|
2551
|
+
content: typeof msg.content === "string" ? msg.content : ""
|
|
2552
|
+
});
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
if (typeof msg.content === "string") {
|
|
2556
|
+
if (msg.role === "user") {
|
|
2557
|
+
input.push({
|
|
2558
|
+
role: "user",
|
|
2559
|
+
content: [{ type: "input_text", text: msg.content }]
|
|
2560
|
+
});
|
|
2561
|
+
} else {
|
|
2562
|
+
input.push({
|
|
2563
|
+
role: "assistant",
|
|
2564
|
+
content: [{ type: "output_text", text: msg.content }]
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
const userContent = [];
|
|
2570
|
+
const assistantContent = [];
|
|
2571
|
+
for (const block of msg.content) {
|
|
2572
|
+
if (block.type === "text") {
|
|
2573
|
+
if (msg.role === "user") {
|
|
2574
|
+
userContent.push({ type: "input_text", text: block.text });
|
|
2575
|
+
} else if (msg.role === "assistant") {
|
|
2576
|
+
assistantContent.push({ type: "output_text", text: block.text });
|
|
2577
|
+
}
|
|
2578
|
+
} else if (block.type === "image" && msg.role === "user") {
|
|
2579
|
+
if (block.source.type === "base64") {
|
|
2580
|
+
userContent.push({
|
|
2581
|
+
type: "input_image",
|
|
2582
|
+
image_url: `data:${block.source.media_type};base64,${block.source.data}`
|
|
2583
|
+
});
|
|
2584
|
+
} else if (block.source.type === "url") {
|
|
2585
|
+
userContent.push({
|
|
2586
|
+
type: "input_image",
|
|
2587
|
+
image_url: block.source.url
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
} else if (block.type === "tool_use") {
|
|
2591
|
+
input.push({
|
|
2592
|
+
type: "function_call",
|
|
2593
|
+
call_id: block.id,
|
|
2594
|
+
name: block.name,
|
|
2595
|
+
arguments: JSON.stringify(block.input)
|
|
2596
|
+
});
|
|
2597
|
+
} else if (block.type === "tool_result") {
|
|
2598
|
+
input.push({
|
|
2599
|
+
type: "function_call_output",
|
|
2600
|
+
call_id: block.tool_use_id,
|
|
2601
|
+
output: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
if (msg.role === "user" && userContent.length > 0) {
|
|
2606
|
+
input.push({ role: "user", content: userContent });
|
|
2607
|
+
} else if (msg.role === "assistant" && assistantContent.length > 0) {
|
|
2608
|
+
input.push({ role: "assistant", content: assistantContent });
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
return input;
|
|
2612
|
+
}
|
|
2613
|
+
convertResponsesResponse(data) {
|
|
2614
|
+
const content = [];
|
|
2615
|
+
for (const item of data.output ?? []) {
|
|
2616
|
+
if (item.type === "message" && item.content) {
|
|
2617
|
+
for (const part of item.content) {
|
|
2618
|
+
if (part.type === "output_text") {
|
|
2619
|
+
content.push({ type: "text", text: part.text });
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
} else if (item.type === "function_call" && item.call_id && item.name) {
|
|
2623
|
+
content.push({
|
|
2624
|
+
type: "tool_use",
|
|
2625
|
+
id: item.call_id,
|
|
2626
|
+
name: item.name,
|
|
2627
|
+
input: item.arguments ? JSON.parse(item.arguments) : {}
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return {
|
|
2632
|
+
id: data.id,
|
|
2633
|
+
model: data.model,
|
|
2634
|
+
content,
|
|
2635
|
+
stopReason: "end_turn",
|
|
2636
|
+
stopSequence: null,
|
|
2637
|
+
usage: {
|
|
2638
|
+
input_tokens: data.usage?.input_tokens ?? 0,
|
|
2639
|
+
output_tokens: data.usage?.output_tokens ?? 0
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
normalizeBaseUrl(baseUrl) {
|
|
2644
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
2645
|
+
try {
|
|
2646
|
+
const url = new URL(trimmed);
|
|
2647
|
+
const path2 = url.pathname.replace(/\/+$/, "");
|
|
2648
|
+
if (path2 === "" || path2 === "/") {
|
|
2649
|
+
url.pathname = "/v1";
|
|
2650
|
+
return url.toString().replace(/\/+$/, "");
|
|
2651
|
+
}
|
|
2652
|
+
if (path2.endsWith("/openai")) {
|
|
2653
|
+
url.pathname = `${path2}/v1`;
|
|
2654
|
+
return url.toString().replace(/\/+$/, "");
|
|
2655
|
+
}
|
|
2656
|
+
if (!/\/v\d/.test(path2)) {
|
|
2657
|
+
url.pathname = `${path2}/v1`;
|
|
2658
|
+
return url.toString().replace(/\/+$/, "");
|
|
2659
|
+
}
|
|
2660
|
+
return url.toString().replace(/\/+$/, "");
|
|
2661
|
+
} catch {
|
|
2662
|
+
return trimmed;
|
|
2663
|
+
}
|
|
2449
2664
|
}
|
|
2450
2665
|
convertMessages(messages, systemPrompt) {
|
|
2451
2666
|
const result = [];
|
|
@@ -2528,6 +2743,14 @@ class OpenAIProvider {
|
|
|
2528
2743
|
}
|
|
2529
2744
|
}));
|
|
2530
2745
|
}
|
|
2746
|
+
convertResponsesTools(tools) {
|
|
2747
|
+
return tools.map((tool) => ({
|
|
2748
|
+
type: "function",
|
|
2749
|
+
name: tool.name,
|
|
2750
|
+
description: tool.description,
|
|
2751
|
+
parameters: tool.inputSchema
|
|
2752
|
+
}));
|
|
2753
|
+
}
|
|
2531
2754
|
convertResponse(data) {
|
|
2532
2755
|
const choice = data.choices[0];
|
|
2533
2756
|
const content = [];
|
|
@@ -2623,10 +2846,765 @@ class OpenAIProvider {
|
|
|
2623
2846
|
usage: { input_tokens: 0, output_tokens: 0 }
|
|
2624
2847
|
}
|
|
2625
2848
|
};
|
|
2626
|
-
options?.onEvent?.(startEvent);
|
|
2627
|
-
yield startEvent;
|
|
2849
|
+
options?.onEvent?.(startEvent);
|
|
2850
|
+
yield startEvent;
|
|
2851
|
+
}
|
|
2852
|
+
if (delta?.content) {
|
|
2853
|
+
if (!textBlockStarted) {
|
|
2854
|
+
textBlockStarted = true;
|
|
2855
|
+
const startText = {
|
|
2856
|
+
type: "content_block_start",
|
|
2857
|
+
index: 0,
|
|
2858
|
+
content_block: { type: "text", text: "" }
|
|
2859
|
+
};
|
|
2860
|
+
options?.onEvent?.(startText);
|
|
2861
|
+
yield startText;
|
|
2862
|
+
}
|
|
2863
|
+
const textEvent = {
|
|
2864
|
+
type: "content_block_delta",
|
|
2865
|
+
index: 0,
|
|
2866
|
+
delta: {
|
|
2867
|
+
type: "text_delta",
|
|
2868
|
+
text: delta.content
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
options?.onText?.(delta.content);
|
|
2872
|
+
options?.onEvent?.(textEvent);
|
|
2873
|
+
yield textEvent;
|
|
2874
|
+
}
|
|
2875
|
+
if (delta?.tool_calls) {
|
|
2876
|
+
for (const tc of delta.tool_calls) {
|
|
2877
|
+
const index = tc.index;
|
|
2878
|
+
const blockIndex = 1 + index;
|
|
2879
|
+
if (!toolCalls.has(index)) {
|
|
2880
|
+
toolCalls.set(index, {
|
|
2881
|
+
id: tc.id || "",
|
|
2882
|
+
name: tc.function?.name || "",
|
|
2883
|
+
arguments: tc.function?.arguments || ""
|
|
2884
|
+
});
|
|
2885
|
+
const startEvent = {
|
|
2886
|
+
type: "content_block_start",
|
|
2887
|
+
index: blockIndex,
|
|
2888
|
+
content_block: {
|
|
2889
|
+
type: "tool_use",
|
|
2890
|
+
id: tc.id || "",
|
|
2891
|
+
name: tc.function?.name || "",
|
|
2892
|
+
input: {}
|
|
2893
|
+
}
|
|
2894
|
+
};
|
|
2895
|
+
options?.onEvent?.(startEvent);
|
|
2896
|
+
yield startEvent;
|
|
2897
|
+
} else {
|
|
2898
|
+
const existing = toolCalls.get(index);
|
|
2899
|
+
if (tc.id)
|
|
2900
|
+
existing.id = tc.id;
|
|
2901
|
+
if (tc.function?.name)
|
|
2902
|
+
existing.name = tc.function.name;
|
|
2903
|
+
if (tc.function?.arguments)
|
|
2904
|
+
existing.arguments += tc.function.arguments;
|
|
2905
|
+
}
|
|
2906
|
+
if (tc.function?.arguments) {
|
|
2907
|
+
const deltaEvent = {
|
|
2908
|
+
type: "content_block_delta",
|
|
2909
|
+
index: blockIndex,
|
|
2910
|
+
delta: {
|
|
2911
|
+
type: "input_json_delta",
|
|
2912
|
+
partial_json: tc.function.arguments
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
options?.onEvent?.(deltaEvent);
|
|
2916
|
+
yield deltaEvent;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
if (finishReason) {
|
|
2921
|
+
finished = true;
|
|
2922
|
+
if (textBlockStarted) {
|
|
2923
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
2924
|
+
options?.onEvent?.(stopText);
|
|
2925
|
+
yield stopText;
|
|
2926
|
+
}
|
|
2927
|
+
for (const [index, tc] of toolCalls) {
|
|
2928
|
+
const stopEvent2 = {
|
|
2929
|
+
type: "content_block_stop",
|
|
2930
|
+
index: 1 + index
|
|
2931
|
+
};
|
|
2932
|
+
options?.onEvent?.(stopEvent2);
|
|
2933
|
+
yield stopEvent2;
|
|
2934
|
+
try {
|
|
2935
|
+
const input = JSON.parse(tc.arguments);
|
|
2936
|
+
options?.onToolUse?.({ id: tc.id, name: tc.name, input });
|
|
2937
|
+
} catch {}
|
|
2938
|
+
}
|
|
2939
|
+
const messageDelta = {
|
|
2940
|
+
type: "message_delta",
|
|
2941
|
+
delta: {
|
|
2942
|
+
stop_reason: self.convertStopReason(finishReason),
|
|
2943
|
+
stop_sequence: null
|
|
2944
|
+
},
|
|
2945
|
+
usage: {
|
|
2946
|
+
output_tokens: json.usage?.completion_tokens ?? 0,
|
|
2947
|
+
input_tokens: json.usage?.prompt_tokens ?? 0
|
|
2948
|
+
}
|
|
2949
|
+
};
|
|
2950
|
+
options?.onEvent?.(messageDelta);
|
|
2951
|
+
yield messageDelta;
|
|
2952
|
+
const stopEvent = { type: "message_stop" };
|
|
2953
|
+
options?.onEvent?.(stopEvent);
|
|
2954
|
+
yield stopEvent;
|
|
2955
|
+
}
|
|
2956
|
+
} catch {}
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
} finally {
|
|
2961
|
+
reader.releaseLock();
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
createResponsesStreamIterator(body, options) {
|
|
2967
|
+
const self = this;
|
|
2968
|
+
return {
|
|
2969
|
+
async* [Symbol.asyncIterator]() {
|
|
2970
|
+
const reader = body.getReader();
|
|
2971
|
+
const decoder = new TextDecoder;
|
|
2972
|
+
let buffer = "";
|
|
2973
|
+
let emittedMessageStart = false;
|
|
2974
|
+
let textBlockStarted = false;
|
|
2975
|
+
let finished = false;
|
|
2976
|
+
const toolCalls = new Map;
|
|
2977
|
+
let nextToolBlockIndex = 1;
|
|
2978
|
+
const ensureMessageStart = (id, model) => {
|
|
2979
|
+
if (emittedMessageStart)
|
|
2980
|
+
return;
|
|
2981
|
+
emittedMessageStart = true;
|
|
2982
|
+
const startEvent = {
|
|
2983
|
+
type: "message_start",
|
|
2984
|
+
message: {
|
|
2985
|
+
id: id ?? "",
|
|
2986
|
+
type: "message",
|
|
2987
|
+
role: "assistant",
|
|
2988
|
+
content: [],
|
|
2989
|
+
model: model ?? "",
|
|
2990
|
+
stop_reason: null,
|
|
2991
|
+
stop_sequence: null,
|
|
2992
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
2993
|
+
}
|
|
2994
|
+
};
|
|
2995
|
+
options?.onEvent?.(startEvent);
|
|
2996
|
+
return startEvent;
|
|
2997
|
+
};
|
|
2998
|
+
try {
|
|
2999
|
+
while (true) {
|
|
3000
|
+
const { done, value } = await reader.read();
|
|
3001
|
+
if (done)
|
|
3002
|
+
break;
|
|
3003
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3004
|
+
const lines = buffer.split(`
|
|
3005
|
+
`);
|
|
3006
|
+
buffer = lines.pop() || "";
|
|
3007
|
+
for (const line of lines) {
|
|
3008
|
+
if (!line.startsWith("data: "))
|
|
3009
|
+
continue;
|
|
3010
|
+
const data = line.slice(6).trim();
|
|
3011
|
+
if (!data)
|
|
3012
|
+
continue;
|
|
3013
|
+
if (data === "[DONE]") {
|
|
3014
|
+
if (!finished) {
|
|
3015
|
+
const stopEvent = { type: "message_stop" };
|
|
3016
|
+
options?.onEvent?.(stopEvent);
|
|
3017
|
+
yield stopEvent;
|
|
3018
|
+
}
|
|
3019
|
+
finished = true;
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
let payload;
|
|
3023
|
+
try {
|
|
3024
|
+
payload = JSON.parse(data);
|
|
3025
|
+
} catch {
|
|
3026
|
+
continue;
|
|
3027
|
+
}
|
|
3028
|
+
const type = payload?.type;
|
|
3029
|
+
if (type === "response.created") {
|
|
3030
|
+
const startEvent = ensureMessageStart(payload.response?.id, payload.response?.model);
|
|
3031
|
+
if (startEvent)
|
|
3032
|
+
yield startEvent;
|
|
3033
|
+
continue;
|
|
3034
|
+
}
|
|
3035
|
+
if (!emittedMessageStart) {
|
|
3036
|
+
const startEvent = ensureMessageStart(payload?.response?.id, payload?.response?.model);
|
|
3037
|
+
if (startEvent)
|
|
3038
|
+
yield startEvent;
|
|
3039
|
+
}
|
|
3040
|
+
if (type === "response.output_text.delta") {
|
|
3041
|
+
if (!textBlockStarted) {
|
|
3042
|
+
textBlockStarted = true;
|
|
3043
|
+
const startText = {
|
|
3044
|
+
type: "content_block_start",
|
|
3045
|
+
index: 0,
|
|
3046
|
+
content_block: { type: "text", text: "" }
|
|
3047
|
+
};
|
|
3048
|
+
options?.onEvent?.(startText);
|
|
3049
|
+
yield startText;
|
|
3050
|
+
}
|
|
3051
|
+
const textDelta = payload.delta ?? "";
|
|
3052
|
+
if (textDelta) {
|
|
3053
|
+
const textEvent = {
|
|
3054
|
+
type: "content_block_delta",
|
|
3055
|
+
index: 0,
|
|
3056
|
+
delta: { type: "text_delta", text: textDelta }
|
|
3057
|
+
};
|
|
3058
|
+
options?.onText?.(textDelta);
|
|
3059
|
+
options?.onEvent?.(textEvent);
|
|
3060
|
+
yield textEvent;
|
|
3061
|
+
}
|
|
3062
|
+
} else if (type === "response.output_item.added") {
|
|
3063
|
+
const item = payload.item;
|
|
3064
|
+
if (item?.type === "function_call") {
|
|
3065
|
+
const blockIndex = nextToolBlockIndex++;
|
|
3066
|
+
const callId = item.call_id ?? item.id ?? "";
|
|
3067
|
+
toolCalls.set(item.id, {
|
|
3068
|
+
callId,
|
|
3069
|
+
name: item.name ?? "",
|
|
3070
|
+
arguments: item.arguments ?? "",
|
|
3071
|
+
blockIndex,
|
|
3072
|
+
done: false
|
|
3073
|
+
});
|
|
3074
|
+
const startEvent = {
|
|
3075
|
+
type: "content_block_start",
|
|
3076
|
+
index: blockIndex,
|
|
3077
|
+
content_block: {
|
|
3078
|
+
type: "tool_use",
|
|
3079
|
+
id: callId,
|
|
3080
|
+
name: item.name ?? "",
|
|
3081
|
+
input: {}
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
options?.onEvent?.(startEvent);
|
|
3085
|
+
yield startEvent;
|
|
3086
|
+
if (item.arguments) {
|
|
3087
|
+
const deltaEvent = {
|
|
3088
|
+
type: "content_block_delta",
|
|
3089
|
+
index: blockIndex,
|
|
3090
|
+
delta: {
|
|
3091
|
+
type: "input_json_delta",
|
|
3092
|
+
partial_json: item.arguments
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
options?.onEvent?.(deltaEvent);
|
|
3096
|
+
yield deltaEvent;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
} else if (type === "response.function_call_arguments.delta") {
|
|
3100
|
+
const entry = toolCalls.get(payload.item_id);
|
|
3101
|
+
if (entry && payload.delta) {
|
|
3102
|
+
entry.arguments += payload.delta;
|
|
3103
|
+
const deltaEvent = {
|
|
3104
|
+
type: "content_block_delta",
|
|
3105
|
+
index: entry.blockIndex,
|
|
3106
|
+
delta: { type: "input_json_delta", partial_json: payload.delta }
|
|
3107
|
+
};
|
|
3108
|
+
options?.onEvent?.(deltaEvent);
|
|
3109
|
+
yield deltaEvent;
|
|
3110
|
+
}
|
|
3111
|
+
} else if (type === "response.output_item.done") {
|
|
3112
|
+
const item = payload.item;
|
|
3113
|
+
if (item?.type === "function_call") {
|
|
3114
|
+
const entry = toolCalls.get(item.id);
|
|
3115
|
+
if (entry && !entry.done) {
|
|
3116
|
+
entry.done = true;
|
|
3117
|
+
const stopEvent = {
|
|
3118
|
+
type: "content_block_stop",
|
|
3119
|
+
index: entry.blockIndex
|
|
3120
|
+
};
|
|
3121
|
+
options?.onEvent?.(stopEvent);
|
|
3122
|
+
yield stopEvent;
|
|
3123
|
+
try {
|
|
3124
|
+
const input = entry.arguments ? JSON.parse(entry.arguments) : {};
|
|
3125
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input });
|
|
3126
|
+
} catch {
|
|
3127
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input: {} });
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
} else if (type === "response.completed" || type === "response.incomplete") {
|
|
3132
|
+
finished = true;
|
|
3133
|
+
if (textBlockStarted) {
|
|
3134
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3135
|
+
options?.onEvent?.(stopText);
|
|
3136
|
+
yield stopText;
|
|
3137
|
+
}
|
|
3138
|
+
for (const entry of toolCalls.values()) {
|
|
3139
|
+
if (entry.done)
|
|
3140
|
+
continue;
|
|
3141
|
+
entry.done = true;
|
|
3142
|
+
const stopEvent2 = {
|
|
3143
|
+
type: "content_block_stop",
|
|
3144
|
+
index: entry.blockIndex
|
|
3145
|
+
};
|
|
3146
|
+
options?.onEvent?.(stopEvent2);
|
|
3147
|
+
yield stopEvent2;
|
|
3148
|
+
try {
|
|
3149
|
+
const input = entry.arguments ? JSON.parse(entry.arguments) : {};
|
|
3150
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input });
|
|
3151
|
+
} catch {
|
|
3152
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input: {} });
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
const finishReason = payload.response?.incomplete_details?.reason;
|
|
3156
|
+
const messageDelta = {
|
|
3157
|
+
type: "message_delta",
|
|
3158
|
+
delta: {
|
|
3159
|
+
stop_reason: self.convertResponsesStopReason(finishReason),
|
|
3160
|
+
stop_sequence: null
|
|
3161
|
+
},
|
|
3162
|
+
usage: {
|
|
3163
|
+
output_tokens: payload.response?.usage?.output_tokens ?? 0,
|
|
3164
|
+
input_tokens: payload.response?.usage?.input_tokens ?? 0
|
|
3165
|
+
}
|
|
3166
|
+
};
|
|
3167
|
+
options?.onEvent?.(messageDelta);
|
|
3168
|
+
yield messageDelta;
|
|
3169
|
+
const stopEvent = { type: "message_stop" };
|
|
3170
|
+
options?.onEvent?.(stopEvent);
|
|
3171
|
+
yield stopEvent;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
} finally {
|
|
3176
|
+
reader.releaseLock();
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
getHeaders() {
|
|
3182
|
+
const headers = {
|
|
3183
|
+
"Content-Type": "application/json",
|
|
3184
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
3185
|
+
};
|
|
3186
|
+
if (this.config.organization) {
|
|
3187
|
+
headers["OpenAI-Organization"] = this.config.organization;
|
|
3188
|
+
}
|
|
3189
|
+
return headers;
|
|
3190
|
+
}
|
|
3191
|
+
convertResponsesStopReason(reason) {
|
|
3192
|
+
if (reason === "max_output_tokens") {
|
|
3193
|
+
return "max_tokens";
|
|
3194
|
+
}
|
|
3195
|
+
return "end_turn";
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// src/llm/gemini.ts
|
|
3200
|
+
class GeminiProvider {
|
|
3201
|
+
id = "gemini";
|
|
3202
|
+
name = "Gemini";
|
|
3203
|
+
supportedModels = [/^gemini-/, /^models\/gemini-/];
|
|
3204
|
+
config;
|
|
3205
|
+
constructor(config = {}) {
|
|
3206
|
+
const apiKey = config.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
3207
|
+
if (!apiKey) {
|
|
3208
|
+
throw new Error("Gemini API key is required. Set GEMINI_API_KEY/GOOGLE_API_KEY or pass apiKey in config.");
|
|
3209
|
+
}
|
|
3210
|
+
this.config = {
|
|
3211
|
+
apiKey,
|
|
3212
|
+
baseUrl: config.baseUrl ?? process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta",
|
|
3213
|
+
defaultMaxTokens: config.defaultMaxTokens ?? 4096
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
supportsModel(model) {
|
|
3217
|
+
return this.supportedModels.some((pattern) => pattern.test(model));
|
|
3218
|
+
}
|
|
3219
|
+
async complete(request) {
|
|
3220
|
+
const geminiRequest = this.buildRequest(request);
|
|
3221
|
+
const url = this.buildUrl(this.getModelPath(request.config.model) + ":generateContent");
|
|
3222
|
+
const response = await fetch(url, {
|
|
3223
|
+
method: "POST",
|
|
3224
|
+
headers: this.getHeaders(),
|
|
3225
|
+
body: JSON.stringify(geminiRequest),
|
|
3226
|
+
signal: request.abortSignal
|
|
3227
|
+
});
|
|
3228
|
+
if (!response.ok) {
|
|
3229
|
+
const error = await response.text();
|
|
3230
|
+
throw new Error(`Gemini API error: ${response.status} ${error}`);
|
|
3231
|
+
}
|
|
3232
|
+
const data = await response.json();
|
|
3233
|
+
return this.convertResponse(data, request.config.model);
|
|
3234
|
+
}
|
|
3235
|
+
async stream(request, options) {
|
|
3236
|
+
const geminiRequest = this.buildRequest(request);
|
|
3237
|
+
const url = this.buildUrl(this.getModelPath(request.config.model) + ":streamGenerateContent", {
|
|
3238
|
+
alt: "sse"
|
|
3239
|
+
});
|
|
3240
|
+
const response = await fetch(url, {
|
|
3241
|
+
method: "POST",
|
|
3242
|
+
headers: this.getHeaders(),
|
|
3243
|
+
body: JSON.stringify(geminiRequest),
|
|
3244
|
+
signal: request.abortSignal
|
|
3245
|
+
});
|
|
3246
|
+
if (!response.ok) {
|
|
3247
|
+
const error = await response.text();
|
|
3248
|
+
throw new Error(`Gemini API error: ${response.status} ${error}`);
|
|
3249
|
+
}
|
|
3250
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3251
|
+
if (!contentType.includes("text/event-stream")) {
|
|
3252
|
+
const data = await response.json();
|
|
3253
|
+
return this.createResponseIterator(data, options, request.config.model);
|
|
3254
|
+
}
|
|
3255
|
+
return this.createStreamIterator(response.body, options, request.config.model);
|
|
3256
|
+
}
|
|
3257
|
+
buildRequest(request) {
|
|
3258
|
+
const { contents, systemInstruction } = this.convertMessages(request.messages, request.systemPrompt);
|
|
3259
|
+
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
3260
|
+
return {
|
|
3261
|
+
contents,
|
|
3262
|
+
systemInstruction,
|
|
3263
|
+
generationConfig: {
|
|
3264
|
+
temperature: request.config.temperature,
|
|
3265
|
+
topP: request.config.topP,
|
|
3266
|
+
maxOutputTokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
3267
|
+
stopSequences: request.config.stopSequences
|
|
3268
|
+
},
|
|
3269
|
+
tools,
|
|
3270
|
+
toolConfig: tools ? { functionCallingConfig: { mode: "AUTO" } } : undefined
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
convertMessages(messages, systemPrompt) {
|
|
3274
|
+
const contents = [];
|
|
3275
|
+
const systemTexts = [];
|
|
3276
|
+
const toolNameById = new Map;
|
|
3277
|
+
for (const msg of messages) {
|
|
3278
|
+
if (typeof msg.content !== "string") {
|
|
3279
|
+
for (const block of msg.content) {
|
|
3280
|
+
if (block.type === "tool_use") {
|
|
3281
|
+
toolNameById.set(block.id, block.name);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
if (systemPrompt) {
|
|
3287
|
+
systemTexts.push(systemPrompt);
|
|
3288
|
+
}
|
|
3289
|
+
for (const msg of messages) {
|
|
3290
|
+
if (msg.role === "system") {
|
|
3291
|
+
if (typeof msg.content === "string") {
|
|
3292
|
+
systemTexts.push(msg.content);
|
|
3293
|
+
} else {
|
|
3294
|
+
for (const block of msg.content) {
|
|
3295
|
+
if (block.type === "text") {
|
|
3296
|
+
systemTexts.push(block.text);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
continue;
|
|
3301
|
+
}
|
|
3302
|
+
if (typeof msg.content === "string") {
|
|
3303
|
+
contents.push({
|
|
3304
|
+
role: msg.role === "assistant" ? "model" : "user",
|
|
3305
|
+
parts: [{ text: msg.content }]
|
|
3306
|
+
});
|
|
3307
|
+
continue;
|
|
3308
|
+
}
|
|
3309
|
+
const parts = [];
|
|
3310
|
+
const toolResponses = [];
|
|
3311
|
+
for (const block of msg.content) {
|
|
3312
|
+
if (block.type === "text") {
|
|
3313
|
+
parts.push({ text: block.text });
|
|
3314
|
+
} else if (block.type === "image") {
|
|
3315
|
+
if (block.source.type === "base64") {
|
|
3316
|
+
parts.push({
|
|
3317
|
+
inlineData: {
|
|
3318
|
+
mimeType: block.source.media_type ?? "image/jpeg",
|
|
3319
|
+
data: block.source.data ?? ""
|
|
3320
|
+
}
|
|
3321
|
+
});
|
|
3322
|
+
} else if (block.source.type === "url") {
|
|
3323
|
+
parts.push({
|
|
3324
|
+
fileData: {
|
|
3325
|
+
mimeType: block.source.media_type ?? "image/jpeg",
|
|
3326
|
+
fileUri: block.source.url ?? ""
|
|
3327
|
+
}
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
} else if (block.type === "tool_result") {
|
|
3331
|
+
const toolName = toolNameById.get(block.tool_use_id) ?? "tool";
|
|
3332
|
+
const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
3333
|
+
toolResponses.push({
|
|
3334
|
+
functionResponse: {
|
|
3335
|
+
name: toolName,
|
|
3336
|
+
response: { output }
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
if (parts.length > 0) {
|
|
3342
|
+
contents.push({
|
|
3343
|
+
role: msg.role === "assistant" ? "model" : "user",
|
|
3344
|
+
parts
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
if (toolResponses.length > 0) {
|
|
3348
|
+
contents.push({
|
|
3349
|
+
role: "user",
|
|
3350
|
+
parts: toolResponses
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
const systemInstruction = systemTexts.length > 0 ? { parts: [{ text: systemTexts.join(`
|
|
3355
|
+
|
|
3356
|
+
`) }] } : undefined;
|
|
3357
|
+
return { contents, systemInstruction };
|
|
3358
|
+
}
|
|
3359
|
+
convertTools(tools) {
|
|
3360
|
+
return [
|
|
3361
|
+
{
|
|
3362
|
+
functionDeclarations: tools.map((tool) => ({
|
|
3363
|
+
name: tool.name,
|
|
3364
|
+
description: tool.description,
|
|
3365
|
+
parameters: this.sanitizeSchema(tool.inputSchema)
|
|
3366
|
+
}))
|
|
3367
|
+
}
|
|
3368
|
+
];
|
|
3369
|
+
}
|
|
3370
|
+
sanitizeSchema(schema) {
|
|
3371
|
+
const visited = new WeakMap;
|
|
3372
|
+
const scrub = (value) => {
|
|
3373
|
+
if (value === null || typeof value !== "object") {
|
|
3374
|
+
return value;
|
|
3375
|
+
}
|
|
3376
|
+
if (Array.isArray(value)) {
|
|
3377
|
+
return value.map((item) => scrub(item));
|
|
3378
|
+
}
|
|
3379
|
+
const existing = visited.get(value);
|
|
3380
|
+
if (existing) {
|
|
3381
|
+
return existing;
|
|
3382
|
+
}
|
|
3383
|
+
const result = {};
|
|
3384
|
+
visited.set(value, result);
|
|
3385
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
3386
|
+
if (key === "additionalProperties") {
|
|
3387
|
+
continue;
|
|
3388
|
+
}
|
|
3389
|
+
result[key] = scrub(inner);
|
|
3390
|
+
}
|
|
3391
|
+
return result;
|
|
3392
|
+
};
|
|
3393
|
+
return scrub(schema);
|
|
3394
|
+
}
|
|
3395
|
+
convertResponse(data, model) {
|
|
3396
|
+
const candidate = data.candidates?.[0];
|
|
3397
|
+
const content = [];
|
|
3398
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3399
|
+
let toolIndex = 0;
|
|
3400
|
+
for (const part of parts) {
|
|
3401
|
+
if ("text" in part && part.text) {
|
|
3402
|
+
content.push({ type: "text", text: part.text });
|
|
3403
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3404
|
+
const callId = `${part.functionCall.name}_${toolIndex++}`;
|
|
3405
|
+
content.push({
|
|
3406
|
+
type: "tool_use",
|
|
3407
|
+
id: callId,
|
|
3408
|
+
name: part.functionCall.name,
|
|
3409
|
+
input: part.functionCall.args ?? {}
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
return {
|
|
3414
|
+
id: "",
|
|
3415
|
+
model: data.model ?? model,
|
|
3416
|
+
content,
|
|
3417
|
+
stopReason: this.convertStopReason(candidate?.finishReason),
|
|
3418
|
+
stopSequence: null,
|
|
3419
|
+
usage: this.convertUsage(data.usageMetadata)
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
convertUsage(usage) {
|
|
3423
|
+
return {
|
|
3424
|
+
input_tokens: usage?.promptTokenCount ?? 0,
|
|
3425
|
+
output_tokens: usage?.candidatesTokenCount ?? 0
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
convertStopReason(reason) {
|
|
3429
|
+
switch (reason) {
|
|
3430
|
+
case "MAX_TOKENS":
|
|
3431
|
+
return "max_tokens";
|
|
3432
|
+
case "STOP":
|
|
3433
|
+
return "end_turn";
|
|
3434
|
+
default:
|
|
3435
|
+
return "end_turn";
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
createStreamIterator(body, options, model) {
|
|
3439
|
+
const self = this;
|
|
3440
|
+
return {
|
|
3441
|
+
async* [Symbol.asyncIterator]() {
|
|
3442
|
+
const reader = body.getReader();
|
|
3443
|
+
const decoder = new TextDecoder;
|
|
3444
|
+
let buffer = "";
|
|
3445
|
+
let emittedMessageStart = false;
|
|
3446
|
+
let textBlockStarted = false;
|
|
3447
|
+
let finished = false;
|
|
3448
|
+
let toolIndex = 0;
|
|
3449
|
+
let emittedAny = false;
|
|
3450
|
+
const emitMessageStart = (modelId) => {
|
|
3451
|
+
if (emittedMessageStart)
|
|
3452
|
+
return;
|
|
3453
|
+
emittedMessageStart = true;
|
|
3454
|
+
const startEvent = {
|
|
3455
|
+
type: "message_start",
|
|
3456
|
+
message: {
|
|
3457
|
+
id: "",
|
|
3458
|
+
type: "message",
|
|
3459
|
+
role: "assistant",
|
|
3460
|
+
content: [],
|
|
3461
|
+
model: modelId,
|
|
3462
|
+
stop_reason: null,
|
|
3463
|
+
stop_sequence: null,
|
|
3464
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
options?.onEvent?.(startEvent);
|
|
3468
|
+
return startEvent;
|
|
3469
|
+
};
|
|
3470
|
+
try {
|
|
3471
|
+
while (true) {
|
|
3472
|
+
const { done, value } = await reader.read();
|
|
3473
|
+
if (done)
|
|
3474
|
+
break;
|
|
3475
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3476
|
+
const lines = buffer.split(`
|
|
3477
|
+
`);
|
|
3478
|
+
buffer = lines.pop() || "";
|
|
3479
|
+
for (const line of lines) {
|
|
3480
|
+
const trimmed = line.trim();
|
|
3481
|
+
if (!trimmed)
|
|
3482
|
+
continue;
|
|
3483
|
+
let jsonText = trimmed;
|
|
3484
|
+
if (trimmed.startsWith("data:")) {
|
|
3485
|
+
jsonText = trimmed.slice(5).trim();
|
|
3486
|
+
}
|
|
3487
|
+
let payload;
|
|
3488
|
+
try {
|
|
3489
|
+
payload = JSON.parse(jsonText);
|
|
3490
|
+
} catch {
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
const startEvent = emitMessageStart(payload.model ?? model);
|
|
3494
|
+
if (startEvent) {
|
|
3495
|
+
yield startEvent;
|
|
3496
|
+
emittedAny = true;
|
|
3497
|
+
}
|
|
3498
|
+
const candidate = payload.candidates?.[0];
|
|
3499
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3500
|
+
for (const part of parts) {
|
|
3501
|
+
if ("text" in part && part.text) {
|
|
3502
|
+
if (!textBlockStarted) {
|
|
3503
|
+
textBlockStarted = true;
|
|
3504
|
+
const startText = {
|
|
3505
|
+
type: "content_block_start",
|
|
3506
|
+
index: 0,
|
|
3507
|
+
content_block: { type: "text", text: "" }
|
|
3508
|
+
};
|
|
3509
|
+
options?.onEvent?.(startText);
|
|
3510
|
+
yield startText;
|
|
3511
|
+
emittedAny = true;
|
|
3512
|
+
}
|
|
3513
|
+
const textEvent = {
|
|
3514
|
+
type: "content_block_delta",
|
|
3515
|
+
index: 0,
|
|
3516
|
+
delta: { type: "text_delta", text: part.text }
|
|
3517
|
+
};
|
|
3518
|
+
options?.onText?.(part.text);
|
|
3519
|
+
options?.onEvent?.(textEvent);
|
|
3520
|
+
yield textEvent;
|
|
3521
|
+
emittedAny = true;
|
|
3522
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3523
|
+
const callId = `${part.functionCall.name}_${toolIndex}`;
|
|
3524
|
+
const blockIndex = 1 + toolIndex;
|
|
3525
|
+
toolIndex += 1;
|
|
3526
|
+
const startTool = {
|
|
3527
|
+
type: "content_block_start",
|
|
3528
|
+
index: blockIndex,
|
|
3529
|
+
content_block: {
|
|
3530
|
+
type: "tool_use",
|
|
3531
|
+
id: callId,
|
|
3532
|
+
name: part.functionCall.name,
|
|
3533
|
+
input: {}
|
|
3534
|
+
}
|
|
3535
|
+
};
|
|
3536
|
+
options?.onEvent?.(startTool);
|
|
3537
|
+
yield startTool;
|
|
3538
|
+
emittedAny = true;
|
|
3539
|
+
const args = JSON.stringify(part.functionCall.args ?? {});
|
|
3540
|
+
if (args) {
|
|
3541
|
+
const deltaEvent = {
|
|
3542
|
+
type: "content_block_delta",
|
|
3543
|
+
index: blockIndex,
|
|
3544
|
+
delta: { type: "input_json_delta", partial_json: args }
|
|
3545
|
+
};
|
|
3546
|
+
options?.onEvent?.(deltaEvent);
|
|
3547
|
+
yield deltaEvent;
|
|
3548
|
+
emittedAny = true;
|
|
2628
3549
|
}
|
|
2629
|
-
|
|
3550
|
+
const stopTool = {
|
|
3551
|
+
type: "content_block_stop",
|
|
3552
|
+
index: blockIndex
|
|
3553
|
+
};
|
|
3554
|
+
options?.onEvent?.(stopTool);
|
|
3555
|
+
yield stopTool;
|
|
3556
|
+
emittedAny = true;
|
|
3557
|
+
options?.onToolUse?.({
|
|
3558
|
+
id: callId,
|
|
3559
|
+
name: part.functionCall.name,
|
|
3560
|
+
input: part.functionCall.args ?? {}
|
|
3561
|
+
});
|
|
3562
|
+
emittedAny = true;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
if (candidate?.finishReason && !finished) {
|
|
3566
|
+
finished = true;
|
|
3567
|
+
if (textBlockStarted) {
|
|
3568
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3569
|
+
options?.onEvent?.(stopText);
|
|
3570
|
+
yield stopText;
|
|
3571
|
+
}
|
|
3572
|
+
const messageDelta = {
|
|
3573
|
+
type: "message_delta",
|
|
3574
|
+
delta: {
|
|
3575
|
+
stop_reason: self.convertStopReason(candidate.finishReason),
|
|
3576
|
+
stop_sequence: null
|
|
3577
|
+
},
|
|
3578
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3579
|
+
};
|
|
3580
|
+
options?.onEvent?.(messageDelta);
|
|
3581
|
+
yield messageDelta;
|
|
3582
|
+
emittedAny = true;
|
|
3583
|
+
const stopEvent = { type: "message_stop" };
|
|
3584
|
+
options?.onEvent?.(stopEvent);
|
|
3585
|
+
yield stopEvent;
|
|
3586
|
+
emittedAny = true;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
} finally {
|
|
3591
|
+
reader.releaseLock();
|
|
3592
|
+
}
|
|
3593
|
+
if (!emittedAny) {
|
|
3594
|
+
const trimmed = buffer.trim();
|
|
3595
|
+
if (trimmed) {
|
|
3596
|
+
try {
|
|
3597
|
+
const parsed = JSON.parse(trimmed);
|
|
3598
|
+
const responses = Array.isArray(parsed) ? parsed : [parsed];
|
|
3599
|
+
for (const payload of responses) {
|
|
3600
|
+
const startEvent = emitMessageStart(payload.model ?? model);
|
|
3601
|
+
if (startEvent) {
|
|
3602
|
+
yield startEvent;
|
|
3603
|
+
}
|
|
3604
|
+
const candidate = payload.candidates?.[0];
|
|
3605
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3606
|
+
for (const part of parts) {
|
|
3607
|
+
if ("text" in part && part.text) {
|
|
2630
3608
|
if (!textBlockStarted) {
|
|
2631
3609
|
textBlockStarted = true;
|
|
2632
3610
|
const startText = {
|
|
@@ -2640,115 +3618,169 @@ class OpenAIProvider {
|
|
|
2640
3618
|
const textEvent = {
|
|
2641
3619
|
type: "content_block_delta",
|
|
2642
3620
|
index: 0,
|
|
2643
|
-
delta: {
|
|
2644
|
-
type: "text_delta",
|
|
2645
|
-
text: delta.content
|
|
2646
|
-
}
|
|
3621
|
+
delta: { type: "text_delta", text: part.text }
|
|
2647
3622
|
};
|
|
2648
|
-
options?.onText?.(
|
|
3623
|
+
options?.onText?.(part.text);
|
|
2649
3624
|
options?.onEvent?.(textEvent);
|
|
2650
3625
|
yield textEvent;
|
|
2651
3626
|
}
|
|
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
|
-
|
|
3627
|
+
}
|
|
3628
|
+
if (textBlockStarted) {
|
|
3629
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3630
|
+
options?.onEvent?.(stopText);
|
|
3631
|
+
yield stopText;
|
|
3632
|
+
}
|
|
3633
|
+
const messageDelta = {
|
|
3634
|
+
type: "message_delta",
|
|
3635
|
+
delta: {
|
|
3636
|
+
stop_reason: self.convertStopReason(candidate?.finishReason),
|
|
3637
|
+
stop_sequence: null
|
|
3638
|
+
},
|
|
3639
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3640
|
+
};
|
|
3641
|
+
options?.onEvent?.(messageDelta);
|
|
3642
|
+
yield messageDelta;
|
|
3643
|
+
const stopEvent = { type: "message_stop" };
|
|
3644
|
+
options?.onEvent?.(stopEvent);
|
|
3645
|
+
yield stopEvent;
|
|
3646
|
+
}
|
|
3647
|
+
return;
|
|
3648
|
+
} catch {}
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
if (!finished) {
|
|
3652
|
+
if (textBlockStarted) {
|
|
3653
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3654
|
+
options?.onEvent?.(stopText);
|
|
3655
|
+
yield stopText;
|
|
3656
|
+
}
|
|
3657
|
+
const stopEvent = { type: "message_stop" };
|
|
3658
|
+
options?.onEvent?.(stopEvent);
|
|
3659
|
+
yield stopEvent;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
createResponseIterator(data, options, model) {
|
|
3665
|
+
const responses = Array.isArray(data) ? data : [data];
|
|
3666
|
+
const self = this;
|
|
3667
|
+
return {
|
|
3668
|
+
async* [Symbol.asyncIterator]() {
|
|
3669
|
+
for (const payload of responses) {
|
|
3670
|
+
const candidate = payload.candidates?.[0];
|
|
3671
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3672
|
+
let textIndex = 0;
|
|
3673
|
+
let toolIndex = 0;
|
|
3674
|
+
const startEvent = {
|
|
3675
|
+
type: "message_start",
|
|
3676
|
+
message: {
|
|
3677
|
+
id: "",
|
|
3678
|
+
type: "message",
|
|
3679
|
+
role: "assistant",
|
|
3680
|
+
content: [],
|
|
3681
|
+
model: payload.model ?? model,
|
|
3682
|
+
stop_reason: null,
|
|
3683
|
+
stop_sequence: null,
|
|
3684
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
3685
|
+
}
|
|
3686
|
+
};
|
|
3687
|
+
options?.onEvent?.(startEvent);
|
|
3688
|
+
yield startEvent;
|
|
3689
|
+
for (const part of parts) {
|
|
3690
|
+
if ("text" in part && part.text) {
|
|
3691
|
+
const startText = {
|
|
3692
|
+
type: "content_block_start",
|
|
3693
|
+
index: textIndex,
|
|
3694
|
+
content_block: { type: "text", text: "" }
|
|
3695
|
+
};
|
|
3696
|
+
options?.onEvent?.(startText);
|
|
3697
|
+
yield startText;
|
|
3698
|
+
const textEvent = {
|
|
3699
|
+
type: "content_block_delta",
|
|
3700
|
+
index: textIndex,
|
|
3701
|
+
delta: { type: "text_delta", text: part.text }
|
|
3702
|
+
};
|
|
3703
|
+
options?.onText?.(part.text);
|
|
3704
|
+
options?.onEvent?.(textEvent);
|
|
3705
|
+
yield textEvent;
|
|
3706
|
+
const stopText = { type: "content_block_stop", index: textIndex };
|
|
3707
|
+
options?.onEvent?.(stopText);
|
|
3708
|
+
yield stopText;
|
|
3709
|
+
textIndex += 1;
|
|
3710
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3711
|
+
const callId = `${part.functionCall.name}_${toolIndex}`;
|
|
3712
|
+
const blockIndex = textIndex + toolIndex + 1;
|
|
3713
|
+
toolIndex += 1;
|
|
3714
|
+
const startTool = {
|
|
3715
|
+
type: "content_block_start",
|
|
3716
|
+
index: blockIndex,
|
|
3717
|
+
content_block: {
|
|
3718
|
+
type: "tool_use",
|
|
3719
|
+
id: callId,
|
|
3720
|
+
name: part.functionCall.name,
|
|
3721
|
+
input: {}
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
options?.onEvent?.(startTool);
|
|
3725
|
+
yield startTool;
|
|
3726
|
+
const args = JSON.stringify(part.functionCall.args ?? {});
|
|
3727
|
+
if (args) {
|
|
3728
|
+
const deltaEvent = {
|
|
3729
|
+
type: "content_block_delta",
|
|
3730
|
+
index: blockIndex,
|
|
3731
|
+
delta: { type: "input_json_delta", partial_json: args }
|
|
3732
|
+
};
|
|
3733
|
+
options?.onEvent?.(deltaEvent);
|
|
3734
|
+
yield deltaEvent;
|
|
2734
3735
|
}
|
|
3736
|
+
const stopTool = { type: "content_block_stop", index: blockIndex };
|
|
3737
|
+
options?.onEvent?.(stopTool);
|
|
3738
|
+
yield stopTool;
|
|
3739
|
+
options?.onToolUse?.({
|
|
3740
|
+
id: callId,
|
|
3741
|
+
name: part.functionCall.name,
|
|
3742
|
+
input: part.functionCall.args ?? {}
|
|
3743
|
+
});
|
|
2735
3744
|
}
|
|
2736
3745
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
3746
|
+
const messageDelta = {
|
|
3747
|
+
type: "message_delta",
|
|
3748
|
+
delta: {
|
|
3749
|
+
stop_reason: self.convertStopReason(candidate?.finishReason),
|
|
3750
|
+
stop_sequence: null
|
|
3751
|
+
},
|
|
3752
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3753
|
+
};
|
|
3754
|
+
options?.onEvent?.(messageDelta);
|
|
3755
|
+
yield messageDelta;
|
|
3756
|
+
const stopEvent = { type: "message_stop" };
|
|
3757
|
+
options?.onEvent?.(stopEvent);
|
|
3758
|
+
yield stopEvent;
|
|
2739
3759
|
}
|
|
2740
3760
|
}
|
|
2741
3761
|
};
|
|
2742
3762
|
}
|
|
2743
3763
|
getHeaders() {
|
|
2744
|
-
|
|
3764
|
+
return {
|
|
2745
3765
|
"Content-Type": "application/json",
|
|
2746
|
-
|
|
3766
|
+
"x-goog-api-key": this.config.apiKey
|
|
2747
3767
|
};
|
|
2748
|
-
|
|
2749
|
-
|
|
3768
|
+
}
|
|
3769
|
+
buildUrl(path2, params) {
|
|
3770
|
+
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
3771
|
+
const url = new URL(`${base}/${path2.replace(/^\/+/, "")}`);
|
|
3772
|
+
if (!url.searchParams.has("key")) {
|
|
3773
|
+
url.searchParams.set("key", this.config.apiKey);
|
|
2750
3774
|
}
|
|
2751
|
-
|
|
3775
|
+
if (params) {
|
|
3776
|
+
for (const [key, value] of Object.entries(params)) {
|
|
3777
|
+
url.searchParams.set(key, value);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
return url.toString();
|
|
3781
|
+
}
|
|
3782
|
+
getModelPath(model) {
|
|
3783
|
+
return model.startsWith("models/") ? model : `models/${model}`;
|
|
2752
3784
|
}
|
|
2753
3785
|
}
|
|
2754
3786
|
|
|
@@ -2766,8 +3798,13 @@ function getGlobalManager() {
|
|
|
2766
3798
|
apiKey: process.env.OPENAI_API_KEY,
|
|
2767
3799
|
baseUrl: process.env.OPENAI_BASE_URL
|
|
2768
3800
|
});
|
|
3801
|
+
} else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
3802
|
+
defaultProvider = new GeminiProvider({
|
|
3803
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
3804
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
3805
|
+
});
|
|
2769
3806
|
} else {
|
|
2770
|
-
throw new Error("No default provider set. Set ANTHROPIC_API_KEY or
|
|
3807
|
+
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
3808
|
}
|
|
2772
3809
|
}
|
|
2773
3810
|
globalManager = new SessionManagerImpl({
|
|
@@ -2788,8 +3825,13 @@ async function createSession(options) {
|
|
|
2788
3825
|
apiKey: process.env.OPENAI_API_KEY,
|
|
2789
3826
|
baseUrl: process.env.OPENAI_BASE_URL
|
|
2790
3827
|
});
|
|
3828
|
+
} else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
3829
|
+
provider = new GeminiProvider({
|
|
3830
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
3831
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
3832
|
+
});
|
|
2791
3833
|
} else {
|
|
2792
|
-
throw new Error("No provider available. Set ANTHROPIC_API_KEY or
|
|
3834
|
+
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
3835
|
}
|
|
2794
3836
|
}
|
|
2795
3837
|
const customManager = new SessionManagerImpl({
|
|
@@ -5063,7 +6105,17 @@ function loadEnvOverride(cwd) {
|
|
|
5063
6105
|
|
|
5064
6106
|
// src/cli/cli.ts
|
|
5065
6107
|
loadEnvOverride();
|
|
5066
|
-
|
|
6108
|
+
function getCliVersion() {
|
|
6109
|
+
try {
|
|
6110
|
+
const pkgUrl = new URL("../../package.json", import.meta.url);
|
|
6111
|
+
const raw = readFileSync2(pkgUrl, "utf-8");
|
|
6112
|
+
const parsed = JSON.parse(raw);
|
|
6113
|
+
return parsed.version ?? "0.0.0";
|
|
6114
|
+
} catch {
|
|
6115
|
+
return "0.0.0";
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
var VERSION = getCliVersion();
|
|
5067
6119
|
var SKILLS_PATH = join8(homedir4(), ".claude");
|
|
5068
6120
|
var colors = {
|
|
5069
6121
|
reset: "\x1B[0m",
|
|
@@ -5092,6 +6144,8 @@ var session = null;
|
|
|
5092
6144
|
var totalInputTokens = 0;
|
|
5093
6145
|
var totalOutputTokens = 0;
|
|
5094
6146
|
var messageCount = 0;
|
|
6147
|
+
var currentProviderId = null;
|
|
6148
|
+
var currentModelOverride = null;
|
|
5095
6149
|
function isGitRepo(dir) {
|
|
5096
6150
|
return existsSync10(join8(dir, ".git"));
|
|
5097
6151
|
}
|
|
@@ -5139,6 +6193,7 @@ ${c.bold("Interactive Commands:")}
|
|
|
5139
6193
|
${c.cyan("/clear")} Clear conversation history
|
|
5140
6194
|
${c.cyan("/tools")} List available tools
|
|
5141
6195
|
${c.cyan("/skills")} List available skills
|
|
6196
|
+
${c.cyan("/models")} Show or switch provider/model
|
|
5142
6197
|
${c.cyan("/todos")} Show current todo list
|
|
5143
6198
|
${c.cyan("/usage")} Show token usage statistics
|
|
5144
6199
|
${c.cyan("/debug")} Show debug info (prompt, model, env)
|
|
@@ -5147,8 +6202,11 @@ ${c.bold("Interactive Commands:")}
|
|
|
5147
6202
|
${c.bold("Environment:")}
|
|
5148
6203
|
${c.cyan("ANTHROPIC_API_KEY")} Anthropic API key (for Claude models)
|
|
5149
6204
|
${c.cyan("ANTHROPIC_MODEL")} Optional. Claude model (default: claude-sonnet-4-20250514)
|
|
6205
|
+
${c.cyan("GEMINI_API_KEY")} Gemini API key (for Gemini models)
|
|
6206
|
+
${c.cyan("GEMINI_MODEL")} Optional. Gemini model (default: gemini-1.5-pro)
|
|
6207
|
+
${c.cyan("GEMINI_BASE_URL")} Optional. Custom Gemini API base URL
|
|
5150
6208
|
${c.cyan("OPENAI_API_KEY")} OpenAI API key (for GPT models)
|
|
5151
|
-
${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-
|
|
6209
|
+
${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-5.2)
|
|
5152
6210
|
${c.cyan("OPENAI_BASE_URL")} Optional. Custom OpenAI-compatible API URL
|
|
5153
6211
|
|
|
5154
6212
|
${c.bold("Examples:")}
|
|
@@ -5166,7 +6224,7 @@ function printVersion() {
|
|
|
5166
6224
|
console.log(`formagent-sdk v${VERSION}`);
|
|
5167
6225
|
}
|
|
5168
6226
|
function printBanner() {
|
|
5169
|
-
const model =
|
|
6227
|
+
const model = getActiveModel();
|
|
5170
6228
|
console.log();
|
|
5171
6229
|
console.log(c.cyan("╔═══════════════════════════════════════════════════════════╗"));
|
|
5172
6230
|
console.log(c.cyan("║") + c.bold(" FormAgent CLI v" + VERSION + " ") + c.cyan("║"));
|
|
@@ -5174,6 +6232,7 @@ function printBanner() {
|
|
|
5174
6232
|
console.log(c.cyan("╚═══════════════════════════════════════════════════════════╝"));
|
|
5175
6233
|
console.log();
|
|
5176
6234
|
console.log(c.dim(" Model: ") + c.green(model));
|
|
6235
|
+
console.log(c.dim(" Provider: ") + c.green(getActiveProviderId() ?? "auto"));
|
|
5177
6236
|
console.log(c.dim(" Type your message and press Enter to chat."));
|
|
5178
6237
|
console.log(c.dim(" Use /help for commands, /exit to quit."));
|
|
5179
6238
|
console.log();
|
|
@@ -5186,6 +6245,7 @@ function printInteractiveHelp() {
|
|
|
5186
6245
|
console.log(` ${c.cyan("/clear")} Clear conversation history`);
|
|
5187
6246
|
console.log(` ${c.cyan("/tools")} List available tools`);
|
|
5188
6247
|
console.log(` ${c.cyan("/skills")} List available skills`);
|
|
6248
|
+
console.log(` ${c.cyan("/models")} Show or switch provider/model`);
|
|
5189
6249
|
console.log(` ${c.cyan("/todos")} Show current todo list`);
|
|
5190
6250
|
console.log(` ${c.cyan("/usage")} Show token usage statistics`);
|
|
5191
6251
|
console.log(` ${c.cyan("/debug")} Show debug info (prompt, model, env)`);
|
|
@@ -5259,8 +6319,238 @@ function printUsage() {
|
|
|
5259
6319
|
console.log(` ${c.cyan("Est. cost:")} $${(inputCost + outputCost).toFixed(4)}`);
|
|
5260
6320
|
console.log();
|
|
5261
6321
|
}
|
|
6322
|
+
async function resetSessionForModelChange() {
|
|
6323
|
+
if (session) {
|
|
6324
|
+
await session.close();
|
|
6325
|
+
session = null;
|
|
6326
|
+
}
|
|
6327
|
+
totalInputTokens = 0;
|
|
6328
|
+
totalOutputTokens = 0;
|
|
6329
|
+
messageCount = 0;
|
|
6330
|
+
}
|
|
6331
|
+
function printModelsHelp() {
|
|
6332
|
+
const provider = getActiveProviderId() ?? "auto";
|
|
6333
|
+
const model = getActiveModel();
|
|
6334
|
+
console.log();
|
|
6335
|
+
console.log(c.bold("Model Selection:"));
|
|
6336
|
+
console.log();
|
|
6337
|
+
console.log(` ${c.cyan("Current provider:")} ${provider}`);
|
|
6338
|
+
console.log(` ${c.cyan("Current model:")} ${model}`);
|
|
6339
|
+
console.log();
|
|
6340
|
+
console.log(c.bold("Usage:"));
|
|
6341
|
+
console.log(` ${c.cyan("/models")}`);
|
|
6342
|
+
console.log(c.dim(" List models for the active provider"));
|
|
6343
|
+
console.log(` ${c.cyan("/models")} openai gpt-5-mini`);
|
|
6344
|
+
console.log(` ${c.cyan("/models")} anthropic claude-sonnet-4-20250514`);
|
|
6345
|
+
console.log(` ${c.cyan("/models")} gemini gemini-1.5-pro`);
|
|
6346
|
+
console.log(` ${c.cyan("/models")} gpt-5.2`);
|
|
6347
|
+
console.log(` ${c.cyan("/models")} reset`);
|
|
6348
|
+
console.log();
|
|
6349
|
+
}
|
|
6350
|
+
async function handleModelsCommand(args) {
|
|
6351
|
+
if (args.length === 0) {
|
|
6352
|
+
await listModelsSummary();
|
|
6353
|
+
return;
|
|
6354
|
+
}
|
|
6355
|
+
if (args[0].toLowerCase() === "reset") {
|
|
6356
|
+
currentProviderId = null;
|
|
6357
|
+
currentModelOverride = null;
|
|
6358
|
+
await resetSessionForModelChange();
|
|
6359
|
+
console.log(c.green(`
|
|
6360
|
+
✓ Model selection reset to environment defaults.
|
|
6361
|
+
`));
|
|
6362
|
+
return;
|
|
6363
|
+
}
|
|
6364
|
+
if (args.length === 1) {
|
|
6365
|
+
const provider2 = parseProvider(args[0]);
|
|
6366
|
+
if (provider2) {
|
|
6367
|
+
currentProviderId = provider2;
|
|
6368
|
+
currentModelOverride = null;
|
|
6369
|
+
await resetSessionForModelChange();
|
|
6370
|
+
console.log(c.green(`
|
|
6371
|
+
✓ Provider set to ${provider2}. Model: ${getActiveModel()}.
|
|
6372
|
+
`));
|
|
6373
|
+
return;
|
|
6374
|
+
}
|
|
6375
|
+
currentModelOverride = args[0];
|
|
6376
|
+
currentProviderId = inferProviderFromModel(args[0]) ?? currentProviderId;
|
|
6377
|
+
await resetSessionForModelChange();
|
|
6378
|
+
console.log(c.green(`
|
|
6379
|
+
✓ Model set to ${currentModelOverride} (provider: ${getActiveProviderId() ?? "auto"}).
|
|
6380
|
+
`));
|
|
6381
|
+
return;
|
|
6382
|
+
}
|
|
6383
|
+
const provider = parseProvider(args[0]);
|
|
6384
|
+
if (!provider) {
|
|
6385
|
+
console.log(c.yellow(`
|
|
6386
|
+
Unknown provider: ${args[0]}. Use "openai", "anthropic", or "gemini".
|
|
6387
|
+
`));
|
|
6388
|
+
return;
|
|
6389
|
+
}
|
|
6390
|
+
const model = args.slice(1).join(" ");
|
|
6391
|
+
if (!model) {
|
|
6392
|
+
console.log(c.yellow(`
|
|
6393
|
+
Missing model name. Example: /models openai gpt-5-mini
|
|
6394
|
+
`));
|
|
6395
|
+
return;
|
|
6396
|
+
}
|
|
6397
|
+
currentProviderId = provider;
|
|
6398
|
+
currentModelOverride = model;
|
|
6399
|
+
await resetSessionForModelChange();
|
|
6400
|
+
console.log(c.green(`
|
|
6401
|
+
✓ Provider set to ${provider}, model set to ${model}.
|
|
6402
|
+
`));
|
|
6403
|
+
}
|
|
6404
|
+
function normalizeOpenAIBaseUrl(baseUrl) {
|
|
6405
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
6406
|
+
if (trimmed.endsWith("/v1")) {
|
|
6407
|
+
return trimmed;
|
|
6408
|
+
}
|
|
6409
|
+
return `${trimmed}/v1`;
|
|
6410
|
+
}
|
|
6411
|
+
function getOpenAIApiType(baseUrl) {
|
|
6412
|
+
const normalized = baseUrl.toLowerCase();
|
|
6413
|
+
return normalized.includes("api.openai.com") ? "openai" : "openai-compatible";
|
|
6414
|
+
}
|
|
6415
|
+
function isGoogleGeminiBaseUrl(baseUrl) {
|
|
6416
|
+
const normalized = baseUrl.toLowerCase();
|
|
6417
|
+
return normalized.includes("generativelanguage.googleapis.com") || normalized.includes("/v1beta");
|
|
6418
|
+
}
|
|
6419
|
+
async function listAnthropicModels() {
|
|
6420
|
+
const baseUrlRaw = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\/+$/, "");
|
|
6421
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
6422
|
+
const baseUrl = baseUrlRaw.endsWith("/v1") ? baseUrlRaw : `${baseUrlRaw}/v1`;
|
|
6423
|
+
console.log(c.bold("Anthropic Models:"));
|
|
6424
|
+
console.log(c.dim(" API Type: anthropic (official)"));
|
|
6425
|
+
console.log(c.dim(` Base URL: ${baseUrl}`));
|
|
6426
|
+
if (!apiKey) {
|
|
6427
|
+
console.log(c.red(" ✗ ANTHROPIC_API_KEY not set"));
|
|
6428
|
+
console.log();
|
|
6429
|
+
return;
|
|
6430
|
+
}
|
|
6431
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
6432
|
+
headers: {
|
|
6433
|
+
"x-api-key": apiKey,
|
|
6434
|
+
"anthropic-version": "2023-06-01"
|
|
6435
|
+
}
|
|
6436
|
+
});
|
|
6437
|
+
if (!res.ok) {
|
|
6438
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6439
|
+
console.log(c.dim(` URL: ${baseUrl}/models`));
|
|
6440
|
+
console.log();
|
|
6441
|
+
return;
|
|
6442
|
+
}
|
|
6443
|
+
const payload = await res.json();
|
|
6444
|
+
const items = payload.data ?? [];
|
|
6445
|
+
for (const item of items) {
|
|
6446
|
+
const name = item.display_name ? ` (${item.display_name})` : "";
|
|
6447
|
+
console.log(` ${c.green("●")} ${item.id}${name}`);
|
|
6448
|
+
}
|
|
6449
|
+
console.log();
|
|
6450
|
+
}
|
|
6451
|
+
async function listOpenAIModels() {
|
|
6452
|
+
const baseUrl = normalizeOpenAIBaseUrl(process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1");
|
|
6453
|
+
const apiFlavor = getOpenAIApiType(baseUrl);
|
|
6454
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
6455
|
+
console.log(c.bold("OpenAI Models:"));
|
|
6456
|
+
console.log(c.dim(` API Type: ${apiFlavor}`));
|
|
6457
|
+
console.log(c.dim(` Base URL: ${baseUrl}`));
|
|
6458
|
+
if (!apiKey) {
|
|
6459
|
+
console.log(c.red(" ✗ OPENAI_API_KEY not set"));
|
|
6460
|
+
console.log();
|
|
6461
|
+
return;
|
|
6462
|
+
}
|
|
6463
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
6464
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
6465
|
+
});
|
|
6466
|
+
if (!res.ok) {
|
|
6467
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6468
|
+
console.log(c.dim(` URL: ${baseUrl}/models`));
|
|
6469
|
+
console.log();
|
|
6470
|
+
return;
|
|
6471
|
+
}
|
|
6472
|
+
const payload = await res.json();
|
|
6473
|
+
const items = payload.data ?? [];
|
|
6474
|
+
for (const item of items) {
|
|
6475
|
+
const owner = item.owned_by ? ` (${item.owned_by})` : "";
|
|
6476
|
+
console.log(` ${c.green("●")} ${item.id}${owner}`);
|
|
6477
|
+
}
|
|
6478
|
+
console.log();
|
|
6479
|
+
}
|
|
6480
|
+
async function listGeminiModels() {
|
|
6481
|
+
const baseUrlRaw = (process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/+$/, "");
|
|
6482
|
+
const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
6483
|
+
console.log(c.bold("Gemini Models:"));
|
|
6484
|
+
console.log(c.dim(` Base URL: ${baseUrlRaw}`));
|
|
6485
|
+
if (!apiKey) {
|
|
6486
|
+
console.log(c.red(" ✗ GEMINI_API_KEY not set"));
|
|
6487
|
+
console.log();
|
|
6488
|
+
return;
|
|
6489
|
+
}
|
|
6490
|
+
if (isGoogleGeminiBaseUrl(baseUrlRaw)) {
|
|
6491
|
+
console.log(c.dim(" API Type: gemini"));
|
|
6492
|
+
const url = `${baseUrlRaw}/models`;
|
|
6493
|
+
const res2 = await fetch(url, {
|
|
6494
|
+
headers: { "x-goog-api-key": apiKey }
|
|
6495
|
+
});
|
|
6496
|
+
if (!res2.ok) {
|
|
6497
|
+
console.log(c.red(` ✗ Failed to fetch models (${res2.status})`));
|
|
6498
|
+
console.log(c.dim(` URL: ${url}`));
|
|
6499
|
+
console.log();
|
|
6500
|
+
return;
|
|
6501
|
+
}
|
|
6502
|
+
const payload2 = await res2.json();
|
|
6503
|
+
const items2 = payload2.models ?? [];
|
|
6504
|
+
for (const item of items2) {
|
|
6505
|
+
console.log(` ${c.green("●")} ${item.name}`);
|
|
6506
|
+
}
|
|
6507
|
+
console.log();
|
|
6508
|
+
return;
|
|
6509
|
+
}
|
|
6510
|
+
const openaiBase = normalizeOpenAIBaseUrl(baseUrlRaw);
|
|
6511
|
+
console.log(c.dim(" API Type: openai-compatible"));
|
|
6512
|
+
console.log(c.dim(" Auth: Bearer (GEMINI_API_KEY)"));
|
|
6513
|
+
const res = await fetch(`${openaiBase}/models`, {
|
|
6514
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
6515
|
+
});
|
|
6516
|
+
if (!res.ok) {
|
|
6517
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6518
|
+
console.log(c.dim(` URL: ${openaiBase}/models`));
|
|
6519
|
+
console.log();
|
|
6520
|
+
return;
|
|
6521
|
+
}
|
|
6522
|
+
const payload = await res.json();
|
|
6523
|
+
const items = payload.data ?? [];
|
|
6524
|
+
for (const item of items) {
|
|
6525
|
+
const owner = item.owned_by ? ` (${item.owned_by})` : "";
|
|
6526
|
+
console.log(` ${c.green("●")} ${item.id}${owner}`);
|
|
6527
|
+
}
|
|
6528
|
+
console.log();
|
|
6529
|
+
}
|
|
6530
|
+
async function listModelsSummary() {
|
|
6531
|
+
const provider = getActiveProviderId();
|
|
6532
|
+
const apiType = provider ?? "auto";
|
|
6533
|
+
console.log();
|
|
6534
|
+
console.log(c.bold("Available Models:"));
|
|
6535
|
+
console.log(c.dim(` Active Provider: ${apiType}`));
|
|
6536
|
+
console.log();
|
|
6537
|
+
printModelsHelp();
|
|
6538
|
+
try {
|
|
6539
|
+
await listOpenAIModels();
|
|
6540
|
+
} catch (error) {
|
|
6541
|
+
console.log(c.red(` ✗ OpenAI: ${error instanceof Error ? error.message : String(error)}`));
|
|
6542
|
+
console.log();
|
|
6543
|
+
}
|
|
6544
|
+
try {
|
|
6545
|
+
await listGeminiModels();
|
|
6546
|
+
} catch (error) {
|
|
6547
|
+
console.log(c.red(` ✗ Gemini: ${error instanceof Error ? error.message : String(error)}`));
|
|
6548
|
+
console.log();
|
|
6549
|
+
}
|
|
6550
|
+
await listAnthropicModels();
|
|
6551
|
+
}
|
|
5262
6552
|
function printDebug() {
|
|
5263
|
-
const model =
|
|
6553
|
+
const model = getActiveModel();
|
|
5264
6554
|
const tools = getAllTools();
|
|
5265
6555
|
const systemPrompt = buildSystemPrompt();
|
|
5266
6556
|
const cwd = process.cwd();
|
|
@@ -5271,14 +6561,20 @@ function printDebug() {
|
|
|
5271
6561
|
console.log();
|
|
5272
6562
|
console.log(c.bold("Model:"));
|
|
5273
6563
|
console.log(` ${c.cyan("Current:")} ${model}`);
|
|
6564
|
+
console.log(` ${c.cyan("Provider:")} ${getActiveProviderId() ?? "auto"}`);
|
|
6565
|
+
console.log(` ${c.cyan("Override:")} ${currentModelOverride ?? c.dim("(not set)")}`);
|
|
5274
6566
|
console.log(` ${c.cyan("ANTHROPIC_MODEL:")} ${process.env.ANTHROPIC_MODEL || c.dim("(not set)")}`);
|
|
6567
|
+
console.log(` ${c.cyan("GEMINI_MODEL:")} ${process.env.GEMINI_MODEL || c.dim("(not set)")}`);
|
|
6568
|
+
console.log(` ${c.cyan("GEMINI_BASE_URL:")} ${process.env.GEMINI_BASE_URL || c.dim("(not set)")}`);
|
|
5275
6569
|
console.log(` ${c.cyan("OPENAI_MODEL:")} ${process.env.OPENAI_MODEL || c.dim("(not set)")}`);
|
|
5276
6570
|
console.log(` ${c.cyan("OPENAI_BASE_URL:")} ${process.env.OPENAI_BASE_URL || c.dim("(not set)")}`);
|
|
5277
6571
|
console.log();
|
|
5278
6572
|
console.log(c.bold("API Keys:"));
|
|
5279
6573
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
6574
|
+
const geminiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
5280
6575
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
5281
6576
|
console.log(` ${c.cyan("ANTHROPIC_API_KEY:")} ${anthropicKey ? c.green("✓ set") + c.dim(` (${anthropicKey.slice(0, 8)}...${anthropicKey.slice(-4)})`) : c.red("✗ not set")}`);
|
|
6577
|
+
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
6578
|
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
6579
|
console.log();
|
|
5284
6580
|
console.log(c.bold("Environment:"));
|
|
@@ -5342,19 +6638,94 @@ function formatToolInput(name, input) {
|
|
|
5342
6638
|
return JSON.stringify(input).slice(0, 50);
|
|
5343
6639
|
}
|
|
5344
6640
|
}
|
|
5345
|
-
function
|
|
6641
|
+
function getDefaultProviderFromEnv() {
|
|
5346
6642
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
5347
|
-
return
|
|
6643
|
+
return "anthropic";
|
|
5348
6644
|
}
|
|
5349
6645
|
if (process.env.OPENAI_API_KEY) {
|
|
5350
|
-
return
|
|
6646
|
+
return "openai";
|
|
6647
|
+
}
|
|
6648
|
+
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
6649
|
+
return "gemini";
|
|
6650
|
+
}
|
|
6651
|
+
return null;
|
|
6652
|
+
}
|
|
6653
|
+
function inferProviderFromModel(model) {
|
|
6654
|
+
const normalized = model.toLowerCase();
|
|
6655
|
+
if (normalized.startsWith("claude")) {
|
|
6656
|
+
return "anthropic";
|
|
6657
|
+
}
|
|
6658
|
+
if (normalized.startsWith("gpt") || normalized.startsWith("o1") || normalized.startsWith("chatgpt")) {
|
|
6659
|
+
return "openai";
|
|
6660
|
+
}
|
|
6661
|
+
if (normalized.startsWith("gemini") || normalized.startsWith("models/gemini")) {
|
|
6662
|
+
return "gemini";
|
|
6663
|
+
}
|
|
6664
|
+
return null;
|
|
6665
|
+
}
|
|
6666
|
+
function getDefaultModelForProvider(providerId) {
|
|
6667
|
+
if (providerId === "anthropic") {
|
|
6668
|
+
return process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
|
|
6669
|
+
}
|
|
6670
|
+
if (providerId === "gemini") {
|
|
6671
|
+
return process.env.GEMINI_MODEL || "gemini-1.5-pro";
|
|
6672
|
+
}
|
|
6673
|
+
return process.env.OPENAI_MODEL || "gpt-5.2";
|
|
6674
|
+
}
|
|
6675
|
+
function getActiveProviderId() {
|
|
6676
|
+
if (currentProviderId) {
|
|
6677
|
+
return currentProviderId;
|
|
6678
|
+
}
|
|
6679
|
+
if (currentModelOverride) {
|
|
6680
|
+
return inferProviderFromModel(currentModelOverride);
|
|
6681
|
+
}
|
|
6682
|
+
return getDefaultProviderFromEnv();
|
|
6683
|
+
}
|
|
6684
|
+
function getActiveModel() {
|
|
6685
|
+
if (currentModelOverride) {
|
|
6686
|
+
return currentModelOverride;
|
|
6687
|
+
}
|
|
6688
|
+
const provider = getActiveProviderId();
|
|
6689
|
+
if (provider) {
|
|
6690
|
+
return getDefaultModelForProvider(provider);
|
|
5351
6691
|
}
|
|
5352
6692
|
return "claude-sonnet-4-20250514";
|
|
5353
6693
|
}
|
|
6694
|
+
function parseProvider(arg) {
|
|
6695
|
+
const normalized = arg.toLowerCase();
|
|
6696
|
+
if (normalized === "anthropic" || normalized === "claude") {
|
|
6697
|
+
return "anthropic";
|
|
6698
|
+
}
|
|
6699
|
+
if (normalized === "openai" || normalized === "gpt") {
|
|
6700
|
+
return "openai";
|
|
6701
|
+
}
|
|
6702
|
+
if (normalized === "gemini" || normalized === "google") {
|
|
6703
|
+
return "gemini";
|
|
6704
|
+
}
|
|
6705
|
+
return null;
|
|
6706
|
+
}
|
|
6707
|
+
function createProvider(providerId) {
|
|
6708
|
+
if (providerId === "anthropic") {
|
|
6709
|
+
return new AnthropicProvider;
|
|
6710
|
+
}
|
|
6711
|
+
if (providerId === "gemini") {
|
|
6712
|
+
return new GeminiProvider({
|
|
6713
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
6714
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
6715
|
+
});
|
|
6716
|
+
}
|
|
6717
|
+
return new OpenAIProvider({
|
|
6718
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
6719
|
+
baseUrl: process.env.OPENAI_BASE_URL
|
|
6720
|
+
});
|
|
6721
|
+
}
|
|
5354
6722
|
async function getSession() {
|
|
5355
6723
|
if (!session) {
|
|
6724
|
+
const providerId = getActiveProviderId();
|
|
6725
|
+
const provider = providerId ? createProvider(providerId) : undefined;
|
|
5356
6726
|
session = await createSession({
|
|
5357
|
-
model:
|
|
6727
|
+
model: getActiveModel(),
|
|
6728
|
+
provider,
|
|
5358
6729
|
tools: getAllTools(),
|
|
5359
6730
|
systemPrompt: buildSystemPrompt()
|
|
5360
6731
|
});
|
|
@@ -5412,7 +6783,9 @@ async function handleInput(input) {
|
|
|
5412
6783
|
return true;
|
|
5413
6784
|
}
|
|
5414
6785
|
if (trimmed.startsWith("/")) {
|
|
5415
|
-
const
|
|
6786
|
+
const parts = trimmed.split(/\s+/);
|
|
6787
|
+
const cmd = parts[0].toLowerCase();
|
|
6788
|
+
const args = parts.slice(1);
|
|
5416
6789
|
switch (cmd) {
|
|
5417
6790
|
case "/help":
|
|
5418
6791
|
printInteractiveHelp();
|
|
@@ -5436,6 +6809,9 @@ async function handleInput(input) {
|
|
|
5436
6809
|
case "/skills":
|
|
5437
6810
|
await printSkills();
|
|
5438
6811
|
return true;
|
|
6812
|
+
case "/models":
|
|
6813
|
+
await handleModelsCommand(args);
|
|
6814
|
+
return true;
|
|
5439
6815
|
case "/todos":
|
|
5440
6816
|
printTodos();
|
|
5441
6817
|
return true;
|
|
@@ -5468,9 +6844,9 @@ Error: ${error instanceof Error ? error.message : String(error)}
|
|
|
5468
6844
|
return true;
|
|
5469
6845
|
}
|
|
5470
6846
|
async function runQuickQuery(query) {
|
|
5471
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
6847
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
|
|
5472
6848
|
console.error(c.red("Error: No API key found"));
|
|
5473
|
-
console.error(c.dim("Set ANTHROPIC_API_KEY or
|
|
6849
|
+
console.error(c.dim("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable"));
|
|
5474
6850
|
process.exit(1);
|
|
5475
6851
|
}
|
|
5476
6852
|
try {
|
|
@@ -5484,9 +6860,9 @@ async function runQuickQuery(query) {
|
|
|
5484
6860
|
}
|
|
5485
6861
|
}
|
|
5486
6862
|
async function runInteractive() {
|
|
5487
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
6863
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
|
|
5488
6864
|
console.error(c.red("Error: No API key found"));
|
|
5489
|
-
console.error(c.dim("Set ANTHROPIC_API_KEY or
|
|
6865
|
+
console.error(c.dim("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable"));
|
|
5490
6866
|
process.exit(1);
|
|
5491
6867
|
}
|
|
5492
6868
|
setTodoChangeCallback(() => {});
|