Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

233 lines
8.0 KiB
Swift

import Foundation
import MLCEngineObjC
import os
class BackgroundWorker : Thread {
private var task: ()->Void;
public init(task: @escaping () -> Void) {
self.task = task
}
public override func main() {
self.task();
}
}
@available(iOS 14.0.0, *)
public class MLCEngine {
struct RequestState {
let request: ChatCompletionRequest
let continuation: AsyncStream<ChatCompletionStreamResponse>.Continuation
init(
request: ChatCompletionRequest,
continuation: AsyncStream<ChatCompletionStreamResponse>.Continuation
) {
self.request = request
self.continuation = continuation
}
}
// internal engine state
// that maintains logger and continuations
// we decouple it from MLCEngine
// and explicitly pass in jsonFFIEngine
// so there is no cyclic dependency
// when we capture things
actor EngineState {
public let logger = Logger()
private var requestStateMap = Dictionary<String, RequestState>()
// completion function
func chatCompletion(
jsonFFIEngine: JSONFFIEngine,
request: ChatCompletionRequest
) -> AsyncStream<ChatCompletionStreamResponse> {
let encoder = JSONEncoder()
let data = try! encoder.encode(request)
let jsonRequest = String(data: data, encoding: .utf8)!
// generate a UUID for the request
let requestID = UUID().uuidString
let stream = AsyncStream(ChatCompletionStreamResponse.self) { continuation in
continuation.onTermination = { termination in
if termination == .cancelled {
jsonFFIEngine.abort(requestID);
}
}
// store continuation map for further callbacks
self.requestStateMap[requestID] = RequestState(
request: request, continuation: continuation
)
// start invoking engine for completion
jsonFFIEngine.chatCompletion(jsonRequest, requestID: requestID)
}
return stream
}
func streamCallback(result: String?) {
var responses: [ChatCompletionStreamResponse] = []
let decoder = JSONDecoder()
do {
responses = try decoder.decode([ChatCompletionStreamResponse].self, from: result!.data(using: .utf8)!)
} catch let lastError {
logger.error("Swift json parsing error: error=\(lastError), jsonsrc=\(result!)")
}
// dispatch to right request ID
for res in responses {
if let requestState = self.requestStateMap[res.id] {
// final chunk always come with usage
if let finalUsage = res.usage {
if let include_usage = requestState.request.stream_options?.include_usage {
if include_usage {
requestState.continuation.yield(res)
}
}
requestState.continuation.finish()
self.requestStateMap.removeValue(forKey: res.id)
} else {
requestState.continuation.yield(res)
}
}
}
// Todo(mlc-team): check the last error in engine and report if there's any
}
}
public class Completions {
private let jsonFFIEngine: JSONFFIEngine
private let state: EngineState
init(jsonFFIEngine: JSONFFIEngine, state: EngineState) {
self.jsonFFIEngine = jsonFFIEngine
self.state = state
}
private func create(
request: ChatCompletionRequest
) async -> AsyncStream<ChatCompletionStreamResponse> {
return await state.chatCompletion(jsonFFIEngine: jsonFFIEngine, request: request)
}
// offer a direct convenient method to pass in messages
public func create(
messages: [ChatCompletionMessage],
model: Optional<String> = nil,
frequency_penalty: Optional<Float> = nil,
presence_penalty: Optional<Float> = nil,
logprobs: Bool = false,
top_logprobs: Int = 0,
logit_bias: Optional<[Int : Float]> = nil,
max_tokens: Optional<Int> = nil,
n: Int = 1,
seed: Optional<Int> = nil,
stop: Optional<[String]> = nil,
stream: Bool = true,
stream_options: Optional<StreamOptions> = nil,
temperature: Optional<Float> = nil,
top_p: Optional<Float> = nil,
tools: Optional<[ChatTool]> = nil,
user: Optional<String> = nil,
response_format: Optional<ResponseFormat> = nil
) async -> AsyncStream<ChatCompletionStreamResponse> {
if !stream {
state.logger.error("Only stream=true is supported in MLCSwift")
}
let request = ChatCompletionRequest(
messages: messages,
model: model,
frequency_penalty: frequency_penalty,
presence_penalty: presence_penalty,
logprobs: logprobs,
top_logprobs: top_logprobs,
logit_bias: logit_bias,
max_tokens: max_tokens,
n: n,
seed: seed,
stop: stop,
stream: stream,
stream_options: stream_options,
temperature: temperature,
top_p: top_p,
tools: tools,
user: user,
response_format: response_format
)
return await self.create(request: request)
}
}
public class Chat {
public let completions: Completions
init(jsonFFIEngine: JSONFFIEngine, state: EngineState) {
self.completions = Completions(
jsonFFIEngine: jsonFFIEngine,
state: state
)
}
}
private let state : EngineState;
private let jsonFFIEngine: JSONFFIEngine;
public let chat : Chat;
private var threads = Array<Thread>();
public init() {
let state_ = EngineState();
let jsonFFIEngine_ = JSONFFIEngine();
self.chat = Chat(jsonFFIEngine: jsonFFIEngine_, state: state_)
self.jsonFFIEngine = jsonFFIEngine_
self.state = state_
// note: closure do not capture self
jsonFFIEngine_.initBackgroundEngine {
[state_](result : String?) -> Void in
state_.streamCallback(result: result)
}
let backgroundWorker = BackgroundWorker { [jsonFFIEngine_] in
Thread.setThreadPriority(1)
jsonFFIEngine_.runBackgroundLoop()
}
let backgroundStreamBackWorker = BackgroundWorker {
[jsonFFIEngine_] in
jsonFFIEngine_.runBackgroundStreamBackLoop()
}
// set background worker to be high QoS so it gets higher p for gpu
backgroundWorker.qualityOfService = QualityOfService.userInteractive
threads.append(backgroundWorker)
threads.append(backgroundStreamBackWorker)
backgroundWorker.start()
backgroundStreamBackWorker.start()
}
deinit {
jsonFFIEngine.exitBackgroundLoop()
}
// The following functions do not have to be async for now
// But to be safe and consistent with chat.completions.create
// and for future API changes we keep them as async calls
public func reload(modelPath: String, modelLib: String) async {
let engineConfig = """
{
"model": "\(modelPath)",
"model_lib": "system://\(modelLib)",
"mode": "interactive"
}
"""
jsonFFIEngine.reload(engineConfig)
}
public func reset() async {
jsonFFIEngine.reset()
}
public func unload() async {
jsonFFIEngine.unload()
}
}