UNPKG

@endlessriver/optimaiz

Version:

SDK for LLM observability - track costs, latency, and usage across OpenAI, Anthropic, Gemini, and more

458 lines (455 loc) 14.1 kB
// src/errors.ts var OptimaizError = class extends Error { constructor(message, status, details, type) { super(message); this.name = "OptimaizError"; this.status = status; this.details = details; this.type = type; } }; var OptimaizAuthenticationError = class extends OptimaizError { constructor(message, details) { super(message, 401, details, "AUTHENTICATION_ERROR"); this.name = "OptimaizAuthenticationError"; } }; var OptimaizValidationError = class extends OptimaizError { constructor(message, details) { super(message, 400, details, "VALIDATION_ERROR"); this.name = "OptimaizValidationError"; } }; var OptimaizServerError = class extends OptimaizError { constructor(message, details) { super(message, 500, details, "SERVER_ERROR"); this.name = "OptimaizServerError"; } }; var OptimaizNetworkError = class extends OptimaizError { constructor(message, details) { super(message, 0, details, "NETWORK_ERROR"); this.name = "OptimaizNetworkError"; } }; function isOptimaizError(error) { return error instanceof OptimaizError; } function isAuthenticationError(error) { return error instanceof OptimaizAuthenticationError || error?.type === "AUTHENTICATION_ERROR"; } function isValidationError(error) { return error instanceof OptimaizValidationError || error?.type === "VALIDATION_ERROR"; } function isServerError(error) { return error instanceof OptimaizServerError || error?.type === "SERVER_ERROR"; } function isNetworkError(error) { return error instanceof OptimaizNetworkError || error?.type === "NETWORK_ERROR"; } // src/tools.ts function validateTools(tools) { const errors = []; if (!Array.isArray(tools)) { errors.push("Tools must be an array"); return { valid: false, errors }; } for (const tool of tools) { if (!tool.name || typeof tool.name !== "string") { errors.push(`Tool must have a valid name: ${tool.name}`); } if (!tool.description || typeof tool.description !== "string") { errors.push(`Tool ${tool.name} must have a valid description`); } if (!tool.parameters || typeof tool.parameters !== "object") { errors.push(`Tool ${tool.name} must have valid parameters`); } else { if (tool.parameters.type !== "object") { errors.push(`Tool ${tool.name} parameters must have type 'object'`); } if (!tool.parameters.properties || typeof tool.parameters.properties !== "object") { errors.push(`Tool ${tool.name} must have properties defined`); } } } const names = tools.map((t) => t.name); const duplicates = names.filter((name, i) => names.indexOf(name) !== i); if (duplicates.length > 0) { errors.push(`Duplicate tool names found: ${duplicates.join(", ")}`); } return { valid: errors.length === 0, errors }; } function toOpenAIFormat(tools) { return tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } })); } function toAnthropicFormat(tools) { return tools.map((tool) => ({ name: tool.name, description: tool.description, input_schema: { type: "object", properties: tool.parameters.properties, required: tool.parameters.required } })); } function toMistralFormat(tools) { return tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } })); } function toProviderFormat(tools, provider) { const lowerProvider = provider.toLowerCase(); if (lowerProvider.includes("openai") || lowerProvider.includes("gpt")) { return toOpenAIFormat(tools); } if (lowerProvider.includes("anthropic") || lowerProvider.includes("claude")) { return toAnthropicFormat(tools); } if (lowerProvider.includes("mistral")) { return toMistralFormat(tools); } return toOpenAIFormat(tools); } function fromOpenAIFormat(tools) { return tools.map((tool) => ({ name: tool.function.name, description: tool.function.description, parameters: tool.function.parameters })); } function fromAnthropicFormat(tools) { return tools.map((tool) => ({ name: tool.name, description: tool.description, parameters: { type: "object", properties: tool.input_schema.properties, required: tool.input_schema.required } })); } function fromMistralFormat(tools) { return tools.map((tool) => ({ name: tool.function.name, description: tool.function.description, parameters: tool.function.parameters })); } function fromProviderFormat(tools, provider) { const lowerProvider = provider.toLowerCase(); if (lowerProvider.includes("openai") || lowerProvider.includes("gpt")) { return fromOpenAIFormat(tools); } if (lowerProvider.includes("anthropic") || lowerProvider.includes("claude")) { return fromAnthropicFormat(tools); } if (lowerProvider.includes("mistral")) { return fromMistralFormat(tools); } if (tools.length > 0) { const first = tools[0]; if (first.function) { return fromOpenAIFormat(tools); } if (first.input_schema) { return fromAnthropicFormat(tools); } } return tools; } // src/client.ts var DEFAULT_BASE_URL = "http://localhost:3456"; var DEFAULT_TIMEOUT = 3e4; var DEFAULT_RETRIES = 0; function generateId() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); } var OptimaizClient = class { constructor(config) { if (!config.token || typeof config.token !== "string") { throw new OptimaizValidationError("OptimaizClient requires a valid token"); } this.token = config.token; this.baseUrl = config.baseUrl || DEFAULT_BASE_URL; this.timeout = config.timeout || DEFAULT_TIMEOUT; this.retries = config.retries || DEFAULT_RETRIES; this.debug = config.debug || false; } log(message, data) { if (this.debug) { console.log(`[Optimaiz] ${message}`, data || ""); } } async request(path, body, retriesLeft = this.retries) { const url = `${this.baseUrl}${path}`; this.log(`POST ${path}`, { bodyKeys: Object.keys(body) }); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.token}` }, body: JSON.stringify(body), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { let errorMessage; let errorDetails; try { const errorData = await response.json(); errorMessage = errorData.error || `HTTP ${response.status}`; errorDetails = errorData.details; } catch { errorMessage = `HTTP ${response.status}`; } if (response.status === 401) { throw new OptimaizAuthenticationError(errorMessage, errorDetails); } else if (response.status === 400) { throw new OptimaizValidationError(errorMessage, errorDetails); } else if (response.status >= 500) { throw new OptimaizServerError(errorMessage, errorDetails); } else { throw new OptimaizError(errorMessage, response.status, errorDetails); } } return response.json(); } catch (error) { if (error.name === "AbortError") { throw new OptimaizNetworkError(`Request timeout after ${this.timeout}ms`); } if (error.name === "TypeError" && error.message.includes("fetch")) { if (retriesLeft > 0) { this.log(`Retrying request (${retriesLeft} retries left)`); await new Promise((resolve) => setTimeout(resolve, 1e3)); return this.request(path, body, retriesLeft - 1); } throw new OptimaizNetworkError(`Unable to connect to ${this.baseUrl}`); } throw error; } } /** * Start a new trace */ async startTrace(data) { if (!data.promptTemplate && !data.promptVariables) { throw new OptimaizValidationError( "Either promptTemplate or promptVariables must be provided" ); } if (data.tools) { const validation = validateTools(data.tools); if (!validation.valid) { throw new OptimaizValidationError( `Invalid tool definitions: ${validation.errors.join(", ")}` ); } } const traceId = data.traceId || generateId(); this.request("/api/v1/interactions/start", { ...data, traceId }).catch((err) => this.log("Failed to start trace in background", err)); return { success: true, traceId }; } /** * Append LLM response to a trace */ async appendResponse(data) { if (!data.traceId) { throw new OptimaizValidationError("traceId is required"); } this.request("/api/v1/interactions/append", data).catch((err) => this.log("Failed to append response in background", err)); return { success: true, traceId: data.traceId }; } /** * Finalize a trace (calculate latency, etc.) */ async finalizeTrace(traceId) { if (!traceId) { throw new OptimaizValidationError("traceId is required"); } this.request("/api/v1/interactions/finalize", { traceId }).catch((err) => this.log("Failed to finalize trace in background", err)); return { success: true, traceId }; } /** * Log an error to a trace */ async logError(traceId, error) { if (!traceId) { throw new OptimaizValidationError("traceId is required"); } this.request("/api/v1/interactions/error", { traceId, error }).catch((err) => this.log("Failed to log error in background", err)); return { success: true, traceId }; } /** * Send feedback for a trace */ async sendFeedback(traceId, feedback) { if (!traceId) { throw new OptimaizValidationError("traceId is required"); } this.request("/api/v1/interactions/feedback", { traceId, feedback }).catch((err) => this.log("Failed to send feedback in background", err)); return { success: true, traceId }; } /** * Add tool execution record */ async addToolExecution(data) { if (!data.traceId) { throw new OptimaizValidationError("traceId is required"); } this.request("/api/v1/interactions/tool-execution", { ...data, executionTime: data.executionTime || /* @__PURE__ */ new Date() }).catch((err) => this.log("Failed to add tool execution in background", err)); return { success: true, traceId: data.traceId }; } /** * Add tool results to a trace */ async addToolResults(data) { if (!data.traceId) { throw new OptimaizValidationError("traceId is required"); } if (!Array.isArray(data.toolResults) || data.toolResults.length === 0) { throw new OptimaizValidationError("toolResults must be a non-empty array"); } this.request("/api/v1/interactions/tool-results", data).catch((err) => this.log("Failed to add tool results in background", err)); return { success: true, traceId: data.traceId }; } /** * Compose prompts from templates and variables */ composePrompts(templates, variables) { if (!Array.isArray(templates)) { throw new OptimaizValidationError("Templates must be an array"); } const prompts = []; const promptTemplate = []; for (const { role, content, type = "text" } of templates) { const resolved = Object.entries(variables).reduce( (acc, [key, value]) => acc.replace(new RegExp(`\\{${key}\\}`, "g"), String(value || "")), content ); prompts.push({ type, role, value: resolved }); promptTemplate.push({ type, role, value: content }); } return { prompts, promptTemplate, promptVariables: variables }; } /** * High-level wrapper for LLM calls with automatic tracing */ async wrapLLMCall({ traceId = generateId(), agentId, flowId, threadId, sessionId, userId, promptTemplate, promptVariables, tools, provider, model, modelParams, metadata, tags, call }) { this.startTrace({ traceId, agentId, flowId, threadId, sessionId, userId, promptTemplate, promptVariables, tools, provider, model, modelParams, metadata, tags }).catch((err) => this.log("Failed to start trace", err)); try { const response = await call(); this.appendResponse({ traceId, rawResponse: response, provider, model }).catch((err) => this.log("Failed to append response", err)); this.finalizeTrace(traceId).catch((err) => this.log("Failed to finalize trace", err)); return { response, traceId }; } catch (err) { this.logError(traceId, { message: err.message, code: err.code || "unknown_error", details: err.stack }).catch((logErr) => this.log("Failed to log error", logErr)); throw err; } } /** * Convert tools to provider-specific format */ convertToolsToProvider(tools, provider) { return toProviderFormat(tools, provider); } /** * Convert provider-specific tools to unified format */ convertToolsFromProvider(tools, provider) { return fromProviderFormat(tools, provider); } /** * Validate tool definitions */ validateTools(tools) { return validateTools(tools); } }; export { OptimaizAuthenticationError, OptimaizClient, OptimaizError, OptimaizNetworkError, OptimaizServerError, OptimaizValidationError, OptimaizClient as default, fromAnthropicFormat, fromMistralFormat, fromOpenAIFormat, fromProviderFormat, isAuthenticationError, isNetworkError, isOptimaizError, isServerError, isValidationError, toAnthropicFormat, toMistralFormat, toOpenAIFormat, toProviderFormat, validateTools };