Initial commit: E4B-MarkBase model integration with passing tests
CI / build-and-test (push) Has been cancelled
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
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user