@keo-ai/axiom 0.2.0 → 0.2.2

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,30 +170,40 @@ 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
+ }
180
+ if (chunk.type === 'finish' && chunk.usage) {
181
+ console.log('Token usage:', chunk.usage);
182
+ // { promptTokens, completionTokens, totalTokens, cachedPromptTokens? }
183
+ }
168
184
  }
169
185
  ```
170
186
 
187
+ > 💡 流式调用自动启用 `stream_options: { include_usage: true }`,`finish` 事件携带完整的 token 消耗统计。usage 可能与 `finish_reason` 在同一个 SSE chunk,也可能在独立的 chunk(`choices: []`)中返回,两种情况均已兼容。
188
+
171
189
  ### 模型列表
172
190
 
173
191
  当前支持的模型(通过 `Model` 类型枚举):
174
192
 
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`。
193
+ | 模型 | 推理深度 | json 模式 | 说明 |
194
+ |------|----------|-----------|------|
195
+ | `qwen-max` | 不支持 | | **默认模型** |
196
+ | `qwen3.7-max` | ✅ 映射支持 | ✅ | `low`/`medium`/`high` 映射为 `enable_thinking` |
197
+ | `qwen-plus` | ✅ 映射支持 | | 同上 |
198
+ | `qwen-turbo` | ✅ 映射支持 | | 同上 |
199
+ | `deepseek-v4-pro` | ✅ 原生支持 | | `low`/`medium`/`high` 直接传递 |
200
+ | `deepseek-v4-flash` | ✅ 原生支持 | ✅ | 同上 |
201
+ | `kimi-k2.6` | 原生支持 | | 同上 |
202
+ | `qwq-plus` | ❌ 不支持 | | 固定推理行为,不可调节 |
203
+ | `glm-5.1` | ❌ 不支持 | | 不支持推理参数,不支持 json 模式 |
204
+ | `qwen-vl-plus` | ❌ 不支持 | ✅ | 视觉模型,不支持推理参数 |
205
+
206
+ > 推理深度的支持方式由 Provider 内部维护。百炼 Provider 中,Qwen 系列通过 `extra_body.enable_thinking` 映射实现(`low` 关闭,`medium`/`high` 开启),DeepSeek / Kimi 则原生透传 `reasoning_effort`。
188
207
  >
189
208
  > 目前所有模型均路由到百炼 Provider。后续接入其他厂商时,通过 `MODEL_REGISTRY` 扩展映射即可。
190
209
 
@@ -194,7 +213,7 @@ Predict 模块遵循 Axiom 的统一错误策略:**直接抛异常,调用方
194
213
 
195
214
  ```ts
196
215
  try {
197
- const res = await LLM.predict({ model: 'qwen3.7-max', prompt: 'hi' });
216
+ const { content, usage } = await LLM.predict({ model: 'qwen-max', prompt: 'hi' });
198
217
  } catch (e) {
199
218
  // e.message 包含 Provider 汇总错误信息
200
219
  }
@@ -232,7 +251,7 @@ const result = await FunctionCallLoop.runLoop({
232
251
  { role: 'system', content: 'You are a helpful assistant.' },
233
252
  { role: 'user', content: 'What is the weather in Beijing?' },
234
253
  ],
235
- model: 'qwen3.7-max',
254
+ model: 'qwen-max',
236
255
  temperature: 0.7,
237
256
  maxTokens: 2048,
238
257
  tools: [
@@ -262,6 +281,8 @@ const result = await FunctionCallLoop.runLoop({
262
281
  console.log(result.finalContent);
263
282
  console.log('Turns:', result.turns);
264
283
  console.log('Harness:', result.harness);
284
+ console.log('Token usage:', result.totalUsage); // 累计 token 消耗
285
+ console.log('Usage history:', result.usageHistory); // 每轮明细
265
286
  ```
266
287
 
267
288
  > 💡 需要**流式输出**(实时展示 LLM 生成内容)?使用下方的 [`runLoopStream`](#流式调用runloopstream)。
@@ -313,6 +334,9 @@ for await (const chunk of stream) {
313
334
  break;
314
335
  case 'turn_end':
315
336
  console.log(`\n--- Turn ${chunk.turn} End ---`);
337
+ if (chunk.usage) {
338
+ console.log(`Token usage: prompt=${chunk.usage.promptTokens}, completion=${chunk.usage.completionTokens}`);
339
+ }
316
340
  break;
317
341
  }
318
342
  }
@@ -349,7 +373,9 @@ console.log('Turns:', result!.turns);
349
373
  | `content` | LLM 输出 content(正式回复)增量 | `delta`, `turn` |
350
374
  | `tool_call` | LLM 决定调用 tool(流结束、完整 tool_calls 解析完成) | `toolCalls`, `turn` |
