@codehz/ai 0.4.5 → 0.7.0

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.
Files changed (46) hide show
  1. package/README.md +223 -77
  2. package/dist/index.d.mts +664 -525
  3. package/dist/index.mjs +3632 -2205
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +19 -8
  6. package/.github/workflows/publish.yml +0 -56
  7. package/.oxfmtrc.json +0 -12
  8. package/.oxlintrc.json +0 -34
  9. package/AGENTS.md +0 -37
  10. package/src/adapters/chat-completions.ts +0 -624
  11. package/src/adapters/index.ts +0 -44
  12. package/src/adapters/messages.ts +0 -635
  13. package/src/adapters/mock.ts +0 -934
  14. package/src/adapters/ollama.ts +0 -526
  15. package/src/adapters/responses.ts +0 -818
  16. package/src/core/aggregator.ts +0 -428
  17. package/src/core/client.ts +0 -36
  18. package/src/core/collect-stream.ts +0 -19
  19. package/src/core/errors.ts +0 -105
  20. package/src/core/event-factory.ts +0 -151
  21. package/src/core/index.ts +0 -18
  22. package/src/core/merge-auxiliary.ts +0 -22
  23. package/src/core/normalize.ts +0 -65
  24. package/src/core/validation.ts +0 -404
  25. package/src/helpers/adapter-auxiliary.ts +0 -155
  26. package/src/helpers/adapter-base.ts +0 -218
  27. package/src/helpers/adapter-security.ts +0 -126
  28. package/src/helpers/auxiliary-collector.ts +0 -166
  29. package/src/helpers/incremental-stream-parser.ts +0 -142
  30. package/src/helpers/index.ts +0 -87
  31. package/src/helpers/mapping.ts +0 -192
  32. package/src/helpers/provider-request-options.ts +0 -25
  33. package/src/helpers/provider-stream.ts +0 -147
  34. package/src/helpers/reasoning-level.ts +0 -85
  35. package/src/helpers/request-mapper.ts +0 -94
  36. package/src/helpers/synthetic-stream.ts +0 -188
  37. package/src/helpers/usage-mapping.ts +0 -110
  38. package/src/index.ts +0 -17
  39. package/src/types/adapter.ts +0 -42
  40. package/src/types/content.ts +0 -15
  41. package/src/types/events.ts +0 -138
  42. package/src/types/index.ts +0 -49
  43. package/src/types/items.ts +0 -57
  44. package/src/types/request.ts +0 -52
  45. package/src/types/response.ts +0 -68
  46. package/tsdown.config.ts +0 -10
