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. Anthropicsendsmax_tokens=64000unless you give one, or the model's own output limit if it is lower. It looks the limit up once per model name with the Anthropic Models API; if that fails (a proxy without it), it sends 64000 and, if the API says that is too many, sends the request again with the model's limit.OpenAICompatiblesends nomax_tokensunless you give one.- 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.
Replies cut off by the output limit¶
A reply that hit the output limit has reply.cut_off true, and so does its message in the State:
state.messages[-1].cut_off. stop_reason keeps what the provider said ("max_tokens", or "length" from an
OpenAI-compatible server). The text of such a reply may end mid-sentence; it is still the answer. A tool call cut in
the middle does not run, and the model gets an input error telling it to split the work.
When the limit cut the reply off before any text or tool call, which happens when a model thinks for the whole
max_tokens, think raises CutOffError instead of taking the empty reply for an answer. The reply and its tokens
stay in the State. Raise max_tokens, or lower the thinking, and go on with a new message:
from alpineagents import Agent, Anthropic, CutOffError, Message, State
agent = Agent(model=Anthropic("claude-haiku-5-5"))
state = State(messages=[Message.user("Map the product")])
try:
agent.run(state)
except CutOffError as e:
print(f"cut off after {e.reply.usage.output_tokens} tokens of thinking")
bigger = agent.copy(model=Anthropic("claude-haiku-5-5", max_tokens=128_000))
state.add_message(Message.user("Go on, and write the files one at a time."))
bigger.run(state)
A Model of your own reports a cut-off reply with one of the stop_reason values in CUT_OFF_STOP_REASONS
("max_tokens", "model_context_window_exceeded", "length" or "max_output_tokens").
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