@codehz/ai 0.2.0 → 0.2.2
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/README.md +9 -3
- package/dist/index.d.mts +151 -27
- package/dist/index.mjs +1291 -703
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +236 -197
- package/src/adapters/messages.ts +150 -124
- package/src/adapters/mock.ts +44 -11
- package/src/adapters/ollama.ts +219 -191
- package/src/adapters/responses.ts +222 -137
- package/src/core/aggregator.ts +233 -62
- package/src/core/errors.ts +7 -1
- package/src/core/event-factory.ts +24 -14
- package/src/core/merge-auxiliary.ts +22 -0
- package/src/core/normalize.ts +15 -1
- package/src/core/validation.ts +29 -21
- package/src/helpers/adapter-base.ts +23 -25
- package/src/helpers/adapter-security.ts +126 -0
- package/src/helpers/incremental-stream-parser.ts +84 -0
- package/src/helpers/index.ts +19 -0
- package/src/helpers/request-mapper.ts +72 -0
- package/src/helpers/sse-parser.ts +51 -25
- package/src/helpers/synthetic-stream.ts +13 -21
- package/src/helpers/usage-mapping.ts +4 -9
- package/src/types/adapter.ts +12 -1
- package/src/types/events.ts +14 -10
- package/src/types/index.ts +9 -1
- package/src/types/response.ts +0 -1
package/src/core/aggregator.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* 流聚合器
|
|
3
3
|
*
|
|
4
4
|
* 将 AIStreamEvent 序列聚合为统一的 AIResponse。
|
|
5
|
+
*
|
|
5
6
|
* 职责:
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
7
|
+
* - 严格 item 状态机:started 创建 active item,delta 累积,completed 构建 OutputItem
|
|
8
|
+
* - 校验:未 started 的 delta/completed、ID 重用、类型错配、response.completed 时 active items 非空
|
|
9
|
+
* - 响应级 sequence/responseId/started/completed 校验
|
|
10
|
+
* - usage/billing/auxiliary 仅来自 response.auxiliary
|
|
11
|
+
* - warning 仅来自 response.warning
|
|
12
|
+
* - 最终 AIResponse 由聚合器唯一构建
|
|
10
13
|
*
|
|
11
14
|
* 约束:
|
|
12
15
|
* - replay 由 adapter 显式提供,聚合器不猜测
|
|
@@ -25,7 +28,35 @@ import type {
|
|
|
25
28
|
AuxiliaryInfo,
|
|
26
29
|
BackendTrace,
|
|
27
30
|
StopReason,
|
|
31
|
+
ContentBlock,
|
|
28
32
|
} from "../types/index.js";
|
|
33
|
+
import { AIStreamError } from "./errors.js";
|
|
34
|
+
import { mergeAuxiliary } from "./merge-auxiliary.js";
|
|
35
|
+
|
|
36
|
+
// ── Active item types ─────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
type ActiveMessage = {
|
|
39
|
+
type: "message";
|
|
40
|
+
id: string;
|
|
41
|
+
role: "assistant";
|
|
42
|
+
content: ContentBlock[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type ActiveReasoning = {
|
|
46
|
+
type: "reasoning";
|
|
47
|
+
id: string;
|
|
48
|
+
visibility: "full" | "summary" | "redacted" | "opaque";
|
|
49
|
+
content: ContentBlock[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type ActiveToolCall = {
|
|
53
|
+
type: "tool_call";
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
argumentsText: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type ActiveItem = ActiveMessage | ActiveReasoning | ActiveToolCall;
|
|
29
60
|
|
|
30
61
|
// ── 聚合器状态 ────────────────────────────────────────────────
|
|
31
62
|
|
|
@@ -43,12 +74,17 @@ export interface AggregatorState {
|
|
|
43
74
|
textParts: string[];
|
|
44
75
|
toolCalls: ToolCallItem[];
|
|
45
76
|
lastEventType?: AIStreamEvent["type"];
|
|
77
|
+
started: boolean;
|
|
78
|
+
completed: boolean;
|
|
79
|
+
nextSequence?: number;
|
|
80
|
+
activeItems: Map<string, ActiveItem>;
|
|
81
|
+
itemOrder: string[];
|
|
82
|
+
completedItems: Map<string, OutputItem>;
|
|
46
83
|
|
|
47
84
|
/** adapter 在 response.completed 中提供的 replay */
|
|
48
85
|
replayFromAdapter?: import("../types/index.js").ReplayItem[];
|
|
49
|
-
responseIdFromAdapter?: string;
|
|
50
86
|
stopReasonFromAdapter?: StopReason;
|
|
51
|
-
backendFromAdapter?: BackendTrace
|
|
87
|
+
backendFromAdapter?: Partial<BackendTrace>;
|
|
52
88
|
}
|
|
53
89
|
|
|
54
90
|
export function createAggregatorState(): AggregatorState {
|
|
@@ -59,12 +95,61 @@ export function createAggregatorState(): AggregatorState {
|
|
|
59
95
|
output: [],
|
|
60
96
|
textParts: [],
|
|
61
97
|
toolCalls: [],
|
|
98
|
+
started: false,
|
|
99
|
+
completed: false,
|
|
100
|
+
activeItems: new Map(),
|
|
101
|
+
itemOrder: [],
|
|
102
|
+
completedItems: new Map(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Active item helpers ───────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function getActiveItem(state: AggregatorState, itemId: string, expectedType: ActiveItem["type"]): ActiveItem {
|
|
109
|
+
const item = state.activeItems.get(itemId);
|
|
110
|
+
if (!item) {
|
|
111
|
+
throw streamProtocolError(`Received ${expectedType} delta/completed for unknown item: ${itemId}`);
|
|
112
|
+
}
|
|
113
|
+
if (item.type !== expectedType) {
|
|
114
|
+
throw streamProtocolError(`Item ${itemId} started as ${item.type} but received ${expectedType} event`);
|
|
115
|
+
}
|
|
116
|
+
return item;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function finalizeMessage(active: ActiveMessage): MessageItem {
|
|
120
|
+
return {
|
|
121
|
+
type: "message",
|
|
122
|
+
id: active.id,
|
|
123
|
+
role: active.role,
|
|
124
|
+
content: active.content,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function finalizeReasoning(active: ActiveReasoning): import("../types/index.js").ReasoningItem {
|
|
129
|
+
return {
|
|
130
|
+
type: "reasoning",
|
|
131
|
+
id: active.id,
|
|
132
|
+
visibility: active.visibility,
|
|
133
|
+
content: active.content,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function finalizeToolCall(active: ActiveToolCall): ToolCallItem {
|
|
138
|
+
return {
|
|
139
|
+
type: "tool_call",
|
|
140
|
+
id: active.id,
|
|
141
|
+
name: active.name,
|
|
142
|
+
argumentsText: active.argumentsText,
|
|
62
143
|
};
|
|
63
144
|
}
|
|
64
145
|
|
|
65
146
|
// ── Event handlers ────────────────────────────────────────────
|
|
66
147
|
|
|
67
148
|
function handleResponseStarted(state: AggregatorState, event: AIStreamEvent & { type: "response.started" }): void {
|
|
149
|
+
if (state.started) {
|
|
150
|
+
throw streamProtocolError("Stream must contain exactly one response.started event");
|
|
151
|
+
}
|
|
152
|
+
state.started = true;
|
|
68
153
|
state.responseId = event.responseId;
|
|
69
154
|
state.model = event.model;
|
|
70
155
|
state.backendInfo = event.backend;
|
|
@@ -82,65 +167,132 @@ function handleResponseAuxiliary(state: AggregatorState, event: AIStreamEvent &
|
|
|
82
167
|
state.billing = { ...state.billing, ...event.billing };
|
|
83
168
|
}
|
|
84
169
|
if (event.auxiliary) {
|
|
85
|
-
state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary);
|
|
170
|
+
state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
|
|
86
171
|
}
|
|
87
172
|
}
|
|
88
173
|
|
|
174
|
+
function handleMessageStarted(state: AggregatorState, event: AIStreamEvent & { type: "message.started" }): void {
|
|
175
|
+
const id = event.item.id;
|
|
176
|
+
if (state.activeItems.has(id)) {
|
|
177
|
+
throw streamProtocolError(`Item with id ${id} is already active`);
|
|
178
|
+
}
|
|
179
|
+
state.activeItems.set(id, {
|
|
180
|
+
type: "message",
|
|
181
|
+
id,
|
|
182
|
+
role: event.item.role,
|
|
183
|
+
content: [],
|
|
184
|
+
});
|
|
185
|
+
state.itemOrder.push(id);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleMessageDelta(state: AggregatorState, event: AIStreamEvent & { type: "message.delta" }): void {
|
|
189
|
+
const active = getActiveItem(state, event.itemId, "message") as ActiveMessage;
|
|
190
|
+
active.content.push(event.delta);
|
|
191
|
+
}
|
|
192
|
+
|
|
89
193
|
function handleMessageCompleted(state: AggregatorState, event: AIStreamEvent & { type: "message.completed" }): void {
|
|
90
|
-
state
|
|
91
|
-
|
|
194
|
+
const active = getActiveItem(state, event.itemId, "message") as ActiveMessage;
|
|
195
|
+
state.activeItems.delete(event.itemId);
|
|
196
|
+
const item = finalizeMessage(active);
|
|
197
|
+
state.completedItems.set(event.itemId, item);
|
|
198
|
+
pushMessageText(state, item);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function handleReasoningStarted(state: AggregatorState, event: AIStreamEvent & { type: "reasoning.started" }): void {
|
|
202
|
+
const id = event.item.id;
|
|
203
|
+
if (state.activeItems.has(id)) {
|
|
204
|
+
throw streamProtocolError(`Item with id ${id} is already active`);
|
|
205
|
+
}
|
|
206
|
+
state.activeItems.set(id, {
|
|
207
|
+
type: "reasoning",
|
|
208
|
+
id,
|
|
209
|
+
visibility: event.item.visibility,
|
|
210
|
+
content: [],
|
|
211
|
+
});
|
|
212
|
+
state.itemOrder.push(id);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function handleReasoningDelta(state: AggregatorState, event: AIStreamEvent & { type: "reasoning.delta" }): void {
|
|
216
|
+
const active = getActiveItem(state, event.itemId, "reasoning") as ActiveReasoning;
|
|
217
|
+
active.content.push(event.delta);
|
|
92
218
|
}
|
|
93
219
|
|
|
94
220
|
function handleReasoningCompleted(
|
|
95
221
|
state: AggregatorState,
|
|
96
222
|
event: AIStreamEvent & { type: "reasoning.completed" },
|
|
97
223
|
): void {
|
|
98
|
-
state
|
|
224
|
+
const active = getActiveItem(state, event.itemId, "reasoning") as ActiveReasoning;
|
|
225
|
+
state.activeItems.delete(event.itemId);
|
|
226
|
+
state.completedItems.set(event.itemId, finalizeReasoning(active));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function handleToolCallStarted(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.started" }): void {
|
|
230
|
+
const id = event.item.id;
|
|
231
|
+
if (state.activeItems.has(id)) {
|
|
232
|
+
throw streamProtocolError(`Item with id ${id} is already active`);
|
|
233
|
+
}
|
|
234
|
+
state.activeItems.set(id, {
|
|
235
|
+
type: "tool_call",
|
|
236
|
+
id,
|
|
237
|
+
name: event.item.name,
|
|
238
|
+
argumentsText: "",
|
|
239
|
+
});
|
|
240
|
+
state.itemOrder.push(id);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function handleToolCallDelta(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.delta" }): void {
|
|
244
|
+
const active = getActiveItem(state, event.itemId, "tool_call") as ActiveToolCall;
|
|
245
|
+
if (event.delta.argumentsText) {
|
|
246
|
+
active.argumentsText += event.delta.argumentsText;
|
|
247
|
+
}
|
|
99
248
|
}
|
|
100
249
|
|
|
101
250
|
function handleToolCallCompleted(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.completed" }): void {
|
|
102
|
-
state
|
|
103
|
-
state.
|
|
251
|
+
const active = getActiveItem(state, event.itemId, "tool_call") as ActiveToolCall;
|
|
252
|
+
state.activeItems.delete(event.itemId);
|
|
253
|
+
const item = finalizeToolCall(active);
|
|
254
|
+
state.completedItems.set(event.itemId, item);
|
|
255
|
+
state.toolCalls.push(item);
|
|
104
256
|
}
|
|
105
257
|
|
|
106
258
|
function handleResponseCompleted(state: AggregatorState, event: AIStreamEvent & { type: "response.completed" }): void {
|
|
107
|
-
state.
|
|
108
|
-
|
|
109
|
-
state.stopReasonFromAdapter = event.response.stopReason;
|
|
110
|
-
state.backendFromAdapter = event.response.backend;
|
|
111
|
-
|
|
112
|
-
// 从 response.completed 中提取 usage/billing(适配器可能未发 auxiliary 事件)
|
|
113
|
-
if (event.response.usage) {
|
|
114
|
-
state.usage = { ...state.usage, ...event.response.usage };
|
|
115
|
-
}
|
|
116
|
-
if (event.response.billing) {
|
|
117
|
-
state.billing = { ...state.billing, ...event.response.billing };
|
|
118
|
-
}
|
|
119
|
-
if (event.response.auxiliary) {
|
|
120
|
-
state.auxiliary = mergeAuxiliary(state.auxiliary, event.response.auxiliary);
|
|
121
|
-
}
|
|
122
|
-
if (event.response.warnings) {
|
|
123
|
-
pushWarnings(state, event.response.warnings);
|
|
259
|
+
if (state.activeItems.size > 0) {
|
|
260
|
+
throw streamProtocolError("response.completed received while active items still pending");
|
|
124
261
|
}
|
|
262
|
+
state.completed = true;
|
|
263
|
+
state.replayFromAdapter = event.replay;
|
|
264
|
+
state.stopReasonFromAdapter = event.stopReason;
|
|
265
|
+
state.backendFromAdapter = event.trace;
|
|
266
|
+
if (event.usage) state.usage = { ...state.usage, ...event.usage };
|
|
267
|
+
if (event.billing) state.billing = { ...state.billing, ...event.billing };
|
|
268
|
+
if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
|
|
269
|
+
if (event.warnings) pushWarnings(state, event.warnings);
|
|
270
|
+
if (event.opaqueOutput) state.output.push(...event.opaqueOutput);
|
|
125
271
|
}
|
|
126
272
|
|
|
127
273
|
// ── 从聚合状态构建最终 AIResponse ─────────────────────────────
|
|
128
274
|
|
|
129
275
|
function buildResponse(state: AggregatorState): AIResponse {
|
|
130
|
-
|
|
131
|
-
const backendFromResponse = state.backendFromAdapter;
|
|
276
|
+
const backendFromCompleted = state.backendFromAdapter;
|
|
132
277
|
const backend: BackendTrace = {
|
|
133
|
-
adapter:
|
|
134
|
-
isSyntheticStream:
|
|
135
|
-
requestId:
|
|
136
|
-
rawResponseId:
|
|
137
|
-
metadataSources:
|
|
138
|
-
warnings:
|
|
278
|
+
adapter: backendFromCompleted?.adapter ?? state.backendInfo?.kind ?? ("unknown" as BackendTrace["adapter"]),
|
|
279
|
+
isSyntheticStream: backendFromCompleted?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
|
|
280
|
+
requestId: backendFromCompleted?.requestId ?? state.responseId,
|
|
281
|
+
rawResponseId: backendFromCompleted?.rawResponseId,
|
|
282
|
+
metadataSources: backendFromCompleted?.metadataSources,
|
|
283
|
+
warnings: backendFromCompleted?.warnings,
|
|
139
284
|
};
|
|
285
|
+
const orderedOutput = state.itemOrder.map((id) => {
|
|
286
|
+
const item = state.completedItems.get(id);
|
|
287
|
+
if (!item) {
|
|
288
|
+
throw streamProtocolError(`Item ${id} was started but not completed`);
|
|
289
|
+
}
|
|
290
|
+
return item;
|
|
291
|
+
});
|
|
140
292
|
|
|
141
293
|
return {
|
|
142
|
-
id: state.
|
|
143
|
-
output: state.output,
|
|
294
|
+
id: state.responseId,
|
|
295
|
+
output: [...orderedOutput, ...state.output],
|
|
144
296
|
replay: state.replayFromAdapter ?? [],
|
|
145
297
|
text: state.textParts.join(""),
|
|
146
298
|
toolCalls: state.toolCalls,
|
|
@@ -159,7 +311,7 @@ function buildResponse(state: AggregatorState): AIResponse {
|
|
|
159
311
|
* 将事件数组聚合为 AIResponse。
|
|
160
312
|
* 适用于测试和离线处理场景。
|
|
161
313
|
*/
|
|
162
|
-
export function aggregateEvents(events: AIStreamEvent[]): AIResponse {
|
|
314
|
+
export function aggregateEvents(events: readonly AIStreamEvent[]): AIResponse {
|
|
163
315
|
const state = createAggregatorState();
|
|
164
316
|
for (const event of events) {
|
|
165
317
|
aggregateEvent(state, event);
|
|
@@ -168,6 +320,7 @@ export function aggregateEvents(events: AIStreamEvent[]): AIResponse {
|
|
|
168
320
|
}
|
|
169
321
|
|
|
170
322
|
export function aggregateEvent(state: AggregatorState, event: AIStreamEvent): void {
|
|
323
|
+
validateEventEnvelope(state, event);
|
|
171
324
|
state.lastEventType = event.type;
|
|
172
325
|
|
|
173
326
|
switch (event.type) {
|
|
@@ -181,18 +334,29 @@ export function aggregateEvent(state: AggregatorState, event: AIStreamEvent): vo
|
|
|
181
334
|
handleResponseAuxiliary(state, event);
|
|
182
335
|
break;
|
|
183
336
|
case "message.started":
|
|
337
|
+
handleMessageStarted(state, event);
|
|
338
|
+
break;
|
|
184
339
|
case "message.delta":
|
|
185
|
-
|
|
186
|
-
case "reasoning.delta":
|
|
187
|
-
case "tool_call.started":
|
|
188
|
-
case "tool_call.delta":
|
|
340
|
+
handleMessageDelta(state, event);
|
|
189
341
|
break;
|
|
190
342
|
case "message.completed":
|
|
191
343
|
handleMessageCompleted(state, event);
|
|
192
344
|
break;
|
|
345
|
+
case "reasoning.started":
|
|
346
|
+
handleReasoningStarted(state, event);
|
|
347
|
+
break;
|
|
348
|
+
case "reasoning.delta":
|
|
349
|
+
handleReasoningDelta(state, event);
|
|
350
|
+
break;
|
|
193
351
|
case "reasoning.completed":
|
|
194
352
|
handleReasoningCompleted(state, event);
|
|
195
353
|
break;
|
|
354
|
+
case "tool_call.started":
|
|
355
|
+
handleToolCallStarted(state, event);
|
|
356
|
+
break;
|
|
357
|
+
case "tool_call.delta":
|
|
358
|
+
handleToolCallDelta(state, event);
|
|
359
|
+
break;
|
|
196
360
|
case "tool_call.completed":
|
|
197
361
|
handleToolCallCompleted(state, event);
|
|
198
362
|
break;
|
|
@@ -203,27 +367,14 @@ export function aggregateEvent(state: AggregatorState, event: AIStreamEvent): vo
|
|
|
203
367
|
}
|
|
204
368
|
|
|
205
369
|
export function finalizeAggregation(state: AggregatorState): AIResponse {
|
|
206
|
-
if (state.
|
|
207
|
-
throw
|
|
370
|
+
if (!state.started) {
|
|
371
|
+
throw streamProtocolError("Stream must start with response.started event");
|
|
208
372
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function mergeAuxiliary(base: AuxiliaryInfo, patch: Partial<AuxiliaryInfo>): AuxiliaryInfo {
|
|
214
|
-
const merged: AuxiliaryInfo = {
|
|
215
|
-
...base,
|
|
216
|
-
...patch,
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
if (base.providerMetadata || patch.providerMetadata) {
|
|
220
|
-
merged.providerMetadata = {
|
|
221
|
-
...base.providerMetadata,
|
|
222
|
-
...patch.providerMetadata,
|
|
223
|
-
};
|
|
373
|
+
if (!state.completed || state.lastEventType !== "response.completed") {
|
|
374
|
+
throw streamProtocolError("Stream must end with response.completed event to produce a valid AIResponse");
|
|
224
375
|
}
|
|
225
376
|
|
|
226
|
-
return
|
|
377
|
+
return buildResponse(state);
|
|
227
378
|
}
|
|
228
379
|
|
|
229
380
|
function pushWarnings(state: AggregatorState, warnings: readonly string[]): void {
|
|
@@ -242,3 +393,23 @@ function pushMessageText(state: AggregatorState, item: MessageItem): void {
|
|
|
242
393
|
}
|
|
243
394
|
}
|
|
244
395
|
}
|
|
396
|
+
|
|
397
|
+
function validateEventEnvelope(state: AggregatorState, event: AIStreamEvent): void {
|
|
398
|
+
if (state.completed) {
|
|
399
|
+
throw streamProtocolError("response.completed must be the final stream event");
|
|
400
|
+
}
|
|
401
|
+
if (!state.started && event.type !== "response.started") {
|
|
402
|
+
throw streamProtocolError("Stream must start with response.started event");
|
|
403
|
+
}
|
|
404
|
+
if (state.responseId !== undefined && event.responseId !== state.responseId) {
|
|
405
|
+
throw streamProtocolError("All stream events must use the same responseId");
|
|
406
|
+
}
|
|
407
|
+
if (state.nextSequence !== undefined && event.sequence !== state.nextSequence) {
|
|
408
|
+
throw streamProtocolError(`Expected event sequence ${state.nextSequence}, received ${event.sequence}`);
|
|
409
|
+
}
|
|
410
|
+
state.nextSequence = event.sequence + 1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function streamProtocolError(message: string): AIStreamError {
|
|
414
|
+
return new AIStreamError(message, "STREAM_PROTOCOL_ERROR");
|
|
415
|
+
}
|
package/src/core/errors.ts
CHANGED
|
@@ -40,7 +40,11 @@ export class AIError extends Error {
|
|
|
40
40
|
|
|
41
41
|
/** 请求构造失败 — 参数校验不通过。在进入 adapter 前同步抛错。 */
|
|
42
42
|
export class AIRequestError extends AIError {
|
|
43
|
-
constructor(
|
|
43
|
+
constructor(
|
|
44
|
+
message: string,
|
|
45
|
+
code: ErrorCode,
|
|
46
|
+
public readonly issues?: readonly { field: string; code: string; message: string }[],
|
|
47
|
+
) {
|
|
44
48
|
super(message, code, "AIRequestError");
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -96,4 +100,6 @@ export const WarningCode = {
|
|
|
96
100
|
CAPABILITY_DOWNGRADE: "CAPABILITY_DOWNGRADE",
|
|
97
101
|
/** 模拟流式 */
|
|
98
102
|
SYNTHETIC_STREAM: "SYNTHETIC_STREAM",
|
|
103
|
+
/** 工具调用以批量方式到达(非 token 级流式) */
|
|
104
|
+
TOOL_CALL_BATCHED: "TOOL_CALL_BATCHED",
|
|
99
105
|
} as const;
|
|
@@ -19,14 +19,15 @@ import type {
|
|
|
19
19
|
ToolCallStartedEvent,
|
|
20
20
|
ToolCallDeltaEvent,
|
|
21
21
|
ToolCallCompletedEvent,
|
|
22
|
-
MessageItem,
|
|
23
|
-
ReasoningItem,
|
|
24
|
-
ToolCallItem,
|
|
25
22
|
ContentBlock,
|
|
26
23
|
Usage,
|
|
27
24
|
BillingInfo,
|
|
28
25
|
AuxiliaryInfo,
|
|
29
|
-
|
|
26
|
+
ReplayItem,
|
|
27
|
+
StopReason,
|
|
28
|
+
BackendTrace,
|
|
29
|
+
OpaqueItem,
|
|
30
|
+
ReasoningItem,
|
|
30
31
|
} from "../types/index.js";
|
|
31
32
|
|
|
32
33
|
export type EventFactoryBackend = {
|
|
@@ -80,8 +81,17 @@ export function createEventFactory(state: EventFactoryState) {
|
|
|
80
81
|
return { ...base(), type: "response.auxiliary", ...data };
|
|
81
82
|
},
|
|
82
83
|
|
|
83
|
-
responseCompleted(
|
|
84
|
-
|
|
84
|
+
responseCompleted(completion: {
|
|
85
|
+
replay: ReplayItem[];
|
|
86
|
+
stopReason?: StopReason;
|
|
87
|
+
usage?: Usage;
|
|
88
|
+
billing?: BillingInfo;
|
|
89
|
+
auxiliary?: AuxiliaryInfo;
|
|
90
|
+
warnings?: string[];
|
|
91
|
+
opaqueOutput?: OpaqueItem[];
|
|
92
|
+
trace?: Partial<BackendTrace>;
|
|
93
|
+
}): ResponseCompletedEvent {
|
|
94
|
+
return { ...base(), type: "response.completed", ...completion };
|
|
85
95
|
},
|
|
86
96
|
|
|
87
97
|
// ── 消息流事件 ──────────────────────────────────────────
|
|
@@ -90,12 +100,12 @@ export function createEventFactory(state: EventFactoryState) {
|
|
|
90
100
|
return { ...base(), type: "message.started", item: { id, role: "assistant" } };
|
|
91
101
|
},
|
|
92
102
|
|
|
93
|
-
messageDelta(itemId: string,
|
|
94
|
-
return { ...base(), type: "message.delta", itemId, delta
|
|
103
|
+
messageDelta(itemId: string, delta: ContentBlock): MessageDeltaEvent {
|
|
104
|
+
return { ...base(), type: "message.delta", itemId, delta };
|
|
95
105
|
},
|
|
96
106
|
|
|
97
|
-
messageCompleted(
|
|
98
|
-
return { ...base(), type: "message.completed",
|
|
107
|
+
messageCompleted(itemId: string): MessageCompletedEvent {
|
|
108
|
+
return { ...base(), type: "message.completed", itemId };
|
|
99
109
|
},
|
|
100
110
|
|
|
101
111
|
// ── 思维链流事件 ────────────────────────────────────────
|
|
@@ -108,8 +118,8 @@ export function createEventFactory(state: EventFactoryState) {
|
|
|
108
118
|
return { ...base(), type: "reasoning.delta", itemId, delta };
|
|
109
119
|
},
|
|
110
120
|
|
|
111
|
-
reasoningCompleted(
|
|
112
|
-
return { ...base(), type: "reasoning.completed",
|
|
121
|
+
reasoningCompleted(itemId: string): ReasoningCompletedEvent {
|
|
122
|
+
return { ...base(), type: "reasoning.completed", itemId };
|
|
113
123
|
},
|
|
114
124
|
|
|
115
125
|
// ── 工具调用流事件 ──────────────────────────────────────
|
|
@@ -122,8 +132,8 @@ export function createEventFactory(state: EventFactoryState) {
|
|
|
122
132
|
return { ...base(), type: "tool_call.delta", itemId, delta };
|
|
123
133
|
},
|
|
124
134
|
|
|
125
|
-
toolCallCompleted(
|
|
126
|
-
return { ...base(), type: "tool_call.completed",
|
|
135
|
+
toolCallCompleted(itemId: string): ToolCallCompletedEvent {
|
|
136
|
+
return { ...base(), type: "tool_call.completed", itemId };
|
|
127
137
|
},
|
|
128
138
|
|
|
129
139
|
/** 返回当前已发出的 sequence 计数(用于断言) */
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AuxiliaryInfo } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export function mergeAuxiliary(
|
|
4
|
+
base?: Partial<AuxiliaryInfo>,
|
|
5
|
+
patch?: Partial<AuxiliaryInfo>,
|
|
6
|
+
): AuxiliaryInfo | undefined {
|
|
7
|
+
if (!base && !patch) return undefined;
|
|
8
|
+
|
|
9
|
+
const merged: AuxiliaryInfo = {
|
|
10
|
+
...base,
|
|
11
|
+
...patch,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
if (base?.providerMetadata || patch?.providerMetadata) {
|
|
15
|
+
merged.providerMetadata = {
|
|
16
|
+
...base?.providerMetadata,
|
|
17
|
+
...patch?.providerMetadata,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return merged;
|
|
22
|
+
}
|
package/src/core/normalize.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { AIRequest, NormalizedRequest } from "../types/index.js";
|
|
9
|
-
import { assertValidRequest } from "./validation.js";
|
|
9
|
+
import { assertValidRequest, validateInclude } from "./validation.js";
|
|
10
|
+
import { AIRequestError } from "./errors.js";
|
|
10
11
|
|
|
11
12
|
export type NormalizeOptions = {
|
|
12
13
|
model: string;
|
|
@@ -29,6 +30,19 @@ const DEFAULT_INCLUDE = {
|
|
|
29
30
|
export function normalizeRequest(request: AIRequest, options: NormalizeOptions): NormalizedRequest {
|
|
30
31
|
const { model, defaults } = options;
|
|
31
32
|
|
|
33
|
+
// 在展开 include 前先校验,防止非法值被合并掩盖
|
|
34
|
+
const earlyIncludeIssues: { field: string; code: string; message: string }[] = [];
|
|
35
|
+
if (request.include !== undefined) {
|
|
36
|
+
validateInclude(request.include, earlyIncludeIssues);
|
|
37
|
+
}
|
|
38
|
+
if (defaults?.include !== undefined) {
|
|
39
|
+
validateInclude(defaults.include, earlyIncludeIssues);
|
|
40
|
+
}
|
|
41
|
+
const firstIncludeIssue = earlyIncludeIssues[0];
|
|
42
|
+
if (firstIncludeIssue) {
|
|
43
|
+
throw new AIRequestError(firstIncludeIssue.message, firstIncludeIssue.code, earlyIncludeIssues);
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
// 合并 defaults(浅合并,input/tools 由 request 完全覆盖)
|
|
33
47
|
const merged: AIRequest = {
|
|
34
48
|
...defaults,
|
package/src/core/validation.ts
CHANGED
|
@@ -226,6 +226,31 @@ function validateToolChoice(toolChoice: unknown, issues: ValidationIssue[]): voi
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/** Validate include settings, appending issues to the given array. */
|
|
230
|
+
export function validateInclude(include: unknown, issues: ValidationIssue[]): void {
|
|
231
|
+
if (!isRecord(include)) {
|
|
232
|
+
pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (include.usage !== undefined && (typeof include.usage !== "string" || !INCLUDE_MODES.has(include.usage))) {
|
|
236
|
+
pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
|
|
237
|
+
}
|
|
238
|
+
if (include.billing !== undefined && (typeof include.billing !== "string" || !INCLUDE_MODES.has(include.billing))) {
|
|
239
|
+
pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
include.providerMetadata !== undefined &&
|
|
243
|
+
(typeof include.providerMetadata !== "string" || !INCLUDE_MODES.has(include.providerMetadata))
|
|
244
|
+
) {
|
|
245
|
+
pushIssue(
|
|
246
|
+
issues,
|
|
247
|
+
"include.providerMetadata",
|
|
248
|
+
"INCLUDE_PROVIDER_METADATA_INVALID",
|
|
249
|
+
"include.providerMetadata must be off or best_effort",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
229
254
|
/**
|
|
230
255
|
* 校验 AIRequest,返回校验问题列表。
|
|
231
256
|
* 空数组表示无问题。
|
|
@@ -257,7 +282,7 @@ export function validateRequest(request: AIRequest): ValidationIssue[] {
|
|
|
257
282
|
|
|
258
283
|
// temperature 范围
|
|
259
284
|
if (request.temperature !== undefined) {
|
|
260
|
-
if (typeof request.temperature !== "number" ||
|
|
285
|
+
if (typeof request.temperature !== "number" || !Number.isFinite(request.temperature)) {
|
|
261
286
|
issues.push({
|
|
262
287
|
field: "temperature",
|
|
263
288
|
code: "TEMPERATURE_NOT_NUMBER",
|
|
@@ -274,7 +299,7 @@ export function validateRequest(request: AIRequest): ValidationIssue[] {
|
|
|
274
299
|
|
|
275
300
|
// maxOutputTokens 合法性
|
|
276
301
|
if (request.maxOutputTokens !== undefined) {
|
|
277
|
-
if (typeof request.maxOutputTokens !== "number" ||
|
|
302
|
+
if (typeof request.maxOutputTokens !== "number" || !Number.isFinite(request.maxOutputTokens)) {
|
|
278
303
|
issues.push({
|
|
279
304
|
field: "maxOutputTokens",
|
|
280
305
|
code: "MAX_OUTPUT_TOKENS_NOT_NUMBER",
|
|
@@ -290,24 +315,7 @@ export function validateRequest(request: AIRequest): ValidationIssue[] {
|
|
|
290
315
|
}
|
|
291
316
|
|
|
292
317
|
if (request.include !== undefined) {
|
|
293
|
-
|
|
294
|
-
pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
|
|
295
|
-
} else {
|
|
296
|
-
if (request.include.usage !== undefined && !INCLUDE_MODES.has(request.include.usage)) {
|
|
297
|
-
pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
|
|
298
|
-
}
|
|
299
|
-
if (request.include.billing !== undefined && !INCLUDE_MODES.has(request.include.billing)) {
|
|
300
|
-
pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
|
|
301
|
-
}
|
|
302
|
-
if (request.include.providerMetadata !== undefined && !INCLUDE_MODES.has(request.include.providerMetadata)) {
|
|
303
|
-
pushIssue(
|
|
304
|
-
issues,
|
|
305
|
-
"include.providerMetadata",
|
|
306
|
-
"INCLUDE_PROVIDER_METADATA_INVALID",
|
|
307
|
-
"include.providerMetadata must be off or best_effort",
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
318
|
+
validateInclude(request.include, issues);
|
|
311
319
|
}
|
|
312
320
|
|
|
313
321
|
if (request.metadata !== undefined) {
|
|
@@ -359,6 +367,6 @@ export function assertValidRequest(request: AIRequest): void {
|
|
|
359
367
|
const issues = validateRequest(request);
|
|
360
368
|
const first = issues[0];
|
|
361
369
|
if (first) {
|
|
362
|
-
throw new AIRequestError(first.message, first.code);
|
|
370
|
+
throw new AIRequestError(first.message, first.code, issues);
|
|
363
371
|
}
|
|
364
372
|
}
|