@huyooo/ai-chat-core 0.2.19 → 0.2.21
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/events.d.ts +452 -0
- package/dist/events.js +1 -0
- package/dist/index.d.ts +202 -550
- package/dist/index.js +1 -1
- package/package.json +23 -4
- package/src/agent.ts +399 -0
- package/src/constants.ts +125 -0
- package/src/events.ts +797 -0
- package/src/index.ts +309 -0
- package/src/internal/update-plan.ts +2 -0
- package/src/internal/web-search.ts +78 -0
- package/src/mcp/client-manager.ts +301 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/types.ts +43 -0
- package/src/providers/context-compressor.ts +149 -0
- package/src/providers/index.ts +120 -0
- package/src/providers/model-registry.ts +320 -0
- package/src/providers/orchestrator.ts +761 -0
- package/src/providers/protocols/anthropic.ts +406 -0
- package/src/providers/protocols/ark.ts +362 -0
- package/src/providers/protocols/deepseek.ts +344 -0
- package/src/providers/protocols/error-utils.ts +74 -0
- package/src/providers/protocols/gemini.ts +350 -0
- package/src/providers/protocols/index.ts +36 -0
- package/src/providers/protocols/openai.ts +420 -0
- package/src/providers/protocols/qwen.ts +326 -0
- package/src/providers/protocols/types.ts +189 -0
- package/src/providers/types.ts +272 -0
- package/src/providers/unified-adapter.ts +367 -0
- package/src/router.ts +72 -0
- package/src/test-utils/mock-sse.ts +32 -0
- package/src/tools.ts +162 -0
- package/src/types.ts +531 -0
- package/src/utils.ts +86 -0
package/src/mcp/types.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) 相关类型定义
|
|
3
|
+
*
|
|
4
|
+
* 支持两种传输方式:
|
|
5
|
+
* - stdio: 启动本地子进程,通过 stdin/stdout 通信(操作本地文件、命令、应用)
|
|
6
|
+
* - sse: 连接远程 HTTP 服务器(调用远程 API/服务)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** MCP Server 配置 */
|
|
10
|
+
export interface McpServerConfig {
|
|
11
|
+
/** 服务器唯一标识(用于日志和工具命名空间) */
|
|
12
|
+
name: string;
|
|
13
|
+
|
|
14
|
+
/** 传输方式 */
|
|
15
|
+
transport: 'stdio' | 'sse';
|
|
16
|
+
|
|
17
|
+
/** stdio 模式:启动命令(如 "npx", "python3", "/usr/local/bin/my-server") */
|
|
18
|
+
command?: string;
|
|
19
|
+
/** stdio 模式:命令参数 */
|
|
20
|
+
args?: string[];
|
|
21
|
+
/** stdio 模式:环境变量 */
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
/** stdio 模式:工作目录 */
|
|
24
|
+
cwd?: string;
|
|
25
|
+
|
|
26
|
+
/** sse 模式:服务器 URL */
|
|
27
|
+
url?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** MCP 连接状态 */
|
|
31
|
+
export type McpConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
32
|
+
|
|
33
|
+
/** 单个 MCP 连接信息 */
|
|
34
|
+
export interface McpConnectionInfo {
|
|
35
|
+
/** 服务器名称 */
|
|
36
|
+
name: string;
|
|
37
|
+
/** 连接状态 */
|
|
38
|
+
status: McpConnectionStatus;
|
|
39
|
+
/** 提供的工具数量 */
|
|
40
|
+
toolCount: number;
|
|
41
|
+
/** 错误信息(status === 'error' 时) */
|
|
42
|
+
error?: string;
|
|
43
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context 压缩模块
|
|
3
|
+
*
|
|
4
|
+
* 当消息历史过长时自动压缩,避免超出模型 context window。
|
|
5
|
+
*
|
|
6
|
+
* 策略参考 Claude Code(92% context window 触发摘要)和
|
|
7
|
+
* OpenAI Codex(auto_compact_limit 触发压缩)。
|
|
8
|
+
*
|
|
9
|
+
* 压缩算法:
|
|
10
|
+
* 1. 保留 system prompt
|
|
11
|
+
* 2. 保留第一条 user 消息(任务描述)
|
|
12
|
+
* 3. 将中间的 assistant/tool 交互压缩为一条摘要
|
|
13
|
+
* 4. 保留最近 N 条消息(工作上下文)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { StandardMessage } from './types';
|
|
17
|
+
import { DebugLogger } from '../utils';
|
|
18
|
+
|
|
19
|
+
const logger = DebugLogger.module('ContextCompressor');
|
|
20
|
+
|
|
21
|
+
// ==================== 配置 ====================
|
|
22
|
+
|
|
23
|
+
/** 压缩配置 */
|
|
24
|
+
export interface CompactConfig {
|
|
25
|
+
/**
|
|
26
|
+
* 字符数阈值,超过此值触发压缩
|
|
27
|
+
*
|
|
28
|
+
* 使用字符数而非 token 数(粗略估计:1 token ≈ 3-4 中文字符 / 4 英文字符)。
|
|
29
|
+
* 80K 字符 ≈ 20K-27K tokens,约为最小 context window (128K tokens) 的 15-20%。
|
|
30
|
+
*
|
|
31
|
+
* @default 80_000
|
|
32
|
+
*/
|
|
33
|
+
charThreshold?: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 压缩后保留的最近消息数
|
|
37
|
+
*
|
|
38
|
+
* 保留最近的消息对(assistant + tool),确保模型有足够上下文继续工作。
|
|
39
|
+
*
|
|
40
|
+
* @default 10
|
|
41
|
+
*/
|
|
42
|
+
keepRecentMessages?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CHAR_THRESHOLD = 80_000;
|
|
46
|
+
const DEFAULT_KEEP_RECENT = 10;
|
|
47
|
+
|
|
48
|
+
// ==================== 核心函数 ====================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 压缩消息历史
|
|
52
|
+
*
|
|
53
|
+
* 当消息总字符数超过阈值时,保留首尾、压缩中间部分。
|
|
54
|
+
*
|
|
55
|
+
* @param messages - 消息数组(会被原地修改)
|
|
56
|
+
* @param config - 压缩配置(可选)
|
|
57
|
+
* @returns 是否执行了压缩
|
|
58
|
+
*/
|
|
59
|
+
export function compactMessages(messages: StandardMessage[], config?: CompactConfig): boolean {
|
|
60
|
+
const charThreshold = config?.charThreshold ?? DEFAULT_CHAR_THRESHOLD;
|
|
61
|
+
const keepRecent = config?.keepRecentMessages ?? DEFAULT_KEEP_RECENT;
|
|
62
|
+
|
|
63
|
+
// 计算总字符数
|
|
64
|
+
const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
|
|
65
|
+
|
|
66
|
+
if (totalChars < charThreshold) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger.info(`Context 压缩触发: ${totalChars} 字符, ${messages.length} 条消息`);
|
|
71
|
+
|
|
72
|
+
// ---- 找到各段边界 ----
|
|
73
|
+
// [0..systemEnd): system prompt(可能没有)
|
|
74
|
+
// [systemEnd..firstUserEnd): 第一条 user 消息
|
|
75
|
+
// [firstUserEnd..recentStart): 中间的 assistant/tool 交互(压缩目标)
|
|
76
|
+
// [recentStart..end): 最近 N 条消息(保留)
|
|
77
|
+
|
|
78
|
+
let systemEnd = 0;
|
|
79
|
+
if (messages[0]?.role === 'system') {
|
|
80
|
+
systemEnd = 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 找到第一条 user 消息
|
|
84
|
+
let firstUserEnd = systemEnd;
|
|
85
|
+
for (let i = systemEnd; i < messages.length; i++) {
|
|
86
|
+
if (messages[i].role === 'user') {
|
|
87
|
+
firstUserEnd = i + 1;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const recentStart = Math.max(firstUserEnd, messages.length - keepRecent);
|
|
93
|
+
|
|
94
|
+
// 中间部分太短,不值得压缩
|
|
95
|
+
if (recentStart - firstUserEnd < 4) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- 生成摘要 ----
|
|
100
|
+
const middleMessages = messages.slice(firstUserEnd, recentStart);
|
|
101
|
+
const summary = buildSummary(middleMessages);
|
|
102
|
+
|
|
103
|
+
// ---- 组装压缩后的消息 ----
|
|
104
|
+
const compressed: StandardMessage[] = [
|
|
105
|
+
...messages.slice(0, firstUserEnd), // system + first user
|
|
106
|
+
{ role: 'system', content: summary }, // 压缩摘要
|
|
107
|
+
...messages.slice(recentStart), // 最近 N 条
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const compressedChars = compressed.reduce((s, m) => s + (m.content?.length ?? 0), 0);
|
|
111
|
+
logger.info(`Context 压缩完成: ${messages.length} → ${compressed.length} 条消息, ${totalChars} → ${compressedChars} 字符`);
|
|
112
|
+
|
|
113
|
+
// 原地替换
|
|
114
|
+
messages.length = 0;
|
|
115
|
+
messages.push(...compressed);
|
|
116
|
+
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ==================== 内部函数 ====================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 从中间消息中构建压缩摘要
|
|
124
|
+
*/
|
|
125
|
+
function buildSummary(middleMessages: StandardMessage[]): string {
|
|
126
|
+
const toolCallNames: string[] = [];
|
|
127
|
+
let textPreview = '';
|
|
128
|
+
|
|
129
|
+
for (const msg of middleMessages) {
|
|
130
|
+
if (msg.role === 'assistant' && msg.toolCalls) {
|
|
131
|
+
for (const tc of msg.toolCalls) {
|
|
132
|
+
toolCallNames.push(tc.name);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
136
|
+
textPreview += msg.content.slice(0, 200) + '\n';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return [
|
|
141
|
+
`[上下文压缩] 以下是之前 ${middleMessages.length} 条消息的摘要:`,
|
|
142
|
+
toolCallNames.length > 0
|
|
143
|
+
? `- 执行了 ${toolCallNames.length} 次工具调用: ${[...new Set(toolCallNames)].join(', ')}`
|
|
144
|
+
: '',
|
|
145
|
+
textPreview
|
|
146
|
+
? `- AI 回复摘要: ${textPreview.slice(0, 500)}`
|
|
147
|
+
: '',
|
|
148
|
+
].filter(Boolean).join('\n');
|
|
149
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider 模块导出
|
|
3
|
+
*
|
|
4
|
+
* 新架构(v2):
|
|
5
|
+
* - ModelRegistry: 模型注册表,管理所有模型配置(Protocol + Family)
|
|
6
|
+
* - ChatOrchestrator: 统一处理工具调用循环、消息历史、事件发射
|
|
7
|
+
* - ProviderAdapter: 只负责 API 格式转换,不处理业务逻辑
|
|
8
|
+
*
|
|
9
|
+
* 设计优势:
|
|
10
|
+
* 1. Protocol + Family 分离:同协议不同行为的模型可复用协议层
|
|
11
|
+
* 2. 所有 Provider 共享相同的工具调用逻辑
|
|
12
|
+
* 3. 修改 Orchestrator = 修改所有 Provider 行为
|
|
13
|
+
* 4. 每个 Adapter 只关心自己的 API 格式转换
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ==================== 模型注册表 ====================
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
// 家族配置
|
|
20
|
+
MODEL_FAMILIES,
|
|
21
|
+
DOUBAO_FAMILY,
|
|
22
|
+
DEEPSEEK_FAMILY,
|
|
23
|
+
QWEN_FAMILY,
|
|
24
|
+
GEMINI_FAMILY,
|
|
25
|
+
GPT_FAMILY,
|
|
26
|
+
CLAUDE_FAMILY,
|
|
27
|
+
|
|
28
|
+
// 模型注册表
|
|
29
|
+
MODEL_REGISTRY,
|
|
30
|
+
|
|
31
|
+
// 查询函数
|
|
32
|
+
getModelEntry,
|
|
33
|
+
getModelFamily,
|
|
34
|
+
getModelProtocol,
|
|
35
|
+
getVisibleModels,
|
|
36
|
+
getModelsByFamily,
|
|
37
|
+
getModelsByProtocol,
|
|
38
|
+
modelSupportsThinking,
|
|
39
|
+
modelSupportsNativeSearch,
|
|
40
|
+
getModelSearchStrategy,
|
|
41
|
+
} from './model-registry';
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
ModelFamilyId,
|
|
45
|
+
ProtocolId,
|
|
46
|
+
ThinkingFormat,
|
|
47
|
+
SearchStrategy,
|
|
48
|
+
ToolCallFormat,
|
|
49
|
+
ModelFamilyConfig,
|
|
50
|
+
ModelRegistryEntry,
|
|
51
|
+
} from './model-registry';
|
|
52
|
+
|
|
53
|
+
// ==================== 核心类型 ====================
|
|
54
|
+
|
|
55
|
+
export type {
|
|
56
|
+
// 流式响应
|
|
57
|
+
StreamChunk,
|
|
58
|
+
StreamChunkType,
|
|
59
|
+
ToolCallRequest,
|
|
60
|
+
SearchResultItem,
|
|
61
|
+
|
|
62
|
+
// 标准化消息
|
|
63
|
+
StandardMessage,
|
|
64
|
+
|
|
65
|
+
// Adapter 接口
|
|
66
|
+
ProviderAdapter,
|
|
67
|
+
AdapterConfig,
|
|
68
|
+
StreamOnceOptions,
|
|
69
|
+
SimpleToolDefinition,
|
|
70
|
+
|
|
71
|
+
// Orchestrator 类型
|
|
72
|
+
ToolExecutor,
|
|
73
|
+
OrchestratorConfig,
|
|
74
|
+
OrchestratorContext,
|
|
75
|
+
OrchestratorOptions,
|
|
76
|
+
} from './types';
|
|
77
|
+
|
|
78
|
+
// ==================== Orchestrator ====================
|
|
79
|
+
|
|
80
|
+
export { ChatOrchestrator, createOrchestrator } from './orchestrator';
|
|
81
|
+
|
|
82
|
+
// ==================== UnifiedAdapter(新架构) ====================
|
|
83
|
+
|
|
84
|
+
export { UnifiedAdapter, createUnifiedAdapter } from './unified-adapter';
|
|
85
|
+
export type { UnifiedAdapterConfig, StreamOptions } from './unified-adapter';
|
|
86
|
+
|
|
87
|
+
// ==================== Protocol Layer ====================
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
// Protocol 实现
|
|
91
|
+
ArkProtocol,
|
|
92
|
+
createArkProtocol,
|
|
93
|
+
DeepSeekProtocol,
|
|
94
|
+
createDeepSeekProtocol,
|
|
95
|
+
QwenProtocol,
|
|
96
|
+
createQwenProtocol,
|
|
97
|
+
GeminiProtocol,
|
|
98
|
+
createGeminiProtocol,
|
|
99
|
+
OpenAIProtocol,
|
|
100
|
+
createOpenAIProtocol,
|
|
101
|
+
AnthropicProtocol,
|
|
102
|
+
createAnthropicProtocol,
|
|
103
|
+
} from './protocols';
|
|
104
|
+
|
|
105
|
+
export type {
|
|
106
|
+
// Protocol 类型
|
|
107
|
+
Protocol,
|
|
108
|
+
ProtocolConfig,
|
|
109
|
+
ProtocolFactory,
|
|
110
|
+
ProtocolMessage,
|
|
111
|
+
ProtocolToolDefinition,
|
|
112
|
+
ProtocolToolCall,
|
|
113
|
+
ProtocolRequestOptions,
|
|
114
|
+
RawEvent,
|
|
115
|
+
RawEventType,
|
|
116
|
+
RawToolCall,
|
|
117
|
+
RawSearchResult,
|
|
118
|
+
} from './protocols';
|
|
119
|
+
|
|
120
|
+
// Legacy Adapters 已删除,使用 UnifiedAdapter + Protocol 新架构
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型注册表
|
|
3
|
+
*
|
|
4
|
+
* 核心设计:Protocol + Family 分离
|
|
5
|
+
* - Protocol:负责 API 通信协议(HTTP/SSE/认证)
|
|
6
|
+
* - Family:负责模型行为差异(thinking格式、搜索方式、工具格式)
|
|
7
|
+
*
|
|
8
|
+
* 这样设计的好处:
|
|
9
|
+
* 1. 同协议不同行为的模型可以复用协议层(如 ARK 上的豆包和 DeepSeek)
|
|
10
|
+
* 2. 新增模型只需配置 family,无需修改 adapter 代码
|
|
11
|
+
* 3. 行为差异集中管理,易于维护
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ==================== 模型家族定义 ====================
|
|
15
|
+
|
|
16
|
+
/** 模型家族 ID */
|
|
17
|
+
export type ModelFamilyId =
|
|
18
|
+
| 'doubao' // 豆包系列
|
|
19
|
+
| 'deepseek' // DeepSeek 系列
|
|
20
|
+
| 'qwen' // 通义千问系列
|
|
21
|
+
| 'gemini' // Gemini 系列
|
|
22
|
+
| 'gpt' // GPT 系列(OpenAI)
|
|
23
|
+
| 'claude'; // Claude 系列(Anthropic)
|
|
24
|
+
|
|
25
|
+
/** 协议类型 */
|
|
26
|
+
export type ProtocolId =
|
|
27
|
+
| 'ark' // 火山引擎 Responses API(豆包)
|
|
28
|
+
| 'deepseek' // DeepSeek(通过火山引擎 ARK)
|
|
29
|
+
| 'qwen' // 通义千问 DashScope API
|
|
30
|
+
| 'gemini' // Google Gemini API
|
|
31
|
+
| 'openai' // OpenAI API(通过 OpenRouter)
|
|
32
|
+
| 'anthropic'; // Anthropic API(通过 OpenRouter)
|
|
33
|
+
|
|
34
|
+
/** Thinking 输出格式 */
|
|
35
|
+
export type ThinkingFormat =
|
|
36
|
+
| 'reasoning' // ARK/豆包/DeepSeek: reasoning_summary_text
|
|
37
|
+
| 'thinking_enabled' // Qwen: thinking 参数
|
|
38
|
+
| 'thought_signature' // Gemini: 需要 thought_signature 循环
|
|
39
|
+
| 'none'; // 不支持 thinking
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 搜索实现方式(谁提供搜索、行为是否由我们统一)
|
|
43
|
+
* - provider_native:厂商原生,协议层各自实现(ARK type:web_search / Gemini googleSearch 等),行为各异
|
|
44
|
+
* - tavily:我们注入 Tavily web_search 工具,行为一致(GPT/Claude/Qwen/Gemini 有工具时)
|
|
45
|
+
*/
|
|
46
|
+
export type SearchStrategy =
|
|
47
|
+
| 'provider_native'
|
|
48
|
+
| 'tavily';
|
|
49
|
+
|
|
50
|
+
/** 工具调用格式 */
|
|
51
|
+
export type ToolCallFormat =
|
|
52
|
+
| 'responses' // ARK Responses API: function_call/function_call_output
|
|
53
|
+
| 'openai' // OpenAI 兼容: tool_calls/tool
|
|
54
|
+
| 'gemini'; // Gemini: functionCall/functionResponse
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 模型家族配置
|
|
58
|
+
* 定义同一家族模型的共同行为特征
|
|
59
|
+
*/
|
|
60
|
+
export interface ModelFamilyConfig {
|
|
61
|
+
/** 家族 ID */
|
|
62
|
+
id: ModelFamilyId;
|
|
63
|
+
/** 显示名称 */
|
|
64
|
+
displayName: string;
|
|
65
|
+
|
|
66
|
+
// === 多模态配置 ===
|
|
67
|
+
/** 是否支持图片理解 */
|
|
68
|
+
supportsVision: boolean;
|
|
69
|
+
|
|
70
|
+
// === Thinking 配置 ===
|
|
71
|
+
/** 是否支持 thinking */
|
|
72
|
+
supportsThinking: boolean;
|
|
73
|
+
/** Thinking 输出格式 */
|
|
74
|
+
thinkingFormat: ThinkingFormat;
|
|
75
|
+
|
|
76
|
+
// === 搜索配置 ===
|
|
77
|
+
/** 是否支持原生搜索 */
|
|
78
|
+
supportsNativeSearch: boolean;
|
|
79
|
+
/** 搜索实现方式 */
|
|
80
|
+
searchStrategy: SearchStrategy;
|
|
81
|
+
|
|
82
|
+
// === 工具调用配置 ===
|
|
83
|
+
/** 工具调用格式 */
|
|
84
|
+
toolCallFormat: ToolCallFormat;
|
|
85
|
+
|
|
86
|
+
// === 默认参数 ===
|
|
87
|
+
/** 默认最大输出 token */
|
|
88
|
+
defaultMaxTokens?: number;
|
|
89
|
+
/** 是否需要特殊处理(如 Gemini 的 thought_signature) */
|
|
90
|
+
requiresSpecialHandling?: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ==================== 家族配置定义 ====================
|
|
94
|
+
|
|
95
|
+
/** 豆包家族(联网搜索统一走 web_search_ai/Tavily,与其它模型事件与数据格式一致) */
|
|
96
|
+
export const DOUBAO_FAMILY: ModelFamilyConfig = {
|
|
97
|
+
id: 'doubao',
|
|
98
|
+
displayName: '豆包',
|
|
99
|
+
supportsVision: true,
|
|
100
|
+
supportsThinking: true,
|
|
101
|
+
thinkingFormat: 'reasoning',
|
|
102
|
+
supportsNativeSearch: false,
|
|
103
|
+
searchStrategy: 'tavily',
|
|
104
|
+
toolCallFormat: 'responses',
|
|
105
|
+
defaultMaxTokens: 32768,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** DeepSeek 家族(联网搜索统一走 web_search_ai/Tavily,与其它模型事件与数据格式一致) */
|
|
109
|
+
export const DEEPSEEK_FAMILY: ModelFamilyConfig = {
|
|
110
|
+
id: 'deepseek',
|
|
111
|
+
displayName: 'DeepSeek',
|
|
112
|
+
supportsVision: false,
|
|
113
|
+
supportsThinking: true,
|
|
114
|
+
thinkingFormat: 'reasoning',
|
|
115
|
+
supportsNativeSearch: false,
|
|
116
|
+
searchStrategy: 'tavily',
|
|
117
|
+
toolCallFormat: 'responses',
|
|
118
|
+
defaultMaxTokens: 32768,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** 通义千问家族 */
|
|
122
|
+
/** Qwen 家族(使用 Tavily 统一搜索) */
|
|
123
|
+
export const QWEN_FAMILY: ModelFamilyConfig = {
|
|
124
|
+
id: 'qwen',
|
|
125
|
+
displayName: '通义千问',
|
|
126
|
+
supportsVision: false,
|
|
127
|
+
supportsThinking: true,
|
|
128
|
+
thinkingFormat: 'thinking_enabled',
|
|
129
|
+
supportsNativeSearch: false, // 关闭原生搜索,使用 Tavily
|
|
130
|
+
searchStrategy: 'tavily',
|
|
131
|
+
toolCallFormat: 'openai',
|
|
132
|
+
defaultMaxTokens: 32768,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Gemini 家族(注意:googleSearch 不能与其他工具同时使用,有工具时走 Tavily) */
|
|
136
|
+
export const GEMINI_FAMILY: ModelFamilyConfig = {
|
|
137
|
+
id: 'gemini',
|
|
138
|
+
displayName: 'Gemini',
|
|
139
|
+
supportsVision: true,
|
|
140
|
+
supportsThinking: true,
|
|
141
|
+
thinkingFormat: 'thought_signature',
|
|
142
|
+
supportsNativeSearch: false, // googleSearch 与其他工具冲突,统一使用 Tavily
|
|
143
|
+
searchStrategy: 'tavily',
|
|
144
|
+
toolCallFormat: 'gemini',
|
|
145
|
+
defaultMaxTokens: 65536,
|
|
146
|
+
requiresSpecialHandling: ['thought_signature'],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** GPT 家族 */
|
|
150
|
+
export const GPT_FAMILY: ModelFamilyConfig = {
|
|
151
|
+
id: 'gpt',
|
|
152
|
+
displayName: 'GPT',
|
|
153
|
+
supportsVision: true,
|
|
154
|
+
supportsThinking: true, // GPT-5.x 支持 reasoning
|
|
155
|
+
thinkingFormat: 'reasoning',
|
|
156
|
+
supportsNativeSearch: false,
|
|
157
|
+
searchStrategy: 'tavily',
|
|
158
|
+
toolCallFormat: 'openai',
|
|
159
|
+
defaultMaxTokens: 128000,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Claude 家族(使用 Vercel AI SDK / @ai-sdk/anthropic) */
|
|
163
|
+
export const CLAUDE_FAMILY: ModelFamilyConfig = {
|
|
164
|
+
id: 'claude',
|
|
165
|
+
displayName: 'Claude',
|
|
166
|
+
supportsVision: true,
|
|
167
|
+
supportsThinking: true, // 通过 Vercel AI SDK 支持 extended thinking
|
|
168
|
+
thinkingFormat: 'reasoning',
|
|
169
|
+
supportsNativeSearch: false,
|
|
170
|
+
searchStrategy: 'tavily',
|
|
171
|
+
toolCallFormat: 'openai',
|
|
172
|
+
defaultMaxTokens: 200000,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** 家族配置映射 */
|
|
176
|
+
export const MODEL_FAMILIES: Record<ModelFamilyId, ModelFamilyConfig> = {
|
|
177
|
+
doubao: DOUBAO_FAMILY,
|
|
178
|
+
deepseek: DEEPSEEK_FAMILY,
|
|
179
|
+
qwen: QWEN_FAMILY,
|
|
180
|
+
gemini: GEMINI_FAMILY,
|
|
181
|
+
gpt: GPT_FAMILY,
|
|
182
|
+
claude: CLAUDE_FAMILY,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// ==================== 模型注册项 ====================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 模型注册项
|
|
189
|
+
*/
|
|
190
|
+
export interface ModelRegistryEntry {
|
|
191
|
+
/** 模型 ID(API 调用用) */
|
|
192
|
+
id: string;
|
|
193
|
+
/** 显示名称 */
|
|
194
|
+
displayName: string;
|
|
195
|
+
/** 所属家族 */
|
|
196
|
+
family: ModelFamilyId;
|
|
197
|
+
/** 使用的协议 */
|
|
198
|
+
protocol: ProtocolId;
|
|
199
|
+
/** 是否在前端显示 */
|
|
200
|
+
visible?: boolean;
|
|
201
|
+
/** 是否支持图片理解(优先级高于 family.supportsVision) */
|
|
202
|
+
supportsVision?: boolean;
|
|
203
|
+
/** 上下文窗口大小(如 "256K") */
|
|
204
|
+
contextWindow?: string;
|
|
205
|
+
/** 价格信息(数组,分行显示) */
|
|
206
|
+
pricing?: string[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ==================== 模型注册表 ====================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 全局模型注册表
|
|
213
|
+
*
|
|
214
|
+
* 每个模型只有一家供应商,无需分组
|
|
215
|
+
*/
|
|
216
|
+
export const MODEL_REGISTRY: ModelRegistryEntry[] = [
|
|
217
|
+
// 豆包(价格为输入<=32k档,输出价格取决于输出长度)
|
|
218
|
+
{ id: 'doubao-seed-1-6-250615', displayName: '豆包 Seed 1.6', family: 'doubao', protocol: 'ark', visible: true, supportsVision: true, contextWindow: '256K', pricing: ['输入 0.8 元/百万tokens', '输出 2-8 元/百万tokens'] },
|
|
219
|
+
{ id: 'doubao-seed-1-8-251215', displayName: '豆包 Seed 1.8', family: 'doubao', protocol: 'ark', contextWindow: '256K', pricing: ['输入 0.8 元/百万tokens', '输出 2-8 元/百万tokens'] },
|
|
220
|
+
|
|
221
|
+
// DeepSeek(价格为输入<=32k档)
|
|
222
|
+
{ id: 'deepseek-v3-2-251201', displayName: 'DeepSeek V3.2', family: 'deepseek', protocol: 'deepseek', visible: true, supportsVision: false, contextWindow: '128K', pricing: ['输入 2 元/百万tokens', '输出 3 元/百万tokens'] },
|
|
223
|
+
|
|
224
|
+
// 通义千问
|
|
225
|
+
{ id: 'qwen3-vl-plus', displayName: '通义千问 3 VL', family: 'qwen', protocol: 'qwen', visible: true, supportsVision: true, contextWindow: '128K', pricing: ['输入 1 元/百万tokens', '输出 10 元/百万tokens'] },
|
|
226
|
+
|
|
227
|
+
// Gemini
|
|
228
|
+
{ id: 'gemini-3-pro-preview', displayName: 'Gemini 3 Pro', family: 'gemini', protocol: 'gemini', visible: true, supportsVision: true, contextWindow: '1M', pricing: ['输入 1.25 元/百万tokens', '输出 10 元/百万tokens'] },
|
|
229
|
+
{ id: 'gemini-2.5-flash-preview-05-20', displayName: 'Gemini 2.5 Flash', family: 'gemini', protocol: 'gemini', contextWindow: '1M', pricing: ['输入 0.15 元/百万tokens', '输出 0.6 元/百万tokens'] },
|
|
230
|
+
{ id: 'gemini-2.5-pro-preview-05-06', displayName: 'Gemini 2.5 Pro', family: 'gemini', protocol: 'gemini', contextWindow: '1M', pricing: ['输入 1.25 元/百万tokens', '输出 10 元/百万tokens'] },
|
|
231
|
+
|
|
232
|
+
// GPT(OpenRouter,美元价格按约7.2汇率换算)
|
|
233
|
+
{ id: 'openai/gpt-5.2', displayName: 'GPT-5.2', family: 'gpt', protocol: 'openai', visible: true, supportsVision: true, contextWindow: '400K', pricing: ['输入 12.6 元/百万tokens', '输出 100.8 元/百万tokens'] },
|
|
234
|
+
|
|
235
|
+
// Claude(Vercel AI SDK,美元价格按约7.2汇率换算)
|
|
236
|
+
{ id: 'anthropic/claude-opus-4.5', displayName: 'Claude Opus 4.5', family: 'claude', protocol: 'anthropic', visible: true, supportsVision: true, contextWindow: '200K', pricing: ['输入 36 元/百万tokens', '输出 180 元/百万tokens'] },
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
// ==================== 查询辅助函数 ====================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 根据模型 ID 获取注册信息
|
|
243
|
+
*/
|
|
244
|
+
export function getModelEntry(modelId: string): ModelRegistryEntry | undefined {
|
|
245
|
+
return MODEL_REGISTRY.find(m => m.id === modelId || modelId.includes(m.id));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 根据模型 ID 获取家族配置
|
|
250
|
+
*/
|
|
251
|
+
export function getModelFamily(modelId: string): ModelFamilyConfig | undefined {
|
|
252
|
+
const entry = getModelEntry(modelId);
|
|
253
|
+
if (!entry) return undefined;
|
|
254
|
+
return MODEL_FAMILIES[entry.family];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 根据模型 ID 获取协议类型
|
|
259
|
+
*/
|
|
260
|
+
export function getModelProtocol(modelId: string): ProtocolId | undefined {
|
|
261
|
+
const entry = getModelEntry(modelId);
|
|
262
|
+
return entry?.protocol;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 获取所有可见模型(用于前端显示)
|
|
267
|
+
*/
|
|
268
|
+
export function getVisibleModels(): ModelRegistryEntry[] {
|
|
269
|
+
return MODEL_REGISTRY.filter(m => m.visible);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 根据家族 ID 获取所有模型
|
|
274
|
+
*/
|
|
275
|
+
export function getModelsByFamily(familyId: ModelFamilyId): ModelRegistryEntry[] {
|
|
276
|
+
return MODEL_REGISTRY.filter(m => m.family === familyId);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 根据协议获取所有模型
|
|
281
|
+
*/
|
|
282
|
+
export function getModelsByProtocol(protocol: ProtocolId): ModelRegistryEntry[] {
|
|
283
|
+
return MODEL_REGISTRY.filter(m => m.protocol === protocol);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 检查模型是否支持 thinking
|
|
288
|
+
*/
|
|
289
|
+
export function modelSupportsThinking(modelId: string): boolean {
|
|
290
|
+
const family = getModelFamily(modelId);
|
|
291
|
+
return family?.supportsThinking ?? false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 检查模型是否支持原生搜索
|
|
296
|
+
*/
|
|
297
|
+
export function modelSupportsNativeSearch(modelId: string): boolean {
|
|
298
|
+
const family = getModelFamily(modelId);
|
|
299
|
+
return family?.supportsNativeSearch ?? false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 检查模型是否支持图片理解
|
|
304
|
+
*/
|
|
305
|
+
export function modelSupportsVision(modelId: string): boolean {
|
|
306
|
+
const entry = getModelEntry(modelId);
|
|
307
|
+
if (!entry) return false;
|
|
308
|
+
if (typeof entry.supportsVision === 'boolean') return entry.supportsVision;
|
|
309
|
+
const family = getModelFamily(modelId);
|
|
310
|
+
return family?.supportsVision ?? false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 获取模型的搜索策略
|
|
315
|
+
*/
|
|
316
|
+
export function getModelSearchStrategy(modelId: string): SearchStrategy {
|
|
317
|
+
const family = getModelFamily(modelId);
|
|
318
|
+
return family?.searchStrategy ?? 'tavily';
|
|
319
|
+
}
|
|
320
|
+
|