Skip to content

Get started

Install, then make a real validated call — happy path and failure branch — without an API key.

By the end you will know the shape of the contract, what happens when a model returns something unusable, and which of the two layers you actually need.

  • Node 24 or later. Declared in engines. npm warns with EBADENGINE on an older version rather than refusing to install, so nothing stops you at install time. The library itself then warns once on first use — at makeProvider or completeValidatedJSON — naming the version you are on. It does not refuse to run: nothing here uses a Node-24-only API, so 24 is the support floor rather than a hard one. Treat either warning as an error.
  • ESM only. The exports map exposes an import condition and no require one.

require("@hawkeyexl/inference") therefore fails with ERR_PACKAGE_PATH_NOT_EXPORTED on every Node version. From CommonJS, use a dynamic import instead — that works:

const { completeValidatedJSON } = await import("@hawkeyexl/inference");

If either constraint is a problem, better to know now than after writing code.

Terminal window
npm install @hawkeyexl/inference

Three runtime dependencies come with it. Local models need one more, and it is an optional peer dependency you install only if you want it.

You do not need a provider account to start. MockProvider is exported for exactly this — it satisfies the same contract a real provider does, returns responses you script, and never touches the network.

examples/first-call.mjs
// One schema-constrained call, and the failure branch beside it.
// Runs with no API key: MockProvider stands in for a real provider.
import { MockProvider, completeValidatedJSON } from "@hawkeyexl/inference";
const schema = {
type: "object",
required: ["summary"],
properties: { summary: { type: "string" } },
additionalProperties: false,
};
// A provider scripted to return a valid response.
const ok = new MockProvider([{ json: { summary: "Covers authentication and token refresh." } }]);
const run = await completeValidatedJSON({
provider: ok,
system: "You summarize documentation pages.",
user: "# Authentication\nUse a bearer token. Refresh it every 24 hours.",
schema,
});
console.log("result:", run.result);
console.log("usage:", run.usage);
console.log("identity:", run.provider, run.model, "cached:", run.cached);
// A provider scripted to return something the schema rejects, every time.
// completeValidatedJSON tries twice by default, then gives up honestly.
const bad = new MockProvider([{ json: { oops: true } }]);
const failed = await completeValidatedJSON({
provider: bad,
system: "You summarize documentation pages.",
user: "# Authentication\nUse a bearer token.",
schema,
});
// No throw, and no invented result. The error is recorded on the run.
console.log("failed.result:", failed.result);
console.log("failed.error:", failed.error);

Run it:

Terminal window
node examples/first-call.mjs
result: { summary: 'Covers authentication and token refresh.' }
usage: { inputTokens: 500, outputTokens: 100 }
identity: mock mock-model cached: false
failed.result: undefined
failed.error: Response failed schema validation: must have required property 'summary'; must NOT have additional properties
  1. You described the output with a JSON Schema. Not a prompt instruction — an actual schema. The provider uses whatever native mechanism it has to constrain the model to it.

  2. The response was validated with Ajv. If it fails, the call is retried once. attempts defaults to 2 — one call plus one retry.

  3. A failure was recorded, not thrown. The second call returned an InferenceRun with error set and result absent.

That third point is the one to internalise:

An errored run is recorded, never dropped and never coerced.

completeValidatedJSON does not throw on a model failure, and it never invents a result to keep going. Your code checks run.error and decides. This is deliberate, and everything downstream — including the judge layer’s refusal to auto-pass an ensemble containing an error — is built on it.

InferenceRun carries the result and everything you need to account for the call:

Field Type Notes
result T | undefined Absent when the run errored
error string | undefined Present when the run errored
provider string Stable provider id — feeds cache keys
model string Feeds cache keys and price lookups
cached boolean true when replayed from cache; costs nothing
usage TokenUsage | undefined Absent when the provider does not report it
durationMs number Wall-clock for the run

Full signatures are in the completion reference.

Everything exports from the package root, but the two layers are independent.

You want to… Use Start at
Get one structured object per subject Completion only Structured extraction
Turn N model opinions into a defensible decision Judge, built on completion Judge & consensus
Run with no API key at all Either, with a local provider Run models locally

Point the same call at a real model: choose a provider.