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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.5.14",
3
+ "version": "1.5.15",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
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 batch: any[] = [
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
- batch.push({
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 errors = await ingestBatch(rt, batch, signal);
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(`📊 Langfuse: OTel trace ${trace.id} was not visible; wrote fallback trace via REST ingestion`);
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