From 61eb3190d3e6e84a6c3993a0804c628709d71d43 Mon Sep 17 00:00:00 2001 From: Tomer Shlasky Date: Wed, 7 Oct 2026 10:21:58 +0000 Subject: [PATCH 1/2] feat: add lucy-vton-3.6 (realtime + batch) at 720p Registers lucy-vton-3.6 on the realtime and video surfaces. The model is already served in production; this change only makes it reachable through the typed SDK surface. - 1280x720 on both surfaces, fps {ideal:30,max:30} realtime / 20 video. - No supportedSpeeds: lucy-vton-3.6 is not offered on the fast tier. - lucy-vton-latest still resolves to lucy-vton-3.5, so its registry entries and alias comments are untouched. 3.6 is opt-in by explicit name. - lucy-vton-3.5 is unchanged; this is purely additive. Also adds an "Adding a Model" runbook to AGENTS.md, since 3.5 had to be landed twice after the first attempt advertised a resolution that did not match how the model was served. --- packages/sdk/AGENTS.md | 63 +++++++++++++++++++++++++ packages/sdk/index.html | 3 +- packages/sdk/src/process/types.ts | 2 +- packages/sdk/src/shared/model.ts | 45 ++++++++++++++++-- packages/sdk/tests/e2e-realtime.test.ts | 1 + packages/sdk/tests/e2e.test.ts | 12 +++++ packages/sdk/tests/unit.test.ts | 32 ++++++++++++- 7 files changed, 150 insertions(+), 8 deletions(-) diff --git a/packages/sdk/AGENTS.md b/packages/sdk/AGENTS.md index 99049d7..d947868 100644 --- a/packages/sdk/AGENTS.md +++ b/packages/sdk/AGENTS.md @@ -75,6 +75,7 @@ - `lucy-2.1` - Real-time video editing (supports reference image) - `lucy-2.5` - Real-time video editing (supports reference image) - `lucy-vton-3.5` - Real-time virtual try-on 3.5 +- `lucy-vton-3.6` - Real-time virtual try-on 3.6 (opt-in by name; no `speed: "fast"` tier) - `lucy-restyle-2` - Real-time video restyling - `lucy-latest`, `lucy-vton-latest`, `lucy-restyle-latest` - Server-resolved aliases for the latest stable version (`lucy-vton-latest` → `lucy-vton-3.5`) @@ -83,9 +84,71 @@ - `lucy-2.1` - long-form video editing (720p) - `lucy-2.5` - long-form video editing (720p) - `lucy-vton-3.5` - virtual try-on 3.5 video editing +- `lucy-vton-3.6` - virtual try-on 3.6 video editing (opt-in by name) - `lucy-restyle-2` - video restyling - `lucy-latest`, `lucy-vton-latest`, `lucy-restyle-latest`, `lucy-clip-latest` - Server-resolved aliases (`lucy-vton-latest` → `lucy-vton-3.5`) ### Image Models (Process API) - `lucy-image-2` - image-to-image editing - `lucy-image-latest` - Server-resolved alias + +## Adding a Model + +`src/shared/model.ts` is the shared source of truth for the Decart client SDKs — the Python, Swift, Kotlin and +C++ SDKs each carry a test asserting their registry matches this one. Land a new model here first, release, then +mirror it. + +### Before writing any code: confirm what the API actually serves + +Every registry value must be read off the deployed model, not copied from a sibling: + +| Registry field | Must match | +| --- | --- | +| `width` / `height` | the resolution the API serves for that model | +| realtime vs video entry | which surfaces the model is enabled on | +| `queueUrlPath` | the live `/v1/jobs/` route | +| `supportedSpeeds` | the speed tiers the API actually offers — **omit the field when there are none** | +| `inputSchema` | the input types the model accepts | + +`lucy-vton-3.5` was landed twice because the first attempt advertised a resolution that did not match how the +model was served; it was reverted across all five SDK repos the next day. Confirm geometry before pinning it. + +### The nine sites in `src/shared/model.ts` + +1. `CANONICAL_MODEL_NAMES` +2. `CANONICAL_REALTIME_MODEL_NAMES` (realtime models only) +3. `CANONICAL_VIDEO_MODEL_NAMES` (video models only) +4. the `realtimeModels` zod union +5. the `videoModels` zod union +6. `modelInputSchemas` +7. the `_models.realtime` registry entry +8. the `_models.video` registry entry +9. the JSDoc option lists on `models.realtime()` / `models.video()` + +Then `src/process/types.ts` — add the name to the right branch of `ModelSpecificInputs`, and `AGENTS.md` +(Supported Models) + `index.html` (playground ` +
diff --git a/packages/sdk/src/process/types.ts b/packages/sdk/src/process/types.ts index 92305dd..f1d0b11 100644 --- a/packages/sdk/src/process/types.ts +++ b/packages/sdk/src/process/types.ts @@ -163,7 +163,7 @@ export type ModelSpecificInputs = T["name"] extends " ? ImageEditingInputs : T["name"] extends "lucy-restyle-v2v" | "lucy-restyle-2" ? VideoRestyleInputs - : T["name"] extends "lucy-2.1" | "lucy-2.5" | "lucy-vton-3.5" | "lucy-vton-latest" + : T["name"] extends "lucy-2.1" | "lucy-2.5" | "lucy-vton-3.5" | "lucy-vton-3.6" | "lucy-vton-latest" ? VideoEdit2Inputs : T["name"] extends "lucy-pro-v2v" | "lucy-clip" ? VideoEditInputs diff --git a/packages/sdk/src/shared/model.ts b/packages/sdk/src/shared/model.ts index 8a2253d..b827cf4 100644 --- a/packages/sdk/src/shared/model.ts +++ b/packages/sdk/src/shared/model.ts @@ -6,13 +6,27 @@ const CANONICAL_MODEL_NAMES = [ "lucy-2.1", "lucy-2.5", "lucy-vton-3.5", + "lucy-vton-3.6", "lucy-restyle-2", "lucy-clip", "lucy-image-2", ] as const; -const CANONICAL_REALTIME_MODEL_NAMES = ["lucy-2.1", "lucy-2.5", "lucy-vton-3.5", "lucy-restyle-2"] as const; -const CANONICAL_VIDEO_MODEL_NAMES = ["lucy-clip", "lucy-2.1", "lucy-2.5", "lucy-vton-3.5", "lucy-restyle-2"] as const; +const CANONICAL_REALTIME_MODEL_NAMES = [ + "lucy-2.1", + "lucy-2.5", + "lucy-vton-3.5", + "lucy-vton-3.6", + "lucy-restyle-2", +] as const; +const CANONICAL_VIDEO_MODEL_NAMES = [ + "lucy-clip", + "lucy-2.1", + "lucy-2.5", + "lucy-vton-3.5", + "lucy-vton-3.6", + "lucy-restyle-2", +] as const; const CANONICAL_IMAGE_MODEL_NAMES = ["lucy-image-2"] as const; export const canonicalRealtimeModels = z.enum(CANONICAL_REALTIME_MODEL_NAMES); @@ -53,6 +67,7 @@ export const realtimeModels = z.union([ z.literal("lucy-2.1"), z.literal("lucy-2.5"), z.literal("lucy-vton-3.5"), + z.literal("lucy-vton-3.6"), z.literal("lucy-restyle-2"), // Latest aliases (server-side resolution) z.literal("lucy-latest"), @@ -65,6 +80,7 @@ export const videoModels = z.union([ z.literal("lucy-2.1"), z.literal("lucy-2.5"), z.literal("lucy-vton-3.5"), + z.literal("lucy-vton-3.6"), z.literal("lucy-restyle-2"), // Latest aliases (server-side resolution) z.literal("lucy-latest"), @@ -240,6 +256,7 @@ export const modelInputSchemas = { "lucy-2.1": videoEdit2Schema, "lucy-2.5": videoEdit2Schema, "lucy-vton-3.5": videoEdit2Schema, + "lucy-vton-3.6": videoEdit2Schema, // Latest aliases (server-side resolution) "lucy-latest": videoEdit2Schema, "lucy-vton-latest": videoEdit2Schema, @@ -354,6 +371,15 @@ const _models = { inputSchema: z.object({}), supportedSpeeds: ["fast"], }, + // No supportedSpeeds: lucy-vton-3.6 is not offered on the fast tier. + "lucy-vton-3.6": { + urlPath: "/v1/stream", + name: "lucy-vton-3.6" as const, + fps: { ideal: 30, max: 30 }, + width: 1280, + height: 720, + inputSchema: z.object({}), + }, "lucy-restyle-2": { urlPath: "/v1/stream", name: "lucy-restyle-2" as const, @@ -461,6 +487,15 @@ const _models = { height: 720, inputSchema: modelInputSchemas["lucy-vton-3.5"], }, + "lucy-vton-3.6": { + urlPath: "/v1/generate/lucy-vton-3.6", + queueUrlPath: "/v1/jobs/lucy-vton-3.6", + name: "lucy-vton-3.6" as const, + fps: 20, + width: 1280, + height: 720, + inputSchema: modelInputSchemas["lucy-vton-3.6"], + }, "lucy-restyle-2": { urlPath: "/v1/generate/lucy-restyle-2", queueUrlPath: "/v1/jobs/lucy-restyle-2", @@ -565,7 +600,8 @@ export const models = { * Available options: * - `"lucy-2.1"` - Lucy 2.1 realtime video editing * - `"lucy-2.5"` - Lucy 2.5 realtime video editing - * - `"lucy-vton-3.5"` - Lucy virtual try-on 3.5 (latest) + * - `"lucy-vton-3.5"` - Lucy virtual try-on 3.5 (what `"lucy-vton-latest"` resolves to) + * - `"lucy-vton-3.6"` - Lucy virtual try-on 3.6 (opt-in by name) * - `"lucy-restyle-2"` - Realtime video restyling */ realtime: (model: T): ModelDefinition => { @@ -583,7 +619,8 @@ export const models = { * - `"lucy-clip"` - Video-to-video editing * - `"lucy-2.1"` - Long-form video editing (Lucy 2.1) * - `"lucy-2.5"` - Long-form video editing (Lucy 2.5) - * - `"lucy-vton-3.5"` - Virtual try-on 3.5 video editing (latest) + * - `"lucy-vton-3.5"` - Virtual try-on 3.5 video editing (what `"lucy-vton-latest"` resolves to) + * - `"lucy-vton-3.6"` - Virtual try-on 3.6 video editing (opt-in by name) * - `"lucy-restyle-2"` - Video restyling */ video: (model: T): ModelDefinition & { fps: number; queueUrlPath: string } => { diff --git a/packages/sdk/tests/e2e-realtime.test.ts b/packages/sdk/tests/e2e-realtime.test.ts index afa7d7e..80e9cc4 100644 --- a/packages/sdk/tests/e2e-realtime.test.ts +++ b/packages/sdk/tests/e2e-realtime.test.ts @@ -61,6 +61,7 @@ const REALTIME_MODELS: RealTimeModels[] = [ "lucy-2.1", "lucy-2.5", "lucy-vton-3.5", + "lucy-vton-3.6", "lucy-latest", ]; diff --git a/packages/sdk/tests/e2e.test.ts b/packages/sdk/tests/e2e.test.ts index 5c35b2f..bf9cb83 100644 --- a/packages/sdk/tests/e2e.test.ts +++ b/packages/sdk/tests/e2e.test.ts @@ -156,6 +156,18 @@ describe.concurrent("E2E Tests", { timeout: TIMEOUT, retry: 2 }, () => { await expectResult(result, "lucy-vton-3.5", ".mp4"); }); + it("lucy-vton-3.6: virtual try-on (prompt + reference_image)", async () => { + const result = await client.queue.submitAndPoll({ + model: models.video("lucy-vton-3.6"), + prompt: "Wearing the garment from the reference image", + reference_image: garmentBlob, + data: videoBlob, + seed: 42, + }); + + await expectResult(result, "lucy-vton-3.6", ".mp4"); + }); + it("lucy-latest: video editing", async () => { const result = await client.queue.submitAndPoll({ model: models.video("lucy-latest"), diff --git a/packages/sdk/tests/unit.test.ts b/packages/sdk/tests/unit.test.ts index 218b7b9..03308d2 100644 --- a/packages/sdk/tests/unit.test.ts +++ b/packages/sdk/tests/unit.test.ts @@ -2574,12 +2574,19 @@ describe("Canonical Model Names", () => { expect(canonicalModelSchema.safeParse(alias).success).toBe(false); } - expect(canonicalRealtimeModels.options).toEqual(["lucy-2.1", "lucy-2.5", "lucy-vton-3.5", "lucy-restyle-2"]); + expect(canonicalRealtimeModels.options).toEqual([ + "lucy-2.1", + "lucy-2.5", + "lucy-vton-3.5", + "lucy-vton-3.6", + "lucy-restyle-2", + ]); expect(canonicalVideoModels.options).toEqual([ "lucy-clip", "lucy-2.1", "lucy-2.5", "lucy-vton-3.5", + "lucy-vton-3.6", "lucy-restyle-2", ]); expect(canonicalImageModels.options).toEqual(["lucy-image-2"]); @@ -2639,7 +2646,7 @@ describe("Canonical Model Names", () => { it("lists all models when called without options", () => { const listedModels = listModels(); - expect(listedModels).toHaveLength(21); + expect(listedModels).toHaveLength(23); expect(listedModels.some((model) => model.kind === "realtime" && model.name === "lucy-2.1")).toBe(true); expect(listedModels.some((model) => model.kind === "video" && model.name === "lucy-clip")).toBe(true); expect(listedModels.some((model) => model.kind === "image" && model.name === "lucy-image-2")).toBe(true); @@ -2722,6 +2729,17 @@ describe("Canonical Model Names", () => { expect(model.height).toBe(720); }); + it("lucy-vton-3.6 canonical name works", () => { + const model = models.realtime("lucy-vton-3.6"); + expect(model.name).toBe("lucy-vton-3.6"); + expect(model.urlPath).toBe("/v1/stream"); + expect(model.fps).toEqual({ ideal: 30, max: 30 }); + expect(model.width).toBe(1280); + expect(model.height).toBe(720); + // 3.6 is not offered on the fast tier, unlike 3.5. + expect(model.supportedSpeeds).toBeUndefined(); + }); + it("lucy-restyle-2 canonical name works", () => { const model = models.realtime("lucy-restyle-2"); expect(model.name).toBe("lucy-restyle-2"); @@ -2765,6 +2783,16 @@ describe("Canonical Model Names", () => { expect(model.height).toBe(720); }); + it("lucy-vton-3.6 as video model works", () => { + const model = models.video("lucy-vton-3.6"); + expect(model.name).toBe("lucy-vton-3.6"); + expect(model.urlPath).toBe("/v1/generate/lucy-vton-3.6"); + expect(model.queueUrlPath).toBe("/v1/jobs/lucy-vton-3.6"); + expect(model.fps).toBe(20); + expect(model.width).toBe(1280); + expect(model.height).toBe(720); + }); + it("lucy-restyle-2 as video model works", () => { const model = models.video("lucy-restyle-2"); expect(model.name).toBe("lucy-restyle-2"); From 1699736fb1eab9731efc9b549f0a7762680a13e7 Mon Sep 17 00:00:00 2001 From: Tomer Shlasky Date: Wed, 7 Oct 2026 11:39:44 +0000 Subject: [PATCH 2/2] chore: drop annotations that would drift out of sync Per review: the JSDoc option lists carried alias-resolution and opt-in notes that have to be re-edited every time the alias moves, and the registry entry explained the absence of a field that is simply absent. Both are state that can go stale without anything catching it. --- packages/sdk/src/shared/model.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/shared/model.ts b/packages/sdk/src/shared/model.ts index b827cf4..83ba88b 100644 --- a/packages/sdk/src/shared/model.ts +++ b/packages/sdk/src/shared/model.ts @@ -371,7 +371,6 @@ const _models = { inputSchema: z.object({}), supportedSpeeds: ["fast"], }, - // No supportedSpeeds: lucy-vton-3.6 is not offered on the fast tier. "lucy-vton-3.6": { urlPath: "/v1/stream", name: "lucy-vton-3.6" as const, @@ -600,8 +599,8 @@ export const models = { * Available options: * - `"lucy-2.1"` - Lucy 2.1 realtime video editing * - `"lucy-2.5"` - Lucy 2.5 realtime video editing - * - `"lucy-vton-3.5"` - Lucy virtual try-on 3.5 (what `"lucy-vton-latest"` resolves to) - * - `"lucy-vton-3.6"` - Lucy virtual try-on 3.6 (opt-in by name) + * - `"lucy-vton-3.5"` - Lucy virtual try-on 3.5 + * - `"lucy-vton-3.6"` - Lucy virtual try-on 3.6 * - `"lucy-restyle-2"` - Realtime video restyling */ realtime: (model: T): ModelDefinition => { @@ -619,8 +618,8 @@ export const models = { * - `"lucy-clip"` - Video-to-video editing * - `"lucy-2.1"` - Long-form video editing (Lucy 2.1) * - `"lucy-2.5"` - Long-form video editing (Lucy 2.5) - * - `"lucy-vton-3.5"` - Virtual try-on 3.5 video editing (what `"lucy-vton-latest"` resolves to) - * - `"lucy-vton-3.6"` - Virtual try-on 3.6 video editing (opt-in by name) + * - `"lucy-vton-3.5"` - Virtual try-on 3.5 video editing + * - `"lucy-vton-3.6"` - Virtual try-on 3.6 video editing * - `"lucy-restyle-2"` - Video restyling */ video: (model: T): ModelDefinition & { fps: number; queueUrlPath: string } => {