Skip to content
Mayura

Agents and models

Model registry

Name models as provider/model from @mayurajs provider packages, with explicit or catalog prices, a per-call cost bound, safe retries and fallback chains.

A model registry turns provider packages into models you can name. Each @mayurajs/provider-* package knows how to call one provider through that provider's official SDK. createModels gives every model an id of the form <provider>/<model>, a price and a per-call cost bound, and runtimes grant each model by that id.

bash
npm install mayura @mayurajs/provider-openai
ts
import { createModels, createRuntime, defineAgent, z } from 'mayura';
import { openai } from '@mayurajs/provider-openai';

const models = createModels({
  providers: [openai({ apiKey: process.env['OPENAI_API_KEY']! })],
  prices: { 'openai/gpt-5.1': { inputMicrosPerMillionTokens: 1_250_000, outputMicrosPerMillionTokens: 10_000_000 } },
  maxCallCostMicros: 50_000, // at most $0.05 per model call
});

const agent = defineAgent({
  id: 'helper', version: '1', instructions: 'Answer in one sentence.',
  model: models.model('openai/gpt-5.1'), tools: [],
  input: z.object({ question: z.string() }), output: z.object({ answer: z.string() }),
});

const runtime = createRuntime({ profile: 'ephemeral', permissions: { allow: ['model:openai/gpt-5.1'] }, limits: { maxCostMicros: 200_000 } });

The adapter's id is the model id, so the grant names the exact model: model:openai/gpt-5.1. Granting one model never grants another.

Prices

Every model needs a price, in micros per million tokens, and the registry refuses a model without one. There are two ways to give it:

  • Your own prices, in prices (by model id) or as pricing on one model. Use these when the numbers matter: they are the prices you pay, including any discount.
  • The provider package's catalog, with prices: 'catalog'. Each provider package ships its provider's list prices and the date they were checked. Prices change, so a catalog can be out of date: registry.list() shows each model's catalog date.
ts
const models = createModels({ providers: [openai({ apiKey })], prices: 'catalog', maxCallCostMicros: 50_000 });

for (const model of models.list()) console.log(model.id, model.catalogAsOf, model.pricing);

A price you give always wins over the catalog. See Costs and budgets for how a call's cost is reserved and charged.

Long prompts

Some providers bill long prompts at higher rates: OpenAI, for example, bills a prompt over 272K input tokens at twice the input rate and one and a half times the output rate, for the whole request. Give those rates as longContext, and provider packages charge them for any call whose input is over the threshold:

ts
const models = createModels({
  providers: [openai({ apiKey })],
  maxCallCostMicros: 500_000,
  prices: {
    'openai/gpt-6-sol': {
      inputMicrosPerMillionTokens: 2_000_000, outputMicrosPerMillionTokens: 10_000_000,
      longContext: { aboveInputTokens: 272_000, inputMicrosPerMillionTokens: 4_000_000, outputMicrosPerMillionTokens: 15_000_000 },
    },
  },
});

Long-context rates must be at least the standard ones. Catalogs include them, and err towards charging more: they charge cached input at the full input rate, apply the long-context rule wherever it might apply, and leave out promotional prices, which rise when the promotion ends.

The per-call bound

maxCallCostMicros is the most one model call may cost. The runtime reserves it before every call, and a call that doesn't fit in the run's budget never starts. It is required: a registry without a bound is refused. A model can have its own bound:

ts
const large = models.model('openai/gpt-5.1', { maxCostMicros: 200_000 });

Retries

retry calls the same model again after a rate limit, an unavailable provider or a timeout:

ts
const model = models.model('openai/gpt-5.1', { retry: { attempts: 3, backoffMs: 500 } });
  • It never retries after the caller cancels, after a failure that would repeat (an authentication error or a refused request), or once streamed text has been shown.
  • Every attempt is charged: an attempt with a known cost is charged that, and one with an unknown cost its full bound. The model's per-call bound becomes attempts times its bound, so the reservation covers every attempt.
  • Waits double from backoffMs (default 500 ms) and end as soon as the call is cancelled.

Model calls change nothing outside Mayura, so retrying them is safe. Tools are different: see Outcomes.

Provider packages turn off their SDK's own retries, so the registry is the only place a call is repeated.

Chains

A chain tries several models in order, for example to keep answering through one provider's outage. It is a model router over registry models:

ts
const support = models.chain('support', ['openai/gpt-5.1', 'openai/gpt-5.1-mini'], {
  models: { 'openai/gpt-5.1': { retry: { attempts: 2 } } },
});

The chain's id is yours to choose, and it is what runtimes grant: model:support. Granting a chain grants every model in it, which is why its name is explicit rather than derived.

Provider packages

Package Provider Model ids
@mayurajs/provider-anthropic Anthropic Claude, through the official @anthropic-ai/sdk and the Messages API anthropic/<model>
@mayurajs/provider-azure Azure OpenAI, through the official openai SDK and Azure's v1 API; built on responsesProvider azure/<deployment>
@mayurajs/provider-bedrock Amazon Bedrock (Claude, Nova, Llama, Mistral and more), through the official AWS SDK and the Converse API bedrock/<model or inference profile id>
@mayurajs/provider-google Google Gemini, through the official @google/genai SDK and the Gemini API google/<model>
@mayurajs/provider-groq Groq, through the official groq-sdk and Groq's Chat Completions API groq/<model>, such as groq/openai/gpt-oss-120b
@mayurajs/provider-mistral Mistral, through the official @mistralai/mistralai SDK and the Chat Completions API mistral/<model>
@mayurajs/provider-ollama Ollama, on your machine or in Ollama's cloud, through the official ollama SDK; no catalog, since local models cost nothing ollama/<model>, such as ollama/gpt-oss:20b
@mayurajs/provider-openai OpenAI, through the official openai SDK and the Responses API openai/<model>

Each package:

  • takes its credentials as options and never reads the environment: no API keys, base URLs or organizations are discovered;
  • turns off its SDK's retries, and uses the time limit the registry gives it;
  • sends tool inputs and structured output as strict JSON Schema, and reports why a call failed without passing on the provider's own error text;
  • bounds request and response sizes;
  • is published in step with mayura, with mayura as its peer dependency.

The packages of the 1.0 releases, mayura/provider-openai and mayura/provider-anthropic, keep working as before. See Model providers.

Writing a provider package

A provider is an object with an id (lowercase letters, digits and -) and a model(name, settings) function that returns a ModelAdapter. The adapter must use settings.id as its id and settings.maxCostMicros as its bound, and compute each call's cost from settings.pricing:

ts
import type { ModelProvider } from 'mayura';

export const acme: ModelProvider = {
  id: 'acme',
  model: (name, settings) => acmeAdapter({ model: name, ...settings }),
};

mayura/testing has the contract every adapter must keep, as test cases any test runner can run. Give it a harness that makes your adapter's transport answer each scenario, usually through your SDK's fetch option:

ts
import { it } from 'vitest';
import { modelAdapterConformance, type ModelAdapterHarness } from 'mayura/testing';

const harness: ModelAdapterHarness = {
  adapter: (scenario, settings) => acme.model('model-1', settings),
};

for (const test of modelAdapterConformance) it(test.name, () => test.run(harness));

The cases check response shapes, costs, tool calls, streaming, the reason for each kind of failure, that provider error text never leaks, cancellation, timeouts and strict schemas.