Use other models¶
Model strings¶
Agent(model=...) accepts a string:
| String | Model | API key |
|---|---|---|
"claude-sonnet-5", any name starting with claude- |
Anthropic |
ANTHROPIC_API_KEY |
"anthropic/<name>" |
Anthropic |
ANTHROPIC_API_KEY |
"gpt-5", any name starting with gpt- |
OpenAICompatible at api.openai.com |
OPENAI_API_KEY |
"openai/<name>" |
OpenAICompatible at api.openai.com |
OPENAI_API_KEY |
"ollama/<name>", for example "ollama/llama3:8b" |
OpenAICompatible at http://localhost:11434/v1 |
Not needed |
Any other string raises ValueError with the strings you can use instead.
Options with a model string¶
resolve_model turns a model string into a Model, the same way Agent(model=...) does. Keyword options go to the
Model it picks:
from alpineagents import Agent
from alpineagents.models import resolve_model
model = resolve_model("anthropic/claude-sonnet-5", max_tokens=16_000, thinking=True)
# the same as Anthropic("claude-sonnet-5", max_tokens=16_000, thinking=True)
agent = Agent(model=model)
print(agent.run("Say hello"))
"ollama/<name>"useshttp://localhost:11434/v1unless you passbase_url=, for an Ollama server on another machine.- An option the picked Model does not take raises
TypeErrorthat lists the options it takes.
With base_url=, the string is not split into a provider and a name. The model is always OpenAICompatible, and
the whole string is the model name the server receives. Routers such as OpenRouter name their models this way:
import os
from alpineagents import Agent
from alpineagents.models import resolve_model
model = resolve_model(
"anthropic/claude-sonnet-5",
base_url="https://openrouter.ai/api/v1",
api_key=os.environ["OPENROUTER_API_KEY"],
)
# the same as OpenAICompatible("anthropic/claude-sonnet-5", base_url=..., api_key=...)
agent = Agent(model=model)
print(agent.run("Say hello"))
A proxy that speaks the Anthropic API rather than the OpenAI one needs Anthropic itself:
Anthropic("claude-sonnet-5", base_url="https://my-proxy/anthropic").
Model settings¶
Create the Model yourself to change its settings:
import os
from alpineagents import Agent, Anthropic, OpenAICompatible
claude = Anthropic("claude-sonnet-5", thinking=True, max_tokens=16_000)
ollama = OpenAICompatible(
"qwen3:8b",
base_url="http://localhost:11434/v1",
context_window=32_000,
)
openrouter = OpenAICompatible(
"moonshotai/kimi-k2",
base_url="https://openrouter.ai/api/v1",
api_key=os.environ["OPENROUTER_API_KEY"],
)
agent = Agent(model=claude)
OpenAICompatibleworks with any server that speaks the OpenAI Chat Completions API: OpenAI, Ollama, vLLM, OpenRouter and others.- Set
context_windowto the model's real window.compact_if_fullandagent.context_used(state)use it. - Creating a Model uses no network and needs no API key. The key is read at the first request.
- Retries are left to the provider SDK:
retries=2by default.
See the Models API for every setting.
Cost¶
state.usage.cost is None unless the Model has prices. Prices are dollars per million tokens:
from alpineagents import Agent, Anthropic, Message, Price, State
model = Anthropic("claude-sonnet-5", price=Price(input=3, output=15, cache_read=0.3))
agent = Agent(model=model)
state = State(messages=[Message.user("Say hello")])
agent.run(state)
print(state.usage.cost)
Each Reply records the model that answered in reply.model, as the provider reported it. It can differ from the
requested name: an alias resolved to a dated id, or a router that picked another model. The cost still uses the
requested Model's price.
Models with no price¶
usage.cost is None when the Model has no price, and a None anywhere makes a total None. That includes the
total of a run with subagents, which adds up every request of the tree. A model you pay for with a
subscription, or run on your own machine, costs nothing extra, which is not the same as an unknown price. Say so with a
price of zero:
from alpineagents import OpenAICompatible, Price
free = Price(input=0, output=0)
local = OpenAICompatible("llama3:8b", base_url="http://localhost:11434/v1", price=free)
Its requests add 0 to the cost, so the total stays a number. Each request is priced with the price of the Model that
made it, so the cost of a lead and its subagents is right even when they use different models. Token counts add up
across models too, so read them as a sum over all the models.
Switch models in one conversation¶
A State does not belong to the Agent that ran it. Any Agent may run, think, use tools and compact the same State, so a
conversation can start on a fast model and go on with a strong one. agent.copy(model=...) makes the second Agent
with every other setting the same:
from alpineagents import Agent, Message, State
fast = Agent(model="claude-haiku-5", system="You are a helpful assistant.")
strong = fast.copy(model="claude-opus-5")
state = State(messages=[Message.user("Outline a plan to move the users table to a new schema")])
print(fast.run(state))
state.add_message(Message.user("Now write the data backfill. It must not lock the table"))
print(strong.run(state))
- Both runs add to
state. The second model sees the whole conversation, including the replies of the first. state.historyrecords the model of everythinkrequest in amodel_requestentry, so you can see which model wrote each reply. Tokens ofaskandcompactrequests count instate.usage, but they write nomodel_requestentry.usage.costisNonewhen any model used has no price.- Thinking blocks and other provider-specific blocks are sent back only to the provider that wrote them.
- Providers cache a long context per model, so the first request after a switch pays for the whole context again.
- The windows differ. Check
strong.context_used(state)before you switch to a model with a smaller window, and compact first if needed:strong.compact(state). - Switching never warns.
ResumeWarningis only for the first run afterstore.load(State and history).
Write a Model¶
Subclass Model to add a provider. Implement respond and context_window:
from alpineagents import Agent, Message, Model, Reply, Usage
from alpineagents.types import TextBlock
class Echo(Model):
"""Replies with the last user message. Uses no network."""
name = "echo"
provider = "echo"
@property
def context_window(self) -> int:
return 100_000
def respond(self, request, on_text=None, on_event=None):
text = request.messages[-1].text
if on_text:
on_text(text)
return Reply(
message=Message("assistant", (TextBlock(text),)),
usage=Usage(requests=1),
context_tokens=self.count_tokens(request),
model=self.name,
)
agent = Agent(model=Echo())
print(agent.run("hello"))
Rules for a Model:
- Return a
Reply. Do not change the State. The Agent records the reply. - User messages can hold
Imageblocks. Convert them to the provider's image format, or raise a clear error if the provider cannot read images. - Put the model that actually answered in
Reply.model: the name the provider reported, or the requested name if it did not say. - Use no network and no credentials in
__init__. Create the SDK client at the first request. - Call
on_textwith each piece of text as it streams in, ifon_textis notNone. - Wrap provider exceptions that finally fail as
RateLimitError,AuthError,ContextTooLongErrororProviderError, withraise ... from e. - Print nothing. Report events such as a fallback to another model with
on_event(ModelEvent(...)).
Model has working defaults for everything else, including arespond for async code.
Related¶
- Test your agent:
FakeModelreplaces the model in tests - Handle unreliable models: wrap a Model to retry