LangChain and LangGraph with an uncensored model

ChatOpenAI with base_url and api_key in Python, configuration.baseURL in JavaScript, an uncensored model id, and the same instance handed to create_agent for a tool-calling agent. Token counts on every message; the cost per agent turn and the errors.

Steps checked November 2, 2026 · prices as of November 9, 2026

LangChain’s ChatOpenAI takes a base URL and a key, so an uncensored build answers through the same class, chains and agents your code already uses. One constructor in Python or JavaScript, the model id from the models page, and create_agent runs its tool loop on it.

Where the settings are

There is no settings screen. The endpoint and the key are constructor arguments on the OpenAI chat model class: ChatOpenAI from langchain-openai in Python, ChatOpenAI from @langchain/openai in JavaScript. Python takes base_url and api_key; JavaScript takes apiKey and a configuration object with baseURL. init_chat_model and initChatModel take the same two values with model_provider="openai".

Agents reuse that instance. LangChain 1.x builds them with create_agent (createAgent in JavaScript); LangGraph’s create_react_agent is the earlier name and now points at it. Both take the model object, so the endpoint is configured once.

Steps

  1. Install the chat model package, and the agent package if you want the tool loop:
shell
1pip install -U langchain langchain-openai
shell
1npm install langchain @langchain/openai @langchain/core zod
  1. Create a key in the console and put it in an environment variable. The code reads the variable; it never holds the key.
shell
1export UNCENSOREDLLM_API_KEY="sk-ullm-live-…"
  1. Python: construct ChatOpenAI with the model id, the key and this base URL. invoke returns an AIMessage whose usage_metadata carries the tokens you were billed for; stream yields chunks as the model writes. The ids on this page (glm-5.3-uncensored, glm-5.3-flash-uncensored, deepseek-v4.1-flash-abliterated) are the three LangChain users call most here; any id on the models page goes in the same way.
python
1import os
2from langchain_openai import ChatOpenAI
3
4model = ChatOpenAI(
5 model="glm-5.3-uncensored",
6 api_key=os.environ["UNCENSOREDLLM_API_KEY"],
7 base_url="https://api.uncensoredllm.ai/v1",
8 max_tokens=1024,
9 stream_usage=True, # token counts on streamed answers too; off by default once base_url is set
10)
11
12response = model.invoke("Explain, in three sentences, when a bloom filter beats a hash set.")
13print(response.content)
14print(response.usage_metadata) # {'input_tokens': …, 'output_tokens': …, 'total_tokens': …}
15
16for chunk in model.stream("List the POSIX signals a daemon should handle."):
17 print(chunk.content, end="", flush=True)

init_chat_model builds the same object from a provider name, which suits code that switches models from config:

python
1from langchain.chat_models import init_chat_model
2
3model = init_chat_model(
4 model="glm-5.3-flash-uncensored",
5 model_provider="openai",
6 base_url="https://api.uncensoredllm.ai/v1",
7 api_key=os.environ["UNCENSOREDLLM_API_KEY"],
8)

The LangChain docs also let the environment carry both values: api_key falls back to OPENAI_API_KEY, and base_url to OPENAI_API_BASE, then OPENAI_BASE_URL. With these two exported, ChatOpenAI(model="…") alone reaches the API. Keep the explicit arguments in code that also talks to OpenAI, or the two will share one base URL.

shell
1export OPENAI_API_KEY="sk-ullm-live-…"
2export OPENAI_BASE_URL="https://api.uncensoredllm.ai/v1"
  1. JavaScript: the same three values, with the URL under configuration.baseURL. The configuration object is passed to the OpenAI SDK client, so its other client options go there too.
typescript
1import { ChatOpenAI } from "@langchain/openai";
2
3const model = new ChatOpenAI({
4 model: "glm-5.3-uncensored",
5 apiKey: process.env.UNCENSOREDLLM_API_KEY,
6 configuration: { baseURL: "https://api.uncensoredllm.ai/v1" },
7 maxTokens: 1024,
8});
9
10const response = await model.invoke("Explain, in three sentences, when a bloom filter beats a hash set.");
11console.log(response.content);
12console.log(response.usage_metadata); // { input_tokens, output_tokens, total_tokens }
13
14const stream = await model.stream("List the POSIX signals a daemon should handle.");
15for await (const chunk of stream) process.stdout.write(String(chunk.content));
  1. Agent: hand the instance to create_agent with your tools. The loop calls the model, runs the tool it asked for, appends the result and calls again until the model answers in text. Use a build whose model page lists tool calls; the table below says which.
