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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.5.17",
3
+ "version": "1.5.18",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -90,7 +90,12 @@ const FLAG_TO_FIELD = {
90
90
  LANGFUSE_CAPTURE_PATHS: "capturePaths",
91
91
  } as const;
92
92
 
93
- function parseFlag(value: string | undefined): boolean | undefined {
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
- export function extractUsage(messageOrEvent: Record<string, unknown>): Record<string, number> | undefined {
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),