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
- Install the chat model package, and the agent package if you want the tool loop:
pip install -U langchain langchain-openainpm install langchain @langchain/openai @langchain/core zod- Create a key in the console and put it in an environment variable. The code reads the variable; it never holds the key.
export UNCENSOREDLLM_API_KEY="sk-ullm-live-…"- Python: construct
ChatOpenAIwith the model id, the key and this base URL.invokereturns anAIMessagewhoseusage_metadatacarries the tokens you were billed for;streamyields 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.
import osfrom langchain_openai import ChatOpenAI model = ChatOpenAI( model="glm-5.3-uncensored", api_key=os.environ["UNCENSOREDLLM_API_KEY"], base_url="https://api.uncensoredllm.ai/v1", max_tokens=1024, stream_usage=True, # token counts on streamed answers too; off by default once base_url is set) response = model.invoke("Explain, in three sentences, when a bloom filter beats a hash set.")print(response.content)print(response.usage_metadata) # {'input_tokens': …, 'output_tokens': …, 'total_tokens': …} for chunk in model.stream("List the POSIX signals a daemon should handle."): 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:
from langchain.chat_models import init_chat_model model = init_chat_model( model="glm-5.3-flash-uncensored", model_provider="openai", base_url="https://api.uncensoredllm.ai/v1", api_key=os.environ["UNCENSOREDLLM_API_KEY"],)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.
export OPENAI_API_KEY="sk-ullm-live-…"export OPENAI_BASE_URL="https://api.uncensoredllm.ai/v1"- JavaScript: the same three values, with the URL under
configuration.baseURL. Theconfigurationobject is passed to the OpenAI SDK client, so its other client options go there too.
import { ChatOpenAI } from "@langchain/openai"; const model = new ChatOpenAI({ model: "glm-5.3-uncensored", apiKey: process.env.UNCENSOREDLLM_API_KEY, configuration: { baseURL: "https://api.uncensoredllm.ai/v1" }, maxTokens: 1024,}); const response = await model.invoke("Explain, in three sentences, when a bloom filter beats a hash set.");console.log(response.content);console.log(response.usage_metadata); // { input_tokens, output_tokens, total_tokens } const stream = await model.stream("List the POSIX signals a daemon should handle.");for await (const chunk of stream) process.stdout.write(String(chunk.content));- Agent: hand the instance to
create_agentwith 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.
import osfrom langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAI @tooldef read_file(path: str) -> str: """Read a UTF-8 text file and return its contents.""" with open(path, encoding="utf-8") as f: return f.read() model = ChatOpenAI( model="glm-5.3-uncensored", api_key=os.environ["UNCENSOREDLLM_API_KEY"], base_url="https://api.uncensoredllm.ai/v1",) agent = create_agent( model=model, tools=[read_file], system_prompt="You review Python code. Read a file before you comment on it.",) result = agent.invoke({"messages": [{"role": "user", "content": "Review ./app.py for bugs."}]})print(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:
import { readFile } from "node:fs/promises";import { ChatOpenAI } from "@langchain/openai";import { createAgent, tool } from "langchain";import * as z from "zod"; const readTextFile = tool(async ({ path }) => readFile(path, "utf8"), { name: "read_file", description: "Read a UTF-8 text file and return its contents.", schema: z.object({ path: z.string() }),}); const model = new ChatOpenAI({ model: "glm-5.3-uncensored", apiKey: process.env.UNCENSOREDLLM_API_KEY, configuration: { baseURL: "https://api.uncensoredllm.ai/v1" },}); const agent = createAgent({ model, tools: [readTextFile], systemPrompt: "You review TypeScript code. Read a file before you comment on it.",}); const result = await agent.invoke({ messages: [{ role: "user", content: "Review ./app.ts for bugs." }] });console.log(result.messages.at(-1)?.content);What each setting does
- model
- glm-5.3-uncensoredthe 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/v1where the requests go; the client appends /chat/completions, so the /v1 at the end matters
- max_tokens / maxTokens
- 1024the cap on output tokens per answer; the model page gives each build’s maximum
- stream_usage
- TruePython 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
- Falsemakes invoke stream internally and assemble the answer; stream() streams either way
- temperature, timeout, max_retries
- optionalsampling temperature, the request timeout, and how many times the client retries a failed call
- tools / system_prompt
- create_agentthe 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.
| Model | One agent turn | Tool calls |
|---|---|---|
| GLM-5.3 Uncensored | $0.01 | model page |
| GLM-5.3 Flash Uncensored | $0.0047 | model page |
| DeepSeek V4.1 Flash Abliterated | $0.04 | model page |
| DeepSeek V4 Pro Uncensored | $0.04 | model page |
| GLM-5.3 Abliterated | $0.09 | model page |
| GLM-5.3 Flash Code Uncensored | $0.25 | model page |
| GLM-5.3 Uncensored | Uncensored | $0.282 | $0.846 |
| GLM-5.3 Flash Uncensored | Uncensored | $0.1053 | $0.3685 |
| DeepSeek V4.1 Flash Abliterated | Abliterated | $0.8488 | $1.6975 |
| DeepSeek V4 Pro Uncensored | Uncensored | $0.97 | $1.94 |
| GLM-5.3 Abliterated | Abliterated | $2.1825 | $4.365 |
| GLM-5.3 Flash Code Uncensored | Uncensored | $6.00 | $6.00 |
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:
curl 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.
curl -i https://api.uncensoredllm.ai/v1/chat/completions \ -H "Authorization: Bearer sk-ullm-live-…" \ -H "Content-Type: application/json" \ -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, orUNCENSOREDLLM_API_KEYwas 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 meansbase_urlwas never set, so the request went to OpenAI with this key.404 model_not_found: the id inmodelis not on the models page. Check the spelling against the page.404 route_not_foundmeans the request hit a path the API does not serve: a base URL missing its/v1, so the client posted to/chat/completionsat the host root, or/responsesbecauseuse_responses_api=Trueor a Responses-only feature such asreasoningor 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_reachedmeans 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 carriesretry-after;max_retriesmakes the client wait and try again on its own.usage_metadataisNoneon a Python stream: LangChain leavesstream_usageoff wheneverbase_urlis set. Passstream_usage=Trueto the constructor or to thestreamcall.- The agent answers in prose and never calls a tool, or the request fails on the
toolsfield: the build does not take tool calls. Switch to one whose model page lists them. The same applies towith_structured_outputon its default method;method="json_schema"needs a build that takesresponse_formatinstead. - Output stops mid-sentence with
finish_reasonoflength:max_tokensis 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.