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
17 changes: 17 additions & 0 deletions docs/skill-parity/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1102,3 +1102,20 @@ behind it, and what a reviewer should check. IDs are stable; append, do not renu
- Credits are meshy-t2's: 5 for a mesh only; image-to-3d with 2K or 4K textures
15, with 8K textures 20. A text-to-3d refine of a meshy-t2.5 preview works
like one of a meshy-t2 preview.

## D-079 Smart Topology defaults to Meshy T2.5

- Supersedes the omitted-model and flag-only polycount defaults in D-076 / D-078.
On image-to-3d and text-to-3d preview, `model_type: "smart-topology"` now
sends `ai_model: "meshy-t2.5"` when no model was supplied. This aligns the
CLI with 3D Agent's Smart Topology generation.
- Conditional defaults run after defaults, raw JSON and flags have merged,
so `--data` and flags select models consistently. Explicit model and
polycount values survive; flags continue to take precedence over JSON.
- T2.5 leaves `target_polycount` omitted for the API's 15,000 default and
accepts 100–30,000. Explicit `meshy-t2` retains the CLI's 10,000 default
and 100–15,000 range. Standard generation and standalone refine retain
their existing model defaults.
- Keep the public API's canonical model ID `meshy-t2.5`; the CLI does not
introduce a renamed alias. Public API model names need not follow billing
SKU conventions from the image-generation endpoints.
4 changes: 2 additions & 2 deletions docs/skill-parity/endpoint-contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@
],
"smart_topology": {
"applies_to": ["image-to-3d", "text-to-3d (preview)"],
"request": { "model_type": "smart-topology (required for these models)", "ai_model": { "enum": ["meshy-t2", "meshy-t2.5"], "default": "omitted: the API picks meshy-t2", "cli_flag": "--ai-model" }, "topology": "triangle only; quad is refused" },
"request": { "model_type": "smart-topology (required for these models)", "ai_model": { "enum": ["meshy-t2", "meshy-t2.5"], "default": "CLI sends meshy-t2.5; API omission still picks meshy-t2", "cli_flag": "--ai-model" }, "topology": "triangle only; quad is refused" },
"target_polycount": { "meshy-t2": { "range": "[100,15000]", "api_default": 4000, "cli_default": 10000 }, "meshy-t2.5": { "range": "[100,30000]", "api_default": 15000, "cli_default": "omitted (the API's 15000)" } },
"credits": "both models: 5 mesh only; image-to-3d with 2K or 4K textures 15, with 8K textures 20",
"refine": "a text-to-3d refine of a meshy-t2.5 preview works like one of a meshy-t2 preview",
"decision": "D-078"
"decision": "D-079"
},
"creative_lab_products": {
"figure": {
Expand Down
8 changes: 8 additions & 0 deletions docs/skill-parity/migration-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ Each difference names the legacy behaviour, the CLI behaviour, and the evidence.
| smart-topology polycount default with `--ai-model meshy-t2.5` | — | none sent (the API's 15,000); meshy-t2 still gets 10,000 | D-078 |
| a Smart Topology `ai_model` without smart-topology, or smart-topology with `topology: "quad"` (through `--data`) | sent, refused by the API | refused locally (exit 2) | D-078 |

### 3.10 Unreleased: Smart Topology defaults to T2.5

`image-to-3d` and `text-to-3d` preview now send `ai_model: "meshy-t2.5"`
when `--model-type smart-topology` has no explicit model. The default face
count is the API's 15,000 (allowed range 100–30,000). To keep T2 and its CLI
10,000 default, pass `--ai-model meshy-t2`. Models supplied through `--data`
receive the same conditional defaults as flags. See D-079.

New global flags: `--output-schema`, `--api-key-file`, `--workspace`, `--no-update-check`,
`--base-url-creative-lab`. New per-command flags on task verbs: `--save-json`,
`--include-raw`, `--project`, `--stage`, `--operation-id`, `--stop-after-first` (make),
Expand Down
9 changes: 5 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,12 @@ meshy-cli image-to-3d create --image-url https://example.com/cat.png --geometry-
meshy-cli text-to-motion create --prompt "a character waving" --duration 3

# smart topology: component-aware low-poly with a native polycount (triangles only).
# meshy-t2 (the API's choice when --ai-model is omitted): 100-15000, CLI default 10000;
# meshy-t2.5: 100-30000, default the API's 15000. Credits for either: 5 mesh only,
# meshy-t2.5 (CLI default): 100-30000, default the API's 15000;
# --ai-model meshy-t2: 100-15000, CLI default 10000. Credits for either: 5 mesh only,
# 15 textured at 2k/4k, 20 at 8k. A meshy-t2.5 preview refines like any other.
meshy-cli image-to-3d create --image-url cat.png --model-type smart-topology --target-polycount 10000
meshy-cli image-to-3d create --image-url cat.png --model-type smart-topology --ai-model meshy-t2.5 --target-polycount 25000
meshy-cli image-to-3d create --image-url cat.png --model-type smart-topology
meshy-cli image-to-3d create --image-url cat.png --model-type smart-topology --target-polycount 25000
meshy-cli image-to-3d create --image-url cat.png --model-type smart-topology --ai-model meshy-t2
meshy-cli text-to-3d create --mode preview --prompt "a toy robot" --model-type smart-topology --target-polycount 8000

# printability with a print size: also checks (analyze) or thickens (repair) thin walls
Expand Down
4 changes: 2 additions & 2 deletions skills/meshy-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ These are API rules, not preferences — ignoring them produces failed tasks:
text-to-3d: preview only; multi-image: `2k` only; not with smart-topology);
`--ultra-mode` on image-to-3d is its deprecated spelling of `2k`.
`--model-type smart-topology` (text-to-3d and image-to-3d) has a native
`--target-polycount`: meshy-t2 100-15000 (default 10000); `--ai-model
meshy-t2.5` up to 30000 (default 15000). `retexture`'s
`--target-polycount`: meshy-t2.5 is the CLI default, 100-30000 (default
15000); `--ai-model meshy-t2` selects T2, 100-15000 (default 10000). `retexture`'s
`--multiview-image-urls` needs Meshy 7 — it takes 1-4 views of **the same
object**, not style references.
- Printing: `analyze-`/`repair-printability` take `--printer-type
Expand Down
18 changes: 8 additions & 10 deletions src/cmd/image-to-3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* The mode picks the model:
* - standard → meshy-7.1 (server default "latest") — full-detail
* generation; polycount is remesh's job afterwards.
* - smart-topology → meshy-t2 (server default), or meshy-t2.5 with
* - smart-topology → meshy-t2.5 (CLI default), or meshy-t2 with
* --ai-model — component-aware low-poly with a native
* --target-polycount; the budget pick for game-ready geometry.
*
Expand All @@ -17,7 +17,7 @@ import { Option } from "commander";
import { parseBool, parseCsv, parseInt10 } from "../internal/flags.js";
import { UsageError } from "../internal/errors.js";
import { buildResourceCommand, type ResourceCommandSpec } from "../internal/task-command.js";
import { checkSmartTopology, defaultPolycount, SMART_TOPOLOGY_MODELS } from "../internal/smart-topology.js";
import { prepareSmartTopologyPayload, SMART_TOPOLOGY_MODELS } from "../internal/smart-topology.js";

const spec: ResourceCommandSpec = {
name: "image-to-3d",
Expand All @@ -39,13 +39,13 @@ const spec: ResourceCommandSpec = {
.addOption(
new Option(
"--model-type <kind>",
"standard (default; meshy-7.1 full-detail generation) | smart-topology (component-aware low-poly: clean topology, separated parts; meshy-t2, or --ai-model meshy-t2.5)",
"standard (default; meshy-7.1 full-detail generation) | smart-topology (component-aware low-poly: clean topology, separated parts; meshy-t2.5 by default)",
).choices(["standard", "smart-topology"]),
)
.addOption(
new Option(
"--ai-model <model>",
"smart-topology only: meshy-t2 (the API's choice when omitted) | meshy-t2.5 (polycount up to 30000)",
"smart-topology only: meshy-t2.5 (default; polycount up to 30000) | meshy-t2",
).choices([...SMART_TOPOLOGY_MODELS]),
)
.option(
Expand Down Expand Up @@ -159,23 +159,21 @@ const spec: ResourceCommandSpec = {
// texturing is its own step, and the API rejects texture knobs on
// untextured runs, so they ride only when texturing is on — then the
// game-ready defaults apply: full PBR map set, 4k base color. The
// model follows the mode (standard → meshy-7.1, smart-topology →
// meshy-t2, both server defaults) unless --ai-model picks meshy-t2.5.
// meshy-t2 gets the rigging-friendly 10k polycount; meshy-t2.5 keeps
// the API's 15k (D-078). The Ultra pass stays off
// standard follows the server's meshy-7.1 default. Smart Topology's
// model and polycount defaults are applied after payload merging.
// The Ultra pass stays off
// unless asked for: it bills extra. GLB-only output — omitting target_formats
// makes the API produce every format.
const texturing = opts.shouldTexture === true;
return {
should_texture: false,
enable_pbr: texturing ? true : undefined,
texture_resolution: texturing ? "4k" : undefined,
target_polycount: defaultPolycount(opts),
target_formats: ["glb"],
};
},
validatePayload(payload) {
checkSmartTopology(payload);
prepareSmartTopologyPayload(payload);
// Smart topology has neither knob; v1 answers 400 for either (it does not ignore them).
if (payload.model_type === "smart-topology") {
for (const [flag, key] of [["--remove-lighting", "remove_lighting"], ["--image-enhancement", "image_enhancement"]] as const) {
Expand Down
15 changes: 7 additions & 8 deletions src/cmd/text-to-3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
* remesh's job (except smart topology's native polycount), resizing is
* resize's job, deprecated parameters (symmetry_mode, lowpoly, ultra_mode)
* are gone, and the mode picks the model: standard runs the server default
* "latest" (Meshy 7.1), smart-topology runs meshy-t2, or meshy-t2.5 with
* "latest" (Meshy 7.1), smart-topology runs meshy-t2.5, or meshy-t2 with
* --ai-model. The `--data` escape hatch still reaches anything the API accepts.
*/

import { Option } from "commander";
import { parseBool, parseCsv, parseInt10 } from "../internal/flags.js";
import { UsageError } from "../internal/errors.js";
import { buildResourceCommand, type ResourceCommandSpec } from "../internal/task-command.js";
import { checkSmartTopology, defaultPolycount, SMART_TOPOLOGY_MODELS } from "../internal/smart-topology.js";
import { prepareSmartTopologyPayload, SMART_TOPOLOGY_MODELS } from "../internal/smart-topology.js";

const spec: ResourceCommandSpec = {
name: "text-to-3d",
Expand Down Expand Up @@ -45,13 +45,13 @@ const spec: ResourceCommandSpec = {
.addOption(
new Option(
"--model-type <kind>",
"preview only. standard (default; full-detail generation) | smart-topology (clean triangle topology, separated parts, native --target-polycount; meshy-t2, or --ai-model meshy-t2.5)",
"preview only. standard (default; full-detail generation) | smart-topology (clean triangle topology, separated parts, native --target-polycount; meshy-t2.5 by default)",
).choices(["standard", "smart-topology"]),
)
.addOption(
new Option(
"--ai-model <model>",
"preview, smart-topology only: meshy-t2 (the API's choice when omitted) | meshy-t2.5 (polycount up to 30000)",
"preview, smart-topology only: meshy-t2.5 (default; polycount up to 30000) | meshy-t2",
).choices([...SMART_TOPOLOGY_MODELS]),
)
.option(
Expand Down Expand Up @@ -130,16 +130,15 @@ const spec: ResourceCommandSpec = {
// removed on meshy-6/7) apply otherwise. `make` still asks for 4k: it
// made the preview itself, on latest.
// GLB-only output — omitting target_formats makes the API produce
// every format. Smart topology defaults like image-to-3d: 10k on
// meshy-t2, the API's 15k on meshy-t2.5 (D-078).
// every format. Smart Topology's model and polycount defaults are
// applied after payload merging, as on image-to-3d.
const refine = opts.mode === "refine";
return {
enable_pbr: refine ? true : undefined,
target_polycount: defaultPolycount(opts),
target_formats: ["glb"],
};
},
validatePayload: checkSmartTopology,
validatePayload: prepareSmartTopologyPayload,
},
endpointOf: (c) => c.textTo3d,
};
Expand Down
25 changes: 12 additions & 13 deletions src/internal/smart-topology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,28 @@
* Smart Topology models, shared by image-to-3d and text-to-3d preview so the
* two commands cannot drift apart.
*
* Without --ai-model the request carries no ai_model and the API picks
* meshy-t2. meshy-t2.5 takes a polycount up to 30,000 (meshy-t2: 15,000).
* The CLI pins meshy-t2.5 when Smart Topology has no explicit ai_model.
* meshy-t2.5 takes a polycount up to 30,000 (meshy-t2: 15,000).
* Both need model_type "smart-topology", and both output triangles only.
*/

import { UsageError } from "./errors.js";

export const SMART_TOPOLOGY_MODELS = ["meshy-t2", "meshy-t2.5"] as const;
export const DEFAULT_SMART_TOPOLOGY_MODEL = "meshy-t2.5";

const MAX_POLYCOUNT: Readonly<Record<string, number>> = { "meshy-t2": 15_000, "meshy-t2.5": 30_000 };

/**
* The CLI's polycount default (D-078): 10,000 for meshy-t2 (D-076); meshy-t2.5
* is left to the API's own 15,000. Follows --ai-model only.
*/
export function defaultPolycount(opts: Record<string, unknown>): number | undefined {
return opts.modelType === "smart-topology" && opts.aiModel !== "meshy-t2.5" ? 10_000 : undefined;
}

/** Checks the merged payload, so values sent through --data meet the same rules. */
export function checkSmartTopology(payload: Record<string, unknown>): void {
const model = typeof payload.ai_model === "string" ? payload.ai_model : undefined;
/** Applies conditional defaults and validates after --data and flags have merged. */
export function prepareSmartTopologyPayload(payload: Record<string, unknown>): void {
const smart = payload.model_type === "smart-topology";
if (smart) {
payload.ai_model ??= DEFAULT_SMART_TOPOLOGY_MODEL;
// Keep the explicit T2 option's rigging-friendly 10k default; T2.5 uses
// the API's own 15k. Resolve this after merging so --data works like flags.
if (payload.ai_model === "meshy-t2") payload.target_polycount ??= 10_000;
}
const model = typeof payload.ai_model === "string" ? payload.ai_model : undefined;
if (model && model in MAX_POLYCOUNT && !smart) {
throw new UsageError(`${model} is a Smart Topology model: pass --model-type smart-topology`);
}
Expand Down
38 changes: 32 additions & 6 deletions tests/api-alignment-features.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,13 @@ test("generation: --geometry-resolution on text-to-3d preview and image-to-3d; t
assert.equal(st.code, 0, st.stderr);
assert.equal(body(api, 1)["model_type"], "smart-topology");
assert.equal(body(api, 1)["target_polycount"], 8000);
assert.ok(!("ai_model" in body(api, 1)), "the server picks meshy-t2 for smart-topology");
assert.equal(body(api, 1)["ai_model"], "meshy-t2.5");
const i = await runCli(["image-to-3d", "create", "--image-url", "https://example.invalid/a.png", "--geometry-resolution", "2k", "--async", "--data", "{}"], { env: api.env() });
assert.equal(i.code, 2, "the preflight of an unreachable URL fails before the POST");
for (const args of [
["text-to-3d", "create", "--mode", "preview", "--prompt", "x", "--model-type", "smart-topology", "--geometry-resolution", "2k"],
["text-to-3d", "create", "--mode", "preview", "--prompt", "x", "--target-polycount", "500"],
["text-to-3d", "create", "--mode", "preview", "--prompt", "x", "--model-type", "smart-topology", "--target-polycount", "20000"],
["text-to-3d", "create", "--mode", "preview", "--prompt", "x", "--model-type", "smart-topology", "--target-polycount", "30001"],
["text-to-3d", "create", "--mode", "refine", "--preview-task-id", "p", "--geometry-resolution", "2k"],
["image-to-3d", "create", "--input-task-id", "img", "--ultra-mode", "true", "--geometry-resolution", "2k"],
["image-to-3d", "create", "--input-task-id", "img", "--model-type", "smart-topology", "--geometry-resolution", "4k"],
Expand All @@ -323,10 +323,11 @@ test("generation: --geometry-resolution on text-to-3d preview and image-to-3d; t
const geo = await runCli(["image-to-3d", "create", "--input-task-id", "img", "--geometry-resolution", "2k", "--async"], { env: api.env() });
assert.equal(geo.code, 0, geo.stderr);
assert.equal(body(api, 3)["geometry_resolution"], "2k");
// Smart topology defaults alike on both commands: 10000 unless asked.
// Smart topology defaults to T2.5 and leaves its 15k polycount to the API.
const stDefault = await runCli(["text-to-3d", "create", "--mode", "preview", "--prompt", "a robot", "--model-type", "smart-topology", "--async"], { env: api.env() });
assert.equal(stDefault.code, 0, stDefault.stderr);
assert.equal(body(api, 4)["target_polycount"], 10_000);
assert.equal(body(api, 4)["ai_model"], "meshy-t2.5");
assert.ok(!("target_polycount" in body(api, 4)));
});
});

Expand All @@ -352,21 +353,46 @@ test("smart topology meshy-t2.5: --ai-model on image-to-3d and text-to-3d previe
assert.equal((req.json as Record<string, unknown>)["target_polycount"], 30_000);
// Without --target-polycount meshy-t2.5 keeps the API's default (15000); meshy-t2 still gets 10000.
for (const base of [img, txt]) {
req = await ok([...base]);
assert.equal((req.json as Record<string, unknown>)["ai_model"], "meshy-t2.5");
assert.ok(!("target_polycount" in (req.json as object)));
req = await ok([...base, "--target-polycount", "30000"]);
assert.equal((req.json as Record<string, unknown>)["ai_model"], "meshy-t2.5");
assert.equal((req.json as Record<string, unknown>)["target_polycount"], 30_000);
req = await ok([...base, "--ai-model", "meshy-t2.5"]);
assert.ok(!("target_polycount" in (req.json as object)), `${base[0]}: meshy-t2.5 polycount is left to the API`);
req = await ok([...base, "--ai-model", "meshy-t2"]);
assert.equal((req.json as Record<string, unknown>)["ai_model"], "meshy-t2");
assert.equal((req.json as Record<string, unknown>)["target_polycount"], 10_000);
// Explicit JSON models and polycounts override the conditional defaults.
req = await ok([...base, "--data", '{"ai_model":"meshy-t2"}']);
assert.equal((req.json as Record<string, unknown>)["ai_model"], "meshy-t2");
assert.equal((req.json as Record<string, unknown>)["target_polycount"], 10_000);
req = await ok([...base, "--data", '{"ai_model":"meshy-t2","target_polycount":8000}']);
assert.equal((req.json as Record<string, unknown>)["target_polycount"], 8_000);
req = await ok([...base, "--data", '{"ai_model":"meshy-t2"}', "--ai-model", "meshy-t2.5"]);
assert.equal((req.json as Record<string, unknown>)["ai_model"], "meshy-t2.5");
assert.ok(!("target_polycount" in (req.json as object)));
}
// Raw JSON and flag-based model_type use the same defaults.
for (const base of [img.slice(0, -2), txt.slice(0, -2)]) {
req = await ok([...base, "--data", '{"model_type":"smart-topology"}']);
assert.equal((req.json as Record<string, unknown>)["ai_model"], "meshy-t2.5");
assert.ok(!("target_polycount" in (req.json as object)));
req = await ok([...base]);
assert.ok(!("ai_model" in (req.json as object)), "standard mode keeps the server's model default");
assert.ok(!("target_polycount" in (req.json as object)));
}
// A model_type sent through --data counts as smart-topology.
req = await ok(["image-to-3d", "create", "--input-task-id", "img", "--data", '{"model_type":"smart-topology"}', "--ai-model", "meshy-t2.5"]);
assert.equal((req.json as Record<string, unknown>)["ai_model"], "meshy-t2.5");
const posts = api.requests.length;

for (const args of [
[...img, "--target-polycount", "30000"],
[...img, "--target-polycount", "30001"],
[...img, "--ai-model", "meshy-t2", "--target-polycount", "30000"],
[...txt, "--target-polycount", "30000"],
[...txt, "--target-polycount", "30001"],
[...img, "--data", '{"ai_model":"meshy-t2","target_polycount":30000}'],
[...img, "--ai-model", "meshy-t2.5", "--target-polycount", "30001"],
[...txt, "--ai-model", "meshy-t2.5", "--target-polycount", "99"],
["image-to-3d", "create", "--input-task-id", "img", "--ai-model", "meshy-t2.5"],
Expand Down
Loading
Loading