TypeScript client archive

The former TypeScript SDK is archived. Existing source and package versions may
remain available for historical use, but they receive no compatibility, update,
publication, or support guarantees. Use the supported
REST/OpenAPI contract for new integrations. See the
authoritative client support matrix for lifecycle definitions.

The remaining examples are retained as historical reference. They do not imply
that an archived package matches the current API.

Quick Start

import { ModelRelay } from "@modelrelay/sdk";

const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_API_KEY!);

const answer = await mr.responses.text(
  "claude-sonnet-5",
  "You are a helpful assistant.",
  "What is the capital of France?"
);

console.log(answer);
// "The capital of France is Paris."

Convenience API

The simplest way to get started. Three methods cover the most common use cases:

Ask — Get a Quick Answer

import { ModelRelay } from "@modelrelay/sdk";

const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_API_KEY!);

const answer = await mr.ask("claude-sonnet-5", "What is 2 + 2?");
console.log(answer); // "4"

Chat — Full Response with Metadata

const response = await mr.chat("claude-sonnet-5", "Explain quantum computing", {
  system: "You are a physics professor",
});

console.log(response.output);
console.log("Tokens:", response.usage.totalTokens);

Agent — Agentic Tool Loops

Run an agent that automatically executes tools until completion:

import { z } from "zod";

const tools = mr
  .tools()
  .add(
    "read_file",
    "Read a file from the filesystem",
    z.object({ path: z.string().describe("File path to read") }),
    async (args) => {
      const content = await fs.readFile(args.path, "utf-8");
      return content;
    }
  );

const result = await mr.agent("claude-sonnet-5", {
  tools,
  prompt: "Read config.json and summarize it",
  system: "You are a helpful file assistant",
});

console.log(result.output);
console.log("Tool calls:", result.usage.toolCalls);

Configuration

From API Key

The simplest way to create a client:

import { ModelRelay } from "@modelrelay/sdk";

// From secret key (backend use)
const mr = ModelRelay.fromSecretKey("mr_sk_...");

// From API key string
const mr = ModelRelay.fromApiKey(process.env.MODELRELAY_API_KEY!);

Full Configuration

For more control over client behavior:

import { ModelRelay, parseSecretKey } from "@modelrelay/sdk";

const mr = new ModelRelay({
  key: parseSecretKey("mr_sk_..."),
  baseUrl: "https://api.modelrelay.ai",  // Optional, defaults to production
  timeoutMs: 30_000,                      // Request timeout
  connectTimeoutMs: 10_000,               // Connection timeout
  retry: {
    maxAttempts: 3,                       // Retry failed requests
  },
});

Configuration Options

Option Type Default Description
key string Secret API key (mr_sk_*)
baseUrl string https://api.modelrelay.ai API base URL
timeoutMs number 120000 Request timeout in milliseconds
connectTimeoutMs number 10000 Connection timeout in milliseconds
retry.maxAttempts number 0 Number of retry attempts

Making Requests

Simple Text Response

For the common “system prompt + user message → text” pattern:

const answer = await mr.responses.text(
  "claude-sonnet-5",  // model
  "You are a helpful assistant.",  // system prompt
  "What is 2 + 2?"  // user message
);

Request Builder

For more control over request parameters:

const response = await mr.responses.create(
  mr.responses
    .new()
    .model("claude-sonnet-5")
    .system("You are a helpful assistant.")
    .user("What is 2 + 2?")
    .maxOutputTokens(256)
    .temperature(0.7)
    .build()
);

console.log(response.output);  // Array of output items
console.log(response.usage);   // { input_tokens, output_tokens }

Customer-Attributed Requests

For metered billing, attribute requests to customers:

// Option 1: Use customerId in the request
const req = mr.responses
  .new()
  .customerId("cust_abc123")
  .system("You are a helpful assistant.")
  .user("Hello!")
  .build();

const response = await mr.responses.create(req);

// Option 2: Use the convenience method
const answer = await mr.responses.textForCustomer({
  customerId: "cust_abc123",
  system: "You are a helpful assistant.",
  user: "Hello!",
});

// Option 3: Create a customer-scoped client
const customer = mr.forCustomer("cust_abc123");
const answer = await customer.responses.text(
  "You are a helpful assistant.",
  "Hello!"
);

Streaming

Stream Text Deltas

For real-time response streaming:

const stream = await mr.responses.streamTextDeltas(
  "claude-sonnet-5",
  "You are a helpful assistant.",
  "Write a haiku about programming."
);