python
1import os
2from langchain.agents import create_agent
3from langchain.tools import tool
4from langchain_openai import ChatOpenAI
5
6
7@tool
8def read_file(path: str) -> str:
9 """Read a UTF-8 text file and return its contents."""
10 with open(path, encoding="utf-8") as f:
11 return f.read()
12
13
14model = ChatOpenAI(
15 model="glm-5.3-uncensored",
16 api_key=os.environ["UNCENSOREDLLM_API_KEY"],
17 base_url="https://api.uncensoredllm.ai/v1",
18)
19
20agent = create_agent(
21 model=model,
22 tools=[read_file],
23 system_prompt="You review Python code. Read a file before you comment on it.",
24)
25
26result = agent.invoke({"messages": [{"role": "user", "content": "Review ./app.py for bugs."}]})
27print(result["messages"][-1].content)

Code on LangGraph’s create_react_agent keeps working with the same model and tools arguments (its system prompt argument is prompt); the deprecation notice asks for the import above. The JavaScript agent is the same shape:

typescript
1import { readFile } from "node:fs/promises";
2import { ChatOpenAI } from "@langchain/openai";
3import { createAgent, tool } from "langchain";
4import * as z from "zod";
5
6const readTextFile = tool(async ({ path }) => readFile(path, "utf8"), {
7 name: "read_file",
8 description: "Read a UTF-8 text file and return its contents.",
9 schema: z.object({ path: z.string() }),
10});
11
12const model = new ChatOpenAI({
13 model: "glm-5.3-uncensored",
14 apiKey: process.env.UNCENSOREDLLM_API_KEY,
15 configuration: { baseURL: "https://api.uncensoredllm.ai/v1" },
16});
17
18const agent = createAgent({
19 model,
20 tools: [readTextFile],
21 systemPrompt: "You review TypeScript code. Read a file before you comment on it.",
22});
23
24const result = await agent.invoke({ messages: [{ role: "user", content: "Review ./app.ts for bugs." }] });
25console.log(result.messages.at(-1)?.content);

What each setting does

model
glm-5.3-uncensored
the id as the models page spells it; sent as the model field of each request
api_key / apiKey
os.environ["UNCENSOREDLLM_API_KEY"]
the whole sk-ullm- key, sent as Authorization: Bearer; unset, Python falls back to OPENAI_API_KEY
base_url / configuration.baseURL
https://api.uncensoredllm.ai/v1
where the requests go; the client appends /chat/completions, so the /v1 at the end matters
max_tokens / maxTokens
1024
the cap on output tokens per answer; the model page gives each build’s maximum
stream_usage
True
Python only: asks for the usage chunk on streams. LangChain turns it off once base_url is set; this API answers it, so turn it back on. JavaScript sends it by default (streamUsage)
streaming
False
makes invoke stream internally and assemble the answer; stream() streams either way
temperature, timeout, max_retries
optional
sampling temperature, the request timeout, and how many times the client retries a failed call
tools / system_prompt
create_agent
the functions the agent may call, and the message put first in every call; createAgent spells it systemPrompt

Picking a model

ChatOpenAI sends OpenAI-format requests, and every build on the models page answers in that format. For plain invoke and stream any id works. The agent loop needs tool calls, and so does bind_tools and the default with_structured_output method, which is function calling. An agent turn carries the conversation, the files read and every tool result so far, so input tokens are most of the bill and a build with a low input price costs the least per turn.

ModelOne agent turnTool calls
GLM-5.3 Uncensored$0.01model page
GLM-5.3 Flash Uncensored$0.0047model page
DeepSeek V4.1 Flash Abliterated$0.04model page
DeepSeek V4 Pro Uncensored$0.04model page
GLM-5.3 Abliterated$0.09model page
GLM-5.3 Flash Code Uncensored$0.25model page

40,000 input and 1,200 output tokens at the catalog prices on 2026-11-09. Tool calls as the catalog publishes them; where it does not, the model page says.

Uncensored models and their prices per million tokens
GLM-5.3 Uncensoredglm-5.3-uncensoredUncensored$0.282$0.846
GLM-5.3 Flash Uncensoredglm-5.3-flash-uncensoredUncensored$0.1053$0.3685
DeepSeek V4.1 Flash Abliterateddeepseek-v4.1-flash-abliteratedAbliterated$0.8488$1.6975
DeepSeek V4 Pro Uncensoreddeepseek-v4-pro-uncensoredUncensored$0.97$1.94
GLM-5.3 Abliteratedglm-5.3-abliteratedAbliterated$2.1825$4.365
GLM-5.3 Flash Code Uncensoredglm-5.3-flash-code-uncensoredUncensored$6.00$6.00

