OpenAI Proxy in Python: Set base_url and Key
To use an openai proxy python setup, pass a different base_url and api_key to the OpenAI client. Everything else in your code, from streaming to tools, stays the same.
Last updated

Summary
Two arguments change: base_url and api_key. The model becomes uncensored.
Environment variables let you switch endpoints without editing code.
A forwarder in FastAPI is only needed when you must hide the upstream key.
Handle HTTP 429 with the Retry-After header and set a sensible timeout.
The quickest version
Install the SDK with pip install openai, then create the client with your endpoint and key. This is the whole openai proxy python setup for most projects:
from openai import OpenAI
client = OpenAI(
base_url="https://api.proxyopenai.com/v1",
api_key="YOUR_KEY",
)
reply = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Explain what a base URL is in two sentences."},
],
)
print(reply.choices[0].message.content)
The endpoint is OpenAI-compatible, but it is not OpenAI. It serves one open-weight model from an independent provider, tuned to answer without content refusals for lawful adult use, under the model ID uncensored. Get a key on the API key page.
Why people put a proxy in front of the SDK
Most developers end up here for one of three reasons. They want to try a different model without rewriting the client. They want a lower price for text-only work. Or they want an endpoint that does not refuse lawful adult requests such as fiction and roleplay, where hosted assistants often decline. All three come down to the same edit: the base URL.
The SDK does not care what is behind that URL as long as the paths and JSON shapes match. That is what OpenAI-compatible means in practice. The paths you can use here are POST /v1/chat/completions, POST /v1/responses and GET /v1/models, and the JSON is the same you already send. Parameters such as temperature, top_p, stop, seed and the penalties are passed through to the model.
Switch endpoints with environment variables
The SDK reads OPENAI_API_KEY and OPENAI_BASE_URL when you create a client without arguments. That lets you move between environments without touching code.
export OPENAI_BASE_URL="https://api.proxyopenai.com/v1"
export OPENAI_API_KEY="YOUR_KEY"
python app.py
from openai import OpenAI
client = OpenAI() # picks up both variables; base URL is https://api.proxyopenai.com/v1
print([m.id for m in client.models.list().data])
The second snippet calls GET /v1/models, which makes a good smoke test. You should see uncensored in the list. If the call fails, the cause is nearly always the URL or the key, and the table of fixes further down covers both.
Async client, streaming and timeouts
Chat apps want tokens as they arrive. Streaming uses server-sent events and the SDK turns them into an iterator. The async client works the same way:
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI(
base_url="https://api.proxyopenai.com/v1",
api_key="YOUR_KEY",
timeout=120.0,
max_retries=3,
)
async def main():
stream = await client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": "Tell a two-paragraph story about a lighthouse."}],
stream=True,
stream_options={"include_usage": True},
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage:
print("\ntokens:", chunk.usage.prompt_tokens, chunk.usage.completion_tokens)
asyncio.run(main())
The final chunk carries usage, so you can compute the cost of each call. The SDK retries on temporary failures by itself; max_retries sets how many times. Keep the timeout generous for long completions, because the context window is 100,000 tokens and max_tokens is optional.
Tool calling through the same client
Function calling is supported, including streamed tool_calls. Results go back with role: "tool".
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.proxyopenai.com/v1", api_key="YOUR_KEY")
tools = [{
"type": "function",
"function": {
"name": "get_temperature",
"description": "Current temperature in a city, in Celsius",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "How warm is it in Lisbon?"}]
first = client.chat.completions.create(model="uncensored", messages=messages, tools=tools)
call = first.choices[0].message.tool_calls[0]
messages.append(first.choices[0].message)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps({"city": "Lisbon", "celsius": 21}),
})
final = client.chat.completions.create(model="uncensored", messages=messages, tools=tools)
print(final.choices[0].message.content)
The temperature here is hard-coded for the demo; in real code you would run your own function. Models do not always call a tool, so check that tool_calls is not empty before indexing into it. JSON mode is available too, via response_format={"type": "json_object"}.
Controlling parallel calls and retries yourself
The SDK retries a few times, but batch jobs need their own guard rails. A full key allows 8 concurrent requests, so a semaphore of 8 keeps you inside the limit; use 2 on a trial key. A streamed request holds its slot until the last chunk, so the semaphore must wrap the whole loop over chunks, including the call that opens it.
import asyncio
from openai import AsyncOpenAI, RateLimitError
client = AsyncOpenAI(base_url="https://api.proxyopenai.com/v1", api_key="YOUR_KEY", max_retries=0)
gate = asyncio.Semaphore(8)
async def ask(prompt: str) -> str:
async with gate:
for attempt in range(4):
try:
r = await client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": prompt}],
)
return r.choices[0].message.content
except RateLimitError as err:
wait = float(err.response.headers.get("retry-after", 5))
await asyncio.sleep(wait)
raise RuntimeError("still rate limited after 4 tries")
async def main():
prompts = [f"Write one tagline for product {i}." for i in range(20)]
results = await asyncio.gather(*(ask(p) for p in prompts))
print(len(results), "done")
asyncio.run(main())
Setting max_retries=0 here makes the loop the only place that retries, so you can see exactly how many times a call was repeated. In production, add jitter to the wait so many workers do not wake up in the same second.
Proxy in the other sense: an HTTP proxy
"Proxy" also means a network proxy that your company or hosting provider requires for outbound traffic. That is a separate setting. The SDK accepts a custom httpx client, so you can set one:
import httpx
from openai import OpenAI
client = OpenAI(
base_url="https://api.proxyopenai.com/v1",
api_key="YOUR_KEY",
http_client=httpx.Client(proxy="http://corp-proxy.internal:3128"),
)
Recent httpx versions take proxy=; older ones use proxies=, so check the httpx docs for your installed version. The two settings combine: the base URL says where the request goes, and the HTTP proxy says how it travels there.
A small FastAPI forwarder
If you want browsers or other services to call your own URL without ever seeing the real key, forward the request on the server. This version handles POST /v1/chat/completions, including streams:
import os
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
UPSTREAM = "https://api.proxyopenai.com/v1"
KEY = os.environ["UPSTREAM_KEY"]
app = FastAPI()
client = httpx.AsyncClient(timeout=300)
@app.post("/v1/chat/completions")
async def chat(request: Request):
body = await request.body()
req = client.build_request(
"POST", f"{UPSTREAM}/chat/completions", content=body,
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"},
)
r = await client.send(req, stream=True)
async def pipe():
try:
async for chunk in r.aiter_raw():
yield chunk
finally:
await r.aclose()
return StreamingResponse(pipe(), status_code=r.status_code,
media_type=r.headers.get("content-type"))
Run it with uvicorn main:app --port 8000 and point a client at http://localhost:8000/v1. Add your own authentication before you expose it. As written, anyone who can reach the port can spend your credit.
Reading cost and balance from Python
Every response reports token usage, and every response header carries your remaining credit in X-Balance-USD. Use the raw-response wrapper to get both in one call and turn them into a per-call cost at $0.15 per 1M input tokens and $0.50 per 1M output tokens.
from openai import OpenAI
client = OpenAI(base_url="https://api.proxyopenai.com/v1", api_key="YOUR_KEY")
raw = client.chat.completions.with_raw_response.create(
model="uncensored",
messages=[{"role": "user", "content": "Give me a one-line summary of what HTTP is."}],
)
reply = raw.parse()
u = reply.usage
cost = u.prompt_tokens * 0.15 / 1_000_000 + u.completion_tokens * 0.50 / 1_000_000
print(f"cost ${cost:.6f}, balance {raw.headers.get('X-Balance-USD')}")
A call with 1,200 tokens in and 400 out costs 1,200 x $0.15 / 1M = $0.00018 plus 400 x $0.50 / 1M = $0.0002, so $0.00038. Log that number next to X-Request-Id and you can trace any expensive request later.
A short pre-launch checklist
- The key comes from an environment variable or a secrets store, never from source code.
- Every client has an explicit timeout, and streams are closed in a
finallyblock. - Parallel work is capped at 8 tasks on a full key, 2 on a trial key.
- Your code checks for an empty
tool_callslist and for afinish_reasonoflength. - A low-balance alert fires before credit runs out.
Errors you will meet and how to handle them
| Symptom | Likely cause | Fix |
|---|---|---|
| Authentication error | Wrong or truncated key, or a key from another service | Copy it again from the key page |
| Not found | base_url missing /v1, or the full path pasted in | Use exactly https://api.proxyopenai.com/v1 |
429, code rate_limited | Over 300 requests a minute (60 on a trial key) | Slow down or queue calls |
429, code concurrency | More than 8 requests at once (2 on a trial key) | Cap parallel tasks with a semaphore |
429 plus Retry-After: 5 | Temporary overload | Wait five seconds, then retry |
A streamed request occupies its concurrency slot until the stream ends, so a hung stream blocks a slot. Set a timeout and close streams in a finally block. Each response includes X-Request-Id and X-Balance-USD; read them from client.chat.completions.with_raw_response.create(...) if you want to log them. Details on every option are in the docs, and the cost breakdown for uncensored proxies shows what typical calls cost at $0.15 and $0.50 per 1M tokens.
More guides
FAQ: billing and refunds
- Q: How do I top up my credit? A: You can top up using USDT on the TRC20 network or USDC on the Base network. Deposits range from $20 to $500. The bonus credit is applied immediately to your account.
- Q: Do my credits expire? A: No. Your prepaid credit never expires. You can use it whenever you need it, even years later. This is a key advantage over subscription models.
- Q: What happens if I make a mistake? A: If you make a mistake, such as a double charge, contact us via the Support page. We will fix the issue. Your credit is not refunded, but we ensure your balance is correct.
- Q: Is there a free trial? A: Yes. Every new account gets $0.50 of trial credit valid for 7 days. This allows you to test the API before committing to a top-up.
Frequently asked questions
Does the OpenAI Python SDK work with another base URL?
Yes. Pass base_url and api_key to OpenAI(), or set OPENAI_BASE_URL and OPENAI_API_KEY. The rest of your code stays unchanged.
What model name should I use?
Use uncensored. It is the only model ID, and GET /v1/models lists it.
Do I need a forwarder in front of the endpoint?
Not for server-side scripts. Add one only when a browser or another service must call you without holding the real key.
Can I use the Responses API?
Yes. POST /v1/responses is supported alongside /v1/chat/completions and /v1/models.
Get your key instantly
Sign up, copy the token, update your base URL. That’s the entire process.