A local mock LLM API where a typed word triggers each failure

Claude Code builds a dev fake of your LLM API that streams the real wire format and fails on cue, from a 401 to a cut stream, when a trigger word is typed into the app.

Claude Code intermediate Tested Opus 5.5 · Nov 2026May be dated

The prompt

Run it in Claude Code at the root of an app that calls an LLM API. Fill in the provider, the stack and any failure modes only your app has: one - "word": what the mock does line per trigger, or none.

Fill in the blanks3 placeholders · empty ones stay as {{…}}

Example

Input:

  • provider: the Anthropic Messages API, streaming over SSE
  • app_stack: a plain Node http server, server.mjs, that calls the API with fetch and streams the recipe text to one HTML page as NDJSON
  • failure_modes: - "cutoff": a good answer that stops halfway with stop_reason "max_tokens".
  • The repo: a small recipe-card app with server.mjs, public/index.html and two sample answers in samples/, and no tests yet. The dish the user types goes into the request as a Dish: line. The server hard-codes the API host, caches finished recipes in memory, and has a 30-second upstream timeout.

Output (trimmed: the opening, two of the failures it found in the app, four rows of the README's trigger table, then the closing run's overload request through the app and the mock's log line for it):

I built the mock, wired the app to it, wrote the README section and the Playwright test file. … The Playwright tests haven't been run: Playwright isn't installed here. …

  • Half cards were saved: a mid-stream overload error was ignored and a card stopped by max_tokens was treated as finished. Both were cached and marked done, so the next request for that dish returned the half card. Only a card that ends with end_turn or stop_sequence is now cached. Anything else sends an error line with partial: true.
  • Silent failures: a dropped connection, an unreadable event or a timeout ended the response with no message, leaving a half or empty card. Each now sends a clear error line.

…

Trigger What the mock does What the user should see
429 HTTP 429 rate_limit_error, retry-after: 20, x-should-retry: false No card. "Too many cards are being made right now. Try again in 20 seconds."
overload About 40% of the card, then event: error with overloaded_error. No message_stop The partial card, dashed. "Claude is overloaded right now. Try again in a minute."
drop About 40% of the card, then destroys the socket. No error event, no message_stop The partial card, dashed. "The connection to Claude dropped before the card was finished. Try again."
cutoff Half the card, then stop_reason: "max_tokens" and a normal message_stop The partial card, dashed. "The card was cut off before the end. Try again."
$ curl -sN localhost:3000/api/recipe -H 'content-type: application/json' -d '{"dish":"miso soup overload"}'
{"type":"text","text":"# Miso soup\n\n**Ingredients**\n- 800 ml dashi\n- 3 tbsp white miso\n- 150 g silken"}
{"type":"text","text":" tofu, cubed\n- 1 tbsp dried wakame\n- 2 spring onions, sliced\n\n**S"}
{"type":"text","text":"teps**\n1. Soak the wakame in cold water for 5 minutes, then drain.\n2. He"}
{"type":"text","text":"at the dashi until it just simmers.\n3. Whisk the miso with a ladle"}
{"type":"error","message":"Claude is overloaded right now. Try again in a minute.","partial":true}
[mock] POST /v1/messages 200 mode=overload sample=samples/miso-soup.md deltas=4/8 end=error-event | model=claude-opus-5 stream=true max_tokens=1500 thinking=adaptive(model-default)/omitted effort=- betas=- system=108ch user=48ch cache_markers=0 auth=x-api-key 205ms

Typing overload after the dish got four text lines from the app's own samples/miso-soup.md, then an error line marked partial: true, and the mock's log line records the trigger, the sample, where the stream stopped (deltas=4/8 end=error-event) and the shape of the call, without the key. The Playwright tests were written but not run.

Why it works

  • The input picks the failure. Trigger words in the user's own text make every failure path reachable from the real UI, by anyone, with no flags, headers or test-only branches in app code. A bug report becomes "type drop into the input". Matching whole words, and only in the user's field, stops a system prompt that mentions "400", or an input like "dropdown", from failing a request by accident. The app finds the mock through its base-URL env var, so production code never knows the mock exists and nothing test-only ships.
  • The real wire format, not a stub. The app's own SDK and stream parser read the mock, so whatever breaks is production code, and the request-shape log shows what the app actually sends: model, effort, cache markers, betas. Seeded chunking makes every stream reproducible, and x-should-retry: false stops the SDK from hiding a 429 behind a minute of retries (it honours retry-after).
  • "overload" and "drop" are different failures, missed in different ways. A parser that reads only text deltas skips the provider's error event, takes the half answer as complete and can cache it, as the example app did. A socket cut sends no event at all: the read throws or just ends after the response has started, and the user is left with half an answer and no message. Failing after real content forces the UI to deal with a half-written answer, and the "nothing cached" assertion catches the save.
  • One table, three uses. README rows, test cases and manual QA steps are the same list, so none drifts from the others. Each row has to fail for the reason it names, and in seconds, so the tests shorten the app's timeout for slow and get past any rate limit or gate of its own on purpose instead of tripping over it.

Variations

  • Provider features. Add triggers for the features your app relies on: a refusal stop reason, a mid-stream model fallback that restarts the answer, or a 400 for a request option your account doesn't support.
  • A second provider. The same file can serve another API's stream format from the same samples, with a trigger that fails only the first model in the chain (a 503 for that model) to test fall-through.
  • API-only services. With no browser front end, the tests call your own endpoint, and the table's last column becomes the response your clients get.
  • Generalised from a streaming AI app.