@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.
@@ -2,11 +2,14 @@
2
2
  * 流聚合器
3
3
  *
4
4
  * 将 AIStreamEvent 序列聚合为统一的 AIResponse。
5
+ *
5
6
  * 职责:
6
- * - 合并 message.delta / reasoning.delta / tool_call.delta
7
- * - 合并多次 response.auxiliary 补丁
8
- * - 生成 output / text / toolCalls
9
- * - 保持 output 顺序稳定
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.output.push(event.item);
91
- pushMessageText(state, event.item);
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.output.push(event.item);
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.output.push(event.item);
103
- state.toolCalls.push(event.item);
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.replayFromAdapter = event.response.replay;
108
- state.responseIdFromAdapter = event.response.id;
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
- // 合并 backend trace
131
- const backendFromResponse = state.backendFromAdapter;
276
+ const backendFromCompleted = state.backendFromAdapter;
132
277
  const backend: BackendTrace = {
133
- adapter: backendFromResponse?.adapter ?? state.backendInfo?.kind ?? ("unknown" as BackendTrace["adapter"]),
134
- isSyntheticStream: backendFromResponse?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
135
- requestId: backendFromResponse?.requestId ?? state.responseId,
136
- rawResponseId: backendFromResponse?.rawResponseId,
137
- metadataSources: backendFromResponse?.metadataSources,
138
- warnings: backendFromResponse?.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.responseIdFromAdapter ?? state.responseId,
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
- case "reasoning.started":
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.lastEventType !== "response.completed") {
207
- throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
370
+ if (!state.started) {
371
+ throw streamProtocolError("Stream must start with response.started event");
208
372
  }
209
-
210
- return buildResponse(state);
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 merged;
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
+ }
@@ -40,7 +40,11 @@ export class AIError extends Error {
40
40
 
41
41
  /** 请求构造失败 — 参数校验不通过。在进入 adapter 前同步抛错。 */
42
42
  export class AIRequestError extends AIError {
43
- constructor(message: string, code: ErrorCode) {
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
- AIResponse,
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(response: AIResponse): ResponseCompletedEvent {
84
- return { ...base(), type: "response.completed", response };
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, text: string): MessageDeltaEvent {
94
- return { ...base(), type: "message.delta", itemId, delta: { type: "text", text } };
103
+ messageDelta(itemId: string, delta: ContentBlock): MessageDeltaEvent {
104
+ return { ...base(), type: "message.delta", itemId, delta };
95
105
  },
96
106
 
97
- messageCompleted(item: MessageItem): MessageCompletedEvent {
98
- return { ...base(), type: "message.completed", item };
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(item: ReasoningItem): ReasoningCompletedEvent {
112
- return { ...base(), type: "reasoning.completed", item };
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(item: ToolCallItem): ToolCallCompletedEvent {
126
- return { ...base(), type: "tool_call.completed", item };
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
+ }
@@ -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,
@@ -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" || isNaN(request.temperature)) {
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" || isNaN(request.maxOutputTokens)) {
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
- if (!isRecord(request.include)) {
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
  }