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
package/src/types/ai.types.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
export enum MessageRole {
|
|
7
7
|
USER = 'user',
|
|
8
8
|
ASSISTANT = 'assistant',
|
|
9
|
-
SYSTEM = 'system'
|
|
9
|
+
SYSTEM = 'system',
|
|
10
|
+
TOOL = 'tool' // 工具结果角色(部分 AI 需要)
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
// 聊天消息
|
|
@@ -140,7 +141,7 @@ export interface ValidationResult {
|
|
|
140
141
|
|
|
141
142
|
// API消息接口(支持压缩标记)
|
|
142
143
|
export interface ApiMessage {
|
|
143
|
-
role: 'user' | 'assistant' | 'system';
|
|
144
|
+
role: 'user' | 'assistant' | 'system' | 'tool' | 'function';
|
|
144
145
|
content: string | ContentBlock[];
|
|
145
146
|
ts: number; // 时间戳(毫秒)
|
|
146
147
|
|
|
@@ -148,6 +149,11 @@ export interface ApiMessage {
|
|
|
148
149
|
isSummary?: boolean; // 是否为摘要消息
|
|
149
150
|
condenseId?: string; // 摘要ID
|
|
150
151
|
condenseParent?: string; // 被哪个摘要压缩
|
|
152
|
+
summaryMeta?: { // 摘要元数据
|
|
153
|
+
originalMessageCount: number;
|
|
154
|
+
tokensCost: number;
|
|
155
|
+
compressionRatio: number;
|
|
156
|
+
};
|
|
151
157
|
|
|
152
158
|
// 截断相关元数据
|
|
153
159
|
isTruncationMarker?: boolean; // 是否为截断标记
|
|
@@ -245,6 +251,16 @@ export interface ExtendedChatMessage extends ChatMessage {
|
|
|
245
251
|
tokenUsage?: TokenUsage;
|
|
246
252
|
}
|
|
247
253
|
|
|
254
|
+
// 压缩后的检查点数据接口
|
|
255
|
+
export interface CompressedCheckpointData {
|
|
256
|
+
compressed: boolean;
|
|
257
|
+
compressionRatio: number;
|
|
258
|
+
originalSize: number;
|
|
259
|
+
compressedSize: number;
|
|
260
|
+
messages?: ApiMessage[]; // 可选,用于即时访问
|
|
261
|
+
messagesJson: string; // 压缩后的JSON字符串
|
|
262
|
+
}
|
|
263
|
+
|
|
248
264
|
// 检查点接口
|
|
249
265
|
export interface Checkpoint {
|
|
250
266
|
id: string;
|
|
@@ -253,6 +269,11 @@ export interface Checkpoint {
|
|
|
253
269
|
summary: string;
|
|
254
270
|
createdAt: number; // 时间戳(毫秒)
|
|
255
271
|
tokenUsage: TokenUsage;
|
|
272
|
+
compressedData?: CompressedCheckpointData; // 压缩数据(可选)
|
|
273
|
+
|
|
274
|
+
// 新增字段
|
|
275
|
+
tags?: string[]; // 标签列表
|
|
276
|
+
isArchived?: boolean; // 是否已归档
|
|
256
277
|
}
|
|
257
278
|
|
|
258
279
|
// ============================================================================
|
|
@@ -261,7 +282,7 @@ export interface Checkpoint {
|
|
|
261
282
|
|
|
262
283
|
// 流式事件类型
|
|
263
284
|
export interface StreamEvent {
|
|
264
|
-
type: 'text_delta' | 'tool_use_start' | 'tool_use_delta' | 'tool_use_end' | 'message_end' | 'error';
|
|
285
|
+
type: 'text_delta' | 'tool_use_start' | 'tool_use_delta' | 'tool_use_end' | 'tool_result' | 'tool_error' | 'message_end' | 'error';
|
|
265
286
|
// 文本增量
|
|
266
287
|
textDelta?: string;
|
|
267
288
|
// 工具调用(完整时才有)
|
|
@@ -270,8 +291,140 @@ export interface StreamEvent {
|
|
|
270
291
|
name: string;
|
|
271
292
|
input: any;
|
|
272
293
|
};
|
|
294
|
+
// 工具结果(tool_result 事件)
|
|
295
|
+
result?: {
|
|
296
|
+
tool_use_id: string;
|
|
297
|
+
content: string;
|
|
298
|
+
is_error?: boolean;
|
|
299
|
+
};
|
|
273
300
|
// 错误信息
|
|
274
301
|
error?: string;
|
|
275
302
|
// 最终消息(message_end 时)
|
|
276
303
|
message?: ChatMessage;
|
|
277
304
|
}
|
|
305
|
+
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// Agent 循环相关类型定义
|
|
308
|
+
// ============================================================================
|
|
309
|
+
|
|
310
|
+
// 工具调用接口
|
|
311
|
+
export interface ToolCall {
|
|
312
|
+
id: string;
|
|
313
|
+
name: string;
|
|
314
|
+
input: any;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 工具结果接口
|
|
318
|
+
export interface ToolResult {
|
|
319
|
+
tool_use_id: string;
|
|
320
|
+
name?: string; // 工具名称
|
|
321
|
+
content: string;
|
|
322
|
+
is_error?: boolean;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Agent 事件类型
|
|
326
|
+
export type AgentEventType =
|
|
327
|
+
| 'text_delta' // 文本增量
|
|
328
|
+
| 'tool_use_start' // 工具开始
|
|
329
|
+
| 'tool_use_end' // 工具调用结束(收集参数)
|
|
330
|
+
| 'tool_executing' // 工具正在执行
|
|
331
|
+
| 'tool_executed' // 工具执行完成(带结果)
|
|
332
|
+
| 'tool_error' // 工具执行错误
|
|
333
|
+
| 'round_start' // 新一轮开始
|
|
334
|
+
| 'round_end' // 一轮结束
|
|
335
|
+
| 'agent_complete' // Agent 循环完成
|
|
336
|
+
| 'error'; // 错误
|
|
337
|
+
|
|
338
|
+
// Agent 流式事件
|
|
339
|
+
export interface AgentStreamEvent {
|
|
340
|
+
type: AgentEventType;
|
|
341
|
+
|
|
342
|
+
// text_delta 事件
|
|
343
|
+
textDelta?: string;
|
|
344
|
+
|
|
345
|
+
// 工具相关事件
|
|
346
|
+
toolCall?: {
|
|
347
|
+
id: string;
|
|
348
|
+
name: string;
|
|
349
|
+
input: any;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// tool_executed/tool_error 事件
|
|
353
|
+
toolResult?: {
|
|
354
|
+
tool_use_id: string;
|
|
355
|
+
content: string;
|
|
356
|
+
is_error?: boolean;
|
|
357
|
+
duration?: number;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// round_start/round_end 事件
|
|
361
|
+
round?: number;
|
|
362
|
+
|
|
363
|
+
// agent_complete 事件
|
|
364
|
+
reason?: TerminationReason;
|
|
365
|
+
totalRounds?: number;
|
|
366
|
+
terminationMessage?: string; // 可选的终止详情消息
|
|
367
|
+
|
|
368
|
+
// error 事件
|
|
369
|
+
error?: string;
|
|
370
|
+
|
|
371
|
+
// message_end 保留
|
|
372
|
+
message?: ChatMessage;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Agent 循环配置
|
|
376
|
+
export interface AgentLoopConfig {
|
|
377
|
+
maxRounds?: number; // 最大轮数,默认 15
|
|
378
|
+
timeoutMs?: number; // 默认 120000 (2分钟)
|
|
379
|
+
repeatThreshold?: number; // 默认 3 次
|
|
380
|
+
failureThreshold?: number; // 默认 2 次
|
|
381
|
+
enableTaskComplete?: boolean; // 默认 true
|
|
382
|
+
onRoundStart?: (round: number) => void;
|
|
383
|
+
onRoundEnd?: (round: number) => void;
|
|
384
|
+
onAgentComplete?: (reason: string, totalRounds: number) => void;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ============================================================================
|
|
388
|
+
// 智能 Agent 终止相关类型定义
|
|
389
|
+
// ============================================================================
|
|
390
|
+
|
|
391
|
+
// 终止原因枚举
|
|
392
|
+
export type TerminationReason =
|
|
393
|
+
| 'task_complete' // AI 主动调用 task_complete 工具
|
|
394
|
+
| 'no_tools' // 本轮无工具调用
|
|
395
|
+
| 'summarizing' // 检测到 AI 正在总结
|
|
396
|
+
| 'repeated_tool' // 重复调用相同工具
|
|
397
|
+
| 'high_failure_rate' // 连续失败率过高
|
|
398
|
+
| 'timeout' // 总时间超时
|
|
399
|
+
| 'max_rounds' // 达到最大轮数(安全保底)
|
|
400
|
+
| 'user_cancel'; // 用户取消
|
|
401
|
+
|
|
402
|
+
// Agent 状态追踪
|
|
403
|
+
export interface AgentState {
|
|
404
|
+
currentRound: number;
|
|
405
|
+
startTime: number;
|
|
406
|
+
toolCallHistory: ToolCallRecord[];
|
|
407
|
+
lastAiResponse: string;
|
|
408
|
+
isActive: boolean;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 工具调用记录
|
|
412
|
+
export interface ToolCallRecord {
|
|
413
|
+
name: string;
|
|
414
|
+
input: any;
|
|
415
|
+
inputHash: string; // 用于快速比较
|
|
416
|
+
success: boolean;
|
|
417
|
+
timestamp: number;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 终止检测结果
|
|
421
|
+
export interface TerminationResult {
|
|
422
|
+
shouldTerminate: boolean;
|
|
423
|
+
reason: TerminationReason;
|
|
424
|
+
message?: string;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 扩展 ToolResult 添加任务完成标记
|
|
428
|
+
export interface ExtendedToolResult extends ToolResult {
|
|
429
|
+
isTaskComplete?: boolean; // 特殊标记:task_complete 工具调用
|
|
430
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API成本计算工具类
|
|
3
|
+
* 提供各AI提供商的API调用成本估算功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AI提供商类型
|
|
8
|
+
*/
|
|
9
|
+
export type AIProvider = 'openai' | 'anthropic' | 'minimax' | 'glm' | 'openai-compatible';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 模型定价信息
|
|
13
|
+
*/
|
|
14
|
+
export interface ModelPricing {
|
|
15
|
+
provider: AIProvider;
|
|
16
|
+
model: string;
|
|
17
|
+
inputPricePerMillion: number; // 每百万输入token的价格(美元)
|
|
18
|
+
outputPricePerMillion: number; // 每百万输出token的价格(美元)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Token使用信息
|
|
23
|
+
*/
|
|
24
|
+
export interface TokenUsage {
|
|
25
|
+
inputTokens: number;
|
|
26
|
+
outputTokens: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 成本计算结果
|
|
31
|
+
*/
|
|
32
|
+
export interface CostResult {
|
|
33
|
+
inputCost: number; // 输入成本(美元)
|
|
34
|
+
outputCost: number; // 输出成本(美元)
|
|
35
|
+
totalCost: number; // 总成本(美元)
|
|
36
|
+
inputPricePerMillion: number;
|
|
37
|
+
outputPricePerMillion: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 默认模型定价表(2024年最新价格)
|
|
42
|
+
*/
|
|
43
|
+
const DEFAULT_PRICING: ModelPricing[] = [
|
|
44
|
+
// OpenAI
|
|
45
|
+
{ provider: 'openai', model: 'gpt-4', inputPricePerMillion: 30, outputPricePerMillion: 60 },
|
|
46
|
+
{ provider: 'openai', model: 'gpt-4-turbo', inputPricePerMillion: 10, outputPricePerMillion: 30 },
|
|
47
|
+
{ provider: 'openai', model: 'gpt-4o', inputPricePerMillion: 5, outputPricePerMillion: 15 },
|
|
48
|
+
{ provider: 'openai', model: 'gpt-3.5-turbo', inputPricePerMillion: 0.5, outputPricePerMillion: 1.5 },
|
|
49
|
+
|
|
50
|
+
// Anthropic
|
|
51
|
+
{ provider: 'anthropic', model: 'claude-3-5-sonnet-20241022', inputPricePerMillion: 3, outputPricePerMillion: 15 },
|
|
52
|
+
{ provider: 'anthropic', model: 'claude-3-opus-20240229', inputPricePerMillion: 15, outputPricePerMillion: 75 },
|
|
53
|
+
{ provider: 'anthropic', model: 'claude-3-haiku-20240307', inputPricePerMillion: 0.25, outputPricePerMillion: 1.25 },
|
|
54
|
+
|
|
55
|
+
// Minimax (智谱AI)
|
|
56
|
+
{ provider: 'minimax', model: 'abab6.5s-chat', inputPricePerMillion: 0.3, outputPricePerMillion: 0.3 },
|
|
57
|
+
{ provider: 'minimax', model: 'abab6.5-chat', inputPricePerMillion: 0.5, outputPricePerMillion: 0.5 },
|
|
58
|
+
{ provider: 'minimax', model: 'abab5.5-chat', inputPricePerMillion: 1, outputPricePerMillion: 1 },
|
|
59
|
+
|
|
60
|
+
// GLM (智谱AI)
|
|
61
|
+
{ provider: 'glm', model: 'glm-4', inputPricePerMillion: 0.5, outputPricePerMillion: 1.5 },
|
|
62
|
+
{ provider: 'glm', model: 'glm-4v', inputPricePerMillion: 0.5, outputPricePerMillion: 1.5 },
|
|
63
|
+
{ provider: 'glm', model: 'glm-3-turbo', inputPricePerMillion: 0.1, outputPricePerMillion: 0.1 },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// 自定义定价表(可扩展)
|
|
67
|
+
let customPricing: ModelPricing[] = [];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 设置自定义模型定价
|
|
71
|
+
*/
|
|
72
|
+
export function setCustomPricing(pricing: ModelPricing[]): void {
|
|
73
|
+
customPricing = [...pricing];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 获取模型定价信息
|
|
78
|
+
*/
|
|
79
|
+
export function getModelPricing(provider: AIProvider, model: string): ModelPricing | undefined {
|
|
80
|
+
// 首先查找自定义定价
|
|
81
|
+
const custom = customPricing.find(p => p.provider === provider && p.model === model);
|
|
82
|
+
if (custom) {
|
|
83
|
+
return custom;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 然后查找默认定价
|
|
87
|
+
const defaultPricing = DEFAULT_PRICING.find(p => p.provider === provider && p.model === model);
|
|
88
|
+
|
|
89
|
+
if (defaultPricing) {
|
|
90
|
+
return defaultPricing;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 返回该提供商的通用定价
|
|
94
|
+
return getDefaultPricingForProvider(provider);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 获取提供商的默认定价
|
|
99
|
+
*/
|
|
100
|
+
function getDefaultPricingForProvider(provider: AIProvider): ModelPricing | undefined {
|
|
101
|
+
const providerDefaults: Record<AIProvider, Partial<ModelPricing>> = {
|
|
102
|
+
'openai': { inputPricePerMillion: 5, outputPricePerMillion: 15 },
|
|
103
|
+
'anthropic': { inputPricePerMillion: 3, outputPricePerMillion: 15 },
|
|
104
|
+
'minimax': { inputPricePerMillion: 0.5, outputPricePerMillion: 0.5 },
|
|
105
|
+
'glm': { inputPricePerMillion: 0.5, outputPricePerMillion: 1 },
|
|
106
|
+
'openai-compatible': { inputPricePerMillion: 1, outputPricePerMillion: 2 }
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const defaults = providerDefaults[provider];
|
|
110
|
+
if (defaults) {
|
|
111
|
+
return {
|
|
112
|
+
provider,
|
|
113
|
+
model: 'default',
|
|
114
|
+
inputPricePerMillion: defaults.inputPricePerMillion ?? 1,
|
|
115
|
+
outputPricePerMillion: defaults.outputPricePerMillion ?? 2
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 计算API调用成本
|
|
124
|
+
* @param provider AI提供商
|
|
125
|
+
* @param model 模型名称
|
|
126
|
+
* @param usage Token使用情况
|
|
127
|
+
* @returns 成本计算结果
|
|
128
|
+
*/
|
|
129
|
+
export function calculateCost(
|
|
130
|
+
provider: AIProvider,
|
|
131
|
+
model: string,
|
|
132
|
+
usage: TokenUsage
|
|
133
|
+
): CostResult {
|
|
134
|
+
const pricing = getModelPricing(provider, model);
|
|
135
|
+
|
|
136
|
+
if (!pricing) {
|
|
137
|
+
// 未知提供商,返回零成本
|
|
138
|
+
return {
|
|
139
|
+
inputCost: 0,
|
|
140
|
+
outputCost: 0,
|
|
141
|
+
totalCost: 0,
|
|
142
|
+
inputPricePerMillion: 0,
|
|
143
|
+
outputPricePerMillion: 0
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const inputCost = (usage.inputTokens / 1000000) * pricing.inputPricePerMillion;
|
|
148
|
+
const outputCost = (usage.outputTokens / 1000000) * pricing.outputPricePerMillion;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
inputCost: Math.round(inputCost * 1000000) / 1000000, // 保留6位小数
|
|
152
|
+
outputCost: Math.round(outputCost * 1000000) / 1000000,
|
|
153
|
+
totalCost: Math.round((inputCost + outputCost) * 1000000) / 1000000,
|
|
154
|
+
inputPricePerMillion: pricing.inputPricePerMillion,
|
|
155
|
+
outputPricePerMillion: pricing.outputPricePerMillion
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 计算摘要生成成本
|
|
161
|
+
*/
|
|
162
|
+
export function calculateSummaryCost(
|
|
163
|
+
provider: AIProvider,
|
|
164
|
+
model: string,
|
|
165
|
+
originalMessageCount: number,
|
|
166
|
+
tokensUsed: number
|
|
167
|
+
): CostResult {
|
|
168
|
+
// 摘要生成主要是输入成本
|
|
169
|
+
const pricing = getModelPricing(provider, model);
|
|
170
|
+
|
|
171
|
+
if (!pricing) {
|
|
172
|
+
return {
|
|
173
|
+
inputCost: 0,
|
|
174
|
+
outputCost: 0,
|
|
175
|
+
totalCost: 0,
|
|
176
|
+
inputPricePerMillion: 0,
|
|
177
|
+
outputPricePerMillion: 0
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 估算摘要的输入和输出token(假设输出占输入的5%)
|
|
182
|
+
const estimatedInputTokens = tokensUsed;
|
|
183
|
+
const estimatedOutputTokens = Math.floor(tokensUsed * 0.05);
|
|
184
|
+
|
|
185
|
+
return calculateCost(provider, model, {
|
|
186
|
+
inputTokens: estimatedInputTokens,
|
|
187
|
+
outputTokens: estimatedOutputTokens
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 格式化成本为可读字符串
|
|
193
|
+
*/
|
|
194
|
+
export function formatCost(cost: number): string {
|
|
195
|
+
if (cost < 0.001) {
|
|
196
|
+
return `$${(cost * 1000000).toFixed(2)}`;
|
|
197
|
+
} else if (cost < 1) {
|
|
198
|
+
return `$${cost.toFixed(4)}`;
|
|
199
|
+
} else {
|
|
200
|
+
return `$${cost.toFixed(2)}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 格式化成本详细信息
|
|
206
|
+
*/
|
|
207
|
+
export function formatCostDetail(result: CostResult): string {
|
|
208
|
+
const parts: string[] = [];
|
|
209
|
+
|
|
210
|
+
if (result.inputCost > 0) {
|
|
211
|
+
parts.push(`输入: ${formatCost(result.inputCost)}`);
|
|
212
|
+
}
|
|
213
|
+
if (result.outputCost > 0) {
|
|
214
|
+
parts.push(`输出: ${formatCost(result.outputCost)}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return parts.join(', ') + ` (总计: ${formatCost(result.totalCost)})`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 估算消息的Token数量
|
|
222
|
+
*/
|
|
223
|
+
export function estimateTokenCount(text: string): number {
|
|
224
|
+
// 粗略估算:1个Token约等于4个字符(英文)
|
|
225
|
+
// 中文:1个Token约等于1.5个字符
|
|
226
|
+
const chineseCharCount = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
|
|
227
|
+
const englishCharCount = text.length - chineseCharCount;
|
|
228
|
+
|
|
229
|
+
return Math.ceil(chineseCharCount / 1.5 + englishCharCount / 4);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 计算批量请求的总成本
|
|
234
|
+
*/
|
|
235
|
+
export function calculateBatchCost(
|
|
236
|
+
provider: AIProvider,
|
|
237
|
+
model: string,
|
|
238
|
+
requests: TokenUsage[]
|
|
239
|
+
): CostResult {
|
|
240
|
+
const totalUsage = requests.reduce(
|
|
241
|
+
(acc, usage) => ({
|
|
242
|
+
inputTokens: acc.inputTokens + usage.inputTokens,
|
|
243
|
+
outputTokens: acc.outputTokens + usage.outputTokens
|
|
244
|
+
}),
|
|
245
|
+
{ inputTokens: 0, outputTokens: 0 }
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return calculateCost(provider, model, totalUsage);
|
|
249
|
+
}
|