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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions packages/sdk/src/shared/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export const realtimeModels = z.union([
z.literal("lucy-restyle"),
z.literal("lucy-restyle-2"),
z.literal("live-avatar"),
// Latest aliases (server-side resolution)
z.literal("lucy-latest"),
z.literal("lucy-vton-latest"),
z.literal("lucy-restyle-latest"),
// Deprecated names (use canonical names above instead)
z.literal("mirage"),
z.literal("mirage_v2"),
Expand All @@ -57,6 +61,11 @@ export const videoModels = z.union([
z.literal("lucy-2.1"),
z.literal("lucy-restyle-2"),
z.literal("lucy-motion"),
// Latest aliases (server-side resolution)
z.literal("lucy-latest"),
z.literal("lucy-restyle-latest"),
z.literal("lucy-clip-latest"),
z.literal("lucy-motion-latest"),
// Deprecated names (use canonical names above instead)
z.literal("lucy-pro-v2v"),
z.literal("lucy-restyle-v2v"),
Expand All @@ -65,6 +74,8 @@ export const videoModels = z.union([
export const imageModels = z.union([
// Canonical name
z.literal("lucy-image-2"),
// Latest alias (server-side resolution)
z.literal("lucy-image-latest"),
// Deprecated name (use canonical name above instead)
z.literal("lucy-pro-i2i"),
]);
Expand Down Expand Up @@ -216,6 +227,29 @@ export const modelInputSchemas = {
seed: z.number().optional().describe("The seed to use for the generation"),
resolution: motionResolutionSchema,
}),
// Latest aliases (server-side resolution)
"lucy-latest": videoEdit2Schema,
"lucy-restyle-latest": restyleSchema,
"lucy-clip-latest": videoEditSchema,
"lucy-motion-latest": z.object({
data: fileInputSchema.describe(
"The image data to use for generation (File, Blob, ReadableStream, URL, or string URL). Output video is limited to 5 seconds.",
),
trajectory: z
.array(
z.object({
frame: z.number().min(0),
x: z.number().min(0),
y: z.number().min(0),
}),
)
.min(2)
.max(1000)
.describe("The trajectory of the desired movement of the object in the image"),
seed: z.number().optional().describe("The seed to use for the generation"),
resolution: motionResolutionSchema,
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated motion schema risks future inconsistency

Medium Severity

The lucy-motion-latest entry in modelInputSchemas is a full copy-paste of the inline lucy-motion schema definition. Every other -latest alias reuses a named schema variable (videoEdit2Schema, restyleSchema, videoEditSchema, imageEditSchema), but the motion schema was defined inline and then duplicated. If the motion schema is later updated (e.g., constraint changes, new fields), only one copy may get updated, causing silent divergence between lucy-motion and lucy-motion-latest.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 97e714b. Configure here.

"lucy-image-latest": imageEditSchema,
// Deprecated names (kept for backward compatibility)
"lucy-pro-v2v": videoEditSchema,
"lucy-pro-i2i": imageEditSchema,
Expand Down Expand Up @@ -327,6 +361,31 @@ const _models = {
height: 720,
inputSchema: z.object({}),
},
// Latest aliases (server-side resolution)
"lucy-latest": {
urlPath: "/v1/stream",
name: "lucy-latest" as const,
fps: 20,
width: 1088,
height: 624,
inputSchema: z.object({}),
},
"lucy-vton-latest": {
urlPath: "/v1/stream",
name: "lucy-vton-latest" as const,
fps: 20,
width: 1088,
height: 624,
inputSchema: z.object({}),
},
"lucy-restyle-latest": {
urlPath: "/v1/stream",
name: "lucy-restyle-latest" as const,
fps: 22,
width: 1280,
height: 704,
inputSchema: z.object({}),
},
// Deprecated names (use canonical names above instead)
mirage: {
urlPath: "/v1/stream",
Expand Down Expand Up @@ -380,6 +439,16 @@ const _models = {
height: 704,
inputSchema: modelInputSchemas["lucy-image-2"],
},
// Latest alias (server-side resolution)
"lucy-image-latest": {
urlPath: "/v1/generate/lucy-image-latest",
queueUrlPath: "/v1/jobs/lucy-image-latest",
name: "lucy-image-latest" as const,
fps: 25,
width: 1280,
height: 704,
inputSchema: modelInputSchemas["lucy-image-latest"],
},
// Deprecated name
"lucy-pro-i2i": {
urlPath: "/v1/generate/lucy-pro-i2i",
Expand Down Expand Up @@ -438,6 +507,43 @@ const _models = {
height: 704,
inputSchema: modelInputSchemas["lucy-motion"],
},
// Latest aliases (server-side resolution)
"lucy-latest": {
urlPath: "/v1/generate/lucy-latest",
queueUrlPath: "/v1/jobs/lucy-latest",
name: "lucy-latest" as const,
fps: 20,
width: 1088,
height: 624,
inputSchema: modelInputSchemas["lucy-latest"],
},
"lucy-restyle-latest": {
urlPath: "/v1/generate/lucy-restyle-latest",
queueUrlPath: "/v1/jobs/lucy-restyle-latest",
name: "lucy-restyle-latest" as const,
fps: 22,
width: 1280,
height: 704,
inputSchema: modelInputSchemas["lucy-restyle-latest"],
},
"lucy-clip-latest": {
urlPath: "/v1/generate/lucy-clip-latest",
queueUrlPath: "/v1/jobs/lucy-clip-latest",
name: "lucy-clip-latest" as const,
fps: 25,
width: 1280,
height: 704,
inputSchema: modelInputSchemas["lucy-clip-latest"],
},
"lucy-motion-latest": {
urlPath: "/v1/generate/lucy-motion-latest",
queueUrlPath: "/v1/jobs/lucy-motion-latest",
name: "lucy-motion-latest" as const,
fps: 25,
width: 1280,
height: 704,
inputSchema: modelInputSchemas["lucy-motion-latest"],
},
// Deprecated names (use canonical names above instead)
"lucy-pro-v2v": {
urlPath: "/v1/generate/lucy-pro-v2v",
Expand Down
64 changes: 64 additions & 0 deletions packages/sdk/tests/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ describe.concurrent("E2E Tests", { timeout: TIMEOUT, retry: 2 }, () => {
});
});

describe("Process API - Image Models (latest aliases)", () => {
it("lucy-image-latest: image-to-image", async () => {
const result = await client.process({
model: models.image("lucy-image-latest"),
prompt: "Oil painting in the style of Van Gogh",
data: imageBlob,
seed: 333,
enhance_prompt: false,
});

await expectResult(result, "lucy-image-latest", ".png");
});
});

describe("Process API - Image Models (deprecated names)", () => {
it("lucy-pro-i2i (deprecated): image-to-image", async () => {
const result = await client.process({
Expand Down Expand Up @@ -233,6 +247,56 @@ describe.concurrent("E2E Tests", { timeout: TIMEOUT, retry: 2 }, () => {
await expectResult(result, "lucy-2-v2v", ".mp4");
});

// Latest aliases (server-side resolution)
it("lucy-latest: video editing", async () => {
const result = await client.queue.submitAndPoll({
model: models.video("lucy-latest"),
prompt: "Watercolor painting style with soft brushstrokes",
data: videoBlob,
seed: 42,
});

await expectResult(result, "lucy-latest", ".mp4");
});

it("lucy-restyle-latest: video restyling", async () => {
const result = await client.queue.submitAndPoll({
model: models.video("lucy-restyle-latest"),
prompt: "Cyberpunk neon city style",
data: videoBlob,
seed: 777,
});

await expectResult(result, "lucy-restyle-latest", ".mp4");
});

it("lucy-clip-latest: video-to-video", async () => {
const result = await client.queue.submitAndPoll({
model: models.video("lucy-clip-latest"),
prompt: "Lego World animated style",
data: videoBlob,
seed: 999,
enhance_prompt: true,
});

await expectResult(result, "lucy-clip-latest", ".mp4");
});

it("lucy-motion-latest: motion-guided image-to-video", async () => {
const result = await client.queue.submitAndPoll({
model: models.video("lucy-motion-latest"),
data: imageBlob,
trajectory: [
{ frame: 0, x: 0, y: 0 },
{ frame: 1, x: 0.1, y: 0.2 },
{ frame: 2, x: 0.2, y: 0.4 },
],
seed: 555,
});

await expectResult(result, "lucy-motion-latest", ".mp4");
});

it("lucy-motion: motion-guided image-to-video", async () => {
const result = await client.queue.submitAndPoll({
model: models.video("lucy-motion"),
Expand Down
97 changes: 97 additions & 0 deletions packages/sdk/tests/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3504,6 +3504,103 @@ describe("Canonical Model Names", () => {
});
});

describe("Latest aliases", () => {
it("lucy-latest works as realtime model", () => {
const model = models.realtime("lucy-latest");
expect(model.name).toBe("lucy-latest");
expect(model.urlPath).toBe("/v1/stream");
expect(model.fps).toBe(20);
expect(model.width).toBe(1088);
expect(model.height).toBe(624);
});

it("lucy-vton-latest works as realtime model", () => {
const model = models.realtime("lucy-vton-latest");
expect(model.name).toBe("lucy-vton-latest");
expect(model.urlPath).toBe("/v1/stream");
expect(model.fps).toBe(20);
expect(model.width).toBe(1088);
expect(model.height).toBe(624);
});

it("lucy-restyle-latest works as realtime model", () => {
const model = models.realtime("lucy-restyle-latest");
expect(model.name).toBe("lucy-restyle-latest");
expect(model.urlPath).toBe("/v1/stream");
expect(model.fps).toBe(22);
expect(model.width).toBe(1280);
expect(model.height).toBe(704);
});

it("lucy-latest works as video model", () => {
const model = models.video("lucy-latest");
expect(model.name).toBe("lucy-latest");
expect(model.urlPath).toBe("/v1/generate/lucy-latest");
expect(model.queueUrlPath).toBe("/v1/jobs/lucy-latest");
expect(model.fps).toBe(20);
expect(model.width).toBe(1088);
expect(model.height).toBe(624);
});

it("lucy-restyle-latest works as video model", () => {
const model = models.video("lucy-restyle-latest");
expect(model.name).toBe("lucy-restyle-latest");
expect(model.urlPath).toBe("/v1/generate/lucy-restyle-latest");
expect(model.queueUrlPath).toBe("/v1/jobs/lucy-restyle-latest");
expect(model.fps).toBe(22);
});

it("lucy-clip-latest works as video model", () => {
const model = models.video("lucy-clip-latest");
expect(model.name).toBe("lucy-clip-latest");
expect(model.urlPath).toBe("/v1/generate/lucy-clip-latest");
expect(model.queueUrlPath).toBe("/v1/jobs/lucy-clip-latest");
expect(model.fps).toBe(25);
});

it("lucy-motion-latest works as video model", () => {
const model = models.video("lucy-motion-latest");
expect(model.name).toBe("lucy-motion-latest");
expect(model.urlPath).toBe("/v1/generate/lucy-motion-latest");
expect(model.queueUrlPath).toBe("/v1/jobs/lucy-motion-latest");
expect(model.fps).toBe(25);
});

it("lucy-image-latest works as image model", () => {
const model = models.image("lucy-image-latest");
expect(model.name).toBe("lucy-image-latest");
expect(model.urlPath).toBe("/v1/generate/lucy-image-latest");
expect(model.queueUrlPath).toBe("/v1/jobs/lucy-image-latest");
});

it("lucy-latest is both a realtime and video model", () => {
expect(isRealtimeModel("lucy-latest")).toBe(true);
expect(isVideoModel("lucy-latest")).toBe(true);
});

it("lucy-restyle-latest is both a realtime and video model", () => {
expect(isRealtimeModel("lucy-restyle-latest")).toBe(true);
expect(isVideoModel("lucy-restyle-latest")).toBe(true);
});

it("does not log deprecation warnings for -latest aliases", () => {
_resetDeprecationWarnings();
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

models.realtime("lucy-latest");
models.realtime("lucy-vton-latest");
models.realtime("lucy-restyle-latest");
models.video("lucy-latest");
models.video("lucy-restyle-latest");
models.video("lucy-clip-latest");
models.video("lucy-motion-latest");
models.image("lucy-image-latest");

expect(warnSpy).not.toHaveBeenCalled();
warnSpy.mockRestore();
});
});

describe("Dual-surface models", () => {
it("lucy-2 is both a realtime and video model", () => {
expect(isRealtimeModel("lucy-2")).toBe(true);
Expand Down
Loading