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.
Files changed (3) hide show
  1. package/dist/cli/index.js +1506 -130
  2. package/dist/index.js +1152 -27
  3. 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(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)` : "";
@@ -1753,7 +1754,7 @@ class SessionImpl {
1753
1754
  };
1754
1755
  }
1755
1756
  if (this.hooksManager) {
1756
- const postResult = await this.hooksManager.runPostToolUse(block.name, toolInput, toolResponse, block.id, abortSignal);
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
- const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
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
- const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
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
- return {
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
- if (delta?.content) {
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?.(delta.content);
3623
+ options?.onText?.(part.text);
2649
3624
  options?.onEvent?.(textEvent);
2650
3625
  yield textEvent;
2651
3626
  }
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 {}
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
- } finally {
2738
- reader.releaseLock();
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
- const headers = {
3764
+ return {
2745
3765
  "Content-Type": "application/json",
2746
- Authorization: `Bearer ${this.config.apiKey}`
3766
+ "x-goog-api-key": this.config.apiKey
2747
3767
  };
2748
- if (this.config.organization) {
2749
- headers["OpenAI-Organization"] = this.config.organization;
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
- return headers;
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 OPENAI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
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 OPENAI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
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
- var VERSION = "0.1.0";
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-4o)
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 = getDefaultModel();
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 = getDefaultModel();
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 getDefaultModel() {
6641
+ function getDefaultProviderFromEnv() {
5346
6642
  if (process.env.ANTHROPIC_API_KEY) {
5347
- return process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
6643
+ return "anthropic";
5348
6644
  }
5349
6645
  if (process.env.OPENAI_API_KEY) {
5350
- return process.env.OPENAI_MODEL || "gpt-4o";
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: getDefaultModel(),
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 cmd = trimmed.toLowerCase();
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 OPENAI_API_KEY environment variable"));
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 OPENAI_API_KEY environment variable"));
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(() => {});