for await (const delta of stream) {
  process.stdout.write(delta);
}

Full Event Stream

For access to all streaming events:

const req = mr.responses
  .new()
  .model("claude-sonnet-5")
  .user("Hello!")
  .build();

const stream = await mr.responses.stream(req);

for await (const event of stream) {
  switch (event.type) {
    case "message_start":
      console.log("Started:", event.message.id);
      break;
    case "message_delta":
      if (event.textDelta) {
        process.stdout.write(event.textDelta);
      }
      break;
    case "message_complete":
      console.log("\nUsage:", event.message.usage);
      break;
  }
}

Collect Stream to Response

To aggregate an entire stream into a final response object:

const stream = await mr.responses.stream(req);
const response = await stream.collect();

// response is now a complete Response object
console.log(response.output);
console.log(response.usage);

This is useful when you want streaming progress indicators but need the complete response for further processing.

Sessions

The archived SDK retains a client-managed LocalSession utility for multi-turn
conversations with automatic tool handling. The former server-backed
ModelRelay.sessions client and remote session synchronization API were
removed with the /sessions API.

import { LocalSession, ModelRelay } from "@modelrelay/sdk";

const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_API_KEY!);
const session = LocalSession.create(mr, {
  defaultModel: "gpt-5.2",
});

const result = await session.run("Summarize the last meeting.", {
  contextManagement: "truncate",
});

console.log(result.output);

Context Management

When contextManagement is "truncate", the SDK trims older messages to fit
within the model’s context window. It fetches model metadata from /models to
derive a default history budget. You can override the budget with
maxHistoryTokens. A model is required (set defaultModel or options.model).
Use reserveOutputTokens to override the output-token reservation when
computing the history budget. If maxHistoryTokens is set, it takes precedence.

const result = await session.run("Continue the plan.", {
  contextManagement: "truncate",
  maxHistoryTokens: 8_000,
});

"summarize" is reserved for a future release and currently throws an error.

You can also observe truncation decisions:

await session.run("Continue the plan.", {
  contextManagement: "truncate",
  maxHistoryTokens: 8_000,
  onContextTruncate: ({ originalMessages, keptMessages }) => {
    console.log(`Trimmed ${originalMessages}${keptMessages} messages`);
  },
});

Structured Output

With Zod Schemas

Parse responses into typed objects:

import { ModelRelay } from "@modelrelay/sdk";
import { z } from "zod";

const mr = ModelRelay.fromSecretKey("mr_sk_...");

const Person = z.object({
  name: z.string(),
  age: z.number(),
});

// Simple one-call API (recommended)
const person = await mr.responses.object<z.infer<typeof Person>>({
  model: "claude-sonnet-5",
  schema: Person,
  prompt: "Extract: John Doe is 30 years old",
});

console.log(person.name);  // "John Doe"
console.log(person.age);   // 30

For parallel structured output:

const [security, performance] = await Promise.all([
  mr.responses.object<SecurityReview>({
    model: "claude-sonnet-5",
    schema: SecuritySchema,
    system: "You are a security expert.",
    prompt: code,
  }),
  mr.responses.object<PerformanceReview>({
    model: "claude-sonnet-5",
    schema: PerformanceSchema,
    system: "You are a performance expert.",
    prompt: code,
  }),
]);

For metadata (attempts, request ID) or more control:

const result = await mr.responses.objectWithMetadata<z.infer<typeof Person>>({
  model: "claude-sonnet-5",
  schema: Person,
  prompt: "Extract: John Doe is 30 years old",
  maxRetries: 2,
});

console.log(result.value);     // { name: "John Doe", age: 30 }
console.log(result.attempts);  // 1 (first try succeeded)
console.log(result.requestId); // Server request ID

Streaming Structured Output

Build progressive UIs that render fields as they complete:

import { z } from "zod";

const Article = z.object({
  title: z.string(),
  summary: z.string(),
  body: z.string(),
});

const stream = await mr.responses.streamStructured(
  Article,
  mr.responses
    .new()
    .model("claude-sonnet-5")
    .user("Write an article about TypeScript")
    .build()
);

for await (const event of stream) {
  // Render fields as soon as they're complete
  if (event.completeFields.has("title")) {
    renderTitle(event.payload.title);
  }
  if (event.completeFields.has("summary")) {
    renderSummary(event.payload.summary);
  }
  // Show streaming preview of incomplete fields
  if (!event.completeFields.has("body")) {
    renderBodyPreview(event.payload.body + "▋");
  }
}

