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
241 lines
10 KiB
Swift
241 lines
10 KiB
Swift
import Foundation
|
|
import MarkBase
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Complete API Router Implementation
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/// API endpoint handler
|
|
public final class APIRouter: @unchecked Sendable {
|
|
private let modelManager: ModelManager
|
|
private let metricsCollector: MetricsCollector
|
|
private let concurrencyController: DynamicConcurrencyController
|
|
private let requestQueue: RequestQueue
|
|
|
|
public init(
|
|
modelManager: ModelManager,
|
|
metricsCollector: MetricsCollector = .shared,
|
|
concurrencyController: DynamicConcurrencyController
|
|
) {
|
|
self.modelManager = modelManager
|
|
self.metricsCollector = metricsCollector
|
|
self.concurrencyController = concurrencyController
|
|
self.requestQueue = RequestQueue(controller: concurrencyController)
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Health & Info Endpoints
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/// GET /health
|
|
public func handleHealth() async -> [String: Any] {
|
|
let currentModel = await modelManager.getCurrentModel()
|
|
|
|
return [
|
|
"status": "healthy",
|
|
"model": currentModel?.name ?? "none",
|
|
"model_id": currentModel?.id ?? "",
|
|
"loaded": currentModel?.loaded ?? false,
|
|
"version": "1.0.0"
|
|
]
|
|
}
|
|
|
|
/// GET /v1/models
|
|
public func handleListModels() async -> [String: Any] {
|
|
let models = await modelManager.listModels()
|
|
|
|
return [
|
|
"object": "list",
|
|
"data": models.map { model in
|
|
[
|
|
"id": model.id,
|
|
"object": "model",
|
|
"created": Int(Date().timeIntervalSince1970),
|
|
"owned_by": "markbase",
|
|
"loaded": model.loaded
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Model Management Endpoints
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/// POST /v1/models/load
|
|
public func handleLoadModel(modelId: String) async throws -> [String: Any] {
|
|
try await modelManager.loadModel(id: modelId)
|
|
|
|
return [
|
|
"status": "success",
|
|
"message": "Model loaded: \(modelId)"
|
|
]
|
|
}
|
|
|
|
/// POST /v1/models/unload
|
|
public func handleUnloadModel() async -> [String: Any] {
|
|
await modelManager.unloadModel()
|
|
|
|
return [
|
|
"status": "success",
|
|
"message": "Model unloaded"
|
|
]
|
|
}
|
|
|
|
/// POST /v1/models/switch
|
|
public func handleSwitchModel(modelId: String) async throws -> [String: Any] {
|
|
try await modelManager.switchModel(to: modelId)
|
|
|
|
return [
|
|
"status": "success",
|
|
"message": "Model switched to: \(modelId)"
|
|
]
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Chat Completions Endpoint
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/// POST /v1/chat/completions
|
|
public func handleChatCompletion(
|
|
messages: [ChatMessage],
|
|
config: GenerationConfig
|
|
) async throws -> ChatCompletionResponse {
|
|
try await requestQueue.execute { [weak self] in
|
|
guard let self = self else {
|
|
throw ModelError.noModelLoaded
|
|
}
|
|
|
|
let generator = try await self.modelManager.getGenerator()
|
|
let tokenizer = try await self.modelManager.getTokenizer()
|
|
let _ = try await self.modelManager.getModel()
|
|
|
|
// Build prompt
|
|
let prompt = self.buildChatPrompt(messages: messages, tokenizer: tokenizer)
|
|
|
|
// Generate
|
|
let startTime = Date()
|
|
let response = try generator.generateComplete(prompt: prompt, config: config)
|
|
let duration = Date().timeIntervalSince(startTime)
|
|
|
|
// Record metrics
|
|
let tokens = tokenizer.encode(text: response).count
|
|
let resolvedModelId = (await modelManager.getCurrentModel())?.id ?? "unknown"
|
|
self.metricsCollector.recordRequest(duration: duration, tokens: tokens, model: resolvedModelId)
|
|
|
|
return ChatCompletionResponse(
|
|
id: self.generateId("chatcmpl"),
|
|
object: "chat.completion",
|
|
created: Int(Date().timeIntervalSince1970),
|
|
model: resolvedModelId,
|
|
choices: [
|
|
Choice(
|
|
index: 0,
|
|
message: ChatMessage(role: "assistant", content: response),
|
|
finish_reason: "stop"
|
|
)
|
|
],
|
|
usage: Usage(
|
|
promptTokens: tokenizer.encode(text: prompt).count,
|
|
completionTokens: tokens,
|
|
totalTokens: tokenizer.encode(text: prompt + response).count
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Embeddings Endpoint
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/// POST /v1/embeddings
|
|
public func handleEmbeddings(
|
|
input: EmbeddingsRequest.InputType
|
|
) async throws -> EmbeddingsResponse {
|
|
try await requestQueue.execute { [weak self] in
|
|
guard let self = self else {
|
|
throw ModelError.noModelLoaded
|
|
}
|
|
|
|
let tokenizer = try await self.modelManager.getTokenizer()
|
|
|
|
var embeddings: [EmbeddingData] = []
|
|
var totalTokens = 0
|
|
|
|
switch input {
|
|
case .string(let text):
|
|
let embedding = try await self.generateEmbedding(text: text)
|
|
let tokens = tokenizer.encode(text: text).count
|
|
totalTokens += tokens
|
|
embeddings.append(EmbeddingData(index: 0, embedding: embedding))
|
|
|
|
case .strings(let texts):
|
|
for (index, text) in texts.enumerated() {
|
|
let embedding = try await self.generateEmbedding(text: text)
|
|
let tokens = tokenizer.encode(text: text).count
|
|
totalTokens += tokens
|
|
embeddings.append(EmbeddingData(index: index, embedding: embedding))
|
|
}
|
|
|
|
case .tokens(let tokens):
|
|
let text = tokenizer.decode(tokens: tokens)
|
|
let embedding = try await self.generateEmbedding(text: text)
|
|
totalTokens += tokens.count
|
|
embeddings.append(EmbeddingData(index: 0, embedding: embedding))
|
|
|
|
case .tokensList(let tokensList):
|
|
for (index, tokens) in tokensList.enumerated() {
|
|
let text = tokenizer.decode(tokens: tokens)
|
|
let embedding = try await self.generateEmbedding(text: text)
|
|
totalTokens += tokens.count
|
|
embeddings.append(EmbeddingData(index: index, embedding: embedding))
|
|
}
|
|
}
|
|
|
|
return EmbeddingsResponse(
|
|
data: embeddings,
|
|
model: (await self.modelManager.getCurrentModel())?.id ?? "unknown",
|
|
usage: EmbeddingUsage(
|
|
prompt_tokens: totalTokens,
|
|
total_tokens: totalTokens
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Private Helpers
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
private func buildChatPrompt(messages: [ChatMessage], tokenizer: Tokenizer) -> String {
|
|
var prompt = ""
|
|
|
|
for message in messages {
|
|
let role = message.role == "assistant" ? "model" : message.role
|
|
prompt += "<|turn>\(role)\n\(message.content ?? "")<turn|>\n"
|
|
}
|
|
|
|
prompt += "<|turn>model\n"
|
|
return prompt
|
|
}
|
|
|
|
private func generateEmbedding(text: String) async throws -> [Float] {
|
|
let tokenizer = try await modelManager.getTokenizer()
|
|
let model = try await modelManager.getModel()
|
|
|
|
let tokens = tokenizer.encode(text: text)
|
|
|
|
var lastHidden: [Float] = []
|
|
for (position, tokenId) in tokens.enumerated() {
|
|
lastHidden = try model.forward(tokenId: tokenId, position: position)
|
|
}
|
|
|
|
return lastHidden
|
|
}
|
|
|
|
private func generateId(_ prefix: String) -> String {
|
|
let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "")
|
|
return "\(prefix)-\(uuid.prefix(29))"
|
|
}
|
|
}
|