thatgfsj-code 0.4.0 → 0.5.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 (53) hide show
  1. package/dist/app/index.d.ts +6 -14
  2. package/dist/app/index.d.ts.map +1 -1
  3. package/dist/app/index.js +28 -30
  4. package/dist/app/index.js.map +1 -1
  5. package/dist/cmd/index.js +64 -8
  6. package/dist/cmd/index.js.map +1 -1
  7. package/dist/llm/anthropic.d.ts +4 -17
  8. package/dist/llm/anthropic.d.ts.map +1 -1
  9. package/dist/llm/anthropic.js +54 -12
  10. package/dist/llm/anthropic.js.map +1 -1
  11. package/dist/llm/gemini.d.ts +3 -16
  12. package/dist/llm/gemini.d.ts.map +1 -1
  13. package/dist/llm/gemini.js +26 -22
  14. package/dist/llm/gemini.js.map +1 -1
  15. package/dist/llm/index.d.ts +5 -19
  16. package/dist/llm/index.d.ts.map +1 -1
  17. package/dist/llm/index.js +86 -87
  18. package/dist/llm/index.js.map +1 -1
  19. package/dist/llm/openai.d.ts +9 -23
  20. package/dist/llm/openai.d.ts.map +1 -1
  21. package/dist/llm/openai.js +48 -14
  22. package/dist/llm/openai.js.map +1 -1
  23. package/dist/llm/provider.d.ts +11 -16
  24. package/dist/llm/provider.d.ts.map +1 -1
  25. package/dist/tui/input.d.ts +0 -1
  26. package/dist/tui/input.d.ts.map +1 -1
  27. package/dist/tui/input.js +2 -5
  28. package/dist/tui/input.js.map +1 -1
  29. package/dist/tui/output.d.ts +36 -12
  30. package/dist/tui/output.d.ts.map +1 -1
  31. package/dist/tui/output.js +172 -40
  32. package/dist/tui/output.js.map +1 -1
  33. package/dist/tui/repl.d.ts +0 -10
  34. package/dist/tui/repl.d.ts.map +1 -1
  35. package/dist/tui/repl.js +91 -43
  36. package/dist/tui/repl.js.map +1 -1
  37. package/dist/tui/welcome.d.ts +1 -11
  38. package/dist/tui/welcome.d.ts.map +1 -1
  39. package/dist/tui/welcome.js +60 -56
  40. package/dist/tui/welcome.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/app/index.ts +28 -33
  43. package/src/cmd/index.ts +53 -8
  44. package/src/llm/anthropic.ts +60 -14
  45. package/src/llm/gemini.ts +31 -24
  46. package/src/llm/index.ts +93 -92
  47. package/src/llm/openai.ts +58 -15
  48. package/src/llm/provider.ts +12 -16
  49. package/src/tui/input.ts +2 -5
  50. package/src/tui/output.ts +192 -40
  51. package/src/tui/repl.ts +94 -44
  52. package/src/tui/welcome.ts +62 -59
  53. package/src/app/agent.ts +0 -140
package/src/llm/gemini.ts CHANGED
@@ -1,20 +1,10 @@
1
1
  /**
2
2
  * Google Gemini Provider
3
- *
4
- * API: POST {baseUrl}/models/{model}:streamGenerateContent?key={apiKey}
5
- * Auth: API key in URL query parameter
6
- * Streaming: JSON array chunks (not SSE)
7
- *
8
- * Key differences:
9
- * - No "system" role in contents, use "systemInstruction" field
10
- * - Roles are "user" and "model" (not "assistant")
11
- * - Content is array of "parts" with "text" field
12
- * - Auth via URL query param, not header
13
3
  */
14
4
 
15
- import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
5
+ import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
16
6
  import type { Tool } from '../tools/types.js';
17
- import type { LLMProvider, ProviderConfig } from './provider.js';
7
+ import type { LLMProvider, StreamChunk, ProviderConfig } from './provider.js';
18
8
 
19
9
  export class GeminiProvider implements LLMProvider {
20
10
  readonly name = 'gemini';
@@ -40,8 +30,8 @@ export class GeminiProvider implements LLMProvider {
40
30
  }];
41
31
  }
42
32
 
43
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
44
- const body = this.buildRequest(messages, options);
33
+ async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
34
+ const body = this.buildRequest(messages, options, tools);
45
35
  const url = `${this.config.baseUrl}/models/${this.config.model}:generateContent?key=${this.config.apiKey}`;
