Skip to content
Ask
Reference

API Reference

createRelay, NormalizedRequest, the re-exported ModelEvent/ModelClient contract types, ModelClientFactory, the handlers, adapters, SSE helpers, and the client.

This page describes the public surface of @inbrowser/relay.

Exports

Import pathExports
@inbrowser/relaycreateRelay; relay/transport types (Relay, CreateRelayOpts, ApiKeySource, StreamCtx, NormalizedRequest, Logger); the re-exported model contract types (ModelEvent, ModelMessage, ModelRequest, ModelUsage, ToolSpec, ReasoningEffort); the ModelClientFactory type for the registration site; the client and SSE re-exports
@inbrowser/relay/ssereadSseDataLines, encodeSseEvent, SSE_DONE_LINE, SSE_STREAM_OPEN
@inbrowser/relay/adapters/astrocreateAstroRoutes
@inbrowser/relay/adapters/expresscreateExpressHandlers
@inbrowser/relay/clientcreateResumableClient, installBrowserLifecycle

The relay does not export any providers. Import the cloud provider factories (geminiModelClient, openrouterModelClient, anthropicModelClient, ollamaModelClient, claudeCliModelClient, claudeCodeModelClient) from @inbrowser/model.

createRelay

TS
function createRelay(opts: CreateRelayOpts): Relay;

CreateRelayOpts:

FieldTypeDescription
storeJobStore<ModelEvent>Required resumable job store.
providersRecord<string, ModelClientFactory>Provider map keyed by NormalizedRequest.provider. Each value is a ModelClientFactory from @inbrowser/model: the relay calls factory({ apiKey, model }) per request to build a ModelClient, then drives its .chat().
loggerLoggerOptional structured logger. Defaults to silent.
sweepSweepScheduleOptional periodic sweep passed to @inbrowser/resumable.
apiKeysRecord<string, ApiKeySource>Optional per-provider server-managed keys. See Server-managed API keys.

Relay:

MemberDescription
handleStart(request)Parses a NormalizedRequest, constructs the provider’s ModelClient, starts a resumable job, and returns { jobId }.
handleStream(request, ctx)Streams the job log as SSE from ctx.from or the request query string.
engineUnderlying JobEngine<ModelEvent>.
stop()Closes in-flight producers and stops the scheduled sweep.

handleStart returns:

StatusMeaning
201Job created. Body is { "jobId": "..." }.
400Invalid JSON, missing provider, unknown provider, missing apiKey in BYOK mode, or a client-supplied apiKey for a server-managed provider.
500Store or engine failed, or a server-managed apiKey resolver threw, before the job could be created.

handleStream returns:

StatusMeaning
200SSE stream opened.
400Missing job id.
404Job not found.
502Store read failed before streaming began.

NormalizedRequest

NormalizedRequest is the shared ModelRequest (from @inbrowser/model) plus the relay-only transport fields:

TS
type NormalizedRequest = ModelRequest & {
  // ModelRequest carries: messages: ModelMessage[]; tools: ToolSpec[];
  // toolUseEnabled: boolean; temperature?; topP?; topK?; reasoningEffort?
  provider: string; // routing key — looked up in createRelay's providers map
  model: string; // upstream model id, passed to the ModelClientFactory
  apiKey?: string;
  signal?: AbortSignal; // page-direct consumer cancellation only
};

provider is the lookup key in the providers map. model and apiKey are handed to the ModelClientFactory (factory({ apiKey, model })); the per-call settings (messages, tools, sampling) ride the ModelRequest into .chat(). apiKey is not stored in job metadata by the relay. It is optional on the wire because the relay resolves it differently per mode (see below); by the time a provider’s ModelClient runs, the relay has guaranteed a resolved value.

Server-managed API keys

By default the relay is BYOK: the client sends apiKey in the request body and the relay 400s if it is missing. To keep the key on the server instead, list the provider in CreateRelayOpts.apiKeys:

TS
import { geminiModelClient } from '@inbrowser/model/providers/gemini';
import { anthropicModelClient } from '@inbrowser/model/providers/anthropic';
import { ollamaModelClient } from '@inbrowser/model/providers/ollama';

type ApiKeySource =
  | string
  | ((ctx: { req: NormalizedRequest; request: Request }) => string | Promise<string>);

const relay = createRelay({
  store,
  providers: {
    gemini: geminiModelClient,
    anthropic: anthropicModelClient,
    ollama: ollamaModelClient,
  },
  apiKeys: {
    gemini: () => process.env.GEMINI_API_KEY ?? '',
    anthropic: () => process.env.ANTHROPIC_API_KEY ?? '',
    // ollama omitted, so it stays BYOK (the client supplies its base URL)
  },
});

Rules per provider:

  • Listed (server-managed): the relay resolves the key and overwrites whatever the client sent. A client that sends a non-empty apiKey anyway gets a 400 so a forgotten BYOK field cannot silently leak to the wire. If the resolver throws, handleStart returns 500 and no job is created.
  • Not listed (BYOK): unchanged. The client supplies apiKey; a missing key is a 400.

