Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5f0f7d0
Make Router model pages practical and retry-safe
stale2000 Sep 5, 2026
5edbe8f
Keep model-page footers focused on first calls
stale2000 Sep 8, 2026
0f61579
Use plain retry language in model snippets
stale2000 Sep 8, 2026
57e3a1d
Describe Router error headers as responses
stale2000 Sep 8, 2026
2b82dbc
Make generated model pages honest and useful
stale2000 Sep 9, 2026
ceb74d0
Keep model-page improvements compatible with the latest schema sync
stale2000 Sep 10, 2026
c123bf7
Keep curl examples concise
stale2000 Sep 10, 2026
3d610e5
Let TypeScript examples use automatic SDK idempotency
stale2000 Sep 10, 2026
042d08b
Use the SDK default timeout in TypeScript examples
stale2000 Sep 10, 2026
9a68f59
Use the Python SDK default timeout in model examples
stale2000 Sep 10, 2026
d5c20c4
Keep model examples focused on the request and response
stale2000 Sep 10, 2026
5f31284
Preserve the original curl example setup
stale2000 Sep 10, 2026
491bccb
Keep curated model pages to one request and response example
stale2000 Sep 10, 2026
a1a1888
Keep type fallback and response examples within the requested scope
stale2000 Sep 10, 2026
8039323
Make the Opus example identify the model being called
stale2000 Sep 10, 2026
d7d2b79
Make shared response examples identify the documented model
stale2000 Sep 10, 2026
89ee88c
Keep only focused model-page regression tests
stale2000 Sep 10, 2026
14e8525
Keep this PR free of the added generator test file
stale2000 Sep 10, 2026
665fd41
Use the verified BytePlus model identifier in its example
stale2000 Sep 10, 2026
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
24 changes: 14 additions & 10 deletions .github/scripts/snippets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ Pages come in two kinds, and both are generated:
`router-schemas/<provider>/<model>.json` alone, the document the spec-sync bot
commits from `GET /v2/models/<provider>/<model>/openapi.json`.

A derived page says only what Router has authored. The OUTPUT schema is authored
for every model, so the response is documented in full. The INPUT schema usually
is not (`x-comfy-input-schema-authored: false` means Router forwards the body to
the provider unvalidated and cannot state its fields), so the page says exactly
that and points at the provider instead of inventing a request shape, and it
carries no example unless the served document has one. Writing a `code.yaml`
upgrades a derived page to a curated one; nothing else has to change.
A derived page uses the synced Router schema. When
`x-comfy-input-schema-authored` is false, Router forwards the open input object
without model-specific validation, so the page links to the provider's input
documentation. Provider validation still applies. Output schemas and examples
remain available when published. Without a non-empty request example, the page
shows **Request setup** instead of runnable snippets with an empty body.

These pages live in the **developer** section, not under `tutorials/`: the
tutorials tree is for end users driving the nodes in the app, and mixing API
Expand Down Expand Up @@ -78,17 +77,22 @@ commit.

## Schema sections

Every Code page ends with a Schema section (Input, Output) and an Examples
section (Input, Output). They render from `router-schemas/<provider>/<model>.json`,
Every Code page includes a Schema section and the examples available for it.
Schema fields render from `router-schemas/<provider>/<model>.json`,
which is the exact body of `GET https://api.comfy.org/v2/models/<provider>/<model>/openapi.json`
(a standalone OpenAPI document; the spec-sync bot drops these in, do not hand
write them). When the file is absent, or reports
`x-comfy-input-schema-authored: false`, the page falls back to the spec's
`input` / `output` JSON Schema blocks (rendered as the same ParamField /
ResponseField list) and `example` / `result.example`, with a note that Router
ResponseField list), with a note that Router
has not published the schema yet. Variants that resolve to the same
schema share one block; variants with different schemas get tabs.

Curated pages pair their `example` with `result.example`.
Derived pages use the synced examples with provider model identifiers adjusted for the page.
Fix other incorrect fixture data in the source contract rather than
editing synced JSON snapshots.

## Provider drift check

