Skip to content
Router One

Connect Mastra agents to Router One with one model url and one key

Mastra is a TypeScript framework for building AI agents and workflows. Its model router normally takes a provider/model string and calls that provider's own API with that provider's key from the environment; for any other OpenAI-compatible server, the documented form is a model object with id, url, and apiKey. With url set, Mastra sends Chat Completions to that base URL, so pointing it at Router One reaches every chat model in the catalog served on /v1/chat/completions, with a cost and latency trace per request, while the agent loop, your tools, memory, storage, and Studio keep running in your own process. This guide configures one Agent with the object form, explains how the first segment of id is treated (the detail that decides which model string reaches the gateway), runs generate() with a tool and then stream(), and shows what maxSteps and maxRetries bound, which memory features need an embedder the gateway does not serve, and how to reconcile Mastra's token counts with Dashboard → Logs.

Install Mastra and set your credentials

Mastra v1 requires Node.js 22.13.0 or newer, and its docs note that Node.js 22.18.0 and later run TypeScript files directly, which is how this guide runs agent.ts. For a new project, create-mastra builds a starter configured for the model provider you select (OpenAI, Anthropic, Gemini, or xAI), and its --empty flag creates a provider-free scaffold without agents, which is the closer fit here. To add Mastra to an existing project, make sure package.json has "type": "module" and install the packages the Mastra quickstart lists, as below. No AI SDK provider package is needed for the object form used in this guide: @mastra/core bundles the OpenAI-compatible provider it builds from that object. The shell example is for macOS/Linux; replace the placeholder with a Router One key created for this agent. ROUTER_ONE_API_KEY is a name this example chooses and reads explicitly. Mastra does not look up any environment variable for a model that has url set, so the key has to be passed as apiKey, and OPENAI_API_KEY is not needed.

terminal
# New project instead: npx create-mastra@latest my-app --empty
npm install @mastra/core@latest zod@latest typescript@latest @types/node@latest mastra@latest
export ROUTER_ONE_API_KEY="sk-your-router-one-key"

Configure Mastra to use the Router One base URL

