Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
TypeScript SDK

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

MethodWhat 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.

  • analysisJobId fails with a 422 when the job is not yours, does not exist, or is not a finished video analysis. The codes are analysis_not_found, not_analysis, analysis_failed, analysis_not_ready and invalid_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

StrategystrategyConfigWhat 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 defaultEvery input joined into one text
first-non-emptynoneThe first input that is not empty
countnoneThe number of inputs
vote{ caseSensitive? }, false by defaultThe 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 it

The 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

Last updated on

On this page