@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 +32 -31
- package/dist/index.d.mts +6 -3
- package/dist/index.mjs +30 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +1 -0
- package/src/adapters/messages.ts +1 -0
- package/src/adapters/mock.ts +8 -2
- package/src/adapters/ollama.ts +1 -0
- package/src/adapters/responses.ts +1 -0
- package/src/core/client.ts +15 -2
- package/src/helpers/adapter-base.ts +3 -0
- package/src/index.ts +1 -1
- package/src/types/adapter.ts +2 -0
- package/src/types/request.ts +2 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @codehz/ai
|
|
2
2
|
|
|
3
|
-
统一流式 AI 客户端,提供一套 canonical API,对接真实模型后端与面向测试的回调驱动 `MockAdapter`(`responses` / `messages` / `chat
|
|
3
|
+
统一流式 AI 客户端,提供一套 canonical API,对接真实模型后端与面向测试的回调驱动 `MockAdapter`(`responses` / `messages` / `chat-completions` / `ollama` / `mock`)。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun add
|
|
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 "
|
|
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` |
|
|
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 "
|
|
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
|
|
106
|
-
| `billing` | `BillingInfo
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
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 "
|
|
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
|
-
`
|
|
179
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
|
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
|
};
|