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.
Requirements
Section titled “Requirements”- Node 24 or later. Declared in
engines. npm warns withEBADENGINEon an older version rather than refusing to install, so nothing stops you at install time. The library itself then warns once on first use — atmakeProviderorcompleteValidatedJSON— 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
exportsmap exposes animportcondition and norequireone.
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.
Install
Section titled “Install”npm install @hawkeyexl/inferenceThree runtime dependencies come with it. Local models need one more, and it is an optional peer dependency you install only if you want it.
Make a call
Section titled “Make a call”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.
// 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:
node examples/first-call.mjsresult: { summary: 'Covers authentication and token refresh.' }usage: { inputTokens: 500, outputTokens: 100 }identity: mock mock-model cached: falsefailed.result: undefinedfailed.error: Response failed schema validation: must have required property 'summary'; must NOT have additional propertiesWhat just happened
Section titled “What just happened”-
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.
-
The response was validated with Ajv. If it fails, the call is retried once.
attemptsdefaults to2— one call plus one retry. -
A failure was recorded, not thrown. The second call returned an
InferenceRunwitherrorset andresultabsent.
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.
What comes back
Section titled “What comes back”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.
Which layer do you need?
Section titled “Which layer do you need?”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.