diff --git a/README.md b/README.md index bd530e4..00b0bc5 100644 --- a/README.md +++ b/README.md @@ -347,7 +347,8 @@ status report instead of JSON. Single-file outputs get a per-file task that is not yet SUCCEEDED yields `downloads.state: "not_ready"` with exit 0. ```bash -meshy-cli text-to-image create --ai-model nano-banana --prompt "a leaf" -o assets/leaf.jpeg +meshy-cli text-to-image create --ai-model nano-banana-2-lite --prompt "a leaf" -o assets/leaf.jpeg +meshy-cli text-to-image create --ai-model gpt-image-2-5-sunburst --aspect-ratio 3:2 --prompt "a leaf" -o assets/leaf-wide.png meshy-cli image-to-3d wait -o out/robot/ ``` diff --git a/src/internal/image-models.ts b/src/internal/image-models.ts index fd691f4..22c3533 100644 --- a/src/internal/image-models.ts +++ b/src/internal/image-models.ts @@ -1,37 +1,50 @@ /** * Shared model/aspect-ratio table for the 2D image commands, so text-to-image * and image-to-image can't drift apart as Meshy ships new models. + * + * The ratio rules mirror the server's own per-model check + * (meshyd pkg/server/api_image_generation_handlers.go): every GPT Image model + * accepts all seven ratios; the nano-banana family rejects 3:2 and 2:3. */ import { UsageError } from "./errors.js"; export const IMAGE_MODELS = [ "gpt-image-2", + "gpt-image-2-5-flare", + "gpt-image-2-5-sunburst", "nano-banana-pro", "nano-banana-2", "nano-banana-2-lite", - "nano-banana", ] as const; export const DEFAULT_IMAGE_MODEL = "gpt-image-2"; +/** OpenAI GPT Image models of any generation — the ones that take every aspect ratio. */ +export const GPT_IMAGE_MODELS = ["gpt-image-2", "gpt-image-2-5-flare", "gpt-image-2-5-sunburst"] as const; + export const IMAGE_MODEL_HELP = - "gpt-image-2 (default, best; ratios 1:1|16:9|9:16|3:2|2:3) | nano-banana-pro | nano-banana-2 | " + - "nano-banana-2-lite (fastest, cheapest) | nano-banana (legacy)"; + "gpt-image-2 (default) | gpt-image-2-5-flare (GPT Image 2.5, fast tier) | " + + "gpt-image-2-5-sunburst (GPT Image 2.5, precision-edit tier) | nano-banana-pro | " + + "nano-banana-2 | nano-banana-2-lite (fastest, cheapest)"; export const ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"] as const; export const ASPECT_RATIO_HELP = - "default: 1:1. nano-banana*: 1:1|16:9|9:16|4:3|3:4. " + - "gpt-image-2: 1:1|16:9|9:16|3:2|2:3. " + + "default: 1:1. gpt-image-*: 1:1|16:9|9:16|4:3|3:4|3:2|2:3. " + + "nano-banana*: 1:1|16:9|9:16|4:3|3:4. " + "Mutually exclusive with --generate-multi-view"; -const GPT_IMAGE_2_RATIOS = new Set(["1:1", "16:9", "9:16", "3:2", "2:3"]); -const NANO_BANANA_RATIOS = new Set(["1:1", "16:9", "9:16", "4:3", "3:4"]); +const GPT_IMAGE_MODEL_SET: ReadonlySet = new Set(GPT_IMAGE_MODELS); +const NANO_BANANA_RATIOS: ReadonlySet = new Set(["1:1", "16:9", "9:16", "4:3", "3:4"]); + +export function isGptImageModel(model: string): boolean { + return GPT_IMAGE_MODEL_SET.has(model); +} /** * Cross-validate model × aspect-ratio × multi-view before any credits are - * spent; the API's own 400 is less specific. When the model is unknown at + * spent, with a message that names the fix. When the model is unknown at * parse time (driven via --data), the ratio check is left to the server. */ export function checkImageCombo(opts: { @@ -44,12 +57,12 @@ export function checkImageCombo(opts: { } const ratio = opts.aspectRatio; if (!ratio || !opts.aiModel) return; - const supported = opts.aiModel === "gpt-image-2" ? GPT_IMAGE_2_RATIOS : NANO_BANANA_RATIOS; - if (!supported.has(ratio)) { + // GPT Image models (2 and 2.5) accept every ratio the flag offers; only the + // nano-banana family is narrower. + if (isGptImageModel(opts.aiModel)) return; + if (!NANO_BANANA_RATIOS.has(ratio)) { throw new UsageError( - opts.aiModel === "gpt-image-2" - ? `--aspect-ratio ${ratio} is not supported by gpt-image-2 (use 1:1, 16:9, 9:16, 3:2, or 2:3)` - : `--aspect-ratio ${ratio} is only supported by gpt-image-2`, + `--aspect-ratio ${ratio} is only supported by the GPT Image models (${GPT_IMAGE_MODELS.join(", ")})`, ); } } diff --git a/tests/image-models.test.ts b/tests/image-models.test.ts index f780fdc..21ba7b2 100644 --- a/tests/image-models.test.ts +++ b/tests/image-models.test.ts @@ -4,44 +4,65 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { checkImageCombo, DEFAULT_IMAGE_MODEL, IMAGE_MODELS } from "../src/internal/image-models.js"; +import { + ASPECT_RATIOS, + checkImageCombo, + DEFAULT_IMAGE_MODEL, + GPT_IMAGE_MODELS, + IMAGE_MODELS, + isGptImageModel, +} from "../src/internal/image-models.js"; import { UsageError } from "../src/internal/errors.js"; +const NANO_BANANA_MODELS = ["nano-banana-2", "nano-banana-2-lite", "nano-banana-pro"]; + test("gpt-image-2 is the default model and part of the model list", () => { assert.equal(DEFAULT_IMAGE_MODEL, "gpt-image-2"); assert.ok((IMAGE_MODELS as readonly string[]).includes(DEFAULT_IMAGE_MODEL)); }); +test("the model list covers both GPT Image 2.5 tiers", () => { + for (const model of ["gpt-image-2-5-flare", "gpt-image-2-5-sunburst"]) { + assert.ok((IMAGE_MODELS as readonly string[]).includes(model), `missing ${model}`); + assert.ok((GPT_IMAGE_MODELS as readonly string[]).includes(model), `${model} is not a GPT Image model`); + assert.ok(isGptImageModel(model)); + } + // Every model is classified exactly one way, so a new entry can't fall + // through the ratio check unnoticed. + for (const model of IMAGE_MODELS) { + assert.equal(isGptImageModel(model), !NANO_BANANA_MODELS.includes(model), model); + } +}); + test("checkImageCombo — multi-view excludes aspect-ratio", () => { assert.throws( - () => checkImageCombo({ aiModel: "nano-banana", aspectRatio: "1:1", generateMultiView: true }), + () => checkImageCombo({ aiModel: "nano-banana-2-lite", aspectRatio: "1:1", generateMultiView: true }), UsageError, ); // multi-view without a ratio is fine - checkImageCombo({ aiModel: "nano-banana", generateMultiView: true }); + checkImageCombo({ aiModel: "nano-banana-2-lite", generateMultiView: true }); }); -test("checkImageCombo — gpt-image-2 accepts square, widescreen, and native ratios", () => { - for (const ratio of ["1:1", "16:9", "9:16", "3:2", "2:3"]) { - checkImageCombo({ aiModel: "gpt-image-2", aspectRatio: ratio }); - } - for (const ratio of ["4:3", "3:4"]) { - assert.throws( - () => checkImageCombo({ aiModel: "gpt-image-2", aspectRatio: ratio }), - /not supported by gpt-image-2/, - ); +test("checkImageCombo — every GPT Image model accepts every aspect ratio the flag offers", () => { + // Mirrors meshyd: the server's per-model check lets gpt-image-2 and both 2.5 + // tiers through for 1:1/16:9/9:16/4:3/3:4/3:2/2:3 (verified live: gpt-image-2 + // + 4:3 is accepted with 202). + for (const model of GPT_IMAGE_MODELS) { + for (const ratio of ASPECT_RATIOS) { + checkImageCombo({ aiModel: model, aspectRatio: ratio }); + } } }); -test("checkImageCombo — nano-banana family accepts photo/video ratios, not gpt-only ones", () => { - for (const model of ["nano-banana", "nano-banana-2", "nano-banana-2-lite", "nano-banana-pro"]) { +test("checkImageCombo — nano-banana family accepts photo/video ratios, not the GPT-only ones", () => { + for (const model of NANO_BANANA_MODELS) { for (const ratio of ["1:1", "16:9", "9:16", "4:3", "3:4"]) { checkImageCombo({ aiModel: model, aspectRatio: ratio }); } for (const ratio of ["3:2", "2:3"]) { assert.throws( () => checkImageCombo({ aiModel: model, aspectRatio: ratio }), - /only supported by gpt-image-2/, + /only supported by the GPT Image models \(gpt-image-2, gpt-image-2-5-flare, gpt-image-2-5-sunburst\)/, ); } } @@ -58,6 +79,9 @@ test("checkImageCombo — unknown model (driven via --data) defers the ratio che assert.throws(() => checkImageCombo({ aspectRatio: "16:9", generateMultiView: true }), UsageError); }); -test("the model list includes nano-banana-2-lite", () => { +test("the model list includes nano-banana-2-lite and no longer offers the original nano-banana", () => { assert.ok((IMAGE_MODELS as readonly string[]).includes("nano-banana-2-lite")); + // The first-generation model was dropped from the flag surface on 2026-09-15; + // the API still accepts it, so --data '{"ai_model":"nano-banana"}' remains the escape hatch. + assert.ok(!(IMAGE_MODELS as readonly string[]).includes("nano-banana")); }); diff --git a/tests/surface.test.ts b/tests/surface.test.ts index 798c719..f7c3424 100644 --- a/tests/surface.test.ts +++ b/tests/surface.test.ts @@ -141,6 +141,9 @@ test("2D image commands default to gpt-image-2 and share the aspect-ratio surfac assert.equal(model?.defaultValue, undefined, resource); assert.match(model?.description ?? "", /gpt-image-2 \(default/, resource); assert.ok(model?.argChoices?.includes("nano-banana-2-lite"), `${resource} lost nano-banana-2-lite`); + for (const tier of ["gpt-image-2-5-flare", "gpt-image-2-5-sunburst"]) { + assert.ok(model?.argChoices?.includes(tier), `${resource} is missing ${tier}`); + } assert.ok(createFlags(resource).has("--aspect-ratio"), `${resource} is missing --aspect-ratio`); } });