@codehz/ai 0.1.0 → 0.1.1

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,6 +1,6 @@
1
1
  # nano-ai
2
2
 
3
- 统一流式 AI 客户端 — 一套 canonical API,三种后端协议(`responses` / `messages` / `chat.completions`)。
3
+ 统一流式 AI 客户端 — 一套 canonical API,支持真实模型后端与面向测试的脚本化 `MockAdapter`(`responses` / `messages` / `chat.completions` / `ollama` / `mock`)。
4
4
 
5
5
  ## 安装
6
6
 
@@ -114,9 +114,11 @@ console.log(response.replay); // 续接材料
114
114
  | OpenAI Responses API | `ResponsesAdapter` | 🌟🌟🌟 |
115
115
  | Anthropic Messages API | `MessagesAdapter` | 🌟🌟☆ |
116
116
  | OpenAI Chat Completions | `ChatCompletionsAdapter` | 🌟☆☆ |
117
+ | Ollama Chat API | `OllamaAdapter` | 🌟☆☆ |
118
+ | Scripted Test Backend | `MockAdapter` | 测试夹具 |
117
119
 
118
120
  ```ts
119
- import { ResponsesAdapter, MessagesAdapter, ChatCompletionsAdapter } from "nano-ai";
121
+ import { ResponsesAdapter, MessagesAdapter, ChatCompletionsAdapter, OllamaAdapter, MockAdapter } from "nano-ai";
120
122
 
121
123
  // OpenAI Responses API(能力最强)
122
124
  const responses = new ResponsesAdapter({ apiKey: "sk-..." });
@@ -126,6 +128,26 @@ const messages = new MessagesAdapter({ apiKey: "sk-ant-..." });
126
128
 
127
129
  // OpenAI Chat Completions(兼容层)
128
130
  const chat = new ChatCompletionsAdapter({ apiKey: "sk-..." });
131
+
132
+ // Ollama
133
+ const ollama = new OllamaAdapter({ baseUrl: "http://localhost:11434" });
134
+
135
+ // 面向测试的脚本化 mock backend
136
+ const mock = new MockAdapter({
137
+ turns: [
138
+ {
139
+ steps: [
140
+ { type: "message", content: "我先调用天气工具。" },
141
+ {
142
+ type: "tool_call",
143
+ id: "mock-call-weather",
144
+ name: "get_weather",
145
+ argumentsText: '{"city":"Hangzhou"}',
146
+ },
147
+ ],
148
+ },
149
+ ],
150
+ });
129
151
  ```
130
152
 
131
153
  各 adapter 的能力差异通过 `capabilities` 字段暴露:
@@ -136,6 +158,89 @@ adapter.capabilities.toolCallStreaming; // 是否支持工具调用流
136
158
  adapter.capabilities.replayFidelity; // "high" | "medium" | "low"
