Python SDK
Official openai Python package with uttapen: install, client, chat, streaming with uttapen.meta, tool calling, embeddings, async and error handling.
Updated: 23 Sep 2026
The official openai package (version 1 or newer) works with uttapen without a single patch or wrapper. Every example on this page has been run against the gateway and verified.
Install and client
pip install openai
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.uttapen.ir/v1",
api_key=os.environ["UTTAPEN_API_KEY"], # sk-up-...
)
If you would rather not touch your code at all, just set the OPENAI_BASE_URL and OPENAI_API_KEY environment variables; OpenAI() picks them up with no arguments. Useful constructor options: timeout (10 minutes by default), max_retries (2 by default; applied to 429 and 5xx), and default_headers if you want to send X-Uttapen-Include-Meta on every call.
Chat
resp = client.chat.completions.create(
model="openai/gpt-5-mini",
messages=[
{"role": "system", "content": "Answer briefly."},
{"role": "user", "content": "What is the difference between an index and a unique index in Postgres?"},
],
max_tokens=300,
temperature=0.3,
)
print(resp.choices[0].message.content)
print(resp.usage.prompt_tokens, resp.usage.completion_tokens)
To read the cost headers, grab the raw response:
raw = client.chat.completions.with_raw_response.create(
model="openai/gpt-5-mini",
messages=[{"role": "user", "content": "Hello"}],
)
print(raw.headers["x-uttapen-cost-toman"], raw.headers["x-uttapen-balance-toman"])
resp = raw.parse() # the same ChatCompletion object
Streaming
stream = client.chat.completions.create(
model="openai/gpt-5-mini",
messages=[{"role": "user", "content": "Write a Python function that validates an Iranian national ID."}],
stream=True,
extra_headers={"X-Uttapen-Include-Meta": "1"},
)
for chunk in stream:
if chunk.object == "uttapen.meta":
print(f"\n[cost: {chunk.model_extra['cost_toman']} toman, balance: {chunk.model_extra['balance_toman']}]")
continue
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
The uttapen.meta event carries no choices, so always check that the list is non-empty before you touch chunk.choices[0]. To cancel, call stream.close(), or use with client.chat.completions.create(...) as stream: so the connection closes when the block exits. Format details are in Streaming.
Tool calling
import json
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current temperature of a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
},
}]
def get_weather(city: str) -> dict:
return {"city": city, "temp_c": 31}
messages = [{"role": "user", "content": "What is the weather in Tehran?"}]
resp = client.chat.completions.create(model="openai/gpt-5-mini", messages=messages, tools=tools)
msg = resp.choices[0].message
messages.append(msg)
for call in msg.tool_calls or []:
result = get_weather(**json.loads(call.function.arguments))
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False)})
final = client.chat.completions.create(model="openai/gpt-5-mini", messages=messages, tools=tools)
print(final.choices[0].message.content)
The full loop, with several tools and with streaming, is in Tool calling.
Embeddings
/v1/embeddings is a passthrough route like the others and takes embeddings models only; any id from the embeddings category works. You pay for input tokens only, charged from actual usage. The dimensions parameter works on the openai/text-embedding-3-* models. One request takes at most 2048 inputs; split bigger batches.
emb = client.embeddings.create(
model="openai/text-embedding-3-small", # or any id from /models/category/embeddings
input=["lease agreement", "residential property rental contract", "ghormeh sabzi recipe"],
)
vectors = [d.embedding for d in emb.data]
print(len(vectors), len(vectors[0]), emb.usage.prompt_tokens)
input accepts either a single string or a list of strings; for a large batch, send the list so the whole thing is metered as one request.
Async
import asyncio
from openai import AsyncOpenAI
aclient = AsyncOpenAI(base_url="https://api.uttapen.ir/v1", api_key=os.environ["UTTAPEN_API_KEY"])
async def ask(q: str) -> str:
r = await aclient.chat.completions.create(model="openai/gpt-5-nano", messages=[{"role": "user", "content": q}], max_tokens=100)
return r.choices[0].message.content
async def main():
answers = await asyncio.gather(*(ask(q) for q in ["1+1?", "Capital of France?", "Color of the sky?"]))
print(answers)
asyncio.run(main())
gather over hundreds of requests will blow through your key's per-minute limit; cap concurrency with an asyncio.Semaphore (Rate limits).
Error handling
import time
import openai
try:
resp = client.chat.completions.create(model="openai/gpt-5-mini", messages=messages)
except openai.AuthenticationError:
raise SystemExit("uttapen API key is invalid or revoked")
except openai.NotFoundError as e:
print("model not found:", e.message)
except openai.RateLimitError as e:
time.sleep(int(e.response.headers.get("retry-after", "5")))
except openai.APIStatusError as e:
if e.status_code == 402:
info = e.body["error"].get("uttapen", {})
print("top-up needed:", info.get("required_toman"), "toman", info.get("topup_url"))
else:
print(e.status_code, e.code, e.message)
except openai.APIConnectionError:
print("could not connect; check your network")
e.code is our error.code, and e.response.headers["x-uttapen-request-id"] is the id to quote when you report a problem to us. The full table is in Errors.
Extra fields
Anything the SDK does not know about (reasoning, provider, models, …) goes through extra_body:
resp = client.chat.completions.create(
model="deepseek/deepseek-r1",
messages=messages,
extra_body={"reasoning": {"effort": "low"}, "models": ["openai/o3-mini"]},
)
On a server (FastAPI and Django)
Build the client once at module level and reuse it for every request; OpenAI() owns an httpx connection pool, and constructing one per request is both slow and a good way to leak half-open connections. In FastAPI use AsyncOpenAI so you never block the event loop; in Django the synchronous OpenAI is fine unless you are on ASGI. For requests that take minutes (reasoning models, long documents), hand the work to a worker (Celery, RQ, Dramatiq) and store the result against the request id — holding a user's HTTP connection open for two minutes is not a good idea.
# core/llm.py — one instance for the whole project
import httpx
from openai import OpenAI
client = OpenAI(
base_url="https://api.uttapen.ir/v1",
api_key=os.environ["UTTAPEN_API_KEY"],
timeout=httpx.Timeout(120.0, connect=10.0),
max_retries=1,
)
Write X-Uttapen-Request-Id into your own logs; it is available through with_raw_response, or from e.response.headers on errors. If your server sits behind an outbound proxy, pass http_client=httpx.Client(proxy="http://...") to the constructor.
Common mistakes
- A stale
OPENAI_API_KEYin the environment. If you do not passapi_keyexplicitly and an OpenAI key is present in the environment, the SDK will use it and you get a401. Always passapi_keyexplicitly fromUTTAPEN_API_KEY. - Double retrying. The SDK already retries
429and5xx. If you have your own retry layer, setmax_retries=0, otherwise one transient error turns into nine calls. - Counting tokens with tiktoken. For non-OpenAI models it is only an approximation; the real numbers are in the response's
usage. - Package version. You need
openai>=1.0; the 0.x line had a different contract. Pin the version inrequirements.txt. - Garbled non-ASCII output on Windows. If the console mangles the text, set
PYTHONIOENCODING=utf-8; this has nothing to do with the API.
Responses API
client.responses.create(...) currently returns 404. Use chat.completions — everything (tools, images, JSON, reasoning) is available there (migration).
Sign up with a phone number, top up your wallet from 100,000 toman and create a key. Free models work without a top-up — up to 50 requests a day. Create an API key →