@codehz/ai 0.1.4 → 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/README.md +2 -2
- package/dist/index.d.mts +62 -15
- package/dist/index.mjs +122 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +10 -18
- package/src/adapters/messages.ts +18 -25
- package/src/adapters/ollama.ts +7 -17
- package/src/adapters/responses.ts +4 -11
- package/src/core/validation.ts +21 -3
- package/src/helpers/index.ts +6 -0
- package/src/helpers/mapping.ts +3 -2
- package/src/helpers/usage-mapping.ts +150 -0
- package/src/types/content.ts +5 -2
- package/src/types/index.ts +1 -1
- package/src/types/items.ts +1 -1
- package/src/types/request.ts +2 -2
- package/src/types/response.ts +7 -0
- package/bun.lock +0 -231
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 = {
|
|
@@ -279,14 +286,7 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
279
286
|
for (const item of request.input) {
|
|
280
287
|
switch (item.type) {
|
|
281
288
|
case "message": {
|
|
282
|
-
const role =
|
|
283
|
-
item.role === "developer"
|
|
284
|
-
? "system"
|
|
285
|
-
: item.role === "system"
|
|
286
|
-
? "system"
|
|
287
|
-
: item.role === "user"
|
|
288
|
-
? "user"
|
|
289
|
-
: "assistant";
|
|
289
|
+
const role = item.role;
|
|
290
290
|
const text = contentBlocksToChatText(item.content, `input message (${item.role}) content`);
|
|
291
291
|
messages.push({ role, content: text || null });
|
|
292
292
|
break;
|
|
@@ -492,15 +492,7 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
492
492
|
|
|
493
493
|
// usage 可能在最终 chunk 中
|
|
494
494
|
if (chunk.usage) {
|
|
495
|
-
auxiliary.recordUsage(
|
|
496
|
-
{
|
|
497
|
-
inputTokens: chunk.usage.prompt_tokens,
|
|
498
|
-
outputTokens: chunk.usage.completion_tokens,
|
|
499
|
-
totalTokens: chunk.usage.total_tokens,
|
|
500
|
-
},
|
|
501
|
-
"final",
|
|
502
|
-
chunk.usage,
|
|
503
|
-
);
|
|
495
|
+
auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
|
|
504
496
|
}
|
|
505
497
|
|
|
506
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
|
|
|
@@ -105,7 +106,7 @@ function ensureMessagesReasoningBlocks(
|
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
function instructionsToMessagesText(instructions: string | import("../index.js").
|
|
109
|
+
function instructionsToMessagesText(instructions: string | import("../index.js").InstructionBlock[]): string {
|
|
109
110
|
return typeof instructions === "string"
|
|
110
111
|
? instructions
|
|
111
112
|
: contentBlocksToText(ensureMessagesTextBlocks(instructions, "instructions"));
|
|
@@ -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> }
|
|
@@ -162,6 +168,11 @@ function rollbackTrailingAssistantMessages(messages: MessagesAPIMessage[]): void
|
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
170
|
|
|
171
|
+
/** 用 response 级别的命名空间合成 content block 的 item ID,避免多轮工具循环 ID 碰撞 */
|
|
172
|
+
function synthesizeItemId(kind: "msg" | "reason" | "reason-redacted", blockIndex: number, responseId: string): string {
|
|
173
|
+
return `${kind}-${blockIndex}-${responseId}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
function parseToolUseInput(input: string): Record<string, unknown> {
|
|
166
177
|
try {
|
|
167
178
|
const parsed = JSON.parse(input);
|
|
@@ -272,16 +283,6 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
272
283
|
for (const item of request.input) {
|
|
273
284
|
switch (item.type) {
|
|
274
285
|
case "message": {
|
|
275
|
-
if (item.role === "system" || item.role === "developer") {
|
|
276
|
-
// Anthropic 不支持 system/developer role 在 messages 中
|
|
277
|
-
// 合并到 system prompt
|
|
278
|
-
const text = contentBlocksToText(
|
|
279
|
-
ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`),
|
|
280
|
-
);
|
|
281
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n${text}` : text;
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
286
|
const role = item.role === "user" ? "user" : "assistant";
|
|
286
287
|
const supportedContent = ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`);
|
|
287
288
|
if (supportedContent.length === 1 && supportedContent[0]?.type === "text") {
|
|
@@ -456,7 +457,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
456
457
|
// 完成响应数据
|
|
457
458
|
let stopReason: string | undefined;
|
|
458
459
|
let stopSequence: string | null | undefined;
|
|
459
|
-
let rawResponseId
|
|
460
|
+
let rawResponseId = "";
|
|
460
461
|
|
|
461
462
|
if (request.include?.providerMetadata !== "off") {
|
|
462
463
|
const headerMetadata = pickProviderHeaders(response.headers);
|
|
@@ -513,7 +514,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
513
514
|
switch (block.type) {
|
|
514
515
|
case "text": {
|
|
515
516
|
currentItemType = "message";
|
|
516
|
-
currentItemId =
|
|
517
|
+
currentItemId = synthesizeItemId("msg", currentContentBlockIndex, rawResponseId);
|
|
517
518
|
textBuffer = "";
|
|
518
519
|
yield factory.messageStarted(currentItemId);
|
|
519
520
|
break;
|
|
@@ -521,7 +522,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
521
522
|
case "thinking": {
|
|
522
523
|
hasStreamedReasoning = true;
|
|
523
524
|
currentItemType = "reasoning";
|
|
524
|
-
currentItemId =
|
|
525
|
+
currentItemId = synthesizeItemId("reason", currentContentBlockIndex, rawResponseId);
|
|
525
526
|
currentThinkingVisibility = "full";
|
|
526
527
|
thinkingBuffer = "";
|
|
527
528
|
yield factory.reasoningStarted(currentItemId, "full");
|
|
@@ -530,7 +531,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
530
531
|
case "redacted_thinking": {
|
|
531
532
|
hasStreamedReasoning = true;
|
|
532
533
|
currentItemType = "reasoning";
|
|
533
|
-
currentItemId =
|
|
534
|
+
currentItemId = synthesizeItemId("reason-redacted", currentContentBlockIndex, rawResponseId);
|
|
534
535
|
currentThinkingVisibility = "redacted";
|
|
535
536
|
const data = (block as unknown as { data: string }).data;
|
|
536
537
|
yield factory.reasoningStarted(currentItemId, "redacted");
|
|
@@ -621,15 +622,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
621
622
|
stopSequence = sseEvent.data.delta.stop_sequence;
|
|
622
623
|
const u = sseEvent.data.usage;
|
|
623
624
|
if (u) {
|
|
624
|
-
auxiliary.recordUsage(
|
|
625
|
-
{
|
|
626
|
-
inputTokens: u.input_tokens,
|
|
627
|
-
outputTokens: u.output_tokens,
|
|
628
|
-
totalTokens: u.input_tokens + u.output_tokens,
|
|
629
|
-
},
|
|
630
|
-
"stream",
|
|
631
|
-
u,
|
|
632
|
-
);
|
|
625
|
+
auxiliary.recordUsage(usageFromAnthropicMessages(u), "stream", u);
|
|
633
626
|
}
|
|
634
627
|
continue;
|
|
635
628
|
}
|
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
|
|
|
@@ -112,7 +113,7 @@ function ensureOllamaReasoningBlocks(
|
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function instructionsToOllamaText(instructions: string | import("../index.js").
|
|
116
|
+
function instructionsToOllamaText(instructions: string | import("../index.js").InstructionBlock[]): string {
|
|
116
117
|
return typeof instructions === "string"
|
|
117
118
|
? instructions
|
|
118
119
|
: contentBlocksToText(ensureOllamaTextBlocks(instructions, "instructions"));
|
|
@@ -259,14 +260,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
259
260
|
for (const item of request.input) {
|
|
260
261
|
switch (item.type) {
|
|
261
262
|
case "message": {
|
|
262
|
-
const role =
|
|
263
|
-
item.role === "developer"
|
|
264
|
-
? "system"
|
|
265
|
-
: item.role === "system"
|
|
266
|
-
? "system"
|
|
267
|
-
: item.role === "user"
|
|
268
|
-
? "user"
|
|
269
|
-
: "assistant";
|
|
263
|
+
const role = item.role;
|
|
270
264
|
messages.push({
|
|
271
265
|
role,
|
|
272
266
|
content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`)),
|
|
@@ -485,14 +479,10 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
485
479
|
(chunk.prompt_eval_count !== undefined || chunk.eval_count !== undefined)
|
|
486
480
|
) {
|
|
487
481
|
auxiliary.recordUsage(
|
|
488
|
-
{
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
chunk.prompt_eval_count !== undefined && chunk.eval_count !== undefined
|
|
493
|
-
? chunk.prompt_eval_count + chunk.eval_count
|
|
494
|
-
: undefined,
|
|
495
|
-
},
|
|
482
|
+
usageFromOllama({
|
|
483
|
+
prompt_eval_count: chunk.prompt_eval_count,
|
|
484
|
+
eval_count: chunk.eval_count,
|
|
485
|
+
}),
|
|
496
486
|
"final",
|
|
497
487
|
{
|
|
498
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
|
|
|
@@ -51,7 +52,7 @@ type ResponsesAPIRequest = {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
type ResponsesInputItem =
|
|
54
|
-
| { type: "message"; role: "user" | "assistant"
|
|
55
|
+
| { type: "message"; role: "user" | "assistant"; content: string }
|
|
55
56
|
| { type: "message"; role: "assistant"; content: ResponsesContentBlock[] }
|
|
56
57
|
| { type: "function_call"; id: string; name: string; arguments: string; call_id?: string }
|
|
57
58
|
| { type: "function_call_output"; call_id: string; output: string }
|
|
@@ -104,7 +105,7 @@ function ensureResponsesReasoningBlocks(
|
|
|
104
105
|
});
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
function instructionsToResponsesText(instructions: string | import("../index.js").
|
|
108
|
+
function instructionsToResponsesText(instructions: string | import("../index.js").InstructionBlock[]): string {
|
|
108
109
|
return typeof instructions === "string"
|
|
109
110
|
? instructions
|
|
110
111
|
: contentBlocksToText(ensureResponsesTextBlocks(instructions, "instructions"));
|
|
@@ -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/core/validation.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type ValidationIssue = {
|
|
|
14
14
|
message: string;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
const MESSAGE_ROLES = new Set(["user", "assistant"
|
|
17
|
+
const MESSAGE_ROLES = new Set(["user", "assistant"]);
|
|
18
18
|
const REASONING_VISIBILITIES = new Set(["full", "summary", "redacted", "opaque"]);
|
|
19
19
|
const TOOL_RESULT_OUTCOMES = new Set(["success", "error", "rejected"]);
|
|
20
20
|
const INCLUDE_MODES = new Set(["off", "best_effort"]);
|
|
@@ -75,6 +75,24 @@ function validateContentArray(content: unknown, field: string, issues: Validatio
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function validateInstructionArray(content: unknown, field: string, issues: ValidationIssue[]): void {
|
|
79
|
+
if (!Array.isArray(content)) {
|
|
80
|
+
pushIssue(issues, field, "INSTRUCTIONS_INVALID", `${field} must be an InstructionBlock[]`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < content.length; i++) {
|
|
85
|
+
const block = content[i];
|
|
86
|
+
const blockField = `${field}[${i}]`;
|
|
87
|
+
validateContentBlock(block, blockField, issues);
|
|
88
|
+
|
|
89
|
+
if (!isRecord(block) || typeof block.type !== "string") continue;
|
|
90
|
+
if (block.type !== "text" && block.type !== "json") {
|
|
91
|
+
pushIssue(issues, blockField, "INSTRUCTIONS_INVALID", `${blockField} only supports text/json blocks`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
function validateInputItem(item: unknown, field: string, issues: ValidationIssue[]): void {
|
|
79
97
|
if (!isRecord(item)) {
|
|
80
98
|
pushIssue(issues, field, "INPUT_INVALID_ITEM", `${field} must be a valid InputItem`);
|
|
@@ -219,9 +237,9 @@ export function validateRequest(request: AIRequest): ValidationIssue[] {
|
|
|
219
237
|
if (typeof request.instructions === "string") {
|
|
220
238
|
// no-op
|
|
221
239
|
} else if (Array.isArray(request.instructions)) {
|
|
222
|
-
|
|
240
|
+
validateInstructionArray(request.instructions, "instructions", issues);
|
|
223
241
|
} else {
|
|
224
|
-
pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or
|
|
242
|
+
pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or InstructionBlock[]");
|
|
225
243
|
}
|
|
226
244
|
}
|
|
227
245
|
|
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";
|
package/src/helpers/mapping.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type {
|
|
13
13
|
StopReason,
|
|
14
14
|
ContentBlock,
|
|
15
|
+
InstructionBlock,
|
|
15
16
|
MessageItem,
|
|
16
17
|
ReasoningItem,
|
|
17
18
|
ToolCallItem,
|
|
@@ -179,9 +180,9 @@ export function contentBlocksToText(blocks: ContentBlock[]): string {
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
/**
|
|
182
|
-
* 将 instructions(string |
|
|
183
|
+
* 将 instructions(string | InstructionBlock[])归一化为纯文本。
|
|
183
184
|
*/
|
|
184
|
-
export function instructionsToText(instructions: string |
|
|
185
|
+
export function instructionsToText(instructions: string | InstructionBlock[]): string {
|
|
185
186
|
return typeof instructions === "string" ? instructions : contentBlocksToText(instructions);
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -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/content.ts
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* 覆盖文本、JSON、图片、二进制引用和后端私有内容。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export type TextContentBlock = { type: "text"; text: string };
|
|
8
|
+
export type JsonContentBlock = { type: "json"; json: unknown };
|
|
9
|
+
export type InstructionBlock = TextContentBlock | JsonContentBlock;
|
|
10
|
+
|
|
7
11
|
export type ContentBlock =
|
|
8
|
-
|
|
|
9
|
-
| { type: "json"; json: unknown }
|
|
12
|
+
| InstructionBlock
|
|
10
13
|
| { type: "image"; imageUrl: string }
|
|
11
14
|
| { type: "binary_ref"; ref: string }
|
|
12
15
|
| { type: "opaque"; payload: unknown };
|
package/src/types/index.ts
CHANGED
package/src/types/items.ts
CHANGED
package/src/types/request.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 所有 adapter 都接受同一形状的 canonical request。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type { InstructionBlock } from "./content.js";
|
|
8
8
|
import type { InputItem } from "./items.js";
|
|
9
9
|
|
|
10
10
|
// ── 工具定义 ──────────────────────────────────────────────────
|
|
@@ -28,7 +28,7 @@ export type IncludeSettings = {
|
|
|
28
28
|
// ── 统一请求 ──────────────────────────────────────────────────
|
|
29
29
|
|
|
30
30
|
export type AIRequest = {
|
|
31
|
-
instructions?: string |
|
|
31
|
+
instructions?: string | InstructionBlock[];
|
|
32
32
|
input: InputItem[];
|
|
33
33
|
tools?: ToolDefinition[];
|
|
34
34
|
toolChoice?: ToolChoice;
|
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
|
|