The mock model server

The mock model server is a standalone local process that stands in for the model with scripted responses, so a test or a demo can exercise both successes and failures without a key. It serves the messages endpoint that model adapters use, plays one scripted behaviour per accepted request, and never retries or interprets recovery policy of its own. To choose between the test tiers, see Testing tiers.

Run the server

Run the standalone process from your checkout of the Oda source:

sh
pnpm run mock:llm \
  --port 8000 \
  --api-key mock-key \
  --sequence partial_disconnect,success \
  --partial-text "discard this half"

Script the behaviours

The catalogue of 24 behaviours:

BehaviourWhat the wire does
successStreams a complete text response, then a stop with reason end_turn.
slow_successLike success, with a pause of --chunk-delay-ms between text chunks.
reasoning_successStreams a reasoning block first, then a complete text response.
tool_call_successStreams a tool call using --tool-name and --tool-arguments, then a stop with reason tool_use.
max_tokensStreams a complete text response, then a stop with reason max_tokens.
emptySends a valid completion with no content: a stop with reason end_turn, then the closing marker.
connection_resetDestroys the connection before any HTTP headers are sent.
stream_disconnectSends stream headers, then resets the connection before the first event.
partial_disconnectStreams text from --partial-text, then resets the connection.
stallSends stream headers, then stays idle until the client or the server closes the connection.
empty_bodyEnds the response immediately: no events and no closing marker.
stream_eofSends the message start, then ends without the closing marker.
partial_eofStreams text from --partial-text, then ends without the closing marker.
malformed_jsonSends a data event that is not valid JSON, then the closing marker.
malformed_eventSends a chunk with an invalid shape, then the closing marker.
wrong_content_typeSends a valid stream body under the JSON content type.
rate_limitReturns a 429 error with a Retry-After header.
server_errorReturns a 500 error.
service_unavailableReturns a 503 error.
auth_errorReturns a 401 authentication error.
invalid_requestReturns a 400 invalid-request error.
context_overflowReturns a 400 error with the context-length-exceeded code.
quota_exceededReturns a 429 insufficient-quota error.
randomDraws this request's behaviour from seeded weights (see Random mode).

Random mode

Timing, content and the key

FlagDefaultWhat it controls
--success-textmock response recoveredText of a complete response.
--partial-textdiscarded partial responseText sent before a partial end or a reset.
--reasoning-textmock reasoningReasoning text of reasoning_success.
--chunk-size8Code points per stream delta.
--chunk-delay-ms25Pause between chunks in slow_success.
--disconnect-delay-ms10Pause before a forced reset.
--retry-after-ms1000Retry delay; the Retry-After header rounds up to whole seconds.
--request-id(none)Value returned as x-request-id on error responses.
--tool-namemock_toolTool name used by tool_call_success.
--tool-arguments{"value":"mock"}Raw JSON arguments used by tool_call_success; must be valid JSON.
--api-key(none)When set, every request must carry this exact value in x-api-key; without it, any token is accepted.

Millisecond delays are whole numbers within Node's timer range; --retry-after-ms must also be positive.

Which requests consume the script

RefusalAnswer
Not a POST405 with allow: POST
A path that does not end in /v1/messages404
x-api-key does not match the configured key401 invalid mock API key
A body that does not parse as JSON400 request body must be valid JSON

A misconfigured client can burn retries this way without advancing the sequence.

The errors a script returns

MessageStatusCode
mock rate limit429rate_limit, with Retry-After
mock server error500server_error
mock service unavailable503service_unavailable
mock authentication failed401invalid_api_key
mock invalid request400invalid_request
mock input exceeds the model context window400context_length_exceeded
mock insufficient quota429insufficient_quota
mock script exhausted500MOCK_SCRIPT_EXHAUSTED
mock server handler failed500MOCK_HANDLER_FAILED

How a request ends

Each request record carries one outcome:

Limits

Questions, answered

What should I do when the server answers mock script exhausted?

The sequence ran out. Lengthen --sequence, or pass --repeat-last so the final entry is reused for every further request.

Why did a request get a 4xx and not use a script entry?

Only valid requests consume the script: a POST whose path ends in /v1/messages, with the configured key when one is set and a JSON body. A wrong method, path, key or body gets its ordinary 4xx first, so a misconfigured client can retry without advancing the sequence.

How do I make a random run reproducible?

Pass a fixed --seed, or record the generated seed from the ready record, together with your weights. The same seed and weights draw the same sequence of behaviours.

How do I keep fault assignment deterministic across parallel tests?

Run a separate server instance per test or session. Entries are served in arrival order and concurrent callers share one cursor, so one instance cannot give each caller its own script.

What does connection_refused need to work?

It must be the first sequence entry, the port must not be 0, and its window is --listen-delay-ms (default 750 ms). During the window requests get a real TCP refusal; afterwards the remaining entries play in order.

Which behaviours end without the closing stream marker?

empty_body, stream_eof and partial_eof end cleanly without the closing marker, so clients that require it fail. Use empty when you want a well-formed completion with no content.

Which error responses add a Retry-After header?

Only rate_limit does, as a whole number of seconds rounded up from --retry-after-ms (default 1000 ms, so one second). Other errors never add the header.

What happens when a client disconnects mid-stream?

The request record settles as client_closed, and the server keeps serving other requests. Closing the server stops it accepting requests and ends stalled connections.

Must every request carry an API key?

Only when you start the server with --api-key. Then every request must carry that exact value in x-api-key, or it gets 401 invalid mock API key. Without --api-key, any token is accepted.

What timeout should a client use when the weights can produce stall?

Give the client under test a short stream-idle timeout. stall holds the connection open until the client or the server closes it, so the scenario ends only when one side gives up.