OpenAI Python SDK Custom base_url: Setup and Fixes

A cable moved from one server socket to another, showing a base URL switch for an SDK

The OpenAI SDKs let you override the endpoint with one argument. This page shows the exact OpenAI Python SDK custom base_url setup for this API, plus the Node equivalent, streaming, and the mistakes that cause most failed first calls.

Last updated

Summary

  • Set base_url to https://api.apicompatible.com/v1 and pass your key as api_key; nothing else in your code has to change.
  • The model id is uncensored. Leaving an old model name in place is the most common reason a first call fails.
  • Streaming works with stream=True, and each streamed request holds one of your concurrent slots until it ends.
  • Environment variables let you switch endpoints without touching code.

The short answer

Create the client with two arguments, base_url and api_key, and keep using the same methods you already know. The base URL for this API is https://api.apicompatible.com/v1, the model id is uncensored, and authentication is a standard bearer token that the SDK builds from your key.

from openai import OpenAI

client = OpenAI(
    base_url="https://api.apicompatible.com/v1",
    api_key="YOUR_API_KEY",
)

resp = client.chat.completions.create(
    model="uncensored",
    messages=[
        {"role": "system", "content": "You are a concise assistant."},
        {"role": "user", "content": "Name three uses for a hash map."},
    ],
)

print(resp.choices[0].message.content)
print(resp.usage)

That is the whole change. The SDK appends /chat/completions to the base URL, so the request lands on POST /v1/chat/completions. If you do not have a key yet, the API key page explains sign-up and the trial credit.

Setting the base URL with environment variables

The official Python SDK reads OPENAI_API_KEY and OPENAI_BASE_URL from the environment when you do not pass them in code. That is useful when the same application runs against different endpoints in development and production, or when a third-party tool builds the client for you and gives you no place to edit constructor arguments.

export OPENAI_BASE_URL="https://api.apicompatible.com/v1"
export OPENAI_API_KEY="YOUR_API_KEY"

python - <<'EOF'
from openai import OpenAI

client = OpenAI()
r = client.chat.completions.create(
    model="uncensored",
    messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(r.choices[0].message.content)
EOF

Explicit arguments win over environment variables. If a call goes to the wrong host, check whether some other part of your code builds a client with its own base_url, and whether a .env loader runs before your script reads the variables.

Keep the key out of source control. A key in an environment variable or a secrets manager is easy to rotate; a key in a committed file is not.

The same change in Node

The Node package uses camel case for the same option. Everything else, including the method names and the response shape, matches what you would write against any OpenAI-format endpoint.

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.apicompatible.com/v1",
  apiKey: process.env.API_KEY,
});

const completion = await client.chat.completions.create({
  model: "uncensored",
  messages: [{ role: "user", content: "Give me a one-line commit message for a typo fix." }],
});

console.log(completion.choices[0].message.content);

For a raw HTTP check without any SDK, this curl call sends the same request and is handy for separating SDK problems from key problems:

curl https://api.apicompatible.com/v1/chat/completions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"uncensored","messages":[{"role":"user","content":"ping"}]}'

Streaming, async and retries

Streaming uses server-sent events, and the SDK turns them into an iterator. Pass stream=True and read the deltas as they arrive. Usage numbers arrive at the end of the stream, so guard for chunks that carry no text.

from openai import OpenAI

client = OpenAI(
    base_url="https://api.apicompatible.com/v1",
    api_key="YOUR_API_KEY",
)

stream = client.chat.completions.create(
    model="uncensored",
    messages=[{"role": "user", "content": "Explain idempotency in two sentences."}],
    stream=True,
)

for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
print()

The async client takes the same two arguments. Swap OpenAI for AsyncOpenAI, add await, and use async for on streams.

Two limits matter once you run more than one request at a time. A full key allows 8 concurrent requests and 300 requests per minute, and a trial key allows 2 and 60. A streamed request keeps its concurrency slot until the stream finishes, so a stalled consumer that never drains the stream wastes a slot. Over a limit you get HTTP 429 with the error code rate_limited or concurrency. The SDK retries failed requests a couple of times by default, and you can change that with max_retries on the client. A 429 with a Retry-After: 5 header means temporary overload, so waiting five seconds is the correct response.

