@huyooo/ai-chat-core 0.2.44 → 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.
Files changed (247) hide show
  1. package/dist/adapter/index.d.ts +11 -0
  2. package/dist/adapter/index.d.ts.map +1 -0
  3. package/dist/adapter/model-adapter.d.ts +25 -0
  4. package/dist/adapter/model-adapter.d.ts.map +1 -0
  5. package/dist/adapter/model-options.d.ts +53 -0
  6. package/dist/adapter/model-options.d.ts.map +1 -0
  7. package/dist/adapter/types.d.ts +28 -0
  8. package/dist/adapter/types.d.ts.map +1 -0
  9. package/dist/chat-runtime.d.ts +96 -0
  10. package/dist/chat-runtime.d.ts.map +1 -0
  11. package/dist/constants.d.ts +12 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/events.d.ts +605 -1
  14. package/dist/events.d.ts.map +1 -0
  15. package/dist/events.js +1 -1
  16. package/dist/extension/index.d.ts +9 -0
  17. package/dist/extension/index.d.ts.map +1 -0
  18. package/dist/extension/types.d.ts +46 -0
  19. package/dist/extension/types.d.ts.map +1 -0
  20. package/dist/families/index.d.ts +11 -0
  21. package/dist/families/index.d.ts.map +1 -0
  22. package/dist/families/presets.d.ts +31 -0
  23. package/dist/families/presets.d.ts.map +1 -0
  24. package/dist/families/resolver.d.ts +11 -0
  25. package/dist/families/resolver.d.ts.map +1 -0
  26. package/dist/families/types.d.ts +29 -0
  27. package/dist/families/types.d.ts.map +1 -0
  28. package/dist/governance/command-safety.d.ts +34 -0
  29. package/dist/governance/command-safety.d.ts.map +1 -0
  30. package/dist/governance/governance.d.ts +19 -0
  31. package/dist/governance/governance.d.ts.map +1 -0
  32. package/dist/governance/index.d.ts +12 -0
  33. package/dist/governance/index.d.ts.map +1 -0
  34. package/dist/governance/types.d.ts +29 -0
  35. package/dist/governance/types.d.ts.map +1 -0
  36. package/dist/index.d.ts +72 -804
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +51 -1
  39. package/dist/internal/management-args.d.ts +13 -0
  40. package/dist/internal/management-args.d.ts.map +1 -0
  41. package/dist/internal/management-results.d.ts +21 -0
  42. package/dist/internal/management-results.d.ts.map +1 -0
  43. package/dist/llm-config.d.ts +108 -0
  44. package/dist/llm-config.d.ts.map +1 -0
  45. package/dist/logger/core.d.ts +31 -0
  46. package/dist/logger/core.d.ts.map +1 -0
  47. package/dist/logger/index.d.ts +9 -0
  48. package/dist/logger/index.d.ts.map +1 -0
  49. package/dist/orchestrator/compression-handler.d.ts +29 -0
  50. package/dist/orchestrator/compression-handler.d.ts.map +1 -0
  51. package/dist/orchestrator/context-compressor.d.ts +51 -0
  52. package/dist/orchestrator/context-compressor.d.ts.map +1 -0
  53. package/dist/orchestrator/context-summarizer.d.ts +41 -0
  54. package/dist/orchestrator/context-summarizer.d.ts.map +1 -0
  55. package/dist/orchestrator/index.d.ts +12 -0
  56. package/dist/orchestrator/index.d.ts.map +1 -0
  57. package/dist/orchestrator/orchestrator.d.ts +46 -0
  58. package/dist/orchestrator/orchestrator.d.ts.map +1 -0
  59. package/dist/orchestrator/types.d.ts +58 -0
  60. package/dist/orchestrator/types.d.ts.map +1 -0
  61. package/dist/parts/index.d.ts +13 -0
  62. package/dist/parts/index.d.ts.map +1 -0
  63. package/dist/parts/registry.d.ts +11 -0
  64. package/dist/parts/registry.d.ts.map +1 -0
  65. package/dist/parts/summaries.d.ts +9 -0
  66. package/dist/parts/summaries.d.ts.map +1 -0
  67. package/dist/parts/types.d.ts +61 -0
  68. package/dist/parts/types.d.ts.map +1 -0
  69. package/dist/platform.d.ts +17 -0
  70. package/dist/platform.d.ts.map +1 -0
  71. package/dist/platform.js +1 -0
  72. package/dist/protocols/anthropic.d.ts +20 -0
  73. package/dist/protocols/anthropic.d.ts.map +1 -0
  74. package/dist/protocols/ark.d.ts +36 -0
  75. package/dist/protocols/ark.d.ts.map +1 -0
  76. package/dist/protocols/deepseek.d.ts +24 -0
  77. package/dist/protocols/deepseek.d.ts.map +1 -0
  78. package/dist/protocols/error-utils.d.ts +14 -0
  79. package/dist/protocols/error-utils.d.ts.map +1 -0
  80. package/dist/protocols/gemini.d.ts +24 -0
  81. package/dist/protocols/gemini.d.ts.map +1 -0
  82. package/dist/protocols/glm.d.ts +20 -0
  83. package/dist/protocols/glm.d.ts.map +1 -0
  84. package/dist/protocols/grok.d.ts +20 -0
  85. package/dist/protocols/grok.d.ts.map +1 -0
  86. package/dist/protocols/index.d.ts +31 -0
  87. package/dist/protocols/index.d.ts.map +1 -0
  88. package/dist/protocols/minimax.d.ts +38 -0
  89. package/dist/protocols/minimax.d.ts.map +1 -0
  90. package/dist/protocols/moonshot.d.ts +20 -0
  91. package/dist/protocols/moonshot.d.ts.map +1 -0
  92. package/dist/protocols/openai-sse.d.ts +33 -0
  93. package/dist/protocols/openai-sse.d.ts.map +1 -0
  94. package/dist/protocols/openai.d.ts +19 -0
  95. package/dist/protocols/openai.d.ts.map +1 -0
  96. package/dist/protocols/qwen.d.ts +26 -0
  97. package/dist/protocols/qwen.d.ts.map +1 -0
  98. package/dist/protocols/responses-sse.d.ts +30 -0
  99. package/dist/protocols/responses-sse.d.ts.map +1 -0
  100. package/dist/protocols/sse-reader.d.ts +23 -0
  101. package/dist/protocols/sse-reader.d.ts.map +1 -0
  102. package/dist/protocols/tool-arguments.d.ts +8 -0
  103. package/dist/protocols/tool-arguments.d.ts.map +1 -0
  104. package/dist/protocols/types.d.ts +148 -0
  105. package/dist/protocols/types.d.ts.map +1 -0
  106. package/dist/protocols/vercel-gateway.d.ts +15 -0
  107. package/dist/protocols/vercel-gateway.d.ts.map +1 -0
  108. package/dist/runtime.d.ts +151 -0
  109. package/dist/runtime.d.ts.map +1 -0
  110. package/dist/runtime.js +1 -0
  111. package/dist/skills/index.d.ts +14 -0
  112. package/dist/skills/index.d.ts.map +1 -0
  113. package/dist/skills/management/admin.d.ts +10 -0
  114. package/dist/skills/management/admin.d.ts.map +1 -0
  115. package/dist/skills/management/index.d.ts +11 -0
  116. package/dist/skills/management/index.d.ts.map +1 -0
  117. package/dist/skills/management/inputs.d.ts +44 -0
  118. package/dist/skills/management/inputs.d.ts.map +1 -0
  119. package/dist/skills/management/operations.d.ts +78 -0
  120. package/dist/skills/management/operations.d.ts.map +1 -0
  121. package/dist/skills/management/types.d.ts +70 -0
  122. package/dist/skills/management/types.d.ts.map +1 -0
  123. package/dist/skills/registry.d.ts +37 -0
  124. package/dist/skills/registry.d.ts.map +1 -0
  125. package/dist/skills/summaries.d.ts +9 -0
  126. package/dist/skills/summaries.d.ts.map +1 -0
  127. package/dist/skills/types.d.ts +61 -0
  128. package/dist/skills/types.d.ts.map +1 -0
  129. package/dist/test-utils/mock-sse.d.ts +13 -0
  130. package/dist/test-utils/mock-sse.d.ts.map +1 -0
  131. package/dist/tool-manager/define-tool.d.ts +35 -0
  132. package/dist/tool-manager/define-tool.d.ts.map +1 -0
  133. package/dist/tool-manager/formats.d.ts +46 -0
  134. package/dist/tool-manager/formats.d.ts.map +1 -0
  135. package/dist/tool-manager/identity.d.ts +18 -0
  136. package/dist/tool-manager/identity.d.ts.map +1 -0
  137. package/dist/tool-manager/in-process-provider.d.ts +15 -0
  138. package/dist/tool-manager/in-process-provider.d.ts.map +1 -0
  139. package/dist/tool-manager/index.d.ts +18 -0
  140. package/dist/tool-manager/index.d.ts.map +1 -0
  141. package/dist/tool-manager/manager.d.ts +18 -0
  142. package/dist/tool-manager/manager.d.ts.map +1 -0
  143. package/dist/tool-manager/mcp-provider.d.ts +21 -0
  144. package/dist/tool-manager/mcp-provider.d.ts.map +1 -0
  145. package/dist/tool-manager/summaries.d.ts +39 -0
  146. package/dist/tool-manager/summaries.d.ts.map +1 -0
  147. package/dist/tool-manager/types.d.ts +314 -0
  148. package/dist/tool-manager/types.d.ts.map +1 -0
  149. package/dist/types.d.ts +663 -0
  150. package/dist/types.d.ts.map +1 -0
  151. package/package.json +26 -15
  152. package/src/adapter/index.ts +25 -0
  153. package/src/adapter/model-adapter.ts +196 -0
  154. package/src/adapter/model-options.ts +143 -0
  155. package/src/adapter/types.ts +41 -0
  156. package/src/chat-runtime.ts +515 -0
  157. package/src/constants.ts +9 -102
  158. package/src/events.ts +364 -150
  159. package/src/extension/index.ts +24 -0
  160. package/src/extension/types.ts +49 -0
  161. package/src/families/index.ts +28 -0
  162. package/src/families/presets.ts +124 -0
  163. package/src/families/resolver.ts +22 -0
  164. package/src/families/types.ts +55 -0
  165. package/src/governance/command-safety.ts +224 -0
  166. package/src/governance/governance.ts +125 -0
  167. package/src/governance/index.ts +38 -0
  168. package/src/governance/types.ts +44 -0
  169. package/src/index.ts +250 -145
  170. package/src/internal/management-args.ts +39 -0
  171. package/src/internal/management-results.ts +60 -0
  172. package/src/llm-config.ts +137 -0
  173. package/src/logger/core.ts +96 -0
  174. package/src/logger/index.ts +8 -0
  175. package/src/orchestrator/compression-handler.ts +137 -0
  176. package/src/{providers → orchestrator}/context-compressor.ts +79 -47
  177. package/src/orchestrator/context-summarizer.ts +123 -0
  178. package/src/orchestrator/index.ts +20 -0
  179. package/src/orchestrator/orchestrator.ts +1002 -0
  180. package/src/orchestrator/types.ts +70 -0
  181. package/src/parts/index.ts +20 -0
  182. package/src/parts/registry.ts +95 -0
  183. package/src/parts/summaries.ts +40 -0
  184. package/src/parts/types.ts +63 -0
  185. package/src/platform.ts +73 -0
  186. package/src/protocols/anthropic.ts +377 -0
  187. package/src/protocols/ark.ts +300 -0
  188. package/src/protocols/deepseek.ts +192 -0
  189. package/src/{providers/protocols → protocols}/error-utils.ts +17 -20
  190. package/src/protocols/gemini.ts +352 -0
  191. package/src/protocols/glm.ts +212 -0
  192. package/src/protocols/grok.ts +98 -0
  193. package/src/protocols/index.ts +48 -0
  194. package/src/protocols/minimax.ts +308 -0
  195. package/src/protocols/moonshot.ts +186 -0
  196. package/src/protocols/openai-sse.ts +156 -0
  197. package/src/protocols/openai.ts +97 -0
  198. package/src/protocols/qwen.ts +358 -0
  199. package/src/protocols/responses-sse.ts +224 -0
  200. package/src/protocols/sse-reader.ts +54 -0
  201. package/src/protocols/tool-arguments.ts +32 -0
  202. package/src/{providers/protocols → protocols}/types.ts +46 -37
  203. package/src/protocols/vercel-gateway.ts +391 -0
  204. package/src/runtime.ts +167 -0
  205. package/src/skills/index.ts +29 -0
  206. package/src/skills/management/admin.ts +170 -0
  207. package/src/skills/management/index.ts +27 -0
  208. package/src/skills/management/inputs.ts +79 -0
  209. package/src/skills/management/operations.ts +256 -0
  210. package/src/skills/management/types.ts +57 -0
  211. package/src/skills/registry.ts +120 -0
  212. package/src/skills/summaries.ts +48 -0
  213. package/src/skills/types.ts +65 -0
  214. package/src/test-utils/mock-sse.ts +3 -3
  215. package/src/tool-manager/define-tool.ts +201 -0
  216. package/src/tool-manager/formats.ts +146 -0
  217. package/src/tool-manager/identity.ts +80 -0
  218. package/src/tool-manager/in-process-provider.ts +164 -0
  219. package/src/tool-manager/index.ts +63 -0
  220. package/src/tool-manager/manager.ts +562 -0
  221. package/src/tool-manager/mcp-provider.ts +509 -0
  222. package/src/tool-manager/summaries.ts +136 -0
  223. package/src/tool-manager/types.ts +389 -0
  224. package/src/types.ts +750 -191
  225. package/dist/events-CU5D5ray.d.ts +0 -1128
  226. package/src/agent.ts +0 -409
  227. package/src/internal/update-plan.ts +0 -2
  228. package/src/internal/web-search.ts +0 -77
  229. package/src/mcp/client-manager.ts +0 -302
  230. package/src/mcp/index.ts +0 -2
  231. package/src/mcp/types.ts +0 -43
  232. package/src/providers/context-summarizer.ts +0 -70
  233. package/src/providers/index.ts +0 -125
  234. package/src/providers/model-registry.ts +0 -466
  235. package/src/providers/orchestrator.ts +0 -839
  236. package/src/providers/protocols/anthropic.ts +0 -406
  237. package/src/providers/protocols/ark.ts +0 -362
  238. package/src/providers/protocols/deepseek.ts +0 -344
  239. package/src/providers/protocols/gemini.ts +0 -350
  240. package/src/providers/protocols/index.ts +0 -36
  241. package/src/providers/protocols/openai.ts +0 -420
  242. package/src/providers/protocols/qwen.ts +0 -315
  243. package/src/providers/types.ts +0 -264
  244. package/src/providers/unified-adapter.ts +0 -367
  245. package/src/router.ts +0 -72
  246. package/src/tools.ts +0 -162
  247. package/src/utils.ts +0 -86
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Qwen Protocol(DashScope 原生 API)
3
+ *
4
+ * 使用 DashScope 原生端点,不依赖 OpenAI 兼容模式。
5
+ * - text-only 模型: /api/v1/services/aigc/text-generation/generation
6
+ * - multimodal 模型: /api/v1/services/aigc/multimodal-generation/generation
7
+ *
8
+ * qwen3.5-* 和 *-vl-* 系列归为 multimodal,其余走 text-only 端点。
9
+ */
10
+
11
+ import type {
12
+ Protocol,
13
+ ProtocolConfig,
14
+ ProtocolMessage,
15
+ ProtocolToolDefinition,
16
+ ProtocolRequestOptions,
17
+ RawEvent,
18
+ RawToolCall,
19
+ } from './types';
20
+ import { createModuleLogger } from '../logger';
21
+ import { friendlyHttpError } from './error-utils';
22
+ import { readSSEJsonStream } from './sse-reader';
23
+
24
+ const logger = createModuleLogger('QwenProtocol');
25
+
26
+ const DEFAULT_QWEN_NATIVE_URL = 'https://dashscope.aliyuncs.com/api/v1';
27
+
28
+ const TEXT_GENERATION_PATH = '/services/aigc/text-generation/generation';
29
+ const MULTIMODAL_GENERATION_PATH = '/services/aigc/multimodal-generation/generation';
30
+
31
+ /**
32
+ * qwen3.5-* 和 *-vl-* 系列需要使用 multimodal 端点
33
+ */
34
+ function isMultimodalModel(model: string): boolean {
35
+ return model.startsWith('qwen3.5-') || model.includes('-vl-');
36
+ }
37
+
38
+ /**
39
+ * 从 DashScope 原生 content 中提取文本
40
+ * text-only 端点返回 string,multimodal 端点返回 [{text: "..."}]
41
+ */
42
+ function extractText(content: unknown): string {
43
+ if (typeof content === 'string') return content;
44
+ if (Array.isArray(content)) {
45
+ for (const item of content) {
46
+ if (typeof item === 'object' && item !== null && 'text' in item) {
47
+ return (item as { text: string }).text || '';
48
+ }
49
+ }
50
+ }
51
+ return '';
52
+ }
53
+
54
+ export function extractIncrementalText(nextText: string, previousText: string): string {
55
+ if (!nextText) return '';
56
+ if (!previousText) return nextText;
57
+ if (nextText === previousText) return '';
58
+ if (nextText.startsWith(previousText)) {
59
+ return nextText.slice(previousText.length);
60
+ }
61
+ return nextText;
62
+ }
63
+
64
+ /**
65
+ * Qwen Protocol 实现(DashScope 原生 API)
66
+ */
67
+ export class QwenProtocol implements Protocol {
68
+ readonly name = 'qwen';
69
+
70
+ private apiKey: string;
71
+ private apiUrl: string;
72
+
73
+ constructor(config: ProtocolConfig) {
74
+ this.apiKey = config.apiKey;
75
+ this.apiUrl = config.apiUrl ?? DEFAULT_QWEN_NATIVE_URL;
76
+ }
77
+
78
+ async *stream(
79
+ messages: ProtocolMessage[],
80
+ tools: ProtocolToolDefinition[],
81
+ options: ProtocolRequestOptions
82
+ ): AsyncGenerator<RawEvent> {
83
+ const multimodal = isMultimodalModel(options.model);
84
+ const path = multimodal ? MULTIMODAL_GENERATION_PATH : TEXT_GENERATION_PATH;
85
+ const url = `${this.apiUrl.replace(/\/$/, '')}${path}`;
86
+ const requestBody = buildRequestBody(messages, tools, options, multimodal);
87
+
88
+ logger.debug({
89
+ url,
90
+ model: options.model,
91
+ multimodal,
92
+ enableThinking: options.enableThinking,
93
+ toolsCount: tools.length,
94
+ }, '发送 Qwen 请求(DashScope 原生)');
95
+
96
+ const response = await fetch(url, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Authorization': `Bearer ${this.apiKey}`,
100
+ 'Content-Type': 'application/json',
101
+ 'X-DashScope-SSE': 'enable',
102
+ },
103
+ body: JSON.stringify(requestBody),
104
+ signal: options.signal,
105
+ });
106
+
107
+ if (!response.ok) {
108
+ const errorText = await response.text();
109
+ logger.error({ status: response.status, body: errorText.slice(0, 500) }, 'Qwen API 错误');
110
+ yield { type: 'error', error: friendlyHttpError(response.status, errorText, 'Qwen') };
111
+ return;
112
+ }
113
+
114
+ const reader = response.body?.getReader();
115
+ if (!reader) {
116
+ yield { type: 'error', error: '无法获取响应流' };
117
+ return;
118
+ }
119
+
120
+ yield* parseSSE(reader, multimodal);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 构建 DashScope 原生请求体
126
+ *
127
+ * 结构: { model, input: { messages }, parameters: { ... } }
128
+ */
129
+ function buildRequestBody(
130
+ messages: ProtocolMessage[],
131
+ tools: ProtocolToolDefinition[],
132
+ options: ProtocolRequestOptions,
133
+ multimodal: boolean,
134
+ ): Record<string, unknown> {
135
+ const convertedMessages = convertMessages(messages, multimodal);
136
+
137
+ const parameters: Record<string, unknown> = {
138
+ result_format: 'message',
139
+ incremental_output: true,
140
+ };
141
+
142
+ if (options.enableThinking) {
143
+ parameters.enable_thinking = true;
144
+ parameters.thinking_budget = 38400;
145
+ }
146
+
147
+ if (tools.length > 0) {
148
+ parameters.tools = tools.map(t => ({
149
+ type: 'function',
150
+ function: {
151
+ name: t.name,
152
+ description: t.description,
153
+ parameters: t.parameters,
154
+ },
155
+ }));
156
+ }
157
+
158
+ return {
159
+ model: options.model,
160
+ input: { messages: convertedMessages },
161
+ parameters,
162
+ };
163
+ }
164
+
165
+ /**
166
+ * 转换消息格式
167
+ *
168
+ * text-only: content 是 string
169
+ * multimodal: content 是 [{text: "..."}, {image: "url"}] 数组
170
+ */
171
+ function convertMessages(messages: ProtocolMessage[], multimodal: boolean): unknown[] {
172
+ const result: unknown[] = [];
173
+
174
+ for (const msg of messages) {
175
+ switch (msg.role) {
176
+ case 'system':
177
+ result.push({
178
+ role: 'system',
179
+ content: multimodal ? [{ text: msg.content }] : msg.content,
180
+ });
181
+ break;
182
+
183
+ case 'user': {
184
+ const textContent = msg.content || (msg.images?.length ? '请分析这张图片' : '');
185
+
186
+ if (multimodal) {
187
+ const content: unknown[] = [{ text: textContent }];
188
+ if (msg.images?.length) {
189
+ for (const img of msg.images) {
190
+ content.push({
191
+ image: img.startsWith('data:') ? img : `data:image/jpeg;base64,${img}`,
192
+ });
193
+ }
194
+ }
195
+ result.push({ role: 'user', content });
196
+ } else {
197
+ result.push({ role: 'user', content: textContent });
198
+ }
199
+ break;
200
+ }
201
+
202
+ case 'assistant':
203
+ if (msg.toolCalls?.length) {
204
+ result.push({
205
+ role: 'assistant',
206
+ content: multimodal ? (msg.content ? [{ text: msg.content }] : []) : (msg.content || null),
207
+ ...(msg.thinkingContent ? { reasoning_content: msg.thinkingContent } : {}),
208
+ tool_calls: msg.toolCalls.map(tc => ({
209
+ id: tc.id,
210
+ type: 'function',
211
+ function: { name: tc.name, arguments: tc.arguments },
212
+ })),
213
+ });
214
+ } else {
215
+ result.push({
216
+ role: 'assistant',
217
+ content: multimodal ? (msg.content ? [{ text: msg.content }] : []) : msg.content,
218
+ });
219
+ }
220
+ break;
221
+
222
+ case 'tool':
223
+ result.push({
224
+ role: 'tool',
225
+ tool_call_id: msg.toolCallId,
226
+ content: msg.content,
227
+ });
228
+ break;
229
+ }
230
+ }
231
+
232
+ return result;
233
+ }
234
+
235
+ /**
236
+ * 解析 DashScope 原生 SSE 流
237
+ *
238
+ * Layer 1(readSSEJsonStream)负责 bytes → JSON,此处仅做语义映射。
239
+ * 无 [DONE] 信号,通过 finish_reason !== "null" 判断结束。
240
+ */
241
+ async function* parseSSE(
242
+ reader: ReadableStreamDefaultReader<Uint8Array>,
243
+ multimodal: boolean,
244
+ ): AsyncGenerator<RawEvent> {
245
+ const toolCallsMap = new Map<number, RawToolCall>();
246
+ let textStarted = false;
247
+ let thinkingDone = false;
248
+ let lastUsage: RawEvent['usage'];
249
+ let lastReasoningText = '';
250
+ let lastText = '';
251
+
252
+ for await (const json of readSSEJsonStream(reader)) {
253
+ if (json.code) {
254
+ yield { type: 'error', error: `${json.code}: ${json.message}` };
255
+ return;
256
+ }
257
+
258
+ if (json.usage) {
259
+ const usage = json.usage as Record<string, unknown>;
260
+ lastUsage = {
261
+ promptTokens: (usage.input_tokens as number) ?? 0,
262
+ completionTokens: (usage.output_tokens as number) ?? 0,
263
+ totalTokens: (usage.total_tokens as number) ?? 0,
264
+ reasoningTokens: (usage.output_tokens_details as Record<string, number> | undefined)?.reasoning_tokens ?? 0,
265
+ cachedTokens: (usage.prompt_tokens_details as Record<string, number> | undefined)?.cached_tokens ?? 0,
266
+ };
267
+ }
268
+
269
+ const output = json.output as Record<string, unknown> | undefined;
270
+ const choices = output?.choices as Array<Record<string, unknown>> | undefined;
271
+ const choice = choices?.[0];
272
+ if (!choice) continue;
273
+
274
+ const message = choice.message as Record<string, unknown> | undefined;
275
+ if (!message) continue;
276
+
277
+ const reasoningText = typeof message.reasoning_content === 'string' ? message.reasoning_content : '';
278
+ if (reasoningText && !thinkingDone) {
279
+ const reasoningDelta = extractIncrementalText(reasoningText, lastReasoningText);
280
+ lastReasoningText = reasoningText;
281
+ if (reasoningDelta) {
282
+ yield { type: 'thinking_delta', delta: reasoningDelta };
283
+ }
284
+ }
285
+
286
+ const textSnapshot = multimodal ? extractText(message.content) : ((message.content as string) || '');
287
+ const textDelta = extractIncrementalText(textSnapshot, lastText);
288
+ if (textDelta) {
289
+ lastText = textSnapshot;
290
+ if (!textStarted && !thinkingDone) {
291
+ thinkingDone = true;
292
+ yield { type: 'thinking_done' };
293
+ }
294
+ textStarted = true;
295
+ yield { type: 'text_delta', delta: textDelta };
296
+ } else if (textSnapshot) {
297
+ lastText = textSnapshot;
298
+ }
299
+
300
+ const msgToolCalls = message.tool_calls as Array<Record<string, unknown>> | undefined;
301
+ if (msgToolCalls?.length) {
302
+ for (const tc of msgToolCalls) {
303
+ const index = (tc.index as number) ?? 0;
304
+ const fn = tc.function as Record<string, string> | undefined;
305
+ const existing = toolCallsMap.get(index);
306
+
307
+ if (existing) {
308
+ if (fn?.arguments) {
309
+ existing.arguments += fn.arguments;
310
+ }
311
+ } else {
312
+ if (!tc.id) logger.warn({ index }, 'SSE tool_call 首块缺少 id');
313
+ if (!fn?.name) logger.warn({ index, id: tc.id }, 'SSE tool_call 首块缺少 name');
314
+ const id = (tc.id as string) || `call_${index}`;
315
+ const name = fn?.name || '';
316
+ toolCallsMap.set(index, { id, name, arguments: fn?.arguments || '' });
317
+ yield { type: 'tool_call_start', toolCall: { id, name } };
318
+ }
319
+ }
320
+ }
321
+
322
+ // DashScope 用字符串 "null" 表示未完成
323
+ const finishReason = choice.finish_reason as string | undefined;
324
+ if (finishReason && finishReason !== 'null') {
325
+ const toolCalls = Array.from(toolCallsMap.values());
326
+ if (toolCalls.length > 0) {
327
+ for (const tc of toolCalls) {
328
+ yield { type: 'tool_call_done', toolCall: tc };
329
+ }
330
+ yield { type: 'done', finishReason: 'tool_calls', usage: lastUsage };
331
+ } else {
332
+ const reason: RawEvent['finishReason'] = (finishReason === 'tool_calls' || finishReason === 'length' || finishReason === 'error')
333
+ ? finishReason
334
+ : 'stop';
335
+ yield { type: 'done', finishReason: reason, usage: lastUsage };
336
+ }
337
+ return;
338
+ }
339
+ }
340
+
341
+ // 流异常中断兜底
342
+ const toolCalls = Array.from(toolCallsMap.values());
343
+ if (toolCalls.length > 0) {
344
+ for (const tc of toolCalls) {
345
+ yield { type: 'tool_call_done', toolCall: tc };
346
+ }
347
+ yield { type: 'done', finishReason: 'tool_calls', usage: lastUsage };
348
+ } else {
349
+ yield { type: 'done', finishReason: 'stop', usage: lastUsage };
350
+ }
351
+ }
352
+
353
+ /**
354
+ * 创建 Qwen Protocol
355
+ */
356
+ export function createQwenProtocol(config: ProtocolConfig): QwenProtocol {
357
+ return new QwenProtocol(config);
358
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Responses API 共享逻辑
3
+ *
4
+ * OpenAI 和 xAI (Grok) 均使用相同的 Responses API 格式,
5
+ * 此模块提取共享的消息转换和 SSE 事件解析逻辑。
6
+ *
7
+ * 使用方:openai.ts, grok.ts
8
+ */
9
+
10
+ import type {
11
+ ProtocolMessage,
12
+ ProtocolToolDefinition,
13
+ RawEvent,
14
+ RawToolCall,
15
+ RawTokenUsage,
16
+ } from './types';
17
+ import { readSSEJsonStream } from './sse-reader';
18
+
19
+ // ==================== 消息转换 ====================
20
+
21
+ /** 将 ProtocolMessage[] 转换为 Responses API 的 input 格式 */
22
+ export function convertToResponsesInput(messages: ProtocolMessage[]): unknown[] {
23
+ const input: unknown[] = [];
24
+
25
+ for (const msg of messages) {
26
+ switch (msg.role) {
27
+ case 'system':
28
+ input.push({ role: 'system', content: msg.content });
29
+ break;
30
+
31
+ case 'user': {
32
+ const textContent = msg.content || (msg.images?.length ? '请分析这张图片' : '');
33
+ const content: unknown[] = [{ type: 'input_text', text: textContent }];
34
+ if (msg.images?.length) {
35
+ for (const img of msg.images) {
36
+ content.push({
37
+ type: 'input_image',
38
+ image_url: img.startsWith('data:') ? img : `data:image/jpeg;base64,${img}`,
39
+ });
40
+ }
41
+ }
42
+ input.push({ role: 'user', content });
43
+ break;
44
+ }
45
+
46
+ case 'assistant':
47
+ if (msg.toolCalls?.length) {
48
+ if (msg.content) {
49
+ input.push({
50
+ type: 'message',
51
+ role: 'assistant',
52
+ content: [{ type: 'output_text', text: msg.content }],
53
+ });
54
+ }
55
+ for (const tc of msg.toolCalls) {
56
+ input.push({
57
+ type: 'function_call',
58
+ call_id: tc.id,
59
+ name: tc.name,
60
+ arguments: tc.arguments,
61
+ });
62
+ }
63
+ } else {
64
+ input.push({ role: 'assistant', content: msg.content });
65
+ }
66
+ break;
67
+
68
+ case 'tool':
69
+ input.push({
70
+ type: 'function_call_output',
71
+ call_id: msg.toolCallId,
72
+ output: msg.content,
73
+ });
74
+ break;
75
+ }
76
+ }
77
+
78
+ return input;
79
+ }
80
+
81
+ // ==================== 工具定义转换 ====================
82
+
83
+ /** 将 ProtocolToolDefinition[] 转换为 Responses API 的 tools 格式 */
84
+ export function convertToResponsesTools(tools: ProtocolToolDefinition[]): unknown[] {
85
+ return tools.map(t => ({
86
+ type: 'function',
87
+ name: t.name,
88
+ description: t.description,
89
+ parameters: t.parameters,
90
+ }));
91
+ }
92
+
93
+ // ==================== SSE 事件解析 ====================
94
+
95
+ export interface ResponsesStreamConfig {
96
+ /** 协议名(用于错误消息,如 "OpenAI"、"Grok") */
97
+ protocolName: string;
98
+ }
99
+
100
+ /**
101
+ * 解析 Responses API 的 SSE 事件流,映射为 RawEvent
102
+ *
103
+ * 处理的事件:
104
+ * - response.output_item.added (message / function_call / reasoning)
105
+ * - response.output_text.delta → text_delta
106
+ * - response.function_call_arguments.delta → tool_call_delta
107
+ * - response.reasoning_summary_text.delta → thinking_delta
108
+ * - response.completed → usage
109
+ * - response.failed → error
110
+ */
111
+ export async function* mapResponsesStream(
112
+ reader: ReadableStreamDefaultReader<Uint8Array>,
113
+ config: ResponsesStreamConfig,
114
+ ): AsyncGenerator<RawEvent> {
115
+ const pendingToolCalls = new Map<string, RawToolCall>();
116
+ let currentFunctionCallId: string | null = null;
117
+ let thinkingActive = false;
118
+ let thinkingDone = false;
119
+ let textStarted = false;
120
+ let lastUsage: RawTokenUsage | undefined;
121
+
122
+ for await (const event of readSSEJsonStream(reader)) {
123
+ const eventUsage = (event.response as Record<string, unknown>)?.usage ?? event.usage;
124
+ if (eventUsage) {
125
+ const u = eventUsage as Record<string, unknown>;
126
+ const inputTokens = (u.input_tokens as number) ?? 0;
127
+ const outputTokens = (u.output_tokens as number) ?? 0;
128
+ const outputDetails = u.output_tokens_details as Record<string, number> | undefined;
129
+ const inputDetails = u.input_tokens_details as Record<string, number> | undefined;
130
+ lastUsage = {
131
+ promptTokens: inputTokens,
132
+ completionTokens: outputTokens,
133
+ totalTokens: (u.total_tokens as number) ?? inputTokens + outputTokens,
134
+ reasoningTokens: outputDetails?.reasoning_tokens ?? 0,
135
+ cachedTokens: inputDetails?.cached_tokens ?? 0,
136
+ };
137
+ }
138
+
139
+ switch (event.type) {
140
+ case 'response.output_item.added': {
141
+ const item = event.item as Record<string, unknown> | undefined;
142
+ if (!item) break;
143
+
144
+ if (item.type === 'function_call' && item.call_id) {
145
+ currentFunctionCallId = item.call_id as string;
146
+ const name = (item.name as string) || '';
147
+ pendingToolCalls.set(currentFunctionCallId, {
148
+ id: currentFunctionCallId,
149
+ name,
150
+ arguments: (item.arguments as string) || '',
151
+ });
152
+ yield { type: 'tool_call_start', toolCall: { id: currentFunctionCallId, name } };
153
+ }
154
+
155
+ if (item.type === 'reasoning') {
156
+ thinkingActive = true;
157
+ }
158
+ break;
159
+ }
160
+
161
+ case 'response.function_call_arguments.delta': {
162
+ if (currentFunctionCallId) {
163
+ const call = pendingToolCalls.get(currentFunctionCallId);
164
+ if (call) {
165
+ call.arguments += (event.delta as string) || '';
166
+ yield { type: 'tool_call_delta', toolCall: { id: currentFunctionCallId, arguments: (event.delta as string) || '' } };
167
+ }
168
+ }
169
+ break;
170
+ }
171
+
172
+ case 'response.function_call_arguments.done':
173
+ case 'response.output_item.done': {
174
+ const item = event.item as Record<string, unknown> | undefined;
175
+ if (item?.type === 'function_call' && item.call_id) {
176
+ const callId = item.call_id as string;
177
+ const existing = pendingToolCalls.get(callId);
178
+ pendingToolCalls.set(callId, {
179
+ id: callId,
180
+ name: (item.name as string) || existing?.name || '',
181
+ arguments: (item.arguments as string) || existing?.arguments || '{}',
182
+ });
183
+ }
184
+ break;
185
+ }
186
+
187
+ case 'response.output_text.delta':
188
+ if (event.delta) {
189
+ if (!textStarted) {
190
+ textStarted = true;
191
+ if (thinkingActive && !thinkingDone) {
192
+ thinkingDone = true;
193
+ yield { type: 'thinking_done' };
194
+ }
195
+ }
196
+ yield { type: 'text_delta', delta: event.delta as string };
197
+ }
198
+ break;
199
+
200
+ case 'response.reasoning_summary_text.delta':
201
+ if (event.delta && !thinkingDone) {
202
+ yield { type: 'thinking_delta', delta: event.delta as string };
203
+ }
204
+ break;
205
+
206
+ case 'response.failed': {
207
+ const resp = event.response as Record<string, unknown> | undefined;
208
+ const errObj = resp?.error as Record<string, unknown> | undefined;
209
+ const errMsg = (errObj?.message as string) || `${config.protocolName} Responses API 失败`;
210
+ yield { type: 'error', error: errMsg };
211
+ break;
212
+ }
213
+ }
214
+ }
215
+
216
+ if (pendingToolCalls.size > 0) {
217
+ for (const tc of pendingToolCalls.values()) {
218
+ yield { type: 'tool_call_done', toolCall: tc };
219
+ }
220
+ yield { type: 'done', finishReason: 'tool_calls', usage: lastUsage };
221
+ } else {
222
+ yield { type: 'done', finishReason: 'stop', usage: lastUsage };
223
+ }
224
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Layer 1: 通用 SSE 字节流读取器
3
+ *
4
+ * 纯机械逻辑:bytes → lines → JSON objects
5
+ * 不含任何协议语义,所有协议通用。
6
+ *
7
+ * 处理:
8
+ * - 字节解码 + 缓冲区拼接
9
+ * - 行分割
10
+ * - data: 前缀提取
11
+ * - [DONE] 信号跳过
12
+ * - JSON.parse
13
+ *
14
+ * 忽略:event:、id:、:HTTP_STATUS 等非 data 行
15
+ */
16
+
17
+ import { createModuleLogger } from '../logger';
18
+
19
+ const logger = createModuleLogger('SSEReader');
20
+
21
+ /**
22
+ * 将 ReadableStream 转换为 JSON 对象流
23
+ *
24
+ * 所有协议共用此函数,消除重复的 buffer/decode/split/parse 逻辑。
25
+ * 消费方根据 JSON 结构自行做语义映射。
26
+ */
27
+ export async function* readSSEJsonStream(
28
+ reader: ReadableStreamDefaultReader<Uint8Array>,
29
+ ): AsyncGenerator<Record<string, unknown>> {
30
+ const decoder = new TextDecoder();
31
+ let buffer = '';
32
+
33
+ while (true) {
34
+ const { done, value } = await reader.read();
35
+ if (done) break;
36
+
37
+ buffer += decoder.decode(value, { stream: true });
38
+ const lines = buffer.split('\n');
39
+ buffer = lines.pop() || '';
40
+
41
+ for (const line of lines) {
42
+ if (!line.startsWith('data:')) continue;
43
+
44
+ const data = line.slice(5).trim();
45
+ if (!data || data === '[DONE]') continue;
46
+
47
+ try {
48
+ yield JSON.parse(data) as Record<string, unknown>;
49
+ } catch (e) {
50
+ logger.warn({ line: line.slice(0, 100), error: String(e) }, 'SSE JSON 解析失败');
51
+ }
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,32 @@
1
+ interface ParseProtocolToolArgumentsOptions {
2
+ protocol: string;
3
+ toolCallId: string;
4
+ toolName: string;
5
+ }
6
+
7
+ function isRecord(value: unknown): value is Record<string, unknown> {
8
+ return !!value && typeof value === 'object' && !Array.isArray(value);
9
+ }
10
+
11
+ export function parseProtocolToolArguments(
12
+ argumentsJson: string,
13
+ options: ParseProtocolToolArgumentsOptions,
14
+ ): Record<string, unknown> {
15
+ const source = argumentsJson?.trim() ? argumentsJson : '{}';
16
+ let parsed: unknown;
17
+ try {
18
+ parsed = JSON.parse(source);
19
+ } catch (error) {
20
+ throw new Error(
21
+ `[${options.protocol}] tool call arguments must be valid JSON: ${options.toolName}(${options.toolCallId}): ${String(error)}`,
22
+ );
23
+ }
24
+
25
+ if (!isRecord(parsed)) {
26
+ throw new Error(
27
+ `[${options.protocol}] tool call arguments must be a JSON object: ${options.toolName}(${options.toolCallId})`,
28
+ );
29
+ }
30
+
31
+ return parsed;
32
+ }