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_KEY in the environment. If you do not pass api_key explicitly and an OpenAI key is present in the environment, the SDK will use it and you get a 401. Always pass api_key explicitly from UTTAPEN_API_KEY.
  • Double retrying. The SDK already retries 429 and 5xx. If you have your own retry layer, set max_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 in requirements.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).

No API key yet?

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 →