tabby-ai-assistant 1.0.13 → 1.0.16
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/.editorconfig +18 -0
- package/README.md +40 -10
- package/dist/index.js +1 -1
- package/package.json +5 -3
- package/src/components/chat/ai-sidebar.component.scss +220 -9
- package/src/components/chat/ai-sidebar.component.ts +379 -29
- package/src/components/chat/chat-input.component.ts +36 -4
- package/src/components/chat/chat-interface.component.ts +225 -5
- package/src/components/chat/chat-message.component.ts +6 -1
- package/src/components/settings/context-settings.component.ts +91 -91
- package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
- package/src/components/terminal/command-suggestion.component.ts +148 -6
- package/src/index.ts +81 -19
- package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
- package/src/services/chat/ai-sidebar.service.ts +448 -410
- package/src/services/chat/chat-session.service.ts +36 -12
- package/src/services/context/compaction.ts +110 -134
- package/src/services/context/manager.ts +27 -7
- package/src/services/context/memory.ts +17 -33
- package/src/services/context/summary.service.ts +136 -0
- package/src/services/core/ai-assistant.service.ts +1060 -37
- package/src/services/core/ai-provider-manager.service.ts +154 -25
- package/src/services/core/checkpoint.service.ts +218 -18
- package/src/services/core/toast.service.ts +106 -106
- package/src/services/providers/anthropic-provider.service.ts +126 -30
- package/src/services/providers/base-provider.service.ts +90 -7
- package/src/services/providers/glm-provider.service.ts +151 -38
- package/src/services/providers/minimax-provider.service.ts +55 -40
- package/src/services/providers/ollama-provider.service.ts +117 -28
- package/src/services/providers/openai-compatible.service.ts +164 -34
- package/src/services/providers/openai-provider.service.ts +169 -34
- package/src/services/providers/vllm-provider.service.ts +116 -28
- package/src/services/terminal/terminal-context.service.ts +265 -5
- package/src/services/terminal/terminal-manager.service.ts +845 -748
- package/src/services/terminal/terminal-tools.service.ts +612 -441
- package/src/types/ai.types.ts +156 -3
- package/src/utils/cost.utils.ts +249 -0
- package/src/utils/validation.utils.ts +306 -2
- package/dist/index.js.LICENSE.txt +0 -18
- package/src/services/terminal/command-analyzer.service.ts +0 -43
- package/src/services/terminal/context-menu.service.ts +0 -45
- package/src/services/terminal/hotkey.service.ts +0 -53
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
|
-
import { Observable,
|
|
2
|
+
import { Observable, Observer } from 'rxjs';
|
|
3
3
|
import axios, { AxiosInstance } from 'axios';
|
|
4
4
|
import { BaseAiProvider } from './base-provider.service';
|
|
5
|
-
import { ProviderCapability,
|
|
6
|
-
import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole } from '../../types/ai.types';
|
|
5
|
+
import { ProviderCapability, ValidationResult } from '../../types/provider.types';
|
|
6
|
+
import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole, StreamEvent } from '../../types/ai.types';
|
|
7
7
|
import { LoggerService } from '../core/logger.service';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -108,11 +108,157 @@ export class OpenAiCompatibleProviderService extends BaseAiProvider {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* 流式聊天功能 -
|
|
111
|
+
* 流式聊天功能 - 支持工具调用事件
|
|
112
112
|
*/
|
|
113
|
-
chatStream(request: ChatRequest): Observable<
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
chatStream(request: ChatRequest): Observable<StreamEvent> {
|
|
114
|
+
return new Observable<StreamEvent>((subscriber: Observer<StreamEvent>) => {
|
|
115
|
+
if (!this.client) {
|
|
116
|
+
const error = new Error('OpenAI compatible client not initialized');
|
|
117
|
+
subscriber.next({ type: 'error', error: error.message });
|
|
118
|
+
subscriber.error(error);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const abortController = new AbortController();
|
|
123
|
+
|
|
124
|
+
const runStream = async () => {
|
|
125
|
+
try {
|
|
126
|
+
const response = await this.client!.post('/chat/completions', {
|
|
127
|
+
model: this.config?.model || 'gpt-3.5-turbo',
|
|
128
|
+
messages: this.transformMessages(request.messages),
|
|
129
|
+
max_tokens: request.maxTokens || 1000,
|
|
130
|
+
temperature: request.temperature || 0.7,
|
|
131
|
+
stream: true
|
|
132
|
+
}, {
|
|
133
|
+
responseType: 'stream'
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const stream = response.data;
|
|
137
|
+
let currentToolCallId = '';
|
|
138
|
+
let currentToolCallName = '';
|
|
139
|
+
let currentToolInput = '';
|
|
140
|
+
let currentToolIndex = -1;
|
|
141
|
+
let fullContent = '';
|
|
142
|
+
|
|
143
|
+
for await (const chunk of stream) {
|
|
144
|
+
if (abortController.signal.aborted) break;
|
|
145
|
+
|
|
146
|
+
const lines = chunk.toString().split('\n').filter(Boolean);
|
|
147
|
+
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
if (line.startsWith('data: ')) {
|
|
150
|
+
const data = line.slice(6);
|
|
151
|
+
if (data === '[DONE]') continue;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(data);
|
|
155
|
+
const choice = parsed.choices?.[0];
|
|
156
|
+
|
|
157
|
+
this.logger.debug('Stream event', { type: 'delta', hasToolCalls: !!choice?.delta?.tool_calls });
|
|
158
|
+
|
|
159
|
+
// 处理工具调用块
|
|
160
|
+
if (choice?.delta?.tool_calls?.length > 0) {
|
|
161
|
+
for (const toolCall of choice.delta.tool_calls) {
|
|
162
|
+
const index = toolCall.index || 0;
|
|
163
|
+
|
|
164
|
+
if (currentToolIndex !== index) {
|
|
165
|
+
if (currentToolIndex >= 0) {
|
|
166
|
+
let parsedInput = {};
|
|
167
|
+
try {
|
|
168
|
+
parsedInput = JSON.parse(currentToolInput || '{}');
|
|
169
|
+
} catch (e) {
|
|
170
|
+
// 使用原始输入
|
|
171
|
+
}
|
|
172
|
+
subscriber.next({
|
|
173
|
+
type: 'tool_use_end',
|
|
174
|
+
toolCall: {
|
|
175
|
+
id: currentToolCallId,
|
|
176
|
+
name: currentToolCallName,
|
|
177
|
+
input: parsedInput
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolCallName });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
currentToolIndex = index;
|
|
184
|
+
currentToolCallId = toolCall.id || `tool_${Date.now()}_${index}`;
|
|
185
|
+
currentToolCallName = toolCall.function?.name || '';
|
|
186
|
+
currentToolInput = toolCall.function?.arguments || '';
|
|
187
|
+
|
|
188
|
+
subscriber.next({
|
|
189
|
+
type: 'tool_use_start',
|
|
190
|
+
toolCall: {
|
|
191
|
+
id: currentToolCallId,
|
|
192
|
+
name: currentToolCallName,
|
|
193
|
+
input: {}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
this.logger.debug('Stream event', { type: 'tool_use_start', name: currentToolCallName });
|
|
197
|
+
} else {
|
|
198
|
+
if (toolCall.function?.arguments) {
|
|
199
|
+
currentToolInput += toolCall.function.arguments;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// 处理文本增量
|
|
205
|
+
else if (choice?.delta?.content) {
|
|
206
|
+
const textDelta = choice.delta.content;
|
|
207
|
+
fullContent += textDelta;
|
|
208
|
+
subscriber.next({
|
|
209
|
+
type: 'text_delta',
|
|
210
|
+
textDelta
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// 忽略解析错误
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (currentToolIndex >= 0) {
|
|
221
|
+
let parsedInput = {};
|
|
222
|
+
try {
|
|
223
|
+
parsedInput = JSON.parse(currentToolInput || '{}');
|
|
224
|
+
} catch (e) {
|
|
225
|
+
// 使用原始输入
|
|
226
|
+
}
|
|
227
|
+
subscriber.next({
|
|
228
|
+
type: 'tool_use_end',
|
|
229
|
+
toolCall: {
|
|
230
|
+
id: currentToolCallId,
|
|
231
|
+
name: currentToolCallName,
|
|
232
|
+
input: parsedInput
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolCallName });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
subscriber.next({
|
|
239
|
+
type: 'message_end',
|
|
240
|
+
message: {
|
|
241
|
+
id: this.generateId(),
|
|
242
|
+
role: MessageRole.ASSISTANT,
|
|
243
|
+
content: fullContent,
|
|
244
|
+
timestamp: new Date()
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
this.logger.debug('Stream event', { type: 'message_end', contentLength: fullContent.length });
|
|
248
|
+
subscriber.complete();
|
|
249
|
+
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const errorMessage = `OpenAI compatible stream failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
252
|
+
this.logger.error('Stream error', error);
|
|
253
|
+
subscriber.next({ type: 'error', error: errorMessage });
|
|
254
|
+
subscriber.error(new Error(errorMessage));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
runStream();
|
|
259
|
+
|
|
260
|
+
return () => abortController.abort();
|
|
261
|
+
});
|
|
116
262
|
}
|
|
117
263
|
|
|
118
264
|
async generateCommand(request: CommandRequest): Promise<CommandResponse> {
|
|
@@ -175,35 +321,19 @@ export class OpenAiCompatibleProviderService extends BaseAiProvider {
|
|
|
175
321
|
return this.parseAnalysisResponse(response.message.content);
|
|
176
322
|
}
|
|
177
323
|
|
|
178
|
-
async
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const response = await this.client.post('/chat/completions', {
|
|
185
|
-
model: this.config?.model || 'gpt-3.5-turbo',
|
|
186
|
-
max_tokens: 1,
|
|
187
|
-
messages: [
|
|
188
|
-
{
|
|
189
|
-
role: 'user',
|
|
190
|
-
content: 'Hi'
|
|
191
|
-
}
|
|
192
|
-
]
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
if (response.status === 200) {
|
|
196
|
-
this.lastHealthCheck = { status: HealthStatus.HEALTHY, timestamp: new Date() };
|
|
197
|
-
return HealthStatus.HEALTHY;
|
|
198
|
-
}
|
|
324
|
+
protected async sendTestRequest(request: ChatRequest): Promise<ChatResponse> {
|
|
325
|
+
if (!this.client) {
|
|
326
|
+
throw new Error('OpenAI compatible client not initialized');
|
|
327
|
+
}
|
|
199
328
|
|
|
200
|
-
|
|
329
|
+
const response = await this.client.post('/chat/completions', {
|
|
330
|
+
model: this.config?.model || 'gpt-3.5-turbo',
|
|
331
|
+
messages: this.transformMessages(request.messages),
|
|
332
|
+
max_tokens: request.maxTokens || 1,
|
|
333
|
+
temperature: request.temperature || 0
|
|
334
|
+
});
|
|
201
335
|
|
|
202
|
-
|
|
203
|
-
this.logger.error('OpenAI compatible health check failed', error);
|
|
204
|
-
this.lastHealthCheck = { status: HealthStatus.UNHEALTHY, timestamp: new Date() };
|
|
205
|
-
return HealthStatus.UNHEALTHY;
|
|
206
|
-
}
|
|
336
|
+
return this.transformChatResponse(response.data);
|
|
207
337
|
}
|
|
208
338
|
|
|
209
339
|
validateConfig(): ValidationResult {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
|
-
import { Observable,
|
|
2
|
+
import { Observable, Observer } from 'rxjs';
|
|
3
3
|
import axios, { AxiosInstance } from 'axios';
|
|
4
4
|
import { BaseAiProvider } from './base-provider.service';
|
|
5
|
-
import { ProviderCapability,
|
|
6
|
-
import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole } from '../../types/ai.types';
|
|
5
|
+
import { ProviderCapability, ValidationResult } from '../../types/provider.types';
|
|
6
|
+
import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole, StreamEvent } from '../../types/ai.types';
|
|
7
7
|
import { LoggerService } from '../core/logger.service';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -96,11 +96,162 @@ export class OpenAiProviderService extends BaseAiProvider {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* 流式聊天功能 -
|
|
99
|
+
* 流式聊天功能 - 支持工具调用事件
|
|
100
100
|
*/
|
|
101
|
-
chatStream(request: ChatRequest): Observable<
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
chatStream(request: ChatRequest): Observable<StreamEvent> {
|
|
102
|
+
return new Observable<StreamEvent>((subscriber) => {
|
|
103
|
+
if (!this.client) {
|
|
104
|
+
const error = new Error('OpenAI client not initialized');
|
|
105
|
+
subscriber.next({ type: 'error', error: error.message });
|
|
106
|
+
subscriber.error(error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const abortController = new AbortController();
|
|
111
|
+
|
|
112
|
+
const runStream = async () => {
|
|
113
|
+
try {
|
|
114
|
+
const response = await this.client!.post('/chat/completions', {
|
|
115
|
+
model: this.config?.model || 'gpt-4',
|
|
116
|
+
messages: this.transformMessages(request.messages),
|
|
117
|
+
max_tokens: request.maxTokens || 1000,
|
|
118
|
+
temperature: request.temperature || 0.7,
|
|
119
|
+
stream: true
|
|
120
|
+
}, {
|
|
121
|
+
responseType: 'stream'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const stream = response.data;
|
|
125
|
+
let currentToolCallId = '';
|
|
126
|
+
let currentToolCallName = '';
|
|
127
|
+
let currentToolInput = '';
|
|
128
|
+
let currentToolIndex = -1;
|
|
129
|
+
let fullContent = '';
|
|
130
|
+
|
|
131
|
+
for await (const chunk of stream) {
|
|
132
|
+
if (abortController.signal.aborted) break;
|
|
133
|
+
|
|
134
|
+
const lines = chunk.toString().split('\n').filter(Boolean);
|
|
135
|
+
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
if (line.startsWith('data: ')) {
|
|
138
|
+
const data = line.slice(6);
|
|
139
|
+
if (data === '[DONE]') continue;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(data);
|
|
143
|
+
const choice = parsed.choices?.[0];
|
|
144
|
+
|
|
145
|
+
this.logger.debug('Stream event', { type: 'delta', hasToolCalls: !!choice?.delta?.tool_calls });
|
|
146
|
+
|
|
147
|
+
// 处理工具调用块
|
|
148
|
+
if (choice?.delta?.tool_calls?.length > 0) {
|
|
149
|
+
for (const toolCall of choice.delta.tool_calls) {
|
|
150
|
+
const index = toolCall.index || 0;
|
|
151
|
+
|
|
152
|
+
// 新工具调用开始
|
|
153
|
+
if (currentToolIndex !== index) {
|
|
154
|
+
if (currentToolIndex >= 0) {
|
|
155
|
+
// 发送前一个工具调用的结束事件
|
|
156
|
+
let parsedInput = {};
|
|
157
|
+
try {
|
|
158
|
+
parsedInput = JSON.parse(currentToolInput || '{}');
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// 使用原始输入
|
|
161
|
+
}
|
|
162
|
+
subscriber.next({
|
|
163
|
+
type: 'tool_use_end',
|
|
164
|
+
toolCall: {
|
|
165
|
+
id: currentToolCallId,
|
|
166
|
+
name: currentToolCallName,
|
|
167
|
+
input: parsedInput
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolCallName });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
currentToolIndex = index;
|
|
174
|
+
currentToolCallId = toolCall.id || `tool_${Date.now()}_${index}`;
|
|
175
|
+
currentToolCallName = toolCall.function?.name || '';
|
|
176
|
+
currentToolInput = toolCall.function?.arguments || '';
|
|
177
|
+
|
|
178
|
+
// 发送工具调用开始事件
|
|
179
|
+
subscriber.next({
|
|
180
|
+
type: 'tool_use_start',
|
|
181
|
+
toolCall: {
|
|
182
|
+
id: currentToolCallId,
|
|
183
|
+
name: currentToolCallName,
|
|
184
|
+
input: {}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
this.logger.debug('Stream event', { type: 'tool_use_start', name: currentToolCallName });
|
|
188
|
+
} else {
|
|
189
|
+
// 继续累积参数
|
|
190
|
+
if (toolCall.function?.arguments) {
|
|
191
|
+
currentToolInput += toolCall.function.arguments;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// 处理文本增量
|
|
197
|
+
else if (choice?.delta?.content) {
|
|
198
|
+
const textDelta = choice.delta.content;
|
|
199
|
+
fullContent += textDelta;
|
|
200
|
+
subscriber.next({
|
|
201
|
+
type: 'text_delta',
|
|
202
|
+
textDelta
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// 忽略解析错误
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 发送最后一个工具调用的结束事件
|
|
213
|
+
if (currentToolIndex >= 0) {
|
|
214
|
+
let parsedInput = {};
|
|
215
|
+
try {
|
|
216
|
+
parsedInput = JSON.parse(currentToolInput || '{}');
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// 使用原始输入
|
|
219
|
+
}
|
|
220
|
+
subscriber.next({
|
|
221
|
+
type: 'tool_use_end',
|
|
222
|
+
toolCall: {
|
|
223
|
+
id: currentToolCallId,
|
|
224
|
+
name: currentToolCallName,
|
|
225
|
+
input: parsedInput
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolCallName });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
subscriber.next({
|
|
232
|
+
type: 'message_end',
|
|
233
|
+
message: {
|
|
234
|
+
id: this.generateId(),
|
|
235
|
+
role: MessageRole.ASSISTANT,
|
|
236
|
+
content: fullContent,
|
|
237
|
+
timestamp: new Date()
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
this.logger.debug('Stream event', { type: 'message_end', contentLength: fullContent.length });
|
|
241
|
+
subscriber.complete();
|
|
242
|
+
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const errorMessage = `OpenAI stream failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
245
|
+
this.logger.error('Stream error', error);
|
|
246
|
+
subscriber.next({ type: 'error', error: errorMessage });
|
|
247
|
+
subscriber.error(new Error(errorMessage));
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
runStream();
|
|
252
|
+
|
|
253
|
+
return () => abortController.abort();
|
|
254
|
+
});
|
|
104
255
|
}
|
|
105
256
|
|
|
106
257
|
async generateCommand(request: CommandRequest): Promise<CommandResponse> {
|
|
@@ -163,35 +314,19 @@ export class OpenAiProviderService extends BaseAiProvider {
|
|
|
163
314
|
return this.parseAnalysisResponse(response.message.content);
|
|
164
315
|
}
|
|
165
316
|
|
|
166
|
-
async
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const response = await this.client.post('/chat/completions', {
|
|
173
|
-
model: this.config?.model || 'gpt-4',
|
|
174
|
-
max_tokens: 1,
|
|
175
|
-
messages: [
|
|
176
|
-
{
|
|
177
|
-
role: 'user',
|
|
178
|
-
content: 'Hi'
|
|
179
|
-
}
|
|
180
|
-
]
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (response.status === 200) {
|
|
184
|
-
this.lastHealthCheck = { status: HealthStatus.HEALTHY, timestamp: new Date() };
|
|
185
|
-
return HealthStatus.HEALTHY;
|
|
186
|
-
}
|
|
317
|
+
protected async sendTestRequest(request: ChatRequest): Promise<ChatResponse> {
|
|
318
|
+
if (!this.client) {
|
|
319
|
+
throw new Error('OpenAI client not initialized');
|
|
320
|
+
}
|
|
187
321
|
|
|
188
|
-
|
|
322
|
+
const response = await this.client.post('/chat/completions', {
|
|
323
|
+
model: this.config?.model || 'gpt-4',
|
|
324
|
+
messages: this.transformMessages(request.messages),
|
|
325
|
+
max_tokens: request.maxTokens || 1,
|
|
326
|
+
temperature: request.temperature || 0
|
|
327
|
+
});
|
|
189
328
|
|
|
190
|
-
|
|
191
|
-
this.logger.error('OpenAI health check failed', error);
|
|
192
|
-
this.lastHealthCheck = { status: HealthStatus.UNHEALTHY, timestamp: new Date() };
|
|
193
|
-
return HealthStatus.UNHEALTHY;
|
|
194
|
-
}
|
|
329
|
+
return this.transformChatResponse(response.data);
|
|
195
330
|
}
|
|
196
331
|
|
|
197
332
|
validateConfig(): ValidationResult {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
2
|
import { Observable, Observer } from 'rxjs';
|
|
3
3
|
import { BaseAiProvider } from './base-provider.service';
|
|
4
|
-
import { ProviderCapability,
|
|
4
|
+
import { ProviderCapability, ValidationResult } from '../../types/provider.types';
|
|
5
5
|
import { ChatRequest, ChatResponse, StreamEvent, MessageRole, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse } from '../../types/ai.types';
|
|
6
6
|
import { LoggerService } from '../core/logger.service';
|
|
7
7
|
|
|
@@ -89,7 +89,7 @@ export class VllmProviderService extends BaseAiProvider {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* 流式聊天功能 - 支持工具调用事件
|
|
93
93
|
*/
|
|
94
94
|
chatStream(request: ChatRequest): Observable<StreamEvent> {
|
|
95
95
|
return new Observable<StreamEvent>((subscriber: Observer<StreamEvent>) => {
|
|
@@ -123,9 +123,16 @@ export class VllmProviderService extends BaseAiProvider {
|
|
|
123
123
|
throw new Error('No response body');
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// 工具调用状态跟踪
|
|
127
|
+
let currentToolCallId = '';
|
|
128
|
+
let currentToolCallName = '';
|
|
129
|
+
let currentToolInput = '';
|
|
130
|
+
let currentToolIndex = -1;
|
|
126
131
|
let fullContent = '';
|
|
127
132
|
|
|
128
133
|
while (true) {
|
|
134
|
+
if (abortController.signal.aborted) break;
|
|
135
|
+
|
|
129
136
|
const { done, value } = await reader.read();
|
|
130
137
|
if (done) break;
|
|
131
138
|
|
|
@@ -138,8 +145,62 @@ export class VllmProviderService extends BaseAiProvider {
|
|
|
138
145
|
|
|
139
146
|
try {
|
|
140
147
|
const parsed = JSON.parse(data);
|
|
141
|
-
const
|
|
142
|
-
|
|
148
|
+
const choice = parsed.choices?.[0];
|
|
149
|
+
|
|
150
|
+
this.logger.debug('Stream event', { type: 'delta', hasToolCalls: !!choice?.delta?.tool_calls });
|
|
151
|
+
|
|
152
|
+
// 处理工具调用块
|
|
153
|
+
if (choice?.delta?.tool_calls?.length > 0) {
|
|
154
|
+
for (const toolCall of choice.delta.tool_calls) {
|
|
155
|
+
const index = toolCall.index || 0;
|
|
156
|
+
|
|
157
|
+
// 新工具调用开始
|
|
158
|
+
if (currentToolIndex !== index) {
|
|
159
|
+
if (currentToolIndex >= 0) {
|
|
160
|
+
// 发送前一个工具调用的结束事件
|
|
161
|
+
let parsedInput = {};
|
|
162
|
+
try {
|
|
163
|
+
parsedInput = JSON.parse(currentToolInput || '{}');
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// 使用原始输入
|
|
166
|
+
}
|
|
167
|
+
subscriber.next({
|
|
168
|
+
type: 'tool_use_end',
|
|
169
|
+
toolCall: {
|
|
170
|
+
id: currentToolCallId,
|
|
171
|
+
name: currentToolCallName,
|
|
172
|
+
input: parsedInput
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolCallName });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
currentToolIndex = index;
|
|
179
|
+
currentToolCallId = toolCall.id || `tool_${Date.now()}_${index}`;
|
|
180
|
+
currentToolCallName = toolCall.function?.name || '';
|
|
181
|
+
currentToolInput = toolCall.function?.arguments || '';
|
|
182
|
+
|
|
183
|
+
// 发送工具调用开始事件
|
|
184
|
+
subscriber.next({
|
|
185
|
+
type: 'tool_use_start',
|
|
186
|
+
toolCall: {
|
|
187
|
+
id: currentToolCallId,
|
|
188
|
+
name: currentToolCallName,
|
|
189
|
+
input: {}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
this.logger.debug('Stream event', { type: 'tool_use_start', name: currentToolCallName });
|
|
193
|
+
} else {
|
|
194
|
+
// 继续累积参数
|
|
195
|
+
if (toolCall.function?.arguments) {
|
|
196
|
+
currentToolInput += toolCall.function.arguments;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 处理文本增量
|
|
202
|
+
else if (choice?.delta?.content) {
|
|
203
|
+
const delta = choice.delta.content;
|
|
143
204
|
fullContent += delta;
|
|
144
205
|
subscriber.next({
|
|
145
206
|
type: 'text_delta',
|
|
@@ -152,6 +213,25 @@ export class VllmProviderService extends BaseAiProvider {
|
|
|
152
213
|
}
|
|
153
214
|
}
|
|
154
215
|
|
|
216
|
+
// 发送最后一个工具调用的结束事件
|
|
217
|
+
if (currentToolIndex >= 0) {
|
|
218
|
+
let parsedInput = {};
|
|
219
|
+
try {
|
|
220
|
+
parsedInput = JSON.parse(currentToolInput || '{}');
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// 使用原始输入
|
|
223
|
+
}
|
|
224
|
+
subscriber.next({
|
|
225
|
+
type: 'tool_use_end',
|
|
226
|
+
toolCall: {
|
|
227
|
+
id: currentToolCallId,
|
|
228
|
+
name: currentToolCallName,
|
|
229
|
+
input: parsedInput
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolCallName });
|
|
233
|
+
}
|
|
234
|
+
|
|
155
235
|
subscriber.next({
|
|
156
236
|
type: 'message_end',
|
|
157
237
|
message: {
|
|
@@ -161,11 +241,14 @@ export class VllmProviderService extends BaseAiProvider {
|
|
|
161
241
|
timestamp: new Date()
|
|
162
242
|
}
|
|
163
243
|
});
|
|
244
|
+
this.logger.debug('Stream event', { type: 'message_end', contentLength: fullContent.length });
|
|
164
245
|
subscriber.complete();
|
|
165
246
|
} catch (error) {
|
|
166
247
|
if ((error as any).name !== 'AbortError') {
|
|
248
|
+
const errorMessage = `vLLM stream failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
167
249
|
this.logError(error, { request });
|
|
168
|
-
subscriber.
|
|
250
|
+
subscriber.next({ type: 'error', error: errorMessage });
|
|
251
|
+
subscriber.error(new Error(errorMessage));
|
|
169
252
|
}
|
|
170
253
|
}
|
|
171
254
|
};
|
|
@@ -177,31 +260,36 @@ export class VllmProviderService extends BaseAiProvider {
|
|
|
177
260
|
});
|
|
178
261
|
}
|
|
179
262
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
signal: controller.signal
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
clearTimeout(timeoutId);
|
|
263
|
+
protected async sendTestRequest(request: ChatRequest): Promise<ChatResponse> {
|
|
264
|
+
const response = await fetch(`${this.getBaseURL()}/chat/completions`, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: this.getAuthHeaders(),
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
model: this.config?.model || 'meta-llama/Llama-3.1-8B',
|
|
269
|
+
messages: this.transformMessages(request.messages),
|
|
270
|
+
max_tokens: request.maxTokens || 1,
|
|
271
|
+
temperature: request.temperature || 0
|
|
272
|
+
})
|
|
273
|
+
});
|
|
195
274
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return HealthStatus.HEALTHY;
|
|
199
|
-
}
|
|
200
|
-
return HealthStatus.DEGRADED;
|
|
201
|
-
} catch (error) {
|
|
202
|
-
this.logger.warn('vLLM health check failed', error);
|
|
203
|
-
return HealthStatus.UNHEALTHY;
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
throw new Error(`vLLM API error: ${response.status}`);
|
|
204
277
|
}
|
|
278
|
+
|
|
279
|
+
const data = await response.json();
|
|
280
|
+
return {
|
|
281
|
+
message: {
|
|
282
|
+
id: this.generateId(),
|
|
283
|
+
role: MessageRole.ASSISTANT,
|
|
284
|
+
content: data.choices[0]?.message?.content || '',
|
|
285
|
+
timestamp: new Date()
|
|
286
|
+
},
|
|
287
|
+
usage: data.usage ? {
|
|
288
|
+
promptTokens: data.usage.prompt_tokens,
|
|
289
|
+
completionTokens: data.usage.completion_tokens,
|
|
290
|
+
totalTokens: data.usage.total_tokens
|
|
291
|
+
} : undefined
|
|
292
|
+
};
|
|
205
293
|
}
|
|
206
294
|
|
|
207
295
|
/**
|