46
36
 
47
37
  const response = await fetch(url, {
@@ -60,7 +50,6 @@ export class GeminiProvider implements LLMProvider {
60
50
  const parts = candidate?.content?.parts || [];
61
51
  const text = parts.map((p: any) => p.text).filter(Boolean).join('');
62
52
 
63
- // Extract function calls
64
53
  const functionCalls = parts.filter((p: any) => p.functionCall).map((p: any) => ({
65
54
  id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
66
55
  type: 'function' as const,
@@ -82,8 +71,8 @@ export class GeminiProvider implements LLMProvider {
82
71
  };
83
72
  }
84
73
 
85
- async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse> {
86
- const body = this.buildRequest(messages, options);
74
+ async *chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse> {
75
+ const body = this.buildRequest(messages, options, tools);
87
76
  const url = `${this.config.baseUrl}/models/${this.config.model}:streamGenerateContent?alt=sse&key=${this.config.apiKey}`;
88
77
 
89
78
  const response = await fetch(url, {
@@ -101,6 +90,7 @@ export class GeminiProvider implements LLMProvider {
101
90
  const decoder = new TextDecoder();
102
91
  let fullContent = '';
103
92
  let buffer = '';
93
+ const functionCalls: ToolCall[] = [];
104
94
 
105
95
  try {
106
96
  while (true) {
@@ -121,7 +111,17 @@ export class GeminiProvider implements LLMProvider {
121
111
  for (const part of parts) {
122
112
  if (part.text) {
123
113
  fullContent += part.text;
124
- yield part.text;
114
+ yield { type: 'text', content: part.text };
115
+ }
116
+ if (part.functionCall) {
117
+ functionCalls.push({
118
+ id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
119
+ type: 'function',
120
+ function: {
121
+ name: part.functionCall.name,
122
+ arguments: JSON.stringify(part.functionCall.args || {}),
123
+ },
124
+ });
125
125
  }
126
126
  }
127
127
  } catch {
@@ -133,13 +133,14 @@ export class GeminiProvider implements LLMProvider {
133
133
  reader.releaseLock();
134
134
  }
135
135
 
136
- return { content: fullContent, role: 'assistant' };
136
+ if (functionCalls.length > 0) {
137
+ yield { type: 'tool_calls', toolCalls: functionCalls };
138
+ }
139
+
140
+ return { content: fullContent, role: 'assistant', tool_calls: functionCalls.length > 0 ? functionCalls : undefined };
137
141
  }
138
142
 
139
- /**
140
- * Build request body for Gemini API
141
- */
142
- private buildRequest(messages: ChatMessage[], options?: ChatOptions) {
143
+ private buildRequest(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]) {
143
144
  const systemMsg = messages.find(m => m.role === 'system');
144
145
  const nonSystemMsgs = messages.filter(m => m.role !== 'system');
145
146
 
@@ -148,7 +149,7 @@ export class GeminiProvider implements LLMProvider {
148
149
  parts: [{ text: m.content }],
149
150
  }));
150
151
 
151
- return {
152
+ const body: any = {
152
153
  contents,
153
154
  ...(systemMsg && {
154
155
  systemInstruction: { parts: [{ text: systemMsg.content }] },
@@ -158,5 +159,11 @@ export class GeminiProvider implements LLMProvider {
158
159
  maxOutputTokens: options?.maxTokens ?? this.config.maxTokens,
159
160
  },
160
161
  };
162
+
163
+ if (tools && tools.length > 0) {
164
+ body.tools = this.buildTools(tools);
165
+ }
166
+
167
+ return body;
161
168
  }
162
169
  }
package/src/llm/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import chalk from 'chalk';
7
7
  import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
8
8
  import type { Tool } from '../tools/types.js';
9
- import type { LLMProvider } from './provider.js';
9
+ import type { LLMProvider, StreamChunk } from './provider.js';
10
10
  import type { AIConfig, ProviderName } from '../config/types.js';
11
11
  import { PROVIDERS } from '../config/providers.js';
12
12
  import { OpenAIProvider } from './openai.js';
@@ -23,9 +23,6 @@ export class LLMService {
23
23
  this.apiKey = apiKey;
24
24
  }
25
25
 
26
- /**
27
- * Create LLMService from AIConfig
28
- */
29
26
  static fromConfig(config: AIConfig): LLMService {
30
27
  const providerName = config.provider || 'siliconflow';
31
28
  const providerConfig = PROVIDERS[providerName];
@@ -55,49 +52,32 @@ export class LLMService {
55
52
  return new LLMService(provider, providerCfg.apiKey);
56
53
  }
57
54
 
58
- /**
59
- * Register tools for function calling
60
- */
61
55
  registerTools(tools: Tool[]): void {
62
56
  for (const tool of tools) {
63
57
  this.tools.set(tool.name, tool);
64
58
  }
65
59
  }
66
60
 
67
- /**
68
- * Get provider name
69
- */
70
- getProviderName(): string {
71
- return this.provider.name;
72
- }
61
+ getProviderName(): string { return this.provider.name; }
62
+ hasApiKey(): boolean { return !!this.apiKey; }
73
63
 
74
- /**
75
- * Check if API key is available
76
- */
77
- hasApiKey(): boolean {
78
- return !!this.apiKey;
79
- }
80
-
81
- /**
82
- * Non-streaming chat
83
- */
84
64
  async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
85
- if (!this.hasApiKey()) {
86
- throw new Error(this.getNoKeyMessage());
87
- }
88
- return this.provider.chat(messages, options);
65
+ if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
66
+ const toolsArray = [...this.tools.values()];
67
+ return this.provider.chat(messages, options, toolsArray.length > 0 ? toolsArray : undefined);
89
68
  }
90
69
 
91
70
  /**
92
71
  * Streaming chat with agent loop (tool call support)
72
+ * - Streams text to the user
73
+ * - Detects structured tool calls from the API
74
+ * - Executes tools and loops back for more
93
75
  */
94
76
  async *chatStream(
95
77
  messages: ChatMessage[],
96
78
  options?: ChatOptions & { maxIterations?: number }
97
79
  ): AsyncGenerator<string, ChatResponse> {
98
- if (!this.hasApiKey()) {
99
- throw new Error(this.getNoKeyMessage());
100
- }
80
+ if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
101
81
 
102
82
  const maxIterations = options?.maxIterations ?? 10;
103
83
  let currentMessages = [...messages];
@@ -105,95 +85,116 @@ export class LLMService {
105
85
 
106
86
  while (iterations < maxIterations) {
107
87
  iterations++;
88
+ const toolsArray = [...this.tools.values()];
89
+ const hasTools = toolsArray.length > 0;
108
90
 
109
- // Stream the response
110
91
  let fullContent = '';
111
- for await (const chunk of this.provider.chatStream(currentMessages, options)) {
112
- fullContent += chunk;
113
- yield chunk;
92
+ let detectedToolCalls: ToolCall[] | undefined;
93
+
94
+ const stream = this.provider.chatStream(currentMessages, options, hasTools ? toolsArray : undefined);
95
+
96
+ for await (const chunk of stream) {
97
+ if (chunk.type === 'text' && chunk.content) {
98
+ fullContent += chunk.content;
99
+ yield chunk.content;
100
+ } else if (chunk.type === 'tool_calls' && chunk.toolCalls) {
101
+ detectedToolCalls = chunk.toolCalls;
102
+ }
114
103
  }
115
104
 
116
- // Check for tool calls by making a non-streaming request
117
- // (streaming tool calls are complex, so we check after stream completes)
118
- const toolsArray = [...this.tools.values()];
119
- if (toolsArray.length > 0 && iterations < maxIterations) {
120
- try {
121
- const response = await this.provider.chat(currentMessages, options);
122
- const toolCalls = response.tool_calls;
105
+ // If we got tool calls, execute them and loop
106
+ if (detectedToolCalls && detectedToolCalls.length > 0) {
107
+ // Add assistant message with tool calls
108
+ currentMessages.push({
109
+ role: 'assistant',
110
+ content: fullContent || '',
111
+ tool_calls: detectedToolCalls,
112
+ });
113
+
114
+ // Execute each tool
115
+ for (const toolCall of detectedToolCalls) {
116
+ const tool = this.tools.get(toolCall.function.name);
117
+
118
+ // Yield structured tool call as JSON line
119
+ yield `\n@@TOOL@@${JSON.stringify({ action: 'call', name: toolCall.function.name, args: toolCall.function.arguments })}\n`;
123
120
 
124
- if (toolCalls && toolCalls.length > 0) {
121
+ if (!tool) {
122
+ const errMsg = `Tool "${toolCall.function.name}" not found`;
125
123
  currentMessages.push({
126
- role: 'assistant',
127
- content: response.content || fullContent,
128
- tool_calls: toolCalls,
124
+ role: 'tool',
125
+ content: errMsg,
126
+ tool_call_id: toolCall.id,
127
+ name: toolCall.function.name,
129
128
  });
130
-
131
- for (const toolCall of toolCalls) {
132
- const tool = this.tools.get(toolCall.function.name);
133
- if (!tool) {
134
- currentMessages.push({
135
- role: 'tool',
136
- content: `Tool "${toolCall.function.name}" not found`,
137
- tool_call_id: toolCall.id,
138
- name: toolCall.function.name,
139
- });
140
- continue;
141
- }
142
-
143
- try {
144
- const params = JSON.parse(toolCall.function.arguments || '{}');
145
- const result = await tool.execute(params);
146
- currentMessages.push({
147
- role: 'tool',
148
- content: result.success ? (result.output || JSON.stringify(result.data)) : (result.error || 'Tool failed'),
149
- tool_call_id: toolCall.id,
150
- name: toolCall.function.name,
151
- });
152
- yield `\n${chalk.gray(`[tool: ${toolCall.function.name}]`)}`;
153
- } catch (error: any) {
154
- currentMessages.push({
155
- role: 'tool',
156
- content: `Tool execution error: ${error.message}`,
157
- tool_call_id: toolCall.id,
158
- name: toolCall.function.name,
159
- });
160
- }
161
- }
129
+ yield `@@TOOL@@${JSON.stringify({ action: 'result', error: errMsg })}\n`;
162
130
  continue;
163
131
  }
164
- } catch {
165
- // If the non-streaming check fails, just return the streamed content
132
+
133
+ try {
134
+ const params = JSON.parse(toolCall.function.arguments || '{}');
135
+ const result = await tool.execute(params);
136
+ const output = result.success ? (result.output || JSON.stringify(result.data)) : (result.error || 'Tool failed');
137
+
138
+ currentMessages.push({
139
+ role: 'tool',
140
+ content: output,
141
+ tool_call_id: toolCall.id,
142
+ name: toolCall.function.name,
143
+ });
144
+
145
+ yield `@@TOOL@@${JSON.stringify({ action: 'result', output })}\n`;
146
+ } catch (error: any) {
147
+ const errMsg = `Error: ${error.message}`;
148
+ currentMessages.push({
149
+ role: 'tool',
150
+ content: errMsg,
151
+ tool_call_id: toolCall.id,
152
+ name: toolCall.function.name,
153
+ });
154
+ yield `@@TOOL@@${JSON.stringify({ action: 'result', error: errMsg })}\n`;
155
+ }
166
156
  }
157
+
158
+ yield '\n';
159
+ continue;
167
160
  }
168
161
 
162
+ // No tool calls - done
169
163
  return { content: fullContent, role: 'assistant' };
170
164
  }
171
165
 
172
166
  return { content: '[Agent loop exceeded maximum iterations]', role: 'assistant' };
173
167
  }
174
168
 
175
- /**
176
- * Error message when no API key is configured
177
- */
169
+ private truncateArgs(args: string): string {
170
+ try {
171
+ const obj = JSON.parse(args || '{}');
172
+ const entries = Object.entries(obj);
173
+ if (entries.length === 0) return '';
174
+ return entries.map(([k, v]) => {
175
+ const val = typeof v === 'string' && v.length > 50 ? v.slice(0, 50) + '...' : v;
176
+ return `${k}: ${JSON.stringify(val)}`;
177
+ }).join(', ');
178
+ } catch {
179
+ return args.length > 80 ? args.slice(0, 80) + '...' : args;
180
+ }
181
+ }
182
+
178
183
  private getNoKeyMessage(): string {
179
184
  return [
180
185
  'āŒ ęœŖé…ē½® API Keyļ¼Œę— ę³•č°ƒē”Ø AI怂',
181
186
  '',
182
- 'čÆ·å…ˆčæč”Œä»„äø‹å‘½ä»¤é…ē½®:',
183
- ' gfcode init',
187
+ 'čÆ·å…ˆčæč”Œ: gfcode init',
184
188
  '',
185
- 'ęˆ–ę‰‹åŠØč®¾ē½®ēŽÆå¢ƒå˜é‡:',
186
- ' export SILICONFLOW_API_KEY="sk-..." # ē”…åŸŗęµåŠØ',
187
- ' export OPENAI_API_KEY="sk-..." # OpenAI',
188
- ' export DEEPSEEK_API_KEY="sk-..." # DeepSeek',
189
- ' export KIMI_API_KEY="sk-..." # Kimi',
190
- ' export ANTHROPIC_API_KEY="sk-ant-..." # Claude',
189
+ 'ęˆ–č®¾ē½®ēŽÆå¢ƒå˜é‡:',
190
+ ' export SILICONFLOW_API_KEY="sk-..."',
191
+ ' export OPENAI_API_KEY="sk-..."',
192
+ ' export DEEPSEEK_API_KEY="sk-..."',
191
193
  ].join('\n');
192
194
  }
193
195
  }
194
196
 
195
- // Re-export provider types
196
- export type { LLMProvider, ProviderConfig } from './provider.js';
197
+ export type { LLMProvider, ProviderConfig, StreamChunk } from './provider.js';
197
198
  export { OpenAIProvider } from './openai.js';
198
199
  export { AnthropicProvider } from './anthropic.js';
199
200
  export { GeminiProvider } from './gemini.js';
package/src/llm/openai.ts CHANGED
@@ -2,16 +2,18 @@
2
2
  * OpenAI-compatible Provider
3
3
  * Works with: OpenAI, SiliconFlow, DeepSeek, Kimi, Zhipu, MiniMax, Baichuan, Stepfun, Doubao, Ollama, ERNIE
4
4
  * Also works with any OpenAI-compatible relay station (中转站)
5
- *
6
- * API: POST {baseUrl}/chat/completions
7
- * Auth: Authorization: Bearer {apiKey}
8
- * Streaming: SSE with "data: {...}" lines, ends with "data: [DONE]"
9
5
  */
10
6
 
11
- import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
7
+ import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
12
8
  import type { Tool } from '../tools/types.js';
13
9
  import type { LLMProvider, ProviderConfig } from './provider.js';
14
10
 
11
+ export interface StreamChunk {
12
+ type: 'text' | 'tool_calls';
13
+ content?: string;
14
+ toolCalls?: ToolCall[];
15
+ }
16
+
15
17
  export class OpenAIProvider implements LLMProvider {
16
18
  readonly name = 'openai';
17
19
  protected config: ProviderConfig;
@@ -37,8 +39,8 @@ export class OpenAIProvider implements LLMProvider {
37
39
  }));
38
40
  }
39
41
 
40
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
41
- const body = this.buildRequest(messages, false, options);
42
+ async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
43
+ const body = this.buildRequest(messages, false, options, tools);
42
44
  const response = await this.doRequest(body);
43
45
  const data = await response.json();
44
46
  const choice = data.choices?.[0];
@@ -55,8 +57,8 @@ export class OpenAIProvider implements LLMProvider {
55
57
  };
56
58
  }
57
59
 
58
- async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse> {
59
- const body = this.buildRequest(messages, true, options);
60
+ async *chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse> {
61
+ const body = this.buildRequest(messages, true, options, tools);
60
62
  const response = await this.doRequest(body);
61
63
 
62
64
  if (!response.ok || !response.body) {
@@ -68,6 +70,8 @@ export class OpenAIProvider implements LLMProvider {
68
70
  const decoder = new TextDecoder();
69
71
  let fullContent = '';
70
72
  let buffer = '';
73
+ // Accumulate streaming tool call chunks
74
+ const toolCallBuffers: Map<number, { id: string; name: string; arguments: string }> = new Map();
71
75
 
72
76
  try {
73
77
  while (true) {
@@ -87,13 +91,25 @@ export class OpenAIProvider implements LLMProvider {
87
91
  try {
88
92
  const data = JSON.parse(trimmed.slice(6));
89
93
  const delta = data.choices?.[0]?.delta;
94
+
95
+ // Text content
90
96
  if (delta?.content) {
91
97
  fullContent += delta.content;
92
- yield delta.content;
98
+ yield { type: 'text', content: delta.content };
93
99
  }
94
- // Handle tool calls in streaming
100
+
101
+ // Streaming tool calls - accumulate chunks
95
102
  if (delta?.tool_calls) {
96
- // Store tool calls for final response
103
+ for (const tc of delta.tool_calls) {
104
+ const idx = tc.index ?? 0;
105
+ if (!toolCallBuffers.has(idx)) {
106
+ toolCallBuffers.set(idx, { id: '', name: '', arguments: '' });
107
+ }
108
+ const buf = toolCallBuffers.get(idx)!;
109
+ if (tc.id) buf.id = tc.id;
110
+ if (tc.function?.name) buf.name += tc.function.name;
111
+ if (tc.function?.arguments) buf.arguments += tc.function.arguments;
112
+ }
97
113
  }
98
114
  } catch {
99
115
  // Skip invalid JSON lines
@@ -104,14 +120,34 @@ export class OpenAIProvider implements LLMProvider {
104
120
  reader.releaseLock();
105
121
  }
106
122
 
107
- return { content: fullContent, role: 'assistant' };
123
+ // Convert accumulated tool call buffers to ToolCall[]
124
+ const toolCalls: ToolCall[] = [];
125
+ for (const [, buf] of toolCallBuffers) {
126
+ if (buf.id && buf.name) {
127
+ toolCalls.push({
128
+ id: buf.id,
129
+ type: 'function',
130
+ function: { name: buf.name, arguments: buf.arguments },
131
+ });
132
+ }
133
+ }
134
+
135
+ if (toolCalls.length > 0) {
136
+ yield { type: 'tool_calls', toolCalls };
137
+ }
138
+
139
+ return {
140
+ content: fullContent,
141
+ role: 'assistant',
142
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
143
+ };
108
144
  }
109
145
 
110
146
  /**
111
147
  * Build the request body for OpenAI-compatible API
112
148
  */
113
- protected buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions) {
114
- return {
149
+ protected buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions, tools?: Tool[]) {
150
+ const body: any = {
115
151
  model: this.config.model,
116
152
  messages: messages.map(m => ({
117
153
  role: m.role,
@@ -125,6 +161,13 @@ export class OpenAIProvider implements LLMProvider {
125
161
  stream,
126
162
  ...(stream && { stream_options: { include_usage: true } }),
127
163
  };
164
+
165
+ // Add tools if provided - this is critical for structured tool calling
166
+ if (tools && tools.length > 0) {
167
+ body.tools = this.buildTools(tools);
168
+ }
169
+
170
+ return body;
128
171
  }
129
172
 
130
173
  /**
@@ -3,32 +3,28 @@
3
3
  * All providers implement this interface
4
4
  */
5
5
 
6
- import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
6
+ import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
7
7
  import type { Tool } from '../tools/types.js';
8
8
 
9
+ export interface StreamChunk {
10
+ type: 'text' | 'tool_calls';
11
+ content?: string;
12
+ toolCalls?: ToolCall[];
13
+ }
14
+
9
15
  export interface LLMProvider {
10
- /** Provider name for logging */
11
16
  readonly name: string;
12
17
 
13
- /**
14
- * Non-streaming chat completion
15
- */
16
- chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse>;
18
+ /** Non-streaming chat with optional tools */
19
+ chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse>;
17
20
 
18
- /**
19
- * Streaming chat completion - yields text chunks
20
- */
21
- chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse>;
21
+ /** Streaming chat with optional tools - yields StreamChunks */
22
+ chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse>;
22
23
 
23
- /**
24
- * Convert Tool[] to provider-specific format
25
- */
24
+ /** Convert Tool[] to provider-specific format */
26
25
  buildTools(tools: Tool[]): any[];
27
26
  }
28
27
 
29
- /**
30
- * Provider configuration passed to each provider
31
- */
32
28
  export interface ProviderConfig {
33
29
  apiKey: string;
34
30
  model: string;
package/src/tui/input.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * REPL Input Handler
3
- * Migrated from old src/repl/input.ts
4
3
  */
5
4
 
6
5
  import readline from 'readline';
@@ -20,13 +19,11 @@ export class REPLInput {
20
19
  tabSize: 4,
21
20
  });
22
21
 
23
- // Handle Ctrl+C
24
22
  this.rl.on('SIGINT', () => {
25
- process.stdout.write(chalk.gray('\n\nšŸ‘‹ Exiting...\n'));
23
+ process.stdout.write(chalk.gray('\n Goodbye! šŸ‘‹\n'));
26
24
  process.exit(0);
27
25
  });
28
26
 
29
- // Track history
30
27
  this.rl.on('line', (line) => {
31
28
  if (line.trim()) {
32
29
  this.history.push(line);
@@ -36,7 +33,7 @@ export class REPLInput {
36
33
  });
37
34
  }
38
35
 
39
- async prompt(prefix: string = chalk.green('\n> ')): Promise<string> {
36
+ async prompt(prefix: string = chalk.cyan('āÆ ')): Promise<string> {
40
37
  return new Promise((resolve) => {
41
38
  this.rl.question(prefix, (answer) => {
42
39
  resolve(answer);