@codehz/ai 0.1.0 → 0.1.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.
@@ -84,6 +84,7 @@ function ensureOllamaTextBlocks(
84
84
  ): import("../index.js").ContentBlock[] {
85
85
  for (let i = 0; i < blocks.length; i++) {
86
86
  const block = blocks[i];
87
+ if (!block) continue;
87
88
  if (block.type !== "text" && block.type !== "json") {
88
89
  throw new AIRequestError(
89
90
  `ollama does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`,
@@ -519,6 +520,7 @@ export class OllamaAdapter extends AdapterBase {
519
520
  );
520
521
  }
521
522
 
523
+ // oxlint-disable-next-line no-await-in-loop
522
524
  const auxiliaryResult = await auxiliary.finalize(factory);
523
525
  for (const event of auxiliaryResult.events) {
524
526
  yield event;
@@ -77,6 +77,7 @@ function ensureResponsesTextBlocks(
77
77
  ): import("../index.js").ContentBlock[] {
78
78
  for (let i = 0; i < blocks.length; i++) {
79
79
  const block = blocks[i];
80
+ if (!block) continue;
80
81
  if (block.type !== "text" && block.type !== "json") {
81
82
  throw new AIRequestError(
82
83
  `responses does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`,
@@ -17,9 +17,7 @@
17
17
  import type {
18
18
  AIStreamEvent,
19
19
  AIResponse,
20
- ContentBlock,
21
20
  MessageItem,
22
- ReasoningItem,
23
21
  ToolCallItem,
24
22
  OutputItem,
25
23
  Usage,
@@ -29,26 +27,9 @@ import type {
29
27
  StopReason,
30
28
  } from "../types/index.js";
31
29
 
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
30
  // ── 聚合器状态 ────────────────────────────────────────────────
50
31
 
51
- interface AggregatorState {
32
+ export interface AggregatorState {
52
33
  responseId?: string;
53
34
  model?: string;
54
35
  backendInfo?: { kind: BackendTrace["adapter"]; isSynthetic: boolean };
@@ -57,16 +38,11 @@ interface AggregatorState {
57
38
  billing?: BillingInfo;
58
39
  auxiliary: AuxiliaryInfo;
59
40
  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>;
41
+ warningSet: Set<string>;
42
+ output: OutputItem[];
43
+ textParts: string[];
44
+ toolCalls: ToolCallItem[];
45
+ lastEventType?: AIStreamEvent["type"];
70
46
 
71
47
  /** adapter 在 response.completed 中提供的 replay */
72
48
  replayFromAdapter?: import("../types/index.js").ReplayItem[];
@@ -75,17 +51,14 @@ interface AggregatorState {
75
51
  backendFromAdapter?: BackendTrace;
76
52
  }
77
53
 
78
- function createInitialState(): AggregatorState {
54
+ export function createAggregatorState(): AggregatorState {
79
55
  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
56
  auxiliary: {},
88
57
  warnings: [],
58
+ warningSet: new Set(),
59
+ output: [],
60
+ textParts: [],
61
+ toolCalls: [],
89
62
  };
90
63
  }
91
64
 
@@ -113,72 +86,21 @@ function handleResponseAuxiliary(state: AggregatorState, event: AIStreamEvent &
113
86
  }
114
87
  }
115
88
 
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
89
  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
- }
90
+ state.output.push(event.item);
91
+ pushMessageText(state, event.item);
150
92
  }
151
93
 
152
94
  function handleReasoningCompleted(
153
95
  state: AggregatorState,
154
96
  event: AIStreamEvent & { type: "reasoning.completed" },
155
97
  ): 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
- }
98
+ state.output.push(event.item);
175
99
  }
176
100
 
177
101
  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);
102
+ state.output.push(event.item);
103
+ state.toolCalls.push(event.item);
182
104
  }
183
105
 
184
106
  function handleResponseCompleted(state: AggregatorState, event: AIStreamEvent & { type: "response.completed" }): void {
@@ -205,37 +127,6 @@ function handleResponseCompleted(state: AggregatorState, event: AIStreamEvent &
205
127
  // ── 从聚合状态构建最终 AIResponse ─────────────────────────────
206
128
 
207
129
  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
130
  // 合并 backend trace
240
131
  const backendFromResponse = state.backendFromAdapter;
241
132
  const backend: BackendTrace = {
@@ -249,10 +140,10 @@ function buildResponse(state: AggregatorState): AIResponse {
249
140
 
250
141
  return {
251
142
  id: state.responseIdFromAdapter ?? state.responseId,
252
- output,
143
+ output: state.output,
253
144
  replay: state.replayFromAdapter ?? [],
254
- text,
255
- toolCalls,
145
+ text: state.textParts.join(""),
146
+ toolCalls: state.toolCalls,
256
147
  stopReason: state.stopReasonFromAdapter,
257
148
  usage: state.usage,
258
149
  billing: state.billing,
@@ -269,55 +160,50 @@ function buildResponse(state: AggregatorState): AIResponse {
269
160
  * 适用于测试和离线处理场景。
270
161
  */
271
162
  export function aggregateEvents(events: AIStreamEvent[]): AIResponse {
272
- const state = createInitialState();
273
-
163
+ const state = createAggregatorState();
274
164
  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
- }
165
+ aggregateEvent(state, event);
316
166
  }
167
+ return finalizeAggregation(state);
168
+ }
169
+
170
+ export function aggregateEvent(state: AggregatorState, event: AIStreamEvent): void {
171
+ state.lastEventType = event.type;
172
+
173
+ switch (event.type) {
174
+ case "response.started":
175
+ handleResponseStarted(state, event);
176
+ break;
177
+ case "response.warning":
178
+ handleResponseWarning(state, event);
179
+ break;
180
+ case "response.auxiliary":
181
+ handleResponseAuxiliary(state, event);
182
+ break;
183
+ case "message.started":
184
+ case "message.delta":
185
+ case "reasoning.started":
186
+ case "reasoning.delta":
187
+ case "tool_call.started":
188
+ case "tool_call.delta":
189
+ break;
190
+ case "message.completed":
191
+ handleMessageCompleted(state, event);
192
+ break;
193
+ case "reasoning.completed":
194
+ handleReasoningCompleted(state, event);
195
+ break;
196
+ case "tool_call.completed":
197
+ handleToolCallCompleted(state, event);
198
+ break;
199
+ case "response.completed":
200
+ handleResponseCompleted(state, event);
201
+ break;
202
+ }
203
+ }
317
204
 
318
- // response.completed 中的 response 做最终构建
319
- const lastEvent = events[events.length - 1];
320
- if (!lastEvent || lastEvent.type !== "response.completed") {
205
+ export function finalizeAggregation(state: AggregatorState): AIResponse {
206
+ if (state.lastEventType !== "response.completed") {
321
207
  throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
322
208
  }
323
209
 
@@ -332,8 +218,8 @@ function mergeAuxiliary(base: AuxiliaryInfo, patch: Partial<AuxiliaryInfo>): Aux
332
218
 
333
219
  if (base.providerMetadata || patch.providerMetadata) {
334
220
  merged.providerMetadata = {
335
- ...(base.providerMetadata ?? {}),
336
- ...(patch.providerMetadata ?? {}),
221
+ ...base.providerMetadata,
222
+ ...patch.providerMetadata,
337
223
  };
338
224
  }
339
225
 
@@ -342,8 +228,17 @@ function mergeAuxiliary(base: AuxiliaryInfo, patch: Partial<AuxiliaryInfo>): Aux
342
228
 
343
229
  function pushWarnings(state: AggregatorState, warnings: readonly string[]): void {
344
230
  for (const warning of warnings) {
345
- if (!state.warnings.includes(warning)) {
231
+ if (!state.warningSet.has(warning)) {
232
+ state.warningSet.add(warning);
346
233
  state.warnings.push(warning);
347
234
  }
348
235
  }
349
236
  }
237
+
238
+ function pushMessageText(state: AggregatorState, item: MessageItem): void {
239
+ for (const block of item.content) {
240
+ if (block.type === "text") {
241
+ state.textParts.push(block.text);
242
+ }
243
+ }
244
+ }
@@ -6,14 +6,14 @@
6
6
  */
7
7
 
8
8
  import type { AIStreamEvent, AIResponse } from "../types/index.js";
9
- import { aggregateEvents } from "./aggregator.js";
9
+ import { aggregateEvent, createAggregatorState, finalizeAggregation } from "./aggregator.js";
10
10
 
11
11
  export async function collectStream(stream: AsyncIterable<AIStreamEvent>): Promise<AIResponse> {
12
- const events: AIStreamEvent[] = [];
12
+ const state = createAggregatorState();
13
13
 
14
14
  for await (const event of stream) {
15
- events.push(event);
15
+ aggregateEvent(state, event);
16
16
  }
17
17
 
18
- return aggregateEvents(events);
18
+ return finalizeAggregation(state);
19
19
  }
@@ -30,7 +30,7 @@ import type {
30
30
  } from "../types/index.js";
31
31
 
32
32
  export type EventFactoryBackend = {
33
- kind: "chat-completions" | "messages" | "responses" | "ollama";
33
+ kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
34
34
  isSynthetic: boolean;
35
35
  };
36
36
 
@@ -55,7 +55,7 @@ export type StreamResult = {
55
55
  // ── 抽象基类 ──────────────────────────────────────────────────
56
56
 
57
57
  export abstract class AdapterBase implements BackendAdapter {
58
- abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama";
58
+ abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
59
59
  abstract readonly capabilities: AdapterCapabilities;
60
60
 
61
61
  /**
@@ -154,14 +154,14 @@ function mergeAuxiliary(
154
154
  if (!base && !patch) return undefined;
155
155
 
156
156
  const merged: AuxiliaryInfo = {
157
- ...(base ?? {}),
158
- ...(patch ?? {}),
157
+ ...base,
158
+ ...patch,
159
159
  };
160
160
 
161
161
  if (base?.providerMetadata || patch?.providerMetadata) {
162
162
  merged.providerMetadata = {
163
- ...(base?.providerMetadata ?? {}),
164
- ...(patch?.providerMetadata ?? {}),
163
+ ...base?.providerMetadata,
164
+ ...patch?.providerMetadata,
165
165
  };
166
166
  }
167
167
 
@@ -35,7 +35,7 @@ export type SyntheticStreamOptions = {
35
35
  model: string;
36
36
  responseId: string;
37
37
  backend: {
38
- kind: "chat-completions" | "messages" | "responses";
38
+ kind: "chat-completions" | "messages" | "responses" | "mock";
39
39
  /** syntheticStream 强制设为 true */
40
40
  };
41
41
  output: OutputItem[];
@@ -86,12 +86,24 @@ export const CAPABILITY_MATRIX = {
86
86
  billing: "none" as const,
87
87
  providerMetadata: false,
88
88
  },
89
+ mock: {
90
+ nativeStreaming: false,
91
+ messageStreaming: true,
92
+ reasoningStreaming: false,
93
+ toolCallStreaming: true,
94
+ hiddenReasoningReplay: "none" as const,
95
+ replayFidelity: "high" as const,
96
+ tools: true,
97
+ usage: "none" as const,
98
+ billing: "none" as const,
99
+ providerMetadata: true,
100
+ },
89
101
  } as const satisfies Record<string, AdapterCapabilities>;
90
102
 
91
103
  // ── Adapter 接口 ──────────────────────────────────────────────
92
104
 
93
105
  export interface BackendAdapter {
94
- readonly kind: "chat-completions" | "messages" | "responses" | "ollama";
106
+ readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
95
107
  readonly capabilities: AdapterCapabilities;
96
108
  stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
97
109
  }
@@ -18,7 +18,7 @@ export type StreamEventBase = {
18
18
  sequence: number;
19
19
  timestamp: string;
20
20
  backend: {
21
- kind: "chat-completions" | "messages" | "responses" | "ollama";
21
+ kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
22
22
  isSynthetic: boolean;
23
23
  };
24
24
  };
@@ -42,7 +42,7 @@ export type AuxiliaryInfo = {
42
42
  export type BackendTrace = {
43
43
  requestId?: string;
44
44
  rawResponseId?: string;
45
- adapter: "chat-completions" | "messages" | "responses" | "ollama";
45
+ adapter: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
46
46
  isSyntheticStream: boolean;
47
47
  metadataSources?: string[];
48
48
  warnings?: string[];