@codehz/ai 0.1.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,2758 @@
1
+ //#region src/types/adapter.ts
2
+ const CAPABILITY_MATRIX = {
3
+ responses: {
4
+ nativeStreaming: true,
5
+ messageStreaming: true,
6
+ reasoningStreaming: true,
7
+ toolCallStreaming: true,
8
+ hiddenReasoningReplay: "full",
9
+ replayFidelity: "high",
10
+ tools: true,
11
+ usage: "full",
12
+ billing: "lookup",
13
+ providerMetadata: true
14
+ },
15
+ messages: {
16
+ nativeStreaming: true,
17
+ messageStreaming: true,
18
+ reasoningStreaming: false,
19
+ toolCallStreaming: true,
20
+ hiddenReasoningReplay: "partial",
21
+ replayFidelity: "medium",
22
+ tools: true,
23
+ usage: "full",
24
+ billing: "lookup",
25
+ providerMetadata: true
26
+ },
27
+ "chat.completions": {
28
+ nativeStreaming: true,
29
+ messageStreaming: true,
30
+ reasoningStreaming: false,
31
+ toolCallStreaming: false,
32
+ hiddenReasoningReplay: "none",
33
+ replayFidelity: "low",
34
+ tools: true,
35
+ usage: "full",
36
+ billing: "derived",
37
+ providerMetadata: false
38
+ },
39
+ ollama: {
40
+ nativeStreaming: true,
41
+ messageStreaming: true,
42
+ reasoningStreaming: false,
43
+ toolCallStreaming: false,
44
+ hiddenReasoningReplay: "none",
45
+ replayFidelity: "low",
46
+ tools: true,
47
+ usage: "partial",
48
+ billing: "none",
49
+ providerMetadata: false
50
+ }
51
+ };
52
+ //#endregion
53
+ //#region src/core/errors.ts
54
+ var AIError = class extends Error {
55
+ code;
56
+ name;
57
+ constructor(message, code, name) {
58
+ super(message);
59
+ this.code = code;
60
+ this.name = name ?? "AIError";
61
+ Object.setPrototypeOf(this, new.target.prototype);
62
+ }
63
+ };
64
+ /** 请求构造失败 — 参数校验不通过。在进入 adapter 前同步抛错。 */
65
+ var AIRequestError = class extends AIError {
66
+ constructor(message, code) {
67
+ super(message, code, "AIRequestError");
68
+ }
69
+ };
70
+ /** Provider 调用失败 — HTTP 非 2xx、网络错误。由 AdapterBase 捕获转为 warning。 */
71
+ var AIProviderError = class extends AIError {
72
+ statusCode;
73
+ responseBody;
74
+ constructor(message, code, statusCode, responseBody) {
75
+ super(message, code, "AIProviderError");
76
+ this.statusCode = statusCode;
77
+ this.responseBody = responseBody;
78
+ }
79
+ };
80
+ /** 流协议损坏 — SSE 解析失败、chunk 格式异常。 */
81
+ var AIStreamError = class extends AIError {
82
+ constructor(message, code) {
83
+ super(message, code, "AIStreamError");
84
+ }
85
+ };
86
+ /** Canonical 映射失败 — 无法将 provider 响应映射到 canonical 类型。 */
87
+ var AIMappingError = class extends AIError {
88
+ constructor(message, code) {
89
+ super(message, code, "AIMappingError");
90
+ }
91
+ };
92
+ /**
93
+ * 标准 warning 代码列表。
94
+ * 用于非致命差异的记录。
95
+ */
96
+ const WarningCode = {
97
+ /** replay fidelity 低于预期 */
98
+ REPLAY_FIDELITY_LOW: "REPLAY_FIDELITY_LOW",
99
+ /** usage 字段缺失 */
100
+ USAGE_MISSING: "USAGE_MISSING",
101
+ /** billing 字段缺失 */
102
+ BILLING_MISSING: "BILLING_MISSING",
103
+ /** billing 只能给估算值 */
104
+ BILLING_ESTIMATED: "BILLING_ESTIMATED",
105
+ /** follow-up lookup 失败 */
106
+ LOOKUP_FAILED: "LOOKUP_FAILED",
107
+ /** lookup 超时 */
108
+ LOOKUP_TIMEOUT: "LOOKUP_TIMEOUT",
109
+ /** 流提前中断 */
110
+ STREAM_INCOMPLETE: "STREAM_INCOMPLETE",
111
+ /** 能力降级 */
112
+ CAPABILITY_DOWNGRADE: "CAPABILITY_DOWNGRADE",
113
+ /** 模拟流式 */
114
+ SYNTHETIC_STREAM: "SYNTHETIC_STREAM"
115
+ };
116
+ //#endregion
117
+ //#region src/core/validation.ts
118
+ const MESSAGE_ROLES = /* @__PURE__ */ new Set([
119
+ "user",
120
+ "assistant",
121
+ "system",
122
+ "developer"
123
+ ]);
124
+ const REASONING_VISIBILITIES = /* @__PURE__ */ new Set([
125
+ "full",
126
+ "summary",
127
+ "redacted",
128
+ "opaque"
129
+ ]);
130
+ const TOOL_RESULT_OUTCOMES = /* @__PURE__ */ new Set([
131
+ "success",
132
+ "error",
133
+ "rejected"
134
+ ]);
135
+ const INCLUDE_MODES = /* @__PURE__ */ new Set(["off", "best_effort"]);
136
+ function isRecord(value) {
137
+ return typeof value === "object" && value !== null;
138
+ }
139
+ function pushIssue(issues, field, code, message) {
140
+ issues.push({
141
+ field,
142
+ code,
143
+ message
144
+ });
145
+ }
146
+ function validateContentBlock(block, field, issues) {
147
+ if (!isRecord(block) || typeof block.type !== "string") {
148
+ pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field} must be a valid ContentBlock`);
149
+ return;
150
+ }
151
+ switch (block.type) {
152
+ case "text":
153
+ if (typeof block.text !== "string") pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.text must be a string`);
154
+ return;
155
+ case "json":
156
+ if (!("json" in block)) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.json must be present`);
157
+ return;
158
+ case "image":
159
+ if (typeof block.imageUrl !== "string" || block.imageUrl.length === 0) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.imageUrl must be a non-empty string`);
160
+ return;
161
+ case "binary_ref":
162
+ if (typeof block.ref !== "string" || block.ref.length === 0) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.ref must be a non-empty string`);
163
+ return;
164
+ case "opaque":
165
+ if (!("payload" in block)) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.payload must be present`);
166
+ return;
167
+ default: pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.type "${block.type}" is not supported`);
168
+ }
169
+ }
170
+ function validateContentArray(content, field, issues, code) {
171
+ if (!Array.isArray(content)) {
172
+ pushIssue(issues, field, code, `${field} must be a ContentBlock[]`);
173
+ return;
174
+ }
175
+ for (let i = 0; i < content.length; i++) validateContentBlock(content[i], `${field}[${i}]`, issues);
176
+ }
177
+ function validateInputItem(item, field, issues) {
178
+ if (!isRecord(item)) {
179
+ pushIssue(issues, field, "INPUT_INVALID_ITEM", `${field} must be a valid InputItem`);
180
+ return;
181
+ }
182
+ if (typeof item.type !== "string") {
183
+ pushIssue(issues, field, "INPUT_ITEM_UNKNOWN_TYPE", `${field}.type must be a supported InputItem type`);
184
+ return;
185
+ }
186
+ switch (item.type) {
187
+ case "message":
188
+ if (typeof item.role !== "string" || !MESSAGE_ROLES.has(item.role)) pushIssue(issues, `${field}.role`, "MESSAGE_ROLE_INVALID", `${field}.role must be a valid message role`);
189
+ validateContentArray(item.content, `${field}.content`, issues, "MESSAGE_CONTENT_INVALID");
190
+ return;
191
+ case "reasoning":
192
+ if (typeof item.visibility !== "string" || !REASONING_VISIBILITIES.has(item.visibility)) pushIssue(issues, `${field}.visibility`, "REASONING_VISIBILITY_INVALID", `${field}.visibility must be a valid reasoning visibility`);
193
+ validateContentArray(item.content, `${field}.content`, issues, "REASONING_CONTENT_INVALID");
194
+ return;
195
+ case "tool_call":
196
+ if (typeof item.id !== "string" || item.id.length === 0) pushIssue(issues, `${field}.id`, "TOOL_CALL_ID_INVALID", `${field}.id must be a non-empty string`);
197
+ if (typeof item.name !== "string" || item.name.length === 0) pushIssue(issues, `${field}.name`, "TOOL_CALL_NAME_INVALID", `${field}.name must be a non-empty string`);
198
+ if (typeof item.argumentsText !== "string") pushIssue(issues, `${field}.argumentsText`, "TOOL_CALL_ARGUMENTS_INVALID", `${field}.argumentsText must be a string`);
199
+ return;
200
+ case "tool_result":
201
+ if (typeof item.callId !== "string" || item.callId.length === 0) pushIssue(issues, `${field}.callId`, "TOOL_RESULT_CALL_ID_INVALID", `${field}.callId must be a non-empty string`);
202
+ if (typeof item.toolName !== "string" || item.toolName.length === 0) pushIssue(issues, `${field}.toolName`, "TOOL_RESULT_NAME_INVALID", `${field}.toolName must be a non-empty string`);
203
+ if (typeof item.outcome !== "string" || !TOOL_RESULT_OUTCOMES.has(item.outcome)) pushIssue(issues, `${field}.outcome`, "TOOL_RESULT_OUTCOME_INVALID", `${field}.outcome must be success, error, or rejected`);
204
+ validateContentArray(item.content, `${field}.content`, issues, "TOOL_RESULT_CONTENT_INVALID");
205
+ return;
206
+ case "opaque":
207
+ if (typeof item.source !== "string" || item.source.length === 0) pushIssue(issues, `${field}.source`, "OPAQUE_SOURCE_INVALID", `${field}.source must be a non-empty string`);
208
+ if (typeof item.purpose !== "string" || item.purpose.length === 0) pushIssue(issues, `${field}.purpose`, "OPAQUE_PURPOSE_INVALID", `${field}.purpose must be a non-empty string`);
209
+ return;
210
+ default: pushIssue(issues, `${field}.type`, "INPUT_ITEM_UNKNOWN_TYPE", `${field}.type "${item.type}" is not supported`);
211
+ }
212
+ }
213
+ function validateTools(tools, issues) {
214
+ if (tools === void 0) return;
215
+ if (!Array.isArray(tools)) {
216
+ pushIssue(issues, "tools", "TOOLS_INVALID", "tools must be an array");
217
+ return;
218
+ }
219
+ const seenNames = /* @__PURE__ */ new Set();
220
+ for (let i = 0; i < tools.length; i++) {
221
+ const tool = tools[i];
222
+ const field = `tools[${i}]`;
223
+ if (!isRecord(tool)) {
224
+ pushIssue(issues, field, "TOOL_INVALID", `${field} must be a valid ToolDefinition`);
225
+ continue;
226
+ }
227
+ if (typeof tool.name !== "string" || tool.name.length === 0) pushIssue(issues, `${field}.name`, "TOOL_NAME_INVALID", `${field}.name must be a non-empty string`);
228
+ else {
229
+ if (seenNames.has(tool.name)) pushIssue(issues, `${field}.name`, "TOOLS_DUPLICATE_NAME", `tool name "${tool.name}" is duplicated`);
230
+ seenNames.add(tool.name);
231
+ }
232
+ if (tool.description !== void 0 && typeof tool.description !== "string") pushIssue(issues, `${field}.description`, "TOOL_DESCRIPTION_INVALID", `${field}.description must be a string`);
233
+ if (!isRecord(tool.inputSchema)) pushIssue(issues, `${field}.inputSchema`, "TOOL_INPUT_SCHEMA_INVALID", `${field}.inputSchema must be an object`);
234
+ }
235
+ }
236
+ function validateToolChoice(toolChoice, issues) {
237
+ if (toolChoice === void 0) return;
238
+ if (toolChoice === "auto" || toolChoice === "none") return;
239
+ if (!isRecord(toolChoice) || toolChoice.type !== "tool" || typeof toolChoice.name !== "string" || toolChoice.name.length === 0) pushIssue(issues, "toolChoice", "TOOL_CHOICE_INVALID", "toolChoice must be auto, none, or { type: \"tool\", name }");
240
+ }
241
+ /**
242
+ * 校验 AIRequest,返回校验问题列表。
243
+ * 空数组表示无问题。
244
+ */
245
+ function validateRequest(request) {
246
+ const issues = [];
247
+ if (request.instructions !== void 0) if (typeof request.instructions === "string") {} else if (Array.isArray(request.instructions)) validateContentArray(request.instructions, "instructions", issues, "INSTRUCTIONS_INVALID");
248
+ else pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or ContentBlock[]");
249
+ if (!Array.isArray(request.input) || request.input.length === 0) pushIssue(issues, "input", "INPUT_EMPTY", "input must be a non-empty array");
250
+ if (Array.isArray(request.input)) for (let i = 0; i < request.input.length; i++) validateInputItem(request.input[i], `input[${i}]`, issues);
251
+ if (request.temperature !== void 0) {
252
+ if (typeof request.temperature !== "number" || isNaN(request.temperature)) issues.push({
253
+ field: "temperature",
254
+ code: "TEMPERATURE_NOT_NUMBER",
255
+ message: "temperature must be a number"
256
+ });
257
+ else if (request.temperature < 0 || request.temperature > 2) issues.push({
258
+ field: "temperature",
259
+ code: "TEMPERATURE_OUT_OF_RANGE",
260
+ message: "temperature must be between 0 and 2"
261
+ });
262
+ }
263
+ if (request.maxOutputTokens !== void 0) {
264
+ if (typeof request.maxOutputTokens !== "number" || isNaN(request.maxOutputTokens)) issues.push({
265
+ field: "maxOutputTokens",
266
+ code: "MAX_OUTPUT_TOKENS_NOT_NUMBER",
267
+ message: "maxOutputTokens must be a number"
268
+ });
269
+ else if (!Number.isInteger(request.maxOutputTokens) || request.maxOutputTokens < 1) issues.push({
270
+ field: "maxOutputTokens",
271
+ code: "MAX_OUTPUT_TOKENS_INVALID",
272
+ message: "maxOutputTokens must be a positive integer"
273
+ });
274
+ }
275
+ if (request.include !== void 0) if (!isRecord(request.include)) pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
276
+ else {
277
+ if (request.include.usage !== void 0 && !INCLUDE_MODES.has(request.include.usage)) pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
278
+ if (request.include.billing !== void 0 && !INCLUDE_MODES.has(request.include.billing)) pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
279
+ if (request.include.providerMetadata !== void 0 && !INCLUDE_MODES.has(request.include.providerMetadata)) pushIssue(issues, "include.providerMetadata", "INCLUDE_PROVIDER_METADATA_INVALID", "include.providerMetadata must be off or best_effort");
280
+ }
281
+ if (request.metadata !== void 0) {
282
+ if (!isRecord(request.metadata)) pushIssue(issues, "metadata", "METADATA_INVALID", "metadata must be an object");
283
+ else for (const [key, value] of Object.entries(request.metadata)) if (typeof value !== "string") pushIssue(issues, `metadata.${key}`, "METADATA_VALUE_INVALID", `metadata.${key} must be a string`);
284
+ }
285
+ validateTools(request.tools, issues);
286
+ validateToolChoice(request.toolChoice, issues);
287
+ if (request.toolChoice && typeof request.toolChoice === "object" && "type" in request.toolChoice && request.toolChoice.type === "tool") {
288
+ const chosenName = request.toolChoice.name;
289
+ if (!request.tools || request.tools.length === 0) issues.push({
290
+ field: "toolChoice",
291
+ code: "TOOL_CHOICE_NO_TOOLS",
292
+ message: `toolChoice specifies tool "${chosenName}" but no tools are defined`
293
+ });
294
+ else if (!request.tools.some((t) => t.name === chosenName)) issues.push({
295
+ field: "toolChoice",
296
+ code: "TOOL_CHOICE_UNKNOWN_TOOL",
297
+ message: `toolChoice specifies tool "${chosenName}" which is not in tools array`
298
+ });
299
+ }
300
+ return issues;
301
+ }
302
+ /**
303
+ * 校验请求并抛出首个问题。
304
+ * 适用于客户端入口的快速失败检查。
305
+ */
306
+ function assertValidRequest(request) {
307
+ const first = validateRequest(request)[0];
308
+ if (first) throw new AIRequestError(first.message, first.code);
309
+ }
310
+ //#endregion
311
+ //#region src/core/normalize.ts
312
+ const DEFAULT_INCLUDE = {
313
+ usage: "best_effort",
314
+ billing: "best_effort",
315
+ providerMetadata: "best_effort"
316
+ };
317
+ /**
318
+ * 归一化请求:
319
+ * 1. 合并 defaults
320
+ * 2. 填充 include 默认值
321
+ * 3. 生成 requestId
322
+ * 4. 校验请求合法性
323
+ */
324
+ function normalizeRequest(request, options) {
325
+ const { model, defaults } = options;
326
+ const merged = {
327
+ ...defaults,
328
+ ...request,
329
+ include: {
330
+ ...DEFAULT_INCLUDE,
331
+ ...defaults?.include,
332
+ ...request.include
333
+ }
334
+ };
335
+ assertValidRequest(merged);
336
+ return {
337
+ ...merged,
338
+ model,
339
+ requestId: crypto.randomUUID()
340
+ };
341
+ }
342
+ //#endregion
343
+ //#region src/core/client.ts
344
+ function createAIClient(options) {
345
+ const { adapter, model, defaults } = options;
346
+ return { stream(request) {
347
+ const normalized = normalizeRequest(request, {
348
+ model,
349
+ defaults
350
+ });
351
+ return adapter.stream(normalized);
352
+ } };
353
+ }
354
+ //#endregion
355
+ //#region src/core/event-factory.ts
356
+ function timestamp() {
357
+ return (/* @__PURE__ */ new Date()).toISOString();
358
+ }
359
+ function createEventFactory(state) {
360
+ let seq = 0;
361
+ const warnings = [];
362
+ function next() {
363
+ return seq++;
364
+ }
365
+ function base() {
366
+ return {
367
+ responseId: state.responseId,
368
+ sequence: next(),
369
+ timestamp: timestamp(),
370
+ backend: { ...state.backend }
371
+ };
372
+ }
373
+ return {
374
+ responseStarted(model) {
375
+ return {
376
+ ...base(),
377
+ type: "response.started",
378
+ model
379
+ };
380
+ },
381
+ responseWarning(message, code) {
382
+ warnings.push(message);
383
+ return {
384
+ ...base(),
385
+ type: "response.warning",
386
+ message,
387
+ code
388
+ };
389
+ },
390
+ responseAuxiliary(data) {
391
+ return {
392
+ ...base(),
393
+ type: "response.auxiliary",
394
+ ...data
395
+ };
396
+ },
397
+ responseCompleted(response) {
398
+ return {
399
+ ...base(),
400
+ type: "response.completed",
401
+ response
402
+ };
403
+ },
404
+ messageStarted(id) {
405
+ return {
406
+ ...base(),
407
+ type: "message.started",
408
+ item: {
409
+ id,
410
+ role: "assistant"
411
+ }
412
+ };
413
+ },
414
+ messageDelta(itemId, text) {
415
+ return {
416
+ ...base(),
417
+ type: "message.delta",
418
+ itemId,
419
+ delta: {
420
+ type: "text",
421
+ text
422
+ }
423
+ };
424
+ },
425
+ messageCompleted(item) {
426
+ return {
427
+ ...base(),
428
+ type: "message.completed",
429
+ item
430
+ };
431
+ },
432
+ reasoningStarted(id, visibility) {
433
+ return {
434
+ ...base(),
435
+ type: "reasoning.started",
436
+ item: {
437
+ id,
438
+ visibility
439
+ }
440
+ };
441
+ },
442
+ reasoningDelta(itemId, delta) {
443
+ return {
444
+ ...base(),
445
+ type: "reasoning.delta",
446
+ itemId,
447
+ delta
448
+ };
449
+ },
450
+ reasoningCompleted(item) {
451
+ return {
452
+ ...base(),
453
+ type: "reasoning.completed",
454
+ item
455
+ };
456
+ },
457
+ toolCallStarted(id, name) {
458
+ return {
459
+ ...base(),
460
+ type: "tool_call.started",
461
+ item: {
462
+ id,
463
+ name
464
+ }
465
+ };
466
+ },
467
+ toolCallDelta(itemId, delta) {
468
+ return {
469
+ ...base(),
470
+ type: "tool_call.delta",
471
+ itemId,
472
+ delta
473
+ };
474
+ },
475
+ toolCallCompleted(item) {
476
+ return {
477
+ ...base(),
478
+ type: "tool_call.completed",
479
+ item
480
+ };
481
+ },
482
+ /** 返回当前已发出的 sequence 计数(用于断言) */
483
+ get sequence() {
484
+ return seq;
485
+ },
486
+ /** 返回当前已记录的 warning 副本。 */
487
+ get warnings() {
488
+ return [...warnings];
489
+ }
490
+ };
491
+ }
492
+ //#endregion
493
+ //#region src/core/aggregator.ts
494
+ function createInitialState() {
495
+ return {
496
+ pendingMessages: /* @__PURE__ */ new Map(),
497
+ pendingReasonings: /* @__PURE__ */ new Map(),
498
+ pendingToolCalls: /* @__PURE__ */ new Map(),
499
+ outputOrder: [],
500
+ completedMessages: /* @__PURE__ */ new Map(),
501
+ completedReasonings: /* @__PURE__ */ new Map(),
502
+ completedToolCalls: /* @__PURE__ */ new Map(),
503
+ auxiliary: {},
504
+ warnings: []
505
+ };
506
+ }
507
+ function handleResponseStarted(state, event) {
508
+ state.responseId = event.responseId;
509
+ state.model = event.model;
510
+ state.backendInfo = event.backend;
511
+ }
512
+ function handleResponseWarning(state, event) {
513
+ pushWarnings(state, [event.message]);
514
+ }
515
+ function handleResponseAuxiliary(state, event) {
516
+ if (event.usage) state.usage = {
517
+ ...state.usage,
518
+ ...event.usage
519
+ };
520
+ if (event.billing) state.billing = {
521
+ ...state.billing,
522
+ ...event.billing
523
+ };
524
+ if (event.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.auxiliary);
525
+ }
526
+ function handleMessageStarted(state, event) {
527
+ state.pendingMessages.set(event.item.id, {
528
+ role: event.item.role,
529
+ texts: []
530
+ });
531
+ }
532
+ function handleMessageDelta(state, event) {
533
+ const pending = state.pendingMessages.get(event.itemId);
534
+ if (pending) pending.texts.push(event.delta.text);
535
+ }
536
+ function handleMessageCompleted(state, event) {
537
+ const item = event.item;
538
+ const itemId = item.id ?? `msg-${state.outputOrder.length}`;
539
+ state.completedMessages.set(itemId, item);
540
+ state.outputOrder.push(itemId);
541
+ state.pendingMessages.delete(itemId);
542
+ }
543
+ function handleReasoningStarted(state, event) {
544
+ state.pendingReasonings.set(event.item.id, {
545
+ visibility: event.item.visibility,
546
+ blocks: []
547
+ });
548
+ }
549
+ function handleReasoningDelta(state, event) {
550
+ const pending = state.pendingReasonings.get(event.itemId);
551
+ if (pending) pending.blocks.push(event.delta);
552
+ }
553
+ function handleReasoningCompleted(state, event) {
554
+ const item = event.item;
555
+ const stableId = item.id ?? `reason-${state.outputOrder.length}-${Date.now()}`;
556
+ state.completedReasonings.set(stableId, item);
557
+ state.outputOrder.push(stableId);
558
+ state.pendingReasonings.delete(item.id ?? "");
559
+ }
560
+ function handleToolCallStarted(state, event) {
561
+ state.pendingToolCalls.set(event.item.id, {
562
+ name: event.item.name,
563
+ argsParts: []
564
+ });
565
+ }
566
+ function handleToolCallDelta(state, event) {
567
+ const pending = state.pendingToolCalls.get(event.itemId);
568
+ if (pending && event.delta.argumentsText) pending.argsParts.push(event.delta.argumentsText);
569
+ }
570
+ function handleToolCallCompleted(state, event) {
571
+ const item = event.item;
572
+ state.completedToolCalls.set(item.id, item);
573
+ state.outputOrder.push(item.id);
574
+ state.pendingToolCalls.delete(item.id);
575
+ }
576
+ function handleResponseCompleted(state, event) {
577
+ state.replayFromAdapter = event.response.replay;
578
+ state.responseIdFromAdapter = event.response.id;
579
+ state.stopReasonFromAdapter = event.response.stopReason;
580
+ state.backendFromAdapter = event.response.backend;
581
+ if (event.response.usage) state.usage = {
582
+ ...state.usage,
583
+ ...event.response.usage
584
+ };
585
+ if (event.response.billing) state.billing = {
586
+ ...state.billing,
587
+ ...event.response.billing
588
+ };
589
+ if (event.response.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.response.auxiliary);
590
+ if (event.response.warnings) pushWarnings(state, event.response.warnings);
591
+ }
592
+ function buildResponse(state) {
593
+ const output = [];
594
+ for (const id of state.outputOrder) {
595
+ const msg = state.completedMessages.get(id);
596
+ if (msg) {
597
+ output.push(msg);
598
+ continue;
599
+ }
600
+ const reason = state.completedReasonings.get(id);
601
+ if (reason) {
602
+ output.push(reason);
603
+ continue;
604
+ }
605
+ const tc = state.completedToolCalls.get(id);
606
+ if (tc) {
607
+ output.push(tc);
608
+ continue;
609
+ }
610
+ }
611
+ const text = output.filter((item) => item.type === "message").flatMap((m) => m.content).filter((b) => b.type === "text").map((b) => b.text).join("");
612
+ const toolCalls = output.filter((item) => item.type === "tool_call");
613
+ const backendFromResponse = state.backendFromAdapter;
614
+ const backend = {
615
+ adapter: backendFromResponse?.adapter ?? state.backendInfo?.kind ?? "unknown",
616
+ isSyntheticStream: backendFromResponse?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
617
+ requestId: backendFromResponse?.requestId ?? state.responseId,
618
+ rawResponseId: backendFromResponse?.rawResponseId,
619
+ metadataSources: backendFromResponse?.metadataSources,
620
+ warnings: backendFromResponse?.warnings
621
+ };
622
+ return {
623
+ id: state.responseIdFromAdapter ?? state.responseId,
624
+ output,
625
+ replay: state.replayFromAdapter ?? [],
626
+ text,
627
+ toolCalls,
628
+ stopReason: state.stopReasonFromAdapter,
629
+ usage: state.usage,
630
+ billing: state.billing,
631
+ auxiliary: state.auxiliary,
632
+ warnings: state.warnings.length > 0 ? state.warnings : void 0,
633
+ backend
634
+ };
635
+ }
636
+ /**
637
+ * 将事件数组聚合为 AIResponse。
638
+ * 适用于测试和离线处理场景。
639
+ */
640
+ function aggregateEvents(events) {
641
+ const state = createInitialState();
642
+ for (const event of events) switch (event.type) {
643
+ case "response.started":
644
+ handleResponseStarted(state, event);
645
+ break;
646
+ case "response.warning":
647
+ handleResponseWarning(state, event);
648
+ break;
649
+ case "response.auxiliary":
650
+ handleResponseAuxiliary(state, event);
651
+ break;
652
+ case "message.started":
653
+ handleMessageStarted(state, event);
654
+ break;
655
+ case "message.delta":
656
+ handleMessageDelta(state, event);
657
+ break;
658
+ case "message.completed":
659
+ handleMessageCompleted(state, event);
660
+ break;
661
+ case "reasoning.started":
662
+ handleReasoningStarted(state, event);
663
+ break;
664
+ case "reasoning.delta":
665
+ handleReasoningDelta(state, event);
666
+ break;
667
+ case "reasoning.completed":
668
+ handleReasoningCompleted(state, event);
669
+ break;
670
+ case "tool_call.started":
671
+ handleToolCallStarted(state, event);
672
+ break;
673
+ case "tool_call.delta":
674
+ handleToolCallDelta(state, event);
675
+ break;
676
+ case "tool_call.completed":
677
+ handleToolCallCompleted(state, event);
678
+ break;
679
+ case "response.completed":
680
+ handleResponseCompleted(state, event);
681
+ break;
682
+ }
683
+ const lastEvent = events[events.length - 1];
684
+ if (!lastEvent || lastEvent.type !== "response.completed") throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
685
+ return buildResponse(state);
686
+ }
687
+ function mergeAuxiliary$1(base, patch) {
688
+ const merged = {
689
+ ...base,
690
+ ...patch
691
+ };
692
+ if (base.providerMetadata || patch.providerMetadata) merged.providerMetadata = {
693
+ ...base.providerMetadata ?? {},
694
+ ...patch.providerMetadata ?? {}
695
+ };
696
+ return merged;
697
+ }
698
+ function pushWarnings(state, warnings) {
699
+ for (const warning of warnings) if (!state.warnings.includes(warning)) state.warnings.push(warning);
700
+ }
701
+ //#endregion
702
+ //#region src/core/collect-stream.ts
703
+ async function collectStream(stream) {
704
+ const events = [];
705
+ for await (const event of stream) events.push(event);
706
+ return aggregateEvents(events);
707
+ }
708
+ //#endregion
709
+ //#region src/helpers/mapping.ts
710
+ /**
711
+ * 常见 provider stop_reason / finish_reason 到 canonical StopReason 的映射表。
712
+ * adapter 可先查此表,未覆盖时走 fallback 规则。
713
+ */
714
+ const STOP_REASON_MAP = {
715
+ stop: "end_turn",
716
+ length: "max_output_tokens",
717
+ content_filter: "content_filter",
718
+ tool_calls: "tool_call",
719
+ end_turn: "end_turn",
720
+ max_tokens: "max_output_tokens",
721
+ tool_use: "tool_call",
722
+ error: "error"
723
+ };
724
+ function mapStopReason(providerReason) {
725
+ return STOP_REASON_MAP[providerReason] ?? "unknown";
726
+ }
727
+ function mapReasoningVisibility(hasThinking, hasRedacted) {
728
+ if (hasRedacted) return "redacted";
729
+ if (hasThinking) return "full";
730
+ return "opaque";
731
+ }
732
+ function textBlock(text) {
733
+ return {
734
+ type: "text",
735
+ text
736
+ };
737
+ }
738
+ function jsonBlock(json) {
739
+ return {
740
+ type: "json",
741
+ json
742
+ };
743
+ }
744
+ function imageBlock(imageUrl) {
745
+ return {
746
+ type: "image",
747
+ imageUrl
748
+ };
749
+ }
750
+ function opaqueBlock(payload) {
751
+ return {
752
+ type: "opaque",
753
+ payload
754
+ };
755
+ }
756
+ function messageItem(content, overrides) {
757
+ return {
758
+ type: "message",
759
+ role: "assistant",
760
+ ...overrides,
761
+ content
762
+ };
763
+ }
764
+ function reasoningItem(content, visibility = "full", id) {
765
+ return {
766
+ type: "reasoning",
767
+ id,
768
+ visibility,
769
+ content
770
+ };
771
+ }
772
+ function toolCallItem(id, name, argumentsText, argumentsJson) {
773
+ return {
774
+ type: "tool_call",
775
+ id,
776
+ name,
777
+ argumentsText,
778
+ argumentsJson
779
+ };
780
+ }
781
+ function toolResultItem(callId, toolName, outcome, content) {
782
+ return {
783
+ type: "tool_result",
784
+ callId,
785
+ toolName,
786
+ outcome,
787
+ content
788
+ };
789
+ }
790
+ function opaqueItem(source, purpose, payload, id) {
791
+ return {
792
+ type: "opaque",
793
+ id,
794
+ source,
795
+ purpose,
796
+ payload
797
+ };
798
+ }
799
+ /**
800
+ * 从 output items 构建标准 replay items。
801
+ * 简单场景下 replay 与 output 一致。
802
+ * 复杂场景(需要 opaque continuation)由 adapter 自行扩展。
803
+ */
804
+ function replayFromOutput(output) {
805
+ return output.map((item) => {
806
+ switch (item.type) {
807
+ case "message":
808
+ case "reasoning":
809
+ case "tool_call": return item;
810
+ case "opaque": return item;
811
+ }
812
+ });
813
+ }
814
+ /**
815
+ * 将单个 ContentBlock 转为纯文本。
816
+ * text 块直接返回文本,json 块序列化,其余返回空串。
817
+ */
818
+ function blockToText(b) {
819
+ if (b.type === "text") return b.text;
820
+ if (b.type === "json") return JSON.stringify(b.json);
821
+ return "";
822
+ }
823
+ /**
824
+ * 将 ContentBlock 数组拼接为纯文本,块间以换行符分隔。
825
+ */
826
+ function contentBlocksToText(blocks) {
827
+ return blocks.map(blockToText).join("\n");
828
+ }
829
+ /**
830
+ * 将 instructions(string | ContentBlock[])归一化为纯文本。
831
+ */
832
+ function instructionsToText(instructions) {
833
+ return typeof instructions === "string" ? instructions : contentBlocksToText(instructions);
834
+ }
835
+ /**
836
+ * 从 OutputItem 数组中提取所有 message 类型 item 的文本内容。
837
+ */
838
+ function extractText(output) {
839
+ return output.filter((item) => item.type === "message").flatMap((m) => m.content).filter((b) => b.type === "text").map((b) => b.text).join("");
840
+ }
841
+ //#endregion
842
+ //#region src/helpers/auxiliary-collector.ts
843
+ var AuxiliaryCollector = class {
844
+ usage = {};
845
+ usageSource;
846
+ billing;
847
+ billingSource;
848
+ providerMetadata = {};
849
+ providerUsage;
850
+ providerBilling;
851
+ warnings = [];
852
+ lookupAttempted = false;
853
+ /**
854
+ * 记录 usage 信息。
855
+ * 后调用的覆盖先调用的(优先级由调用方控制)。
856
+ */
857
+ recordUsage(usage, source, raw) {
858
+ this.usage = {
859
+ ...this.usage,
860
+ ...usage
861
+ };
862
+ this.usageSource = source;
863
+ if (raw !== void 0) this.providerUsage = raw;
864
+ return this;
865
+ }
866
+ /**
867
+ * 记录 billing 信息。
868
+ * 后调用的覆盖先调用的。
869
+ */
870
+ recordBilling(billing, source, raw) {
871
+ this.billing = {
872
+ ...this.billing,
873
+ ...billing
874
+ };
875
+ this.billingSource = source;
876
+ if (raw !== void 0) this.providerBilling = raw;
877
+ return this;
878
+ }
879
+ /**
880
+ * 记录 provider 元数据(非 canonical 的 key-value 信息)。
881
+ */
882
+ recordMetadata(metadata) {
883
+ this.providerMetadata = {
884
+ ...this.providerMetadata,
885
+ ...metadata
886
+ };
887
+ return this;
888
+ }
889
+ /**
890
+ * 记录一条 warning。
891
+ */
892
+ recordWarning(message) {
893
+ this.warnings.push(message);
894
+ return this;
895
+ }
896
+ /**
897
+ * 执行一次有界 follow-up lookup。
898
+ * 最多调用一次;后续调用被忽略。
899
+ * lookup 失败(抛错)仅记录 warning,不传播异常。
900
+ */
901
+ async tryLookup(lookupFn, timeoutMs = 5e3) {
902
+ if (this.lookupAttempted) return;
903
+ this.lookupAttempted = true;
904
+ try {
905
+ const result = await withTimeout(lookupFn(), timeoutMs);
906
+ if (result.usage) this.recordUsage(result.usage, "lookup", result.usage);
907
+ if (result.billing) {
908
+ const bill = {
909
+ ...result.billing,
910
+ source: result.billing?.source ?? "lookup"
911
+ };
912
+ this.recordBilling(bill, "lookup", result.billing);
913
+ }
914
+ if (result.providerMetadata) this.recordMetadata(result.providerMetadata);
915
+ } catch (err) {
916
+ this.recordWarning(`Auxiliary lookup failed: ${err instanceof Error ? err.message : String(err)}`);
917
+ }
918
+ }
919
+ /**
920
+ * 构建最终的 usage / billing / auxiliary。
921
+ * 所有字段均为可选的 — 拿不到就不给。
922
+ */
923
+ build() {
924
+ const result = {};
925
+ if (Object.keys(this.usage).length > 0) result.usage = this.usage;
926
+ if (this.billing) result.billing = this.billing;
927
+ const aux = {};
928
+ if (this.usageSource) aux.usageSource = this.usageSource;
929
+ if (this.billingSource) aux.billingSource = this.billingSource;
930
+ if (this.providerUsage !== void 0) aux.providerUsage = this.providerUsage;
931
+ if (this.providerBilling !== void 0) aux.providerBilling = this.providerBilling;
932
+ if (Object.keys(this.providerMetadata).length > 0) aux.providerMetadata = this.providerMetadata;
933
+ if (Object.keys(aux).length > 0) result.auxiliary = aux;
934
+ if (this.warnings.length > 0) result.warnings = [...this.warnings];
935
+ return result;
936
+ }
937
+ /**
938
+ * 已使用的来源列表(用于 debugging)。
939
+ */
940
+ get sources() {
941
+ return {
942
+ usage: this.usageSource,
943
+ billing: this.billingSource
944
+ };
945
+ }
946
+ };
947
+ function withTimeout(promise, ms) {
948
+ return Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`Lookup timed out after ${ms}ms`)), ms))]);
949
+ }
950
+ //#endregion
951
+ //#region src/helpers/adapter-auxiliary.ts
952
+ var AdapterAuxiliaryState = class {
953
+ request;
954
+ capabilities;
955
+ collector = new AuxiliaryCollector();
956
+ metadataSources = /* @__PURE__ */ new Set();
957
+ constructor(request, capabilities) {
958
+ this.request = request;
959
+ this.capabilities = capabilities;
960
+ }
961
+ recordUsage(usage, source, raw) {
962
+ if (this.request.include?.usage === "off" || isEmptyRecord(usage)) return;
963
+ this.collector.recordUsage(usage, source, raw);
964
+ }
965
+ recordBilling(billing, source, raw) {
966
+ if (this.request.include?.billing === "off" || isEmptyRecord(billing)) return;
967
+ this.collector.recordBilling(billing, source, raw);
968
+ }
969
+ recordProviderMetadata(source, metadata) {
970
+ if (this.request.include?.providerMetadata === "off" || !metadata || isEmptyRecord(metadata)) return;
971
+ this.collector.recordMetadata(metadata);
972
+ this.metadataSources.add(source);
973
+ }
974
+ async finalize(factory, options = {}) {
975
+ if (options.lookup && this.shouldAttemptLookup()) await this.collector.tryLookup(options.lookup, options.lookupTimeoutMs);
976
+ if (this.request.include?.billing !== "off" && options.postprocessBilling) {
977
+ const snapshot = this.collector.build();
978
+ if (!snapshot.billing) {
979
+ const derived = await options.postprocessBilling({
980
+ request: this.request,
981
+ usage: snapshot.usage,
982
+ billing: snapshot.billing,
983
+ auxiliary: snapshot.auxiliary,
984
+ capabilities: this.capabilities
985
+ });
986
+ if (derived && !isEmptyRecord(derived)) this.collector.recordBilling({
987
+ ...derived,
988
+ isEstimated: derived.isEstimated ?? true,
989
+ source: derived.source ?? "derived"
990
+ }, options.postprocessBillingSource ?? "derived", derived);
991
+ }
992
+ }
993
+ const built = this.collector.build();
994
+ const events = [];
995
+ if (built.usage || built.billing || built.auxiliary) events.push(factory.responseAuxiliary({
996
+ usage: built.usage,
997
+ billing: built.billing,
998
+ auxiliary: built.auxiliary
999
+ }));
1000
+ if (this.request.include?.usage !== "off" && !built.usage) events.push(factory.responseWarning("Usage information was not provided by the provider", WarningCode.USAGE_MISSING));
1001
+ if (this.request.include?.billing !== "off") {
1002
+ if (!built.billing) events.push(factory.responseWarning("Billing information was not provided by the provider", WarningCode.BILLING_MISSING));
1003
+ else if (built.billing.isEstimated) events.push(factory.responseWarning("Billing amount is an estimate", WarningCode.BILLING_ESTIMATED));
1004
+ }
1005
+ return {
1006
+ events,
1007
+ usage: built.usage,
1008
+ billing: built.billing,
1009
+ auxiliary: built.auxiliary,
1010
+ warnings: built.warnings,
1011
+ metadataSources: this.metadataSources.size > 0 ? [...this.metadataSources] : void 0
1012
+ };
1013
+ }
1014
+ shouldAttemptLookup() {
1015
+ if (this.request.include?.usage === "off" && this.request.include?.billing === "off" && this.request.include?.providerMetadata === "off") return false;
1016
+ const snapshot = this.collector.build();
1017
+ return this.request.include?.usage !== "off" && !snapshot.usage || this.request.include?.billing !== "off" && !snapshot.billing || this.request.include?.providerMetadata !== "off" && !snapshot.auxiliary?.providerMetadata;
1018
+ }
1019
+ };
1020
+ function emitMalformedStreamWarning(factory, options) {
1021
+ if (options.count < 1) return void 0;
1022
+ return factory.responseWarning(`Skipped ${options.count} malformed ${options.providerLabel} ${options.transportLabel}`, "STREAM_ERROR");
1023
+ }
1024
+ function metadataSourceList(...groups) {
1025
+ const sources = /* @__PURE__ */ new Set();
1026
+ for (const group of groups) {
1027
+ if (!group) continue;
1028
+ for (const source of group) sources.add(source);
1029
+ }
1030
+ return sources.size > 0 ? [...sources] : void 0;
1031
+ }
1032
+ function isEmptyRecord(value) {
1033
+ return Object.keys(value).length === 0;
1034
+ }
1035
+ //#endregion
1036
+ //#region src/helpers/adapter-base.ts
1037
+ var AdapterBase = class {
1038
+ /**
1039
+ * stream 模板方法:
1040
+ * 1. 创建事件工厂,发射 response.started
1041
+ * 2. 构建 provider 请求
1042
+ * 3. 委托 runStream 发射全部流事件(含 response.completed)
1043
+ */
1044
+ async *stream(request) {
1045
+ const factory = createEventFactory({
1046
+ responseId: request.requestId,
1047
+ backend: {
1048
+ kind: this.kind,
1049
+ isSynthetic: !this.capabilities.nativeStreaming
1050
+ }
1051
+ });
1052
+ yield factory.responseStarted(request.model);
1053
+ try {
1054
+ const providerRequest = await this.buildRequest(request);
1055
+ yield* this.runStream(providerRequest, factory, request);
1056
+ } catch (err) {
1057
+ if (err instanceof AIRequestError || err instanceof AIStreamError || err instanceof AIMappingError) throw err;
1058
+ yield factory.responseWarning(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
1059
+ yield factory.responseCompleted(this.buildResponse(request, {
1060
+ output: [],
1061
+ replay: []
1062
+ }, factory));
1063
+ }
1064
+ }
1065
+ /**
1066
+ * 从 StreamResult 构建完整 AIResponse。
1067
+ * 子类可在返回前自定义覆盖。
1068
+ */
1069
+ buildResponse(request, result, _factory) {
1070
+ const text = this.extractText(result.output);
1071
+ const warnings = mergeWarnings(result.warnings, _factory.warnings);
1072
+ const auxiliary = mergeAuxiliary(result.auxiliary, result.providerMetadata ? { providerMetadata: result.providerMetadata } : void 0);
1073
+ return {
1074
+ id: request.requestId,
1075
+ output: result.output,
1076
+ replay: result.replay,
1077
+ text,
1078
+ toolCalls: result.output.filter((item) => item.type === "tool_call"),
1079
+ stopReason: result.stopReason,
1080
+ usage: result.usage,
1081
+ billing: result.billing,
1082
+ auxiliary,
1083
+ warnings,
1084
+ backend: {
1085
+ requestId: request.requestId,
1086
+ rawResponseId: result.rawResponseId,
1087
+ adapter: this.kind,
1088
+ isSyntheticStream: !this.capabilities.nativeStreaming,
1089
+ metadataSources: result.metadataSources,
1090
+ warnings
1091
+ }
1092
+ };
1093
+ }
1094
+ /** 从 output items 中提取文本内容。 */
1095
+ extractText(output) {
1096
+ return extractText(output);
1097
+ }
1098
+ createAuxiliaryState(request) {
1099
+ return new AdapterAuxiliaryState(request, this.capabilities);
1100
+ }
1101
+ };
1102
+ function mergeAuxiliary(base, patch) {
1103
+ if (!base && !patch) return void 0;
1104
+ const merged = {
1105
+ ...base ?? {},
1106
+ ...patch ?? {}
1107
+ };
1108
+ if (base?.providerMetadata || patch?.providerMetadata) merged.providerMetadata = {
1109
+ ...base?.providerMetadata ?? {},
1110
+ ...patch?.providerMetadata ?? {}
1111
+ };
1112
+ return merged;
1113
+ }
1114
+ function mergeWarnings(...groups) {
1115
+ const merged = [];
1116
+ for (const group of groups) {
1117
+ if (!group) continue;
1118
+ for (const warning of group) if (!merged.includes(warning)) merged.push(warning);
1119
+ }
1120
+ return merged.length > 0 ? merged : void 0;
1121
+ }
1122
+ //#endregion
1123
+ //#region src/helpers/sse-parser.ts
1124
+ /**
1125
+ * 将 SSE 文本块解析为事件数组。
1126
+ * 累积事件行直到遇到空行,支持 [DONE] 标记。
1127
+ * 返回已解析的事件和未处理的剩余 buffer(用于增量解析)。
1128
+ *
1129
+ * 关键行为:
1130
+ * - 只解析完整的 event(以空行结尾)
1131
+ * - 未完成的行保留在 rest 中,等待下次 chunk 补全
1132
+ * - 支持跨 chunk 的 event 分片
1133
+ */
1134
+ function parseSSEEvents(chunk) {
1135
+ const events = [];
1136
+ let eventType = "";
1137
+ let dataLines = [];
1138
+ let consumedUntil = 0;
1139
+ let cursor = 0;
1140
+ let malformedEvents = 0;
1141
+ while (cursor < chunk.length) {
1142
+ const lineEnd = chunk.indexOf("\n", cursor);
1143
+ if (lineEnd === -1) break;
1144
+ let line = chunk.slice(cursor, lineEnd);
1145
+ cursor = lineEnd + 1;
1146
+ if (line.endsWith("\r")) line = line.slice(0, -1);
1147
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1148
+ else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
1149
+ else if (line === "" && eventType && dataLines.length > 0) {
1150
+ const dataStr = dataLines.join("\n");
1151
+ if (dataStr === "[DONE]") {
1152
+ eventType = "";
1153
+ dataLines = [];
1154
+ consumedUntil = cursor;
1155
+ continue;
1156
+ }
1157
+ try {
1158
+ const data = JSON.parse(dataStr);
1159
+ events.push({
1160
+ type: eventType,
1161
+ data
1162
+ });
1163
+ } catch {
1164
+ malformedEvents++;
1165
+ }
1166
+ eventType = "";
1167
+ dataLines = [];
1168
+ consumedUntil = cursor;
1169
+ } else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = cursor;
1170
+ }
1171
+ return {
1172
+ events,
1173
+ rest: chunk.slice(consumedUntil),
1174
+ malformedEvents
1175
+ };
1176
+ }
1177
+ //#endregion
1178
+ //#region src/adapters/responses.ts
1179
+ /**
1180
+ * Responses Adapter
1181
+ *
1182
+ * 接入 OpenAI Responses API (responses 端点)。
1183
+ * 职责分层:
1184
+ * 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
1185
+ * 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
1186
+ *
1187
+ * 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
1188
+ */
1189
+ function ensureResponsesTextBlocks(blocks, field) {
1190
+ for (let i = 0; i < blocks.length; i++) {
1191
+ const block = blocks[i];
1192
+ if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`responses does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
1193
+ }
1194
+ return blocks;
1195
+ }
1196
+ function ensureResponsesReasoningBlocks(blocks, field) {
1197
+ return blocks.map((block, index) => {
1198
+ if (block.type !== "text") throw new AIRequestError(`responses does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`, "UNSUPPORTED_CONTENT_BLOCK");
1199
+ return block;
1200
+ });
1201
+ }
1202
+ function instructionsToResponsesText(instructions) {
1203
+ return typeof instructions === "string" ? instructions : contentBlocksToText(ensureResponsesTextBlocks(instructions, "instructions"));
1204
+ }
1205
+ function assertResponsesToolResultOutcome(outcome) {
1206
+ if (outcome !== "success") throw new AIRequestError(`responses does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1207
+ }
1208
+ function parseSSE(chunk) {
1209
+ const result = parseSSEEvents(chunk);
1210
+ return {
1211
+ events: result.events,
1212
+ rest: result.rest,
1213
+ malformedEvents: result.malformedEvents
1214
+ };
1215
+ }
1216
+ function isReplayCanonicalInput(item) {
1217
+ return item.type === "message" && item.role === "assistant" || item.type === "reasoning" || item.type === "function_call";
1218
+ }
1219
+ function rollbackTrailingReplayCanonicalItems(input) {
1220
+ while (input.length > 0) {
1221
+ const last = input[input.length - 1];
1222
+ if (!last || !isReplayCanonicalInput(last)) break;
1223
+ input.pop();
1224
+ }
1225
+ }
1226
+ function canonicalToResponsesBlock(b) {
1227
+ if (b.type === "text") return {
1228
+ type: "text",
1229
+ text: b.text
1230
+ };
1231
+ if (b.type === "json") return {
1232
+ type: "text",
1233
+ text: JSON.stringify(b.json)
1234
+ };
1235
+ throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
1236
+ }
1237
+ var ResponsesAdapter = class extends AdapterBase {
1238
+ kind = "responses";
1239
+ capabilities = CAPABILITY_MATRIX.responses;
1240
+ apiKey;
1241
+ baseUrl;
1242
+ fetchFn;
1243
+ constructor(options) {
1244
+ super();
1245
+ this.apiKey = options.apiKey;
1246
+ this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
1247
+ this.fetchFn = options.fetch ?? globalThis.fetch;
1248
+ }
1249
+ buildRequest(request) {
1250
+ const input = [];
1251
+ for (const item of request.input) switch (item.type) {
1252
+ case "message":
1253
+ if (item.role === "assistant") {
1254
+ const blocks = ensureResponsesTextBlocks(item.content, `assistant message (${item.role}) content`).map(canonicalToResponsesBlock);
1255
+ input.push({
1256
+ type: "message",
1257
+ role: item.role,
1258
+ content: blocks
1259
+ });
1260
+ } else input.push({
1261
+ type: "message",
1262
+ role: item.role,
1263
+ content: contentBlocksToText(ensureResponsesTextBlocks(item.content, `input message (${item.role}) content`))
1264
+ });
1265
+ break;
1266
+ case "reasoning": {
1267
+ const blocks = ensureResponsesReasoningBlocks(item.content, "reasoning content").map((b) => ({
1268
+ type: "reasoning",
1269
+ text: b.text
1270
+ }));
1271
+ input.push({
1272
+ type: "reasoning",
1273
+ content: blocks
1274
+ });
1275
+ break;
1276
+ }
1277
+ case "tool_call":
1278
+ input.push({
1279
+ type: "function_call",
1280
+ id: item.id,
1281
+ name: item.name,
1282
+ arguments: item.argumentsText
1283
+ });
1284
+ break;
1285
+ case "tool_result": {
1286
+ assertResponsesToolResultOutcome(item.outcome);
1287
+ const output = ensureResponsesTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1288
+ input.push({
1289
+ type: "function_call_output",
1290
+ call_id: item.callId,
1291
+ output
1292
+ });
1293
+ break;
1294
+ }
1295
+ case "opaque":
1296
+ if (item.source === "responses" && item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null && "id" in item.payload) {
1297
+ const { id } = item.payload;
1298
+ if (typeof id === "string") {
1299
+ rollbackTrailingReplayCanonicalItems(input);
1300
+ input.push({
1301
+ type: "item_reference",
1302
+ id
1303
+ });
1304
+ }
1305
+ }
1306
+ break;
1307
+ }
1308
+ const body = {
1309
+ model: request.model,
1310
+ input,
1311
+ stream: true
1312
+ };
1313
+ if (request.instructions) body.instructions = instructionsToResponsesText(request.instructions);
1314
+ if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
1315
+ type: "function",
1316
+ name: t.name,
1317
+ description: t.description,
1318
+ input_schema: t.inputSchema
1319
+ }));
1320
+ if (request.toolChoice) {
1321
+ if (request.toolChoice === "auto") body.tool_choice = "auto";
1322
+ else if (request.toolChoice === "none") body.tool_choice = "none";
1323
+ else if (request.toolChoice.type === "tool") body.tool_choice = {
1324
+ type: "function",
1325
+ name: request.toolChoice.name
1326
+ };
1327
+ }
1328
+ if (request.temperature !== void 0) body.temperature = request.temperature;
1329
+ if (request.maxOutputTokens !== void 0) body.max_output_tokens = request.maxOutputTokens;
1330
+ if (request.metadata) body.metadata = request.metadata;
1331
+ return body;
1332
+ }
1333
+ async *runStream(providerRequest, factory, request) {
1334
+ const auxiliary = this.createAuxiliaryState(request);
1335
+ const response = await this.fetchFn(`${this.baseUrl}/responses`, {
1336
+ method: "POST",
1337
+ headers: {
1338
+ "Content-Type": "application/json",
1339
+ Authorization: `Bearer ${this.apiKey}`
1340
+ },
1341
+ body: JSON.stringify(providerRequest)
1342
+ });
1343
+ if (!response.ok) {
1344
+ const errorText = await response.text().catch(() => "unknown error");
1345
+ throw new Error(`Responses API error ${response.status}: ${errorText}`);
1346
+ }
1347
+ const reader = response.body?.getReader();
1348
+ if (!reader) throw new Error("Response body is not readable");
1349
+ const output = [];
1350
+ const decoder = new TextDecoder();
1351
+ let buffer = "";
1352
+ let completedResponse;
1353
+ try {
1354
+ while (true) {
1355
+ const { done, value } = await reader.read();
1356
+ if (done) break;
1357
+ buffer += decoder.decode(value, { stream: true });
1358
+ const { events, rest, malformedEvents } = parseSSE(buffer);
1359
+ buffer = rest;
1360
+ const malformedWarning = emitMalformedStreamWarning(factory, {
1361
+ count: malformedEvents,
1362
+ providerLabel: "Responses",
1363
+ transportLabel: "SSE event(s)"
1364
+ });
1365
+ if (malformedWarning) yield malformedWarning;
1366
+ for (const sseEvent of events) {
1367
+ if (sseEvent.type === "error") {
1368
+ yield factory.responseWarning(sseEvent.data.message, sseEvent.data.code);
1369
+ continue;
1370
+ }
1371
+ if (sseEvent.type === "response.output_item.added") {
1372
+ const item = sseEvent.data.item;
1373
+ switch (item.type) {
1374
+ case "message":
1375
+ yield factory.messageStarted(item.id);
1376
+ break;
1377
+ case "reasoning":
1378
+ yield factory.reasoningStarted(item.id, "full");
1379
+ break;
1380
+ case "function_call":
1381
+ yield factory.toolCallStarted(item.id, item.name ?? "unknown");
1382
+ break;
1383
+ }
1384
+ continue;
1385
+ }
1386
+ if (sseEvent.type === "response.output_text.delta") {
1387
+ yield factory.messageDelta(sseEvent.data.item_id, sseEvent.data.delta);
1388
+ continue;
1389
+ }
1390
+ if (sseEvent.type === "response.output_text.done") {
1391
+ yield factory.messageCompleted(messageItem([textBlock(sseEvent.data.text)], { id: sseEvent.data.item_id }));
1392
+ output.push(messageItem([textBlock(sseEvent.data.text)], { id: sseEvent.data.item_id }));
1393
+ continue;
1394
+ }
1395
+ if (sseEvent.type === "response.reasoning.delta") {
1396
+ yield factory.reasoningDelta(sseEvent.data.item_id, textBlock(sseEvent.data.delta));
1397
+ continue;
1398
+ }
1399
+ if (sseEvent.type === "response.reasoning.done") {
1400
+ yield factory.reasoningCompleted(reasoningItem([textBlock(sseEvent.data.text)], "full", sseEvent.data.item_id));
1401
+ output.push(reasoningItem([textBlock(sseEvent.data.text)], "full", sseEvent.data.item_id));
1402
+ continue;
1403
+ }
1404
+ if (sseEvent.type === "response.tool_call.delta") {
1405
+ if (sseEvent.data.delta.arguments) yield factory.toolCallDelta(sseEvent.data.item_id, { argumentsText: sseEvent.data.delta.arguments });
1406
+ continue;
1407
+ }
1408
+ if (sseEvent.type === "response.tool_call.done") {
1409
+ const tcItem = toolCallItem(sseEvent.data.item_id, sseEvent.data.name ?? "unknown", sseEvent.data.arguments ?? "");
1410
+ yield factory.toolCallCompleted(tcItem);
1411
+ output.push(tcItem);
1412
+ continue;
1413
+ }
1414
+ if (sseEvent.type === "response.completed") completedResponse = sseEvent.data.response;
1415
+ }
1416
+ }
1417
+ } finally {
1418
+ reader.releaseLock();
1419
+ }
1420
+ if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
1421
+ let rawResponseId;
1422
+ if (completedResponse) {
1423
+ rawResponseId = completedResponse.id;
1424
+ if (completedResponse.usage) auxiliary.recordUsage({
1425
+ inputTokens: completedResponse.usage.input_tokens,
1426
+ outputTokens: completedResponse.usage.output_tokens,
1427
+ totalTokens: completedResponse.usage.total_tokens
1428
+ }, "final", completedResponse.usage);
1429
+ }
1430
+ const replay = [...replayFromOutput(output)];
1431
+ if (completedResponse?.id) replay.push(opaqueItem("responses", "replay", { id: completedResponse.id }));
1432
+ const stopReason = completedResponse ? this.inferStopReason(completedResponse) : void 0;
1433
+ const auxiliaryResult = await auxiliary.finalize(factory);
1434
+ for (const event of auxiliaryResult.events) yield event;
1435
+ yield factory.responseCompleted(this.buildResponse(request, {
1436
+ output,
1437
+ replay,
1438
+ stopReason,
1439
+ usage: auxiliaryResult.usage,
1440
+ billing: auxiliaryResult.billing,
1441
+ auxiliary: auxiliaryResult.auxiliary,
1442
+ warnings: auxiliaryResult.warnings,
1443
+ metadataSources: auxiliaryResult.metadataSources,
1444
+ rawResponseId
1445
+ }, factory));
1446
+ }
1447
+ inferStopReason(response) {
1448
+ const output = response.output;
1449
+ if (!output || output.length === 0) return "unknown";
1450
+ if (output.some((item) => item.type === "function_call")) return "tool_call";
1451
+ if (output[output.length - 1]?.status === "incomplete") return "max_output_tokens";
1452
+ return "end_turn";
1453
+ }
1454
+ };
1455
+ //#endregion
1456
+ //#region src/adapters/messages.ts
1457
+ /**
1458
+ * Messages Adapter
1459
+ *
1460
+ * 接入 Anthropic Messages API (messages 端点)。
1461
+ * 支持:
1462
+ * - 文本消息流 (text content block)
1463
+ * - 思维链流 (thinking content block)
1464
+ * - 工具调用流 (tool_use content block)
1465
+ * - 高保真 replay(含 opaque continuation)
1466
+ * - 能力降级 warning
1467
+ */
1468
+ function ensureMessagesTextBlocks(blocks, field) {
1469
+ for (let i = 0; i < blocks.length; i++) {
1470
+ const block = blocks[i];
1471
+ if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`messages does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
1472
+ }
1473
+ return blocks;
1474
+ }
1475
+ function ensureMessagesReasoningBlocks(blocks, field) {
1476
+ return blocks.map((block, index) => {
1477
+ if (block.type !== "text") throw new AIRequestError(`messages does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`, "UNSUPPORTED_CONTENT_BLOCK");
1478
+ return block;
1479
+ });
1480
+ }
1481
+ function instructionsToMessagesText(instructions) {
1482
+ return typeof instructions === "string" ? instructions : contentBlocksToText(ensureMessagesTextBlocks(instructions, "instructions"));
1483
+ }
1484
+ function assertMessagesToolResultOutcome(outcome) {
1485
+ if (outcome === "rejected") throw new AIRequestError("messages does not preserve tool_result outcome \"rejected\"; only \"success\" and \"error\" are supported", "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1486
+ }
1487
+ function parseMessagesSSE(chunk) {
1488
+ const result = parseSSEEvents(chunk);
1489
+ return {
1490
+ events: result.events,
1491
+ rest: result.rest,
1492
+ malformedEvents: result.malformedEvents
1493
+ };
1494
+ }
1495
+ function rollbackTrailingAssistantMessages$2(messages) {
1496
+ while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
1497
+ }
1498
+ function parseToolUseInput(input) {
1499
+ try {
1500
+ const parsed = JSON.parse(input);
1501
+ return parsed && typeof parsed === "object" ? parsed : {};
1502
+ } catch {
1503
+ return {};
1504
+ }
1505
+ }
1506
+ function canonicalToMessagesBlock(b) {
1507
+ if (b.type === "text") return {
1508
+ type: "text",
1509
+ text: b.text
1510
+ };
1511
+ if (b.type === "json") return {
1512
+ type: "text",
1513
+ text: JSON.stringify(b.json)
1514
+ };
1515
+ throw new AIRequestError(`messages does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
1516
+ }
1517
+ function pickProviderHeaders(headers) {
1518
+ const metadata = {};
1519
+ headers.forEach((value, key) => {
1520
+ const normalizedKey = key.toLowerCase();
1521
+ if (normalizedKey === "request-id" || normalizedKey === "x-request-id" || normalizedKey === "anthropic-organization-id" || normalizedKey === "anthropic-beta" || normalizedKey === "retry-after" || normalizedKey.startsWith("anthropic-ratelimit-")) metadata[normalizedKey] = value;
1522
+ });
1523
+ return metadata;
1524
+ }
1525
+ function buildStreamMetadata(options) {
1526
+ const { apiVersion, message, stopReason, stopSequence } = options;
1527
+ const metadata = { apiVersion };
1528
+ if (message) metadata.message = {
1529
+ id: message.id,
1530
+ type: message.type,
1531
+ role: message.role,
1532
+ model: message.model
1533
+ };
1534
+ if (stopReason !== void 0 || stopSequence !== void 0) metadata.stop = {
1535
+ reason: stopReason,
1536
+ sequence: stopSequence
1537
+ };
1538
+ return metadata;
1539
+ }
1540
+ var MessagesAdapter = class extends AdapterBase {
1541
+ kind = "messages";
1542
+ capabilities = CAPABILITY_MATRIX.messages;
1543
+ apiKey;
1544
+ apiVersion;
1545
+ baseUrl;
1546
+ fetchFn;
1547
+ warningAccumulator;
1548
+ constructor(options) {
1549
+ super();
1550
+ this.apiKey = options.apiKey;
1551
+ this.apiVersion = options.apiVersion ?? "2023-06-01";
1552
+ this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
1553
+ this.fetchFn = options.fetch ?? globalThis.fetch;
1554
+ this.warningAccumulator = [];
1555
+ }
1556
+ warn(message, _code) {
1557
+ this.warningAccumulator.push(message);
1558
+ }
1559
+ buildRequest(request) {
1560
+ const messages = [];
1561
+ let systemPrompt;
1562
+ if (request.instructions) systemPrompt = instructionsToMessagesText(request.instructions);
1563
+ for (const item of request.input) switch (item.type) {
1564
+ case "message": {
1565
+ if (item.role === "system" || item.role === "developer") {
1566
+ const text = contentBlocksToText(ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`));
1567
+ systemPrompt = systemPrompt ? `${systemPrompt}\n${text}` : text;
1568
+ break;
1569
+ }
1570
+ const role = item.role === "user" ? "user" : "assistant";
1571
+ const supportedContent = ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`);
1572
+ if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
1573
+ role,
1574
+ content: supportedContent[0].text
1575
+ });
1576
+ else messages.push({
1577
+ role,
1578
+ content: supportedContent.map(canonicalToMessagesBlock)
1579
+ });
1580
+ break;
1581
+ }
1582
+ case "tool_call": {
1583
+ const lastMsg = messages[messages.length - 1];
1584
+ const toolBlock = {
1585
+ type: "tool_use",
1586
+ id: item.id,
1587
+ name: item.name,
1588
+ input: item.argumentsJson ?? parseToolUseInput(item.argumentsText)
1589
+ };
1590
+ if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(toolBlock);
1591
+ else messages.push({
1592
+ role: "assistant",
1593
+ content: [toolBlock]
1594
+ });
1595
+ break;
1596
+ }
1597
+ case "tool_result": {
1598
+ assertMessagesToolResultOutcome(item.outcome);
1599
+ const content = ensureMessagesTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1600
+ const block = {
1601
+ type: "tool_result",
1602
+ tool_use_id: item.callId,
1603
+ content,
1604
+ is_error: item.outcome === "error"
1605
+ };
1606
+ messages.push({
1607
+ role: "user",
1608
+ content: [block]
1609
+ });
1610
+ break;
1611
+ }
1612
+ case "reasoning": {
1613
+ const block = {
1614
+ type: "thinking",
1615
+ thinking: contentBlocksToText(ensureMessagesReasoningBlocks(item.content, "reasoning content"))
1616
+ };
1617
+ const lastMsg = messages[messages.length - 1];
1618
+ if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
1619
+ else messages.push({
1620
+ role: "assistant",
1621
+ content: [block]
1622
+ });
1623
+ break;
1624
+ }
1625
+ case "opaque":
1626
+ if (item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
1627
+ const payload = item.payload;
1628
+ if (payload.role === "assistant" && Array.isArray(payload.content)) {
1629
+ if (payload.content.every((b) => typeof b === "object" && b !== null && "type" in b && (b.type === "text" || b.type === "thinking" || b.type === "redacted_thinking" || b.type === "tool_use" || b.type === "tool_result"))) {
1630
+ rollbackTrailingAssistantMessages$2(messages);
1631
+ messages.push({
1632
+ role: "assistant",
1633
+ content: payload.content
1634
+ });
1635
+ }
1636
+ }
1637
+ }
1638
+ break;
1639
+ }
1640
+ const body = {
1641
+ model: request.model,
1642
+ max_tokens: request.maxOutputTokens ?? 4096,
1643
+ messages,
1644
+ stream: true
1645
+ };
1646
+ if (systemPrompt) body.system = systemPrompt;
1647
+ if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
1648
+ name: t.name,
1649
+ description: t.description,
1650
+ input_schema: t.inputSchema
1651
+ }));
1652
+ if (request.toolChoice) {
1653
+ if (request.toolChoice === "auto") body.tool_choice = { type: "auto" };
1654
+ else if (request.toolChoice === "none") body.tool_choice = { type: "none" };
1655
+ else if (request.toolChoice.type === "tool") body.tool_choice = {
1656
+ type: "tool",
1657
+ name: request.toolChoice.name
1658
+ };
1659
+ }
1660
+ if (request.temperature !== void 0) body.temperature = request.temperature;
1661
+ return body;
1662
+ }
1663
+ async *runStream(providerRequest, factory, request) {
1664
+ this.warningAccumulator = [];
1665
+ const auxiliary = this.createAuxiliaryState(request);
1666
+ if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Messages adapter", "UNSUPPORTED_METADATA");
1667
+ const response = await this.fetchFn(`${this.baseUrl}/messages`, {
1668
+ method: "POST",
1669
+ headers: {
1670
+ "Content-Type": "application/json",
1671
+ "x-api-key": this.apiKey,
1672
+ "anthropic-version": this.apiVersion
1673
+ },
1674
+ body: JSON.stringify(providerRequest)
1675
+ });
1676
+ if (!response.ok) {
1677
+ const errorText = await response.text().catch(() => "unknown error");
1678
+ throw new Error(`Messages API error ${response.status}: ${errorText}`);
1679
+ }
1680
+ const reader = response.body?.getReader();
1681
+ if (!reader) throw new Error("Response body is not readable");
1682
+ const output = [];
1683
+ const decoder = new TextDecoder();
1684
+ let buffer = "";
1685
+ let messageResponse;
1686
+ let currentContentBlockIndex = -1;
1687
+ let currentItemType = null;
1688
+ let currentItemId = "";
1689
+ let currentToolName = "";
1690
+ let currentArgsText = "";
1691
+ let currentThinkingVisibility = "full";
1692
+ let hasStreamedReasoning = false;
1693
+ const rawReplayContent = [];
1694
+ let textBuffer = "";
1695
+ let thinkingBuffer = "";
1696
+ let argsBuffer = "";
1697
+ let stopReason;
1698
+ let stopSequence;
1699
+ let rawResponseId;
1700
+ if (request.include?.providerMetadata !== "off") {
1701
+ const headerMetadata = pickProviderHeaders(response.headers);
1702
+ auxiliary.recordProviderMetadata("header", Object.keys(headerMetadata).length > 0 ? { headers: headerMetadata } : void 0);
1703
+ }
1704
+ try {
1705
+ while (true) {
1706
+ const { done, value } = await reader.read();
1707
+ if (done) break;
1708
+ buffer += decoder.decode(value, { stream: true });
1709
+ const { events, rest, malformedEvents } = parseMessagesSSE(buffer);
1710
+ buffer = rest;
1711
+ const malformedWarning = emitMalformedStreamWarning(factory, {
1712
+ count: malformedEvents,
1713
+ providerLabel: "Messages",
1714
+ transportLabel: "SSE event(s)"
1715
+ });
1716
+ if (malformedWarning) yield malformedWarning;
1717
+ for (const sseEvent of events) switch (sseEvent.type) {
1718
+ case "ping": continue;
1719
+ case "error": {
1720
+ const err = sseEvent.data.error;
1721
+ yield factory.responseWarning(err.message, err.type);
1722
+ this.warn(err.message, err.type);
1723
+ continue;
1724
+ }
1725
+ case "message_start":
1726
+ messageResponse = sseEvent.data.message;
1727
+ rawResponseId = messageResponse.id;
1728
+ if (messageResponse.content.some((b) => b.type === "thinking" || b.type === "redacted_thinking")) hasStreamedReasoning = true;
1729
+ continue;
1730
+ case "content_block_start": {
1731
+ const block = sseEvent.data.content_block;
1732
+ currentContentBlockIndex = sseEvent.data.index;
1733
+ switch (block.type) {
1734
+ case "text":
1735
+ currentItemType = "message";
1736
+ currentItemId = `msg-${block.type}-${currentContentBlockIndex}`;
1737
+ textBuffer = "";
1738
+ yield factory.messageStarted(currentItemId);
1739
+ break;
1740
+ case "thinking":
1741
+ hasStreamedReasoning = true;
1742
+ currentItemType = "reasoning";
1743
+ currentItemId = `reason-${currentContentBlockIndex}`;
1744
+ currentThinkingVisibility = "full";
1745
+ thinkingBuffer = "";
1746
+ yield factory.reasoningStarted(currentItemId, "full");
1747
+ break;
1748
+ case "redacted_thinking": {
1749
+ hasStreamedReasoning = true;
1750
+ currentItemType = "reasoning";
1751
+ currentItemId = `reason-redacted-${currentContentBlockIndex}`;
1752
+ currentThinkingVisibility = "redacted";
1753
+ const data = block.data;
1754
+ yield factory.reasoningStarted(currentItemId, "redacted");
1755
+ yield factory.reasoningDelta(currentItemId, textBlock(data));
1756
+ const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
1757
+ yield factory.reasoningCompleted(redactedItem);
1758
+ output.push(redactedItem);
1759
+ rawReplayContent.push({
1760
+ type: "redacted_thinking",
1761
+ data
1762
+ });
1763
+ currentItemType = null;
1764
+ break;
1765
+ }
1766
+ case "tool_use": {
1767
+ const tuBlock = block;
1768
+ currentItemType = "tool_call";
1769
+ currentItemId = tuBlock.id;
1770
+ currentToolName = tuBlock.name;
1771
+ currentArgsText = "";
1772
+ argsBuffer = "";
1773
+ yield factory.toolCallStarted(currentItemId, currentToolName);
1774
+ break;
1775
+ }
1776
+ }
1777
+ continue;
1778
+ }
1779
+ case "content_block_delta": {
1780
+ const delta = sseEvent.data.delta;
1781
+ switch (delta.type) {
1782
+ case "text_delta":
1783
+ if (currentItemType === "message" && currentItemId) {
1784
+ const txt = delta.text;
1785
+ textBuffer += txt;
1786
+ yield factory.messageDelta(currentItemId, txt);
1787
+ }
1788
+ break;
1789
+ case "thinking_delta":
1790
+ if (currentItemType === "reasoning" && currentItemId) {
1791
+ const txt = delta.thinking;
1792
+ thinkingBuffer += txt;
1793
+ yield factory.reasoningDelta(currentItemId, textBlock(txt));
1794
+ }
1795
+ break;
1796
+ case "input_json_delta":
1797
+ if (currentItemType === "tool_call" && currentItemId) {
1798
+ const partial = delta.partial_json;
1799
+ argsBuffer += partial;
1800
+ yield factory.toolCallDelta(currentItemId, { argumentsText: partial });
1801
+ }
1802
+ break;
1803
+ }
1804
+ continue;
1805
+ }
1806
+ case "content_block_stop":
1807
+ if (currentItemType === "message" && currentItemId) {
1808
+ yield factory.messageCompleted(messageItem([textBlock(textBuffer)], { id: currentItemId }));
1809
+ output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
1810
+ rawReplayContent.push({
1811
+ type: "text",
1812
+ text: textBuffer
1813
+ });
1814
+ } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
1815
+ yield factory.reasoningCompleted(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
1816
+ output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
1817
+ rawReplayContent.push({
1818
+ type: "thinking",
1819
+ thinking: thinkingBuffer
1820
+ });
1821
+ } else if (currentItemType === "tool_call" && currentItemId) {
1822
+ const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
1823
+ yield factory.toolCallCompleted(tcItem);
1824
+ output.push(tcItem);
1825
+ rawReplayContent.push({
1826
+ type: "tool_use",
1827
+ id: currentItemId,
1828
+ name: currentToolName,
1829
+ input: parseToolUseInput(currentArgsText || argsBuffer)
1830
+ });
1831
+ }
1832
+ currentItemType = null;
1833
+ currentItemId = "";
1834
+ continue;
1835
+ case "message_delta": {
1836
+ stopReason = sseEvent.data.delta.stop_reason;
1837
+ stopSequence = sseEvent.data.delta.stop_sequence;
1838
+ const u = sseEvent.data.usage;
1839
+ if (u) auxiliary.recordUsage({
1840
+ inputTokens: u.input_tokens,
1841
+ outputTokens: u.output_tokens,
1842
+ totalTokens: u.input_tokens + u.output_tokens
1843
+ }, "stream", u);
1844
+ continue;
1845
+ }
1846
+ case "message_stop": break;
1847
+ }
1848
+ }
1849
+ } finally {
1850
+ reader.releaseLock();
1851
+ }
1852
+ if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
1853
+ const replay = [...replayFromOutput(output)];
1854
+ if (messageResponse) {
1855
+ const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
1856
+ replay.push(opaqueItem("messages", "replay", {
1857
+ replaceCanonical: true,
1858
+ role: messageResponse.role,
1859
+ content: replayContent,
1860
+ messageId: messageResponse.id,
1861
+ stopReason: stopReason ?? messageResponse.stop_reason
1862
+ }));
1863
+ }
1864
+ if (request.include?.providerMetadata !== "off") auxiliary.recordProviderMetadata("stream", buildStreamMetadata({
1865
+ apiVersion: this.apiVersion,
1866
+ message: messageResponse,
1867
+ stopReason,
1868
+ stopSequence
1869
+ }));
1870
+ if (!hasStreamedReasoning) {}
1871
+ const auxiliaryResult = await auxiliary.finalize(factory);
1872
+ for (const event of auxiliaryResult.events) yield event;
1873
+ yield factory.responseCompleted(this.buildResponse(request, {
1874
+ output,
1875
+ replay,
1876
+ stopReason: stopReason ? mapStopReason(stopReason) : void 0,
1877
+ usage: auxiliaryResult.usage,
1878
+ billing: auxiliaryResult.billing,
1879
+ auxiliary: auxiliaryResult.auxiliary,
1880
+ warnings: auxiliaryResult.warnings,
1881
+ metadataSources: auxiliaryResult.metadataSources,
1882
+ rawResponseId
1883
+ }, factory));
1884
+ }
1885
+ };
1886
+ //#endregion
1887
+ //#region src/adapters/chat-completions.ts
1888
+ /**
1889
+ * Chat Completions Adapter
1890
+ *
1891
+ * 接入 OpenAI Chat Completions API (chat/completions 端点)。
1892
+ * 弱能力兼容层:
1893
+ * - third-party reasoning 字段仅做 best-effort 提取
1894
+ * - 工具调用通常整块到达(非逐 token 流)
1895
+ * - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
1896
+ */
1897
+ const REASONING_FIELDS = ["reasoning_content", "reasoning"];
1898
+ function assertChatToolResultOutcome(outcome) {
1899
+ if (outcome !== "success") throw new AIRequestError(`chat-completions does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1900
+ }
1901
+ /**
1902
+ * Chat Completions 的简化 SSE 解析器。
1903
+ *
1904
+ * 约束:
1905
+ * - 每条 `data:` 行必须已经是一个完整 JSON 对象
1906
+ * - 允许传输层把单行拆成多个 chunk,但不接受 provider 把一个 JSON event 改写成多条 `data:` 行
1907
+ */
1908
+ function parseChatSSE(buffer) {
1909
+ const chunks = [];
1910
+ let rest = buffer;
1911
+ let malformedEvents = 0;
1912
+ while (true) {
1913
+ const lineEnd = rest.indexOf("\n");
1914
+ if (lineEnd === -1) break;
1915
+ const line = rest.slice(0, lineEnd).trim();
1916
+ rest = rest.slice(lineEnd + 1);
1917
+ if (!line.startsWith("data: ")) continue;
1918
+ const data = line.slice(6).trim();
1919
+ if (data === "[DONE]") continue;
1920
+ try {
1921
+ chunks.push(JSON.parse(data));
1922
+ } catch {
1923
+ malformedEvents++;
1924
+ }
1925
+ }
1926
+ return {
1927
+ chunks,
1928
+ rest,
1929
+ malformedEvents
1930
+ };
1931
+ }
1932
+ function ensureTextCompatibleBlocks(blocks, field) {
1933
+ for (let i = 0; i < blocks.length; i++) {
1934
+ const block = blocks[i];
1935
+ if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`chat-completions does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
1936
+ }
1937
+ return blocks;
1938
+ }
1939
+ function contentBlocksToChatText(blocks, field) {
1940
+ return contentBlocksToText(ensureTextCompatibleBlocks(blocks, field));
1941
+ }
1942
+ function extractReasoningText(value) {
1943
+ if (typeof value === "string") return value;
1944
+ if (Array.isArray(value)) return value.map(extractReasoningText).join("");
1945
+ if (value && typeof value === "object") {
1946
+ const record = value;
1947
+ for (const key of [
1948
+ "text",
1949
+ "content",
1950
+ "reasoning",
1951
+ "reasoning_content",
1952
+ "thinking",
1953
+ "value"
1954
+ ]) {
1955
+ const nested = extractReasoningText(record[key]);
1956
+ if (nested) return nested;
1957
+ }
1958
+ }
1959
+ return "";
1960
+ }
1961
+ function extractReasoningDeltas(delta) {
1962
+ const deltas = [];
1963
+ for (const field of REASONING_FIELDS) {
1964
+ const text = extractReasoningText(delta[field]);
1965
+ if (text) deltas.push({
1966
+ field,
1967
+ text
1968
+ });
1969
+ }
1970
+ return deltas;
1971
+ }
1972
+ function rollbackTrailingAssistantMessages$1(messages) {
1973
+ while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
1974
+ }
1975
+ function buildAssistantReplayMessage(params) {
1976
+ const { content, reasoningByField, toolCalls } = params;
1977
+ if (!content && reasoningByField.size === 0 && toolCalls.length === 0) return null;
1978
+ const replayMessage = {
1979
+ role: "assistant",
1980
+ content: content || null
1981
+ };
1982
+ for (const [field, text] of reasoningByField) replayMessage[field] = text;
1983
+ if (toolCalls.length > 0) replayMessage.tool_calls = toolCalls.map((toolCall) => ({
1984
+ id: toolCall.id,
1985
+ type: "function",
1986
+ function: {
1987
+ name: toolCall.name,
1988
+ arguments: toolCall.args
1989
+ }
1990
+ }));
1991
+ return replayMessage;
1992
+ }
1993
+ var ChatCompletionsAdapter = class extends AdapterBase {
1994
+ kind = "chat-completions";
1995
+ capabilities = {
1996
+ nativeStreaming: true,
1997
+ messageStreaming: true,
1998
+ reasoningStreaming: false,
1999
+ toolCallStreaming: false,
2000
+ hiddenReasoningReplay: "none",
2001
+ replayFidelity: "low",
2002
+ tools: true,
2003
+ usage: "full",
2004
+ billing: "derived",
2005
+ providerMetadata: false
2006
+ };
2007
+ apiKey;
2008
+ baseUrl;
2009
+ fetchFn;
2010
+ markReasoningCompatibility() {
2011
+ this.capabilities.reasoningStreaming = true;
2012
+ this.capabilities.hiddenReasoningReplay = "partial";
2013
+ this.capabilities.replayFidelity = "medium";
2014
+ }
2015
+ constructor(options) {
2016
+ super();
2017
+ this.apiKey = options.apiKey;
2018
+ this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
2019
+ this.fetchFn = options.fetch ?? globalThis.fetch;
2020
+ }
2021
+ buildRequest(request) {
2022
+ const messages = [];
2023
+ if (request.instructions) {
2024
+ const content = typeof request.instructions === "string" ? request.instructions : contentBlocksToChatText(request.instructions, "instructions");
2025
+ messages.push({
2026
+ role: "system",
2027
+ content
2028
+ });
2029
+ }
2030
+ for (const item of request.input) switch (item.type) {
2031
+ case "message": {
2032
+ const role = item.role === "developer" ? "system" : item.role === "system" ? "system" : item.role === "user" ? "user" : "assistant";
2033
+ const text = contentBlocksToChatText(item.content, `input message (${item.role}) content`);
2034
+ messages.push({
2035
+ role,
2036
+ content: text || null
2037
+ });
2038
+ break;
2039
+ }
2040
+ case "tool_call": {
2041
+ const lastAssistant = messages.length > 0 && messages[messages.length - 1]?.role === "assistant" ? messages[messages.length - 1] : null;
2042
+ const tc = {
2043
+ id: item.id,
2044
+ type: "function",
2045
+ function: {
2046
+ name: item.name,
2047
+ arguments: item.argumentsText
2048
+ }
2049
+ };
2050
+ if (lastAssistant) lastAssistant.tool_calls = [...lastAssistant.tool_calls ?? [], tc];
2051
+ else messages.push({
2052
+ role: "assistant",
2053
+ content: null,
2054
+ tool_calls: [tc]
2055
+ });
2056
+ break;
2057
+ }
2058
+ case "tool_result":
2059
+ assertChatToolResultOutcome(item.outcome);
2060
+ messages.push({
2061
+ role: "tool",
2062
+ tool_call_id: item.callId,
2063
+ name: item.toolName,
2064
+ content: contentBlocksToChatText(item.content, `tool_result ${item.callId} content`)
2065
+ });
2066
+ break;
2067
+ case "reasoning":
2068
+ messages.push({
2069
+ role: "assistant",
2070
+ content: contentBlocksToChatText(item.content, "reasoning content")
2071
+ });
2072
+ break;
2073
+ case "opaque":
2074
+ if (item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
2075
+ const payload = item.payload;
2076
+ if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
2077
+ role: "assistant",
2078
+ content: payload.content
2079
+ });
2080
+ else if (payload.replaceCanonical === true && Array.isArray(payload.messages)) {
2081
+ rollbackTrailingAssistantMessages$1(messages);
2082
+ for (const m of payload.messages) messages.push(m);
2083
+ } else if (Array.isArray(payload.messages)) for (const m of payload.messages) messages.push(m);
2084
+ }
2085
+ break;
2086
+ }
2087
+ const body = {
2088
+ model: request.model,
2089
+ messages,
2090
+ stream: true
2091
+ };
2092
+ if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2093
+ type: "function",
2094
+ function: {
2095
+ name: t.name,
2096
+ description: t.description,
2097
+ parameters: t.inputSchema
2098
+ }
2099
+ }));
2100
+ if (request.toolChoice) {
2101
+ if (request.toolChoice === "auto") body.tool_choice = "auto";
2102
+ else if (request.toolChoice === "none") body.tool_choice = "none";
2103
+ else if (request.toolChoice.type === "tool") body.tool_choice = {
2104
+ type: "function",
2105
+ function: { name: request.toolChoice.name }
2106
+ };
2107
+ }
2108
+ if (request.temperature !== void 0) body.temperature = request.temperature;
2109
+ if (request.maxOutputTokens !== void 0) body.max_tokens = request.maxOutputTokens;
2110
+ if (request.metadata) body.metadata = request.metadata;
2111
+ return body;
2112
+ }
2113
+ async *runStream(providerRequest, factory, request) {
2114
+ const auxiliary = this.createAuxiliaryState(request);
2115
+ const response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
2116
+ method: "POST",
2117
+ headers: {
2118
+ "Content-Type": "application/json",
2119
+ Authorization: `Bearer ${this.apiKey}`
2120
+ },
2121
+ body: JSON.stringify(providerRequest)
2122
+ });
2123
+ if (!response.ok) {
2124
+ const errorText = await response.text().catch(() => "unknown error");
2125
+ throw new Error(`Chat Completions API error ${response.status}: ${errorText}`);
2126
+ }
2127
+ const reader = response.body?.getReader();
2128
+ if (!reader) throw new Error("Response body is not readable");
2129
+ const output = [];
2130
+ const decoder = new TextDecoder();
2131
+ let buffer = "";
2132
+ let responseId;
2133
+ let accumulatedContent = "";
2134
+ let accumulatedReasoning = "";
2135
+ let currentMessageId = "";
2136
+ let currentReasoningId = "";
2137
+ let hasMessageStarted = false;
2138
+ let hasReasoningStarted = false;
2139
+ let hasStreamedReasoning = false;
2140
+ const pendingToolCalls = /* @__PURE__ */ new Map();
2141
+ const reasoningByField = /* @__PURE__ */ new Map();
2142
+ const finalizePendingTurn = () => {
2143
+ const events = [];
2144
+ const finalizedToolCalls = [...pendingToolCalls.values()];
2145
+ const finalizedReasoningByField = new Map(reasoningByField);
2146
+ if (hasReasoningStarted && accumulatedReasoning) {
2147
+ const reasoning = reasoningItem([textBlock(accumulatedReasoning)], "full", currentReasoningId);
2148
+ events.push(factory.reasoningCompleted(reasoning));
2149
+ output.push(reasoning);
2150
+ }
2151
+ if (hasMessageStarted && accumulatedContent) {
2152
+ const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2153
+ events.push(factory.messageCompleted(message));
2154
+ output.push(message);
2155
+ }
2156
+ for (const pending of finalizedToolCalls) {
2157
+ const toolCall = toolCallItem(pending.id, pending.name, pending.args);
2158
+ events.push(factory.toolCallCompleted(toolCall));
2159
+ output.push(toolCall);
2160
+ }
2161
+ const assistantReplayMessage = buildAssistantReplayMessage({
2162
+ content: accumulatedContent,
2163
+ reasoningByField: finalizedReasoningByField,
2164
+ toolCalls: finalizedToolCalls
2165
+ });
2166
+ accumulatedContent = "";
2167
+ accumulatedReasoning = "";
2168
+ currentMessageId = "";
2169
+ currentReasoningId = "";
2170
+ hasMessageStarted = false;
2171
+ hasReasoningStarted = false;
2172
+ pendingToolCalls.clear();
2173
+ reasoningByField.clear();
2174
+ return {
2175
+ events,
2176
+ assistantReplayMessage
2177
+ };
2178
+ };
2179
+ try {
2180
+ while (true) {
2181
+ const { done, value } = await reader.read();
2182
+ if (done) break;
2183
+ buffer += decoder.decode(value, { stream: true });
2184
+ const { chunks, rest, malformedEvents } = parseChatSSE(buffer);
2185
+ buffer = rest;
2186
+ const malformedWarning = emitMalformedStreamWarning(factory, {
2187
+ count: malformedEvents,
2188
+ providerLabel: "Chat Completions",
2189
+ transportLabel: "SSE event(s)"
2190
+ });
2191
+ if (malformedWarning) yield malformedWarning;
2192
+ for (const chunk of chunks) {
2193
+ responseId = chunk.id;
2194
+ if (chunk.usage) auxiliary.recordUsage({
2195
+ inputTokens: chunk.usage.prompt_tokens,
2196
+ outputTokens: chunk.usage.completion_tokens,
2197
+ totalTokens: chunk.usage.total_tokens
2198
+ }, "final", chunk.usage);
2199
+ for (const choice of chunk.choices) {
2200
+ if (choice.index !== 0) continue;
2201
+ const delta = choice.delta;
2202
+ const finishReason = choice.finish_reason;
2203
+ const reasoningDeltas = extractReasoningDeltas(delta);
2204
+ if (delta.role === "assistant" && typeof delta.content === "string" && !hasMessageStarted) {
2205
+ currentMessageId = `msg-${chunk.id}`;
2206
+ hasMessageStarted = true;
2207
+ accumulatedContent = "";
2208
+ yield factory.messageStarted(currentMessageId);
2209
+ }
2210
+ if (reasoningDeltas.length > 0) {
2211
+ if (!hasReasoningStarted) {
2212
+ currentReasoningId = `reason-${chunk.id}`;
2213
+ hasReasoningStarted = true;
2214
+ hasStreamedReasoning = true;
2215
+ accumulatedReasoning = "";
2216
+ yield factory.reasoningStarted(currentReasoningId, "full");
2217
+ }
2218
+ for (const reasoningDelta of reasoningDeltas) {
2219
+ accumulatedReasoning += reasoningDelta.text;
2220
+ reasoningByField.set(reasoningDelta.field, (reasoningByField.get(reasoningDelta.field) ?? "") + reasoningDelta.text);
2221
+ yield factory.reasoningDelta(currentReasoningId, textBlock(reasoningDelta.text));
2222
+ }
2223
+ }
2224
+ if (delta.content) {
2225
+ if (!hasMessageStarted) {
2226
+ currentMessageId = `msg-${chunk.id}`;
2227
+ hasMessageStarted = true;
2228
+ yield factory.messageStarted(currentMessageId);
2229
+ }
2230
+ accumulatedContent += delta.content;
2231
+ yield factory.messageDelta(currentMessageId, delta.content);
2232
+ }
2233
+ if (delta.tool_calls) for (const tc of delta.tool_calls) {
2234
+ const idx = tc.index;
2235
+ if (tc.id) {
2236
+ pendingToolCalls.set(idx, {
2237
+ id: tc.id,
2238
+ name: tc.function?.name ?? "",
2239
+ args: ""
2240
+ });
2241
+ yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
2242
+ }
2243
+ if (tc.function?.arguments) {
2244
+ const pending = pendingToolCalls.get(idx);
2245
+ if (pending) {
2246
+ pending.args += tc.function.arguments;
2247
+ yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
2248
+ }
2249
+ }
2250
+ }
2251
+ if (delta.function_call) {
2252
+ if (delta.function_call.name) {
2253
+ const fcId = `fc-${chunk.id}-0`;
2254
+ pendingToolCalls.set(0, {
2255
+ id: fcId,
2256
+ name: delta.function_call.name,
2257
+ args: ""
2258
+ });
2259
+ yield factory.toolCallStarted(fcId, delta.function_call.name);
2260
+ }
2261
+ if (delta.function_call.arguments) {
2262
+ const pending = pendingToolCalls.get(0);
2263
+ if (pending) {
2264
+ pending.args += delta.function_call.arguments;
2265
+ yield factory.toolCallDelta(pending.id, { argumentsText: delta.function_call.arguments });
2266
+ }
2267
+ }
2268
+ }
2269
+ if (finishReason && finishReason !== null) {
2270
+ const { events, assistantReplayMessage } = finalizePendingTurn();
2271
+ for (const event of events) yield event;
2272
+ if (hasStreamedReasoning) this.markReasoningCompatibility();
2273
+ const stopReason = mapStopReason(finishReason);
2274
+ const replay = [...replayFromOutput(output)];
2275
+ if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2276
+ replaceCanonical: true,
2277
+ messages: [assistantReplayMessage]
2278
+ }));
2279
+ const auxiliaryResult = await auxiliary.finalize(factory);
2280
+ for (const event of auxiliaryResult.events) yield event;
2281
+ yield factory.responseCompleted(this.buildResponse(request, {
2282
+ output,
2283
+ replay,
2284
+ stopReason,
2285
+ usage: auxiliaryResult.usage,
2286
+ billing: auxiliaryResult.billing,
2287
+ auxiliary: auxiliaryResult.auxiliary,
2288
+ warnings: auxiliaryResult.warnings,
2289
+ metadataSources: auxiliaryResult.metadataSources,
2290
+ rawResponseId: chunk.id
2291
+ }, factory));
2292
+ }
2293
+ }
2294
+ }
2295
+ }
2296
+ } finally {
2297
+ reader.releaseLock();
2298
+ }
2299
+ if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
2300
+ if (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0) {
2301
+ yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
2302
+ if (hasStreamedReasoning) this.markReasoningCompatibility();
2303
+ const { events, assistantReplayMessage } = finalizePendingTurn();
2304
+ for (const event of events) yield event;
2305
+ const replay = [...replayFromOutput(output)];
2306
+ if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2307
+ replaceCanonical: true,
2308
+ messages: [assistantReplayMessage]
2309
+ }));
2310
+ const auxiliaryResult = await auxiliary.finalize(factory);
2311
+ for (const event of auxiliaryResult.events) yield event;
2312
+ yield factory.responseCompleted(this.buildResponse(request, {
2313
+ output,
2314
+ replay,
2315
+ usage: auxiliaryResult.usage,
2316
+ billing: auxiliaryResult.billing,
2317
+ auxiliary: auxiliaryResult.auxiliary,
2318
+ warnings: auxiliaryResult.warnings,
2319
+ metadataSources: auxiliaryResult.metadataSources,
2320
+ rawResponseId: responseId
2321
+ }, factory));
2322
+ }
2323
+ }
2324
+ };
2325
+ //#endregion
2326
+ //#region src/adapters/ollama.ts
2327
+ /**
2328
+ * Ollama Adapter
2329
+ *
2330
+ * 接入 Ollama 原生 Chat API (/api/chat)。
2331
+ * 与 Chat Completions 兼容层不同,此处直接使用 Ollama 的 NDJSON 流格式。
2332
+ *
2333
+ * 能力:
2334
+ * - 消息流(完整 content 逐块到达)
2335
+ * - 工具调用(整块到达,非逐 token)
2336
+ * - 用量信息(仅 prompt_eval_count / eval_count)
2337
+ *
2338
+ * 限制:
2339
+ * - 不流式输出 reasoning(Ollama 原生 API 无独立思考字段)
2340
+ * - tool_call 不支持逐 token 流式
2341
+ * - replay 保真度低(无 opaque continuation 机制)
2342
+ */
2343
+ function ensureOllamaTextBlocks(blocks, field) {
2344
+ for (let i = 0; i < blocks.length; i++) {
2345
+ const block = blocks[i];
2346
+ if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`ollama does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
2347
+ }
2348
+ return blocks;
2349
+ }
2350
+ function ensureOllamaReasoningBlocks(blocks, field) {
2351
+ return blocks.map((block, index) => {
2352
+ if (block.type !== "text") throw new AIRequestError(`ollama does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`, "UNSUPPORTED_CONTENT_BLOCK");
2353
+ return block;
2354
+ });
2355
+ }
2356
+ function instructionsToOllamaText(instructions) {
2357
+ return typeof instructions === "string" ? instructions : contentBlocksToText(ensureOllamaTextBlocks(instructions, "instructions"));
2358
+ }
2359
+ function parseOllamaToolArguments(item) {
2360
+ if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) return item.argumentsJson;
2361
+ try {
2362
+ const parsed = JSON.parse(item.argumentsText);
2363
+ if (parsed && typeof parsed === "object") return parsed;
2364
+ } catch {}
2365
+ throw new AIRequestError("ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent", "TOOL_CALL_ARGUMENTS_INVALID");
2366
+ }
2367
+ function assertOllamaToolResultOutcome(outcome) {
2368
+ if (outcome !== "success") throw new AIRequestError(`ollama does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
2369
+ }
2370
+ function parseOllamaNDJSON(buffer) {
2371
+ const chunks = [];
2372
+ let rest = buffer;
2373
+ let malformedLines = 0;
2374
+ while (true) {
2375
+ const lineEnd = rest.indexOf("\n");
2376
+ if (lineEnd === -1) break;
2377
+ const line = rest.slice(0, lineEnd).trim();
2378
+ rest = rest.slice(lineEnd + 1);
2379
+ if (!line) continue;
2380
+ try {
2381
+ const parsed = JSON.parse(line);
2382
+ if (parsed && typeof parsed === "object" && "message" in parsed) chunks.push(parsed);
2383
+ else malformedLines++;
2384
+ } catch {
2385
+ malformedLines++;
2386
+ }
2387
+ }
2388
+ return {
2389
+ chunks,
2390
+ rest,
2391
+ malformedLines
2392
+ };
2393
+ }
2394
+ function rollbackTrailingAssistantMessages(messages) {
2395
+ while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
2396
+ }
2397
+ function isOllamaToolCalls(value) {
2398
+ return Array.isArray(value) && value.every((entry) => {
2399
+ if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
2400
+ const fn = entry.function;
2401
+ return !!fn && typeof fn === "object" && "name" in fn && typeof fn.name === "string" && "arguments" in fn && typeof fn.arguments === "object" && fn.arguments !== null;
2402
+ });
2403
+ }
2404
+ var OllamaAdapter = class extends AdapterBase {
2405
+ kind = "ollama";
2406
+ capabilities = {
2407
+ nativeStreaming: true,
2408
+ messageStreaming: true,
2409
+ reasoningStreaming: false,
2410
+ toolCallStreaming: false,
2411
+ hiddenReasoningReplay: "none",
2412
+ replayFidelity: "low",
2413
+ tools: true,
2414
+ usage: "partial",
2415
+ billing: "none",
2416
+ providerMetadata: false
2417
+ };
2418
+ baseUrl;
2419
+ apiKey;
2420
+ fetchFn;
2421
+ constructor(options = {}) {
2422
+ super();
2423
+ this.baseUrl = options.baseUrl ?? "http://localhost:11434";
2424
+ this.apiKey = options.apiKey;
2425
+ this.fetchFn = options.fetch ?? globalThis.fetch;
2426
+ }
2427
+ buildRequest(request) {
2428
+ if (request.toolChoice && request.toolChoice !== "auto") throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
2429
+ const messages = [];
2430
+ if (request.instructions) messages.push({
2431
+ role: "system",
2432
+ content: instructionsToOllamaText(request.instructions)
2433
+ });
2434
+ for (const item of request.input) switch (item.type) {
2435
+ case "message": {
2436
+ const role = item.role === "developer" ? "system" : item.role === "system" ? "system" : item.role === "user" ? "user" : "assistant";
2437
+ messages.push({
2438
+ role,
2439
+ content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`))
2440
+ });
2441
+ break;
2442
+ }
2443
+ case "tool_call": {
2444
+ const lastAssistant = messages.findLast((m) => m.role === "assistant");
2445
+ const tc = { function: {
2446
+ name: item.name,
2447
+ arguments: parseOllamaToolArguments(item)
2448
+ } };
2449
+ if (lastAssistant) lastAssistant.tool_calls = [...lastAssistant.tool_calls ?? [], tc];
2450
+ else messages.push({
2451
+ role: "assistant",
2452
+ content: "",
2453
+ tool_calls: [tc]
2454
+ });
2455
+ break;
2456
+ }
2457
+ case "tool_result":
2458
+ assertOllamaToolResultOutcome(item.outcome);
2459
+ messages.push({
2460
+ role: "tool",
2461
+ content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `tool_result ${item.callId} content`))
2462
+ });
2463
+ break;
2464
+ case "reasoning":
2465
+ messages.push({
2466
+ role: "assistant",
2467
+ content: contentBlocksToText(ensureOllamaReasoningBlocks(item.content, "reasoning content"))
2468
+ });
2469
+ break;
2470
+ case "opaque":
2471
+ if (item.source === "ollama" && item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
2472
+ const payload = item.payload;
2473
+ if (payload.role === "assistant" && typeof payload.content === "string") {
2474
+ rollbackTrailingAssistantMessages(messages);
2475
+ messages.push({
2476
+ role: "assistant",
2477
+ content: payload.content,
2478
+ tool_calls: isOllamaToolCalls(payload.tool_calls) ? payload.tool_calls : void 0
2479
+ });
2480
+ }
2481
+ }
2482
+ break;
2483
+ }
2484
+ const body = {
2485
+ model: request.model,
2486
+ messages,
2487
+ stream: true
2488
+ };
2489
+ if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2490
+ type: "function",
2491
+ function: {
2492
+ name: t.name,
2493
+ description: t.description,
2494
+ parameters: t.inputSchema
2495
+ }
2496
+ }));
2497
+ if (request.temperature !== void 0 || request.maxOutputTokens !== void 0) {
2498
+ body.options = {};
2499
+ if (request.temperature !== void 0) body.options.temperature = request.temperature;
2500
+ if (request.maxOutputTokens !== void 0) body.options.num_predict = request.maxOutputTokens;
2501
+ }
2502
+ return body;
2503
+ }
2504
+ async *runStream(providerRequest, factory, request) {
2505
+ const auxiliary = this.createAuxiliaryState(request);
2506
+ if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
2507
+ const headers = { "Content-Type": "application/json" };
2508
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
2509
+ const response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
2510
+ method: "POST",
2511
+ headers,
2512
+ body: JSON.stringify(providerRequest)
2513
+ });
2514
+ if (!response.ok) {
2515
+ const errorText = await response.text().catch(() => "unknown error");
2516
+ throw new Error(`Ollama API error ${response.status}: ${errorText}`);
2517
+ }
2518
+ const reader = response.body?.getReader();
2519
+ if (!reader) throw new Error("Response body is not readable");
2520
+ const output = [];
2521
+ const decoder = new TextDecoder();
2522
+ let buffer = "";
2523
+ let responseId;
2524
+ let accumulatedContent = "";
2525
+ let currentMessageId = "";
2526
+ let hasMessageStarted = false;
2527
+ let pendingToolCalls = [];
2528
+ try {
2529
+ while (true) {
2530
+ const { done, value } = await reader.read();
2531
+ if (done) break;
2532
+ buffer += decoder.decode(value, { stream: true });
2533
+ const { chunks, rest, malformedLines } = parseOllamaNDJSON(buffer);
2534
+ buffer = rest;
2535
+ const malformedWarning = emitMalformedStreamWarning(factory, {
2536
+ count: malformedLines,
2537
+ providerLabel: "Ollama",
2538
+ transportLabel: "NDJSON line(s)"
2539
+ });
2540
+ if (malformedWarning) yield malformedWarning;
2541
+ for (const chunk of chunks) {
2542
+ responseId = chunk.created_at;
2543
+ const msg = chunk.message;
2544
+ if (msg.content) {
2545
+ if (!hasMessageStarted) {
2546
+ currentMessageId = `msg-${chunk.created_at}`;
2547
+ hasMessageStarted = true;
2548
+ yield factory.messageStarted(currentMessageId);
2549
+ }
2550
+ accumulatedContent += msg.content;
2551
+ yield factory.messageDelta(currentMessageId, msg.content);
2552
+ }
2553
+ if (msg.tool_calls && msg.tool_calls.length > 0) for (const tc of msg.tool_calls) {
2554
+ const tcId = `tc-${chunk.created_at}-${tc.function.name}`;
2555
+ const argsText = JSON.stringify(tc.function.arguments);
2556
+ pendingToolCalls.push({
2557
+ id: tcId,
2558
+ name: tc.function.name,
2559
+ argumentsText: argsText,
2560
+ argumentsJson: tc.function.arguments
2561
+ });
2562
+ }
2563
+ if (chunk.done) {
2564
+ if (accumulatedContent === "" && pendingToolCalls.length > 0 && !hasMessageStarted) {
2565
+ currentMessageId = `msg-${chunk.created_at}`;
2566
+ hasMessageStarted = true;
2567
+ yield factory.messageStarted(currentMessageId);
2568
+ }
2569
+ if (hasMessageStarted) {
2570
+ const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2571
+ yield factory.messageCompleted(message);
2572
+ if (accumulatedContent) output.push(message);
2573
+ }
2574
+ for (const pending of pendingToolCalls) {
2575
+ const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
2576
+ yield factory.toolCallStarted(pending.id, pending.name);
2577
+ yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
2578
+ yield factory.toolCallCompleted(toolCall);
2579
+ output.push(toolCall);
2580
+ }
2581
+ if (request.include?.usage !== "off" && (chunk.prompt_eval_count !== void 0 || chunk.eval_count !== void 0)) auxiliary.recordUsage({
2582
+ inputTokens: chunk.prompt_eval_count,
2583
+ outputTokens: chunk.eval_count,
2584
+ totalTokens: chunk.prompt_eval_count !== void 0 && chunk.eval_count !== void 0 ? chunk.prompt_eval_count + chunk.eval_count : void 0
2585
+ }, "final", {
2586
+ prompt_eval_count: chunk.prompt_eval_count,
2587
+ eval_count: chunk.eval_count
2588
+ });
2589
+ const stopReason = chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0;
2590
+ const replay = replayFromOutput(output);
2591
+ if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
2592
+ role: "assistant",
2593
+ content: accumulatedContent,
2594
+ tool_calls: pendingToolCalls.map((tc) => ({ function: {
2595
+ name: tc.name,
2596
+ arguments: tc.argumentsJson
2597
+ } }))
2598
+ }));
2599
+ const auxiliaryResult = await auxiliary.finalize(factory);
2600
+ for (const event of auxiliaryResult.events) yield event;
2601
+ yield factory.responseCompleted(this.buildResponse(request, {
2602
+ output,
2603
+ replay,
2604
+ stopReason,
2605
+ usage: auxiliaryResult.usage,
2606
+ billing: auxiliaryResult.billing,
2607
+ auxiliary: auxiliaryResult.auxiliary,
2608
+ warnings: auxiliaryResult.warnings,
2609
+ metadataSources: auxiliaryResult.metadataSources,
2610
+ rawResponseId: chunk.created_at
2611
+ }, factory));
2612
+ accumulatedContent = "";
2613
+ currentMessageId = "";
2614
+ hasMessageStarted = false;
2615
+ pendingToolCalls = [];
2616
+ }
2617
+ }
2618
+ }
2619
+ } finally {
2620
+ reader.releaseLock();
2621
+ }
2622
+ if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
2623
+ if (hasMessageStarted || pendingToolCalls.length > 0) {
2624
+ yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
2625
+ if (hasMessageStarted) {
2626
+ const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2627
+ yield factory.messageCompleted(message);
2628
+ if (accumulatedContent) output.push(message);
2629
+ }
2630
+ for (const pending of pendingToolCalls) {
2631
+ const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
2632
+ yield factory.toolCallStarted(pending.id, pending.name);
2633
+ yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
2634
+ yield factory.toolCallCompleted(toolCall);
2635
+ output.push(toolCall);
2636
+ }
2637
+ const replay = replayFromOutput(output);
2638
+ const auxiliaryResult = await auxiliary.finalize(factory);
2639
+ for (const event of auxiliaryResult.events) yield event;
2640
+ yield factory.responseCompleted(this.buildResponse(request, {
2641
+ output,
2642
+ replay,
2643
+ usage: auxiliaryResult.usage,
2644
+ billing: auxiliaryResult.billing,
2645
+ auxiliary: auxiliaryResult.auxiliary,
2646
+ warnings: auxiliaryResult.warnings,
2647
+ metadataSources: auxiliaryResult.metadataSources,
2648
+ rawResponseId: responseId
2649
+ }, factory));
2650
+ }
2651
+ }
2652
+ };
2653
+ //#endregion
2654
+ //#region src/helpers/synthetic-stream.ts
2655
+ /**
2656
+ * 模拟流式 (Synthetic Streaming)
2657
+ *
2658
+ * 将一组已解析的 canonical OutputItem 包装为规范事件流。
2659
+ * 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
2660
+ * 即可产出一致的事件序列,无需自己逐事件组装。
2661
+ *
2662
+ * 约束:
2663
+ * - 每个 item 只发一块完整 delta(不模拟逐 token)
2664
+ * - 保持 item 边界
2665
+ * - 保持后端原始顺序
2666
+ * - 不发明 reasoning
2667
+ * - 不改写工具参数
2668
+ */
2669
+ /**
2670
+ * 将已解析的 output items 包装为完整规范事件流。
2671
+ *
2672
+ * 用法示例(在 adapter 的 runStream 中):
2673
+ * ```ts
2674
+ * const result = parseNonStreamingResponse(data);
2675
+ * yield* syntheticStream({
2676
+ * model: request.model,
2677
+ * responseId: request.requestId,
2678
+ * backend: { kind: "chat-completions" },
2679
+ * output: result.output,
2680
+ * stopReason: result.stopReason,
2681
+ * usage: result.usage,
2682
+ * });
2683
+ * ```
2684
+ */
2685
+ async function* syntheticStream(options) {
2686
+ const { model, responseId, backend, output, replay, stopReason, usage, billing, providerMetadata, rawResponseId, warnings: extraWarnings } = options;
2687
+ const factory = createEventFactory({
2688
+ responseId,
2689
+ backend: {
2690
+ kind: backend.kind,
2691
+ isSynthetic: true
2692
+ }
2693
+ });
2694
+ yield factory.responseStarted(model);
2695
+ for (const item of output) yield* emitItemEvents(item, factory);
2696
+ if (usage || billing) yield factory.responseAuxiliary({
2697
+ usage,
2698
+ billing
2699
+ });
2700
+ const finalReplay = replay ?? replayFromOutput(output);
2701
+ const allWarnings = [];
2702
+ allWarnings.push("Response is synthetically streamed; delta granularity may differ from native streaming");
2703
+ if (extraWarnings) allWarnings.push(...extraWarnings);
2704
+ const response = {
2705
+ id: responseId,
2706
+ output,
2707
+ replay: finalReplay,
2708
+ text: extractText(output),
2709
+ toolCalls: output.filter((item) => item.type === "tool_call"),
2710
+ stopReason,
2711
+ usage,
2712
+ billing,
2713
+ auxiliary: providerMetadata ? { providerMetadata } : void 0,
2714
+ warnings: allWarnings.length > 0 ? allWarnings : void 0,
2715
+ backend: {
2716
+ requestId: responseId,
2717
+ rawResponseId,
2718
+ adapter: backend.kind,
2719
+ isSyntheticStream: true
2720
+ }
2721
+ };
2722
+ yield factory.responseCompleted(response);
2723
+ }
2724
+ function* emitItemEvents(item, factory) {
2725
+ switch (item.type) {
2726
+ case "message":
2727
+ yield* emitMessageEvents(item, factory);
2728
+ break;
2729
+ case "reasoning":
2730
+ yield* emitReasoningEvents(item, factory);
2731
+ break;
2732
+ case "tool_call":
2733
+ yield* emitToolCallEvents(item, factory);
2734
+ break;
2735
+ case "opaque": break;
2736
+ }
2737
+ }
2738
+ function* emitMessageEvents(item, factory) {
2739
+ const id = item.id ?? `syn-msg-${crypto.randomUUID()}`;
2740
+ yield factory.messageStarted(id);
2741
+ for (const block of item.content) if (block.type === "text") yield factory.messageDelta(id, block.text);
2742
+ yield factory.messageCompleted(item);
2743
+ }
2744
+ function* emitReasoningEvents(item, factory) {
2745
+ const id = item.id ?? `syn-reason-${crypto.randomUUID()}`;
2746
+ yield factory.reasoningStarted(id, item.visibility);
2747
+ for (const block of item.content) if (block.type === "text") yield factory.reasoningDelta(id, block);
2748
+ yield factory.reasoningCompleted(item);
2749
+ }
2750
+ function* emitToolCallEvents(item, factory) {
2751
+ yield factory.toolCallStarted(item.id, item.name);
2752
+ if (item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
2753
+ yield factory.toolCallCompleted(item);
2754
+ }
2755
+ //#endregion
2756
+ export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, CAPABILITY_MATRIX, ChatCompletionsAdapter, MessagesAdapter, OllamaAdapter, ResponsesAdapter, WarningCode, aggregateEvents, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, reasoningItem, replayFromOutput, syntheticStream, textBlock, toolCallItem, toolResultItem, validateRequest };
2757
+
2758
+ //# sourceMappingURL=index.mjs.map