@codehz/ai 0.2.2 → 0.2.4

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.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # nano-ai
1
+ # @codehz/ai
2
2
 
3
- 统一流式 AI 客户端,提供一套 canonical API,对接真实模型后端与面向测试的回调驱动 `MockAdapter`(`responses` / `messages` / `chat.completions` / `ollama` / `mock`)。
3
+ 统一流式 AI 客户端,提供一套 canonical API,对接真实模型后端与面向测试的回调驱动 `MockAdapter`(`responses` / `messages` / `chat-completions` / `ollama` / `mock`)。
4
4
 
5
5
  ## 安装
6
6
 
7
7
  ```bash
8
- bun add nano-ai
8
+ bun add @codehz/ai
9
9
  ```
10
10
 
11
11
  依赖:Bun(内置 `fetch`、`crypto`),无需额外运行时依赖。
@@ -13,7 +13,7 @@ bun add nano-ai
13
13
  ## 快速开始
14
14
 
15
15
  ```ts
16
- import { createAIClient, ResponsesAdapter } from "nano-ai";
16
+ import { createAIClient, ResponsesAdapter } from "@codehz/ai";
17
17
 
18
18
  const client = createAIClient({
19
19
  adapter: new ResponsesAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
@@ -69,22 +69,22 @@ response.started → (item.started → item.delta* → item.completed)* → resp
69
69
 
70
70
  事件类型:
71
71
 
72
- | 事件 | 含义 |
73
- | ------------------------------------- | ------------------------------- |
74
- | `response.started` | 响应开始 |
75
- | `message.{started,delta,completed}` | 消息输出 |
76
- | `reasoning.{started,delta,completed}` | 思维链 |
77
- | `tool_call.{started,delta,completed}` | 工具调用 |
78
- | `response.warning` | 非致命警告 |
79
- | `response.auxiliary` | usage / billing 辅助信息 |
80
- | `response.completed` | 响应结束,内含完整 `AIResponse` |
72
+ | 事件 | 含义 |
73
+ | ------------------------------------- | ------------------------------------------- |
74
+ | `response.started` | 响应开始 |
75
+ | `message.{started,delta,completed}` | 消息输出 |
76
+ | `reasoning.{started,delta,completed}` | 思维链 |
77
+ | `tool_call.{started,delta,completed}` | 工具调用 |
78
+ | `response.warning` | 非致命警告 |
79
+ | `response.auxiliary` | usage / billing 辅助信息 |
80
+ | `response.completed` | 响应结束,携带 replay、终止原因及最终元数据 |
81
81
 
82
82
  ### 统一终结结果
83
83
 
84
84
  流结束后可通过 `collectStream()` 聚合为 `AIResponse`:
85
85
 
86
86
  ```ts
87
- import { collectStream } from "nano-ai";
87
+ import { collectStream } from "@codehz/ai";
88
88
 
89
89
  const response = await collectStream(client.stream({ input }));
90
90
  console.log(response.text); // 全部文本
@@ -95,17 +95,22 @@ console.log(response.replay); // 续接材料
95
95
 
96
96
  `AIResponse` 包含:
97
97
 
