pi-langfuse 1.5.16 → 1.5.17
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 +26 -0
- package/README_CN.md +24 -0
- package/package.json +1 -1
- package/src/capture-policy.ts +29 -10
- package/src/commands.ts +3 -1
- package/src/handlers/tool.ts +6 -3
- package/src/redaction.ts +21 -7
- package/src/utils.ts +71 -6
package/README.md
CHANGED
|
@@ -109,12 +109,38 @@ export LANGFUSE_CAPTURE_TOOL_IO=false
|
|
|
109
109
|
export LANGFUSE_CAPTURE_SYSTEM_PROMPT=false
|
|
110
110
|
export LANGFUSE_CAPTURE_CWD=false
|
|
111
111
|
export LANGFUSE_CAPTURE_SOURCE_METADATA=false
|
|
112
|
+
export LANGFUSE_CAPTURE_PATHS=false
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
Source metadata remains off in every preset unless `LANGFUSE_CAPTURE_SOURCE_METADATA=true` is set explicitly.
|
|
116
|
+
The same holds for absolute paths and `LANGFUSE_CAPTURE_PATHS`.
|
|
115
117
|
|
|
116
118
|
All captured payloads are redacted before upload. The extension masks common API keys, bearer tokens, passwords, cookies, private keys, Langfuse keys, GitHub/npm/AWS-style tokens, and local absolute paths.
|
|
117
119
|
|
|
120
|
+
### Absolute paths
|
|
121
|
+
|
|
122
|
+
By default, local absolute paths (`/Users/...`, `/home/...`, `/tmp/...`, `C:\Users\...`) are
|
|
123
|
+
replaced everywhere with a stable `[PATH_HASH:<12 hex chars>]` digest, so usernames and repository
|
|
124
|
+
names never reach Langfuse. This applies to inputs, outputs, tool I/O, tool error messages, and the
|
|
125
|
+
`cwd` metadata field. Opt in to see real paths in traces:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
export LANGFUSE_CAPTURE_PATHS=true
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Or persist it in `config.json`:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{ "capture": { "LANGFUSE_CAPTURE_PATHS": "true" } }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Like `LANGFUSE_CAPTURE_SOURCE_METADATA`, this stays off in every privacy preset until it is set
|
|
138
|
+
explicitly, and it only affects paths — secret masking (tokens, keys, cookies, passwords) is always
|
|
139
|
+
on regardless. Note that `LANGFUSE_CAPTURE_CWD=false` is a different control: it drops the `cwd`
|
|
140
|
+
metadata field entirely rather than changing how paths are rendered.
|
|
141
|
+
|
|
142
|
+
`/langfuse-status` reports the current setting under `Capture: absolute paths`.
|
|
143
|
+
|
|
118
144
|
### Payload limits
|
|
119
145
|
|
|
120
146
|
Before upload, payloads are shaped: strings are truncated and deeply nested or
|
package/README_CN.md
CHANGED
|
@@ -109,12 +109,36 @@ export LANGFUSE_CAPTURE_TOOL_IO=false
|
|
|
109
109
|
export LANGFUSE_CAPTURE_SYSTEM_PROMPT=false
|
|
110
110
|
export LANGFUSE_CAPTURE_CWD=false
|
|
111
111
|
export LANGFUSE_CAPTURE_SOURCE_METADATA=false
|
|
112
|
+
export LANGFUSE_CAPTURE_PATHS=false
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
所有隐私预设默认都关闭源码元数据;只有显式设置 `LANGFUSE_CAPTURE_SOURCE_METADATA=true` 才会启用。
|
|
116
|
+
绝对路径与 `LANGFUSE_CAPTURE_PATHS` 同理。
|
|
115
117
|
|
|
116
118
|
所有被采集的负载在上传前仍会脱敏。扩展会隐藏常见 API key、Bearer token、密码、Cookie、私钥、Langfuse key、GitHub/npm/AWS 风格 token,并对本地绝对路径做 hash。
|
|
117
119
|
|
|
120
|
+
### 绝对路径
|
|
121
|
+
|
|
122
|
+
默认情况下,本地绝对路径(`/Users/...`、`/home/...`、`/tmp/...`、`C:\Users\...`)会在所有位置被替换为
|
|
123
|
+
稳定的 `[PATH_HASH:<12 位十六进制>]` 摘要,因此用户名和仓库名不会进入 Langfuse。该规则作用于输入、输出、
|
|
124
|
+
工具 I/O、工具错误信息以及 `cwd` 元数据字段。若希望在 trace 中看到真实路径,需要显式开启:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
export LANGFUSE_CAPTURE_PATHS=true
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
也可以持久化到 `config.json`:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{ "capture": { "LANGFUSE_CAPTURE_PATHS": "true" } }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
与 `LANGFUSE_CAPTURE_SOURCE_METADATA` 一样,它在所有隐私预设中默认关闭,且只影响路径;密钥脱敏(token、key、
|
|
137
|
+
Cookie、密码)始终生效。注意 `LANGFUSE_CAPTURE_CWD=false` 是另一个控制项——它会直接丢弃 `cwd` 元数据字段,
|
|
138
|
+
而不是改变路径的呈现方式。
|
|
139
|
+
|
|
140
|
+
`/langfuse-status` 会在 `Capture: absolute paths` 下显示当前设置。
|
|
141
|
+
|
|
118
142
|
### 负载上限
|
|
119
143
|
|
|
120
144
|
上传前会对负载做整形:字符串会被截断,过深或过宽的结构会被裁剪。这些上限让 trace 保持精简,同时保护
|
package/package.json
CHANGED
package/src/capture-policy.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hashPath, redactValue } from "./redaction.js";
|
|
1
|
+
import { hashPath, redactValue, type RedactOptions } from "./redaction.js";
|
|
2
2
|
|
|
3
3
|
export interface CapturePolicy {
|
|
4
4
|
readonly captureInputs: boolean;
|
|
@@ -7,6 +7,17 @@ export interface CapturePolicy {
|
|
|
7
7
|
readonly captureSystemPrompt: boolean;
|
|
8
8
|
readonly captureCwd: boolean;
|
|
9
9
|
readonly captureSourceMetadata: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Capture local absolute paths verbatim. Off in every preset, like
|
|
12
|
+
* `captureSourceMetadata`: paths are replaced with `[PATH_HASH:...]` unless
|
|
13
|
+
* `LANGFUSE_CAPTURE_PATHS` opts in explicitly.
|
|
14
|
+
*/
|
|
15
|
+
readonly capturePaths: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Redaction options implied by a policy, so path rendering follows the same switch everywhere. */
|
|
19
|
+
export function redactOptionsFor(policy: CapturePolicy): Partial<RedactOptions> {
|
|
20
|
+
return { redactPaths: !policy.capturePaths };
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
export type PrivacyPreset = "metadata-only" | "prompts-only" | "conversations" | "full-debug";
|
|
@@ -38,6 +49,7 @@ const PRESETS: Record<PrivacyPreset, CapturePolicy> = {
|
|
|
38
49
|
captureSystemPrompt: false,
|
|
39
50
|
captureCwd: false,
|
|
40
51
|
captureSourceMetadata: false,
|
|
52
|
+
capturePaths: false,
|
|
41
53
|
},
|
|
42
54
|
"prompts-only": {
|
|
43
55
|
captureInputs: true,
|
|
@@ -46,6 +58,7 @@ const PRESETS: Record<PrivacyPreset, CapturePolicy> = {
|
|
|
46
58
|
captureSystemPrompt: false,
|
|
47
59
|
captureCwd: false,
|
|
48
60
|
captureSourceMetadata: false,
|
|
61
|
+
capturePaths: false,
|
|
49
62
|
},
|
|
50
63
|
conversations: {
|
|
51
64
|
captureInputs: true,
|
|
@@ -54,6 +67,7 @@ const PRESETS: Record<PrivacyPreset, CapturePolicy> = {
|
|
|
54
67
|
captureSystemPrompt: false,
|
|
55
68
|
captureCwd: false,
|
|
56
69
|
captureSourceMetadata: false,
|
|
70
|
+
capturePaths: false,
|
|
57
71
|
},
|
|
58
72
|
"full-debug": {
|
|
59
73
|
captureInputs: true,
|
|
@@ -62,6 +76,7 @@ const PRESETS: Record<PrivacyPreset, CapturePolicy> = {
|
|
|
62
76
|
captureSystemPrompt: true,
|
|
63
77
|
captureCwd: true,
|
|
64
78
|
captureSourceMetadata: false,
|
|
79
|
+
capturePaths: false,
|
|
65
80
|
},
|
|
66
81
|
};
|
|
67
82
|
|
|
@@ -72,6 +87,7 @@ const FLAG_TO_FIELD = {
|
|
|
72
87
|
LANGFUSE_CAPTURE_SYSTEM_PROMPT: "captureSystemPrompt",
|
|
73
88
|
LANGFUSE_CAPTURE_CWD: "captureCwd",
|
|
74
89
|
LANGFUSE_CAPTURE_SOURCE_METADATA: "captureSourceMetadata",
|
|
90
|
+
LANGFUSE_CAPTURE_PATHS: "capturePaths",
|
|
75
91
|
} as const;
|
|
76
92
|
|
|
77
93
|
function parseFlag(value: string | undefined): boolean | undefined {
|
|
@@ -109,18 +125,20 @@ function redactMetadata(metadata: Record<string, unknown> | undefined, policy: C
|
|
|
109
125
|
return undefined;
|
|
110
126
|
}
|
|
111
127
|
|
|
128
|
+
const redactOptions = redactOptionsFor(policy);
|
|
112
129
|
const output: Record<string, unknown> = {};
|
|
113
130
|
for (const [key, value] of Object.entries(metadata)) {
|
|
114
131
|
if (key === "cwd") {
|
|
115
132
|
if (!policy.captureCwd) {
|
|
116
133
|
continue;
|
|
117
134
|
}
|
|
118
|
-
output[key] =
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
output[key] =
|
|
136
|
+
!policy.capturePaths && typeof value === "string" && /^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(value)
|
|
137
|
+
? hashPath(value)
|
|
138
|
+
: redactValue(value, redactOptions);
|
|
121
139
|
continue;
|
|
122
140
|
}
|
|
123
|
-
output[key] = redactValue(value);
|
|
141
|
+
output[key] = redactValue(value, redactOptions);
|
|
124
142
|
}
|
|
125
143
|
return Object.keys(output).length > 0 ? output : undefined;
|
|
126
144
|
}
|
|
@@ -129,24 +147,25 @@ export function applyCapturePolicy(
|
|
|
129
147
|
payload: RawTelemetryPayload,
|
|
130
148
|
policy: CapturePolicy = createCapturePolicy(),
|
|
131
149
|
): CapturedTelemetryPayload {
|
|
150
|
+
const redactOptions = redactOptionsFor(policy);
|
|
132
151
|
const captured: CapturedTelemetryPayload = {
|
|
133
152
|
metadata: redactMetadata(payload.metadata, policy),
|
|
134
153
|
};
|
|
135
154
|
|
|
136
155
|
if (policy.captureInputs && "input" in payload) {
|
|
137
|
-
captured.input = redactValue(payload.input);
|
|
156
|
+
captured.input = redactValue(payload.input, redactOptions);
|
|
138
157
|
}
|
|
139
158
|
if (policy.captureOutputs && "output" in payload) {
|
|
140
|
-
captured.output = redactValue(payload.output);
|
|
159
|
+
captured.output = redactValue(payload.output, redactOptions);
|
|
141
160
|
}
|
|
142
161
|
if (policy.captureToolIo && "toolInput" in payload) {
|
|
143
|
-
captured.toolInput = redactValue(payload.toolInput);
|
|
162
|
+
captured.toolInput = redactValue(payload.toolInput, redactOptions);
|
|
144
163
|
}
|
|
145
164
|
if (policy.captureToolIo && "toolOutput" in payload) {
|
|
146
|
-
captured.toolOutput = redactValue(payload.toolOutput);
|
|
165
|
+
captured.toolOutput = redactValue(payload.toolOutput, redactOptions);
|
|
147
166
|
}
|
|
148
167
|
if (policy.captureSystemPrompt && "systemPrompt" in payload) {
|
|
149
|
-
captured.systemPrompt = redactValue(payload.systemPrompt);
|
|
168
|
+
captured.systemPrompt = redactValue(payload.systemPrompt, redactOptions);
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
return captured;
|
package/src/commands.ts
CHANGED
|
@@ -75,7 +75,7 @@ function isPrivacyPreset(value: string | undefined): value is PrivacyPreset {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function inferPreset(policy: CapturePolicy): PrivacyPreset | "custom" {
|
|
78
|
-
const entries: Array<[PrivacyPreset, Omit<CapturePolicy, "captureSourceMetadata">]> = [
|
|
78
|
+
const entries: Array<[PrivacyPreset, Omit<CapturePolicy, "captureSourceMetadata" | "capturePaths">]> = [
|
|
79
79
|
[
|
|
80
80
|
"metadata-only",
|
|
81
81
|
{
|
|
@@ -140,6 +140,7 @@ function describePolicy(policy: CapturePolicy) {
|
|
|
140
140
|
`captureSystemPrompt: ${policy.captureSystemPrompt}`,
|
|
141
141
|
`captureCwd: ${policy.captureCwd}`,
|
|
142
142
|
`captureSourceMetadata: ${policy.captureSourceMetadata}`,
|
|
143
|
+
`capturePaths: ${policy.capturePaths}`,
|
|
143
144
|
].join("\n");
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -218,6 +219,7 @@ function formatStatus(configPath: string, env: Record<string, string | undefined
|
|
|
218
219
|
` tool IO: ${flag(policy.captureToolIo)}`,
|
|
219
220
|
` system prompt: ${flag(policy.captureSystemPrompt)}`,
|
|
220
221
|
` cwd: ${flag(policy.captureCwd)}`,
|
|
222
|
+
` absolute paths: ${flag(policy.capturePaths)}`,
|
|
221
223
|
`Active run: ${hasActiveAgentObservation() ? "yes" : "no"}`,
|
|
222
224
|
`Last error: ${lastErrorSummary()}`,
|
|
223
225
|
].join("\n");
|
package/src/handlers/tool.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getCapturePolicy,
|
|
13
13
|
getLimits,
|
|
14
14
|
} from "../utils.js";
|
|
15
|
-
import { applyCapturePolicy } from "../capture-policy.js";
|
|
15
|
+
import { applyCapturePolicy, redactOptionsFor } from "../capture-policy.js";
|
|
16
16
|
import { redactString } from "../redaction.js";
|
|
17
17
|
|
|
18
18
|
export async function startToolObservation(event: Record<string, unknown>) {
|
|
@@ -87,6 +87,7 @@ export async function finishToolObservation(event: Record<string, unknown>) {
|
|
|
87
87
|
event;
|
|
88
88
|
|
|
89
89
|
try {
|
|
90
|
+
const policy = getCapturePolicy();
|
|
90
91
|
const shapedOutput = shapePayload(output, { maxString: getLimits().maxToolPayload });
|
|
91
92
|
const captured = applyCapturePolicy(
|
|
92
93
|
{
|
|
@@ -97,7 +98,7 @@ export async function finishToolObservation(event: Record<string, unknown>) {
|
|
|
97
98
|
isError,
|
|
98
99
|
},
|
|
99
100
|
},
|
|
100
|
-
|
|
101
|
+
policy,
|
|
101
102
|
);
|
|
102
103
|
const outputBytes = estimatePayloadBytes(captured.toolOutput, getLimits().maxToolPayload);
|
|
103
104
|
const durationMs = Math.max(0, Date.now() - activeTool.startedAt);
|
|
@@ -106,7 +107,9 @@ export async function finishToolObservation(event: Record<string, unknown>) {
|
|
|
106
107
|
.update({
|
|
107
108
|
output: captured.toolOutput,
|
|
108
109
|
level: isError ? "ERROR" : "DEFAULT",
|
|
109
|
-
statusMessage: isError
|
|
110
|
+
statusMessage: isError
|
|
111
|
+
? redactString(truncate(String(event.error ?? output), 1_000), redactOptionsFor(policy))
|
|
112
|
+
: undefined,
|
|
110
113
|
metadata: {
|
|
111
114
|
...(captured.metadata ?? {}),
|
|
112
115
|
durationMs,
|
package/src/redaction.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { getLimits } from "./limits.js";
|
|
3
|
+
import { state } from "./state.js";
|
|
3
4
|
|
|
4
5
|
export const REDACTED = "[REDACTED_SECRET]";
|
|
5
6
|
|
|
@@ -8,8 +9,16 @@ export interface RedactOptions {
|
|
|
8
9
|
maxArrayItems: number;
|
|
9
10
|
maxObjectKeys: number;
|
|
10
11
|
maxStringLength: number;
|
|
12
|
+
/** When false, absolute filesystem paths are emitted verbatim instead of `[PATH_HASH:...]`. */
|
|
13
|
+
redactPaths: boolean;
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Fallback for callers that do not pass the resolved capture policy. Mirrors
|
|
18
|
+
* `getLimits()` by reading the session config, and defaults to redacting so a
|
|
19
|
+
* missing policy can never widen disclosure. Callers that hold a policy should
|
|
20
|
+
* pass `redactOptionsFor(policy)` instead of relying on this.
|
|
21
|
+
*/
|
|
13
22
|
function defaultOptions(): RedactOptions {
|
|
14
23
|
const limits = getLimits();
|
|
15
24
|
return {
|
|
@@ -17,6 +26,7 @@ function defaultOptions(): RedactOptions {
|
|
|
17
26
|
maxArrayItems: limits.maxArrayItems,
|
|
18
27
|
maxObjectKeys: limits.maxObjectKeys,
|
|
19
28
|
maxStringLength: limits.maxString,
|
|
29
|
+
redactPaths: !(state.config?.capturePolicy?.capturePaths ?? false),
|
|
20
30
|
};
|
|
21
31
|
}
|
|
22
32
|
|
|
@@ -41,16 +51,20 @@ function truncate(value: string, maxStringLength: number): string {
|
|
|
41
51
|
|
|
42
52
|
export function redactString(value: string, options: Partial<RedactOptions> = {}): string {
|
|
43
53
|
const merged = { ...defaultOptions(), ...options };
|
|
44
|
-
const
|
|
45
|
-
return truncated
|
|
54
|
+
const secretsRedacted = truncate(value, merged.maxStringLength)
|
|
46
55
|
.replace(PRIVATE_KEY_RE, REDACTED)
|
|
47
56
|
.replace(BEARER_RE, REDACTED)
|
|
48
57
|
.replace(KNOWN_TOKEN_RE, REDACTED)
|
|
49
|
-
.replace(SECRET_ASSIGNMENT_RE, (_match, key: string) => `${key}=${REDACTED}`)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
.replace(SECRET_ASSIGNMENT_RE, (_match, key: string) => `${key}=${REDACTED}`);
|
|
59
|
+
|
|
60
|
+
if (!merged.redactPaths) {
|
|
61
|
+
return secretsRedacted;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return secretsRedacted.replace(ABSOLUTE_PATH_RE, (path: string) => {
|
|
65
|
+
const envSuffix = path.match(/([/\\]\.env(?:\.[A-Za-z0-9_-]+)?)$/)?.[1];
|
|
66
|
+
return `${hashPath(envSuffix ? path.slice(0, -envSuffix.length) : path)}${envSuffix ?? ""}`;
|
|
67
|
+
});
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
function visit(value: unknown, options: RedactOptions, depth: number, seen: WeakSet<object>): unknown {
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLimits } from "./limits.js";
|
|
2
|
-
import { createCapturePolicy, type CapturePolicy } from "./capture-policy.js";
|
|
2
|
+
import { createCapturePolicy, redactOptionsFor, type CapturePolicy } from "./capture-policy.js";
|
|
3
3
|
import { redactValue } from "./redaction.js";
|
|
4
4
|
import { state } from "./state.js";
|
|
5
5
|
|
|
@@ -140,6 +140,7 @@ export function shapePayload(
|
|
|
140
140
|
return options.redact === false
|
|
141
141
|
? shaped
|
|
142
142
|
: redactValue(shaped, {
|
|
143
|
+
...redactOptionsFor(getCapturePolicy()),
|
|
143
144
|
maxDepth: depth,
|
|
144
145
|
maxStringLength: maxString,
|
|
145
146
|
maxArrayItems,
|
|
@@ -369,6 +370,58 @@ export function getMessageFromEvent(event: Record<string, unknown>): Record<stri
|
|
|
369
370
|
return undefined;
|
|
370
371
|
}
|
|
371
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Langfuse buckets usage and cost details by substring match on the key name:
|
|
375
|
+
* `input` rolls up into the Input row of the breakdown, `output` into Output,
|
|
376
|
+
* and anything matching neither falls into the catch-all Other row. Camel-cased
|
|
377
|
+
* `cacheWrite` lands in Other, so cache is reported outside the Input bucket it
|
|
378
|
+
* belongs to and never reaches Langfuse's own price table, which resolves
|
|
379
|
+
* usage types by exact key match (`price.usageType === key`) and therefore
|
|
380
|
+
* cannot price cache for providers that do not report cost themselves.
|
|
381
|
+
*
|
|
382
|
+
* These are the key names Langfuse documents for Anthropic-style caching.
|
|
383
|
+
* Pi reports `input` exclusive of cached tokens, so the buckets stay
|
|
384
|
+
* non-overlapping and nothing is double counted. Emitting both spellings would
|
|
385
|
+
* be worse than either: the canonical key would be counted in Input and the
|
|
386
|
+
* camel-cased duplicate again in Other.
|
|
387
|
+
*/
|
|
388
|
+
const CACHE_READ_KEY = "cache_read_input_tokens";
|
|
389
|
+
const CACHE_WRITE_KEY = "cache_creation_input_tokens";
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Anthropic prices one-hour cache writes at a different rate from the default
|
|
393
|
+
* five-minute ones, and Langfuse mirrors that with two TTL-specific usage keys.
|
|
394
|
+
* Pi reports the total under `cacheWrite` and the hour-TTL share of it under
|
|
395
|
+
* `cacheWrite1h`, so the two Langfuse buckets are `cacheWrite - cacheWrite1h`
|
|
396
|
+
* and `cacheWrite1h`. Both keys contain `input`, so the Input row of the
|
|
397
|
+
* breakdown is unchanged; only the price lookup differs.
|
|
398
|
+
*/
|
|
399
|
+
const CACHE_WRITE_5M_KEY = "input_cache_creation_5m";
|
|
400
|
+
const CACHE_WRITE_1H_KEY = "input_cache_creation_1h";
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Pi emits `cacheWrite1h: 0` on every Anthropic-family response, so a zero
|
|
404
|
+
* carries no TTL information. Only a non-zero hour-TTL figure is a real
|
|
405
|
+
* breakdown; everything else stays on the TTL-agnostic key so dashboards and
|
|
406
|
+
* custom model prices keyed on it keep working.
|
|
407
|
+
*/
|
|
408
|
+
function splitCacheWrite(cacheWrite: number, cacheWrite1h: number): Record<string, number> {
|
|
409
|
+
if (!cacheWrite) {
|
|
410
|
+
return {};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const longWrite = Math.min(Math.max(cacheWrite1h, 0), cacheWrite);
|
|
414
|
+
if (!longWrite) {
|
|
415
|
+
return { [CACHE_WRITE_KEY]: cacheWrite };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const shortWrite = cacheWrite - longWrite;
|
|
419
|
+
return {
|
|
420
|
+
...(shortWrite ? { [CACHE_WRITE_5M_KEY]: shortWrite } : {}),
|
|
421
|
+
[CACHE_WRITE_1H_KEY]: longWrite,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
372
425
|
export function extractUsage(messageOrEvent: Record<string, unknown>): Record<string, number> | undefined {
|
|
373
426
|
const usage = (messageOrEvent.usage ??
|
|
374
427
|
(messageOrEvent.message && typeof messageOrEvent.message === "object"
|
|
@@ -383,13 +436,14 @@ export function extractUsage(messageOrEvent: Record<string, unknown>): Record<st
|
|
|
383
436
|
const total = Number(usage.total ?? usage.totalTokens ?? usage.total_tokens ?? input + output);
|
|
384
437
|
const cacheRead = Number(usage.cacheRead ?? usage.cache_read ?? usage.cachedTokens ?? 0);
|
|
385
438
|
const cacheWrite = Number(usage.cacheWrite ?? usage.cache_write ?? 0);
|
|
439
|
+
const cacheWrite1h = Number(usage.cacheWrite1h ?? usage.cache_write_1h ?? 0);
|
|
386
440
|
|
|
387
441
|
return {
|
|
388
442
|
input,
|
|
389
443
|
output,
|
|
390
444
|
total,
|
|
391
|
-
...(cacheRead ? { cacheRead } : {}),
|
|
392
|
-
...(cacheWrite
|
|
445
|
+
...(cacheRead ? { [CACHE_READ_KEY]: cacheRead } : {}),
|
|
446
|
+
...splitCacheWrite(cacheWrite, cacheWrite1h),
|
|
393
447
|
};
|
|
394
448
|
}
|
|
395
449
|
|
|
@@ -405,12 +459,23 @@ export function extractCostDetails(messageOrEvent: Record<string, unknown>): Rec
|
|
|
405
459
|
|
|
406
460
|
const input = Number(cost.input ?? cost.inputCost ?? 0);
|
|
407
461
|
const output = Number(cost.output ?? cost.outputCost ?? 0);
|
|
408
|
-
const
|
|
409
|
-
|
|
462
|
+
const cacheRead = Number(cost.cacheRead ?? cost.cache_read ?? 0);
|
|
463
|
+
// Pi has no per-TTL cost figure: `cost.cacheWrite` already prices the
|
|
464
|
+
// five-minute and one-hour shares at their own rates and sums them, so the
|
|
465
|
+
// cost side stays on the aggregate key even when usage is split by TTL.
|
|
466
|
+
const cacheWrite = Number(cost.cacheWrite ?? cost.cache_write ?? 0);
|
|
467
|
+
const total = Number(cost.total ?? cost.totalCost ?? input + output + cacheRead + cacheWrite);
|
|
468
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0 && total === 0) {
|
|
410
469
|
return undefined;
|
|
411
470
|
}
|
|
412
471
|
|
|
413
|
-
return {
|
|
472
|
+
return {
|
|
473
|
+
input,
|
|
474
|
+
output,
|
|
475
|
+
total,
|
|
476
|
+
...(cacheRead ? { [CACHE_READ_KEY]: cacheRead } : {}),
|
|
477
|
+
...(cacheWrite ? { [CACHE_WRITE_KEY]: cacheWrite } : {}),
|
|
478
|
+
};
|
|
414
479
|
}
|
|
415
480
|
|
|
416
481
|
export function extractResponseMetadata(event: Record<string, unknown>): Record<string, unknown> {
|