@megarouter/react
v0.4.0
Published
React adapter hooks for @megarouter/sdk. UI components are supplied by the host app.
Maintainers
Readme
@megarouter/react
React adapter for @megarouter/sdk.
This package is intentionally not a component library. It is hooks and types
only. It owns the React state transitions around buildFormPlan and
applyFormChange, and leaves all rendering — inputs, switches, selects,
uploaders, layout, styling — to the host application.
Install
npm install @megarouter/react @megarouter/sdk react
# or: pnpm add @megarouter/react @megarouter/sdk react
# or: yarn add @megarouter/react @megarouter/sdk reactReact 18 or newer is required. @megarouter/sdk is a peer dependency because
the host application and this adapter must use the same SDK runtime.
Basic usage
import { useMegarouterForm } from "@megarouter/react";
function ModelForm({ schema }) {
const form = useMegarouterForm({
schema,
context: { capability: "image.generate" },
payload: {
logicalModel: "gpt-image-2",
capability: "image.generate",
mode: "async",
},
});
return (
<form>
{form.plan.visibleFields.map((field) => (
<MyFieldRenderer
key={field.name}
field={field}
value={form.values[field.name]}
error={form.plan.errors.find((e) => e.field === field.name)?.message}
onChange={(value) => form.setValue(field.name, value)}
/>
))}
</form>
);
}MyFieldRenderer can use shadcn, MUI, Ant Design, native inputs, or any
in-house design system. The hook only supplies data and state transitions.
Template forms
Use useMegarouterTemplateForm for business templates. It projects the
template's parameters into the same field plan, but returns
templatePayload for /templates/:code/quote and the unified /jobs
submission endpoint.
import { useMegarouterTemplateForm } from "@megarouter/react";
function TemplateForm({ template }) {
const form = useMegarouterTemplateForm({ template });
async function submit() {
const quote = await client.quoteTemplate(template.code, form.templatePayload);
if (!quote.margin_ok && quote.margin_policy?.mode !== "warn") return;
await client.createJob({
...form.templatePayload,
invocation: { type: "template", code: template.code },
});
}
return form.plan.visibleFields.map((field) => (
<MyFieldRenderer
key={field.name}
field={field}
value={form.values[field.name]}
error={form.errorsByField[field.name]}
onChange={(value) => form.setValue(field.name, value)}
/>
));
}Hook surface
const form = useMegarouterForm({
schema, // ModelFormSchema from /v1/logical_models/.../schema
initialValues, // optional seed values
context, // { capability, logicalModel, tier, version, mode }
runtime, // { evaluateDynamic, cleanupHidden, applyDefaults, ... }
payload, // { logicalModel, capability, mode, policy, ... }
});
form.plan; // FormPlan — fields, visibleFields, groups, errors, changed
form.values; // cleaned values after defaults + hidden cleanup
form.setValue; // (name, value) => void — recomputes the plan
form.setValues; // (next) => void — replace the entire value bag
form.reset; // () => void — back to initialValues
form.payload; // GenerationPayload ready for POST /v1/generations
form.canSubmit; // boolean — false while plan.errors is non-emptysetValue runs the full plan rebuild, which means:
- Hidden values are pruned when a dependency flips visibility off.
- Defaults are reapplied when a field becomes visible again.
plan.errorsandcanSubmitupdate in the same render cycle.
Host-owned FieldRenderer
The hook never picks UI. The host app owns a FieldRenderer that branches on
field.type and field.enum. The example below uses inline native elements
on purpose — it is not a recommendation, just a self-contained illustration.
import type { FieldRendererProps } from "@megarouter/react";
function FieldRenderer({ field, value, error, onChange }: FieldRendererProps) {
// Enum-shaped fields → select / radio / segmented control.
if (field.enum) {
return (
<label>
<span>{field.label}</span>
<select
value={String(value ?? "")}
onChange={(event) => onChange(event.target.value)}
>
{field.enum.map((option) => (
<option key={String(option)} value={String(option)}>
{String(option)}
</option>
))}
</select>
{error ? <em>{error}</em> : null}
</label>
);
}
// Boolean → switch / checkbox.
if (field.type === "bool") {
return (
<label>
<input
type="checkbox"
checked={Boolean(value)}
onChange={(event) => onChange(event.target.checked)}
/>
{field.label}
</label>
);
}
// images / videos / audios → uploader or media picker. Prefer
// ResourceItem-backed MediaAsset[] values: { id, url?, thumbnail_url? }.
// Legacy URL strings still work, but the upload widget itself is host-owned.
if (field.type === "images" || field.type === "videos" || field.type === "audios") {
return (
<MyMediaPicker
kind={field.type}
value={(value as Array<{ id?: string; url?: string; thumbnail_url?: string }>) ?? []}
onChange={onChange}
/>
);
}
// Numeric.
if (field.type === "int" || field.type === "float") {
return (
<label>
<span>{field.label}</span>
<input
type="number"
value={value === undefined || value === null ? "" : Number(value)}
min={field.min}
max={field.max}
step={field.type === "int" ? 1 : "any"}
onChange={(event) => {
const next = event.target.value;
onChange(next === "" ? undefined : Number(next));
}}
/>
{error ? <em>{error}</em> : null}
</label>
);
}
// Default: string / textarea / unknown — host decides between input and
// textarea based on field.name, field.doc, or its own heuristics.
return (
<label>
<span>{field.label}</span>
<input
type="text"
value={String(value ?? "")}
onChange={(event) => onChange(event.target.value)}
/>
{error ? <em>{error}</em> : null}
</label>
);
}The same FieldRenderer works for any model schema — image generation, video
generation, audio, chat — because the runtime hands back UI-neutral metadata.
plan.groups exposes primary and advanced groupings if the host wants to
collapse advanced parameters into an accordion or drawer.
Browser usage with a host backend proxy
API keys must never reach the browser. The standard pattern is:
- The browser-side
MegarouterClientpoints at the host app's own backend route, e.g./api/megarouter. - The host backend route injects
MEGAROUTER_API_KEYfrom a server-only env var, enforces user permissions, and forwards to Megarouter.
// app/components/ImageGenerateForm.tsx (Next.js App Router, or any
// Vite/CRA app — the pattern is identical).
"use client";
import { useEffect, useState } from "react";
import { MegarouterClient, type ModelFormSchema } from "@megarouter/sdk";
import { useMegarouterForm } from "@megarouter/react";
// `baseUrl` points at the host backend, not directly at Megarouter.
// `auth: { type: "cookie" }` tells the client to rely on the host session
// cookie. The host backend is responsible for injecting MEGAROUTER_API_KEY
// before forwarding the request to api.megarouter.ai.
const client = new MegarouterClient({
baseUrl: "/api/megarouter",
auth: { type: "cookie" },
});
export function ImageGenerateForm({ logicalModel }: { logicalModel: string }) {
const [schema, setSchema] = useState<ModelFormSchema | null>(null);
useEffect(() => {
let cancelled = false;
client
.getLogicalModelSchema(logicalModel, { capability: "image.generate" })
.then((res) => {
if (!cancelled) setSchema(res.schema);
});
return () => {
cancelled = true;
};
}, [logicalModel]);
const form = useMegarouterForm({
// schema is guaranteed by the early return below.
schema: schema ?? { params: [] },
context: { capability: "image.generate", logicalModel },
payload: { logicalModel, capability: "image.generate", mode: "async" },
});
if (!schema) return <p>Loading model schema…</p>;
async function onSubmit(event: React.FormEvent) {
event.preventDefault();
if (!form.canSubmit) return;
// The browser POSTs to the host backend proxy, which forwards to
// Megarouter with the server-side API key attached.
await client.createGeneration(form.payload);
}
return (
<form onSubmit={onSubmit}>
{form.plan.visibleFields.map((field) => (
<FieldRenderer
key={field.name}
field={field}
value={form.values[field.name]}
error={form.plan.errors.find((e) => e.field === field.name)?.message}
onChange={(value) => form.setValue(field.name, value)}
/>
))}
<button type="submit" disabled={!form.canSubmit}>
Generate
</button>
</form>
);
}Security: never ship the Megarouter API key to the browser
- Do not set
NEXT_PUBLIC_MEGAROUTER_API_KEY,VITE_MEGAROUTER_API_KEY, or any other public env var with the key. The form runtime intentionally does not readNEXT_PUBLIC_MEGAROUTER_API_KEY. - Keep
MEGAROUTER_API_KEYin server-only env (Next.js Route Handlers, Vite/Astro/Nuxt server endpoints, your own Node/Go backend, etc.). - In the browser, configure
MegarouterClientwithbaseUrl: "/api/megarouter"andauth: { type: "cookie" }(or{ type: "bearer" }with a short-lived session token issued by your own backend). - Use the backend proxy to enforce per-user authorization, quotas, and any custom pricing your platform charges on top of Megarouter base pricing.
Server-only and non-React usage examples live in
@megarouter/sdk.
