uttapen

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-5openai/gpt-5
gpt-5-miniopenai/gpt-5-mini
o3openai/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

EndpointStatus
POST /v1/chat/completionsFull: streaming and non-streaming, tools, vision, files, response_format, reasoning
POST /v1/embeddingsFull
POST /v1/completionsLegacy, for models that still support it
GET /v1/models, GET /v1/models/{id}Full, plus an extra uttapen_pricing block
POST /v1/responses404 for now, with a message pointing you at chat/completions
/v1/files, /v1/fine_tuning, /v1/assistants, /v1/batches, /v1/audio, /v1/imagesNot 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, n and best_of are rejected with 400 invalid_request if you send "100" (a string), 100.0 or 1e2. The reason is that the hold must not be side-stepped through a notation the provider accepts but we don't parse.
  • 402 happens before the request goes out. If your available balance is below what this request needs to hold, it never reaches the provider. A smaller max_tokens means 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

SymptomCauseFix
404 not_found on every requestbase_url missing /v1, or including the full /v1/chat/completionsJust https://api.uttapen.ir/v1
404 model_not_found with a valid modelId without a provider, or with uppercase lettersopenai/gpt-5-mini, all lowercase
401 after days of working fineKey expired or revoked, or an old OpenAI key left in the environmentCheck UTTAPEN_API_KEY; a stray OPENAI_API_KEY doesn't take priority but it does confuse people
400 invalid_request on max_tokensA string or a decimal valueSend an integer
402 on a small requestmax_tokens not set, so the default 4096 output tokens were held for an expensive modelSet a realistic max_tokens
The stream stalls in your clientAn intermediate proxy (nginx, Cloudflare) is bufferingproxy_buffering off, or the X-Accel-Buffering: no header on your own proxy
client.responses.create failsThe Responses API isn't enabled yetUse chat.completions.create
PHP SDK output breaks while streamingThe X-Uttapen-Include-Meta header used with createStreamedSend 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.