thatgfsj-code 0.3.0 → 0.4.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/cmd/index.js +1 -1
- package/dist/config/index.d.ts +5 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +22 -4
- package/dist/config/index.js.map +1 -1
- package/dist/config/providers.d.ts +5 -10
- package/dist/config/providers.d.ts.map +1 -1
- package/dist/config/providers.js +152 -78
- package/dist/config/providers.js.map +1 -1
- package/dist/config/types.d.ts +1 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/anthropic.d.ts +34 -3
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +83 -35
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/gemini.d.ts +14 -1
- package/dist/llm/gemini.d.ts.map +1 -1
- package/dist/llm/gemini.js +56 -23
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/index.d.ts +9 -8
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +75 -80
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +30 -3
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +45 -28
- package/dist/llm/openai.js.map +1 -1
- package/dist/tui/output.js +1 -1
- package/dist/tui/welcome.d.ts +5 -1
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +75 -29
- package/dist/tui/welcome.js.map +1 -1
- package/package.json +1 -1
- package/src/cmd/index.ts +1 -1
- package/src/config/index.ts +21 -4
- package/src/config/providers.ts +153 -79
- package/src/config/types.ts +11 -5
- package/src/llm/anthropic.ts +90 -39
- package/src/llm/gemini.ts +65 -30
- package/src/llm/index.ts +77 -87
- package/src/llm/openai.ts +46 -34
- package/src/tui/output.ts +1 -1
- package/src/tui/welcome.ts +86 -32
package/src/llm/gemini.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
|
3
13
|
*/
|
|
4
14
|
|
|
5
15
|
import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
|
|
@@ -8,30 +18,32 @@ import type { LLMProvider, ProviderConfig } from './provider.js';
|
|
|
8
18
|
|
|
9
19
|
export class GeminiProvider implements LLMProvider {
|
|
10
20
|
readonly name = 'gemini';
|
|
11
|
-
|
|
21
|
+
protected config: ProviderConfig;
|
|
12
22
|
|
|
13
23
|
constructor(config: ProviderConfig) {
|
|
14
24
|
this.config = config;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
buildTools(tools: Tool[]): any[] {
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
return [{
|
|
29
|
+
functionDeclarations: tools.map(tool => ({
|
|
30
|
+
name: tool.name,
|
|
31
|
+
description: tool.description,
|
|
32
|
+
parameters: tool.inputSchema || {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: Object.fromEntries(
|
|
35
|
+
tool.parameters.map(p => [p.name, { type: p.type, description: p.description }])
|
|
36
|
+
),
|
|
37
|
+
required: tool.parameters.filter(p => p.required).map(p => p.name),
|
|
38
|
+
},
|
|
39
|
+
})),
|
|
40
|
+
}];
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
|
|
32
44
|
const body = this.buildRequest(messages, options);
|
|
33
|
-
|
|
34
45
|
const url = `${this.config.baseUrl}/models/${this.config.model}:generateContent?key=${this.config.apiKey}`;
|
|
46
|
+
|
|
35
47
|
const response = await fetch(url, {
|
|
36
48
|
method: 'POST',
|
|
37
49
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -45,7 +57,18 @@ export class GeminiProvider implements LLMProvider {
|
|
|
45
57
|
|
|
46
58
|
const data = await response.json();
|
|
47
59
|
const candidate = data.candidates?.[0];
|
|
48
|
-
const
|
|
60
|
+
const parts = candidate?.content?.parts || [];
|
|
61
|
+
const text = parts.map((p: any) => p.text).filter(Boolean).join('');
|
|
62
|
+
|
|
63
|
+
// Extract function calls
|
|
64
|
+
const functionCalls = parts.filter((p: any) => p.functionCall).map((p: any) => ({
|
|
65
|
+
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
66
|
+
type: 'function' as const,
|
|
67
|
+
function: {
|
|
68
|
+
name: p.functionCall.name,
|
|
69
|
+
arguments: JSON.stringify(p.functionCall.args || {}),
|
|
70
|
+
},
|
|
71
|
+
}));
|
|
49
72
|
|
|
50
73
|
return {
|
|
51
74
|
content: text,
|
|
@@ -55,13 +78,14 @@ export class GeminiProvider implements LLMProvider {
|
|
|
55
78
|
completion_tokens: data.usageMetadata.candidatesTokenCount || 0,
|
|
56
79
|
total_tokens: data.usageMetadata.totalTokenCount || 0,
|
|
57
80
|
} : undefined,
|
|
81
|
+
tool_calls: functionCalls.length > 0 ? functionCalls : undefined,
|
|
58
82
|
};
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse> {
|
|
62
86
|
const body = this.buildRequest(messages, options);
|
|
87
|
+
const url = `${this.config.baseUrl}/models/${this.config.model}:streamGenerateContent?alt=sse&key=${this.config.apiKey}`;
|
|
63
88
|
|
|
64
|
-
const url = `${this.config.baseUrl}/models/${this.config.model}:streamGenerateContent?key=${this.config.apiKey}`;
|
|
65
89
|
const response = await fetch(url, {
|
|
66
90
|
method: 'POST',
|
|
67
91
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -84,21 +108,25 @@ export class GeminiProvider implements LLMProvider {
|
|
|
84
108
|
if (done) break;
|
|
85
109
|
|
|
86
110
|
buffer += decoder.decode(value, { stream: true });
|
|
111
|
+
const lines = buffer.split('\n');
|
|
112
|
+
buffer = lines.pop() || '';
|
|
113
|
+
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
|
87
117
|
|
|
88
|
-
// Gemini streams JSON array chunks
|
|
89
|
-
const jsonMatch = buffer.match(/\{"candidates":\[.*?\]\}/);
|
|
90
|
-
if (jsonMatch) {
|
|
91
118
|
try {
|
|
92
|
-
const data = JSON.parse(
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
120
|
+
const parts = data.candidates?.[0]?.content?.parts || [];
|
|
121
|
+
for (const part of parts) {
|
|
122
|
+
if (part.text) {
|
|
123
|
+
fullContent += part.text;
|
|
124
|
+
yield part.text;
|
|
125
|
+
}
|
|
97
126
|
}
|
|
98
127
|
} catch {
|
|
99
128
|
// Skip invalid JSON
|
|
100
129
|
}
|
|
101
|
-
buffer = buffer.slice(jsonMatch.index! + jsonMatch[0].length);
|
|
102
130
|
}
|
|
103
131
|
}
|
|
104
132
|
} finally {
|
|
@@ -108,16 +136,23 @@ export class GeminiProvider implements LLMProvider {
|
|
|
108
136
|
return { content: fullContent, role: 'assistant' };
|
|
109
137
|
}
|
|
110
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Build request body for Gemini API
|
|
141
|
+
*/
|
|
111
142
|
private buildRequest(messages: ChatMessage[], options?: ChatOptions) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
143
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
144
|
+
const nonSystemMsgs = messages.filter(m => m.role !== 'system');
|
|
145
|
+
|
|
146
|
+
const contents = nonSystemMsgs.map(m => ({
|
|
147
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
148
|
+
parts: [{ text: m.content }],
|
|
149
|
+
}));
|
|
118
150
|
|
|
119
151
|
return {
|
|
120
152
|
contents,
|
|
153
|
+
...(systemMsg && {
|
|
154
|
+
systemInstruction: { parts: [{ text: systemMsg.content }] },
|
|
155
|
+
}),
|
|
121
156
|
generationConfig: {
|
|
122
157
|
temperature: options?.temperature ?? this.config.temperature,
|
|
123
158
|
maxOutputTokens: options?.maxTokens ?? this.config.maxTokens,
|
package/src/llm/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LLM Service - Factory for creating providers
|
|
3
|
-
*
|
|
3
|
+
* Supports all providers + custom relay stations (中转站)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk';
|
|
@@ -16,9 +16,11 @@ import { GeminiProvider } from './gemini.js';
|
|
|
16
16
|
export class LLMService {
|
|
17
17
|
private provider: LLMProvider;
|
|
18
18
|
private tools: Map<string, Tool> = new Map();
|
|
19
|
+
private apiKey: string;
|
|
19
20
|
|
|
20
|
-
constructor(provider: LLMProvider) {
|
|
21
|
+
constructor(provider: LLMProvider, apiKey: string) {
|
|
21
22
|
this.provider = provider;
|
|
23
|
+
this.apiKey = apiKey;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -50,7 +52,7 @@ export class LLMService {
|
|
|
50
52
|
provider = new OpenAIProvider(providerCfg);
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
return new LLMService(provider);
|
|
55
|
+
return new LLMService(provider, providerCfg.apiKey);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
@@ -69,12 +71,19 @@ export class LLMService {
|
|
|
69
71
|
return this.provider.name;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Check if API key is available
|
|
76
|
+
*/
|
|
77
|
+
hasApiKey(): boolean {
|
|
78
|
+
return !!this.apiKey;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
/**
|
|
73
82
|
* Non-streaming chat
|
|
74
83
|
*/
|
|
75
84
|
async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
|
|
76
85
|
if (!this.hasApiKey()) {
|
|
77
|
-
|
|
86
|
+
throw new Error(this.getNoKeyMessage());
|
|
78
87
|
}
|
|
79
88
|
return this.provider.chat(messages, options);
|
|
80
89
|
}
|
|
@@ -87,12 +96,7 @@ export class LLMService {
|
|
|
87
96
|
options?: ChatOptions & { maxIterations?: number }
|
|
88
97
|
): AsyncGenerator<string, ChatResponse> {
|
|
89
98
|
if (!this.hasApiKey()) {
|
|
90
|
-
|
|
91
|
-
for (const char of mock.content) {
|
|
92
|
-
await new Promise(r => setTimeout(r, 10));
|
|
93
|
-
yield char;
|
|
94
|
-
}
|
|
95
|
-
return mock;
|
|
99
|
+
throw new Error(this.getNoKeyMessage());
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
const maxIterations = options?.maxIterations ?? 10;
|
|
@@ -102,103 +106,89 @@ export class LLMService {
|
|
|
102
106
|
while (iterations < maxIterations) {
|
|
103
107
|
iterations++;
|
|
104
108
|
|
|
105
|
-
// Get tools in provider format
|
|
106
|
-
const toolsArray = [...this.tools.values()];
|
|
107
|
-
const providerTools = toolsArray.length > 0 ? this.provider.buildTools(toolsArray) : [];
|
|
108
|
-
|
|
109
109
|
// Stream the response
|
|
110
110
|
let fullContent = '';
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
// Add tools to the request if available
|
|
114
|
-
if (providerTools.length > 0) {
|
|
115
|
-
// We need to pass tools differently depending on provider
|
|
116
|
-
// For now, inject into messages context
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for await (const chunk of this.provider.chatStream(currentMessages, streamOptions)) {
|
|
111
|
+
for await (const chunk of this.provider.chatStream(currentMessages, options)) {
|
|
120
112
|
fullContent += chunk;
|
|
121
113
|
yield chunk;
|
|
122
114
|
}
|
|
123
115
|
|
|
124
|
-
// Check for tool calls
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
role: 'assistant',
|
|
132
|
-
content: fullContent,
|
|
133
|
-
tool_calls: toolCalls,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Execute each tool
|
|
137
|
-
for (const toolCall of toolCalls) {
|
|
138
|
-
const tool = this.tools.get(toolCall.function.name);
|
|
139
|
-
if (!tool) {
|
|
140
|
-
currentMessages.push({
|
|
141
|
-
role: 'tool',
|
|
142
|
-
content: `Tool "${toolCall.function.name}" not found`,
|
|
143
|
-
tool_call_id: toolCall.id,
|
|
144
|
-
name: toolCall.function.name,
|
|
145
|
-
});
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
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;
|
|
148
123
|
|
|
149
|
-
|
|
150
|
-
const params = JSON.parse(toolCall.function.arguments || '{}');
|
|
151
|
-
const result = await tool.execute(params);
|
|
152
|
-
currentMessages.push({
|
|
153
|
-
role: 'tool',
|
|
154
|
-
content: result.success ? (result.output || JSON.stringify(result.data)) : (result.error || 'Tool failed'),
|
|
155
|
-
tool_call_id: toolCall.id,
|
|
156
|
-
name: toolCall.function.name,
|
|
157
|
-
});
|
|
158
|
-
yield `\n${chalk.gray(`[tool: ${toolCall.function.name}]`)}`;
|
|
159
|
-
} catch (error: any) {
|
|
124
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
160
125
|
currentMessages.push({
|
|
161
|
-
role: '
|
|
162
|
-
content:
|
|
163
|
-
|
|
164
|
-
name: toolCall.function.name,
|
|
126
|
+
role: 'assistant',
|
|
127
|
+
content: response.content || fullContent,
|
|
128
|
+
tool_calls: toolCalls,
|
|
165
129
|
});
|
|
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
|
+
}
|
|
162
|
+
continue;
|
|
166
163
|
}
|
|
164
|
+
} catch {
|
|
165
|
+
// If the non-streaming check fails, just return the streamed content
|
|
167
166
|
}
|
|
168
|
-
|
|
169
|
-
continue; // Loop back for next iteration
|
|
170
167
|
}
|
|
171
168
|
|
|
172
|
-
|
|
173
|
-
return response;
|
|
169
|
+
return { content: fullContent, role: 'assistant' };
|
|
174
170
|
}
|
|
175
171
|
|
|
176
172
|
return { content: '[Agent loop exceeded maximum iterations]', role: 'assistant' };
|
|
177
173
|
}
|
|
178
174
|
|
|
179
175
|
/**
|
|
180
|
-
*
|
|
176
|
+
* Error message when no API key is configured
|
|
181
177
|
*/
|
|
182
|
-
private
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
content = 'I am Thatgfsj Code, an AI coding assistant.';
|
|
197
|
-
} else {
|
|
198
|
-
content = `I understand you said: "${last.slice(0, 50)}..."\n\nThis is a demo response. Configure an API key for full AI capabilities.\nRun: gfcode init`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return { content, role: 'assistant' };
|
|
178
|
+
private getNoKeyMessage(): string {
|
|
179
|
+
return [
|
|
180
|
+
'❌ 未配置 API Key,无法调用 AI。',
|
|
181
|
+
'',
|
|
182
|
+
'请先运行以下命令配置:',
|
|
183
|
+
' gfcode init',
|
|
184
|
+
'',
|
|
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',
|
|
191
|
+
].join('\n');
|
|
202
192
|
}
|
|
203
193
|
}
|
|
204
194
|
|
package/src/llm/openai.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI-compatible Provider
|
|
3
|
-
* Works with: OpenAI, SiliconFlow,
|
|
3
|
+
* Works with: OpenAI, SiliconFlow, DeepSeek, Kimi, Zhipu, MiniMax, Baichuan, Stepfun, Doubao, Ollama, ERNIE
|
|
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]"
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
|
|
@@ -9,7 +14,7 @@ import type { LLMProvider, ProviderConfig } from './provider.js';
|
|
|
9
14
|
|
|
10
15
|
export class OpenAIProvider implements LLMProvider {
|
|
11
16
|
readonly name = 'openai';
|
|
12
|
-
|
|
17
|
+
protected config: ProviderConfig;
|
|
13
18
|
|
|
14
19
|
constructor(config: ProviderConfig) {
|
|
15
20
|
this.config = config;
|
|
@@ -34,47 +39,29 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
34
39
|
|
|
35
40
|
async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
|
|
36
41
|
const body = this.buildRequest(messages, false, options);
|
|
37
|
-
|
|
38
|
-
const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: {
|
|
41
|
-
'Content-Type': 'application/json',
|
|
42
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
43
|
-
},
|
|
44
|
-
body: JSON.stringify(body),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (!response.ok) {
|
|
48
|
-
const text = await response.text().catch(() => '');
|
|
49
|
-
throw new Error(`OpenAI API error ${response.status}: ${text}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
42
|
+
const response = await this.doRequest(body);
|
|
52
43
|
const data = await response.json();
|
|
53
44
|
const choice = data.choices?.[0];
|
|
54
45
|
|
|
55
46
|
return {
|
|
56
47
|
content: choice?.message?.content || '',
|
|
57
48
|
role: 'assistant',
|
|
58
|
-
usage: data.usage
|
|
49
|
+
usage: data.usage ? {
|
|
50
|
+
prompt_tokens: data.usage.prompt_tokens || 0,
|
|
51
|
+
completion_tokens: data.usage.completion_tokens || 0,
|
|
52
|
+
total_tokens: data.usage.total_tokens || 0,
|
|
53
|
+
} : undefined,
|
|
59
54
|
tool_calls: choice?.message?.tool_calls,
|
|
60
55
|
};
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse> {
|
|
64
59
|
const body = this.buildRequest(messages, true, options);
|
|
65
|
-
|
|
66
|
-
const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: {
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify(body),
|
|
73
|
-
});
|
|
60
|
+
const response = await this.doRequest(body);
|
|
74
61
|
|
|
75
62
|
if (!response.ok || !response.body) {
|
|
76
63
|
const text = await response.text().catch(() => '');
|
|
77
|
-
throw new Error(`
|
|
64
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
78
65
|
}
|
|
79
66
|
|
|
80
67
|
const reader = response.body.getReader();
|
|
@@ -93,14 +80,20 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
93
80
|
|
|
94
81
|
for (const line of lines) {
|
|
95
82
|
const trimmed = line.trim();
|
|
96
|
-
if (!trimmed
|
|
83
|
+
if (!trimmed) continue;
|
|
84
|
+
if (trimmed === 'data: [DONE]') continue;
|
|
85
|
+
if (!trimmed.startsWith('data: ')) continue;
|
|
97
86
|
|
|
98
87
|
try {
|
|
99
88
|
const data = JSON.parse(trimmed.slice(6));
|
|
100
|
-
const
|
|
101
|
-
if (content) {
|
|
102
|
-
fullContent += content;
|
|
103
|
-
yield content;
|
|
89
|
+
const delta = data.choices?.[0]?.delta;
|
|
90
|
+
if (delta?.content) {
|
|
91
|
+
fullContent += delta.content;
|
|
92
|
+
yield delta.content;
|
|
93
|
+
}
|
|
94
|
+
// Handle tool calls in streaming
|
|
95
|
+
if (delta?.tool_calls) {
|
|
96
|
+
// Store tool calls for final response
|
|
104
97
|
}
|
|
105
98
|
} catch {
|
|
106
99
|
// Skip invalid JSON lines
|
|
@@ -114,7 +107,10 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
114
107
|
return { content: fullContent, role: 'assistant' };
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Build the request body for OpenAI-compatible API
|
|
112
|
+
*/
|
|
113
|
+
protected buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions) {
|
|
118
114
|
return {
|
|
119
115
|
model: this.config.model,
|
|
120
116
|
messages: messages.map(m => ({
|
|
@@ -127,6 +123,22 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
127
123
|
temperature: options?.temperature ?? this.config.temperature,
|
|
128
124
|
max_tokens: options?.maxTokens ?? this.config.maxTokens,
|
|
129
125
|
stream,
|
|
126
|
+
...(stream && { stream_options: { include_usage: true } }),
|
|
130
127
|
};
|
|
131
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Execute the HTTP request
|
|
132
|
+
*/
|
|
133
|
+
protected async doRequest(body: any): Promise<Response> {
|
|
134
|
+
const url = `${this.config.baseUrl}/chat/completions`;
|
|
135
|
+
return fetch(url, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify(body),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
132
144
|
}
|
package/src/tui/output.ts
CHANGED
|
@@ -60,7 +60,7 @@ export class REPLOutput {
|
|
|
60
60
|
printBanner(): void {
|
|
61
61
|
console.log(chalk.cyan(`
|
|
62
62
|
╔═══════════════════════════════════════╗
|
|
63
|
-
║ 🤖 Thatgfsj Code v0.
|
|
63
|
+
║ 🤖 Thatgfsj Code v0.4.0 ║
|
|
64
64
|
║ AI Coding Assistant ║
|
|
65
65
|
╚═══════════════════════════════════════╝
|
|
66
66
|
`));
|