137
159
  ```
138
160
 
161
+ ## Mock 后端
162
+
163
+ `MockAdapter` 现在不是“按关键词回文本”的通用假后端,而是专门用于测试长流程工具调用、`replay` 续接、以及异常路径的脚本化测试夹具。
164
+
165
+ 核心思路是按 turn 写脚本:
166
+
167
+ - 每一轮可声明对请求格式的期望
168
+ - 每一轮可脚本化发出 `message` / `reasoning` / `tool_call`
169
+ - 可注入 `warning`、`content_filter`、transport interruption、provider-style error
170
+ - 可验证调用方是否把上一轮 `replay` 和当前 `tool_result` 正确带回
171
+
172
+ ```ts
173
+ import { createAIClient, MockAdapter } from "nano-ai";
174
+
175
+ const client = createAIClient({
176
+ adapter: new MockAdapter({
177
+ turns: [
178
+ {
179
+ name: "request-tool",
180
+ expect: {
181
+ items: [{ type: "message", role: "user", textIncludes: "weather" }],
182
+ tools: "present",
183
+ toolChoice: "present",
184
+ },
185
+ steps: [
186
+ { type: "message", content: "Checking weather now." },
187
+ {
188
+ type: "tool_call",
189
+ id: "mock-call-weather",
190
+ name: "get_weather",
191
+ argumentsText: '{"city":"Hangzhou"}',
192
+ },
193
+ ],
194
+ },
195
+ {
196
+ name: "consume-tool-result",
197
+ expect: {
198
+ requireReplayFromPreviousTurn: true,
199
+ requireToolResultsForPendingCalls: true,
200
+ },
201
+ steps: [{ type: "message", content: "Hangzhou is 28C and sunny." }],
202
+ },
203
+ ],
204
+ }),
205
+ model: "mock-model",
206
+ });
207
+ ```
208
+
209
+ 核心类型:
210
+
211
+ ```ts
212
+ type MockTurn = {
213
+ name?: string;
214
+ expect?: MockRequestExpectation | MockTurnValidator;
215
+ steps: MockStep[];
216
+ };
217
+ ```
218
+
219
+ 测试工具循环时,第二轮通常会要求:
220
+
221
+ - `requireReplayFromPreviousTurn: true`
222
+ - `requireToolResultsForPendingCalls: true`
223
+
224
+ 畸形路径示例:
225
+
226
+ ```ts
227
+ {
228
+ steps: [
229
+ { type: "message", content: "partial answer" },
230
+ { type: "interrupt" }, // 不发 response.completed,collectStream() 应失败
231
+ ],
232
+ }
233
+ ```
234
+
235
+ ```ts
236
+ {
237
+ steps: [
238
+ { type: "warning", message: "content filtered by policy", code: "CONTENT_FILTERED" },
239
+ { type: "complete", stopReason: "content_filter" },
240
+ ],
241
+ }
242
+ ```
243
+
139
244
  ## 多轮对话
140
245
 
141
246
  库不托管会话状态。调用方自行保留 `response.replay` 并在下一轮带回:
package/package.json CHANGED
@@ -26,5 +26,5 @@
26
26
  "tsdown": "^0.22.3",
27
27
  "typescript": "^6"
28
28
  },
29
- "version": "0.1.0"
29
+ "version": "0.1.1"
30
30
  }
@@ -161,6 +161,7 @@ function ensureTextCompatibleBlocks(
161
161
  ): import("../index.js").ContentBlock[] {
162
162
  for (let i = 0; i < blocks.length; i++) {
163
163
  const block = blocks[i];
164
+ if (!block) continue;
164
165
  if (block.type !== "text" && block.type !== "json") {
165
166
  throw new AIRequestError(
166
167
  `chat-completions does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`,
@@ -6,6 +6,7 @@
6
6
  * - messages
7
7
  * - chat.completions
8
8
  * - ollama
9
+ * - mock
9
10
  *
10
11
  * 每个 adapter 实现 BackendAdapter 内部协议。
11
12
  */
@@ -18,3 +19,23 @@ export { ChatCompletionsAdapter } from "./chat-completions.js";
18
19
  export type { ChatCompletionsAdapterOptions } from "./chat-completions.js";
19
20
  export { OllamaAdapter } from "./ollama.js";
20
21
  export type { OllamaAdapterOptions } from "./ollama.js";
22
+ export { MockAdapter } from "./mock.js";
23
+ export type {
24
+ MockAdapterOptions,
25
+ MockInputExpectation,
26
+ MockRequestExpectation,
27
+ MockTurnContext,
28
+ MockTurnValidator,
29
+ MockWarningStep,
30
+ MockAuxiliaryStep,
31
+ MockMessageStep,
32
+ MockReasoningStep,
33
+ MockToolCallStep,
34
+ MockOutputStep,
35
+ MockCompleteStep,
36
+ MockErrorStep,
37
+ MockInterruptStep,
38
+ MockThrowStep,
39
+ MockStep,
40
+ MockTurn,
41
+ } from "./mock.js";
@@ -78,6 +78,7 @@ function ensureMessagesTextBlocks(
78
78
  ): import("../index.js").ContentBlock[] {
79
79
  for (let i = 0; i < blocks.length; i++) {
80
80
  const block = blocks[i];
81
+ if (!block) continue;
81
82
  if (block.type !== "text" && block.type !== "json") {
82
83
  throw new AIRequestError(
83
84
  `messages does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`,