Most teams discover drift the same way: an incident. Production code that worked fine against a mock starts throwing parse errors once it talks to the real provider. The mock was green the whole time. The gap between the mock and the provider is rarely a single bug; it is a classification of gaps, and knowing the classes tells you which checks to write.
The first class is transport-level. Status codes are the obvious case: a mock that returns 200 for everything teaches the client that malformed requests succeed. A permissive catch-all mock that answers 200 to any path is the single most damaging pattern, because it silently removes error handling from the test surface stop mock APIs from drifting. Error bodies matter just as much. If the provider returns a structured error object with a type, code and message, and your mock returns a bare string, your client's error parser is never exercised.
The second class is streaming. When a provider sets stream=true, the response arrives as server-sent events, and the client's parser is sensitive to frame order, the exact event names and the terminal frame. A mock that sends the concatenated text as one chunk, or that omits the final [DONE]-style frame, passes tests and fails in production. This is not hypothetical: in LiteLLM, rate-limit headers are populated by a post-call success hook, and for stream=true requests the SSE headers are flushed before that hook runs, so the client never sees the x-ratelimit-* keys at all the LiteLLM issue. A mock built by capturing a non-streaming response would never reveal that difference.
The third class is metadata: token accounting and rate-limit headers. Usage numbers appear in the response body, and limit headers appear in the envelope. A mock that hardcodes input_tokens: 12 is fine as a fixture, but if your application parses usage to compute cost or budget, the mock must carry the same field names and nesting as the provider, or the budget code is untested.
The fourth class is schema drift over time. Providers retire models and change defaults on a published schedule; OpenAI, for instance, publishes tiered notice periods, at least six months for generally available models and as little as two weeks for preview models OpenAI deprecations, and a cross-vendor calendar collects the same for Anthropic, Google and Bedrock the lifecycle calendar. The deeper problem is that pinned IDs are not always what they look like: Anthropic states that dateless IDs for the 4.6 generation and later are fixed snapshots, not evergreen pointers, and that serving infrastructure changes can produce minor behavioral differences even when weights do not change Claude model IDs and versioning. A fixture pinned to "whatever the provider does today" has no anchor.
| Divergence class | Symptom you will see | What the check must assert |
|---|---|---|
| Status codes | Client never handles 429 or 5xx | Exact status per scenario, including error paths |
| Error body | Crash or generic message on failures | Same error type, code and required fields |
| Streaming frames | Truncated or garbled output under stream | Frame order, event names, terminal frame |
| Token accounting | Cost or budget code silently wrong | Same field names and nesting as provider |
| Rate-limit headers | Quota tracking shows nothing | Headers present per endpoint and per mode |
| Schema version | Field renamed or removed upstream | Mock built from a pinned spec artifact |



