UNPKG

langsmith

Version:

Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.

217 lines (216 loc) 8.95 kB
import { getCurrentRunTree, traceable } from "../../traceable.js"; import { extractInputTokenDetails, extractOutputTokenDetails, } from "../../utils/vercel.js"; export const populateToolCallsForTracing = (message) => { const formattedMessage = { ...message, }; if (formattedMessage.role !== "assistant") { return formattedMessage; } if (Array.isArray(formattedMessage.content)) { const toolCalls = formattedMessage.content .filter((block) => { return (block != null && typeof block === "object" && block.type == "tool-call"); }) .map((block) => { return { id: block.toolCallId, type: "function", function: { name: block.toolName, arguments: typeof block.input !== "string" ? JSON.stringify(block.input) : block.input, }, }; }); if (toolCalls.length > 0) { formattedMessage.tool_calls = toolCalls; } } return formattedMessage; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const _formatTracedInputs = (params) => { const { prompt, ...rest } = params; if (prompt == null) { return params; } if (Array.isArray(prompt)) { return { ...rest, messages: prompt.map(populateToolCallsForTracing) }; } return rest; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const _formatTracedOutputs = (outputs) => { const { request, response, ...rest } = outputs; const formattedOutputs = { ...rest }; if (formattedOutputs.role == null) { formattedOutputs.role = formattedOutputs.type ?? "assistant"; } return populateToolCallsForTracing(formattedOutputs); }; const setUsageMetadataOnRunTree = (result, runTree) => { if (result.usage == null || typeof result.usage !== "object") { return; } const langsmithUsage = { input_tokens: result.usage?.inputTokens, output_tokens: result.usage?.outputTokens, total_tokens: result.usage?.totalTokens, }; const inputTokenDetails = extractInputTokenDetails(result.providerMetadata ?? {}, result.usage?.cachedInputTokens); const outputTokenDetails = extractOutputTokenDetails(result.usage?.reasoningTokens); runTree.extra = { ...runTree.extra, metadata: { ...runTree.extra?.metadata, usage_metadata: { ...langsmithUsage, input_token_details: { ...inputTokenDetails, }, output_token_details: { ...outputTokenDetails, }, }, }, }; }; /** * AI SDK middleware that wraps an AI SDK 5 model and adds LangSmith tracing. */ export function LangSmithMiddleware(config) { const { name, modelId, lsConfig } = config ?? {}; return { wrapGenerate: async ({ doGenerate, params }) => { const traceableFunc = traceable(async (_params) => { const result = await doGenerate(); const currentRunTree = getCurrentRunTree(true); if (currentRunTree !== undefined) { setUsageMetadataOnRunTree(result, currentRunTree); } return result; }, { ...lsConfig, name: name ?? "ai.doGenerate", run_type: "llm", metadata: { ls_model_name: modelId, ai_sdk_method: "ai.doGenerate", ...lsConfig?.metadata, }, processInputs: (inputs) => { const typedInputs = inputs; return _formatTracedInputs(typedInputs); }, processOutputs: (outputs) => { const typedOutputs = outputs; return _formatTracedOutputs(typedOutputs); }, }); return traceableFunc(params); }, wrapStream: async ({ doStream, params }) => { const parentRunTree = getCurrentRunTree(true); let runTree; if (parentRunTree != null && typeof parentRunTree === "object" && typeof parentRunTree.createChild === "function") { runTree = parentRunTree?.createChild({ ...lsConfig, name: name ?? "ai.doStream", run_type: "llm", metadata: { ls_model_name: modelId, ai_sdk_method: "ai.doStream", ...lsConfig?.metadata, }, inputs: _formatTracedInputs(params), }); } await runTree?.postRun(); try { const { stream, ...rest } = await doStream(); const chunks = []; const transformStream = new TransformStream({ async transform(chunk, controller) { chunks.push(chunk); controller.enqueue(chunk); }, async flush() { try { const output = chunks.reduce((aggregated, chunk) => { if (chunk.type === "text-delta") { if (chunk.delta == null) { return aggregated; } return { ...aggregated, content: aggregated.content + chunk.delta, }; } else if (chunk.type === "tool-call") { const matchingToolCall = aggregated.tool_calls.find((call) => call.id === chunk.toolCallId); if (matchingToolCall != null) { return aggregated; } return { ...aggregated, tool_calls: [ ...aggregated.tool_calls, { id: chunk.toolCallId, type: "function", function: { name: chunk.toolName, arguments: chunk.input, }, }, ], }; } else if (chunk.type === "finish") { if (runTree != null) { setUsageMetadataOnRunTree(chunk, runTree); } return { ...aggregated, providerMetadata: chunk.providerMetadata, finishReason: chunk.finishReason, }; } else { return aggregated; } }, { content: "", // eslint-disable-next-line @typescript-eslint/no-explicit-any tool_calls: [], }); await runTree?.end(_formatTracedOutputs(output)); } catch (error) { await runTree?.end(undefined, error.message ?? String(error)); throw error; } finally { await runTree?.patchRun(); } }, }); return { stream: stream.pipeThrough(transformStream), ...rest, }; } catch (error) { await runTree?.end(undefined, error.message ?? String(error)); await runTree?.patchRun(); throw error; } }, }; }