351
375
  | `tool_result` | tool 执行完成 | `callId`, `toolName`, `content`, `status`, `turn` |
352
- | `turn_end` | 一轮结束(tool 全部执行完或 content 直接返回) | `turn` |
376
+ | `turn_end` | 一轮结束(tool 全部执行完或 content 直接返回) | `turn`, `usage?` |
377
+
378
+ > 💡 `turn_end` 的 `usage` 字段携带该轮 LLM 调用的 token 消耗统计(`promptTokens`、`completionTokens`、`totalTokens`、`cachedPromptTokens?`)。流式请求自动启用 `stream_options: { include_usage: true }`,确保 API 返回 usage 数据。
353
379
 
354
380
  #### 流式 vs 非流式的选择
355
381
 
@@ -678,6 +704,15 @@ interface LoopResult {
678
704
  harness: HarnessRecord[]; // 执行历史
679
705
  finalContent: string | null; // 最终回复内容
680
706
  turns: number; // 实际执行轮数
707
+ usageHistory: TokenUsage[]; // 每轮 LLM 调用的 token 消耗明细
708
+ totalUsage: TokenUsage; // 累计 token 消耗(含 cachedPromptTokens)
709
+ }
710
+
711
+ interface TokenUsage {
712
+ promptTokens: number;
713
+ completionTokens: number;
714
+ totalTokens: number;
715
+ cachedPromptTokens?: number; // 命中缓存的 prompt token(Prompt Caching)
681
716
  }
682
717
  ```
683
718
 
@@ -825,7 +860,7 @@ interface SearchResult {
825
860
 
826
861
  ### 过滤
827
862
 
828
- 支持对表的任意独立列做等值和范围过滤:
863
+ 支持对表的任意独立列做等值、范围和 IN 查询:
829
864
 
830
865
  ```ts
831
866
  // 等值过滤
@@ -833,7 +868,7 @@ const results = await EmbeddingSearch.query('query', {
833
868
  filter: { category: '衣服', shop: '旗舰店' },
834
869
  }, pool);
835
870
 
