@codehz/ai 0.1.2 → 0.1.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.
@@ -28,7 +28,7 @@ import {
28
28
  } from "../helpers/mapping.js";
29
29
  import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
30
30
 
31
- import type { AdapterCapabilities, NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
31
+ import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
32
32
 
33
33
  // ── 选项类型 ──────────────────────────────────────────────────
34
34
 
@@ -207,37 +207,29 @@ function rollbackTrailingAssistantMessages(messages: OllamaMessage[]): void {
207
207
  }
208
208
 
209
209
  function isOllamaToolCalls(value: unknown): value is OllamaToolCall[] {
210
- return Array.isArray(value) && value.every((entry) => {
211
- if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
212
- const fn = (entry as { function?: unknown }).function;
213
- return (
214
- !!fn &&
215
- typeof fn === "object" &&
216
- "name" in fn &&
217
- typeof (fn as { name?: unknown }).name === "string" &&
218
- "arguments" in fn &&
219
- typeof (fn as { arguments?: unknown }).arguments === "object" &&
220
- (fn as { arguments?: unknown }).arguments !== null
221
- );
222
- });
210
+ return (
211
+ Array.isArray(value) &&
212
+ value.every((entry) => {
213
+ if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
214
+ const fn = (entry as { function?: unknown }).function;
215
+ return (
216
+ !!fn &&
217
+ typeof fn === "object" &&
218
+ "name" in fn &&
219
+ typeof (fn as { name?: unknown }).name === "string" &&
220
+ "arguments" in fn &&
221
+ typeof (fn as { arguments?: unknown }).arguments === "object" &&
222
+ (fn as { arguments?: unknown }).arguments !== null
223
+ );
224
+ })
225
+ );
223
226
  }
224
227
 
225
228
  // ── Adapter ───────────────────────────────────────────────────
226
229
 
227
230
  export class OllamaAdapter extends AdapterBase {
228
231
  readonly kind = "ollama" as const;
229
- readonly capabilities: AdapterCapabilities = {
230
- nativeStreaming: true,
231
- messageStreaming: true,
232
- reasoningStreaming: false,
233
- toolCallStreaming: false,
234
- hiddenReasoningReplay: "none" as const,
235
- replayFidelity: "low" as const,
236
- tools: true,
237
- usage: "partial" as const,
238
- billing: "none" as const,
239
- providerMetadata: false,
240
- };
232
+ readonly nativeStreaming = true;
241
233
 
242
234
  private baseUrl: string;
243
235
  private apiKey: string | undefined;
@@ -275,7 +267,10 @@ export class OllamaAdapter extends AdapterBase {
275
267
  : item.role === "user"
276
268
  ? "user"
277
269
  : "assistant";
278
- messages.push({ role, content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`)) });
270
+ messages.push({
271
+ role,
272
+ content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`)),
273
+ });
279
274
  break;
280
275
  }