98
- | 字段 | 类型 | 说明 |
99
- | ------------ | ---------------- | ------------------------ |
100
- | `output` | `OutputItem[]` | 当前轮输出 |
101
- | `replay` | `ReplayItem[]` | 续接材料(下次请求带回) |
102
- | `text` | `string` | 全部文本拼接 |
103
- | `toolCalls` | `ToolCallItem[]` | 工具调用 |
104
- | `stopReason` | `StopReason` | 终止原因 |
105
- | `usage` | `Usage` | token 统计 |
106
- | `billing` | `BillingInfo` | 计费信息 |
107
- | `warnings` | `string[]` | 非致命警告 |
108
- | `backend` | `BackendTrace` | 调用链路元数据 |
98
+ | 字段 | 类型 | 说明 |
99
+ | ------------ | ---------------- | ------------------------- |
100
+ | `output` | `OutputItem[]` | 当前轮输出 |
101
+ | `replay` | `ReplayItem[]` | 续接材料(下次请求带回) |
102
+ | `text` | `string` | 全部文本拼接 |
103
+ | `toolCalls` | `ToolCallItem[]` | 工具调用 |
104
+ | `stopReason` | `StopReason?` | 终止原因(可选) |
105
+ | `usage` | `Usage?` | token 统计(可选) |
106
+ | `billing` | `BillingInfo?` | 计费信息(可选) |
107
+ | `auxiliary` | `AuxiliaryInfo?` | Provider 辅助信息(可选) |
108
+ | `warnings` | `string[]?` | 非致命警告(可选) |
109
+ | `backend` | `BackendTrace` | 调用链路元数据 |
110
+
111
+ 流式 `message.delta` / `reasoning.delta` 保持后端分片粒度;完成态 `output` 中的
112
+ `message` / `reasoning` 会合并相邻 `text` content blocks(直接拼接且不添加分隔符),
113
+ 非文本 block 仍保留原有边界。
109
114
 
110
115
  ## 后端 Adapter
111
116
 
@@ -125,7 +130,7 @@ import {
125
130
  OllamaAdapter,
126
131
  MockAdapter,
127
132
  withMockStreaming,
128
- } from "nano-ai";
133
+ } from "@codehz/ai";
129
134
 
130
135
  // OpenAI Responses API
131
136
  const responses = new ResponsesAdapter({ apiKey: "sk-..." });
@@ -171,8 +176,8 @@ adapter.capabilities.usage; // "stream" | "final" | "none"
171
176
  adapter.capabilities.toolResultOutcomes;
172
177
  ```
173
178
 
174
- `nativeStreaming` 已由 `capabilities` 直接替代。响应级 `backend.isSyntheticStream` 根据
175
- `textStreaming === "synthetic"` 推导;具体响应内容仍应从本次事件流、warning 和 `replay` 判断。
179
+ 响应级 `backend.isSyntheticStream` 根据 `textStreaming === "synthetic"` 推导;具体响应内容仍应从
180
+ 本次事件流、warning 和 `replay` 判断。
176
181
 
177
182
  ## Mock 后端
178
183
 
@@ -204,7 +209,7 @@ const handler = withMockStreaming(
204
209
  - 可用 `assertMockRequest()` 验证调用方是否把上一轮 `replay` 和当前 `tool_result` 正确带回
205
210
 
206
211
  ```ts
207
- import { assertMockRequest, createAIClient, MockAdapter } from "nano-ai";
212
+ import { assertMockRequest, createAIClient, MockAdapter } from "@codehz/ai";
208
213
 
