| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996 |
- import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
- // Mock the langfuse modules at the top level
- const mockStartObservation = vi.fn();
- const mockPropagateAttributes = vi.fn();
- const mockSpanEnd = vi.fn();
- const mockGenerationEnd = vi.fn();
- const mockGenerationUpdate = vi.fn();
- const mockGuardSpanEnd = vi.fn();
- const mockEventEnd = vi.fn();
- const mockGeneration: any = {
- update: (...args: unknown[]) => {
- mockGenerationUpdate(...args);
- return mockGeneration;
- },
- end: mockGenerationEnd,
- };
- const mockGuardSpan: any = {
- end: mockGuardSpanEnd,
- };
- const mockEventObs: any = {
- end: mockEventEnd,
- };
- const mockUpdateTrace = vi.fn();
- const mockRootSpan = {
- startObservation: vi.fn(),
- updateTrace: mockUpdateTrace,
- end: mockSpanEnd,
- };
- // Default: route by observation name
- function setupDefaultStartObservation() {
- mockRootSpan.startObservation.mockImplementation((name: string) => {
- if (name === "guard-pipeline") return mockGuardSpan;
- if (name === "provider-attempt") return mockEventObs;
- return mockGeneration; // "llm-call"
- });
- }
- vi.mock("@langfuse/tracing", () => ({
- startObservation: (...args: unknown[]) => {
- mockStartObservation(...args);
- return mockRootSpan;
- },
- propagateAttributes: async (attrs: unknown, fn: () => Promise<void>) => {
- mockPropagateAttributes(attrs);
- await fn();
- },
- }));
- vi.mock("@/lib/logger", () => ({
- logger: {
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
- debug: vi.fn(),
- },
- }));
- let langfuseEnabled = true;
- vi.mock("@/lib/langfuse/index", () => ({
- isLangfuseEnabled: () => langfuseEnabled,
- }));
- function createMockSession(overrides: Record<string, unknown> = {}) {
- const startTime = (overrides.startTime as number) ?? Date.now() - 500;
- return {
- startTime,
- method: "POST",
- headers: new Headers({
- "content-type": "application/json",
- "x-api-key": "test-mock-key-not-real",
- "user-agent": "claude-code/1.0",
- }),
- request: {
- message: {
- model: "claude-sonnet-4-20250514",
- messages: [{ role: "user", content: "Hello" }],
- stream: true,
- max_tokens: 4096,
- tools: [{ name: "tool1" }],
- },
- model: "claude-sonnet-4-20250514",
- },
- originalFormat: "claude",
- userAgent: "claude-code/1.0",
- sessionId: "sess_abc12345_def67890",
- provider: {
- id: 1,
- name: "anthropic-main",
- providerType: "claude",
- },
- messageContext: {
- id: 42,
- user: { id: 7, name: "testuser" },
- key: { name: "default-key" },
- },
- ttfbMs: 200,
- forwardStartTime: startTime + 5,
- forwardedRequestBody: null,
- getEndpoint: () => "/v1/messages",
- getRequestSequence: () => 3,
- getMessagesLength: () => 1,
- getCurrentModel: () => "claude-sonnet-4-20250514",
- getOriginalModel: () => "claude-sonnet-4-20250514",
- isModelRedirected: () => false,
- getProviderChain: () => [
- {
- id: 1,
- name: "anthropic-main",
- providerType: "claude",
- reason: "initial_selection",
- timestamp: startTime + 2,
- },
- ],
- getSpecialSettings: () => null,
- getCacheTtlResolved: () => null,
- getContext1mApplied: () => false,
- ...overrides,
- } as any;
- }
- describe("traceProxyRequest", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- langfuseEnabled = true;
- setupDefaultStartObservation();
- });
- test("should not trace when Langfuse is disabled", async () => {
- langfuseEnabled = false;
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- expect(mockStartObservation).not.toHaveBeenCalled();
- });
- test("should trace when Langfuse is enabled with actual bodies", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const responseBody = { content: "Hi there" };
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers({ "content-type": "application/json" }),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- responseText: JSON.stringify(responseBody),
- });
- // Root span should have actual request body as input (not summary)
- const rootCall = mockStartObservation.mock.calls[0];
- expect(rootCall[0]).toBe("proxy-request");
- // Input should be the actual request message (since forwardedRequestBody is null)
- expect(rootCall[1].input).toEqual(
- expect.objectContaining({
- model: "claude-sonnet-4-20250514",
- messages: expect.any(Array),
- })
- );
- // Output should be actual response body
- expect(rootCall[1].output).toEqual(responseBody);
- // Should have level
- expect(rootCall[1].level).toBe("DEFAULT");
- // Should have metadata with former summaries
- expect(rootCall[1].metadata).toEqual(
- expect.objectContaining({
- endpoint: "/v1/messages",
- method: "POST",
- statusCode: 200,
- durationMs: 500,
- })
- );
- // Should have child observations
- const callNames = mockRootSpan.startObservation.mock.calls.map((c: unknown[]) => c[0]);
- expect(callNames).toContain("guard-pipeline");
- expect(callNames).toContain("llm-call");
- expect(mockSpanEnd).toHaveBeenCalledWith(expect.any(Date));
- expect(mockGenerationEnd).toHaveBeenCalledWith(expect.any(Date));
- });
- test("should use actual request messages as generation input", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const session = createMockSession();
- await traceProxyRequest({
- session,
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- responseText: '{"content": "response"}',
- });
- // Find the llm-call invocation
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall).toBeDefined();
- expect(llmCall[1].input).toEqual(session.request.message);
- });
- test("should use actual response body as generation output", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const responseBody = { content: [{ type: "text", text: "Hello!" }] };
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- responseText: JSON.stringify(responseBody),
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[1].output).toEqual(responseBody);
- });
- test("should pass raw headers without redaction", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers({ "x-api-key": "secret-mock" }),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- const metadata = llmCall[1].metadata;
- expect(metadata.requestHeaders["x-api-key"]).toBe("test-mock-key-not-real");
- expect(metadata.requestHeaders["content-type"]).toBe("application/json");
- expect(metadata.responseHeaders["x-api-key"]).toBe("secret-mock");
- });
- test("should include provider name and model in tags", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- expect(mockPropagateAttributes).toHaveBeenCalledWith(
- expect.objectContaining({
- userId: "testuser",
- sessionId: "sess_abc12345_def67890",
- tags: expect.arrayContaining([
- "claude",
- "anthropic-main",
- "claude-sonnet-4-20250514",
- "2xx",
- ]),
- })
- );
- });
- test("should include usage details when provided", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- usageMetrics: {
- input_tokens: 100,
- output_tokens: 50,
- cache_read_input_tokens: 20,
- },
- costUsd: "0.0015",
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[1].usageDetails).toEqual({
- input: 100,
- output: 50,
- cache_read_input_tokens: 20,
- });
- expect(llmCall[1].costDetails).toEqual({
- total: 0.0015,
- });
- });
- test("should include providerChain, specialSettings, and model in metadata", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const providerChain = [
- {
- id: 1,
- name: "anthropic-main",
- providerType: "claude",
- reason: "initial_selection",
- timestamp: Date.now(),
- },
- ];
- await traceProxyRequest({
- session: createMockSession({
- getSpecialSettings: () => ({ maxThinking: 8192 }),
- getProviderChain: () => providerChain,
- }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- const metadata = llmCall[1].metadata;
- expect(metadata.providerChain).toEqual(providerChain);
- expect(metadata.specialSettings).toEqual({ maxThinking: 8192 });
- expect(metadata.model).toBe("claude-sonnet-4-20250514");
- expect(metadata.originalModel).toBe("claude-sonnet-4-20250514");
- expect(metadata.providerName).toBe("anthropic-main");
- expect(metadata.requestSummary).toEqual(
- expect.objectContaining({
- model: "claude-sonnet-4-20250514",
- messageCount: 1,
- })
- );
- });
- test("should handle model redirect metadata", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession({
- isModelRedirected: () => true,
- getOriginalModel: () => "claude-sonnet-4-20250514",
- getCurrentModel: () => "glm-4",
- request: {
- message: { model: "glm-4", messages: [] },
- model: "glm-4",
- },
- }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[1].metadata.modelRedirected).toBe(true);
- expect(llmCall[1].metadata.originalModel).toBe("claude-sonnet-4-20250514");
- });
- test("should set completionStartTime from ttfbMs", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const startTime = Date.now() - 500;
- await traceProxyRequest({
- session: createMockSession({ startTime, ttfbMs: 200 }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- expect(mockGenerationUpdate).toHaveBeenCalledWith({
- completionStartTime: new Date(startTime + 200),
- });
- });
- test("should pass correct startTime and endTime to observations", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const startTime = 1700000000000;
- const durationMs = 5000;
- await traceProxyRequest({
- session: createMockSession({ startTime, forwardStartTime: startTime + 5 }),
- responseHeaders: new Headers(),
- durationMs,
- statusCode: 200,
- isStreaming: false,
- });
- const expectedStart = new Date(startTime);
- const expectedEnd = new Date(startTime + durationMs);
- const expectedForwardStart = new Date(startTime + 5);
- // Root span gets startTime in options (3rd arg)
- expect(mockStartObservation).toHaveBeenCalledWith("proxy-request", expect.any(Object), {
- startTime: expectedStart,
- });
- // Generation gets forwardStartTime in options (3rd arg)
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[2]).toEqual({
- asType: "generation",
- startTime: expectedForwardStart,
- });
- // Both end() calls receive the computed endTime
- expect(mockGenerationEnd).toHaveBeenCalledWith(expectedEnd);
- expect(mockSpanEnd).toHaveBeenCalledWith(expectedEnd);
- });
- test("should handle errors gracefully without throwing", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- // Make startObservation throw
- mockStartObservation.mockImplementationOnce(() => {
- throw new Error("SDK error");
- });
- await expect(
- traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- })
- ).resolves.toBeUndefined();
- });
- test("should include correct tags for error responses", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 502,
- isStreaming: false,
- errorMessage: "upstream error",
- });
- expect(mockPropagateAttributes).toHaveBeenCalledWith(
- expect.objectContaining({
- tags: expect.arrayContaining(["5xx"]),
- })
- );
- });
- test("should pass large input/output without truncation", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- // Generate a large response text
- const largeContent = "x".repeat(200_000);
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- responseText: largeContent,
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- const output = llmCall[1].output as string;
- // Should be the full content, no truncation
- expect(output).toBe(largeContent);
- expect(output).not.toContain("...[truncated]");
- });
- test("should show streaming output with sseEventCount when no responseText", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: true,
- sseEventCount: 42,
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[1].output).toEqual({
- streaming: true,
- sseEventCount: 42,
- });
- });
- test("should include costUsd in root span metadata", 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",
- });
- const rootCall = mockStartObservation.mock.calls[0];
- expect(rootCall[1].metadata).toEqual(
- expect.objectContaining({
- costUsd: "0.05",
- })
- );
- });
- test("should set trace-level input/output via updateTrace with actual bodies", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const responseBody = { result: "ok" };
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- responseText: JSON.stringify(responseBody),
- costUsd: "0.05",
- });
- expect(mockUpdateTrace).toHaveBeenCalledWith({
- input: expect.objectContaining({
- model: "claude-sonnet-4-20250514",
- messages: expect.any(Array),
- }),
- output: responseBody,
- });
- });
- // --- New tests for multi-span hierarchy ---
- test("should create guard-pipeline span with correct timing", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const startTime = 1700000000000;
- const forwardStartTime = startTime + 8; // 8ms guard pipeline
- await traceProxyRequest({
- session: createMockSession({ startTime, forwardStartTime }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const guardCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "guard-pipeline"
- );
- expect(guardCall).toBeDefined();
- expect(guardCall[1]).toEqual({
- output: { durationMs: 8, passed: true },
- });
- expect(guardCall[2]).toEqual({ startTime: new Date(startTime) });
- // Guard span should end at forwardStartTime
- expect(mockGuardSpanEnd).toHaveBeenCalledWith(new Date(forwardStartTime));
- });
- test("should skip guard-pipeline span when forwardStartTime is null", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession({ forwardStartTime: null }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const guardCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "guard-pipeline"
- );
- expect(guardCall).toBeUndefined();
- expect(mockGuardSpanEnd).not.toHaveBeenCalled();
- });
- test("should create provider-attempt events for failed chain items", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const startTime = 1700000000000;
- const failTimestamp = startTime + 100;
- await traceProxyRequest({
- session: createMockSession({
- startTime,
- getProviderChain: () => [
- {
- id: 1,
- name: "provider-a",
- providerType: "claude",
- reason: "retry_failed",
- errorMessage: "502 Bad Gateway",
- statusCode: 502,
- attemptNumber: 1,
- timestamp: failTimestamp,
- },
- {
- id: 2,
- name: "provider-b",
- providerType: "claude",
- reason: "system_error",
- errorMessage: "ECONNREFUSED",
- timestamp: failTimestamp + 50,
- },
- {
- id: 3,
- name: "provider-c",
- providerType: "claude",
- reason: "request_success",
- timestamp: failTimestamp + 200,
- },
- ],
- }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const eventCalls = mockRootSpan.startObservation.mock.calls.filter(
- (c: unknown[]) => c[0] === "provider-attempt"
- );
- // 2 failed items (retry_failed + system_error), success is skipped
- expect(eventCalls).toHaveLength(2);
- // First event: retry_failed -> WARNING level
- expect(eventCalls[0][1]).toEqual(
- expect.objectContaining({
- level: "WARNING",
- input: expect.objectContaining({
- providerId: 1,
- providerName: "provider-a",
- attempt: 1,
- }),
- output: expect.objectContaining({
- reason: "retry_failed",
- errorMessage: "502 Bad Gateway",
- statusCode: 502,
- }),
- })
- );
- expect(eventCalls[0][2]).toEqual({
- asType: "event",
- startTime: new Date(failTimestamp),
- });
- // Second event: system_error -> ERROR level
- expect(eventCalls[1][1].level).toBe("ERROR");
- expect(eventCalls[1][1].output.reason).toBe("system_error");
- });
- test("should set generation startTime to forwardStartTime", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const startTime = 1700000000000;
- const forwardStartTime = startTime + 10;
- await traceProxyRequest({
- session: createMockSession({ startTime, forwardStartTime }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[2]).toEqual({
- asType: "generation",
- startTime: new Date(forwardStartTime),
- });
- });
- test("should fall back to requestStartTime when forwardStartTime is null", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const startTime = 1700000000000;
- await traceProxyRequest({
- session: createMockSession({ startTime, forwardStartTime: null }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[2]).toEqual({
- asType: "generation",
- startTime: new Date(startTime),
- });
- });
- test("should include timingBreakdown in root span metadata and generation metadata", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const startTime = 1700000000000;
- const forwardStartTime = startTime + 5;
- await traceProxyRequest({
- session: createMockSession({
- startTime,
- forwardStartTime,
- ttfbMs: 105,
- getProviderChain: () => [
- { id: 1, name: "p1", reason: "retry_failed", timestamp: startTime + 50 },
- { id: 2, name: "p2", reason: "request_success", timestamp: startTime + 100 },
- ],
- }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const expectedTimingBreakdown = {
- guardPipelineMs: 5,
- upstreamTotalMs: 495,
- ttfbFromForwardMs: 100, // ttfbMs(105) - guardPipelineMs(5)
- tokenGenerationMs: 395, // durationMs(500) - ttfbMs(105)
- failedAttempts: 1, // only retry_failed is non-success
- providersAttempted: 2, // 2 unique provider ids
- };
- // Root span metadata should have timingBreakdown
- const rootCall = mockStartObservation.mock.calls[0];
- expect(rootCall[1].metadata.timingBreakdown).toEqual(expectedTimingBreakdown);
- // Generation metadata should also have timingBreakdown
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[1].metadata.timingBreakdown).toEqual(expectedTimingBreakdown);
- });
- test("should not create provider-attempt events when all providers succeeded", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession({
- getProviderChain: () => [
- { id: 1, name: "p1", reason: "initial_selection", timestamp: Date.now() },
- { id: 1, name: "p1", reason: "request_success", timestamp: Date.now() },
- ],
- }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const eventCalls = mockRootSpan.startObservation.mock.calls.filter(
- (c: unknown[]) => c[0] === "provider-attempt"
- );
- expect(eventCalls).toHaveLength(0);
- });
- // --- New tests for input/output, level, and cost breakdown ---
- test("should use forwardedRequestBody as trace input when available", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const forwardedBody = JSON.stringify({
- model: "claude-sonnet-4-20250514",
- messages: [{ role: "user", content: "Preprocessed Hello" }],
- stream: true,
- });
- await traceProxyRequest({
- session: createMockSession({
- forwardedRequestBody: forwardedBody,
- }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- responseText: '{"ok": true}',
- });
- // Root span input should be the forwarded body (parsed JSON)
- const rootCall = mockStartObservation.mock.calls[0];
- expect(rootCall[1].input).toEqual(JSON.parse(forwardedBody));
- // updateTrace should also use forwarded body
- expect(mockUpdateTrace).toHaveBeenCalledWith({
- input: JSON.parse(forwardedBody),
- output: { ok: true },
- });
- });
- test("should set root span level to DEFAULT for successful request", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const rootCall = mockStartObservation.mock.calls[0];
- expect(rootCall[1].level).toBe("DEFAULT");
- });
- test("should set root span level to WARNING when retries occurred", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const startTime = Date.now() - 500;
- await traceProxyRequest({
- session: createMockSession({
- startTime,
- getProviderChain: () => [
- { id: 1, name: "p1", reason: "retry_failed", timestamp: startTime + 50 },
- { id: 2, name: "p2", reason: "request_success", timestamp: startTime + 200 },
- ],
- }),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- });
- const rootCall = mockStartObservation.mock.calls[0];
- expect(rootCall[1].level).toBe("WARNING");
- });
- test("should set root span level to ERROR for non-200 status", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 502,
- isStreaming: false,
- });
- const rootCall = mockStartObservation.mock.calls[0];
- expect(rootCall[1].level).toBe("ERROR");
- });
- test("should set root span level to ERROR for 499 client abort", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 499,
- isStreaming: false,
- });
- const rootCall = mockStartObservation.mock.calls[0];
- expect(rootCall[1].level).toBe("ERROR");
- });
- test("should include cost breakdown in costDetails when provided", async () => {
- const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
- const costBreakdown = {
- input: 0.001,
- output: 0.002,
- cache_creation: 0.0005,
- cache_read: 0.0001,
- total: 0.0036,
- };
- await traceProxyRequest({
- session: createMockSession(),
- responseHeaders: new Headers(),
- durationMs: 500,
- statusCode: 200,
- isStreaming: false,
- costUsd: "0.0036",
- costBreakdown,
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[1].costDetails).toEqual(costBreakdown);
- });
- test("should fallback to total-only costDetails when no breakdown", 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",
- });
- const llmCall = mockRootSpan.startObservation.mock.calls.find(
- (c: unknown[]) => c[0] === "llm-call"
- );
- expect(llmCall[1].costDetails).toEqual({ total: 0.05 });
- });
- test("should include former summaries in root span metadata", 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",
- });
- const rootCall = mockStartObservation.mock.calls[0];
- const metadata = rootCall[1].metadata;
- // Former input summary fields
- expect(metadata.endpoint).toBe("/v1/messages");
- expect(metadata.method).toBe("POST");
- expect(metadata.model).toBe("claude-sonnet-4-20250514");
- expect(metadata.clientFormat).toBe("claude");
- expect(metadata.providerName).toBe("anthropic-main");
- // Former output summary fields
- expect(metadata.statusCode).toBe(200);
- expect(metadata.durationMs).toBe(500);
- expect(metadata.costUsd).toBe("0.05");
- expect(metadata.timingBreakdown).toBeDefined();
- });
- });
- describe("isLangfuseEnabled", () => {
- const originalPublicKey = process.env.LANGFUSE_PUBLIC_KEY;
- const originalSecretKey = process.env.LANGFUSE_SECRET_KEY;
- afterEach(() => {
- // Restore env
- if (originalPublicKey !== undefined) {
- process.env.LANGFUSE_PUBLIC_KEY = originalPublicKey;
- } else {
- delete process.env.LANGFUSE_PUBLIC_KEY;
- }
- if (originalSecretKey !== undefined) {
- process.env.LANGFUSE_SECRET_KEY = originalSecretKey;
- } else {
- delete process.env.LANGFUSE_SECRET_KEY;
- }
- });
- test("should return false when env vars are not set", () => {
- delete process.env.LANGFUSE_PUBLIC_KEY;
- delete process.env.LANGFUSE_SECRET_KEY;
- // Direct function test (not using the mock)
- const isEnabled = !!(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY);
- expect(isEnabled).toBe(false);
- });
- test("should return true when both keys are set", () => {
- process.env.LANGFUSE_PUBLIC_KEY = "pk-lf-test-mock";
- process.env.LANGFUSE_SECRET_KEY = "test-mock-not-real";
- const isEnabled = !!(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY);
- expect(isEnabled).toBe(true);
- });
- });
|