Error Handling

Error Types

The SDK provides typed errors for different failure modes:

import {
  ModelRelay,
  ModelRelayError,
  APIError,
  ConfigError,
  TransportError,
  ErrorCodes,
} from "@modelrelay/sdk";

try {
  const answer = await mr.responses.text(
    "claude-sonnet-5",
    "You are helpful.",
    "Hello!"
  );
} catch (error) {
  if (error instanceof APIError) {
    // Server returned an error response
    console.error(`API error ${error.status}: ${error.message}`);
    console.error(`Code: ${error.code}`);
    console.error(`Request ID: ${error.requestId}`);

    // Check specific error types
    if (error.isRateLimit()) {
      console.error("Rate limited, retry later");
    }
    if (error.isUnauthorized()) {
      console.error("Invalid API key");
    }
  } else if (error instanceof TransportError) {
    // Network or connection error
    console.error(`Transport error: ${error.message}`);
  } else if (error instanceof ConfigError) {
    // Invalid configuration
    console.error(`Config error: ${error.message}`);
  } else {
    throw error;
  }
}

Error Codes

Use error codes for programmatic handling:

import { ErrorCodes } from "@modelrelay/sdk";

if (error instanceof APIError) {
  switch (error.code) {
    case ErrorCodes.RATE_LIMIT:
      // Back off and retry
      break;
    case ErrorCodes.UNAUTHORIZED:
      // Re-authenticate
      break;
    case ErrorCodes.NOT_FOUND:
      // Resource doesn't exist
      break;
    case ErrorCodes.VALIDATION_ERROR:
      // Check request parameters
      console.error("Field errors:", error.fields);
      break;
  }
}

Customer Management

For customer CRUD operations, use the REST API directly. See Customer API Reference for details.

// Create or update a customer using fetch
const response = await fetch("https://api.modelrelay.ai/api/v1/customers/upsert", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-ModelRelay-Api-Key": process.env.MODELRELAY_SECRET_KEY!,
  },
  body: JSON.stringify({
    external_id: "your-user-id",
    email: "user@example.com",
  }),
});
const customer = await response.json();

Tool Use

Define tools that models can call:

import {
  ModelRelay,
  createFunctionTool,
  hasToolCalls,
  firstToolCall,
  toolResultMessage,
  zodToJsonSchema,
} from "@modelrelay/sdk";
import { z } from "zod";

const weatherSchema = z.object({
  location: z.string().describe("City name"),
});

const weatherTool = createFunctionTool({
  name: "get_weather",
  description: "Get current weather for a location",
  parameters: zodToJsonSchema(weatherSchema),
});

const response = await mr.responses.create(
  mr.responses
    .new()
    .model("claude-sonnet-5")
    .tools([weatherTool])
    .user("What's the weather in Paris?")
    .build()
);

if (hasToolCalls(response)) {
  const call = firstToolCall(response);
  console.log(call.name);       // "get_weather"
  console.log(call.arguments);  // { location: "Paris" }

  // Execute tool and continue conversation
  const weatherData = await getWeather(call.arguments.location);

  const followUp = await mr.responses.create(
    mr.responses
      .new()
      .model("claude-sonnet-5")
      .tools([weatherTool])
      .fromResponse(response)
      .message(toolResultMessage(call.id, JSON.stringify(weatherData)))
      .build()
  );
}

Handling Multiple Tool Calls

Models may return multiple tool calls in a single response. Iterate over all calls:

import {
  hasToolCalls,
  toolResultMessage,
  assistantMessageWithToolCalls,
  asModelId,
} from "@modelrelay/sdk";
import type { ToolCall } from "@modelrelay/sdk";

if (hasToolCalls(response)) {
  // Collect all tool calls from the response
  const allToolCalls: ToolCall[] = [];
  for (const item of response.output || []) {
    for (const call of item?.toolCalls || []) {
      allToolCalls.push(call);
    }
  }

  // Execute each tool and collect results
  const toolResultItems = [];
  for (const call of allToolCalls) {
    const result = await executeMyTool(call);
    toolResultItems.push(toolResultMessage(call.id, JSON.stringify(result)));
  }

  // Build follow-up request with conversation history
  let builder = mr.responses
    .new()
    .model(asModelId("claude-sonnet-5"))
    .tools(myTools)
    .user("Original user message")
    .item(assistantMessageWithToolCalls("", allToolCalls));

  // Add all tool results
  for (const resultItem of toolResultItems) {
    builder = builder.item(resultItem);
  }

  const followUp = await mr.responses.create(builder.build());
}

