ac75faa0cc
CI / build-and-test (push) Has been cancelled
- E4B-MarkBase model (42 layers, 4.4GB) loaded successfully - All Phase 1-6 tests passed (model loading, forward pass, vision/audio towers, token generation, performance) - All stress tests passed (5/5 in 127.6s) - Concurrent inference - Memory stress (67.5 tok/s, 0 NaN) - Continuous generation - Batch processing - Long-running stability - Swift Metal inference engine with multimodal support
311 lines
11 KiB
Swift
311 lines
11 KiB
Swift
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<T> {
|
|
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<U>(_ transform: (T) -> U) -> Result<U> {
|
|
switch self {
|
|
case .success(let value):
|
|
return .success(transform(value))
|
|
case .failure(let error):
|
|
return .failure(error)
|
|
}
|
|
}
|
|
}
|