UNPKG

langsmith

Version:

Client library to connect to the LangSmith Observability and Evaluation Platform.

239 lines (238 loc) 9.78 kB
/** * Shared helper functions for error handling. * * These functions are used to parse error responses and raise appropriate * exceptions. They contain no I/O operations. */ import { LangSmithQuotaExceededError, LangSmithResourceNotFoundError, LangSmithResourceTimeoutError, LangSmithSandboxAPIError, LangSmithSandboxAuthenticationError, LangSmithSandboxError, LangSmithSandboxConnectionError, LangSmithSandboxCreationError, LangSmithSandboxNotReadyError, LangSmithSandboxOperationError, LangSmithValidationError, } from "./errors.js"; // ============================================================================= // Input validation // ============================================================================= /** * Validate TTL values for sandbox create/update (minute resolution). * * @param value - TTL in seconds (`undefined` means unset; `0` disables). * @param name - Parameter name for error messages. * @throws LangSmithValidationError if negative or not a multiple of 60 (when non-zero). */ export function validateTtl(value, name) { if (value === undefined) { return; } if (value < 0) { throw new LangSmithValidationError(`${name} must be >= 0, got ${value}`, name); } if (value !== 0 && value % 60 !== 0) { throw new LangSmithValidationError(`${name} must be a multiple of 60 seconds, got ${value}`, name); } } /** * Parse standardized error response. * * Expected format: {"detail": {"error": "...", "message": "..."}} */ export async function parseErrorResponse(response) { try { const data = await response.json(); const detail = data?.detail; // Standardized format: {"detail": {"error": "...", "message": "..."}} if (detail && typeof detail === "object" && !Array.isArray(detail)) { return { errorType: detail.error, message: detail.message || `HTTP ${response.status}: ${response.statusText}`, }; } // Pydantic validation error format: {"detail": [{"loc": [...], "msg": "..."}]} if (Array.isArray(detail) && detail.length > 0) { const messages = detail .filter((d) => typeof d === "object" && d !== null) .map((d) => d.msg || String(d)) .filter(Boolean); return { errorType: undefined, message: messages.length > 0 ? messages.join("; ") : `HTTP ${response.status}: ${response.statusText}`, }; } // Fallback for plain string detail return { errorType: undefined, message: detail || `HTTP ${response.status}: ${response.statusText}`, }; } catch { return { errorType: undefined, message: `HTTP ${response.status}: ${response.statusText}`, }; } } /** * Parse Pydantic validation error response. * * Returns a list of validation error details. */ export async function parseValidationError(response) { try { const data = await response.json(); const detail = data?.detail; if (Array.isArray(detail)) { return detail; } return []; } catch { return []; } } /** * Extract quota type from error message. */ export function extractQuotaType(message) { const messageLower = message.toLowerCase(); // Check for sandbox count quota if (messageLower.includes("sandbox") && (messageLower.includes("count") || messageLower.includes("limit"))) { return "sandbox_count"; } else if (messageLower.includes("cpu")) { return "cpu"; } else if (messageLower.includes("memory")) { return "memory"; } else if (messageLower.includes("storage")) { return "storage"; } return undefined; } // ============================================================================= // Client Error Handlers // ============================================================================= /** * Handle HTTP errors specific to sandbox creation. * * Maps API error responses to specific exception types: * - 408: LangSmithResourceTimeoutError (sandbox didn't become ready in time) * - 422: LangSmithValidationError (bad input) or LangSmithSandboxCreationError (runtime) * - 429: LangSmithQuotaExceededError (org limits exceeded) * - 503: LangSmithSandboxCreationError (no resources available) * - Other: Falls through to generic error handling */ export async function handleSandboxCreationError(response) { const status = response.status; const clonedResponse = response.clone(); const data = await parseErrorResponse(response); if (status === 408) { // Timeout - include the message which contains last known status throw new LangSmithResourceTimeoutError(data.message, "sandbox"); } else if (status === 422) { // Check if this is a Pydantic validation error (bad input) vs creation error const details = await parseValidationError(clonedResponse); if (details.length > 0 && details.some((d) => d.type === "value_error")) { // Pydantic validation error (bad input - exceeds server limits) const field = details[0]?.loc?.slice(-1)[0]; throw new LangSmithValidationError(data.message, field, details); } else { // Sandbox creation failed (runtime error like image pull failure) throw new LangSmithSandboxCreationError(data.message, data.errorType); } } else if (status === 429) { // Organization quota exceeded - extract type or default to sandbox_count const quotaType = extractQuotaType(data.message) ?? "unknown"; throw new LangSmithQuotaExceededError(data.message, quotaType); } else if (status === 503) { // Service Unavailable - scheduling failed throw new LangSmithSandboxCreationError(data.message, data.errorType || "Unschedulable"); } // Fall through to generic handling — pass clone since body is already consumed return handleClientHttpError(clonedResponse); } /** * Handle HTTP errors and raise appropriate exceptions (for client operations). */ export async function handleClientHttpError(response) { const status = response.status; // Only clone when we need to read the body twice (status 422 reads it again // for structured validation details after parseErrorResponse consumes it). const clonedResponse = status === 422 ? response.clone() : null; const data = await parseErrorResponse(response); const message = data.message; const errorType = data.errorType; if (status === 401 || status === 403) { throw new LangSmithSandboxAuthenticationError(message); } if (status === 404) { throw new LangSmithResourceNotFoundError(message); } // Handle validation errors (invalid resource values, formats, etc.) if (status === 422 && clonedResponse) { const details = await parseValidationError(clonedResponse); const field = details[0]?.loc?.slice(-1)[0]; throw new LangSmithValidationError(message, field, details); } // Handle quota exceeded errors (org limits) if (status === 429) { const quotaType = extractQuotaType(message); throw new LangSmithQuotaExceededError(message, quotaType); } if (status === 502 && errorType === "ConnectionError") { throw new LangSmithSandboxConnectionError(message); } if (status === 500) { throw new LangSmithSandboxAPIError(message); } throw new LangSmithSandboxError(message); } // ============================================================================= // Sandbox Operation Error Handlers // ============================================================================= /** * Handle HTTP errors for sandbox operations (run, read, write). * * Maps API error types to specific exceptions: * - WriteError -> LangSmithSandboxOperationError (operation="write") * - ReadError -> LangSmithSandboxOperationError (operation="read") * - CommandError -> LangSmithSandboxOperationError (operation="command") * - ConnectionError (502) -> LangSmithSandboxConnectionError * - FileNotFound / 404 -> LangSmithResourceNotFoundError (resourceType="file") * - NotReady (400) -> LangSmithSandboxNotReadyError * - 403 -> LangSmithSandboxOperationError (permission denied) */ export async function handleSandboxHttpError(response) { const data = await parseErrorResponse(response); const message = data.message; const errorType = data.errorType; const status = response.status; // Operation-specific errors (from sandbox runtime) if (errorType === "WriteError") { throw new LangSmithSandboxOperationError(message, "write", errorType); } if (errorType === "ReadError") { throw new LangSmithSandboxOperationError(message, "read", errorType); } if (errorType === "CommandError") { throw new LangSmithSandboxOperationError(message, "command", errorType); } // Permission denied if (status === 403) { throw new LangSmithSandboxOperationError(message, undefined, "PermissionDenied"); } // Connection to sandbox failed if (status === 502 && errorType === "ConnectionError") { throw new LangSmithSandboxConnectionError(message); } // Not ready / not found if (status === 400 && errorType === "NotReady") { throw new LangSmithSandboxNotReadyError(message); } if (status === 404 || errorType === "FileNotFound") { throw new LangSmithResourceNotFoundError(message, "file"); } throw new LangSmithSandboxError(message); }