pi-langfuse 1.5.14 → 1.5.16
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 +13 -0
- package/README_CN.md +28 -0
- package/package.json +1 -1
- package/src/langfuse.ts +121 -5
package/README.md
CHANGED
|
@@ -136,6 +136,19 @@ defaults shown above. To capture a very large system prompt or big tool
|
|
|
136
136
|
payloads in full, raise or disable the relevant limit (e.g.
|
|
137
137
|
`PI_LANGFUSE_MAX_STRING_LENGTH=off`).
|
|
138
138
|
|
|
139
|
+
The REST fallback ingestion is chunked so each request body stays well below
|
|
140
|
+
the Langfuse gateway's payload limit (~4.5MB). These knobs control the chunk
|
|
141
|
+
budget and a hard ceiling for the whole fallback payload:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
export PI_LANGFUSE_MAX_INGESTION_BATCH_BYTES=4194304 # per-request body budget, default 4MB
|
|
145
|
+
|
|
146
|
+
export PI_LANGFUSE_MAX_FALLBACK_TOTAL_BYTES=33554432 # whole-payload ceiling, default 32MB
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
When the accumulated fallback payload exceeds the 32MB ceiling, ingestion is
|
|
150
|
+
skipped with a warning instead of attempting an unrecoverably large upload.
|
|
151
|
+
|
|
139
152
|
### Method 3: Persistent `config.json`
|
|
140
153
|
|
|
141
154
|
Create or update `~/.pi/agent/pi-langfuse/config.json`:
|
package/README_CN.md
CHANGED
|
@@ -115,6 +115,34 @@ export LANGFUSE_CAPTURE_SOURCE_METADATA=false
|
|
|
115
115
|
|
|
116
116
|
所有被采集的负载在上传前仍会脱敏。扩展会隐藏常见 API key、Bearer token、密码、Cookie、私钥、Langfuse key、GitHub/npm/AWS 风格 token,并对本地绝对路径做 hash。
|
|
117
117
|
|
|
118
|
+
### 负载上限
|
|
119
|
+
|
|
120
|
+
上传前会对负载做整形:字符串会被截断,过深或过宽的结构会被裁剪。这些上限让 trace 保持精简,同时保护
|
|
121
|
+
Langfuse 摄取管线。任何一项都可以覆盖(无需重新构建):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
export PI_LANGFUSE_MAX_STRING_LENGTH=12000 # 单字符串字符数(system prompt、输入)
|
|
125
|
+
export PI_LANGFUSE_MAX_TOOL_PAYLOAD_LENGTH=24000 # 工具输入/输出字符数
|
|
126
|
+
export PI_LANGFUSE_MAX_DEPTH=6 # 最大嵌套深度
|
|
127
|
+
export PI_LANGFUSE_MAX_ARRAY_ITEMS=50 # 数组保留的最大元素数
|
|
128
|
+
export PI_LANGFUSE_MAX_OBJECT_KEYS=80 # 对象保留的最大键数
|
|
129
|
+
export PI_LANGFUSE_MAX_PAYLOAD_NODES=2000 # 单个负载的最大总节点数
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
任意上限设置为 `0`、`off`、`none` 或 `unlimited` 即表示关闭该限制(采集完整值);未设置或非法值回退到上方默认值。
|
|
133
|
+
若想完整采集很大的 system prompt 或工具负载,可以调高或关闭相关限制(例如 `PI_LANGFUSE_MAX_STRING_LENGTH=off`)。
|
|
134
|
+
|
|
135
|
+
REST 回退摄取会按字节切分,保证每个请求体都远低于 Langfuse 网关的负载限制(约 4.5MB)。以下变量控制切分预算
|
|
136
|
+
与整个回退负载的硬上限:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
export PI_LANGFUSE_MAX_INGESTION_BATCH_BYTES=4194304 # 单次请求体预算,默认 4MB
|
|
140
|
+
|
|
141
|
+
export PI_LANGFUSE_MAX_FALLBACK_TOTAL_BYTES=33554432 # 整体负载上限,默认 32MB
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
当累积的回退负载超过 32MB 上限时,会跳过摄取并给出告警,而不是尝试一次注定失败的超大上传。
|
|
145
|
+
|
|
118
146
|
### 方式 3:持久化 `config.json`
|
|
119
147
|
|
|
120
148
|
创建或更新 `~/.pi/agent/pi-langfuse/config.json`:
|
package/package.json
CHANGED
package/src/langfuse.ts
CHANGED
|
@@ -69,6 +69,13 @@ const DEFAULT_SCORE_FLUSH_AT = 10;
|
|
|
69
69
|
const DEFAULT_SCORE_FLUSH_INTERVAL_MS = 1_000;
|
|
70
70
|
const MAX_SCORE_QUEUE_SIZE = 100_000;
|
|
71
71
|
const MAX_SCORE_BATCH_SIZE = 100;
|
|
72
|
+
// Langfuse gateways (incl. langfuse-dx.wair.ac.cn) reject ingestion bodies above
|
|
73
|
+
// ~4.5MB with HTTP 413. Keep the default chunk budget safely below that limit.
|
|
74
|
+
const DEFAULT_MAX_INGESTION_BATCH_BYTES = 4 * 1024 * 1024;
|
|
75
|
+
// Hard ceiling for the whole REST fallback payload. Above this the fallback is
|
|
76
|
+
// pointless (it would need dozens of requests during a bounded shutdown) and is
|
|
77
|
+
// skipped with a warning.
|
|
78
|
+
const DEFAULT_MAX_FALLBACK_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
72
79
|
|
|
73
80
|
let shutdownStepTimeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS;
|
|
74
81
|
|
|
@@ -91,6 +98,22 @@ function getScoreShutdownTimeoutMs(): number {
|
|
|
91
98
|
) * 1_000;
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
function getMaxIngestionBatchBytes(): number {
|
|
102
|
+
return resolvePositiveEnvNumber(
|
|
103
|
+
"PI_LANGFUSE_MAX_INGESTION_BATCH_BYTES",
|
|
104
|
+
DEFAULT_MAX_INGESTION_BATCH_BYTES,
|
|
105
|
+
true,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getMaxFallbackTotalBytes(): number {
|
|
110
|
+
return resolvePositiveEnvNumber(
|
|
111
|
+
"PI_LANGFUSE_MAX_FALLBACK_TOTAL_BYTES",
|
|
112
|
+
DEFAULT_MAX_FALLBACK_TOTAL_BYTES,
|
|
113
|
+
true,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
function delay(ms: number, signal?: AbortSignal) {
|
|
95
118
|
return new Promise<void>((resolve, reject) => {
|
|
96
119
|
if (signal?.aborted) {
|
|
@@ -244,6 +267,41 @@ async function ingestBatch(rt: LangfuseRuntime, batch: unknown[], signal: AbortS
|
|
|
244
267
|
return Array.isArray(responseBody.errors) ? responseBody.errors : [];
|
|
245
268
|
}
|
|
246
269
|
|
|
270
|
+
function serializedBytes(value: unknown): number {
|
|
271
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Split an ingestion event list into chunks whose serialized request bodies each
|
|
276
|
+
* stay under `maxBytes`. Each chunk, when wrapped as `{ batch: chunk }`, matches
|
|
277
|
+
* the measurement exactly because entries are serialized individually and joined
|
|
278
|
+
* with single commas inside the `{"batch":[...]}` envelope.
|
|
279
|
+
*/
|
|
280
|
+
export function splitIngestionBatch(entries: unknown[], maxBytes: number): unknown[][] {
|
|
281
|
+
const serialized = entries.map((entry) => JSON.stringify(entry));
|
|
282
|
+
const envelopeBytes = Buffer.byteLength('{"batch":[]}');
|
|
283
|
+
const chunks: unknown[][] = [];
|
|
284
|
+
let chunk: unknown[] = [];
|
|
285
|
+
let chunkBytes = envelopeBytes;
|
|
286
|
+
|
|
287
|
+
for (let index = 0; index < serialized.length; index++) {
|
|
288
|
+
const entryBytes = Buffer.byteLength(serialized[index], "utf8");
|
|
289
|
+
const separators = chunk.length > 0 ? 1 : 0;
|
|
290
|
+
const projectedBytes = chunkBytes + entryBytes + separators;
|
|
291
|
+
if (chunk.length > 0 && projectedBytes > maxBytes) {
|
|
292
|
+
chunks.push(chunk);
|
|
293
|
+
chunk = [];
|
|
294
|
+
chunkBytes = envelopeBytes;
|
|
295
|
+
}
|
|
296
|
+
chunk.push(entries[index]);
|
|
297
|
+
chunkBytes += entryBytes + (chunk.length > 1 ? 1 : 0);
|
|
298
|
+
}
|
|
299
|
+
if (chunk.length > 0) {
|
|
300
|
+
chunks.push(chunk);
|
|
301
|
+
}
|
|
302
|
+
return chunks;
|
|
303
|
+
}
|
|
304
|
+
|
|
247
305
|
async function flushPendingScores(rt: LangfuseRuntime, signal: AbortSignal): Promise<void> {
|
|
248
306
|
const pendingScores = rt.pendingScores;
|
|
249
307
|
if (!pendingScores || pendingScores.length === 0) {
|
|
@@ -400,6 +458,32 @@ function observationType(asType?: string): FallbackObservationType {
|
|
|
400
458
|
return asType === "generation" ? "GENERATION" : "SPAN";
|
|
401
459
|
}
|
|
402
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Langfuse stamps propagated attributes onto a span in
|
|
463
|
+
* `LangfuseSpanProcessor.onStart(span, parentContext)`, reading them from the
|
|
464
|
+
* OTel context that was active when the span was created. `propagateAttributes`
|
|
465
|
+
* only seeds that context for the duration of its callback, so a child created
|
|
466
|
+
* later — from a separate event handler, outside the callback — is written with
|
|
467
|
+
* an empty `session.id`.
|
|
468
|
+
*
|
|
469
|
+
* That is invisible in the legacy data model, where the session lives on the
|
|
470
|
+
* trace, but Langfuse v4 stores `session_id` per event row and aggregates a
|
|
471
|
+
* session with `WHERE session_id != ''`. Unstamped children drop out of
|
|
472
|
+
* `sumMap(cost_details)` and `sumMap(usage_details)`, which is why such
|
|
473
|
+
* sessions report their trace count correctly but no cost and no usage at all.
|
|
474
|
+
*
|
|
475
|
+
* Re-entering the propagated context for every child keeps the whole tree in
|
|
476
|
+
* the session. The id is read back off the parent span rather than from
|
|
477
|
+
* `state.currentSessionId`, so a child created outside an active session scope
|
|
478
|
+
* still inherits whatever its parent was actually stamped with.
|
|
479
|
+
*/
|
|
480
|
+
const OTEL_SESSION_ID_ATTRIBUTE = "session.id";
|
|
481
|
+
|
|
482
|
+
function readSessionId(observation: any): string | undefined {
|
|
483
|
+
const value = observation?.otelSpan?.attributes?.[OTEL_SESSION_ID_ATTRIBUTE];
|
|
484
|
+
return typeof value === "string" && value ? value : undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
403
487
|
function wrapObservation(
|
|
404
488
|
observation: any,
|
|
405
489
|
store: RestFallbackStore,
|
|
@@ -459,7 +543,12 @@ function wrapObservation(
|
|
|
459
543
|
return observation.end();
|
|
460
544
|
},
|
|
461
545
|
startObservation(childName: string, childBody?: Record<string, unknown>, options?: { asType?: string }) {
|
|
462
|
-
const
|
|
546
|
+
const inheritedSessionId = readSessionId(observation) ?? state.currentSessionId ?? undefined;
|
|
547
|
+
const propagate = runtime?.propagateAttributes;
|
|
548
|
+
const createChild = () => observation.startObservation(childName, childBody, options);
|
|
549
|
+
const child = inheritedSessionId && propagate
|
|
550
|
+
? propagate({ sessionId: inheritedSessionId.slice(0, 200) }, createChild)
|
|
551
|
+
: createChild();
|
|
463
552
|
return wrapObservation(child, store, childName, childBody, options?.asType, id);
|
|
464
553
|
},
|
|
465
554
|
setTraceIO(traceBody?: { input?: unknown; output?: unknown }) {
|
|
@@ -587,7 +676,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal)
|
|
|
587
676
|
}
|
|
588
677
|
|
|
589
678
|
const trace = store.trace;
|
|
590
|
-
const
|
|
679
|
+
const entries: any[] = [
|
|
591
680
|
{
|
|
592
681
|
type: "trace-create",
|
|
593
682
|
id: randomUUID(),
|
|
@@ -627,7 +716,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal)
|
|
|
627
716
|
}
|
|
628
717
|
: {}),
|
|
629
718
|
};
|
|
630
|
-
|
|
719
|
+
entries.push({
|
|
631
720
|
type: observation.type === "GENERATION" ? "generation-create" : "span-create",
|
|
632
721
|
id: randomUUID(),
|
|
633
722
|
timestamp: eventTimestamp(observation),
|
|
@@ -635,12 +724,39 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal)
|
|
|
635
724
|
});
|
|
636
725
|
}
|
|
637
726
|
|
|
638
|
-
const
|
|
727
|
+
const maxTotalBytes = getMaxFallbackTotalBytes();
|
|
728
|
+
const totalBytes = serializedBytes({ batch: entries });
|
|
729
|
+
if (totalBytes > maxTotalBytes) {
|
|
730
|
+
const message = `REST fallback payload is ${(totalBytes / 1024 / 1024).toFixed(1)}MB, above the ${(maxTotalBytes / 1024 / 1024).toFixed(1)}MB ceiling; skipping fallback ingestion`;
|
|
731
|
+
rememberRuntimeError("REST fallback ingestion", new Error(message));
|
|
732
|
+
console.warn(`📊 Langfuse: ${message}`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const chunks = splitIngestionBatch(entries, getMaxIngestionBatchBytes());
|
|
737
|
+
const errors: unknown[] = [];
|
|
738
|
+
for (const chunk of chunks) {
|
|
739
|
+
if (signal.aborted) {
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
errors.push(...(await ingestBatch(rt, chunk, signal)));
|
|
744
|
+
} catch (error) {
|
|
745
|
+
if (signal.aborted && isAbortError(error)) {
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
rememberRuntimeError("REST fallback ingestion", error);
|
|
749
|
+
console.warn("📊 Langfuse: REST fallback ingestion chunk failed", error);
|
|
750
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
639
753
|
if (errors.length > 0) {
|
|
640
754
|
rememberRuntimeError("REST fallback ingestion", new Error(JSON.stringify(errors)));
|
|
641
755
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
642
756
|
} else {
|
|
643
|
-
debugLog(
|
|
757
|
+
debugLog(
|
|
758
|
+
`📊 Langfuse: OTel trace ${trace.id} was not visible; wrote fallback trace via REST ingestion (${chunks.length} chunk(s))`,
|
|
759
|
+
);
|
|
644
760
|
}
|
|
645
761
|
}
|
|
646
762
|
|