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.
Files changed (42) hide show
  1. package/.editorconfig +18 -0
  2. package/README.md +40 -10
  3. package/dist/index.js +1 -1
  4. package/package.json +5 -3
  5. package/src/components/chat/ai-sidebar.component.scss +220 -9
  6. package/src/components/chat/ai-sidebar.component.ts +379 -29
  7. package/src/components/chat/chat-input.component.ts +36 -4
  8. package/src/components/chat/chat-interface.component.ts +225 -5
  9. package/src/components/chat/chat-message.component.ts +6 -1
  10. package/src/components/settings/context-settings.component.ts +91 -91
  11. package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
  12. package/src/components/terminal/command-suggestion.component.ts +148 -6
  13. package/src/index.ts +81 -19
  14. package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
  15. package/src/services/chat/ai-sidebar.service.ts +448 -410
  16. package/src/services/chat/chat-session.service.ts +36 -12
  17. package/src/services/context/compaction.ts +110 -134
  18. package/src/services/context/manager.ts +27 -7
  19. package/src/services/context/memory.ts +17 -33
  20. package/src/services/context/summary.service.ts +136 -0
  21. package/src/services/core/ai-assistant.service.ts +1060 -37
  22. package/src/services/core/ai-provider-manager.service.ts +154 -25
  23. package/src/services/core/checkpoint.service.ts +218 -18
  24. package/src/services/core/toast.service.ts +106 -106
  25. package/src/services/providers/anthropic-provider.service.ts +126 -30
  26. package/src/services/providers/base-provider.service.ts +90 -7
  27. package/src/services/providers/glm-provider.service.ts +151 -38
  28. package/src/services/providers/minimax-provider.service.ts +55 -40
  29. package/src/services/providers/ollama-provider.service.ts +117 -28
  30. package/src/services/providers/openai-compatible.service.ts +164 -34
  31. package/src/services/providers/openai-provider.service.ts +169 -34
  32. package/src/services/providers/vllm-provider.service.ts +116 -28
  33. package/src/services/terminal/terminal-context.service.ts +265 -5
  34. package/src/services/terminal/terminal-manager.service.ts +845 -748
  35. package/src/services/terminal/terminal-tools.service.ts +612 -441
  36. package/src/types/ai.types.ts +156 -3
  37. package/src/utils/cost.utils.ts +249 -0
  38. package/src/utils/validation.utils.ts +306 -2
  39. package/dist/index.js.LICENSE.txt +0 -18
  40. package/src/services/terminal/command-analyzer.service.ts +0 -43
  41. package/src/services/terminal/context-menu.service.ts +0 -45
  42. package/src/services/terminal/hotkey.service.ts +0 -53
@@ -1,9 +1,9 @@
1
1
  import { Injectable } from '@angular/core';
2
- import { Observable, from } from 'rxjs';
2
+ import { Observable, Observer } from 'rxjs';
3
3
  import axios, { AxiosInstance } from 'axios';
4
4
  import { BaseAiProvider } from './base-provider.service';
5
- import { ProviderCapability, HealthStatus, ValidationResult } from '../../types/provider.types';
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<any> {
114
- // 回退到非流式
115
- return from(this.chat(request));
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 healthCheck(): Promise<HealthStatus> {
179
- try {
180
- if (!this.client) {
181
- return HealthStatus.UNHEALTHY;
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
- return HealthStatus.DEGRADED;
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
- } catch (error) {
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, from } from 'rxjs';
2
+ import { Observable, Observer } from 'rxjs';
3
3
  import axios, { AxiosInstance } from 'axios';
4
4
  import { BaseAiProvider } from './base-provider.service';
5
- import { ProviderCapability, HealthStatus, ValidationResult } from '../../types/provider.types';
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<any> {
102
- // 回退到非流式
103
- return from(this.chat(request));
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 healthCheck(): Promise<HealthStatus> {
167
- try {
168
- if (!this.client) {
169
- return HealthStatus.UNHEALTHY;
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
- return HealthStatus.DEGRADED;
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
- } catch (error) {
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, HealthStatus, ValidationResult } from '../../types/provider.types';
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 delta = parsed.choices[0]?.delta?.content;
142
- if (delta) {
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.error(new Error(`vLLM stream failed: ${error instanceof Error ? error.message : String(error)}`));
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
- * 健康检查 - 检测 vLLM 服务是否运行
182
- */
183
- async healthCheck(): Promise<HealthStatus> {
184
- try {
185
- const controller = new AbortController();
186
- const timeoutId = setTimeout(() => controller.abort(), 5000);
187
-
188
- const response = await fetch(`${this.getBaseURL()}/models`, {
189
- method: 'GET',
190
- headers: this.getAuthHeaders(),
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
- if (response.ok) {
197
- this.lastHealthCheck = { status: HealthStatus.HEALTHY, timestamp: new Date() };
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
  /**