@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/dist/index.d.mts +51 -7
- package/dist/index.mjs +172 -90
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +9 -10
- package/src/adapters/messages.ts +19 -11
- package/src/adapters/ollama.ts +5 -8
- package/src/adapters/responses.ts +2 -9
- package/src/helpers/index.ts +6 -0
- package/src/helpers/usage-mapping.ts +150 -0
- package/src/types/response.ts +7 -0
package/package.json
CHANGED
|
@@ -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?: {
|
|
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) {
|
package/src/adapters/messages.ts
CHANGED
|
@@ -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: {
|
|
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
|
-
|
|
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
|
}
|
package/src/adapters/ollama.ts
CHANGED
|
@@ -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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
package/src/helpers/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/types/response.ts
CHANGED
|
@@ -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
|
|