@codehz/ai 0.1.2 → 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/.oxlintrc.json +1 -16
- package/AGENTS.md +37 -0
- package/README.md +40 -16
- package/dist/index.d.mts +29 -119
- package/dist/index.mjs +88 -123
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +2 -25
- package/src/adapters/index.ts +1 -0
- package/src/adapters/messages.ts +1 -2
- package/src/adapters/mock.ts +162 -13
- package/src/adapters/ollama.ts +2 -15
- package/src/adapters/responses.ts +1 -2
- package/src/helpers/adapter-auxiliary.ts +1 -7
- package/src/helpers/adapter-base.ts +4 -5
- package/src/types/adapter.ts +1 -82
- package/src/types/index.ts +0 -4
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
|
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;
|
package/src/adapters/index.ts
CHANGED
package/src/adapters/messages.ts
CHANGED
|
@@ -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
|
|
239
|
+
readonly nativeStreaming = true;
|
|
241
240
|
|
|
242
241
|
private apiKey: string;
|
|
243
242
|
private apiVersion: string;
|
package/src/adapters/mock.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/adapters/ollama.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
145
|
+
return new AdapterAuxiliaryState(request);
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
|
package/src/types/adapter.ts
CHANGED
|
@@ -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
|
|
26
|
+
readonly nativeStreaming: boolean;
|
|
108
27
|
stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
|
|
109
28
|
}
|
|
110
29
|
|
package/src/types/index.ts
CHANGED
|
@@ -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";
|