From b58f38d715c1330300747978948f2eeaaff4597f Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Wed, 23 Sep 2026 14:58:32 +0000 Subject: [PATCH 1/3] feat(realtime): add speed=fast option and supportedSpeeds model capability Add `decart::Speed { Fast }` with `toString(Speed)` -> "fast", an optional `ConnectOptions::speed` that appends `speed=fast` to the signaling URL (omitted when unset, so the default URL is byte-identical), and `ModelDefinition::supportedSpeeds` advertising fast mode on lucy-2.5, lucy-latest, lucy-vton-3.5 and lucy-vton-latest only. Requesting a speed on a model without it logs a warning and still sends the parameter. Extract the signaling URL construction into the pure detail helper `buildStreamUrl` so it is unit-testable without LiveKit, and document `resolution`/`speed` in the README and the warmup example. --- README.md | 26 +++++++ examples/realtime_warmup.cpp | 6 ++ include/decart/models.h | 6 ++ include/decart/realtime/realtime.h | 12 +++- include/decart/realtime/types.h | 14 ++++ src/detail/stream_url.h | 46 +++++++++++++ src/models.cpp | 15 ++-- src/realtime/session.cpp | 12 ++-- src/realtime/types.cpp | 8 +++ tests/CMakeLists.txt | 5 +- tests/test_enums.cpp | 2 + tests/test_models.cpp | 34 +++++++++ tests/test_stream_url.cpp | 106 +++++++++++++++++++++++++++++ 13 files changed, 278 insertions(+), 14 deletions(-) create mode 100644 src/detail/stream_url.h create mode 100644 tests/test_stream_url.cpp diff --git a/README.md b/README.md index 6f1ed19..0229de5 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,32 @@ session->unmute(); // generation (and billing) See [`examples/realtime_warmup.cpp`](examples/realtime_warmup.cpp) for a complete, runnable example. +### Connect options: resolution and speed + +`ConnectOptions` carries two optional per-session hints. Both are omitted from the +request when unset, which selects the server defaults. + +- `resolution` (`std::optional`): output resolution hint, `"720p"` or + `"1080p"`. +- `speed` (`std::optional`): compute tier. Fast mode + (`decart::Speed::Fast`, sent as `speed=fast`) serves the session from a + higher-compute tier for lower latency and higher throughput; output quality is + unchanged. It is currently available for `lucy-2.5` / `lucy-latest` and + `lucy-vton-3.5` / `lucy-vton-latest`, in the US region only, and is billed at 2x + the standard realtime rate for those models. Other models ignore the option (the + SDK logs a warning). Omit it (the default) for standard mode. + `ModelDefinition::supportedSpeeds` lists the tiers a model advertises. + +```cpp +decart::ConnectOptions options; +options.model = decart::models::realtime("lucy-2.5"); +options.resolution = "1080p"; // optional output resolution hint +options.speed = decart::Speed::Fast; // optional fast mode (2x rate, US only) +``` + +See the [Decart platform docs](https://docs.platform.decart.ai) for current model +availability and pricing. + ### Authentication (client tokens) Create a short-lived token server-side to hand to an untrusted client: diff --git a/examples/realtime_warmup.cpp b/examples/realtime_warmup.cpp index 6791fa1..078d6b3 100644 --- a/examples/realtime_warmup.cpp +++ b/examples/realtime_warmup.cpp @@ -8,6 +8,9 @@ // // DECART_API_KEY=sk-... ./realtime_warmup [model] // +// Set DECART_SPEED=fast to request fast mode (lucy-2.5 / lucy-vton-3.5 families +// only; US region; billed at 2x the standard realtime rate). +// #include #include @@ -36,6 +39,9 @@ int main(int argc, char** argv) { options.model = model; options.initialState.prompt = decart::Prompt{"A watercolor painting", true}; options.startMuted = true; // warm the connection without transmitting (no billing) + if (const char* speed = std::getenv("DECART_SPEED"); speed != nullptr && std::string(speed) == "fast") { + options.speed = decart::Speed::Fast; // higher-compute tier: lower latency, 2x rate, US only + } options.onConnectionState = [](decart::ConnectionState state) { // Stays "connected" while warmed; flips to "generating" once frames flow. std::cout << "[state] " << decart::toString(state) << "\n"; diff --git a/include/decart/models.h b/include/decart/models.h index de7c532..1c19690 100644 --- a/include/decart/models.h +++ b/include/decart/models.h @@ -5,6 +5,8 @@ #include #include +#include "decart/realtime/types.h" + namespace decart { /// A realtime model definition: everything the SDK needs to open a session and @@ -21,6 +23,10 @@ struct ModelDefinition { int width = 0; /// Recommended capture height in pixels. int height = 0; + /// Compute tiers this model accepts via `ConnectOptions::speed` (empty when the + /// model only offers the standard tier). Passing an unlisted speed is not an + /// error: the server ignores it and serves the standard tier. + std::vector supportedSpeeds; }; namespace models { diff --git a/include/decart/realtime/realtime.h b/include/decart/realtime/realtime.h index e245c85..f83845f 100644 --- a/include/decart/realtime/realtime.h +++ b/include/decart/realtime/realtime.h @@ -58,9 +58,19 @@ struct ConnectOptions { /// Optional prompt/image to apply during the handshake. InitialState initialState; - /// Optional output resolution hint ("720p" or "1080p"). + /// Optional output resolution hint ("720p" or "1080p"). Omitted from the + /// request when unset. std::optional resolution; + /// Optional compute tier. `Speed::Fast` serves the session from a + /// higher-compute tier for lower latency and higher throughput; output quality + /// is unchanged. Currently available for `lucy-2.5` / `lucy-latest` and + /// `lucy-vton-3.5` / `lucy-vton-latest` (see + /// `ModelDefinition::supportedSpeeds`), in the US region only, and billed at + /// 2x the standard realtime rate for those models. Other models ignore the + /// option (the SDK logs a warning). Leave unset (the default) for standard mode. + std::optional speed; + /// Publish the input track muted, so no frames reach the model until you call /// `RealtimeSession::unmute()`. Use this to pre-warm a connection: the session /// is fully authenticated and the WebRTC media path is established, but nothing diff --git a/include/decart/realtime/types.h b/include/decart/realtime/types.h index baa31a9..b051ac3 100644 --- a/include/decart/realtime/types.h +++ b/include/decart/realtime/types.h @@ -24,6 +24,20 @@ enum class ConnectionState { /// Human-readable name for a ConnectionState (e.g. "generating"). const char* toString(ConnectionState state) noexcept; +/// Compute tier for a realtime session, requested via `ConnectOptions::speed`. +/// Matches the `speed` option across the Decart SDKs. +enum class Speed { + /// Fast mode: serves the session from a higher-compute tier for lower latency + /// and higher throughput; output quality is unchanged. Currently available + /// for `lucy-2.5` / `lucy-latest` and `lucy-vton-3.5` / `lucy-vton-latest`, + /// in the US region only, and billed at 2x the standard realtime rate for + /// those models. Other models ignore it. + Fast, +}; + +/// Wire value for a Speed (e.g. "fast"), as sent in the `speed` query parameter. +const char* toString(Speed speed) noexcept; + /// A text prompt plus whether the server should enhance it. struct Prompt { std::string text; diff --git a/src/detail/stream_url.h b/src/detail/stream_url.h new file mode 100644 index 0000000..7d5fcc1 --- /dev/null +++ b/src/detail/stream_url.h @@ -0,0 +1,46 @@ +// Copyright 2026 Decart. SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include + +#include "decart/models.h" +#include "decart/realtime/types.h" +#include "detail/log.h" +#include "detail/url.h" + +namespace decart::detail { + +/// Build the realtime signaling URL for a session: `` plus +/// the `api_key`, `model`, and optional `resolution` / `speed` query parameters, +/// in that order. Optional parameters are omitted entirely when unset. The +/// `user_agent` parameter is appended later by SignalingChannel. Pure, so it can +/// be unit-tested without LiveKit. +inline std::string buildStreamUrl(const std::string& base, const ModelDefinition& model, + const std::string& apiKey, const std::optional& resolution, + std::optional speed) { + std::string url = base + model.urlPath; + url = appendQuery(url, "api_key", apiKey); + url = appendQuery(url, "model", model.name); + if (resolution.has_value()) url = appendQuery(url, "resolution", *resolution); + if (speed.has_value()) url = appendQuery(url, "speed", toString(*speed)); + return url; +} + +/// True when `model` advertises `speed` in `ModelDefinition::supportedSpeeds`. +inline bool supportsSpeed(const ModelDefinition& model, Speed speed) noexcept { + return std::find(model.supportedSpeeds.begin(), model.supportedSpeeds.end(), speed) != + model.supportedSpeeds.end(); +} + +/// Log a warning when `speed` is requested for a model that does not advertise +/// it. Not an error: the parameter is still sent and the server ignores it, +/// serving (and billing) the standard tier. +inline void warnIfSpeedUnsupported(const ModelDefinition& model, std::optional speed) { + if (!speed.has_value() || supportsSpeed(model, *speed)) return; + logWarn("speed=" + std::string(toString(*speed)) + " is not supported by model '" + model.name + + "'; the server will ignore it and serve the standard tier"); +} + +} // namespace decart::detail diff --git a/src/models.cpp b/src/models.cpp index 0709d92..5790132 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -16,7 +16,8 @@ struct Entry { int fps; int width; int height; - bool canonical; // false for "latest"/deprecated aliases + bool canonical; // false for "latest"/deprecated aliases + bool fastSpeed = false; // accepts `speed=fast` (see ModelDefinition::supportedSpeeds) }; // Realtime model registry. Kept in sync with the shared model list across the @@ -24,13 +25,13 @@ struct Entry { constexpr std::array kRealtime = {{ // Canonical {"lucy-2.1", 30, 1088, 624, true}, - {"lucy-2.5", 30, 1280, 720, true}, - {"lucy-vton-3.5", 30, 1280, 720, true}, + {"lucy-2.5", 30, 1280, 720, true, /*fastSpeed=*/true}, + {"lucy-vton-3.5", 30, 1280, 720, true, /*fastSpeed=*/true}, {"lucy-restyle-2", 30, 1280, 704, true}, // Server-resolved "latest" aliases - {"lucy-latest", 30, 1088, 624, false}, + {"lucy-latest", 30, 1088, 624, false, /*fastSpeed=*/true}, // Resolves server-side to lucy-vton-3.5. - {"lucy-vton-latest", 30, 1280, 720, false}, + {"lucy-vton-latest", 30, 1280, 720, false, /*fastSpeed=*/true}, {"lucy-restyle-latest", 30, 1280, 704, false}, }}; @@ -42,7 +43,9 @@ const Entry* find(std::string_view name) { } ModelDefinition toDefinition(const Entry& e) { - return ModelDefinition{e.name, kStreamPath, e.fps, e.width, e.height}; + ModelDefinition def{e.name, kStreamPath, e.fps, e.width, e.height, /*supportedSpeeds=*/{}}; + if (e.fastSpeed) def.supportedSpeeds.push_back(Speed::Fast); + return def; } } // namespace diff --git a/src/realtime/session.cpp b/src/realtime/session.cpp index a30fb14..5adeb92 100644 --- a/src/realtime/session.cpp +++ b/src/realtime/session.cpp @@ -19,7 +19,7 @@ #include "detail/livekit_runtime.h" #include "detail/log.h" #include "detail/signaling_channel.h" -#include "detail/url.h" +#include "detail/stream_url.h" #include "detail/user_agent.h" #include "realtime/messages.h" #include "realtime/session_internal.h" @@ -448,10 +448,12 @@ std::unique_ptr connectRealtime(std::shared_ptr c impl->media.onError = options.onError; impl->media.setState(ConnectionState::Connecting); - std::string url = config->realtimeBaseUrl + options.model.urlPath; - url = appendQuery(url, "api_key", config->apiKey); - url = appendQuery(url, "model", options.model.name); - if (options.resolution.has_value()) url = appendQuery(url, "resolution", *options.resolution); + // Signaling is single-shot: this URL is built once per connect() and never + // rebuilt (no signaling reconnect), so per-session options such as `speed` + // only need to be applied here. + warnIfSpeedUnsupported(options.model, options.speed); + const std::string url = buildStreamUrl(config->realtimeBaseUrl, options.model, config->apiKey, + options.resolution, options.speed); impl->signaling = std::make_unique(url, buildUserAgent(config->integration)); diff --git a/src/realtime/types.cpp b/src/realtime/types.cpp index 5b983f9..ff68986 100644 --- a/src/realtime/types.cpp +++ b/src/realtime/types.cpp @@ -19,4 +19,12 @@ const char* toString(ConnectionState state) noexcept { return "unknown"; } +const char* toString(Speed speed) noexcept { + switch (speed) { + case Speed::Fast: + return "fast"; + } + return "unknown"; +} + } // namespace decart diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d804642..7538ec5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,7 +17,8 @@ set(DECART_TEST_SOURCES test_main.cpp test_models.cpp test_base64.cpp - test_enums.cpp) + test_enums.cpp + test_stream_url.cpp) # The signaling message codec only exists in realtime builds. if(DECART_BUILD_REALTIME) @@ -27,7 +28,7 @@ endif() add_executable(decart_tests ${DECART_TEST_SOURCES}) target_include_directories(decart_tests PRIVATE - ${CMAKE_SOURCE_DIR}/src) # internal headers under test (messages, base64) + ${CMAKE_SOURCE_DIR}/src) # internal headers under test (messages, base64, stream_url) target_link_libraries(decart_tests PRIVATE decart::decart diff --git a/tests/test_enums.cpp b/tests/test_enums.cpp index 24e1a9a..1d5eff2 100644 --- a/tests/test_enums.cpp +++ b/tests/test_enums.cpp @@ -22,6 +22,8 @@ TEST_CASE("ConnectionState names match the other SDKs") { CHECK(std::string(toString(ConnectionState::Reconnecting)) == "reconnecting"); } +TEST_CASE("Speed wire values match the other SDKs") { CHECK(std::string(toString(Speed::Fast)) == "fast"); } + TEST_CASE("ImageInput factory helpers set the right field") { auto p = ImageInput::fromPath("/tmp/a.png"); CHECK(p.image.has_value()); diff --git a/tests/test_models.cpp b/tests/test_models.cpp index 683f463..49a6962 100644 --- a/tests/test_models.cpp +++ b/tests/test_models.cpp @@ -1,6 +1,10 @@ // Copyright 2026 Decart. SPDX-License-Identifier: MIT #include +#include +#include +#include + #include "decart/errors.h" #include "decart/models.h" @@ -48,6 +52,36 @@ TEST_CASE("realtime() throws ModelNotFound for unknown names") { } } +TEST_CASE("supportedSpeeds advertises fast mode on exactly the lucy-2.5 and lucy-vton-3.5 families") { + // Compare via integer form: doctest stringifies enums through our ADL + // toString() (which returns const char*) and cannot concatenate that. + const auto fastOnly = std::vector{static_cast(Speed::Fast)}; + auto speeds = [](const ModelDefinition& m) { + std::vector out; + for (auto s : m.supportedSpeeds) out.push_back(static_cast(s)); + return out; + }; + + CHECK(speeds(models::realtime("lucy-2.5")) == fastOnly); + CHECK(speeds(models::realtime("lucy-latest")) == fastOnly); + CHECK(speeds(models::realtime("lucy-vton-3.5")) == fastOnly); + CHECK(speeds(models::realtime("lucy-vton-latest")) == fastOnly); + + const std::set withFast = {"lucy-2.5", "lucy-latest", "lucy-vton-3.5", "lucy-vton-latest"}; + for (const auto& m : models::listRealtime(/*canonicalOnly=*/false)) { + INFO("model " << m.name); + if (withFast.count(m.name)) { + CHECK(speeds(m) == fastOnly); + } else { + CHECK(m.supportedSpeeds.empty()); + } + } + // Every other realtime model (lucy-2.1, lucy-restyle-2, lucy-restyle-latest) is standard-only. + CHECK(models::realtime("lucy-2.1").supportedSpeeds.empty()); + CHECK(models::realtime("lucy-restyle-2").supportedSpeeds.empty()); + CHECK(models::realtime("lucy-restyle-latest").supportedSpeeds.empty()); +} + TEST_CASE("listRealtime() honors canonicalOnly") { auto all = models::listRealtime(/*canonicalOnly=*/false); auto canonical = models::listRealtime(/*canonicalOnly=*/true); diff --git a/tests/test_stream_url.cpp b/tests/test_stream_url.cpp new file mode 100644 index 0000000..fddcd56 --- /dev/null +++ b/tests/test_stream_url.cpp @@ -0,0 +1,106 @@ +// Copyright 2026 Decart. SPDX-License-Identifier: MIT +#include + +#include +#include + +#include "decart/logging.h" +#include "decart/models.h" +#include "detail/stream_url.h" + +using namespace decart; +using namespace decart::detail; + +namespace { + +const std::string kBase = "wss://api3.decart.ai"; +const std::string kKey = "sk-test"; + +std::size_t count(const std::string& haystack, const std::string& needle) { + std::size_t n = 0; + for (auto pos = haystack.find(needle); pos != std::string::npos; pos = haystack.find(needle, pos + 1)) ++n; + return n; +} + +// Installs a capturing log sink for the duration of a test and restores the +// default sink afterwards. +struct LogCapture { + std::vector warnings; + LogCapture() { + setLogHandler([this](LogLevel level, const std::string& message) { + if (level == LogLevel::Warn) warnings.push_back(message); + }); + } + ~LogCapture() { setLogHandler(nullptr); } +}; + +} // namespace + +TEST_CASE("buildStreamUrl without options is unchanged and carries no speed param") { + const auto model = models::realtime("lucy-2.5"); + const auto url = buildStreamUrl(kBase, model, kKey, std::nullopt, std::nullopt); + CHECK(url == "wss://api3.decart.ai/v1/stream?api_key=sk-test&model=lucy-2.5"); + CHECK(url.find("speed") == std::string::npos); + CHECK(url.find("resolution") == std::string::npos); +} + +TEST_CASE("buildStreamUrl appends resolution after model") { + const auto model = models::realtime("lucy-2.5"); + const auto url = buildStreamUrl(kBase, model, kKey, std::string("1080p"), std::nullopt); + CHECK(url == "wss://api3.decart.ai/v1/stream?api_key=sk-test&model=lucy-2.5&resolution=1080p"); +} + +TEST_CASE("buildStreamUrl appends speed=fast exactly once, after resolution") { + const auto model = models::realtime("lucy-2.5"); + + SUBCASE("speed only") { + const auto url = buildStreamUrl(kBase, model, kKey, std::nullopt, Speed::Fast); + CHECK(url == "wss://api3.decart.ai/v1/stream?api_key=sk-test&model=lucy-2.5&speed=fast"); + CHECK(count(url, "speed=") == 1); + } + + SUBCASE("speed with resolution") { + const auto url = buildStreamUrl(kBase, model, kKey, std::string("720p"), Speed::Fast); + CHECK(url == "wss://api3.decart.ai/v1/stream?api_key=sk-test&model=lucy-2.5&resolution=720p&speed=fast"); + CHECK(count(url, "speed=") == 1); + } + + SUBCASE("still sent for models without the capability (server ignores it)") { + const auto restyle = models::realtime("lucy-restyle-2"); + const auto url = buildStreamUrl(kBase, restyle, kKey, std::nullopt, Speed::Fast); + CHECK(url == "wss://api3.decart.ai/v1/stream?api_key=sk-test&model=lucy-restyle-2&speed=fast"); + } +} + +TEST_CASE("buildStreamUrl is deterministic: rebuilding yields the same speed param") { + // Signaling is single-shot (no reconnect rebuilds the URL), but the builder is + // pure, so any future re-dial that reuses the same inputs preserves `speed`. + const auto model = models::realtime("lucy-vton-latest"); + const auto first = buildStreamUrl(kBase, model, kKey, std::nullopt, Speed::Fast); + const auto second = buildStreamUrl(kBase, model, kKey, std::nullopt, Speed::Fast); + CHECK(first == second); + CHECK(count(first, "&speed=fast") == 1); +} + +TEST_CASE("supportsSpeed reflects ModelDefinition::supportedSpeeds") { + CHECK(supportsSpeed(models::realtime("lucy-2.5"), Speed::Fast)); + CHECK(supportsSpeed(models::realtime("lucy-latest"), Speed::Fast)); + CHECK(supportsSpeed(models::realtime("lucy-vton-3.5"), Speed::Fast)); + CHECK(supportsSpeed(models::realtime("lucy-vton-latest"), Speed::Fast)); + CHECK_FALSE(supportsSpeed(models::realtime("lucy-2.1"), Speed::Fast)); + CHECK_FALSE(supportsSpeed(models::realtime("lucy-restyle-2"), Speed::Fast)); + CHECK_FALSE(supportsSpeed(models::realtime("lucy-restyle-latest"), Speed::Fast)); +} + +TEST_CASE("warnIfSpeedUnsupported logs only for models lacking the capability") { + LogCapture capture; + + warnIfSpeedUnsupported(models::realtime("lucy-2.5"), Speed::Fast); + warnIfSpeedUnsupported(models::realtime("lucy-restyle-2"), std::nullopt); + CHECK(capture.warnings.empty()); + + warnIfSpeedUnsupported(models::realtime("lucy-restyle-2"), Speed::Fast); + REQUIRE(capture.warnings.size() == 1); + CHECK(capture.warnings[0].find("speed=fast") != std::string::npos); + CHECK(capture.warnings[0].find("lucy-restyle-2") != std::string::npos); +} From f71ef5ed0dfed0ff454de4b28e0e6094f36103a9 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Wed, 23 Sep 2026 15:12:40 +0000 Subject: [PATCH 2/3] fix(realtime): append ConnectOptions::speed after connectTimeout Keep the pre-existing positional aggregate initializer shape of ConnectOptions compiling unchanged by placing the new optional speed field last, and pin that shape with a compile-check test. --- include/decart/realtime/realtime.h | 18 ++++++------ tests/CMakeLists.txt | 3 +- tests/test_connect_options.cpp | 44 ++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 tests/test_connect_options.cpp diff --git a/include/decart/realtime/realtime.h b/include/decart/realtime/realtime.h index f83845f..2188f8b 100644 --- a/include/decart/realtime/realtime.h +++ b/include/decart/realtime/realtime.h @@ -62,15 +62,6 @@ struct ConnectOptions { /// request when unset. std::optional resolution; - /// Optional compute tier. `Speed::Fast` serves the session from a - /// higher-compute tier for lower latency and higher throughput; output quality - /// is unchanged. Currently available for `lucy-2.5` / `lucy-latest` and - /// `lucy-vton-3.5` / `lucy-vton-latest` (see - /// `ModelDefinition::supportedSpeeds`), in the US region only, and billed at - /// 2x the standard realtime rate for those models. Other models ignore the - /// option (the SDK logs a warning). Leave unset (the default) for standard mode. - std::optional speed; - /// Publish the input track muted, so no frames reach the model until you call /// `RealtimeSession::unmute()`. Use this to pre-warm a connection: the session /// is fully authenticated and the WebRTC media path is established, but nothing @@ -83,6 +74,15 @@ struct ConnectOptions { /// Bound on the signaling handshake (socket open + room join). The LiveKit /// media connect manages its own timeout. Default 60s. std::chrono::milliseconds connectTimeout{60000}; + + /// Optional compute tier. `Speed::Fast` serves the session from a + /// higher-compute tier for lower latency and higher throughput; output quality + /// is unchanged. Currently available for `lucy-2.5` / `lucy-latest` and + /// `lucy-vton-3.5` / `lucy-vton-latest` (see + /// `ModelDefinition::supportedSpeeds`), in the US region only, and billed at + /// 2x the standard realtime rate for those models. Other models ignore the + /// option (the SDK logs a warning). Leave unset (the default) for standard mode. + std::optional speed; }; /// Entry point for realtime video transformation. Obtained from `Client::realtime()`. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7538ec5..2261f41 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,7 +18,8 @@ set(DECART_TEST_SOURCES test_models.cpp test_base64.cpp test_enums.cpp - test_stream_url.cpp) + test_stream_url.cpp + test_connect_options.cpp) # The signaling message codec only exists in realtime builds. if(DECART_BUILD_REALTIME) diff --git a/tests/test_connect_options.cpp b/tests/test_connect_options.cpp new file mode 100644 index 0000000..083d28c --- /dev/null +++ b/tests/test_connect_options.cpp @@ -0,0 +1,44 @@ +// Copyright 2026 Decart. SPDX-License-Identifier: MIT +#include + +#include +#include +#include + +#include "decart/models.h" +#include "decart/realtime/realtime.h" + +using namespace decart; + +// Compile-time guard: the field order of ConnectOptions up to `connectTimeout` +// is part of the source-compatibility contract. A positional aggregate +// initializer written against the pre-`speed` shape must still compile and +// bind every field to the same member; new options are appended after it. +TEST_CASE("ConnectOptions keeps its pre-speed positional initializer shape") { + const ConnectOptions options{ + models::realtime("lucy-2.5"), // model + nullptr, // onRemoteFrame + nullptr, // onConnectionState + nullptr, // onError + nullptr, // onQueuePosition + nullptr, // onGenerationTick + nullptr, // onGenerationEnded + InitialState{}, // initialState + std::string("1080p"), // resolution + true, // startMuted + std::chrono::milliseconds{5000}, + }; + CHECK(options.model.name == "lucy-2.5"); + CHECK(options.resolution == std::optional("1080p")); + CHECK(options.startMuted == true); + CHECK(options.connectTimeout == std::chrono::milliseconds{5000}); + CHECK_FALSE(options.speed.has_value()); // trailing, defaulted +} + +TEST_CASE("ConnectOptions::speed defaults to unset and accepts Speed::Fast") { + ConnectOptions options; + CHECK_FALSE(options.speed.has_value()); + options.speed = Speed::Fast; + REQUIRE(options.speed.has_value()); + CHECK(std::string(toString(*options.speed)) == "fast"); +} From 8c304971d7c75ed56cc6b0b3edf7a4e1901501e8 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Wed, 23 Sep 2026 15:19:03 +0000 Subject: [PATCH 3/3] ci: pin LiveKit SDK 1.11.0 while upstream latest release has no assets --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 326527d..a38e15b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: env: # Used by cmake/LiveKitSDK.cmake to resolve & download the LiveKit release. GITHUB_TOKEN: ${{ github.token }} + # CI pin: upstream's "latest" LiveKit release currently has no assets; unpin when fixed upstream. + DECART_LIVEKIT_VERSION: "1.11.0" steps: - uses: actions/checkout@v4 @@ -35,7 +37,8 @@ jobs: if: runner.os != 'Windows' run: | cmake -B build -S . -G Ninja -DCMAKE_BUILD_TYPE=Release \ - -DDECART_BUILD_EXAMPLES=ON -DDECART_BUILD_TESTS=ON + -DDECART_BUILD_EXAMPLES=ON -DDECART_BUILD_TESTS=ON \ + -DDECART_LIVEKIT_VERSION="$DECART_LIVEKIT_VERSION" cmake --build build - name: Configure & build (Windows, vcpkg) @@ -45,7 +48,8 @@ jobs: cmake -B build -S . ` -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT\scripts\buildsystems\vcpkg.cmake" ` -DVCPKG_MANIFEST_FEATURES=tests ` - -DDECART_BUILD_EXAMPLES=ON -DDECART_BUILD_TESTS=ON + -DDECART_BUILD_EXAMPLES=ON -DDECART_BUILD_TESTS=ON ` + -DDECART_LIVEKIT_VERSION="$env:DECART_LIVEKIT_VERSION" cmake --build build --config Release - name: Test