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.
- package/dist/app/index.d.ts +6 -14
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +28 -30
- package/dist/app/index.js.map +1 -1
- package/dist/cmd/index.js +64 -8
- package/dist/cmd/index.js.map +1 -1
- package/dist/llm/anthropic.d.ts +4 -17
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +54 -12
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/gemini.d.ts +3 -16
- package/dist/llm/gemini.d.ts.map +1 -1
- package/dist/llm/gemini.js +26 -22
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/index.d.ts +5 -19
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +86 -87
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +9 -23
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +48 -14
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/provider.d.ts +11 -16
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/tui/input.d.ts +0 -1
- package/dist/tui/input.d.ts.map +1 -1
- package/dist/tui/input.js +2 -5
- package/dist/tui/input.js.map +1 -1
- package/dist/tui/output.d.ts +36 -12
- package/dist/tui/output.d.ts.map +1 -1
- package/dist/tui/output.js +172 -40
- package/dist/tui/output.js.map +1 -1
- package/dist/tui/repl.d.ts +0 -10
- package/dist/tui/repl.d.ts.map +1 -1
- package/dist/tui/repl.js +91 -43
- package/dist/tui/repl.js.map +1 -1
- package/dist/tui/welcome.d.ts +1 -11
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +60 -56
- package/dist/tui/welcome.js.map +1 -1
- package/package.json +1 -1
- package/src/app/index.ts +28 -33
- package/src/cmd/index.ts +53 -8
- package/src/llm/anthropic.ts +60 -14
- package/src/llm/gemini.ts +31 -24
- package/src/llm/index.ts +93 -92
- package/src/llm/openai.ts +58 -15
- package/src/llm/provider.ts +12 -16
- package/src/tui/input.ts +2 -5
- package/src/tui/output.ts +192 -40
- package/src/tui/repl.ts +94 -44
- package/src/tui/welcome.ts +62 -59
- 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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 (
|
|
121
|
+
if (!tool) {
|
|
122
|
+
const errMsg = `Tool "${toolCall.function.name}" not found`;
|
|
125
123
|
currentMessages.push({
|
|
126
|
-
role: '
|
|
127
|
-
content:
|
|
128
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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-..."
|
|
188
|
-
' export DEEPSEEK_API_KEY="sk-..."
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
100
|
+
|
|
101
|
+
// Streaming tool calls - accumulate chunks
|
|
95
102
|
if (delta?.tool_calls) {
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/src/llm/provider.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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);
|