diff --git a/bun.lock b/bun.lock index 02bb787..a658872 100644 --- a/bun.lock +++ b/bun.lock @@ -32,7 +32,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", @@ -46,14 +46,14 @@ }, "package": { "name": "react-native-nitro-mlx", - "version": "0.4.0", + "version": "0.5.0", "dependencies": { "zod": "^4.3.5", }, "devDependencies": { "@release-it/bumper": "^7.0.5", "@release-it/conventional-changelog": "^10.0.1", - "nitrogen": "^0.35.4", + "nitrogen": "^0.36.5", "react-native-builder-bob": "^0.40.13", "release-it": "^19.0.4", }, @@ -600,7 +600,7 @@ "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], - "@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="], + "@ts-morph/common": ["@ts-morph/common@0.29.0", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -1330,7 +1330,7 @@ "nitro-mlx-example": ["nitro-mlx-example@workspace:example"], - "nitrogen": ["nitrogen@0.35.4", "", { "dependencies": { "chalk": "^5.3.0", "react-native-nitro-modules": "^0.35.4", "ts-morph": "^27.0.0", "yargs": "^18.0.0", "zod": "^4.0.5" }, "bin": { "nitrogen": "lib/index.js" } }, "sha512-mGw76rMS+c5wyxx0VHVpJU5jluYBYffRkGGtdRtS+0jNZmo2KwN66YjRDYYtPpxVPw6pAoGLTwNvYvTE0ly3ww=="], + "nitrogen": ["nitrogen@0.36.5", "", { "dependencies": { "chalk": "^5.3.0", "react-native-nitro-modules": "^0.36.5", "ts-morph": "^28.0.0", "yargs": "^18.0.0", "zod": "^4.4.3" }, "bin": { "nitrogen": "lib/index.js" } }, "sha512-PvlHrBVaoKEaD6CCUCG7om8MTvLeh4ZSDxHoRx2YiEWUVXPXrRN9NwMn1gjie9Bd91jHA3dYMF7no9Eme4txMA=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], @@ -1486,7 +1486,7 @@ "react-native-nitro-mlx": ["react-native-nitro-mlx@workspace:package"], - "react-native-nitro-modules": ["react-native-nitro-modules@0.35.4", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4qZa+1kgR/sPRNZv+UShxyArEPpovWxw76Dfd/DtCVtkQ92wOOxGIzdYvndprabd+t+r8zNYgYEPYE74gzkuVQ=="], + "react-native-nitro-modules": ["react-native-nitro-modules@0.36.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-SJby84+hovD70JmQ1fq+56tw3oapJeChehUqXA4Gqjm+ktJhMAcj8LGwoAjStSOUwRaK7czbzBPB0Widj+EDoQ=="], "react-native-reanimated": ["react-native-reanimated@4.2.1", "", { "dependencies": { "react-native-is-edge-to-edge": "1.2.1", "semver": "7.7.3" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" } }, "sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg=="], @@ -1666,7 +1666,7 @@ "toqr": ["toqr@0.1.1", "", {}, "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA=="], - "ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="], + "ts-morph": ["ts-morph@28.0.0", "", { "dependencies": { "@ts-morph/common": "~0.29.0", "code-block-writer": "^13.0.3" } }, "sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1976,6 +1976,8 @@ "nitrogen/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + "nitrogen/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "normalize-package-data/hosted-git-info": ["hosted-git-info@8.1.0", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw=="], "normalize-package-data/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], diff --git a/example/package.json b/example/package.json index ef9a7ae..1edaf40 100644 --- a/example/package.json +++ b/example/package.json @@ -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", diff --git a/package/MLXReactNative.podspec b/package/MLXReactNative.podspec index 1720881..cf04477 100644 --- a/package/MLXReactNative.podspec +++ b/package/MLXReactNative.podspec @@ -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"] ) diff --git a/package/ios/Sources/HybridLLM.swift b/package/ios/Sources/HybridLLM.swift index d4d4036..403c6a8 100644 --- a/package/ios/Sources/HybridLLM.swift +++ b/package/ios/Sources/HybridLLM.swift @@ -123,7 +123,7 @@ class HybridLLM: HybridLLMSpec { func streamWithEvents( prompt: String, - onEvent: @escaping (String) -> Void + onEvent: @escaping (StreamEventEnvelope) -> Void ) throws -> Promise { Promise.async { [core] in try await core.streamWithEvents(prompt: prompt, onEvent: onEvent) @@ -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 diff --git a/package/ios/Sources/HybridTTS.swift b/package/ios/Sources/HybridTTS.swift index 4726237..cd31efa 100644 --- a/package/ios/Sources/HybridTTS.swift +++ b/package/ios/Sources/HybridTTS.swift @@ -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 } diff --git a/package/ios/Sources/StreamEventEmitter.swift b/package/ios/Sources/StreamEventEmitter.swift index 3f80a1b..1ebf4c1 100644 --- a/package/ios/Sources/StreamEventEmitter.swift +++ b/package/ios/Sources/StreamEventEmitter.swift @@ -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(_ 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) } } diff --git a/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Bridge.cpp b/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Bridge.cpp index dc1c94a..f382dcb 100644 --- a/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Bridge.cpp +++ b/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Bridge.cpp @@ -115,6 +115,14 @@ namespace margelo::nitro::mlxreactnative::bridge::swift { }; } + // pragma MARK: std::function + Func_void_StreamEventEnvelope create_Func_void_StreamEventEnvelope(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = MLXReactNative::Func_void_StreamEventEnvelope::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const StreamEventEnvelope& event) mutable -> void { + swiftClosure.call(event); + }; + } + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_HybridLLMSpec_(void* NON_NULL swiftUnsafePointer) noexcept { MLXReactNative::HybridLLMSpec_cxx swiftPart = MLXReactNative::HybridLLMSpec_cxx::fromUnsafe(swiftUnsafePointer); diff --git a/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Bridge.hpp b/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Bridge.hpp index dbdf605..943ccc3 100644 --- a/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Bridge.hpp +++ b/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Bridge.hpp @@ -34,6 +34,10 @@ namespace margelo::nitro::mlxreactnative { struct LLMLoadOptions; } namespace margelo::nitro::mlxreactnative { struct LLMMessage; } // Forward declaration of `STTLoadOptions` to properly resolve imports. namespace margelo::nitro::mlxreactnative { struct STTLoadOptions; } +// Forward declaration of `StreamEventEnvelope` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { struct StreamEventEnvelope; } +// Forward declaration of `StreamEventKind` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { enum class StreamEventKind; } // Forward declaration of `TTSGenerateOptions` to properly resolve imports. namespace margelo::nitro::mlxreactnative { struct TTSGenerateOptions; } // Forward declaration of `TTSLoadOptions` to properly resolve imports. @@ -68,6 +72,8 @@ namespace MLXReactNative { class HybridTTSSpec_cxx; } #include "LLMLoadOptions.hpp" #include "LLMMessage.hpp" #include "STTLoadOptions.hpp" +#include "StreamEventEnvelope.hpp" +#include "StreamEventKind.hpp" #include "TTSGenerateOptions.hpp" #include "TTSLoadOptions.hpp" #include "ToolDefinition.hpp" @@ -637,6 +643,58 @@ namespace margelo::nitro::mlxreactnative::bridge::swift { return optional.value(); } + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_std__string_ = std::optional; + inline std::optional create_std__optional_std__string_(const std::string& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_std__string_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline std::string get_std__optional_std__string_(const std::optional& optional) noexcept { + return optional.value(); + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_GenerationStats_ = std::optional; + inline std::optional create_std__optional_GenerationStats_(const GenerationStats& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_GenerationStats_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline GenerationStats get_std__optional_GenerationStats_(const std::optional& optional) noexcept { + return optional.value(); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_StreamEventEnvelope = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_StreamEventEnvelope_Wrapper final { + public: + explicit Func_void_StreamEventEnvelope_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(StreamEventEnvelope event) const noexcept { + _function->operator()(event); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_StreamEventEnvelope create_Func_void_StreamEventEnvelope(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_StreamEventEnvelope_Wrapper wrap_Func_void_StreamEventEnvelope(Func_void_StreamEventEnvelope value) noexcept { + return Func_void_StreamEventEnvelope_Wrapper(std::move(value)); + } + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -816,21 +874,6 @@ namespace margelo::nitro::mlxreactnative::bridge::swift { return optional.value(); } - // pragma MARK: std::optional - /** - * Specialized version of `std::optional`. - */ - using std__optional_std__string_ = std::optional; - inline std::optional create_std__optional_std__string_(const std::string& value) noexcept { - return std::optional(value); - } - inline bool has_value_std__optional_std__string_(const std::optional& optional) noexcept { - return optional.has_value(); - } - inline std::string get_std__optional_std__string_(const std::optional& optional) noexcept { - return optional.value(); - } - // pragma MARK: std::optional /** * Specialized version of `std::optional`. diff --git a/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Umbrella.hpp b/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Umbrella.hpp index a973ce8..6b203e1 100644 --- a/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Umbrella.hpp +++ b/package/nitrogen/generated/ios/MLXReactNative-Swift-Cxx-Umbrella.hpp @@ -32,6 +32,10 @@ namespace margelo::nitro::mlxreactnative { struct LLMLoadOptions; } namespace margelo::nitro::mlxreactnative { struct LLMMessage; } // Forward declaration of `STTLoadOptions` to properly resolve imports. namespace margelo::nitro::mlxreactnative { struct STTLoadOptions; } +// Forward declaration of `StreamEventEnvelope` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { struct StreamEventEnvelope; } +// Forward declaration of `StreamEventKind` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { enum class StreamEventKind; } // Forward declaration of `TTSGenerateOptions` to properly resolve imports. namespace margelo::nitro::mlxreactnative { struct TTSGenerateOptions; } // Forward declaration of `TTSLoadOptions` to properly resolve imports. @@ -54,6 +58,8 @@ namespace margelo::nitro::mlxreactnative { struct ToolParameter; } #include "LLMLoadOptions.hpp" #include "LLMMessage.hpp" #include "STTLoadOptions.hpp" +#include "StreamEventEnvelope.hpp" +#include "StreamEventKind.hpp" #include "TTSGenerateOptions.hpp" #include "TTSLoadOptions.hpp" #include "ToolDefinition.hpp" diff --git a/package/nitrogen/generated/ios/c++/HybridLLMSpecSwift.hpp b/package/nitrogen/generated/ios/c++/HybridLLMSpecSwift.hpp index e391479..8ff7822 100644 --- a/package/nitrogen/generated/ios/c++/HybridLLMSpecSwift.hpp +++ b/package/nitrogen/generated/ios/c++/HybridLLMSpecSwift.hpp @@ -24,6 +24,10 @@ namespace margelo::nitro::mlxreactnative { struct ToolParameter; } namespace margelo::nitro::mlxreactnative { struct LLMGenerationConfig; } // Forward declaration of `LLMContextConfig` to properly resolve imports. namespace margelo::nitro::mlxreactnative { struct LLMContextConfig; } +// Forward declaration of `StreamEventEnvelope` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { struct StreamEventEnvelope; } +// Forward declaration of `StreamEventKind` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { enum class StreamEventKind; } // Forward declaration of `GenerationStats` to properly resolve imports. namespace margelo::nitro::mlxreactnative { struct GenerationStats; } @@ -39,6 +43,8 @@ namespace margelo::nitro::mlxreactnative { struct GenerationStats; } #include #include "LLMGenerationConfig.hpp" #include "LLMContextConfig.hpp" +#include "StreamEventEnvelope.hpp" +#include "StreamEventKind.hpp" #include "GenerationStats.hpp" #include "MLXReactNative-Swift-Cxx-Umbrella.hpp" @@ -137,7 +143,7 @@ namespace margelo::nitro::mlxreactnative { auto __value = std::move(__result.value()); return __value; } - inline std::shared_ptr> streamWithEvents(const std::string& prompt, const std::function& onEvent) override { + inline std::shared_ptr> streamWithEvents(const std::string& prompt, const std::function& onEvent) override { auto __result = _swiftPart.streamWithEvents(prompt, onEvent); if (__result.hasError()) [[unlikely]] { std::rethrow_exception(__result.error()); diff --git a/package/nitrogen/generated/ios/swift/Func_void_StreamEventEnvelope.swift b/package/nitrogen/generated/ios/swift/Func_void_StreamEventEnvelope.swift new file mode 100644 index 0000000..56ecb08 --- /dev/null +++ b/package/nitrogen/generated/ios/swift/Func_void_StreamEventEnvelope.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_StreamEventEnvelope.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ event: StreamEventEnvelope) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_StreamEventEnvelope { + public typealias bridge = margelo.nitro.mlxreactnative.bridge.swift + + private let closure: (_ event: StreamEventEnvelope) -> Void + + public init(_ closure: @escaping (_ event: StreamEventEnvelope) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(event: StreamEventEnvelope) -> Void { + self.closure(event) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_StreamEventEnvelope`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_StreamEventEnvelope { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/package/nitrogen/generated/ios/swift/HybridLLMSpec.swift b/package/nitrogen/generated/ios/swift/HybridLLMSpec.swift index 68c587a..a21071e 100644 --- a/package/nitrogen/generated/ios/swift/HybridLLMSpec.swift +++ b/package/nitrogen/generated/ios/swift/HybridLLMSpec.swift @@ -20,7 +20,7 @@ public protocol HybridLLMSpec_protocol: HybridObject { func load(modelId: String, options: LLMLoadOptions?) throws -> Promise func generate(prompt: String) throws -> Promise func stream(prompt: String, onToken: @escaping (_ token: String) -> Void, onToolCall: ((_ toolName: String, _ args: String) -> Void)?) throws -> Promise - func streamWithEvents(prompt: String, onEvent: @escaping (_ eventJson: String) -> Void) throws -> Promise + func streamWithEvents(prompt: String, onEvent: @escaping (_ event: StreamEventEnvelope) -> Void) throws -> Promise func stop() throws -> Void func unload() throws -> Void func getLastGenerationStats() throws -> GenerationStats diff --git a/package/nitrogen/generated/ios/swift/HybridLLMSpec_cxx.swift b/package/nitrogen/generated/ios/swift/HybridLLMSpec_cxx.swift index 1172ec0..0d6dcf9 100644 --- a/package/nitrogen/generated/ios/swift/HybridLLMSpec_cxx.swift +++ b/package/nitrogen/generated/ios/swift/HybridLLMSpec_cxx.swift @@ -240,12 +240,12 @@ open class HybridLLMSpec_cxx { } @inline(__always) - public final func streamWithEvents(prompt: std.string, onEvent: bridge.Func_void_std__string) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + public final func streamWithEvents(prompt: std.string, onEvent: bridge.Func_void_StreamEventEnvelope) -> bridge.Result_std__shared_ptr_Promise_std__string___ { do { - let __result = try self.__implementation.streamWithEvents(prompt: String(prompt), onEvent: { () -> (String) -> Void in - let __wrappedFunction = bridge.wrap_Func_void_std__string(onEvent) - return { (__eventJson: String) -> Void in - __wrappedFunction.call(std.string(__eventJson)) + let __result = try self.__implementation.streamWithEvents(prompt: String(prompt), onEvent: { () -> (StreamEventEnvelope) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_StreamEventEnvelope(onEvent) + return { (__event: StreamEventEnvelope) -> Void in + __wrappedFunction.call(__event) } }()) let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in diff --git a/package/nitrogen/generated/ios/swift/StreamEventEnvelope.swift b/package/nitrogen/generated/ios/swift/StreamEventEnvelope.swift new file mode 100644 index 0000000..e8f5bde --- /dev/null +++ b/package/nitrogen/generated/ios/swift/StreamEventEnvelope.swift @@ -0,0 +1,202 @@ +/// +/// StreamEventEnvelope.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `StreamEventEnvelope`, backed by a C++ struct. + */ +public typealias StreamEventEnvelope = margelo.nitro.mlxreactnative.StreamEventEnvelope + +public extension StreamEventEnvelope { + private typealias bridge = margelo.nitro.mlxreactnative.bridge.swift + + /** + * Create a new instance of `StreamEventEnvelope`. + */ + init(kind: StreamEventKind, timestamp: Double?, token: String?, chunk: String?, content: String?, id: String?, name: String?, arguments: String?, result: String?, error: String?, stats: GenerationStats?) { + self.init(kind, { () -> bridge.std__optional_double_ in + if let __unwrappedValue = timestamp { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = token { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = chunk { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = content { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = id { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = name { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = arguments { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = result { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = error { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_GenerationStats_ in + if let __unwrappedValue = stats { + return bridge.create_std__optional_GenerationStats_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + @inline(__always) + var kind: StreamEventKind { + return self.__kind + } + + @inline(__always) + var timestamp: Double? { + return { () -> Double? in + if bridge.has_value_std__optional_double_(self.__timestamp) { + let __unwrapped = bridge.get_std__optional_double_(self.__timestamp) + return __unwrapped + } else { + return nil + } + }() + } + + @inline(__always) + var token: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__token) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__token) + return String(__unwrapped) + } else { + return nil + } + }() + } + + @inline(__always) + var chunk: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__chunk) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__chunk) + return String(__unwrapped) + } else { + return nil + } + }() + } + + @inline(__always) + var content: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__content) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__content) + return String(__unwrapped) + } else { + return nil + } + }() + } + + @inline(__always) + var id: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__id) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__id) + return String(__unwrapped) + } else { + return nil + } + }() + } + + @inline(__always) + var name: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__name) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__name) + return String(__unwrapped) + } else { + return nil + } + }() + } + + @inline(__always) + var arguments: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__arguments) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__arguments) + return String(__unwrapped) + } else { + return nil + } + }() + } + + @inline(__always) + var result: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__result) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__result) + return String(__unwrapped) + } else { + return nil + } + }() + } + + @inline(__always) + var error: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__error) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__error) + return String(__unwrapped) + } else { + return nil + } + }() + } + + @inline(__always) + var stats: GenerationStats? { + return self.__stats.value + } +} diff --git a/package/nitrogen/generated/ios/swift/StreamEventKind.swift b/package/nitrogen/generated/ios/swift/StreamEventKind.swift new file mode 100644 index 0000000..abcf905 --- /dev/null +++ b/package/nitrogen/generated/ios/swift/StreamEventKind.swift @@ -0,0 +1,72 @@ +/// +/// StreamEventKind.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `StreamEventKind`, backed by a C++ enum. + */ +public typealias StreamEventKind = margelo.nitro.mlxreactnative.StreamEventKind + +public extension StreamEventKind { + /** + * Get a StreamEventKind for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "generation_start": + self = .generationStart + case "token": + self = .token + case "thinking_start": + self = .thinkingStart + case "thinking_chunk": + self = .thinkingChunk + case "thinking_end": + self = .thinkingEnd + case "tool_call_start": + self = .toolCallStart + case "tool_call_executing": + self = .toolCallExecuting + case "tool_call_completed": + self = .toolCallCompleted + case "tool_call_failed": + self = .toolCallFailed + case "generation_end": + self = .generationEnd + default: + return nil + } + } + + /** + * Get the String value this StreamEventKind represents. + */ + var stringValue: String { + switch self { + case .generationStart: + return "generation_start" + case .token: + return "token" + case .thinkingStart: + return "thinking_start" + case .thinkingChunk: + return "thinking_chunk" + case .thinkingEnd: + return "thinking_end" + case .toolCallStart: + return "tool_call_start" + case .toolCallExecuting: + return "tool_call_executing" + case .toolCallCompleted: + return "tool_call_completed" + case .toolCallFailed: + return "tool_call_failed" + case .generationEnd: + return "generation_end" + } + } +} diff --git a/package/nitrogen/generated/ios/swift/TTSGenerateOptions.swift b/package/nitrogen/generated/ios/swift/TTSGenerateOptions.swift index 318c36d..be8a800 100644 --- a/package/nitrogen/generated/ios/swift/TTSGenerateOptions.swift +++ b/package/nitrogen/generated/ios/swift/TTSGenerateOptions.swift @@ -18,7 +18,7 @@ public extension TTSGenerateOptions { /** * Create a new instance of `TTSGenerateOptions`. */ - init(voice: String?, speed: Double?) { + init(voice: String?, speed: Double?, onProgress: ((_ progress: Double) -> Void)?) { self.init({ () -> bridge.std__optional_std__string_ in if let __unwrappedValue = voice { return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) @@ -31,6 +31,15 @@ public extension TTSGenerateOptions { } else { return .init() } + }(), { () -> bridge.std__optional_std__function_void_double____progress______ in + if let __unwrappedValue = onProgress { + return bridge.create_std__optional_std__function_void_double____progress______({ () -> bridge.Func_void_double in + let __closureWrapper = Func_void_double(__unwrappedValue) + return bridge.create_Func_void_double(__closureWrapper.toUnsafe()) + }()) + } else { + return .init() + } }()) } @@ -57,4 +66,21 @@ public extension TTSGenerateOptions { } }() } + + @inline(__always) + var onProgress: ((_ progress: Double) -> Void)? { + return { () -> ((_ progress: Double) -> Void)? in + if bridge.has_value_std__optional_std__function_void_double____progress______(self.__onProgress) { + let __unwrapped = bridge.get_std__optional_std__function_void_double____progress______(self.__onProgress) + return { () -> (Double) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_double(__unwrapped) + return { (__progress: Double) -> Void in + __wrappedFunction.call(__progress) + } + }() + } else { + return nil + } + }() + } } diff --git a/package/nitrogen/generated/shared/c++/HybridLLMSpec.hpp b/package/nitrogen/generated/shared/c++/HybridLLMSpec.hpp index f4fc177..266db26 100644 --- a/package/nitrogen/generated/shared/c++/HybridLLMSpec.hpp +++ b/package/nitrogen/generated/shared/c++/HybridLLMSpec.hpp @@ -15,6 +15,8 @@ // Forward declaration of `LLMLoadOptions` to properly resolve imports. namespace margelo::nitro::mlxreactnative { struct LLMLoadOptions; } +// Forward declaration of `StreamEventEnvelope` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { struct StreamEventEnvelope; } // Forward declaration of `GenerationStats` to properly resolve imports. namespace margelo::nitro::mlxreactnative { struct GenerationStats; } // Forward declaration of `LLMMessage` to properly resolve imports. @@ -25,6 +27,7 @@ namespace margelo::nitro::mlxreactnative { struct LLMMessage; } #include "LLMLoadOptions.hpp" #include #include +#include "StreamEventEnvelope.hpp" #include "GenerationStats.hpp" #include "LLMMessage.hpp" #include @@ -69,7 +72,7 @@ namespace margelo::nitro::mlxreactnative { virtual std::shared_ptr> load(const std::string& modelId, const std::optional& options) = 0; virtual std::shared_ptr> generate(const std::string& prompt) = 0; virtual std::shared_ptr> stream(const std::string& prompt, const std::function& onToken, const std::optional>& onToolCall) = 0; - virtual std::shared_ptr> streamWithEvents(const std::string& prompt, const std::function& onEvent) = 0; + virtual std::shared_ptr> streamWithEvents(const std::string& prompt, const std::function& onEvent) = 0; virtual void stop() = 0; virtual void unload() = 0; virtual GenerationStats getLastGenerationStats() = 0; diff --git a/package/nitrogen/generated/shared/c++/StreamEventEnvelope.hpp b/package/nitrogen/generated/shared/c++/StreamEventEnvelope.hpp new file mode 100644 index 0000000..d3671c5 --- /dev/null +++ b/package/nitrogen/generated/shared/c++/StreamEventEnvelope.hpp @@ -0,0 +1,129 @@ +/// +/// StreamEventEnvelope.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `StreamEventKind` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { enum class StreamEventKind; } +// Forward declaration of `GenerationStats` to properly resolve imports. +namespace margelo::nitro::mlxreactnative { struct GenerationStats; } + +#include "StreamEventKind.hpp" +#include +#include +#include "GenerationStats.hpp" + +namespace margelo::nitro::mlxreactnative { + + /** + * A struct which can be represented as a JavaScript object (StreamEventEnvelope). + */ + struct StreamEventEnvelope final { + public: + StreamEventKind kind SWIFT_PRIVATE; + std::optional timestamp SWIFT_PRIVATE; + std::optional token SWIFT_PRIVATE; + std::optional chunk SWIFT_PRIVATE; + std::optional content SWIFT_PRIVATE; + std::optional id SWIFT_PRIVATE; + std::optional name SWIFT_PRIVATE; + std::optional arguments SWIFT_PRIVATE; + std::optional result SWIFT_PRIVATE; + std::optional error SWIFT_PRIVATE; + std::optional stats SWIFT_PRIVATE; + + public: + StreamEventEnvelope() = default; + explicit StreamEventEnvelope(StreamEventKind kind, std::optional timestamp, std::optional token, std::optional chunk, std::optional content, std::optional id, std::optional name, std::optional arguments, std::optional result, std::optional error, std::optional stats): kind(kind), timestamp(timestamp), token(token), chunk(chunk), content(content), id(id), name(name), arguments(arguments), result(result), error(error), stats(stats) {} + + public: + friend bool operator==(const StreamEventEnvelope& lhs, const StreamEventEnvelope& rhs) = default; + }; + +} // namespace margelo::nitro::mlxreactnative + +namespace margelo::nitro { + + // C++ StreamEventEnvelope <> JS StreamEventEnvelope (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::mlxreactnative::StreamEventEnvelope fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::mlxreactnative::StreamEventEnvelope( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "kind"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestamp"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "token"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chunk"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "content"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "arguments"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "result"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "error"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "stats"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::mlxreactnative::StreamEventEnvelope& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "kind"), JSIConverter::toJSI(runtime, arg.kind)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "timestamp"), JSIConverter>::toJSI(runtime, arg.timestamp)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "token"), JSIConverter>::toJSI(runtime, arg.token)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "chunk"), JSIConverter>::toJSI(runtime, arg.chunk)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "content"), JSIConverter>::toJSI(runtime, arg.content)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "id"), JSIConverter>::toJSI(runtime, arg.id)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "name"), JSIConverter>::toJSI(runtime, arg.name)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "arguments"), JSIConverter>::toJSI(runtime, arg.arguments)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "result"), JSIConverter>::toJSI(runtime, arg.result)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "error"), JSIConverter>::toJSI(runtime, arg.error)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "stats"), JSIConverter>::toJSI(runtime, arg.stats)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "kind")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestamp")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "token")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chunk")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "content")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "arguments")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "result")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "error")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "stats")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/package/nitrogen/generated/shared/c++/StreamEventKind.hpp b/package/nitrogen/generated/shared/c++/StreamEventKind.hpp new file mode 100644 index 0000000..69e0da5 --- /dev/null +++ b/package/nitrogen/generated/shared/c++/StreamEventKind.hpp @@ -0,0 +1,108 @@ +/// +/// StreamEventKind.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::mlxreactnative { + + /** + * An enum which can be represented as a JavaScript union (StreamEventKind). + */ + enum class StreamEventKind { + GENERATION_START SWIFT_NAME(generationStart) = 0, + TOKEN SWIFT_NAME(token) = 1, + THINKING_START SWIFT_NAME(thinkingStart) = 2, + THINKING_CHUNK SWIFT_NAME(thinkingChunk) = 3, + THINKING_END SWIFT_NAME(thinkingEnd) = 4, + TOOL_CALL_START SWIFT_NAME(toolCallStart) = 5, + TOOL_CALL_EXECUTING SWIFT_NAME(toolCallExecuting) = 6, + TOOL_CALL_COMPLETED SWIFT_NAME(toolCallCompleted) = 7, + TOOL_CALL_FAILED SWIFT_NAME(toolCallFailed) = 8, + GENERATION_END SWIFT_NAME(generationEnd) = 9, + } CLOSED_ENUM; + +} // namespace margelo::nitro::mlxreactnative + +namespace margelo::nitro { + + // C++ StreamEventKind <> JS StreamEventKind (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::mlxreactnative::StreamEventKind fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("generation_start"): return margelo::nitro::mlxreactnative::StreamEventKind::GENERATION_START; + case hashString("token"): return margelo::nitro::mlxreactnative::StreamEventKind::TOKEN; + case hashString("thinking_start"): return margelo::nitro::mlxreactnative::StreamEventKind::THINKING_START; + case hashString("thinking_chunk"): return margelo::nitro::mlxreactnative::StreamEventKind::THINKING_CHUNK; + case hashString("thinking_end"): return margelo::nitro::mlxreactnative::StreamEventKind::THINKING_END; + case hashString("tool_call_start"): return margelo::nitro::mlxreactnative::StreamEventKind::TOOL_CALL_START; + case hashString("tool_call_executing"): return margelo::nitro::mlxreactnative::StreamEventKind::TOOL_CALL_EXECUTING; + case hashString("tool_call_completed"): return margelo::nitro::mlxreactnative::StreamEventKind::TOOL_CALL_COMPLETED; + case hashString("tool_call_failed"): return margelo::nitro::mlxreactnative::StreamEventKind::TOOL_CALL_FAILED; + case hashString("generation_end"): return margelo::nitro::mlxreactnative::StreamEventKind::GENERATION_END; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum StreamEventKind - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::mlxreactnative::StreamEventKind arg) { + switch (arg) { + case margelo::nitro::mlxreactnative::StreamEventKind::GENERATION_START: return JSIConverter::toJSI(runtime, "generation_start"); + case margelo::nitro::mlxreactnative::StreamEventKind::TOKEN: return JSIConverter::toJSI(runtime, "token"); + case margelo::nitro::mlxreactnative::StreamEventKind::THINKING_START: return JSIConverter::toJSI(runtime, "thinking_start"); + case margelo::nitro::mlxreactnative::StreamEventKind::THINKING_CHUNK: return JSIConverter::toJSI(runtime, "thinking_chunk"); + case margelo::nitro::mlxreactnative::StreamEventKind::THINKING_END: return JSIConverter::toJSI(runtime, "thinking_end"); + case margelo::nitro::mlxreactnative::StreamEventKind::TOOL_CALL_START: return JSIConverter::toJSI(runtime, "tool_call_start"); + case margelo::nitro::mlxreactnative::StreamEventKind::TOOL_CALL_EXECUTING: return JSIConverter::toJSI(runtime, "tool_call_executing"); + case margelo::nitro::mlxreactnative::StreamEventKind::TOOL_CALL_COMPLETED: return JSIConverter::toJSI(runtime, "tool_call_completed"); + case margelo::nitro::mlxreactnative::StreamEventKind::TOOL_CALL_FAILED: return JSIConverter::toJSI(runtime, "tool_call_failed"); + case margelo::nitro::mlxreactnative::StreamEventKind::GENERATION_END: return JSIConverter::toJSI(runtime, "generation_end"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert StreamEventKind to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("generation_start"): + case hashString("token"): + case hashString("thinking_start"): + case hashString("thinking_chunk"): + case hashString("thinking_end"): + case hashString("tool_call_start"): + case hashString("tool_call_executing"): + case hashString("tool_call_completed"): + case hashString("tool_call_failed"): + case hashString("generation_end"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/package/nitrogen/generated/shared/c++/TTSGenerateOptions.hpp b/package/nitrogen/generated/shared/c++/TTSGenerateOptions.hpp index f16aaa5..65d34c9 100644 --- a/package/nitrogen/generated/shared/c++/TTSGenerateOptions.hpp +++ b/package/nitrogen/generated/shared/c++/TTSGenerateOptions.hpp @@ -32,6 +32,7 @@ #include #include +#include namespace margelo::nitro::mlxreactnative { @@ -42,13 +43,14 @@ namespace margelo::nitro::mlxreactnative { public: std::optional voice SWIFT_PRIVATE; std::optional speed SWIFT_PRIVATE; + std::optional> onProgress SWIFT_PRIVATE; public: TTSGenerateOptions() = default; - explicit TTSGenerateOptions(std::optional voice, std::optional speed): voice(voice), speed(speed) {} + explicit TTSGenerateOptions(std::optional voice, std::optional speed, std::optional> onProgress): voice(voice), speed(speed), onProgress(onProgress) {} public: - friend bool operator==(const TTSGenerateOptions& lhs, const TTSGenerateOptions& rhs) = default; + // TTSGenerateOptions is not equatable because these properties are not equatable: onProgress }; } // namespace margelo::nitro::mlxreactnative @@ -62,13 +64,15 @@ namespace margelo::nitro { jsi::Object obj = arg.asObject(runtime); return margelo::nitro::mlxreactnative::TTSGenerateOptions( JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "voice"))), - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "speed"))) + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "speed"))), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "onProgress"))) ); } static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::mlxreactnative::TTSGenerateOptions& arg) { jsi::Object obj(runtime); obj.setProperty(runtime, PropNameIDCache::get(runtime, "voice"), JSIConverter>::toJSI(runtime, arg.voice)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "speed"), JSIConverter>::toJSI(runtime, arg.speed)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "onProgress"), JSIConverter>>::toJSI(runtime, arg.onProgress)); return obj; } static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { @@ -81,6 +85,7 @@ namespace margelo::nitro { } if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "voice")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "speed")))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "onProgress")))) return false; return true; } }; diff --git a/package/package.json b/package/package.json index 3f8c16f..a492c56 100644 --- a/package/package.json +++ b/package/package.json @@ -56,7 +56,7 @@ "devDependencies": { "@release-it/bumper": "^7.0.5", "@release-it/conventional-changelog": "^10.0.1", - "nitrogen": "^0.35.4", + "nitrogen": "^0.36.5", "react-native-builder-bob": "^0.40.13", "release-it": "^19.0.4" }, diff --git a/package/src/llm.ts b/package/src/llm.ts index d6dcd22..3aa2f90 100644 --- a/package/src/llm.ts +++ b/package/src/llm.ts @@ -3,6 +3,7 @@ import { assertBoolean, assertNonEmptyString, createSafeCallback, + mapStreamEventEnvelope, safeJsonParse, validateLLMLoadOptions, } from './runtime' @@ -11,6 +12,7 @@ import type { LLMLoadOptions, LLM as LLMSpec, StreamEvent, + StreamEventEnvelope, } from './specs/LLM.nitro' export type EventCallback = (event: StreamEvent) => void @@ -158,8 +160,8 @@ export const LLM = { return getInstance().streamWithEvents( assertNonEmptyString(prompt, 'LLM prompt'), - (eventJson: string) => { - const event = safeJsonParse(eventJson, null) + (envelope: StreamEventEnvelope) => { + const event = mapStreamEventEnvelope(envelope) if (event) { safeOnEvent?.(event) } diff --git a/package/src/runtime.test.ts b/package/src/runtime.test.ts index 83f2aa6..f3de526 100644 --- a/package/src/runtime.test.ts +++ b/package/src/runtime.test.ts @@ -4,6 +4,7 @@ import { assertBoolean, assertNonEmptyString, createSafeCallback, + mapStreamEventEnvelope, safeJsonParse, validateLLMLoadOptions, validateTTSGenerateOptions, @@ -83,3 +84,68 @@ describe('runtime guards', () => { expect(safeJsonParse('{bad json', { ok: false })).toEqual({ ok: false }) }) }) + +describe('stream event envelope mapping', () => { + it('expands each kind into its discriminated event', () => { + expect(mapStreamEventEnvelope({ kind: 'token', token: 'hi' })).toEqual({ + type: 'token', + token: 'hi', + }) + expect(mapStreamEventEnvelope({ kind: 'thinking_chunk', chunk: 'why' })).toEqual({ + type: 'thinking_chunk', + chunk: 'why', + }) + expect( + mapStreamEventEnvelope({ + kind: 'tool_call_start', + id: '1', + name: 'search', + arguments: '{"q":"x"}', + }), + ).toEqual({ + type: 'tool_call_start', + id: '1', + name: 'search', + arguments: '{"q":"x"}', + }) + expect(mapStreamEventEnvelope({ kind: 'generation_start', timestamp: 42 })).toEqual({ + type: 'generation_start', + timestamp: 42, + }) + }) + + it('carries stats through on generation_end', () => { + const stats = { + tokenCount: 10, + tokensPerSecond: 5, + timeToFirstToken: 100, + totalTime: 2000, + toolExecutionTime: 0, + } + expect( + mapStreamEventEnvelope({ kind: 'generation_end', content: 'done', stats }), + ).toEqual({ type: 'generation_end', content: 'done', stats }) + }) + + it('emits generation_end with zeroed stats rather than dropping it', () => { + expect(mapStreamEventEnvelope({ kind: 'generation_end', content: 'done' })).toEqual({ + type: 'generation_end', + content: 'done', + stats: { + tokenCount: 0, + tokensPerSecond: 0, + timeToFirstToken: 0, + totalTime: 0, + toolExecutionTime: 0, + }, + }) + }) + + it('returns null for an unrecognized kind', () => { + expect( + mapStreamEventEnvelope({ + kind: 'not_a_kind', + } as unknown as Parameters[0]), + ).toBeNull() + }) +}) diff --git a/package/src/runtime.ts b/package/src/runtime.ts index 987e741..08004b0 100644 --- a/package/src/runtime.ts +++ b/package/src/runtime.ts @@ -1,5 +1,11 @@ import type { EmbeddingsLoadOptions } from './specs/Embeddings.nitro' -import type { LLMLoadOptions, ToolDefinition } from './specs/LLM.nitro' +import type { + GenerationStats, + LLMLoadOptions, + StreamEvent, + StreamEventEnvelope, + ToolDefinition, +} from './specs/LLM.nitro' import type { STTLoadOptions } from './specs/STT.nitro' import type { TTSGenerateOptions, TTSLoadOptions } from './specs/TTS.nitro' @@ -157,7 +163,76 @@ export function validateTTSGenerateOptions( } } - return options + return { + ...options, + onProgress: createSafeCallback('TTS.stream onProgress', options.onProgress), + } +} + +const EMPTY_STATS: GenerationStats = { + tokenCount: 0, + tokensPerSecond: 0, + timeToFirstToken: 0, + totalTime: 0, + toolExecutionTime: 0, +} + +/** + * Expand the flat `StreamEventEnvelope` that crosses the bridge back into the + * discriminated `StreamEvent` union that consumers switch on. + * + * The native side always populates the fields its `kind` implies, so the `??` fallbacks + * are defensive only. `generation_end` falls back to zeroed stats rather than being + * dropped, because swallowing the terminal event would strand UI state mid-generation. + */ +export function mapStreamEventEnvelope( + envelope: StreamEventEnvelope, +): StreamEvent | null { + switch (envelope.kind) { + case 'generation_start': + return { type: 'generation_start', timestamp: envelope.timestamp ?? 0 } + case 'token': + return { type: 'token', token: envelope.token ?? '' } + case 'thinking_start': + return { type: 'thinking_start', timestamp: envelope.timestamp ?? 0 } + case 'thinking_chunk': + return { type: 'thinking_chunk', chunk: envelope.chunk ?? '' } + case 'thinking_end': + return { + type: 'thinking_end', + content: envelope.content ?? '', + timestamp: envelope.timestamp ?? 0, + } + case 'tool_call_start': + return { + type: 'tool_call_start', + id: envelope.id ?? '', + name: envelope.name ?? '', + arguments: envelope.arguments ?? '', + } + case 'tool_call_executing': + return { type: 'tool_call_executing', id: envelope.id ?? '' } + case 'tool_call_completed': + return { + type: 'tool_call_completed', + id: envelope.id ?? '', + result: envelope.result ?? '', + } + case 'tool_call_failed': + return { + type: 'tool_call_failed', + id: envelope.id ?? '', + error: envelope.error ?? '', + } + case 'generation_end': + return { + type: 'generation_end', + content: envelope.content ?? '', + stats: envelope.stats ?? EMPTY_STATS, + } + default: + return null + } } export function safeJsonParse(value: string, fallback: T): T { diff --git a/package/src/specs/LLM.nitro.ts b/package/src/specs/LLM.nitro.ts index 35652ce..b6dbe9e 100644 --- a/package/src/specs/LLM.nitro.ts +++ b/package/src/specs/LLM.nitro.ts @@ -79,6 +79,45 @@ export type StreamEvent = | ToolCallFailedEvent | GenerationEndEvent +/** + * Discriminant for `StreamEventEnvelope`. + * + * Nitro cannot represent a discriminated union of structs (`TokenEvent | ThinkingEvent | ...`) + * — an inline string literal on a struct field is ambiguous between a string and a union + * enum. A *named* literal union compiles to a native enum, so the events cross the bridge + * as one envelope struct discriminated by this, and `llm.ts` maps it back to `StreamEvent`. + */ +export type StreamEventKind = + | 'generation_start' + | 'token' + | 'thinking_start' + | 'thinking_chunk' + | 'thinking_end' + | 'tool_call_start' + | 'tool_call_executing' + | 'tool_call_completed' + | 'tool_call_failed' + | 'generation_end' + +/** + * Flat wire representation of a `StreamEvent`. Which fields are populated depends on + * `kind`; consumers should use the mapped `StreamEvent` union from `llm.ts` instead. + * @internal + */ +export interface StreamEventEnvelope { + kind: StreamEventKind + timestamp?: number + token?: string + chunk?: string + content?: string + id?: string + name?: string + arguments?: string + result?: string + error?: string + stats?: GenerationStats +} + export interface LLMMessage { role: string content: string @@ -195,7 +234,10 @@ export interface LLM extends HybridObject<{ ios: 'swift' }> { onToolCall?: (toolName: string, args: string) => void, ): Promise - streamWithEvents(prompt: string, onEvent: (eventJson: string) => void): Promise + streamWithEvents( + prompt: string, + onEvent: (event: StreamEventEnvelope) => void, + ): Promise /** * Stop the current generation. diff --git a/package/src/specs/TTS.nitro.ts b/package/src/specs/TTS.nitro.ts index eec80aa..a1a163a 100644 --- a/package/src/specs/TTS.nitro.ts +++ b/package/src/specs/TTS.nitro.ts @@ -7,6 +7,12 @@ export interface TTSLoadOptions { export interface TTSGenerateOptions { voice?: string speed?: number + /** + * Fractional generation progress (0-1). Only fires during `stream()`, and only for + * models with a deterministic step count (e.g. diffusion denoise steps). `generate()` + * is a one-shot call that emits no events, so this is never invoked there. + */ + onProgress?: (progress: number) => void } export interface TTS extends HybridObject<{ ios: 'swift' }> {