tripo-cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,8 @@ Works with both Tripo regions from the same install — international (`openapi.
20
20
  ## Why this CLI
21
21
 
22
22
  - **One magic command.** `tripo make <anything>` detects what you gave it — prompt text, an image, 2–4 view images, a model file, a URL, a task id, or `@last` — picks the right endpoint, model, and parameters, then waits and downloads the artifacts. No file_token, no polling loops.
23
- - **A 3D domain brain, not a thin API wrapper.** Model choice is a fixed rule (low-poly intent or face budget ≤ 20000 → `tripo-p1`, everything else → `tripo-v3.1`; opt into the P-series preview with `--model tripo-p2` for quad low-poly), P-series illegal parameters are stripped locally, quad + GLB is rejected before it costs you credits, and 7 scenario presets (`--for game-mobile|game-pc|film|print|ar-web|anim|toy`) encode sane parameters + processing chains + delivery formats.
23
+ - **A 3D domain brain, not a thin API wrapper.** Model choice is a fixed rule (low-poly intent or face budget ≤ 20000 → `tripo-p1`, everything else → `tripo-v3.1`; opt into the P-series **Preview** with `--model tripo-p2` for quad low-poly). Those are CLI aliases — the wire value the API receives is `P1-20260311` / `v3.1-20260211` / `P2-20260801` (full alias ↔ wire ↔ status table: `tripo docs --topic commands/generate`; official [Models & Versions](https://developers.tripo3d.ai/en/docs/models-and-versions) and [P Series](https://developers.tripo3d.ai/en/models/p1) pages). P-series illegal parameters are stripped locally, quad + GLB is rejected before it costs you credits, and 7 scenario presets (`--for game-mobile|game-pc|film|print|ar-web|anim|toy`) encode sane parameters + processing chains + delivery formats.
24
+ - **You see the plan before you pay.** `tripo make` prints a plan card — one numbered line per API call (endpoint, what it does, its arguments) — and asks before submitting; afterwards it reports the total credits **and** a per-task breakdown. `--yes` (automatic when headless) skips the question but keeps the card in the log.
24
25
  - **Agent-native.** Non-TTY runs are automatically headless (`--json --yes --no-open`), stdout carries exactly one JSON line per command, progress goes to stderr, exit codes are stable, and NDJSON pipes compose: `tripo make cat.png | tripo model texture | tripo anim rig`. An LLM-facing skill package ships inside the npm package (`tripo docs --llm`).
25
26
 
26
27
  ## Commands
@@ -39,7 +40,7 @@ Works with both Tripo regions from the same install — international (`openapi.
39
40
  | `tripo files upload` | upload a file, print the `file_token` |
40
41
  | `tripo batch run <manifest.yaml>` | bulk pipelines with concurrency, retries, and resume |
41
42
  | `tripo config / doctor` | settings (global + per-directory `.tripo/context.json`) and self-diagnosis |
42
- | `tripo docs [--topic t]` | print the bundled agent/skill docs |
43
+ | `tripo docs [--topic t]` | print the bundled agent/skill docs (`--topic commands/generate` has the model versions / P series table) |
43
44
  | `tripo mcp` | run as an MCP server (Cursor / Claude Desktop) |
44
45
  | `tripo completion <shell>` | bash/zsh/fish completion |
45
46
 
@@ -88,6 +89,7 @@ tripo batch run assets.yaml --concurrency 2
88
89
  ## For AI coding agents
89
90
 
90
91
  - `make`/`watch` are blocking: run them, wait for exit, read the single JSON line on stdout. Do not poll yourself.
92
+ - The plan card on stderr lists every call before it runs; the final JSON carries `credits_consumed` (total for the run) plus `credits_breakdown` (per task) when a chain ran.
91
93
  - Exit codes: `0` ok · `2` usage · `3` auth · `4` insufficient credits · `5` content policy · `6` task failed (credits auto-refunded) · `7` network · `8` not found · `9` rate limit.
92
94
  - `preview.png` in every output directory is your eyes: look at it, then decide to `tripo redo` or continue the chain.
93
95
  - Full behavior rules: `tripo docs --llm`, per-command docs via `tripo docs --topic commands/make`.
@@ -5,7 +5,71 @@
5
5
  */
6
6
  import pc from 'picocolors';
7
7
  import { t } from '../i18n.js';
8
+ import { getStep } from '../knowledge/chains.js';
8
9
  import { getScenario } from '../knowledge/scenarios.js';
10
+ const GENERATION_DESCRIBE = {
11
+ text_to_model: { zh: '文本生成 3D 模型', en: 'generate a 3D model from text' },
12
+ image_to_model: { zh: '单图生成 3D 模型', en: 'generate a 3D model from one image' },
13
+ multiview_to_model: { zh: '多视图生成 3D 模型', en: 'generate a 3D model from multiple views' },
14
+ };
15
+ /** Keys that describe the input itself (already shown on the "input" line). */
16
+ const INPUT_KEYS = new Set(['prompt', 'input', 'inputs']);
17
+ /** Build the pre-run card for `tripo make` from the plan it is about to execute. */
18
+ export function cardFromMakePlan(plan, opts) {
19
+ const steps = [];
20
+ const genParams = {};
21
+ if (plan.generation) {
22
+ const { taskType, request } = plan.generation;
23
+ const args = {};
24
+ for (const [k, v] of Object.entries(request.payload)) {
25
+ if (INPUT_KEYS.has(k))
26
+ continue;
27
+ args[k] = v;
28
+ if (k !== 'model')
29
+ genParams[k] = v;
30
+ }
31
+ const importDef = taskType === 'import_model' ? getStep('import') : undefined;
32
+ steps.push({
33
+ name: importDef ? importDef.name : taskType.replace(/_/g, '-'),
34
+ endpoint: request.endpoint,
35
+ describe: importDef ? t(importDef.describe) : t(GENERATION_DESCRIBE[taskType] ?? { zh: '生成', en: 'generate' }),
36
+ args,
37
+ });
38
+ }
39
+ for (const spec of plan.chain) {
40
+ const def = getStep(spec.step);
41
+ steps.push({
42
+ name: spec.step,
43
+ endpoint: def?.endpoint ?? '',
44
+ describe: def ? t(def.describe) : '',
45
+ args: spec.args,
46
+ });
47
+ }
48
+ const scenario = opts.scenario ? getScenario(opts.scenario) : undefined;
49
+ const notes = [];
50
+ if (opts.candidates && opts.candidates > 1) {
51
+ notes.push(t({
52
+ zh: `并行生成 ${opts.candidates} 个候选(不同 seed),选中一个后再继续加工链`,
53
+ en: `${opts.candidates} candidates in parallel (distinct seeds); pick one before the chain continues`,
54
+ }));
55
+ }
56
+ // Task refs (@last / @name) resolve to an id the human has not seen yet — show it.
57
+ let inputSummary = opts.inputs.join(' ');
58
+ if (plan.sourceTaskId && inputSummary !== plan.sourceTaskId) {
59
+ inputSummary += ` → ${plan.sourceTaskId}${plan.sourceType ? ` (${plan.sourceType})` : ''}`;
60
+ }
61
+ return {
62
+ inputSummary,
63
+ scenario: opts.scenario,
64
+ model: plan.model,
65
+ modelReason: plan.modelReason,
66
+ genParams,
67
+ chain: plan.chain,
68
+ deliverables: scenario?.deliverables ?? [],
69
+ notes,
70
+ steps,
71
+ };
72
+ }
9
73
  /** Cost-impact annotations derived from the parameter set. */
10
74
  export function costNotes(card) {
11
75
  const notes = [];
@@ -48,15 +112,27 @@ export function renderPlanCard(card) {
48
112
  if (card.model) {
49
113
  lines.push(`${t({ zh: '模型', en: 'model' })}: ${card.model}${card.modelReason ? pc.dim(` — ${card.modelReason}`) : ''}`);
50
114
  }
51
- const params = Object.entries(card.genParams);
52
- if (params.length > 0) {
53
- lines.push(`${t({ zh: '参数', en: 'params' })}: ${params.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')}`);
115
+ const fmtArgs = (args) => Object.entries(args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ');
116
+ if (card.steps && card.steps.length > 0) {
117
+ // Numbered step list: what each call does, which endpoint, with which args.
118
+ lines.push(`${t({ zh: '步骤', en: 'steps' })}:`);
119
+ card.steps.forEach((step, i) => {
120
+ lines.push(` ${i + 1}. ${step.name} ${pc.dim(`POST ${step.endpoint}`)}${step.describe ? ` — ${step.describe}` : ''}`);
121
+ if (Object.keys(step.args).length > 0)
122
+ lines.push(pc.dim(` ${fmtArgs(step.args)}`));
123
+ });
54
124
  }
55
- if (card.chain.length > 0) {
56
- const chainStr = card.chain
57
- .map((s) => (Object.keys(s.args).length > 0 ? `${s.step}(${Object.entries(s.args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(',')})` : s.step))
58
- .join(' → ');
59
- lines.push(`${t({ zh: '加工链', en: 'chain' })}: ${chainStr}`);
125
+ else {
126
+ const params = Object.entries(card.genParams);
127
+ if (params.length > 0) {
128
+ lines.push(`${t({ zh: '参数', en: 'params' })}: ${fmtArgs(card.genParams)}`);
129
+ }
130
+ if (card.chain.length > 0) {
131
+ const chainStr = card.chain
132
+ .map((s) => (Object.keys(s.args).length > 0 ? `${s.step}(${Object.entries(s.args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(',')})` : s.step))
133
+ .join(' → ');
134
+ lines.push(`${t({ zh: '加工链', en: 'chain' })}: ${chainStr}`);
135
+ }
60
136
  }
61
137
  if (card.deliverables.length > 0) {
62
138
  lines.push(`${t({ zh: '交付物', en: 'deliverables' })}: ${card.deliverables.join(', ')}`);
@@ -4,12 +4,18 @@
4
4
  * model, the parameters, runs the chain, and downloads the artifacts.
5
5
  */
6
6
  import * as p from '@clack/prompts';
7
- import { UsageError } from '../core/errors.js';
7
+ import { TripoClient } from '../core/client.js';
8
+ import { AuthError, UsageError } from '../core/errors.js';
8
9
  import { emitResult, isInteractive, log, warn } from '../core/output.js';
10
+ import { buildStepRequest } from '../core/requests.js';
9
11
  import { t } from '../i18n.js';
12
+ import { ChainError, getStep } from '../knowledge/chains.js';
13
+ import { EXIT } from '../knowledge/error-catalog.js';
10
14
  import { scenarioIds } from '../knowledge/scenarios.js';
11
- import { executeMake, planMake } from '../pipeline/make.js';
12
- import { newClient, parseChainFlag, printTaskResult, withTaskOptions } from './shared.js';
15
+ import { cardFromMakePlan, costNotes, renderPlanCard } from '../ai/plan-card.js';
16
+ import { checkPlanChain, executeMake, planMake } from '../pipeline/make.js';
17
+ import { detectInput, InputDetectError } from '../util/input-detect.js';
18
+ import { newClient, parseChainFlag, printTaskResult, summarizeCredits, withTaskOptions } from './shared.js';
13
19
  import { printInlineImage } from '../util/term-image.js';
14
20
  export function registerMakeCommand(program) {
15
21
  const cmd = program
@@ -20,15 +26,38 @@ export function registerMakeCommand(program) {
20
26
  .option('--then <steps>', 'processing chain, e.g. "texture,rig,convert:fbx"')
21
27
  .option('--model <model>', 'force model version (tripo-v3.1 / tripo-p1 / tripo-p2 / ...)')
22
28
  .option('-n, --candidates <n>', 'generate N candidates in parallel', (v) => Number(v), 1)
23
- .option('--seed <n>', 'model_seed for reproducible geometry', (v) => Number(v));
29
+ .option('--seed <n>', 'model_seed for reproducible geometry', (v) => Number(v))
30
+ .option('--dry-run', 'plan + validate only: print model, completed params and chain steps; no upload, no submit, no credits');
24
31
  withTaskOptions(cmd);
25
32
  cmd.action(async (inputs, flags) => {
26
33
  const result = await runMake(inputs, flags);
34
+ if (result.cancelled)
35
+ return;
27
36
  emitMakeOutcome(result);
28
37
  });
29
38
  }
39
+ /**
40
+ * Show what is about to run (one numbered line per API call) and ask before
41
+ * spending credits. `--yes` / headless callers see the card on stderr but are
42
+ * not prompted. Returns false when the human declines.
43
+ *
44
+ * Gate on isInteractive() (both stdin and stdout TTYs), not just --yes: when
45
+ * stdout is a pipe (`tripo make x | tripo model texture`) the clack prompt
46
+ * would land in the pipe, corrupt the JSON contract and block forever.
47
+ */
48
+ async function confirmPlan(plan, inputs, flags) {
49
+ const card = cardFromMakePlan(plan, { inputs, scenario: flags.for, candidates: flags.candidates });
50
+ log(renderPlanCard(card));
51
+ if (!isInteractive())
52
+ return true;
53
+ const go = await p.confirm({ message: t({ zh: '按方案执行?', en: 'Run this plan?' }) });
54
+ if (p.isCancel(go) || !go) {
55
+ log(t({ zh: '已取消,未消耗积分', en: 'cancelled; no credits spent' }));
56
+ return false;
57
+ }
58
+ return true;
59
+ }
30
60
  export async function runMake(inputs, flags) {
31
- const client = newClient();
32
61
  const params = { ...flags.param };
33
62
  if (flags.seed !== undefined)
34
63
  params.model_seed = flags.seed;
@@ -45,9 +74,18 @@ export async function runMake(inputs, flags) {
45
74
  notify: flags.notify,
46
75
  timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
47
76
  };
77
+ const cancelled = { results: [], chainResults: [], cancelled: true };
78
+ if (flags.dryRun) {
79
+ // The plan itself is the result; nothing is submitted.
80
+ await dryRunMake(inputs, flags, options);
81
+ return cancelled;
82
+ }
83
+ const client = newClient();
48
84
  const n = flags.candidates;
49
85
  if (n <= 1) {
50
86
  const plan = await planMake(client, options);
87
+ if (!(await confirmPlan(plan, inputs, flags)))
88
+ return cancelled;
51
89
  const outcome = await executeMake(client, plan, options);
52
90
  return { results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly };
53
91
  }
@@ -63,6 +101,8 @@ export async function runMake(inputs, flags) {
63
101
  if (chain.length > 0 && !interactive) {
64
102
  throw new UsageError('-n with --then needs an interactive terminal to pick the winner; run the chain afterwards with "tripo make <task-id> --then ..."');
65
103
  }
104
+ if (!(await confirmPlan(basePlan, inputs, flags)))
105
+ return cancelled;
66
106
  log(t({ zh: `并行生成 ${n} 个候选…`, en: `generating ${n} candidates in parallel…` }));
67
107
  const seeds = Array.from({ length: n }, () => Math.floor(Math.random() * 2 ** 31));
68
108
  const runs = seeds.map((seed, i) => (async () => {
@@ -118,8 +158,156 @@ export async function runMake(inputs, flags) {
118
158
  }
119
159
  return { results, chainResults, stoppedEarly };
120
160
  }
161
+ const UPSTREAM_PLACEHOLDER = '<upstream>';
162
+ /** Dry runs never send a request, so a missing API key must not block them. */
163
+ function dryRunClient() {
164
+ try {
165
+ return newClient();
166
+ }
167
+ catch (err) {
168
+ if (err instanceof AuthError)
169
+ return new TripoClient({ apiKey: 'dry-run' });
170
+ throw err;
171
+ }
172
+ }
173
+ /**
174
+ * `--dry-run`: build and validate the plan with zero network calls, then emit
175
+ * it as the business result (JSON or the plan card). Blocking problems set
176
+ * exit code 2 so an agent can fix the request before spending credits.
177
+ */
178
+ async function dryRunMake(inputs, flags, options) {
179
+ const warnings = [];
180
+ const errors = [];
181
+ const pendingUploads = inputs.filter((raw) => {
182
+ try {
183
+ const kind = detectInput(raw).kind;
184
+ return kind === 'image-file' || kind === 'model-file';
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ });
190
+ let plan;
191
+ try {
192
+ plan = await planMake(dryRunClient(), { ...options, dryRun: true });
193
+ }
194
+ catch (err) {
195
+ if (!(err instanceof UsageError || err instanceof InputDetectError || err instanceof ChainError))
196
+ throw err;
197
+ errors.push(err.message);
198
+ }
199
+ if (!plan) {
200
+ emitDryRun({
201
+ dry_run: true,
202
+ valid: false,
203
+ input: inputs.join(' '),
204
+ scenario: flags.for,
205
+ steps: [],
206
+ pending_uploads: pendingUploads,
207
+ deliverables: [],
208
+ candidates: flags.candidates,
209
+ warnings,
210
+ errors,
211
+ cost_notes: [],
212
+ });
213
+ return;
214
+ }
215
+ warnings.push(...plan.warnings);
216
+ try {
217
+ checkPlanChain(plan);
218
+ }
219
+ catch (err) {
220
+ if (!(err instanceof UsageError))
221
+ throw err;
222
+ errors.push(err.message);
223
+ }
224
+ if (plan.chain.length > 0 && !plan.generation && !plan.sourceType) {
225
+ warnings.push(`chain legality unverified: upstream task type unknown (no local history for ${plan.sourceTaskId}); the real run checks it via the API`);
226
+ }
227
+ if (flags.candidates > 4)
228
+ errors.push('at most 4 candidates per run (-n 4)');
229
+ if (flags.candidates > 1 && !plan.generation) {
230
+ errors.push('-n only applies to generation inputs (text/image), not task references');
231
+ }
232
+ // Same card the real run shows; chain-step args are replaced by the
233
+ // normalized payloads so the human sees the completed parameters too.
234
+ const card = cardFromMakePlan(plan, { inputs, scenario: flags.for, candidates: flags.candidates });
235
+ const cardSteps = card.steps ?? [];
236
+ const steps = [];
237
+ if (plan.generation) {
238
+ steps.push({
239
+ name: cardSteps[0]?.name ?? plan.generation.taskType,
240
+ endpoint: plan.generation.request.endpoint,
241
+ task_type: plan.generation.taskType,
242
+ payload: plan.generation.request.payload,
243
+ });
244
+ }
245
+ plan.chain.forEach((spec, i) => {
246
+ const cardStep = cardSteps[(plan.generation ? 1 : 0) + i];
247
+ try {
248
+ const built = buildStepRequest(spec.step, UPSTREAM_PLACEHOLDER, spec.args);
249
+ warnings.push(...built.warnings);
250
+ if (cardStep) {
251
+ const args = { ...built.request.payload };
252
+ delete args.input;
253
+ cardStep.args = args;
254
+ }
255
+ steps.push({ name: spec.step, endpoint: built.request.endpoint, task_type: built.taskType, payload: built.request.payload });
256
+ }
257
+ catch (err) {
258
+ if (!(err instanceof UsageError))
259
+ throw err;
260
+ errors.push(`${spec.step}: ${err.message}`);
261
+ steps.push({
262
+ name: spec.step,
263
+ endpoint: cardStep?.endpoint ?? '',
264
+ task_type: getStep(spec.step)?.produces ?? '',
265
+ payload: { input: UPSTREAM_PLACEHOLDER, ...spec.args },
266
+ });
267
+ }
268
+ });
269
+ emitDryRun({
270
+ dry_run: true,
271
+ valid: errors.length === 0,
272
+ input: card.inputSummary,
273
+ source_task_id: plan.sourceTaskId,
274
+ source_type: plan.sourceType,
275
+ scenario: flags.for,
276
+ model: plan.model,
277
+ model_reason: plan.modelReason,
278
+ steps,
279
+ pending_uploads: pendingUploads,
280
+ deliverables: card.deliverables,
281
+ candidates: flags.candidates,
282
+ warnings,
283
+ errors,
284
+ cost_notes: costNotes(card),
285
+ }, renderPlanCard(card));
286
+ }
287
+ /** stdout: one JSON line, or the card followed by warnings/errors and the verdict. */
288
+ function emitDryRun(result, cardText) {
289
+ const lines = cardText ? [cardText] : [];
290
+ if (result.warnings.length > 0) {
291
+ lines.push(t({ zh: '警告(已自动调整):', en: 'warnings (auto-adjusted):' }));
292
+ for (const w of result.warnings)
293
+ lines.push(` ! ${w}`);
294
+ }
295
+ if (result.errors.length > 0) {
296
+ lines.push(t({ zh: '错误(阻断):', en: 'errors (blocking):' }));
297
+ for (const e of result.errors)
298
+ lines.push(` ✗ ${e}`);
299
+ }
300
+ lines.push(result.valid
301
+ ? t({ zh: 'dry run:方案有效;未提交任务,未消耗积分', en: 'dry run: plan is valid; nothing submitted, no credits spent' })
302
+ : t({ zh: 'dry run:方案无效;修正上述错误后重试', en: 'dry run: plan is invalid; fix the errors above and retry' }));
303
+ emitResult(result, lines.join('\n'));
304
+ if (!result.valid)
305
+ process.exitCode = EXIT.USAGE;
306
+ }
121
307
  export function emitMakeOutcome(outcome) {
122
308
  const finalChain = outcome.chainResults[outcome.chainResults.length - 1];
309
+ // Every task this run paid for: generation(s) first, then each chain step.
310
+ const credits = summarizeCredits([...outcome.results, ...outcome.chainResults]);
123
311
  if (outcome.results.length === 1) {
124
312
  const primary = outcome.results[0];
125
313
  const merged = finalChain
@@ -129,14 +317,26 @@ export function emitMakeOutcome(outcome) {
129
317
  merged.chain = outcome.chainResults;
130
318
  if (outcome.stoppedEarly)
131
319
  merged.stopped_early = outcome.stoppedEarly;
320
+ if (credits.breakdown.length > 1) {
321
+ merged.credits_consumed = credits.total;
322
+ merged.credits_breakdown = credits.breakdown;
323
+ }
132
324
  printTaskResult(merged);
133
325
  if (merged.preview)
134
326
  printInlineImage(merged.preview);
135
327
  }
136
328
  else {
137
- const human = outcome.results
138
- .map((r, i) => `#${i + 1} ${r.task_id} ${r.output_dir ?? r.status}`)
139
- .join('\n');
140
- emitResult({ candidates: outcome.results, chain: outcome.chainResults }, human);
329
+ const lines = outcome.results.map((r, i) => `#${i + 1} ${r.task_id} ${r.output_dir ?? r.status}`);
330
+ if (credits.breakdown.length > 0) {
331
+ lines.push(t({ zh: `消耗积分合计: ${credits.total}`, en: `credits consumed (total): ${credits.total}` }));
332
+ for (const line of credits.breakdown)
333
+ lines.push(` ${line.type} ${line.task_id} ${line.credits_consumed}`);
334
+ }
335
+ emitResult({
336
+ candidates: outcome.results,
337
+ chain: outcome.chainResults,
338
+ credits_consumed: credits.total,
339
+ credits_breakdown: credits.breakdown,
340
+ }, lines.join('\n'));
141
341
  }
142
342
  }
@@ -51,6 +51,21 @@ export function parseChainFlag(value) {
51
51
  return undefined;
52
52
  return parseThenExpression(value);
53
53
  }
54
+ /**
55
+ * Sum the spend of every task in a run and keep the per-task lines. Tasks
56
+ * without a settled `credits_consumed` (e.g. --no-wait) are skipped.
57
+ */
58
+ export function summarizeCredits(tasks) {
59
+ const breakdown = [];
60
+ let total = 0;
61
+ for (const task of tasks) {
62
+ if (task.credits_consumed === undefined)
63
+ continue;
64
+ breakdown.push({ task_id: task.task_id, type: task.type, credits_consumed: task.credits_consumed });
65
+ total += task.credits_consumed;
66
+ }
67
+ return { total, breakdown };
68
+ }
54
69
  /** Standard result output for a finished task (human + JSON modes). */
55
70
  export function printTaskResult(result) {
56
71
  const lines = [];
@@ -58,7 +73,14 @@ export function printTaskResult(result) {
58
73
  zh: `任务 ${result.task_id} ${result.status === 'success' ? '完成' : result.status}`,
59
74
  en: `task ${result.task_id} ${result.status}`,
60
75
  }));
61
- if (result.credits_consumed !== undefined) {
76
+ if (result.credits_breakdown && result.credits_breakdown.length > 1) {
77
+ lines.push(t({ zh: `消耗积分合计: ${result.credits_consumed}`, en: `credits consumed (total): ${result.credits_consumed}` }));
78
+ const typeWidth = Math.max(...result.credits_breakdown.map((l) => l.type.length));
79
+ for (const line of result.credits_breakdown) {
80
+ lines.push(` ${line.type.padEnd(typeWidth)} ${line.task_id} ${line.credits_consumed}`);
81
+ }
82
+ }
83
+ else if (result.credits_consumed !== undefined) {
62
84
  lines.push(t({ zh: `消耗积分: ${result.credits_consumed}`, en: `credits consumed: ${result.credits_consumed}` }));
63
85
  }
64
86
  if (result.output_dir) {
@@ -16,8 +16,16 @@ import { defaultOutputDir, downloadArtifacts } from './download.js';
16
16
  import { ApiError, UsageError, balanceSuggestion } from './errors.js';
17
17
  import { isQuiet, log, warn } from './output.js';
18
18
  import { uploadLocalFile } from './upload.js';
19
- /** Resolve a user input string into the API `input` value (upload as needed). */
20
- export async function resolveInputValue(client, raw) {
19
+ /** Placeholder `input` value for a local file that a dry run would upload. */
20
+ export function uploadPlaceholder(path) {
21
+ return `<upload:${path}>`;
22
+ }
23
+ /**
24
+ * Resolve a user input string into the API `input` value (upload as needed).
25
+ * `dryRun` skips the upload and returns a `<upload:path>` placeholder so the
26
+ * plan can be built with zero network calls.
27
+ */
28
+ export async function resolveInputValue(client, raw, opts = {}) {
21
29
  const detected = detectInput(raw);
22
30
  switch (detected.kind) {
23
31
  case 'history-ref': {
@@ -36,6 +44,8 @@ export async function resolveInputValue(client, raw) {
36
44
  }
37
45
  case 'image-file':
38
46
  case 'model-file': {
47
+ if (opts.dryRun)
48
+ return { value: uploadPlaceholder(detected.value) };
39
49
  const token = await uploadLocalFile(client, detected.value);
40
50
  return { value: token };
41
51
  }
@@ -21,23 +21,23 @@ export const MODEL_PRODUCING_TYPES = new Set([
21
21
  'animate_retarget',
22
22
  ]);
23
23
  export const STEPS = [
24
- { name: 'refine', endpoint: '/v3/models/refine', produces: 'refine_model', accepts: ['text_to_model'], acceptsFile: false },
25
- { name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true },
26
- { name: 'stylize', endpoint: '/v3/models/stylize', produces: 'stylize_model', accepts: 'any-model', acceptsFile: true },
27
- { name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true },
28
- { name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true },
29
- { name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'] },
30
- { name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true },
31
- { name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'] },
32
- { name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true },
33
- { name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false },
34
- { name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'] },
24
+ { name: 'refine', endpoint: '/v3/models/refine', produces: 'refine_model', accepts: ['text_to_model'], acceptsFile: false, describe: { zh: '精修文生模型的几何细节', en: 'refine geometry of a text-to-model result' } },
25
+ { name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '重新生成贴图 / PBR 材质', en: 'regenerate textures / PBR materials' } },
26
+ { name: 'stylize', endpoint: '/v3/models/stylize', produces: 'stylize_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '按风格重塑模型(lego/voxel/...)', en: 'restyle the model (lego/voxel/...)' } },
27
+ { name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '转换输出格式 / 贴图尺寸 / 朝向', en: 'convert format / texture size / orientation' } },
28
+ { name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true, describe: { zh: '导入本地模型文件作为加工起点', en: 'import a local model file as the chain source' } },
29
+ { name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'], describe: { zh: '检查是否可绑骨(不产出模型,不可绑则止损)', en: 'check riggability (no model output; stops the chain if not riggable)' } },
30
+ { name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true, describe: { zh: '自动绑定骨骼', en: 'auto-rig a skeleton' } },
31
+ { name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'], describe: { zh: '把预设动画重定向到绑骨模型', en: 'retarget preset animations onto the rigged model' } },
32
+ { name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true, describe: { zh: '把网格分割成部件', en: 'segment the mesh into parts' } },
33
+ { name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false, describe: { zh: '补全分割后的部件', en: 'complete the segmented parts' } },
34
+ { name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'], describe: { zh: '减面到目标面数', en: 'decimate to a target face count' } },
35
35
  // mesh/edit: local retry/refine inside a bbox, P2 pipeline (model fixed to P2-20260801).
36
- { name: 'edit', endpoint: '/v3/mesh/edit', produces: 'mesh_edit', accepts: 'any-model', acceptsFile: true },
36
+ { name: 'edit', endpoint: '/v3/mesh/edit', produces: 'mesh_edit', accepts: 'any-model', acceptsFile: true, describe: { zh: '在 bbox 区域内局部重做(P2 管线)', en: 'redo one bbox region locally (P2 pipeline)' } },
37
37
  // smartsegment's `input` is file_token/URL only (the server does not resolve
38
38
  // task ids here), so it cannot chain after a task. Type is refined to
39
39
  // smartsegment_image|smartsegment_model at request-build time per seg_type.
40
- { name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smartsegment_model', accepts: [], acceptsFile: true },
40
+ { name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smartsegment_model', accepts: [], acceptsFile: true, describe: { zh: '智能语义分割', en: 'smart semantic segmentation' } },
41
41
  ];
42
42
  const stepIndex = new Map();
43
43
  for (const s of STEPS) {
@@ -53,7 +53,7 @@ export async function planMake(client, options) {
53
53
  case 'image':
54
54
  case 'url-image': {
55
55
  const raw = classified.values[0].value;
56
- const { value } = await resolveInputValue(client, raw);
56
+ const { value } = await resolveInputValue(client, raw, { dryRun: options.dryRun });
57
57
  const choice = chooseModel({
58
58
  explicit: options.model ?? scenario?.model,
59
59
  faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
@@ -69,7 +69,7 @@ export async function planMake(client, options) {
69
69
  };
70
70
  }
71
71
  case 'multiview': {
72
- const views = await assignViews(client, classified.values.map((v) => v.value));
72
+ const views = await assignViews(client, classified.values.map((v) => v.value), { dryRun: options.dryRun });
73
73
  const choice = chooseModel({
74
74
  explicit: options.model ?? scenario?.model,
75
75
  faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
@@ -86,7 +86,7 @@ export async function planMake(client, options) {
86
86
  }
87
87
  case 'model': {
88
88
  const raw = classified.values[0].value;
89
- const { value } = await resolveInputValue(client, raw);
89
+ const { value } = await resolveInputValue(client, raw, { dryRun: options.dryRun });
90
90
  const generation = buildStepRequest('import', value, {});
91
91
  if (chain.length === 0) {
92
92
  warnings.push(t({
@@ -116,24 +116,31 @@ export async function planMake(client, options) {
116
116
  }
117
117
  }
118
118
  }
119
+ /**
120
+ * Chain legality against the plan's upstream task type (throws UsageError).
121
+ * Skipped when the upstream type is unknown (raw task id without local history).
122
+ */
123
+ export function checkPlanChain(plan) {
124
+ if (plan.chain.length === 0)
125
+ return;
126
+ const upstream = plan.generation?.taskType ?? plan.sourceType;
127
+ if (!upstream)
128
+ return;
129
+ try {
130
+ validateChain(plan.chain, upstream);
131
+ }
132
+ catch (error) {
133
+ if (error instanceof ChainError)
134
+ throw new UsageError(error.message);
135
+ throw error;
136
+ }
137
+ }
119
138
  /** Execute a plan produced by planMake. */
120
139
  export async function executeMake(client, plan, options) {
121
140
  for (const w of plan.warnings)
122
141
  warn(w);
123
142
  // Validate chain legality up front so an illegal plan spends zero credits.
124
- if (plan.chain.length > 0) {
125
- const upstream = plan.generation?.taskType ?? plan.sourceType;
126
- if (upstream) {
127
- try {
128
- validateChain(plan.chain, upstream);
129
- }
130
- catch (error) {
131
- if (error instanceof ChainError)
132
- throw new UsageError(error.message);
133
- throw error;
134
- }
135
- }
136
- }
143
+ checkPlanChain(plan);
137
144
  await precheckBalance(client);
138
145
  let sourceTaskId;
139
146
  let sourceType;
@@ -195,7 +202,7 @@ export async function executeMake(client, plan, options) {
195
202
  * Map 2–4 images to views. Filename hints (front/back/left/right, 前/后/左/右)
196
203
  * win; remaining slots fill positionally in [front, left, back, right] order.
197
204
  */
198
- export async function assignViews(client, files) {
205
+ export async function assignViews(client, files, opts = {}) {
199
206
  if (files.length < 2 || files.length > 4) {
200
207
  throw new UsageError(`multiview needs 2–4 images (got ${files.length})`);
201
208
  }
@@ -227,7 +234,7 @@ export async function assignViews(client, files) {
227
234
  }
228
235
  const resolved = {};
229
236
  for (const [view, file] of Object.entries(views)) {
230
- resolved[view] = (await resolveInputValue(client, file)).value;
237
+ resolved[view] = (await resolveInputValue(client, file, { dryRun: opts.dryRun })).value;
231
238
  }
232
239
  return resolved;
233
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tripo-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Tripo 3D intelligent CLI — turn a prompt or an image into engine-ready 3D assets from your terminal (Tripo V3 API)",
5
5
  "keywords": [
6
6
  "tripo",
package/skill/SKILL.md CHANGED
@@ -30,13 +30,14 @@ Regions (ov = international, cn = China mainland) share one CLI: `tripo login --
30
30
 
31
31
  ## Behavior rules (read first)
32
32
 
33
- 1. **`tripo make` is synchronous and blocking.** It submits the task, polls, downloads artifacts, then exits. Wait for the process to finish and read the final JSON from stdout. Do NOT re-implement polling, do NOT add your own timeout shorter than 15 minutes, do NOT stop just because you saw a task_id in the logs.
33
+ 1. **`tripo make` is synchronous and blocking.** It prints a plan card on stderr (one numbered line per API call: endpoint, purpose, args), then submits, polls, downloads artifacts, and exits. In a TTY it asks "Run this plan?" first; `--yes` (auto-enabled when headless) skips the prompt but still prints the card. Wait for the process to finish and read the final JSON from stdout. Do NOT re-implement polling, do NOT add your own timeout shorter than 15 minutes, do NOT stop just because you saw a task_id in the logs.
34
34
  2. **stdout is the contract; stderr is commentary.** With `--json` (auto-enabled when piped), stdout carries exactly one final JSON line. Progress/logs go to stderr.
35
35
  3. **Artifacts are local files.** The result JSON has `output_dir`, `model_file`, `preview`. Look at `preview.png` to judge quality; re-roll with `tripo redo` if needed.
36
36
  4. **Exit codes are stable** — branch on them:
37
37
  - 0 success · 2 usage/params · 3 auth · 4 insufficient credits (tell the human to run `tripo topup`) · 5 content policy · 6 task failed (credits auto-refunded) · 7 network · 8 not found · 9 rate limit (retry with backoff)
38
38
  5. **Never invent parameters.** Unknown `--param key=value` pairs pass through to the API; stick to documented ones. The full parameter/model/preset reference from the official API docs is bundled: `tripo docs --topic commands/generate` (3D + image generation) and `tripo docs --topic commands/process` (texture/convert/rig/retarget/mesh) — read those instead of browsing the website.
39
- 6. **Don't pick legacy model versions.** The CLI auto-selects `tripo-v3.1` (high fidelity) or `tripo-p1` (low-poly, face budget ≤ 20000). Only override `--model` when the human asks (e.g. `--model tripo-p2` for the P-series preview with quad support, or `tripo-v3.0` / `tripo-v2.5` for reproducing old projects).
39
+ 6. **Validate before spending: `--dry-run --json`.** Same command plus `--dry-run` prints the plan as JSON with zero network calls and no credits: the chosen `model`, the completed `steps[].payload` for every API call, `warnings` (auto-adjustments) and `errors` (blocking; exit 2). When `valid` is true, re-run without `--dry-run`. Details: `tripo docs --topic commands/make` § "Dry run".
40
+ 7. **Don't pick legacy model versions.** The CLI auto-selects `tripo-v3.1` (high fidelity) or `tripo-p1` (low-poly, face budget ≤ 20000). Only override `--model` when the human asks (e.g. `--model tripo-p2` for the P-series **Preview** with quad support, or `tripo-v3.0` / `tripo-v2.5` for reproducing old projects). `tripo-*` names are CLI aliases — the CLI sends the dated wire value (`tripo-p2` → `P2-20260801`); the API rejects the alias itself with 1004. Alias ↔ wire ↔ status table: `tripo docs --topic commands/generate` ("Models & Versions").
40
41
 
41
42
  ## The one command you usually need
42
43
 
@@ -51,7 +52,7 @@ tripo make @last --then convert:fbx --json --yes # continue from last t
51
52
  - `--for` scenario presets: `game-mobile` `game-pc` `film` `print` `ar-web` `anim` `toy`
52
53
  - `--then` chain steps: `refine texture stylize convert import rig-check rig retarget segment complete decimate smartsegment`
53
54
  - step args: `convert:format=FBX,texture_size=2048` (bare value = primary arg: `convert:fbx`, `stylize:lego`, `decimate:5000`)
54
- - Output JSON: `{"task_id","type","status","credits_consumed","output_dir","files",["model_file"],["preview"],["chain"]}`
55
+ - Output JSON: `{"task_id","type","status","credits_consumed","output_dir","files",["model_file"],["preview"],["chain"],["credits_breakdown"]}` — with `--then`, `credits_consumed` is the **total** for the run and `credits_breakdown` itemizes every task (`[{task_id,type,credits_consumed}]`, generation first)
55
56
 
56
57
  ## Pipes (composing steps yourself)
57
58
 
@@ -77,6 +78,7 @@ tripo make cat.png --json | tripo anim rig --json | tripo anim retarget --param
77
78
  ## Detailed docs in this package
78
79
 
79
80
  - `commands/` — one file per command with every flag and parameter
81
+ - model versions · H series (`tripo-v3.x`) · P series (`tripo-p1`, `tripo-p2` Preview) · alias → wire value · GA / Preview / Legacy status · quad → `commands/generate` § "Models & Versions" (also the P Series endpoint entry points)
80
82
  - `examples/` — copy-paste recipes per scenario (game/print/animation/...)
81
83
  - `common-errors.md` — error table with fixes
82
84
 
@@ -6,21 +6,46 @@ Everything below is the full parameter reference from the official API docs —
6
6
  ## 3D
7
7
 
8
8
  ```bash
9
- tripo generate text-to-model "a ceramic teapot" [--model tripo-v3.1|tripo-p1] [-p face_limit=15000] ...
9
+ tripo generate text-to-model "a ceramic teapot" [--model tripo-v3.1|tripo-p1|tripo-p2] [-p face_limit=15000] ...
10
10
  tripo generate image-to-model photo.png [-p texture_alignment=original_image] [-p orientation=align_image]
11
11
  tripo generate multiview-to-model front.png back.png [left.png right.png]
12
12
  ```
13
13
 
14
- ### 3D models (`--model`)
14
+ ### P Series (`tripo-p1` / `tripo-p2`) — same endpoints, different model
15
15
 
16
- | alias | wire value sent | positioning | notes |
17
- | --- | --- | --- | --- |
18
- | `tripo-v3.1` (default) | `v3.1-20260211` | latest high-precision; AAA / 3D printing | full feature set |
19
- | `tripo-p1` | `P1-20260311` | best low-poly; games / UGC / mobile | face_limit 48–20000 (CLI enforces ≥50); **no** `quad` `smart_low_poly` `generate_parts` `geometry_quality`; ~2s mesh; extra `export_uv` |
20
- | `tripo-p2` | `P2-20260801` | P1 upgrade (P series, Preview) | adds `quad` (forces FBX); face_limit 48–50000 tri / 48–25000 quad (validated locally); still **no** `smart_low_poly` `generate_parts` `geometry_quality` (stripped with a warning); credits 100 (no texture) / 110 / 120 / 130 for standard / detailed / extreme. Explicit `--model` only — the CLI never auto-selects it |
21
- | `tripo-v3.0` / `tripo-v2.5` | `v3.0-20250812` / `v2.5-20250123` | legacy | only when the human asks |
16
+ The P series (low-poly, clean topology) uses the **same three endpoints**; only the `model` value changes. The official docs document it as a separate "P Series" tab on each generation page — this is the direct entry point:
17
+
18
+ ```bash
19
+ tripo generate text-to-model "a low poly knight" --model tripo-p1 -p face_limit=8000 # → model: P1-20260311
20
+ tripo generate image-to-model photo.png --model tripo-p2 -p quad=true -p face_limit=12000 # → model: P2-20260801 (Preview)
21
+ tripo generate multiview-to-model front.png back.png --model tripo-p1
22
+ ```
23
+
24
+ | endpoint | H series (standard) page | P series page |
25
+ | --- | --- | --- |
26
+ | `POST /v3/generation/text-to-model` | `…/docs/generation-text-to-model/standard` | <https://developers.tripo3d.ai/en/docs/generation-text-to-model/p> |
27
+ | `POST /v3/generation/image-to-model` | `…/docs/generation-image-to-model/standard` | <https://developers.tripo3d.ai/en/docs/generation-image-to-model/p> |
28
+ | `POST /v3/generation/multiview-to-model` | `…/docs/generation-multiview-to-model/standard` | <https://developers.tripo3d.ai/en/docs/generation-multiview-to-model/p> |
29
+
30
+ (cn: replace `developers.tripo3d.ai/en` with `developers.tripo3d.com/zh`.) P-series constraints — face_limit ranges, unsupported params, P2's `quad` — are in the table below; the CLI enforces them locally.
22
31
 
23
- `tripo-turbo` / `tripo-v2.0` appear in some docs but are **not** on the v3 server whitelist (400).
32
+ ### 3D models (`--model`) — Models & Versions
33
+
34
+ Keywords: model versions · H series (v3.x) · P series (P1 / P2) · Preview · quad · alias → wire value.
35
+
36
+ | alias (what you type) | wire value (what the API receives) | status | positioning | notes |
37
+ | --- | --- | --- | --- | --- |
38
+ | `tripo-v3.1` (default) | `v3.1-20260211` | GA | latest high-precision; AAA / 3D printing | full feature set |
39
+ | `tripo-p1` | `P1-20260311` | GA | best low-poly; games / UGC / mobile | face_limit 48–20000 (CLI enforces ≥50); **no** `quad` `smart_low_poly` `generate_parts` `geometry_quality`; ~2s mesh; extra `export_uv` |
40
+ | `tripo-p2` | `P2-20260801` | **Preview** (2026-08) | P1 upgrade (P series) | adds `quad` (forces FBX); face_limit 48–50000 tri / 48–25000 quad (validated locally); still **no** `smart_low_poly` `generate_parts` `geometry_quality` (stripped with a warning); credits 100 (no texture) / 110 / 120 / 130 for standard / detailed / extreme. Explicit `--model` only — the CLI never auto-selects it |
41
+ | `tripo-v3.0` / `tripo-v2.5` | `v3.0-20250812` / `v2.5-20250123` | Legacy | previous generations | only when the human asks |
42
+
43
+ **Alias vs wire value.** `tripo-*`, `p1`, `p2`, `v3.1` … are CLI-side aliases; the CLI always sends the dated wire value in the `model` field (`--model tripo-p2` → `"model": "P2-20260801"`). The v3 server whitelist accepts wire values only — sending an alias straight to the API returns 1004 `invalid model` with the allowed list. `task.json` records the wire value that was actually used. `tripo-turbo` / `tripo-v2.0` appear in some docs but are **not** on the v3 whitelist (400).
44
+
45
+ Official docs (ov / cn):
46
+ - Models & Versions: <https://developers.tripo3d.ai/en/docs/models-and-versions> · <https://developers.tripo3d.com/zh/docs/models-and-versions>
47
+ - P Series model detail: <https://developers.tripo3d.ai/en/models/p1> · <https://developers.tripo3d.com/zh/models/p1>
48
+ - P Series endpoints: `…/docs/generation-text-to-model/p`, `…/docs/generation-image-to-model/p`, `…/docs/generation-multiview-to-model/p` (the `/standard` tab on each page is the H-series version)
24
49
 
25
50
  ### Parameters (text / image / multiview-to-model)
26
51
 
@@ -77,12 +102,15 @@ tripo generate image-to-splat photo.png # gaussian splat
77
102
 
78
103
  ### Image models (`--model`)
79
104
 
80
- | model | provider | notes |
81
- | --- | --- | --- |
82
- | `seedream_v4` | ByteDance | text-to-image default (balanced); **not accepted by image-to-image** |
83
- | `seedream_v5` | ByteDance | image-to-image default; higher quality |
84
- | `gemini-2.5-flash` / `gemini-3-pro` / `gemini-3.1-flash` | Google | fast / high quality / latest fast. Backend aliases `banana` / `banana_pro` / `banana2` are also accepted |
85
- | `chat_image_1` / `chat_image_1.5` / `chat_image_2` | OpenAI | GPT image; 1.5 = higher quality, 2 = latest |
105
+ | model | provider | status | notes |
106
+ | --- | --- | --- | --- |
107
+ | `seedream_v4` | ByteDance | GA | text-to-image default (balanced); **not accepted by image-to-image** |
108
+ | `seedream_v5` | ByteDance | GA | image-to-image default; higher quality |
109
+ | `gemini-2.5-flash` / `gemini-3-pro` / `gemini-3.1-flash` | Google | GA | fast / high quality / latest fast. Backend aliases `banana` / `banana_pro` / `banana2` are also accepted |
110
+ | `chat_image_1` / `chat_image_1.5` | OpenAI | retiring 2026-10-23 / 2026-12-01 | GPT image; 1.5 = higher quality — move to `chat_image_2` |
111
+ | `chat_image_2` | OpenAI | GA | GPT latest |
112
+
113
+ Image model ids are sent as-is (no alias normalization). Full list with retirement dates: <https://developers.tripo3d.ai/en/docs/models-and-versions>.
86
114
 
87
115
  ### Image parameters
88
116
 
@@ -6,6 +6,27 @@ The magic command: input in, finished local 3D artifacts out. Blocking; exits wh
6
6
  tripo make <input...> [options]
7
7
  ```
8
8
 
9
+ ## Before anything is spent: the plan card
10
+
11
+ `make` first prints a plan card on stderr — input, scenario, model, then **one numbered line per API call** (endpoint, what it does, its args), deliverables and cost notes:
12
+
13
+ ```
14
+ ── Plan ──────────────────────────
15
+ input: a medieval knight
16
+ scenario: game-mobile (Mobile game / low-poly)
17
+ model: P1-20260311 — scenario game-mobile
18
+ steps:
19
+ 1. text-to-model POST /v3/generation/text-to-model — generate a 3D model from text
20
+ model="P1-20260311" texture=true pbr=true texture_quality="standard" face_limit=15000
21
+ 2. texture POST /v3/models/texture — regenerate textures / PBR materials
22
+ 3. convert POST /v3/models/convert — convert format / texture size / orientation
23
+ format="FBX" texture_size=2048
24
+ deliverables: FBX, GLB
25
+ ──────────────────────────────────
26
+ ```
27
+
28
+ In a TTY it then asks `Run this plan?`; answering no exits 0 with `cancelled; no credits spent`. `--yes` (auto-enabled when stdin/stdout are not TTYs, i.e. for agents and pipes) skips the question but still prints the card, so the log shows exactly what ran. `--quiet` hides it.
29
+
9
30
  ## Inputs (auto-detected)
10
31
 
11
32
  | you pass | it runs |
@@ -20,7 +41,7 @@ tripo make <input...> [options]
20
41
 
21
42
  - `--for <scenario>` — `game-mobile` `game-pc` `film` `print` `ar-web` `anim` `toy`. Sets model+params+chain+format from the domain knowledge base.
22
43
  - `--then <steps>` — processing chain, comma-separated. Steps: `refine texture stylize convert import rig-check rig retarget segment complete decimate smartsegment`. Args: `step:key=value,...`; bare value maps to the step's primary arg (`convert:fbx` → format, `stylize:lego` → style, `decimate:5000` → face_limit, `retarget:preset:walk` → animation). Overrides the scenario chain.
23
- - `--model <m>` — force `tripo-v3.1` (high fidelity, default), `tripo-p1` (low-poly; face_limit 50–20000; quad/parts/geometry_quality unsupported and stripped) or `tripo-p2` (P-series preview; adds quad, face_limit 48–50000 tri / 48–25000 quad; never auto-selected). The CLI normalizes these aliases to the wire versions (`v3.1-20260211` / `P1-20260311` / `P2-20260801`) the server actually accepts.
44
+ - `--model <m>` — force `tripo-v3.1` (high fidelity, default), `tripo-p1` (low-poly; face_limit 50–20000; quad/parts/geometry_quality unsupported and stripped) or `tripo-p2` (P-series preview; adds quad, face_limit 48–50000 tri / 48–25000 quad; never auto-selected). The CLI normalizes these aliases to the wire versions (`v3.1-20260211` / `P1-20260311` / `P2-20260801`) the server actually accepts. Full table with status (GA / Preview / Legacy) and official links: `tripo docs --topic commands/generate` → "Models & Versions".
24
45
  - `-n, --candidates <n>` — up to 4 parallel candidates with different seeds (interactive pick when chaining).
25
46
  - `--seed <n>` — fixed model_seed for reproducible geometry.
26
47
  - `-p, --param key=value` — extra API parameter, repeatable. Common: `texture=false pbr=false` (bare geometry, cheaper), `texture_quality=detailed`, `face_limit=15000`, `quad=true` (forces FBX), `auto_size=true`, `negative_prompt=...`.
@@ -30,18 +51,62 @@ tripo make <input...> [options]
30
51
  - `--name <name>` — history name for `@name` references.
31
52
  - `--notify` — desktop notification when done.
32
53
  - `--timeout <seconds>` — watch timeout (default 1800).
54
+ - `--yes` (global) — skip the plan-card confirmation; implied when headless.
55
+ - `--dry-run` — plan + validate only; nothing is uploaded or submitted, no credits spent. See below.
56
+
57
+ ## Dry run (agents: validate before spending)
58
+
59
+ `tripo make ... --dry-run --json` runs the whole planning stage with **zero network calls** and prints the plan as the result on stdout: the model the dual-model rule picked, the completed request body for every call (scenario defaults merged, aliases normalized to wire values, illegal params stripped, step defaults filled in), and the outcome of every local validation. Works without an API key.
60
+
61
+ ```bash
62
+ tripo make concept.png --for game-mobile --then rig,convert:fbx --dry-run --json
63
+ ```
64
+
65
+ ```json
66
+ {"dry_run":true,"valid":true,
67
+ "input":"concept.png","scenario":"game-mobile",
68
+ "model":"P1-20260311","model_reason":"explicit --model",
69
+ "steps":[
70
+ {"name":"image-to-model","endpoint":"/v3/generation/image-to-model","task_type":"image_to_model",
71
+ "payload":{"input":"<upload:concept.png>","model":"P1-20260311","texture":true,"pbr":true,"texture_quality":"standard","face_limit":15000}},
72
+ {"name":"rig","endpoint":"/v3/animations/rig","task_type":"animate_rig","payload":{"input":"<upstream>","model":"v2.5-20260210"}},
73
+ {"name":"convert","endpoint":"/v3/models/convert","task_type":"convert_model","payload":{"input":"<upstream>","format":"FBX"}}],
74
+ "pending_uploads":["concept.png"],
75
+ "deliverables":["FBX","GLB"],
76
+ "candidates":1,
77
+ "warnings":["--then overrides the default game-mobile processing chain"],
78
+ "errors":[],
79
+ "cost_notes":[]}
80
+ ```
81
+
82
+ - `valid` / `errors` — `errors` are blocking (illegal chain order, out-of-range `face_limit`, quad + GLTF, missing `animation` for retarget, ...). The real run would exit 2 on them before spending anything; the dry run exits 2 too, so branch on the exit code or on `valid`.
83
+ - `warnings` — adjustments the CLI applies automatically (e.g. `P1 does not support quad (removed)`); safe to proceed, but check they match your intent.
84
+ - `steps[].payload` — the exact body that would be POSTed. Local files show as `<upload:path>` (also listed in `pending_uploads`); chain steps use `<upstream>` for the task id that does not exist yet.
85
+ - A bare task id with no local history entry cannot have its type checked offline: chain legality is reported as unverified in `warnings` and checked by the real run.
86
+ - Without `--json` the same plan card as the real run is printed on stdout, followed by the warnings/errors and a verdict line.
87
+
88
+ Typical agent loop: dry-run → fix `errors` / adjust params → dry-run again until `valid` → drop `--dry-run` and run for real.
33
89
 
34
90
  ## Output (stdout, --json)
35
91
 
36
92
  ```json
37
- {"task_id":"...","type":"convert_model","status":"success","credits_consumed":25,
93
+ {"task_id":"...","type":"convert_model","status":"success",
94
+ "credits_consumed":145,
95
+ "credits_breakdown":[
96
+ {"task_id":"<generation>","type":"text_to_model","credits_consumed":110},
97
+ {"task_id":"<texture>","type":"texture_model","credits_consumed":30},
98
+ {"task_id":"<convert>","type":"convert_model","credits_consumed":5}],
38
99
  "output_dir":"tripo-out/knight-1a2b3c4d","files":["model.fbx","preview.png","task.json"],
39
100
  "model_file":".../model.fbx","preview":".../preview.png",
40
- "chain":[{"task_id":"...","type":"texture_model"}]}
101
+ "source_task_id":"<generation>",
102
+ "chain":[{"task_id":"<texture>","type":"texture_model","credits_consumed":30},{"task_id":"<convert>","type":"convert_model","credits_consumed":5}]}
41
103
  ```
42
104
 
105
+ - Single task (no `--then`): `credits_consumed` is that task's spend; no `credits_breakdown`.
106
+ - Chained run: `credits_consumed` is the **total** across generation + every step, and `credits_breakdown` lists each task in run order (generation first). Human mode prints the same as `credits consumed (total): 145` followed by one indented line per task.
107
+
43
108
  Exit codes: 0 ok · 2 params · 3 auth · 4 credits · 5 content policy · 6 task failed · 7 network · 9 rate limit.
44
109
 
45
110
  ## Cost behavior
46
111
 
47
- Balance is pre-checked before submitting; failed tasks auto-refund frozen credits. `texture=false pbr=false` skips texture credits entirely (right for 3D printing).
112
+ The plan card shows every call before anything is submitted; balance is pre-checked before submitting; failed tasks auto-refund frozen credits. After the run, `credits_consumed` / `credits_breakdown` report what was actually billed per task. `texture=false pbr=false` skips texture credits entirely (right for 3D printing).
@@ -4,7 +4,7 @@
4
4
  | --- | --- | --- | --- |
5
5
  | 3 | 1002 | authentication failed (invalid/missing API key) | set `TRIPO_API_KEY` or run `tripo login`; check the key wasn't revoked |
6
6
  | 3 | 1005 | forbidden (no permission for this resource) | check account permissions |
7
- | 2 | 1003/1004 | malformed request / invalid parameter | check spelling against `tripo docs --topic commands/<cmd>`; the CLI already strips illegal P1 params. Classic 1004s: `generate_parts=true` with texture/pbr on, `ref_image` on segment v1, `quad=true` on P1, non-GLB input to smartsegment `model` / rig-check |
7
+ | 2 | 1003/1004 | malformed request / invalid parameter | check spelling against `tripo docs --topic commands/<cmd>`; the CLI already strips illegal P1 params. Classic 1004s: `generate_parts=true` with texture/pbr on, `ref_image` on segment v1, `quad=true` on P1, non-GLB input to smartsegment `model` / rig-check. `invalid model`: the API only accepts dated wire values (`P2-20260801`, `v3.1-20260211`…) — pass the CLI alias (`--model tripo-p2`) and let the CLI normalize, or the wire value itself; never `tripo-*` straight to the API. Table: `commands/generate` § "Models & Versions" |
8
8
  | 9 | 1007 | per-API-key rate limit | wait and retry with backoff; honour `Retry-After` / `X-RateLimit-Reset` headers |
9
9
  | 9 | 2000 | per-account concurrency pool full (see table below) | wait for a task in that category to finish, or lower `--concurrency` in batch; other categories are unaffected |
10
10
  | 8 | 2001 | task not found | task ids are account-scoped — same key that created it? right region (ov/cn)? |
@@ -45,6 +45,8 @@ Pools are independent: a full H-series pool does not block P-series or image tas
45
45
  - multiview without a front view → rejected (name a file "front" or pass it first)
46
46
  - `-n` with `--then` needs an interactive terminal (pick the winner first, then chain)
47
47
 
48
+ All of these surface in `tripo make ... --dry-run --json` as `errors` (blocking) or `warnings` (auto-adjusted) before anything is uploaded or submitted.
49
+
48
50
  ## Weird-but-normal
49
51
 
50
52
  - `model_url` in task output points to a file that may be named `pbr_model` etc. — normal, just download it (the CLI does).