Skip to main content

infer

infer is a function passed into onCheckpoint on CheckpointContext. It runs an inference request bound to the just-saved checkpoint adapter and returns the raw Response. There is no top-level infer export; the SDK exposes it as a callback argument so that the call is automatically scoped to the right job + checkpoint step.

Signature

Parameters

Tool calling example

Structured-output example

Choosing between responseFormat modes

responseFormat is OpenAI-compatible and is the right knob for ~all “give me JSON” cases. Reach for structuredOutputs only for constraints responseFormat cannot express. strict: true requires additionalProperties: false on every object schema. OpenAI’s strict mode is satisfied only when each type: "object" schema (the root, plus every nested object) explicitly sets additionalProperties: false and lists every property in required. Schemas that omit it are rejected by the backend with a 400 invalid_schema. The triage example above (and the cookbook recipe) follow this rule; copy them when you write your own schema.

structuredOutputs examples

The vLLM-specific extension. Exactly one of json / regex / choice / grammar / json_object per call: the TypeScript type rejects two at once. Don’t combine with a responseFormat constraint either: that would put two constraints into vLLM’s sampling params and the request is rejected at ingress. Fixed choice list. Forces the response to one of a small enumerated set. Useful for classifier-style outputs where any prefix is a regression.
Regex. Constrains the output to a regex match. Fits ID formats, currency strings, structured tokens.
Grammar. EBNF grammar for fully custom shapes. The string is forwarded to vLLM verbatim; see the vLLM structured outputs docs for the supported grammar syntax.
json_object: true. The structuredOutputs equivalent of responseFormat: { type: "json_object" }, present for parity with vLLM’s wire format. Only true is accepted; false is rejected at compile time (the type literal) and at ingress (vLLM only flips into JSON-object mode on a truthy value, so false would silently produce an unconstrained generation). Follows the same one-constraint-per-call rule as the others.

Tool calling round-trip

After the model emits tool_calls, run the tool yourself, append the result as a tool message, and call infer again. Pass the same tools and toolChoice so the second turn sees the same surface.
tool_calls[i].function.arguments is a JSON-encoded string, not a parsed object. JSON.parse it on receipt. tool_call_id on the tool message must match the id from the assistant’s prior tool_calls[i] so the model can attribute the result to the right call.

Type definitions

The supporting types are exported from arkor. Inlined here for reference:
The assistant role splits into two sub-shapes so { role: "assistant" } with neither content nor tool_calls does not type-check; at least one must be present. The [ToolCall, ...ToolCall[]] form encodes the non-empty tool_calls constraint at the type level.

Returns

infer returns Promise<Response>: the raw Fetch Response. The SDK does not parse the body; you decide how to consume it:
When stream: true (the default), the body is an SSE event stream in the same shape Studio’s Playground consumes. The SDK does not currently expose a frame parser for this stream; if you need decoded text deltas, copy the small extractInferenceDelta helper from packages/studio-app/src/lib/api.ts or write a parser around eventsource-parser.

Response envelope (stream: false)

Non-streaming responses are an OpenAI-compatible chat-completion object:
Where the result lands depends on which constraint you used:
  • No constraint or responseFormat: { type: "text" }: choices[0].message.content is plain text.
  • responseFormat: { type: "json_object" } or type: "json_schema": choices[0].message.content is a string containing the JSON. You call JSON.parse yourself; the SDK does not pre-parse.
  • structuredOutputs: { json } or { json_object: true }: same; choices[0].message.content is a JSON string. JSON.parse it.
  • structuredOutputs: { choice } / { regex } / { grammar }: choices[0].message.content is a string matching the constraint. Not JSON; do not parse.
  • tools request that returned a tool call: choices[0].message.tool_calls is populated; content is omitted or null. Each tool_calls[i].function.arguments is itself a JSON-encoded string.
finish_reason: "tool_calls" is the signal the model wants to call a function rather than emit a final answer; loop with the tool calling round-trip.

Errors

infer does not hand you a non-OK Response. The SDK calls into CloudApiClient.chat, which throws a CloudApiError whenever the backend returns a non-2xx status. By the time control returns from await infer(...), you’ve either got a successful Response or an exception. Wrap each call in try / catch (or use .catch()) and branch on err instanceof CloudApiError to read err.status and err.message. The class is exported from arkor for that purpose.
An inference error you don’t catch escapes onCheckpoint, and a throw out of onCheckpoint rejects trainer.wait() immediately; it is not caught by the runtime’s reconnect loop, which retries transport failures only. For non-fatal inference errors, catch inside the callback so a recoverable failure doesn’t abort the whole run.

Constraints

  • infer lives only on CheckpointContext. There is no equivalent for completed jobs from the SDK side; for that path use the cloud-api directly or trigger the run again. Studio’s Playground is the UI-level route to chat with a completed adapter.
  • The call is scoped to { kind: "checkpoint", jobId, step }. You cannot retarget it to a different checkpoint or a different model from inside onCheckpoint.
  • The function is not memoized: every call hits the backend.

Use cases

  • Sanity check during a run. Compare a checkpoint at step 50 to one at step 100 against a fixed prompt. If the loss curve looks fine but outputs are degraded, you find out before the run finishes.
  • Custom early-stopping. Combine with a simple eval prompt: if outputs diverge, abort the run via controller.abort() (see abortSignal) and call trainer.cancel() to stop the backend. See the Early stopping recipe for the full pattern.
  • Live preview into your own UI. Send the checkpoint output to Slack, an internal review queue, or your own app’s preview channel.

See also