Browse Source

fix(langfuse): set trace-level input/output via updateTrace

propagateAttributes() does not support input/output fields, and the
root span's input/output do not auto-inherit to the trace level. This
caused Langfuse UI to show Input: undefined and Output: undefined.

Add explicit rootSpan.updateTrace() call to set trace-level input/output
per Langfuse SDK documentation (Solution B: set input/output directly on
the trace).
ding113 2 weeks ago
parent
commit
69fc61b975
2 changed files with 48 additions and 0 deletions
  1. 18 0
      src/lib/langfuse/trace-proxy-request.ts
  2. 30 0
      tests/unit/langfuse/langfuse-trace.test.ts

+ 18 - 0
src/lib/langfuse/trace-proxy-request.ts

@@ -237,6 +237,24 @@ export async function traceProxyRequest(ctx: TraceContext): Promise<void> {
       }
     );
 
+    // Explicitly set trace-level input/output (propagateAttributes does not support these)
+    rootSpan.updateTrace({
+      input: {
+        endpoint: session.getEndpoint(),
+        method: session.method,
+        model: session.getCurrentModel(),
+        clientFormat: session.originalFormat,
+        providerName: provider?.name,
+      },
+      output: {
+        statusCode,
+        durationMs,
+        model: session.getCurrentModel(),
+        hasUsage: !!ctx.usageMetrics,
+        costUsd: ctx.costUsd,
+      },
+    });
+
     rootSpan.end();
   } catch (error) {
     logger.warn("[Langfuse] Failed to trace proxy request", {

+ 30 - 0
tests/unit/langfuse/langfuse-trace.test.ts

@@ -15,8 +15,11 @@ const mockGeneration: any = {
   end: mockGenerationEnd,
 };
 
+const mockUpdateTrace = vi.fn();
+
 const mockRootSpan = {
   startObservation: vi.fn().mockReturnValue(mockGeneration),
+  updateTrace: mockUpdateTrace,
   end: mockSpanEnd,
 };
 
@@ -450,6 +453,33 @@ describe("traceProxyRequest", () => {
       })
     );
   });
+  test("should set trace-level input/output via updateTrace", async () => {
+    const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
+
+    await traceProxyRequest({
+      session: createMockSession(),
+      responseHeaders: new Headers(),
+      durationMs: 500,
+      statusCode: 200,
+      isStreaming: false,
+      costUsd: "0.05",
+    });
+
+    expect(mockUpdateTrace).toHaveBeenCalledWith({
+      input: expect.objectContaining({
+        endpoint: "/v1/messages",
+        method: "POST",
+        model: "claude-sonnet-4-20250514",
+        clientFormat: "claude",
+        providerName: "anthropic-main",
+      }),
+      output: expect.objectContaining({
+        statusCode: 200,
+        durationMs: 500,
+        costUsd: "0.05",
+      }),
+    });
+  });
 });
 
 describe("isLangfuseEnabled", () => {