`pnpm code-pages:check-providers` (`check-provider-schemas.ts`) fetches each
Expand Down
117 changes: 68 additions & 49 deletions .github/scripts/snippets/gen-code-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,8 @@ function pythonSnippet(model: string, example: Record<string, unknown>, files: F
.join("\n");
return `${files.length ? "import base64\n\n" : ""}from comfy_sdk import Comfy
${reads ? `\n${reads}\n` : ""}
# Reads COMFY_API_KEY from the environment. Each call sends a fresh
# Idempotency-Key and waits up to 10 minutes for the finished result.
# Reads COMFY_API_KEY from the environment.
# The SDK automatically creates an idempotency key and reuses it for automatic retries.
with Comfy() as client:
result = client.models.run(
"${model}",
Expand All @@ -331,8 +331,8 @@ function typescriptSnippet(model: string, example: Record<string, unknown>, file
.map(([k, v]) => ` ${/^[a-zA-Z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)}: ${tsLiteral(v, 2, files, k)},`)
.join("\n");
return `${imports}
${reads ? `${reads}\n\n` : ""}// Reads COMFY_API_KEY from the environment. Each call sends a fresh
// Idempotency-Key and waits up to 10 minutes for the finished result.
${reads ? `${reads}\n\n` : ""}// Reads COMFY_API_KEY from the environment.
// The SDK automatically creates an idempotency key and reuses it for automatic retries.
type Result = ${tsResultType(resultPath)};
const { data } = await comfy.models.run<Result>("${model}", {
${body}
Expand Down Expand Up @@ -426,7 +426,9 @@ function deref(schema: any, components: Record<string, any>, depth = 0): any {

function typeLabel(schema: any, components: Record<string, any>): string {
const s = deref(schema, components);
if (s.oneOf || s.anyOf) return (s.oneOf ?? s.anyOf).map((x: any) => typeLabel(x, components)).join(" | ");
if (s.oneOf || s.anyOf) {
return [...new Set((s.oneOf ?? s.anyOf).map((x: any) => typeLabel(x, components)))].join(" | ");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Collapses duplicate labels such as any | any without removing schema alternatives.

}
if (s.const !== undefined) return JSON.stringify(s.const);
if (s.enum) return s.enum.map((v: unknown) => `\`${String(v)}\``).join(", ");
if (s.type === "array") return `${typeLabel(s.items ?? {}, components)}[]`;
Expand Down Expand Up @@ -457,6 +459,13 @@ const mdxText = (v: unknown) =>

/** Render a JSON Schema object as Mintlify ParamField (input) or ResponseField (output) blocks. */
function schemaFields(schema: any, components: Record<string, any>, kind: "param" | "response", docBase?: string): string {
const root = deref(schema, components);
const variants = Object.entries<string>(root.discriminator?.mapping ?? {});
if (variants.length) {
return variants
.map(([name, ref]) => `#### \`${name}\` variant\n\n${schemaFields({ $ref: ref }, components, kind, docBase)}`)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renders Meshy’s preview and refine schemas separately so their required fields remain visible.

.join("\n\n");
}
const blocks: string[] = [];
const walk = (s: any, prefix: string, depth: number) => {
s = deref(s, components);
Expand Down Expand Up @@ -500,7 +509,7 @@ function schemaFields(schema: any, components: Record<string, any>, kind: "param
}
};
walk(schema, "", 0);
if (!blocks.length) return "_The schema declares no fixed fields: any JSON object is accepted._";
if (!blocks.length) return "_This schema does not declare named properties._";
return blocks.join("\n\n");
}

Expand All @@ -518,8 +527,7 @@ function sectionBlocks(v: Variant, spec: Spec) {
const notPublished = checked
? `_Fields follow ${possessive(spec.provider)} published API specification and are checked against it in CI. Router's own schema for this model is not published yet, so requests are forwarded to the provider unvalidated._`
: `<Note>\nRouter has not published an authored input schema for this model yet: \`GET ${ROUTE}/${v.model}/openapi.json\` returns an open object with \`x-comfy-input-schema-authored: false\`. The fields below follow the provider's own API documentation and are not yet validated server side.\n</Note>`;
// `x-comfy-input-schema-authored: false` disqualifies the whole served document, not just its input
// half: the page then reads its fields AND its examples from the spec, as the README describes.
// Curated provider fields replace an unauthored Router input schema.
const published = s?.authored ? s : null;
let input: string;
const docBase = PROVIDER_DOC_BASE[providerOf(v.model)];
Expand All @@ -530,7 +538,7 @@ function sectionBlocks(v: Variant, spec: Spec) {
} else {
input = `${notPublished}\n\n${fields}`;
}
const inputExample = JSON.stringify(published?.inputExample ?? example, null, 2).replace(/"@file:([^"]+)"/g, '"<base64 of $1>"');
const inputExample = JSON.stringify(example, null, 2).replace(/"@file:([^"]+)"/g, '"<base64 of $1>"');
let output: string;
if (published?.output) {
output = schemaFields(published.output, published.components, "response", docBase);
Expand All @@ -539,7 +547,7 @@ function sectionBlocks(v: Variant, spec: Spec) {
} else {
output = `Router returns ${possessive(spec.provider)} native output unchanged and does not publish an output schema for this model. The ${spec.result.label} is at \`${spec.result.path}\`; the example below is representative of the provider's response.`;
}
const outputExample = JSON.stringify(published?.outputExample ?? spec.result.example, null, 2);
const outputExample = JSON.stringify(spec.result.example, null, 2);
return { input, inputExample, output, outputExample };
}

Expand Down Expand Up @@ -661,18 +669,11 @@ ${body}
// its page from that document alone, so the sidebar tracks the catalog instead of
// tracking who found time to write a spec.
//
// What such a page can honestly say is bounded by what Router has authored. The
// OUTPUT schema is authored for every model, so the response is documented in
// full. The INPUT schema mostly is not (`x-comfy-input-schema-authored: false`
// means Router forwards the body to the provider unvalidated and cannot state its
// fields), so the page says exactly that and points at the provider rather than
// inventing a request shape. No example is fabricated: a derived page shows an
// example only when the served document carries one.
// Document only the schemas and examples that are available. A missing request
// example produces setup guidance, not an empty executable request. Provider
// validation still applies when Router has no authored input schema.
// ---------------------------------------------------------------------------

/** The one-line body placeholder for a model whose request fields Router does not publish. */
const BODY_HINT = "Request fields are the provider's own \u2014 see Input below.";

/**
* The published input example, when it is a JSON object we can render as a body.
*
Expand All @@ -683,9 +684,9 @@ const BODY_HINT = "Request fields are the provider's own \u2014 see Input below.
* field. Inlining it is what makes the quick start copy-pasteable rather than a
* shape the reader has to assemble from the Input table below.
*
* Anything else -- absent, null, or a non-object -- falls back to BODY_HINT.
* That is the unauthored case, where Router genuinely cannot state the fields
* and a fabricated body would be worse than an honest placeholder.
* Anything else -- absent, null, an empty object or a non-object -- cannot make
* a runnable snippet. The page keeps the value as reference data and renders
* request setup guidance instead of fabricating a body.
*/
function bodyExample(example: unknown): Record<string, unknown> | undefined {
if (example === null || typeof example !== "object" || Array.isArray(example)) return undefined;
Expand All @@ -699,22 +700,13 @@ function derivedSnippets(model: string, example?: unknown): string {
// the JSON in the Examples section -- the invariant stated at the top of this
// file. Derived pages have no file inputs, so the FileInput list is empty.
const body = bodyExample(example);
const pyBody = body
? Object.entries(body).map(([k, v]) => ` ${JSON.stringify(k)}: ${pyLiteral(v, 12, [], k)},`).join("\n")
: ` # ${BODY_HINT}`;
const tsBody = body
? Object.entries(body).map(([k, v]) => ` ${/^[a-zA-Z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)}: ${tsLiteral(v, 2, [], k)},`).join("\n")
: ` // ${BODY_HINT}`;
const esc = (v: unknown) => JSON.stringify(v).replace(/[\\$`"]/g, (c) => `\\${c}`);
const curlJson = body
? `{${Object.entries(body).map(([k, v]) => `${esc(k)}: ${esc(v)}`).join(", ")}}`
: "{}";
const curlHint = body ? "" : `# ${BODY_HINT}\n`;

if (!body) return "";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Missing or empty examples now produce setup guidance instead of runnable {} calls.

const pyBody = Object.entries(body).map(([k, v]) => ` ${JSON.stringify(k)}: ${pyLiteral(v, 12, [], k)},`).join("\n");
const tsBody = Object.entries(body).map(([k, v]) => ` ${/^[a-zA-Z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)}: ${tsLiteral(v, 2, [], k)},`).join("\n");
const python = `from comfy_sdk import Comfy

# Reads COMFY_API_KEY from the environment. Each call sends a fresh
# Idempotency-Key and waits up to 10 minutes for the finished result.
# Reads COMFY_API_KEY from the environment.
# The SDK automatically creates an idempotency key and reuses it for automatic retries.
with Comfy() as client:
result = client.models.run(
"${model}",
Expand All @@ -726,18 +718,14 @@ ${pyBody}
print(result)`;
const typescript = `import { comfy } from "@comfyorg/sdk";

// Reads COMFY_API_KEY from the environment. Each call sends a fresh
// Idempotency-Key and waits up to 10 minutes for the finished result.
// Reads COMFY_API_KEY from the environment.
// The SDK automatically creates an idempotency key and reuses it for automatic retries.
const { data } = await comfy.models.run("${model}", {
${tsBody}
});

console.log(data);`;
const curl = `${curlHint}curl ${BASE_URL}${ROUTE}/${model} \\
-H "X-API-Key: $COMFY_API_KEY" \\
-H "Idempotency-Key: $(uuidgen)" \\
-H "Content-Type: application/json" \\
-d "${curlJson}"`;
const curl = curlSnippet(model, body, []);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reuses the shared cURL emitter so curated and derived pages cannot drift.

return `<CodeGroup>
\`\`\`python Python
${python}
Expand All @@ -753,20 +741,51 @@ ${curl}
</CodeGroup>`;
}

// Adapt shared response fixtures for display only; never rewrite synced schemas.
// Provider ID/version rules: https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions
// https://docs.cloud.google.com/gemini-enterprise-agent-platform/reference/models/inference
function responseExampleForModel(model: string, example: unknown): unknown {
if (!example || typeof example !== "object" || Array.isArray(example)) return example;
const provider = providerOf(model);
if (!["anthropic", "vertexai", "byteplus", "luma", "luma_2", "xai"].includes(provider)) return example;
const aliases: Record<string, string> = {
// https://docs.x.ai/developers/models/grok-imagine-video-1.5-preview
"xai/grok-imagine-video-1.5-preview": "grok-imagine-video-1.5",
// https://docs.byteplus.com/en/docs/Byteplus_LAS/video_gen_enhanced
"byteplus/dreamina-seedance-2-0-mini": "dreamina-seedance-2-0-mini-260615",
};
const id = aliases[model] ?? modelOf(model);
const sample = { ...example } as Record<string, any>;
const field = provider === "vertexai" ? "modelVersion" : "model";
if (typeof sample[field] === "string") sample[field] = id;
if (provider === "luma" && typeof sample.request?.model === "string") {
sample.request = { ...sample.request, model: id };
}
return sample;
}

function renderDerivedPage(model: string, s: ModelSchema): string {
const provider = providerLabel(providerOf(model));
const setup = `Create a key at [platform.comfy.org/profile/api-keys](https://platform.comfy.org/profile/api-keys) and export it as \`COMFY_API_KEY\`. The Python and TypeScript snippets use the Comfy SDKs (\`pip install comfy-sdk\`, \`npm install @comfyorg/sdk\`); the cURL snippet is the same call over raw HTTP.`;
const requestExample = bodyExample(s.inputExample);
const clients = requestExample
? `The Python and TypeScript snippets use the Comfy SDKs (\`pip install comfy-sdk\`, \`npm install @comfyorg/sdk\`); the cURL snippet is the same call over raw HTTP.`
: `For Python, run \`pip install comfy-sdk\`. For TypeScript, run \`npm install @comfyorg/sdk\`. cURL uses raw HTTP.`;
const setup = `Create a key at [platform.comfy.org/profile/api-keys](https://platform.comfy.org/profile/api-keys) and export it as \`COMFY_API_KEY\`. ${clients}`;
const docBase = PROVIDER_DOC_BASE[providerOf(model)];
const apiDocs = PROVIDER_API_DOCS[providerOf(model)];
const input = s.authored && s.input
? `${schemaFields(s.input, s.components, "param", docBase)}\n\nGenerated from the schema Router serves at \`GET ${ROUTE}/${model}/openapi.json\`, the same document it validates a call against before the request reaches the provider.`
: `<Note>\nRouter has not published an authored input schema for this model yet: \`GET ${ROUTE}/${model}/openapi.json\` returns an open object with \`x-comfy-input-schema-authored: false\`. Router forwards the body to ${provider} unchanged, so ${apiDocs ? `[${provider}'s own API reference](${apiDocs})` : `${provider}'s own API documentation`} is authoritative for the request fields, and nothing is validated server side.\n</Note>`;
: `<Note>\nRouter has not published an authored input schema for this model yet: \`GET ${ROUTE}/${model}/openapi.json\` returns an open object with \`x-comfy-input-schema-authored: false\`. Router forwards the body to ${provider} unchanged, so ${apiDocs ? `[${provider}'s own API reference](${apiDocs})` : `${provider}'s own API documentation`} is authoritative for the request fields, and Router does not perform model-specific input validation. Provider validation still applies.\n</Note>`;
const output = s.output
? schemaFields(s.output, s.components, "response", docBase)
: `Router does not publish an output schema for this model.`;
const outputExample = responseExampleForModel(model, s.outputExample);
const examples = s.inputExample !== undefined || s.outputExample !== undefined
? `\n\n## Examples\n${s.inputExample !== undefined ? `\n### Input\n\n\`\`\`json\n${JSON.stringify(s.inputExample, null, 2)}\n\`\`\`\n` : ""}${s.outputExample !== undefined ? `\n### Output\n\n\`\`\`json\n${JSON.stringify(s.outputExample, null, 2)}\n\`\`\`\n` : ""}`
? `\n\n## Examples\n${s.inputExample !== undefined ? `\n### Input\n\n\`\`\`json\n${JSON.stringify(s.inputExample, null, 2)}\n\`\`\`\n` : ""}${s.outputExample !== undefined ? `\n### Output\n\n\`\`\`json\n${JSON.stringify(outputExample, null, 2)}\n\`\`\`\n` : ""}`
: "";
const requestSetup = requestExample
? derivedSnippets(model, requestExample)
: `<Note>\nThis model has no runnable request example. Build the body from the input documentation below, then use it with the [Router quickstart](/development/comfy-router/quickstart).\n</Note>`;
const title = modelTitle(model);
return `---
title: ${JSON.stringify(`Use ${title} with Comfy Router`)}
Expand All @@ -780,15 +799,15 @@ ${previewNotice.imports}import RouterCodeFooter from "/snippets/comfy-router/mod

API Reference for \`${model}\`, served by Comfy Router from ${provider}.
${previewNotice.body}
## Quick start
## ${requestExample ? "Quick start" : "Request setup"}

${setup}

**Model ID:** \`${model}\`

**Endpoint:** \`POST ${BASE_URL}${ROUTE}/${model}\`

${derivedSnippets(model, s.inputExample)}
${requestSetup}

## Schema

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
```python Python
from comfy_sdk import Comfy

# Reads COMFY_API_KEY from the environment. Each call sends a fresh
# Idempotency-Key and waits up to 10 minutes for the finished result.
# Reads COMFY_API_KEY from the environment.
# The SDK automatically creates an idempotency key and reuses it for automatic retries.
with Comfy() as client:
result = client.models.run(
"anthropic/claude-fable-5-1",
Expand All @@ -44,8 +44,8 @@
```typescript TypeScript
import { comfy } from "@comfyorg/sdk";

// Reads COMFY_API_KEY from the environment. Each call sends a fresh
// Idempotency-Key and waits up to 10 minutes for the finished result.
// Reads COMFY_API_KEY from the environment.
// The SDK automatically creates an idempotency key and reuses it for automatic retries.
const { data } = await comfy.models.run("anthropic/claude-fable-5-1", {
max_tokens: 16,
messages: [
Expand Down Expand Up @@ -81,7 +81,7 @@
</ParamField>

<ParamField body="messages[].content" type="object" required>
Either a string shorthand or an array of content blocks (text, image, document, tool_use, tool_result, ...).

Check warning on line 84 in development/comfy-router/models/anthropic/claude-fable-5-1/code.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

development/comfy-router/models/anthropic/claude-fable-5-1/code.mdx#L84

Did you really mean 'tool_use'?

Check warning on line 84 in development/comfy-router/models/anthropic/claude-fable-5-1/code.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

development/comfy-router/models/anthropic/claude-fable-5-1/code.mdx#L84

Did you really mean 'tool_result'?
</ParamField>

<ParamField body="messages[].role" type="string" required>
Expand Down Expand Up @@ -199,7 +199,7 @@
}
],
"id": "msg_01ExampleInvalidPlaceholder",
"model": "claude-haiku-4-5-20251001",
"model": "claude-fable-5-1",
"role": "assistant",
"stop_reason": "end_turn",
"stop_sequence": null,
Expand Down
Loading
Loading