@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ai",
3
- "version": "0.1.4",
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 = {
@@ -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) {
@@ -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").ContentBlock[]): string {
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: { 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> }
@@ -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: string | undefined;
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 = `msg-${block.type}-${currentContentBlockIndex}`;
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 = `reason-${currentContentBlockIndex}`;
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 = `reason-redacted-${currentContentBlockIndex}`;
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
  }
@@ -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").ContentBlock[]): string {
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
- inputTokens: chunk.prompt_eval_count,
490
- outputTokens: chunk.eval_count,
491
- totalTokens:
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" | "system" | "developer"; content: string }
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").ContentBlock[]): string {
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
 
@@ -14,7 +14,7 @@ export type ValidationIssue = {
14
14
  message: string;
15
15
  };
16
16
 
17
- const MESSAGE_ROLES = new Set(["user", "assistant", "system", "developer"]);
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
- validateContentArray(request.instructions, "instructions", issues, "INSTRUCTIONS_INVALID");
240
+ validateInstructionArray(request.instructions, "instructions", issues);
223
241
  } else {
224
- pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or ContentBlock[]");
242
+ pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or InstructionBlock[]");
225
243
  }
226
244
  }
227
245
 
@@ -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";
@@ -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 | ContentBlock[])归一化为纯文本。
183
+ * 将 instructions(string | InstructionBlock[])归一化为纯文本。
183
184
  */
184
- export function instructionsToText(instructions: string | ContentBlock[]): 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
+ }
@@ -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
- | { type: "text"; text: string }
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 };
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  // 基础内容块
9
- export type { ContentBlock } from "./content.js";
9
+ export type { TextContentBlock, JsonContentBlock, InstructionBlock, ContentBlock } from "./content.js";
10
10
 
11
11
  // Item 类型体系
12
12
  export type {
@@ -11,7 +11,7 @@ import type { ContentBlock } from "./content.js";
11
11
  export type MessageItem = {
12
12
  type: "message";
13
13
  id?: string;
14
- role: "user" | "assistant" | "system" | "developer";
14
+ role: "user" | "assistant";
15
15
  content: ContentBlock[];
16
16
  };
17
17
 
@@ -4,7 +4,7 @@
4
4
  * 所有 adapter 都接受同一形状的 canonical request。
5
5
  */
6
6
 
7
- import type { ContentBlock } from "./content.js";
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 | ContentBlock[];
31
+ instructions?: string | InstructionBlock[];
32
32
  input: InputItem[];
33
33
  tools?: ToolDefinition[];
34
34
  toolChoice?: ToolChoice;
@@ -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