Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"react-native-audio-api": "^0.11.4",
"react-native-keyboard-controller": "1.20.7",
"react-native-nitro-mlx": "*",
"react-native-nitro-modules": "^0.35.4",
"react-native-nitro-modules": "^0.36.5",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.23.0",
Expand Down
4 changes: 3 additions & 1 deletion package/MLXReactNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ Pod::Spec.new do |s|

spm_dependency(s,
url: "https://github.com/Blaizzy/mlx-audio-swift.git",
requirement: {kind: "branch", branch: "main"},
# Pinned to a revision, not `main`: a clean prebuild re-resolves SPM, so tracking a
# branch lets upstream changes break the build with no local change. Bump deliberately.
requirement: {kind: "revision", revision: "4266f988d170a83017d1e82e2e4654602f277f1d"},
products: ["MLXAudioTTS", "MLXAudioSTT", "MLXAudioCore"]
)

Expand Down
4 changes: 2 additions & 2 deletions package/ios/Sources/HybridLLM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class HybridLLM: HybridLLMSpec {

func streamWithEvents(
prompt: String,
onEvent: @escaping (String) -> Void
onEvent: @escaping (StreamEventEnvelope) -> Void
) throws -> Promise<String> {
Promise.async { [core] in
try await core.streamWithEvents(prompt: prompt, onEvent: onEvent)
Expand Down Expand Up @@ -933,7 +933,7 @@ private final class HybridLLMCore {

func streamWithEvents(
prompt: String,
onEvent: @escaping (String) -> Void
onEvent: @escaping (StreamEventEnvelope) -> Void
) async throws -> String {
guard let container else {
throw LLMError.notLoaded
Expand Down
2 changes: 2 additions & 0 deletions package/ios/Sources/HybridTTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class HybridTTS: HybridTTSSpec {
case .audio(let audio):
let buffer = self.mlxArrayToArrayBuffer(audio)
onAudioChunk(buffer)
case .progress(let value):
options?.onProgress?(value)
case .token, .info:
break
}
Expand Down
137 changes: 46 additions & 91 deletions package/ios/Sources/StreamEventEmitter.swift
Original file line number Diff line number Diff line change
@@ -1,134 +1,89 @@
import Foundation
import NitroModules

/// Emits generation lifecycle events to JS as native `StreamEventEnvelope` structs.
///
/// Nitro cannot express a discriminated union of structs, so all event shapes share one
/// envelope discriminated by `StreamEventKind`; `llm.ts` maps it back to the `StreamEvent`
/// union. Fields not relevant to a given kind stay `nil`.
struct StreamEventEmitter {
private let callback: (String) -> Void
private let encoder = JSONEncoder()
private let callback: (StreamEventEnvelope) -> Void

init(callback: @escaping (String) -> Void) {
init(callback: @escaping (StreamEventEnvelope) -> Void) {
self.callback = callback
}

private func emit<T: Encodable>(_ event: T) {
guard let data = try? encoder.encode(event),
let json = String(data: data, encoding: .utf8) else { return }
callback(json)
}

private func timestamp() -> Double {
private func nowMs() -> Double {
Date().timeIntervalSince1970 * 1000
}

struct GenerationStartEvent: Encodable {
let type = "generation_start"
let timestamp: Double
}

struct TokenEvent: Encodable {
let type = "token"
let token: String
}

struct ThinkingStartEvent: Encodable {
let type = "thinking_start"
let timestamp: Double
}

struct ThinkingChunkEvent: Encodable {
let type = "thinking_chunk"
let chunk: String
}

struct ThinkingEndEvent: Encodable {
let type = "thinking_end"
let content: String
let timestamp: Double
}

struct ToolCallStartEvent: Encodable {
let type = "tool_call_start"
let id: String
let name: String
let arguments: String
}

struct ToolCallExecutingEvent: Encodable {
let type = "tool_call_executing"
let id: String
}

struct ToolCallCompletedEvent: Encodable {
let type = "tool_call_completed"
let id: String
let result: String
}

struct ToolCallFailedEvent: Encodable {
let type = "tool_call_failed"
let id: String
let error: String
}

struct StatsPayload: Encodable {
let tokenCount: Double
let tokensPerSecond: Double
let timeToFirstToken: Double
let totalTime: Double
let toolExecutionTime: Double
}

struct GenerationEndEvent: Encodable {
let type = "generation_end"
let content: String
let stats: StatsPayload
private func emit(
_ kind: StreamEventKind,
timestamp: Double? = nil,
token: String? = nil,
chunk: String? = nil,
content: String? = nil,
id: String? = nil,
name: String? = nil,
arguments: String? = nil,
result: String? = nil,
error: String? = nil,
stats: GenerationStats? = nil
) {
callback(
StreamEventEnvelope(
kind: kind,
timestamp: timestamp,
token: token,
chunk: chunk,
content: content,
id: id,
name: name,
arguments: arguments,
result: result,
error: error,
stats: stats
)
)
}

func emitGenerationStart() {
emit(GenerationStartEvent(timestamp: timestamp()))
emit(.generationStart, timestamp: nowMs())
}

func emitToken(_ token: String) {
emit(TokenEvent(token: token))
emit(.token, token: token)
}

func emitThinkingStart() {
emit(ThinkingStartEvent(timestamp: timestamp()))
emit(.thinkingStart, timestamp: nowMs())
}

func emitThinkingChunk(_ chunk: String) {
emit(ThinkingChunkEvent(chunk: chunk))
emit(.thinkingChunk, chunk: chunk)
}

func emitThinkingEnd(_ content: String) {
emit(ThinkingEndEvent(content: content, timestamp: timestamp()))
emit(.thinkingEnd, timestamp: nowMs(), content: content)
}

func emitToolCallStart(id: String, name: String, arguments: String) {
emit(ToolCallStartEvent(id: id, name: name, arguments: arguments))
emit(.toolCallStart, id: id, name: name, arguments: arguments)
}

func emitToolCallExecuting(id: String) {
emit(ToolCallExecutingEvent(id: id))
emit(.toolCallExecuting, id: id)
}

func emitToolCallCompleted(id: String, result: String) {
emit(ToolCallCompletedEvent(id: id, result: result))
emit(.toolCallCompleted, id: id, result: result)
}

func emitToolCallFailed(id: String, error: String) {
emit(ToolCallFailedEvent(id: id, error: error))
emit(.toolCallFailed, id: id, error: error)
}

func emitGenerationEnd(content: String, stats: GenerationStats) {
emit(GenerationEndEvent(
content: content,
stats: StatsPayload(
tokenCount: stats.tokenCount,
tokensPerSecond: stats.tokensPerSecond,
timeToFirstToken: stats.timeToFirstToken,
totalTime: stats.totalTime,
toolExecutionTime: stats.toolExecutionTime
)
))
emit(.generationEnd, content: content, stats: stats)
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading