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 standalone process from your checkout of the Oda source:
pnpm run mock:llm \ --port 8000 \ --api-key mock-key \ --sequence partial_disconnect,success \ --partial-text "discard this half"
127. 0. 0. 1, port 8000, unless you change --host or --port.
--sequence has no default and is required.ready record once the listener is up, carrying the / v1 base URL and the seed used for
random selection, for example
{"type":"ready","baseURL":"http:/ / 127. 0. 0. 1:8000/ v1","randomSeed":42};connection_refused first, an unavailable record before the listener delay, carrying the
base URL and the delay;request record and a result record for every accepted request; both name the attempt, the
scripted entry and the concrete behaviour it played (the two differ for random), and the result
adds the outcome and how many stream chunks were sent.--help prints the usage text.--sequence is a comma-separated first-in, first-out list; each accepted
request consumes exactly one entry, in arrival order.mock script exhausted. --repeat-last reuses the final entry instead.connection_refused is special. It is accepted only as the first entry and is not a request
behaviour: it delays binding the port so that requests during --listen-delay-ms (default 750 ms)
get a real TCP refusal. With it, a port of 0 is refused
(connection_refused requires an explicit nonzero --port). The remaining entries begin once the
listener is up.The catalogue of 24 behaviours:
| Behaviour | What the wire does |
|---|---|
success | Streams a complete text response, then a stop with reason end_turn. |
slow_success | Like success, with a pause of --chunk-delay-ms between text chunks. |
reasoning_success | Streams a reasoning block first, then a complete text response. |
tool_call_success | Streams a tool call using --tool-name and --tool-arguments, then a stop with reason tool_use. |
max_tokens | Streams a complete text response, then a stop with reason max_tokens. |
empty | Sends a valid completion with no content: a stop with reason end_turn, then the closing marker. |
connection_reset | Destroys the connection before any HTTP headers are sent. |
stream_disconnect | Sends stream headers, then resets the connection before the first event. |
partial_disconnect | Streams text from --partial-text, then resets the connection. |
stall | Sends stream headers, then stays idle until the client or the server closes the connection. |
empty_body | Ends the response immediately: no events and no closing marker. |
stream_eof | Sends the message start, then ends without the closing marker. |
partial_eof | Streams text from --partial-text, then ends without the closing marker. |
malformed_json | Sends a data event that is not valid JSON, then the closing marker. |
malformed_event | Sends a chunk with an invalid shape, then the closing marker. |
wrong_content_type | Sends a valid stream body under the JSON content type. |
rate_limit | Returns a 429 error with a Retry-After header. |
server_error | Returns a 500 error. |
service_unavailable | Returns a 503 error. |
auth_error | Returns a 401 authentication error. |
invalid_request | Returns a 400 invalid-request error. |
context_overflow | Returns a 400 error with the context-length-exceeded code. |
quota_exceeded | Returns a 429 insufficient-quota error. |
random | Draws this request's behaviour from seeded weights (see Random mode). |
random entry draws its concrete behaviour at request time from seeded
weights, so a run is reproducible from its printed seed. Omitting --seed generates one and prints
it in the ready record.--random-weights takes behavior=weight entries with non-negative relative weights
and requires at least one positive entry. Entries must name concrete behaviours (random is
refused), and the same behaviour cannot be weighted twice.--random-weights, random uses a
success-heavy default profile (success 48, slow_success 10, partial_disconnect 10, connection_reset
5, stream_disconnect 5, empty 5, rate_limit 5, server_error 4, max_tokens 2, stall 2,
service_unavailable 2, partial_eof 1, malformed_json 1). It is test pressure, not an estimate of how
often these failures happen in production.stall needs a client-side timeout. When the weights can produce stall, give the client under
test a short stream-idle timeout so the scenario ends promptly.random. --seed and --random-weights are refused when the
sequence holds no random entry.| Flag | Default | What it controls |
|---|---|---|
--success-text | mock response recovered | Text of a complete response. |
--partial-text | discarded partial response | Text sent before a partial end or a reset. |
--reasoning-text | mock reasoning | Reasoning text of reasoning_success. |
--chunk-size | 8 | Code points per stream delta. |
--chunk-delay-ms | 25 | Pause between chunks in slow_success. |
--disconnect-delay-ms | 10 | Pause before a forced reset. |
--retry-after-ms | 1000 | Retry delay; the Retry-After header rounds up to whole seconds. |
--request-id | (none) | Value returned as x-request-id on error responses. |
--tool-name | mock_tool | Tool 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.
POST whose path ends in / v1/ messages (any prefix before it is fine),
carrying the configured key when one is set and a body that parses as JSON, consumes the script. An
empty body is accepted.| Refusal | Answer |
|---|---|
Not a POST | 405 with allow: POST |
A path that does not end in / | 404 |
x-api-key does not match the configured key | 401 invalid mock API key |
| A body that does not parse as JSON | 400 request body must be valid JSON |
A misconfigured client can burn retries this way without advancing the sequence.
| Message | Status | Code |
|---|---|---|
mock rate limit | 429 | rate_limit, with Retry-After |
mock server error | 500 | server_error |
mock service unavailable | 503 | service_unavailable |
mock authentication failed | 401 | invalid_api_key |
mock invalid request | 400 | invalid_request |
mock input exceeds the model context window | 400 | context_length_exceeded |
mock insufficient quota | 429 | insufficient_quota |
mock script exhausted | 500 | MOCK_SCRIPT_EXHAUSTED |
mock server handler failed | 500 | MOCK_HANDLER_FAILED |
--request-id set, these responses carry it as x-request-id; only rate_limit adds
Retry-After.Each request record carries one outcome:
completed: the behaviour ran to its end, error responses included.reset: a disconnect behaviour destroyed the connection.stalled: the stall behaviour is holding the connection open.client_closed: the client went away before the response finished.server_error: the handler itself failed.ready record; the same seed and
weights draw the same sequence of behaviours.--listen-delay-ms window must overlap the
client's attempt, and random cannot draw connection_refused, because a bound listener cannot
refuse a connection.mock script exhausted?The sequence ran out. Lengthen --sequence, or pass --repeat-last so the final entry is reused for
every further request.
Only valid requests consume the script: a POST whose path ends in /, 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.
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.
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.
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.
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.
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.
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.
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.
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.