Providers
Providers are the bridge between Standard Agents and LLM APIs. They translate Standard Agents requests into provider-native calls and return responses in the standard format.
Decision providers implement the Decision Provider Interface. Their factories receive standard provider connection configuration. Conversational generation methods are not required by that interface.
Provider Packages
The specification does not require a fixed provider list. Implementations MAY ship any provider package that satisfies the Provider specification. Examples include:
- OpenAI
- Anthropic
- OpenRouter
- Cerebras
- DeepInfra
- Cloudflare
- Groq
- xAI
Runtimes MUST treat provider extensions as separate provider identities from their base providers.
Extending a Provider
A provider extension MAY be defined when an API is compatible with an existing provider package but requires a distinct name, API key, base URL, model list, icons, tools, pricing, or request behavior.
import { defineProvider, providerEnv } from '@standardagents/spec';
import { openai } from '@standardagents/openai';
export default defineProvider({
name: 'openai_compatible',
label: 'OpenAI-compatible endpoint',
baseProvider: openai,
config: {
apiKey: providerEnv('EXAMPLE_PROVIDER_API_KEY', {
valueType: 'secret',
required: true,
description: 'API key for the compatible endpoint.',
}),
baseUrl: providerEnv('EXAMPLE_PROVIDER_BASE_URL', {
valueType: 'url',
default: 'https://api.example.com/v1',
description: 'OpenAI-compatible API base URL.',
}),
},
});
A model definition MAY reference the extension provider:
import { defineModel } from '@standardagents/spec';
import openaiCompatible from '../providers/openai-compatible';
export default defineModel({
name: 'example_chat',
provider: openaiCompatible,
model: 'example-chat',
});
The base provider and extension provider are distinct provider identities. The extension reuses the base provider implementation unless it overrides a method.
Provider Configuration
Provider configuration is connection-level configuration used to construct the provider client. It is separate from providerOptions, which are per-model, per-prompt, or per-request options.
Provider config values MAY include:
- API keys
- base URLs
- regions
- account IDs
- deployment IDs
- request timeouts
- default request headers (
defaultHeaders)
Base providers expose supported config locations through configSlots:
openai.configSlots = {
apiKey: {
type: 'secret',
required: true,
defaultEnv: 'OPENAI_API_KEY',
overridable: true,
},
baseUrl: {
type: 'url',
default: 'https://api.openai.com/v1',
overridable: true,
},
};
A provider extension’s config object chooses where each slot gets its value. If a slot is not overridden, the base provider’s defaultEnv and default are used. For example, an OpenAI-based extension that only overrides baseUrl still uses OPENAI_API_KEY; to use its own key, the extension MUST override apiKey.
Provider config value sources are:
| Source | Use |
|---|---|
providerEnv(name, options) | Environment variable source with metadata for setup UIs and validation. |
providerValue(value) | Supplies a literal value. |
providerConst(value) | Alias for providerValue(value). |
bare literal (string / number / boolean) | Shorthand for a constant value. |
| nested map | An object whose leaves are themselves config values. |
A config slot MAY be a nested map whose leaves are individual config
values, letting a single slot combine environment and constant entries. The
defaultHeaders slot uses this form to declare per-request headers, mixing
secret env values with constant ones. When a leaf resolves to no value — an
optional environment variable that is unset — that leaf is omitted rather than
sent as an empty value.
export default defineProvider({
name: 'openai_compatible',
baseProvider: openai,
config: {
apiKey: providerEnv('EXAMPLE_PROVIDER_API_KEY', { valueType: 'secret', required: true }),
baseUrl: providerEnv('EXAMPLE_PROVIDER_BASE_URL', { valueType: 'url' }),
// Sent on every request to the provider backend.
defaultHeaders: {
'x-account-id': providerEnv('EXAMPLE_ACCOUNT_ID', { valueType: 'secret', required: true }),
'x-api-beta': 'responses=experimental', // constant literal
'x-optional-key': providerEnv('EXAMPLE_OPTIONAL', { required: false }), // omitted when unset
},
},
});
defaultHeaders is a connection-level slot that HTTP-client provider
implementations SHOULD forward on every request, so a provider extension can
target an API-compatible backend that requires additional headers (account
ids, API betas, relay authentication) without overriding a method.
Method Overrides
Provider extensions can override provider behavior with an overrides object. Each override receives a context object and next(), where next() calls the base provider implementation.
export default defineProvider({
name: 'openai_compatible',
label: 'OpenAI-compatible endpoint',
baseProvider: openai,
config: {
apiKey: providerEnv('EXAMPLE_PROVIDER_API_KEY', { valueType: 'secret', required: true }),
baseUrl: providerEnv('EXAMPLE_PROVIDER_BASE_URL', {
valueType: 'url',
default: 'https://api.example.com/v1',
}),
},
overrides: {
getIcon({ modelId }, next) {
if (modelId?.startsWith('example-')) return '/icons/example-provider.svg';
return next();
},
async getModels({ config }) {
const response = await fetch(`${config.baseUrl}/models`, {
headers: { Authorization: `Bearer ${config.apiKey}` },
});
const body = await response.json();
return body.data;
},
},
});
An override MUST call next() to compose with the base provider. If it does not call next(), it replaces the base provider behavior.
Overrideable methods are generate, stream, supportsModel, getModels, getModelsPage, getModelCapabilities, getTools, getIcon, inspectRequest, and getResponseMetadata.
The request-scoped overrides (generate, stream, inspectRequest,
getResponseMetadata) additionally receive the executing thread’s state on
their context when the runtime has one; catalog overrides (getModels,
getIcon, etc.) run without a thread and leave it undefined.
Per-request Transport (createFetch)
A provider extension may declare createFetch to build a request transport per
request. The runtime calls it with the executing thread’s state and uses the
returned transport as ProviderFactoryConfig.fetch. Because the transport can
close over state, an extension declaring createFetch is constructed fresh
per request rather than cached. It is a generic hook — “override the fetch
mechanism however you want” — with no knowledge of what the transport does.
export default defineProvider({
name: 'tunneled_openai',
baseProvider: openai,
config: {
apiKey: providerEnv('EXAMPLE_API_KEY', { valueType: 'secret', required: true }),
},
// Route every request through a custom transport bound to this request.
createFetch: ({ state }) => makeTransport(state),
});
API Key Management
API keys and other credentials MUST be declared as provider config slots or provider extension value sources with type/valueType: 'secret'.
Runtimes and setup UIs SHOULD:
- mark secret values as sensitive
- avoid logging raw secret values
- report missing required provider variables before model execution
- allow compatible provider extensions to use their own secret variables instead of the base provider’s default key
An OpenAI model MAY use OPENAI_API_KEY while an OpenAI-compatible extension uses EXAMPLE_PROVIDER_API_KEY and EXAMPLE_PROVIDER_BASE_URL.