thatgfsj-code 0.3.0 → 0.4.1
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 +56 -8
- package/dist/cmd/index.js.map +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 +24 -6
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +136 -46
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/gemini.d.ts +4 -4
- package/dist/llm/gemini.d.ts.map +1 -1
- package/dist/llm/gemini.js +67 -30
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/index.d.ts +10 -23
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +73 -78
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +19 -6
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +86 -35
- 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 +27 -12
- package/dist/tui/output.d.ts.map +1 -1
- package/dist/tui/output.js +176 -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 +76 -42
- package/dist/tui/repl.js.map +1 -1
- package/dist/tui/welcome.d.ts +2 -8
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +101 -51
- package/dist/tui/welcome.js.map +1 -1
- package/package.json +1 -1
- package/src/cmd/index.ts +49 -8
- 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 +148 -51
- package/src/llm/gemini.ts +81 -39
- package/src/llm/index.ts +75 -83
- package/src/llm/openai.ts +96 -41
- package/src/llm/provider.ts +12 -16
- package/src/tui/input.ts +2 -5
- package/src/tui/output.ts +193 -40
- package/src/tui/repl.ts +82 -43
- package/src/tui/welcome.ts +110 -53
package/src/llm/gemini.ts
CHANGED
|
@@ -2,36 +2,38 @@
|
|
|
2
2
|
* Google Gemini Provider
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
|
|
5
|
+
import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
|
|
6
6
|
import type { Tool } from '../tools/types.js';
|
|
7
|
-
import type { LLMProvider, ProviderConfig } from './provider.js';
|
|
7
|
+
import type { LLMProvider, StreamChunk, ProviderConfig } from './provider.js';
|
|
8
8
|
|
|
9
9
|
export class GeminiProvider implements LLMProvider {
|
|
10
10
|
readonly name = 'gemini';
|
|
11
|
-
|
|
11
|
+
protected config: ProviderConfig;
|
|
12
12
|
|
|
13
13
|
constructor(config: ProviderConfig) {
|
|
14
14
|
this.config = config;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
buildTools(tools: Tool[]): any[] {
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
return [{
|
|
19
|
+
functionDeclarations: tools.map(tool => ({
|
|
20
|
+
name: tool.name,
|
|
21
|
+
description: tool.description,
|
|
22
|
+
parameters: tool.inputSchema || {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: Object.fromEntries(
|
|
25
|
+
tool.parameters.map(p => [p.name, { type: p.type, description: p.description }])
|
|
26
|
+
),
|
|
27
|
+
required: tool.parameters.filter(p => p.required).map(p => p.name),
|
|
28
|
+
},
|
|
29
|
+
})),
|
|
30
|
+
}];
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
|
|
32
|
-
const body = this.buildRequest(messages, options);
|
|
33
|
-
|
|
33
|
+
async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
|
|
34
|
+
const body = this.buildRequest(messages, options, tools);
|
|
34
35
|
const url = `${this.config.baseUrl}/models/${this.config.model}:generateContent?key=${this.config.apiKey}`;
|
|
36
|
+
|
|
35
37
|
const response = await fetch(url, {
|
|
36
38
|
method: 'POST',
|
|
37
39
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -45,7 +47,17 @@ export class GeminiProvider implements LLMProvider {
|
|
|
45
47
|
|
|
46
48
|
const data = await response.json();
|
|
47
49
|
const candidate = data.candidates?.[0];
|
|
48
|
-
const
|
|
50
|
+
const parts = candidate?.content?.parts || [];
|
|
51
|
+
const text = parts.map((p: any) => p.text).filter(Boolean).join('');
|
|
52
|
+
|
|
53
|
+
const functionCalls = parts.filter((p: any) => p.functionCall).map((p: any) => ({
|
|
54
|
+
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
55
|
+
type: 'function' as const,
|
|
56
|
+
function: {
|
|
57
|
+
name: p.functionCall.name,
|
|
58
|
+
arguments: JSON.stringify(p.functionCall.args || {}),
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
49
61
|
|
|
50
62
|
return {
|
|
51
63
|
content: text,
|
|
@@ -55,13 +67,14 @@ export class GeminiProvider implements LLMProvider {
|
|
|
55
67
|
completion_tokens: data.usageMetadata.candidatesTokenCount || 0,
|
|
56
68
|
total_tokens: data.usageMetadata.totalTokenCount || 0,
|
|
57
69
|
} : undefined,
|
|
70
|
+
tool_calls: functionCalls.length > 0 ? functionCalls : undefined,
|
|
58
71
|
};
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<
|
|
62
|
-
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);
|
|
76
|
+
const url = `${this.config.baseUrl}/models/${this.config.model}:streamGenerateContent?alt=sse&key=${this.config.apiKey}`;
|
|
63
77
|
|
|
64
|
-
const url = `${this.config.baseUrl}/models/${this.config.model}:streamGenerateContent?key=${this.config.apiKey}`;
|
|
65
78
|
const response = await fetch(url, {
|
|
66
79
|
method: 'POST',
|
|
67
80
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -77,6 +90,7 @@ export class GeminiProvider implements LLMProvider {
|
|
|
77
90
|
const decoder = new TextDecoder();
|
|
78
91
|
let fullContent = '';
|
|
79
92
|
let buffer = '';
|
|
93
|
+
const functionCalls: ToolCall[] = [];
|
|
80
94
|
|
|
81
95
|
try {
|
|
82
96
|
while (true) {
|
|
@@ -84,44 +98,72 @@ export class GeminiProvider implements LLMProvider {
|
|
|
84
98
|
if (done) break;
|
|
85
99
|
|
|
86
100
|
buffer += decoder.decode(value, { stream: true });
|
|
101
|
+
const lines = buffer.split('\n');
|
|
102
|
+
buffer = lines.pop() || '';
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
|
87
107
|
|
|
88
|
-
// Gemini streams JSON array chunks
|
|
89
|
-
const jsonMatch = buffer.match(/\{"candidates":\[.*?\]\}/);
|
|
90
|
-
if (jsonMatch) {
|
|
91
108
|
try {
|
|
92
|
-
const data = JSON.parse(
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
110
|
+
const parts = data.candidates?.[0]?.content?.parts || [];
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
if (part.text) {
|
|
113
|
+
fullContent += 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
|
+
}
|
|
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 {
|
|
105
133
|
reader.releaseLock();
|
|
106
134
|
}
|
|
107
135
|
|
|
108
|
-
|
|
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 };
|
|
109
141
|
}
|
|
110
142
|
|
|
111
|
-
private buildRequest(messages: ChatMessage[], options?: ChatOptions) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
.map(m => ({
|
|
115
|
-
role: m.role === 'assistant' ? 'model' : 'user',
|
|
116
|
-
parts: [{ text: m.content }],
|
|
117
|
-
}));
|
|
143
|
+
private buildRequest(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]) {
|
|
144
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
145
|
+
const nonSystemMsgs = messages.filter(m => m.role !== 'system');
|
|
118
146
|
|
|
119
|
-
|
|
147
|
+
const contents = nonSystemMsgs.map(m => ({
|
|
148
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
149
|
+
parts: [{ text: m.content }],
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
const body: any = {
|
|
120
153
|
contents,
|
|
154
|
+
...(systemMsg && {
|
|
155
|
+
systemInstruction: { parts: [{ text: systemMsg.content }] },
|
|
156
|
+
}),
|
|
121
157
|
generationConfig: {
|
|
122
158
|
temperature: options?.temperature ?? this.config.temperature,
|
|
123
159
|
maxOutputTokens: options?.maxTokens ?? this.config.maxTokens,
|
|
124
160
|
},
|
|
125
161
|
};
|
|
162
|
+
|
|
163
|
+
if (tools && tools.length > 0) {
|
|
164
|
+
body.tools = this.buildTools(tools);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return body;
|
|
126
168
|
}
|
|
127
169
|
}
|
package/src/llm/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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';
|
|
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';
|
|
@@ -16,14 +16,13 @@ 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
|
-
/**
|
|
25
|
-
* Create LLMService from AIConfig
|
|
26
|
-
*/
|
|
27
26
|
static fromConfig(config: AIConfig): LLMService {
|
|
28
27
|
const providerName = config.provider || 'siliconflow';
|
|
29
28
|
const providerConfig = PROVIDERS[providerName];
|
|
@@ -50,50 +49,35 @@ export class LLMService {
|
|
|
50
49
|
provider = new OpenAIProvider(providerCfg);
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
return new LLMService(provider);
|
|
52
|
+
return new LLMService(provider, providerCfg.apiKey);
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
/**
|
|
57
|
-
* Register tools for function calling
|
|
58
|
-
*/
|
|
59
55
|
registerTools(tools: Tool[]): void {
|
|
60
56
|
for (const tool of tools) {
|
|
61
57
|
this.tools.set(tool.name, tool);
|
|
62
58
|
}
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*/
|
|
68
|
-
getProviderName(): string {
|
|
69
|
-
return this.provider.name;
|
|
70
|
-
}
|
|
61
|
+
getProviderName(): string { return this.provider.name; }
|
|
62
|
+
hasApiKey(): boolean { return !!this.apiKey; }
|
|
71
63
|
|
|
72
|
-
/**
|
|
73
|
-
* Non-streaming chat
|
|
74
|
-
*/
|
|
75
64
|
async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
|
|
76
|
-
if (!this.hasApiKey())
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
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);
|
|
80
68
|
}
|
|
81
69
|
|
|
82
70
|
/**
|
|
83
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
|
|
84
75
|
*/
|
|
85
76
|
async *chatStream(
|
|
86
77
|
messages: ChatMessage[],
|
|
87
78
|
options?: ChatOptions & { maxIterations?: number }
|
|
88
79
|
): AsyncGenerator<string, ChatResponse> {
|
|
89
|
-
if (!this.hasApiKey())
|
|
90
|
-
const mock = this.getMockResponse(messages);
|
|
91
|
-
for (const char of mock.content) {
|
|
92
|
-
await new Promise(r => setTimeout(r, 10));
|
|
93
|
-
yield char;
|
|
94
|
-
}
|
|
95
|
-
return mock;
|
|
96
|
-
}
|
|
80
|
+
if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
|
|
97
81
|
|
|
98
82
|
const maxIterations = options?.maxIterations ?? 10;
|
|
99
83
|
let currentMessages = [...messages];
|
|
@@ -101,109 +85,117 @@ export class LLMService {
|
|
|
101
85
|
|
|
102
86
|
while (iterations < maxIterations) {
|
|
103
87
|
iterations++;
|
|
104
|
-
|
|
105
|
-
// Get tools in provider format
|
|
106
88
|
const toolsArray = [...this.tools.values()];
|
|
107
|
-
const
|
|
89
|
+
const hasTools = toolsArray.length > 0;
|
|
108
90
|
|
|
109
|
-
// Stream the response
|
|
110
91
|
let fullContent = '';
|
|
111
|
-
|
|
92
|
+
let detectedToolCalls: ToolCall[] | undefined;
|
|
112
93
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
// We need to pass tools differently depending on provider
|
|
116
|
-
// For now, inject into messages context
|
|
117
|
-
}
|
|
94
|
+
// Stream the response, passing tools so the API knows about them
|
|
95
|
+
const stream = this.provider.chatStream(currentMessages, options, hasTools ? toolsArray : undefined);
|
|
118
96
|
|
|
119
|
-
for await (const chunk of
|
|
120
|
-
|
|
121
|
-
|
|
97
|
+
for await (const chunk of stream) {
|
|
98
|
+
if (chunk.type === 'text' && chunk.content) {
|
|
99
|
+
fullContent += chunk.content;
|
|
100
|
+
yield chunk.content;
|
|
101
|
+
} else if (chunk.type === 'tool_calls' && chunk.toolCalls) {
|
|
102
|
+
detectedToolCalls = chunk.toolCalls;
|
|
103
|
+
}
|
|
122
104
|
}
|
|
123
105
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
129
|
-
// Append assistant message with tool calls
|
|
106
|
+
// If we got tool calls, execute them and loop
|
|
107
|
+
if (detectedToolCalls && detectedToolCalls.length > 0) {
|
|
108
|
+
// Add assistant message with tool calls
|
|
130
109
|
currentMessages.push({
|
|
131
110
|
role: 'assistant',
|
|
132
|
-
content: fullContent,
|
|
133
|
-
tool_calls:
|
|
111
|
+
content: fullContent || '',
|
|
112
|
+
tool_calls: detectedToolCalls,
|
|
134
113
|
});
|
|
135
114
|
|
|
136
115
|
// Execute each tool
|
|
137
|
-
for (const toolCall of
|
|
116
|
+
for (const toolCall of detectedToolCalls) {
|
|
138
117
|
const tool = this.tools.get(toolCall.function.name);
|
|
118
|
+
|
|
119
|
+
// Yield structured tool call as JSON line
|
|
120
|
+
yield `\n@@TOOL@@${JSON.stringify({ action: 'call', name: toolCall.function.name, args: toolCall.function.arguments })}\n`;
|
|
121
|
+
|
|
139
122
|
if (!tool) {
|
|
123
|
+
const errMsg = `Tool "${toolCall.function.name}" not found`;
|
|
140
124
|
currentMessages.push({
|
|
141
125
|
role: 'tool',
|
|
142
|
-
content:
|
|
126
|
+
content: errMsg,
|
|
143
127
|
tool_call_id: toolCall.id,
|
|
144
128
|
name: toolCall.function.name,
|
|
145
129
|
});
|
|
130
|
+
yield `@@TOOL@@${JSON.stringify({ action: 'result', error: errMsg })}\n`;
|
|
146
131
|
continue;
|
|
147
132
|
}
|
|
148
133
|
|
|
149
134
|
try {
|
|
150
135
|
const params = JSON.parse(toolCall.function.arguments || '{}');
|
|
151
136
|
const result = await tool.execute(params);
|
|
137
|
+
const output = result.success ? (result.output || JSON.stringify(result.data)) : (result.error || 'Tool failed');
|
|
138
|
+
|
|
152
139
|
currentMessages.push({
|
|
153
140
|
role: 'tool',
|
|
154
|
-
content:
|
|
141
|
+
content: output,
|
|
155
142
|
tool_call_id: toolCall.id,
|
|
156
143
|
name: toolCall.function.name,
|
|
157
144
|
});
|
|
158
|
-
|
|
145
|
+
|
|
146
|
+
yield `@@TOOL@@${JSON.stringify({ action: 'result', output })}\n`;
|
|
159
147
|
} catch (error: any) {
|
|
148
|
+
const errMsg = `Error: ${error.message}`;
|
|
160
149
|
currentMessages.push({
|
|
161
150
|
role: 'tool',
|
|
162
|
-
content:
|
|
151
|
+
content: errMsg,
|
|
163
152
|
tool_call_id: toolCall.id,
|
|
164
153
|
name: toolCall.function.name,
|
|
165
154
|
});
|
|
155
|
+
yield `@@TOOL@@${JSON.stringify({ action: 'result', error: errMsg })}\n`;
|
|
166
156
|
}
|
|
167
157
|
}
|
|
168
158
|
|
|
169
|
-
|
|
159
|
+
yield '\n';
|
|
160
|
+
continue;
|
|
170
161
|
}
|
|
171
162
|
|
|
172
|
-
// No tool calls -
|
|
173
|
-
return
|
|
163
|
+
// No tool calls - done
|
|
164
|
+
return { content: fullContent, role: 'assistant' };
|
|
174
165
|
}
|
|
175
166
|
|
|
176
167
|
return { content: '[Agent loop exceeded maximum iterations]', role: 'assistant' };
|
|
177
168
|
}
|
|
178
169
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const last = messages[messages.length - 1]?.content || '';
|
|
191
|
-
let content: string;
|
|
192
|
-
|
|
193
|
-
if (/^(hi|hello|hey)/i.test(last)) {
|
|
194
|
-
content = 'Hi there! How can I help you today?';
|
|
195
|
-
} else if (last.toLowerCase().includes('who are you')) {
|
|
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`;
|
|
170
|
+
private truncateArgs(args: string): string {
|
|
171
|
+
try {
|
|
172
|
+
const obj = JSON.parse(args || '{}');
|
|
173
|
+
const entries = Object.entries(obj);
|
|
174
|
+
if (entries.length === 0) return '';
|
|
175
|
+
return entries.map(([k, v]) => {
|
|
176
|
+
const val = typeof v === 'string' && v.length > 50 ? v.slice(0, 50) + '...' : v;
|
|
177
|
+
return `${k}: ${JSON.stringify(val)}`;
|
|
178
|
+
}).join(', ');
|
|
179
|
+
} catch {
|
|
180
|
+
return args.length > 80 ? args.slice(0, 80) + '...' : args;
|
|
199
181
|
}
|
|
182
|
+
}
|
|
200
183
|
|
|
201
|
-
|
|
184
|
+
private getNoKeyMessage(): string {
|
|
185
|
+
return [
|
|
186
|
+
'❌ 未配置 API Key,无法调用 AI。',
|
|
187
|
+
'',
|
|
188
|
+
'请先运行: gfcode init',
|
|
189
|
+
'',
|
|
190
|
+
'或设置环境变量:',
|
|
191
|
+
' export SILICONFLOW_API_KEY="sk-..."',
|
|
192
|
+
' export OPENAI_API_KEY="sk-..."',
|
|
193
|
+
' export DEEPSEEK_API_KEY="sk-..."',
|
|
194
|
+
].join('\n');
|
|
202
195
|
}
|
|
203
196
|
}
|
|
204
197
|
|
|
205
|
-
|
|
206
|
-
export type { LLMProvider, ProviderConfig } from './provider.js';
|
|
198
|
+
export type { LLMProvider, ProviderConfig, StreamChunk } from './provider.js';
|
|
207
199
|
export { OpenAIProvider } from './openai.js';
|
|
208
200
|
export { AnthropicProvider } from './anthropic.js';
|
|
209
201
|
export { GeminiProvider } from './gemini.js';
|
package/src/llm/openai.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
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 (中转站)
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
|
|
7
|
+
import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
|
|
7
8
|
import type { Tool } from '../tools/types.js';
|
|
8
9
|
import type { LLMProvider, ProviderConfig } from './provider.js';
|
|
9
10
|
|
|
11
|
+
export interface StreamChunk {
|
|
12
|
+
type: 'text' | 'tool_calls';
|
|
13
|
+
content?: string;
|
|
14
|
+
toolCalls?: ToolCall[];
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export class OpenAIProvider implements LLMProvider {
|
|
11
18
|
readonly name = 'openai';
|
|
12
|
-
|
|
19
|
+
protected config: ProviderConfig;
|
|
13
20
|
|
|
14
21
|
constructor(config: ProviderConfig) {
|
|
15
22
|
this.config = config;
|
|
@@ -32,55 +39,39 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
32
39
|
}));
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
|
|
36
|
-
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
|
+
async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
|
|
43
|
+
const body = this.buildRequest(messages, false, options, tools);
|
|
44
|
+
const response = await this.doRequest(body);
|
|
52
45
|
const data = await response.json();
|
|
53
46
|
const choice = data.choices?.[0];
|
|
54
47
|
|
|
55
48
|
return {
|
|
56
49
|
content: choice?.message?.content || '',
|
|
57
50
|
role: 'assistant',
|
|
58
|
-
usage: data.usage
|
|
51
|
+
usage: data.usage ? {
|
|
52
|
+
prompt_tokens: data.usage.prompt_tokens || 0,
|
|
53
|
+
completion_tokens: data.usage.completion_tokens || 0,
|
|
54
|
+
total_tokens: data.usage.total_tokens || 0,
|
|
55
|
+
} : undefined,
|
|
59
56
|
tool_calls: choice?.message?.tool_calls,
|
|
60
57
|
};
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<
|
|
64
|
-
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
|
+
async *chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse> {
|
|
61
|
+
const body = this.buildRequest(messages, true, options, tools);
|
|
62
|
+
const response = await this.doRequest(body);
|
|
74
63
|
|
|
75
64
|
if (!response.ok || !response.body) {
|
|
76
65
|
const text = await response.text().catch(() => '');
|
|
77
|
-
throw new Error(`
|
|
66
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
const reader = response.body.getReader();
|
|
81
70
|
const decoder = new TextDecoder();
|
|
82
71
|
let fullContent = '';
|
|
83
72
|
let buffer = '';
|
|
73
|
+
// Accumulate streaming tool call chunks
|
|
74
|
+
const toolCallBuffers: Map<number, { id: string; name: string; arguments: string }> = new Map();
|
|
84
75
|
|
|
85
76
|
try {
|
|
86
77
|
while (true) {
|
|
@@ -93,14 +84,32 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
93
84
|
|
|
94
85
|
for (const line of lines) {
|
|
95
86
|
const trimmed = line.trim();
|
|
96
|
-
if (!trimmed
|
|
87
|
+
if (!trimmed) continue;
|
|
88
|
+
if (trimmed === 'data: [DONE]') continue;
|
|
89
|
+
if (!trimmed.startsWith('data: ')) continue;
|
|
97
90
|
|
|
98
91
|
try {
|
|
99
92
|
const data = JSON.parse(trimmed.slice(6));
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
const delta = data.choices?.[0]?.delta;
|
|
94
|
+
|
|
95
|
+
// Text content
|
|
96
|
+
if (delta?.content) {
|
|
97
|
+
fullContent += delta.content;
|
|
98
|
+
yield { type: 'text', content: delta.content };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Streaming tool calls - accumulate chunks
|
|
102
|
+
if (delta?.tool_calls) {
|
|
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
|
+
}
|
|
104
113
|
}
|
|
105
114
|
} catch {
|
|
106
115
|
// Skip invalid JSON lines
|
|
@@ -111,11 +120,34 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
111
120
|
reader.releaseLock();
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
}
|
|
116
138
|
|
|
117
|
-
private buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions) {
|
|
118
139
|
return {
|
|
140
|
+
content: fullContent,
|
|
141
|
+
role: 'assistant',
|
|
142
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build the request body for OpenAI-compatible API
|
|
148
|
+
*/
|
|
149
|
+
protected buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions, tools?: Tool[]) {
|
|
150
|
+
const body: any = {
|
|
119
151
|
model: this.config.model,
|
|
120
152
|
messages: messages.map(m => ({
|
|
121
153
|
role: m.role,
|
|
@@ -127,6 +159,29 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
127
159
|
temperature: options?.temperature ?? this.config.temperature,
|
|
128
160
|
max_tokens: options?.maxTokens ?? this.config.maxTokens,
|
|
129
161
|
stream,
|
|
162
|
+
...(stream && { stream_options: { include_usage: true } }),
|
|
130
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;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Execute the HTTP request
|
|
175
|
+
*/
|
|
176
|
+
protected async doRequest(body: any): Promise<Response> {
|
|
177
|
+
const url = `${this.config.baseUrl}/chat/completions`;
|
|
178
|
+
return fetch(url, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify(body),
|
|
185
|
+
});
|
|
131
186
|
}
|
|
132
187
|
}
|