pi-langfuse 1.5.17 → 1.5.18
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 +39 -0
- package/README_CN.md +28 -0
- package/package.json +1 -1
- package/src/capture-policy.ts +6 -1
- package/src/config.ts +3 -0
- package/src/types.ts +2 -0
- package/src/usage-options.ts +41 -0
- package/src/utils.ts +42 -2
package/README.md
CHANGED
|
@@ -175,6 +175,44 @@ export PI_LANGFUSE_MAX_FALLBACK_TOTAL_BYTES=33554432 # whole-payload ceiling, d
|
|
|
175
175
|
When the accumulated fallback payload exceeds the 32MB ceiling, ingestion is
|
|
176
176
|
skipped with a warning instead of attempting an unrecoverably large upload.
|
|
177
177
|
|
|
178
|
+
### Reasoning tokens
|
|
179
|
+
|
|
180
|
+
Pi reports reasoning (thinking) tokens for Anthropic, OpenAI Codex, OpenRouter,
|
|
181
|
+
opencode-go and Qwen. Providers count them inside `output`, and by default the
|
|
182
|
+
extension reports `output` whole, so the reasoning share is not visible in
|
|
183
|
+
Langfuse. Opt in to report it as its own usage bucket:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
export PI_LANGFUSE_SPLIT_REASONING_TOKENS=true
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Or persist it in `config.json`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{ "capture": { "PI_LANGFUSE_SPLIT_REASONING_TOKENS": "true" } }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
With the split on, a generation that used 37 output tokens of which 10 were
|
|
196
|
+
reasoning is reported as `output: 27` plus `output_reasoning_tokens: 10`. Both
|
|
197
|
+
keys contain `output`, so the Output row in Langfuse still shows 37; only the
|
|
198
|
+
breakdown beneath it gains the reasoning share. Reasoning is clamped to the
|
|
199
|
+
reported `output`, so the buckets always add up to the total.
|
|
200
|
+
|
|
201
|
+
> **Before enabling — check your model prices.** Langfuse matches prices to
|
|
202
|
+
> usage by exact key, and model definitions you created in your project take
|
|
203
|
+
> precedence over Langfuse's maintained defaults. A custom model priced on
|
|
204
|
+
> `input` and `output` only would cost `output_reasoning_tokens` at zero, so
|
|
205
|
+
> reasoning-heavy generations would look cheaper than they are. Add a price
|
|
206
|
+
> for `output_reasoning_tokens` to every custom reasoning model in
|
|
207
|
+
> **Settings → Models** first, then turn the split on. Langfuse's built-in
|
|
208
|
+
> prices for reasoning models already include it. Providers that report their
|
|
209
|
+
> own cost are unaffected: Langfuse uses the reported cost as-is and does not
|
|
210
|
+
> recompute it from usage.
|
|
211
|
+
|
|
212
|
+
The split is off by default, so upgrading changes nothing until you enable it.
|
|
213
|
+
Unset `PI_LANGFUSE_SPLIT_REASONING_TOKENS` (or set it to `false`) to go back;
|
|
214
|
+
traces already ingested keep their buckets.
|
|
215
|
+
|
|
178
216
|
### Method 3: Persistent `config.json`
|
|
179
217
|
|
|
180
218
|
Create or update `~/.pi/agent/pi-langfuse/config.json`:
|
|
@@ -230,6 +268,7 @@ This command makes a timeout-bounded authenticated request to Langfuse and, if i
|
|
|
230
268
|
- The trace contains the final assistant output shown in Pi.
|
|
231
269
|
- Tool runs appear as tool observations with arguments, results, and error state.
|
|
232
270
|
- LLM requests appear as generation observations, including usage and cost when the provider exposes them.
|
|
271
|
+
Reasoning tokens are reported as their own usage bucket when `PI_LANGFUSE_SPLIT_REASONING_TOKENS` is enabled.
|
|
233
272
|
- Trace-level scores include tool counts, tool success rate, and whether the run had errors.
|
|
234
273
|
|
|
235
274
|
The package also includes a Langfuse CLI skill, so Langfuse data can be queried directly from Pi:
|
package/README_CN.md
CHANGED
|
@@ -167,6 +167,33 @@ export PI_LANGFUSE_MAX_FALLBACK_TOTAL_BYTES=33554432 # 整体负载上限,默
|
|
|
167
167
|
|
|
168
168
|
当累积的回退负载超过 32MB 上限时,会跳过摄取并给出告警,而不是尝试一次注定失败的超大上传。
|
|
169
169
|
|
|
170
|
+
### 推理(reasoning)token
|
|
171
|
+
|
|
172
|
+
Pi 会为 Anthropic、OpenAI Codex、OpenRouter、opencode-go 和 Qwen 上报推理(thinking)token。提供商将其计入
|
|
173
|
+
`output`,扩展默认也原样上报整个 `output`,因此在 Langfuse 中看不到推理部分的占比。可显式开启,将其作为独立用量桶上报:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
export PI_LANGFUSE_SPLIT_REASONING_TOKENS=true
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
也可以持久化到 `config.json`:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{ "capture": { "PI_LANGFUSE_SPLIT_REASONING_TOKENS": "true" } }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
开启后,一次消耗 37 个输出 token(其中 10 个为推理)的生成会上报为 `output: 27` 加
|
|
186
|
+
`output_reasoning_tokens: 10`。两个键都包含 `output`,所以 Langfuse 的 Output 行仍显示 37,只是其下的明细多出推理占比。
|
|
187
|
+
推理数会被限制在上报的 `output` 范围内,因此各桶之和始终等于总量。
|
|
188
|
+
|
|
189
|
+
> **开启前请先检查模型价格。** Langfuse 按键名精确匹配用量与价格,且你在项目中自定义的模型定义优先于 Langfuse 维护的
|
|
190
|
+
> 默认值。若自定义模型只定义了 `input` 和 `output` 价格,`output_reasoning_tokens` 会按零计价,推理密集的生成会显得比实际便宜。
|
|
191
|
+
> 请先在 **Settings → Models** 中为每个自定义推理模型补上 `output_reasoning_tokens` 的价格,再开启拆分。Langfuse 内置的
|
|
192
|
+
> 推理模型价格已包含该键。自行上报成本的提供商不受影响:Langfuse 会直接使用上报的成本,不会再根据用量重新计算。
|
|
193
|
+
|
|
194
|
+
该拆分默认关闭,升级后在你显式开启之前不会有任何变化。取消设置 `PI_LANGFUSE_SPLIT_REASONING_TOKENS`(或设为 `false`)即可回退;
|
|
195
|
+
已摄取的 trace 保留其原有用量桶。
|
|
196
|
+
|
|
170
197
|
### 方式 3:持久化 `config.json`
|
|
171
198
|
|
|
172
199
|
创建或更新 `~/.pi/agent/pi-langfuse/config.json`:
|
|
@@ -222,6 +249,7 @@ pi list
|
|
|
222
249
|
- trace 中会包含 Pi 实际显示的最终助手回复。
|
|
223
250
|
- 工具执行会以工具观察节点展示参数、结果和错误状态。
|
|
224
251
|
- 模型请求会以生成观察节点展示;如果提供商暴露相关信息,还会包含用量和成本。
|
|
252
|
+
开启 `PI_LANGFUSE_SPLIT_REASONING_TOKENS` 后,推理 token 会作为独立用量桶上报。
|
|
225
253
|
- trace 级别会记录工具调用次数、工具成功率和是否出现错误。
|
|
226
254
|
|
|
227
255
|
此包还包含一个内置 Langfuse 技能,可直接在 Pi 中查询 Langfuse 数据:
|
package/package.json
CHANGED
package/src/capture-policy.ts
CHANGED
|
@@ -90,7 +90,12 @@ const FLAG_TO_FIELD = {
|
|
|
90
90
|
LANGFUSE_CAPTURE_PATHS: "capturePaths",
|
|
91
91
|
} as const;
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Parse a boolean env flag. `1/true/yes/on` and `0/false/no/off` are
|
|
95
|
+
* case-insensitive; anything else (including unset) is `undefined` so the
|
|
96
|
+
* caller keeps its default.
|
|
97
|
+
*/
|
|
98
|
+
export function parseFlag(value: string | undefined): boolean | undefined {
|
|
94
99
|
if (value === undefined) {
|
|
95
100
|
return undefined;
|
|
96
101
|
}
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { state } from "./state.js";
|
|
|
6
6
|
import { forceShutdownRuntime } from "./langfuse.js";
|
|
7
7
|
import { createCapturePolicy, type EnvLike } from "./capture-policy.js";
|
|
8
8
|
import { createPayloadLimits } from "./limits.js";
|
|
9
|
+
import { createUsageOptions } from "./usage-options.js";
|
|
9
10
|
|
|
10
11
|
export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.env as EnvLike): Config | null {
|
|
11
12
|
if (existsSync(path)) {
|
|
@@ -24,6 +25,7 @@ export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.en
|
|
|
24
25
|
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
25
26
|
capturePolicy: createCapturePolicy(captureSource),
|
|
26
27
|
limits: createPayloadLimits(env),
|
|
28
|
+
usage: createUsageOptions(captureSource),
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
} catch (e) {
|
|
@@ -47,6 +49,7 @@ export function loadConfigFromEnv(env: EnvLike = process.env as EnvLike): Config
|
|
|
47
49
|
host: env.LANGFUSE_BASE_URL || env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
48
50
|
capturePolicy: createCapturePolicy(env),
|
|
49
51
|
limits: createPayloadLimits(env),
|
|
52
|
+
usage: createUsageOptions(env),
|
|
50
53
|
};
|
|
51
54
|
}
|
|
52
55
|
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CapturePolicy } from "./capture-policy.js";
|
|
2
2
|
import type { PayloadLimits } from "./limits.js";
|
|
3
|
+
import type { UsageOptions } from "./usage-options.js";
|
|
3
4
|
|
|
4
5
|
export interface Config {
|
|
5
6
|
publicKey: string;
|
|
@@ -7,6 +8,7 @@ export interface Config {
|
|
|
7
8
|
host: string;
|
|
8
9
|
capturePolicy?: CapturePolicy;
|
|
9
10
|
limits?: PayloadLimits;
|
|
11
|
+
usage?: UsageOptions;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export interface LangfuseObservation {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseFlag, type EnvLike } from "./capture-policy.js";
|
|
2
|
+
import { state } from "./state.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Switches that change how token usage is reported to Langfuse. Resolved once
|
|
6
|
+
* from the environment (and the persisted `capture` block, which is merged
|
|
7
|
+
* into it) when config loads; consumers read the resolved values through
|
|
8
|
+
* `getUsageOptions()`.
|
|
9
|
+
*/
|
|
10
|
+
export interface UsageOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Report reasoning tokens as `output_reasoning_tokens` and narrow `output`
|
|
13
|
+
* to the non-reasoning remainder. Off by default because Langfuse prices
|
|
14
|
+
* usage by exact key and a custom model definition priced on `output`
|
|
15
|
+
* alone would cost the reasoning share at zero once it moved to its own key.
|
|
16
|
+
*/
|
|
17
|
+
readonly splitReasoningTokens: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_USAGE_OPTIONS: UsageOptions = {
|
|
21
|
+
splitReasoningTokens: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve usage options from the environment. Namespaced `PI_LANGFUSE_*` like
|
|
26
|
+
* the payload limits: this is extension behaviour, not a Langfuse server knob.
|
|
27
|
+
*/
|
|
28
|
+
export function createUsageOptions(env: EnvLike = process.env as EnvLike): UsageOptions {
|
|
29
|
+
return {
|
|
30
|
+
splitReasoningTokens:
|
|
31
|
+
parseFlag(env.PI_LANGFUSE_SPLIT_REASONING_TOKENS) ?? DEFAULT_USAGE_OPTIONS.splitReasoningTokens,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolved usage options for the current session: the config-loaded values
|
|
37
|
+
* when a config is active, otherwise a fresh resolve from the environment.
|
|
38
|
+
*/
|
|
39
|
+
export function getUsageOptions(): UsageOptions {
|
|
40
|
+
return state.config?.usage ?? createUsageOptions();
|
|
41
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getLimits } from "./limits.js";
|
|
2
|
+
import { getUsageOptions, type UsageOptions } from "./usage-options.js";
|
|
2
3
|
import { createCapturePolicy, redactOptionsFor, type CapturePolicy } from "./capture-policy.js";
|
|
3
4
|
import { redactValue } from "./redaction.js";
|
|
4
5
|
import { state } from "./state.js";
|
|
@@ -422,7 +423,43 @@ function splitCacheWrite(cacheWrite: number, cacheWrite1h: number): Record<strin
|
|
|
422
423
|
};
|
|
423
424
|
}
|
|
424
425
|
|
|
425
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Reasoning tokens are a subset of the completion, not a sibling of it: every
|
|
428
|
+
* provider Pi reports them for counts them inside `output`. Emitting them as an
|
|
429
|
+
* extra bucket alone would therefore inflate Output usage, so `output` is
|
|
430
|
+
* narrowed to the non-reasoning remainder first — the same conversion Langfuse
|
|
431
|
+
* applies to Gemini thought tokens in `OtelIngestionProcessor`. Both keys
|
|
432
|
+
* contain `output`, so Langfuse re-aggregates them into an unchanged Output row.
|
|
433
|
+
*
|
|
434
|
+
* Reasoning is clamped to the reported `output` so an inconsistent provider
|
|
435
|
+
* count cannot drive `output` negative or make the buckets stop summing to
|
|
436
|
+
* `total`.
|
|
437
|
+
*
|
|
438
|
+
* The split is opt-in (`PI_LANGFUSE_SPLIT_REASONING_TOKENS`). Langfuse prices
|
|
439
|
+
* usage by exact key and user-defined model prices shadow the maintained
|
|
440
|
+
* defaults, so a custom model priced on `output` alone would silently cost the
|
|
441
|
+
* reasoning share at zero the moment it moved to its own key. Off by default,
|
|
442
|
+
* `output` stays whole and reasoning is not reported, exactly as before.
|
|
443
|
+
*/
|
|
444
|
+
const REASONING_KEY = "output_reasoning_tokens";
|
|
445
|
+
|
|
446
|
+
function splitReasoning(output: number, reasoning: number, enabled: boolean): Record<string, number> {
|
|
447
|
+
if (!enabled) {
|
|
448
|
+
return { output };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const bounded = Math.min(Math.max(reasoning, 0), output);
|
|
452
|
+
if (!bounded) {
|
|
453
|
+
return { output };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { output: output - bounded, [REASONING_KEY]: bounded };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function extractUsage(
|
|
460
|
+
messageOrEvent: Record<string, unknown>,
|
|
461
|
+
options: UsageOptions = getUsageOptions(),
|
|
462
|
+
): Record<string, number> | undefined {
|
|
426
463
|
const usage = (messageOrEvent.usage ??
|
|
427
464
|
(messageOrEvent.message && typeof messageOrEvent.message === "object"
|
|
428
465
|
? (messageOrEvent.message as Record<string, unknown>).usage
|
|
@@ -437,10 +474,13 @@ export function extractUsage(messageOrEvent: Record<string, unknown>): Record<st
|
|
|
437
474
|
const cacheRead = Number(usage.cacheRead ?? usage.cache_read ?? usage.cachedTokens ?? 0);
|
|
438
475
|
const cacheWrite = Number(usage.cacheWrite ?? usage.cache_write ?? 0);
|
|
439
476
|
const cacheWrite1h = Number(usage.cacheWrite1h ?? usage.cache_write_1h ?? 0);
|
|
477
|
+
const reasoning = Number(
|
|
478
|
+
usage.reasoning ?? usage.reasoningTokens ?? usage.reasoning_tokens ?? usage.thoughtsTokenCount ?? 0,
|
|
479
|
+
);
|
|
440
480
|
|
|
441
481
|
return {
|
|
442
482
|
input,
|
|
443
|
-
output,
|
|
483
|
+
...splitReasoning(output, reasoning, options.splitReasoningTokens),
|
|
444
484
|
total,
|
|
445
485
|
...(cacheRead ? { [CACHE_READ_KEY]: cacheRead } : {}),
|
|
446
486
|
...splitCacheWrite(cacheWrite, cacheWrite1h),
|