Prices per million tokens, read from the live catalog on 2026-11-09.

For coding agents specifically, the uncensored coding AI guide compares the code builds and costs a full coding day on each. The same ids work in Cursor, OpenCode and, through a router, Claude Code.

Check it

List the models your key can call; the id you put in model should be in the answer:

shell
1curl https://api.uncensoredllm.ai/v1/models -H "Authorization: Bearer sk-ullm-live-…"

Then send one request the way ChatOpenAI does. -i prints the headers: x-uplink-request-id names the request, and on a non-streamed answer x-uplink-charge is what it cost in USD.

shell
1curl -i https://api.uncensoredllm.ai/v1/chat/completions \
2 -H "Authorization: Bearer sk-ullm-live-…" \
3 -H "Content-Type: application/json" \
4 -d '{"model": "glm-5.3-uncensored", "messages": [{"role": "user", "content": "Reply with the single word ok."}]}'

Then run the Python or JavaScript snippet. usage_metadata on the response holds the input and output tokens the request was billed on, and the same request shows on the console’s usage page with its charge. On a stream, the counts arrive on the last chunk, and only with stream_usage=True in Python.

Errors and fixes

  • 401 invalid_api_key: the key is wrong or revoked, or UNCENSOREDLLM_API_KEY was not set in the shell that ran the script. Export it again, or copy the key again from the console. A 401 whose body names OpenAI means base_url was never set, so the request went to OpenAI with this key.
  • 404 model_not_found: the id in model is not on the models page. Check the spelling against the page. 404 route_not_found means the request hit a path the API does not serve: a base URL missing its /v1, so the client posted to /chat/completions at the host root, or /responses because use_responses_api=True or a Responses-only feature such as reasoning or a built-in tool was set. This API serves /v1/chat/completions; leave those off.
  • 402 insufficient_balance: the balance does not cover the request. Top up in the console. 402 monthly_limit_reached means the key hit the monthly spend limit you set on it.
  • 403 scope_denied: the key is limited to certain models and this is not one of them.
  • 429 rate_limited: the key hit its rate limit. The response carries retry-after; max_retries makes the client wait and try again on its own.
  • usage_metadata is None on a Python stream: LangChain leaves stream_usage off whenever base_url is set. Pass stream_usage=True to the constructor or to the stream call.
  • The agent answers in prose and never calls a tool, or the request fails on the tools field: the build does not take tool calls. Switch to one whose model page lists them. The same applies to with_structured_output on its default method; method="json_schema" needs a build that takes response_format instead.
  • Output stops mid-sentence with finish_reason of length: max_tokens is too low for the answer. Raise it, up to the maximum on the model page.

Questions

ChatOpenAI speaks the OpenAI request format, and goes to OpenAI’s servers only when no base URL is set. With base_url in Python or configuration.baseURL in JavaScript pointed at this API and a key from the console, the same class, chains and agents run on an uncensored build and no OpenAI account is involved.

ChatOpenAI can route to it, with use_responses_api=True or when a Responses-only feature such as reasoning or a built-in tool is set. This API serves Chat Completions, which is ChatOpenAI’s default route, so leave those options off; invoke, stream, bind_tools and create_agent all run on Chat Completions.

Every id on the models page: each answers /v1/chat/completions in the OpenAI format with the same key, so any of them goes into the model argument as the page spells it. For the agent loop, bind_tools or the default structured output method, pick a build whose page lists tool calls; the table above says which. See the models page.

An endpoint that accepts the OpenAI request shape at /v1/chat/completions and answers in the OpenAI response shape, so a client written for OpenAI works by changing the base URL and the key. This API is one; LangChain’s ChatOpenAI is such a client, and usage_metadata on each answer carries the tokens the request was billed on, priced per token from your prepaid balance.

Every model above is on the uncensored model list with its live price per million tokens. Per-token pricing sets the whole catalog side by side, and the family directory groups the builds by base model.

Get a keyPrepaid from $10, no subscription.

New models and price changes, once a month.

New uncensored builds with their prices, price moves, and what the daily model check found.

Join on Telegram