@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 +36 -31
- package/dist/index.d.mts +6 -3
- package/dist/index.mjs +41 -11
- 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/aggregator.ts +15 -2
- 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,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
|
|
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` | 调用链路元数据 |
|
|
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 "
|
|
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
|
-
`
|
|
175
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
|
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
|
};
|