Repository navigation
feat: raw SSE/JSON passthrough fixtures for LLM endpoints - #580
Merged
Merged
Conversation
…them (#549) Raw fixtures with an error status fail over on OpenRouter like typed error fixtures. A raw fixture that sets its own misbehavior is skipped by the misbehavior planner.
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #549
Summary
This PR adds raw passthrough fixtures. A fixture response of the form
{ raw: { wire, events?, json?, status?, headers?, done? } }sends provider events or JSON exactly as written. aimock adds only the framing of the named wire. No overrides, reasoning gating, usage synthesis, provider shaping or drift checks apply to a raw fixture.Per-wire framing. Each wire uses the same bytes that the typed handlers write for that wire:
event: <type>thendata: <json>):openai-responses,anthropic,cohere.data: <json>):openai-chat,gemini,gemini-interactions. Onopenai-chat, the stream ends withdata: [DONE]unlessraw.doneisfalse.ollama(/api/chat) andollama-generate(/api/generate). These are two wires because the frames of the two routes have different shapes.bedrock-invokewraps each event as achunkframe with base64bytes.bedrock-converseuses the single key of each event as the frame's event type and its value as the payload.That gives 10 wires:
openai-responses,openai-chat(including the OpenRouter and Azure routes),anthropic,gemini(including Vertex),gemini-interactions,cohere,ollama,ollama-generate,bedrock-invokeandbedrock-converse. The router offers a raw fixture only to the handler for itsraw.wire. The handler supplies the wire, never the request body. When a raw fixture matches a request in every way except its wire, and no other fixture matches, the server log names both wires instead of returning a silent 404.Stream vs JSON. A streaming request gets
eventsand a non-streaming request getsjson. If the body that the request needs is missing, aimock answers with the native 500 error of that wire and the codeaimock_raw_mode_unavailable.Status and headers.
statusdefaults to 200 and must be 200-599, except 204 and 304.headersreplace the defaults case-insensitively. Ifheadershas no Content-Type, a stream gets the wire's own type and a non-streaming reply getsapplication/json. aimock rejects headers that it must set itself, because it frames the body.Error codes. An invalid raw fixture gets a 500 in the wire's own error shape with the code
aimock_raw_fixture_invalid. Examples are a factory that returnsstatus: 99, or a factory that returns a raw fixture for another wire. On Gemini, Ollama and OpenRouter the code is prefixed to the message.Validation.
--validate-on-load,addFixturesFromJSONandPOST /__aimock/fixturesall check raw fixtures with one shared checker:eventsorjsonis present;statusand the header names and values are valid;doneis a boolean, and only onopenai-chat;customToolCallsandresponsesBlocksare not next toraw;Unknown keys inside
rawand an emptyeventsare warnings.chunkSizeandopenRouterProcessingon a raw fixture are warnings, because they are ignored. The same check runs again when a fixture is served, so fixtures added withon(),addFixture()or factories are checked too.Misbehavior skip. Model misbehavior faults skip raw fixtures. aimock skips a server-level fault, serves the raw reply, and records the fault as skipped in the journal. An
X-AIMock-Misbehaviorheader returns 501aimock_misbehavior_not_applicable. A raw fixture that sets its own misbehavior is an invalid fixture: load validation reports it, and when it is served it getsaimock_raw_fixture_invalid.Timing rules.
latency,streamingProfile,recordedTimingsandreplaySpeedpace streamed raw events in the same way as typed frames.truncateAfterChunks,disconnectAfterMsand chaos also apply, and each raw event counts as one chunk. A non-streaming raw reply followsnonStreamingTiminglike a typed fixture.nonStreamingTiming: truedelays the reply on OpenAI Chat Completions only.nonStreamingTimingAllProviders: truedelays it on every raw wire. aimock sizes the wait from the served JSON.Error replies. A raw reply with a status of 400 or more is never delayed, streaming or not. If the reply has a
json, a streaming request gets that JSON instead of events, because provider APIs answer errors that way. Error replies are recorded in the journal.WS Responses.
/v1/responsesover WebSocket answers a raw fixture with an error event with the codeaimock_raw_mode_unavailableand the message "raw fixtures are not yet supported on the WebSocket transport". Before this change, it answered with the generic unknown-type error.OpenRouter failover. In an OpenRouter
models[]fallback chain, anopenai-chatraw fixture with a status of 400 or more fails over to the next model. A typed error fixture with the same status does the same.Compatibility
Additive only. The feature is opt-in. A response is raw only if
rawis a non-null, non-array object and no other response type's marker key (content,toolCalls,error,json, and so on) is present. A typed fixture with a strayrawkey is still served, and validated, exactly as before.Typed fixtures unchanged. Existing typed fixtures produce output byte-identical to main: an offline probe compared 321 cases (status, headers except
date, and body) on the pre-review build, and the no-regression snapshot plus the feat: stop delivery on client disconnect across providers, and opt-in non-streaming timing for all providers #579 byte goldens pass at the final head. The no-regression suite (raw-no-regression.test.ts, with a snapshot) is green on main and on this branch.API report. The API report changes are additions only:
RawResponse,RawWireIdandWireId(WireIdwas already declared, and is now exported);RawResponseis added to theFixtureResponseandFixtureFileResponseunions;@internalwirefield onMatchOptions.isRawResponseis internal and not exported.Red-green evidence
Each wire was reproduced against a real aimock server, with and without
--validate-on-load, and with the real provider SDK tests. Before the change, every raw fixture request got a 500 with the bodyFixture response did not match any known type. After the change, the authored events or JSON are served with the wire's framing.text/event-stream/ 200application/jsontext/event-stream, endsdata: [DONE]/ 200application/jsontext/event-stream/ 200text/event-stream/ 200/api/chat500,/api/generate500, "did not match any known type"/api/chat200,/api/generate200,application/x-ndjsonapplication/vnd.amazon.eventstream/ 200 on bothThe split between
ollamaandollama-generatehas its own red-green. Before the split, the test "/api/generate gets the ollama-generate fixture in generate shape" failed (1 failed, 1 passed). After the split, it passes (2 passed).The full suite, typecheck, lint, format check, build, goldens/contract tests and the API report check all pass on this branch.
Not in this PR
record.raw(recording raw fixtures). Both are in spec PR3.Spec: raw SSE/JSON passthrough fixtures (Notion)