Skip to content
Pyrula

Calling an LLM

Inside an agent, call the model with ctx.llm.stream. It runs the model, drives the tool-call loop, and yields events as it goes. Every token and tool result is recorded, so a replay never calls the provider again.

@agent(timeout=60)
async def support(ctx: AgentContext, message: str) -> None:
async for event in ctx.llm.stream(
messages=[{"role": "user", "content": message}],
tools=[search],
system="You are a helpful support assistant.",
):
... # event stream: llm tokens, tool calls, tool results

messages is the conversation so far. tools is the list of @tool functions the model may call. system is the system prompt. The loop runs until the model stops asking for tools.

You don’t have to consume the events yourself if you only want the side effects; iterating the stream to exhaustion is enough.

Pick a client and pass it on the @agent or at run time. Each provider extra installs its own SDK.

from pyrula.agents.llm import OpenAILLM, AnthropicLLM, LiteLLM
OpenAILLM(model="gpt-4o-mini")
AnthropicLLM(model="claude-sonnet-4-6")
LiteLLM(model="...") # routes through LiteLLM to anything it supports

The import is lazy: a provider only shows up if its SDK is installed, so install the one you use.

Raw SDK calls inside an agent body are not recorded and will fire again on replay. Always go through ctx.llm.stream so the model interaction lands in the run stream. Same rule as any other side effect, covered in Determinism.