Migrate from OpenAI, OpenRouter or Azure OpenAI
Move existing code to uttapen by changing base_url and key — model id format, supported endpoints, fields that pass through, and common mistakes.
Updated: September 7, 2026
uttapen implements the same chat/completions contract the official OpenAI SDKs speak. If your code already works against OpenAI, OpenRouter, Azure OpenAI or any other compatible gateway, migrating means two edits: the base URL and the key. Model ids take the provider/model shape. The rest of this page covers those details and a few small behavioural differences.
Change base_url
# before
client = OpenAI(api_key="sk-...")
# after
client = OpenAI(
base_url="https://api.uttapen.ir/v1",
api_key="sk-up-...",
)
In Node the field is baseURL; in PHP it's withBaseUri(); in Go it's option.WithBaseURL(). If your SDK reads the environment, set OPENAI_BASE_URL=https://api.uttapen.ir/v1 and OPENAI_API_KEY=sk-up-... and leave the code untouched. The /v1 segment is part of the address — without it you get 404.
Model ids
Ids look like provider/model, the same convention OpenRouter uses:
| Before (OpenAI) | After (uttapen) |
|---|---|
gpt-5 | openai/gpt-5 |
gpt-5-mini | openai/gpt-5-mini |
o3 | openai/o3 |
| — | anthropic/claude-sonnet-4.5 |
| — | google/gemini-2.5-flash |
| — | deepseek/deepseek-chat |
The full list with toman prices is on the models page or from GET /v1/models, which works without a key. Coming from OpenRouter, the ids are identical — the :free, :batch and :online suffixes are supported too. An id without a provider (gpt-5) is rejected with 404 model_not_found.
What's supported
| Endpoint | Status |
|---|---|
POST /v1/chat/completions | Full: streaming and non-streaming, tools, vision, files, response_format, reasoning |
POST /v1/embeddings | Full |
POST /v1/completions | Legacy, for models that still support it |
GET /v1/models, GET /v1/models/{id} | Full, plus an extra uttapen_pricing block |
POST /v1/responses | 404 for now, with a message pointing you at chat/completions |
/v1/files, /v1/fine_tuning, /v1/assistants, /v1/batches, /v1/audio, /v1/images | Not available; 404 not_found |
The Responses API will land once the shape of its events is stable enough to meter cost reliably. Until then, everything you would use it for — tools, images, JSON, reasoning — is available through chat/completions; only the request body differs.
Fields that pass through
Your request body reaches the provider all but untouched, including OpenRouter-specific fields:
provider— prefer or pin a specific provider for a model (for example{"order": ["Anthropic"], "allow_fallbacks": false}in the body). Useful if you care about latency or region.models— a fallback list. If the first model doesn't answer, the next one is tried. The hold is sized using the most expensive model in the list, but you are charged for the model that actually answered.reasoning,plugins,web_search_options,transforms— passed through and taken into account when sizing the hold.
We add two fields of our own: usage: {include: true}, so that real usage comes back, and stream: true towards the upstream even when you didn't ask for a stream. In the second case the gateway collects the chunks and hands you one standard JSON response, so nothing changes from the outside.
Behavioural differences
- Numeric fields must be JSON integers.
max_tokens,max_completion_tokens,max_output_tokens,nandbest_ofare rejected with400 invalid_requestif you send"100"(a string),100.0or1e2. The reason is that the hold must not be side-stepped through a notation the provider accepts but we don't parse. 402happens before the request goes out. If your available balance is below what this request needs to hold, it never reaches the provider. A smallermax_tokensmeans a smaller hold (pricing).- The body limit is 20 MB; anything larger returns
413. - Provider error messages come back verbatim with the original status, with upstream-identifying strings stripped out.
- No content is stored. Prompts and responses on the API path are never written down (privacy).
- Added latency compared with calling a provider directly is around 100 ms (Iran → relay → provider).
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
404 not_found on every request | base_url missing /v1, or including the full /v1/chat/completions | Just https://api.uttapen.ir/v1 |
404 model_not_found with a valid model | Id without a provider, or with uppercase letters | openai/gpt-5-mini, all lowercase |
401 after days of working fine | Key expired or revoked, or an old OpenAI key left in the environment | Check UTTAPEN_API_KEY; a stray OPENAI_API_KEY doesn't take priority but it does confuse people |
400 invalid_request on max_tokens | A string or a decimal value | Send an integer |
402 on a small request | max_tokens not set, so the default 4096 output tokens were held for an expensive model | Set a realistic max_tokens |
| The stream stalls in your client | An intermediate proxy (nginx, Cloudflare) is buffering | proxy_buffering off, or the X-Accel-Buffering: no header on your own proxy |
client.responses.create fails | The Responses API isn't enabled yet | Use chat.completions.create |
| PHP SDK output breaks while streaming | The X-Uttapen-Include-Meta header used with createStreamed | Send that header only from a raw HTTP client (PHP) |
Coming from OpenRouter
If you were calling OpenRouter directly, only the address and the key change. HTTP-Referer and X-Title aren't needed — we send those ourselves. Responses keep the extra provider field. The meaningful difference: settlement happens in toman from your uttapen wallet, and no request travels from an Iranian IP straight to OpenRouter.
Coming from Azure OpenAI
Replace the AzureOpenAI client with the plain OpenAI one. api_version and azure_endpoint have no meaning here, and the deployment name gives way to a provider/model id.