281
276
  case "tool_call": {
@@ -312,7 +307,12 @@ export class OllamaAdapter extends AdapterBase {
312
307
  }
313
308
  case "opaque": {
314
309
  // Best-effort restore from opaque replay
315
- if (item.source === "ollama" && item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
310
+ if (
311
+ item.source === "ollama" &&
312
+ item.purpose === "replay" &&
313
+ typeof item.payload === "object" &&
314
+ item.payload !== null
315
+ ) {
316
316
  const payload = item.payload as Record<string, unknown>;
317
317
  if (payload.role === "assistant" && typeof payload.content === "string") {
318
318
  rollbackTrailingAssistantMessages(messages);
@@ -406,7 +406,6 @@ export class OllamaAdapter extends AdapterBase {
406
406
 
407
407
  try {
408
408
  while (true) {
409
- // oxlint-disable-next-line no-await-in-loop
410
409
  const { done, value } = await reader.read();
411
410
  if (done) break;
412
411
 
@@ -489,9 +488,10 @@ export class OllamaAdapter extends AdapterBase {
489
488
  {
490
489
  inputTokens: chunk.prompt_eval_count,
491
490
  outputTokens: chunk.eval_count,
492
- totalTokens: chunk.prompt_eval_count !== undefined && chunk.eval_count !== undefined
493
- ? chunk.prompt_eval_count + chunk.eval_count
494
- : undefined,
491
+ totalTokens:
492
+ chunk.prompt_eval_count !== undefined && chunk.eval_count !== undefined
493
+ ? chunk.prompt_eval_count + chunk.eval_count
494
+ : undefined,
495
495
  },
496
496
  "final",
497
497
  {
@@ -520,7 +520,6 @@ export class OllamaAdapter extends AdapterBase {
520
520
  );
521
521
  }
522
522
 
523
- // oxlint-disable-next-line no-await-in-loop
524
523
  const auxiliaryResult = await auxiliary.finalize(factory);
525
524
  for (const event of auxiliaryResult.events) {
526
525
  yield event;
@@ -25,7 +25,6 @@ import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
25
25
 
26
26
  import { parseSSEEvents } from "../helpers/sse-parser.js";
27
27
 
28
- import { CAPABILITY_MATRIX } from "../index.js";
29
28
  import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
30
29
 
31
30
  // ── 类型 ──────────────────────────────────────────────────────
@@ -165,9 +164,7 @@ function parseSSE(chunk: string): { events: ResponsesSSEEvent[]; rest: string; m
165
164
 
166
165
  function isReplayCanonicalInput(item: ResponsesInputItem): boolean {
167
166
  return (
168
- (item.type === "message" && item.role === "assistant") ||
169
- item.type === "reasoning" ||
170
- item.type === "function_call"
167
+ (item.type === "message" && item.role === "assistant") || item.type === "reasoning" || item.type === "function_call"
171
168
  );
172
169
  }
173
170
 
@@ -194,7 +191,7 @@ function canonicalToResponsesBlock(b: import("../index.js").ContentBlock): Respo
194
191
 
195
192
  export class ResponsesAdapter extends AdapterBase {
196
193
  readonly kind = "responses" as const;
197
- readonly capabilities = CAPABILITY_MATRIX.responses;
194
+ readonly nativeStreaming = true;
198
195
 
199
196
  private apiKey: string;
200
197
  private baseUrl: string;
@@ -453,9 +450,9 @@ export class ResponsesAdapter extends AdapterBase {
453
450
  if (completedResponse.usage) {
454
451
  auxiliary.recordUsage(
455
452
  {
456
- inputTokens: completedResponse.usage.input_tokens,
457
- outputTokens: completedResponse.usage.output_tokens,
458
- totalTokens: completedResponse.usage.total_tokens,
453
+ inputTokens: completedResponse.usage.input_tokens,
454
+ outputTokens: completedResponse.usage.output_tokens,
455
+ totalTokens: completedResponse.usage.total_tokens,
459
456
  },
460
457
  "final",
461
458
  completedResponse.usage,
@@ -122,10 +122,20 @@ function validateInputItem(item: unknown, field: string, issues: ValidationIssue
122
122
  return;
123
123
  case "tool_result":
124
124
  if (typeof item.callId !== "string" || item.callId.length === 0) {
125
- pushIssue(issues, `${field}.callId`, "TOOL_RESULT_CALL_ID_INVALID", `${field}.callId must be a non-empty string`);
125
+ pushIssue(
126
+ issues,
127
+ `${field}.callId`,
128
+ "TOOL_RESULT_CALL_ID_INVALID",
129
+ `${field}.callId must be a non-empty string`,
130
+ );
126
131
  }
127
132
  if (typeof item.toolName !== "string" || item.toolName.length === 0) {
128
- pushIssue(issues, `${field}.toolName`, "TOOL_RESULT_NAME_INVALID", `${field}.toolName must be a non-empty string`);
133
+ pushIssue(
134
+ issues,
135
+ `${field}.toolName`,
136
+ "TOOL_RESULT_NAME_INVALID",
137
+ `${field}.toolName must be a non-empty string`,
138
+ );
129
139
  }
130
140
  if (typeof item.outcome !== "string" || !TOOL_RESULT_OUTCOMES.has(item.outcome)) {
131
141
  pushIssue(
@@ -180,12 +190,7 @@ function validateTools(tools: unknown, issues: ValidationIssue[]): void {
180
190
  }
181
191
 
182
192
  if (!isRecord(tool.inputSchema)) {
183
- pushIssue(
184
- issues,
185
- `${field}.inputSchema`,
186
- "TOOL_INPUT_SCHEMA_INVALID",
187
- `${field}.inputSchema must be an object`,
188
- );
193
+ pushIssue(issues, `${field}.inputSchema`, "TOOL_INPUT_SCHEMA_INVALID", `${field}.inputSchema must be an object`);
189
194
  }
190
195
  }
191
196
  }
@@ -193,8 +198,13 @@ function validateTools(tools: unknown, issues: ValidationIssue[]): void {
193
198
  function validateToolChoice(toolChoice: unknown, issues: ValidationIssue[]): void {
194
199
  if (toolChoice === undefined) return;
195
200
  if (toolChoice === "auto" || toolChoice === "none") return;
196
- if (!isRecord(toolChoice) || toolChoice.type !== "tool" || typeof toolChoice.name !== "string" || toolChoice.name.length === 0) {
197
- pushIssue(issues, "toolChoice", "TOOL_CHOICE_INVALID", "toolChoice must be auto, none, or { type: \"tool\", name }");
201
+ if (
202
+ !isRecord(toolChoice) ||
203
+ toolChoice.type !== "tool" ||
204
+ typeof toolChoice.name !== "string" ||
205
+ toolChoice.name.length === 0
206
+ ) {
207
+ pushIssue(issues, "toolChoice", "TOOL_CHOICE_INVALID", 'toolChoice must be auto, none, or { type: "tool", name }');
198
208
  }
199
209
  }
200
210
 
@@ -211,12 +221,7 @@ export function validateRequest(request: AIRequest): ValidationIssue[] {
211
221
  } else if (Array.isArray(request.instructions)) {
212
222
  validateContentArray(request.instructions, "instructions", issues, "INSTRUCTIONS_INVALID");
213
223
  } else {
214
- pushIssue(
215
- issues,
216
- "instructions",
217
- "INSTRUCTIONS_INVALID",
218
- "instructions must be a string or ContentBlock[]",
219
- );
224
+ pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or ContentBlock[]");
220
225
  }
221
226
  }
222
227
 
@@ -1,7 +1,6 @@
1
1
  import { WarningCode } from "../core/errors.js";
2
2
  import type { EventFactory } from "../core/event-factory.js";
3
3
  import type {
4
- AdapterCapabilities,
5
4
  AIStreamEvent,
6
5
  BillingInfo,
7
6
  NormalizedRequest,
@@ -18,7 +17,6 @@ export type BillingPostprocessHook = (context: {
18
17
  usage?: Usage;
19
18
  billing?: BillingInfo;
20
19
  auxiliary?: AuxiliaryInfo;
21
- capabilities: AdapterCapabilities;
22
20
  }) => MaybePromise<Partial<BillingInfo> | undefined>;
23
21
 
24
22
  export type AuxiliaryFinalizeOptions = {
@@ -41,10 +39,7 @@ export class AdapterAuxiliaryState {
41
39
  private readonly collector = new AuxiliaryCollector();
42
40
  private readonly metadataSources = new Set<string>();
43
41
 
44
- constructor(
45
- private readonly request: NormalizedRequest,
46
- private readonly capabilities: AdapterCapabilities,
47
- ) {}
42
+ constructor(private readonly request: NormalizedRequest) {}
48
43
 
49
44
  recordUsage(usage: Partial<Usage>, source: UsageSource, raw?: unknown): void {
50
45
  if (this.request.include?.usage === "off" || isEmptyRecord(usage)) return;
@@ -75,7 +70,6 @@ export class AdapterAuxiliaryState {
75
70
  usage: snapshot.usage,
76
71
  billing: snapshot.billing,
77
72
  auxiliary: snapshot.auxiliary,
78
- capabilities: this.capabilities,
79
73
  });
80
74
  if (derived && !isEmptyRecord(derived)) {
81
75
  this.collector.recordBilling(
@@ -105,7 +99,9 @@ export class AdapterAuxiliaryState {
105
99
  }
106
100
 
107
101
  if (this.request.include?.usage !== "off" && !built.usage) {
108
- events.push(factory.responseWarning("Usage information was not provided by the provider", WarningCode.USAGE_MISSING));
102
+ events.push(
103
+ factory.responseWarning("Usage information was not provided by the provider", WarningCode.USAGE_MISSING),
104
+ );
109
105
  }
110
106
 
111
107
  if (this.request.include?.billing !== "off") {
@@ -14,7 +14,6 @@
14
14
  import type {
15
15
  NormalizedRequest,
16
16
  BackendAdapter,
17
- AdapterCapabilities,
18
17
  AIStreamEvent,
19
18
  AIResponse,
20
19
  AuxiliaryInfo,
@@ -56,7 +55,7 @@ export type StreamResult = {
56
55
 
57
56
  export abstract class AdapterBase implements BackendAdapter {
58
57
  abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
59
- abstract readonly capabilities: AdapterCapabilities;
58
+ abstract readonly nativeStreaming: boolean;
60
59
 
61
60
  /**
62
61
  * stream 模板方法:
@@ -67,7 +66,7 @@ export abstract class AdapterBase implements BackendAdapter {
67
66
  async *stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent> {
68
67
  const factory = createEventFactory({
69
68
  responseId: request.requestId,
70
- backend: { kind: this.kind, isSynthetic: !this.capabilities.nativeStreaming },
69
+ backend: { kind: this.kind, isSynthetic: !this.nativeStreaming },
71
70
  });
72
71
 
73
72
  yield factory.responseStarted(request.model);
@@ -113,7 +112,10 @@ export abstract class AdapterBase implements BackendAdapter {
113
112
  protected buildResponse(request: NormalizedRequest, result: StreamResult, _factory: EventFactory): AIResponse {
114
113
  const text = this.extractText(result.output);
115
114
  const warnings = mergeWarnings(result.warnings, _factory.warnings);
116
- const auxiliary = mergeAuxiliary(result.auxiliary, result.providerMetadata ? { providerMetadata: result.providerMetadata } : undefined);
115
+ const auxiliary = mergeAuxiliary(
116
+ result.auxiliary,
117
+ result.providerMetadata ? { providerMetadata: result.providerMetadata } : undefined,
118
+ );
117
119
 
118
120
  return {
119
121
  id: request.requestId,
@@ -130,7 +132,7 @@ export abstract class AdapterBase implements BackendAdapter {
130
132
  requestId: request.requestId,
131
133
  rawResponseId: result.rawResponseId,
132
134
  adapter: this.kind,
133
- isSyntheticStream: !this.capabilities.nativeStreaming,
135
+ isSyntheticStream: !this.nativeStreaming,
134
136
  metadataSources: result.metadataSources,
135
137
  warnings,
136
138
  },
@@ -143,14 +145,11 @@ export abstract class AdapterBase implements BackendAdapter {
143
145
  }
144
146
 
145
147
  protected createAuxiliaryState(request: NormalizedRequest): AdapterAuxiliaryState {
146
- return new AdapterAuxiliaryState(request, this.capabilities);
148
+ return new AdapterAuxiliaryState(request);
147
149
  }
148
150
  }
149
151
 
150
- function mergeAuxiliary(
151
- base?: Partial<AuxiliaryInfo>,
152
- patch?: Partial<AuxiliaryInfo>,
153
- ): AuxiliaryInfo | undefined {
152
+ function mergeAuxiliary(base?: Partial<AuxiliaryInfo>, patch?: Partial<AuxiliaryInfo>): AuxiliaryInfo | undefined {
154
153
  if (!base && !patch) return undefined;
155
154
 
156
155
  const merged: AuxiliaryInfo = {
@@ -28,11 +28,7 @@ export type { SSEEvent } from "./sse-parser.js";
28
28
 
29
29
  export { AdapterBase } from "./adapter-base.js";
30
30
  export type { StreamResult } from "./adapter-base.js";
31
- export {
32
- AdapterAuxiliaryState,
33
- emitMalformedStreamWarning,
34
- metadataSourceList,
35
- } from "./adapter-auxiliary.js";
31
+ export { AdapterAuxiliaryState, emitMalformedStreamWarning, metadataSourceList } from "./adapter-auxiliary.js";
36
32
  export type { AuxiliaryFinalizeOptions, AuxiliaryFinalizeResult, BillingPostprocessHook } from "./adapter-auxiliary.js";
37
33
  export { syntheticStream } from "./synthetic-stream.js";
38
34
  export type { SyntheticStreamOptions } from "./synthetic-stream.js";
@@ -2,7 +2,6 @@
2
2
  * BackendAdapter — adapter 内部协议和 client 公开类型
3
3
  *
4
4
  * adapter 对前台只暴露一个统一适配点。
5
- * 能力矩阵在此落成代码而非仅存在于文档。
6
5
  */
7
6
 
8
7
  import type { AIRequest } from "./request.js";
@@ -20,91 +19,11 @@ export type NormalizedRequest = AIRequest & {
20
19
  requestId: string;
21
20
  };
22
21
 
23
- // ── 能力矩阵 ──────────────────────────────────────────────────
24
-
25
- export type AdapterCapabilities = {
26
- nativeStreaming: boolean;
27
- messageStreaming: boolean;
28
- reasoningStreaming: boolean;
29
- toolCallStreaming: boolean;
30
- hiddenReasoningReplay: "full" | "partial" | "none";
31
- replayFidelity: "high" | "medium" | "low";
32
- tools: boolean;
33
- usage: "full" | "partial" | "none";
34
- billing: "direct" | "lookup" | "derived" | "none";
35
- providerMetadata: boolean;
36
- };
37
-
38
- // ── 能力矩阵常量(文档中的能力表在此落代码) ────────────────
39
-
40
- export const CAPABILITY_MATRIX = {
41
- responses: {
42
- nativeStreaming: true,
43
- messageStreaming: true,
44
- reasoningStreaming: true,
45
- toolCallStreaming: true,
46
- hiddenReasoningReplay: "full" as const,
47
- replayFidelity: "high" as const,
48
- tools: true,
49
- usage: "full" as const,
50
- billing: "lookup" as const,
51
- providerMetadata: true,
52
- },
53
- messages: {
54
- nativeStreaming: true,
55
- messageStreaming: true,
56
- reasoningStreaming: false, // 条件支持,默认 false
57
- toolCallStreaming: true,
58
- hiddenReasoningReplay: "partial" as const,
59
- replayFidelity: "medium" as const,
60
- tools: true,
61
- usage: "full" as const,
62
- billing: "lookup" as const,
63
- providerMetadata: true,
64
- },
65
- "chat.completions": {
66
- nativeStreaming: true,
67
- messageStreaming: true,
68
- reasoningStreaming: false,
69
- toolCallStreaming: false, // 中,默认 false
70
- hiddenReasoningReplay: "none" as const,
71
- replayFidelity: "low" as const,
72
- tools: true,
73
- usage: "full" as const,
74
- billing: "derived" as const,
75
- providerMetadata: false,
76
- },
77
- ollama: {
78
- nativeStreaming: true,
79
- messageStreaming: true,
80
- reasoningStreaming: false,
81
- toolCallStreaming: false,
82
- hiddenReasoningReplay: "none" as const,
83
- replayFidelity: "low" as const,
84
- tools: true,
85
- usage: "partial" as const,
86
- billing: "none" as const,
87
- providerMetadata: false,
88
- },
89
- mock: {
90
- nativeStreaming: false,
91
- messageStreaming: true,
92
- reasoningStreaming: false,
93
- toolCallStreaming: true,
94
- hiddenReasoningReplay: "none" as const,
95
- replayFidelity: "high" as const,
96
- tools: true,
97
- usage: "none" as const,
98
- billing: "none" as const,
99
- providerMetadata: true,
100
- },
101
- } as const satisfies Record<string, AdapterCapabilities>;
102
-
103
22
  // ── Adapter 接口 ──────────────────────────────────────────────
104
23
 
105
24
  export interface BackendAdapter {
106
25
  readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
107
- readonly capabilities: AdapterCapabilities;
26
+ readonly nativeStreaming: boolean;
108
27
  stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
109
28
  }
110
29
 
@@ -46,14 +46,4 @@ export type {
46
46
  } from "./events.js";
47
47
 
48
48
  // Adapter 协议和 client 类型
49
- export type {
50
- BackendAdapter,
51
- AdapterCapabilities,
52
- FetchFn,
53
- NormalizedRequest,
54
- CreateAIClientOptions,
55
- AIClient,
56
- } from "./adapter.js";
57
-
58
- // 能力矩阵常量
59
- export { CAPABILITY_MATRIX } from "./adapter.js";
49
+ export type { BackendAdapter, FetchFn, NormalizedRequest, CreateAIClientOptions, AIClient } from "./adapter.js";