Files
OmniRoute/src/shared/constants/errorCodes.ts
KooshaPari c522454c79 feat(live-ws): allow non-loopback clients via LIVE_WS_ALLOWED_HOSTS (closes #4873) (#4877)
Integrated into release/v3.8.36 (live-ws + combo-api commits; Tailscale CGNAT commit held pending opt-in/opt-out decision)
2026-06-23 23:21:51 -03:00

246 lines
5.8 KiB
TypeScript

/**
* Error Codes Catalog — T-22
*
* Centralized error code registry for consistent API error responses.
* Each code has a category prefix, numeric ID, message, and HTTP status.
*
* Usage:
* import { ERROR_CODES, createErrorResponse } from "@/shared/constants/errorCodes";
* return createErrorResponse("AUTH_001", { detail: "Token expired" });
*
* @module shared/constants/errorCodes
*/
export interface ErrorCodeDef {
code: string;
message: string;
httpStatus: number;
category: string;
}
interface ErrorDetails {
detail?: string;
requestId?: string;
retryAfter?: number;
}
export const ERROR_CODES: Record<string, ErrorCodeDef> = {
// ── Auth ──
AUTH_001: {
code: "AUTH_001",
message: "Authentication required",
httpStatus: 401,
category: "AUTH",
},
AUTH_002: { code: "AUTH_002", message: "Invalid API key", httpStatus: 401, category: "AUTH" },
AUTH_003: { code: "AUTH_003", message: "API key expired", httpStatus: 401, category: "AUTH" },
AUTH_004: {
code: "AUTH_004",
message: "Insufficient permissions",
httpStatus: 403,
category: "AUTH",
},
AUTH_005: { code: "AUTH_005", message: "Account locked", httpStatus: 423, category: "AUTH" },
AUTH_006: {
code: "AUTH_006",
message: "No credentials for provider",
httpStatus: 400,
category: "AUTH",
},
AUTH_007: {
code: "AUTH_007",
message: "Session expired (re-login required)",
httpStatus: 401,
category: "AUTH",
},
// ── Proxy ──
PROXY_001: {
code: "PROXY_001",
message: "Proxy connection failed",
httpStatus: 502,
category: "PROXY",
},
PROXY_002: { code: "PROXY_002", message: "Proxy timeout", httpStatus: 504, category: "PROXY" },
PROXY_003: {
code: "PROXY_003",
message: "All proxies exhausted",
httpStatus: 503,
category: "PROXY",
},
// ── Rate Limiting ──
RATE_001: {
code: "RATE_001",
message: "Rate limit exceeded",
httpStatus: 429,
category: "RATE_LIMIT",
},
RATE_002: {
code: "RATE_002",
message: "Daily budget exceeded",
httpStatus: 429,
category: "RATE_LIMIT",
},
RATE_003: {
code: "RATE_003",
message: "All accounts rate-limited",
httpStatus: 503,
category: "RATE_LIMIT",
},
// ── Model / Routing ──
MODEL_001: { code: "MODEL_001", message: "Model not found", httpStatus: 404, category: "MODEL" },
MODEL_002: {
code: "MODEL_002",
message: "Ambiguous model identifier",
httpStatus: 400,
category: "MODEL",
},
MODEL_003: {
code: "MODEL_003",
message: "Model temporarily unavailable",
httpStatus: 503,
category: "MODEL",
},
// ── Provider ──
PROVIDER_001: {
code: "PROVIDER_001",
message: "Provider error",
httpStatus: 502,
category: "PROVIDER",
},
PROVIDER_002: {
code: "PROVIDER_002",
message: "Provider timeout",
httpStatus: 504,
category: "PROVIDER",
},
PROVIDER_003: {
code: "PROVIDER_003",
message: "Provider not configured",
httpStatus: 400,
category: "PROVIDER",
},
// ── Validation ──
VALID_001: {
code: "VALID_001",
message: "Invalid request body",
httpStatus: 400,
category: "VALIDATION",
},
VALID_002: {
code: "VALID_002",
message: "Missing required field",
httpStatus: 400,
category: "VALIDATION",
},
VALID_003: {
code: "VALID_003",
message: "Input sanitization blocked",
httpStatus: 400,
category: "VALIDATION",
},
// ── Combos (T-22.b — 400-shape standardization for /api/combos/{id}) ──
COMBO_001: {
code: "COMBO_001",
message: "Request body is not valid JSON",
httpStatus: 400,
category: "COMBO",
},
COMBO_002: {
code: "COMBO_002",
message: "One or more combo fields are invalid",
httpStatus: 400,
category: "COMBO",
},
COMBO_003: {
code: "COMBO_003",
message: "Composite tier configuration is invalid",
httpStatus: 400,
category: "COMBO",
},
COMBO_004: {
code: "COMBO_004",
message: "A combo with this name already exists",
httpStatus: 400,
category: "COMBO",
},
COMBO_005: {
code: "COMBO_005",
message: "Combo reference graph is invalid (cycle or excessive depth)",
httpStatus: 400,
category: "COMBO",
},
COMBO_006: {
code: "COMBO_006",
message: "This combo is managed by Quota Share and cannot be modified here",
httpStatus: 409,
category: "COMBO",
},
COMBO_007: {
code: "COMBO_007",
message: "Combo not found",
httpStatus: 404,
category: "COMBO",
},
// ── Internal ──
INTERNAL_001: {
code: "INTERNAL_001",
message: "Internal server error",
httpStatus: 500,
category: "INTERNAL",
},
INTERNAL_002: {
code: "INTERNAL_002",
message: "Database error",
httpStatus: 500,
category: "INTERNAL",
},
INTERNAL_003: {
code: "INTERNAL_003",
message: "Circuit breaker open",
httpStatus: 503,
category: "INTERNAL",
},
};
export function createErrorResponse(code: string, details: ErrorDetails = {}) {
const def = ERROR_CODES[code];
if (!def) {
return {
error: {
code: "INTERNAL_001",
message: `Unknown error code: ${code}`,
category: "INTERNAL",
},
status: 500,
};
}
const response: any = {
error: {
code: def.code,
message: def.message,
category: def.category,
...(details.detail ? { detail: details.detail } : {}),
...(details.requestId ? { requestId: details.requestId } : {}),
},
status: def.httpStatus,
};
if (details.retryAfter) {
response.retryAfter = details.retryAfter;
}
return response;
}
export function getErrorsByCategory(category: string): ErrorCodeDef[] {
return Object.values(ERROR_CODES).filter((e) => e.category === category);
}