uttapen

Python SDK

Official openai Python package with uttapen: install, client, chat, streaming with uttapen.meta, tool calling, embeddings, async and error handling.

Updated: September 7, 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

emb = client.embeddings.create(
    model="openai/text-embedding-3-small",
    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. Pick the embeddings model id from the models page.

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).