209
214
  const client = createAIClient({
210
215
  adapter: new MockAdapter({
@@ -321,7 +326,7 @@ const r2 = await collectStream(client.stream({ input, tools }));
321
326
  非流式后端可通过 `syntheticStream()` 包装为规范事件流:
322
327
 
323
328
  ```ts
324
- import { syntheticStream } from "nano-ai";
329
+ import { syntheticStream } from "@codehz/ai";
325
330
 
326
331
  const events = syntheticStream({
327
332
  model: "gpt-4o",
@@ -341,7 +346,7 @@ for await (const event of events) {
341
346
  `AuxiliaryCollector` 提供分层 best-effort 采集(流事件 → headers → lookup → derived):
342
347
 
343
348
  ```ts
344
- import { AuxiliaryCollector } from "nano-ai";
349
+ import { AuxiliaryCollector } from "@codehz/ai";
345
350
 
346
351
  const collector = new AuxiliaryCollector();
347
352
  collector.recordUsage({ inputTokens: 10, outputTokens: 5 }, "stream");
package/dist/index.d.mts CHANGED
@@ -88,7 +88,8 @@ type AIRequest = {
88
88
  include?: IncludeSettings;
89
89
  metadata?: Record<string, string>;
90
90
  temperature?: number;
91
- maxOutputTokens?: number;
91
+ maxOutputTokens?: number; /** AbortSignal 用于打断请求。abort 时 fetch 调用会被取消,流迭代器抛出 AbortError。 */
92
+ signal?: AbortSignal;
92
93
  };
93
94
  //#endregion
94
95
  //#region src/types/response.d.ts
@@ -250,7 +251,8 @@ interface BackendAdapter {
250
251
  type CreateAIClientOptions = {
251
252
  adapter: BackendAdapter;
252
253
  model: string;
253
- defaults?: Partial<AIRequest>;
254
+ defaults?: Partial<AIRequest>; /** 全局默认 AbortSignal,当 request.signal 未设置时生效。 */
255
+ signal?: AbortSignal;
254
256
  };
255
257
  interface AIClient {
256
258
  stream(request: AIRequest): AsyncIterable<AIStreamEvent>;
@@ -839,7 +841,8 @@ type MockHandlerContext = {
839
841
  turnIndex: number;
840
842
  previousReplay: ReplayItem[];
841
843
  pendingToolCalls: readonly ToolCallItem[];
842
- history: readonly MockHistoryRecord[];
844
+ history: readonly MockHistoryRecord[]; /** 请求的 AbortSignal,handler 可检查 signal.aborted 提前退出。 */
845
+ signal?: AbortSignal;
843
846
  };
844
847
  type MockWarningStep = {
845
848
  type: "warning";
package/dist/index.mjs CHANGED
@@ -313,15 +313,29 @@ function normalizeRequest(request, options) {
313
313
  //#endregion
314
314
  //#region src/core/client.ts
315
315
  function createAIClient(options) {
316
- const { adapter, model, defaults } = options;
316
+ const { adapter, model, defaults, signal: defaultSignal } = options;
317
317
  return { stream(request) {
318
- const normalized = normalizeRequest(request, {
318
+ const signal = mergeAbortSignals(defaultSignal, request.signal);
319
+ const normalized = normalizeRequest({
320
+ ...request,
321
+ signal
322
+ }, {
319
323
  model,
320
324
  defaults
321
325
  });
322
326
  return adapter.stream(normalized);
323
327
  } };
324
328
  }
329
+ /**
330
+ * 合并多个 AbortSignal:任一 signal abort 即触发。
331
+ * 如果没有 signal 需要合并则返回 undefined。
332
+ */
333
+ function mergeAbortSignals(...signals) {
334
+ const valid = signals.filter((s) => s != null);
335
+ if (valid.length === 0) return void 0;
336
+ if (valid.length === 1) return valid[0];
337
+ return AbortSignal.any(valid);
338
+ }
325
339
  //#endregion
326
340
  //#region src/core/event-factory.ts
327
341
  function timestamp() {
@@ -494,12 +508,21 @@ function getActiveItem(state, itemId, expectedType) {
494
508
  if (item.type !== expectedType) throw streamProtocolError(`Item ${itemId} started as ${item.type} but received ${expectedType} event`);
495
509
  return item;
496
510
  }
511
+ function coalesceContentBlocks(blocks) {
512
+ const result = [];
513
+ for (const block of blocks) {
514
+ const previous = result[result.length - 1];
515
+ if (block.type === "text" && previous?.type === "text") previous.text += block.text;
516
+ else result.push({ ...block });
517
+ }
518
+ return result;
519
+ }
497
520
  function finalizeMessage(active) {
498
521
  return {
499
522
  type: "message",
500
523
  id: active.id,
501
524
  role: active.role,
502
- content: active.content
525
+ content: coalesceContentBlocks(active.content)
503
526
  };
504
527
  }
505
528
  function finalizeReasoning(active) {
@@ -507,7 +530,7 @@ function finalizeReasoning(active) {
507
530
  type: "reasoning",
508
531
  id: active.id,
509
532
  visibility: active.visibility,
510
- content: active.content
533
+ content: coalesceContentBlocks(active.content)
511
534
  };
512
535
  }
513
536
  function finalizeToolCall(active) {
@@ -1067,6 +1090,7 @@ var AdapterBase = class {
1067
1090
  * 3. 委托 runStream 发射全部流事件(含 response.completed)
1068
1091
  */
1069
1092
  async *stream(request) {
1093
+ request.signal?.throwIfAborted();
1070
1094
  const factory = createEventFactory({
1071
1095
  responseId: request.requestId,
1072
1096
  backend: {
@@ -1750,7 +1774,8 @@ var ResponsesAdapter = class extends AdapterBase {
1750
1774
  "Content-Type": "application/json",
1751
1775
  Authorization: `Bearer ${this.apiKey}`
1752
1776
  },
1753
- body: JSON.stringify(providerRequest)
1777
+ body: JSON.stringify(providerRequest),
1778
+ signal: request.signal
1754
1779
  });
1755
1780
  } catch (err) {
1756
1781
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -2166,7 +2191,8 @@ var MessagesAdapter = class extends AdapterBase {
2166
2191
  "x-api-key": this.apiKey,
2167
2192
  "anthropic-version": this.apiVersion
2168
2193
  },
2169
- body: JSON.stringify(providerRequest)
2194
+ body: JSON.stringify(providerRequest),
2195
+ signal: request.signal
2170
2196
  });
2171
2197
  } catch (err) {
2172
2198
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -2634,7 +2660,8 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2634
2660
  "Content-Type": "application/json",
2635
2661
  Authorization: `Bearer ${this.apiKey}`
2636
2662
  },
2637
- body: JSON.stringify(providerRequest)
2663
+ body: JSON.stringify(providerRequest),
2664
+ signal: request.signal
2638
2665
  });
2639
2666
  } catch (err) {
2640
2667
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -3052,7 +3079,8 @@ var OllamaAdapter = class extends AdapterBase {
3052
3079
  response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
3053
3080
  method: "POST",
3054
3081
  headers,
3055
- body: JSON.stringify(providerRequest)
3082
+ body: JSON.stringify(providerRequest),
3083
+ signal: request.signal
3056
3084
  });
3057
3085
  } catch (err) {
3058
3086
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -3287,7 +3315,7 @@ var MockAdapter = class extends AdapterBase {
3287
3315
  }
3288
3316
  async buildRequest(request) {
3289
3317
  const turnIndex = this.cursor;
3290
- const context = this.buildHandlerContext(turnIndex);
3318
+ const context = this.buildHandlerContext(turnIndex, request.signal);
3291
3319
  const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
3292
3320
  const handlerResult = this.handler(request, context);
3293
3321
  this.cursor += 1;
@@ -3306,6 +3334,7 @@ var MockAdapter = class extends AdapterBase {
3306
3334
  const output = [];
3307
3335
  let stepCount = 0;
3308
3336
  for await (const step of mockRequest.handlerResult) {
3337
+ if (request.signal?.aborted) return;
3309
3338
  stepCount += 1;
3310
3339
  switch (step.type) {
3311
3340
  case "warning":
@@ -3425,7 +3454,7 @@ var MockAdapter = class extends AdapterBase {
3425
3454
  rawResponseId: completion.rawResponseId
3426
3455
  }, factory);
3427
3456
  }
3428
- buildHandlerContext(turnIndex) {
3457
+ buildHandlerContext(turnIndex, signal) {
3429
3458
  return {
3430
3459
  turnIndex,
3431
3460
  previousReplay: this.previousReplay.map(cloneItem),
@@ -3434,7 +3463,8 @@ var MockAdapter = class extends AdapterBase {
3434
3463
  ...record,
3435
3464
  replay: record.replay.map(cloneItem),
3436
3465
  toolCalls: record.toolCalls.map(cloneItem)
3437
- }))
3466
+ })),
3467
+ signal
3438
3468
  };
3439
3469
  }
3440
3470
  };