Reference#
Relay's public surface is its package entrypoints. There is no command-line tool. Each section below lists one entrypoint's exports and options; the behaviour they share is explained in Concepts.
Entrypoints#
| Import | Contents | Extra requirement |
|---|---|---|
@kontourai/relay |
Core types, ModelInvocationError, fake, recording and replay runtimes, conformance checks, digests |
None |
@kontourai/relay/process |
createProcessRuntime and the codec types |
None |
@kontourai/relay/claude-code |
createClaudeCodeRuntime, createClaudeCodeCodec |
The claude CLI |
@kontourai/relay/codex |
createCodexRuntime, createCodexCodec |
The codex CLI |
@kontourai/relay/opencode |
createOpenCodeRuntime, createOpenCodeCodec |
The opencode CLI |
@kontourai/relay/anthropic |
createAnthropicRuntime |
@anthropic-ai/sdk, unless you inject a client |
@kontourai/relay/ai-sdk |
createAiSdkRuntime, createAiSdkModel |
@ai-sdk/provider types and an AI SDK v3 model or host |
@kontourai/relay/runtime-profile |
MODEL_RUNTIME_PROFILES, parseModelRuntimeProfile, createModelRuntimeProfile |
Whatever the chosen profile needs |
Runtime comparison#
| Runtime | id |
Text | Structured tools | Tool choice | maxOutputTokens |
Model source |
|---|---|---|---|---|---|---|
| Anthropic-compatible | <provider>:<model> |
yes | native | auto, required, tool |
sent as max_tokens |
provider-reported when the response has a model |
| AI SDK | <provider>:<modelId>, or id |
yes | native | auto, required, tool |
passed through | provider-reported when the model reports one |
| Claude Code | claude-code:<model> |
yes | native, one selected tool | tool, or required with one tool |
not enforced; a warning when exceeded | provider-reported when unambiguous |
| Codex | codex:<model> |
yes | native, one selected tool | tool, or required with one tool |
not enforced | configured |
| OpenCode | opencode:<model> |
yes | refused by default; prompted when opted in | tool, or required with one tool |
not enforced | configured |
None of these runtimes streams or offers a physical batch, and all of them honour an abort signal.
Core types#
Exported as types from @kontourai/relay.
| Type | Shape |
|---|---|
ModelRuntime |
id, capabilities(), invoke(request, options?), optional invokeBatch(requests, options?) |
ModelInvocationRequest |
messages, optional tools, toolChoice, maxOutputTokens, metadata |
ModelMessage |
role: system, user, assistant, or tool; content: a string or ModelMessageContentPart[] |
ModelMessageContentPart |
{ type: "text", text }, { type: "tool-call", id, name, input }, or { type: "tool-result", id, name, output, isError? } |
ModelTool |
name, optional description, inputSchema |
ModelToolChoice |
{ type: "auto" }, { type: "required" }, or { type: "tool", name } |
ModelInvocationResult |
provider, model, modelSource?, outputText, toolCalls, usage, latencyMs, stopReason?, warnings? |
ModelSource |
"provider-reported" or "configured" |
ModelToolCall |
id, name, input |
ModelUsage |
Optional inputTokens, outputTokens, totalTokens, cacheReadTokens, cacheWriteTokens, costUsd |
ModelRuntimeCapabilities |
structuredTools, streaming, abort, usage; optional structuredToolsFidelity, outputTokenLimitFidelity, physicalBatch, maxBatchSize |
ModelInvocationOptions |
Optional signal: AbortSignal |
ModelInvocationFailure |
code, message, retryable: a content-free failure for one batch item |
ModelBatchInvocationOutcome |
{ status: "fulfilled", value } or { status: "rejected", reason } |
ModelInvocationErrorCode |
ABORTED, AUTHENTICATION_FAILED, INVALID_REQUEST, PROVIDER_UNAVAILABLE, RATE_LIMITED, RUNTIME_FAILURE |
JsonSchema |
Readonly<Record<string, unknown>> |
InvocationReplayRecord |
schemaVersion: 1, requestDigest, request, result |
RuntimeConformanceReport, PhysicalBatchConformanceReport |
runtimeId, passed, checks: { name, passed, detail }[] |
ModelInvocationError is a class: new ModelInvocationError(code, message, retryable, options?). Its name is "ModelInvocationError", and it exposes
code and retryable as read-only properties. The codes are explained in
Failures and retryability.
Test and replay runtimes#
Exported from @kontourai/relay.
FakeModelRuntime#
new FakeModelRuntime(results, id = "fake")
resultsis a queue ofModelInvocationResultorModelInvocationFailurevalues. Eachinvoke()takes the next one. A failure entry is thrown as aModelInvocationErrorwith its code, message, andretryableflag. An empty queue throwsRUNTIME_FAILURE.invokeBatch()takes one queued outcome per request and returns failures as rejected items. It accepts 1 to 100 requests and throwsINVALID_REQUESTotherwise. An item with no queued outcome is rejected withRUNTIME_FAILURE.- Capabilities: native structured tools and output limit, abort, usage,
physicalBatch: true,maxBatchSize: 100, no streaming. - Inspection:
requests(every request, frominvokeandinvokeBatch),batchRequests(one array per batch), andphysicalInvocationCount. - An already-aborted signal throws
ABORTED.
RecordingModelRuntime#
new RecordingModelRuntime(runtime)
idisrecording:<wrapped id>.capabilities()returns the wrapped runtime's capabilities withoutphysicalBatchandmaxBatchSize, because the recorder has noinvokeBatch().- After each successful
invoke(), appends a frozenInvocationReplayRecordtorecords. Failed invocations are not recorded.
ReplayModelRuntime#
new ReplayModelRuntime(records)
idisreplay.invoke()returns a copy of the result whose record has the samerequestDigestas the request. A request with no match throwsINVALID_REQUEST(No replay record matches this invocation).- Capabilities: native structured tools and output limit, abort, usage, no streaming.
Digests#
canonicalJson(value):JSON.stringifywith object keys sorted at every level.invocationDigest(value):sha256:followed by the hex SHA-256 ofcanonicalJson(value).
Conformance checks#
Exported from @kontourai/relay. Both return a frozen report and do not throw
for a failed check.
checkRuntimeConformance#
checkRuntimeConformance(runtime): Promise<RuntimeConformanceReport>
Invokes the runtime once with { messages: [{ role: "user", content: "relay-conformance" }], maxOutputTokens: 1 }. Against a real runtime this is a
real model call. The checks are:
| Check | Passes when |
|---|---|
capabilities |
abort is a boolean. |
physical-batch-declaration |
With physicalBatch: true, invokeBatch exists and maxBatchSize is a positive integer. Otherwise invokeBatch is absent and maxBatchSize is undefined. |
structured-tools-fidelity |
Undeclared, or native/prompted with structuredTools: true, or unavailable with structuredTools: false. |
output-token-limit-fidelity |
Undeclared, or one of native, approximated, unavailable. |
identity |
The result has a non-empty provider and model. |
model-source |
modelSource is provider-reported or configured. |
shape |
toolCalls is an array and latencyMs is at least 0. |
invoke |
Present only when the invocation threw; always fails, with the error message as detail. |
checkPhysicalBatchConformance#
checkPhysicalBatchConformance(runtime, requests, options?): Promise<PhysicalBatchConformanceReport>
options accepts signal and requireMixedOutcomes. The batch is issued only
when the first two checks pass.
| Check | Passes when |
|---|---|
physical-batch-declaration |
physicalBatch: true, invokeBatch exists, and maxBatchSize is a positive integer. |
request-count |
requests is non-empty and no larger than maxBatchSize. |
positional-correspondence |
The batch returned one outcome per request. |
typed-outcomes |
Every outcome is fulfilled or rejected. |
partial-item-failure |
Only with requireMixedOutcomes: at least one fulfilled and one rejected outcome. |
invoke-batch |
Present only when invokeBatch rejected; always fails, with the error's name as detail. |
Process transport#
createProcessRuntime(options): ModelRuntime from @kontourai/relay/process.
| Option | Meaning |
|---|---|
id |
The runtime id. |
executable |
The command to spawn. |
capabilities |
The declaration returned by capabilities(). |
codec |
A ProcessRuntimeCodec: prepare(request) returns { args, stdin? }; parse(output, request) returns a result; optional classifyFailure(output, request) returns a ModelInvocationError for a nonzero exit. |
cwd |
Working directory for the child process. |
environment |
Variables added on top of the parent's process.env for the child. Never copied into results or errors. |
maxOutputBytes |
Combined stdout and stderr limit. Default 8 MiB. |
now |
Clock for latencyMs. Default performance.now(). |
The output passed to the codec is { stdout, stderr, exitCode, latencyMs }.
Failure behaviour:
- A signal that is already aborted, or aborts while the child runs, throws
ABORTED. A running child is sentSIGTERM. prepare()throwing anything other than aModelInvocationErrorbecomesINVALID_REQUEST.- A missing executable (
ENOENT) is a retryablePROVIDER_UNAVAILABLE. Any other spawn error isRUNTIME_FAILURE. - Output beyond
maxOutputBytesisRUNTIME_FAILUREand the child is terminated. - A nonzero exit uses
classifyFailure(), orRUNTIME_FAILUREnaming the exit code when there is none. parse()throwing anything other than aModelInvocationErrorbecomesRUNTIME_FAILURE.
Claude Code runtime#
createClaudeCodeRuntime(options) from @kontourai/relay/claude-code.
| Option | Meaning |
|---|---|
model |
Passed to --model. Required. |
executable |
Default claude. |
cwd, environment, maxOutputBytes |
As for the process transport. |
It runs claude --print --output-format json --model <model> --no-session-persistence --tools "" --permission-mode dontAsk, adding
--json-schema <schema> when a tool is selected. The conversation is sent on
stdin as JSON, after a fixed instruction line and any tool and field
descriptions.
- A selected tool's result is
toolCalls[0]with idclaude-code-structured-output, taken from the CLI'sstructured_output. If that field is missing, the invocation fails withRUNTIME_FAILURE. - Usage maps
input_tokens,output_tokens,cache_read_input_tokens,cache_creation_input_tokens, andtotal_cost_usdwhen present. outputTokenLimitFidelityisunavailable. When the CLI reports more output tokens thanmaxOutputTokens, the result carries the warningOUTPUT_TOKEN_LIMIT_NOT_ENFORCED: requested <n>, observed <m>.createClaudeCodeCodec(model)is exported for profile conformance fixtures. Applications should construct the runtime.
Codex runtime#
createCodexRuntime(options) from @kontourai/relay/codex.
| Option | Meaning |
|---|---|
model |
Passed to --model. Required. |
executable |
Default codex. |
cwd |
Working directory. Default: a fresh temporary directory, removed after the invocation. Set it when repository context is intentionally part of the runtime target. |
environment, maxOutputBytes |
As for the process transport. |
It runs codex --ask-for-approval never exec --json --ephemeral --color never --sandbox read-only --skip-git-repo-check --ignore-rules --model <model> -,
adding --output-schema <file> when a tool is selected. Relay writes that
schema file into the temporary directory and removes it afterwards.
The selected tool's inputSchema must meet Codex's output-schema rules, or the
invocation fails with INVALID_REQUEST naming the JSON pointer:
- Every schema node Relay visits (the root, properties,
items, combinator branches, and definitions) is a non-empty object. - An object schema (
type: "object"orproperties) hasadditionalProperties: false, apropertiesobject, and lists every property inrequired. Express an optional value with a nullable schema. - An array schema (
type: "array"oritems) hasitems. anyOf,oneOf, andallOfhold at least one schema;$defsanddefinitionsare objects.
The result text is the last agent_message event. With a selected tool, that
text is parsed as JSON into toolCalls[0] (id codex-structured-output) and
outputText is empty. Usage comes from the last turn.completed event, and
stopReason is completed. createCodexCodec(model, schemaPath?) is exported
for fixtures.
OpenCode runtime#
createOpenCodeRuntime(options) from @kontourai/relay/opencode.
| Option | Meaning |
|---|---|
model |
An OpenCode provider/model identifier, such as zai/glm-5. Required. |
structuredOutput |
"reject" (default) refuses any request with tools. "prompted" puts the selected tool's schema in the prompt. |
executable |
Default opencode. |
cwd |
Working directory. Default: a fresh temporary directory, removed after the invocation. |
environment, maxOutputBytes |
As for the process transport. |
It runs opencode --pure run --format json --model <model> and joins the text
of every text event. Usage and stopReason come from the last step-finish
event.
In prompted mode the response text, with one surrounding Markdown code fence
removed, is parsed as JSON into toolCalls[0] (id opencode-prompted-output),
and the result carries the warning Structured output was prompt-enforced, not schema-enforced by OpenCode. Unparseable JSON fails with a retryable
RUNTIME_FAILURE. createOpenCodeCodec(model, structuredOutput?) is exported
for fixtures.
Anthropic runtime#
createAnthropicRuntime(options) from @kontourai/relay/anthropic.
| Option | Meaning |
|---|---|
model |
The model sent to the Messages API. Required. |
apiKey |
API key. When neither apiKey nor client is given, ANTHROPIC_API_KEY from the environment is used; with no key at all, the invocation fails with AUTHENTICATION_FAILED. |
baseUrl |
Base URL for an Anthropic-compatible endpoint. |
client |
An injected object with create(params, { signal }). When given, the SDK is not loaded and apiKey, baseUrl, maxRetries, and timeoutMs are ignored. |
provider |
The result's provider and the id prefix. Default anthropic-compatible. |
defaultMaxOutputTokens |
max_tokens when the request has no maxOutputTokens. Default 2048. |
maxRetries |
SDK retries per invocation. Default 0, so one invoke() is one provider request. |
timeoutMs |
Per-request SDK timeout. Unset keeps the SDK default. A timeout is a retryable PROVIDER_UNAVAILABLE. |
now |
Clock for latencyMs. |
Request mapping: system messages are joined with blank lines into system;
tool messages are sent as user messages; tool results become tool_result
blocks. toolChoice maps to tool, any (for required), or auto. The
result's model is the response's model, reported as provider-reported; if
the response has no model, the configured model is reported as configured.
Text blocks are joined with newlines.
AI SDK bridge#
From @kontourai/relay/ai-sdk. It works in both directions with the Vercel AI
SDK v3 model contract (LanguageModelV3).
createAiSdkRuntime#
createAiSdkRuntime({ model, id?, now? }): ModelRuntime uses an AI SDK provider
model as a Relay runtime.
iddefaults to<model.provider>:<model.modelId>.- Text, function tools, tool choice, assistant tool calls, tool results,
maxOutputTokens, and the abort signal are passed todoGenerate(). - A Relay
usermessage may contain only text, and atoolmessage only tool results; anything else isINVALID_REQUEST. - The AI SDK's warnings are returned as strings in
result.warnings.
createAiSdkModel#
createAiSdkModel({ runtime, provider?, modelId? }): LanguageModelV3 gives any
Relay runtime to an AI SDK host.
providerdefaults torelay, andmodelIdto the runtime'sid.- Provider-defined tools and non-text, non-tool prompt parts are refused with
INVALID_REQUEST. - Sampling and format options (
temperature,stopSequences,topP,topK,presencePenalty,frequencyPenalty,responseFormat,seed,providerOptions) andtool.strictare not portable and produce anunsupportedwarning.toolChoice: nonedrops the tools, with a warning. - Relay warnings come back as
unsupportedwarnings with featureruntime. doStream()buffers one Relay invocation, then emits it as a stream whosestream-startpart carries acompatibilitywarning forstreaming. It is not live token streaming.
Runtime profile entrypoint#
From @kontourai/relay/runtime-profile.
MODEL_RUNTIME_PROFILES:["claude-code", "codex", "opencode", "anthropic"].parseModelRuntimeProfile(value)splitsPROFILE:MODELat the first colon, so the model may itself contain colons. It returns a frozen{ profile, model }and throws a plainErrorfor a missing part (runtime profile must use PROFILE:MODEL) or an unknown profile (unknown runtime profile: <name>).createModelRuntimeProfile(options)constructs one runtime.
| Option | Applies to | Meaning |
|---|---|---|
profile, model |
all | Usually spread from parseModelRuntimeProfile(). |
executable, cwd, environment, maxOutputBytes |
claude-code, codex, opencode |
Forwarded to the runtime. |
allowPromptedStructuredOutput |
opencode |
Required. Without it, construction throws. The runtime is always built with structuredOutput: "prompted". |
apiKey |
anthropic |
Required. Without it, construction throws. The resolver never reads the environment. |
baseUrl, maxRetries, timeoutMs |
anthropic |
Forwarded to the runtime. |
Troubleshooting#
Common failures and what they mean. The message text is the message of the
ModelInvocationError, or of the plain Error for the profile resolver.
| Message | Cause and fix |
|---|---|
<Harness> profile requires an explicit tool choice |
A harness runtime received tools with no toolChoice or with auto. Select one tool with { type: "tool", name }. |
Required tool choice needs exactly one declared tool |
{ type: "required" } was used with several tools on a harness runtime. Declare one tool or name it. |
Selected tool is not declared |
toolChoice.name does not match any tool in tools. |
Tool choice requires a declared tool |
Claude Code or Codex received a non-auto toolChoice with no tools. |
OpenCode structured output requires explicit prompted opt-in |
createOpenCodeRuntime() received tools while structuredOutput is "reject". Pass structuredOutput: "prompted". |
opencode structured tools require explicit prompted-output opt-in |
createModelRuntimeProfile() was given the opencode profile without allowPromptedStructuredOutput. The profile always runs OpenCode in prompted mode, so the flag is required even for text-only use. |
Codex structured-output schema is incompatible at <pointer>: <reason> |
The selected tool's schema breaks a Codex schema rule. |
Harness executable is unavailable |
The CLI is not on PATH. Install it or pass executable. |
Harness output exceeded the configured limit |
stdout and stderr together passed maxOutputBytes (default 8 MiB). |
<Harness> authentication failed |
The CLI reported an authentication failure. Log in to the harness itself; Relay does not manage harness credentials. |
<Harness> rate limited: <reason> |
The CLI hit a rate or usage limit. The error is retryable; the reason may include the reset time. |
No Anthropic-compatible API key supplied |
The Anthropic runtime had no apiKey, no client, and no ANTHROPIC_API_KEY. |
No replay record matches this invocation |
ReplayModelRuntime received a request whose digest differs from every record. Any change to the request, including metadata, changes the digest. |
Fake runtime has no queued result |
FakeModelRuntime was invoked more times than it had results. |
runtime profile must use PROFILE:MODEL |
The profile string has no colon, or nothing before or after it. |
anthropic runtime requires an API key |
createModelRuntimeProfile() was given the anthropic profile without apiKey. |