Run without a model: the mock server
tools/mock-llm/mock_llm.py in the repo is one file, Python standard library only. Copy it
anywhere and run it:
python3 tools/mock-llm/mock_llm.py --port 8787Flags: --host, --port (0 picks a free one), --script replies.json, --quiet. Any API
key is accepted and headers are never logged. It is also importable:
srv, base = mock_llm.start(port=0).
What it answers
Section titled “What it answers”| Route | Wire | Reply |
|---|---|---|
POST /v1/chat/completions |
OpenAI chat, SSE with stream: true |
Tools offered and no tool result yet: one call to the first tool, arguments built from its schema. Otherwise a final text. |
POST /v1/messages |
Anthropic messages, SSE | Same behaviour. |
POST /v1/systemone |
System One classifier (typesafe, openrouter, vercel, custom) |
noul 0.95; choice picked by sha256(state + instructions), probabilities summing to 1; score inside the rubric. |
POST …/ai/run |
Cloudflare classifier envelope | The same answers inside {success, result}. |
POST /v1/decisions |
OpenAI Decisions (openai backend) |
Positional answers: predicate / choice / score, same picks as System One. |
GET /v1/_mock/media-style.json |
Media style (§7G) | A style whose baseUrl is the mock, declaring all six verbs. Image, video and talking are async jobs. |
GET /v1/models, /health |
Model list, liveness. |
Images sent as attachments are echoed back as Received 1 image(s): image/png 82 bytes sha256=…, so a test proves the bytes arrived intact. To exercise retries, send the header
X-Mock-Fault: 429 (plus X-Mock-Retry-After: 0), or queue faults with
POST /v1/_mock/faults {"path": "/chat/completions", "status": 429, "times": 1}.
GET /v1/_mock/requests returns per-route request counts.
Tool sources, CLI and ACP fakes
Section titled “Tool sources, CLI and ACP fakes”The same folder holds a fake for every other thing toolnexus talks to. All are stdlib Python and deterministic.
| Fake | Run | What it does |
|---|---|---|
| MCP server | python3 tools/mock-llm/mock_mcp.py (stdio) or --http --port 0 |
Tools add(a, b) and echo(text). Answers server/discover with 2026-07-28; --legacy refuses it so the client takes the frozen initialize path. |
| A2A agent | the mock server, card at http://127.0.0.1:8787/.well-known/agent-card.json |
Agent mockagent, skill summarize: one working poll, then A2A mock done: <task>. |
| HTTP tool | POST http://127.0.0.1:8787/v1/tools/add |
{"a": 2, "b": 3} returns {"sum": 5}. |
| One-shot CLI | python3 tools/mock-llm/fake_cli.py |
For createInProcessClient: asks for add(19, 23), then answers The sum is 42. |
| ACP agent | python3 tools/mock-llm/fake_acp_agent.py |
Asks permission, reports a tool call, calls add(17, 25) on the MCP server you hand it, streams The sum is 42. A [wait] prompt waits for session/cancel. |
Media CLIs (Grok, Codex)
Section titled “Media CLIs (Grok, Codex)”tools/mock-llm/fake_media_cli.py --as grok and --as codex stand in for the real grok and
codex binaries when they generate images or video. Everything after --as <cli> is the real
CLI’s own argv, so a CLI media client runs unchanged with command: ["python3", "tools/mock-llm/fake_media_cli.py", "--as", "grok"]. The fake prints the same event stream the
real CLI prints: for Grok, the tool_call / tool_call_update events and an end event with token
usage and cost; for Codex, thread.started … turn.completed. It writes tiny real PNG and MP4
files where the real CLI would. A prompt containing [fail] fails the way each CLI does at its
usage limit, and exits 1.
mock_llm.py --mcp, --cli and --acp run the same fakes from one entry point.
Point a client at it
Section titled “Point a client at it”Use base URL http://127.0.0.1:8787/v1 with any key. The key must be passed explicitly, because
toolnexus only reads a provider’s key env var for that provider’s own host.
| Language | Client options |
|---|---|
| JavaScript | createClient({ baseUrl: "http://127.0.0.1:8787/v1", style: "openai", model: "mock-model", apiKey: "mock" }) |
| Python | create_client(base_url="http://127.0.0.1:8787/v1", style="openai", model="mock-model", api_key="mock") |
| Go | toolnexus.CreateClient(toolnexus.ClientOptions{BaseURL: "http://127.0.0.1:8787/v1", Style: toolnexus.StyleOpenAI, Model: "mock-model", APIKey: "mock"}) |
| Java | LlmClient.create(new LlmClient.Options().baseUrl("http://127.0.0.1:8787/v1").style("openai").model("mock-model").apiKey("mock")) |
| C# | LlmClient.Create(new LlmClient.Options { BaseUrl = "http://127.0.0.1:8787/v1", Style = "openai", Model = "mock-model", ApiKey = "mock" }) |
| Elixir | Toolnexus.Client.create(base_url: "http://127.0.0.1:8787/v1", style: "openai", model: "mock-model", api_key: "mock") |
| Clojure | (client/create-client {:base-url "http://127.0.0.1:8787/v1" :style "openai" :model "mock-model" :api-key "mock"}) |
| Rust | create_client(ClientOptions { base_url: "http://127.0.0.1:8787/v1".into(), style: Style::OpenAI, model: "mock-model".into(), api_key: "mock".into(), ..Default::default() }) |
Use style: "anthropic" with the same base URL for the Anthropic wire. For a classifier, use the
custom backend with the same base URL (/systemone is appended). For Cloudflare use
http://127.0.0.1:8787/v1/accounts/any/ai/run. For media, save
/v1/_mock/media-style.json to a file and load it with loadMediaStyle.
Scripted replies
Section titled “Scripted replies”--script replies.json overrides any part of the default behaviour:
{ "text": "Final answer text", "toolCall": { "name": "add", "arguments": { "a": 2, "b": 2 } }, "answers": { "team": { "choice": "billing" }, "How upset is the customer?": { "score": 2 } }, "faults": [{ "path": "/chat/completions", "status": 503, "times": 2 }]}answers is keyed by question key or by instructions text. Faults listed in the script are
re-armed by POST /v1/_mock/reset.