Skip to content
Closed
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
22 changes: 22 additions & 0 deletions .github/scripts/snippets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ ResponseField list) and `example` / `result.example`, 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.

A rule that spans two fields rather than constraining one (Ideogram 4.0 takes a
`text_prompt` or a `json_prompt`, never both) has no per-field representation, so
it goes on the schema root as a `oneOf` / `anyOf` whose branches carry nothing but
`required`:

```yaml
input:
type: object
oneOf:
- required: [text_prompt]
- required: [json_prompt]
properties: ...
```

That renders as one prose line above the field list (`oneOf` reads "Provide
exactly one of ...", `anyOf` "Provide at least one of ..."), and the alternated
fields stay individually optional, since neither is required on its own. Branches
that carry anything else (a `type`, an `enum`, properties redefining a root field)
are a choice of shapes instead, and keep rendering per field as `a | b`. The
provider drift check collapses required-only unions, so adding one does not change
what it compares.

## Provider drift check

`pnpm code-pages:check-providers` (`check-provider-schemas.ts`) fetches each
Expand Down
62 changes: 59 additions & 3 deletions .github/scripts/snippets/gen-code-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,55 @@ const attr = (v: unknown) => String(v ?? "").replace(/"/g, """).replace(/\s
const mdxText = (v: unknown) =>
String(v).split(/(`[^`]*`)/).map((part, i) => (i % 2 ? part : part.replace(/[{<]/g, "\\$&"))).join("");

/** `["a", "b", "c"]` becomes "`a`, `b`, or `c`" (Oxford comma from three names up). */
function joinOr(names: string[]): string {
const q = names.map((n) => `\`${n}\``);
if (q.length < 3) return q.join(" or ");
return `${q.slice(0, -1).join(", ")}, or ${q.at(-1)}`;
}

/** The keys a branch may carry and still count as differing from its siblings only by `required`. */
const ALTERNATION_KEYS = new Set(["required", "properties"]);

/**
* A root-level `oneOf` / `anyOf` whose branches differ only by `required` states a rule
* ACROSS fields ("exactly one of text_prompt or json_prompt"), not a choice of shapes.
* JSON Schema has no other way to say that, and a per-field ParamField cannot express it
* either, so it renders as one prose line above the field list. Returns null for a real
* shape union, which `typeLabel` already renders per field as `a | b`.
*/
function rootAlternation(s: any, components: Record<string, any>, kind: "param" | "response") {
const key = s?.oneOf ? "oneOf" : s?.anyOf ? "anyOf" : null;
if (!key || !Array.isArray(s[key]) || s[key].length < 2) return null;
const branches = s[key].map((b: any) => deref(b, components));
const rootProps: Record<string, any> = s.properties ?? {};
for (const b of branches) {
if (!b || typeof b !== "object") return null;
// Any key beyond required/properties (a `type`, an `enum`, a nested union) makes this a
// shape union rather than an alternation, and a branch that requires nothing is degenerate.
if (Object.keys(b).some((k) => !ALTERNATION_KEYS.has(k))) return null;
if (!(Array.isArray(b.required) && b.required.length)) return null;
// A branch may re-state a root property, but must not redefine it to a different shape.
for (const [name, prop] of Object.entries<any>(b.properties ?? {}))
if (name in rootProps && JSON.stringify(rootProps[name]) !== JSON.stringify(prop)) return null;
}
const names = [...new Set(branches.flatMap((b: any) => b.required.map(String)))];
if (names.length < 2) return null;
const count = key === "oneOf" ? "exactly one" : "at least one";
// The output half of a page is describing what came back, not asking for a body.
const prose = kind === "param"
? `Provide ${count} of ${joinOr(names)}.`
: `${count[0].toUpperCase()}${count.slice(1)} of ${joinOr(names)} is present.`;
// The branches can carry the only definitions of the alternated fields, so hand them back
// for the walk: without them those fields would vanish and the page would look empty. The
// root keeps both its authored field order and its definitions; branch-only fields append.
const properties: Record<string, any> = { ...rootProps };
for (const b of branches)
for (const [name, prop] of Object.entries<any>(b.properties ?? {}))
if (!(name in properties)) properties[name] = prop;
return { prose, properties };
}

/** Render a JSON Schema object as Mintlify ParamField (input) or ResponseField (output) blocks. */
function schemaFields(schema: any, components: Record<string, any>, kind: "param" | "response"): string {
const blocks: string[] = [];
Expand Down Expand Up @@ -305,9 +354,16 @@ 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._";
return blocks.join("\n\n");
const root = deref(schema, components);
const alternation = rootAlternation(root, components, kind);
// The alternated fields stay individually optional: neither is required on its own, only the
// choice between them is, which is what the prose line says.
walk(alternation ? { ...root, properties: alternation.properties } : root, "", 0);
if (!blocks.length) {
// "any JSON object is accepted" would contradict the alternation, so the rule stands alone.
return alternation?.prose ?? "_The schema declares no fixed fields: any JSON object is accepted._";
}
return [alternation?.prose, blocks.join("\n\n")].filter(Boolean).join("\n\n");
}

// ---------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions tutorials/partner-nodes/ideogram/ideogram-v4/code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ curl https://api.comfy.org/v2/models/ideogram/ideogram-v4 \

_Fields follow Ideogram's 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._

Provide exactly one of `text_prompt` or `json_prompt`.

<ParamField body="text_prompt" type="string">
Text description of the image. Provide this or `json_prompt`.
</ParamField>
Expand Down
8 changes: 8 additions & 0 deletions tutorials/partner-nodes/ideogram/ideogram-v4/code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ example:
rendering_speed: DEFAULT
input:
type: object
# Ideogram's own spec marks neither prompt field required and declares no root constraint, but
# both field descriptions call them mutually exclusive and a generation needs a prompt, so the
# real contract is exactly one of the two.
oneOf:
- required:
- text_prompt
- required:
- json_prompt
properties:
text_prompt:
type: string
Expand Down
Loading