@huyooo/ai-chat-core 0.2.45 → 0.3.2
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/adapter/index.d.ts +11 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/model-adapter.d.ts +25 -0
- package/dist/adapter/model-adapter.d.ts.map +1 -0
- package/dist/adapter/model-options.d.ts +53 -0
- package/dist/adapter/model-options.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +28 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/chat-runtime.d.ts +96 -0
- package/dist/chat-runtime.d.ts.map +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/events.d.ts +605 -1
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +1 -1
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.d.ts.map +1 -0
- package/dist/extension/types.d.ts +46 -0
- package/dist/extension/types.d.ts.map +1 -0
- package/dist/families/index.d.ts +11 -0
- package/dist/families/index.d.ts.map +1 -0
- package/dist/families/presets.d.ts +31 -0
- package/dist/families/presets.d.ts.map +1 -0
- package/dist/families/resolver.d.ts +11 -0
- package/dist/families/resolver.d.ts.map +1 -0
- package/dist/families/types.d.ts +29 -0
- package/dist/families/types.d.ts.map +1 -0
- package/dist/governance/command-safety.d.ts +34 -0
- package/dist/governance/command-safety.d.ts.map +1 -0
- package/dist/governance/governance.d.ts +19 -0
- package/dist/governance/governance.d.ts.map +1 -0
- package/dist/governance/index.d.ts +12 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/types.d.ts +29 -0
- package/dist/governance/types.d.ts.map +1 -0
- package/dist/index.d.ts +72 -804
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -1
- package/dist/internal/management-args.d.ts +13 -0
- package/dist/internal/management-args.d.ts.map +1 -0
- package/dist/internal/management-results.d.ts +21 -0
- package/dist/internal/management-results.d.ts.map +1 -0
- package/dist/llm-config.d.ts +108 -0
- package/dist/llm-config.d.ts.map +1 -0
- package/dist/logger/core.d.ts +31 -0
- package/dist/logger/core.d.ts.map +1 -0
- package/dist/logger/index.d.ts +9 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/orchestrator/compression-handler.d.ts +29 -0
- package/dist/orchestrator/compression-handler.d.ts.map +1 -0
- package/dist/orchestrator/context-compressor.d.ts +51 -0
- package/dist/orchestrator/context-compressor.d.ts.map +1 -0
- package/dist/orchestrator/context-summarizer.d.ts +41 -0
- package/dist/orchestrator/context-summarizer.d.ts.map +1 -0
- package/dist/orchestrator/index.d.ts +12 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/orchestrator.d.ts +46 -0
- package/dist/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/types.d.ts +58 -0
- package/dist/orchestrator/types.d.ts.map +1 -0
- package/dist/parts/index.d.ts +13 -0
- package/dist/parts/index.d.ts.map +1 -0
- package/dist/parts/registry.d.ts +11 -0
- package/dist/parts/registry.d.ts.map +1 -0
- package/dist/parts/summaries.d.ts +9 -0
- package/dist/parts/summaries.d.ts.map +1 -0
- package/dist/parts/types.d.ts +61 -0
- package/dist/parts/types.d.ts.map +1 -0
- package/dist/platform.d.ts +17 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +1 -0
- package/dist/protocols/anthropic.d.ts +20 -0
- package/dist/protocols/anthropic.d.ts.map +1 -0
- package/dist/protocols/ark.d.ts +36 -0
- package/dist/protocols/ark.d.ts.map +1 -0
- package/dist/protocols/deepseek.d.ts +24 -0
- package/dist/protocols/deepseek.d.ts.map +1 -0
- package/dist/protocols/error-utils.d.ts +14 -0
- package/dist/protocols/error-utils.d.ts.map +1 -0
- package/dist/protocols/gemini.d.ts +24 -0
- package/dist/protocols/gemini.d.ts.map +1 -0
- package/dist/protocols/glm.d.ts +20 -0
- package/dist/protocols/glm.d.ts.map +1 -0
- package/dist/protocols/grok.d.ts +20 -0
- package/dist/protocols/grok.d.ts.map +1 -0
- package/dist/protocols/index.d.ts +31 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/minimax.d.ts +38 -0
- package/dist/protocols/minimax.d.ts.map +1 -0
- package/dist/protocols/moonshot.d.ts +20 -0
- package/dist/protocols/moonshot.d.ts.map +1 -0
- package/dist/protocols/openai-sse.d.ts +33 -0
- package/dist/protocols/openai-sse.d.ts.map +1 -0
- package/dist/protocols/openai.d.ts +19 -0
- package/dist/protocols/openai.d.ts.map +1 -0
- package/dist/protocols/qwen.d.ts +26 -0
- package/dist/protocols/qwen.d.ts.map +1 -0
- package/dist/protocols/responses-sse.d.ts +30 -0
- package/dist/protocols/responses-sse.d.ts.map +1 -0
- package/dist/protocols/sse-reader.d.ts +23 -0
- package/dist/protocols/sse-reader.d.ts.map +1 -0
- package/dist/protocols/tool-arguments.d.ts +8 -0
- package/dist/protocols/tool-arguments.d.ts.map +1 -0
- package/dist/protocols/types.d.ts +148 -0
- package/dist/protocols/types.d.ts.map +1 -0
- package/dist/protocols/vercel-gateway.d.ts +15 -0
- package/dist/protocols/vercel-gateway.d.ts.map +1 -0
- package/dist/runtime.d.ts +151 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1 -0
- package/dist/skills/index.d.ts +14 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/management/admin.d.ts +10 -0
- package/dist/skills/management/admin.d.ts.map +1 -0
- package/dist/skills/management/index.d.ts +11 -0
- package/dist/skills/management/index.d.ts.map +1 -0
- package/dist/skills/management/inputs.d.ts +44 -0
- package/dist/skills/management/inputs.d.ts.map +1 -0
- package/dist/skills/management/operations.d.ts +78 -0
- package/dist/skills/management/operations.d.ts.map +1 -0
- package/dist/skills/management/types.d.ts +70 -0
- package/dist/skills/management/types.d.ts.map +1 -0
- package/dist/skills/registry.d.ts +37 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/summaries.d.ts +9 -0
- package/dist/skills/summaries.d.ts.map +1 -0
- package/dist/skills/types.d.ts +61 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/test-utils/mock-sse.d.ts +13 -0
- package/dist/test-utils/mock-sse.d.ts.map +1 -0
- package/dist/tool-manager/define-tool.d.ts +35 -0
- package/dist/tool-manager/define-tool.d.ts.map +1 -0
- package/dist/tool-manager/formats.d.ts +46 -0
- package/dist/tool-manager/formats.d.ts.map +1 -0
- package/dist/tool-manager/identity.d.ts +18 -0
- package/dist/tool-manager/identity.d.ts.map +1 -0
- package/dist/tool-manager/in-process-provider.d.ts +15 -0
- package/dist/tool-manager/in-process-provider.d.ts.map +1 -0
- package/dist/tool-manager/index.d.ts +18 -0
- package/dist/tool-manager/index.d.ts.map +1 -0
- package/dist/tool-manager/manager.d.ts +18 -0
- package/dist/tool-manager/manager.d.ts.map +1 -0
- package/dist/tool-manager/mcp-provider.d.ts +21 -0
- package/dist/tool-manager/mcp-provider.d.ts.map +1 -0
- package/dist/tool-manager/summaries.d.ts +39 -0
- package/dist/tool-manager/summaries.d.ts.map +1 -0
- package/dist/tool-manager/types.d.ts +314 -0
- package/dist/tool-manager/types.d.ts.map +1 -0
- package/dist/types.d.ts +663 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +26 -15
- package/src/adapter/index.ts +25 -0
- package/src/adapter/model-adapter.ts +196 -0
- package/src/adapter/model-options.ts +143 -0
- package/src/adapter/types.ts +41 -0
- package/src/chat-runtime.ts +515 -0
- package/src/constants.ts +9 -102
- package/src/events.ts +364 -150
- package/src/extension/index.ts +24 -0
- package/src/extension/types.ts +49 -0
- package/src/families/index.ts +28 -0
- package/src/families/presets.ts +124 -0
- package/src/families/resolver.ts +22 -0
- package/src/families/types.ts +55 -0
- package/src/governance/command-safety.ts +224 -0
- package/src/governance/governance.ts +125 -0
- package/src/governance/index.ts +38 -0
- package/src/governance/types.ts +44 -0
- package/src/index.ts +250 -145
- package/src/internal/management-args.ts +39 -0
- package/src/internal/management-results.ts +60 -0
- package/src/llm-config.ts +137 -0
- package/src/logger/core.ts +96 -0
- package/src/logger/index.ts +8 -0
- package/src/orchestrator/compression-handler.ts +137 -0
- package/src/{providers → orchestrator}/context-compressor.ts +79 -47
- package/src/orchestrator/context-summarizer.ts +123 -0
- package/src/orchestrator/index.ts +20 -0
- package/src/orchestrator/orchestrator.ts +1002 -0
- package/src/orchestrator/types.ts +70 -0
- package/src/parts/index.ts +20 -0
- package/src/parts/registry.ts +95 -0
- package/src/parts/summaries.ts +40 -0
- package/src/parts/types.ts +63 -0
- package/src/platform.ts +73 -0
- package/src/protocols/anthropic.ts +377 -0
- package/src/protocols/ark.ts +300 -0
- package/src/protocols/deepseek.ts +192 -0
- package/src/{providers/protocols → protocols}/error-utils.ts +17 -20
- package/src/protocols/gemini.ts +352 -0
- package/src/protocols/glm.ts +212 -0
- package/src/protocols/grok.ts +98 -0
- package/src/protocols/index.ts +48 -0
- package/src/protocols/minimax.ts +308 -0
- package/src/protocols/moonshot.ts +186 -0
- package/src/protocols/openai-sse.ts +156 -0
- package/src/protocols/openai.ts +97 -0
- package/src/protocols/qwen.ts +358 -0
- package/src/protocols/responses-sse.ts +224 -0
- package/src/protocols/sse-reader.ts +54 -0
- package/src/protocols/tool-arguments.ts +32 -0
- package/src/{providers/protocols → protocols}/types.ts +46 -37
- package/src/protocols/vercel-gateway.ts +391 -0
- package/src/runtime.ts +167 -0
- package/src/skills/index.ts +29 -0
- package/src/skills/management/admin.ts +170 -0
- package/src/skills/management/index.ts +27 -0
- package/src/skills/management/inputs.ts +79 -0
- package/src/skills/management/operations.ts +256 -0
- package/src/skills/management/types.ts +57 -0
- package/src/skills/registry.ts +120 -0
- package/src/skills/summaries.ts +48 -0
- package/src/skills/types.ts +65 -0
- package/src/test-utils/mock-sse.ts +3 -3
- package/src/tool-manager/define-tool.ts +201 -0
- package/src/tool-manager/formats.ts +146 -0
- package/src/tool-manager/identity.ts +80 -0
- package/src/tool-manager/in-process-provider.ts +164 -0
- package/src/tool-manager/index.ts +63 -0
- package/src/tool-manager/manager.ts +562 -0
- package/src/tool-manager/mcp-provider.ts +509 -0
- package/src/tool-manager/summaries.ts +136 -0
- package/src/tool-manager/types.ts +389 -0
- package/src/types.ts +750 -191
- package/dist/events-CU5D5ray.d.ts +0 -1128
- package/src/agent.ts +0 -409
- package/src/internal/update-plan.ts +0 -2
- package/src/internal/web-search.ts +0 -77
- package/src/mcp/client-manager.ts +0 -302
- package/src/mcp/index.ts +0 -2
- package/src/mcp/types.ts +0 -43
- package/src/providers/context-summarizer.ts +0 -70
- package/src/providers/index.ts +0 -125
- package/src/providers/model-registry.ts +0 -466
- package/src/providers/orchestrator.ts +0 -839
- package/src/providers/protocols/anthropic.ts +0 -406
- package/src/providers/protocols/ark.ts +0 -362
- package/src/providers/protocols/deepseek.ts +0 -344
- package/src/providers/protocols/gemini.ts +0 -350
- package/src/providers/protocols/index.ts +0 -36
- package/src/providers/protocols/openai.ts +0 -420
- package/src/providers/protocols/qwen.ts +0 -315
- package/src/providers/types.ts +0 -264
- package/src/providers/unified-adapter.ts +0 -367
- package/src/router.ts +0 -72
- package/src/tools.ts +0 -162
- package/src/utils.ts +0 -86
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Protocol(原生 Chat Completions API)
|
|
3
|
+
*
|
|
4
|
+
* 基于 OpenAI 兼容格式:
|
|
5
|
+
* - 端点:https://api.deepseek.com/chat/completions
|
|
6
|
+
* - thinking:{ type: "enabled" }
|
|
7
|
+
* - SSE delta:reasoning_content(思考)+ content(正文)
|
|
8
|
+
* - 工具调用循环内需回传 reasoning_content
|
|
9
|
+
* - finish_reason 额外值:content_filter / insufficient_system_resource
|
|
10
|
+
*
|
|
11
|
+
* 文档:https://api-docs.deepseek.com/zh-cn/api/create-chat-completion
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
Protocol,
|
|
16
|
+
ProtocolConfig,
|
|
17
|
+
ProtocolMessage,
|
|
18
|
+
ProtocolToolDefinition,
|
|
19
|
+
ProtocolRequestOptions,
|
|
20
|
+
RawEvent,
|
|
21
|
+
} from './types';
|
|
22
|
+
import { createModuleLogger } from '../logger';
|
|
23
|
+
import { friendlyHttpError } from './error-utils';
|
|
24
|
+
import { readSSEJsonStream } from './sse-reader';
|
|
25
|
+
import { mapOpenAIStream } from './openai-sse';
|
|
26
|
+
import type { OpenAIStreamConfig } from './openai-sse';
|
|
27
|
+
|
|
28
|
+
const logger = createModuleLogger('DeepSeekProtocol');
|
|
29
|
+
|
|
30
|
+
const DEFAULT_DEEPSEEK_URL = 'https://api.deepseek.com';
|
|
31
|
+
|
|
32
|
+
const DEEPSEEK_SSE_CONFIG: OpenAIStreamConfig = {
|
|
33
|
+
thinkingField: (delta) => delta.reasoning_content as string | undefined,
|
|
34
|
+
errorFinishReasons: ['content_filter', 'insufficient_system_resource'],
|
|
35
|
+
protocolName: 'DeepSeek',
|
|
36
|
+
parseUsage: (usage) => ({
|
|
37
|
+
promptTokens: (usage.prompt_tokens as number) ?? 0,
|
|
38
|
+
completionTokens: (usage.completion_tokens as number) ?? 0,
|
|
39
|
+
totalTokens: (usage.total_tokens as number) ?? 0,
|
|
40
|
+
reasoningTokens: (usage.completion_tokens_details as Record<string, number> | undefined)?.reasoning_tokens ?? 0,
|
|
41
|
+
cachedTokens: (usage.prompt_cache_hit_tokens as number) ?? 0,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export class DeepSeekProtocol implements Protocol {
|
|
46
|
+
readonly name = 'deepseek';
|
|
47
|
+
|
|
48
|
+
private apiKey: string;
|
|
49
|
+
private apiUrl: string;
|
|
50
|
+
|
|
51
|
+
constructor(config: ProtocolConfig) {
|
|
52
|
+
this.apiKey = config.apiKey;
|
|
53
|
+
this.apiUrl = config.apiUrl ?? DEFAULT_DEEPSEEK_URL;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async *stream(
|
|
57
|
+
messages: ProtocolMessage[],
|
|
58
|
+
tools: ProtocolToolDefinition[],
|
|
59
|
+
options: ProtocolRequestOptions
|
|
60
|
+
): AsyncGenerator<RawEvent> {
|
|
61
|
+
const requestBody = this.buildRequestBody(messages, tools, options);
|
|
62
|
+
const url = `${this.apiUrl}/chat/completions`;
|
|
63
|
+
|
|
64
|
+
logger.debug({
|
|
65
|
+
url,
|
|
66
|
+
model: options.model,
|
|
67
|
+
enableThinking: options.enableThinking,
|
|
68
|
+
toolsCount: tools.length,
|
|
69
|
+
}, '发送 DeepSeek 请求');
|
|
70
|
+
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify(requestBody),
|
|
78
|
+
signal: options.signal,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const errorText = await response.text();
|
|
83
|
+
logger.error({ status: response.status, body: errorText.slice(0, 500) }, 'DeepSeek API 错误');
|
|
84
|
+
yield { type: 'error', error: friendlyHttpError(response.status, errorText, 'DeepSeek') };
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const reader = response.body?.getReader();
|
|
89
|
+
if (!reader) {
|
|
90
|
+
yield { type: 'error', error: '无法获取响应流' };
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
yield* mapOpenAIStream(readSSEJsonStream(reader), DEEPSEEK_SSE_CONFIG);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private buildRequestBody(
|
|
98
|
+
messages: ProtocolMessage[],
|
|
99
|
+
tools: ProtocolToolDefinition[],
|
|
100
|
+
options: ProtocolRequestOptions
|
|
101
|
+
): Record<string, unknown> {
|
|
102
|
+
const convertedMessages = this.convertMessages(messages);
|
|
103
|
+
|
|
104
|
+
const body: Record<string, unknown> = {
|
|
105
|
+
model: options.model,
|
|
106
|
+
messages: convertedMessages,
|
|
107
|
+
stream: true,
|
|
108
|
+
stream_options: { include_usage: true },
|
|
109
|
+
max_tokens: options.maxOutputTokens,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// DeepSeek thinking 参数格式与 GLM 一致
|
|
113
|
+
if (options.enableThinking) {
|
|
114
|
+
body.thinking = { type: 'enabled' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (tools.length > 0) {
|
|
118
|
+
body.tools = tools.map(t => ({
|
|
119
|
+
type: 'function',
|
|
120
|
+
function: {
|
|
121
|
+
name: t.name,
|
|
122
|
+
description: t.description,
|
|
123
|
+
parameters: t.parameters,
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return body;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private convertMessages(messages: ProtocolMessage[]): unknown[] {
|
|
132
|
+
const result: unknown[] = [];
|
|
133
|
+
|
|
134
|
+
for (const msg of messages) {
|
|
135
|
+
switch (msg.role) {
|
|
136
|
+
case 'system':
|
|
137
|
+
result.push({ role: 'system', content: msg.content });
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'user': {
|
|
141
|
+
const textContent = msg.content || (msg.images?.length ? '请分析这张图片' : '');
|
|
142
|
+
if (msg.images?.length) {
|
|
143
|
+
const content: unknown[] = [{ type: 'text', text: textContent }];
|
|
144
|
+
for (const img of msg.images) {
|
|
145
|
+
content.push({
|
|
146
|
+
type: 'image_url',
|
|
147
|
+
image_url: { url: img.startsWith('data:') ? img : `data:image/jpeg;base64,${img}` },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
result.push({ role: 'user', content });
|
|
151
|
+
} else {
|
|
152
|
+
result.push({ role: 'user', content: textContent });
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'assistant':
|
|
158
|
+
if (msg.toolCalls?.length) {
|
|
159
|
+
// 工具调用循环内需回传 reasoning_content 给 API
|
|
160
|
+
result.push({
|
|
161
|
+
role: 'assistant',
|
|
162
|
+
content: msg.content || null,
|
|
163
|
+
...(msg.thinkingContent ? { reasoning_content: msg.thinkingContent } : {}),
|
|
164
|
+
tool_calls: msg.toolCalls.map(tc => ({
|
|
165
|
+
id: tc.id,
|
|
166
|
+
type: 'function',
|
|
167
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
168
|
+
})),
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
result.push({ role: 'assistant', content: msg.content });
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case 'tool':
|
|
176
|
+
result.push({
|
|
177
|
+
role: 'tool',
|
|
178
|
+
tool_call_id: msg.toolCallId,
|
|
179
|
+
content: msg.content,
|
|
180
|
+
});
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createDeepSeekProtocol(config: ProtocolConfig): DeepSeekProtocol {
|
|
191
|
+
return new DeepSeekProtocol(config);
|
|
192
|
+
}
|
|
@@ -26,36 +26,30 @@ const STATUS_MESSAGES: Record<number, string> = {
|
|
|
26
26
|
* @param providerName 供应商名称(如 "Gemini"、"OpenAI")
|
|
27
27
|
*/
|
|
28
28
|
export function friendlyHttpError(status: number, body: string, providerName: string): string {
|
|
29
|
-
// 1. 尝试从 JSON body 提取 message
|
|
30
29
|
const parsed = tryParseErrorBody(body);
|
|
31
30
|
const apiMessage = parsed?.message;
|
|
32
|
-
|
|
33
|
-
// 2. 已知 status → 友好中文
|
|
34
31
|
const statusMessage = STATUS_MESSAGES[status];
|
|
35
|
-
if (statusMessage) {
|
|
36
|
-
// 如果有 API 返回的具体消息且不太长,附在后面
|
|
37
|
-
if (apiMessage && apiMessage.length <= 100) {
|
|
38
|
-
return `${statusMessage}(${apiMessage})`;
|
|
39
|
-
}
|
|
40
|
-
return statusMessage;
|
|
41
|
-
}
|
|
42
32
|
|
|
43
|
-
//
|
|
33
|
+
// API 返回了具体错误信息:始终附加,截断过长内容防止 UI 溢出
|
|
44
34
|
if (apiMessage) {
|
|
45
|
-
const
|
|
46
|
-
|
|
35
|
+
const trimmed = apiMessage.length > 300 ? apiMessage.slice(0, 300) + '...' : apiMessage;
|
|
36
|
+
const prefix = statusMessage ?? `${providerName} 错误 (${status})`;
|
|
37
|
+
return `${prefix}(${trimmed})`;
|
|
47
38
|
}
|
|
48
39
|
|
|
49
|
-
//
|
|
50
|
-
return `${providerName} 错误 (${status})`;
|
|
40
|
+
// 无 API 消息:已知 status 用友好文案,未知 status 兜底
|
|
41
|
+
return statusMessage ?? `${providerName} 错误 (${status})`;
|
|
51
42
|
}
|
|
52
43
|
|
|
53
44
|
/**
|
|
54
|
-
* 尝试从 JSON
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* { "
|
|
58
|
-
* { "
|
|
45
|
+
* 尝试从 JSON 响应体中提取错误消息
|
|
46
|
+
*
|
|
47
|
+
* 各供应商实测错误格式(2026-03 验证):
|
|
48
|
+
* { "error": { "message": "..." } } — OpenAI / DeepSeek / Gemini / Claude / GLM / ARK / Moonshot
|
|
49
|
+
* { "error": "字符串" } — Grok(error 直接是字符串,code 在顶层)
|
|
50
|
+
* { "error_msg": "..." } — ARK 部分旧接口
|
|
51
|
+
* { "message": "..." } — Qwen / DashScope(顶层 message + code)
|
|
52
|
+
* HTML / 非 JSON — MiniMax 某些错误,返回 null 用 status 兜底
|
|
59
53
|
*/
|
|
60
54
|
function tryParseErrorBody(body: string): { message?: string; type?: string } | null {
|
|
61
55
|
try {
|
|
@@ -63,6 +57,9 @@ function tryParseErrorBody(body: string): { message?: string; type?: string } |
|
|
|
63
57
|
if (json?.error?.message) {
|
|
64
58
|
return { message: json.error.message, type: json.error.type };
|
|
65
59
|
}
|
|
60
|
+
if (typeof json?.error === 'string') {
|
|
61
|
+
return { message: json.error, type: json.code };
|
|
62
|
+
}
|
|
66
63
|
if (typeof json?.error_msg === 'string') {
|
|
67
64
|
return { message: json.error_msg };
|
|
68
65
|
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Protocol(Google 原生 API)
|
|
3
|
+
*
|
|
4
|
+
* 支持两种部署模式:
|
|
5
|
+
* - 直连 Google API:apiUrl 含 googleapis.com,Key 通过 URL 参数传递
|
|
6
|
+
* - CF Worker 代理:apiUrl 指向代理地址(如 https://proxy/google-ai-studio/v1beta),
|
|
7
|
+
* Key 通过 Authorization 头传递,代理负责注入厂商 Key
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
Protocol,
|
|
12
|
+
ProtocolConfig,
|
|
13
|
+
ProtocolMessage,
|
|
14
|
+
ProtocolToolDefinition,
|
|
15
|
+
ProtocolRequestOptions,
|
|
16
|
+
RawEvent,
|
|
17
|
+
RawOutputPart,
|
|
18
|
+
RawToolCall,
|
|
19
|
+
} from './types';
|
|
20
|
+
import { createModuleLogger } from '../logger';
|
|
21
|
+
import { friendlyHttpError } from './error-utils';
|
|
22
|
+
import { readSSEJsonStream } from './sse-reader';
|
|
23
|
+
import { parseProtocolToolArguments } from './tool-arguments';
|
|
24
|
+
|
|
25
|
+
const logger = createModuleLogger('GeminiProtocol');
|
|
26
|
+
|
|
27
|
+
const DEFAULT_GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 历史中无 thoughtSignature 时使用的占位值(跳过 Gemini 校验)。
|
|
31
|
+
* 见 https://ai.google.dev/gemini-api/docs/thought-signatures
|
|
32
|
+
*/
|
|
33
|
+
const THOUGHT_SIGNATURE_DUMMY = 'skip_thought_signature_validator';
|
|
34
|
+
|
|
35
|
+
export class GeminiProtocol implements Protocol {
|
|
36
|
+
readonly name = 'gemini';
|
|
37
|
+
|
|
38
|
+
private apiKey: string;
|
|
39
|
+
private apiUrl: string;
|
|
40
|
+
|
|
41
|
+
constructor(config: ProtocolConfig) {
|
|
42
|
+
this.apiKey = config.apiKey;
|
|
43
|
+
this.apiUrl = config.apiUrl ?? DEFAULT_GEMINI_URL;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async *stream(
|
|
47
|
+
messages: ProtocolMessage[],
|
|
48
|
+
tools: ProtocolToolDefinition[],
|
|
49
|
+
options: ProtocolRequestOptions,
|
|
50
|
+
): AsyncGenerator<RawEvent> {
|
|
51
|
+
const requestBody = buildRequestBody(messages, tools, options);
|
|
52
|
+
|
|
53
|
+
// 直连 Google:Key 走 URL 参数;代理模式:Key 走 Authorization 头
|
|
54
|
+
const isDirect = this.apiUrl.includes('googleapis.com');
|
|
55
|
+
const url = isDirect
|
|
56
|
+
? `${this.apiUrl}/models/${options.model}:streamGenerateContent?key=${this.apiKey}&alt=sse`
|
|
57
|
+
: `${this.apiUrl}/models/${options.model}:streamGenerateContent?alt=sse`;
|
|
58
|
+
|
|
59
|
+
logger.debug({
|
|
60
|
+
url: url.replace(this.apiKey, '***'),
|
|
61
|
+
model: options.model,
|
|
62
|
+
enableThinking: options.enableThinking,
|
|
63
|
+
toolsCount: tools.length,
|
|
64
|
+
}, 'Gemini 请求');
|
|
65
|
+
|
|
66
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
67
|
+
if (!isDirect) {
|
|
68
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers,
|
|
74
|
+
body: JSON.stringify(requestBody),
|
|
75
|
+
signal: options.signal,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorText = await response.text();
|
|
80
|
+
logger.error({ status: response.status, body: errorText.slice(0, 500) }, 'Gemini API 错误');
|
|
81
|
+
yield { type: 'error', error: friendlyHttpError(response.status, errorText, 'Gemini') };
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const reader = response.body?.getReader();
|
|
86
|
+
if (!reader) {
|
|
87
|
+
yield { type: 'error', error: '无法获取响应流' };
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
yield* parseGeminiSSE(reader);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ==================== 请求体构建 ====================
|
|
96
|
+
|
|
97
|
+
function buildRequestBody(
|
|
98
|
+
messages: ProtocolMessage[],
|
|
99
|
+
tools: ProtocolToolDefinition[],
|
|
100
|
+
options: ProtocolRequestOptions,
|
|
101
|
+
): Record<string, unknown> {
|
|
102
|
+
const { systemInstruction, contents } = convertMessages(messages);
|
|
103
|
+
|
|
104
|
+
const body: Record<string, unknown> = {
|
|
105
|
+
contents,
|
|
106
|
+
generationConfig: {
|
|
107
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (systemInstruction) {
|
|
112
|
+
body.systemInstruction = systemInstruction;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options.enableThinking) {
|
|
116
|
+
(body.generationConfig as Record<string, unknown>).thinkingConfig = {
|
|
117
|
+
thinkingBudget: 24576,
|
|
118
|
+
includeThoughts: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (tools.length > 0) {
|
|
123
|
+
const byName = new Map<string, unknown>();
|
|
124
|
+
for (const t of tools) {
|
|
125
|
+
if (!byName.has(t.name)) {
|
|
126
|
+
byName.set(t.name, {
|
|
127
|
+
name: t.name,
|
|
128
|
+
description: t.description,
|
|
129
|
+
// 使用 parametersJsonSchema 而非 parameters:
|
|
130
|
+
// parameters 走 protobuf Schema 校验,字段受限(不支持 additionalProperties 等);
|
|
131
|
+
// parametersJsonSchema 直接接受原始 JSON Schema,绕过 proto 限制。
|
|
132
|
+
// 二者互斥,见 https://ai.google.dev/api/caching#FunctionDeclaration
|
|
133
|
+
parametersJsonSchema: stripUnsupportedRefs(t.parameters),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
body.tools = [{ functionDeclarations: Array.from(byName.values()) }];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return body;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ==================== Schema 兼容 ====================
|
|
144
|
+
//
|
|
145
|
+
// 使用 parametersJsonSchema 而非 parameters 传递工具参数 schema。
|
|
146
|
+
//
|
|
147
|
+
// FunctionDeclaration 有两种互斥方式传参数 schema:
|
|
148
|
+
// 1. parameters — 走 protobuf Schema 对象,字段严格受限
|
|
149
|
+
// (不支持 additionalProperties、prefixItems、$ref/$defs 等)
|
|
150
|
+
// 2. parametersJsonSchema — 直接接受原始 JSON Schema(google.protobuf.Value)
|
|
151
|
+
// 绕过 proto 限制,支持 additionalProperties、oneOf、allOf、const 等标准特性
|
|
152
|
+
//
|
|
153
|
+
// 我们选方案 2,TypeBox 输出的标准 JSON Schema 几乎可以直接透传。
|
|
154
|
+
//
|
|
155
|
+
// 唯一已知限制:$ref 不被支持(googleapis/python-genai#1122)
|
|
156
|
+
// 副作用:$ref/$defs 被递归剥离 → Type.Ref() / Type.Recursive() 的引用丢失,
|
|
157
|
+
// 被引用的位置变为无约束的空 schema,AI 靠 description 补偿。
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 递归剥离 $ref/$defs——Gemini parametersJsonSchema 唯一不支持的 JSON Schema 特性。
|
|
161
|
+
* 其它所有标准 JSON Schema 关键字(const、oneOf、allOf、additionalProperties、
|
|
162
|
+
* prefixItems 等)均直接透传,无需转换。
|
|
163
|
+
*/
|
|
164
|
+
export function stripUnsupportedRefs(schema: unknown): unknown {
|
|
165
|
+
if (schema == null || typeof schema !== 'object') return schema;
|
|
166
|
+
if (Array.isArray(schema)) return schema.map(stripUnsupportedRefs);
|
|
167
|
+
|
|
168
|
+
const obj = schema as Record<string, unknown>;
|
|
169
|
+
const result: Record<string, unknown> = {};
|
|
170
|
+
|
|
171
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
172
|
+
if (key === '$ref' || key === '$defs') continue;
|
|
173
|
+
result[key] = stripUnsupportedRefs(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ==================== 消息转换 ====================
|
|
180
|
+
|
|
181
|
+
function convertMessages(messages: ProtocolMessage[]): {
|
|
182
|
+
systemInstruction?: { parts: { text: string }[] };
|
|
183
|
+
contents: unknown[];
|
|
184
|
+
} {
|
|
185
|
+
let systemInstruction: { parts: { text: string }[] } | undefined;
|
|
186
|
+
const contents: unknown[] = [];
|
|
187
|
+
|
|
188
|
+
for (const msg of messages) {
|
|
189
|
+
switch (msg.role) {
|
|
190
|
+
case 'system':
|
|
191
|
+
systemInstruction = { parts: [{ text: msg.content }] };
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'user': {
|
|
195
|
+
const hasMedia = Boolean(msg.images?.length || msg.attachments?.length);
|
|
196
|
+
const textContent = msg.content || (hasMedia ? '请分析这个媒体文件' : '');
|
|
197
|
+
const parts: unknown[] = [{ text: textContent }];
|
|
198
|
+
if (msg.images?.length) {
|
|
199
|
+
for (const img of msg.images) {
|
|
200
|
+
if (img.startsWith('data:')) {
|
|
201
|
+
const match = img.match(/^data:([^;]+);base64,(.+)$/);
|
|
202
|
+
if (match) {
|
|
203
|
+
parts.push({ inlineData: { mimeType: match[1], data: match[2] } });
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
parts.push({ inlineData: { mimeType: 'image/jpeg', data: img } });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (msg.attachments?.length) {
|
|
211
|
+
for (const attachment of msg.attachments) {
|
|
212
|
+
parts.push({ fileData: { mimeType: attachment.mimeType, fileUri: attachment.fileUri } });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
contents.push({ role: 'user', parts });
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case 'assistant':
|
|
220
|
+
if (msg.toolCalls?.length) {
|
|
221
|
+
const parts: unknown[] = [];
|
|
222
|
+
for (const tc of msg.toolCalls) {
|
|
223
|
+
const funcPart: Record<string, unknown> = {
|
|
224
|
+
functionCall: {
|
|
225
|
+
name: tc.name,
|
|
226
|
+
args: parseProtocolToolArguments(tc.arguments, {
|
|
227
|
+
protocol: 'gemini',
|
|
228
|
+
toolCallId: tc.id,
|
|
229
|
+
toolName: tc.name,
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
// Gemini 3:每个 functionCall part 必须带 thoughtSignature,否则 400
|
|
234
|
+
funcPart.thoughtSignature = tc.thoughtSignature ?? THOUGHT_SIGNATURE_DUMMY;
|
|
235
|
+
parts.push(funcPart);
|
|
236
|
+
}
|
|
237
|
+
contents.push({ role: 'model', parts });
|
|
238
|
+
} else {
|
|
239
|
+
contents.push({ role: 'model', parts: [{ text: msg.content }] });
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
case 'tool':
|
|
244
|
+
contents.push({
|
|
245
|
+
role: 'user',
|
|
246
|
+
parts: [{
|
|
247
|
+
functionResponse: {
|
|
248
|
+
name: msg.toolName || 'unknown',
|
|
249
|
+
response: { result: msg.content },
|
|
250
|
+
},
|
|
251
|
+
}],
|
|
252
|
+
});
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { systemInstruction, contents };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ==================== SSE 流解析 ====================
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 解析 Gemini SSE 流
|
|
264
|
+
*
|
|
265
|
+
* Layer 1 负责 bytes → JSON,此处做 Gemini 原生格式的语义映射。
|
|
266
|
+
*/
|
|
267
|
+
async function* parseGeminiSSE(
|
|
268
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
269
|
+
): AsyncGenerator<RawEvent> {
|
|
270
|
+
const pendingToolCalls = new Map<string, RawToolCall>();
|
|
271
|
+
const outputParts: RawOutputPart[] = [];
|
|
272
|
+
let textStarted = false;
|
|
273
|
+
let toolCallIndex = 0;
|
|
274
|
+
|
|
275
|
+
for await (const json of readSSEJsonStream(reader)) {
|
|
276
|
+
const candidates = json.candidates as Array<Record<string, unknown>> | undefined;
|
|
277
|
+
const candidate = candidates?.[0];
|
|
278
|
+
const content = candidate?.content as Record<string, unknown> | undefined;
|
|
279
|
+
const parts = content?.parts as Array<Record<string, unknown>> | undefined;
|
|
280
|
+
if (!parts) continue;
|
|
281
|
+
|
|
282
|
+
for (const part of parts) {
|
|
283
|
+
if (part.text && part.thought === true) {
|
|
284
|
+
yield { type: 'thinking_delta', delta: part.text as string };
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (part.text) {
|
|
289
|
+
outputParts.push({ type: 'text', text: part.text as string });
|
|
290
|
+
if (!textStarted) {
|
|
291
|
+
textStarted = true;
|
|
292
|
+
yield { type: 'thinking_done' };
|
|
293
|
+
}
|
|
294
|
+
yield { type: 'text_delta', delta: part.text as string };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (part.inlineData) {
|
|
298
|
+
const inlineData = part.inlineData as Record<string, unknown>;
|
|
299
|
+
const mimeType = inlineData.mimeType;
|
|
300
|
+
const data = inlineData.data;
|
|
301
|
+
if (typeof mimeType === 'string' && typeof data === 'string') {
|
|
302
|
+
outputParts.push({ type: 'inline_data', mimeType, data });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (part.functionCall) {
|
|
307
|
+
const fc = part.functionCall as Record<string, unknown>;
|
|
308
|
+
const callId = `gemini-${toolCallIndex++}`;
|
|
309
|
+
const toolCall: RawToolCall = {
|
|
310
|
+
id: callId,
|
|
311
|
+
name: fc.name as string,
|
|
312
|
+
arguments: JSON.stringify(fc.args || {}),
|
|
313
|
+
};
|
|
314
|
+
if (part.thoughtSignature) {
|
|
315
|
+
toolCall.thoughtSignature = part.thoughtSignature as string;
|
|
316
|
+
}
|
|
317
|
+
pendingToolCalls.set(callId, toolCall);
|
|
318
|
+
yield { type: 'tool_call_start', toolCall: { id: callId, name: toolCall.name } };
|
|
319
|
+
yield { type: 'tool_call_done', toolCall };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (candidate?.finishReason) {
|
|
324
|
+
const meta = json.usageMetadata as Record<string, number> | undefined;
|
|
325
|
+
const usage = meta ? {
|
|
326
|
+
promptTokens: meta.promptTokenCount ?? 0,
|
|
327
|
+
completionTokens: meta.candidatesTokenCount ?? 0,
|
|
328
|
+
totalTokens: meta.totalTokenCount ?? 0,
|
|
329
|
+
reasoningTokens: meta.thoughtsTokenCount ?? 0,
|
|
330
|
+
cachedTokens: meta.cachedContentTokenCount ?? 0,
|
|
331
|
+
} : undefined;
|
|
332
|
+
|
|
333
|
+
yield {
|
|
334
|
+
type: 'done',
|
|
335
|
+
finishReason: pendingToolCalls.size > 0 ? 'tool_calls' : 'stop',
|
|
336
|
+
usage,
|
|
337
|
+
outputParts,
|
|
338
|
+
};
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
yield {
|
|
344
|
+
type: 'done',
|
|
345
|
+
finishReason: pendingToolCalls.size > 0 ? 'tool_calls' : 'stop',
|
|
346
|
+
outputParts,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function createGeminiProtocol(config: ProtocolConfig): GeminiProtocol {
|
|
351
|
+
return new GeminiProtocol(config);
|
|
352
|
+
}
|