Azure OpenAI · Endpoints
Azure serves the same models as OpenAI through a different front door. Most of the differences are in the routing rather than the payloads — the exception being content filtering, which Azure adds to every response. Two surfaces are served under the /azure/openai prefix.
| Method | Path | Notes |
|---|---|---|
| POST | …/deployments/{deployment}/chat/completions | Full chat.completion object, SSE streaming with [DONE], tool calls |
| POST | …/deployments/{deployment}/embeddings | Deterministic unit vectors, correct dimension per model |
| POST | /azure/openai/v1/chat/completions | The newer surface: no api-version, no deployments |
| POST | /azure/openai/v1/embeddings | Same handlers, addressed by model |
| GET | /azure/openai/models?api-version= | Model catalog, outside the deployment path |
The classic surface addresses a deployment rather than a model and requires api-version on every call. The v1 surface drops both:
# classic
/azure/openai/deployments/{deployment}/chat/completions?api-version=2024-10-21
# v1
/azure/openai/v1/chat/completions
{resource}.openai.azure.com) and keeps
/openai in the path. A mock cannot hand out subdomains, but it does not need to — the SDK
takes an arbitrary URL, so the resource simply becomes part of the prefix. Both surfaces run the same
handlers, so the same request returns the same body on either.
Azure · Authentication
Azure takes the key in an api-key header rather than a bearer token — the single most common reason an OpenAI-shaped client fails against it. Authorization: Bearer is accepted too, as Entra ID callers use it.
import { AzureOpenAI } from "openai";
const client = new AzureOpenAI({
baseURL: "http://localhost:3000/azure/openai",
apiKey: "sk-mock-key-01",
apiVersion: "2024-10-21",
});
Use baseURL rather than endpoint: the SDK reads OPENAI_BASE_URL from the environment and then refuses to combine it with endpoint, so a stray env var breaks the client before it sends anything. The v1 surface needs no Azure-specific client at all — a plain OpenAI pointed one level deeper at /azure/openai/v1.
The two failures answer differently, and that is Azure's own doing: a missing key is rejected by the API gateway, which knows nothing about the service's error envelope, while an invalid one gets the wrapped shape.
// missing credential — no `error` wrapper at all
{ "statusCode": 401, "message": "Access denied due to missing subscription key…" }
// invalid credential — the service's envelope
{ "error": { "code": "401", "message": "Access denied due to invalid subscription key…" } }
Note that Azure's code is a symbolic string (DeploymentNotFound, BadRequest) where OpenAI uses a snake_case slug or null.
Azure · Deployments
A deployment is a named alias a customer creates in their own Azure resource, pointing at a model. A mock has no resource and therefore no deployments, so any name is accepted — the way every provider here accepts any model id.
What cannot be dropped is the 404. DeploymentNotFound is the error Azure users hit most, and the one their code most needs to handle, so it is driven by the name itself: anything starting with missing- returns the real thing.
{
"error": {
"code": "DeploymentNotFound",
"message": "The API deployment for this resource does not exist. If you created the deployment within the last 5 minutes, please wait a moment and try again."
}
}
model echoed back is the one the client asked for, since
the two need not agree — the SDK only uses model as the deployment segment when no fixed
deployment is configured. The reserved prefix belongs to the deployment path only: on the v1 surface there
is no deployment to be missing, and missing-one is just a model name.
Azure · Content filtering
The most Azure-specific thing about the service, and the reason an OpenAI-shaped client can meet a response it did not expect. Every chat completion carries the filter's verdict — on the prompt at the top level, on each choice individually.
{
"choices": [{
"message": { "content": "Echo: hello" },
"finish_reason": "stop",
"content_filter_results": {
"hate": { "filtered": false, "severity": "safe" },
"self_harm": { "filtered": false, "severity": "safe" },
"sexual": { "filtered": false, "severity": "safe" },
"violence": { "filtered": false, "severity": "safe" }
}
}],
"prompt_filter_results": [{
"prompt_index": 0,
"content_filter_results": { /* the four above, plus binary detectors */
"jailbreak": { "filtered": false, "detected": false } }
}]
}
To exercise the failure paths, pin the verdict with x-llm-mock-content-filter, as <target> or <target>:<category>:<severity>:
| Value | Result |
|---|---|
prompt | 400 with code: "content_filter", and the verdict nested in innererror |
completion | 200 with content: null and finish_reason: "content_filter" |
unavailable | 200, with an error object where the verdict would be — the documented "filter did not run" case |
Categories are hate, self_harm, sexual, violence and jailbreak; severities safe, low, medium, high. So completion:violence:high filters the reply and says which category did it.
chunk.choices[0] breaks there. And the
blocked-prompt error nests its verdict under content_filter_result, singular,
against the plural used everywhere else.
None of this provider's shapes could be checked against a live Azure resource. Its routes come from observing what AzureOpenAI puts on the wire, and its payloads from Microsoft's documentation and published examples — but they are not observed responses. The empty first streaming chunk in particular is reconstructed from reported behaviour rather than seen.
Other providers
Same server, same key set, one page each.
| Provider | Prefix | Reference |
|---|---|---|
| OpenAI | /openai/v1 | Read → |
| Anthropic | /anthropic | Read → |
| Gemini (AI Studio) | /gemini | Read → |
| Gemini Enterprise | /gemini-enterprise | Read → |