Key points
- ChatOpenAI works unchanged: set base_url, api_key and model to uncensored.
- The API supports tool calling, so bind_tools and the prebuilt agent both work.
- Cap loop iterations and validate tool arguments; open-weight models sometimes get them wrong.
- Respect 8 concurrent requests on a full key when you run agents in parallel.
What you need before you start
You need Python 3.10 or newer, an API key, and a few packages. The key comes from the API key page: sign up with Google or with email and password, then claim the $0.50 trial credit with one click. The trial key allows 2 concurrent requests and 60 per minute, which is enough for everything in this article if you run one agent at a time.
pip install langchain-openai langchain-core langgraph
export API_KEY="paste-your-key-here"The API exposes POST /v1/chat/completions and GET /v1/models. LangChain only needs the first one. The model is a single open-weight model tuned to answer without content refusals for lawful adult use; it is run by an independent provider and is not the official API of any model vendor. The docs list the headers and limits if you want them next to your editor.
How the pieces map to the API
It helps to know what LangChain does on the wire. A HumanMessage becomes a message with role user. An AIMessage becomes assistant, and when it carries tool calls, those appear in the assistant message as a tool_calls array. A ToolMessage becomes role tool with the matching call ID. The system prompt goes first as role system.
That mapping matters for debugging. If an agent behaves strangely, print the message list before each call and read it like a transcript. You will usually spot the problem within a minute: a tool result that is empty, a call ID that does not match, or a history that has grown far longer than you expected. The word agentic can make this sound like magic, but every step is one ordinary chat completion request.
Step 1: Connect ChatOpenAI
The whole integration is three arguments. LangChain builds standard OpenAI-style requests, so the server cannot tell the difference.
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.uncensoredvideo.cc/v1",
api_key=os.environ["API_KEY"],
temperature=0.7,
max_retries=2,
)
print(llm.invoke("Name three beats of a heist scene, one line each.").content)If this prints three lines, the plumbing works. If you get a 401, check the key. If you get a 404, check that the URL ends in /v1 and has no extra path. max_retries is a client option, and it helps with the temporary 429 responses that carry Retry-After: 5.
Streaming works the same way you would use it with any OpenAI model: call llm.stream(...) and iterate. The final chunk carries usage, so you can log token counts per call.
Step 2: Define tools
An agent is a loop: the model reads the conversation, either answers or asks for a tool, your code runs the tool, and the result goes back to the model. The tools below are plain Python functions. They are deliberately boring, because the point is to see the mechanics.
from langchain_core.tools import tool
@tool
def word_count(text: str) -> int:
"""Count the words in a piece of text."""
return len(text.split())
@tool
def spoken_minutes(words: int, words_per_minute: int = 150) -> float:
"""Estimate how many minutes a script takes to read aloud."""
return round(words / words_per_minute, 2)
tools = [word_count, spoken_minutes]The docstring and the type hints become the tool description and schema that the model sees. Write them as if you were briefing a colleague. A vague docstring is the most common reason an agent picks the wrong tool. If you write scripts for video, the same pattern scales to tools that look up shot lists or check a length budget, a topic covered on the Sora 2 script generator page.
Step 3: Run the agent loop by hand
The prebuilt helpers hide the loop. Writing it once yourself makes every later bug easier to find.
from langchain_core.messages import HumanMessage, ToolMessage
by_name = {t.name: t for t in tools}
llm_tools = llm.bind_tools(tools)
messages = [HumanMessage(
"Here is a draft: 'The vault door hums. Nina counts to three, "
"then cuts the power.' How many words is it, and how long is it read aloud?"
)]
for step in range(6):
ai = llm_tools.invoke(messages)
messages.append(ai)
if not ai.tool_calls:
break
for call in ai.tool_calls:
result = by_name[call["name"]].invoke(call["args"])
messages.append(ToolMessage(content=str(result), tool_call_id=call["id"]))
print(messages[-1].content)Three details matter. The range(6) cap stops a confused model from looping forever, and it also caps your spend. The tool_call_id must match the call it answers. And the model can request several tools in one reply, which is why the inner loop handles a list.
Under the hood, the request carries a tools array and the reply carries tool_calls, with results returned as messages with role: "tool". You can force a specific tool with tool_choice, which LangChain exposes as an argument to bind_tools.
Step 4: Use the prebuilt LangGraph agent
Once the manual loop makes sense, the prebuilt agent saves lines. It takes the same model and the same tools.
import os
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.uncensoredvideo.cc/v1",
api_key=os.environ["API_KEY"],
)
agent = create_react_agent(llm, tools)
out = agent.invoke({"messages": [("user", "How long is a 450 word script read aloud?")]})
print(out["messages"][-1].content)LangChain and LangGraph change their APIs between releases, and some helpers have been renamed or moved. If an import fails, check the current LangChain documentation for the equivalent agent constructor. The connection part, ChatOpenAI with a base URL, stays the same.
Choosing between a loop and a graph
The hand-written loop is enough for agents that pick from a few tools and stop. It is easy to read, easy to log and has no framework surprises. Move to LangGraph when you need branches that depend on state: a review step that sends work back for another pass, a human approval before a risky tool runs, or several specialised sub-agents that pass results to each other.
Whatever you pick, keep each node small and give it one job. A planning node that outputs a short numbered list as JSON, an execution node that runs tools, and a writing node that produces the final text are easier to test than one giant prompt. They also let you use different temperatures: low for planning and tool use, higher for the writing step. Since every node is a normal chat completion call, the same key, URL and model name serve all of them, and the cost is simply the sum of the calls.
One last habit: print the running token total. Agents that look cheap in a demo can spend ten times more on a messy real task, and the usage figures on every response make that visible on the first run, before it turns into a surprise on your balance.
Memory, testing and structured output
The loop above forgets everything when the script ends. The simplest memory is the message list itself: keep it between user turns, append each new question, and trim the oldest messages when it gets long. For long sessions, replace old messages with a short summary written by the model. Because input is billed per token, a 500-token summary beats 20,000 tokens of raw history on both cost and focus.
Testing agents is mostly about checking behaviour you can observe. Set temperature to 0 or pass a fixed seed, which the API passes through, so runs are more repeatable. Then write a handful of questions where you know which tool should be called, and assert on the tool names and arguments in the message list, not on the exact wording of the answer. Run these after every change to a prompt or a tool description. Changing a docstring can change which tool the model picks, and a test is the cheapest way to notice.
Log three things for every run: the number of steps, the tool calls with their arguments, and the token usage. When a run goes wrong, those three lines tell you whether the model chose badly, a tool failed, or the loop ran out of steps.
Structured output and JSON mode
Agents often need to hand back data, not prose. The API supports JSON mode through response_format: {"type":"json_object"}. In LangChain you can ask for it with llm.bind(response_format={"type": "json_object"}). Tell the model in the prompt that the answer must be a JSON object and name the keys you expect, then parse the string and validate it with Pydantic. Do not trust the parse step to always succeed; wrap it in a retry that sends the error text back to the model once.
Pitfalls and how to fix them
- Bad tool arguments. An open-weight model sometimes sends a string where you declared an integer. Validate arguments, return a short error message from the tool, and let the model correct itself on the next step.
- Runaway loops. Always cap iterations. Log each step while you develop.
- Parallel agents. A full key allows 8 concurrent requests. If you run a batch, set
config={"max_concurrency": 4}onllm.batchto leave headroom. Exceeding the limit returns 429 with the codeconcurrency; exceeding 300 requests per minute returnsrate_limited. - Streaming plus tools. A streamed request holds its concurrency slot until the stream ends, so do not start new calls from inside a stream handler on a trial key.
- Growing history. Every step resends the full message list. The context window is 100,000 tokens, but you pay for input on each call.
What an agent run costs
Take a modest agent that makes 4 model calls to finish a task, with an average prompt of 3,000 tokens and 200 output tokens per call. Input: 4 x 3,000 = 12,000 tokens, which is 12,000 x $0.15 / 1,000,000 = $0.0018. Output: 4 x 200 = 800 tokens, which is $0.0004. One run costs $0.0022, so 1,000 runs cost $2.20. The $0.50 trial credit covers about 227 such runs. Failed or refused requests are not charged, and credit never expires. See pricing for top-up amounts and bonuses. This site takes crypto only, USDT on TRC20 or USDC on Base.
