Oxyy API Documentation
Oxyy gives you one endpoint and one API key for 200+ models — text, vision, images, video, speech and embeddings — behind the OpenAI wire format. If your code already speaks to OpenAI, changing the base URL is the whole migration.
https://api.oxyy.ai/v1 — set it as base_url (Python) or baseURL (JavaScript) and keep the rest of your code. The Anthropic and Google SDKs use the bare host instead; see Vendor SDKs.What you can call
| Endpoint | What it does | Shape |
|---|---|---|
POST /v1/chat/completions | Text, vision, tools, structured output, and images answered over chat | Sync or streaming |
POST /v1/responses | The OpenAI Responses API, for the Agents SDK and Codex | Sync or streaming |
POST /v1/images/generations | Text to image | Sync, or a job with async |
POST /v1/images/editsPOST /v1/images/variations | Edit or vary an existing image | Sync |
POST /v1/videos/generationsPOST /v1/videos/image-to-video | Text or image to video | Job — always asynchronous |
POST /v1/audio/speech | Text to speech, and music | Audio bytes, or a job with async |
POST /v1/audio/transcriptionsPOST /v1/audio/translations | Speech to text, and speech to English text | Sync |
POST /v1/embeddings | Vectors for search, clustering and RAG | Sync |
GET /v1/models | The catalogue your key can reach, with pricing | Sync |
POST /v1/assets/upload | Host a file for 12 hours and get a URL to reference | Sync |
POST /v1/messages | Anthropic Messages API — Claude models | Sync or streaming |
POST /v1beta/models/{model}:generateContent | Google GenAI API — Gemini models | Sync or streaming |
Authentication
Every request carries your API key. Create one on the API keys page after signing up. The standard form is a bearer token:
# The standard form — every endpoint accepts it. curl https://api.oxyy.ai/v1/models \ -H "Authorization: Bearer $OXYY_API_KEY" # Vendor-SDK forms, accepted on every endpoint so an SDK # configured for Anthropic or Google needs no interception. curl https://api.oxyy.ai/v1/models -H "x-api-key: $OXYY_API_KEY" curl https://api.oxyy.ai/v1/models -H "x-goog-api-key: $OXYY_API_KEY"
# Read the key from the environment. Never commit it, and never # ship it in browser or mobile code — anyone can read it there. import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" )
Three header forms are accepted on every endpoint, so an SDK written for another vendor works without interception:
| Form | Sent by | Example |
|---|---|---|
Authorization: Bearer <key> | OpenAI SDK, plain HTTP | Authorization: Bearer sk-oxyy-… |
x-api-key: <key> | Anthropic SDK | x-api-key: sk-oxyy-… |
x-goog-api-key: <key> or ?key= | Google GenAI SDK | x-goog-api-key: sk-oxyy-… |
Quick start
OXYY_API_KEY.https://api.oxyy.ai/v1. Nothing else changes.GET /v1/models.# pip install openai import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) response = client.chat.completions.create( model="gpt-5", messages=[{"role": "user", "content": "Hello!"}] ) print(response.choices[0].message.content) print(response.usage.cost, "USD")
// npm install openai import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai/v1' }); const response = await client.chat.completions.create({ model: 'gpt-5', messages: [{ role: 'user', content: 'Hello!' }] }); console.log(response.choices[0].message.content); console.log(response.usage.cost, 'USD');
curl https://api.oxyy.ai/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{"model": "gpt-5", "messages": [{"role": "user", "content": "Hello!"}]}'
usage.cost is what the request charged, in USD. You never have to open a dashboard to find out what a call cost — see Usage & cost.Models & discovery
The catalogue is live: models are added, repriced and retired without a release on your side. Read it from the API rather than hard-coding a list.
The list returns every enabled model with its pricing, context window and modalities. Two fields are Oxyy's own: accessible says whether your tier may call that model, and access_reason says why not when it is false — so a model picker can grey a row out and explain itself. The detail endpoint adds every parameter described rather than merely named, the endpoints and SDKs that serve it, and your own rate limits on it.
# Every model your key can reach, with pricing and capabilities. curl https://api.oxyy.ai/v1/models \ -H "Authorization: Bearer $OXYY_API_KEY" # One model, with every parameter described rather than just named. curl https://api.oxyy.ai/v1/models/gpt-5 \ -H "Authorization: Bearer $OXYY_API_KEY"
import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) # `accessible` says whether YOUR tier may call it; `access_reason` # says why not when it is false. for m in client.models.list().data: if m.oxyy["type"] == "TEXT" and m.accessible: print(m.id, m.oxyy["contextWindow"])
Example response
{
"object": "list",
"total_count": 207,
"data": [
{
"id": "gpt-5",
"object": "model",
"created": 1700000000,
"owned_by": "openai",
"accessible": true,
"access_reason": null,
"oxyy": {
"displayName": "GPT-5",
"type": "TEXT",
"contextWindow": 400000,
"maxTokens": 128000,
"inputModalities": ["text", "image"],
"outputModalities": ["text"],
"supportedParameters": ["tools", "response_format", "reasoning_effort"],
"pricing": { "input": 1.25, "output": 10, "cacheRead": 0.125, "unit": "per_1m_tokens" },
"accessible": true
}
}
]
}Which endpoint serves which model
Each model has a type, and each endpoint serves a set of types. Send a model to the wrong endpoint and the answer is a 400 with code model_type_mismatch that names the endpoint you wanted — before anything is billed.
| Endpoint | Model types | Notes |
|---|---|---|
POST /v1/chat/completions | Any chat-served model | Text, vision, and image models answered over chat |
POST /v1/responses | TEXT, IMAGE_TO_TEXT, IMAGE_GENERATION | The Responses object model has no item for audio, video or vectors |
POST /v1/images/* | IMAGE_GENERATION, IMAGE_TO_IMAGE | |
POST /v1/videos/* | TEXT_TO_VIDEO, IMAGE_TO_VIDEO, VIDEO_TO_VIDEO | |
POST /v1/audio/speech | AUDIO_TTS, MUSIC_GENERATION | Music is billed and returned as speech-shaped audio |
POST /v1/audio/transcriptionsPOST /v1/audio/translations | AUDIO_STT | |
POST /v1/embeddings | EMBEDDING |
Streaming
Set stream: true on chat completions, responses or either vendor SDK and the reply arrives as server-sent events. Every official SDK handles the framing for you; the shape below is what you get over plain HTTP.
import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) stream = client.chat.completions.create( model="gpt-5", messages=[{"role": "user", "content": "Write a haiku about caching."}], stream=True, stream_options={"include_usage": True}, # final chunk carries usage + cost ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True) elif chunk.usage: print(f"\n\ncost: {chunk.usage.cost}")
import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai/v1' }); const stream = await client.chat.completions.create({ model: 'gpt-5', messages: [{ role: 'user', content: 'Write a haiku about caching.' }], stream: true, stream_options: { include_usage: true }, }); for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta?.content; if (delta) process.stdout.write(delta); if (chunk.usage) console.log('\ncost:', chunk.usage.cost); }
curl -N https://api.oxyy.ai/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "gpt-5", "messages": [{"role": "user", "content": "Hello!"}], "stream": true, "stream_options": {"include_usage": true} }'
On the wire
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1700000000,"model":"gpt-5","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1700000000,"model":"gpt-5","choices":[{"index":0,"delta":{"content":"Cold"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1700000000,"model":"gpt-5","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
// Only when stream_options.include_usage is true — choices is empty here.
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1700000000,"model":"gpt-5","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":17,"total_tokens":29,"cost":0.000106}}
data: [DONE]| Parameter | Type | Required | Description |
|---|---|---|---|
| stream | boolean | Optional | Stream the reply as server-sent events. Default false |
| stream_options.include_usage | boolean | Optional | Emit one final chunk carrying usage — token counts and cost — with an empty choices array, just before [DONE]. Default false |
data: [DONE]. Closing the connection cancels the upstream generation, and you are billed only for what was produced. A failure that happens after the first byte arrives as an error frame inside the stream rather than an HTTP status, because the status was already sent.Idempotency-Key is ignored on streaming requests — a stream cannot be replayed.Tool & function calling
Describe your functions, and a model that supports tools will ask you to run one. Oxyy translates the declaration into whatever each provider expects, so the same tools array works across OpenAI, Anthropic, Google, xAI and the rest.
| Parameter | Type | Required | Description |
|---|---|---|---|
| tools | array | Optional | Function declarations: {type: "function", function: {name, description, parameters}}, where parameters is a JSON Schema object. |
| tool_choice | string|object | Optional | How freely the model may call a tool. One of:noneautorequired{type:"function",function:{name}} Default auto |
| parallel_tool_calls | boolean | Optional | Allow more than one tool call per turn. Default true |
import json, os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) tools = [{ "type": "function", "function": { "name": "get_weather", "description": "Current weather for a city.", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }, }] messages = [{"role": "user", "content": "Weather in Dhaka?"}] response = client.chat.completions.create( model="gpt-5", messages=messages, tools=tools, tool_choice="auto" ) call = response.choices[0].message.tool_calls[0] args = json.loads(call.function.arguments) # Run the tool yourself, then send the result back as a tool message. messages.append(response.choices[0].message) messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps({"temp_c": 31, "sky": "humid"}), }) final = client.chat.completions.create(model="gpt-5", messages=messages, tools=tools) print(final.choices[0].message.content)
import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai/v1' }); const tools = [{ type: 'function', function: { name: 'get_weather', description: 'Current weather for a city.', parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'], }, }, }]; const messages = [{ role: 'user', content: 'Weather in Dhaka?' }]; const res = await client.chat.completions.create({ model: 'gpt-5', messages, tools, tool_choice: 'auto', }); const call = res.choices[0].message.tool_calls[0]; const args = JSON.parse(call.function.arguments); messages.push(res.choices[0].message, { role: 'tool', tool_call_id: call.id, content: JSON.stringify({ temp_c: 31, sky: 'humid' }), }); const final = await client.chat.completions.create({ model: 'gpt-5', messages, tools }); console.log(final.choices[0].message.content);
The tool-call turn
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1700000000,
"model": "gpt-5",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Dhaka\"}"
}
}
],
"refusal": null
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": { "prompt_tokens": 61, "completion_tokens": 18, "total_tokens": 79, "cost": 0.00029 }
}{role:"tool", tool_call_id, content}message per call, and send the whole conversation again. finish_reason is tool_callswhenever calls are present, even on providers that report stop. On reasoning models, send back any reasoning_details unmodified so the model's own thinking survives the round trip.Structured outputs
response_format makes the model answer with JSON instead of prose. Use json_schemawhen you have a shape to enforce, and json_object when you only need valid JSON.
| Parameter | Type | Required | Description |
|---|---|---|---|
| response_format | object | Optional | The output contract. Forms:{type:"text"}{type:"json_object"}{type:"json_schema", json_schema:{name, schema, strict}} |
import json, os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) response = client.chat.completions.create( model="gpt-5", messages=[{"role": "user", "content": "Extract the invoice fields."}], response_format={ "type": "json_schema", "json_schema": { "name": "invoice", "strict": True, "schema": { "type": "object", "properties": { "total": {"type": "number"}, "currency": {"type": "string"}, }, "required": ["total", "currency"], "additionalProperties": False, }, }, }, ) data = json.loads(response.choices[0].message.content)
# json_object is the looser mode: valid JSON, no schema enforced. curl https://api.oxyy.ai/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "gpt-5", "messages": [{"role": "user", "content": "List 3 colours as JSON."}], "response_format": {"type": "json_object"} }'
```json fence some models add. Either way choices[0].message.content is a JSON document you can parse. strict: true needs a fully closed schema (additionalProperties: false and every property in required); leave it off if yours is not.Images, audio & documents as input
Send an array of content parts instead of a string, and a model that accepts that modality will read them. Both the OpenAI spelling and the Responses spelling are understood on /v1/chat/completions:
| Part | Carries | Source |
|---|---|---|
image_url / input_image | An image | {"url": "https://…"} or a data:image/…;base64,… URL |
input_audio | An audio clip | {"data": "<base64>", "format": "wav"} |
file / input_file | A PDF or other document | {"filename": "…", "file_data": "data:application/pdf;base64,…"} |
import base64, os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) # A public https URL, or a data URL you build yourself. with open("chart.png", "rb") as f: b64 = base64.b64encode(f.read()).decode() response = client.chat.completions.create( model="gpt-5", messages=[{ "role": "user", "content": [ {"type": "text", "text": "What does this chart show?"}, {"type": "image_url", "image_url": { "url": f"data:image/png;base64,{b64}", "detail": "auto", }}, ], }], ) print(response.choices[0].message.content)
curl https://api.oxyy.ai/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "gemini-3-pro", "messages": [{ "role": "user", "content": [ {"type": "text", "text": "Describe this image."}, {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}} ] }] }'
# A PDF or other document rides in a `file` content part. import base64, os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) with open("contract.pdf", "rb") as f: b64 = base64.b64encode(f.read()).decode() response = client.chat.completions.create( model="claude-sonnet-4.5", messages=[{ "role": "user", "content": [ {"type": "text", "text": "Summarise the termination clause."}, {"type": "file", "file": { "filename": "contract.pdf", "file_data": f"data:application/pdf;base64,{b64}", }}, ], }], )
inputModalities in GET /v1/models. Attaching an image to a text-only model is refused rather than quietly dropped, because a silently ignored attachment produces a confident answer about nothing. Per-file caps are 20 MB for images, 50 MB for audio and 100 MB for video and documents, and an operator can set a lower cap per model.400 that says so, not a confusing provider error.Reasoning
Reasoning models think before answering. reasoning_effort is the portable control — it is translated into each vendor's own dial, including Anthropic's thinking-token budget — and each vendor's native spelling is also accepted as sent.
| Parameter | Type | Required | Description |
|---|---|---|---|
| reasoning_effort | string | Optional | How much the model should think. One of:minimallowmediumhigh On Claude this becomes a thinking-token budget. |
| thinking | object | Optional | Anthropic's native control, forwarded verbatim: {type: "enabled"|"adaptive"|"disabled", budget_tokens}. |
import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) response = client.chat.completions.create( model="claude-opus-4.6", messages=[{"role": "user", "content": "Prove that sqrt(2) is irrational."}], reasoning_effort="high", # minimal | low | medium | high ) msg = response.choices[0].message print(msg.reasoning_content) # the model's thinking, when it emits any print(msg.content) print(response.usage.completion_tokens_details.reasoning_tokens)
# Anthropic-style thinking controls pass through untouched. curl https://api.oxyy.ai/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "claude-opus-4.6", "messages": [{"role": "user", "content": "Plan a migration."}], "max_tokens": 8000, "thinking": {"type": "enabled", "budget_tokens": 4000} }'
| Field on the response | What it is |
|---|---|
message.reasoning_content | The thinking, as text, when the model emits any |
message.reasoning_details | Structured reasoning blocks. Send them back unmodified in a tool loop so signatures and encrypted reasoning survive. |
usage.completion_tokens_details.reasoning_tokens | Thinking tokens, counted inside completion_tokens and billed at the output rate |
Prompt caching
A repeated prefix — a long system prompt, a document, a tool catalogue — can be read from the provider's cache instead of being processed again, at a fraction of the input rate. Caching is reported on every response so you can verify it is working.
| Field on <code>usage.prompt_tokens_details</code> | What it is |
|---|---|
cached_tokens | Input tokens served from cache, billed at the cache-read rate |
cache_write_tokens | Input tokens written into the cache on this request |
cache_creation.ephemeral_5m_input_tokenscache_creation.ephemeral_1h_input_tokens | The TTL split of that write. The two are billed at different rates, so the split is reported rather than summed. |
cache_control on a system block — the Anthropic SDK passes it through per block. Whether a prefix is cached at all is the provider's decision, which is why the numbers above are reported rather than promised.Usage & cost accounting
Every response carries what it cost. usage.cost is the exact amount charged in USD — the same number written to your invoice — so you can attribute spend per request without reconciling anything.
| Field | What it is |
|---|---|
usage.cost | What this request charged, in USD |
usage.cost_details | The same amount split by axis (input, output, cache, image…), plus currency and any discount that applied |
usage.is_byok | Always false: Oxyy serves every request on its own provider keys |
X-Request-Id (header) | The id this request is recorded under. Present on media endpoints and on every error; quote it in support requests. |
On a stream, set stream_options.include_usage to receive the same object in the final chunk. Token counts are reported in both the chat vocabulary (prompt_tokens / completion_tokens) and the newer one (input_tokens / output_tokens), so one client library reading either shape sees the same numbers.
Chat completions
The main endpoint. GPT-5, Claude Opus 4.6, Gemini 3 Pro, DeepSeek, Grok, Llama and the rest — all with the same request body.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| model | string | Required | A model id from GET /v1/models, e.g. gpt-5, claude-opus-4.6, gemini-3-pro. |
| messages | array | Required | The conversation. Each item has a role and content; content may be a string or an array of content parts. Roles:systemdeveloperuserassistanttoolfunction |
| temperature | number | Optional | Sampling temperature, 0–2. Higher is more random. Default the provider's own |
| max_tokens | integer | Optional | Maximum tokens to generate, ≥ 1. Default the provider's own, or the model's default output limit where the provider requires a value |
| top_p | number | Optional | Nucleus sampling, 0–1. Use this or temperature, not both. |
| top_k | integer | Optional | Sample from the k most likely tokens. Forwarded to the providers that support it. |
| n | integer | Optional | How many completions to generate, 1–128. Every choice is billed. Default 1 |
| stream | boolean | Optional | Stream the reply as server-sent events. See Streaming. Default false |
| stream_options | object | Optional | {include_usage: true} adds a final chunk with token counts and cost. |
| stop | string|array | Optional | Up to four sequences that end the generation. |
| seed | integer | Optional | Best-effort determinism on the providers that support it. |
| frequency_penalty | number | Optional | -2–2. Discourages repeating tokens by frequency. Default 0 |
| presence_penalty | number | Optional | -2–2. Discourages repeating any token already used. Default 0 |
| tools | array | Optional | Function declarations. See Tool calling. |
| tool_choice | string|object | Optional | One of:noneautorequired{type:"function",…} Default auto |
| parallel_tool_calls | boolean | Optional | Allow several tool calls in one turn. Default true |
| response_format | object | Optional | Force JSON output. See Structured outputs. Types:textjson_objectjson_schema |
| reasoning_effort | string | Optional | Thinking budget on reasoning models. One of:minimallowmediumhigh |
| logprobs | boolean | Optional | Return log probabilities for the chosen tokens. |
| top_logprobs | integer | Optional | How many alternatives to report per position. Requires logprobs. |
| logit_bias | object | Optional | Token id to bias, -100–100. |
| user | string | Optional | A stable id for your own end user. Helps you attribute usage. |
| modalities | array | Optional | What the model should return. Use ["image","text"] to have an image model answer over chat. One of:textimageaudio |
| image_config | object | Optional | Picture controls when a model answers with an image: {image_size, aspect_ratio}. |
| service_tier | string | Optional | Forwarded to providers that offer tiers, and echoed back on the response. |
| metadata | object | Optional | Opaque key/value pairs carried with the request. |
Code examples
import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) response = client.chat.completions.create( model="claude-sonnet-4.5", messages=[ {"role": "system", "content": "You are a concise assistant."}, {"role": "user", "content": "Explain HTTP caching in two sentences."}, ], temperature=0.7, max_tokens=4096 ) print(response.choices[0].message.content)
import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai/v1' }); const response = await client.chat.completions.create({ model: 'claude-sonnet-4.5', messages: [ { role: 'system', content: 'You are a concise assistant.' }, { role: 'user', content: 'Explain HTTP caching in two sentences.' }, ], temperature: 0.7, max_tokens: 4096 }); console.log(response.choices[0].message.content);
curl https://api.oxyy.ai/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "claude-sonnet-4.5", "messages": [{"role": "user", "content": "Explain HTTP caching."}], "temperature": 0.7, "max_tokens": 4096 }'
// composer require openai-php/client $client = OpenAI::factory() ->withApiKey(getenv('OXYY_API_KEY')) ->withBaseUri('https://api.oxyy.ai/v1') ->make(); $response = $client->chat()->create([ 'model' => 'claude-sonnet-4.5', 'messages' => [['role' => 'user', 'content' => 'Hello!']], 'temperature' => 0.7, ]); echo $response->choices[0]->message->content;
// go get github.com/sashabaranov/go-openai cfg := openai.DefaultConfig(os.Getenv("OXYY_API_KEY")) cfg.BaseURL = "https://api.oxyy.ai/v1" client := openai.NewClientWithConfig(cfg) resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{ Model: "claude-sonnet-4.5", Messages: []openai.ChatCompletionMessage{ {Role: openai.ChatMessageRoleUser, Content: "Hello!"}, }, })
Example response
{
"id": "chatcmpl-abc123def456",
"object": "chat.completion",
"created": 1700000000,
"model": "claude-sonnet-4.5",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you today?",
"refusal": null
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 12,
"completion_tokens": 9,
"total_tokens": 21,
"prompt_tokens_details": {
"cached_tokens": 0,
"cache_write_tokens": 0,
"text_tokens": 12,
"image_tokens": null,
"audio_tokens": null
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"text_tokens": 9,
"audio_tokens": null
},
"is_byok": false,
"cost": 0.000106,
"cost_details": { "currency": "USD", "input": 0.000036, "output": 0.00007 }
}
}finish_reason is one of stop, length, tool_calls, content_filter or function_call. When the provider reports something of its own, it is passed through beside it as native_finish_reason. An image answered over chat arrives on message.images.
Available models
Responses API
The OpenAI Responses API, for clients built on client.responses.create such as the OpenAI Agents SDK. It runs on the same models, routing and billing as chat completions, and every chat model is available through it. Use the OpenAI SDK with base_url set to https://api.oxyy.ai/v1.
input on every turn. previous_response_id is not supported, and neither are hosted tools such as web_search; both are refused with a 400 that says what to send instead.Code examples
# pip install openai import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) response = client.responses.create( model="gpt-5", instructions="Answer in one sentence.", input="What is the capital of France?" ) print(response.output_text)
// npm install openai import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai/v1' }); const response = await client.responses.create({ model: 'gpt-5', instructions: 'Answer in one sentence.', input: 'What is the capital of France?' }); console.log(response.output_text);
curl https://api.oxyy.ai/v1/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{"model": "gpt-5", "instructions": "Answer in one sentence.", "input": "What is the capital of France?"}'
Example response
{
"id": "resp_abc123def456",
"object": "response",
"created_at": 1700000000,
"status": "completed",
"model": "gpt-5",
"output": [
{
"id": "msg_abc123def456",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{ "type": "output_text", "text": "The capital of France is Paris.", "annotations": [] }
]
}
],
"output_text": "The capital of France is Paris.",
"previous_response_id": null,
"store": false,
"usage": {
"input_tokens": 21,
"input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
"output_tokens": 8,
"output_tokens_details": { "reasoning_tokens": 0 },
"total_tokens": 29,
"cost": 0.000106
}
}Multi-turn conversations
Keep the conversation in your application: append the previous output items and the next user message to input, then send all of it again. Reasoning and function-call items from output can be sent back as they are.
history = [{"role": "user", "content": "Name a prime number."}]
response = client.responses.create(model="gpt-5", input=history)
print(response.output_text)
# Nothing is stored between calls: append the reply and the next
# question, then send the whole conversation again.
history += [item.model_dump(exclude_none=True) for item in response.output]
history.append({"role": "user", "content": "Now double it."})
response = client.responses.create(model="gpt-5", input=history)
print(response.output_text)const input = [{ role: 'user', content: 'Name a prime number.' }]; let response = await client.responses.create({ model: 'gpt-5', input }); console.log(response.output_text); // Nothing is stored between calls: append the reply and the next // question, then send the whole conversation again. input.push(...response.output, { role: 'user', content: 'Now double it.' }); response = await client.responses.create({ model: 'gpt-5', input }); console.log(response.output_text);
Supported features
| Feature | Status | Notes |
|---|---|---|
input as a string or a list of items | Supported | message, function_call, function_call_output and reasoning items |
instructions, max_output_tokens, temperature, top_p | Supported | |
stream | Supported | Server-sent events such as response.output_text.delta and response.completed |
Function tools, tool_choice, parallel_tool_calls | Supported | tool_choice accepts none, auto, required, a function or allowed_tools |
text.format | Supported | text, json_object and json_schema |
reasoning.effort | Supported | |
input_image and input_file parts | Supported | Images as a URL or a data URL; files as file_data (a data URL). file_id and file_url are refused. |
Hosted tool: image_generation | Supported | The only hosted tool served here. Its size, quality, background and output_format drive an image model; other fields on the tool are reported as dropped. |
store, include, truncation, metadata | Supported | Accepted, but nothing is stored: store is always returned as false |
previous_response_id, conversation | Not supported | Refused with a 400. Send the full conversation in input instead. |
background, stored prompt templates, item_reference items | Not supported | Refused with a 400 |
Other hosted tools: web_search, file_search, code_interpreter, computer_use, mcp | Not supported | Refused with a 400. Declare a function tool and run it in your application. |
| Retrieving, deleting or cancelling a response by ID | Not supported | Only POST /v1/responses is available |
Image generation
DALL·E 3, Flux, Imagen 4, Nano Banana Pro, GPT Image and more — one request shape for all of them.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| model | string | Required | An image model id, e.g. dall-e-3, flux.1-pro, nano-banana-pro. |
| prompt | string | Required | What to draw. Up to 32,000 characters. |
| n | integer | Optional | How many images, 1–10. Each one is billed. Default 1 |
| size | string | Optional | A pixel size such as 1024x1024, a named tier, or auto. A pixel size on a tier-priced model is mapped up to the smallest tier that contains it. Accepted:0.5K1K2K4KautoWIDTHxHEIGHT |
| aspect_ratio | string | Optional | For models that take a ratio rather than a size, e.g. 16:9. |
| quality | string | Optional | One of:autohighmediumlowhdstandard |
| style | string | Optional | DALL·E 3. One of:vividnatural |
| background | string | Optional | GPT Image models. One of:transparentopaqueauto |
| output_format | string | Optional | One of:pngjpegwebp |
| output_compression | integer | Optional | 0–100, for jpeg and webp. |
| response_format | string | Optional | How the image comes back. One of:urlb64_json |
| negative_prompt | string | Optional | What to avoid. Forwarded to the models that accept it. |
| seed | integer | Optional | Reproducibility, where the provider supports it. |
| async | boolean | Optional | Return a job immediately (202) instead of waiting. See Jobs. Default false |
| user | string | Optional | A stable id for your own end user. |
auto always pass, because that is what the OpenAI SDK sends by default. The exact options per model are in GET /v1/models/{model}.Code examples
import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) response = client.images.generate( model="dall-e-3", prompt="A beautiful sunset over the ocean", n=1, size="1024x1024", quality="hd", style="vivid", ) print(response.data[0].url)
import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai/v1' }); const response = await client.images.generate({ model: 'dall-e-3', prompt: 'A beautiful sunset over the ocean', n: 1, size: '1024x1024', }); console.log(response.data[0].url);
curl https://api.oxyy.ai/v1/images/generations \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "dall-e-3", "prompt": "A beautiful sunset over the ocean", "n": 1, "size": "1024x1024", "response_format": "url" }'
Example response
{
"created": 1700000000,
"data": [
{
"url": "https://api.oxyy.ai/storage/images/2026/01/abc123.png",
"revised_prompt": "A beautiful sunset over a calm ocean..."
}
],
"usage": {
"input_tokens": 12,
"output_tokens": 1290,
"total_tokens": 1302,
"input_tokens_details": { "text_tokens": 12, "image_tokens": 0 },
"output_tokens_details": {
"image_tokens": 1290,
"text_tokens": 0,
"image_tokens_source": "catalog"
},
"cost": 0.04,
"cost_details": { "currency": "USD", "image": 0.04 }
}
}output_tokens_details.image_tokens_source says where the image token count came from — provider, the model's own published table (catalog), or an estimate — because a charge you can question deserves to say how it was reached.
Editing & variations
Both take the source image the same three ways as every other media endpoint — upload, URL or base64 — and both accept several reference images. See File inputs.
Asynchronous jobs
Send async: true and the endpoint answers 202 with a job instead of waiting. The job endpoints below behave exactly like the video ones.
Available models
Video generation
Generate video with Veo and the other video models in the catalogue.
202 Accepted with a job id — never a video. Poll the job until its status is completed, then read the file from result.data[0].url. A clip typically takes 30–120 seconds.Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| model | string | Required | A video model id, e.g. veo-3, veo-3.1-fast. |
| prompt | string | Required* | What should happen in the shot. *Optional when you supply an image to animate. |
| image | file|string | Optional | A starting frame — an upload, a URL or a data URL. Turns the request into image-to-video. |
| duration | number | Optional | Clip length in seconds, 1–60. Providers clamp to what they support (Veo renders 4–8s) and you are billed for the length actually produced. Default 4 |
| seconds | string|integer | Optional | OpenAI's spelling of the same thing. One of:4812 |
| resolution | string | Optional | One of:480p720p1080p4k |
| size | string | Optional | WIDTHxHEIGHT, e.g. 1280x720. An alternative to resolution. |
| aspect_ratio | string | Optional | e.g. 16:9, 9:16. |
| negative_prompt | string | Optional | What to keep out of the shot. |
| generate_audio | boolean | Optional | Ask for a soundtrack on the models that produce one. |
| person_generation | string | Optional | Provider-side policy for depicting people. |
| seed | integer | Optional | Reproducibility, where supported. |
| n | integer | Optional | Clips to generate, 1–4. Not every provider returns more than one. Default 1 |
| sync | boolean | Optional | Wait for the clip on the request instead of returning a job. Only for short clips — long generations will hit the request timeout. Default false |
| user | string | Optional | A stable id for your own end user. |
Submitting a job
import os, requests headers = { "Authorization": f"Bearer {os.environ['OXYY_API_KEY']}", "Content-Type": "application/json", } # Video is asynchronous: this returns 202 with a job, not a video. response = requests.post( "https://api.oxyy.ai/v1/videos/generations", headers=headers, json={ "model": "veo-3", "prompt": "A serene lake with mountains, slow dolly in", "duration": 8, "resolution": "1080p", "aspect_ratio": "16:9", }, ) job = response.json() print(response.status_code, job["id"], job["status"])
curl https://api.oxyy.ai/v1/videos/generations \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "veo-3", "prompt": "A serene lake with mountains", "duration": 8, "resolution": "1080p" }'
HTTP/1.1 202 Accepted
{
"id": "gen_01JQXY8M3K2T4V6W8Z",
"object": "generation.job",
"status": "queued",
"type": "video",
"model": "veo-3",
"created_at": 1700000000,
"expires_at": 1700086400,
"_links": {
"self": "/v1/videos/generations/gen_01JQXY8M3K2T4V6W8Z",
"cancel": "/v1/videos/generations/gen_01JQXY8M3K2T4V6W8Z"
}
}Polling for the result
Poll every five seconds or so. status moves through queued → processing → completed, with progress as a percentage; the terminal failures are failed and cancelled.
import os, time, requests headers = { "Authorization": f"Bearer {os.environ['OXYY_API_KEY']}" } job_id = "gen_..." # the `id` the 202 returned while True: job = requests.get( f"https://api.oxyy.ai/v1/videos/generations/{job_id}", headers=headers, ).json() if job["status"] == "completed": # The video lives under result.data[0].url print("Video:", job["result"]["data"][0]["url"]) print("Charged:", job["usage"]["credits_consumed"]) break if job["status"] in ("failed", "cancelled"): print("Failed:", job.get("error")) break print(f"{job['status']} — {job['progress']}%") time.sleep(5)
const headers = { Authorization: `Bearer ${process.env.OXYY_API_KEY}` }; const jobId = 'gen_...'; while (true) { const job = await fetch( `https://api.oxyy.ai/v1/videos/generations/${jobId}`, { headers } ).then((r) => r.json()); if (job.status === 'completed') { console.log('Video:', job.result.data[0].url); break; } if (job.status === 'failed' || job.status === 'cancelled') { console.error('Failed:', job.error); break; } console.log(`${job.status} — ${job.progress}%`); await new Promise((r) => setTimeout(r, 5000)); }
# 1. Submit — the 202 body carries the job id. curl -X POST https://api.oxyy.ai/v1/videos/generations \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{"model": "veo-3", "prompt": "A serene lake", "duration": 8}' # 2. Poll until status is completed, failed or cancelled. curl https://api.oxyy.ai/v1/videos/generations/gen_abc123 \ -H "Authorization: Bearer $OXYY_API_KEY" # 3. Cancel a job you no longer want. curl -X DELETE https://api.oxyy.ai/v1/videos/generations/gen_abc123 \ -H "Authorization: Bearer $OXYY_API_KEY"
Completed job
{
"id": "gen_01JQXY8M3K2T4V6W8Z",
"object": "generation.job",
"status": "completed",
"type": "video",
"model": "veo-3",
"progress": 100,
"created_at": 1700000000,
"started_at": 1700000004,
"completed_at": 1700000098,
"expires_at": 1700086400,
"result": {
"created": 1700000098,
"data": [
{
"url": "https://api.oxyy.ai/storage/videos/2026/01/abc123.mp4",
"duration": 8,
"resolution": "1080p"
}
]
},
"usage": { "credits_consumed": 3.2 }
}Failed job
{
"id": "gen_01JQXY8M3K2T4V6W8Z",
"object": "generation.job",
"status": "failed",
"type": "video",
"model": "veo-3",
"progress": 30,
"created_at": 1700000000,
"error": {
"message": "The video was filtered by the provider's safety system",
"code": "CONTENT_FILTER"
}
}Managing jobs
A job is yours alone — another account's id answers 404, not 403. Jobs expire at expires_at; download the file before then or re-host it yourself.
Available models
Text to speech
ElevenLabs, OpenAI TTS and Gemini TTS, through the OpenAI speech endpoint. Music models are served here too.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| model | string | Required | A TTS model id, e.g. tts-1-hd, eleven_v3, gemini-2.5-flash-tts. |
| input | string | Required | The text to speak. Up to 4,096 characters. |
| voice | string | Required | On OpenAI models, a voice name (alloy, echo, fable, onyx, nova, shimmer). On ElevenLabs, a voice name from your library or a 20-character voice id — both are resolved for you. |
| response_format | string | Optional | The audio container. One of:mp3opusaacflacwavpcm Default mp3 |
| speed | number | Optional | Playback rate, 0.25–4. Default 1 |
| instructions | string | Optional | Delivery direction for the models that accept it (e.g. gpt-4o-mini-tts). |
| stream_format | string | Optional | One of:sseaudio |
| async | boolean | Optional | Return a job instead of waiting — useful for long input. Poll GET /v1/audio/speech/{id}. Default false |
| user | string | Optional | A stable id for your own end user. |
Code examples
import os from pathlib import Path from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) with client.audio.speech.with_streaming_response.create( model="tts-1-hd", voice="alloy", input="Hello! This is a text-to-speech test.", response_format="mp3", speed=1.0, ) as response: response.stream_to_file(Path("output.mp3"))
# The body is raw audio bytes — write it straight to a file. curl https://api.oxyy.ai/v1/audio/speech \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "eleven_v3", "voice": "Rachel", "input": "Hello from Oxyy.", "response_format": "mp3" }' \ --output speech.mp3
Response
// The response body is raw audio bytes, not JSON. // // HTTP/1.1 200 OK // Content-Type: audio/mpeg // Content-Length: 45321 // X-Request-Id: 8f2c... // // Write it straight to a file or pipe it to a player. // response_format picks the container: mp3, opus, aac, flac, wav, pcm.
Available models
Speech to text
Transcribe audio with timestamps, language detection and speaker diarization, in several output formats.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| model | string | Required | An STT model id, e.g. whisper-1, gpt-4o-transcribe, scribe_v1. |
| file | file | Required* | The audio, as multipart/form-data. Containers: mp3, mp4, mpeg, mpga, m4a, wav, webm, ogg, flac. |
| file_url | string | Required* | A public URL to fetch instead of uploading. Up to 25 MB; private and loopback addresses are refused. |
| file_base64 | string | Required* | Base64 audio, with or without a data: prefix. |
| response_format | string | Optional | One of:jsontextverbose_jsonsrtvttdiarized_json Default json |
| language | string | Optional | ISO-639-1 code (en, ja, bn). Omit to auto-detect. |
| prompt | string | Optional | Vocabulary or style hint — names, jargon, expected spelling. |
| temperature | number | Optional | Sampling temperature, 0–1. |
| timestamp_granularities | array | Optional | Requires verbose_json. One of:wordsegment |
| diarize | boolean | Optional | Label speakers, on the models that support it. |
| num_speakers | integer | Optional | A hint for diarization. |
file, file_url or file_base64 is required. /v1/audio/translations takes the same parameters and returns English text.Code examples
import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) # Supported containers: mp3, mp4, mpeg, mpga, m4a, wav, webm, ogg, flac with open("interview.mp3", "rb") as audio_file: response = client.audio.transcriptions.create( model="whisper-1", file=audio_file, response_format="verbose_json", timestamp_granularities=["segment"], language="en", ) print(response.text) for seg in response.segments: print(f"{seg.start:.1f}s-{seg.end:.1f}s: {seg.text}")
import fs from 'fs'; import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai/v1' }); const response = await client.audio.transcriptions.create({ model: 'whisper-1', file: fs.createReadStream('interview.mp3'), response_format: 'verbose_json', language: 'en', }); console.log(response.text);
# multipart upload curl https://api.oxyy.ai/v1/audio/transcriptions \ -H "Authorization: Bearer $OXYY_API_KEY" \ -F "model=whisper-1" \ -F "file=@interview.mp3" \ -F "response_format=verbose_json" \ -F "language=en" # …or JSON with a hosted URL instead of an upload curl https://api.oxyy.ai/v1/audio/transcriptions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{"model": "whisper-1", "file_url": "https://example.com/interview.mp3"}'
# Same inputs as /transcriptions; the transcript comes back in English. curl https://api.oxyy.ai/v1/audio/translations \ -H "Authorization: Bearer $OXYY_API_KEY" \ -F "model=whisper-1" \ -F "file=@bengali-interview.mp3" \ -F "response_format=json"
Example response (verbose_json)
{
"task": "transcribe",
"language": "en",
"duration": 5.42,
"text": "Hello, this is a transcription test.",
"segments": [
{ "id": 0, "start": 0.0, "end": 2.1, "text": "Hello, this is" },
{ "id": 1, "start": 2.1, "end": 5.42, "text": "a transcription test." }
]
}verbose_json is requested from the provider as sent. The one exception is the text-shaped formats: text, srt and vtt are rendered here from a timed result, so they work on every provider.
Available models
Embeddings
Vectors for semantic search, clustering, recommendations and RAG.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| model | string | Required | An embedding model id, e.g. text-embedding-3-large, gemini-embedding-001. |
| input | string|array | Required | One string, or a batch of up to 2,048. The batch is capped at 2,000,000 characters in total. |
| encoding_format | string | Optional | One of:floatbase64 Default float |
| dimensions | integer | Optional | Truncate the vector, on models that support shortening. |
| user | string | Optional | A stable id for your own end user. |
Code examples
import os from openai import OpenAI client = OpenAI( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai/v1" ) # `input` takes one string or a batch of up to 2048. response = client.embeddings.create( model="text-embedding-3-large", input=["The quick brown fox", "jumps over the lazy dog"], encoding_format="float", dimensions=1024, ) for item in response.data: print(item.index, len(item.embedding))
curl https://api.oxyy.ai/v1/embeddings \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "text-embedding-3-large", "input": "The quick brown fox", "encoding_format": "float" }'
Example response
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.0023, -0.0091, 0.0156, -0.0042, "..."]
}
],
"model": "text-embedding-3-large",
"usage": { "prompt_tokens": 8, "total_tokens": 8, "cost": 0.0000011 }
}Available models
File inputs & uploads
Image editing, image-to-video and transcription all take a file. Every one of them accepts the same three methods, so you can pick whichever fits your architecture.
| Method | Content-Type | Field | Best for |
|---|---|---|---|
| Upload | multipart/form-data | image / file | A file you already have on disk. The official SDKs do this for you. |
| URL | application/json | image / file_url | A file you already host, or one uploaded to the asset store. |
| Base64 | application/json | image_base64 / file_base64 | Bytes you hold in memory, with no hosting step. |
images_base64 for base64, or images for data URLs, and a multipart request may carry up to ten files. The first is the primary reference; the rest are additional context for models that blend them.The asset store
Upload once, reference many times. Useful when the same image feeds several requests, or when you would rather not resend megabytes of base64 on every call.
# 1. Upload once — the URL is reusable for 12 hours. curl https://api.oxyy.ai/v1/assets/upload \ -H "Authorization: Bearer $OXYY_API_KEY" \ -F "file=@source.png" # => {"asset_id":"...","url":"https://.../source.png","expires_at":...} # 2. Reference that URL from any media request. curl https://api.oxyy.ai/v1/images/edits \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "flux.1-kontext", "image": "https://api.oxyy.ai/storage/images/source.png", "prompt": "Change the background to a beach sunset" }'
import os, requests headers = { "Authorization": f"Bearer {os.environ['OXYY_API_KEY']}" } with open("source.png", "rb") as f: asset = requests.post( "https://api.oxyy.ai/v1/assets/upload", headers=headers, files={"file": ("source.png", f, "image/png")}, ).json() print(asset["asset_id"], asset["url"], asset["expires_at"])
{
"asset_id": "a1b2c3d4e5f6",
"url": "https://api.oxyy.ai/storage/images/a1b2c3d4e5f6.png",
"expires_at": 1700043200,
"size": 184320,
"mime_type": "image/png"
}Limits & accepted types
| Category | Maximum per file | Accepted types |
|---|---|---|
| Image | 20 MB | png, jpeg, gif, webp |
| Audio | 50 MB | mp3, wav, ogg, flac, m4a, mp4 |
| Video | 100 MB | mp4, webm, mov, avi |
| Document | 100 MB | PDF and other documents, as a chat content part |
A multipart request may carry up to 10 files and 100 MB per file overall; the per-category caps above are what each endpoint enforces, and an operator can set a lower cap on an individual model. A file_url on transcription is fetched with a 25 MB ceiling. Uploads are stored under a type derived from the content type, never from the filename you send — so scriptable formats such as SVG and HTML are refused outright.
Image editing
| Parameter | Type | Required | Description |
|---|---|---|---|
| model | string | Required | An image model that supports editing, e.g. flux.1-kontext. |
| prompt | string | Required | The edit to make. |
| image | file|string | Required* | The source image — an upload, a URL or a data URL. |
| image_base64 | string|array | Required* | Base64 source image(s). |
| images_base64 | array | Optional | Several base64 references, in order. |
| mask | file | Optional | A mask marking the region to change, on models that take one. |
| n | integer | Optional | Default 1 |
| size | string | Optional | As on image generation. |
| response_format | string | Optional | One of:urlb64_json |
image, image_base64 or a multipart upload is required.# Image edit — multipart upload straight from disk. import os, requests headers = { "Authorization": f"Bearer {os.environ['OXYY_API_KEY']}" } response = requests.post( "https://api.oxyy.ai/v1/images/edits", headers=headers, files={"image": open("source.png", "rb")}, data={ "model": "flux.1-kontext", "prompt": "Change the background to a beach sunset", "n": "1", "size": "1024x1024", }, ) print(response.json()["data"][0]["url"])
import fs from 'fs'; import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai/v1' }); const response = await client.images.edit({ model: 'flux.1-kontext', image: fs.createReadStream('source.png'), prompt: 'Change the background to a beach sunset', n: 1, size: '1024x1024', }); console.log(response.data[0].url);
curl https://api.oxyy.ai/v1/images/edits \ -H "Authorization: Bearer $OXYY_API_KEY" \ -F "model=flux.1-kontext" \ -F "image=@source.png" \ -F "prompt=Change the background to a beach sunset" \ -F "n=1" \ -F "size=1024x1024"
# Base64 — no upload step, no hosting. Note the two spellings: # `image_base64` for one reference, `images_base64` for several. curl https://api.oxyy.ai/v1/images/edits \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -d '{ "model": "flux.1-kontext", "prompt": "Blend these two scenes", "images_base64": ["iVBORw0KGgo...", "data:image/png;base64,iVBORw0..."] }'
{
"created": 1700000000,
"data": [
{ "url": "https://api.oxyy.ai/storage/images/2026/01/edited_abc123.png" }
],
"usage": { "input_tokens": 9, "output_tokens": 1290, "cost": 0.04 }
}Image to video
Same parameters as video generation, with the image required rather than optional. It is a job like every other video request — poll it the same way. /v1/videos/generations also accepts an image and behaves identically.
# Image-to-video — the starting frame plus a motion prompt. import os, requests headers = { "Authorization": f"Bearer {os.environ['OXYY_API_KEY']}" } response = requests.post( "https://api.oxyy.ai/v1/videos/image-to-video", headers=headers, files={"image": open("photo.jpg", "rb")}, data={ "model": "veo-3", "prompt": "Slowly pan across the scene", "duration": "8", "resolution": "1080p", }, ) job = response.json() print(job["id"], job["status"])
curl https://api.oxyy.ai/v1/videos/image-to-video \ -H "Authorization: Bearer $OXYY_API_KEY" \ -F "model=veo-3" \ -F "image=@photo.jpg" \ -F "prompt=Slowly pan across the scene" \ -F "duration=8" \ -F "resolution=1080p"
Audio transcription
See Speech to text for the full parameter list. All three input methods apply.
Anthropic SDK
Claude models can be called with Anthropic's official SDK through the Messages API. Set the base URL to https://api.oxyy.ai without /v1 — the SDK adds /v1/messages itself.
Authenticate with your Oxyy API key in the x-api-key header, which is what the SDK's api_key option sends, or as Authorization: Bearer. As on Anthropic's own API, max_tokens is required.
/v1/messages is refused with a 400, in Anthropic's own error shape so your SDK raises the exception it expects. Use the OpenAI SDK for every other model.Code examples
# pip install anthropic import anthropic, os client = anthropic.Anthropic( api_key=os.environ["OXYY_API_KEY"], base_url="https://api.oxyy.ai", # no /v1: the SDK adds it ) message = client.messages.create( model="claude-sonnet-4.5", max_tokens=1024, system="Be concise.", messages=[{"role": "user", "content": "Hello!"}], ) print(message.content[0].text)
// npm install @anthropic-ai/sdk import Anthropic from '@anthropic-ai/sdk'; const client = new Anthropic({ apiKey: process.env.OXYY_API_KEY, baseURL: 'https://api.oxyy.ai', // no /v1: the SDK adds it }); const message = await client.messages.create({ model: 'claude-sonnet-4.5', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello!' }], }); console.log(message.content[0].text);
curl https://api.oxyy.ai/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: $OXYY_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4.5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello!"}] }'
# cache_control survives translation, per block — a long system # prompt is written once and read at the cache rate afterwards. message = client.messages.create( model="claude-sonnet-4.5", max_tokens=1024, system=[{ "type": "text", "text": long_style_guide, "cache_control": {"type": "ephemeral"}, }], thinking={"type": "enabled", "budget_tokens": 2048}, messages=[{"role": "user", "content": "Review this draft."}], ) print(message.usage.cache_read_input_tokens)
Supported features
| Feature | Status | Notes |
|---|---|---|
system as a string or a block array | Supported | Per-block cache_control is preserved, so a long system prompt is cached rather than re-billed |
max_tokens | Supported | Required, exactly as on Anthropic's own API |
temperature, top_p, top_k, stop_sequences | Supported | |
stream | Supported | Native Anthropic events: message_start, content_block_delta, message_delta, message_stop |
thinking | Supported | Forwarded verbatim — enabled, adaptive and disabled all reach the model |
tools, tool_choice | Supported | auto, any, none and {type:"tool", name}. disable_parallel_tool_use is honoured. |
Server tools (web_search, code_execution, …) | Supported | Passed to the provider untouched; whether one runs depends on the upstream serving that model |
output_config.format | Supported | A json_schema format becomes a structured output; other formats are reported as dropped |
metadata, service_tier, betas / anthropic_beta | Supported | betas becomes the anthropic-beta header |
container, mcp_servers, context_management, inference_geo | Supported | Forwarded as sent |
POST /v1/messages/count_tokens | Supported | Answered from this gateway's own tokenizer; it does not spend your rate limit |
Legacy Text Completions (prompt, max_tokens_to_sample) | Not supported | Retired by Anthropic. Refused with a 400 that names the Messages shape to send instead. |
| Non-Claude models | Not supported | Refused with a 400. Use the OpenAI SDK for every other model. |
| Undocumented top-level fields | Not supported | Dropped rather than forwarded, so a validating upstream cannot 400 on a field it has never seen |
Google GenAI SDK
Gemini models can be called with Google's google-genai (Python) and @google/genai (JavaScript) SDKs. Set the base URL in the SDK's HTTP options (http_options in Python, httpOptions in JavaScript) to https://api.oxyy.ai; the SDK adds /v1beta itself.
The SDK's api_key is sent as the x-goog-api-key header; a ?key=query parameter or Authorization: Bearer works too.
Code examples
# pip install google-genai import os from google import genai from google.genai import types client = genai.Client( api_key=os.environ["OXYY_API_KEY"], http_options=types.HttpOptions(base_url="https://api.oxyy.ai"), ) response = client.models.generate_content( model="gemini-2.5-flash", contents="Hello!", config=types.GenerateContentConfig( system_instruction="Be concise.", temperature=0.7, max_output_tokens=1024, ), ) print(response.text)
// npm install @google/genai import { GoogleGenAI } from '@google/genai'; const ai = new GoogleGenAI({ apiKey: process.env.OXYY_API_KEY, httpOptions: { baseUrl: 'https://api.oxyy.ai' } }); const response = await ai.models.generateContent({ model: 'gemini-2.5-flash', contents: 'Hello!' }); console.log(response.text);
curl https://api.oxyy.ai/v1beta/models/gemini-2.5-flash:generateContent \ -H "Content-Type: application/json" \ -H "x-goog-api-key: $OXYY_API_KEY" \ -d '{"contents": [{"parts": [{"text": "Hello!"}]}]}'
Supported features
| Feature | Status | Notes |
|---|---|---|
:generateContent, :streamGenerateContent | Supported | Streaming is what ?alt=sse and the SDK's generate_content_stream use |
:countTokens | Supported | Answered from this gateway's tokenizer; exempt from your rate limit |
GET /v1beta/models | Supported | Lists only the models offered over this SDK. Requires your API key. |
systemInstruction | Supported | |
generationConfig | Supported | temperature, topP, topK, maxOutputTokens, stopSequences, responseMimeType, responseSchema, thinkingConfig |
tools / functionDeclarations | Supported | Both parameters and parametersJsonSchema, in camelCase or snake_case — the Python SDK sends snake_case |
safetySettings | Supported | Forwarded to the provider |
| Inline and file data parts | Supported | inlineData (base64) and fileData for images, audio, video and documents |
| Non-Gemini models | Not supported | Refused with a 400 in Google's own error shape |
| Imagen and Veo through this SDK | Not supported | Use /v1/images/generations and /v1/videos/generations |
SDKs & libraries
Oxyy speaks the OpenAI wire format, so the official OpenAI SDK in any language works unchanged. There is no Oxyy SDK to install.
| Language | Package | Install |
|---|---|---|
| Python | openai | pip install openai |
| JavaScript / TypeScript | openai | npm install openai |
| PHP | openai-php/client | composer require openai-php/client |
| Go | sashabaranov/go-openai | go get github.com/sashabaranov/go-openai |
| Ruby | ruby-openai | gem install ruby-openai |
| Java / Kotlin | com.openai:openai-java | Maven / Gradle |
| C# | OpenAI | dotnet add package OpenAI |
Frameworks built on the OpenAI client — LangChain, LlamaIndex, the Vercel AI SDK, PydanticAI, Instructor — work by setting the same base URL in their OpenAI provider.
Vendor SDKs
Claude and Gemini can also be called with their vendor's own SDK: see Anthropic SDK and Google GenAI SDK. A vendor SDK serves only that vendor's models; the OpenAI SDK reaches every model.
/messages, /responses) and one where the prefix was doubled (/v1/v1/messages, /v1beta/v1beta/…) reach the same pipeline with the same billing.Grok (xAI) and Llama (Meta)
xai-sdk package speaks gRPC, which this REST API does not serve. Call Grok models with the OpenAI SDK and base_url set to https://api.oxyy.ai/v1.Meta Llama models are also called through the OpenAI SDK, with chat completions or the Responses API. There is no separate Llama SDK surface.
Differences from other gateways
| Field | Oxyy |
|---|---|
provider (root) | Not returned. The upstream that served a request is intentionally not disclosed. |
usage.cost | The amount charged for the request, in USD — on every response, streaming included. |
usage.is_byok | Always false: every request is served on Oxyy's own provider keys. |
models / route / provider (request) | Not accepted. Fallback and provider choice are handled for you; send one model. |
transforms, plugins | Not accepted. Nothing is injected into or rewritten in your prompt. |
Rate limits
Limits are set by your tier and apply per API key. Four things are counted independently: requests per minute, tokens per minute, per-modality daily caps, and how many requests you may have in flight at once. Exceeding any of them is a 429.
HTTP/1.1 200 OK X-RateLimit-Limit: 600 X-RateLimit-Remaining: 587 X-RateLimit-Reset: 1700000060 X-RateLimit-Limit-Tokens: 2000000 X-RateLimit-Remaining-Tokens: 1984210 X-Request-Id: 9c1f4e7a-2b8d-4e31-9a0c-5f7b1d2e3a44 // On a 429 the same headers are present, plus: Retry-After: 43
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests per minute allowed on your tier |
X-RateLimit-Remaining | Requests left in the current minute |
X-RateLimit-Reset | Unix seconds at which the request window resets |
X-RateLimit-Limit-Tokens | Tokens per minute allowed, when your tier caps them |
X-RateLimit-Remaining-Tokens | Tokens left in the current window |
Retry-After | Seconds to wait. Sent on every 429; honour it rather than retrying at once. |
X-Request-Id | The id this request is recorded under. Quote it in any support request. |
429 carries Retry-After in seconds. The official SDKs already honour it; if you call the API directly, wait that long rather than retrying immediately — a tight retry loop makes the limit last longer. Your current limits for a given model are in GET /v1/models/{model}.Idempotency
A network timeout does not tell you whether the work happened. Send an Idempotency-Key and a retry returns the original answer instead of generating — and charging for — a second one.
# Same key + same body within 24h => the stored reply, charged once. # The replay carries `idempotent-replay: true`. curl https://api.oxyy.ai/v1/images/generations \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OXYY_API_KEY" \ -H "Idempotency-Key: order-4417-thumbnail" \ -d '{"model": "dall-e-3", "prompt": "A red bicycle"}'
response = client.images.generate( model="dall-e-3", prompt="A red bicycle", extra_headers={"Idempotency-Key": "order-4417-thumbnail"}, )
| Rule | Detail |
|---|---|
| Where it applies | Every billable POST: chat completions, responses, messages, images, audio, video and embeddings. |
| How long | A finished reply is replayable for 24 hours. |
| Same key, same body | The stored reply is returned with idempotent-replay: true, and you are charged once. |
| Same key, different body | Refused with 409. Use a new key. |
| Still in flight | Refused with 409 while the first attempt is running. Retry shortly. |
| What is never stored | Authentication failures, rate-limit refusals and server errors — so a retry after one of those really does retry. |
| Streaming | Not covered: a stream cannot be replayed, so the header is ignored on stream: true. |
| Scope | Keys are scoped to the credential that sent them, so two customers cannot collide on the same key. |
Errors
Errors use standard HTTP status codes and the OpenAI error envelope, so the SDKs raise the exception classes you already handle. error.param names the offending field when there is one.
{
"error": {
"message": "Invalid API key provided",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}| Status | Type | Meaning | What to do |
|---|---|---|---|
400 | invalid_request_error | A parameter is missing, malformed or outside its range. error.param names it. | Fix the request. Retrying will not help. |
401 | authentication_error | Missing, malformed, disabled or unknown API key. | Check the Authorization header and the key's status. |
402 | insufficient_quota | The account has no credit left for this request. | Top up. Do not retry — the balance will not change on its own. |
403 | permission_error | The key or tier may not use this model, endpoint or SDK. | Use a model your tier allows, or a key with the right scope. |
404 | not_found_error | No such model, job or asset. | Check the id against GET /v1/models. |
408 | timeout_error | The generation exceeded the budget for this request. | Retry, or ask for a shorter output. |
409 | invalid_request_error | An Idempotency-Key was reused with a different body, or a job is already in flight. | Use a new key, or resend the identical body. |
413 | invalid_request_error | The upload or request body is over the limit. | See file size limits. |
422 | invalid_request_error | The request is well formed but the values cannot be served together. | Read the message; it names the conflict. |
429 | rate_limit_error | Requests per minute, tokens per minute, a daily cap, or too many concurrent requests. | Back off for Retry-After seconds. See rate limits. |
500 | server_error | An unexpected fault in the gateway. | Retry with backoff. Quote X-Request-Id if it persists. |
501 | server_error | The provider adapter does not implement this capability for this model. | Use a different model for that modality. |
502 | provider_error | The upstream provider returned something unusable. | Retry; routing will usually pick a different provider. |
503 | service_unavailable | No provider can serve this model right now, or the gateway is shedding load. | Retry with backoff, or use another model. |
504 | timeout_error | The upstream did not answer in time. | Retry. |
Handling them
import openai try: response = client.chat.completions.create(model="gpt-5", messages=msgs) except openai.RateLimitError as e: # 429 — honour Retry-After rather than retrying immediately. wait = int(e.response.headers.get("retry-after", 60)) except openai.APIStatusError as e: if e.status_code == 402: # Out of credits — top up, do not retry. ... print(e.status_code, e.body["error"]["code"])
X-Request-Id. Quote it and we can find the exact request — without it, we cannot.Models and providers you can call
Every model below works with the quickstart above — same endpoint, same key. Open one for its exact id, pricing and limits.
AI providers
Popular models
- chat-latest
- Claude Fable 5
- Claude Fable 5.1
- Claude Haiku 4.5
- Claude Opus 4.5
- Claude Opus 4.6
- Claude Opus 4.7
- Claude Opus 4.8
- Claude Opus 5
- Claude Sonnet 4.5
- Claude Sonnet 4.6
- Claude Sonnet 5
- DeepSeek-V4.1-Flash
- DeepSeek-V4-Pro-0813
- Gemini 2.5 Flash
- Gemini 2.5 Flash-Lite
- Gemini 2.5 Flash TTS
- Gemini 2.5 Pro
- Gemini 2.5 Pro TTS
- Nano Banana 2
- Gemini 3.1 Flash-Lite
- Google: Nano Banana 2 Lite (Gemini 3.1 Flash Lite Image)
- Gemini 3.1 Flash TTS
- Gemini 3.1 Pro
