LLM and Reduce
Get validated JSON from a language model with client.llm, and choose the best of many results, vote, join or merge them with client.reduce.
client.llm asks a language model for structured output: you send a system prompt, an input and a JSON Schema, and you get back an object that matches the schema. client.reduce runs the Reduce fan-in step on its own: it picks the best of many results, counts them, votes, joins text or merges JSON, the job the Choose Best node does in a workflow. Both cost credits by model tier.
Methods
| Method | What it does |
|---|---|
llm.structured(input) | Get a validated object from a language model, in one request |
llm.structuredJob(input) | The same call as a job you poll |
reduce.run(input) | Pick, count, vote, join or merge many inputs into one |
client.llm
Structured output: your system prompt and JSON Schema in, a validated object out. The platform chooses the model lane, forces JSON output, validates it against your schema, and feeds invalid answers back to the model before it gives up. It is billed as llm-structured, by model tier.
llm.structured(input)
Asks the model and waits for the answer (POST /v1/llm/structured). A call can take several minutes, which is longer than the client's default 60-second timeout. Create the client with a larger timeoutMs for it, or use structuredJob().
structured<T>(input: LlmStructuredInput): Promise<{
jobId: string
output: T
usage: { inputTokens: number; outputTokens: number }
}>Prop
Type
type Plan = { title: string; scenes: string[] }
const { output } = await client.llm.structured<Plan>({
system: "You write production plans for short films.",
input: "A rainy chase through Rome, 60 seconds.",
jsonSchema: {
type: "object",
properties: {
title: { type: "string" },
scenes: { type: "array", items: { type: "string" } },
},
required: ["title", "scenes"],
},
schemaName: "production_plan",
})
console.log(output.title, output.scenes.length)llm.structuredJob(input)
The same call as a job (POST /v1/llm/structured/jobs). It returns a jobId at once; poll it with client.jobs.getStatus(). A job can also draft from a video: the platform analyzes the video first, then adds the analysis to your input.
structuredJob(input: LlmStructuredJobInput): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.llm.structuredJob({
system: "You write production plans.",
input: "A rainy chase through Rome.",
jsonSchema: { type: "object", properties: { title: { type: "string" } }, required: ["title"] },
origin: "my-app",
label: "Rome chase",
})
// later, even from another session
const { data } = await client.jobs.getStatus(jobId)
if (data.status === "completed") {
console.log((data.output_data as { output: { title: string } }).output.title)
}
const { data: runs } = await client.jobs.list({ type: "llm-structured", origin: "my-app" })While the job runs, its output_data holds stage: analyzing (for video drafts) or drafting. When it completes, output_data holds output, inputTokens and outputTokens, plus analysisJobId and analysisCredits for a video draft.
analysisJobIdfails with a 422 when the job is not yours, does not exist, or is not a finished video analysis. The codes areanalysis_not_found,not_analysis,analysis_failed,analysis_not_readyandinvalid_analysis.- A platform without this route throws
NotFoundError. - A self-hosted instance that sends its language-model calls to Nodaro Cloud answers
503 provider_unavailable. Treat it as unavailable on that instance, not as a passing error.
client.reduce
reduce.run(input)
Reduces many inputs to one. It mirrors the MCP reduce tool and the Choose Best node.
run(input: ReduceInput): Promise<ReduceResult>Prop
Type
| Strategy | strategyConfig | What it returns |
|---|---|---|
pick-best-llm | { criteria, inputKind?, llmModel? }. inputKind is "text" or "image-url". llmModel chooses the judge, and its credit tier applies. | The input a language model judges best, with its index and reasoning |
concat | { separator? }, a blank line by default | Every input joined into one text |
first-non-empty | none | The first input that is not empty |
count | none | The number of inputs |
vote | { caseSensitive? }, false by default | The most frequent input. A tie goes to the first. |
merge-json | { strategy? }: "deep" (the default) or "shallow" | The JSON inputs merged into one object |
const result = await client.reduce.run({
strategyId: "pick-best-llm",
strategyConfig: { criteria: "The sharpest image with no artifacts", inputKind: "image-url" },
inputs: [url1, url2, url3, url4, url5],
})
console.log(result.output) // the chosen URL
console.log(result.meta.selectedIndex) // 0 to 4
console.log(result.meta.reasoning) // why the model chose itThe result is { jobId, output, meta }. output is the chosen or combined value, as a string. meta.summary is always set. pick-best-llm and vote set meta.selectedIndex, and pick-best-llm also sets meta.reasoning.
// Majority vote
const winner = await client.reduce.run({ strategyId: "vote", inputs: ["red", "blue", "red"] })
// Deep-merge JSON fragments
const merged = await client.reduce.run({
strategyId: "merge-json",
inputs: [JSON.stringify({ a: 1, nested: { x: 1 } }), JSON.stringify({ b: 2, nested: { y: 2 } })],
})
JSON.parse(merged.output) // { a: 1, b: 2, nested: { x: 1, y: 2 } }When every input is empty or only whitespace, the call fails with a NodaroError whose status is 400 and code is no_valid_inputs. Credits are reserved like every generation, so a short balance throws InsufficientCreditsError.
Frequently asked questions
Related
Choose Best
Run nodes
Jobs and executions
Pickers, presets and prompts
Last updated on
Apps and templates
Browse and run published Nodaro apps from TypeScript, read their run history, clone workflow templates into a project, and list the tutorials.
Media and uploads
Upload files, list your media library, download social videos, trim clips, burn captions and composite images and videos from TypeScript with the Nodaro SDK.