Providers and your own keys
A run makes model calls for three roles:
actor: the persona deciding each step.judge: the observer that reads each session and merges the run’s findings.reproduction: the check that asks whether a replayed failure supports the issue’s claim.
Saram bills the run itself (Usage and billing). Who bills the model calls depends on
where they are made, and that is what the spec’s providers block says.
Who pays for the model calls
Section titled “Who pays for the model calls”| The calls are made on | Who bills them | How the spec names it |
|---|---|---|
| Your own provider key, stored with Saram | Your provider, on your account with them | A providers entry whose credential_ref is your key’s secret_ref |
| Your ChatGPT login, where Saram offers it | OpenAI, within your ChatGPT subscription’s limits | The providers block codex_status answers |
| Saram’s own credential | Nobody separately | No providers block |
On the Free and Pro plans, every role is on your own provider. A spec with no providers block,
or one that leaves a role on a Saram-held credential, is refused with a 422 that names the role:
plan 'free' pays for none of a run's model calls, and the actor role would make its calls on a Saram-held credential (this spec carries no providers block): bring your own key for it - POST /api/v1/credentials returns the credential_ref to name in providers.roles.actorSaram’s own models run where your organisation has an agreement with Saram for them, or on an older
plan that includes them. house_models: true in get_usage says Saram carries
your model spend.
Every validate answer says who bills each role, in cost_ceiling.model_calls. A model call is never
part of cost_ceiling.usd.
Store your own key
Section titled “Store your own key”Supported providers, by the kind a credential is stored as:
anthropic, bedrock, openai, openrouter, azure, google, google-cloud, litellm
Store a key on the dashboard under Settings > Credentials, or with
create_credential. The key needs the credentials:write scope. Build
the body with jq, so the key stays out of your shell history:
jq -n --arg v "$ANTHROPIC_API_KEY" '{name: "anthropic", kind: "anthropic", value: $v}' | curl -s -X POST https://api.saram.sh/api/v1/credentials \ -H "Authorization: Bearer $API_KEY" -H 'content-type: application/json' -d @-It answers 201 with the credential’s metadata:
{ "id": "...", "name": "anthropic", "kind": "anthropic", "secret_ref": "awssm://.../anthropic", "created_at": "2026-10-23T09:00:00Z"}secret_refis what the spec names ascredential_ref. Only your organisation’s specs may name it; another organisation’s spec naming it is refused.- The value is never returned. No response anywhere has a
valuefield, and the value is not logged.list_credentialsanswers metadata only. nameis lowercase letters, digits,_and-, up to 64 characters. A name you already have is a409.valueis the key as one string. Forgoogle-cloudit is the service-account JSON document.- There is no replace. To rotate a key,
delete_credentialit, then store it again under the same name.
The same route stores the other credentials a spec names: account for test logins
(The run spec), webhook for a webhook’s
signing key, and github for a GitHub token.
Name it in the spec
Section titled “Name it in the spec”The smallest block is one entry, which every role uses:
"providers": { "entries": { "main": { "model": "anthropic:claude-opus-5", "credential_ref": "<your secret_ref>" } }}To put roles on different models, name several entries, pick a default, and bind roles to
entries in roles:
"providers": { "entries": { "fast": { "model": "openrouter:anthropic/claude-opus-5", "credential_ref": "<your OpenRouter secret_ref>" }, "careful": { "model": "anthropic:claude-opus-5", "credential_ref": "<your Anthropic secret_ref>" } }, "default": "careful", "roles": { "actor": "fast" }}Here the personas run on OpenRouter, and the judge and the reproduction check on Anthropic.
| Field | |
|---|---|
entries |
Required, at least one. Each name is lowercase letters, digits, _ and -. |
default |
The entry every role uses unless roles says otherwise. Left out with one entry, that entry. |
roles |
actor, judge, reproduction: an entry name each. |
An entry:
| Field | |
|---|---|
model |
Required. <provider>:<model>, such as anthropic:claude-opus-5, openai:gpt-5.6, openrouter:anthropic/claude-opus-5, bedrock:us.anthropic.claude-opus-5, google:gemini-3-pro or azure:<deployment>. The prefix must be the kind the credential was stored as. |
credential_ref |
The secret_ref of your stored key. Never the key itself: an entry has no field for one, and a field it does not list is refused. |
base_url |
An endpoint in place of the provider’s public one: a LiteLLM or other OpenAI-compatible gateway, an Azure resource, a Bedrock VPC endpoint. |
region |
bedrock: the AWS region. |
project, location |
google-cloud: the GCP project and the Vertex location. |
api_version |
azure: the Azure OpenAI API version. |
A providers block wins over actor.actor_model and actor.judge_model.
Validate checks the whole block before anything starts: that every role resolves to an entry, that
each prefix is a provider Saram resolves, and that every credential_ref is yours. A model Saram has
no price for, which is either new or misspelt, is a warning rather than a refusal, so read
warnings. For example:
run spec rejected: providers entry 'j' names model 'nope-provider:abc', and 'nope-provider' is not a provider this platform resolves: the prefix must be one of anthropic, bedrock, openai, openrouter, azure, google, google-cloud, litellmYour ChatGPT login
Section titled “Your ChatGPT login”Where Saram offers it, a run’s model calls can be made on your ChatGPT subscription instead of a provider key:
codex_login(POST /api/v1/providers/codex/login) answers averification_urland auser_code. Open the URL and type the code.- Poll
codex_status(GET /api/v1/providers/codex) untilstateisconnected. It isnone,pending,connected,expiredorerror, and it shows the plan, when the login expires and the subscription’s rate-limit use. - Once connected, the same answer carries the exact
providersblock to paste into a spec, the credential’s name included. You never type one.
The login is yours alone. It serves only the caller that connected it, and only runs that caller
starts: a spec on it with trigger.kind: ci is refused, as a subscription login serves runs a person
starts. A 403 from codex_login means Saram does not offer ChatGPT logins to your account.
codex_disconnect revokes the login and forgets it; your specs keep their
providers block, so connecting again is all a later run needs.
Over MCP, get_provider_status answers the same state and block, and start_chatgpt_login starts the
login (MCP tools).
What was checked
Section titled “What was checked”| Block | Checked |
|---|---|
| Storing a key | Not run for this page; the body and answer are the reference’s CredentialWrite and CredentialRecord |
list_credentials |
2026-10-23 against api.saram.sh (platform-api 0.9.0): metadata only, no value |
| The refusals for a ref that is not yours and for an unknown prefix | 2026-10-23, POST /api/v1/runs/validate |
The Free and Pro refusal without a providers block |
Not run: the organisation used for this page is not on Free or Pro. The sentence is the platform’s, for plan 'free' |
| The ChatGPT login | 2026-10-23: GET /api/v1/providers/codex answered connected with a providers block; validate accepted that block with trigger.kind: manual and refused it with ci. codex_login not run |