Skip to content

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:

8787/v1
python3 tools/mock-llm/mock_llm.py --port 8787

Flags: --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).

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.

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.

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.

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.

--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.