ScriptedModel in place of a real provider and allow_model_requests(False) as a hard gate.
ScriptedModel subclasses the real LLM, so everything around the call — system prompt assembly, tool schemas, the tool loop, streaming, async — runs production code; only the provider-facing methods are replaced.
Quick Start
1
Install and gate the suite
Add the gate to
conftest.py so nothing in the suite can reach a provider.2
Script one text reply
3
Script a tool call + follow-up
4
Assert on what the agent sent
Canonical example
How it works
ScriptedModel is a real LLM subclass that replaces only the methods which talk to a provider — nothing else.
- System prompts are assembled normally, tools are serialised to real schemas, scripted tool calls are dispatched through the agent’s own executor, and results are fed back as real tool messages.
- Replies are built as genuine
litellm.ModelResponseobjects, parsed by the same code path a live response would take — so the double cannot drift from reality. - When the agent asks for one more reply than the script holds, the double raises
ScriptExhausted(usually the script is one reply short — after a tool call the agent comes back for a follow-up answer).
str (a final answer), a ScriptedModel.tool_call(...), a list of tool calls, a ScriptedReply, or a callable receiving the RecordedRequest:
The request gate
allow_model_requests(False) blocks all six litellm request sites and both OpenAI client properties process-wide. Any attempt to reach a provider raises ModelRequestBlocked, whose message names the offending call site in your code.
allow_model_requests(True) to restore normal behaviour, or scope a single block with no_model_requests():
When to use offline vs. real-API testing
Offline tests assert what the agent does — which tool it calls, in what order, with what arguments. For prompt quality and model behaviour, use Real API Testing.ScriptedModel configuration
Inspection surface after a run:
ScriptedReply, ScriptedToolCall, and RecordedRequest are importable from praisonaiagents.model_harness when you need to build or type them explicitly.
Best Practices
Gate the whole session in a fixture
Gate the whole session in a fixture
Put
allow_model_requests(False) in a session-scoped fixture (or conftest.py) so a forgotten mock cannot leak a real call and quietly bill you.Script the arguments the agent will really produce
Script the arguments the agent will really produce
Assert
requests[i] reflects the call you expect — a scripted tool_call("refund", {"order_id": "A1"}) should mirror what your instructions steer the model toward.Prefer real ModelResponse shapes
Prefer real ModelResponse shapes
Script with strings and
ScriptedModel.tool_call(...) rather than inventing raw provider dicts — the helpers build genuine litellm response objects that the production parser understands.Never mix scripted and real in one session
Never mix scripted and real in one session
Keep offline tests and
RUN_REAL_KEY_TESTS integration tests in separate sessions. allow_model_requests(False) blocks the whole process, so a real-API test in the same run would fail with ModelRequestBlocked.Related
Real API Testing
Gated integration tests against live providers.
Guardrails
Test guardrail wiring offline with scripted replies.
Tools
How the tool loop the double exercises works.

