@codehz/ai 0.1.8 → 0.2.1
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 +153 -31
- package/dist/index.mjs +1290 -727
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +243 -196
- 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 +218 -61
- 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 +6 -43
- 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 -4
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,15 @@ 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>;
|
|
46
81
|
|
|
47
82
|
/** adapter 在 response.completed 中提供的 replay */
|
|
48
83
|
replayFromAdapter?: import("../types/index.js").ReplayItem[];
|
|
49
|
-
responseIdFromAdapter?: string;
|
|
50
84
|
stopReasonFromAdapter?: StopReason;
|
|
51
|
-
backendFromAdapter?: BackendTrace
|
|
85
|
+
backendFromAdapter?: Partial<BackendTrace>;
|
|
52
86
|
}
|
|
53
87
|
|
|
54
88
|
export function createAggregatorState(): AggregatorState {
|
|
@@ -59,12 +93,59 @@ export function createAggregatorState(): AggregatorState {
|
|
|
59
93
|
output: [],
|
|
60
94
|
textParts: [],
|
|
61
95
|
toolCalls: [],
|
|
96
|
+
started: false,
|
|
97
|
+
completed: false,
|
|
98
|
+
activeItems: new Map(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Active item helpers ───────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function getActiveItem(state: AggregatorState, itemId: string, expectedType: ActiveItem["type"]): ActiveItem {
|
|
105
|
+
const item = state.activeItems.get(itemId);
|
|
106
|
+
if (!item) {
|
|
107
|
+
throw streamProtocolError(`Received ${expectedType} delta/completed for unknown item: ${itemId}`);
|
|
108
|
+
}
|
|
109
|
+
if (item.type !== expectedType) {
|
|
110
|
+
throw streamProtocolError(`Item ${itemId} started as ${item.type} but received ${expectedType} event`);
|
|
111
|
+
}
|
|
112
|
+
return item;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function finalizeMessage(active: ActiveMessage): MessageItem {
|
|
116
|
+
return {
|
|
117
|
+
type: "message",
|
|
118
|
+
id: active.id,
|
|
119
|
+
role: active.role,
|
|
120
|
+
content: active.content,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function finalizeReasoning(active: ActiveReasoning): import("../types/index.js").ReasoningItem {
|
|
125
|
+
return {
|
|
126
|
+
type: "reasoning",
|
|
127
|
+
id: active.id,
|
|
128
|
+
visibility: active.visibility,
|
|
129
|
+
content: active.content,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function finalizeToolCall(active: ActiveToolCall): ToolCallItem {
|
|
134
|
+
return {
|
|
135
|
+
type: "tool_call",
|
|
136
|
+
id: active.id,
|
|
137
|
+
name: active.name,
|
|
138
|
+
argumentsText: active.argumentsText,
|
|
62
139
|
};
|
|
63
140
|
}
|
|
64
141
|
|
|
65
142
|
// ── Event handlers ────────────────────────────────────────────
|
|
66
143
|
|
|
67
144
|
function handleResponseStarted(state: AggregatorState, event: AIStreamEvent & { type: "response.started" }): void {
|
|
145
|
+
if (state.started) {
|
|
146
|
+
throw streamProtocolError("Stream must contain exactly one response.started event");
|
|
147
|
+
}
|
|
148
|
+
state.started = true;
|
|
68
149
|
state.responseId = event.responseId;
|
|
69
150
|
state.model = event.model;
|
|
70
151
|
state.backendInfo = event.backend;
|
|
@@ -82,64 +163,121 @@ function handleResponseAuxiliary(state: AggregatorState, event: AIStreamEvent &
|
|
|
82
163
|
state.billing = { ...state.billing, ...event.billing };
|
|
83
164
|
}
|
|
84
165
|
if (event.auxiliary) {
|
|
85
|
-
state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary);
|
|
166
|
+
state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleMessageStarted(state: AggregatorState, event: AIStreamEvent & { type: "message.started" }): void {
|
|
171
|
+
const id = event.item.id;
|
|
172
|
+
if (state.activeItems.has(id)) {
|
|
173
|
+
throw streamProtocolError(`Item with id ${id} is already active`);
|
|
86
174
|
}
|
|
175
|
+
state.activeItems.set(id, {
|
|
176
|
+
type: "message",
|
|
177
|
+
id,
|
|
178
|
+
role: event.item.role,
|
|
179
|
+
content: [],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function handleMessageDelta(state: AggregatorState, event: AIStreamEvent & { type: "message.delta" }): void {
|
|
184
|
+
const active = getActiveItem(state, event.itemId, "message") as ActiveMessage;
|
|
185
|
+
active.content.push(event.delta);
|
|
87
186
|
}
|
|
88
187
|
|
|
89
188
|
function handleMessageCompleted(state: AggregatorState, event: AIStreamEvent & { type: "message.completed" }): void {
|
|
90
|
-
state
|
|
91
|
-
|
|
189
|
+
const active = getActiveItem(state, event.itemId, "message") as ActiveMessage;
|
|
190
|
+
state.activeItems.delete(event.itemId);
|
|
191
|
+
const item = finalizeMessage(active);
|
|
192
|
+
state.output.push(item);
|
|
193
|
+
pushMessageText(state, item);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleReasoningStarted(state: AggregatorState, event: AIStreamEvent & { type: "reasoning.started" }): void {
|
|
197
|
+
const id = event.item.id;
|
|
198
|
+
if (state.activeItems.has(id)) {
|
|
199
|
+
throw streamProtocolError(`Item with id ${id} is already active`);
|
|
200
|
+
}
|
|
201
|
+
state.activeItems.set(id, {
|
|
202
|
+
type: "reasoning",
|
|
203
|
+
id,
|
|
204
|
+
visibility: event.item.visibility,
|
|
205
|
+
content: [],
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function handleReasoningDelta(state: AggregatorState, event: AIStreamEvent & { type: "reasoning.delta" }): void {
|
|
210
|
+
const active = getActiveItem(state, event.itemId, "reasoning") as ActiveReasoning;
|
|
211
|
+
active.content.push(event.delta);
|
|
92
212
|
}
|
|
93
213
|
|
|
94
214
|
function handleReasoningCompleted(
|
|
95
215
|
state: AggregatorState,
|
|
96
216
|
event: AIStreamEvent & { type: "reasoning.completed" },
|
|
97
217
|
): void {
|
|
98
|
-
state
|
|
218
|
+
const active = getActiveItem(state, event.itemId, "reasoning") as ActiveReasoning;
|
|
219
|
+
state.activeItems.delete(event.itemId);
|
|
220
|
+
state.output.push(finalizeReasoning(active));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function handleToolCallStarted(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.started" }): void {
|
|
224
|
+
const id = event.item.id;
|
|
225
|
+
if (state.activeItems.has(id)) {
|
|
226
|
+
throw streamProtocolError(`Item with id ${id} is already active`);
|
|
227
|
+
}
|
|
228
|
+
state.activeItems.set(id, {
|
|
229
|
+
type: "tool_call",
|
|
230
|
+
id,
|
|
231
|
+
name: event.item.name,
|
|
232
|
+
argumentsText: "",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function handleToolCallDelta(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.delta" }): void {
|
|
237
|
+
const active = getActiveItem(state, event.itemId, "tool_call") as ActiveToolCall;
|
|
238
|
+
if (event.delta.argumentsText) {
|
|
239
|
+
active.argumentsText += event.delta.argumentsText;
|
|
240
|
+
}
|
|
99
241
|
}
|
|
100
242
|
|
|
101
243
|
function handleToolCallCompleted(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.completed" }): void {
|
|
102
|
-
state
|
|
103
|
-
state.
|
|
244
|
+
const active = getActiveItem(state, event.itemId, "tool_call") as ActiveToolCall;
|
|
245
|
+
state.activeItems.delete(event.itemId);
|
|
246
|
+
const item = finalizeToolCall(active);
|
|
247
|
+
state.output.push(item);
|
|
248
|
+
state.toolCalls.push(item);
|
|
104
249
|
}
|
|
105
250
|
|
|
106
251
|
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);
|
|
252
|
+
if (state.activeItems.size > 0) {
|
|
253
|
+
throw streamProtocolError("response.completed received while active items still pending");
|
|
124
254
|
}
|
|
255
|
+
state.completed = true;
|
|
256
|
+
state.replayFromAdapter = event.replay;
|
|
257
|
+
state.stopReasonFromAdapter = event.stopReason;
|
|
258
|
+
state.backendFromAdapter = event.trace;
|
|
259
|
+
if (event.usage) state.usage = { ...state.usage, ...event.usage };
|
|
260
|
+
if (event.billing) state.billing = { ...state.billing, ...event.billing };
|
|
261
|
+
if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
|
|
262
|
+
if (event.warnings) pushWarnings(state, event.warnings);
|
|
263
|
+
if (event.opaqueOutput) state.output.push(...event.opaqueOutput);
|
|
125
264
|
}
|
|
126
265
|
|
|
127
266
|
// ── 从聚合状态构建最终 AIResponse ─────────────────────────────
|
|
128
267
|
|
|
129
268
|
function buildResponse(state: AggregatorState): AIResponse {
|
|
130
|
-
|
|
131
|
-
const backendFromResponse = state.backendFromAdapter;
|
|
269
|
+
const backendFromCompleted = state.backendFromAdapter;
|
|
132
270
|
const backend: BackendTrace = {
|
|
133
|
-
adapter:
|
|
134
|
-
isSyntheticStream:
|
|
135
|
-
requestId:
|
|
136
|
-
rawResponseId:
|
|
137
|
-
metadataSources:
|
|
138
|
-
warnings:
|
|
271
|
+
adapter: backendFromCompleted?.adapter ?? state.backendInfo?.kind ?? ("unknown" as BackendTrace["adapter"]),
|
|
272
|
+
isSyntheticStream: backendFromCompleted?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
|
|
273
|
+
requestId: backendFromCompleted?.requestId ?? state.responseId,
|
|
274
|
+
rawResponseId: backendFromCompleted?.rawResponseId,
|
|
275
|
+
metadataSources: backendFromCompleted?.metadataSources,
|
|
276
|
+
warnings: backendFromCompleted?.warnings,
|
|
139
277
|
};
|
|
140
278
|
|
|
141
279
|
return {
|
|
142
|
-
id: state.
|
|
280
|
+
id: state.responseId,
|
|
143
281
|
output: state.output,
|
|
144
282
|
replay: state.replayFromAdapter ?? [],
|
|
145
283
|
text: state.textParts.join(""),
|
|
@@ -159,7 +297,7 @@ function buildResponse(state: AggregatorState): AIResponse {
|
|
|
159
297
|
* 将事件数组聚合为 AIResponse。
|
|
160
298
|
* 适用于测试和离线处理场景。
|
|
161
299
|
*/
|
|
162
|
-
export function aggregateEvents(events: AIStreamEvent[]): AIResponse {
|
|
300
|
+
export function aggregateEvents(events: readonly AIStreamEvent[]): AIResponse {
|
|
163
301
|
const state = createAggregatorState();
|
|
164
302
|
for (const event of events) {
|
|
165
303
|
aggregateEvent(state, event);
|
|
@@ -168,6 +306,7 @@ export function aggregateEvents(events: AIStreamEvent[]): AIResponse {
|
|
|
168
306
|
}
|
|
169
307
|
|
|
170
308
|
export function aggregateEvent(state: AggregatorState, event: AIStreamEvent): void {
|
|
309
|
+
validateEventEnvelope(state, event);
|
|
171
310
|
state.lastEventType = event.type;
|
|
172
311
|
|
|
173
312
|
switch (event.type) {
|
|
@@ -181,18 +320,29 @@ export function aggregateEvent(state: AggregatorState, event: AIStreamEvent): vo
|
|
|
181
320
|
handleResponseAuxiliary(state, event);
|
|
182
321
|
break;
|
|
183
322
|
case "message.started":
|
|
323
|
+
handleMessageStarted(state, event);
|
|
324
|
+
break;
|
|
184
325
|
case "message.delta":
|
|
185
|
-
|
|
186
|
-
case "reasoning.delta":
|
|
187
|
-
case "tool_call.started":
|
|
188
|
-
case "tool_call.delta":
|
|
326
|
+
handleMessageDelta(state, event);
|
|
189
327
|
break;
|
|
190
328
|
case "message.completed":
|
|
191
329
|
handleMessageCompleted(state, event);
|
|
192
330
|
break;
|
|
331
|
+
case "reasoning.started":
|
|
332
|
+
handleReasoningStarted(state, event);
|
|
333
|
+
break;
|
|
334
|
+
case "reasoning.delta":
|
|
335
|
+
handleReasoningDelta(state, event);
|
|
336
|
+
break;
|
|
193
337
|
case "reasoning.completed":
|
|
194
338
|
handleReasoningCompleted(state, event);
|
|
195
339
|
break;
|
|
340
|
+
case "tool_call.started":
|
|
341
|
+
handleToolCallStarted(state, event);
|
|
342
|
+
break;
|
|
343
|
+
case "tool_call.delta":
|
|
344
|
+
handleToolCallDelta(state, event);
|
|
345
|
+
break;
|
|
196
346
|
case "tool_call.completed":
|
|
197
347
|
handleToolCallCompleted(state, event);
|
|
198
348
|
break;
|
|
@@ -203,27 +353,14 @@ export function aggregateEvent(state: AggregatorState, event: AIStreamEvent): vo
|
|
|
203
353
|
}
|
|
204
354
|
|
|
205
355
|
export function finalizeAggregation(state: AggregatorState): AIResponse {
|
|
206
|
-
if (state.
|
|
207
|
-
throw
|
|
356
|
+
if (!state.started) {
|
|
357
|
+
throw streamProtocolError("Stream must start with response.started event");
|
|
208
358
|
}
|
|
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
|
-
};
|
|
359
|
+
if (!state.completed || state.lastEventType !== "response.completed") {
|
|
360
|
+
throw streamProtocolError("Stream must end with response.completed event to produce a valid AIResponse");
|
|
224
361
|
}
|
|
225
362
|
|
|
226
|
-
return
|
|
363
|
+
return buildResponse(state);
|
|
227
364
|
}
|
|
228
365
|
|
|
229
366
|
function pushWarnings(state: AggregatorState, warnings: readonly string[]): void {
|
|
@@ -242,3 +379,23 @@ function pushMessageText(state: AggregatorState, item: MessageItem): void {
|
|
|
242
379
|
}
|
|
243
380
|
}
|
|
244
381
|
}
|
|
382
|
+
|
|
383
|
+
function validateEventEnvelope(state: AggregatorState, event: AIStreamEvent): void {
|
|
384
|
+
if (state.completed) {
|
|
385
|
+
throw streamProtocolError("response.completed must be the final stream event");
|
|
386
|
+
}
|
|
387
|
+
if (!state.started && event.type !== "response.started") {
|
|
388
|
+
throw streamProtocolError("Stream must start with response.started event");
|
|
389
|
+
}
|
|
390
|
+
if (state.responseId !== undefined && event.responseId !== state.responseId) {
|
|
391
|
+
throw streamProtocolError("All stream events must use the same responseId");
|
|
392
|
+
}
|
|
393
|
+
if (state.nextSequence !== undefined && event.sequence !== state.nextSequence) {
|
|
394
|
+
throw streamProtocolError(`Expected event sequence ${state.nextSequence}, received ${event.sequence}`);
|
|
395
|
+
}
|
|
396
|
+
state.nextSequence = event.sequence + 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function streamProtocolError(message: string): AIStreamError {
|
|
400
|
+
return new AIStreamError(message, "STREAM_PROTOCOL_ERROR");
|
|
401
|
+
}
|
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
|
}
|