@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/.oxlintrc.json +1 -16
- package/AGENTS.md +37 -0
- package/README.md +42 -16
- package/dist/index.d.mts +178 -101
- package/dist/index.mjs +442 -202
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- 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/dist/index.mjs
CHANGED
|
@@ -1,55 +1,3 @@
|
|
|
1
|
-
//#region src/types/adapter.ts
|
|
2
|
-
const CAPABILITY_MATRIX = {
|
|
3
|
-
responses: {
|
|
4
|
-
nativeStreaming: true,
|
|
5
|
-
messageStreaming: true,
|
|
6
|
-
reasoningStreaming: true,
|
|
7
|
-
toolCallStreaming: true,
|
|
8
|
-
hiddenReasoningReplay: "full",
|
|
9
|
-
replayFidelity: "high",
|
|
10
|
-
tools: true,
|
|
11
|
-
usage: "full",
|
|
12
|
-
billing: "lookup",
|
|
13
|
-
providerMetadata: true
|
|
14
|
-
},
|
|
15
|
-
messages: {
|
|
16
|
-
nativeStreaming: true,
|
|
17
|
-
messageStreaming: true,
|
|
18
|
-
reasoningStreaming: false,
|
|
19
|
-
toolCallStreaming: true,
|
|
20
|
-
hiddenReasoningReplay: "partial",
|
|
21
|
-
replayFidelity: "medium",
|
|
22
|
-
tools: true,
|
|
23
|
-
usage: "full",
|
|
24
|
-
billing: "lookup",
|
|
25
|
-
providerMetadata: true
|
|
26
|
-
},
|
|
27
|
-
"chat.completions": {
|
|
28
|
-
nativeStreaming: true,
|
|
29
|
-
messageStreaming: true,
|
|
30
|
-
reasoningStreaming: false,
|
|
31
|
-
toolCallStreaming: false,
|
|
32
|
-
hiddenReasoningReplay: "none",
|
|
33
|
-
replayFidelity: "low",
|
|
34
|
-
tools: true,
|
|
35
|
-
usage: "full",
|
|
36
|
-
billing: "derived",
|
|
37
|
-
providerMetadata: false
|
|
38
|
-
},
|
|
39
|
-
ollama: {
|
|
40
|
-
nativeStreaming: true,
|
|
41
|
-
messageStreaming: true,
|
|
42
|
-
reasoningStreaming: false,
|
|
43
|
-
toolCallStreaming: false,
|
|
44
|
-
hiddenReasoningReplay: "none",
|
|
45
|
-
replayFidelity: "low",
|
|
46
|
-
tools: true,
|
|
47
|
-
usage: "partial",
|
|
48
|
-
billing: "none",
|
|
49
|
-
providerMetadata: false
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
//#endregion
|
|
53
1
|
//#region src/core/errors.ts
|
|
54
2
|
var AIError = class extends Error {
|
|
55
3
|
code;
|
|
@@ -491,17 +439,14 @@ function createEventFactory(state) {
|
|
|
491
439
|
}
|
|
492
440
|
//#endregion
|
|
493
441
|
//#region src/core/aggregator.ts
|
|
494
|
-
function
|
|
442
|
+
function createAggregatorState() {
|
|
495
443
|
return {
|
|
496
|
-
pendingMessages: /* @__PURE__ */ new Map(),
|
|
497
|
-
pendingReasonings: /* @__PURE__ */ new Map(),
|
|
498
|
-
pendingToolCalls: /* @__PURE__ */ new Map(),
|
|
499
|
-
outputOrder: [],
|
|
500
|
-
completedMessages: /* @__PURE__ */ new Map(),
|
|
501
|
-
completedReasonings: /* @__PURE__ */ new Map(),
|
|
502
|
-
completedToolCalls: /* @__PURE__ */ new Map(),
|
|
503
444
|
auxiliary: {},
|
|
504
|
-
warnings: []
|
|
445
|
+
warnings: [],
|
|
446
|
+
warningSet: /* @__PURE__ */ new Set(),
|
|
447
|
+
output: [],
|
|
448
|
+
textParts: [],
|
|
449
|
+
toolCalls: []
|
|
505
450
|
};
|
|
506
451
|
}
|
|
507
452
|
function handleResponseStarted(state, event) {
|
|
@@ -523,55 +468,16 @@ function handleResponseAuxiliary(state, event) {
|
|
|
523
468
|
};
|
|
524
469
|
if (event.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.auxiliary);
|
|
525
470
|
}
|
|
526
|
-
function handleMessageStarted(state, event) {
|
|
527
|
-
state.pendingMessages.set(event.item.id, {
|
|
528
|
-
role: event.item.role,
|
|
529
|
-
texts: []
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
function handleMessageDelta(state, event) {
|
|
533
|
-
const pending = state.pendingMessages.get(event.itemId);
|
|
534
|
-
if (pending) pending.texts.push(event.delta.text);
|
|
535
|
-
}
|
|
536
471
|
function handleMessageCompleted(state, event) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
state.completedMessages.set(itemId, item);
|
|
540
|
-
state.outputOrder.push(itemId);
|
|
541
|
-
state.pendingMessages.delete(itemId);
|
|
542
|
-
}
|
|
543
|
-
function handleReasoningStarted(state, event) {
|
|
544
|
-
state.pendingReasonings.set(event.item.id, {
|
|
545
|
-
visibility: event.item.visibility,
|
|
546
|
-
blocks: []
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
function handleReasoningDelta(state, event) {
|
|
550
|
-
const pending = state.pendingReasonings.get(event.itemId);
|
|
551
|
-
if (pending) pending.blocks.push(event.delta);
|
|
472
|
+
state.output.push(event.item);
|
|
473
|
+
pushMessageText(state, event.item);
|
|
552
474
|
}
|
|
553
475
|
function handleReasoningCompleted(state, event) {
|
|
554
|
-
|
|
555
|
-
const stableId = item.id ?? `reason-${state.outputOrder.length}-${Date.now()}`;
|
|
556
|
-
state.completedReasonings.set(stableId, item);
|
|
557
|
-
state.outputOrder.push(stableId);
|
|
558
|
-
state.pendingReasonings.delete(item.id ?? "");
|
|
559
|
-
}
|
|
560
|
-
function handleToolCallStarted(state, event) {
|
|
561
|
-
state.pendingToolCalls.set(event.item.id, {
|
|
562
|
-
name: event.item.name,
|
|
563
|
-
argsParts: []
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
function handleToolCallDelta(state, event) {
|
|
567
|
-
const pending = state.pendingToolCalls.get(event.itemId);
|
|
568
|
-
if (pending && event.delta.argumentsText) pending.argsParts.push(event.delta.argumentsText);
|
|
476
|
+
state.output.push(event.item);
|
|
569
477
|
}
|
|
570
478
|
function handleToolCallCompleted(state, event) {
|
|
571
|
-
|
|
572
|
-
state.
|
|
573
|
-
state.outputOrder.push(item.id);
|
|
574
|
-
state.pendingToolCalls.delete(item.id);
|
|
479
|
+
state.output.push(event.item);
|
|
480
|
+
state.toolCalls.push(event.item);
|
|
575
481
|
}
|
|
576
482
|
function handleResponseCompleted(state, event) {
|
|
577
483
|
state.replayFromAdapter = event.response.replay;
|
|
@@ -590,26 +496,6 @@ function handleResponseCompleted(state, event) {
|
|
|
590
496
|
if (event.response.warnings) pushWarnings(state, event.response.warnings);
|
|
591
497
|
}
|
|
592
498
|
function buildResponse(state) {
|
|
593
|
-
const output = [];
|
|
594
|
-
for (const id of state.outputOrder) {
|
|
595
|
-
const msg = state.completedMessages.get(id);
|
|
596
|
-
if (msg) {
|
|
597
|
-
output.push(msg);
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
const reason = state.completedReasonings.get(id);
|
|
601
|
-
if (reason) {
|
|
602
|
-
output.push(reason);
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
|
-
const tc = state.completedToolCalls.get(id);
|
|
606
|
-
if (tc) {
|
|
607
|
-
output.push(tc);
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
const text = output.filter((item) => item.type === "message").flatMap((m) => m.content).filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
612
|
-
const toolCalls = output.filter((item) => item.type === "tool_call");
|
|
613
499
|
const backendFromResponse = state.backendFromAdapter;
|
|
614
500
|
const backend = {
|
|
615
501
|
adapter: backendFromResponse?.adapter ?? state.backendInfo?.kind ?? "unknown",
|
|
@@ -621,10 +507,10 @@ function buildResponse(state) {
|
|
|
621
507
|
};
|
|
622
508
|
return {
|
|
623
509
|
id: state.responseIdFromAdapter ?? state.responseId,
|
|
624
|
-
output,
|
|
510
|
+
output: state.output,
|
|
625
511
|
replay: state.replayFromAdapter ?? [],
|
|
626
|
-
text,
|
|
627
|
-
toolCalls,
|
|
512
|
+
text: state.textParts.join(""),
|
|
513
|
+
toolCalls: state.toolCalls,
|
|
628
514
|
stopReason: state.stopReasonFromAdapter,
|
|
629
515
|
usage: state.usage,
|
|
630
516
|
billing: state.billing,
|
|
@@ -638,8 +524,13 @@ function buildResponse(state) {
|
|
|
638
524
|
* 适用于测试和离线处理场景。
|
|
639
525
|
*/
|
|
640
526
|
function aggregateEvents(events) {
|
|
641
|
-
const state =
|
|
642
|
-
for (const event of events)
|
|
527
|
+
const state = createAggregatorState();
|
|
528
|
+
for (const event of events) aggregateEvent(state, event);
|
|
529
|
+
return finalizeAggregation(state);
|
|
530
|
+
}
|
|
531
|
+
function aggregateEvent(state, event) {
|
|
532
|
+
state.lastEventType = event.type;
|
|
533
|
+
switch (event.type) {
|
|
643
534
|
case "response.started":
|
|
644
535
|
handleResponseStarted(state, event);
|
|
645
536
|
break;
|
|
@@ -650,29 +541,17 @@ function aggregateEvents(events) {
|
|
|
650
541
|
handleResponseAuxiliary(state, event);
|
|
651
542
|
break;
|
|
652
543
|
case "message.started":
|
|
653
|
-
handleMessageStarted(state, event);
|
|
654
|
-
break;
|
|
655
544
|
case "message.delta":
|
|
656
|
-
handleMessageDelta(state, event);
|
|
657
|
-
break;
|
|
658
|
-
case "message.completed":
|
|
659
|
-
handleMessageCompleted(state, event);
|
|
660
|
-
break;
|
|
661
545
|
case "reasoning.started":
|
|
662
|
-
handleReasoningStarted(state, event);
|
|
663
|
-
break;
|
|
664
546
|
case "reasoning.delta":
|
|
665
|
-
|
|
547
|
+
case "tool_call.started":
|
|
548
|
+
case "tool_call.delta": break;
|
|
549
|
+
case "message.completed":
|
|
550
|
+
handleMessageCompleted(state, event);
|
|
666
551
|
break;
|
|
667
552
|
case "reasoning.completed":
|
|
668
553
|
handleReasoningCompleted(state, event);
|
|
669
554
|
break;
|
|
670
|
-
case "tool_call.started":
|
|
671
|
-
handleToolCallStarted(state, event);
|
|
672
|
-
break;
|
|
673
|
-
case "tool_call.delta":
|
|
674
|
-
handleToolCallDelta(state, event);
|
|
675
|
-
break;
|
|
676
555
|
case "tool_call.completed":
|
|
677
556
|
handleToolCallCompleted(state, event);
|
|
678
557
|
break;
|
|
@@ -680,8 +559,9 @@ function aggregateEvents(events) {
|
|
|
680
559
|
handleResponseCompleted(state, event);
|
|
681
560
|
break;
|
|
682
561
|
}
|
|
683
|
-
|
|
684
|
-
|
|
562
|
+
}
|
|
563
|
+
function finalizeAggregation(state) {
|
|
564
|
+
if (state.lastEventType !== "response.completed") throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
|
|
685
565
|
return buildResponse(state);
|
|
686
566
|
}
|
|
687
567
|
function mergeAuxiliary$1(base, patch) {
|
|
@@ -690,20 +570,26 @@ function mergeAuxiliary$1(base, patch) {
|
|
|
690
570
|
...patch
|
|
691
571
|
};
|
|
692
572
|
if (base.providerMetadata || patch.providerMetadata) merged.providerMetadata = {
|
|
693
|
-
...base.providerMetadata
|
|
694
|
-
...patch.providerMetadata
|
|
573
|
+
...base.providerMetadata,
|
|
574
|
+
...patch.providerMetadata
|
|
695
575
|
};
|
|
696
576
|
return merged;
|
|
697
577
|
}
|
|
698
578
|
function pushWarnings(state, warnings) {
|
|
699
|
-
for (const warning of warnings) if (!state.
|
|
579
|
+
for (const warning of warnings) if (!state.warningSet.has(warning)) {
|
|
580
|
+
state.warningSet.add(warning);
|
|
581
|
+
state.warnings.push(warning);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function pushMessageText(state, item) {
|
|
585
|
+
for (const block of item.content) if (block.type === "text") state.textParts.push(block.text);
|
|
700
586
|
}
|
|
701
587
|
//#endregion
|
|
702
588
|
//#region src/core/collect-stream.ts
|
|
703
589
|
async function collectStream(stream) {
|
|
704
|
-
const
|
|
705
|
-
for await (const event of stream)
|
|
706
|
-
return
|
|
590
|
+
const state = createAggregatorState();
|
|
591
|
+
for await (const event of stream) aggregateEvent(state, event);
|
|
592
|
+
return finalizeAggregation(state);
|
|
707
593
|
}
|
|
708
594
|
//#endregion
|
|
709
595
|
//#region src/helpers/mapping.ts
|
|
@@ -951,12 +837,10 @@ function withTimeout(promise, ms) {
|
|
|
951
837
|
//#region src/helpers/adapter-auxiliary.ts
|
|
952
838
|
var AdapterAuxiliaryState = class {
|
|
953
839
|
request;
|
|
954
|
-
capabilities;
|
|
955
840
|
collector = new AuxiliaryCollector();
|
|
956
841
|
metadataSources = /* @__PURE__ */ new Set();
|
|
957
|
-
constructor(request
|
|
842
|
+
constructor(request) {
|
|
958
843
|
this.request = request;
|
|
959
|
-
this.capabilities = capabilities;
|
|
960
844
|
}
|
|
961
845
|
recordUsage(usage, source, raw) {
|
|
962
846
|
if (this.request.include?.usage === "off" || isEmptyRecord(usage)) return;
|
|
@@ -980,8 +864,7 @@ var AdapterAuxiliaryState = class {
|
|
|
980
864
|
request: this.request,
|
|
981
865
|
usage: snapshot.usage,
|
|
982
866
|
billing: snapshot.billing,
|
|
983
|
-
auxiliary: snapshot.auxiliary
|
|
984
|
-
capabilities: this.capabilities
|
|
867
|
+
auxiliary: snapshot.auxiliary
|
|
985
868
|
});
|
|
986
869
|
if (derived && !isEmptyRecord(derived)) this.collector.recordBilling({
|
|
987
870
|
...derived,
|
|
@@ -1046,7 +929,7 @@ var AdapterBase = class {
|
|
|
1046
929
|
responseId: request.requestId,
|
|
1047
930
|
backend: {
|
|
1048
931
|
kind: this.kind,
|
|
1049
|
-
isSynthetic: !this.
|
|
932
|
+
isSynthetic: !this.nativeStreaming
|
|
1050
933
|
}
|
|
1051
934
|
});
|
|
1052
935
|
yield factory.responseStarted(request.model);
|
|
@@ -1085,7 +968,7 @@ var AdapterBase = class {
|
|
|
1085
968
|
requestId: request.requestId,
|
|
1086
969
|
rawResponseId: result.rawResponseId,
|
|
1087
970
|
adapter: this.kind,
|
|
1088
|
-
isSyntheticStream: !this.
|
|
971
|
+
isSyntheticStream: !this.nativeStreaming,
|
|
1089
972
|
metadataSources: result.metadataSources,
|
|
1090
973
|
warnings
|
|
1091
974
|
}
|
|
@@ -1096,18 +979,18 @@ var AdapterBase = class {
|
|
|
1096
979
|
return extractText(output);
|
|
1097
980
|
}
|
|
1098
981
|
createAuxiliaryState(request) {
|
|
1099
|
-
return new AdapterAuxiliaryState(request
|
|
982
|
+
return new AdapterAuxiliaryState(request);
|
|
1100
983
|
}
|
|
1101
984
|
};
|
|
1102
985
|
function mergeAuxiliary(base, patch) {
|
|
1103
986
|
if (!base && !patch) return void 0;
|
|
1104
987
|
const merged = {
|
|
1105
|
-
...base
|
|
1106
|
-
...patch
|
|
988
|
+
...base,
|
|
989
|
+
...patch
|
|
1107
990
|
};
|
|
1108
991
|
if (base?.providerMetadata || patch?.providerMetadata) merged.providerMetadata = {
|
|
1109
|
-
...base?.providerMetadata
|
|
1110
|
-
...patch?.providerMetadata
|
|
992
|
+
...base?.providerMetadata,
|
|
993
|
+
...patch?.providerMetadata
|
|
1111
994
|
};
|
|
1112
995
|
return merged;
|
|
1113
996
|
}
|
|
@@ -1189,6 +1072,7 @@ function parseSSEEvents(chunk) {
|
|
|
1189
1072
|
function ensureResponsesTextBlocks(blocks, field) {
|
|
1190
1073
|
for (let i = 0; i < blocks.length; i++) {
|
|
1191
1074
|
const block = blocks[i];
|
|
1075
|
+
if (!block) continue;
|
|
1192
1076
|
if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`responses does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
1193
1077
|
}
|
|
1194
1078
|
return blocks;
|
|
@@ -1236,7 +1120,7 @@ function canonicalToResponsesBlock(b) {
|
|
|
1236
1120
|
}
|
|
1237
1121
|
var ResponsesAdapter = class extends AdapterBase {
|
|
1238
1122
|
kind = "responses";
|
|
1239
|
-
|
|
1123
|
+
nativeStreaming = true;
|
|
1240
1124
|
apiKey;
|
|
1241
1125
|
baseUrl;
|
|
1242
1126
|
fetchFn;
|
|
@@ -1468,6 +1352,7 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1468
1352
|
function ensureMessagesTextBlocks(blocks, field) {
|
|
1469
1353
|
for (let i = 0; i < blocks.length; i++) {
|
|
1470
1354
|
const block = blocks[i];
|
|
1355
|
+
if (!block) continue;
|
|
1471
1356
|
if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`messages does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
1472
1357
|
}
|
|
1473
1358
|
return blocks;
|
|
@@ -1539,7 +1424,7 @@ function buildStreamMetadata(options) {
|
|
|
1539
1424
|
}
|
|
1540
1425
|
var MessagesAdapter = class extends AdapterBase {
|
|
1541
1426
|
kind = "messages";
|
|
1542
|
-
|
|
1427
|
+
nativeStreaming = true;
|
|
1543
1428
|
apiKey;
|
|
1544
1429
|
apiVersion;
|
|
1545
1430
|
baseUrl;
|
|
@@ -1932,6 +1817,7 @@ function parseChatSSE(buffer) {
|
|
|
1932
1817
|
function ensureTextCompatibleBlocks(blocks, field) {
|
|
1933
1818
|
for (let i = 0; i < blocks.length; i++) {
|
|
1934
1819
|
const block = blocks[i];
|
|
1820
|
+
if (!block) continue;
|
|
1935
1821
|
if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`chat-completions does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
1936
1822
|
}
|
|
1937
1823
|
return blocks;
|
|
@@ -1992,26 +1878,10 @@ function buildAssistantReplayMessage(params) {
|
|
|
1992
1878
|
}
|
|
1993
1879
|
var ChatCompletionsAdapter = class extends AdapterBase {
|
|
1994
1880
|
kind = "chat-completions";
|
|
1995
|
-
|
|
1996
|
-
nativeStreaming: true,
|
|
1997
|
-
messageStreaming: true,
|
|
1998
|
-
reasoningStreaming: false,
|
|
1999
|
-
toolCallStreaming: false,
|
|
2000
|
-
hiddenReasoningReplay: "none",
|
|
2001
|
-
replayFidelity: "low",
|
|
2002
|
-
tools: true,
|
|
2003
|
-
usage: "full",
|
|
2004
|
-
billing: "derived",
|
|
2005
|
-
providerMetadata: false
|
|
2006
|
-
};
|
|
1881
|
+
nativeStreaming = true;
|
|
2007
1882
|
apiKey;
|
|
2008
1883
|
baseUrl;
|
|
2009
1884
|
fetchFn;
|
|
2010
|
-
markReasoningCompatibility() {
|
|
2011
|
-
this.capabilities.reasoningStreaming = true;
|
|
2012
|
-
this.capabilities.hiddenReasoningReplay = "partial";
|
|
2013
|
-
this.capabilities.replayFidelity = "medium";
|
|
2014
|
-
}
|
|
2015
1885
|
constructor(options) {
|
|
2016
1886
|
super();
|
|
2017
1887
|
this.apiKey = options.apiKey;
|
|
@@ -2136,7 +2006,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2136
2006
|
let currentReasoningId = "";
|
|
2137
2007
|
let hasMessageStarted = false;
|
|
2138
2008
|
let hasReasoningStarted = false;
|
|
2139
|
-
let hasStreamedReasoning = false;
|
|
2140
2009
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2141
2010
|
const reasoningByField = /* @__PURE__ */ new Map();
|
|
2142
2011
|
const finalizePendingTurn = () => {
|
|
@@ -2211,7 +2080,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2211
2080
|
if (!hasReasoningStarted) {
|
|
2212
2081
|
currentReasoningId = `reason-${chunk.id}`;
|
|
2213
2082
|
hasReasoningStarted = true;
|
|
2214
|
-
hasStreamedReasoning = true;
|
|
2215
2083
|
accumulatedReasoning = "";
|
|
2216
2084
|
yield factory.reasoningStarted(currentReasoningId, "full");
|
|
2217
2085
|
}
|
|
@@ -2269,7 +2137,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2269
2137
|
if (finishReason && finishReason !== null) {
|
|
2270
2138
|
const { events, assistantReplayMessage } = finalizePendingTurn();
|
|
2271
2139
|
for (const event of events) yield event;
|
|
2272
|
-
if (hasStreamedReasoning) this.markReasoningCompatibility();
|
|
2273
2140
|
const stopReason = mapStopReason(finishReason);
|
|
2274
2141
|
const replay = [...replayFromOutput(output)];
|
|
2275
2142
|
if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
|
|
@@ -2299,7 +2166,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2299
2166
|
if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
|
|
2300
2167
|
if (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0) {
|
|
2301
2168
|
yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
|
|
2302
|
-
if (hasStreamedReasoning) this.markReasoningCompatibility();
|
|
2303
2169
|
const { events, assistantReplayMessage } = finalizePendingTurn();
|
|
2304
2170
|
for (const event of events) yield event;
|
|
2305
2171
|
const replay = [...replayFromOutput(output)];
|
|
@@ -2343,6 +2209,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2343
2209
|
function ensureOllamaTextBlocks(blocks, field) {
|
|
2344
2210
|
for (let i = 0; i < blocks.length; i++) {
|
|
2345
2211
|
const block = blocks[i];
|
|
2212
|
+
if (!block) continue;
|
|
2346
2213
|
if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`ollama does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
2347
2214
|
}
|
|
2348
2215
|
return blocks;
|
|
@@ -2403,18 +2270,7 @@ function isOllamaToolCalls(value) {
|
|
|
2403
2270
|
}
|
|
2404
2271
|
var OllamaAdapter = class extends AdapterBase {
|
|
2405
2272
|
kind = "ollama";
|
|
2406
|
-
|
|
2407
|
-
nativeStreaming: true,
|
|
2408
|
-
messageStreaming: true,
|
|
2409
|
-
reasoningStreaming: false,
|
|
2410
|
-
toolCallStreaming: false,
|
|
2411
|
-
hiddenReasoningReplay: "none",
|
|
2412
|
-
replayFidelity: "low",
|
|
2413
|
-
tools: true,
|
|
2414
|
-
usage: "partial",
|
|
2415
|
-
billing: "none",
|
|
2416
|
-
providerMetadata: false
|
|
2417
|
-
};
|
|
2273
|
+
nativeStreaming = true;
|
|
2418
2274
|
baseUrl;
|
|
2419
2275
|
apiKey;
|
|
2420
2276
|
fetchFn;
|
|
@@ -2651,6 +2507,390 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2651
2507
|
}
|
|
2652
2508
|
};
|
|
2653
2509
|
//#endregion
|
|
2510
|
+
//#region src/adapters/mock.ts
|
|
2511
|
+
/**
|
|
2512
|
+
* Mock Adapter
|
|
2513
|
+
*
|
|
2514
|
+
* 面向测试的脚本化 adapter:
|
|
2515
|
+
* - 按 turn 顺序消费请求,验证调用方是否正确续接 replay / tool_result
|
|
2516
|
+
* - 发出可控的 message / reasoning / tool_call 流
|
|
2517
|
+
* - 注入 warning / auxiliary / content_filter / 中断 / provider error
|
|
2518
|
+
*
|
|
2519
|
+
* 这不是通用“假模型”,而是测试工具调用编排与错误路径的测试夹具。
|
|
2520
|
+
*/
|
|
2521
|
+
var MockAdapter = class extends AdapterBase {
|
|
2522
|
+
kind = "mock";
|
|
2523
|
+
nativeStreaming = false;
|
|
2524
|
+
turns;
|
|
2525
|
+
onExhausted;
|
|
2526
|
+
providerMetadata;
|
|
2527
|
+
defaultStream;
|
|
2528
|
+
cursor = 0;
|
|
2529
|
+
previousReplay = [];
|
|
2530
|
+
pendingToolCalls = [];
|
|
2531
|
+
history = [];
|
|
2532
|
+
activeStream = false;
|
|
2533
|
+
constructor(options) {
|
|
2534
|
+
super();
|
|
2535
|
+
this.turns = options.turns;
|
|
2536
|
+
this.onExhausted = options.onExhausted ?? "throw";
|
|
2537
|
+
this.providerMetadata = options.providerMetadata;
|
|
2538
|
+
this.defaultStream = resolveMockTextStreamOptions(options.stream, "adapter stream");
|
|
2539
|
+
}
|
|
2540
|
+
async buildRequest(request) {
|
|
2541
|
+
const turnIndex = this.cursor;
|
|
2542
|
+
const turn = this.resolveTurn(turnIndex);
|
|
2543
|
+
const turnName = turn.name;
|
|
2544
|
+
const context = this.buildTurnContext(turnIndex);
|
|
2545
|
+
if (turn.expect) if (typeof turn.expect === "function") await turn.expect(request, context);
|
|
2546
|
+
else assertRequestMatchesExpectation(request, turn.expect, context);
|
|
2547
|
+
const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
|
|
2548
|
+
this.cursor += 1;
|
|
2549
|
+
return {
|
|
2550
|
+
request,
|
|
2551
|
+
turn,
|
|
2552
|
+
turnIndex,
|
|
2553
|
+
turnName,
|
|
2554
|
+
remainingPendingToolCalls
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
async *runStream(providerRequest, factory, request) {
|
|
2558
|
+
if (this.activeStream) throw new AIRequestError("MockAdapter does not support concurrent streams", "MOCK_CONCURRENT_STREAM");
|
|
2559
|
+
this.activeStream = true;
|
|
2560
|
+
try {
|
|
2561
|
+
const mockRequest = providerRequest;
|
|
2562
|
+
const output = [];
|
|
2563
|
+
for (const [stepIndex, step] of mockRequest.turn.steps.entries()) switch (step.type) {
|
|
2564
|
+
case "warning":
|
|
2565
|
+
yield factory.responseWarning(step.message, step.code);
|
|
2566
|
+
break;
|
|
2567
|
+
case "auxiliary":
|
|
2568
|
+
yield factory.responseAuxiliary({
|
|
2569
|
+
usage: step.usage,
|
|
2570
|
+
billing: step.billing,
|
|
2571
|
+
auxiliary: step.auxiliary
|
|
2572
|
+
});
|
|
2573
|
+
break;
|
|
2574
|
+
case "message": {
|
|
2575
|
+
const item = createMessageFromStep(step, request, mockRequest.turnIndex, stepIndex);
|
|
2576
|
+
yield* emitMessage(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "message"));
|
|
2577
|
+
output.push(item);
|
|
2578
|
+
break;
|
|
2579
|
+
}
|
|
2580
|
+
case "reasoning": {
|
|
2581
|
+
const item = createReasoningFromStep(step, request, mockRequest.turnIndex, stepIndex);
|
|
2582
|
+
yield* emitReasoning(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "reasoning"));
|
|
2583
|
+
output.push(item);
|
|
2584
|
+
break;
|
|
2585
|
+
}
|
|
2586
|
+
case "tool_call": {
|
|
2587
|
+
const item = createToolCallFromStep(step);
|
|
2588
|
+
yield* emitToolCall(factory, item, step.streamArguments ?? true, resolveStepStreamOptions(this.defaultStream, step.stream, "tool_call"));
|
|
2589
|
+
output.push(item);
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
case "output": {
|
|
2593
|
+
assertSupportedOutputItem(step.item);
|
|
2594
|
+
const item = attachSyntheticId(step.item, request, mockRequest.turnIndex, stepIndex);
|
|
2595
|
+
yield* emitOutputItem(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "output"));
|
|
2596
|
+
output.push(item);
|
|
2597
|
+
break;
|
|
2598
|
+
}
|
|
2599
|
+
case "complete": {
|
|
2600
|
+
const response = this.finalizeTurn(request, factory, mockRequest, output, step);
|
|
2601
|
+
yield factory.responseCompleted(response);
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
case "error": {
|
|
2605
|
+
yield factory.responseWarning(step.message, step.code);
|
|
2606
|
+
const response = this.finalizeTurn(request, factory, mockRequest, output, {
|
|
2607
|
+
type: "complete",
|
|
2608
|
+
stopReason: step.stopReason ?? "error",
|
|
2609
|
+
providerMetadata: step.providerMetadata
|
|
2610
|
+
});
|
|
2611
|
+
yield factory.responseCompleted(response);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
case "interrupt":
|
|
2615
|
+
this.pendingToolCalls = mockRequest.remainingPendingToolCalls;
|
|
2616
|
+
return;
|
|
2617
|
+
case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
|
|
2618
|
+
}
|
|
2619
|
+
const response = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" });
|
|
2620
|
+
yield factory.responseCompleted(response);
|
|
2621
|
+
} finally {
|
|
2622
|
+
this.activeStream = false;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
finalizeTurn(request, factory, mockRequest, output, completion) {
|
|
2626
|
+
const replay = completion.replay ?? replayFromOutput(output);
|
|
2627
|
+
const toolCalls = output.filter((item) => item.type === "tool_call");
|
|
2628
|
+
this.previousReplay = replay;
|
|
2629
|
+
this.pendingToolCalls = [...mockRequest.remainingPendingToolCalls, ...toolCalls];
|
|
2630
|
+
this.history.push({
|
|
2631
|
+
turnIndex: mockRequest.turnIndex,
|
|
2632
|
+
turnName: mockRequest.turnName,
|
|
2633
|
+
requestId: request.requestId,
|
|
2634
|
+
replay,
|
|
2635
|
+
toolCalls
|
|
2636
|
+
});
|
|
2637
|
+
return this.buildResponse(request, {
|
|
2638
|
+
output,
|
|
2639
|
+
replay,
|
|
2640
|
+
stopReason: completion.stopReason ?? resolveStopReason(output),
|
|
2641
|
+
usage: completion.usage,
|
|
2642
|
+
billing: completion.billing,
|
|
2643
|
+
auxiliary: completion.auxiliary,
|
|
2644
|
+
providerMetadata: {
|
|
2645
|
+
turnIndex: mockRequest.turnIndex,
|
|
2646
|
+
turnName: mockRequest.turnName,
|
|
2647
|
+
scriptedSteps: mockRequest.turn.steps.length,
|
|
2648
|
+
pendingToolCallIds: this.pendingToolCalls.map((item) => item.id),
|
|
2649
|
+
historyLength: this.history.length,
|
|
2650
|
+
...this.providerMetadata,
|
|
2651
|
+
...completion.providerMetadata
|
|
2652
|
+
},
|
|
2653
|
+
warnings: completion.warnings,
|
|
2654
|
+
metadataSources: ["mock"],
|
|
2655
|
+
rawResponseId: completion.rawResponseId
|
|
2656
|
+
}, factory);
|
|
2657
|
+
}
|
|
2658
|
+
resolveTurn(turnIndex) {
|
|
2659
|
+
const turn = this.turns[turnIndex];
|
|
2660
|
+
if (turn !== void 0) return turn;
|
|
2661
|
+
const lastTurn = this.turns.at(-1);
|
|
2662
|
+
if (this.onExhausted === "repeat-last" && lastTurn !== void 0) return lastTurn;
|
|
2663
|
+
if (this.onExhausted === "complete-empty") return {
|
|
2664
|
+
name: "exhausted",
|
|
2665
|
+
steps: []
|
|
2666
|
+
};
|
|
2667
|
+
throw new AIRequestError(`MockAdapter turn ${turnIndex + 1} requested, but only ${this.turns.length} turn(s) were scripted`, "MOCK_TURN_EXHAUSTED");
|
|
2668
|
+
}
|
|
2669
|
+
buildTurnContext(turnIndex) {
|
|
2670
|
+
return {
|
|
2671
|
+
turnIndex,
|
|
2672
|
+
previousReplay: this.previousReplay.map(cloneItem),
|
|
2673
|
+
pendingToolCalls: this.pendingToolCalls.map(cloneItem),
|
|
2674
|
+
history: this.history.map((record) => ({
|
|
2675
|
+
...record,
|
|
2676
|
+
replay: record.replay.map(cloneItem),
|
|
2677
|
+
toolCalls: record.toolCalls.map(cloneItem)
|
|
2678
|
+
}))
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
function createMessageFromStep(step, request, turnIndex, stepIndex) {
|
|
2683
|
+
return {
|
|
2684
|
+
...messageItem(normalizeBlocks(step.content), { id: step.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}` }),
|
|
2685
|
+
role: "assistant"
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
function createReasoningFromStep(step, request, turnIndex, stepIndex) {
|
|
2689
|
+
return reasoningItem(normalizeBlocks(step.content), step.visibility ?? "full", step.id ?? `mock-reason-${request.requestId}-${turnIndex}-${stepIndex}`);
|
|
2690
|
+
}
|
|
2691
|
+
function createToolCallFromStep(step) {
|
|
2692
|
+
return {
|
|
2693
|
+
type: "tool_call",
|
|
2694
|
+
id: step.id,
|
|
2695
|
+
name: step.name,
|
|
2696
|
+
argumentsText: step.argumentsText,
|
|
2697
|
+
argumentsJson: step.argumentsJson
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
function normalizeBlocks(content) {
|
|
2701
|
+
return typeof content === "string" ? [textBlock(content)] : content;
|
|
2702
|
+
}
|
|
2703
|
+
function assertSupportedOutputItem(item) {
|
|
2704
|
+
if (item.type === "opaque") throw new AIRequestError("MockAdapter does not stream opaque output items; use complete.replay if needed", "MOCK_OPAQUE_OUTPUT");
|
|
2705
|
+
}
|
|
2706
|
+
function attachSyntheticId(item, request, turnIndex, stepIndex) {
|
|
2707
|
+
if (item.type === "message") return {
|
|
2708
|
+
...item,
|
|
2709
|
+
id: item.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}`,
|
|
2710
|
+
role: "assistant"
|
|
2711
|
+
};
|
|
2712
|
+
if (item.type === "reasoning") return {
|
|
2713
|
+
...item,
|
|
2714
|
+
id: item.id ?? `mock-reason-${request.requestId}-${turnIndex}-${stepIndex}`
|
|
2715
|
+
};
|
|
2716
|
+
return item;
|
|
2717
|
+
}
|
|
2718
|
+
async function* emitOutputItem(factory, item, stream) {
|
|
2719
|
+
if (item.type === "message") {
|
|
2720
|
+
yield* emitMessage(factory, item, stream);
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
if (item.type === "reasoning") {
|
|
2724
|
+
yield* emitReasoning(factory, item, stream);
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
yield* emitToolCall(factory, item, true, stream);
|
|
2728
|
+
}
|
|
2729
|
+
async function* emitMessage(factory, item, stream) {
|
|
2730
|
+
if (!item.id) throw new AIRequestError("Mock message output requires an id after normalization", "MOCK_MESSAGE_ID_MISSING");
|
|
2731
|
+
yield factory.messageStarted(item.id);
|
|
2732
|
+
let chunkIndex = 0;
|
|
2733
|
+
for (const block of item.content) if (block.type === "text") for (const chunk of chunkText(block.text, stream)) {
|
|
2734
|
+
await delayForChunk(stream, chunkIndex, chunk.length);
|
|
2735
|
+
yield factory.messageDelta(item.id, chunk);
|
|
2736
|
+
chunkIndex += 1;
|
|
2737
|
+
}
|
|
2738
|
+
yield factory.messageCompleted(item);
|
|
2739
|
+
}
|
|
2740
|
+
async function* emitReasoning(factory, item, stream) {
|
|
2741
|
+
if (!item.id) throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
|
|
2742
|
+
yield factory.reasoningStarted(item.id, item.visibility);
|
|
2743
|
+
let chunkIndex = 0;
|
|
2744
|
+
for (const block of item.content) {
|
|
2745
|
+
if (block.type !== "text") {
|
|
2746
|
+
yield factory.reasoningDelta(item.id, block);
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
for (const chunk of chunkText(block.text, stream)) {
|
|
2750
|
+
await delayForChunk(stream, chunkIndex, chunk.length);
|
|
2751
|
+
yield factory.reasoningDelta(item.id, textBlock(chunk));
|
|
2752
|
+
chunkIndex += 1;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
yield factory.reasoningCompleted(item);
|
|
2756
|
+
}
|
|
2757
|
+
async function* emitToolCall(factory, item, streamArguments, stream) {
|
|
2758
|
+
yield factory.toolCallStarted(item.id, item.name);
|
|
2759
|
+
if (streamArguments && item.argumentsText) {
|
|
2760
|
+
let chunkIndex = 0;
|
|
2761
|
+
for (const chunk of chunkText(item.argumentsText, stream)) {
|
|
2762
|
+
await delayForChunk(stream, chunkIndex, chunk.length);
|
|
2763
|
+
yield factory.toolCallDelta(item.id, { argumentsText: chunk });
|
|
2764
|
+
chunkIndex += 1;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
yield factory.toolCallCompleted(item);
|
|
2768
|
+
}
|
|
2769
|
+
function resolveStepStreamOptions(defaults, override, label) {
|
|
2770
|
+
if (override === false) return;
|
|
2771
|
+
return resolveMockTextStreamOptions(override, `${label} stream`, defaults);
|
|
2772
|
+
}
|
|
2773
|
+
function resolveMockTextStreamOptions(options, label, defaults) {
|
|
2774
|
+
if (options === void 0) return defaults;
|
|
2775
|
+
const chunkSize = options.chunkSize ?? defaults?.chunkSize ?? 1;
|
|
2776
|
+
const initialDelayMs = options.initialDelayMs ?? defaults?.initialDelayMs ?? 0;
|
|
2777
|
+
const charsPerSecond = options.charsPerSecond ?? defaults?.charsPerSecond;
|
|
2778
|
+
if (!Number.isInteger(chunkSize) || chunkSize < 1) throw new AIRequestError(`${label}: chunkSize must be a positive integer`, "MOCK_STREAM_CONFIG_INVALID");
|
|
2779
|
+
if (!Number.isFinite(initialDelayMs) || initialDelayMs < 0) throw new AIRequestError(`${label}: initialDelayMs must be a non-negative number`, "MOCK_STREAM_CONFIG_INVALID");
|
|
2780
|
+
if (charsPerSecond !== void 0 && (!Number.isFinite(charsPerSecond) || charsPerSecond <= 0)) throw new AIRequestError(`${label}: charsPerSecond must be a positive number`, "MOCK_STREAM_CONFIG_INVALID");
|
|
2781
|
+
return {
|
|
2782
|
+
chunkSize,
|
|
2783
|
+
initialDelayMs,
|
|
2784
|
+
charsPerSecond
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
function chunkText(text, stream) {
|
|
2788
|
+
if (!text) return [];
|
|
2789
|
+
if (!stream) return [text];
|
|
2790
|
+
const chars = Array.from(text);
|
|
2791
|
+
const chunks = [];
|
|
2792
|
+
for (let index = 0; index < chars.length; index += stream.chunkSize) chunks.push(chars.slice(index, index + stream.chunkSize).join(""));
|
|
2793
|
+
return chunks;
|
|
2794
|
+
}
|
|
2795
|
+
async function delayForChunk(stream, chunkIndex, chunkLength) {
|
|
2796
|
+
if (!stream) return;
|
|
2797
|
+
if (chunkIndex === 0 && stream.initialDelayMs > 0) {
|
|
2798
|
+
await sleep(stream.initialDelayMs);
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
if (chunkIndex > 0 && stream.charsPerSecond !== void 0) await sleep(chunkLength / stream.charsPerSecond * 1e3);
|
|
2802
|
+
}
|
|
2803
|
+
async function sleep(ms) {
|
|
2804
|
+
if (ms <= 0) return;
|
|
2805
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
2806
|
+
}
|
|
2807
|
+
function resolveStopReason(output) {
|
|
2808
|
+
return output.some((item) => item.type === "tool_call") ? "tool_call" : "end_turn";
|
|
2809
|
+
}
|
|
2810
|
+
function consumePendingToolCalls(pending, input) {
|
|
2811
|
+
const fulfilledIds = new Set(input.filter((item) => item.type === "tool_result").map((item) => item.callId));
|
|
2812
|
+
return pending.filter((item) => !fulfilledIds.has(item.id)).map(cloneItem);
|
|
2813
|
+
}
|
|
2814
|
+
function assertRequestMatchesExpectation(request, expectation, context) {
|
|
2815
|
+
const prefix = `MockAdapter turn ${context.turnIndex + 1} expectation failed`;
|
|
2816
|
+
if (expectation.minItems !== void 0 && request.input.length < expectation.minItems) throw new AIRequestError(`${prefix}: expected at least ${expectation.minItems} input item(s)`, "MOCK_EXPECTATION_FAILED");
|
|
2817
|
+
if (expectation.maxItems !== void 0 && request.input.length > expectation.maxItems) throw new AIRequestError(`${prefix}: expected at most ${expectation.maxItems} input item(s)`, "MOCK_EXPECTATION_FAILED");
|
|
2818
|
+
if (expectation.tools === "present" && (!request.tools || request.tools.length === 0)) throw new AIRequestError(`${prefix}: expected tools to be present`, "MOCK_EXPECTATION_FAILED");
|
|
2819
|
+
if (expectation.tools === "absent" && request.tools && request.tools.length > 0) throw new AIRequestError(`${prefix}: expected tools to be absent`, "MOCK_EXPECTATION_FAILED");
|
|
2820
|
+
if (expectation.toolChoice === "present" && request.toolChoice === void 0) throw new AIRequestError(`${prefix}: expected toolChoice to be present`, "MOCK_EXPECTATION_FAILED");
|
|
2821
|
+
if (expectation.toolChoice === "absent" && request.toolChoice !== void 0) throw new AIRequestError(`${prefix}: expected toolChoice to be absent`, "MOCK_EXPECTATION_FAILED");
|
|
2822
|
+
if (expectation.requireReplayFromPreviousTurn && context.previousReplay.length > 0) assertReplayIncluded(request.input, context.previousReplay, prefix);
|
|
2823
|
+
if (expectation.requireToolResultsForPendingCalls && context.pendingToolCalls.length > 0) {
|
|
2824
|
+
const toolResultIds = new Set(request.input.filter((item) => item.type === "tool_result").map((item) => item.callId));
|
|
2825
|
+
for (const call of context.pendingToolCalls) if (!toolResultIds.has(call.id)) throw new AIRequestError(`${prefix}: expected tool_result for pending tool call "${call.id}"`, "MOCK_EXPECTATION_FAILED");
|
|
2826
|
+
}
|
|
2827
|
+
if (expectation.items && expectation.items.length > 0) if (expectation.ordered) assertOrderedItems(request.input, expectation.items, prefix);
|
|
2828
|
+
else assertUnorderedItems(request.input, expectation.items, prefix);
|
|
2829
|
+
}
|
|
2830
|
+
function assertReplayIncluded(input, replay, prefix) {
|
|
2831
|
+
const fingerprints = input.map(fingerprintItem);
|
|
2832
|
+
let cursor = 0;
|
|
2833
|
+
for (const replayItem of replay) {
|
|
2834
|
+
const target = fingerprintItem(replayItem);
|
|
2835
|
+
const foundIndex = fingerprints.indexOf(target, cursor);
|
|
2836
|
+
if (foundIndex === -1) throw new AIRequestError(`${prefix}: previous replay item was not carried into the next request`, "MOCK_EXPECTATION_FAILED");
|
|
2837
|
+
cursor = foundIndex + 1;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
function assertOrderedItems(input, expectations, prefix) {
|
|
2841
|
+
let cursor = 0;
|
|
2842
|
+
for (const expected of expectations) {
|
|
2843
|
+
let matched = false;
|
|
2844
|
+
while (cursor < input.length) {
|
|
2845
|
+
const item = input[cursor];
|
|
2846
|
+
if (item !== void 0 && matchesItemExpectation(item, expected)) {
|
|
2847
|
+
matched = true;
|
|
2848
|
+
cursor += 1;
|
|
2849
|
+
break;
|
|
2850
|
+
}
|
|
2851
|
+
cursor += 1;
|
|
2852
|
+
}
|
|
2853
|
+
if (!matched) throw new AIRequestError(`${prefix}: missing ordered input item ${describeExpectation(expected)}`, "MOCK_EXPECTATION_FAILED");
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
function assertUnorderedItems(input, expectations, prefix) {
|
|
2857
|
+
for (const expected of expectations) if (!input.some((item) => matchesItemExpectation(item, expected))) throw new AIRequestError(`${prefix}: missing input item ${describeExpectation(expected)}`, "MOCK_EXPECTATION_FAILED");
|
|
2858
|
+
}
|
|
2859
|
+
function matchesItemExpectation(item, expected) {
|
|
2860
|
+
if (item.type !== expected.type) return false;
|
|
2861
|
+
if (expected.id !== void 0 && "id" in item && item.id !== expected.id) return false;
|
|
2862
|
+
switch (item.type) {
|
|
2863
|
+
case "message": return (expected.role === void 0 || item.role === expected.role) && matchesText(item.content, expected.textIncludes);
|
|
2864
|
+
case "reasoning": return (expected.visibility === void 0 || item.visibility === expected.visibility) && matchesText(item.content, expected.textIncludes);
|
|
2865
|
+
case "tool_call": return (expected.name === void 0 || item.name === expected.name) && (expected.textIncludes === void 0 || item.argumentsText.includes(expected.textIncludes));
|
|
2866
|
+
case "tool_result": return (expected.toolName === void 0 || item.toolName === expected.toolName) && (expected.callId === void 0 || item.callId === expected.callId) && (expected.outcome === void 0 || item.outcome === expected.outcome) && matchesText(item.content, expected.textIncludes);
|
|
2867
|
+
case "opaque": return (expected.source === void 0 || item.source === expected.source) && (expected.purpose === void 0 || item.purpose === expected.purpose);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
function matchesText(blocks, textIncludes) {
|
|
2871
|
+
if (textIncludes === void 0) return true;
|
|
2872
|
+
return blocks.some((block) => {
|
|
2873
|
+
if (block.type === "text") return block.text.includes(textIncludes);
|
|
2874
|
+
if (block.type === "json") return JSON.stringify(block.json).includes(textIncludes);
|
|
2875
|
+
return false;
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
function fingerprintItem(item) {
|
|
2879
|
+
return JSON.stringify(item);
|
|
2880
|
+
}
|
|
2881
|
+
function describeExpectation(expectation) {
|
|
2882
|
+
const parts = [`type=${expectation.type}`];
|
|
2883
|
+
if (expectation.role) parts.push(`role=${expectation.role}`);
|
|
2884
|
+
if (expectation.name) parts.push(`name=${expectation.name}`);
|
|
2885
|
+
if (expectation.toolName) parts.push(`toolName=${expectation.toolName}`);
|
|
2886
|
+
if (expectation.callId) parts.push(`callId=${expectation.callId}`);
|
|
2887
|
+
if (expectation.textIncludes) parts.push(`textIncludes=${JSON.stringify(expectation.textIncludes)}`);
|
|
2888
|
+
return `{ ${parts.join(", ")} }`;
|
|
2889
|
+
}
|
|
2890
|
+
function cloneItem(item) {
|
|
2891
|
+
return structuredClone(item);
|
|
2892
|
+
}
|
|
2893
|
+
//#endregion
|
|
2654
2894
|
//#region src/helpers/synthetic-stream.ts
|
|
2655
2895
|
/**
|
|
2656
2896
|
* 模拟流式 (Synthetic Streaming)
|
|
@@ -2753,6 +2993,6 @@ function* emitToolCallEvents(item, factory) {
|
|
|
2753
2993
|
yield factory.toolCallCompleted(item);
|
|
2754
2994
|
}
|
|
2755
2995
|
//#endregion
|
|
2756
|
-
export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector,
|
|
2996
|
+
export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, ChatCompletionsAdapter, MessagesAdapter, MockAdapter, OllamaAdapter, ResponsesAdapter, WarningCode, aggregateEvents, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, reasoningItem, replayFromOutput, syntheticStream, textBlock, toolCallItem, toolResultItem, validateRequest };
|
|
2757
2997
|
|
|
2758
2998
|
//# sourceMappingURL=index.mjs.map
|