UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

304 lines (303 loc) 10.8 kB
/** * Laminar Exporter * Exports spans to Laminar LLM tracing platform * @see https://docs.laminar.run/ */ import { logger } from "../../utils/logger.js"; import { SpanStatus, SpanType } from "../../types/index.js"; import { BaseExporter } from "./baseExporter.js"; /** * Laminar exporter for LLM pipeline tracing and monitoring * Supports detailed traces with input/output tracking */ export class LaminarExporter extends BaseExporter { apiKey; projectApiKey; baseUrl; constructor(config) { super("laminar", config); this.apiKey = config.apiKey; this.projectApiKey = config.projectApiKey; this.baseUrl = config.baseUrl ?? "https://api.laminar.run"; } async initialize() { if (this.initialized) { return; } // Verify API key by making a test call try { const response = await fetch(`${this.baseUrl}/v1/health`, { headers: this.getHeaders(), }); if (!response.ok) { logger.warn("[Laminar] Could not verify API connection:", response.statusText); } } catch (error) { logger.warn("[Laminar] Could not verify API connection:", error instanceof Error ? error.message : error); } this.initialized = true; this.startFlushInterval(this.config.flushIntervalMs ?? 5000); } /** * Get authorization headers */ getHeaders() { const headers = { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, }; // Add project API key if provided if (this.projectApiKey) { headers["X-Project-Api-Key"] = this.projectApiKey; } return headers; } async exportSpan(span) { const startTime = Date.now(); try { const trace = this.convertToLaminarTrace(span); const response = await fetch(`${this.baseUrl}/v1/traces`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify(trace), }); if (!response.ok) { throw new Error(`Export failed: ${response.statusText}`); } return this.createSuccessResult(1, Date.now() - startTime); } catch (error) { return this.createFailureResult([span.spanId], error instanceof Error ? error.message : String(error), Date.now() - startTime); } } async exportBatch(spans) { const startTime = Date.now(); try { const traces = spans.map((s) => this.convertToLaminarTrace(s)); const response = await fetch(`${this.baseUrl}/v1/traces/batch`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify({ traces }), }); if (!response.ok) { throw new Error(`Batch export failed: ${response.statusText}`); } return this.createSuccessResult(spans.length, Date.now() - startTime); } catch (error) { return this.createFailureResult(spans.map((s) => s.spanId), error instanceof Error ? error.message : String(error), Date.now() - startTime); } } async flush() { if (this.buffer.length > 0) { const spans = [...this.buffer]; this.buffer = []; await this.exportBatch(spans); } } async shutdown() { await this.flush(); this.stopFlushInterval(); this.initialized = false; } async healthCheck() { try { await this.withRetry(() => this.ping(), "health check"); return this.createHealthStatus(true); } catch { return this.createHealthStatus(false, ["Health check failed"]); } } /** * Verify connectivity to Laminar API */ async ping() { const response = await fetch(`${this.baseUrl}/v1/health`, { headers: this.getHeaders(), }); if (!response.ok) { throw new Error(`Laminar API unreachable: ${response.status}`); } } /** * Convert span to Laminar trace format */ convertToLaminarTrace(span) { return { // Core identifiers trace_id: span.traceId, span_id: span.spanId, parent_span_id: span.parentSpanId, // Trace metadata name: span.name, type: this.mapSpanTypeToLaminarType(span.type), start_time: span.startTime, end_time: span.endTime, duration_ms: span.durationMs, // Status status: this.mapSpanStatus(span.status), status_message: span.statusMessage, // Model information model: { provider: span.attributes["ai.provider"], name: span.attributes["ai.model"], version: span.attributes["ai.model.version"], }, // Token usage usage: { input_tokens: span.attributes["ai.tokens.input"], output_tokens: span.attributes["ai.tokens.output"], total_tokens: span.attributes["ai.tokens.total"], cache_read_tokens: span.attributes["ai.tokens.cache_read"], cache_creation_tokens: span.attributes["ai.tokens.cache_creation"], reasoning_tokens: span.attributes["ai.tokens.reasoning"], }, // Cost tracking cost: { input: span.attributes["ai.cost.input"], output: span.attributes["ai.cost.output"], total: span.attributes["ai.cost.total"], currency: span.attributes["ai.cost.currency"] || "USD", }, // Generation parameters parameters: { temperature: span.attributes["ai.temperature"], max_tokens: span.attributes["ai.max_tokens"], top_p: span.attributes["ai.top_p"], stop_sequences: span.attributes["ai.stop_sequences"], }, // Input/Output input: span.attributes["input"], output: span.attributes["output"], // Tool information tool: span.attributes["tool.name"] ? { name: span.attributes["tool.name"], server: span.attributes["tool.server"], success: span.attributes["tool.success"], } : undefined, // Error information error: span.status === SpanStatus.ERROR ? { type: span.attributes["error.type"], message: span.attributes["error.message"], stack: span.attributes["error.stack"], } : undefined, // User and session context context: { user_id: span.attributes["user.id"], session_id: span.attributes["session.id"], environment: span.attributes["deployment.environment"] || this.config.environment, service: { name: span.attributes["service.name"], version: span.attributes["service.version"] || this.config.version, }, }, // Events events: span.events.map((event) => ({ name: event.name, timestamp: event.timestamp, attributes: event.attributes, })), // Links to related spans links: span.links.map((link) => ({ trace_id: link.traceId, span_id: link.spanId, attributes: link.attributes, })), // Additional metadata metadata: this.extractMetadata(span.attributes), }; } /** * Map NeuroLink span type to Laminar type */ mapSpanTypeToLaminarType(type) { const typeMap = { [SpanType.AGENT_RUN]: "agent", [SpanType.WORKFLOW_STEP]: "workflow", [SpanType.TOOL_CALL]: "tool", [SpanType.MODEL_GENERATION]: "llm", [SpanType.EMBEDDING]: "embedding", [SpanType.RETRIEVAL]: "retrieval", [SpanType.MEMORY]: "memory", [SpanType.CONTEXT_COMPACTION]: "custom", [SpanType.RAG]: "retrieval", [SpanType.EVALUATION]: "custom", [SpanType.MCP_TRANSPORT]: "tool", [SpanType.MEDIA_GENERATION]: "llm", [SpanType.PPT_GENERATION]: "custom", [SpanType.WORKFLOW]: "workflow", [SpanType.TTS]: "custom", [SpanType.STT]: "custom", [SpanType.SERVER_REQUEST]: "custom", [SpanType.CUSTOM]: "custom", }; return typeMap[type] || "custom"; } /** * Map span status to Laminar status format */ mapSpanStatus(status) { switch (status) { case SpanStatus.OK: return "success"; case SpanStatus.ERROR: return "error"; default: return "unset"; } } /** * Extract additional metadata from span attributes * Filters out standard attributes that are already handled */ extractMetadata(attributes) { const standardKeys = new Set([ "service.name", "service.version", "deployment.environment", "user.id", "session.id", "ai.provider", "ai.model", "ai.model.version", "ai.tokens.input", "ai.tokens.output", "ai.tokens.total", "ai.tokens.cache_read", "ai.tokens.cache_creation", "ai.tokens.reasoning", "ai.cost.input", "ai.cost.output", "ai.cost.total", "ai.cost.currency", "ai.temperature", "ai.max_tokens", "ai.top_p", "ai.stop_sequences", "tool.name", "tool.server", "tool.success", "error.type", "error.message", "error.stack", "error", "input", "output", "expected", "scores", ]); const metadata = {}; for (const [key, value] of Object.entries(attributes)) { if (!standardKeys.has(key) && value !== undefined) { metadata[key] = value; } } return metadata; } }