@yesimagent/gateway
v0.0.9
Published
An AI SDK model registry. Resolve a `provider:model` reference to a native AI SDK model, with failover groups and circuit breakers on top. Cordis-free — no framework, no service shell.
Readme
@yesimagent/gateway
An AI SDK model registry. Resolve a provider:model reference to a native AI SDK model, with failover groups and circuit breakers on top. Cordis-free — no framework, no service shell.
The gateway owns the boring part of using many providers: building AI SDK providers from configuration, expanding credentials, keeping a catalog of declared models, and offering interchangeable models through a group with health tracking. It hands out whatever the AI SDK factories return — unless a reference pins a think level, it never wraps or reinterprets models.
- Native models out — every resolve returns what the AI SDK factories built.
providerOptionskeys, wire protocols, and capabilities behave exactly as the AI SDK documents them. The one exception is a reference that pins a think level (provider:model:high), which adds a thin middleware setting the AI SDK'sreasoningcall option. - Configuration in — a host reads its own file format (yaml, json, …) and hands the parsed shape over. The gateway only defines the shape and expands
${VAR}references against itsenv. - Fail small — bad configuration fails at construction with a
GatewayError, not at the first call.
Install
npm add @yesimagent/gatewayQuickstart
import { createGateway } from "@yesimagent/gateway";
const gateway = createGateway({
config: {
providers: {
relay: {
api: "openai-completions",
baseUrl: "https://relay.example.com/v1",
apiKey: "${RELAY_KEY}",
models: [{ id: "main", name: "Main", contextWindow: 262144 }],
},
},
groups: {
fast: { strategy: "failover", models: ["relay:main"], circuitBreaker: { failureThreshold: 2, cooldownSeconds: 60 } },
},
},
});
const model = gateway.languageModel("relay:main");
const [candidate] = gateway.group("fast").candidates();apiKey: "${RELAY_KEY}" is read from process.env when the gateway is built; an unset variable fails construction with the variable named.
Configuration
A GatewayConfig has two sections, both optional:
interface GatewayConfig {
providers?: Record<string, ProviderConfig>;
groups?: Record<string, GroupConfig>;
}Providers
Each entry in providers builds one AI SDK provider through a registered api.
interface ProviderConfig {
api: string; // which ApiFactory builds it, see the table below
apiKey: string; // required; ${NAME} reads it from the gateway's env
baseUrl?: string; // omitted means the api's own default endpoint
headers?: Record<string, string>; // values may use ${NAME} the same way
settings?: Record<string, unknown>; // spread verbatim into the AI SDK factory
models?: ModelConfig[]; // what this endpoint serves
}
interface ModelConfig {
id: string; // model id as the provider knows it, e.g. "gpt-5.1"
type?: ModelType; // defaults to "language"
name?: string; // display name
toolCall?: boolean; // whether the model calls tools
reasoning?: boolean; // whether the model produces reasoning content
input?: ModelModality[]; // accepted input modalities
contextWindow?: number; // total context, in tokens
maxTokens?: number; // maximum output, in tokens
dimensions?: number; // embedding models only
}Everything a model needs is in the configuration: the catalog is built once from models at load time, and gateway.model("relay:main") looks it up. Everything a declaration carries besides its identity becomes the entry's metadata, typed by modality — ModelMetadata<T> is derived from ModelConfig, so a language entry carries toolCall / contextWindow while an image entry carries name alone, and a new config variant needs no extra mapping. There is no discovery, no background refresh, no derived state to invalidate — reconfigure() replaces the whole picture.
Groups
A group is a named set of interchangeable language models:
interface GroupConfig {
strategy?: "failover" | "round-robin" | "random"; // defaults to "failover"
models: string[]; // "provider:model" references
circuitBreaker?: Partial<CircuitBreakerConfig>; // defaults: 3 failures / 60s cooldown
}gateway.group("fast").candidates() returns the members in the strategy's order, skipping models whose breaker is open. Each candidate carries success() / failure() feedback into the group's breakers. When every breaker is open, the full list is offered anyway — a mute bot is worse than one more doomed attempt.
Every member must resolve when the gateway is built. A group with a typo in it fails at construction, with the reference named.
Think levels
A language reference may pin a think level as a trailing segment — provider:modelId[:level] — with the levels none, minimal, low, medium, high, xhigh, max. The pinned level rides into every call the model sees as the AI SDK's portable reasoning option, which each provider translates to its native controls (reasoningEffort, thinking, thinkingConfig, …).
gateway.languageModel("relay:main:low");groups:
fast:
models: ["relay:main:low", "relay:turbo"] # levels mix freely with plain members- The trailing segment only counts when it exactly names a level, so model ids that contain colons (
ollama:qwen3:32b) parse as before. The flip side: a model id that itself ends in a level name cannot be referenced. - A member pinned at a level is its own candidate and its own breaker entry — the same model at two levels are independent members.
- When a declaration lists
thinking.efforts, a pinned level outside that list fails at construction with the level named. maxsits outside the AI SDK spec's enum: OpenAI-family endpoints accept it, other providers emit an unsupported warning and ignore it.
Apis
The api key selects the factory that builds the provider. Five are built in (BUILTIN_APIS is exported, so a host can inspect or spread it):
| api | Factory | Wire |
| ---------------------- | --------------------------- | -------------------------------------------------------------------------------- |
| openai-completions | @ai-sdk/openai-compatible | /chat/completions; every endpoint reports its own id.chat provider namespace |
| openai-responses | @ai-sdk/openai | OpenAI Responses API |
| anthropic-messages | @ai-sdk/anthropic | Anthropic Messages |
| google-generative-ai | @ai-sdk/google | Gemini |
| typesafe-ai | @ai-sdk/typesafe-ai | TypeSafe System One; decision models only |
settings is spread straight into the AI SDK factory, so anything the factory accepts can be passed — authToken for Anthropic enterprise relays, includeUsage for compatible endpoints, and so on. Explicit fields (baseUrl, apiKey, headers, the gateway's fetch) always win over settings.
Custom apis
Anything the built-in dialects don't cover, register through apis — including a provider you built yourself and want to keep a typed reference to:
import { createOpenAI } from "@ai-sdk/openai";
import { createGateway } from "@yesimagent/gateway";
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const gateway = createGateway({
config: { providers: { openai: { api: "openai", apiKey: "unused", models: [{ id: "gpt-5.1" }] } } },
apis: { openai: () => openai },
});
// The concrete instance is yours, so provider-native tools stay fully typed:
const tools = { web_search: openai.tools.webSearch({}) };Gateway
interface Gateway {
defineApi(name: string, factory: ApiFactory): () => void;
provider(id: string): ProviderV4 | undefined; // for ProviderV4 parts the gateway doesn't model
readonly apis: readonly string[];
reconfigure(config: GatewayConfig): void;
languageModel(name: string): LanguageModelV4;
embeddingModel(name: string): EmbeddingModelV4;
imageModel(name: string): ImageModelV4;
speechModel(name: string): SpeechModelV4;
transcriptionModel(name: string): TranscriptionModelV4;
rerankingModel(name: string): RerankingModelV4;
decisionModel(name: string): DecisionModelV4;
group(name: string): Group;
groups(): readonly string[];
models<T extends ModelType = ModelType>(type?: T): readonly Model<T>[]; // the catalog, for host-side decisions
model(name: string): Model | undefined;
providers(): readonly string[];
}models("language")narrows both halves of an entry: the result isModel<"language">, whosemetadataisModelMetadata<"language">.models()without an argument returns the whole catalog.languageModel("relay:main")resolves and builds; a typo, a wrong modality, or an unlisted model throws aGatewayErrornaming the reference.model("relay:main")is the pure catalog query:undefinedwhen nothing is declared under that reference. Hosts that want pre-flight validation check it before resolving.provider("relay")hands back the AI SDK provider for the parts the gateway doesn't model, such asfiles()andskills().
