@codehz/ai 0.1.5 → 0.1.7

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.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "module": "dist/index.mjs",
6
6
  "exports": {
@@ -21,6 +21,7 @@ import {
21
21
  contentBlocksToText,
22
22
  } from "../helpers/mapping.js";
23
23
  import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
24
+ import { usageFromChatCompletions } from "../helpers/usage-mapping.js";
24
25
 
25
26
  import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
26
27
 
@@ -73,7 +74,13 @@ type ChatChunk = {
73
74
  created: number;
74
75
  model: string;
75
76
  choices: ChatChunkChoice[];
76
- usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
77
+ usage?: {
78
+ prompt_tokens: number;
79
+ completion_tokens: number;
80
+ total_tokens: number;
81
+ prompt_tokens_details?: { cached_tokens?: number };
82
+ completion_tokens_details?: { reasoning_tokens?: number };
83
+ };
77
84
  };
78
85
 
79
86
  type ChatChunkChoice = {
@@ -485,15 +492,7 @@ export class ChatCompletionsAdapter extends AdapterBase {
485
492
 
486
493
  // usage 可能在最终 chunk 中
487
494
  if (chunk.usage) {
488
- auxiliary.recordUsage(
489
- {
490
- inputTokens: chunk.usage.prompt_tokens,
491
- outputTokens: chunk.usage.completion_tokens,
492
- totalTokens: chunk.usage.total_tokens,
493
- },
494
- "final",
495
- chunk.usage,
496
- );
495
+ auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
497
496
  }
498
497
 
499
498
  for (const choice of chunk.choices) {
@@ -24,6 +24,7 @@ import {
24
24
  contentBlocksToText,
25
25
  } from "../helpers/mapping.js";
26
26
  import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
27
+ import { usageFromAnthropicMessages } from "../helpers/usage-mapping.js";
27
28
 
28
29
  import { parseSSEEvents } from "../helpers/sse-parser.js";
29
30
 
@@ -131,7 +132,12 @@ type MessagesSSEEvent =
131
132
  type: "message_delta";
132
133
  data: {
133
134
  delta: { stop_reason?: string; stop_sequence?: string | null };
134
- usage: { input_tokens: number; output_tokens: number };
135
+ usage: {
136
+ input_tokens: number;
137
+ output_tokens: number;
138
+ cache_creation_input_tokens?: number;
139
+ cache_read_input_tokens?: number;
140
+ };
135
141
  };
136
142
  }
137
143
  | { type: "message_stop"; data: Record<string, never> }
@@ -267,6 +273,7 @@ export class MessagesAdapter extends AdapterBase {
267
273
  protected buildRequest(request: NormalizedRequest): MessagesAPIRequest {
268
274
  const messages: MessagesAPIMessage[] = [];
269
275
  let systemPrompt: string | undefined;
276
+ let pendingToolResultMessage: MessagesAPIMessage | undefined;
270
277
 
271
278
  // 处理 instructions → system prompt
272
279
  if (request.instructions) {
@@ -275,6 +282,10 @@ export class MessagesAdapter extends AdapterBase {
275
282
 
276
283
  // 处理 input items
277
284
  for (const item of request.input) {
285
+ if (item.type !== "tool_result") {
286
+ pendingToolResultMessage = undefined;
287
+ }
288
+
278
289
  switch (item.type) {
279
290
  case "message": {
280
291
  const role = item.role === "user" ? "user" : "assistant";
@@ -314,7 +325,12 @@ export class MessagesAdapter extends AdapterBase {
314
325
  content,
315
326
  is_error: item.outcome === "error",
316
327
  };
317
- messages.push({ role: "user", content: [block] });
328
+ if (pendingToolResultMessage && typeof pendingToolResultMessage.content !== "string") {
329
+ pendingToolResultMessage.content.push(block);
330
+ } else {
331
+ pendingToolResultMessage = { role: "user", content: [block] };
332
+ messages.push(pendingToolResultMessage);
333
+ }
318
334
  break;
319
335
  }
320
336
  case "reasoning": {
@@ -616,15 +632,7 @@ export class MessagesAdapter extends AdapterBase {
616
632
  stopSequence = sseEvent.data.delta.stop_sequence;
617
633
  const u = sseEvent.data.usage;
618
634
  if (u) {
619
- auxiliary.recordUsage(
620
- {
621
- inputTokens: u.input_tokens,
622
- outputTokens: u.output_tokens,
623
- totalTokens: u.input_tokens + u.output_tokens,
624
- },
625
- "stream",
626
- u,
627
- );
635
+ auxiliary.recordUsage(usageFromAnthropicMessages(u), "stream", u);
628
636
  }
629
637
  continue;
630
638
  }
@@ -27,6 +27,7 @@ import {
27
27
  contentBlocksToText,
28
28
  } from "../helpers/mapping.js";
29
29
  import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
30
+ import { usageFromOllama } from "../helpers/usage-mapping.js";
30
31
 
31
32
  import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
32
33
 
@@ -478,14 +479,10 @@ export class OllamaAdapter extends AdapterBase {
478
479
  (chunk.prompt_eval_count !== undefined || chunk.eval_count !== undefined)
479
480
  ) {
480
481
  auxiliary.recordUsage(
481
- {
482
- inputTokens: chunk.prompt_eval_count,
483
- outputTokens: chunk.eval_count,
484
- totalTokens:
485
- chunk.prompt_eval_count !== undefined && chunk.eval_count !== undefined
486
- ? chunk.prompt_eval_count + chunk.eval_count
487
- : undefined,
488
- },
482
+ usageFromOllama({
483
+ prompt_eval_count: chunk.prompt_eval_count,
484
+ eval_count: chunk.eval_count,
485
+ }),
489
486
  "final",
490
487
  {
491
488
  prompt_eval_count: chunk.prompt_eval_count,
@@ -22,6 +22,7 @@ import {
22
22
  contentBlocksToText,
23
23
  } from "../helpers/mapping.js";
24
24
  import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
25
+ import { usageFromOpenAIResponses } from "../helpers/usage-mapping.js";
25
26
 
26
27
  import { parseSSEEvents } from "../helpers/sse-parser.js";
27
28
 
@@ -448,15 +449,7 @@ export class ResponsesAdapter extends AdapterBase {
448
449
  if (completedResponse) {
449
450
  rawResponseId = completedResponse.id;
450
451
  if (completedResponse.usage) {
451
- auxiliary.recordUsage(
452
- {
453
- inputTokens: completedResponse.usage.input_tokens,
454
- outputTokens: completedResponse.usage.output_tokens,
455
- totalTokens: completedResponse.usage.total_tokens,
456
- },
457
- "final",
458
- completedResponse.usage,
459
- );
452
+ auxiliary.recordUsage(usageFromOpenAIResponses(completedResponse.usage), "final", completedResponse.usage);
460
453
  }
461
454
  }
462
455
 
@@ -34,3 +34,9 @@ export { syntheticStream } from "./synthetic-stream.js";
34
34
  export type { SyntheticStreamOptions } from "./synthetic-stream.js";
35
35
  export { AuxiliaryCollector } from "./auxiliary-collector.js";
36
36
  export type { UsageSource, BillingSource, LookupResult } from "./auxiliary-collector.js";
37
+ export {
38
+ usageFromAnthropicMessages,
39
+ usageFromChatCompletions,
40
+ usageFromOllama,
41
+ usageFromOpenAIResponses,
42
+ } from "./usage-mapping.js";
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Provider usage → canonical Usage 映射
3
+ *
4
+ * best-effort 提取 reasoning / cache / billable 等扩展字段。
5
+ */
6
+
7
+ import type { Usage } from "../types/index.js";
8
+
9
+ function num(value: unknown): number | undefined {
10
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
11
+ }
12
+
13
+ function record(obj: Record<string, number | undefined>): Partial<Usage> {
14
+ const out: Partial<Usage> = {};
15
+ for (const [key, value] of Object.entries(obj)) {
16
+ if (value !== undefined) {
17
+ (out as Record<string, number>)[key] = value;
18
+ }
19
+ }
20
+ return out;
21
+ }
22
+
23
+ function billableFromOpenAIStyle(
24
+ inputTokens: number | undefined,
25
+ outputTokens: number | undefined,
26
+ cachedInputTokens: number | undefined,
27
+ reasoningTokens: number | undefined,
28
+ ): Pick<Usage, "billableInputTokens" | "billableOutputTokens"> {
29
+ let billableInputTokens: number | undefined;
30
+ if (inputTokens !== undefined) {
31
+ billableInputTokens =
32
+ cachedInputTokens !== undefined ? Math.max(0, inputTokens - cachedInputTokens) : inputTokens;
33
+ }
34
+
35
+ let billableOutputTokens: number | undefined;
36
+ if (outputTokens !== undefined) {
37
+ billableOutputTokens =
38
+ reasoningTokens !== undefined ? Math.max(0, outputTokens - reasoningTokens) : outputTokens;
39
+ }
40
+
41
+ return record({ billableInputTokens, billableOutputTokens });
42
+ }
43
+
44
+ /** OpenAI Chat Completions `usage` */
45
+ export function usageFromChatCompletions(raw: {
46
+ prompt_tokens?: number;
47
+ completion_tokens?: number;
48
+ total_tokens?: number;
49
+ prompt_tokens_details?: { cached_tokens?: number; [key: string]: unknown };
50
+ completion_tokens_details?: { reasoning_tokens?: number; [key: string]: unknown };
51
+ }): Partial<Usage> {
52
+ const inputTokens = num(raw.prompt_tokens);
53
+ const outputTokens = num(raw.completion_tokens);
54
+ const cachedInputTokens = num(raw.prompt_tokens_details?.cached_tokens);
55
+ const reasoningTokens = num(raw.completion_tokens_details?.reasoning_tokens);
56
+ const totalTokens =
57
+ num(raw.total_tokens) ??
58
+ (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
59
+
60
+ return record({
61
+ inputTokens,
62
+ outputTokens,
63
+ totalTokens,
64
+ cachedInputTokens,
65
+ reasoningTokens,
66
+ ...billableFromOpenAIStyle(inputTokens, outputTokens, cachedInputTokens, reasoningTokens),
67
+ });
68
+ }
69
+
70
+ /** OpenAI Responses API `usage` */
71
+ export function usageFromOpenAIResponses(raw: {
72
+ input_tokens?: number;
73
+ output_tokens?: number;
74
+ total_tokens?: number;
75
+ input_tokens_details?: { cached_tokens?: number; [key: string]: unknown };
76
+ output_tokens_details?: { reasoning_tokens?: number; [key: string]: unknown };
77
+ [key: string]: unknown;
78
+ }): Partial<Usage> {
79
+ const inputTokens = num(raw.input_tokens);
80
+ const outputTokens = num(raw.output_tokens);
81
+ const cachedInputTokens = num(raw.input_tokens_details?.cached_tokens);
82
+ const reasoningTokens = num(raw.output_tokens_details?.reasoning_tokens);
83
+ const totalTokens =
84
+ num(raw.total_tokens) ??
85
+ (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
86
+
87
+ return record({
88
+ inputTokens,
89
+ outputTokens,
90
+ totalTokens,
91
+ cachedInputTokens,
92
+ reasoningTokens,
93
+ ...billableFromOpenAIStyle(inputTokens, outputTokens, cachedInputTokens, reasoningTokens),
94
+ });
95
+ }
96
+
97
+ /** Anthropic Messages `usage`(message_start / message_delta) */
98
+ export function usageFromAnthropicMessages(raw: {
99
+ input_tokens?: number;
100
+ output_tokens?: number;
101
+ cache_creation_input_tokens?: number;
102
+ cache_read_input_tokens?: number;
103
+ [key: string]: unknown;
104
+ }): Partial<Usage> {
105
+ const inputTokens = num(raw.input_tokens);
106
+ const outputTokens = num(raw.output_tokens);
107
+ const cacheWriteInputTokens = num(raw.cache_creation_input_tokens);
108
+ const cachedInputTokens = num(raw.cache_read_input_tokens);
109
+
110
+ const inputParts = [inputTokens, cacheWriteInputTokens, cachedInputTokens].filter(
111
+ (n): n is number => n !== undefined,
112
+ );
113
+ const summedInput = inputParts.length > 0 ? inputParts.reduce((sum, n) => sum + n, 0) : undefined;
114
+ const totalTokens =
115
+ summedInput !== undefined && outputTokens !== undefined ? summedInput + outputTokens : undefined;
116
+
117
+ let billableInputTokens: number | undefined;
118
+ if (inputTokens !== undefined || cacheWriteInputTokens !== undefined) {
119
+ billableInputTokens = (inputTokens ?? 0) + (cacheWriteInputTokens ?? 0);
120
+ }
121
+
122
+ return record({
123
+ inputTokens,
124
+ outputTokens,
125
+ totalTokens,
126
+ cachedInputTokens,
127
+ cacheWriteInputTokens,
128
+ billableInputTokens,
129
+ billableOutputTokens: outputTokens,
130
+ });
131
+ }
132
+
133
+ /** Ollama 流式 chunk(无 cache / reasoning 细分时仅填基础与 billable 镜像) */
134
+ export function usageFromOllama(raw: {
135
+ prompt_eval_count?: number;
136
+ eval_count?: number;
137
+ }): Partial<Usage> {
138
+ const inputTokens = num(raw.prompt_eval_count);
139
+ const outputTokens = num(raw.eval_count);
140
+ const totalTokens =
141
+ inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined;
142
+
143
+ return record({
144
+ inputTokens,
145
+ outputTokens,
146
+ totalTokens,
147
+ billableInputTokens: inputTokens,
148
+ billableOutputTokens: outputTokens,
149
+ });
150
+ }
@@ -13,13 +13,20 @@ export type StopReason = "end_turn" | "tool_call" | "max_output_tokens" | "conte
13
13
  // ── 辅助信息类型 ──────────────────────────────────────────────
14
14
 
15
15
  export type Usage = {
16
+ /** Provider prompt / input token count */
16
17
  inputTokens?: number;
18
+ /** Provider completion / output token count */
17
19
  outputTokens?: number;
20
+ /** Reasoning tokens when provider exposes output breakdown (e.g. OpenAI Responses) */
18
21
  reasoningTokens?: number;
19
22
  totalTokens?: number;
23
+ /** Tokens read from prompt cache (OpenAI cached_tokens, Anthropic cache_read_input_tokens) */
20
24
  cachedInputTokens?: number;
25
+ /** Tokens written to prompt cache (Anthropic cache_creation_input_tokens) */
21
26
  cacheWriteInputTokens?: number;
27
+ /** Best-effort billable input (full-rate input; excludes discounted cache reads where known) */
22
28
  billableInputTokens?: number;
29
+ /** Best-effort billable output (non-reasoning slice when provider gives reasoning breakdown) */
23
30
  billableOutputTokens?: number;
24
31
  };
25
32