What is supported and what is not
| Feature | Status | Notes |
|---|---|---|
| Chat Completions | Yes | POST /v1/chat/completions |
| Responses | Yes | POST /v1/responses |
| Streaming (SSE) | Yes | stream: true, with usage at the end |
| Tool calling | Yes | tools, tool_choice, streamed tool_calls, results as role: "tool" |
| JSON mode | Yes | response_format: {"type":"json_object"} |
| Sampling controls | Yes | temperature, top_p, stop, seed, penalties |
| Images, audio, video | No | Text only |
| Embeddings, fine-tuning | No | Not offered |
The model is one open-weight text model with the ID uncensored, run by an independent provider. It answers without content refusals for lawful adult use, and it is not an official API of OpenAI or any other vendor. Every example below uses the base URL https://api.chatgptunfiltered.com/v1.
Anything not on this list should be treated as unavailable until the documentation says otherwise. That includes vendor-specific hosted tools, which are different from the function calling shown below.
Streaming
Set stream: true and iterate over the chunks. Print the text pieces as they arrive and read usage from the final chunk.
from openai import OpenAI
client = OpenAI(
base_url="https://api.chatgptunfiltered.com/v1",
api_key="YOUR_KEY",
)
stream = client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": "Write a four-line poem about a night train."}],
stream=True,
)
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("\nusage:", chunk.usage)
Two things to remember. First, a streamed request occupies its concurrency slot until the stream ends, so an abandoned stream keeps a slot busy; close it when the user leaves. Second, the final chunk may carry usage and no text, so guard the access to choices as above.
In a web app, forward the chunks to the browser as they arrive, for example over your own SSE route, and let the page append each piece of text to the message. Do the usage bookkeeping on the server once the stream closes, where the final chunk tells you what the request cost.
Tool calling, end to end
Declare a function, let the model request it, run it yourself, and send the result back with role: "tool".
import json
tools = [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up the status of an order by its ID.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
},
}]
def get_order_status(order_id):
return {"order_id": order_id, "status": "shipped"}
messages = [{"role": "user", "content": "Where is order A-1042?"}]
r = client.chat.completions.create(
model="uncensored", messages=messages, tools=tools, tool_choice="auto"
)
msg = r.choices[0].message
if msg.tool_calls:
messages.append(msg.model_dump(exclude_none=True))
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = get_order_status(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})
final = client.chat.completions.create(model="uncensored", messages=messages, tools=tools)
print(final.choices[0].message.content)
else:
print(msg.content)
The model decides whether to call the function; tool_choice lets you steer that. The function itself runs in your code, never on the server, so you control what it can touch. Validate the arguments before acting on them, because a model can send a wrong type or a missing field.
Streaming tool calls
When you combine stream: true with tools, the call arrives in pieces. The function name comes first, and the arguments string arrives in fragments you have to join by index.
calls = {}
stream = client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": "Where is order A-1042?"}],
tools=tools,
stream=True,
)
for chunk in stream:
if not chunk.choices:
continue
for tc in chunk.choices[0].delta.tool_calls or []:
slot = calls.setdefault(tc.index, {"id": "", "name": "", "args": ""})
if tc.id:
slot["id"] = tc.id
if tc.function and tc.function.name:
slot["name"] = tc.function.name
if tc.function and tc.function.arguments:
slot["args"] += tc.function.arguments
for slot in calls.values():
print(slot["name"], json.loads(slot["args"]))
Only parse args after the stream has finished. A half-received string is not valid JSON. After that, run the function and send the result back exactly as in the non-streaming example.
JSON mode
For structured replies, turn on JSON mode and describe the shape you want in the prompt.
r = client.chat.completions.create(
model="uncensored",
temperature=0.2,
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": "Reply with JSON only: {\"mood\": string, \"intensity\": integer from 1 to 5}."},
{"role": "user", "content": "Rate this line: The door slammed and nobody spoke for an hour."},
],
)
try:
data = json.loads(r.choices[0].message.content)
except json.JSONDecodeError:
data = None # retry once, or fall back
print(data)
JSON mode makes valid JSON far more likely, but it does not enforce your schema. Check the keys and types yourself, and keep a retry path. Low temperature helps for extraction. Sampling settings such as temperature, top_p, stop, seed and the penalties are passed through.
A useful pattern is to combine JSON mode with a small validation function that returns a clear error message, then feed that message back to the model on a single retry: "Your last reply was missing the key intensity. Reply again with valid JSON." Models usually correct themselves on the second try, and the extra request costs a fraction of a cent.
Handling failures during a stream
Streams fail in ways plain requests do not: the connection can drop after half the text. Plan for it.
import time
from openai import RateLimitError, APIConnectionError
def stream_reply(messages):
for attempt in range(3):
parts = []
try:
stream = client.chat.completions.create(
model="uncensored", messages=messages, stream=True
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
parts.append(chunk.choices[0].delta.content)
return "".join(parts)
except RateLimitError:
time.sleep(5)
except APIConnectionError:
time.sleep(1)
raise RuntimeError("stream failed after retries")
A 429 here means a limit or temporary overload, and the right move is to wait 5 seconds, which matches the Retry-After: 5 the API sends. If a stream breaks halfway, you have a partial answer. Decide whether to discard it and retry, or to keep it and ask the model to continue. For chat UIs, discarding and retrying is usually less confusing to users.
Limits and cost to plan for
- Rate: 300 requests per minute on a full key, 60 on a trial key.
- Concurrency: 8 on a full key, 2 on a trial key. Remember that every open stream counts.
- Size: request bodies up to 8 MB; context of 100,000 tokens, prompt plus completion.
- 429 handling: codes
rate_limitedandconcurrencymean you went over a limit; temporary overload returns 429 withRetry-After: 5.
Cost example: a tool-calling exchange with two requests, 1,200 tokens in and 80 out for the first, 1,400 in and 150 out for the second. Input is 2,600 x $0.15 / 1M = $0.00039. Output is 230 x $0.50 / 1M = $0.000115. Total $0.000505. Failed or refused requests are not charged. Rates are on pricing, and the full method list is in the docs. Start with the $0.50 trial credit from the API key page. For background on the model, see understanding uncensored ChatGPT models. The hard limit applies in every mode: no sexual content involving minors.
A pre-launch checklist
- Tool arguments are parsed with
json.loadsin a try block, and checked against the expected keys and types before anything runs. - The functions you expose can do only what you would let an anonymous user do. The model chooses when to call them, not you.
- JSON replies are validated, with one retry on failure.
- Streams are closed when the user leaves the page, so they do not hold a concurrency slot.
- Your concurrency limit in code is at or below the limit of the key: 8 for a full key.
- Request IDs from
X-Request-Idare logged. - The key lives on the server and can be regenerated; the old one stops working at once.
The service is text only, so a feature that needs an image or an embedding must come from another provider.
