@codehz/ai 0.2.3 → 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,18 @@ 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` | 调用链路元数据 |
109
110
 
110
111
  流式 `message.delta` / `reasoning.delta` 保持后端分片粒度;完成态 `output` 中的
111
112
  `message` / `reasoning` 会合并相邻 `text` content blocks(直接拼接且不添加分隔符),
@@ -129,7 +130,7 @@ import {
129
130
  OllamaAdapter,
130
131
  MockAdapter,
131
132
  withMockStreaming,
132
- } from "nano-ai";
133
+ } from "@codehz/ai";
133
134
 
134
135
  // OpenAI Responses API
135
136
  const responses = new ResponsesAdapter({ apiKey: "sk-..." });
@@ -175,8 +176,8 @@ adapter.capabilities.usage; // "stream" | "final" | "none"
175
176
  adapter.capabilities.toolResultOutcomes;
176
177
  ```
177
178
 
178
- `nativeStreaming` 已由 `capabilities` 直接替代。响应级 `backend.isSyntheticStream` 根据
179
- `textStreaming === "synthetic"` 推导;具体响应内容仍应从本次事件流、warning 和 `replay` 判断。
179
+ 响应级 `backend.isSyntheticStream` 根据 `textStreaming === "synthetic"` 推导;具体响应内容仍应从
180
+ 本次事件流、warning 和 `replay` 判断。
180
181
 
181
182
  ## Mock 后端
182
183
 
@@ -208,7 +209,7 @@ const handler = withMockStreaming(
208
209
  - 可用 `assertMockRequest()` 验证调用方是否把上一轮 `replay` 和当前 `tool_result` 正确带回
209
210
 
210
211
  ```ts
211
- import { assertMockRequest, createAIClient, MockAdapter } from "nano-ai";
212
+ import { assertMockRequest, createAIClient, MockAdapter } from "@codehz/ai";
212
213
 
213
214
  const client = createAIClient({
214
215
  adapter: new MockAdapter({
@@ -325,7 +326,7 @@ const r2 = await collectStream(client.stream({ input, tools }));
325
326
  非流式后端可通过 `syntheticStream()` 包装为规范事件流:
326
327
 
327
328
  ```ts
328
- import { syntheticStream } from "nano-ai";
329
+ import { syntheticStream } from "@codehz/ai";
329
330
 
330
331
  const events = syntheticStream({
331
332
  model: "gpt-4o",
@@ -345,7 +346,7 @@ for await (const event of events) {
345
346
  `AuxiliaryCollector` 提供分层 best-effort 采集(流事件 → headers → lookup → derived):
346
347
 
347
348
  ```ts
348
- import { AuxiliaryCollector } from "nano-ai";
349
+ import { AuxiliaryCollector } from "@codehz/ai";
349
350
 
350
351
  const collector = new AuxiliaryCollector();
351
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() {
@@ -1076,6 +1090,7 @@ var AdapterBase = class {
1076
1090
  * 3. 委托 runStream 发射全部流事件(含 response.completed)
1077
1091
  */
1078
1092
  async *stream(request) {
1093
+ request.signal?.throwIfAborted();
1079
1094
  const factory = createEventFactory({
1080
1095
  responseId: request.requestId,
1081
1096
  backend: {
@@ -1759,7 +1774,8 @@ var ResponsesAdapter = class extends AdapterBase {
1759
1774
  "Content-Type": "application/json",
1760
1775
  Authorization: `Bearer ${this.apiKey}`
1761
1776
  },
1762
- body: JSON.stringify(providerRequest)
1777
+ body: JSON.stringify(providerRequest),
1778
+ signal: request.signal
1763
1779
  });
1764
1780
  } catch (err) {
1765
1781
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -2175,7 +2191,8 @@ var MessagesAdapter = class extends AdapterBase {
2175
2191
  "x-api-key": this.apiKey,
2176
2192
  "anthropic-version": this.apiVersion
2177
2193
  },
2178
- body: JSON.stringify(providerRequest)
2194
+ body: JSON.stringify(providerRequest),
2195
+ signal: request.signal
2179
2196
  });
2180
2197
  } catch (err) {
2181
2198
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -2643,7 +2660,8 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2643
2660
  "Content-Type": "application/json",
2644
2661
  Authorization: `Bearer ${this.apiKey}`
2645
2662
  },
2646
- body: JSON.stringify(providerRequest)
2663
+ body: JSON.stringify(providerRequest),
2664
+ signal: request.signal
2647
2665
  });
2648
2666
  } catch (err) {
2649
2667
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -3061,7 +3079,8 @@ var OllamaAdapter = class extends AdapterBase {
3061
3079
  response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
3062
3080
  method: "POST",
3063
3081
  headers,
3064
- body: JSON.stringify(providerRequest)
3082
+ body: JSON.stringify(providerRequest),
3083
+ signal: request.signal
3065
3084
  });
3066
3085
  } catch (err) {
3067
3086
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -3296,7 +3315,7 @@ var MockAdapter = class extends AdapterBase {
3296
3315
  }
3297
3316
  async buildRequest(request) {
3298
3317
  const turnIndex = this.cursor;
3299
- const context = this.buildHandlerContext(turnIndex);
3318
+ const context = this.buildHandlerContext(turnIndex, request.signal);
3300
3319
  const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
3301
3320
  const handlerResult = this.handler(request, context);
3302
3321
  this.cursor += 1;
@@ -3315,6 +3334,7 @@ var MockAdapter = class extends AdapterBase {
3315
3334
  const output = [];
3316
3335
  let stepCount = 0;
3317
3336
  for await (const step of mockRequest.handlerResult) {
3337
+ if (request.signal?.aborted) return;
3318
3338
  stepCount += 1;
3319
3339
  switch (step.type) {
3320
3340
  case "warning":
@@ -3434,7 +3454,7 @@ var MockAdapter = class extends AdapterBase {
3434
3454
  rawResponseId: completion.rawResponseId
3435
3455
  }, factory);
3436
3456
  }
3437
- buildHandlerContext(turnIndex) {
3457
+ buildHandlerContext(turnIndex, signal) {
3438
3458
  return {
3439
3459
  turnIndex,
3440
3460
  previousReplay: this.previousReplay.map(cloneItem),
@@ -3443,7 +3463,8 @@ var MockAdapter = class extends AdapterBase {
3443
3463
  ...record,
3444
3464
  replay: record.replay.map(cloneItem),
3445
3465
  toolCalls: record.toolCalls.map(cloneItem)
3446
- }))
3466
+ })),
3467
+ signal
3447
3468
  };
3448
3469
  }
3449
3470
  };