langsmith
Version:
Client library to connect to the LangSmith Observability and Evaluation Platform.
239 lines (238 loc) • 9.78 kB
JavaScript
/**
* 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);
}