For parallel execution, use the ToolRegistry:

import { ToolRegistry, asModelId } from "@modelrelay/sdk";

const registry = new ToolRegistry();

// Handler receives (args, call) - args are already parsed from JSON
registry.register<{ location: string }, WeatherData>(
  "get_weather",
  async (args) => {
    return await getWeather(args.location);
  }
);

registry.register<{ timezone: string }, string>(
  "get_time",
  async (args) => {
    return new Date().toLocaleTimeString(args.timezone);
  }
);

// Execute all tool calls in parallel
const results = await registry.executeAll(response.output[0].toolCalls);
const messages = registry.resultsToMessages(results);

Tool Loops

For agentic workflows where the model may call tools multiple times, you can use the pure tool loop helper or LocalSession:

import { runToolLoop, createUserMessage, createSystemMessage } from "@modelrelay/sdk";
import { z } from "zod";
import fs from "node:fs/promises";

const tools = mr.tools()
  .add("read_file", "Read a file", z.object({ path: z.string() }), async (args) => {
    return fs.readFile(args.path, "utf-8");
  })
  .add("write_file", "Write a file", z.object({ path: z.string(), content: z.string() }), async (args) => {
    await fs.writeFile(args.path, args.content);
    return "File written";
  });

const { definitions: toolDefs, registry } = tools.build();

const input = [
  createSystemMessage("You are a careful refactor bot."),
  createUserMessage("Read config.json and add a version field."),
];

const outcome = await runToolLoop({
  client: mr.responses,
  input,
  tools: toolDefs,
  registry,
  maxTurns: 25,
  buildRequest: (builder) => builder.model(asModelId("claude-sonnet-5")),
});

if (outcome.status === "complete") {
  console.log(outcome.output);
}

For multi-turn conversations with persistence, use LocalSession with a tool registry:

import {
  LocalSession,
  ModelRelay,
  ToolRegistry,
  asModelId,
  ContextManager,
  createModelContextResolver,
} from "@modelrelay/sdk";
import fs from "node:fs/promises";

const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_API_KEY!);

// Define your tools
const registry = new ToolRegistry();

// Handler receives (args, call) - args are already parsed from JSON
registry.register<{ path: string }, string>(
  "read_file",
  async (args) => {
    return await fs.readFile(args.path, "utf-8");
  }
);

registry.register<{ path: string; content: string }, string>(
  "write_file",
  async (args) => {
    await fs.writeFile(args.path, args.content);
    return "File written successfully";
  }
);

// Create a session with automatic tool execution
const session = LocalSession.create(mr, {
  defaultModel: asModelId("claude-sonnet-5"),
  toolRegistry: registry,
  persistence: "sqlite", // or "file", "memory"
});

const context = new ContextManager(createModelContextResolver(mr), {
  strategy: "truncate",
  maxHistoryTokens: 4000,
});

const bounded = LocalSession.create(mr, {
  defaultModel: asModelId("claude-sonnet-5"),
  toolRegistry: registry,
  contextManager: context,
  persistence: "file",
});

// The session handles the tool loop automatically
const result = await session.run("Read config.json and add a new field 'version': '1.0.0'");

console.log(result.output);  // Final text response
console.log(result.usage);   // Total token usage

LocalSession will:

  1. Send your prompt to the model via /responses
  2. Execute any tool calls the model makes
  3. Send tool results back to the model
  4. Repeat until the model responds with text (no more tool calls)

Note: file and sqlite persistence require a Node.js-compatible runtime. SQLite persistence also requires installing the optional better-sqlite3 dependency.

Type Exports

The SDK exports types for all request and response objects:

import type {
  // Client types
  ModelRelayOptions,
  Response,
  ModelId,

  // Customer types
  Customer,
  CustomerCreateRequest,
  CheckoutSession,
  SubscriptionStatus,

  // Tier types
  Tier,
  PriceInterval,

  // Error types
  FieldError,
  ErrorCode,
} from "@modelrelay/sdk";

// Generated types from OpenAPI spec
import { generated } from "@modelrelay/sdk";
type ResponsesResponse = generated.components["schemas"]["ResponsesResponse"];

Next Steps