NsfwAiApi/Guide
NSFW LLM API in Python: Streaming, JSON and Tools
To use an nsfw llm api python style, you only need the standard openai package and two changes: the base URL and the key. This guide shows a first request, streaming, JSON mode, tool calling and clean handling of rate limits.

Setup: install and make the first call
You need a current Python 3, the openai package (pip install openai) and an API key. New accounts can claim a $0.50 trial on the API key page with one click after sign-up; it needs no payment details. Store the key in an environment variable, not in source code.
The service speaks the OpenAI chat completions protocol: POST /v1/chat/completions for generation and GET /v1/models for the model list. Auth is a bearer token, and the model ID is uncensored. This is the smallest working script:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.nsfwaiapi.com/v1",
api_key=os.environ["NSFWAIAPI_KEY"],
)
resp = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "You are a fiction co-writer for adult readers. Write vivid, non-explicit scenes."},
{"role": "user", "content": "Open a noir story in a rainy harbor town, three sentences."},
],
temperature=0.8,
)
print(resp.choices[0].message.content)
print(resp.usage)
If you prefer to check the connection from a shell first, the same request works with curl:
curl https://api.nsfwaiapi.com/v1/chat/completions \
-H "Authorization: Bearer $NSFWAIAPI_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "uncensored", "messages": [{"role": "user", "content": "Say hello in five words."}]}'
The model is tuned to answer without content refusals for lawful adult use. That affects the tone of what it will write, not the shape of the API, so the rest of your code looks like any other OpenAI integration. Keep prompts for fiction, character chat and other lawful adult projects, and keep every character an adult.
A few habits help from the first day. Put the instructions that never change in the system message, and keep the user message for the part that changes each turn. Pick one temperature for a task and leave it alone while you compare prompts, because changing two things at once teaches you nothing. Log the request ID and the token counts from resp.usage next to each call, so that when your bill or your output surprises you, there is a record to check against.
Streaming replies with SSE
Set stream=True and the server sends tokens as server-sent events. For chat apps this matters a great deal, because the reader sees the first words quickly instead of waiting for the full reply. Usage totals arrive at the end of the stream, so a streamed request can still be billed and logged accurately.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.nsfwaiapi.com/v1",
api_key=os.environ["NSFWAIAPI_KEY"],
)
stream = client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": "Write a short scene: two rival smugglers share a lantern during a storm."}],
stream=True,
)
text = ""
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
piece = chunk.choices[0].delta.content
text += piece
print(piece, end="", flush=True)
usage = getattr(chunk, "usage", None)
if usage:
print("\n[usage]", usage)
Two details save debugging time. First, the final chunk can have an empty choices list, which is why the loop checks it before indexing. Second, a streamed request holds its concurrency slot until the stream ends, so if your code abandons a stream without closing it, that slot stays busy. Use with client.chat.completions.create(..., stream=True) as stream: or call stream.close() when you stop reading early.
JSON mode for structured output
When your program needs data and not prose, such as a character sheet, a list of scene beats or tags for a story, use JSON mode with response_format={"type": "json_object"}. Also say "JSON" in the prompt and describe the keys you want. The setting asks for a JSON object, but it cannot promise that your keys are present, so check them.
import json
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.nsfwaiapi.com/v1",
api_key=os.environ["NSFWAIAPI_KEY"],
)
resp = client.chat.completions.create(
model="uncensored",
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": "Reply with a JSON object with keys: name, age, goal, secret. All characters are adults."},
{"role": "user", "content": "Create a ship's cook for a pirate adventure."},
],
temperature=0.7,
)
sheet = json.loads(resp.choices[0].message.content)
missing = [k for k in ("name", "age", "goal", "secret") if k not in sheet]
if missing:
raise ValueError(f"model skipped keys: {missing}")
print(sheet["name"], "-", sheet["goal"])
If a long answer is cut off by max_tokens, the JSON will be incomplete and json.loads will fail. Leave max_tokens unset, since it is optional and the completion can use the rest of the window, or set it generously.
Tool calling
Tool calling lets the model ask your code to do something, like roll dice, look up a character note or fetch a price. You pass tools and, if you want, tool_choice. The reply contains tool_calls; you run them and send the results back as messages with role: "tool". Streamed responses carry tool_calls in the deltas too.
import json
import os
import random
from openai import OpenAI
client = OpenAI(
base_url="https://api.nsfwaiapi.com/v1",
api_key=os.environ["NSFWAIAPI_KEY"],
)
tools = [{
"type": "function",
"function": {
"name": "roll_die",
"description": "Roll one die with the given number of sides",
"parameters": {
"type": "object",
"properties": {"sides": {"type": "integer", "minimum": 2}},
"required": ["sides"],
},
},
}]
messages = [
{"role": "system", "content": "You run a tabletop fiction game. Use roll_die for any chance-based outcome."},
{"role": "user", "content": "I try to pick the lock on the captain's cabin. Roll a d20."},
]
first = client.chat.completions.create(
model="uncensored", messages=messages, tools=tools, tool_choice="auto"
)
msg = first.choices[0].message
if msg.tool_calls:
messages.append(msg)
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = random.randint(1, args["sides"])
messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
final = client.chat.completions.create(
model="uncensored", messages=messages, tools=tools
)
print(final.choices[0].message.content)
else:
print(msg.content)
Treat the arguments as untrusted input. Validate them before you act on them, exactly as you would for a web form. Models sometimes send a missing field or a wrong type, and a small guard around json.loads keeps one bad call from ending your session.
Rate limits, retries and your balance
Limits protect the service and give you predictable behavior. A full key allows 8 concurrent requests and 300 requests per minute. A trial key allows 2 and 60. The maximum body is 8 MB. Two kinds of limit error come back as HTTP 429: the code rate_limited means too many requests per minute, and concurrency means too many at once. A temporary overload also returns 429, with a Retry-After: 5 header.
The OpenAI SDK retries a few errors by default. For control, turn that off and write a small loop that respects Retry-After:
import os
import time
from openai import OpenAI, RateLimitError
client = OpenAI(
base_url="https://api.nsfwaiapi.com/v1",
api_key=os.environ["NSFWAIAPI_KEY"],
max_retries=0,
)
def ask(messages, tries=5):
for attempt in range(tries):
try:
raw = client.chat.completions.with_raw_response.create(
model="uncensored", messages=messages
)
print("balance:", raw.headers.get("X-Balance-USD"), "request id:", raw.headers.get("X-Request-Id"))
return raw.parse()
except RateLimitError as err:
wait = float(err.response.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
raise RuntimeError("still rate limited after retries")
reply = ask([{"role": "user", "content": "Describe a market at dawn in two sentences."}])
print(reply.choices[0].message.content)
Every response carries X-Request-Id, which is what you quote if you contact support, and X-Balance-USD, which shows your remaining credit. The X-RateLimit-Limit-Requests and X-RateLimit-Limit-Concurrency headers tell you which limits apply to your key. To run many jobs at once, cap parallelism in your own code with a semaphore sized to your key, 8 for a full key and 2 for a trial key, so you rarely see a 429 at all.
Running many requests in parallel
Batch jobs, such as generating tags for a few hundred story fragments, are where concurrency limits matter. A full key accepts 8 requests at once, so run the work through an async client and a semaphore of that size. If you are on a trial key, use 2.
import asyncio
import os
from openai import AsyncOpenAI
client = AsyncOpenAI(
base_url="https://api.nsfwaiapi.com/v1",
api_key=os.environ["NSFWAIAPI_KEY"],
)
gate = asyncio.Semaphore(8)
async def summarize(fragment):
async with gate:
resp = await client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "Summarize the passage in one sentence."},
{"role": "user", "content": fragment},
],
temperature=0.3,
)
return resp.choices[0].message.content
async def main(fragments):
return await asyncio.gather(*(summarize(f) for f in fragments))
results = asyncio.run(main(["The tide came in early.", "Marta hid the ledger under the floor."]))
print(results)
The semaphore is the whole trick. Without it, a list of 200 fragments fires 200 requests together, and most of them come back as 429 errors with the code concurrency. With it, the work flows at the speed your key allows and nothing is wasted. If you also run a per-minute budget, 300 requests per minute on a full key works out to five per second, so very short requests can hit the minute limit before the concurrency limit.
Common problems and fixes
- 401 or authentication errors. The key is missing, truncated or sent to the wrong host. Print
client.base_urland check the environment variable. The header must readAuthorization: Bearerfollowed by your key. - Model not found. The only model ID is
uncensored. A copied example that says another name will fail, so callGET /v1/modelsif you are unsure. - Empty or short replies in a story loop. Check that you append the assistant reply to the message list before the next turn. Without it, the model sees only your last message.
- Replies that repeat themselves. Raise
frequency_penaltya little, for example to 0.3, or lowertemperatureif the text drifts. The penalties,top_p,stopandseedare all passed through. - Different output on every run. Set
seedand a lowtemperaturewhen you need results that are as repeatable as possible while you debug. - Request too large. The body limit is 8 MB, and prompt plus completion must fit in 100,000 tokens. Trim or summarize old context before it reaches either limit.
When something looks wrong on the server side, save the X-Request-Id header from the response and send it to the support page contact along with a short description. It lets the operators find your exact request.
What it costs and what it cannot do
Pricing is $0.15 per 1M input tokens and $0.50 per 1M output tokens, from prepaid credit with no subscription. Failed or refused requests are not charged, and credit never expires. As a worked example, a script that sends 3,000 tokens and receives 800 pays $0.00045 for input plus $0.0004 for output, which is $0.00085 per call, so 1,000 calls cost $0.85. Top-ups run from $20 to $500, with bonuses of +5% from $50, +10% from $100 and +15% from $200. This site takes crypto only, in USDT on TRC20 or USDC on Base. See the pricing page for the full table, and the documentation for the endpoint reference.
The boundaries are plain. It is a single open-weight model, run by an independent provider, and not the official API of any model vendor. The context window is 100,000 tokens for prompt plus completion. It does not generate images, audio or video, does not provide embeddings and does not support fine-tuning. Use it for lawful adult use only, and note the hard limit: no sexual content involving minors. Prompts are not used for training.
More guides
Questions and answers
Do I need a special SDK to call this API from Python?
No. The official openai package works. Set base_url to https://api.nsfwaiapi.com/v1, pass your key and use the model ID uncensored.
Does streaming work the same way as with OpenAI?
Yes. Pass stream=True and read the server-sent events chunk by chunk. Usage is reported at the end of the stream.
What should I do when I get a 429 error?
Wait and retry. If the response has a Retry-After header, honor it. The error code rate_limited means too many requests per minute, and concurrency means too many requests at once.
Can I test it without paying?
Yes. After sign-up you can claim $0.50 of trial credit on the API key page. It needs no payment details, is valid for 7 days and is one per person.
Your key is one form away
Create an account, copy the key, change the base URL. That is the whole setup.
Get API keyNsfwAiApiUncensored LLM API for NSFW text generation