836
- // 混合:等值 + 数值范围
871
+ // 数值范围
837
872
  const ranked = await EmbeddingSearch.query('query', {
838
873
  filter: {
839
874
  category: '衣服',
@@ -854,36 +889,7 @@ const multi = await EmbeddingSearch.query('query', {
854
889
 
855
890
  ### 枚举值解析
856
891
 
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
- ```
892
+ 默认开启 `parseEnum`,返回结果中的特定枚举字段会自动从数字值解析为可读文本。如需关闭解析(保留原始数字值),设置 `parseEnum: false`。
887
893
 
888
894
  未知枚举值、非数字类型、非枚举字段均保持原样不变。
889
895
 
@@ -970,5 +976,3 @@ EmbeddingSearch 模块内部调用百炼的 embedding 和 rerank 服务,无需
970
976
  - **项目层填充内容** — 上层准备消息、工具、业务逻辑,Axiom 负责可靠执行
971
977
  - **副作用即状态** — 工具调用改变外部世界,Harness 让这种改变在 Loop 间可见
972
978
  - **单一事实来源** — 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),
@@ -110,6 +118,7 @@ function createLLMCaller(options) {
110
118
  tools: tools.length > 0 ? toOpenAITools(tools) : undefined,
111
119
  reasoning_effort: callOptions === null || callOptions === void 0 ? void 0 : callOptions.reasoningEffort,
112
120
  stream: true,
121
+ stream_options: { include_usage: true },
113
122
  });
114
123
  const url = `${options.baseUrl}/chat/completions`;
115
124
  let response;
@@ -140,7 +149,27 @@ function createLLMCaller(options) {
140
149
  const toolCallAccumulators = [];
141
150
  let fullContent = '';
142
151
  let fullReasoningContent = '';
143
- let hasYieldedFinish = false;
152
+ // 累积 usage:OpenAI 兼容 API 在 stream_options.include_usage 开启时,
153
+ // usage 可能与 finish_reason 在同一个 chunk,也可能在独立的 chunk(choices: [])中返回。
154
+ // 统一在此收集,流结束后合并到 finish 事件中 yield。
155
+ let streamUsage;
156
+ let hasFinishReason = false;
157
+ function buildToolCalls() {
158
+ const toolCalls = [];
159
+ for (const acc of toolCallAccumulators) {
160
+ if (acc.id && acc.type && acc.function.name) {
161
+ toolCalls.push({
162
+ id: acc.id,
163
+ type: acc.type,
164
+ function: {
165
+ name: acc.function.name,
166
+ arguments: acc.function.arguments,
167
+ },
168
+ });
169
+ }
170
+ }
171
+ return toolCalls;
172
+ }
144
173
  try {
145
174
  while (true) {
146
175
  const { done, value } = yield __await(reader.read());
@@ -160,10 +189,19 @@ function createLLMCaller(options) {
160
189
  try {
161
190
  parsed = JSON.parse(data);
162
191
  }
163
- catch (_f) {
192
+ catch (_g) {
164
193
  continue;
165
194
  }
166
- const choice = (_c = parsed.choices) === null || _c === void 0 ? void 0 : _c[0];
195
+ // 从任意 chunk 中收集 usage(包括 choices 为空的独立 usage chunk)
196
+ if (parsed.usage) {
197
+ streamUsage = {
198
+ promptTokens: parsed.usage.prompt_tokens,
199
+ completionTokens: parsed.usage.completion_tokens,
200
+ totalTokens: parsed.usage.total_tokens,
201
+ cachedPromptTokens: (_c = parsed.usage.prompt_tokens_details) === null || _c === void 0 ? void 0 : _c.cached_tokens,
202
+ };
203
+ }
204
+ const choice = (_d = parsed.choices) === null || _d === void 0 ? void 0 : _d[0];
167
205
  if (!choice)
168
206
  continue;
169
207
  const delta = choice.delta;
@@ -191,61 +229,29 @@ function createLLMCaller(options) {
191
229
  acc.id = tcDelta.id;
192
230
  if (tcDelta.type)
193
231
  acc.type = tcDelta.type;
194
- if ((_d = tcDelta.function) === null || _d === void 0 ? void 0 : _d.name) {
232
+ if ((_e = tcDelta.function) === null || _e === void 0 ? void 0 : _e.name) {
195
233
  acc.function.name = tcDelta.function.name;
196
234
  }
197
- if ((_e = tcDelta.function) === null || _e === void 0 ? void 0 : _e.arguments) {
235
+ if ((_f = tcDelta.function) === null || _f === void 0 ? void 0 : _f.arguments) {
198
236
  acc.function.arguments += tcDelta.function.arguments;
199
237
  }
200
238
  }
201
239
  }
202
- // 流结束
240
+ // 标记流结束(不立即 yield finish,等 usage 收集完毕)
203
241
  if (choice.finish_reason) {
204
- const toolCalls = [];
205
- for (const acc of toolCallAccumulators) {
206
- if (acc.id && acc.type && acc.function.name) {
207
- toolCalls.push({
208
- id: acc.id,
209
- type: acc.type,
210
- function: {
211
- name: acc.function.name,
212
- arguments: acc.function.arguments,
213
- },
214
- });
215
- }
216
- }
217
- yield yield __await({
218
- type: 'finish',
219
- content: fullContent || null,
220
- reasoningContent: fullReasoningContent || undefined,
221
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
222
- });
223
- hasYieldedFinish = true;
224
- }
225
- }
226
- }
227
- // 兜底:如果流正常结束但没有收到 finish_reason,也 yield 一个 finish
228
- if (!hasYieldedFinish) {
229
- const toolCalls = [];
230
- for (const acc of toolCallAccumulators) {
231
- if (acc.id && acc.type && acc.function.name) {
232
- toolCalls.push({
233
- id: acc.id,
234
- type: acc.type,
235
- function: {
236
- name: acc.function.name,
237
- arguments: acc.function.arguments,
238
- },
239
- });
242
+ hasFinishReason = true;
240
243
  }
241
244
  }
242
- yield yield __await({
243
- type: 'finish',
244
- content: fullContent || null,
245
- reasoningContent: fullReasoningContent || undefined,
246
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
247
- });
248
245
  }
246
+ // 流结束,统一 yield finish 事件(确保包含累积的 usage)
247
+ const toolCalls = buildToolCalls();
248
+ yield yield __await({
249
+ type: 'finish',
250
+ content: fullContent || null,
251
+ reasoningContent: fullReasoningContent || undefined,
252
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
253
+ usage: streamUsage,
254
+ });
249
255
  }
250
256
  finally {
251
257
  reader.releaseLock();
@@ -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,9 +101,9 @@ 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
- const body = this.adaptRequest(Object.assign(Object.assign({}, this.buildRequestBody(request)), { stream: true }));
106
+ const body = this.adaptRequest(Object.assign(Object.assign({}, this.buildRequestBody(request)), { stream: true, stream_options: { include_usage: true } }));
106
107
  let response;
107
108
  try {
108
109
  response = yield __await(fetch(url, {
@@ -127,6 +128,10 @@ class BailianProvider {
127
128
  const reader = response.body.getReader();
128
129
  const decoder = new TextDecoder();
129
130
  let buffer = '';
131
+ // 累积 usage:OpenAI 兼容 API 在 stream_options.include_usage 开启时,
132
+ // usage 可能与 finish_reason 在同一个 chunk,也可能在独立的 chunk(choices: [])中返回。
133
+ // 统一在此收集,流结束后一次性 yield finish 事件。
134
+ let streamUsage;
130
135
  try {
131
136
  while (true) {
132
137
  const { done, value } = yield __await(reader.read());
@@ -146,10 +151,19 @@ class BailianProvider {
146
151
  try {
147
152
  parsed = JSON.parse(data);
148
153
  }
149
- catch (_c) {
154
+ catch (_d) {
150
155
  continue;
151
156
  }
152
- const choice = (_b = parsed.choices) === null || _b === void 0 ? void 0 : _b[0];
157
+ // 从任意 chunk 中收集 usage(包括 choices 为空的独立 usage chunk)
158
+ if (parsed.usage) {
159
+ streamUsage = {
160
+ promptTokens: parsed.usage.prompt_tokens,
161
+ completionTokens: parsed.usage.completion_tokens,
162
+ totalTokens: parsed.usage.total_tokens,
163
+ cachedPromptTokens: (_b = parsed.usage.prompt_tokens_details) === null || _b === void 0 ? void 0 : _b.cached_tokens,
164
+ };
165
+ }
166
+ const choice = (_c = parsed.choices) === null || _c === void 0 ? void 0 : _c[0];
153
167
  if (!choice)
154
168
  continue;
155
169
  const delta = choice.delta;
@@ -159,19 +173,9 @@ class BailianProvider {
159
173
  if (delta.content) {
160
174
  yield yield __await({ type: 'content', delta: delta.content });
161
175
  }
162
- if (choice.finish_reason && parsed.usage) {
163
- yield yield __await({
164
- type: 'finish',
165
- usage: {
166
- promptTokens: parsed.usage.prompt_tokens,
167
- completionTokens: parsed.usage.completion_tokens,
168
- totalTokens: parsed.usage.total_tokens,
169
- },
170
- });
171
- }
172
176
  }
173
177
  }
174
- yield yield __await({ type: 'finish' });
178
+ yield yield __await({ type: 'finish', usage: streamUsage });
175
179
  }
176
180
  finally {
177
181
  reader.releaseLock();
@@ -192,6 +196,8 @@ class BailianProvider {
192
196
  response_format: request.responseFormat
193
197
  ? { type: request.responseFormat === 'json' ? 'json_object' : 'text' }
194
198
  : undefined,
199
+ stream: request.stream,
200
+ stream_options: request.streamOptions,
195
201
  };
196
202
  }
197
203
  }
@@ -35,6 +35,10 @@ export interface OpenAIChatRequest {
35
35
  type: 'text' | 'json_object';
36
36
  };
37
37
  stream?: boolean;
38
+ /** 流式请求时是否在最后一个 chunk 中返回 usage 信息。OpenAI 兼容 API 默认不返回,需显式开启 */
39
+ stream_options?: {
40
+ include_usage: boolean;
41
+ };
38
42
  reasoning_effort?: 'low' | 'medium' | 'high';
39
43
  /** 百炼等平台的扩展参数 */
40
44
  extra_body?: Record<string, unknown>;
@@ -59,6 +63,9 @@ export interface OpenAIChatResponse {
59
63
  prompt_tokens: number;
60
64
  completion_tokens: number;
61
65
  total_tokens: number;
66
+ prompt_tokens_details?: {
67
+ cached_tokens: number;
68
+ };
62
69
  };
63
70
  model?: string;
64
71
  }
@@ -14,20 +14,29 @@ export interface LLMRequest {
14
14
  readonly maxTokens?: number;
15
15
  readonly topP?: number;
16
16
  readonly stream?: boolean;
17
+ readonly streamOptions?: {
18
+ include_usage: boolean;
19
+ };
17
20
  readonly model?: string;
18
21
  readonly responseFormat?: 'text' | 'json';
19
22
  }
23
+ /**
24
+ * LLM 调用的 token 消耗统计。
25
+ */
26
+ export interface TokenUsage {
27
+ readonly promptTokens: number;
28
+ readonly completionTokens: number;
29
+ readonly totalTokens: number;
30
+ /** 命中缓存的 prompt token 数(Prompt Caching) */
31
+ readonly cachedPromptTokens?: number;
32
+ }
20
33
  /**
21
34
  * 标准化 LLM 响应。各 Provider 将原始响应解析为此格式后返回。
22
35
  */
23
36
  export interface LLMResponse {
24
37
  readonly content: string | null;
25
38
  readonly reasoningContent?: string;
26
- readonly usage?: {
27
- readonly promptTokens: number;
28
- readonly completionTokens: number;
29
- readonly totalTokens: number;
30
- };
39
+ readonly usage?: TokenUsage;
31
40
  readonly model: string;
32
41
  }
33
42
  /**
@@ -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.2",
4
4
  "description": "基于 LLM 的预测与推理库,支持多 Provider 切换",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",