@codehz/ai 0.2.4 → 0.4.0

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.
@@ -82,3 +82,61 @@ export function splitSSEFrames(buffer: string, allowEOF: boolean): StreamSplitRe
82
82
 
83
83
  return { items, rest: normalized.slice(cursor) };
84
84
  }
85
+
86
+ // ── 常用 parse 工厂 ───────────────────────────────────────────
87
+
88
+ export type SseJsonEvent = { type: string; data: unknown };
89
+
90
+ /** 解析标准 SSE frame(event: + data:),用于 Messages / Responses。 */
91
+ export function parseSseJsonFrame(frame: string): StreamParseResult<SseJsonEvent> {
92
+ let eventType = "";
93
+ let dataStr = "";
94
+ for (const rawLine of frame.split("\n")) {
95
+ const line = rawLine.trim();
96
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
97
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
98
+ }
99
+ if (!eventType) return { status: "ignored" };
100
+ try {
101
+ const data: unknown = JSON.parse(dataStr);
102
+ return { status: "parsed", value: { type: eventType, data } };
103
+ } catch {
104
+ return { status: "malformed" };
105
+ }
106
+ }
107
+
108
+ export function createSseJsonParser<T extends SseJsonEvent = SseJsonEvent>(): IncrementalStreamParser<T> {
109
+ return new IncrementalStreamParser(splitSSEFrames, (frame) => parseSseJsonFrame(frame) as StreamParseResult<T>);
110
+ }
111
+
112
+ /** OpenAI Chat Completions 简化 SSE:仅 `data: ...` 行,忽略 `[DONE]`。 */
113
+ export function parseChatCompletionsDataLine(item: string): StreamParseResult<unknown> {
114
+ const trimmed = item.trim();
115
+ if (!trimmed.startsWith("data: ")) return { status: "ignored" };
116
+ const data = trimmed.slice(6).trim();
117
+ if (data === "[DONE]") return { status: "ignored" };
118
+ try {
119
+ return { status: "parsed", value: JSON.parse(data) as unknown };
120
+ } catch {
121
+ return { status: "malformed" };
122
+ }
123
+ }
124
+
125
+ export function createChatCompletionsSseParser<T>(): IncrementalStreamParser<T> {
126
+ return new IncrementalStreamParser(splitLines, (item) => parseChatCompletionsDataLine(item) as StreamParseResult<T>);
127
+ }
128
+
129
+ /** NDJSON 行解析(Ollama 等):空行忽略,JSON 失败为 malformed。 */
130
+ export function createNdjsonLineParser<T>(isValid: (value: unknown) => value is T): IncrementalStreamParser<T> {
131
+ return new IncrementalStreamParser<T>(splitLines, (item: string): StreamParseResult<T> => {
132
+ const trimmed = item.trim();
133
+ if (!trimmed) return { status: "ignored" };
134
+ try {
135
+ const parsed: unknown = JSON.parse(trimmed);
136
+ if (isValid(parsed)) return { status: "parsed", value: parsed };
137
+ return { status: "malformed" };
138
+ } catch {
139
+ return { status: "malformed" };
140
+ }
141
+ });
142
+ }
@@ -13,7 +13,6 @@ export {
13
13
  opaqueBlock,
14
14
  blockToText,
15
15
  contentBlocksToText,
16
- instructionsToText,
17
16
  extractText,
18
17
  messageItem,
19
18
  reasoningItem,
@@ -23,12 +22,9 @@ export {
23
22
  replayFromOutput,
24
23
  } from "./mapping.js";
25
24
 
26
- export { parseSSEEvents } from "./sse-parser.js";
27
- export type { SSEEvent } from "./sse-parser.js";
28
-
29
25
  export { AdapterBase } from "./adapter-base.js";
30
26
  export type { StreamResult } from "./adapter-base.js";
31
- export { AdapterAuxiliaryState, emitMalformedStreamWarning, metadataSourceList } from "./adapter-auxiliary.js";
27
+ export { AdapterAuxiliaryState, emitMalformedStreamWarning } from "./adapter-auxiliary.js";
32
28
  export type { AuxiliaryFinalizeOptions, AuxiliaryFinalizeResult, BillingPostprocessHook } from "./adapter-auxiliary.js";
33
29
  export { syntheticStream } from "./synthetic-stream.js";
34
30
  export type { SyntheticStreamOptions } from "./synthetic-stream.js";
@@ -54,8 +50,24 @@ export {
54
50
  } from "./adapter-security.js";
55
51
  export type { OpaqueEnvelopeResult } from "./adapter-security.js";
56
52
 
57
- export { IncrementalStreamParser, splitLines, splitSSEFrames } from "./incremental-stream-parser.js";
58
- export type { StreamSplitResult, StreamParseResult } from "./incremental-stream-parser.js";
53
+ export {
54
+ IncrementalStreamParser,
55
+ splitLines,
56
+ splitSSEFrames,
57
+ parseSseJsonFrame,
58
+ createSseJsonParser,
59
+ parseChatCompletionsDataLine,
60
+ createChatCompletionsSseParser,
61
+ createNdjsonLineParser,
62
+ } from "./incremental-stream-parser.js";
63
+ export type { StreamSplitResult, StreamParseResult, SseJsonEvent } from "./incremental-stream-parser.js";
64
+
65
+ export { openProviderJsonStream, iterateProviderStreamBatches, createCompletionGate } from "./provider-stream.js";
66
+ export type {
67
+ OpenProviderJsonStreamOptions,
68
+ OpenedProviderStream,
69
+ ProviderStreamBatch,
70
+ ProviderStreamBatchOptions,
71
+ } from "./provider-stream.js";
59
72
 
60
73
  export { NormalizedRequestMapper } from "./request-mapper.js";
61
- export type { ProviderProfile } from "./request-mapper.js";
@@ -12,7 +12,6 @@
12
12
  import type {
13
13
  StopReason,
14
14
  ContentBlock,
15
- InstructionBlock,
16
15
  MessageItem,
17
16
  ReasoningItem,
18
17
  ToolCallItem,
@@ -100,13 +99,12 @@ export function reasoningItem(
100
99
  };
101
100
  }
102
101
 
103
- export function toolCallItem(id: string, name: string, argumentsText: string, argumentsJson?: unknown): ToolCallItem {
102
+ export function toolCallItem(id: string, name: string, argumentsText: string): ToolCallItem {
104
103
  return {
105
104
  type: "tool_call",
106
105
  id,
107
106
  name,
108
107
  argumentsText,
109
- argumentsJson,
110
108
  };
111
109
  }
112
110
 
@@ -179,13 +177,6 @@ export function contentBlocksToText(blocks: ContentBlock[]): string {
179
177
  return blocks.map(blockToText).join("\n");
180
178
  }
181
179
 
182
- /**
183
- * 将 instructions(string | InstructionBlock[])归一化为纯文本。
184
- */
185
- export function instructionsToText(instructions: string | InstructionBlock[]): string {
186
- return typeof instructions === "string" ? instructions : contentBlocksToText(instructions);
187
- }
188
-
189
180
  // ── Output 文本提取 ───────────────────────────────────────────
190
181
 
191
182
  /**
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Provider HTTP 流公共脚手架
3
+ *
4
+ * 收敛 adapter 间重复的:
5
+ * - JSON POST + 错误映射
6
+ * - ReadableStream reader 生命周期
7
+ * - IncrementalStreamParser feed/flush + malformed warning
8
+ * - 不完整尾帧 warning
9
+ */
10
+
11
+ import { AIProviderError, AIStreamError } from "../core/errors.js";
12
+ import type { EventFactory } from "../core/event-factory.js";
13
+ import type { AIStreamEvent, FetchFn } from "../types/index.js";
14
+ import { emitMalformedStreamWarning } from "./adapter-auxiliary.js";
15
+ import { providerHttpError } from "./adapter-security.js";
16
+ import type { IncrementalStreamParser } from "./incremental-stream-parser.js";
17
+
18
+ export type OpenProviderJsonStreamOptions = {
19
+ fetchFn: FetchFn;
20
+ url: string;
21
+ headers: Record<string, string>;
22
+ body: unknown;
23
+ signal?: AbortSignal;
24
+ };
25
+
26
+ export type OpenedProviderStream = {
27
+ reader: ReadableStreamDefaultReader<Uint8Array>;
28
+ headers: Headers;
29
+ };
30
+
31
+ /** POST JSON 并返回可读 body reader + response headers;统一网络/HTTP/空 body 错误。 */
32
+ export async function openProviderJsonStream(options: OpenProviderJsonStreamOptions): Promise<OpenedProviderStream> {
33
+ const { fetchFn, url, headers, body, signal } = options;
34
+
35
+ let response: Response;
36
+ try {
37
+ response = await fetchFn(url, {
38
+ method: "POST",
39
+ headers,
40
+ body: JSON.stringify(body),
41
+ signal,
42
+ });
43
+ } catch (err) {
44
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
45
+ }
46
+
47
+ if (!response.ok) {
48
+ const errorBody = await response.text().catch(() => "");
49
+ throw providerHttpError(response.status, errorBody);
50
+ }
51
+
52
+ const bodyStream = response.body;
53
+ if (!bodyStream) {
54
+ throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
55
+ }
56
+
57
+ // Bun/DOM ReadableStreamDefaultReader 类型略有差异,按最小接口使用
58
+ return {
59
+ reader: bodyStream.getReader() as ReadableStreamDefaultReader<Uint8Array>,
60
+ headers: response.headers,
61
+ };
62
+ }
63
+
64
+ export type ProviderStreamBatchOptions<T> = {
65
+ reader: ReadableStreamDefaultReader<Uint8Array>;
66
+ parser: IncrementalStreamParser<T>;
67
+ factory: EventFactory;
68
+ providerLabel: string;
69
+ transportLabel: string;
70
+ incompleteMessage: string;
71
+ };
72
+
73
+ export type ProviderStreamBatch<T> = {
74
+ items: T[];
75
+ warnings: AIStreamEvent[];
76
+ };
77
+
78
+ /**
79
+ * 读取并解析 provider 流。
80
+ * 每个 batch 携带本轮解析出的 items 与(可选)malformed / incomplete warning。
81
+ * 调用方应 `for await` 消费完毕;reader 在迭代结束时 cancel/release。
82
+ */
83
+ export async function* iterateProviderStreamBatches<T>(
84
+ options: ProviderStreamBatchOptions<T>,
85
+ ): AsyncGenerator<ProviderStreamBatch<T>, void, undefined> {
86
+ const { reader, parser, factory, providerLabel, transportLabel, incompleteMessage } = options;
87
+ let streamDone = false;
88
+
89
+ try {
90
+ while (true) {
91
+ const readResult = await reader.read().catch((err: unknown) => {
92
+ throw new AIStreamError(
93
+ `Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`,
94
+ "STREAM_ERROR",
95
+ );
96
+ });
97
+ const { done, value } = readResult;
98
+ const { items, malformed } = done ? parser.flush() : parser.feed(value as Uint8Array);
99
+
100
+ const warnings: AIStreamEvent[] = [];
101
+ const malformedWarning = emitMalformedStreamWarning(factory, {
102
+ count: malformed,
103
+ providerLabel,
104
+ transportLabel,
105
+ });
106
+ if (malformedWarning) warnings.push(malformedWarning);
107
+
108
+ yield { items, warnings };
109
+
110
+ if (done) {
111
+ streamDone = true;
112
+ break;
113
+ }
114
+ }
115
+ } finally {
116
+ try {
117
+ if (!streamDone) await reader.cancel().catch(() => undefined);
118
+ } finally {
119
+ reader.releaseLock();
120
+ }
121
+ }
122
+
123
+ if (parser.getRemaining().trim().length > 0) {
124
+ yield {
125
+ items: [],
126
+ warnings: [factory.responseWarning(incompleteMessage, "STREAM_ERROR")],
127
+ };
128
+ }
129
+ }
130
+
131
+ /** 一次性 complete 守卫:首次成功,后续返回 false。 */
132
+ export function createCompletionGate(): {
133
+ readonly completed: boolean;
134
+ tryComplete(): boolean;
135
+ } {
136
+ let completed = false;
137
+ return {
138
+ get completed() {
139
+ return completed;
140
+ },
141
+ tryComplete() {
142
+ if (completed) return false;
143
+ completed = true;
144
+ return true;
145
+ },
146
+ };
147
+ }
@@ -1,18 +1,10 @@
1
1
  import { AIRequestError } from "../core/errors.js";
2
2
  import { contentBlocksToText } from "./mapping.js";
3
3
 
4
- import type { AdapterCapabilities, ContentBlock, InstructionBlock, ToolResultItem } from "../types/index.js";
5
-
6
- export type ProviderProfile = {
7
- readonly kind: string;
8
- readonly instructionsMode: "system_message" | "instructions_field" | "none";
9
- readonly supportedBlockTypes: ReadonlyArray<ContentBlock["type"]>;
10
- readonly reasoningBlockTypes: ReadonlyArray<ContentBlock["type"]>;
11
- readonly capabilities: AdapterCapabilities;
12
- };
4
+ import type { ContentBlock, InstructionBlock, ToolCallItem, ToolChoice, ToolDefinition } from "../types/index.js";
13
5
 
14
6
  export class NormalizedRequestMapper {
15
- constructor(readonly profile: ProviderProfile) {}
7
+ constructor(readonly kind: string) {}
16
8
 
17
9
  mapInstructions(instructions: string | InstructionBlock[]): string {
18
10
  return typeof instructions === "string"
@@ -21,27 +13,33 @@ export class NormalizedRequestMapper {
21
13
  }
22
14
 
23
15
  ensureTextBlocks(blocks: ContentBlock[], field: string): ContentBlock[] {
24
- return this.ensureBlocks(blocks, field, this.profile.supportedBlockTypes, "only text/json blocks are supported");
16
+ return this.ensureBlocks(blocks, field, ["text", "json"], "only text/json blocks are supported");
25
17
  }
26
18
 
27
19
  ensureReasoningBlocks(blocks: ContentBlock[], field: string): Array<Extract<ContentBlock, { type: "text" }>> {
28
- return this.ensureBlocks(
29
- blocks,
30
- field,
31
- this.profile.reasoningBlockTypes,
32
- "reasoning only supports text blocks",
33
- ) as Array<Extract<ContentBlock, { type: "text" }>>;
20
+ return this.ensureBlocks(blocks, field, ["text"], "reasoning only supports text blocks") as Array<
21
+ Extract<ContentBlock, { type: "text" }>
22
+ >;
23
+ }
24
+
25
+ /** ensureTextBlocks + contentBlocksToText 的常见组合。 */
26
+ textFromBlocks(blocks: ContentBlock[], field: string): string {
27
+ return contentBlocksToText(this.ensureTextBlocks(blocks, field));
34
28
  }
35
29
 
36
- assertToolResultOutcome(outcome: ToolResultItem["outcome"]): void {
37
- if (this.profile.capabilities.toolResultOutcomes.includes(outcome)) return;
30
+ parseToolArguments(item: ToolCallItem): Record<string, unknown> {
31
+ try {
32
+ const parsed: unknown = JSON.parse(item.argumentsText);
33
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
34
+ return parsed as Record<string, unknown>;
35
+ }
36
+ } catch {
37
+ // handled below
38
+ }
38
39
 
39
- const outcomes = this.profile.capabilities.toolResultOutcomes;
40
- const supported = outcomes.map((value) => `"${value}"`).join(" and ");
41
- const verb = outcomes.length > 1 ? "are" : "is";
42
40
  throw new AIRequestError(
43
- `${this.profile.kind} does not preserve tool_result outcome "${outcome}"; only ${supported} ${verb} supported`,
44
- "UNSUPPORTED_TOOL_RESULT_OUTCOME",
41
+ `${this.kind} requires tool_call argumentsText to be a valid JSON object`,
42
+ "TOOL_CALL_ARGUMENTS_INVALID",
45
43
  );
46
44
  }
47
45
 
@@ -51,6 +49,30 @@ export class NormalizedRequestMapper {
51
49
  }
52
50
  }
53
51
 
52
+ mapToolsIfPresent<T>(tools: ToolDefinition[] | undefined, map: (tool: ToolDefinition) => T): T[] | undefined {
53
+ if (!tools || tools.length === 0) return undefined;
54
+ return tools.map(map);
55
+ }
56
+
57
+ /**
58
+ * 将 canonical toolChoice 映射为 provider 形状。
59
+ * 返回 undefined 表示调用方无需写入 body 字段。
60
+ */
61
+ mapToolChoice<T>(
62
+ toolChoice: ToolChoice | undefined,
63
+ mappers: {
64
+ auto: T;
65
+ none: T;
66
+ tool: (name: string) => T;
67
+ },
68
+ ): T | undefined {
69
+ if (!toolChoice) return undefined;
70
+ if (toolChoice === "auto") return mappers.auto;
71
+ if (toolChoice === "none") return mappers.none;
72
+ if (toolChoice.type === "tool") return mappers.tool(toolChoice.name);
73
+ return undefined;
74
+ }
75
+
54
76
  private ensureBlocks(
55
77
  blocks: ContentBlock[],
56
78
  field: string,
@@ -61,7 +83,7 @@ export class NormalizedRequestMapper {
61
83
  const block = blocks[i];
62
84
  if (block && !supportedTypes.includes(block.type)) {
63
85
  throw new AIRequestError(
64
- `${this.profile.kind} does not support ${field}[${i}] of type "${block.type}"; ${description}`,
86
+ `${this.kind} does not support ${field}[${i}] of type "${block.type}"; ${description}`,
65
87
  "UNSUPPORTED_CONTENT_BLOCK",
66
88
  );
67
89
  }
@@ -20,6 +20,26 @@ function record(obj: Record<string, number | undefined>): Partial<Usage> {
20
20
  return out;
21
21
  }
22
22
 
23
+ function withDerivedTotal(usage: {
24
+ inputTokens?: number;
25
+ outputTokens?: number;
26
+ totalTokens?: number;
27
+ cachedInputTokens?: number;
28
+ reasoningTokens?: number;
29
+ cacheWriteInputTokens?: number;
30
+ }): Partial<Usage> {
31
+ const { inputTokens, outputTokens, totalTokens, cachedInputTokens, reasoningTokens, cacheWriteInputTokens } = usage;
32
+ return record({
33
+ inputTokens,
34
+ outputTokens,
35
+ totalTokens:
36
+ totalTokens ?? (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined),
37
+ cachedInputTokens,
38
+ reasoningTokens,
39
+ cacheWriteInputTokens,
40
+ });
41
+ }
42
+
23
43
  /** OpenAI Chat Completions `usage` */
24
44
  export function usageFromChatCompletions(raw: {
25
45
  prompt_tokens?: number;
@@ -28,20 +48,12 @@ export function usageFromChatCompletions(raw: {
28
48
  prompt_tokens_details?: { cached_tokens?: number; [key: string]: unknown };
29
49
  completion_tokens_details?: { reasoning_tokens?: number; [key: string]: unknown };
30
50
  }): Partial<Usage> {
31
- const inputTokens = num(raw.prompt_tokens);
32
- const outputTokens = num(raw.completion_tokens);
33
- const cachedInputTokens = num(raw.prompt_tokens_details?.cached_tokens);
34
- const reasoningTokens = num(raw.completion_tokens_details?.reasoning_tokens);
35
- const totalTokens =
36
- num(raw.total_tokens) ??
37
- (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
38
-
39
- return record({
40
- inputTokens,
41
- outputTokens,
42
- totalTokens,
43
- cachedInputTokens,
44
- reasoningTokens,
51
+ return withDerivedTotal({
52
+ inputTokens: num(raw.prompt_tokens),
53
+ outputTokens: num(raw.completion_tokens),
54
+ totalTokens: num(raw.total_tokens),
55
+ cachedInputTokens: num(raw.prompt_tokens_details?.cached_tokens),
56
+ reasoningTokens: num(raw.completion_tokens_details?.reasoning_tokens),
45
57
  });
46
58
  }
47
59
 
@@ -54,20 +66,12 @@ export function usageFromOpenAIResponses(raw: {
54
66
  output_tokens_details?: { reasoning_tokens?: number; [key: string]: unknown };
55
67
  [key: string]: unknown;
56
68
  }): Partial<Usage> {
57
- const inputTokens = num(raw.input_tokens);
58
- const outputTokens = num(raw.output_tokens);
59
- const cachedInputTokens = num(raw.input_tokens_details?.cached_tokens);
60
- const reasoningTokens = num(raw.output_tokens_details?.reasoning_tokens);
61
- const totalTokens =
62
- num(raw.total_tokens) ??
63
- (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
64
-
65
- return record({
66
- inputTokens,
67
- outputTokens,
68
- totalTokens,
69
- cachedInputTokens,
70
- reasoningTokens,
69
+ return withDerivedTotal({
70
+ inputTokens: num(raw.input_tokens),
71
+ outputTokens: num(raw.output_tokens),
72
+ totalTokens: num(raw.total_tokens),
73
+ cachedInputTokens: num(raw.input_tokens_details?.cached_tokens),
74
+ reasoningTokens: num(raw.output_tokens_details?.reasoning_tokens),
71
75
  });
72
76
  }
73
77
 
@@ -88,12 +92,10 @@ export function usageFromAnthropicMessages(raw: {
88
92
  (n): n is number => n !== undefined,
89
93
  );
90
94
  const inputTokens = inputParts.length > 0 ? inputParts.reduce((sum, n) => sum + n, 0) : undefined;
91
- const totalTokens = inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined;
92
95
 
93
- return record({
96
+ return withDerivedTotal({
94
97
  inputTokens,
95
98
  outputTokens,
96
- totalTokens,
97
99
  cachedInputTokens,
98
100
  cacheWriteInputTokens,
99
101
  });
@@ -101,13 +103,8 @@ export function usageFromAnthropicMessages(raw: {
101
103
 
102
104
  /** Ollama 流式 chunk */
103
105
  export function usageFromOllama(raw: { prompt_eval_count?: number; eval_count?: number }): Partial<Usage> {
104
- const inputTokens = num(raw.prompt_eval_count);
105
- const outputTokens = num(raw.eval_count);
106
- const totalTokens = inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined;
107
-
108
- return record({
109
- inputTokens,
110
- outputTokens,
111
- totalTokens,
106
+ return withDerivedTotal({
107
+ inputTokens: num(raw.prompt_eval_count),
108
+ outputTokens: num(raw.eval_count),
112
109
  });
113
110
  }
@@ -21,20 +21,9 @@ export type NormalizedRequest = AIRequest & {
21
21
 
22
22
  // ── Adapter 接口 ──────────────────────────────────────────────
23
23
 
24
- export type StreamingCapability = "native" | "synthetic" | "none";
25
-
26
- export type AdapterCapabilities = {
27
- readonly textStreaming: StreamingCapability;
28
- readonly reasoningStreaming: StreamingCapability;
29
- readonly toolCallStreaming: StreamingCapability;
30
- readonly replay: "canonical" | "opaque" | "none";
31
- readonly usage: "stream" | "final" | "none";
32
- readonly toolResultOutcomes: ReadonlyArray<"success" | "error" | "rejected">;
33
- };
34
-
35
24
  export interface BackendAdapter {
36
25
  readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
37
- readonly capabilities: AdapterCapabilities;
26
+ readonly isSyntheticStream: boolean;
38
27
  stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
39
28
  }
40
29
 
@@ -46,12 +46,4 @@ export type {
46
46
  } from "./events.js";
47
47
 
48
48
  // Adapter 协议和 client 类型
49
- export type {
50
- AdapterCapabilities,
51
- StreamingCapability,
52
- BackendAdapter,
53
- FetchFn,
54
- NormalizedRequest,
55
- CreateAIClientOptions,
56
- AIClient,
57
- } from "./adapter.js";
49
+ export type { BackendAdapter, FetchFn, NormalizedRequest, CreateAIClientOptions, AIClient } from "./adapter.js";
@@ -27,7 +27,6 @@ export type ToolCallItem = {
27
27
  id: string;
28
28
  name: string;
29
29
  argumentsText: string;
30
- argumentsJson?: unknown;
31
30
  };
32
31
 
33
32
  export type ToolResultItem = {
@@ -1,113 +0,0 @@
1
- /**
2
- * 通用 SSE (Server-Sent Events) 解析器
3
- *
4
- * 解析标准 SSE 格式(event: + data: 行),适用于:
5
- * - Anthropic Messages API (messages.ts)
6
- * - OpenAI Responses API (responses.ts)
7
- *
8
- * 注意:OpenAI Chat Completions API 使用简化 SSE(仅有 data: 行),
9
- * 由 chat-completions.ts 中的 parseChatSSE 处理。
10
- *
11
- * 用法:
12
- * ```ts
13
- * const { events, rest } = parseSSEEvents(buffer);
14
- * for (const ev of events) {
15
- * // ev.type — 事件类型字符串
16
- * // ev.data — 已解析的 JSON 数据
17
- * }
18
- * // rest 是未处理的剩余 buffer,需要累积到下次调用
19
- * ```
20
- */
21
-
22
- export type SSEEvent = { type: string; data: unknown };
23
-
24
- export type SSEParseResult = {
25
- events: SSEEvent[];
26
- rest: string;
27
- malformedEvents: number;
28
- };
29
-
30
- export type ParseSSEOptions = {
31
- allowEOF?: boolean;
32
- };
33
-
34
- /**
35
- * 将 SSE 文本块解析为事件数组。
36
- * 累积事件行直到遇到空行,支持 [DONE] 标记。
37
- * 返回已解析的事件和未处理的剩余 buffer(用于增量解析)。
38
- *
39
- * 关键行为:
40
- * - 只解析完整的 event(以空行结尾)
41
- * - 未完成的行保留在 rest 中,等待下次 chunk 补全
42
- * - 支持跨 chunk 的 event 分片
43
- */
44
- export function parseSSEEvents(chunk: string, options: ParseSSEOptions = {}): SSEParseResult {
45
- const events: SSEEvent[] = [];
46
- let eventType = "";
47
- let dataLines: string[] = [];
48
- let consumedUntil = 0;
49
- let cursor = 0;
50
- let malformedEvents = 0;
51
-
52
- const emitEvent = (consumedCursor: number): void => {
53
- const dataStr = dataLines.join("\n");
54
- if (dataStr === "[DONE]") {
55
- eventType = "";
56
- dataLines = [];
57
- consumedUntil = consumedCursor;
58
- return;
59
- }
60
-
61
- try {
62
- const data = JSON.parse(dataStr);
63
- events.push({ type: eventType, data });
64
- } catch {
65
- malformedEvents++;
66
- }
67
-
68
- eventType = "";
69
- dataLines = [];
70
- consumedUntil = consumedCursor;
71
- };
72
-
73
- const consumeLine = (line: string, consumedCursor: number): void => {
74
- if (line.startsWith("event: ")) {
75
- eventType = line.slice(7).trim();
76
- } else if (line.startsWith("data: ")) {
77
- dataLines.push(line.slice(6));
78
- } else if (line === "" && eventType && dataLines.length > 0) {
79
- emitEvent(consumedCursor);
80
- } else if (line === "" && !eventType && dataLines.length === 0) {
81
- consumedUntil = consumedCursor;
82
- }
83
- };
84
-
85
- while (cursor < chunk.length) {
86
- const lineEnd = chunk.indexOf("\n", cursor);
87
- if (lineEnd === -1) break;
88
-
89
- let line = chunk.slice(cursor, lineEnd);
90
- cursor = lineEnd + 1;
91
-
92
- if (line.endsWith("\r")) {
93
- line = line.slice(0, -1);
94
- }
95
-
96
- consumeLine(line, cursor);
97
- }
98
-
99
- if (options.allowEOF && cursor < chunk.length) {
100
- let line = chunk.slice(cursor);
101
- if (line.endsWith("\r")) {
102
- line = line.slice(0, -1);
103
- }
104
- consumeLine(line, chunk.length);
105
- cursor = chunk.length;
106
- }
107
-
108
- if (options.allowEOF && eventType && dataLines.length > 0) {
109
- emitEvent(chunk.length);
110
- }
111
-
112
- return { events, rest: chunk.slice(consumedUntil), malformedEvents };
113
- }