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.
npm install mayura @mayurajs/provider-openaiimport { 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 aspricingon 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.
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:
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:
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:
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
attemptstimes 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:
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, withmayuraas 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:
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:
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.
Related
- Model providers: the adapters of Mayura 1.0, and every option.
- Model routing: failover, circuit breakers and accounting.
- Permissions: how
model:<id>grants work.