@codehz/ai 0.1.5 → 0.1.6

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.6",
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> }
@@ -616,15 +622,7 @@ export class MessagesAdapter extends AdapterBase {
616
622
  stopSequence = sseEvent.data.delta.stop_sequence;
617
623
  const u = sseEvent.data.usage;
618
624
  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
- );
625
+ auxiliary.recordUsage(usageFromAnthropicMessages(u), "stream", u);
628
626
  }
629
627
  continue;
630
628
  }
@@ -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