@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.
@@ -0,0 +1,700 @@
1
+ /**
2
+ * Messages Adapter
3
+ *
4
+ * 接入 Anthropic Messages API (messages 端点)。
5
+ * 支持:
6
+ * - 文本消息流 (text content block)
7
+ * - 思维链流 (thinking content block)
8
+ * - 工具调用流 (tool_use content block)
9
+ * - 高保真 replay(含 opaque continuation)
10
+ * - 能力降级 warning
11
+ */
12
+
13
+ import { AdapterBase } from "../helpers/adapter-base.js";
14
+ import { AIRequestError } from "../core/errors.js";
15
+ import {
16
+ textBlock,
17
+ messageItem,
18
+ reasoningItem,
19
+ toolCallItem,
20
+ opaqueItem,
21
+ replayFromOutput,
22
+ mapStopReason,
23
+ blockToText,
24
+ contentBlocksToText,
25
+ } from "../helpers/mapping.js";
26
+ import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
27
+
28
+ import { parseSSEEvents } from "../helpers/sse-parser.js";
29
+
30
+ import { CAPABILITY_MATRIX } from "../index.js";
31
+ import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
32
+
33
+ // ── 类型 ──────────────────────────────────────────────────────
34
+
35
+ export type MessagesAdapterOptions = {
36
+ apiKey: string;
37
+ apiVersion?: string;
38
+ baseUrl?: string;
39
+ /** 可注入自定义 fetch 实现(用于测试/代理) */
40
+ fetch?: FetchFn;
41
+ };
42
+
43
+ // ── Messages API 请求类型 ────────────────────────────────────
44
+
45
+ type MessagesAPIRequest = {
46
+ model: string;
47
+ max_tokens: number;
48
+ messages: MessagesAPIMessage[];
49
+ system?: string;
50
+ tools?: MessagesAPITool[];
51
+ tool_choice?: { type: "auto" | "none" } | { type: "tool"; name: string };
52
+ temperature?: number;
53
+ thinking?: { type: "enabled"; budget_tokens: number };
54
+ stream: true;
55
+ };
56
+
57
+ type MessagesAPIMessage = {
58
+ role: "user" | "assistant";
59
+ content: string | MessagesAPIContentBlock[];
60
+ };
61
+
62
+ type MessagesAPIContentBlock =
63
+ | { type: "text"; text: string }
64
+ | { type: "thinking"; thinking: string; signature?: string }
65
+ | { type: "redacted_thinking"; data: string }
66
+ | { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
67
+ | { type: "tool_result"; tool_use_id: string; content: string | MessagesAPIContentBlock[]; is_error?: boolean };
68
+
69
+ type MessagesAPITool = {
70
+ name: string;
71
+ description?: string;
72
+ input_schema: Record<string, unknown>;
73
+ };
74
+
75
+ function ensureMessagesTextBlocks(
76
+ blocks: import("../index.js").ContentBlock[],
77
+ field: string,
78
+ ): import("../index.js").ContentBlock[] {
79
+ for (let i = 0; i < blocks.length; i++) {
80
+ const block = blocks[i];
81
+ if (block.type !== "text" && block.type !== "json") {
82
+ throw new AIRequestError(
83
+ `messages does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`,
84
+ "UNSUPPORTED_CONTENT_BLOCK",
85
+ );
86
+ }
87
+ }
88
+
89
+ return blocks;
90
+ }
91
+
92
+ function ensureMessagesReasoningBlocks(
93
+ blocks: import("../index.js").ContentBlock[],
94
+ field: string,
95
+ ): Array<Extract<import("../index.js").ContentBlock, { type: "text" }>> {
96
+ return blocks.map((block, index) => {
97
+ if (block.type !== "text") {
98
+ throw new AIRequestError(
99
+ `messages does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`,
100
+ "UNSUPPORTED_CONTENT_BLOCK",
101
+ );
102
+ }
103
+
104
+ return block;
105
+ });
106
+ }
107
+
108
+ function instructionsToMessagesText(instructions: string | import("../index.js").ContentBlock[]): string {
109
+ return typeof instructions === "string"
110
+ ? instructions
111
+ : contentBlocksToText(ensureMessagesTextBlocks(instructions, "instructions"));
112
+ }
113
+
114
+ function assertMessagesToolResultOutcome(outcome: import("../index.js").ToolResultItem["outcome"]): void {
115
+ if (outcome === "rejected") {
116
+ throw new AIRequestError(
117
+ 'messages does not preserve tool_result outcome "rejected"; only "success" and "error" are supported',
118
+ "UNSUPPORTED_TOOL_RESULT_OUTCOME",
119
+ );
120
+ }
121
+ }
122
+
123
+ // ── SSE 事件类型 ──────────────────────────────────────────────
124
+
125
+ type MessagesSSEEvent =
126
+ | { type: "message_start"; data: { message: MessagesAPIMessageResponse } }
127
+ | { type: "content_block_start"; data: { index: number; content_block: { type: string; [key: string]: unknown } } }
128
+ | { type: "content_block_delta"; data: { index: number; delta: { type: string; [key: string]: unknown } } }
129
+ | { type: "content_block_stop"; data: { index: number } }
130
+ | {
131
+ type: "message_delta";
132
+ data: {
133
+ delta: { stop_reason?: string; stop_sequence?: string | null };
134
+ usage: { input_tokens: number; output_tokens: number };
135
+ };
136
+ }
137
+ | { type: "message_stop"; data: Record<string, never> }
138
+ | { type: "ping"; data: Record<string, never> }
139
+ | { type: "error"; data: { error: { type: string; message: string } } };
140
+
141
+ type MessagesAPIMessageResponse = {
142
+ id: string;
143
+ type: string;
144
+ role: "assistant";
145
+ model: string;
146
+ content: MessagesAPIContentBlock[];
147
+ stop_reason?: "end_turn" | "max_tokens" | "tool_use" | string;
148
+ stop_sequence?: string | null;
149
+ usage: { input_tokens: number; output_tokens: number };
150
+ };
151
+
152
+ // ── SSE 解析 ──────────────────────────────────────────────────
153
+
154
+ function parseMessagesSSE(chunk: string): { events: MessagesSSEEvent[]; rest: string; malformedEvents: number } {
155
+ const result = parseSSEEvents(chunk);
156
+ return { events: result.events as MessagesSSEEvent[], rest: result.rest, malformedEvents: result.malformedEvents };
157
+ }
158
+
159
+ function rollbackTrailingAssistantMessages(messages: MessagesAPIMessage[]): void {
160
+ while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") {
161
+ messages.pop();
162
+ }
163
+ }
164
+
165
+ function parseToolUseInput(input: string): Record<string, unknown> {
166
+ try {
167
+ const parsed = JSON.parse(input);
168
+ return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
169
+ } catch {
170
+ return {};
171
+ }
172
+ }
173
+
174
+ // ── Content block 映射 ─────────────────────────────────────────
175
+
176
+ function canonicalToMessagesBlock(b: import("../index.js").ContentBlock): MessagesAPIContentBlock {
177
+ if (b.type === "text") return { type: "text", text: b.text };
178
+ if (b.type === "json") return { type: "text", text: JSON.stringify(b.json) };
179
+ throw new AIRequestError(
180
+ `messages does not support content block type "${b.type}" in canonical mapping`,
181
+ "UNSUPPORTED_CONTENT_BLOCK",
182
+ );
183
+ }
184
+
185
+ function pickProviderHeaders(headers: Headers): Record<string, string> {
186
+ const metadata: Record<string, string> = {};
187
+
188
+ headers.forEach((value, key) => {
189
+ const normalizedKey = key.toLowerCase();
190
+ if (
191
+ normalizedKey === "request-id" ||
192
+ normalizedKey === "x-request-id" ||
193
+ normalizedKey === "anthropic-organization-id" ||
194
+ normalizedKey === "anthropic-beta" ||
195
+ normalizedKey === "retry-after" ||
196
+ normalizedKey.startsWith("anthropic-ratelimit-")
197
+ ) {
198
+ metadata[normalizedKey] = value;
199
+ }
200
+ });
201
+
202
+ return metadata;
203
+ }
204
+
205
+ function buildStreamMetadata(options: {
206
+ apiVersion: string;
207
+ message?: MessagesAPIMessageResponse;
208
+ stopReason?: string;
209
+ stopSequence?: string | null;
210
+ }): Record<string, unknown> {
211
+ const { apiVersion, message, stopReason, stopSequence } = options;
212
+ const metadata: Record<string, unknown> = {
213
+ apiVersion,
214
+ };
215
+
216
+ if (message) {
217
+ metadata.message = {
218
+ id: message.id,
219
+ type: message.type,
220
+ role: message.role,
221
+ model: message.model,
222
+ };
223
+ }
224
+
225
+ if (stopReason !== undefined || stopSequence !== undefined) {
226
+ metadata.stop = {
227
+ reason: stopReason,
228
+ sequence: stopSequence,
229
+ };
230
+ }
231
+
232
+ return metadata;
233
+ }
234
+
235
+ // ── Adapter ───────────────────────────────────────────────────
236
+
237
+ export class MessagesAdapter extends AdapterBase {
238
+ readonly kind = "messages" as const;
239
+ readonly capabilities = CAPABILITY_MATRIX.messages;
240
+
241
+ private apiKey: string;
242
+ private apiVersion: string;
243
+ private baseUrl: string;
244
+ private fetchFn: FetchFn;
245
+ private warningAccumulator: string[];
246
+
247
+ constructor(options: MessagesAdapterOptions) {
248
+ super();
249
+ this.apiKey = options.apiKey;
250
+ this.apiVersion = options.apiVersion ?? "2023-06-01";
251
+ this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
252
+ this.fetchFn = options.fetch ?? globalThis.fetch;
253
+ this.warningAccumulator = [];
254
+ }
255
+
256
+ protected warn(message: string, _code?: string): void {
257
+ this.warningAccumulator.push(message);
258
+ }
259
+
260
+ // ── buildRequest ──────────────────────────────────────────
261
+
262
+ protected buildRequest(request: NormalizedRequest): MessagesAPIRequest {
263
+ const messages: MessagesAPIMessage[] = [];
264
+ let systemPrompt: string | undefined;
265
+
266
+ // 处理 instructions → system prompt
267
+ if (request.instructions) {
268
+ systemPrompt = instructionsToMessagesText(request.instructions);
269
+ }
270
+
271
+ // 处理 input items
272
+ for (const item of request.input) {
273
+ switch (item.type) {
274
+ case "message": {
275
+ if (item.role === "system" || item.role === "developer") {
276
+ // Anthropic 不支持 system/developer role 在 messages 中
277
+ // 合并到 system prompt
278
+ const text = contentBlocksToText(ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`));
279
+ systemPrompt = systemPrompt ? `${systemPrompt}\n${text}` : text;
280
+ break;
281
+ }
282
+
283
+ const role = item.role === "user" ? "user" : "assistant";
284
+ const supportedContent = ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`);
285
+ if (supportedContent.length === 1 && supportedContent[0]?.type === "text") {
286
+ messages.push({ role, content: supportedContent[0].text });
287
+ } else {
288
+ messages.push({ role, content: supportedContent.map(canonicalToMessagesBlock) });
289
+ }
290
+ break;
291
+ }
292
+ case "tool_call": {
293
+ // Anthropic 使用 tool_use block 在 assistant message 中
294
+ const lastMsg = messages[messages.length - 1];
295
+ const toolBlock: MessagesAPIContentBlock = {
296
+ type: "tool_use",
297
+ id: item.id,
298
+ name: item.name,
299
+ input:
300
+ (item.argumentsJson as Record<string, unknown> | undefined) ??
301
+ parseToolUseInput(item.argumentsText),
302
+ };
303
+
304
+ if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") {
305
+ lastMsg.content.push(toolBlock);
306
+ } else {
307
+ messages.push({ role: "assistant", content: [toolBlock] });
308
+ }
309
+ break;
310
+ }
311
+ case "tool_result": {
312
+ assertMessagesToolResultOutcome(item.outcome);
313
+ const content = ensureMessagesTextBlocks(item.content, `tool_result ${item.callId} content`)
314
+ .map(blockToText)
315
+ .join("\n");
316
+ const block: MessagesAPIContentBlock = {
317
+ type: "tool_result",
318
+ tool_use_id: item.callId,
319
+ content,
320
+ is_error: item.outcome === "error",
321
+ };
322
+ messages.push({ role: "user", content: [block] });
323
+ break;
324
+ }
325
+ case "reasoning": {
326
+ // 将 reasoning item 转为 thinking block 在 assistant message 中
327
+ const text = contentBlocksToText(ensureMessagesReasoningBlocks(item.content, "reasoning content"));
328
+ const block: MessagesAPIContentBlock = { type: "thinking", thinking: text };
329
+ const lastMsg = messages[messages.length - 1];
330
+ if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") {
331
+ lastMsg.content.push(block);
332
+ } else {
333
+ messages.push({ role: "assistant", content: [block] });
334
+ }
335
+ break;
336
+ }
337
+ case "opaque": {
338
+ // 尝试从 opaque replay item 中提取 assistant message
339
+ if (item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
340
+ const payload = item.payload as Record<string, unknown>;
341
+ if (payload.role === "assistant" && Array.isArray(payload.content)) {
342
+ // 验证 content 是合法的 MessagesAPIContentBlock[]
343
+ const isValidContent = payload.content.every(
344
+ (b): b is MessagesAPIContentBlock =>
345
+ typeof b === "object" &&
346
+ b !== null &&
347
+ "type" in b &&
348
+ (b.type === "text" || b.type === "thinking" || b.type === "redacted_thinking" || b.type === "tool_use" || b.type === "tool_result"),
349
+ );
350
+ if (isValidContent) {
351
+ rollbackTrailingAssistantMessages(messages);
352
+ messages.push({
353
+ role: "assistant",
354
+ content: payload.content as MessagesAPIContentBlock[],
355
+ });
356
+ }
357
+ }
358
+ }
359
+ break;
360
+ }
361
+ }
362
+ }
363
+
364
+ const body: MessagesAPIRequest = {
365
+ model: request.model,
366
+ max_tokens: request.maxOutputTokens ?? 4096,
367
+ messages,
368
+ stream: true,
369
+ };
370
+
371
+ if (systemPrompt) body.system = systemPrompt;
372
+
373
+ if (request.tools && request.tools.length > 0) {
374
+ body.tools = request.tools.map(
375
+ (t): MessagesAPITool => ({
376
+ name: t.name,
377
+ description: t.description,
378
+ input_schema: t.inputSchema,
379
+ }),
380
+ );
381
+ }
382
+
383
+ if (request.toolChoice) {
384
+ if (request.toolChoice === "auto") body.tool_choice = { type: "auto" };
385
+ else if (request.toolChoice === "none") body.tool_choice = { type: "none" };
386
+ else if (request.toolChoice.type === "tool") {
387
+ body.tool_choice = { type: "tool", name: request.toolChoice.name };
388
+ }
389
+ }
390
+
391
+ if (request.temperature !== undefined) body.temperature = request.temperature;
392
+
393
+ return body;
394
+ }
395
+
396
+ // ── runStream ─────────────────────────────────────────────
397
+
398
+ protected async *runStream(
399
+ providerRequest: MessagesAPIRequest,
400
+ factory: EventFactory,
401
+ request: NormalizedRequest,
402
+ ): AsyncIterable<AIStreamEvent> {
403
+ this.warningAccumulator = [];
404
+ const auxiliary = this.createAuxiliaryState(request);
405
+
406
+ if (request.metadata) {
407
+ yield factory.responseWarning("Request metadata is not supported by the Messages adapter", "UNSUPPORTED_METADATA");
408
+ }
409
+
410
+ const response = await this.fetchFn(`${this.baseUrl}/messages`, {
411
+ method: "POST",
412
+ headers: {
413
+ "Content-Type": "application/json",
414
+ "x-api-key": this.apiKey,
415
+ "anthropic-version": this.apiVersion,
416
+ },
417
+ body: JSON.stringify(providerRequest),
418
+ });
419
+
420
+ if (!response.ok) {
421
+ const errorText = await response.text().catch(() => "unknown error");
422
+ throw new Error(`Messages API error ${response.status}: ${errorText}`);
423
+ }
424
+
425
+ const reader = response.body?.getReader();
426
+ if (!reader) {
427
+ throw new Error("Response body is not readable");
428
+ }
429
+
430
+ // 流累积状态
431
+ const output: OutputItem[] = [];
432
+ const decoder = new TextDecoder();
433
+ let buffer = "";
434
+ let messageResponse: MessagesAPIMessageResponse | undefined;
435
+ let currentContentBlockIndex = -1;
436
+ let currentItemType: "message" | "reasoning" | "tool_call" | null = null;
437
+ let currentItemId = "";
438
+ let currentToolName = "";
439
+ let currentArgsText = "";
440
+ let currentThinkingVisibility: "full" | "redacted" = "full";
441
+ let hasStreamedReasoning = false;
442
+ const rawReplayContent: MessagesAPIContentBlock[] = [];
443
+
444
+ // 内容块累积缓冲
445
+ let textBuffer = "";
446
+ let thinkingBuffer = "";
447
+ let argsBuffer = "";
448
+
449
+ // 完成响应数据
450
+ let stopReason: string | undefined;
451
+ let stopSequence: string | null | undefined;
452
+ let rawResponseId: string | undefined;
453
+
454
+ if (request.include?.providerMetadata !== "off") {
455
+ const headerMetadata = pickProviderHeaders(response.headers);
456
+ auxiliary.recordProviderMetadata("header", Object.keys(headerMetadata).length > 0 ? { headers: headerMetadata } : undefined);
457
+ }
458
+
459
+ try {
460
+ while (true) {
461
+ const { done, value } = await reader.read();
462
+ if (done) break;
463
+
464
+ buffer += decoder.decode(value, { stream: true });
465
+ const { events, rest, malformedEvents } = parseMessagesSSE(buffer);
466
+ buffer = rest;
467
+
468
+ const malformedWarning = emitMalformedStreamWarning(factory, {
469
+ count: malformedEvents,
470
+ providerLabel: "Messages",
471
+ transportLabel: "SSE event(s)",
472
+ });
473
+ if (malformedWarning) {
474
+ yield malformedWarning;
475
+ }
476
+
477
+ for (const sseEvent of events) {
478
+ switch (sseEvent.type) {
479
+ case "ping":
480
+ continue;
481
+
482
+ case "error": {
483
+ const err = sseEvent.data.error;
484
+ yield factory.responseWarning(err.message, err.type);
485
+ this.warn(err.message, err.type);
486
+ continue;
487
+ }
488
+
489
+ case "message_start": {
490
+ messageResponse = sseEvent.data.message;
491
+ rawResponseId = messageResponse.id;
492
+ // 检查是否有 thinking 能力
493
+ if (messageResponse.content.some((b) => b.type === "thinking" || b.type === "redacted_thinking")) {
494
+ hasStreamedReasoning = true;
495
+ }
496
+ continue;
497
+ }
498
+
499
+ case "content_block_start": {
500
+ const block = sseEvent.data.content_block;
501
+ currentContentBlockIndex = sseEvent.data.index;
502
+
503
+ switch (block.type) {
504
+ case "text": {
505
+ currentItemType = "message";
506
+ currentItemId = `msg-${block.type}-${currentContentBlockIndex}`;
507
+ textBuffer = "";
508
+ yield factory.messageStarted(currentItemId);
509
+ break;
510
+ }
511
+ case "thinking": {
512
+ hasStreamedReasoning = true;
513
+ currentItemType = "reasoning";
514
+ currentItemId = `reason-${currentContentBlockIndex}`;
515
+ currentThinkingVisibility = "full";
516
+ thinkingBuffer = "";
517
+ yield factory.reasoningStarted(currentItemId, "full");
518
+ break;
519
+ }
520
+ case "redacted_thinking": {
521
+ hasStreamedReasoning = true;
522
+ currentItemType = "reasoning";
523
+ currentItemId = `reason-redacted-${currentContentBlockIndex}`;
524
+ currentThinkingVisibility = "redacted";
525
+ const data = (block as unknown as { data: string }).data;
526
+ yield factory.reasoningStarted(currentItemId, "redacted");
527
+ yield factory.reasoningDelta(currentItemId, textBlock(data));
528
+ const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
529
+ yield factory.reasoningCompleted(redactedItem);
530
+ output.push(redactedItem);
531
+ rawReplayContent.push({ type: "redacted_thinking", data });
532
+ currentItemType = null;
533
+ break;
534
+ }
535
+ case "tool_use": {
536
+ const tuBlock = block as unknown as { id: string; name: string };
537
+ currentItemType = "tool_call";
538
+ currentItemId = tuBlock.id;
539
+ currentToolName = tuBlock.name;
540
+ currentArgsText = "";
541
+ argsBuffer = "";
542
+ yield factory.toolCallStarted(currentItemId, currentToolName);
543
+ break;
544
+ }
545
+ }
546
+ continue;
547
+ }
548
+
549
+ case "content_block_delta": {
550
+ const delta = sseEvent.data.delta;
551
+
552
+ switch (delta.type) {
553
+ case "text_delta": {
554
+ if (currentItemType === "message" && currentItemId) {
555
+ const txt = (delta as unknown as { text: string }).text;
556
+ textBuffer += txt;
557
+ yield factory.messageDelta(currentItemId, txt);
558
+ }
559
+ break;
560
+ }
561
+ case "thinking_delta": {
562
+ if (currentItemType === "reasoning" && currentItemId) {
563
+ const txt = (delta as unknown as { thinking: string }).thinking;
564
+ thinkingBuffer += txt;
565
+ yield factory.reasoningDelta(currentItemId, textBlock(txt));
566
+ }
567
+ break;
568
+ }
569
+ case "input_json_delta": {
570
+ if (currentItemType === "tool_call" && currentItemId) {
571
+ const partial = (delta as unknown as { partial_json: string }).partial_json;
572
+ argsBuffer += partial;
573
+ yield factory.toolCallDelta(currentItemId, { argumentsText: partial });
574
+ }
575
+ break;
576
+ }
577
+ }
578
+ continue;
579
+ }
580
+
581
+ case "content_block_stop": {
582
+ if (currentItemType === "message" && currentItemId) {
583
+ yield factory.messageCompleted(messageItem([textBlock(textBuffer)], { id: currentItemId }));
584
+ output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
585
+ rawReplayContent.push({ type: "text", text: textBuffer });
586
+ } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
587
+ yield factory.reasoningCompleted(
588
+ reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId),
589
+ );
590
+ output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
591
+ rawReplayContent.push({ type: "thinking", thinking: thinkingBuffer });
592
+ } else if (currentItemType === "tool_call" && currentItemId) {
593
+ const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
594
+ yield factory.toolCallCompleted(tcItem);
595
+ output.push(tcItem);
596
+ rawReplayContent.push({
597
+ type: "tool_use",
598
+ id: currentItemId,
599
+ name: currentToolName,
600
+ input: parseToolUseInput(currentArgsText || argsBuffer),
601
+ });
602
+ }
603
+
604
+ currentItemType = null;
605
+ currentItemId = "";
606
+ continue;
607
+ }
608
+
609
+ case "message_delta": {
610
+ stopReason = sseEvent.data.delta.stop_reason;
611
+ stopSequence = sseEvent.data.delta.stop_sequence;
612
+ const u = sseEvent.data.usage;
613
+ if (u) {
614
+ auxiliary.recordUsage(
615
+ {
616
+ inputTokens: u.input_tokens,
617
+ outputTokens: u.output_tokens,
618
+ totalTokens: u.input_tokens + u.output_tokens,
619
+ },
620
+ "stream",
621
+ u,
622
+ );
623
+ }
624
+ continue;
625
+ }
626
+
627
+ case "message_stop": {
628
+ // 流结束,构造 final response
629
+ break;
630
+ }
631
+ }
632
+ }
633
+ }
634
+ } finally {
635
+ reader.releaseLock();
636
+ }
637
+
638
+ if (buffer.trim().length > 0) {
639
+ yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
640
+ }
641
+
642
+ // 构造 replay
643
+ const replay = [...replayFromOutput(output)];
644
+
645
+ // 附加 opaque replay item 用于续接
646
+ // 保存 provider 原始 block 以实现高保真 replay
647
+ if (messageResponse) {
648
+ const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
649
+ replay.push(
650
+ opaqueItem("messages", "replay", {
651
+ replaceCanonical: true,
652
+ role: messageResponse.role,
653
+ content: replayContent,
654
+ messageId: messageResponse.id,
655
+ stopReason: stopReason ?? messageResponse.stop_reason,
656
+ }),
657
+ );
658
+ }
659
+
660
+ if (request.include?.providerMetadata !== "off") {
661
+ auxiliary.recordProviderMetadata(
662
+ "stream",
663
+ buildStreamMetadata({
664
+ apiVersion: this.apiVersion,
665
+ message: messageResponse,
666
+ stopReason,
667
+ stopSequence,
668
+ }),
669
+ );
670
+ }
671
+
672
+ // 警告低 replay fidelity
673
+ if (!hasStreamedReasoning) {
674
+ // 没有 reasoning,replay fidelity 较低
675
+ }
676
+
677
+ const auxiliaryResult = await auxiliary.finalize(factory);
678
+ for (const event of auxiliaryResult.events) {
679
+ yield event;
680
+ }
681
+
682
+ yield factory.responseCompleted(
683
+ this.buildResponse(
684
+ request,
685
+ {
686
+ output,
687
+ replay,
688
+ stopReason: stopReason ? mapStopReason(stopReason) : undefined,
689
+ usage: auxiliaryResult.usage,
690
+ billing: auxiliaryResult.billing,
691
+ auxiliary: auxiliaryResult.auxiliary,
692
+ warnings: auxiliaryResult.warnings,
693
+ metadataSources: auxiliaryResult.metadataSources,
694
+ rawResponseId,
695
+ },
696
+ factory,
697
+ ),
698
+ );
699
+ }
700
+ }