Model Providers¶
model is a plain string on your Agent:
No client to construct, no provider to configure globally. Runa resolves the string to a
provider by its prefix, and reads the matching API key from the environment, typically via
.env, loaded by your app's main.py.
| Prefix | Provider | Env Var |
|---|---|---|
claude |
Anthropic | ANTHROPIC_API_KEY |
gpt, o1, o3, o4, chatgpt, codex |
OpenAI | OPENAI_API_KEY |
gemini |
GEMINI_API_KEY |
|
llama |
Meta | LLAMA_API_KEY |
deepseek |
DeepSeek | DEEPSEEK_API_KEY |
qwen |
Alibaba | DASHSCOPE_API_KEY |
Those keys cover an agent's turns, which is all model decides. An agent that also sets memory
or knowledge needs OPENAI_API_KEY on top of its own provider's key: embedding is the one call
Runa makes that model doesn't route (see
Where embeddings go).
That list is the whole list. A name matching none of it raises UserError when the agent is
constructed, naming the prefix it probably meant, because the alternative is worse than an error:
model = "claud-sonnet-5" would otherwise send your prompts to a different company under a
different key, and report the resulting 404 as model misbehaviour mid-run. The default, when
model isn't set, is "gpt-5.4-nano".
>>> SupportAgent(model="claud-sonnet-5")
UserError: 'claud-sonnet-5' starts with no prefix Runa routes to a provider, so it names no
model. Did you mean a claude-* model? For an endpoint Runa doesn't name, pass a Model instance
instead of a name.
For a gateway, a proxy, or something self-hosted (an endpoint Runa has no name for), pass a
Model instead of a name. model takes any object with the two methods a backend has, so
Runa's own chat-completions backend works, pointed at your base_url:
import httpx2 as httpx
from runa._models import OpenAICompatibleModel
class SupportAgent(Agent):
name = "support_agent"
model = OpenAICompatibleModel(
"mixtral-8x7b",
httpx.AsyncClient(base_url="https://gateway.internal/v1/", headers={...}),
)
This is the sharp knife, and it is deliberately spelled differently from a name: a bare name can't tell "a model Runa has never heard of" and "I mistyped" apart, so the first one has to say so out loud.
Every provider except Anthropic speaks the same chat-completions wire format, so they share one backend. Anthropic's Messages API is shaped differently and gets its own.
Model Settings¶
Tune sampling per agent with model_settings:
from runa import Agent, ModelSettings
class SupportAgent(Agent):
name = "support_agent"
model_settings = ModelSettings(temperature=0.2, max_tokens=500)
max_tokens here caps the length of one response. To cap what a whole run may spend, use
Agent.max_tokens; see Bounding a run.
Reasoning Effort¶
How hard a reasoning model thinks before answering is reasoning_effort, one of three levels:
class ResearchAgent(Agent):
name = "research_agent"
model = "claude-opus-5"
model_settings = ModelSettings(reasoning_effort="high")
low/medium/high is the vocabulary both backends share, so the setting keeps meaning the
same thing when model changes vendor: a chat-completions provider is sent reasoning_effort,
Claude is sent adaptive thinking at that effort. Unset, each provider's own default stands.
Asking a model that can't reason for effort is the provider's error, not a setting Runa drops on
the floor -- a misspelled model is loud for the same reason. A provider with a deeper ladder
than three levels is reached by passing a Model.
What the thinking cost is on run.usage.output_tokens_details.reasoning_tokens, whichever
backend answered. The reasoning itself stays in the conversation: Claude returns its thinking as
content blocks carrying a signature, and those ride along in history, a session and a serialized
RunState, so every later turn of a tool-calling loop hands the model its own reasoning back
instead of paying to derive it again. They're the one part of an item no other provider is ever
sent.
Retries¶
Both backends retry connection errors and 408/409/429/5xx twice, with jittered backoff
and honoring a retry-after header. Tune it per agent, or turn it off:
class SupportAgent(Agent):
model_settings = ModelSettings(max_retries=5) # 0 disables retrying outright
Images¶
Agent.run/run_sync/run_streamed also take a list instead of plain text, for a multimodal
message. Each item is auto-detected: an image URL or data:image/... URI becomes an image, a
Path is read off disk and base64-encoded automatically, and any other string is plain text.
from pathlib import Path
agent.run_sync(["What's in this image?", "https://example.com/cat.png"])
agent.run_sync(["What's in this image?", Path("photo.jpg")]) # a local file, read for you
A local file is a Path, never a bare string: the list often carries a user's own words, and a
string that happens to end in .png would otherwise turn their message into a read of your
filesystem. A bare string naming a local image raises ValueError and names both spellings that
say the path is yours, Path(...) and content.image(...).
For a string the extension heuristic can't classify (a signed URL with no file extension, say),
build the part explicitly with runa.content and mix it into the list:
from runa import content
agent.run_sync(["What's in this image?", content.image("https://example.com/img?id=42")])
Content parts are OpenAI's own image_url/text shape, so every OpenAI-compatible provider (OpenAI,
Gemini, Llama, DeepSeek, Qwen) takes it as-is; Anthropic gets its own translation to Claude's
image content block.
Mixing Providers¶
Because model is per-agent, a single application can freely mix providers across agents: a
fast, cheap model for routing, a stronger one for the agent that actually answers.
class Router(Agent):
name = "router"
model = "gpt-5.4-nano"
subagents = [SupportAgent.handoff]
class SupportAgent(Agent):
name = "support_agent"
model = "claude-opus-5"
Retries¶
Every provider retries the same failures the same way: connection errors, 408, 409, 429 and 5xx
are retried twice with exponential backoff, honoring a retry-after header. A request that still
fails ends the run with status="error", never an uncaught HTTP exception.
Example¶
More in examples/10_model/.