@codehz/ai 0.2.0 → 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.
@@ -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,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.output.push(event.item);
91
- pushMessageText(state, event.item);
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.output.push(event.item);
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.output.push(event.item);
103
- state.toolCalls.push(event.item);
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.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);
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
- // 合并 backend trace
131
- const backendFromResponse = state.backendFromAdapter;
269
+ const backendFromCompleted = state.backendFromAdapter;
132
270
  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,
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.responseIdFromAdapter ?? state.responseId,
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
- case "reasoning.started":
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.lastEventType !== "response.completed") {
207
- throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
356
+ if (!state.started) {
357
+ throw streamProtocolError("Stream must start with response.started event");
208
358
  }
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
- };
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 merged;
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
+ }
@@ -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
  }