pi-langfuse 1.5.0 → 1.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,6 +11,7 @@ import {
11
11
  extractUsage,
12
12
  extractCostDetails,
13
13
  getCapturePolicy,
14
+ extractModelParameters,
14
15
  } from "../utils.js";
15
16
  import type { GenerationState, ObservationUpdate } from "../types.js";
16
17
  import { applyCapturePolicy } from "../capture-policy.js";
@@ -39,6 +40,7 @@ export async function startGeneration(event: Record<string, unknown>) {
39
40
  try {
40
41
  const key = getRequestKey(event, `generation-${++state.agentState.generationSeq}`);
41
42
  const payload = getProviderPayload(event);
43
+ const modelParameters = extractModelParameters(payload);
42
44
  const model = String(event.model ?? event.modelId ?? state.currentModel ?? "");
43
45
  const provider = String(event.provider ?? state.currentProvider ?? "");
44
46
  const metadata = shapePayload({
@@ -63,6 +65,7 @@ export async function startGeneration(event: Record<string, unknown>) {
63
65
  body: {
64
66
  input: captured.input,
65
67
  model: model || undefined,
68
+ modelParameters,
66
69
  metadata: captured.metadata,
67
70
  },
68
71
  asType: "generation",
@@ -73,6 +76,7 @@ export async function startGeneration(event: Record<string, unknown>) {
73
76
  requestKey: key,
74
77
  ended: false,
75
78
  metadata: captured.metadata ?? {},
79
+ modelParameters,
76
80
  });
77
81
  state.agentState.generationOrder.push(key);
78
82
  } catch (e) {
@@ -173,10 +177,12 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
173
177
 
174
178
  const usageDetails = extractUsage({ ...event, message });
175
179
  const costDetails = extractCostDetails({ ...event, message });
180
+ const modelParameters = extractModelParameters(getProviderPayload(event)) ?? generation.modelParameters;
176
181
  const model = String(message.model ?? event.model ?? state.currentModel ?? "");
177
182
  const update: ObservationUpdate = {
178
183
  output,
179
184
  model: model || undefined,
185
+ modelParameters,
180
186
  usageDetails,
181
187
  costDetails,
182
188
  metadata: {
@@ -202,6 +208,7 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
202
208
  try {
203
209
  const usageDetails = extractUsage({ ...event, message });
204
210
  const costDetails = extractCostDetails({ ...event, message });
211
+ const modelParameters = extractModelParameters(getProviderPayload(event));
205
212
  const model = String(message.model ?? event.model ?? state.currentModel ?? "");
206
213
  const captured = applyCapturePolicy(
207
214
  {
@@ -223,6 +230,7 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
223
230
  input: captured.input,
224
231
  output: captured.output,
225
232
  model: model || undefined,
233
+ modelParameters,
226
234
  usageDetails,
227
235
  costDetails,
228
236
  metadata: captured.metadata,
package/src/langfuse.ts CHANGED
@@ -29,6 +29,7 @@ interface RestFallbackObservation {
29
29
  output?: unknown;
30
30
  metadata?: Record<string, unknown>;
31
31
  model?: string;
32
+ modelParameters?: Record<string, string | number>;
32
33
  usageDetails?: Record<string, number>;
33
34
  costDetails?: Record<string, number>;
34
35
  level?: "DEBUG" | "DEFAULT" | "WARNING" | "ERROR";
@@ -57,6 +58,12 @@ function delay(ms: number) {
57
58
  return new Promise((resolve) => setTimeout(resolve, ms));
58
59
  }
59
60
 
61
+ function debugLog(message: string) {
62
+ if (process.env.PI_LANGFUSE_DEBUG === "1" || process.env.PI_LANGFUSE_DEBUG === "true") {
63
+ console.log(message);
64
+ }
65
+ }
66
+
60
67
  async function withTimeout<T>(label: string, operation: Promise<T> | undefined): Promise<T | undefined> {
61
68
  if (!operation) {
62
69
  return undefined;
@@ -68,7 +75,7 @@ async function withTimeout<T>(label: string, operation: Promise<T> | undefined):
68
75
  operation,
69
76
  new Promise<undefined>((resolve) => {
70
77
  timeout = setTimeout(() => {
71
- console.log(`📊 Langfuse: ${label} timed out after ${shutdownStepTimeoutMs}ms`);
78
+ debugLog(`📊 Langfuse: ${label} timed out after ${shutdownStepTimeoutMs}ms`);
72
79
  resolve(undefined);
73
80
  }, shutdownStepTimeoutMs);
74
81
  }),
@@ -108,6 +115,9 @@ function applyObservationUpdate(record: RestFallbackObservation, body: Record<st
108
115
  record.metadata = mergeMetadata(record.metadata, body.metadata as Record<string, unknown>);
109
116
  }
110
117
  if (typeof body.model === "string") record.model = body.model;
118
+ if (body.modelParameters && typeof body.modelParameters === "object") {
119
+ record.modelParameters = body.modelParameters as Record<string, string | number>;
120
+ }
111
121
  if (body.usageDetails && typeof body.usageDetails === "object") {
112
122
  record.usageDetails = body.usageDetails as Record<string, number>;
113
123
  }
@@ -288,6 +298,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
288
298
  ? {
289
299
  completionStartTime: observation.completionStartTime,
290
300
  model: observation.model,
301
+ modelParameters: observation.modelParameters,
291
302
  usageDetails: observation.usageDetails,
292
303
  costDetails: observation.costDetails,
293
304
  }
@@ -303,7 +314,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
303
314
 
304
315
  const ingestionApi = rt.scoreClient.api?.ingestion;
305
316
  if (!ingestionApi?.batch) {
306
- console.log("📊 Langfuse: REST fallback ingestion is unavailable");
317
+ debugLog("📊 Langfuse: REST fallback ingestion is unavailable");
307
318
  return;
308
319
  }
309
320
 
@@ -328,7 +339,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
328
339
  if (errors.length > 0) {
329
340
  console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
330
341
  } else {
331
- console.log(`📊 Langfuse: OTel trace ${trace.id} was not visible; wrote fallback trace via REST ingestion`);
342
+ debugLog(`📊 Langfuse: OTel trace ${trace.id} was not visible; wrote fallback trace via REST ingestion`);
332
343
  }
333
344
  }
334
345
 
package/src/types.ts CHANGED
@@ -25,6 +25,7 @@ export interface ObservationUpdate {
25
25
  output?: unknown;
26
26
  metadata?: Record<string, unknown>;
27
27
  model?: string;
28
+ modelParameters?: Record<string, string | number>;
28
29
  usageDetails?: Record<string, number>;
29
30
  usage?: Record<string, number>;
30
31
  costDetails?: Record<string, number>;
@@ -83,6 +84,7 @@ export interface GenerationState {
83
84
  requestKey: string;
84
85
  ended: boolean;
85
86
  metadata: Record<string, unknown>;
87
+ modelParameters?: Record<string, string | number>;
86
88
  ttftRecorded?: boolean;
87
89
  }
88
90
 
package/src/utils.ts CHANGED
@@ -35,7 +35,7 @@ const PAYLOAD_TOO_LARGE = "[payload too large]";
35
35
 
36
36
  export function shapePayload(
37
37
  value: unknown,
38
- options: { maxString?: number; depth?: number; maxNodes?: number; redact?: boolean } = {},
38
+ options: { maxString?: number; depth?: number; maxNodes?: number; redact?: boolean; parseJson?: boolean } = {},
39
39
  ): unknown {
40
40
  const maxString = options.maxString ?? MAX_STRING_LENGTH;
41
41
  const depth = options.depth ?? MAX_DEPTH;
@@ -55,6 +55,9 @@ export function shapePayload(
55
55
 
56
56
  if (typeof item === "string") {
57
57
  const truncated = truncate(item, maxString);
58
+ if (options.parseJson === false) {
59
+ return truncated;
60
+ }
58
61
  const parsed = tryParseJson(truncated);
59
62
  if (parsed === truncated) {
60
63
  return truncated;
@@ -175,6 +178,63 @@ export function extractTextContent(content: unknown, maxLength?: number): string
175
178
  return maxLength ? truncate(text, maxLength) : text;
176
179
  }
177
180
 
181
+ export function normalizeContentForLangfuse(content: unknown, api?: string): unknown {
182
+ if (!Array.isArray(content)) {
183
+ return content;
184
+ }
185
+
186
+ const toolCallItems = content.filter((item) => {
187
+ return item && typeof item === "object" && (item as { type?: string }).type === "toolCall";
188
+ });
189
+ if (toolCallItems.length === 0) {
190
+ return content;
191
+ }
192
+
193
+ const text = content
194
+ .map((item) => {
195
+ if (!item || typeof item !== "object") return "";
196
+ const block = item as { type?: string; text?: string };
197
+ return block.type === "text" && typeof block.text === "string" ? block.text : "";
198
+ })
199
+ .filter(Boolean)
200
+ .join("");
201
+
202
+ if (api === "anthropic-messages") {
203
+ const blocks: unknown[] = [];
204
+ if (text) {
205
+ blocks.push({ type: "text", text });
206
+ }
207
+ for (const item of toolCallItems) {
208
+ const toolCall = item as { id?: unknown; name?: unknown; arguments?: unknown };
209
+ const toolInput = shapePayload(toolCall.arguments, { parseJson: false });
210
+ blocks.push({
211
+ type: "tool_use",
212
+ id: String(toolCall.id ?? ""),
213
+ name: String(toolCall.name ?? "tool"),
214
+ input: toolInput,
215
+ });
216
+ }
217
+ return blocks;
218
+ }
219
+
220
+ return {
221
+ role: "assistant",
222
+ content: text || null,
223
+ tool_calls: toolCallItems.map((item) => {
224
+ const toolCall = item as { id?: unknown; name?: unknown; arguments?: unknown };
225
+ const toolArguments = shapePayload(toolCall.arguments ?? {}, { parseJson: false });
226
+ return {
227
+ id: String(toolCall.id ?? ""),
228
+ type: "function",
229
+ function: {
230
+ name: String(toolCall.name ?? "tool"),
231
+ arguments: typeof toolArguments === "string" ? toolArguments : JSON.stringify(toolArguments),
232
+ },
233
+ };
234
+ }),
235
+ };
236
+ }
237
+
178
238
  export function extractToolCalls(message: Record<string, unknown>): unknown | undefined {
179
239
  return (
180
240
  message.toolCalls ??
@@ -182,7 +242,7 @@ export function extractToolCalls(message: Record<string, unknown>): unknown | un
182
242
  message.function_calls ??
183
243
  (message.content && Array.isArray(message.content)
184
244
  ? message.content.filter((block) => {
185
- return block && typeof block === "object" && ["tool_use", "tool_call"].includes(String((block as { type?: string }).type));
245
+ return block && typeof block === "object" && ["tool_use", "tool_call", "toolCall"].includes(String((block as { type?: string }).type));
186
246
  })
187
247
  : undefined)
188
248
  );
@@ -194,6 +254,11 @@ export function extractAssistantOutput(message: unknown): unknown | undefined {
194
254
  }
195
255
 
196
256
  const msg = message as Record<string, unknown>;
257
+ const normalizedContent = normalizeContentForLangfuse(msg.content, typeof msg.api === "string" ? msg.api : undefined);
258
+ if (normalizedContent !== msg.content) {
259
+ return shapePayload(normalizedContent, { parseJson: false });
260
+ }
261
+
197
262
  const text = extractTextContent(msg.content);
198
263
  if (text) {
199
264
  return text;
@@ -261,6 +326,33 @@ export function getProviderPayload(event: Record<string, unknown>): unknown {
261
326
  return event.request ?? event.payload ?? event.body ?? event.providerPayload ?? event.messages ?? event;
262
327
  }
263
328
 
329
+ export function extractModelParameters(payload: unknown): Record<string, string | number> | undefined {
330
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
331
+ return undefined;
332
+ }
333
+
334
+ const params: Record<string, string | number> = {};
335
+ const record = payload as Record<string, unknown>;
336
+ for (const key of [
337
+ "temperature",
338
+ "top_p",
339
+ "topP",
340
+ "max_tokens",
341
+ "maxTokens",
342
+ "max_completion_tokens",
343
+ "presence_penalty",
344
+ "frequency_penalty",
345
+ "reasoning_effort",
346
+ ]) {
347
+ const value = record[key];
348
+ if (typeof value === "string" || typeof value === "number") {
349
+ params[key] = value;
350
+ }
351
+ }
352
+
353
+ return Object.keys(params).length > 0 ? params : undefined;
354
+ }
355
+
264
356
  export function getMessageFromEvent(event: Record<string, unknown>): Record<string, unknown> | undefined {
265
357
  if (event.message && typeof event.message === "object") {
266
358
  return event.message as Record<string, unknown>;