@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ai",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "module": "dist/index.mjs",
6
6
  "exports": {
@@ -398,6 +398,7 @@ export class ChatCompletionsAdapter extends AdapterBase {
398
398
  Authorization: `Bearer ${this.apiKey}`,
399
399
  },
400
400
  body: JSON.stringify(providerRequest),
401
+ signal: request.signal,
401
402
  });
402
403
  } catch (err) {
403
404
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -422,6 +422,7 @@ export class MessagesAdapter extends AdapterBase {
422
422
  "anthropic-version": this.apiVersion,
423
423
  },
424
424
  body: JSON.stringify(providerRequest),
425
+ signal: request.signal,
425
426
  });
426
427
  } catch (err) {
427
428
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -68,6 +68,8 @@ export type MockHandlerContext = {
68
68
  previousReplay: ReplayItem[];
69
69
  pendingToolCalls: readonly ToolCallItem[];
70
70
  history: readonly MockHistoryRecord[];
71
+ /** 请求的 AbortSignal,handler 可检查 signal.aborted 提前退出。 */
72
+ signal?: AbortSignal;
71
73
  };
72
74
 
73
75
  export type MockWarningStep = {
@@ -290,7 +292,7 @@ export class MockAdapter extends AdapterBase {
290
292
 
291
293
  protected async buildRequest(request: NormalizedRequest): Promise<MockProviderRequest> {
292
294
  const turnIndex = this.cursor;
293
- const context = this.buildHandlerContext(turnIndex);
295
+ const context = this.buildHandlerContext(turnIndex, request.signal);
294
296
  const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
295
297
  const handlerResult = this.handler(request, context);
296
298
 
@@ -321,6 +323,9 @@ export class MockAdapter extends AdapterBase {
321
323
  let stepCount = 0;
322
324
 
323
325
  for await (const step of mockRequest.handlerResult) {
326
+ // 若 signal 已 abort,停止消费 handler 并返回
327
+ if (request.signal?.aborted) return;
328
+
324
329
  stepCount += 1;
325
330
 
326
331
  switch (step.type) {
@@ -479,7 +484,7 @@ export class MockAdapter extends AdapterBase {
479
484
  );
480
485
  }
481
486
 
482
- private buildHandlerContext(turnIndex: number): MockHandlerContext {
487
+ private buildHandlerContext(turnIndex: number, signal?: AbortSignal): MockHandlerContext {
483
488
  return {
484
489
  turnIndex,
485
490
  previousReplay: this.previousReplay.map(cloneItem),
@@ -489,6 +494,7 @@ export class MockAdapter extends AdapterBase {
489
494
  replay: record.replay.map(cloneItem),
490
495
  toolCalls: record.toolCalls.map(cloneItem),
491
496
  })),
497
+ signal,
492
498
  };
493
499
  }
494
500
  }
@@ -351,6 +351,7 @@ export class OllamaAdapter extends AdapterBase {
351
351
  method: "POST",
352
352
  headers,
353
353
  body: JSON.stringify(providerRequest),
354
+ signal: request.signal,
354
355
  });
355
356
  } catch (err) {
356
357
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -330,6 +330,7 @@ export class ResponsesAdapter extends AdapterBase {
330
330
  Authorization: `Bearer ${this.apiKey}`,
331
331
  },
332
332
  body: JSON.stringify(providerRequest),
333
+ signal: request.signal,
333
334
  });
334
335
  } catch (err) {
335
336
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -8,11 +8,13 @@ import type { AIRequest, AIStreamEvent, AIClient, CreateAIClientOptions } from "
8
8
  import { normalizeRequest } from "./normalize.js";
9
9
 
10
10
  export function createAIClient(options: CreateAIClientOptions): AIClient {
11
- const { adapter, model, defaults } = options;
11
+ const { adapter, model, defaults, signal: defaultSignal } = options;
12
12
 
13
13
  const client: AIClient = {
14
14
  stream(request: AIRequest): AsyncIterable<AIStreamEvent> {
15
- const normalized = normalizeRequest(request, { model, defaults });
15
+ // 合并 client 级别的默认 signal 和请求级别的 signal
16
+ const signal = mergeAbortSignals(defaultSignal, request.signal);
17
+ const normalized = normalizeRequest({ ...request, signal }, { model, defaults });
16
18
  return adapter.stream(normalized);
17
19
  },
18
20
  };
@@ -20,4 +22,15 @@ export function createAIClient(options: CreateAIClientOptions): AIClient {
20
22
  return client;
21
23
  }
22
24
 
25
+ /**
26
+ * 合并多个 AbortSignal:任一 signal abort 即触发。
27
+ * 如果没有 signal 需要合并则返回 undefined。
28
+ */
29
+ function mergeAbortSignals(...signals: (AbortSignal | undefined)[]): AbortSignal | undefined {
30
+ const valid = signals.filter((s): s is AbortSignal => s != null);
31
+ if (valid.length === 0) return undefined;
32
+ if (valid.length === 1) return valid[0];
33
+ return AbortSignal.any(valid);
34
+ }
35
+
23
36
  export type { AIClient, CreateAIClientOptions } from "../types/index.js";
@@ -65,6 +65,9 @@ export abstract class AdapterBase implements BackendAdapter {
65
65
  * 3. 委托 runStream 发射全部流事件(含 response.completed)
66
66
  */
67
67
  async *stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent> {
68
+ // 若请求已被 abort,不发出任何事件
69
+ request.signal?.throwIfAborted();
70
+
68
71
  const factory = createEventFactory({
69
72
  responseId: request.requestId,
70
73
  backend: { kind: this.kind, isSynthetic: this.capabilities.textStreaming === "synthetic" },
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * nano-ai — 统一流式 AI 客户端
2
+ * @codehz/ai — 统一流式 AI 客户端
3
3
  *
4
4
  * 对外只暴露一个 canonical 主入口:client.stream()
5
5
  */
@@ -44,6 +44,8 @@ export type CreateAIClientOptions = {
44
44
  adapter: BackendAdapter;
45
45
  model: string;
46
46
  defaults?: Partial<AIRequest>;
47
+ /** 全局默认 AbortSignal,当 request.signal 未设置时生效。 */
48
+ signal?: AbortSignal;
47
49
  };
48
50
 
49
51
  export interface AIClient {
@@ -36,4 +36,6 @@ export type AIRequest = {
36
36
  metadata?: Record<string, string>;
37
37
  temperature?: number;
38
38
  maxOutputTokens?: number;
39
+ /** AbortSignal 用于打断请求。abort 时 fetch 调用会被取消,流迭代器抛出 AbortError。 */
40
+ signal?: AbortSignal;
39
41
  };