OpenAI Client with Anthropic Upstream
AISIX lets an application keep the OpenAI Chat Completions request shape while the gateway calls an Anthropic upstream model. Use this pattern when application code is already built around an OpenAI-compatible SDK, but the platform team wants to route that traffic to Claude.
AISIX resolves the model alias, translates the request to Anthropic Messages, calls Anthropic with the stored provider credential, and translates the response back into an OpenAI-compatible chat completion.
Prerequisites
Before starting, prepare the following:
- A running AISIX gateway that your application can reach.
- An Anthropic-backed model alias that accepts OpenAI-compatible Chat Completions requests.
- A caller API key allowed to use that model alias.
- Node.js 20 LTS or newer with
npmfor the SDK example, orcurlfor the HTTP example.
If you have not configured the model alias and caller API key, follow Anthropic for AISIX Enterprise, AISIX Cloud, or AISIX OSS.
Request Flow
The application keeps the OpenAI-compatible client contract. Provider selection and protocol translation stay in the gateway.
The application sends the model alias and caller API key to AISIX. The gateway resolves the upstream model, supplies the stored Anthropic credential, and translates both sides of the exchange. The application continues to send and receive OpenAI-compatible data.
Call the Alias
Export the caller API key and model alias used by both request examples:
export AISIX_API_KEY="YOUR_CALLER_API_KEY"
export AISIX_MODEL="claude-sonnet-prod"
OpenAI SDK
Install the OpenAI SDK:
npm install openai
Set the OpenAI-compatible base URL. The OpenAI SDK requires the /v1 path:
# The local quickstarts use http://127.0.0.1:3000/v1
export AISIX_BASE_URL="YOUR_AISIX_GATEWAY_URL/v1"
Create a minimal Chat Completions client:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.AISIX_API_KEY,
baseURL: process.env.AISIX_BASE_URL,
});
const completion = await client.chat.completions.create({
model: process.env.AISIX_MODEL,
messages: [{ role: "user", content: "Say hello from AISIX." }],
});
console.log(completion.choices[0]?.message.content);
console.log(completion.usage);
Run the example from the shell where the AISIX values are set:
node anthropic-via-openai-sdk.mjs
HTTP
To inspect the response without an SDK, export the gateway origin and send the same request with curl:
# AISIX_PROXY has no trailing slash or endpoint path
# The local quickstarts use http://127.0.0.1:3000
export AISIX_PROXY="YOUR_AISIX_GATEWAY_URL"
Send the request:
curl -sS -X POST "$AISIX_PROXY/v1/chat/completions" \
-H "Authorization: Bearer $AISIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "'"$AISIX_MODEL"'",
"messages": [{"role":"user","content":"Say hello from AISIX."}]
}'
Both examples return the OpenAI-compatible Chat Completions shape. The caller does not receive Anthropic-shaped content blocks:
{
"object": "chat.completion",
"model": "claude-sonnet-prod",
"choices": [
{
"message": {
"role": "assistant",
"content": "Hello from AISIX."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 5,
"total_tokens": 14
}
}
Translation Behavior
The translation preserves the parts of the OpenAI-compatible contract that common chat applications depend on:
| Behavior | What AISIX does |
|---|---|
| Model and authentication | Resolves the model alias to the configured Anthropic model, authenticates the caller, and uses the stored provider credential for the upstream request. |
| Messages and tools | Maps leading system messages, user and assistant messages, function tools, tool calls, and tool results to Anthropic Messages structures. Developer messages join the top-level system field wherever they appear in the conversation. A function tool's strict is copied onto the Claude tool. The legacy functions, function_call, and function role are mapped to tools, as described in Legacy Function Calling. |
| Tool choice | Sends none, auto, required, and a named function as Anthropic's none, auto, any, and tool. An allowed_tools or custom choice sets no choice. Any other value is refused with 400 naming tool_choice, as is a replayed assistant tool call without an id or function name. |
| Responses | Converts text and tool_use blocks, stop reasons, and token usage, on both non-streaming and streaming responses. On a stream, each tool_use block becomes a streamed tool call with its own index, numbered over tool calls only in arrival order: the first chunk carries the call's id and function name, and later chunks carry the argument fragments. A call that streams no arguments receives "{}". stop_reason: tool_use becomes finish_reason: tool_calls, refusal becomes content_filter, model_context_window_exceeded becomes length, and pause_turn becomes stop. |
| Thinking | Returns Anthropic thinking blocks as message.reasoning_content, with multiple blocks joined by a newline, and streamed thinking_delta events as delta.reasoning_content. redacted_thinking blocks and signatures are not returned. A reasoning_content field in the request history is not sent to Anthropic. An assistant turn that carries only reasoning, as reasoning_content or as reasoning content parts such as a Mistral thinking or Databricks reasoning part, is left out of the request. |
| Output limit | Sends max_completion_tokens as Anthropic's max_tokens; it wins over max_tokens when the request sends both, and a reasoning_effort thinking budget is sized against it. Supplies max_tokens: 4096 when the request sends neither, because Anthropic requires an output limit. A max_completion_tokens that is not a non-negative integer, or exceeds 4294967295, is refused with 400 naming the field. |
| Stop sequences | Sends stop as stop_sequences, a string as a one-element array. A stop_sequences the request sends itself wins over stop. null or an empty array sets nothing, and a value of another shape is refused with 400 naming stop. |
| Stream options | Does not send stream_options upstream. With stream_options.include_usage: true, the stream ends with one usage-only chunk after the finish chunk, as described in Streaming. |
| Structured output | Sends a response_format as Anthropic's native output_config.format on Claude 4.5 and later, and as a forced synthetic tool on every other model. See Structured Output. |
| OpenAI-only fields | Drops the OpenAI fields Anthropic has no place for: seed, n, logprobs, top_logprobs, logit_bias, frequency_penalty, presence_penalty, store, prompt_cache_key, prompt_cache_retention, verbosity, modalities, audio, prediction, and web_search_options. user and safety_identifier become metadata.user_id: a non-empty safety_identifier wins, otherwise user, and neither field reaches Claude under its own name. A metadata.user_id the request sends wins over both, and metadata keeps only user_id. service_tier and any field OpenAI does not define, such as an Anthropic field placed in the chat body on purpose, are forwarded as sent. |
| Reasoning effort | Sends reasoning_effort as output_config.effort, Anthropic's current depth control. minimal maps to low, Anthropic's floor, and none becomes thinking: {"type": "disabled"} instead, because Anthropic has no none tier. AISIX adds no thinking block in the other cases: the request asked for a depth, not a thinking mode, and current Anthropic models apply their own. A reasoning_effort that is not a string is refused with 400 naming the field. A string outside that set is dropped entirely: it corresponds to no known Anthropic tier, and the original field cannot be forwarded in its place either, because /v1/messages rejects unknown top-level fields. An output_config or thinking the request supplies itself is left as written and takes precedence. Claude families that reject output_config.effort receive a thinking budget instead; see Reasoning Effort on Older Claude Models. |
The output limit, stop sequence, and stream options handling applies to every Claude upstream: Anthropic and Anthropic-compatible providers, Claude on Vertex AI, and Claude on Amazon Bedrock, streamed or not. The OpenAI-only field and tool choice handling applies wherever the request reaches Claude as an Anthropic Messages body: Anthropic and Anthropic-compatible providers, Claude on Vertex AI, and non-streaming Claude requests on Amazon Bedrock InvokeModel. Bedrock Converse places these fields as described in Converse Request Fields. A gateway at 1.6.x or earlier forwards max_completion_tokens, stop, and stream_options to Anthropic and Vertex AI unchanged, and Anthropic refuses them with 400. A gateway at 1.7.0 or earlier forwards the OpenAI-only fields to Claude, where Anthropic refuses them with 400, and silently drops an invalid tool_choice or reasoning_effort. See Request Parameter Handling for the rule behind this translation.
When an OpenAI Chat Completions message uses typed content parts, the Anthropic translation sends image_url parts and PDF file parts as Anthropic image and document blocks. An https:// URL is passed to Anthropic as a URL source, and an http:// URL is downloaded by the gateway and sent inline. Claude takes no audio input, so an input_audio part, or any other part Claude cannot take, is rejected with 400 naming the part rather than dropped. See Media Inputs and URL Fetching.
For a complete tool loop, see Tool Calling. AISIX can also add Anthropic prompt-cache markers to eligible Chat Completions requests; see Anthropic Prompt Caching.
Use the Anthropic-style /v1/messages route when the application must keep the Anthropic request and response shape, especially for provider-specific content or thinking blocks. See Anthropic-Style Messages API for the native client contract and its compatibility boundaries.
Reasoning Effort on Older Claude Models
Claude Haiku 4.5, Claude Sonnet 4.5, and every Claude family before 4.5 reject output_config.effort with a 400. For these models AISIX sends the effort tier as an extended-thinking budget instead, thinking: {"type": "enabled", "budget_tokens": N}, with no output_config.effort:
reasoning_effort | budget_tokens |
|---|---|
minimal, low | 1024 |
medium | 2048 |
high | 4096 |
xhigh | 8192 |
max | 16384 |
Anthropic requires max_tokens to be larger than the thinking budget:
- A request without
max_tokensis sent the budget plus4096, such asmax_tokens: 8192forhigh. - A request with
max_tokenskeeps it, and the budget is lowered to at mostmax_tokens - 1. - A request with
max_tokensof1024or less is sent without thinking, because Anthropic's smallest budget is1024.
A thinking block or output_config.effort in the request takes precedence and is sent as written, and none still becomes thinking: {"type": "disabled"}. Once thinking is enabled, Anthropic's extended-thinking rules apply to the request: it rejects a temperature other than 1 and a tool_choice that forces a tool, and a tool loop must send the assistant's thinking blocks back unchanged. AISIX passes temperature and tool_choice through and leaves those rejections to Anthropic.
AISIX decides by the upstream model name, and the same translation applies on Anthropic, Google Vertex AI, and Amazon Bedrock. Claude Opus 4.5, Claude 4.6 and later, and model names without a recognizable Claude version, such as another vendor's model behind an Anthropic-compatible endpoint, keep receiving output_config.effort only. Claude families before 3.7 have no extended thinking and still reject the request.
Structured Output
A response_format on the Chat Completions request reaches the model. The same translation handles the response_format the Responses bridge builds from text.format, and the one the Anthropic Messages bridge builds from output_config.format.
Which shape goes on the wire depends on the upstream model:
- Claude 4.5 and later take Anthropic's own control: the schema lands in
output_config.format, merged beside whatever else that carrier already holds, such as an effort translated fromreasoning_effort. Aformatthe request supplies natively is the more specific statement of the same setting and wins. No beta header is sent; the current API takes the field without one. - Every other model — older Claude families, and the non-Claude models served behind Anthropic-compatible endpoints — takes the synthetic tool route, which needs no capability beyond tool calling.
AISIX holds no model capability map, so it reads the family version off the upstream model name. Both of Anthropic's orderings are recognized (claude-3-5-haiku-... and claude-sonnet-4-5-...), and the trailing eight-digit release date is read as a date rather than a minor version: claude-sonnet-4-20250514 is 4.0, while claude-sonnet-4-5-20250929 is 4.5. The claude-sonnet-4-5@20250929 spelling is recognized too.
{"type": "json_object"} and {"type": "text"} name no schema, and Anthropic's JSON controls are schema-driven on both routes, so neither puts anything on the Anthropic body. response_format itself never reaches that body either way: /v1/messages rejects unknown top-level fields.
The Synthetic Tool Route
Where the model has no native control, the schema rides a synthetic tool named json_tool_call whose input is the answer. It is appended to the tools the caller sent and forced through tool_choice, which is what makes the reply JSON rather than a suggestion the model may ignore. Two things outrank the forcing, each leaving the synthetic tool on offer under the model's own auto:
- A
tool_choicethe request stated — any value,autoincluded.autois not an absence of intent: it is the client saying the model decides, and a client running an agent loop sends it beside its own tools on every turn, so forcing the synthetic tool there would mean those tools could never be called for as long asresponse_formatis set. An explicit JSONnullcounts as unstated, because it is how SDKs spell an absent optional. - Extended thinking, which Anthropic rejects outright beside a forced tool choice.
The reply is translated back before it reaches the caller. When the synthetic call is the only one, its input replaces the message content, with no tool_calls and finish_reason: stop — a client that never offered a tool must not be told the model stopped to call one. When the model also called tools the caller really offered, the caller did ask for tool calls and is parsing them itself, so those calls and their finish reason survive and the JSON is appended to whatever text came with them.
A streaming request on this route is served as a simulated stream. A tool call cannot be streamed before it is complete, so AISIX makes one non-streaming upstream call and renders the result as ordinary role, content, finish, and usage chunks; downstream encoders and usage reporting see a normal stream. When the reply carries reasoning, it arrives ahead of the content: as delta.reasoning_content on /v1/chat/completions, as a thinking block with signature: "" before the text block on /v1/messages, and as reasoning summary deltas on /v1/responses, where the reasoning item precedes the message item. The cost is first-byte latency — the client waits for the whole completion instead of the first token — and it applies only to a request that asks for a schema on a model with no native control. That leg is measured against the model's end-to-end timeout rather than its stream_timeout, so a chunk-gap deadline shorter than the request deadline does not cut it short.
How AISIX Adapts the Schema
Anthropic compiles the schema into a decoding grammar and returns an error for any keyword outside its documented subset, so a schema that worked against an OpenAI upstream would fail outright if forwarded untouched. AISIX adapts it instead, on both routes:
- Sealed, not closed. Every object in the schema gets
additionalProperties: false, which Anthropic requires.requiredis left exactly as written: Anthropic treats it as an ordinary JSON Schema keyword, so a property left out of it stays optional and merely sorts after the required ones. Promoting every declared property intorequired, the way OpenAI strict mode does, would make an optional field mandatory on this provider and nowhere else. - Narrowed to the supported subset.
minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf,minLength,maxLength,maxItems,uniqueItems, and aminItemsother than0or1are removed and restated in that property'sdescription—"full name (maxLength: 20)"— so the constraint still reaches the model as a sentence even though the decoder no longer enforces it. oneOfbecomesanyOf. Anthropic documentsanyOfand notoneOf, and for constraining output the difference between matching one branch and matching at least one does not bind, since a document the model produced matches whichever branch it followed. Dropping the keyword would take the alternatives with it.- Internal
$ref,$defs, anddefinitionsare supported by Anthropic and are kept. A recursive or external$refis left exactly as it came in, so the provider's own rejection stands rather than a silently mangled schema.
Token Usage
Anthropic and OpenAI count prompt-cache tokens differently, so AISIX converts the counts instead of passing them through. Anthropic reports input_tokens as the non-cached input, with cache_creation_input_tokens and cache_read_input_tokens as separate counters beside it. OpenAI accounting has one prompt_tokens that already includes the cached part, named under prompt_tokens_details.cached_tokens. OpenAI has no cache-write concept at all, so AISIX folds the write into prompt_tokens — it is billed input — and reports it beside the hit.
An upstream response reporting:
{
"usage": {
"input_tokens": 40,
"output_tokens": 10,
"cache_creation_input_tokens": 30,
"cache_read_input_tokens": 70
}
}
reaches an OpenAI-compatible caller as:
{
"usage": {
"prompt_tokens": 140,
"completion_tokens": 10,
"total_tokens": 150,
"prompt_tokens_details": {
"cached_tokens": 70,
"cache_creation_tokens": 30
}
}
}
The rules a caller can rely on:
prompt_tokensis the full input the model read, cache reads and cache writes included.total_tokensisprompt_tokens + completion_tokens.cached_tokensis a subset ofprompt_tokens, and counts only cache reads.cache_creation_tokensis the cache write, also a subset ofprompt_tokens. It is billed input but is not a cache hit, so it is reported separately rather than insidecached_tokens. OpenAI has no cache-write concept, so this field appears only when the upstream reported one — which matters because a provider typically bills a write above the plain input rate. On the first turn of a cached conversation, a write with no read, it is the only signal that a cache was involved at all.
The same conversion applies to streaming responses and to the Responses API over an Anthropic upstream.
Logs, metrics, and spend reporting are not converted: they keep Anthropic's own counters, so a call costs the same whichever protocol addressed it. See Anthropic Prompt Caching for the recorded view.
Next Steps
You have now routed an OpenAI-compatible client to an Anthropic upstream. See OpenAI-Compatible API for caller-facing route behavior. Use Anthropic Messages when you want the Anthropic request and response shape end to end, or review Provider Compatibility for endpoint and provider support boundaries.