@codehz/ai 0.3.0 → 0.4.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.
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { AdapterBase } from "../helpers/adapter-base.js";
14
- import { AIProviderError, AIRequestError, AIStreamError } from "../core/errors.js";
14
+ import { AIRequestError } from "../core/errors.js";
15
15
  import {
16
16
  textBlock,
17
17
  messageItem,
@@ -20,13 +20,17 @@ import {
20
20
  opaqueItem,
21
21
  replayFromOutput,
22
22
  mapStopReason,
23
- blockToText,
24
23
  contentBlocksToText,
25
24
  } from "../helpers/mapping.js";
26
- import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
27
- import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
25
+ import { assertOpaqueReplayEnvelope } from "../helpers/adapter-security.js";
28
26
  import { usageFromAnthropicMessages } from "../helpers/usage-mapping.js";
29
- import { NormalizedRequestMapper, splitSSEFrames, IncrementalStreamParser } from "../helpers/index.js";
27
+ import {
28
+ NormalizedRequestMapper,
29
+ createSseJsonParser,
30
+ openProviderJsonStream,
31
+ iterateProviderStreamBatches,
32
+ createCompletionGate,
33
+ } from "../helpers/index.js";
30
34
 
31
35
  import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
32
36
 
@@ -295,10 +299,7 @@ export class MessagesAdapter extends AdapterBase {
295
299
  break;
296
300
  }
297
301
  case "tool_result": {
298
- const content = mapper
299
- .ensureTextBlocks(item.content, `tool_result ${item.callId} content`)
300
- .map(blockToText)
301
- .join("\n");
302
+ const content = mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`);
302
303
  const block: MessagesAPIContentBlock = {
303
304
  type: "tool_result",
304
305
  tool_use_id: item.callId,
@@ -352,23 +353,20 @@ export class MessagesAdapter extends AdapterBase {
352
353
 
353
354
  if (systemPrompt) body.system = systemPrompt;
354
355
 
355
- if (request.tools && request.tools.length > 0) {
356
- body.tools = request.tools.map(
357
- (t): MessagesAPITool => ({
358
- name: t.name,
359
- description: t.description,
360
- input_schema: t.inputSchema,
361
- }),
362
- );
363
- }
364
-
365
- if (request.toolChoice) {
366
- if (request.toolChoice === "auto") body.tool_choice = { type: "auto" };
367
- else if (request.toolChoice === "none") body.tool_choice = { type: "none" };
368
- else if (request.toolChoice.type === "tool") {
369
- body.tool_choice = { type: "tool", name: request.toolChoice.name };
370
- }
371
- }
356
+ body.tools = mapper.mapToolsIfPresent(
357
+ request.tools,
358
+ (t): MessagesAPITool => ({
359
+ name: t.name,
360
+ description: t.description,
361
+ input_schema: t.inputSchema,
362
+ }),
363
+ );
364
+
365
+ body.tool_choice = mapper.mapToolChoice<Exclude<MessagesAPIRequest["tool_choice"], undefined>>(request.toolChoice, {
366
+ auto: { type: "auto" } as const,
367
+ none: { type: "none" } as const,
368
+ tool: (name) => ({ type: "tool" as const, name }),
369
+ });
372
370
 
373
371
  if (request.temperature !== undefined) body.temperature = request.temperature;
374
372
 
@@ -383,7 +381,7 @@ export class MessagesAdapter extends AdapterBase {
383
381
  request: NormalizedRequest,
384
382
  ): AsyncIterable<AIStreamEvent> {
385
383
  const auxiliary = this.createAuxiliaryState(request);
386
- let completedEmitted = false;
384
+ const gate = createCompletionGate();
387
385
 
388
386
  if (request.metadata) {
389
387
  yield factory.responseWarning(
@@ -392,53 +390,20 @@ export class MessagesAdapter extends AdapterBase {
392
390
  );
393
391
  }
394
392
 
395
- let response: Response;
396
-
397
- try {
398
- response = await this.fetchFn(`${this.baseUrl}/messages`, {
399
- method: "POST",
400
- headers: {
401
- "Content-Type": "application/json",
402
- "x-api-key": this.apiKey,
403
- "anthropic-version": this.apiVersion,
404
- },
405
- body: JSON.stringify(providerRequest),
406
- signal: request.signal,
407
- });
408
- } catch (err) {
409
- throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
410
- }
411
-
412
- if (!response.ok) {
413
- const errorBody = await response.text().catch(() => "");
414
- throw providerHttpError(response.status, errorBody);
415
- }
416
-
417
- const reader = response.body?.getReader();
418
- if (!reader) {
419
- throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
420
- }
421
-
422
- // 流累积状态
423
- const parser = new IncrementalStreamParser<MessagesSSEEvent>(splitSSEFrames, (frame: string) => {
424
- let eventType = "";
425
- let dataStr = "";
426
- for (const rawLine of frame.split("\n")) {
427
- const line = rawLine.trim();
428
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
429
- else if (line.startsWith("data: ")) dataStr += line.slice(6);
430
- }
431
- if (!eventType) return { status: "ignored" };
432
- try {
433
- const data = JSON.parse(dataStr);
434
- return { status: "parsed", value: { type: eventType, data } as MessagesSSEEvent };
435
- } catch {
436
- return { status: "malformed" };
437
- }
393
+ const { reader, headers } = await openProviderJsonStream({
394
+ fetchFn: this.fetchFn,
395
+ url: `${this.baseUrl}/messages`,
396
+ headers: {
397
+ "Content-Type": "application/json",
398
+ "x-api-key": this.apiKey,
399
+ "anthropic-version": this.apiVersion,
400
+ },
401
+ body: providerRequest,
402
+ signal: request.signal,
438
403
  });
439
404
 
405
+ const parser = createSseJsonParser<MessagesSSEEvent>();
440
406
  const output: OutputItem[] = [];
441
- let streamDone = false;
442
407
  let messageResponse: MessagesAPIMessageResponse | undefined;
443
408
  let currentContentBlockIndex = -1;
444
409
  let currentItemType: "message" | "reasoning" | "tool_call" | null = null;
@@ -446,215 +411,176 @@ export class MessagesAdapter extends AdapterBase {
446
411
  let currentToolName = "";
447
412
  let currentArgsText = "";
448
413
  let currentThinkingVisibility: "full" | "redacted" = "full";
449
- let hasStreamedReasoning = false;
450
414
  const rawReplayContent: MessagesAPIContentBlock[] = [];
451
415
 
452
- // 内容块累积缓冲
453
416
  let textBuffer = "";
454
417
  let thinkingBuffer = "";
455
418
  let argsBuffer = "";
456
419
 
457
- // 完成响应数据
458
420
  let stopReason: string | undefined;
459
421
  let stopSequence: string | null | undefined;
460
422
  let rawResponseId = "";
461
423
 
462
424
  if (request.include?.providerMetadata !== "off") {
463
- const headerMetadata = pickProviderHeaders(response.headers);
425
+ const headerMetadata = pickProviderHeaders(headers);
464
426
  auxiliary.recordProviderMetadata(
465
427
  "header",
466
428
  Object.keys(headerMetadata).length > 0 ? { headers: headerMetadata } : undefined,
467
429
  );
468
430
  }
469
431
 
470
- try {
471
- while (true) {
472
- const readResult = await reader.read().catch((err: unknown) => {
473
- throw new AIStreamError(
474
- `Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`,
475
- "STREAM_ERROR",
476
- );
477
- });
478
- const { done, value } = readResult;
479
- const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value as Uint8Array);
480
-
481
- const malformedWarning = emitMalformedStreamWarning(factory, {
482
- count: malformedEvents,
483
- providerLabel: "Messages",
484
- transportLabel: "SSE event(s)",
485
- });
486
- if (malformedWarning) {
487
- yield malformedWarning;
488
- }
489
-
490
- for (const sseEvent of events) {
491
- switch (sseEvent.type) {
492
- case "ping":
493
- continue;
432
+ for await (const batch of iterateProviderStreamBatches({
433
+ reader,
434
+ parser,
435
+ factory,
436
+ providerLabel: "Messages",
437
+ transportLabel: "SSE event(s)",
438
+ incompleteMessage: "Stream ended with an incomplete Messages SSE frame",
439
+ })) {
440
+ for (const warning of batch.warnings) yield warning;
441
+
442
+ for (const sseEvent of batch.items) {
443
+ switch (sseEvent.type) {
444
+ case "ping":
445
+ continue;
446
+
447
+ case "error": {
448
+ const err = sseEvent.data.error;
449
+ yield factory.responseWarning(err.message, err.type);
450
+ continue;
451
+ }
494
452
 
495
- case "error": {
496
- const err = sseEvent.data.error;
497
- yield factory.responseWarning(err.message, err.type);
498
- continue;
499
- }
453
+ case "message_start": {
454
+ messageResponse = sseEvent.data.message;
455
+ rawResponseId = messageResponse.id;
456
+ continue;
457
+ }
500
458
 
501
- case "message_start": {
502
- messageResponse = sseEvent.data.message;
503
- rawResponseId = messageResponse.id;
504
- // 检查是否有 thinking 能力
505
- if (messageResponse.content.some((b) => b.type === "thinking" || b.type === "redacted_thinking")) {
506
- hasStreamedReasoning = true;
459
+ case "content_block_start": {
460
+ const block = sseEvent.data.content_block;
461
+ currentContentBlockIndex = sseEvent.data.index;
462
+
463
+ switch (block.type) {
464
+ case "text": {
465
+ currentItemType = "message";
466
+ currentItemId = synthesizeItemId("msg", currentContentBlockIndex, rawResponseId);
467
+ textBuffer = "";
468
+ yield factory.messageStarted(currentItemId);
469
+ break;
507
470
  }
508
- continue;
509
- }
510
-
511
- case "content_block_start": {
512
- const block = sseEvent.data.content_block;
513
- currentContentBlockIndex = sseEvent.data.index;
514
-
515
- switch (block.type) {
516
- case "text": {
517
- currentItemType = "message";
518
- currentItemId = synthesizeItemId("msg", currentContentBlockIndex, rawResponseId);
519
- textBuffer = "";
520
- yield factory.messageStarted(currentItemId);
521
- break;
522
- }
523
- case "thinking": {
524
- hasStreamedReasoning = true;
525
- currentItemType = "reasoning";
526
- currentItemId = synthesizeItemId("reason", currentContentBlockIndex, rawResponseId);
527
- currentThinkingVisibility = "full";
528
- thinkingBuffer = "";
529
- yield factory.reasoningStarted(currentItemId, "full");
530
- break;
531
- }
532
- case "redacted_thinking": {
533
- hasStreamedReasoning = true;
534
- currentItemType = "reasoning";
535
- currentItemId = synthesizeItemId("reason-redacted", currentContentBlockIndex, rawResponseId);
536
- currentThinkingVisibility = "redacted";
537
- const data = (block as unknown as { data: string }).data;
538
- yield factory.reasoningStarted(currentItemId, "redacted");
539
- yield factory.reasoningDelta(currentItemId, textBlock(data));
540
- const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
541
- yield factory.reasoningCompleted(currentItemId);
542
- output.push(redactedItem);
543
- rawReplayContent.push({ type: "redacted_thinking", data });
544
- currentItemType = null;
545
- break;
546
- }
547
- case "tool_use": {
548
- const tuBlock = block as unknown as { id: string; name: string };
549
- currentItemType = "tool_call";
550
- currentItemId = tuBlock.id;
551
- currentToolName = tuBlock.name;
552
- currentArgsText = "";
553
- argsBuffer = "";
554
- yield factory.toolCallStarted(currentItemId, currentToolName);
555
- break;
556
- }
471
+ case "thinking": {
472
+ currentItemType = "reasoning";
473
+ currentItemId = synthesizeItemId("reason", currentContentBlockIndex, rawResponseId);
474
+ currentThinkingVisibility = "full";
475
+ thinkingBuffer = "";
476
+ yield factory.reasoningStarted(currentItemId, "full");
477
+ break;
478
+ }
479
+ case "redacted_thinking": {
480
+ currentItemType = "reasoning";
481
+ currentItemId = synthesizeItemId("reason-redacted", currentContentBlockIndex, rawResponseId);
482
+ currentThinkingVisibility = "redacted";
483
+ const data = (block as unknown as { data: string }).data;
484
+ yield factory.reasoningStarted(currentItemId, "redacted");
485
+ yield factory.reasoningDelta(currentItemId, textBlock(data));
486
+ const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
487
+ yield factory.reasoningCompleted(currentItemId);
488
+ output.push(redactedItem);
489
+ rawReplayContent.push({ type: "redacted_thinking", data });
490
+ currentItemType = null;
491
+ break;
492
+ }
493
+ case "tool_use": {
494
+ const tuBlock = block as unknown as { id: string; name: string };
495
+ currentItemType = "tool_call";
496
+ currentItemId = tuBlock.id;
497
+ currentToolName = tuBlock.name;
498
+ currentArgsText = "";
499
+ argsBuffer = "";
500
+ yield factory.toolCallStarted(currentItemId, currentToolName);
501
+ break;
557
502
  }
558
- continue;
559
503
  }
504
+ continue;
505
+ }
506
+
507
+ case "content_block_delta": {
508
+ const delta = sseEvent.data.delta;
560
509
 
561
- case "content_block_delta": {
562
- const delta = sseEvent.data.delta;
563
-
564
- switch (delta.type) {
565
- case "text_delta": {
566
- if (currentItemType === "message" && currentItemId) {
567
- const txt = (delta as unknown as { text: string }).text;
568
- textBuffer += txt;
569
- yield factory.messageDelta(currentItemId, textBlock(txt));
570
- }
571
- break;
510
+ switch (delta.type) {
511
+ case "text_delta": {
512
+ if (currentItemType === "message" && currentItemId) {
513
+ const txt = (delta as unknown as { text: string }).text;
514
+ textBuffer += txt;
515
+ yield factory.messageDelta(currentItemId, textBlock(txt));
572
516
  }
573
- case "thinking_delta": {
574
- if (currentItemType === "reasoning" && currentItemId) {
575
- const txt = (delta as unknown as { thinking: string }).thinking;
576
- thinkingBuffer += txt;
577
- yield factory.reasoningDelta(currentItemId, textBlock(txt));
578
- }
579
- break;
517
+ break;
518
+ }
519
+ case "thinking_delta": {
520
+ if (currentItemType === "reasoning" && currentItemId) {
521
+ const txt = (delta as unknown as { thinking: string }).thinking;
522
+ thinkingBuffer += txt;
523
+ yield factory.reasoningDelta(currentItemId, textBlock(txt));
580
524
  }
581
- case "input_json_delta": {
582
- if (currentItemType === "tool_call" && currentItemId) {
583
- const partial = (delta as unknown as { partial_json: string }).partial_json;
584
- argsBuffer += partial;
585
- yield factory.toolCallDelta(currentItemId, { argumentsText: partial });
586
- }
587
- break;
525
+ break;
526
+ }
527
+ case "input_json_delta": {
528
+ if (currentItemType === "tool_call" && currentItemId) {
529
+ const partial = (delta as unknown as { partial_json: string }).partial_json;
530
+ argsBuffer += partial;
531
+ yield factory.toolCallDelta(currentItemId, { argumentsText: partial });
588
532
  }
533
+ break;
589
534
  }
590
- continue;
591
535
  }
536
+ continue;
537
+ }
592
538
 
593
- case "content_block_stop": {
594
- if (currentItemType === "message" && currentItemId) {
595
- yield factory.messageCompleted(currentItemId);
596
- output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
597
- rawReplayContent.push({ type: "text", text: textBuffer });
598
- } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
599
- yield factory.reasoningCompleted(currentItemId);
600
- output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
601
- rawReplayContent.push({ type: "thinking", thinking: thinkingBuffer });
602
- } else if (currentItemType === "tool_call" && currentItemId) {
603
- const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
604
- yield factory.toolCallCompleted(currentItemId);
605
- output.push(tcItem);
606
- rawReplayContent.push({
607
- type: "tool_use",
608
- id: currentItemId,
609
- name: currentToolName,
610
- input: parseProviderToolUseInput(currentArgsText || argsBuffer),
611
- });
612
- }
613
-
614
- currentItemType = null;
615
- currentItemId = "";
616
- continue;
539
+ case "content_block_stop": {
540
+ if (currentItemType === "message" && currentItemId) {
541
+ yield factory.messageCompleted(currentItemId);
542
+ output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
543
+ rawReplayContent.push({ type: "text", text: textBuffer });
544
+ } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
545
+ yield factory.reasoningCompleted(currentItemId);
546
+ output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
547
+ rawReplayContent.push({ type: "thinking", thinking: thinkingBuffer });
548
+ } else if (currentItemType === "tool_call" && currentItemId) {
549
+ const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
550
+ yield factory.toolCallCompleted(currentItemId);
551
+ output.push(tcItem);
552
+ rawReplayContent.push({
553
+ type: "tool_use",
554
+ id: currentItemId,
555
+ name: currentToolName,
556
+ input: parseProviderToolUseInput(currentArgsText || argsBuffer),
557
+ });
617
558
  }
618
559
 
619
- case "message_delta": {
620
- stopReason = sseEvent.data.delta.stop_reason;
621
- stopSequence = sseEvent.data.delta.stop_sequence;
622
- const u = sseEvent.data.usage;
623
- if (u) {
624
- auxiliary.recordUsage(usageFromAnthropicMessages(u), "stream", u);
625
- }
626
- continue;
627
- }
560
+ currentItemType = null;
561
+ currentItemId = "";
562
+ continue;
563
+ }
628
564
 
629
- case "message_stop": {
630
- // 流结束,构造 final response
631
- break;
565
+ case "message_delta": {
566
+ stopReason = sseEvent.data.delta.stop_reason;
567
+ stopSequence = sseEvent.data.delta.stop_sequence;
568
+ const u = sseEvent.data.usage;
569
+ if (u) {
570
+ auxiliary.recordUsage(usageFromAnthropicMessages(u), "stream", u);
632
571
  }
572
+ continue;
633
573
  }
634
- }
635
574
 
636
- if (done) {
637
- streamDone = true;
638
- break;
575
+ case "message_stop": {
576
+ break;
577
+ }
639
578
  }
640
579
  }
641
- } finally {
642
- try {
643
- if (!streamDone) await reader.cancel().catch(() => undefined);
644
- } finally {
645
- reader.releaseLock();
646
- }
647
580
  }
648
581
 
649
- if (parser.getRemaining().trim().length > 0) {
650
- yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
651
- }
652
-
653
- // 构造 replay
654
582
  const replay = [...replayFromOutput(output)];
655
583
 
656
- // 附加 opaque replay item 用于续接
657
- // 保存 provider 原始 block 以实现高保真 replay
658
584
  if (messageResponse) {
659
585
  const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
660
586
  replay.push(
@@ -680,41 +606,12 @@ export class MessagesAdapter extends AdapterBase {
680
606
  );
681
607
  }
682
608
 
683
- // 警告低 replay fidelity
684
- if (!hasStreamedReasoning) {
685
- // 没有 reasoning,replay fidelity 较低
686
- }
687
-
688
- const auxiliaryResult = await auxiliary.finalize(factory);
689
- for (const event of auxiliaryResult.events) {
690
- yield event;
691
- }
692
-
693
- if (!completedEmitted) {
694
- completedEmitted = true;
695
- const finalResponse = this.buildResponse(
696
- request,
697
- {
698
- output,
699
- replay,
700
- stopReason: stopReason ? mapStopReason(stopReason) : undefined,
701
- usage: auxiliaryResult.usage,
702
- billing: auxiliaryResult.billing,
703
- auxiliary: auxiliaryResult.auxiliary,
704
- warnings: auxiliaryResult.warnings,
705
- metadataSources: auxiliaryResult.metadataSources,
706
- rawResponseId,
707
- },
708
- factory,
709
- );
710
- yield factory.responseCompleted({
711
- replay: finalResponse.replay,
712
- stopReason: finalResponse.stopReason,
713
- trace: finalResponse.backend,
714
- usage: finalResponse.usage,
715
- billing: finalResponse.billing,
716
- auxiliary: finalResponse.auxiliary,
717
- warnings: finalResponse.warnings,
609
+ if (gate.tryComplete()) {
610
+ yield* this.emitStreamCompleted(factory, request, auxiliary, {
611
+ output,
612
+ replay,
613
+ stopReason: stopReason ? mapStopReason(stopReason) : undefined,
614
+ rawResponseId,
718
615
  });
719
616
  }
720
617
  }