Key points
- Use ChatOpenAI with base_url set to the API and model set to uncensored.
- Chains, streaming, tool calling and JSON output all go through the standard chat completions route.
- Cap batch concurrency at 8 on a full key, or 2 on a trial key, to avoid 429 errors.
- This is one hosted model, not a router, so there is no per-vendor model switching.
The short setup
The usual langchain + llm question is whether you need a special integration for a third-party endpoint. You do not. LangChain's OpenAI chat wrapper accepts a custom base URL, and this API follows the OpenAI chat completions format, so the standard class works. Install the package, pass three values, and you have a chat model object you can drop into any chain.
pip install -U langchain-openai langchain-core
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
temperature=0.7,
)
print(llm.invoke("Give me one sentence about LangChain.").content)
Get the key from the API key page. New users can claim $0.50 of trial credit there with one click after sign-up, which is enough to run everything in this article many times over. The model ID is always uncensored, and requests go to POST /v1/chat/completions.
A note on the site name: despite the word "router" in the domain, this is a single open-weight model run by an independent provider, not a router across vendors and not any vendor's official API. You do not choose between models here. Your code names one model, and that is the one that answers.
Keep the key out of the source
Hard-coding a key works for a first test and becomes a problem the first time you commit the file. Read it from the environment instead. The OpenAI client libraries that LangChain wraps look for OPENAI_API_KEY and OPENAI_BASE_URL by default, so you can set them in your shell and construct the model with fewer arguments.
export OPENAI_API_KEY="YOUR_API_KEY"
export OPENAI_BASE_URL="https://api.nsfwllmrouter.com/v1"
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.nsfwllmrouter.com/v1"),
api_key=os.environ["OPENAI_API_KEY"],
)
Passing the base URL explicitly, as above, has one advantage. If another project on the same machine uses the real OpenAI service and relies on the same environment variable, your explicit argument prevents the two from colliding.
A first chain with a prompt template
Most real work uses a prompt template, the model and an output parser joined with the pipe operator. This is the LangChain Expression Language style, and it needs nothing special for a custom endpoint.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
)
prompt = ChatPromptTemplate.from_messages([
("system", "You write short story openings in the style the user asks for."),
("human", "Genre: {genre}\nSetting: {setting}\nWrite three sentences."),
])
chain = prompt | llm | StrOutputParser()
print(chain.invoke({"genre": "noir mystery", "setting": "a rainy harbor town"}))
The model has no content refusals for lawful adult use, which matters when your chain generates fiction, roleplay scenes or companion dialogue that a more cautious model would decline. The one fixed rule is that sexual content involving minors is never produced. Build your own input validation on top of that if your product has stricter rules.
Streaming and usage
The API supports streaming over server-sent events with stream: true, and usage is reported at the end of the stream. In LangChain you call .stream() and iterate over chunks.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
)
for chunk in llm.stream("Write a four-line poem about debugging at night."):
print(chunk.content, end="", flush=True)
One detail to keep in mind: a streamed request occupies one of your concurrency slots until the stream ends. If your UI opens a stream and the user wanders off without closing it, that slot stays busy. On a full key you have 8 concurrent requests, so a handful of abandoned streams can block new ones. Cancel the iterator when the client disconnects.
Tool calling and agents
Tool calling is supported: you pass tools and optionally tool_choice, the model replies with tool_calls (which can also be streamed), and you send results back as messages with role: "tool". In LangChain, bind_tools handles the wire format for you.
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
@tool
def word_count(text: str) -> int:
'Count the words in a piece of text.'
return len(text.split())
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
)
llm_with_tools = llm.bind_tools([word_count])
msg = llm_with_tools.invoke("How many words are in: the quick brown fox?")
for call in msg.tool_calls:
print(call["name"], call["args"], word_count.invoke(call["args"]))
For a full agent loop, LangChain's agent helpers or LangGraph run this cycle for you: call the model, run the requested tool, append the tool message, call again. Tool use follows the standard OpenAI shape, so the same code you would write for any compatible endpoint works. As with any open-weight model, test your specific tool schemas. Keep argument names plain, descriptions short, and the list of tools small, since long lists give weaker models more room to pick the wrong one.
Structured output with JSON mode
JSON mode is available through response_format: {"type": "json_object"}. In LangChain you can pass it as a model argument and parse the result yourself, which gives you control over validation and retries.
import json
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
model_kwargs={"response_format": {"type": "json_object"}},
)
msg = llm.invoke(
"Return a JSON object with keys title and tags (a list of three strings) "
"for an article about hiking."
)
data = json.loads(msg.content)
print(data["title"], data["tags"])
Say the word "JSON" in the prompt and describe the keys you want. JSON mode makes the output valid JSON, but it does not guarantee your exact schema, so check the keys after parsing and retry on a failure. Because failed requests are not charged, a retry loop for malformed output costs little.
Concurrency, retries and rate limits
LangChain makes it easy to fan out work with .batch(), and that is where most people hit their first rate limit. A full key allows 8 concurrent requests and 300 requests per minute. A trial key allows 2 concurrent requests and 60 per minute. Go over and the API replies with HTTP 429. The error code is rate_limited when you exceed requests per minute, and concurrency when too many requests run at once. On temporary overload you also get a 429 with a Retry-After: 5 header.
Set the batch concurrency yourself, and let the client retry with backoff:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
max_retries=4,
timeout=120,
)
inputs = [f"Summarize idea number {i} in one line." for i in range(40)]
results = llm.batch(inputs, config={"max_concurrency": 6})
print(len(results))
Staying at 6 on a full key leaves headroom for other requests from the same account. On a trial key use 2. If you still see concurrency errors, look for streams that never finished, since each one holds a slot. Each response carries X-Request-Id, which is the value to quote if you contact support, and X-Balance-USD, which shows your remaining credit.
Async calls for web apps
Inside a web server you rarely want blocking calls. Every LangChain chat model has async twins of its methods, named ainvoke, astream and abatch, and they work against this endpoint the same way as the sync versions.
import asyncio
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
)
async def main():
async for chunk in llm.astream("Describe a quiet harbor at dawn in two sentences."):
print(chunk.content, end="", flush=True)
asyncio.run(main())
In an async service, guard the number of in-flight calls with a semaphore sized to your key, for example 8 on a full key. That keeps one busy endpoint of your own app from filling every slot and starving the others. Catch exceptions around the call, log the X-Request-Id header when you have it, and show the user a short retry message instead of a stack trace.
Debugging tip: if a chain behaves oddly, print the exact messages the model receives. Prompt templates sometimes insert extra whitespace or drop a variable, and the model can only answer what it was sent. LangChain's verbose and debug switches, or LangSmith tracing if you already use it, show the final message list.
What the context window means for chains
The context window is 100,000 tokens for prompt and completion together, and max_tokens is optional. That is large enough for retrieval chains that stuff many documents into one prompt, or for a conversation memory that keeps a long history. It is also large enough to get expensive if you resend a full history every turn without trimming.
A quick calculation. A retrieval step that sends 12,000 input tokens and gets 800 back costs 0.012 x $0.15 + 0.0008 x $0.50 = $0.0018 + $0.0004 = $0.0022. Run that 1,000 times and you pay $2.20. Pricing is $0.15 per 1M input tokens and $0.50 per 1M output tokens, with no subscription, so you pay only for what the chain actually uses. The pricing page has the top-up options, and the quickstart docs list every supported parameter. Prompts are not used for training.