Save this as agent.ts and run node agent.ts. The model field takes an object instead of a provider/model string. id is split at the first slash: the first segment (custom, the label Mastra's docs use for a custom endpoint) becomes the provider name in Mastra's logs and spans, and everything after it is sent as the model field of the request, so custom/anthropic/claude-sonnet-5 sends anthropic/claude-sonnet-5 and custom/grok-4.6 sends grok-4.6. Replace the placeholder with the exact ID from /models and keep custom/ in front of it. url is the /v1 base URL, not the chat endpoint: the bundled OpenAI-compatible provider appends /chat/completions, so each model call is one POST /v1/chat/completions. apiKey becomes the Authorization: Bearer header. The tool is defined with createTool (id, description, a zod inputSchema, execute) and registered under the key addTool; that key is the function name the model sees in the request's tools field, which is sent with tool_choice auto. generate() returns after the whole loop: with this prompt the first request comes back with a tool call, Mastra runs execute in your process, and a second request returns the answer, so result.steps has two entries and result.usage sums the tokens the API reported for both. maxSteps: 3 caps the loop at three model calls; without it the default is 5. stream() sends the same request with stream: true and yields text through textStream as it arrives. In a Mastra project, keep the Agent under src/mastra/agents and register it in src/mastra/index.ts with new Mastra({ agents: { routerOneAgent } }); the model object is identical.

agent.ts
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

const addTool = createTool({
  id: "add",
  description: "Add two numbers",
  inputSchema: z.object({ a: z.number(), b: z.number() }),
  execute: async ({ a, b }) => ({ sum: a + b }),
});

export const routerOneAgent = new Agent({
  id: "router-one-agent",
  name: "Router One Agent",
  instructions: "You are a concise assistant. Use the add tool for arithmetic.",
  model: {
    // Mastra strips the "custom/" label and sends the rest as the model, e.g. custom/anthropic/claude-sonnet-5
    id: "custom/<exact-model-id-from-/models>",
    url: "https://api.router.one/v1",
    apiKey: process.env.ROUTER_ONE_API_KEY,
  },
  tools: { addTool },
});

// 1. generate(): one POST /v1/chat/completions per step; maxSteps caps the loop (default 5)
const result = await routerOneAgent.generate("What is 2 + 3?", { maxSteps: 3 });
console.log(result.text, result.steps.length, result.usage);

// 2. stream(): the same request with stream: true
const stream = await routerOneAgent.stream("Say hello in five words.");
for await (const chunk of stream.textStream) process.stdout.write(chunk);

Which Mastra setting sends which request

The model object carries the connection; the Agent options decide how many requests a call makes and what each one asks for. The rows below give the value used in this guide, the request it produces, and what to confirm before relying on it.

Mastra settingValueWhat to verify
model → urlhttps://api.router.one/v1The bundled provider appends /chat/completions; an AI_APICallError with statusCode 404 whose url lacks /v1 means the base URL lost its /v1
model → apiKeyA Router One key created for this agent, with maxSpendSent as Authorization: Bearer; no environment variable is read when url is set, so an undefined value leaves the header out and the gateway answers 401
model → idcustom/ followed by the exact catalog ID, for example custom/anthropic/claude-sonnet-5The model in each Logs trace matches /models character for character; without the label, the ID's own prefix is removed before sending
tools: { addTool } built with createToolChat Completions with a tools field and tool_choice auto; the object key is the function nameTool calling on the model detail page; execute runs in your process, and the tool result goes out in the next request
maxSteps (default 5)Caps the sequential model calls of one generate() or stream(); each step is one requestWith no retries, result.steps.length equals the number of Logs rows for that call
maxRetries on the Agent (default 0)Extra attempts per model call after 408, 409, 429, 5xx, or an error that is not an HTTP responseEach attempt that reaches the gateway is its own Logs row; 401, 402, and 404 are not retried
structuredOutput: { schema }Adds response_format json_schema to the request; jsonPromptInjection puts the schema into the prompt insteadStructured output on the model detail page; result.object is validated against the schema
agent.stream()The same request with stream: true; the object form sends no stream_optionsStreaming on the model page; an interrupted stream is recorded as HTTP 499 client_cancelled

Budget one agent and reconcile its requests

One generate() or stream() call is a loop: each step is one POST /v1/chat/completions, a step that returns tool calls is followed by another step after Mastra runs the tools, and the loop ends when the model answers without a tool call or maxSteps is reached (default 5). Input tokens grow with every step, because each request resends the instructions, the conversation, and all earlier tool calls and results, so maxSteps bounds the number of requests, not the spend. Retries add requests only if you ask for them: the Agent option maxRetries defaults to 0 in the current API, so a failed call is not repeated; with maxRetries: 2, a persistent 5xx becomes three requests for that step. Retryable means 408, 409, 429, 5xx, or an error that is not an HTTP response; 401, 402, and 404 are never retried. The older generateLegacy() path keeps its own maxRetries with a default of 2. A model array with fallback entries multiplies again, since each entry has its own retry count; the gateway's same-family fallback on 5xx and timeouts happens before a response reaches Mastra, so a client-side list adds requests only when the gateway itself returns an error. Optional features send model requests of their own: structuredOutput with a separate model makes two calls per answer, generateTitle adds a call on the agent's model unless you give it another, and Observational Memory runs background Observer and Reflector agents. Every attempt that reaches the gateway is its own row in Dashboard → Logs with its own request_id, cost, and status. Give each agent a dedicated key with maxSpend: a looping run then gets HTTP 402 at the cap you set, surfaced as AI_APICallError with statusCode 402, and the wallet and your other keys are untouched. result.usage, the per-step usage in result.steps, and the token and cost figures in Mastra's tracing are Mastra's tally and estimate of what the API reported; the charge is what Logs records. Reconcile by key, time, model, and token counts, and keep the request_id when reporting a failure. Router One records model-call metadata; it does not run the agent loop, your tools, workflows, memory, or storage.

Memory, semantic recall, and the embedder stay on your side

Memory is a Mastra feature backed by your own storage: install @mastra/memory and a storage adapter such as @mastra/libsql, and pass resource and thread with each call. Message history then loads the last 10 messages by default (lastMessages) into every request, which raises input tokens per step but adds no request. Semantic recall is disabled by default and needs two more pieces, a vector store and an embedder; the embedder is called on every turn, first to embed the new prompt for the search and afterward to embed the new messages for the index. Router One serves no /v1/embeddings, so do not point the embedder at the gateway: keep it on a provider that offers embeddings, for example ModelRouterEmbeddingModel('openai/text-embedding-3-small') with that provider's own key, or run it locally with fastembed from @mastra/fastembed. Those calls never appear in Dashboard → Logs. Observational Memory is different: its Observer and Reflector are chat-model calls, and when no model is set they default to a Google model resolved through Mastra's model router with a Google API key from your environment, outside the gateway. Its model option takes the same types as an Agent's model, so passing the same id, url, and apiKey object puts those background calls on your Router One key and into Logs. generateTitle likewise accepts a model and otherwise uses the agent's own. Threads, vectors, and working memory stay in the storage you configured; the gateway sees only the messages of each model request.

Which model ID should Mastra send?

Copy the exact model ID from /models, preserving case, hyphens, and version suffixes; do not substitute a display name. Open its detail page and match the supported API endpoints, context window, and capabilities such as tool calling to the provider and features selected in Mastra. A catalog listing does not mean the client can use every feature of that model. Give each tool a dedicated API key with a maxSpend cap.

Which API protocol is Mastra using?

OpenAI-compatible describes an interface format; it does not make Chat Completions (/v1/chat/completions), Responses (/v1/responses), and Anthropic Messages (/v1/messages) interchangeable. Check the installed client version, provider configuration, and actual request path against the model detail page and API compatibility fact sheet. A successful plain-text chat does not establish support for hosted tools, conversation state, or file-editing features.

Verify the Mastra call in your request trace

Send a simple text request from Mastra, then match its trace in Dashboard → Logs by time, model, and request_id: tokens, cost, latency, and status. Next, test streaming, tool calls, and multi-turn history separately. For failures, retain the actual request path, full error message, and request_id. If there is no matching log, check client configuration and connectivity before attributing the error to the gateway or upstream.

FAQ

The call fails with a model error although I copied the ID from /models. What did Mastra send?

Mastra treats the first segment of model.id as a provider label and sends only the rest. With id: 'anthropic/claude-sonnet-5' and url set, the request body carries model: claude-sonnet-5, and 'openai/gpt-5.5' becomes gpt-5.5; on Router One the vendor prefix is part of the catalog ID, so those shortened names are not the model you chose. Write id as custom/ plus the full catalog ID: custom/anthropic/claude-sonnet-5, custom/openai/gpt-5.5, custom/grok-4.6. An ID with no slash at all never leaves your process: Mastra throws [Agent:<name>] - Failed to resolve model configuration, and details.originalError reads Attempted to parse provider/model from grok-4.6 but this ID doesn't appear to contain a provider. Do not use netlify or mastra as the label; those are Mastra gateway names, and gateway-prefixed IDs are parsed as gateway/provider/model. Every HTTP failure arrives as AI_APICallError: generate() throws it, while stream() delivers it to onError and stream.error and its textStream simply ends. Read statusCode, url, and responseBody on the error. 404 with a url of https://api.router.one/chat/completions means model.url is missing /v1, and the gateway's message tells you to fix the base URL. 401 means the key was not accepted, or apiKey was undefined and no Authorization header was sent. Mastra also logs Upstream LLM API error with provider and modelId, and that modelId is the exact model string that went on the wire.

Can I pass an AI SDK provider instead of the model object, and which endpoint does each one call?

Yes. Agent.model also accepts an AI SDK language model, and then no ID parsing happens: the string you pass is the string that is sent. createOpenAICompatible from @ai-sdk/openai-compatible, given name, baseURL, and apiKey, calls /chat/completions, and chatModel('anthropic/claude-sonnet-5') goes out unchanged; it is the same provider Mastra's object form builds internally. Adding includeUsage: true makes streaming requests carry stream_options with include_usage, which the object form does not send, so choose this path when you need token usage on the stream result. createOpenAI from @ai-sdk/openai with baseURL behaves differently: since AI SDK 5 its default call, provider(modelId), uses the Responses API and posts to /v1/responses, which on Router One serves only the currently listed GPT-family and DeepSeek IDs. Use provider.chat(modelId) to stay on /v1/chat/completions, which serves every chat model in the catalog. Mastra's docs also describe api: 'responses' for the object form; leave it unset unless the model and the features you need are listed for /v1/responses on the model detail page.

Do Mastra Studio and Mastra's tracing send anything to Router One?

Only the model calls. mastra dev starts Studio on your machine at localhost:4111, where it lists the agents registered on the Mastra instance in src/mastra/index.ts; a chat there runs the same Agent with the same model object, so each step shows up in Dashboard → Logs like a call from your own code. Traces, logs, and metrics are collected by @mastra/observability and written to the storage or exporters you configure, and the router model leaves apiKey, headers, and url out of what it writes to spans. Token counts there come from the usage the API reported and cost is Mastra's estimate, so treat Logs as the record of what was charged. Use a separate key for local Studio sessions if you want them kept apart from production traffic.

Which models can Mastra use through the gateway?

Choose a current catalog model that supports both the endpoint and the features Mastra uses. Check /models and the model detail page for the exact ID, current rates, and capabilities; a family name such as GPT or Claude is not a compatibility guarantee. Seeing a model in the picker confirms discovery, so verify an actual request too.

Models are listed, but requests fail with 400 or 404. What should I check?

Record the actual request path and error message, then check the exact model ID. A 400 can indicate invalid parameters, unsupported tools, or a model/endpoint mismatch; a 404 can indicate an incorrect path or missing resource, so it does not by itself establish that a model was retired. If the error says must be called via, use the named endpoint or select a model supported on the current endpoint. Do not add or remove /v1 or /chat/completions across all clients indiscriminately.

Does this work from Mainland China?

Yes. The gateway is reachable from Mainland China without a VPN, and the configuration is identical to the global setup.

How do I debug a 401/402/403/429?

Match the request and error message in Dashboard → Logs. For 401, check whether the key was sent and is valid; for 402, check wallet balance and maxSpend; for 403, check key permissions and access restrictions. For 429, distinguish request/token limits from upstream throttling using the error details. Keep the request_id and follow the error-codes reference.