pi-langfuse 1.5.14 → 1.5.15
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 +89 -4
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) {
|
|
@@ -587,7 +645,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal)
|
|
|
587
645
|
}
|
|
588
646
|
|
|
589
647
|
const trace = store.trace;
|
|
590
|
-
const
|
|
648
|
+
const entries: any[] = [
|
|
591
649
|
{
|
|
592
650
|
type: "trace-create",
|
|
593
651
|
id: randomUUID(),
|
|
@@ -627,7 +685,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal)
|
|
|
627
685
|
}
|
|
628
686
|
: {}),
|
|
629
687
|
};
|
|
630
|
-
|
|
688
|
+
entries.push({
|
|
631
689
|
type: observation.type === "GENERATION" ? "generation-create" : "span-create",
|
|
632
690
|
id: randomUUID(),
|
|
633
691
|
timestamp: eventTimestamp(observation),
|
|
@@ -635,12 +693,39 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal)
|
|
|
635
693
|
});
|
|
636
694
|
}
|
|
637
695
|
|
|
638
|
-
const
|
|
696
|
+
const maxTotalBytes = getMaxFallbackTotalBytes();
|
|
697
|
+
const totalBytes = serializedBytes({ batch: entries });
|
|
698
|
+
if (totalBytes > maxTotalBytes) {
|
|
699
|
+
const message = `REST fallback payload is ${(totalBytes / 1024 / 1024).toFixed(1)}MB, above the ${(maxTotalBytes / 1024 / 1024).toFixed(1)}MB ceiling; skipping fallback ingestion`;
|
|
700
|
+
rememberRuntimeError("REST fallback ingestion", new Error(message));
|
|
701
|
+
console.warn(`📊 Langfuse: ${message}`);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const chunks = splitIngestionBatch(entries, getMaxIngestionBatchBytes());
|
|
706
|
+
const errors: unknown[] = [];
|
|
707
|
+
for (const chunk of chunks) {
|
|
708
|
+
if (signal.aborted) {
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
errors.push(...(await ingestBatch(rt, chunk, signal)));
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if (signal.aborted && isAbortError(error)) {
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
rememberRuntimeError("REST fallback ingestion", error);
|
|
718
|
+
console.warn("📊 Langfuse: REST fallback ingestion chunk failed", error);
|
|
719
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
639
722
|
if (errors.length > 0) {
|
|
640
723
|
rememberRuntimeError("REST fallback ingestion", new Error(JSON.stringify(errors)));
|
|
641
724
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
642
725
|
} else {
|
|
643
|
-
debugLog(
|
|
726
|
+
debugLog(
|
|
727
|
+
`📊 Langfuse: OTel trace ${trace.id} was not visible; wrote fallback trace via REST ingestion (${chunks.length} chunk(s))`,
|
|
728
|
+
);
|
|
644
729
|
}
|
|
645
730
|
}
|
|
646
731
|
|