@codehz/ai 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -17,7 +17,8 @@
17
17
  "test": "bun test",
18
18
  "example:basic": "bun run examples/basic.ts",
19
19
  "example:multi-turn": "bun run examples/multi-turn.ts",
20
- "example:tool-loop": "bun run examples/tool-loop.ts"
20
+ "example:tool-loop": "bun run examples/tool-loop.ts",
21
+ "prepack": "tsdown"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/bun": "latest",
@@ -26,5 +27,5 @@
26
27
  "tsdown": "^0.22.3",
27
28
  "typescript": "^6"
28
29
  },
29
- "version": "0.1.1"
30
+ "version": "0.1.3"
30
31
  }
@@ -22,7 +22,7 @@ import {
22
22
  } from "../helpers/mapping.js";
23
23
  import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
24
24
 
25
- import type { AdapterCapabilities, NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
25
+ import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
26
26
 
27
27
  // ── 类型 ──────────────────────────────────────────────────────
28
28
 
@@ -249,29 +249,12 @@ function buildAssistantReplayMessage(params: {
249
249
 
250
250
  export class ChatCompletionsAdapter extends AdapterBase {
251
251
  readonly kind = "chat-completions" as const;
252
- readonly capabilities: AdapterCapabilities = {
253
- nativeStreaming: true,
254
- messageStreaming: true,
255
- reasoningStreaming: false,
256
- toolCallStreaming: false,
257
- hiddenReasoningReplay: "none" as const,
258
- replayFidelity: "low" as const,
259
- tools: true,
260
- usage: "full" as const,
261
- billing: "derived" as const,
262
- providerMetadata: false,
263
- };
252
+ readonly nativeStreaming = true;
264
253
 
265
254
  private apiKey: string;
266
255
  private baseUrl: string;
267
256
  private fetchFn: FetchFn;
268
257
 
269
- private markReasoningCompatibility(): void {
270
- this.capabilities.reasoningStreaming = true;
271
- this.capabilities.hiddenReasoningReplay = "partial";
272
- this.capabilities.replayFidelity = "medium";
273
- }
274
-
275
258
  constructor(options: ChatCompletionsAdapterOptions) {
276
259
  super();
277
260
  this.apiKey = options.apiKey;
@@ -439,7 +422,6 @@ export class ChatCompletionsAdapter extends AdapterBase {
439
422
  let currentReasoningId = "";
440
423
  let hasMessageStarted = false;
441
424
  let hasReasoningStarted = false;
442
- let hasStreamedReasoning = false;
443
425
 
444
426
  // tool_calls 累积: tool call index → { id, name, args }
445
427
  const pendingToolCalls = new Map<number, PendingToolCall>();
@@ -540,7 +522,6 @@ export class ChatCompletionsAdapter extends AdapterBase {
540
522
  if (!hasReasoningStarted) {
541
523
  currentReasoningId = `reason-${chunk.id}`;
542
524
  hasReasoningStarted = true;
543
- hasStreamedReasoning = true;
544
525
  accumulatedReasoning = "";
545
526
  yield factory.reasoningStarted(currentReasoningId, "full");
546
527
  }
@@ -609,8 +590,6 @@ export class ChatCompletionsAdapter extends AdapterBase {
609
590
  yield event;
610
591
  }
611
592
 
612
- if (hasStreamedReasoning) this.markReasoningCompatibility();
613
-
614
593
  // 构建 stop reason
615
594
  const stopReason = mapStopReason(finishReason);
616
595
 
@@ -665,8 +644,6 @@ export class ChatCompletionsAdapter extends AdapterBase {
665
644
  if (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0) {
666
645
  yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
667
646
 
668
- if (hasStreamedReasoning) this.markReasoningCompatibility();
669
-
670
647
  const { events, assistantReplayMessage } = finalizePendingTurn();
671
648
  for (const event of events) {
672
649
  yield event;
@@ -22,6 +22,7 @@ export type { OllamaAdapterOptions } from "./ollama.js";
22
22
  export { MockAdapter } from "./mock.js";
23
23
  export type {
24
24
  MockAdapterOptions,
25
+ MockTextStreamOptions,
25
26
  MockInputExpectation,
26
27
  MockRequestExpectation,
27
28
  MockTurnContext,
@@ -27,7 +27,6 @@ import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
27
27
 
28
28
  import { parseSSEEvents } from "../helpers/sse-parser.js";
29
29
 
30
- import { CAPABILITY_MATRIX } from "../index.js";
31
30
  import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
32
31
 
33
32
  // ── 类型 ──────────────────────────────────────────────────────
@@ -237,7 +236,7 @@ function buildStreamMetadata(options: {
237
236
 
238
237
  export class MessagesAdapter extends AdapterBase {
239
238
  readonly kind = "messages" as const;
240
- readonly capabilities = CAPABILITY_MATRIX.messages;
239
+ readonly nativeStreaming = true;
241
240
 
242
241
  private apiKey: string;
243
242
  private apiVersion: string;
@@ -12,7 +12,6 @@
12
12
  import { AIRequestError } from "../core/errors.js";
13
13
  import { AdapterBase } from "../helpers/adapter-base.js";
14
14
  import { messageItem, reasoningItem, replayFromOutput, textBlock } from "../helpers/mapping.js";
15
- import { CAPABILITY_MATRIX } from "../types/adapter.js";
16
15
 
17
16
  import type {
18
17
  AIStreamEvent,
@@ -81,10 +80,26 @@ export type MockAuxiliaryStep = {
81
80
  auxiliary?: Partial<AuxiliaryInfo>;
82
81
  };
83
82
 
83
+ export type MockTextStreamOptions = {
84
+ /**
85
+ * 每秒吐出的字符数。未设置时仍会按 chunk 拆分,但不会额外等待。
86
+ */
87
+ charsPerSecond?: number;
88
+ /**
89
+ * 每个 delta 最多包含多少个字符,默认 1。
90
+ */
91
+ chunkSize?: number;
92
+ /**
93
+ * 首个 delta 发出前的延迟。
94
+ */
95
+ initialDelayMs?: number;
96
+ };
97
+
84
98
  export type MockMessageStep = {
85
99
  type: "message";
86
100
  id?: string;
87
101
  content: string | ContentBlock[];
102
+ stream?: MockTextStreamOptions | false;
88
103
  };
89
104
 
90
105
  export type MockReasoningStep = {
@@ -92,6 +107,7 @@ export type MockReasoningStep = {
92
107
  id?: string;
93
108
  visibility?: Extract<OutputItem, { type: "reasoning" }>["visibility"];
94
109
  content: string | ContentBlock[];
110
+ stream?: MockTextStreamOptions | false;
95
111
  };
96
112
 
97
113
  export type MockToolCallStep = {
@@ -101,11 +117,13 @@ export type MockToolCallStep = {
101
117
  argumentsText: string;
102
118
  argumentsJson?: unknown;
103
119
  streamArguments?: boolean;
120
+ stream?: MockTextStreamOptions | false;
104
121
  };
105
122
 
106
123
  export type MockOutputStep = {
107
124
  type: "output";
108
125
  item: Extract<OutputItem, { type: "message" | "reasoning" | "tool_call" }>;
126
+ stream?: MockTextStreamOptions | false;
109
127
  };
110
128
 
111
129
  export type MockCompleteStep = {
@@ -159,6 +177,7 @@ export type MockAdapterOptions = {
159
177
  turns: MockTurn[];
160
178
  onExhausted?: "throw" | "repeat-last" | "complete-empty";
161
179
  providerMetadata?: Record<string, unknown>;
180
+ stream?: MockTextStreamOptions;
162
181
  };
163
182
 
164
183
  type MockTurnRecord = {
@@ -177,13 +196,20 @@ type MockProviderRequest = {
177
196
  remainingPendingToolCalls: ToolCallItem[];
178
197
  };
179
198
 
199
+ type ResolvedMockTextStreamOptions = {
200
+ charsPerSecond?: number;
201
+ chunkSize: number;
202
+ initialDelayMs: number;
203
+ };
204
+
180
205
  export class MockAdapter extends AdapterBase {
181
206
  readonly kind = "mock" as const;
182
- readonly capabilities = CAPABILITY_MATRIX.mock;
207
+ readonly nativeStreaming = false;
183
208
 
184
209
  private readonly turns: MockTurn[];
185
210
  private readonly onExhausted: NonNullable<MockAdapterOptions["onExhausted"]>;
186
211
  private readonly providerMetadata?: Record<string, unknown>;
212
+ private readonly defaultStream?: ResolvedMockTextStreamOptions;
187
213
 
188
214
  private cursor = 0;
189
215
  private previousReplay: ReplayItem[] = [];
@@ -196,6 +222,7 @@ export class MockAdapter extends AdapterBase {
196
222
  this.turns = options.turns;
197
223
  this.onExhausted = options.onExhausted ?? "throw";
198
224
  this.providerMetadata = options.providerMetadata;
225
+ this.defaultStream = resolveMockTextStreamOptions(options.stream, "adapter stream");
199
226
  }
200
227
 
201
228
  protected async buildRequest(request: NormalizedRequest): Promise<MockProviderRequest> {
@@ -254,26 +281,31 @@ export class MockAdapter extends AdapterBase {
254
281
  break;
255
282
  case "message": {
256
283
  const item = createMessageFromStep(step, request, mockRequest.turnIndex, stepIndex);
257
- yield* emitMessage(factory, item);
284
+ yield* emitMessage(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "message"));
258
285
  output.push(item);
259
286
  break;
260
287
  }
261
288
  case "reasoning": {
262
289
  const item = createReasoningFromStep(step, request, mockRequest.turnIndex, stepIndex);
263
- yield* emitReasoning(factory, item);
290
+ yield* emitReasoning(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "reasoning"));
264
291
  output.push(item);
265
292
  break;
266
293
  }
267
294
  case "tool_call": {
268
295
  const item = createToolCallFromStep(step);
269
- yield* emitToolCall(factory, item, step.streamArguments ?? true);
296
+ yield* emitToolCall(
297
+ factory,
298
+ item,
299
+ step.streamArguments ?? true,
300
+ resolveStepStreamOptions(this.defaultStream, step.stream, "tool_call"),
301
+ );
270
302
  output.push(item);
271
303
  break;
272
304
  }
273
305
  case "output": {
274
306
  assertSupportedOutputItem(step.item);
275
307
  const item = attachSyntheticId(step.item, request, mockRequest.turnIndex, stepIndex);
276
- yield* emitOutputItem(factory, item);
308
+ yield* emitOutputItem(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "output"));
277
309
  output.push(item);
278
310
  break;
279
311
  }
@@ -464,30 +496,40 @@ function attachSyntheticId(
464
496
  async function* emitOutputItem(
465
497
  factory: EventFactory,
466
498
  item: Extract<OutputItem, { type: "message" | "reasoning" | "tool_call" }>,
499
+ stream?: ResolvedMockTextStreamOptions,
467
500
  ): AsyncIterable<AIStreamEvent> {
468
501
  if (item.type === "message") {
469
- yield* emitMessage(factory, item);
502
+ yield* emitMessage(factory, item, stream);
470
503
  return;
471
504
  }
472
505
 
473
506
  if (item.type === "reasoning") {
474
- yield* emitReasoning(factory, item);
507
+ yield* emitReasoning(factory, item, stream);
475
508
  return;
476
509
  }
477
510
 
478
- yield* emitToolCall(factory, item, true);
511
+ yield* emitToolCall(factory, item, true, stream);
479
512
  }
480
513
 
481
- async function* emitMessage(factory: EventFactory, item: MessageItem): AsyncIterable<AIStreamEvent> {
514
+ async function* emitMessage(
515
+ factory: EventFactory,
516
+ item: MessageItem,
517
+ stream?: ResolvedMockTextStreamOptions,
518
+ ): AsyncIterable<AIStreamEvent> {
482
519
  if (!item.id) {
483
520
  throw new AIRequestError("Mock message output requires an id after normalization", "MOCK_MESSAGE_ID_MISSING");
484
521
  }
485
522
 
486
523
  yield factory.messageStarted(item.id);
487
524
 
525
+ let chunkIndex = 0;
488
526
  for (const block of item.content) {
489
527
  if (block.type === "text") {
490
- yield factory.messageDelta(item.id, block.text);
528
+ for (const chunk of chunkText(block.text, stream)) {
529
+ await delayForChunk(stream, chunkIndex, chunk.length);
530
+ yield factory.messageDelta(item.id, chunk);
531
+ chunkIndex += 1;
532
+ }
491
533
  }
492
534
  }
493
535
 
@@ -497,6 +539,7 @@ async function* emitMessage(factory: EventFactory, item: MessageItem): AsyncIter
497
539
  async function* emitReasoning(
498
540
  factory: EventFactory,
499
541
  item: Extract<OutputItem, { type: "reasoning" }>,
542
+ stream?: ResolvedMockTextStreamOptions,
500
543
  ): AsyncIterable<AIStreamEvent> {
501
544
  if (!item.id) {
502
545
  throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
@@ -504,8 +547,18 @@ async function* emitReasoning(
504
547
 
505
548
  yield factory.reasoningStarted(item.id, item.visibility);
506
549
 
550
+ let chunkIndex = 0;
507
551
  for (const block of item.content) {
508
- yield factory.reasoningDelta(item.id, block);
552
+ if (block.type !== "text") {
553
+ yield factory.reasoningDelta(item.id, block);
554
+ continue;
555
+ }
556
+
557
+ for (const chunk of chunkText(block.text, stream)) {
558
+ await delayForChunk(stream, chunkIndex, chunk.length);
559
+ yield factory.reasoningDelta(item.id, textBlock(chunk));
560
+ chunkIndex += 1;
561
+ }
509
562
  }
510
563
 
511
564
  yield factory.reasoningCompleted(item);
@@ -515,16 +568,112 @@ async function* emitToolCall(
515
568
  factory: EventFactory,
516
569
  item: ToolCallItem,
517
570
  streamArguments: boolean,
571
+ stream?: ResolvedMockTextStreamOptions,
518
572
  ): AsyncIterable<AIStreamEvent> {
519
573
  yield factory.toolCallStarted(item.id, item.name);
520
574
 
521
575
  if (streamArguments && item.argumentsText) {
522
- yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
576
+ let chunkIndex = 0;
577
+ for (const chunk of chunkText(item.argumentsText, stream)) {
578
+ await delayForChunk(stream, chunkIndex, chunk.length);
579
+ yield factory.toolCallDelta(item.id, { argumentsText: chunk });
580
+ chunkIndex += 1;
581
+ }
523
582
  }
524
583
 
525
584
  yield factory.toolCallCompleted(item);
526
585
  }
527
586
 
587
+ function resolveStepStreamOptions(
588
+ defaults: ResolvedMockTextStreamOptions | undefined,
589
+ override: MockTextStreamOptions | false | undefined,
590
+ label: string,
591
+ ): ResolvedMockTextStreamOptions | undefined {
592
+ if (override === false) {
593
+ return undefined;
594
+ }
595
+
596
+ return resolveMockTextStreamOptions(override, `${label} stream`, defaults);
597
+ }
598
+
599
+ function resolveMockTextStreamOptions(
600
+ options: MockTextStreamOptions | undefined,
601
+ label: string,
602
+ defaults?: ResolvedMockTextStreamOptions,
603
+ ): ResolvedMockTextStreamOptions | undefined {
604
+ if (options === undefined) {
605
+ return defaults;
606
+ }
607
+
608
+ const chunkSize = options.chunkSize ?? defaults?.chunkSize ?? 1;
609
+ const initialDelayMs = options.initialDelayMs ?? defaults?.initialDelayMs ?? 0;
610
+ const charsPerSecond = options.charsPerSecond ?? defaults?.charsPerSecond;
611
+
612
+ if (!Number.isInteger(chunkSize) || chunkSize < 1) {
613
+ throw new AIRequestError(`${label}: chunkSize must be a positive integer`, "MOCK_STREAM_CONFIG_INVALID");
614
+ }
615
+
616
+ if (!Number.isFinite(initialDelayMs) || initialDelayMs < 0) {
617
+ throw new AIRequestError(`${label}: initialDelayMs must be a non-negative number`, "MOCK_STREAM_CONFIG_INVALID");
618
+ }
619
+
620
+ if (charsPerSecond !== undefined && (!Number.isFinite(charsPerSecond) || charsPerSecond <= 0)) {
621
+ throw new AIRequestError(`${label}: charsPerSecond must be a positive number`, "MOCK_STREAM_CONFIG_INVALID");
622
+ }
623
+
624
+ return {
625
+ chunkSize,
626
+ initialDelayMs,
627
+ charsPerSecond,
628
+ };
629
+ }
630
+
631
+ function chunkText(text: string, stream?: ResolvedMockTextStreamOptions): string[] {
632
+ if (!text) {
633
+ return [];
634
+ }
635
+
636
+ if (!stream) {
637
+ return [text];
638
+ }
639
+
640
+ const chars = Array.from(text);
641
+ const chunks: string[] = [];
642
+
643
+ for (let index = 0; index < chars.length; index += stream.chunkSize) {
644
+ chunks.push(chars.slice(index, index + stream.chunkSize).join(""));
645
+ }
646
+
647
+ return chunks;
648
+ }
649
+
650
+ async function delayForChunk(
651
+ stream: ResolvedMockTextStreamOptions | undefined,
652
+ chunkIndex: number,
653
+ chunkLength: number,
654
+ ): Promise<void> {
655
+ if (!stream) {
656
+ return;
657
+ }
658
+
659
+ if (chunkIndex === 0 && stream.initialDelayMs > 0) {
660
+ await sleep(stream.initialDelayMs);
661
+ return;
662
+ }
663
+
664
+ if (chunkIndex > 0 && stream.charsPerSecond !== undefined) {
665
+ await sleep((chunkLength / stream.charsPerSecond) * 1000);
666
+ }
667
+ }
668
+
669
+ async function sleep(ms: number): Promise<void> {
670
+ if (ms <= 0) {
671
+ return;
672
+ }
673
+
674
+ await new Promise((resolve) => setTimeout(resolve, ms));
675
+ }
676
+
528
677
  function resolveStopReason(output: OutputItem[]): StopReason {
529
678
  return output.some((item) => item.type === "tool_call") ? "tool_call" : "end_turn";
530
679
  }
@@ -28,7 +28,7 @@ import {
28
28
  } from "../helpers/mapping.js";
29
29
  import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
30
30
 
31
- import type { AdapterCapabilities, NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
31
+ import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
32
32
 
33
33
  // ── 选项类型 ──────────────────────────────────────────────────
34
34
 
@@ -226,18 +226,7 @@ function isOllamaToolCalls(value: unknown): value is OllamaToolCall[] {
226
226
 
227
227
  export class OllamaAdapter extends AdapterBase {
228
228
  readonly kind = "ollama" as const;
229
- readonly capabilities: AdapterCapabilities = {
230
- nativeStreaming: true,
231
- messageStreaming: true,
232
- reasoningStreaming: false,
233
- toolCallStreaming: false,
234
- hiddenReasoningReplay: "none" as const,
235
- replayFidelity: "low" as const,
236
- tools: true,
237
- usage: "partial" as const,
238
- billing: "none" as const,
239
- providerMetadata: false,
240
- };
229
+ readonly nativeStreaming = true;
241
230
 
242
231
  private baseUrl: string;
243
232
  private apiKey: string | undefined;
@@ -406,7 +395,6 @@ export class OllamaAdapter extends AdapterBase {
406
395
 
407
396
  try {
408
397
  while (true) {
409
- // oxlint-disable-next-line no-await-in-loop
410
398
  const { done, value } = await reader.read();
411
399
  if (done) break;
412
400
 
@@ -520,7 +508,6 @@ export class OllamaAdapter extends AdapterBase {
520
508
  );
521
509
  }
522
510
 
523
- // oxlint-disable-next-line no-await-in-loop
524
511
  const auxiliaryResult = await auxiliary.finalize(factory);
525
512
  for (const event of auxiliaryResult.events) {
526
513
  yield event;
@@ -25,7 +25,6 @@ import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
25
25
 
26
26
  import { parseSSEEvents } from "../helpers/sse-parser.js";
27
27
 
28
- import { CAPABILITY_MATRIX } from "../index.js";
29
28
  import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
30
29
 
31
30
  // ── 类型 ──────────────────────────────────────────────────────
@@ -194,7 +193,7 @@ function canonicalToResponsesBlock(b: import("../index.js").ContentBlock): Respo
194
193
 
195
194
  export class ResponsesAdapter extends AdapterBase {
196
195
  readonly kind = "responses" as const;
197
- readonly capabilities = CAPABILITY_MATRIX.responses;
196
+ readonly nativeStreaming = true;
198
197
 
199
198
  private apiKey: string;
200
199
  private baseUrl: string;
@@ -1,7 +1,6 @@
1
1
  import { WarningCode } from "../core/errors.js";
2
2
  import type { EventFactory } from "../core/event-factory.js";
3
3
  import type {
4
- AdapterCapabilities,
5
4
  AIStreamEvent,
6
5
  BillingInfo,
7
6
  NormalizedRequest,
@@ -18,7 +17,6 @@ export type BillingPostprocessHook = (context: {
18
17
  usage?: Usage;
19
18
  billing?: BillingInfo;
20
19
  auxiliary?: AuxiliaryInfo;
21
- capabilities: AdapterCapabilities;
22
20
  }) => MaybePromise<Partial<BillingInfo> | undefined>;
23
21
 
24
22
  export type AuxiliaryFinalizeOptions = {
@@ -41,10 +39,7 @@ export class AdapterAuxiliaryState {
41
39
  private readonly collector = new AuxiliaryCollector();
42
40
  private readonly metadataSources = new Set<string>();
43
41
 
44
- constructor(
45
- private readonly request: NormalizedRequest,
46
- private readonly capabilities: AdapterCapabilities,
47
- ) {}
42
+ constructor(private readonly request: NormalizedRequest) {}
48
43
 
49
44
  recordUsage(usage: Partial<Usage>, source: UsageSource, raw?: unknown): void {
50
45
  if (this.request.include?.usage === "off" || isEmptyRecord(usage)) return;
@@ -75,7 +70,6 @@ export class AdapterAuxiliaryState {
75
70
  usage: snapshot.usage,
76
71
  billing: snapshot.billing,
77
72
  auxiliary: snapshot.auxiliary,
78
- capabilities: this.capabilities,
79
73
  });
80
74
  if (derived && !isEmptyRecord(derived)) {
81
75
  this.collector.recordBilling(
@@ -14,7 +14,6 @@
14
14
  import type {
15
15
  NormalizedRequest,
16
16
  BackendAdapter,
17
- AdapterCapabilities,
18
17
  AIStreamEvent,
19
18
  AIResponse,
20
19
  AuxiliaryInfo,
@@ -56,7 +55,7 @@ export type StreamResult = {
56
55
 
57
56
  export abstract class AdapterBase implements BackendAdapter {
58
57
  abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
59
- abstract readonly capabilities: AdapterCapabilities;
58
+ abstract readonly nativeStreaming: boolean;
60
59
 
61
60
  /**
62
61
  * stream 模板方法:
@@ -67,7 +66,7 @@ export abstract class AdapterBase implements BackendAdapter {
67
66
  async *stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent> {
68
67
  const factory = createEventFactory({
69
68
  responseId: request.requestId,
70
- backend: { kind: this.kind, isSynthetic: !this.capabilities.nativeStreaming },
69
+ backend: { kind: this.kind, isSynthetic: !this.nativeStreaming },
71
70
  });
72
71
 
73
72
  yield factory.responseStarted(request.model);
@@ -130,7 +129,7 @@ export abstract class AdapterBase implements BackendAdapter {
130
129
  requestId: request.requestId,
131
130
  rawResponseId: result.rawResponseId,
132
131
  adapter: this.kind,
133
- isSyntheticStream: !this.capabilities.nativeStreaming,
132
+ isSyntheticStream: !this.nativeStreaming,
134
133
  metadataSources: result.metadataSources,
135
134
  warnings,
136
135
  },
@@ -143,7 +142,7 @@ export abstract class AdapterBase implements BackendAdapter {
143
142
  }
144
143
 
145
144
  protected createAuxiliaryState(request: NormalizedRequest): AdapterAuxiliaryState {
146
- return new AdapterAuxiliaryState(request, this.capabilities);
145
+ return new AdapterAuxiliaryState(request);
147
146
  }
148
147
  }
149
148
 
@@ -2,7 +2,6 @@
2
2
  * BackendAdapter — adapter 内部协议和 client 公开类型
3
3
  *
4
4
  * adapter 对前台只暴露一个统一适配点。
5
- * 能力矩阵在此落成代码而非仅存在于文档。
6
5
  */
7
6
 
8
7
  import type { AIRequest } from "./request.js";
@@ -20,91 +19,11 @@ export type NormalizedRequest = AIRequest & {
20
19
  requestId: string;
21
20
  };
22
21
 
23
- // ── 能力矩阵 ──────────────────────────────────────────────────
24
-
25
- export type AdapterCapabilities = {
26
- nativeStreaming: boolean;
27
- messageStreaming: boolean;
28
- reasoningStreaming: boolean;
29
- toolCallStreaming: boolean;
30
- hiddenReasoningReplay: "full" | "partial" | "none";
31
- replayFidelity: "high" | "medium" | "low";
32
- tools: boolean;
33
- usage: "full" | "partial" | "none";
34
- billing: "direct" | "lookup" | "derived" | "none";
35
- providerMetadata: boolean;
36
- };
37
-
38
- // ── 能力矩阵常量(文档中的能力表在此落代码) ────────────────
39
-
40
- export const CAPABILITY_MATRIX = {
41
- responses: {
42
- nativeStreaming: true,
43
- messageStreaming: true,
44
- reasoningStreaming: true,
45
- toolCallStreaming: true,
46
- hiddenReasoningReplay: "full" as const,
47
- replayFidelity: "high" as const,
48
- tools: true,
49
- usage: "full" as const,
50
- billing: "lookup" as const,
51
- providerMetadata: true,
52
- },
53
- messages: {
54
- nativeStreaming: true,
55
- messageStreaming: true,
56
- reasoningStreaming: false, // 条件支持,默认 false
57
- toolCallStreaming: true,
58
- hiddenReasoningReplay: "partial" as const,
59
- replayFidelity: "medium" as const,
60
- tools: true,
61
- usage: "full" as const,
62
- billing: "lookup" as const,
63
- providerMetadata: true,
64
- },
65
- "chat.completions": {
66
- nativeStreaming: true,
67
- messageStreaming: true,
68
- reasoningStreaming: false,
69
- toolCallStreaming: false, // 中,默认 false
70
- hiddenReasoningReplay: "none" as const,
71
- replayFidelity: "low" as const,
72
- tools: true,
73
- usage: "full" as const,
74
- billing: "derived" as const,
75
- providerMetadata: false,
76
- },
77
- ollama: {
78
- nativeStreaming: true,
79
- messageStreaming: true,
80
- reasoningStreaming: false,
81
- toolCallStreaming: false,
82
- hiddenReasoningReplay: "none" as const,
83
- replayFidelity: "low" as const,
84
- tools: true,
85
- usage: "partial" as const,
86
- billing: "none" as const,
87
- providerMetadata: false,
88
- },
89
- mock: {
90
- nativeStreaming: false,
91
- messageStreaming: true,
92
- reasoningStreaming: false,
93
- toolCallStreaming: true,
94
- hiddenReasoningReplay: "none" as const,
95
- replayFidelity: "high" as const,
96
- tools: true,
97
- usage: "none" as const,
98
- billing: "none" as const,
99
- providerMetadata: true,
100
- },
101
- } as const satisfies Record<string, AdapterCapabilities>;
102
-
103
22
  // ── Adapter 接口 ──────────────────────────────────────────────
104
23
 
105
24
  export interface BackendAdapter {
106
25
  readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
107
- readonly capabilities: AdapterCapabilities;
26
+ readonly nativeStreaming: boolean;
108
27
  stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
109
28
  }
110
29
 
@@ -48,12 +48,8 @@ export type {
48
48
  // Adapter 协议和 client 类型
49
49
  export type {
50
50
  BackendAdapter,
51
- AdapterCapabilities,
52
51
  FetchFn,
53
52
  NormalizedRequest,
54
53
  CreateAIClientOptions,
55
54
  AIClient,
56
55
  } from "./adapter.js";
57
-
58
- // 能力矩阵常量
59
- export { CAPABILITY_MATRIX } from "./adapter.js";