dgxay-code-cli 0.0.0-stage → 1.0.0

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 (45) hide show
  1. package/LICENSE +674 -0
  2. package/README-zh.md +200 -0
  3. package/README.md +182 -2
  4. package/bin/dgxay.js +42 -0
  5. package/package.json +47 -4
  6. package/src/cli/ansi.js +509 -0
  7. package/src/cli/approval.js +186 -0
  8. package/src/cli/args.js +162 -0
  9. package/src/cli/command-table.js +52 -0
  10. package/src/cli/editor.js +574 -0
  11. package/src/cli/index.js +228 -0
  12. package/src/cli/print.js +148 -0
  13. package/src/cli/render.js +324 -0
  14. package/src/cli/repl-state.js +85 -0
  15. package/src/cli/repl.js +1167 -0
  16. package/src/cli/slash.js +74 -0
  17. package/src/cli/ui.js +52 -0
  18. package/src/commands/_shared.js +20 -0
  19. package/src/commands/chat.js +212 -0
  20. package/src/commands/config.js +363 -0
  21. package/src/commands/context.js +43 -0
  22. package/src/commands/doctor.js +192 -0
  23. package/src/commands/export.js +102 -0
  24. package/src/commands/mcp.js +257 -0
  25. package/src/commands/model.js +223 -0
  26. package/src/commands/oauth.js +246 -0
  27. package/src/commands/permission.js +108 -0
  28. package/src/commands/provider.js +332 -0
  29. package/src/commands/run.js +172 -0
  30. package/src/commands/skill.js +94 -0
  31. package/src/commands/workspace.js +100 -0
  32. package/src/core/agent.js +419 -0
  33. package/src/core/llm.js +709 -0
  34. package/src/core/mcp.js +271 -0
  35. package/src/core/oauth.js +737 -0
  36. package/src/core/prompt.js +269 -0
  37. package/src/core/sessions.js +208 -0
  38. package/src/core/tools.js +391 -0
  39. package/src/services/config.js +182 -0
  40. package/src/services/input.js +91 -0
  41. package/src/services/session-bus.js +104 -0
  42. package/src/services/settings.js +340 -0
  43. package/src/services/skills.js +125 -0
  44. package/src/services/terminal.js +124 -0
  45. package/src/services/version.js +25 -0
