@codehz/ai 0.1.0 → 0.1.2

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` 并在下一轮带回:
@@ -212,6 +317,8 @@ const { usage, billing, auxiliary, warnings } = collector.build();
212
317
 
213
318
  ## 开发命令
214
319
 
320
+ `examples/` 下的三个示例默认都基于 `MockAdapter`,可直接运行,无需配置真实模型或 API key。
321
+
215
322
  ```bash
216
323
  bun run typecheck # TypeScript 类型检查
217
324
  bun run test # 运行全部测试(228+)
package/dist/index.d.mts CHANGED
@@ -117,7 +117,7 @@ type AuxiliaryInfo = {
117
117
  type BackendTrace = {
118
118
  requestId?: string;
119
119
  rawResponseId?: string;
120
- adapter: "chat-completions" | "messages" | "responses" | "ollama";
120
+ adapter: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
121
121
  isSyntheticStream: boolean;
122
122
  metadataSources?: string[];
123
123
  warnings?: string[];
@@ -143,7 +143,7 @@ type StreamEventBase = {
143
143
  sequence: number;
144
144
  timestamp: string;
145
145
  backend: {
146
- kind: "chat-completions" | "messages" | "responses" | "ollama";
146
+ kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
147
147
  isSynthetic: boolean;
148
148
  };
149
149
  };
@@ -289,9 +289,21 @@ declare const CAPABILITY_MATRIX: {
289
289
  readonly billing: "none";
290
290
  readonly providerMetadata: false;
291
291
  };
292
+ readonly mock: {
293
+ readonly nativeStreaming: false;
294
+ readonly messageStreaming: true;
295
+ readonly reasoningStreaming: false;
296
+ readonly toolCallStreaming: true;
297
+ readonly hiddenReasoningReplay: "none";
298
+ readonly replayFidelity: "high";
299
+ readonly tools: true;
300
+ readonly usage: "none";
301
+ readonly billing: "none";
302
+ readonly providerMetadata: true;
303
+ };
292
304
  };
293
305
  interface BackendAdapter {
294
- readonly kind: "chat-completions" | "messages" | "responses" | "ollama";
306
+ readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
295
307
  readonly capabilities: AdapterCapabilities;
296
308
  stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
297
309
  }
@@ -389,7 +401,7 @@ declare const WarningCode: {
389
401
  //#endregion
390
402
  //#region src/core/event-factory.d.ts
391
403
  type EventFactoryBackend = {
392
- kind: "chat-completions" | "messages" | "responses" | "ollama";
404
+ kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
393
405
  isSynthetic: boolean;
394
406
  };
395
407
  type EventFactoryState = {
@@ -553,7 +565,7 @@ type StreamResult = {
553
565
  rawResponseId?: string;
554
566
  };
555
567
  declare abstract class AdapterBase implements BackendAdapter {
556
- abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama";
568
+ abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
557
569
  abstract readonly capabilities: AdapterCapabilities;
558
570
  /**
559
571
  * stream 模板方法:
@@ -854,6 +866,161 @@ declare class OllamaAdapter extends AdapterBase {
854
866
  protected runStream(providerRequest: OllamaChatRequest, factory: EventFactory, request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
855
867
  }
856
868
  //#endregion
869
+ //#region src/adapters/mock.d.ts
870
+ type MockInputExpectation = {
871
+ type: InputItem["type"];
872
+ id?: string;
873
+ role?: MessageItem["role"];
874
+ name?: string;
875
+ toolName?: string;
876
+ callId?: string;
877
+ outcome?: ToolResultItem["outcome"];
878
+ visibility?: Extract<InputItem, {
879
+ type: "reasoning";
880
+ }>["visibility"];
881
+ source?: Extract<InputItem, {
882
+ type: "opaque";
883
+ }>["source"];
884
+ purpose?: Extract<InputItem, {
885
+ type: "opaque";
886
+ }>["purpose"];
887
+ textIncludes?: string;
888
+ };
889
+ type MockRequestExpectation = {
890
+ minItems?: number;
891
+ maxItems?: number;
892
+ ordered?: boolean;
893
+ requireReplayFromPreviousTurn?: boolean;
894
+ requireToolResultsForPendingCalls?: boolean;
895
+ tools?: "ignore" | "present" | "absent";
896
+ toolChoice?: "ignore" | "present" | "absent";
897
+ items?: MockInputExpectation[];
898
+ };
899
+ type MockTurnContext = {
900
+ turnIndex: number;
901
+ previousReplay: ReplayItem[];
902
+ pendingToolCalls: readonly ToolCallItem[];
903
+ history: readonly MockTurnRecord[];
904
+ };
905
+ type MockTurnValidator = (request: NormalizedRequest, context: MockTurnContext) => void | Promise<void>;
906
+ type MockWarningStep = {
907
+ type: "warning";
908
+ message: string;
909
+ code?: string;
910
+ };
911
+ type MockAuxiliaryStep = {
912
+ type: "auxiliary";
913
+ usage?: Usage;
914
+ billing?: BillingInfo;
915
+ auxiliary?: Partial<AuxiliaryInfo>;
916
+ };
917
+ type MockMessageStep = {
918
+ type: "message";
919
+ id?: string;
920
+ content: string | ContentBlock[];
921
+ };
922
+ type MockReasoningStep = {
923
+ type: "reasoning";
924
+ id?: string;
925
+ visibility?: Extract<OutputItem, {
926
+ type: "reasoning";
927
+ }>["visibility"];
928
+ content: string | ContentBlock[];
929
+ };
930
+ type MockToolCallStep = {
931
+ type: "tool_call";
932
+ id: string;
933
+ name: string;
934
+ argumentsText: string;
935
+ argumentsJson?: unknown;
936
+ streamArguments?: boolean;
937
+ };
938
+ type MockOutputStep = {
939
+ type: "output";
940
+ item: Extract<OutputItem, {
941
+ type: "message" | "reasoning" | "tool_call";
942
+ }>;
943
+ };
944
+ type MockCompleteStep = {
945
+ type: "complete";
946
+ stopReason?: StopReason;
947
+ replay?: ReplayItem[];
948
+ usage?: Usage;
949
+ billing?: BillingInfo;
950
+ auxiliary?: Partial<AuxiliaryInfo>;
951
+ providerMetadata?: Record<string, unknown>;
952
+ rawResponseId?: string;
953
+ warnings?: string[];
954
+ };
955
+ type MockErrorStep = {
956
+ type: "error";
957
+ message: string;
958
+ code?: string;
959
+ stopReason?: StopReason;
960
+ providerMetadata?: Record<string, unknown>;
961
+ };
962
+ type MockInterruptStep = {
963
+ type: "interrupt";
964
+ };
965
+ type MockThrowStep = {
966
+ type: "throw";
967
+ error: string | Error;
968
+ };
969
+ type MockStep = MockWarningStep | MockAuxiliaryStep | MockMessageStep | MockReasoningStep | MockToolCallStep | MockOutputStep | MockCompleteStep | MockErrorStep | MockInterruptStep | MockThrowStep;
970
+ type MockTurn = {
971
+ name?: string;
972
+ expect?: MockRequestExpectation | MockTurnValidator;
973
+ steps: MockStep[];
974
+ };
975
+ type MockAdapterOptions = {
976
+ turns: MockTurn[];
977
+ onExhausted?: "throw" | "repeat-last" | "complete-empty";
978
+ providerMetadata?: Record<string, unknown>;
979
+ };
980
+ type MockTurnRecord = {
981
+ turnIndex: number;
982
+ turnName?: string;
983
+ requestId: string;
984
+ replay: ReplayItem[];
985
+ toolCalls: ToolCallItem[];
986
+ };
987
+ type MockProviderRequest = {
988
+ request: NormalizedRequest;
989
+ turn: MockTurn;
990
+ turnIndex: number;
991
+ turnName?: string;
992
+ remainingPendingToolCalls: ToolCallItem[];
993
+ };
994
+ declare class MockAdapter extends AdapterBase {
995
+ readonly kind: "mock";
996
+ readonly capabilities: {
997
+ readonly nativeStreaming: false;
998
+ readonly messageStreaming: true;
999
+ readonly reasoningStreaming: false;
1000
+ readonly toolCallStreaming: true;
1001
+ readonly hiddenReasoningReplay: "none";
1002
+ readonly replayFidelity: "high";
1003
+ readonly tools: true;
1004
+ readonly usage: "none";
1005
+ readonly billing: "none";
1006
+ readonly providerMetadata: true;
1007
+ };
1008
+ private readonly turns;
1009
+ private readonly onExhausted;
1010
+ private readonly providerMetadata?;
1011
+ private cursor;
1012
+ private previousReplay;
1013
+ private pendingToolCalls;
1014
+ private history;
1015
+ private activeStream;
1016
+ constructor(options: MockAdapterOptions);
1017
+ protected buildRequest(request: NormalizedRequest): Promise<MockProviderRequest>;
1018
+ protected runStream(providerRequest: unknown, factory: EventFactory, request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
1019
+ private finalizeTurn;
1020
+ private resolveTurn;
1021
+ private buildTurnContext;
1022
+ }
1023
+ //#endregion
857
1024
  //#region src/helpers/mapping.d.ts
858
1025
  declare function mapStopReason(providerReason: string): StopReason;
859
1026
  declare function mapReasoningVisibility(hasThinking: boolean, hasRedacted: boolean): ReasoningItem["visibility"];
@@ -945,7 +1112,7 @@ type SyntheticStreamOptions = {
945
1112
  model: string;
946
1113
  responseId: string;
947
1114
  backend: {
948
- kind: "chat-completions" | "messages" | "responses";
1115
+ kind: "chat-completions" | "messages" | "responses" | "mock";
949
1116
  };
950
1117
  output: OutputItem[];
951
1118
  replay?: ReplayItem[];
@@ -974,5 +1141,5 @@ type SyntheticStreamOptions = {
974
1141
  */
975
1142
  declare function syntheticStream(options: SyntheticStreamOptions): AsyncIterable<AIStreamEvent>;
976
1143
  //#endregion
977
- export { type AIClient, AIError, AIMappingError, AIProviderError, type AIRequest, AIRequestError, type AIResponse, AIStreamError, type AIStreamEvent, AdapterAuxiliaryState, AdapterBase, type AdapterCapabilities, AuxiliaryCollector, type AuxiliaryFinalizeOptions, type AuxiliaryFinalizeResult, type AuxiliaryInfo, type BackendAdapter, type BackendTrace, type BillingInfo, type BillingPostprocessHook, type BillingSource, CAPABILITY_MATRIX, ChatCompletionsAdapter, type ChatCompletionsAdapterOptions, type ContentBlock, type CreateAIClientOptions, type EventFactory, type EventFactoryBackend, type EventFactoryState, type FetchFn, type IncludeSettings, type InputItem, type LookupResult, type MessageCompletedEvent, type MessageDeltaEvent, type MessageItem, type MessageStartedEvent, MessagesAdapter, type MessagesAdapterOptions, type NormalizeOptions, type NormalizedRequest, OllamaAdapter, type OllamaAdapterOptions, type OpaqueItem, type OutputItem, type ReasoningCompletedEvent, type ReasoningDeltaEvent, type ReasoningItem, type ReasoningStartedEvent, type ReplayItem, type ResponseAuxiliaryEvent, type ResponseCompletedEvent, type ResponseStartedEvent, type ResponseWarningEvent, ResponsesAdapter, type ResponsesAdapterOptions, type SSEEvent, type StopReason, type StreamEventBase, type StreamResult, type SyntheticStreamOptions, type ToolCallCompletedEvent, type ToolCallDeltaEvent, type ToolCallItem, type ToolCallStartedEvent, type ToolChoice, type ToolDefinition, type ToolResultItem, type Usage, type UsageSource, type ValidationIssue, WarningCode, aggregateEvents, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, reasoningItem, replayFromOutput, syntheticStream, textBlock, toolCallItem, toolResultItem, validateRequest };
1144
+ export { type AIClient, AIError, AIMappingError, AIProviderError, type AIRequest, AIRequestError, type AIResponse, AIStreamError, type AIStreamEvent, AdapterAuxiliaryState, AdapterBase, type AdapterCapabilities, AuxiliaryCollector, type AuxiliaryFinalizeOptions, type AuxiliaryFinalizeResult, type AuxiliaryInfo, type BackendAdapter, type BackendTrace, type BillingInfo, type BillingPostprocessHook, type BillingSource, CAPABILITY_MATRIX, ChatCompletionsAdapter, type ChatCompletionsAdapterOptions, type ContentBlock, type CreateAIClientOptions, type EventFactory, type EventFactoryBackend, type EventFactoryState, type FetchFn, type IncludeSettings, type InputItem, type LookupResult, type MessageCompletedEvent, type MessageDeltaEvent, type MessageItem, type MessageStartedEvent, MessagesAdapter, type MessagesAdapterOptions, MockAdapter, type MockAdapterOptions, type MockAuxiliaryStep, type MockCompleteStep, type MockErrorStep, type MockInputExpectation, type MockInterruptStep, type MockMessageStep, type MockOutputStep, type MockReasoningStep, type MockRequestExpectation, type MockStep, type MockThrowStep, type MockToolCallStep, type MockTurn, type MockTurnContext, type MockTurnValidator, type MockWarningStep, type NormalizeOptions, type NormalizedRequest, OllamaAdapter, type OllamaAdapterOptions, type OpaqueItem, type OutputItem, type ReasoningCompletedEvent, type ReasoningDeltaEvent, type ReasoningItem, type ReasoningStartedEvent, type ReplayItem, type ResponseAuxiliaryEvent, type ResponseCompletedEvent, type ResponseStartedEvent, type ResponseWarningEvent, ResponsesAdapter, type ResponsesAdapterOptions, type SSEEvent, type StopReason, type StreamEventBase, type StreamResult, type SyntheticStreamOptions, type ToolCallCompletedEvent, type ToolCallDeltaEvent, type ToolCallItem, type ToolCallStartedEvent, type ToolChoice, type ToolDefinition, type ToolResultItem, type Usage, type UsageSource, type ValidationIssue, WarningCode, aggregateEvents, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, reasoningItem, replayFromOutput, syntheticStream, textBlock, toolCallItem, toolResultItem, validateRequest };
978
1145
  //# sourceMappingURL=index.d.mts.map