@@ -1,151 +0,0 @@
1
- /**
2
- * 共享事件工厂
3
- *
4
- * 负责创建带有统一 sequence / timestamp / responseId / backend 的事件对象。
5
- * 每个 factory 实例管理一个单调递增的 sequence 计数器。
6
- */
7
-
8
- import type {
9
- ResponseStartedEvent,
10
- ResponseWarningEvent,
11
- ResponseAuxiliaryEvent,
12
- ResponseCompletedEvent,
13
- MessageStartedEvent,
14
- MessageDeltaEvent,
15
- MessageCompletedEvent,
16
- ReasoningStartedEvent,
17
- ReasoningDeltaEvent,
18
- ReasoningCompletedEvent,
19
- ToolCallStartedEvent,
20
- ToolCallDeltaEvent,
21
- ToolCallCompletedEvent,
22
- ContentBlock,
23
- Usage,
24
- BillingInfo,
25
- AuxiliaryInfo,
26
- ReplayItem,
27
- StopReason,
28
- BackendTrace,
29
- OpaqueItem,
30
- ReasoningItem,
31
- } from "../types/index.js";
32
-
33
- export type EventFactoryBackend = {
34
- kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
35
- isSynthetic: boolean;
36
- };
37
-
38
- export type EventFactoryState = {
39
- responseId: string;
40
- backend: EventFactoryBackend;
41
- };
42
-
43
- function timestamp(): string {
44
- return new Date().toISOString();
45
- }
46
-
47
- export function createEventFactory(state: EventFactoryState) {
48
- let seq = 0;
49
- const warnings: string[] = [];
50
-
51
- function next(): number {
52
- return seq++;
53
- }
54
-
55
- function base(): Pick<ResponseStartedEvent, "responseId" | "sequence" | "timestamp" | "backend"> {
56
- return {
57
- responseId: state.responseId,
58
- sequence: next(),
59
- timestamp: timestamp(),
60
- backend: { ...state.backend },
61
- };
62
- }
63
-
64
- return {
65
- // ── 响应级事件 ──────────────────────────────────────────
66
-
67
- responseStarted(model: string): ResponseStartedEvent {
68
- return { ...base(), type: "response.started", model };
69
- },
70
-
71
- responseWarning(message: string, code?: string): ResponseWarningEvent {
72
- warnings.push(message);
73
- return { ...base(), type: "response.warning", message, code };
74
- },
75
-
76
- responseAuxiliary(data: {
77
- usage?: Usage;
78
- billing?: BillingInfo;
79
- auxiliary?: Partial<AuxiliaryInfo>;
80
- }): ResponseAuxiliaryEvent {
81
- return { ...base(), type: "response.auxiliary", ...data };
82
- },
83
-
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 };
95
- },
96
-
97
- // ── 消息流事件 ──────────────────────────────────────────
98
-
99
- messageStarted(id: string): MessageStartedEvent {
100
- return { ...base(), type: "message.started", item: { id, role: "assistant" } };
101
- },
102
-
103
- messageDelta(itemId: string, delta: ContentBlock): MessageDeltaEvent {
104
- return { ...base(), type: "message.delta", itemId, delta };
105
- },
106
-
107
- messageCompleted(itemId: string): MessageCompletedEvent {
108
- return { ...base(), type: "message.completed", itemId };
109
- },
110
-
111
- // ── 思维链流事件 ────────────────────────────────────────
112
-
113
- reasoningStarted(id: string, visibility: ReasoningItem["visibility"]): ReasoningStartedEvent {
114
- return { ...base(), type: "reasoning.started", item: { id, visibility } };
115
- },
116
-
117
- reasoningDelta(itemId: string, delta: ContentBlock): ReasoningDeltaEvent {
118
- return { ...base(), type: "reasoning.delta", itemId, delta };
119
- },
120
-
121
- reasoningCompleted(itemId: string): ReasoningCompletedEvent {
122
- return { ...base(), type: "reasoning.completed", itemId };
123
- },
124
-
125
- // ── 工具调用流事件 ──────────────────────────────────────
126
-
127
- toolCallStarted(id: string, name: string): ToolCallStartedEvent {
128
- return { ...base(), type: "tool_call.started", item: { id, name } };
129
- },
130
-
131
- toolCallDelta(itemId: string, delta: { argumentsText?: string }): ToolCallDeltaEvent {
132
- return { ...base(), type: "tool_call.delta", itemId, delta };
133
- },
134
-
135
- toolCallCompleted(itemId: string): ToolCallCompletedEvent {
136
- return { ...base(), type: "tool_call.completed", itemId };
137
- },
138
-
139
- /** 返回当前已发出的 sequence 计数(用于断言) */
140
- get sequence(): number {
141
- return seq;
142
- },
143
-
144
- /** 返回当前已记录的 warning 副本。 */
145
- get warnings(): string[] {
146
- return [...warnings];
147
- },
148
- };
149
- }
150
-
151
- export type EventFactory = ReturnType<typeof createEventFactory>;
package/src/core/index.ts DELETED
@@ -1,18 +0,0 @@
1
- /**
2
- * 核心运行时
3
- *
4
- * 模块边界:客户端入口、请求归一化、事件工厂、流聚合器。
5
- * 不依赖具体 adapter 实现。
6
- */
7
-
8
- export { createAIClient } from "./client.js";
9
- export type { AIClient } from "./client.js";
10
- export { normalizeRequest } from "./normalize.js";
11
- export type { NormalizeOptions } from "./normalize.js";
12
- export { validateRequest, assertValidRequest } from "./validation.js";
13
- export type { ValidationIssue } from "./validation.js";
14
- export { AIError, AIRequestError, AIProviderError, AIStreamError, AIMappingError, WarningCode } from "./errors.js";
15
- export { createEventFactory } from "./event-factory.js";
16
- export type { EventFactory, EventFactoryState, EventFactoryBackend } from "./event-factory.js";
17
- export { aggregateEvents } from "./aggregator.js";
18
- export { collectStream } from "./collect-stream.js";
@@ -1,22 +0,0 @@
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
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * 请求归一化
3
- *
4
- * 将 AIRequest + client 配置归一化为 NormalizedRequest,
5
- * 包括默认值合并、requestId 生成、include 默认值填充。
6
- */
7
-
8
- import type { AIRequest, NormalizedRequest } from "../types/index.js";
9
- import { assertValidRequest, validateInclude } from "./validation.js";
10
- import { AIRequestError } from "./errors.js";
11
-
12
- export type NormalizeOptions = {
13
- model: string;
14
- defaults?: Partial<AIRequest>;
15
- };
16
-
17
- const DEFAULT_INCLUDE = {
18
- usage: "best_effort" as const,
19
- billing: "best_effort" as const,
20
- providerMetadata: "best_effort" as const,
21
- };
22
-
23
- /**
24
- * 归一化请求:
25
- * 1. 合并 defaults
26
- * 2. 填充 include 默认值
27
- * 3. 生成 requestId
28
- * 4. 校验请求合法性
29
- */
30
- export function normalizeRequest(request: AIRequest, options: NormalizeOptions): NormalizedRequest {
31
- const { model, defaults } = options;
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
-
46
- // 合并 defaults(浅合并,input/tools 由 request 完全覆盖)
47
- const merged: AIRequest = {
48
- ...defaults,
49
- ...request,
50
- include: {
51
- ...DEFAULT_INCLUDE,
52
- ...defaults?.include,
53
- ...request.include,
54
- },
55
- };
56
-
57
- // 校验
58
- assertValidRequest(merged);
59
-
60
- return {
61
- ...merged,
62
- model,
63
- requestId: crypto.randomUUID(),
64
- };
65
- }
@@ -1,404 +0,0 @@
1
- /**
2
- * 请求校验
3
- *
4
- * 在请求进入 adapter 前对参数合法性做基础检查。
5
- * 校验失败时抛 AIRequestError。
6
- */
7
-
8
- import type { AIRequest } from "../types/index.js";
9
- import { AIRequestError } from "./errors.js";
10
-
11
- export type ValidationIssue = {
12
- field: string;
13
- code: string;
14
- message: string;
15
- };
16
-
17
- const MESSAGE_ROLES = new Set(["user", "assistant"]);
18
- const REASONING_VISIBILITIES = new Set(["full", "summary", "redacted", "opaque"]);
19
- const TOOL_RESULT_OUTCOMES = new Set(["success", "error", "rejected"]);
20
- const INCLUDE_MODES = new Set(["off", "best_effort"]);
21
- const REASONING_LEVELS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]);
22
-
23
- function isRecord(value: unknown): value is Record<string, unknown> {
24
- return typeof value === "object" && value !== null;
25
- }
26
-
27
- function pushIssue(issues: ValidationIssue[], field: string, code: string, message: string): void {
28
- issues.push({ field, code, message });
29
- }
30
-
31
- function validateContentBlock(block: unknown, field: string, issues: ValidationIssue[]): void {
32
- if (!isRecord(block) || typeof block.type !== "string") {
33
- pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field} must be a valid ContentBlock`);
34
- return;
35
- }
36
-
37
- switch (block.type) {
38
- case "text":
39
- if (typeof block.text !== "string") {
40
- pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.text must be a string`);
41
- }
42
- return;
43
- case "json":
44
- if (!("json" in block)) {
45
- pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.json must be present`);
46
- }
47
- return;
48
- case "image":
49
- if (typeof block.imageUrl !== "string" || block.imageUrl.length === 0) {
50
- pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.imageUrl must be a non-empty string`);
51
- }
52
- return;
53
- case "binary_ref":
54
- if (typeof block.ref !== "string" || block.ref.length === 0) {
55
- pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.ref must be a non-empty string`);
56
- }
57
- return;
58
- case "opaque":
59
- if (!("payload" in block)) {
60
- pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.payload must be present`);
61
- }
62
- return;
63
- default:
64
- pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.type "${block.type}" is not supported`);
65
- }
66
- }
67
-
68
- function validateContentArray(content: unknown, field: string, issues: ValidationIssue[], code: string): void {
69
- if (!Array.isArray(content)) {
70
- pushIssue(issues, field, code, `${field} must be a ContentBlock[]`);
71
- return;
72
- }
73
-
74
- for (let i = 0; i < content.length; i++) {
75
- validateContentBlock(content[i], `${field}[${i}]`, issues);
76
- }
77
- }
78
-
79
- function validateInstructionArray(content: unknown, field: string, issues: ValidationIssue[]): void {
80
- if (!Array.isArray(content)) {
81
- pushIssue(issues, field, "INSTRUCTIONS_INVALID", `${field} must be an InstructionBlock[]`);
82
- return;
83
- }
84
-
85
- for (let i = 0; i < content.length; i++) {
86
- const block = content[i];
87
- const blockField = `${field}[${i}]`;
88
- validateContentBlock(block, blockField, issues);
89
-
90
- if (!isRecord(block) || typeof block.type !== "string") continue;
91
- if (block.type !== "text" && block.type !== "json") {
92
- pushIssue(issues, blockField, "INSTRUCTIONS_INVALID", `${blockField} only supports text/json blocks`);
93
- }
94
- }
95
- }
96
-
97
- function validateInputItem(item: unknown, field: string, issues: ValidationIssue[]): void {
98
- if (!isRecord(item)) {
99
- pushIssue(issues, field, "INPUT_INVALID_ITEM", `${field} must be a valid InputItem`);
100
- return;
101
- }
102
-
103
- if (typeof item.type !== "string") {
104
- pushIssue(issues, field, "INPUT_ITEM_UNKNOWN_TYPE", `${field}.type must be a supported InputItem type`);
105
- return;
106
- }
107
-
108
- switch (item.type) {
109
- case "message":
110
- if (typeof item.role !== "string" || !MESSAGE_ROLES.has(item.role)) {
111
- pushIssue(issues, `${field}.role`, "MESSAGE_ROLE_INVALID", `${field}.role must be a valid message role`);
112
- }
113
- validateContentArray(item.content, `${field}.content`, issues, "MESSAGE_CONTENT_INVALID");
114
- return;
115
- case "reasoning":
116
- if (typeof item.visibility !== "string" || !REASONING_VISIBILITIES.has(item.visibility)) {
117
- pushIssue(
118
- issues,
119
- `${field}.visibility`,
120
- "REASONING_VISIBILITY_INVALID",
121
- `${field}.visibility must be a valid reasoning visibility`,
122
- );
123
- }
124
- validateContentArray(item.content, `${field}.content`, issues, "REASONING_CONTENT_INVALID");
125
- return;
126
- case "tool_call":
127
- if (typeof item.id !== "string" || item.id.length === 0) {
128
- pushIssue(issues, `${field}.id`, "TOOL_CALL_ID_INVALID", `${field}.id must be a non-empty string`);
129
- }
130
- if (typeof item.name !== "string" || item.name.length === 0) {
131
- pushIssue(issues, `${field}.name`, "TOOL_CALL_NAME_INVALID", `${field}.name must be a non-empty string`);
132
- }
133
- if (typeof item.argumentsText !== "string") {
134
- pushIssue(
135
- issues,
136
- `${field}.argumentsText`,
137
- "TOOL_CALL_ARGUMENTS_INVALID",
138
- `${field}.argumentsText must be a string`,
139
- );
140
- } else {
141
- try {
142
- const parsed: unknown = JSON.parse(item.argumentsText);
143
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
144
- pushIssue(
145
- issues,
146
- `${field}.argumentsText`,
147
- "TOOL_CALL_ARGUMENTS_INVALID",
148
- `${field}.argumentsText must encode a JSON object`,
149
- );
150
- }
151
- } catch {
152
- pushIssue(
153
- issues,
154
- `${field}.argumentsText`,
155
- "TOOL_CALL_ARGUMENTS_INVALID",
156
- `${field}.argumentsText must encode a JSON object`,
157
- );
158
- }
159
- }
160
- return;
161
- case "tool_result":
162
- if (typeof item.callId !== "string" || item.callId.length === 0) {
163
- pushIssue(
164
- issues,
165
- `${field}.callId`,
166
- "TOOL_RESULT_CALL_ID_INVALID",
167
- `${field}.callId must be a non-empty string`,
168
- );
169
- }
170
- if (typeof item.toolName !== "string" || item.toolName.length === 0) {
171
- pushIssue(
172
- issues,
173
- `${field}.toolName`,
174
- "TOOL_RESULT_NAME_INVALID",
175
- `${field}.toolName must be a non-empty string`,
176
- );
177
- }
178
- if (typeof item.outcome !== "string" || !TOOL_RESULT_OUTCOMES.has(item.outcome)) {
179
- pushIssue(
180
- issues,
181
- `${field}.outcome`,
182
- "TOOL_RESULT_OUTCOME_INVALID",
183
- `${field}.outcome must be success, error, or rejected`,
184
- );
185
- }
186
- validateContentArray(item.content, `${field}.content`, issues, "TOOL_RESULT_CONTENT_INVALID");
187
- return;
188
- case "opaque":
189
- if (typeof item.source !== "string" || item.source.length === 0) {
190
- pushIssue(issues, `${field}.source`, "OPAQUE_SOURCE_INVALID", `${field}.source must be a non-empty string`);
191
- }
192
- if (typeof item.purpose !== "string" || item.purpose.length === 0) {
193
- pushIssue(issues, `${field}.purpose`, "OPAQUE_PURPOSE_INVALID", `${field}.purpose must be a non-empty string`);
194
- }
195
- return;
196
- default:
197
- pushIssue(issues, `${field}.type`, "INPUT_ITEM_UNKNOWN_TYPE", `${field}.type "${item.type}" is not supported`);
198
- }
199
- }
200
-
201
- function validateTools(tools: unknown, issues: ValidationIssue[]): void {
202
- if (tools === undefined) return;
203
- if (!Array.isArray(tools)) {
204
- pushIssue(issues, "tools", "TOOLS_INVALID", "tools must be an array");
205
- return;
206
- }
207
-
208
- const seenNames = new Set<string>();
209
- for (let i = 0; i < tools.length; i++) {
210
- const tool = tools[i];
211
- const field = `tools[${i}]`;
212
- if (!isRecord(tool)) {
213
- pushIssue(issues, field, "TOOL_INVALID", `${field} must be a valid ToolDefinition`);
214
- continue;
215
- }
216
-
217
- if (typeof tool.name !== "string" || tool.name.length === 0) {
218
- pushIssue(issues, `${field}.name`, "TOOL_NAME_INVALID", `${field}.name must be a non-empty string`);
219
- } else {
220
- if (seenNames.has(tool.name)) {
221
- pushIssue(issues, `${field}.name`, "TOOLS_DUPLICATE_NAME", `tool name "${tool.name}" is duplicated`);
222
- }
223
- seenNames.add(tool.name);
224
- }
225
-
226
- if (tool.description !== undefined && typeof tool.description !== "string") {
227
- pushIssue(issues, `${field}.description`, "TOOL_DESCRIPTION_INVALID", `${field}.description must be a string`);
228
- }
229
-
230
- if (!isRecord(tool.inputSchema)) {
231
- pushIssue(issues, `${field}.inputSchema`, "TOOL_INPUT_SCHEMA_INVALID", `${field}.inputSchema must be an object`);
232
- }
233
- }
234
- }
235
-
236
- function validateToolChoice(toolChoice: unknown, issues: ValidationIssue[]): void {
237
- if (toolChoice === undefined) return;
238
- if (toolChoice === "auto" || toolChoice === "none") return;
239
- if (
240
- !isRecord(toolChoice) ||
241
- toolChoice.type !== "tool" ||
242
- typeof toolChoice.name !== "string" ||
243
- toolChoice.name.length === 0
244
- ) {
245
- pushIssue(issues, "toolChoice", "TOOL_CHOICE_INVALID", 'toolChoice must be auto, none, or { type: "tool", name }');
246
- }
247
- }
248
-
249
- /** Validate include settings, appending issues to the given array. */
250
- export function validateInclude(include: unknown, issues: ValidationIssue[]): void {
251
- if (!isRecord(include)) {
252
- pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
253
- return;
254
- }
255
- if (include.usage !== undefined && (typeof include.usage !== "string" || !INCLUDE_MODES.has(include.usage))) {
256
- pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
257
- }
258
- if (include.billing !== undefined && (typeof include.billing !== "string" || !INCLUDE_MODES.has(include.billing))) {
259
- pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
260
- }
261
- if (
262
- include.providerMetadata !== undefined &&
263
- (typeof include.providerMetadata !== "string" || !INCLUDE_MODES.has(include.providerMetadata))
264
- ) {
265
- pushIssue(
266
- issues,
267
- "include.providerMetadata",
268
- "INCLUDE_PROVIDER_METADATA_INVALID",
269
- "include.providerMetadata must be off or best_effort",
270
- );
271
- }
272
- }
273
-
274
- /**
275
- * 校验 AIRequest,返回校验问题列表。
276
- * 空数组表示无问题。
277
- */
278
- export function validateRequest(request: AIRequest): ValidationIssue[] {
279
- const issues: ValidationIssue[] = [];
280
-
281
- if (request.instructions !== undefined) {
282
- if (typeof request.instructions === "string") {
283
- // no-op
284
- } else if (Array.isArray(request.instructions)) {
285
- validateInstructionArray(request.instructions, "instructions", issues);
286
- } else {
287
- pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or InstructionBlock[]");
288
- }
289
- }
290
-
291
- // input 非空约束
292
- if (!Array.isArray(request.input) || request.input.length === 0) {
293
- pushIssue(issues, "input", "INPUT_EMPTY", "input must be a non-empty array");
294
- }
295
-
296
- // input 元素类型检查
297
- if (Array.isArray(request.input)) {
298
- for (let i = 0; i < request.input.length; i++) {
299
- validateInputItem(request.input[i], `input[${i}]`, issues);
300
- }
301
- }
302
-
303
- // temperature 范围
304
- if (request.temperature !== undefined) {
305
- if (typeof request.temperature !== "number" || !Number.isFinite(request.temperature)) {
306
- issues.push({
307
- field: "temperature",
308
- code: "TEMPERATURE_NOT_NUMBER",
309
- message: "temperature must be a number",
310
- });
311
- } else if (request.temperature < 0 || request.temperature > 2) {
312
- issues.push({
313
- field: "temperature",
314
- code: "TEMPERATURE_OUT_OF_RANGE",
315
- message: "temperature must be between 0 and 2",
316
- });
317
- }
318
- }
319
-
320
- // maxOutputTokens 合法性
321
- if (request.maxOutputTokens !== undefined) {
322
- if (typeof request.maxOutputTokens !== "number" || !Number.isFinite(request.maxOutputTokens)) {
323
- issues.push({
324
- field: "maxOutputTokens",
325
- code: "MAX_OUTPUT_TOKENS_NOT_NUMBER",
326
- message: "maxOutputTokens must be a number",
327
- });
328
- } else if (!Number.isInteger(request.maxOutputTokens) || request.maxOutputTokens < 1) {
329
- issues.push({
330
- field: "maxOutputTokens",
331
- code: "MAX_OUTPUT_TOKENS_INVALID",
332
- message: "maxOutputTokens must be a positive integer",
333
- });
334
- }
335
- }
336
-
337
- // reasoningLevel 枚举
338
- if (request.reasoningLevel !== undefined) {
339
- if (typeof request.reasoningLevel !== "string" || !REASONING_LEVELS.has(request.reasoningLevel)) {
340
- pushIssue(
341
- issues,
342
- "reasoningLevel",
343
- "REASONING_LEVEL_INVALID",
344
- 'reasoningLevel must be one of: none, minimal, low, medium, high, xhigh',
345
- );
346
- }
347
- }
348
-
349
- if (request.include !== undefined) {
350
- validateInclude(request.include, issues);
351
- }
352
-
353
- if (request.metadata !== undefined) {
354
- if (!isRecord(request.metadata)) {
355
- pushIssue(issues, "metadata", "METADATA_INVALID", "metadata must be an object");
356
- } else {
357
- for (const [key, value] of Object.entries(request.metadata)) {
358
- if (typeof value !== "string") {
359
- pushIssue(issues, `metadata.${key}`, "METADATA_VALUE_INVALID", `metadata.${key} must be a string`);
360
- }
361
- }
362
- }
363
- }
364
-
365
- validateTools(request.tools, issues);
366
- validateToolChoice(request.toolChoice, issues);
367
-
368
- // toolChoice 与 tools 的一致性
369
- if (
370
- request.toolChoice &&
371
- typeof request.toolChoice === "object" &&
372
- "type" in request.toolChoice &&
373
- request.toolChoice.type === "tool"
374
- ) {
375
- const chosenName = request.toolChoice.name;
376
- if (!request.tools || request.tools.length === 0) {
377
- issues.push({
378
- field: "toolChoice",
379
- code: "TOOL_CHOICE_NO_TOOLS",
380
- message: `toolChoice specifies tool "${chosenName}" but no tools are defined`,
381
- });
382
- } else if (!request.tools.some((t) => t.name === chosenName)) {
383
- issues.push({
384
- field: "toolChoice",
385
- code: "TOOL_CHOICE_UNKNOWN_TOOL",
386
- message: `toolChoice specifies tool "${chosenName}" which is not in tools array`,
387
- });
388
- }
389
- }
390
-
391
- return issues;
392
- }
393
-
394
- /**
395
- * 校验请求并抛出首个问题。
396
- * 适用于客户端入口的快速失败检查。
397
- */
398
- export function assertValidRequest(request: AIRequest): void {
399
- const issues = validateRequest(request);
400
- const first = issues[0];
401
- if (first) {
402
- throw new AIRequestError(first.message, first.code, issues);
403
- }
404
- }