Changing the call: wrap_model_call
wrap_model_call is a middleware hook that runs around each model call: it receives the request, can change it, then hands it to a handler that sends it to the model.
Last updated: 27 Sep, 2026 · LangChain 1.4
The state hooks in lesson 16 see the state. A wrap hook sees the request about to go to the model, with its messages, tools, system prompt and model, and a handler that sends it on. Calling handler(request) once is the normal path; not calling it, or calling it twice, is how a hook skips or retries a call.
The wrap_model_call hook
from langchain.agents.middleware import wrap_model_call
@wrap_model_call
def hook(request, handler): # request: the call about to go to the model
# inspect or change request here
return handler(request) # call the model once, return its resultA prompt from the context
The shop agent from earlier lessons uses ShopModel, a stand-in chat model, and a lookup_order tool. Start with a hook that sets the system prompt. @dynamic_prompt turns a function into a wrap hook whose return value becomes the system prompt.
from dataclasses import dataclass
from langchain.agents.middleware import dynamic_prompt, wrap_model_call
@dataclass
class Customer:
name: str # the customer this run is for
@dynamic_prompt
def with_name(request):
# read the name from the run's context and build the prompt
return f"You help {request.runtime.context.name}, a customer of a small online shop."A wrapper that prints the prompt
A second hook prints the system prompt the model is about to get, then passes the request on unchanged.
@wrap_model_call
def show_prompt(request, handler):
print("system prompt:", request.system_prompt) # what the model will receive
return handler(request) # send it on, unchangedThe agent with both middleware
Build the agent with both hooks. Their order in the list is the layering: with_name is first, so it wraps show_prompt.
from langchain.agents import create_agent
from prompts import Customer, show_prompt, with_name
from shop_model import ShopModel
from tools import lookup_order
agent = create_agent(ShopModel(), tools=[lookup_order], context_schema=Customer,
middleware=[with_name, show_prompt])The prompt the model receives
Run it for Ravi and read what the model is handed.
agent.invoke({"messages": [{"role": "user", "content": "Where is A17?"}]}, context=Customer("ravi"))Two model calls, and each got a system prompt written for Ravi. with_name comes first in the list, so it is the outer layer: it set the prompt before show_prompt saw the request.
A different model per request
wrap_model_call can also swap the model itself. request.override returns a copy of the request with one thing changed. Here a message with no order id goes to a model that only greets.
import re
from langchain.agents.middleware import wrap_model_call
from langchain.messages import AIMessage
from shop_model import ShopModel
class Greeter(ShopModel):
def decide(self, messages):
return AIMessage("Hi! Tell me an order number and I will look it up.")
@wrap_model_call
def pick_model(request, handler):
if not re.search(r"\b[A-Z]\d+\b", request.messages[-1].text): # no order id
request = request.override(model=Greeter()) # swap the model
return handler(request)Build the agent with this one hook, then send it two messages: small talk, and a message with an order id.
from langchain.agents import create_agent
from greeter import pick_model
from shop_model import ShopModel
from tools import lookup_order
agent = create_agent(ShopModel(), tools=[lookup_order], middleware=[pick_model])for text in ["Hello", "Where is A17?"]:
result = agent.invoke({"messages": [{"role": "user", "content": text}]})
print(result["messages"][-1].text)What the wrappers did
- Two prints in the first run. The agent called the model twice, once to ask for the tool and once to reply, and
show_promptran around each call. - The prompt was set before it was seen.
with_nameis first in the list, so it is the outer layer; it wrote Ravi's prompt beforeshow_promptprinted it. request.overridechanges one thing. In the second run the greeting had no order id, sopick_modelswapped inGreeter; the message with A17 kept the normal model and ran the tool.- With hosted models this is how a cheap model handles small talk while a larger one does the real work.
wrap_model_call vs the state hooks
| State hooks | wrap_model_call | |
|---|---|---|
| What it sees | The agent state | The request about to go to the model |
| Can change | State keys it returns | The request, via request.override |
| Controls the call | No | Yes: call handler once, not at all, or again |
| Use for | Reading or editing state | Editing the prompt, model or messages; retrying; skipping |
Where wrap_model_call fits
- Setting a system prompt from who the request is for, as
with_namedoes. - Sending small talk to a cheaper model and real work to a larger one.
- Logging or checking every request before it reaches the model.
handler(request) and return its result. Forget to call it and the model never runs; return nothing and the agent has no reply to work with.Related
- Previous: The order middleware runs in
- Next: Tool errors, caught and retried
- Reference: LangChain agent middleware
- Change
with_nameto add the number of messages inrequest.messagesto the prompt. - Make
show_promptskip the model by returning without callinghandler, and read the error. - Swap the order of
with_nameandshow_promptand compare what is printed.
Little by little, you're building something great.