@keo-ai/axiom 0.2.0 → 0.2.1

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 CHANGED
@@ -4,10 +4,8 @@
4
4
 
5
5
  > **The execution foundation for LLM agents.**
6
6
 
7
-
8
7
  **Axiom 不是编排框架。它是大模型调用工具的「底盘」**
9
8
 
10
-
11
9
  </div>
12
10
 
13
11
  ---
@@ -27,17 +25,13 @@ Axiom 是 **keo** 的 LLM 底座库。它不碰业务编排(意图路由、节
27
25
  ├────────────────────────────────────────────┤
28
26
  │ ⚡ AXIOM │
29
27
  │ ┌─────────┐ ┌──────────┐ ┌─────────────┐ │
30
- │ │ LLM调用 │ │ Harness │ │ 记忆系统 │ │
31
- │ │ 故障转移 │ │ 状态机 │ │ · Faiss │ │
32
- │ │ 流式输出 │ │ 副作用 │ │ · 槽位 │ │
33
- │ └─────────┘ │ 传播 │ │ · 语义 │ │
34
- │ │ 幻觉防控 │ └─────────────┘ │
35
- └──────────┘ ┌─────────────┐
36
- RAG基础 │ │
37
- │ │ Embedding │ │
38
- │ │ pgvector │ │
39
- │ │ rerank │ │
40
- │ └─────────────┘ │
28
+ │ │ LLM调用 │ │ Function │ │ RAG基础 │ │
29
+ │ │ 故障转移 │ │ Call │ │ Embedding │ │
30
+ │ │ 流式输出 │ │ Loop │ │ pgvector │ │
31
+ │ └─────────┘ │ 状态机 │ │ rerank │ │
32
+ │ │ 副作用 │ └─────────────┘ │
33
+ 传播
34
+ └──────────┘
41
35
  └────────────────────────────────────────────┘
42
36
  ```
43
37
 
@@ -46,9 +40,8 @@ Axiom 是 **keo** 的 LLM 底座库。它不碰业务编排(意图路由、节
46
40
  **Axiom 做:**
47
41
  - ✅ LLM 统一调用 + Provider 故障转移
48
42
  - ✅ Function Call Loop 的驱动、记录、校验、副作用传播
49
- - ✅ 记忆系统的抽象与基础实现
50
43
  - ✅ RAG 基础(Embedding / pgvector 向量检索 / rerank)
51
- - ✅ 基础配置中心
44
+ - ✅ 环境变量配置
52
45
 
53
46
  **Axiom 不做:**
54
47
  - ❌ 意图识别与路由
@@ -57,6 +50,7 @@ Axiom 是 **keo** 的 LLM 底座库。它不碰业务编排(意图路由、节
57
50
  - ❌ 消息队列与削峰
58
51
  - ❌ 具体业务 Tool 的实现逻辑
59
52
  - ❌ Provider 排序策略(成本/能力/速度)
53
+ - ❌ 记忆系统(Faiss / 语义记忆 / 槽位)— 由上层业务实现
60
54
 
61
55
  > 上层写业务,Axiom 跑底盘。
62
56
 
@@ -105,39 +99,54 @@ export BAILIAN_API_KEY="your-api-key"
105
99
  ```ts
106
100
  import { LLM } from '@keo-ai/axiom';
107
101
 
108
- const res = await LLM.predict({ model: 'qwen3.7-max', prompt: '你好' });
109
- console.log(res);
102
+ const { content, usage } = await LLM.predict({ model: 'qwen-max', prompt: '你好' });
103
+ console.log(content); // 模型回复
104
+ console.log(usage); // { promptTokens, completionTokens, totalTokens, cachedPromptTokens? }
110
105
  ```
111
106
 
112
107
  ### API 概览
113
108
 
114
109
  | 方法 | 用途 | 返回类型 |
115
110
  |---|---|---|
116
- | `LLM.predict(config)` | 单轮快速调用,自动构建 user message | `responseFormat` 而定(见下) |
111
+ | `LLM.predict(config)` | 单轮快速调用,自动构建 user message | `{ content, usage?: TokenUsage }`,依 `responseFormat` 而定(见下) |
117
112
  | `LLM.predictWithMessages(messages, config)` | 传入完整消息列表,适用于多轮对话 | 同上 |
118
113
  | `LLM.streamPredict(config)` | 流式调用,逐块返回内容 | `AsyncGenerator<StreamChunk>` |
119
114
  | `LLM.streamPredictWithMessages(messages, config)` | 流式 + 自定义消息列表 | `AsyncGenerator<StreamChunk>` |
120
115
 
116
+ 需要自定义 Provider 列表或故障转移策略时,直接使用 `Predictor` 类:
117
+
118
+ ```ts
119
+ import { Predictor, BailianProvider } from '@keo-ai/axiom';
120
+
121
+ const predictor = new Predictor({
122
+ providers: [
123
+ new BailianProvider({ name: 'bailian', apiKey, baseUrl, defaultModel: 'qwen-max' }),
124
+ ],
125
+ });
126
+ const response = await predictor.generateForModel('qwen-max', { messages: [...] });
127
+ ```
128
+
121
129
  ### responseFormat 与返回类型
122
130
 
123
- `predict` 和 `predictWithMessages` 的返回类型由 `responseFormat` 决定:
131
+ `predict` 和 `predictWithMessages` 返回 `{ content, usage?: TokenUsage }`,其中 `content` 的类型由 `responseFormat` 决定:
124
132
 
125
- | `responseFormat` | 返回值 | 说明 |
133
+ | `responseFormat` | `content` 类型 | 说明 |
126
134
  |---|---|---|
127
135
  | 未设置 / `'text'` | `string` | 直接返回模型输出的文本 |
128
- | `'json'` | `any` | 自动 `JSON.parse` |
136
+ | `'json'` | `any` | 自动 `JSON.parse`。注意:`glm-5.1` 不支持此模式 |
129
137
 
130
138
  ```ts
131
- // text → string
132
- const text = await LLM.predict({ model: 'qwen3.7-max', prompt: '讲个故事', responseFormat: 'text' });
139
+ // text → { content: string, usage?: TokenUsage }
140
+ const { content, usage } = await LLM.predict({ model: 'qwen-max', prompt: '讲个故事', responseFormat: 'text' });
133
141
 
134
- // json → 自动解析为对象
135
- const obj = await LLM.predict({
136
- model: 'qwen3.7-max',
142
+ // json → { content: any, usage?: TokenUsage }
143
+ const { content: obj, usage } = await LLM.predict({
144
+ model: 'qwen-max',
137
145
  prompt: '生成一个 JSON',
138
146
  responseFormat: 'json',
139
147
  });
140
148
  console.log(obj.name);
149
+ console.log(usage); // { promptTokens, completionTokens, totalTokens, cachedPromptTokens? }
141
150
  ```
142
151
 
143
152
  ### Provider 配置
@@ -147,7 +156,7 @@ console.log(obj.name);
147
156
  ```bash
148
157
  BAILIAN_API_KEY # 必填
149
158
  BAILIAN_BASE_URL # 可选,默认 https://dashscope.aliyuncs.com/compatible-mode/v1
150
- BAILIAN_DEFAULT_MODEL # 可选,默认 qwen3.7-max
159
+ BAILIAN_DEFAULT_MODEL # 可选,默认 qwen-max
151
160
  ```
152
161
 
153
162
  ### 故障转移
@@ -161,10 +170,13 @@ Predictor 内部按模型查询 `MODEL_REGISTRY`,获取候选 Provider 队列
161
170
  流式版本返回 `AsyncGenerator<StreamChunk>`,迭代即可逐块消费:
162
171
 
163
172
  ```ts
164
- for await (const chunk of LLM.streamPredict({ model: 'qwen3.7-max', prompt: '讲个故事' })) {
173
+ for await (const chunk of LLM.streamPredict({ model: 'qwen-max', prompt: '讲个故事' })) {
165
174
  if (chunk.type === 'content') {
166
175
  process.stdout.write(chunk.delta);
167
176
  }
177
+ if (chunk.type === 'reasoning') {
178
+ process.stdout.write(chunk.delta); // 推理过程
179
+ }
168
180
  }
169
181
  ```
170
182
 
@@ -172,19 +184,20 @@ for await (const chunk of LLM.streamPredict({ model: 'qwen3.7-max', prompt: '讲
172
184
 
173
185
  当前支持的模型(通过 `Model` 类型枚举):
174
186
 
175
- | 模型 | 推理深度 | 说明 |
176
- |------|----------|------|
177
- | `deepseek-v4-pro` | 原生支持 | `low`/`medium`/`high` 直接传递 |
178
- | `deepseek-v4-flash` | ✅ 原生支持 | `low`/`medium`/`high` 直接传递 |
179
- | `kimi-k2.6` | ✅ 原生支持 | `low`/`medium`/`high` 直接传递 |
180
- | `qwen-plus` | ✅ 映射支持 | `low` 关闭推理,`medium`/`high` 映射为 `enable_thinking` |
181
- | `qwen-turbo` | ✅ 映射支持 | 同上 |
182
- | `qwen3.7-max` | ✅ 映射支持 | 同上 |
183
- | `qwq-plus` | 不支持 | 固定推理行为,不可调节 |
184
- | `glm-5.1` | ❌ 不支持 | 不支持推理参数 |
185
- | `qwen-vl-plus` | ❌ 不支持 | 视觉模型,不支持推理参数 |
186
-
187
- > 推理深度的支持方式由 Provider 内部维护。百炼 Provider 中,Qwen 系列通过 `extra_body.enable_thinking` 映射实现,DeepSeek / Kimi 则原生透传 `reasoning_effort`。
187
+ | 模型 | 推理深度 | json 模式 | 说明 |
188
+ |------|----------|-----------|------|
189
+ | `qwen-max` | 不支持 | | **默认模型** |
190
+ | `qwen3.7-max` | ✅ 映射支持 | ✅ | `low`/`medium`/`high` 映射为 `enable_thinking` |
191
+ | `qwen-plus` | ✅ 映射支持 | | 同上 |
192
+ | `qwen-turbo` | ✅ 映射支持 | | 同上 |
193
+ | `deepseek-v4-pro` | ✅ 原生支持 | | `low`/`medium`/`high` 直接传递 |
194
+ | `deepseek-v4-flash` | ✅ 原生支持 | ✅ | 同上 |
195
+ | `kimi-k2.6` | 原生支持 | | 同上 |
196
+ | `qwq-plus` | ❌ 不支持 | | 固定推理行为,不可调节 |
197
+ | `glm-5.1` | ❌ 不支持 | | 不支持推理参数,不支持 json 模式 |
198
+ | `qwen-vl-plus` | ❌ 不支持 | ✅ | 视觉模型,不支持推理参数 |
199
+
200
+ > 推理深度的支持方式由 Provider 内部维护。百炼 Provider 中,Qwen 系列通过 `extra_body.enable_thinking` 映射实现(`low` 关闭,`medium`/`high` 开启),DeepSeek / Kimi 则原生透传 `reasoning_effort`。
188
201
  >
189
202
  > 目前所有模型均路由到百炼 Provider。后续接入其他厂商时,通过 `MODEL_REGISTRY` 扩展映射即可。
190
203
 
@@ -194,7 +207,7 @@ Predict 模块遵循 Axiom 的统一错误策略:**直接抛异常,调用方
194
207
 
195
208
  ```ts
196
209
  try {
197
- const res = await LLM.predict({ model: 'qwen3.7-max', prompt: 'hi' });
210
+ const { content, usage } = await LLM.predict({ model: 'qwen-max', prompt: 'hi' });
198
211
  } catch (e) {
199
212
  // e.message 包含 Provider 汇总错误信息
200
213
  }
@@ -232,7 +245,7 @@ const result = await FunctionCallLoop.runLoop({
232
245
  { role: 'system', content: 'You are a helpful assistant.' },
233
246
  { role: 'user', content: 'What is the weather in Beijing?' },
234
247
  ],
235
- model: 'qwen3.7-max',
248
+ model: 'qwen-max',
236
249
  temperature: 0.7,
237
250
  maxTokens: 2048,
238
251
  tools: [
@@ -262,6 +275,8 @@ const result = await FunctionCallLoop.runLoop({
262
275
  console.log(result.finalContent);
263
276
  console.log('Turns:', result.turns);
264
277
  console.log('Harness:', result.harness);
278
+ console.log('Token usage:', result.totalUsage); // 累计 token 消耗
279
+ console.log('Usage history:', result.usageHistory); // 每轮明细
265
280
  ```
266
281
 
267
282
  > 💡 需要**流式输出**(实时展示 LLM 生成内容)?使用下方的 [`runLoopStream`](#流式调用runloopstream)。
@@ -349,7 +364,7 @@ console.log('Turns:', result!.turns);
349
364
  | `content` | LLM 输出 content(正式回复)增量 | `delta`, `turn` |
350
365
  | `tool_call` | LLM 决定调用 tool(流结束、完整 tool_calls 解析完成) | `toolCalls`, `turn` |
351
366
  | `tool_result` | tool 执行完成 | `callId`, `toolName`, `content`, `status`, `turn` |
352
- | `turn_end` | 一轮结束(tool 全部执行完或 content 直接返回) | `turn` |
367
+ | `turn_end` | 一轮结束(tool 全部执行完或 content 直接返回) | `turn`, `usage?` |
353
368
 
354
369
  #### 流式 vs 非流式的选择
355
370
 
@@ -678,6 +693,15 @@ interface LoopResult {
678
693
  harness: HarnessRecord[]; // 执行历史
679
694
  finalContent: string | null; // 最终回复内容
680
695
  turns: number; // 实际执行轮数
696
+ usageHistory: TokenUsage[]; // 每轮 LLM 调用的 token 消耗明细
697
+ totalUsage: TokenUsage; // 累计 token 消耗(含 cachedPromptTokens)
698
+ }
699
+
700
+ interface TokenUsage {
701
+ promptTokens: number;
702
+ completionTokens: number;
703
+ totalTokens: number;
704
+ cachedPromptTokens?: number; // 命中缓存的 prompt token(Prompt Caching)
681
705
  }
682
706
  ```
683
707
 
@@ -825,7 +849,7 @@ interface SearchResult {
825
849
 
826
850
  ### 过滤
827
851
 
828
- 支持对表的任意独立列做等值和范围过滤:
852
+ 支持对表的任意独立列做等值、范围和 IN 查询:
829
853
 
830
854
  ```ts
831
855
  // 等值过滤
@@ -833,7 +857,7 @@ const results = await EmbeddingSearch.query('query', {
833
857
  filter: { category: '衣服', shop: '旗舰店' },
834
858
  }, pool);
835
859
 
836
- // 混合:等值 + 数值范围
860
+ // 数值范围
837
861
  const ranked = await EmbeddingSearch.query('query', {
838
862
  filter: {
839
863
  category: '衣服',
@@ -854,36 +878,7 @@ const multi = await EmbeddingSearch.query('query', {
854
878
 
855
879
  ### 枚举值解析
856
880
 
857
- 默认开启 `parseEnum`,返回结果中的枚举字段会自动从数字值解析为可读文本。当前支持以下字段:
858
-
859
- | 字段 | 说明 | 示例(原值 → 解析后) |
860
- |---|---|---|
861
- | `type` | 期刊类型 | `11` → `"SCI/SSCI/AHCI"` |
862
- | `subject1` | 学科大类 | `17` → `"计算机"` |
863
- | `db` | 数据库 | `2` → `"SCI(SCIE)"` |
864
- | `attribute` | 刊物属性 | `3` → `"快审刊"` |
865
- | `oa` | 发表模式 | `1` → `"开源模式(OA)"` |
866
-
867
- ```ts
868
- const results = await EmbeddingSearch.query('query', {
869
- tableName: 'journals',
870
- }, pool);
871
-
872
- // 默认 parseEnum: true,枚举字段自动解析为文本
873
- console.log(results[0].type); // "SCI/SSCI/AHCI"
874
- console.log(results[0].subject1); // "计算机"
875
- ```
876
-
877
- 如需关闭解析(保留原始数字值):
878
-
879
- ```ts
880
- const results = await EmbeddingSearch.query('query', {
881
- tableName: 'journals',
882
- parseEnum: false,
883
- }, pool);
884
-
885
- console.log(results[0].type); // 11
886
- ```
881
+ 默认开启 `parseEnum`,返回结果中的特定枚举字段会自动从数字值解析为可读文本。如需关闭解析(保留原始数字值),设置 `parseEnum: false`。
887
882
 
888
883
  未知枚举值、非数字类型、非枚举字段均保持原样不变。
889
884
 
@@ -970,5 +965,3 @@ EmbeddingSearch 模块内部调用百炼的 embedding 和 rerank 服务,无需
970
965
  - **项目层填充内容** — 上层准备消息、工具、业务逻辑,Axiom 负责可靠执行
971
966
  - **副作用即状态** — 工具调用改变外部世界,Harness 让这种改变在 Loop 间可见
972
967
  - **单一事实来源** — Harness 的执行记录是 Function Call 的唯一权威来源
973
-
974
- ---
@@ -44,7 +44,24 @@ function runLoop(config) {
44
44
  const harness = new harness_1.Harness();
45
45
  const messages = buildInitialMessages(config);
46
46
  let turn = 0;
47
+ const usageHistory = [];
47
48
  const signal = config.signal;
49
+ function addUsage(usage) {
50
+ if (usage) {
51
+ usageHistory.push(usage);
52
+ }
53
+ }
54
+ function computeTotalUsage() {
55
+ return usageHistory.reduce((acc, u) => {
56
+ var _a, _b;
57
+ return ({
58
+ promptTokens: acc.promptTokens + u.promptTokens,
59
+ completionTokens: acc.completionTokens + u.completionTokens,
60
+ totalTokens: acc.totalTokens + u.totalTokens,
61
+ cachedPromptTokens: ((_a = acc.cachedPromptTokens) !== null && _a !== void 0 ? _a : 0) + ((_b = u.cachedPromptTokens) !== null && _b !== void 0 ? _b : 0),
62
+ });
63
+ }, { promptTokens: 0, completionTokens: 0, totalTokens: 0 });
64
+ }
48
65
  while (true) {
49
66
  // 检查取消信号
50
67
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
@@ -87,6 +104,7 @@ function runLoop(config) {
87
104
  topP: config.topP,
88
105
  reasoningEffort: config.reasoningEffort,
89
106
  });
107
+ addUsage(llmResponse.usage);
90
108
  // 第六步:检查 LLM 回复
91
109
  if (!llmResponse.tool_calls || llmResponse.tool_calls.length === 0) {
92
110
  messages.push({
@@ -99,6 +117,8 @@ function runLoop(config) {
99
117
  harness: harness.getAll(),
100
118
  finalContent: llmResponse.content,
101
119
  turns: turn,
120
+ usageHistory,
121
+ totalUsage: computeTotalUsage(),
102
122
  };
103
123
  }
104
124
  // 有 tool_calls,将 assistant message 加入 Messages
@@ -133,6 +153,8 @@ function runLoop(config) {
133
153
  args: pending.pendingApproval.args,
134
154
  callId: pending.toolCallId,
135
155
  },
156
+ usageHistory,
157
+ totalUsage: computeTotalUsage(),
136
158
  };
137
159
  }
138
160
  // 第八步:回到第一步继续下一轮
@@ -144,6 +166,8 @@ function runLoop(config) {
144
166
  harness: harness.getAll(),
145
167
  finalContent: (lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.role) === 'assistant' ? lastMessage.content : null,
146
168
  turns: turn,
169
+ usageHistory,
170
+ totalUsage: computeTotalUsage(),
147
171
  };
148
172
  });
149
173
  }
@@ -2,7 +2,7 @@
2
2
  * Function Call Loop 的 LLM Provider 适配器。
3
3
  * 基于 predict 模块的 `callChatCompletions` 构建,共用底层 OpenAI 协议 HTTP 层。
4
4
  */
5
- import type { Message, ToolCall, ToolDefinition, LLMCallOptions, LLMStreamChunk } from './types';
5
+ import type { Message, ToolCall, ToolDefinition, LLMCallOptions, LLMStreamChunk, TokenUsage } from './types';
6
6
  export interface ProviderOptions {
7
7
  readonly apiKey: string;
8
8
  readonly baseUrl: string;
@@ -16,6 +16,7 @@ export declare function createLLMCaller(options: ProviderOptions): {
16
16
  readonly content: string | null;
17
17
  readonly reasoningContent?: string;
18
18
  readonly tool_calls?: ReadonlyArray<ToolCall>;
19
+ readonly usage?: TokenUsage;
19
20
  }>;
20
21
  /**
21
22
  * 流式调用 LLM,支持 content 和 tool_calls 的增量输出。
@@ -67,7 +67,7 @@ function createLLMCaller(options) {
67
67
  return {
68
68
  call(messages, tools, callOptions) {
69
69
  return __awaiter(this, void 0, void 0, function* () {
70
- var _a, _b;
70
+ var _a, _b, _c;
71
71
  const body = provider.adaptRequest({
72
72
  model: (_a = callOptions === null || callOptions === void 0 ? void 0 : callOptions.model) !== null && _a !== void 0 ? _a : options.defaultModel,
73
73
  messages: toOpenAIMessages(messages),
@@ -90,6 +90,14 @@ function createLLMCaller(options) {
90
90
  arguments: tc.function.arguments,
91
91
  },
92
92
  })),
93
+ usage: data.usage
94
+ ? {
95
+ promptTokens: data.usage.prompt_tokens,
96
+ completionTokens: data.usage.completion_tokens,
97
+ totalTokens: data.usage.total_tokens,
98
+ cachedPromptTokens: (_c = data.usage.prompt_tokens_details) === null || _c === void 0 ? void 0 : _c.cached_tokens,
99
+ }
100
+ : undefined,
93
101
  };
94
102
  });
95
103
  },
@@ -100,7 +108,7 @@ function createLLMCaller(options) {
100
108
  */
101
109
  stream(messages, tools, callOptions) {
102
110
  return __asyncGenerator(this, arguments, function* stream_1() {
103
- var _a, _b, _c, _d, _e;
111
+ var _a, _b, _c, _d, _e, _f;
104
112
  const body = provider.adaptRequest({
105
113
  model: (_a = callOptions === null || callOptions === void 0 ? void 0 : callOptions.model) !== null && _a !== void 0 ? _a : options.defaultModel,
106
114
  messages: toOpenAIMessages(messages),
@@ -160,7 +168,7 @@ function createLLMCaller(options) {
160
168
  try {
161
169
  parsed = JSON.parse(data);
162
170
  }
163
- catch (_f) {
171
+ catch (_g) {
164
172
  continue;
165
173
  }
166
174
  const choice = (_c = parsed.choices) === null || _c === void 0 ? void 0 : _c[0];
@@ -219,6 +227,14 @@ function createLLMCaller(options) {
219
227
  content: fullContent || null,
220
228
  reasoningContent: fullReasoningContent || undefined,
221
229
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
230
+ usage: parsed.usage
231
+ ? {
232
+ promptTokens: parsed.usage.prompt_tokens,
233
+ completionTokens: parsed.usage.completion_tokens,
234
+ totalTokens: parsed.usage.total_tokens,
235
+ cachedPromptTokens: (_f = parsed.usage.prompt_tokens_details) === null || _f === void 0 ? void 0 : _f.cached_tokens,
236
+ }
237
+ : undefined,
222
238
  });
223
239
  hasYieldedFinish = true;
224
240
  }
@@ -69,7 +69,24 @@ function runLoopStream(config) {
69
69
  const harness = new harness_1.Harness();
70
70
  const messages = buildInitialMessages(config);
71
71
  let turn = 0;
72
+ const usageHistory = [];
72
73
  const signal = config.signal;
74
+ function addUsage(usage) {
75
+ if (usage) {
76
+ usageHistory.push(usage);
77
+ }
78
+ }
79
+ function computeTotalUsage() {
80
+ return usageHistory.reduce((acc, u) => {
81
+ var _a, _b;
82
+ return ({
83
+ promptTokens: acc.promptTokens + u.promptTokens,
84
+ completionTokens: acc.completionTokens + u.completionTokens,
85
+ totalTokens: acc.totalTokens + u.totalTokens,
86
+ cachedPromptTokens: ((_a = acc.cachedPromptTokens) !== null && _a !== void 0 ? _a : 0) + ((_b = u.cachedPromptTokens) !== null && _b !== void 0 ? _b : 0),
87
+ });
88
+ }, { promptTokens: 0, completionTokens: 0, totalTokens: 0 });
89
+ }
73
90
  while (true) {
74
91
  // 检查取消信号
75
92
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
@@ -145,6 +162,7 @@ function runLoopStream(config) {
145
162
  if (!finishChunk) {
146
163
  throw new Error('LLM stream ended without a finish chunk');
147
164
  }
165
+ addUsage(finishChunk.usage);
148
166
  // 使用 finish chunk 中的 content(优先级更高,可能包含完整格式化内容)
149
167
  const assistantContent = (_j = finishChunk.content) !== null && _j !== void 0 ? _j : fullContent;
150
168
  const assistantReasoningContent = (_k = finishChunk.reasoningContent) !== null && _k !== void 0 ? _k : (fullReasoningContent || undefined);
@@ -155,12 +173,14 @@ function runLoopStream(config) {
155
173
  content: assistantContent,
156
174
  reasoningContent: assistantReasoningContent,
157
175
  });
158
- yield yield __await({ type: 'turn_end', turn });
176
+ yield yield __await({ type: 'turn_end', turn, usage: finishChunk.usage });
159
177
  return yield __await({
160
178
  messages,
161
179
  harness: harness.getAll(),
162
180
  finalContent: assistantContent,
163
181
  turns: turn,
182
+ usageHistory,
183
+ totalUsage: computeTotalUsage(),
164
184
  });
165
185
  }
166
186
  // 有 tool_calls,将 assistant message 加入 Messages
@@ -198,7 +218,7 @@ function runLoopStream(config) {
198
218
  // 检查是否有待审批的 tool
199
219
  const pending = parallelResults.find((r) => r.pendingApproval);
200
220
  if (pending === null || pending === void 0 ? void 0 : pending.pendingApproval) {
201
- yield yield __await({ type: 'turn_end', turn });
221
+ yield yield __await({ type: 'turn_end', turn, usage: finishChunk.usage });
202
222
  return yield __await({
203
223
  messages,
204
224
  harness: harness.getAll(),
@@ -210,9 +230,11 @@ function runLoopStream(config) {
210
230
  args: pending.pendingApproval.args,
211
231
  callId: pending.toolCallId,
212
232
  },
233
+ usageHistory,
234
+ totalUsage: computeTotalUsage(),
213
235
  });
214
236
  }
215
- yield yield __await({ type: 'turn_end', turn });
237
+ yield yield __await({ type: 'turn_end', turn, usage: finishChunk.usage });
216
238
  // 第八步:回到第一步继续下一轮
217
239
  }
218
240
  // Loop 被终止(Turn Policy 或硬限制)
@@ -222,6 +244,8 @@ function runLoopStream(config) {
222
244
  harness: harness.getAll(),
223
245
  finalContent: (lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.role) === 'assistant' ? lastMessage.content : null,
224
246
  turns: turn,
247
+ usageHistory,
248
+ totalUsage: computeTotalUsage(),
225
249
  });
226
250
  });
227
251
  }
@@ -1,3 +1,5 @@
1
+ import type { TokenUsage } from '../llm_provider/types';
2
+ export type { TokenUsage };
1
3
  /**
2
4
  * LLM 对话消息。支持 system、user、assistant、tool 四种角色。
3
5
  * assistant 角色可携带 tool_calls;tool 角色需携带 tool_call_id。
@@ -168,6 +170,7 @@ export type LLMStreamChunk = {
168
170
  readonly content: string | null;
169
171
  readonly reasoningContent?: string;
170
172
  readonly tool_calls?: ReadonlyArray<ToolCall>;
173
+ readonly usage?: TokenUsage;
171
174
  };
172
175
  /**
173
176
  * LLM 调用接口。由外部注入,Loop 内部不绑定具体 Provider。
@@ -177,6 +180,7 @@ export interface LLMCaller {
177
180
  readonly content: string | null;
178
181
  readonly reasoningContent?: string;
179
182
  readonly tool_calls?: ReadonlyArray<ToolCall>;
183
+ readonly usage?: TokenUsage;
180
184
  }>;
181
185
  readonly stream?: (messages: ReadonlyArray<Message>, tools: ReadonlyArray<ToolDefinition>, options?: LLMCallOptions) => AsyncGenerator<LLMStreamChunk, void, unknown>;
182
186
  }
@@ -229,6 +233,10 @@ export interface LoopResult {
229
233
  readonly finalContent: string | null;
230
234
  readonly turns: number;
231
235
  readonly pendingApproval?: PendingApprovalInfo;
236
+ /** 每轮 LLM 调用的 token 消耗明细 */
237
+ readonly usageHistory: TokenUsage[];
238
+ /** 累计 token 消耗 */
239
+ readonly totalUsage: TokenUsage;
232
240
  }
233
241
  /**
234
242
  * runLoopStream 的流式事件类型。
@@ -259,4 +267,5 @@ export type LoopStreamChunk = {
259
267
  } | {
260
268
  readonly type: 'turn_end';
261
269
  readonly turn: number;
270
+ readonly usage?: TokenUsage;
262
271
  };
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * import 'dotenv/config';
7
7
  * import { LLM } from '@keo-ai/axiom';
8
8
  *
9
- * const res = await LLM.predict({ model: 'qwen3.7-max', prompt: '你好' });
9
+ * const { content, usage } = await LLM.predict({ model: 'qwen3.7-max', prompt: '你好' });
10
10
  * ```
11
11
  *
12
12
  * 必需环境变量:
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * import 'dotenv/config';
8
8
  * import { LLM } from '@keo-ai/axiom';
9
9
  *
10
- * const res = await LLM.predict({ model: 'qwen3.7-max', prompt: '你好' });
10
+ * const { content, usage } = await LLM.predict({ model: 'qwen3.7-max', prompt: '你好' });
11
11
  * ```
12
12
  *
13
13
  * 必需环境变量:
@@ -74,7 +74,7 @@ class BailianProvider {
74
74
  */
75
75
  generate(request) {
76
76
  return __awaiter(this, void 0, void 0, function* () {
77
- var _a;
77
+ var _a, _b;
78
78
  const body = this.adaptRequest(Object.assign(Object.assign({}, this.buildRequestBody(request)), { stream: false }));
79
79
  const data = yield (0, index_1.callChatCompletions)(this.config.baseUrl, this.config.apiKey, body, this.name);
80
80
  const choice = data.choices[0];
@@ -86,9 +86,10 @@ class BailianProvider {
86
86
  promptTokens: data.usage.prompt_tokens,
87
87
  completionTokens: data.usage.completion_tokens,
88
88
  totalTokens: data.usage.total_tokens,
89
+ cachedPromptTokens: (_a = data.usage.prompt_tokens_details) === null || _a === void 0 ? void 0 : _a.cached_tokens,
89
90
  }
90
91
  : undefined,
91
- model: (_a = data.model) !== null && _a !== void 0 ? _a : body.model,
92
+ model: (_b = data.model) !== null && _b !== void 0 ? _b : body.model,
92
93
  };
93
94
  });
94
95
  }
@@ -100,7 +101,7 @@ class BailianProvider {
100
101
  */
101
102
  stream(request) {
102
103
  return __asyncGenerator(this, arguments, function* stream_1() {
103
- var _a, _b;
104
+ var _a, _b, _c;
104
105
  const url = `${this.config.baseUrl}/chat/completions`;
105
106
  const body = this.adaptRequest(Object.assign(Object.assign({}, this.buildRequestBody(request)), { stream: true }));
106
107
  let response;
@@ -146,7 +147,7 @@ class BailianProvider {
146
147
  try {
147
148
  parsed = JSON.parse(data);
148
149
  }
149
- catch (_c) {
150
+ catch (_d) {
150
151
  continue;
151
152
  }
152
153
  const choice = (_b = parsed.choices) === null || _b === void 0 ? void 0 : _b[0];
@@ -166,6 +167,7 @@ class BailianProvider {
166
167
  promptTokens: parsed.usage.prompt_tokens,
167
168
  completionTokens: parsed.usage.completion_tokens,
168
169
  totalTokens: parsed.usage.total_tokens,
170
+ cachedPromptTokens: (_c = parsed.usage.prompt_tokens_details) === null || _c === void 0 ? void 0 : _c.cached_tokens,
169
171
  },
170
172
  });
171
173
  }
@@ -59,6 +59,9 @@ export interface OpenAIChatResponse {
59
59
  prompt_tokens: number;
60
60
  completion_tokens: number;
61
61
  total_tokens: number;
62
+ prompt_tokens_details?: {
63
+ cached_tokens: number;
64
+ };
62
65
  };
63
66
  model?: string;
64
67
  }
@@ -17,17 +17,23 @@ export interface LLMRequest {
17
17
  readonly model?: string;
18
18
  readonly responseFormat?: 'text' | 'json';
19
19
  }
20
+ /**
21
+ * LLM 调用的 token 消耗统计。
22
+ */
23
+ export interface TokenUsage {
24
+ readonly promptTokens: number;
25
+ readonly completionTokens: number;
26
+ readonly totalTokens: number;
27
+ /** 命中缓存的 prompt token 数(Prompt Caching) */
28
+ readonly cachedPromptTokens?: number;
29
+ }
20
30
  /**
21
31
  * 标准化 LLM 响应。各 Provider 将原始响应解析为此格式后返回。
22
32
  */
23
33
  export interface LLMResponse {
24
34
  readonly content: string | null;
25
35
  readonly reasoningContent?: string;
26
- readonly usage?: {
27
- readonly promptTokens: number;
28
- readonly completionTokens: number;
29
- readonly totalTokens: number;
30
- };
36
+ readonly usage?: TokenUsage;
31
37
  readonly model: string;
32
38
  }
33
39
  /**
@@ -1,4 +1,4 @@
1
- import type { Message, StreamChunk } from '../llm_provider/types';
1
+ import type { Message, StreamChunk, TokenUsage } from '../llm_provider/types';
2
2
  import type { PredictConfig, PredictWithMessagesConfig } from './config';
3
3
  /**
4
4
  * LLM 静态类。封装 Provider 连接、请求组装、故障转移和返回解析。
@@ -8,8 +8,8 @@ import type { PredictConfig, PredictWithMessagesConfig } from './config';
8
8
  * ```ts
9
9
  * import { LLM } from 'axiom';
10
10
  *
11
- * const res = await LLM.predict({ model: 'qwen-max', prompt: '你好' });
12
- * console.log(res);
11
+ * const { content, usage } = await LLM.predict({ model: 'qwen-max', prompt: '你好' });
12
+ * console.log(content, usage);
13
13
  * ```
14
14
  */
15
15
  export declare class LLM {
@@ -20,33 +20,45 @@ export declare class LLM {
20
20
  * 内部自动构造单条 user message,支持可选的 system prompt。
21
21
  *
22
22
  * 根据 `responseFormat` 返回不同类型:
23
- * - 未设置 / `'text'` → `string`
24
- * - `'json'` → 解析后的对象(`any`)
23
+ * - 未设置 / `'text'` → `{ content: string, usage?: TokenUsage }`
24
+ * - `'json'` → `{ content: any, usage?: TokenUsage }`
25
25
  *
26
26
  * @param config - 预测配置(模型、prompt、温度等)
27
- * @returns 模型生成的完整响应
27
+ * @returns 模型生成的完整响应及 token 消耗统计
28
28
  * @throws Error - Provider 未配置、模型不存在、或所有 Provider 均失败时抛出
29
29
  */
30
30
  static predict(config: PredictConfig & {
31
31
  responseFormat: 'json';
32
- }): Promise<any>;
33
- static predict(config: PredictConfig): Promise<string>;
32
+ }): Promise<{
33
+ content: any;
34
+ usage?: TokenUsage;
35
+ }>;
36
+ static predict(config: PredictConfig): Promise<{
37
+ content: string;
38
+ usage?: TokenUsage;
39
+ }>;
34
40
  /**
35
41
  * 使用自定义消息列表调用 LLM。适用于多轮对话等需要精细控制 message 结构的场景。
36
42
  *
37
43
  * 根据 `responseFormat` 返回不同类型:
38
- * - 未设置 / `'text'` → `string`
39
- * - `'json'` → 解析后的对象(可用泛型指定类型)
44
+ * - 未设置 / `'text'` → `{ content: string, usage?: TokenUsage }`
45
+ * - `'json'` → `{ content: any, usage?: TokenUsage }`
40
46
  *
41
47
  * @param messages - 消息列表(user/assistant 角色)
42
48
  * @param config - 预测配置(不含 prompt,因为由 messages 提供)
43
- * @returns 模型生成的完整响应
49
+ * @returns 模型生成的完整响应及 token 消耗统计
44
50
  * @throws Error - Provider 未配置、模型不存在、或所有 Provider 均失败时抛出
45
51
  */
46
52
  static predictWithMessages(messages: ReadonlyArray<Message>, config: PredictWithMessagesConfig & {
47
53
  responseFormat: 'json';
48
- }): Promise<any>;
49
- static predictWithMessages(messages: ReadonlyArray<Message>, config: PredictWithMessagesConfig): Promise<string>;
54
+ }): Promise<{
55
+ content: any;
56
+ usage?: TokenUsage;
57
+ }>;
58
+ static predictWithMessages(messages: ReadonlyArray<Message>, config: PredictWithMessagesConfig): Promise<{
59
+ content: string;
60
+ usage?: TokenUsage;
61
+ }>;
50
62
  /**
51
63
  * 流式调用 LLM,逐块返回模型输出。
52
64
  *
@@ -69,14 +69,15 @@ function toLLMRequest(config, messages) {
69
69
  }
70
70
  function unwrapResponse(response, responseFormat) {
71
71
  var _a;
72
+ const usage = response.usage;
72
73
  if (responseFormat === 'json') {
73
74
  if (!response.content) {
74
75
  throw new Error('Empty response content when responseFormat is json');
75
76
  }
76
- return JSON.parse(response.content);
77
+ return { content: JSON.parse(response.content), usage };
77
78
  }
78
79
  // 默认按 text 返回(包括未设置 responseFormat 的情况)
79
- return (_a = response.content) !== null && _a !== void 0 ? _a : '';
80
+ return { content: (_a = response.content) !== null && _a !== void 0 ? _a : '', usage };
80
81
  }
81
82
  /**
82
83
  * LLM 静态类。封装 Provider 连接、请求组装、故障转移和返回解析。
@@ -86,8 +87,8 @@ function unwrapResponse(response, responseFormat) {
86
87
  * ```ts
87
88
  * import { LLM } from 'axiom';
88
89
  *
89
- * const res = await LLM.predict({ model: 'qwen-max', prompt: '你好' });
90
- * console.log(res);
90
+ * const { content, usage } = await LLM.predict({ model: 'qwen-max', prompt: '你好' });
91
+ * console.log(content, usage);
91
92
  * ```
92
93
  */
93
94
  class LLM {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keo-ai/axiom",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "基于 LLM 的预测与推理库,支持多 Provider 切换",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",