import Foundation // ───────────────────────────────────────────────────────────── // Unified Error Handling for MarkBase // ───────────────────────────────────────────────────────────── /// MarkBase error types public enum MarkBaseError: Error, LocalizedError { case modelNotFound(String) case invalidRequest(String) case modelLoadingFailed(String) case inferenceFailed(String) case tokenLimitExceeded(current: Int, max: Int) case invalidParameter(parameter: String, message: String) case internalError(String) case multimodalNotSupported case imageProcessingFailed case audioProcessingFailed public var errorDescription: String? { switch self { case .modelNotFound(let path): return "Model not found at: \(path)" case .invalidRequest(let message): return "Invalid request: \(message)" case .modelLoadingFailed(let detail): return "Failed to load model: \(detail)" case .inferenceFailed(let detail): return "Inference failed: \(detail)" case .tokenLimitExceeded(let current, let max): return "Token limit exceeded: \(current) > \(max)" case .invalidParameter(let parameter, let message): return "Invalid parameter '\(parameter)': \(message)" case .internalError(let detail): return "Internal error: \(detail)" case .multimodalNotSupported: return "Multimodal inference not supported for this model" case .imageProcessingFailed: return "Image processing failed" case .audioProcessingFailed: return "Audio processing failed" } } /// HTTP status code for this error public var httpStatus: Int { switch self { case .modelNotFound: return 404 case .invalidRequest, .invalidParameter: return 400 case .modelLoadingFailed, .internalError: return 500 case .inferenceFailed: return 500 case .tokenLimitExceeded: return 400 case .multimodalNotSupported: return 400 case .imageProcessingFailed, .audioProcessingFailed: return 500 } } /// OpenAI-compatible error type public var errorType: String { switch self { case .modelNotFound: return "model_not_found" case .invalidRequest, .invalidParameter, .tokenLimitExceeded: return "invalid_request_error" case .modelLoadingFailed, .internalError: return "server_error" case .inferenceFailed: return "server_error" case .multimodalNotSupported: return "invalid_request_error" case .imageProcessingFailed, .audioProcessingFailed: return "server_error" } } /// Convert to OpenAI error response format public func toErrorResponse(param: String? = nil) -> ErrorResponse { ErrorResponse( error: toErrorDetail(param: param) ) } /// Convert to ErrorDetail public func toErrorDetail(param: String? = nil) -> ErrorDetail { ErrorDetail( message: localizedDescription, type: errorType, code: httpStatus, param: param ) } } /// OpenAI-compatible error response public struct ErrorResponse: Codable { public let error: ErrorDetail public init(error: ErrorDetail) { self.error = error } /// Create error response from MarkBaseError public static func from(_ error: MarkBaseError) -> ErrorResponse { ErrorResponse(error: error.toErrorDetail()) } } public struct ErrorDetail: Codable { public let message: String public let type: String public let code: Int public let param: String? public init(message: String, type: String, code: Int, param: String? = nil) { self.message = message self.type = type self.code = code self.param = param } } /// Validation helpers public enum Validator { /// Validate model path exists public static func validateModelPath(_ path: String) throws { guard FileManager.default.fileExists(atPath: path) else { throw MarkBaseError.modelNotFound(path) } // Check for required files (support both single-file and sharded safetensors) let hasSingleFile = FileManager.default.fileExists(atPath: (path as NSString).appendingPathComponent("model.safetensors")) let hasIndexFile = FileManager.default.fileExists(atPath: (path as NSString).appendingPathComponent("model.safetensors.index.json")) guard hasSingleFile || hasIndexFile else { throw MarkBaseError.modelLoadingFailed("Missing required file: model.safetensors or model.safetensors.index.json") } guard FileManager.default.fileExists(atPath: (path as NSString).appendingPathComponent("config.json")) else { throw MarkBaseError.modelLoadingFailed("Missing required file: config.json") } } /// Validate generation parameters public static func validateGenerationParams( maxTokens: Int?, temperature: Float?, topP: Float?, topK: Int? ) throws { if let maxTokens = maxTokens { guard maxTokens > 0 && maxTokens <= 4096 else { throw MarkBaseError.invalidParameter( parameter: "max_tokens", message: "Must be between 1 and 4096" ) } } if let temperature = temperature { guard temperature >= 0.0 && temperature <= 2.0 else { throw MarkBaseError.invalidParameter( parameter: "temperature", message: "Must be between 0.0 and 2.0" ) } } if let topP = topP { guard topP >= 0.0 && topP <= 1.0 else { throw MarkBaseError.invalidParameter( parameter: "top_p", message: "Must be between 0.0 and 1.0" ) } } if let topK = topK { guard topK > 0 else { throw MarkBaseError.invalidParameter( parameter: "top_k", message: "Must be greater than 0" ) } } } /// Validate prompt public static func validatePrompt(_ prompt: String, maxLength: Int = 4096) throws { guard !prompt.isEmpty else { throw MarkBaseError.invalidRequest("Prompt cannot be empty") } guard prompt.count <= maxLength else { throw MarkBaseError.invalidParameter( parameter: "prompt", message: "Prompt too long (max \(maxLength) characters)" ) } } /// Validate messages public static func validateMessages(_ messages: [ChatMessage]) throws { guard !messages.isEmpty else { throw MarkBaseError.invalidRequest("Messages array cannot be empty") } for (index, message) in messages.enumerated() { guard let content = message.content, !content.isEmpty else { throw MarkBaseError.invalidParameter( parameter: "messages[\(index)].content", message: "Content cannot be empty" ) } guard ["system", "user", "assistant"].contains(message.role) else { throw MarkBaseError.invalidParameter( parameter: "messages[\(index)].role", message: "Role must be 'system', 'user', or 'assistant'" ) } } } /// Validate multimodal messages public static func validateMultimodalMessages(_ messages: [MultimodalMessage]) throws { guard !messages.isEmpty else { throw MarkBaseError.invalidRequest("Messages array cannot be empty") } for (index, message) in messages.enumerated() { guard !message.content.isEmpty else { throw MarkBaseError.invalidParameter( parameter: "messages[\(index)].content", message: "Content array cannot be empty" ) } // Validate content parts for (partIndex, part) in message.content.enumerated() { switch part { case .text(let text): guard !text.isEmpty else { throw MarkBaseError.invalidParameter( parameter: "messages[\(index)].content[\(partIndex)]", message: "Text content cannot be empty" ) } case .imageUrl(let imageUrl): guard !imageUrl.url.isEmpty else { throw MarkBaseError.invalidParameter( parameter: "messages[\(index)].content[\(partIndex)].image_url.url", message: "Image URL cannot be empty" ) } case .audioUrl(let audioUrl): guard !audioUrl.url.isEmpty else { throw MarkBaseError.invalidParameter( parameter: "messages[\(index)].content[\(partIndex)].audio_url.url", message: "Audio URL cannot be empty" ) } case .videoUrl(let videoUrl): guard !videoUrl.url.isEmpty else { throw MarkBaseError.invalidParameter( parameter: "messages[\(index)].content[\(partIndex)].video_url.url", message: "Video URL cannot be empty" ) } } } // Validate role guard ["system", "user", "assistant"].contains(message.role) else { throw MarkBaseError.invalidParameter( parameter: "messages[\(index)].role", message: "Role must be 'system', 'user', or 'assistant'" ) } } } } /// Result type with error handling public enum Result { case success(T) case failure(MarkBaseError) public func get() throws -> T { switch self { case .success(let value): return value case .failure(let error): throw error } } public func map(_ transform: (T) -> U) -> Result { switch self { case .success(let value): return .success(transform(value)) case .failure(let error): return .failure(error) } } }