@@ -0,0 +1,709 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 多协议模型调用(与桌面版 src/main/llm.js 行为保持一致):
5
+ * - openai OpenAI ChatCompletions(/v1/chat/completions,兼容大多数供应商)
6
+ * - anthropic Anthropic Messages(/v1/messages)
7
+ * - responses OpenAI Responses API(/v1/responses)
8
+ *
9
+ * 三种协议统一输出 { content, reasoning, toolCalls, usage, finishReason },
10
+ * 工具调用使用 OpenAI 风格的 { id, type, function: { name, arguments } }。
11
+ */
12
+
13
+ const { USER_AGENT } = require('../services/version');
14
+
15
+ const PROTOCOLS = ['openai', 'anthropic', 'responses'];
16
+
17
+ function normalizeBaseUrl(baseUrl) {
18
+ return String(baseUrl || '').trim().replace(/\/+$/, '');
19
+ }
20
+
21
+ function endpointFor(baseUrl, path) {
22
+ const base = normalizeBaseUrl(baseUrl);
23
+ if (!base) throw new Error('Base URL 不能为空');
24
+ if (base.toLowerCase().endsWith(path.toLowerCase())) return base;
25
+ // 已经带版本前缀的根地址(/v1、/v1beta/openai、/v1beta1 等)直接拼端点
26
+ if (/\/v\d+[a-z0-9]*(?:\/.*)?$/i.test(base)) return `${base}${path}`;
27
+ return `${base}/v1${path}`;
28
+ }
29
+
30
+ function chatEndpoint(baseUrl, protocol = 'openai') {
31
+ if (protocol === 'anthropic') return endpointFor(baseUrl, '/messages');
32
+ if (protocol === 'responses') return endpointFor(baseUrl, '/responses');
33
+ return endpointFor(baseUrl, '/chat/completions');
34
+ }
35
+
36
+ function modelsEndpoint(baseUrl) {
37
+ return endpointFor(baseUrl, '/models');
38
+ }
39
+
40
+ function buildHeaders(apiKey, protocol, accept = 'text/event-stream', authType = 'apiKey') {
41
+ const headers = {
42
+ 'Content-Type': 'application/json',
43
+ Accept: accept,
44
+ 'User-Agent': USER_AGENT
45
+ };
46
+ const key = apiKey ? String(apiKey).trim() : '';
47
+ const oauth = authType === 'oauth';
48
+ if (protocol === 'anthropic') {
49
+ if (key) {
50
+ // OAuth 访问令牌只能放在 Authorization;API Key 走 x-api-key(两种方式不混用)
51
+ if (oauth) headers.Authorization = `Bearer ${key}`;
52
+ else headers['x-api-key'] = key;
53
+ }
54
+ headers['anthropic-version'] = '2023-06-01';
55
+ if (oauth) headers['anthropic-beta'] = 'oauth-2025-04-20';
56
+ return headers;
57
+ }
58
+ if (key) headers.Authorization = `Bearer ${key}`;
59
+ return headers;
60
+ }
61
+
62
+ async function readError(res) {
63
+ let detail = '';
64
+ try {
65
+ const text = await res.text();
66
+ try {
67
+ const json = JSON.parse(text);
68
+ detail = json.error?.message || json.message || json.error || text;
69
+ } catch {
70
+ detail = text;
71
+ }
72
+ } catch {
73
+ detail = '';
74
+ }
75
+ return `HTTP ${res.status} ${res.statusText}${detail ? ` · ${String(detail).slice(0, 600)}` : ''}`;
76
+ }
77
+
78
+ function textOf(content) {
79
+ if (typeof content === 'string') return content;
80
+ if (Array.isArray(content)) {
81
+ return content
82
+ .map((part) => (typeof part === 'string' ? part : part?.text || part?.content || ''))
83
+ .join('');
84
+ }
85
+ return content == null ? '' : String(content);
86
+ }
87
+
88
+ function splitSystem(messages) {
89
+ return messages
90
+ .filter((m) => m.role === 'system')
91
+ .map((m) => textOf(m.content))
92
+ .filter(Boolean)
93
+ .join('\n\n');
94
+ }
95
+
96
+ /** 通用 SSE 读取:每条 data 交给 onData;忽略注释与 [DONE] */
97
+ async function consumeSSE(res, onData) {
98
+ if (!res.body) throw new Error('当前网络响应不支持流式读取');
99
+ const reader = res.body.getReader();
100
+ const decoder = new TextDecoder('utf-8');
101
+ let buffer = '';
102
+
103
+ const handleBlock = (block) => {
104
+ // 一个事件可能有多行 data(SSE 规范:用 \n 连接后再交给上层)
105
+ const dataLines = [];
106
+ for (const line of block.split(/\r?\n/)) {
107
+ const trimmed = line.trim();
108
+ if (!trimmed || trimmed.startsWith(':')) continue;
109
+ if (!trimmed.startsWith('data:')) continue;
110
+ dataLines.push(trimmed.slice(5).trim());
111
+ }
112
+ if (!dataLines.length) return;
113
+ const payload = dataLines.join('\n');
114
+ if (!payload || payload === '[DONE]') return;
115
+ onData(payload);
116
+ };
117
+
118
+ while (true) {
119
+ const { value, done } = await reader.read();
120
+ if (done) break;
121
+ buffer += decoder.decode(value, { stream: true });
122
+ // 用带 length 的匹配定位分隔符,才能准确切走耗掉的字符数
123
+ let match = /\r?\n\r?\n/.exec(buffer);
124
+ while (match) {
125
+ const block = buffer.slice(0, match.index);
126
+ buffer = buffer.slice(match.index + match[0].length);
127
+ handleBlock(block);
128
+ match = /\r?\n\r?\n/.exec(buffer);
129
+ }
130
+ }
131
+ if (buffer.trim()) handleBlock(buffer);
132
+ }
133
+
134
+ function normalizeToolCalls(list) {
135
+ return (list || [])
136
+ .filter((call) => call?.id && call?.function?.name)
137
+ .map((call) => ({
138
+ id: call.id,
139
+ type: 'function',
140
+ function: {
141
+ name: call.function.name,
142
+ arguments: call.function.arguments || '{}'
143
+ }
144
+ }));
145
+ }
146
+
147
+ /* ============================ OpenAI ChatCompletions ============================ */
148
+
149
+ async function streamOpenAI(options) {
150
+ const {
151
+ baseUrl,
152
+ apiKey,
153
+ model,
154
+ messages,
155
+ tools,
156
+ temperature = 0.7,
157
+ maxTokens = 4096,
158
+ stream = true,
159
+ signal,
160
+ onDelta,
161
+ onReasoning,
162
+ authType = 'apiKey'
163
+ } = options;
164
+
165
+ const body = {
166
+ model,
167
+ messages,
168
+ temperature: Number.isFinite(Number(temperature)) ? Number(temperature) : 0.7,
169
+ max_tokens: Number.isFinite(Number(maxTokens)) ? Number(maxTokens) : 4096,
170
+ stream: Boolean(stream)
171
+ };
172
+ if (tools?.length) {
173
+ body.tools = tools;
174
+ body.tool_choice = 'auto';
175
+ }
176
+
177
+ const res = await fetch(chatEndpoint(baseUrl, 'openai'), {
178
+ method: 'POST',
179
+ headers: buildHeaders(apiKey, 'openai', 'text/event-stream', authType),
180
+ body: JSON.stringify(body),
181
+ signal
182
+ });
183
+ if (!res.ok) throw new Error(await readError(res));
184
+
185
+ const toolCalls = [];
186
+ let content = '';
187
+ let reasoning = '';
188
+ let usage = null;
189
+ let finishReason = null;
190
+
191
+ if (!stream) {
192
+ const json = await res.json();
193
+ const choice = json.choices?.[0] || {};
194
+ const message = choice.message || {};
195
+ content = textOf(message.content);
196
+ reasoning = message.reasoning_content || message.reasoning || '';
197
+ if (content) onDelta?.(content);
198
+ if (reasoning) onReasoning?.(reasoning);
199
+ for (const call of message.tool_calls || []) {
200
+ toolCalls.push({
201
+ id: call.id || `call_${toolCalls.length}`,
202
+ type: 'function',
203
+ function: { name: call.function?.name || '', arguments: call.function?.arguments || '{}' }
204
+ });
205
+ }
206
+ return {
207
+ content,
208
+ reasoning,
209
+ toolCalls: normalizeToolCalls(toolCalls),
210
+ usage: json.usage || null,
211
+ finishReason: choice.finish_reason || null
212
+ };
213
+ }
214
+
215
+ await consumeSSE(res, (payload) => {
216
+ let json;
217
+ try {
218
+ json = JSON.parse(payload);
219
+ } catch {
220
+ return;
221
+ }
222
+ if (json.error) throw new Error(json.error.message || '模型返回错误');
223
+ const choice = json.choices?.[0];
224
+ if (choice?.finish_reason) finishReason = choice.finish_reason;
225
+ const delta = choice?.delta || choice?.message || {};
226
+ const reason = delta.reasoning_content ?? delta.reasoning;
227
+ if (typeof reason === 'string' && reason) {
228
+ reasoning += reason;
229
+ onReasoning?.(reason);
230
+ }
231
+ if (typeof delta.content === 'string' && delta.content) {
232
+ content += delta.content;
233
+ onDelta?.(delta.content);
234
+ }
235
+ if (Array.isArray(delta.tool_calls)) {
236
+ for (const part of delta.tool_calls) {
237
+ const index = Number.isInteger(part.index) ? part.index : toolCalls.length;
238
+ if (!toolCalls[index]) {
239
+ toolCalls[index] = { id: part.id || `call_${index}`, type: 'function', function: { name: '', arguments: '' } };
240
+ }
241
+ const target = toolCalls[index];
242
+ if (part.id) target.id = part.id;
243
+ if (part.function?.name) target.function.name += part.function.name;
244
+ if (part.function?.arguments) target.function.arguments += part.function.arguments;
245
+ }
246
+ }
247
+ if (json.usage) usage = json.usage;
248
+ });
249
+
250
+ return { content, reasoning, toolCalls: normalizeToolCalls(toolCalls), usage, finishReason };
251
+ }
252
+
253
+ /* ============================ Anthropic Messages ============================ */
254
+
255
+ function anthropicMessages(messages) {
256
+ const out = [];
257
+ let pendingToolResults = [];
258
+
259
+ const flushToolResults = () => {
260
+ if (!pendingToolResults.length) return;
261
+ out.push({ role: 'user', content: pendingToolResults });
262
+ pendingToolResults = [];
263
+ };
264
+
265
+ for (const message of messages) {
266
+ if (!message || message.role === 'system') continue;
267
+
268
+ if (message.role === 'tool') {
269
+ pendingToolResults.push({
270
+ type: 'tool_result',
271
+ tool_use_id: message.tool_call_id,
272
+ content: textOf(message.content)
273
+ });
274
+ continue;
275
+ }
276
+
277
+ flushToolResults();
278
+
279
+ if (message.role === 'assistant') {
280
+ const blocks = [];
281
+ if (message.content) blocks.push({ type: 'text', text: textOf(message.content) });
282
+ for (const call of message.tool_calls || []) {
283
+ let input = {};
284
+ try {
285
+ input = call.function?.arguments ? JSON.parse(call.function.arguments) : {};
286
+ } catch {
287
+ input = { raw: call.function?.arguments || '' };
288
+ }
289
+ blocks.push({
290
+ type: 'tool_use',
291
+ id: call.id || `tool_${blocks.length}`,
292
+ name: call.function?.name || 'tool',
293
+ input
294
+ });
295
+ }
296
+ if (blocks.length) out.push({ role: 'assistant', content: blocks });
297
+ continue;
298
+ }
299
+
300
+ if (message.role === 'user') {
301
+ out.push({ role: 'user', content: textOf(message.content) });
302
+ }
303
+ }
304
+ flushToolResults();
305
+
306
+ // Anthropic 要求 user / assistant 交替出现,这里合并连续同角色消息
307
+ const merged = [];
308
+ for (const message of out) {
309
+ const last = merged[merged.length - 1];
310
+ if (last && last.role === message.role) {
311
+ if (typeof last.content === 'string' && typeof message.content === 'string') {
312
+ last.content = `${last.content}\n\n${message.content}`;
313
+ } else {
314
+ const left = Array.isArray(last.content) ? last.content : [{ type: 'text', text: last.content }];
315
+ const right = Array.isArray(message.content) ? message.content : [{ type: 'text', text: message.content }];
316
+ last.content = [...left, ...right];
317
+ }
318
+ } else {
319
+ merged.push({ ...message });
320
+ }
321
+ }
322
+ return merged;
323
+ }
324
+
325
+ async function streamAnthropic(options) {
326
+ const {
327
+ baseUrl,
328
+ apiKey,
329
+ model,
330
+ messages,
331
+ tools,
332
+ temperature = 0.7,
333
+ maxTokens = 4096,
334
+ stream = true,
335
+ signal,
336
+ onDelta,
337
+ onReasoning,
338
+ authType = 'apiKey'
339
+ } = options;
340
+
341
+ const system = splitSystem(messages);
342
+ const body = {
343
+ model,
344
+ max_tokens: Number.isFinite(Number(maxTokens)) ? Number(maxTokens) : 4096,
345
+ messages: anthropicMessages(messages),
346
+ stream: Boolean(stream),
347
+ temperature: Math.min(1, Math.max(0, Number(temperature) || 0.7))
348
+ };
349
+ if (system) body.system = system;
350
+ if (tools?.length) {
351
+ body.tools = tools.map((tool) => ({
352
+ name: tool.function.name,
353
+ description: tool.function.description,
354
+ input_schema: tool.function.parameters || { type: 'object', properties: {} }
355
+ }));
356
+ }
357
+
358
+ const res = await fetch(chatEndpoint(baseUrl, 'anthropic'), {
359
+ method: 'POST',
360
+ headers: buildHeaders(apiKey, 'anthropic', 'text/event-stream', authType),
361
+ body: JSON.stringify(body),
362
+ signal
363
+ });
364
+ if (!res.ok) throw new Error(await readError(res));
365
+
366
+ if (!stream) {
367
+ const json = await res.json();
368
+ if (json.error) throw new Error(json.error.message || 'Anthropic 返回错误');
369
+ let content = '';
370
+ let reasoning = '';
371
+ const toolCalls = [];
372
+ for (const block of json.content || []) {
373
+ if (block.type === 'text') content += block.text || '';
374
+ if (block.type === 'thinking') reasoning += block.thinking || '';
375
+ if (block.type === 'tool_use') {
376
+ toolCalls.push({
377
+ id: block.id,
378
+ type: 'function',
379
+ function: { name: block.name, arguments: JSON.stringify(block.input || {}) }
380
+ });
381
+ }
382
+ }
383
+ if (content) onDelta?.(content);
384
+ if (reasoning) onReasoning?.(reasoning);
385
+ return {
386
+ content,
387
+ reasoning,
388
+ toolCalls: normalizeToolCalls(toolCalls),
389
+ usage: json.usage || null,
390
+ finishReason: json.stop_reason || null
391
+ };
392
+ }
393
+
394
+ let content = '';
395
+ let reasoning = '';
396
+ let usage = null;
397
+ let finishReason = null;
398
+ const toolBlocks = new Map();
399
+
400
+ await consumeSSE(res, (payload) => {
401
+ let event;
402
+ try {
403
+ event = JSON.parse(payload);
404
+ } catch {
405
+ return;
406
+ }
407
+
408
+ switch (event.type) {
409
+ case 'message_start':
410
+ usage = event.message?.usage || usage;
411
+ break;
412
+ case 'content_block_start': {
413
+ const block = event.content_block || {};
414
+ if (block.type === 'text') {
415
+ if (block.text) {
416
+ content += block.text;
417
+ onDelta?.(block.text);
418
+ }
419
+ } else if (block.type === 'thinking') {
420
+ if (block.thinking) {
421
+ reasoning += block.thinking;
422
+ onReasoning?.(block.thinking);
423
+ }
424
+ } else if (block.type === 'tool_use') {
425
+ // 同一个 block 索引可能先后出现 text 与 tool_use,用独立 key 避免互相覆盖
426
+ toolBlocks.set(`tool:${event.index}`, {
427
+ id: block.id || `tool_${event.index}`,
428
+ name: block.name || '',
429
+ json: '',
430
+ input: block.input
431
+ });
432
+ }
433
+ break;
434
+ }
435
+ case 'content_block_delta': {
436
+ const delta = event.delta || {};
437
+ if (delta.type === 'text_delta' && delta.text) {
438
+ content += delta.text;
439
+ onDelta?.(delta.text);
440
+ } else if (delta.type === 'thinking_delta' && delta.thinking) {
441
+ reasoning += delta.thinking;
442
+ onReasoning?.(delta.thinking);
443
+ } else if (delta.type === 'input_json_delta') {
444
+ const block = toolBlocks.get(`tool:${event.index}`);
445
+ if (block) block.json += delta.partial_json || '';
446
+ }
447
+ break;
448
+ }
449
+ case 'message_delta': {
450
+ if (event.delta?.stop_reason) finishReason = event.delta.stop_reason;
451
+ if (event.usage) usage = { ...(usage || {}), ...event.usage };
452
+ break;
453
+ }
454
+ case 'error':
455
+ throw new Error(event.error?.message || 'Anthropic 返回错误');
456
+ default:
457
+ break;
458
+ }
459
+ });
460
+
461
+ const toolCalls = [...toolBlocks.entries()]
462
+ .sort((a, b) => Number(a[0].split(':')[1]) - Number(b[0].split(':')[1]))
463
+ .map(([, block]) => ({
464
+ id: block.id,
465
+ type: 'function',
466
+ function: {
467
+ name: block.name,
468
+ arguments: block.json || JSON.stringify(block.input || {})
469
+ }
470
+ }));
471
+
472
+ return { content, reasoning, toolCalls: normalizeToolCalls(toolCalls), usage, finishReason };
473
+ }
474
+
475
+ /* ============================ OpenAI Responses API ============================ */
476
+
477
+ function responsesInput(messages) {
478
+ const input = [];
479
+ for (const message of messages) {
480
+ if (!message || message.role === 'system') continue;
481
+ if (message.role === 'user') {
482
+ input.push({ role: 'user', content: textOf(message.content) });
483
+ continue;
484
+ }
485
+ if (message.role === 'assistant') {
486
+ if (message.content) input.push({ role: 'assistant', content: textOf(message.content) });
487
+ for (const call of message.tool_calls || []) {
488
+ input.push({
489
+ type: 'function_call',
490
+ call_id: call.id,
491
+ name: call.function?.name || 'tool',
492
+ arguments: call.function?.arguments || '{}'
493
+ });
494
+ }
495
+ continue;
496
+ }
497
+ if (message.role === 'tool') {
498
+ input.push({
499
+ type: 'function_call_output',
500
+ call_id: message.tool_call_id,
501
+ output: textOf(message.content)
502
+ });
503
+ }
504
+ }
505
+ return input;
506
+ }
507
+
508
+ async function streamResponses(options) {
509
+ const {
510
+ baseUrl,
511
+ apiKey,
512
+ model,
513
+ messages,
514
+ tools,
515
+ maxTokens = 4096,
516
+ stream = true,
517
+ signal,
518
+ onDelta,
519
+ onReasoning,
520
+ authType = 'apiKey'
521
+ } = options;
522
+
523
+ const system = splitSystem(messages);
524
+ const body = {
525
+ model,
526
+ input: responsesInput(messages),
527
+ stream: Boolean(stream),
528
+ // Responses 的推理模型通常不接受 temperature,交由服务端默认值
529
+ max_output_tokens: Number.isFinite(Number(maxTokens)) ? Number(maxTokens) : 4096
530
+ };
531
+ if (system) body.instructions = system;
532
+ if (tools?.length) {
533
+ body.tools = tools.map((tool) => ({
534
+ type: 'function',
535
+ name: tool.function.name,
536
+ description: tool.function.description,
537
+ parameters: tool.function.parameters || { type: 'object', properties: {} }
538
+ }));
539
+ body.tool_choice = 'auto';
540
+ }
541
+
542
+ const res = await fetch(chatEndpoint(baseUrl, 'responses'), {
543
+ method: 'POST',
544
+ headers: buildHeaders(apiKey, 'responses', 'text/event-stream', authType),
545
+ body: JSON.stringify(body),
546
+ signal
547
+ });
548
+ if (!res.ok) throw new Error(await readError(res));
549
+
550
+ const collectOutput = (json) => {
551
+ let text = '';
552
+ let thought = '';
553
+ const calls = [];
554
+ for (const item of json.output || []) {
555
+ if (item.type === 'message') {
556
+ for (const part of item.content || []) {
557
+ if (part.type === 'output_text' || part.type === 'text') text += part.text || '';
558
+ if (part.type === 'refusal') text += part.refusal || '';
559
+ }
560
+ } else if (item.type === 'reasoning') {
561
+ for (const part of item.summary || []) thought += part.text || '';
562
+ } else if (item.type === 'function_call') {
563
+ calls.push({
564
+ id: item.call_id || item.id,
565
+ type: 'function',
566
+ function: { name: item.name, arguments: item.arguments || '{}' }
567
+ });
568
+ }
569
+ }
570
+ if (!text && json.output_text) text = json.output_text;
571
+ return { text, thought, calls };
572
+ };
573
+
574
+ if (!stream) {
575
+ const json = await res.json();
576
+ if (json.error) throw new Error(json.error.message || 'Responses 返回错误');
577
+ const { text, thought, calls } = collectOutput(json);
578
+ if (text) onDelta?.(text);
579
+ if (thought) onReasoning?.(thought);
580
+ return {
581
+ content: text,
582
+ reasoning: thought,
583
+ toolCalls: normalizeToolCalls(calls),
584
+ usage: json.usage || null,
585
+ finishReason: json.status || null
586
+ };
587
+ }
588
+
589
+ let content = '';
590
+ let reasoning = '';
591
+ let usage = null;
592
+ let finishReason = null;
593
+ const calls = new Map();
594
+
595
+ await consumeSSE(res, (payload) => {
596
+ let event;
597
+ try {
598
+ event = JSON.parse(payload);
599
+ } catch {
600
+ return;
601
+ }
602
+
603
+ switch (event.type) {
604
+ case 'response.output_text.delta':
605
+ if (event.delta) {
606
+ content += event.delta;
607
+ onDelta?.(event.delta);
608
+ }
609
+ break;
610
+ case 'response.refusal.delta':
611
+ if (event.delta) {
612
+ content += event.delta;
613
+ onDelta?.(event.delta);
614
+ }
615
+ break;
616
+ case 'response.reasoning_summary_text.delta':
617
+ case 'response.reasoning_text.delta':
618
+ if (event.delta) {
619
+ reasoning += event.delta;
620
+ onReasoning?.(event.delta);
621
+ }
622
+ break;
623
+ case 'response.output_item.added':
624
+ if (event.item?.type === 'function_call') {
625
+ calls.set(event.item.id, {
626
+ id: event.item.call_id || event.item.id,
627
+ name: event.item.name || '',
628
+ arguments: event.item.arguments || ''
629
+ });
630
+ }
631
+ break;
632
+ case 'response.function_call_arguments.delta': {
633
+ const call = calls.get(event.item_id);
634
+ if (call) call.arguments += event.delta || '';
635
+ break;
636
+ }
637
+ case 'response.function_call_arguments.done': {
638
+ const call = calls.get(event.item_id);
639
+ if (call && event.arguments) call.arguments = event.arguments;
640
+ break;
641
+ }
642
+ case 'response.output_item.done':
643
+ if (event.item?.type === 'function_call') {
644
+ const existing = calls.get(event.item.id) || {
645
+ id: event.item.call_id || event.item.id,
646
+ name: event.item.name || '',
647
+ arguments: ''
648
+ };
649
+ if (event.item.arguments) existing.arguments = event.item.arguments;
650
+ calls.set(event.item.id, existing);
651
+ }
652
+ break;
653
+ case 'response.completed':
654
+ usage = event.response?.usage || usage;
655
+ finishReason = event.response?.status || 'completed';
656
+ break;
657
+ case 'response.failed':
658
+ throw new Error(event.response?.error?.message || 'Responses 请求失败');
659
+ case 'error':
660
+ throw new Error(event.error?.message || 'Responses 返回错误');
661
+ default:
662
+ break;
663
+ }
664
+ });
665
+
666
+ const toolCalls = [...calls.values()].map((call) => ({
667
+ id: call.id,
668
+ type: 'function',
669
+ function: { name: call.name, arguments: call.arguments || '{}' }
670
+ }));
671
+
672
+ return { content, reasoning, toolCalls: normalizeToolCalls(toolCalls), usage, finishReason };
673
+ }
674
+
675
+ /* ============================ 对外接口 ============================ */
676
+
677
+ async function streamChat(options) {
678
+ const protocol = PROTOCOLS.includes(options.protocol) ? options.protocol : 'openai';
679
+ if (!options.model) throw new Error('未选择模型');
680
+ if (!Array.isArray(options.messages) || !options.messages.length) throw new Error('消息不能为空');
681
+ if (protocol === 'anthropic') return streamAnthropic(options);
682
+ if (protocol === 'responses') return streamResponses(options);
683
+ return streamOpenAI(options);
684
+ }
685
+
686
+ async function listModels({ baseUrl, apiKey, protocol = 'openai', signal, authType = 'apiKey' }) {
687
+ const res = await fetch(modelsEndpoint(baseUrl), {
688
+ method: 'GET',
689
+ headers: buildHeaders(apiKey, protocol, 'application/json', authType),
690
+ signal
691
+ });
692
+ if (!res.ok) throw new Error(await readError(res));
693
+ const json = await res.json();
694
+ const rows = Array.isArray(json.data) ? json.data : Array.isArray(json.models) ? json.models : [];
695
+ return rows
696
+ .map((row) => (typeof row === 'string' ? row : row?.id || row?.name))
697
+ .filter(Boolean)
698
+ .map(String)
699
+ .sort((a, b) => a.localeCompare(b));
700
+ }
701
+
702
+ module.exports = {
703
+ streamChat,
704
+ listModels,
705
+ chatEndpoint,
706
+ modelsEndpoint,
707
+ textOf,
708
+ PROTOCOLS
709
+ };