The function form receives the raw Request, so the key can be derived from an Authorization header, a session cookie, or a per-user store:

TS
apiKeys: {
  anthropic: async ({ request }) => {
    const userId = await getUserIdFromSession(request);
    const key = await db.getUserKey(userId, 'anthropic');
    if (!key) throw new Error('no anthropic key for user');
    return key;
  },
}

ModelEvent

The relay’s event type is ModelEvent from @inbrowser/model, re-exported from @inbrowser/relay:

TS
type ModelEvent =
  | { kind: 'text'; text: string }
  | { kind: 'thinking'; text: string }
  | { kind: 'tool_call'; id: string; name: string; args: unknown; signature?: string }
  | { kind: 'usage'; usage: ModelUsage }
  | { kind: 'error'; message: string };

interface ModelUsage {
  promptTokens: number;
  outputTokens: number;
  cachedTokens?: number;
  reasoningTokens?: number;
  costUsd?: number;
}

The turn ends when the chat() iterable returns. On a normal end a usage event is emitted before the return (it carries the final accounting); there is no separate turn_complete event. An error event is itself terminal: after it the iterable returns with no usage event. Consumers can rely on exactly one of {a usage event, an error event} per turn.

Provider contract: ModelClientFactory

A provider is a ModelClient from @inbrowser/model, registered as a ModelClientFactory:

TS
interface ModelClient {
  readonly id: string;
  readonly supportsTools: boolean;
  chat(req: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
}

// The shape createRelay's `providers` map holds (from @inbrowser/model):
type ModelClientFactory = (config: { apiKey?: string; model: string }) => ModelClient;

The relay calls the factory per request with { apiKey, model } (so BYOK per-request keys and routing both work), then drives .chat(req, signal) under @inbrowser/resumable. The ModelClient owns upstream protocol details only.

Built-In Providers

The relay ships none. The cloud provider factories live in @inbrowser/model and are imported from @inbrowser/model. Each is a factory that returns a ModelClient; the cloud ones match ModelClientFactory directly (config { apiKey?, model }).

ProviderFactory (@inbrowser/model)Notes
GeminigeminiModelClientUses the Generative Language REST streaming endpoint. Includes retry handling for selected transient Gemini failures.
OpenRouteropenrouterModelClientUses OpenRouter chat completions SSE, reasoning deltas, tools, and usage cost when reported.
RequestyrequestyModelClientUses Requesty’s OpenAI-compatible gateway SSE, reasoning deltas, tools, and usage cost when reported.
AnthropicanthropicModelClientUses Anthropic native Messages streaming. Tool use is intentionally compact.
OllamaollamaModelClientTalks to a local Ollama server. The request apiKey carries the base URL rather than a secret; or pass baseUrl via a wrapping factory.
Claude (CLI)claudeCliModelClientSubscription auth. Spawns the claude binary in print mode (claude -p) and reads its streaming-JSON output. Accepts ClaudeCliOptions (claudePath, timeoutMs). Node-only; rejects caller-defined tools.
Claude (Agent SDK)claudeCodeModelClientSubscription auth. Drives @anthropic-ai/claude-agent-sdk (an optional peer dependency) in-process; strips ANTHROPIC_API_KEY so the call never falls back to per-token billing. Accepts oauthToken. Node-only; rejects caller-defined tools.

The two Claude providers authorize from a logged-in subscription instead of an API key, so a client sends an empty apiKey. See How to use a subscription Claude provider.

SSE Wire Format

Relay-to-client events are single-line SSE data events:

TEXT
data: {"kind":"text","text":"hello"}

data: [DONE]

SSE_STREAM_OPEN is emitted first as an SSE comment:

TEXT
: stream-open

[DONE] is emitted only when the job reaches terminal state. A connection that closes without [DONE] means the client should reconnect from its current offset.

Client

TS
function createResumableClient(opts: ResumableClientOpts): ResumableClient;

ResumableClientOpts:

FieldDescription
startUrlURL for POST job creation.
streamUrlBuilds the stream URL from (jobId, from).
maxAttemptsReconnect attempt limit. Defaults to 300.
reconnectDelayMsDelay between reconnects. Defaults to 300.
onReconnectDiagnostic callback for reconnect decisions.
onConsumerAbortCallback when the caller aborts the request signal.
installLifecycleHook for browser or host lifecycle integration.
fetchImplOptional fetch implementation.

client.stream(req) starts a job and yields ModelEvents until the relay emits [DONE], an unrecoverable error occurs, or the caller aborts.

Adapters

createAstroRoutes(relay, opts?) returns { start, stream } APIRoute-like handlers. opts.jobIdParam defaults to id.

createExpressHandlers(relay, opts?) returns { start, stream } Express-compatible handlers. Options:

FieldDescription
jobIdParamRoute parameter containing the job id. Defaults to id.
corsAdds permissive CORS headers and handles preflight when true.