Timeouts, client reuse and debugging what was sent

Create the client once and reuse it. Each client keeps its own connection pool, so building a new one for every request wastes time and makes streaming code harder to reason about. Both options below are set on the constructor and apply to every call made through that client.

from openai import OpenAI

client = OpenAI(
    base_url="https://api.apicompatible.com/v1",
    api_key="YOUR_API_KEY",
    timeout=60.0,
    max_retries=2,
)

Long generations can take a while, so a very short timeout produces errors that look like service faults. Choose a timeout that fits your longest expected answer, and raise it for large outputs.

When you cannot tell which URL or key a request used, turn on the SDK's debug logging. Recent versions of the Python package read an OPENAI_LOG environment variable, and setting it to debug prints the request line and response status. Check the current SDK documentation for the exact behaviour in your version, and remove the variable afterwards, since logs can contain sensitive data. The printed host should be api.apicompatible.com. If it shows something else, a different client instance or an environment variable is overriding your setting.

A last habit that saves time: keep the base URL, key and model id in one config module. When all three live in one place, switching endpoints becomes a single edit and a single review.

Mistakes that break the first call

Most failed first calls come from a short list of causes. Work through it in order before you suspect the service.

The base URL is missing /v1 or has a stray path

The SDK adds only the endpoint name, so the base URL must end at /v1. Using https://api.apicompatible.com without the version segment, or pasting the full /v1/chat/completions path, usually produces a not-found error because the final URL ends up wrong. Print client.base_url if you are unsure what the SDK resolved.

The model id is still a name from another provider

This API serves one model and its id is uncensored. A leftover model name from a previous setup is a frequent cause of errors, and it is easy to miss when the name sits in a config file instead of the code. Search your project for the old string.

The key comes from the wrong place

A key from another provider will not work here, and the reverse is also true. If you set both OPENAI_API_KEY and an explicit api_key, the explicit value is used. Check which one reached the client.

You call an endpoint this API does not offer

The API is text only. It exposes /v1/chat/completions, /v1/responses and /v1/models. There are no image, audio or embeddings endpoints and no fine-tuning, so code paths that use them have to stay on their original provider. The cost and trade-off analysis covers when that split makes sense.

The payload is too large

The maximum request body is 8 MB. Long contexts are fine up to 100,000 tokens in total for prompt plus completion, but base64 blobs or huge pasted files can hit the body limit first.

Checking the connection and your balance

Before debugging application code, call GET /v1/models. A clean list means the key and URL are right. Every response also carries an X-Balance-USD header, which lets you watch spend without a separate call. With the Python SDK, use with_raw_response to read headers:

from openai import OpenAI

client = OpenAI(
    base_url="https://api.apicompatible.com/v1",
    api_key="YOUR_API_KEY",
)

raw = client.chat.completions.with_raw_response.create(
    model="uncensored",
    messages=[{"role": "user", "content": "ping"}],
)

print(raw.headers.get("X-Balance-USD"))
print(raw.headers.get("X-Request-Id"))
completion = raw.parse()
print(completion.choices[0].message.content)

Keep the X-Request-Id value when you contact support, because it identifies the exact request. Failed or refused requests are not charged, so a bad call costs nothing. Pricing is $0.15 per 1M input tokens and $0.50 per 1M output tokens from prepaid credit; the pricing page has the details and the documentation lists every parameter.

Common questions

Do I need a special SDK to use a custom base_url?

No. The standard OpenAI Python and Node packages accept a base URL option. You change that option and the key, and keep your existing calls.

Is it base_url or baseURL?

Python uses base_url and Node uses baseURL. Both take the same value, https://api.apicompatible.com/v1.

Which model name should I send?

Send uncensored as the model. This API runs one model, so there is nothing else to choose from.

Why do I get HTTP 429 when I only run a few requests?

A trial key allows 2 concurrent requests, and open streams count against that number until they end. Close or finish streams, or move to a full key for 8 concurrent requests.

Get your key in seconds

Sign up, copy the token, update the endpoint URL. That is the entire configuration.

Open account