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/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(block.name, block.input, block.id, abortSignal);
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 "${block.name}" was denied by hook`,
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
- content = await truncateToolOutput(content);
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(block.name, toolInput, toolResponse, block.id, abortSignal);
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
- const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
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
- const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
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
- return {
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?.(delta.content);
3624
+ options?.onText?.(part.text);
2649
3625
  options?.onEvent?.(textEvent);
2650
3626
  yield textEvent;
2651
3627
  }
2652
- if (delta?.tool_calls) {
2653
- for (const tc of delta.tool_calls) {
2654
- const index = tc.index;
2655
- const blockIndex = 1 + index;
2656
- if (!toolCalls.has(index)) {
2657
- toolCalls.set(index, {
2658
- id: tc.id || "",
2659
- name: tc.function?.name || "",
2660
- arguments: tc.function?.arguments || ""
2661
- });
2662
- const startEvent = {
2663
- type: "content_block_start",
2664
- index: blockIndex,
2665
- content_block: {
2666
- type: "tool_use",
2667
- id: tc.id || "",
2668
- name: tc.function?.name || "",
2669
- input: {}
2670
- }
2671
- };
2672
- options?.onEvent?.(startEvent);
2673
- yield startEvent;
2674
- } else {
2675
- const existing = toolCalls.get(index);
2676
- if (tc.id)
2677
- existing.id = tc.id;
2678
- if (tc.function?.name)
2679
- existing.name = tc.function.name;
2680
- if (tc.function?.arguments)
2681
- existing.arguments += tc.function.arguments;
2682
- }
2683
- if (tc.function?.arguments) {
2684
- const deltaEvent = {
2685
- type: "content_block_delta",
2686
- index: blockIndex,
2687
- delta: {
2688
- type: "input_json_delta",
2689
- partial_json: tc.function.arguments
2690
- }
2691
- };
2692
- options?.onEvent?.(deltaEvent);
2693
- yield deltaEvent;
2694
- }
2695
- }
2696
- }
2697
- if (finishReason) {
2698
- finished = true;
2699
- if (textBlockStarted) {
2700
- const stopText = { type: "content_block_stop", index: 0 };
2701
- options?.onEvent?.(stopText);
2702
- yield stopText;
2703
- }
2704
- for (const [index, tc] of toolCalls) {
2705
- const stopEvent2 = {
2706
- type: "content_block_stop",
2707
- index: 1 + index
2708
- };
2709
- options?.onEvent?.(stopEvent2);
2710
- yield stopEvent2;
2711
- try {
2712
- const input = JSON.parse(tc.arguments);
2713
- options?.onToolUse?.({ id: tc.id, name: tc.name, input });
2714
- } catch {}
2715
- }
2716
- const messageDelta = {
2717
- type: "message_delta",
2718
- delta: {
2719
- stop_reason: self.convertStopReason(finishReason),
2720
- stop_sequence: null
2721
- },
2722
- usage: {
2723
- output_tokens: json.usage?.completion_tokens ?? 0,
2724
- input_tokens: json.usage?.prompt_tokens ?? 0
2725
- }
2726
- };
2727
- options?.onEvent?.(messageDelta);
2728
- yield messageDelta;
2729
- const stopEvent = { type: "message_stop" };
2730
- options?.onEvent?.(stopEvent);
2731
- yield stopEvent;
2732
- }
2733
- } catch {}
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
- } finally {
2738
- reader.releaseLock();
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
- const headers = {
3765
+ return {
2745
3766
  "Content-Type": "application/json",
2746
- Authorization: `Bearer ${this.config.apiKey}`
3767
+ "x-goog-api-key": this.config.apiKey
2747
3768
  };
2748
- if (this.config.organization) {
2749
- headers["OpenAI-Organization"] = this.config.organization;
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
- return headers;
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 OPENAI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
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 OPENAI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
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
- resolve2({
3322
- content: `Command timed out after ${actualTimeout}ms
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
- isError: true
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.entries()),
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
- var VERSION = "0.1.0";
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-4o)
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 = getDefaultModel();
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 = getDefaultModel();
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 getDefaultModel() {
6677
+ function getDefaultProviderFromEnv() {
5346
6678
  if (process.env.ANTHROPIC_API_KEY) {
5347
- return process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
6679
+ return "anthropic";
5348
6680
  }
5349
6681
  if (process.env.OPENAI_API_KEY) {
5350
- return process.env.OPENAI_MODEL || "gpt-4o";
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: getDefaultModel(),
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 cmd = trimmed.toLowerCase();
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 OPENAI_API_KEY environment variable"));
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 OPENAI_API_KEY environment variable"));
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(() => {});