Start now

ChatGPT Unfiltered API: Streaming, Tools and JSON

A ChatGPT unfiltered API is only useful if it supports what real apps need: streamed text, tool calls and structured output. This endpoint does all three, in the OpenAI format. Below is working code for each, and a clear list of what is not supported.

Last updated

A stream of light particles on a conveyor with a robotic arm picking structured blocks, for streaming tools and JSON

Summary

  1. Supported: streaming over SSE, tool calling with streamed tool_calls, JSON mode, and the usual sampling settings.
  2. Not supported: image, audio or video generation, embeddings, fine-tuning.
  3. Streams hold a concurrency slot until they finish: 8 slots on a full key, 2 on a trial key.
  4. Parse tool arguments and JSON replies defensively; models can return malformed output.

What is supported and what is not

FeatureStatusNotes
Chat CompletionsYesPOST /v1/chat/completions
ResponsesYesPOST /v1/responses
Streaming (SSE)Yesstream: true, with usage at the end
Tool callingYestools, tool_choice, streamed tool_calls, results as role: "tool"
JSON modeYesresponse_format: {"type":"json_object"}
Sampling controlsYestemperature, top_p, stop, seed, penalties
Images, audio, videoNoText only
Embeddings, fine-tuningNoNot 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_limited and concurrency mean you went over a limit; temporary overload returns 429 with Retry-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.loads in 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-Id are 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.

Common questions

Does the endpoint stream responses?

Yes. Send stream: true to receive Server-Sent Events, with usage reported at the end of the stream.

Does it support function calling?

Yes. Use tools and tool_choice, read tool_calls from the reply, and send results back as messages with role tool. Streamed tool_calls are supported too.

Will JSON mode always return valid JSON?

It makes valid JSON much more likely but is not a guarantee, and it does not enforce a schema. Parse inside a try block and retry on failure.

Can I stream and use tools in the same request?

Yes. Tool call names and arguments arrive in fragments, so join them by index and parse the arguments after the stream ends.

Get your API key

Sign up, copy the token, update your base URL. That is all.

Start now