@codehz/ai 0.1.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.
@@ -0,0 +1,349 @@
1
+ /**
2
+ * 流聚合器
3
+ *
4
+ * 将 AIStreamEvent 序列聚合为统一的 AIResponse。
5
+ * 职责:
6
+ * - 合并 message.delta / reasoning.delta / tool_call.delta
7
+ * - 合并多次 response.auxiliary 补丁
8
+ * - 生成 output / text / toolCalls
9
+ * - 保持 output 顺序稳定
10
+ *
11
+ * 约束:
12
+ * - replay 由 adapter 显式提供,聚合器不猜测
13
+ * - 不伪造 reasoning
14
+ * - 不解释 opaque payload
15
+ */
16
+
17
+ import type {
18
+ AIStreamEvent,
19
+ AIResponse,
20
+ ContentBlock,
21
+ MessageItem,
22
+ ReasoningItem,
23
+ ToolCallItem,
24
+ OutputItem,
25
+ Usage,
26
+ BillingInfo,
27
+ AuxiliaryInfo,
28
+ BackendTrace,
29
+ StopReason,
30
+ } from "../types/index.js";
31
+
32
+ // ── 内部 pending item 状态 ────────────────────────────────────
33
+
34
+ interface PendingMessage {
35
+ role: "assistant";
36
+ texts: string[];
37
+ }
38
+
39
+ interface PendingReasoning {
40
+ visibility: ReasoningItem["visibility"];
41
+ blocks: ContentBlock[];
42
+ }
43
+
44
+ interface PendingToolCall {
45
+ name: string;
46
+ argsParts: string[];
47
+ }
48
+
49
+ // ── 聚合器状态 ────────────────────────────────────────────────
50
+
51
+ interface AggregatorState {
52
+ responseId?: string;
53
+ model?: string;
54
+ backendInfo?: { kind: BackendTrace["adapter"]; isSynthetic: boolean };
55
+
56
+ usage?: Usage;
57
+ billing?: BillingInfo;
58
+ auxiliary: AuxiliaryInfo;
59
+ warnings: string[];
60
+
61
+ pendingMessages: Map<string, PendingMessage>;
62
+ pendingReasonings: Map<string, PendingReasoning>;
63
+ pendingToolCalls: Map<string, PendingToolCall>;
64
+
65
+ /** 已完成 item 的 id 列表,保持输出顺序 */
66
+ outputOrder: string[];
67
+ completedMessages: Map<string, MessageItem>;
68
+ completedReasonings: Map<string, ReasoningItem>;
69
+ completedToolCalls: Map<string, ToolCallItem>;
70
+
71
+ /** adapter 在 response.completed 中提供的 replay */
72
+ replayFromAdapter?: import("../types/index.js").ReplayItem[];
73
+ responseIdFromAdapter?: string;
74
+ stopReasonFromAdapter?: StopReason;
75
+ backendFromAdapter?: BackendTrace;
76
+ }
77
+
78
+ function createInitialState(): AggregatorState {
79
+ return {
80
+ pendingMessages: new Map(),
81
+ pendingReasonings: new Map(),
82
+ pendingToolCalls: new Map(),
83
+ outputOrder: [],
84
+ completedMessages: new Map(),
85
+ completedReasonings: new Map(),
86
+ completedToolCalls: new Map(),
87
+ auxiliary: {},
88
+ warnings: [],
89
+ };
90
+ }
91
+
92
+ // ── Event handlers ────────────────────────────────────────────
93
+
94
+ function handleResponseStarted(state: AggregatorState, event: AIStreamEvent & { type: "response.started" }): void {
95
+ state.responseId = event.responseId;
96
+ state.model = event.model;
97
+ state.backendInfo = event.backend;
98
+ }
99
+
100
+ function handleResponseWarning(state: AggregatorState, event: AIStreamEvent & { type: "response.warning" }): void {
101
+ pushWarnings(state, [event.message]);
102
+ }
103
+
104
+ function handleResponseAuxiliary(state: AggregatorState, event: AIStreamEvent & { type: "response.auxiliary" }): void {
105
+ if (event.usage) {
106
+ state.usage = { ...state.usage, ...event.usage };
107
+ }
108
+ if (event.billing) {
109
+ state.billing = { ...state.billing, ...event.billing };
110
+ }
111
+ if (event.auxiliary) {
112
+ state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary);
113
+ }
114
+ }
115
+
116
+ function handleMessageStarted(state: AggregatorState, event: AIStreamEvent & { type: "message.started" }): void {
117
+ state.pendingMessages.set(event.item.id, {
118
+ role: event.item.role,
119
+ texts: [],
120
+ });
121
+ }
122
+
123
+ function handleMessageDelta(state: AggregatorState, event: AIStreamEvent & { type: "message.delta" }): void {
124
+ const pending = state.pendingMessages.get(event.itemId);
125
+ if (pending) {
126
+ pending.texts.push(event.delta.text);
127
+ }
128
+ }
129
+
130
+ function handleMessageCompleted(state: AggregatorState, event: AIStreamEvent & { type: "message.completed" }): void {
131
+ const item = event.item;
132
+ const itemId = item.id ?? `msg-${state.outputOrder.length}`;
133
+ state.completedMessages.set(itemId, item);
134
+ state.outputOrder.push(itemId);
135
+ state.pendingMessages.delete(itemId);
136
+ }
137
+
138
+ function handleReasoningStarted(state: AggregatorState, event: AIStreamEvent & { type: "reasoning.started" }): void {
139
+ state.pendingReasonings.set(event.item.id, {
140
+ visibility: event.item.visibility,
141
+ blocks: [],
142
+ });
143
+ }
144
+
145
+ function handleReasoningDelta(state: AggregatorState, event: AIStreamEvent & { type: "reasoning.delta" }): void {
146
+ const pending = state.pendingReasonings.get(event.itemId);
147
+ if (pending) {
148
+ pending.blocks.push(event.delta);
149
+ }
150
+ }
151
+
152
+ function handleReasoningCompleted(
153
+ state: AggregatorState,
154
+ event: AIStreamEvent & { type: "reasoning.completed" },
155
+ ): void {
156
+ const item = event.item;
157
+ const stableId = item.id ?? `reason-${state.outputOrder.length}-${Date.now()}`;
158
+ state.completedReasonings.set(stableId, item);
159
+ state.outputOrder.push(stableId);
160
+ state.pendingReasonings.delete(item.id ?? "");
161
+ }
162
+
163
+ function handleToolCallStarted(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.started" }): void {
164
+ state.pendingToolCalls.set(event.item.id, {
165
+ name: event.item.name,
166
+ argsParts: [],
167
+ });
168
+ }
169
+
170
+ function handleToolCallDelta(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.delta" }): void {
171
+ const pending = state.pendingToolCalls.get(event.itemId);
172
+ if (pending && event.delta.argumentsText) {
173
+ pending.argsParts.push(event.delta.argumentsText);
174
+ }
175
+ }
176
+
177
+ function handleToolCallCompleted(state: AggregatorState, event: AIStreamEvent & { type: "tool_call.completed" }): void {
178
+ const item = event.item;
179
+ state.completedToolCalls.set(item.id, item);
180
+ state.outputOrder.push(item.id);
181
+ state.pendingToolCalls.delete(item.id);
182
+ }
183
+
184
+ function handleResponseCompleted(state: AggregatorState, event: AIStreamEvent & { type: "response.completed" }): void {
185
+ state.replayFromAdapter = event.response.replay;
186
+ state.responseIdFromAdapter = event.response.id;
187
+ state.stopReasonFromAdapter = event.response.stopReason;
188
+ state.backendFromAdapter = event.response.backend;
189
+
190
+ // 从 response.completed 中提取 usage/billing(适配器可能未发 auxiliary 事件)
191
+ if (event.response.usage) {
192
+ state.usage = { ...state.usage, ...event.response.usage };
193
+ }
194
+ if (event.response.billing) {
195
+ state.billing = { ...state.billing, ...event.response.billing };
196
+ }
197
+ if (event.response.auxiliary) {
198
+ state.auxiliary = mergeAuxiliary(state.auxiliary, event.response.auxiliary);
199
+ }
200
+ if (event.response.warnings) {
201
+ pushWarnings(state, event.response.warnings);
202
+ }
203
+ }
204
+
205
+ // ── 从聚合状态构建最终 AIResponse ─────────────────────────────
206
+
207
+ function buildResponse(state: AggregatorState): AIResponse {
208
+ // 按 outputOrder 组装 output
209
+ const output: OutputItem[] = [];
210
+ for (const id of state.outputOrder) {
211
+ const msg = state.completedMessages.get(id);
212
+ if (msg) {
213
+ output.push(msg);
214
+ continue;
215
+ }
216
+ const reason = state.completedReasonings.get(id);
217
+ if (reason) {
218
+ output.push(reason);
219
+ continue;
220
+ }
221
+ const tc = state.completedToolCalls.get(id);
222
+ if (tc) {
223
+ output.push(tc);
224
+ continue;
225
+ }
226
+ }
227
+
228
+ // 汇总 text
229
+ const text = output
230
+ .filter((item): item is MessageItem => item.type === "message")
231
+ .flatMap((m) => m.content)
232
+ .filter((b): b is ContentBlock & { type: "text" } => b.type === "text")
233
+ .map((b) => b.text)
234
+ .join("");
235
+
236
+ // toolCalls
237
+ const toolCalls = output.filter((item): item is ToolCallItem => item.type === "tool_call");
238
+
239
+ // 合并 backend trace
240
+ const backendFromResponse = state.backendFromAdapter;
241
+ const backend: BackendTrace = {
242
+ adapter: backendFromResponse?.adapter ?? state.backendInfo?.kind ?? ("unknown" as BackendTrace["adapter"]),
243
+ isSyntheticStream: backendFromResponse?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
244
+ requestId: backendFromResponse?.requestId ?? state.responseId,
245
+ rawResponseId: backendFromResponse?.rawResponseId,
246
+ metadataSources: backendFromResponse?.metadataSources,
247
+ warnings: backendFromResponse?.warnings,
248
+ };
249
+
250
+ return {
251
+ id: state.responseIdFromAdapter ?? state.responseId,
252
+ output,
253
+ replay: state.replayFromAdapter ?? [],
254
+ text,
255
+ toolCalls,
256
+ stopReason: state.stopReasonFromAdapter,
257
+ usage: state.usage,
258
+ billing: state.billing,
259
+ auxiliary: state.auxiliary,
260
+ warnings: state.warnings.length > 0 ? state.warnings : undefined,
261
+ backend,
262
+ };
263
+ }
264
+
265
+ // ── 公开 API ──────────────────────────────────────────────────
266
+
267
+ /**
268
+ * 将事件数组聚合为 AIResponse。
269
+ * 适用于测试和离线处理场景。
270
+ */
271
+ export function aggregateEvents(events: AIStreamEvent[]): AIResponse {
272
+ const state = createInitialState();
273
+
274
+ for (const event of events) {
275
+ switch (event.type) {
276
+ case "response.started":
277
+ handleResponseStarted(state, event);
278
+ break;
279
+ case "response.warning":
280
+ handleResponseWarning(state, event);
281
+ break;
282
+ case "response.auxiliary":
283
+ handleResponseAuxiliary(state, event);
284
+ break;
285
+ case "message.started":
286
+ handleMessageStarted(state, event);
287
+ break;
288
+ case "message.delta":
289
+ handleMessageDelta(state, event);
290
+ break;
291
+ case "message.completed":
292
+ handleMessageCompleted(state, event);
293
+ break;
294
+ case "reasoning.started":
295
+ handleReasoningStarted(state, event);
296
+ break;
297
+ case "reasoning.delta":
298
+ handleReasoningDelta(state, event);
299
+ break;
300
+ case "reasoning.completed":
301
+ handleReasoningCompleted(state, event);
302
+ break;
303
+ case "tool_call.started":
304
+ handleToolCallStarted(state, event);
305
+ break;
306
+ case "tool_call.delta":
307
+ handleToolCallDelta(state, event);
308
+ break;
309
+ case "tool_call.completed":
310
+ handleToolCallCompleted(state, event);
311
+ break;
312
+ case "response.completed":
313
+ handleResponseCompleted(state, event);
314
+ break;
315
+ }
316
+ }
317
+
318
+ // 用 response.completed 中的 response 做最终构建
319
+ const lastEvent = events[events.length - 1];
320
+ if (!lastEvent || lastEvent.type !== "response.completed") {
321
+ throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
322
+ }
323
+
324
+ return buildResponse(state);
325
+ }
326
+
327
+ function mergeAuxiliary(base: AuxiliaryInfo, patch: Partial<AuxiliaryInfo>): AuxiliaryInfo {
328
+ const merged: AuxiliaryInfo = {
329
+ ...base,
330
+ ...patch,
331
+ };
332
+
333
+ if (base.providerMetadata || patch.providerMetadata) {
334
+ merged.providerMetadata = {
335
+ ...(base.providerMetadata ?? {}),
336
+ ...(patch.providerMetadata ?? {}),
337
+ };
338
+ }
339
+
340
+ return merged;
341
+ }
342
+
343
+ function pushWarnings(state: AggregatorState, warnings: readonly string[]): void {
344
+ for (const warning of warnings) {
345
+ if (!state.warnings.includes(warning)) {
346
+ state.warnings.push(warning);
347
+ }
348
+ }
349
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * AI 客户端入口
3
+ *
4
+ * 打通 createAIClient() 到 adapter 调用之间的公共入口。
5
+ */
6
+
7
+ import type { AIRequest, AIStreamEvent, AIClient, CreateAIClientOptions } from "../types/index.js";
8
+ import { normalizeRequest } from "./normalize.js";
9
+
10
+ export function createAIClient(options: CreateAIClientOptions): AIClient {
11
+ const { adapter, model, defaults } = options;
12
+
13
+ const client: AIClient = {
14
+ stream(request: AIRequest): AsyncIterable<AIStreamEvent> {
15
+ const normalized = normalizeRequest(request, { model, defaults });
16
+ return adapter.stream(normalized);
17
+ },
18
+ };
19
+
20
+ return client;
21
+ }
22
+
23
+ export type { AIClient, CreateAIClientOptions } from "../types/index.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * collectStream — 流收集 helper
3
+ *
4
+ * 将 AsyncIterable<AIStreamEvent> 消费完毕并聚合力 AIResponse。
5
+ * 适用于不需要逐事件处理的调用方。
6
+ */
7
+
8
+ import type { AIStreamEvent, AIResponse } from "../types/index.js";
9
+ import { aggregateEvents } from "./aggregator.js";
10
+
11
+ export async function collectStream(stream: AsyncIterable<AIStreamEvent>): Promise<AIResponse> {
12
+ const events: AIStreamEvent[] = [];
13
+
14
+ for await (const event of stream) {
15
+ events.push(event);
16
+ }
17
+
18
+ return aggregateEvents(events);
19
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 公共错误模型
3
+ *
4
+ * 把失败、降级、断流三类情况明确区分:
5
+ * - 致命错误 → 同步抛错或迭代器抛错
6
+ * - 非致命差异 → warning 通道
7
+ * - 流中断 → 不伪造 response.completed
8
+ */
9
+
10
+ // ── 错误类型 ──────────────────────────────────────────────────
11
+
12
+ export type ErrorCode =
13
+ | "INPUT_EMPTY"
14
+ | "TEMPERATURE_OUT_OF_RANGE"
15
+ | "MAX_OUTPUT_TOKENS_INVALID"
16
+ | "TOOL_CHOICE_NO_TOOLS"
17
+ | "TOOL_CHOICE_UNKNOWN_TOOL"
18
+ | "PROVIDER_ERROR"
19
+ | "AUTH_ERROR"
20
+ | "STREAM_ERROR"
21
+ | "MAPPING_ERROR"
22
+ | "STREAM_INCOMPLETE"
23
+ | "LOOKUP_FAILED"
24
+ | "LOOKUP_TIMEOUT"
25
+ | string;
26
+
27
+ export class AIError extends Error {
28
+ override readonly name: string;
29
+
30
+ constructor(
31
+ message: string,
32
+ public readonly code: ErrorCode,
33
+ name?: string,
34
+ ) {
35
+ super(message);
36
+ this.name = name ?? "AIError";
37
+ Object.setPrototypeOf(this, new.target.prototype);
38
+ }
39
+ }
40
+
41
+ /** 请求构造失败 — 参数校验不通过。在进入 adapter 前同步抛错。 */
42
+ export class AIRequestError extends AIError {
43
+ constructor(message: string, code: ErrorCode) {
44
+ super(message, code, "AIRequestError");
45
+ }
46
+ }
47
+
48
+ /** Provider 调用失败 — HTTP 非 2xx、网络错误。由 AdapterBase 捕获转为 warning。 */
49
+ export class AIProviderError extends AIError {
50
+ constructor(
51
+ message: string,
52
+ code: ErrorCode,
53
+ public readonly statusCode?: number,
54
+ public readonly responseBody?: string,
55
+ ) {
56
+ super(message, code, "AIProviderError");
57
+ }
58
+ }
59
+
60
+ /** 流协议损坏 — SSE 解析失败、chunk 格式异常。 */
61
+ export class AIStreamError extends AIError {
62
+ constructor(message: string, code: ErrorCode) {
63
+ super(message, code, "AIStreamError");
64
+ }
65
+ }
66
+
67
+ /** Canonical 映射失败 — 无法将 provider 响应映射到 canonical 类型。 */
68
+ export class AIMappingError extends AIError {
69
+ constructor(message: string, code: ErrorCode) {
70
+ super(message, code, "AIMappingError");
71
+ }
72
+ }
73
+
74
+ // ── Warning 辅助 ──────────────────────────────────────────────
75
+
76
+ /**
77
+ * 标准 warning 代码列表。
78
+ * 用于非致命差异的记录。
79
+ */
80
+ export const WarningCode = {
81
+ /** replay fidelity 低于预期 */
82
+ REPLAY_FIDELITY_LOW: "REPLAY_FIDELITY_LOW",
83
+ /** usage 字段缺失 */
84
+ USAGE_MISSING: "USAGE_MISSING",
85
+ /** billing 字段缺失 */
86
+ BILLING_MISSING: "BILLING_MISSING",
87
+ /** billing 只能给估算值 */
88
+ BILLING_ESTIMATED: "BILLING_ESTIMATED",
89
+ /** follow-up lookup 失败 */
90
+ LOOKUP_FAILED: "LOOKUP_FAILED",
91
+ /** lookup 超时 */
92
+ LOOKUP_TIMEOUT: "LOOKUP_TIMEOUT",
93
+ /** 流提前中断 */
94
+ STREAM_INCOMPLETE: "STREAM_INCOMPLETE",
95
+ /** 能力降级 */
96
+ CAPABILITY_DOWNGRADE: "CAPABILITY_DOWNGRADE",
97
+ /** 模拟流式 */
98
+ SYNTHETIC_STREAM: "SYNTHETIC_STREAM",
99
+ } as const;
@@ -0,0 +1,141 @@
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
+ MessageItem,
23
+ ReasoningItem,
24
+ ToolCallItem,
25
+ ContentBlock,
26
+ Usage,
27
+ BillingInfo,
28
+ AuxiliaryInfo,
29
+ AIResponse,
30
+ } from "../types/index.js";
31
+
32
+ export type EventFactoryBackend = {
33
+ kind: "chat-completions" | "messages" | "responses" | "ollama";
34
+ isSynthetic: boolean;
35
+ };
36
+
37
+ export type EventFactoryState = {
38
+ responseId: string;
39
+ backend: EventFactoryBackend;
40
+ };
41
+
42
+ function timestamp(): string {
43
+ return new Date().toISOString();
44
+ }
45
+
46
+ export function createEventFactory(state: EventFactoryState) {
47
+ let seq = 0;
48
+ const warnings: string[] = [];
49
+
50
+ function next(): number {
51
+ return seq++;
52
+ }
53
+
54
+ function base(): Pick<ResponseStartedEvent, "responseId" | "sequence" | "timestamp" | "backend"> {
55
+ return {
56
+ responseId: state.responseId,
57
+ sequence: next(),
58
+ timestamp: timestamp(),
59
+ backend: { ...state.backend },
60
+ };
61
+ }
62
+
63
+ return {
64
+ // ── 响应级事件 ──────────────────────────────────────────
65
+
66
+ responseStarted(model: string): ResponseStartedEvent {
67
+ return { ...base(), type: "response.started", model };
68
+ },
69
+
70
+ responseWarning(message: string, code?: string): ResponseWarningEvent {
71
+ warnings.push(message);
72
+ return { ...base(), type: "response.warning", message, code };
73
+ },
74
+
75
+ responseAuxiliary(data: {
76
+ usage?: Usage;
77
+ billing?: BillingInfo;
78
+ auxiliary?: Partial<AuxiliaryInfo>;
79
+ }): ResponseAuxiliaryEvent {
80
+ return { ...base(), type: "response.auxiliary", ...data };
81
+ },
82
+
83
+ responseCompleted(response: AIResponse): ResponseCompletedEvent {
84
+ return { ...base(), type: "response.completed", response };
85
+ },
86
+
87
+ // ── 消息流事件 ──────────────────────────────────────────
88
+
89
+ messageStarted(id: string): MessageStartedEvent {
90
+ return { ...base(), type: "message.started", item: { id, role: "assistant" } };
91
+ },
92
+
93
+ messageDelta(itemId: string, text: string): MessageDeltaEvent {
94
+ return { ...base(), type: "message.delta", itemId, delta: { type: "text", text } };
95
+ },
96
+
97
+ messageCompleted(item: MessageItem): MessageCompletedEvent {
98
+ return { ...base(), type: "message.completed", item };
99
+ },
100
+
101
+ // ── 思维链流事件 ────────────────────────────────────────
102
+
103
+ reasoningStarted(id: string, visibility: ReasoningItem["visibility"]): ReasoningStartedEvent {
104
+ return { ...base(), type: "reasoning.started", item: { id, visibility } };
105
+ },
106
+
107
+ reasoningDelta(itemId: string, delta: ContentBlock): ReasoningDeltaEvent {
108
+ return { ...base(), type: "reasoning.delta", itemId, delta };
109
+ },
110
+
111
+ reasoningCompleted(item: ReasoningItem): ReasoningCompletedEvent {
112
+ return { ...base(), type: "reasoning.completed", item };
113
+ },
114
+
115
+ // ── 工具调用流事件 ──────────────────────────────────────
116
+
117
+ toolCallStarted(id: string, name: string): ToolCallStartedEvent {
118
+ return { ...base(), type: "tool_call.started", item: { id, name } };
119
+ },
120
+
121
+ toolCallDelta(itemId: string, delta: { argumentsText?: string }): ToolCallDeltaEvent {
122
+ return { ...base(), type: "tool_call.delta", itemId, delta };
123
+ },
124
+
125
+ toolCallCompleted(item: ToolCallItem): ToolCallCompletedEvent {
126
+ return { ...base(), type: "tool_call.completed", item };
127
+ },
128
+
129
+ /** 返回当前已发出的 sequence 计数(用于断言) */
130
+ get sequence(): number {
131
+ return seq;
132
+ },
133
+
134
+ /** 返回当前已记录的 warning 副本。 */
135
+ get warnings(): string[] {
136
+ return [...warnings];
137
+ },
138
+ };
139
+ }
140
+
141
+ export type EventFactory = ReturnType<typeof createEventFactory>;
@@ -0,0 +1,18 @@
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";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 请求归一化
3
+ *
4
+ * 将 AIRequest + client 配置归一化为 NormalizedRequest,
5
+ * 包括默认值合并、requestId 生成、include 默认值填充。
6
+ */
7
+
8
+ import type { AIRequest, NormalizedRequest } from "../types/index.js";
9
+ import { assertValidRequest } from "./validation.js";
10
+
11
+ export type NormalizeOptions = {
12
+ model: string;
13
+ defaults?: Partial<AIRequest>;
14
+ };
15
+
16
+ const DEFAULT_INCLUDE = {
17
+ usage: "best_effort" as const,
18
+ billing: "best_effort" as const,
19
+ providerMetadata: "best_effort" as const,
20
+ };
21
+
22
+ /**
23
+ * 归一化请求:
24
+ * 1. 合并 defaults
25
+ * 2. 填充 include 默认值
26
+ * 3. 生成 requestId
27
+ * 4. 校验请求合法性
28
+ */
29
+ export function normalizeRequest(request: AIRequest, options: NormalizeOptions): NormalizedRequest {
30
+ const { model, defaults } = options;
31
+
32
+ // 合并 defaults(浅合并,input/tools 由 request 完全覆盖)
33
+ const merged: AIRequest = {
34
+ ...defaults,
35
+ ...request,
36
+ include: {
37
+ ...DEFAULT_INCLUDE,
38
+ ...defaults?.include,
39
+ ...request.include,
40
+ },
41
+ };
42
+
43
+ // 校验
44
+ assertValidRequest(merged);
45
+
46
+ return {
47
+ ...merged,
48
+ model,
49
+ requestId: crypto.randomUUID(),
50
+ };
51
+ }