LangGraph in Python: a first graph with a model via a gateway
We build a small support graph in LangGraph: two models at different prices, branching, a capped retry loop and a clear picture of how execution works.
5 min · · агенты · development
LangGraph in Python describes a program with a model as a graph: nodes do the work, edges decide where to go next, and shared state travels between steps. That approach helps when a single model call isn't enough: you need branching, a retry for a failed step or a stop condition. Below we build a first working graph, connect models through the Zukko gateway, and look at how LangGraph executes a graph and where to put limits so it doesn't loop forever.
How LangGraph works in Python
In the LangGraph documentation a graph has three parts:
- State: the data schema passed through the graph. Usually a
TypedDict. - Nodes: Python functions. A node receives the state and returns a partial update, not the whole state.
- Edges: transitions. Plain ones lead from one node to another, conditional ones pick the next node with a function.
Each update is applied to the state through the key's reducer. If no reducer is set, the new value simply replaces the old one. To accumulate, for example a message list, you set a reducer via Annotated, such as operator.add. Execution proceeds in steps (super-steps): the nodes of one step run, their updates are merged into the state, then the next step starts. That is enough to understand the example below.
Installation and key
pip install -U langgraph langchain-openai
export ZUKKO_API_KEY="fetch_your_key"Create the key in the dashboard. For models we use ChatOpenAI from langchain-openai: it accepts base_url and a key, so it works with any OpenAI-compatible server. The Zukko address ends in /v1. For the SDK and calls see the Python reference.
The first graph: a support reply
The task: accept a customer request, determine its category, prepare a reply for the right categories, check it and, if needed, rewrite it once. Classification and review go to a cheap model, and the reply draft to a stronger one. This is a simple form of a cascade; the model cascade idea is covered separately.
import os
from typing import Literal, TypedDict
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
BASE_URL = "https://api.zukko.pro/v1"
KEY = os.environ["ZUKKO_API_KEY"]
cheap = ChatOpenAI(model="deepseek-v4.1-flash", base_url=BASE_URL, api_key=KEY, temperature=0)
strong = ChatOpenAI(model="claude-sonnet-5-5", base_url=BASE_URL, api_key=KEY) # no temperature: newer Claude accepts only 1.0
class State(TypedDict):Let's go through the key points.
Nodes return only changes. classify returns {"category": ...}, and the other keys stay as they were. That's why draft increments attempts itself: the counter lives in the state.
Conditional edges. route returns the name of the next node, and the Literal[...] annotation tells LangGraph the allowed options. For review we use a dictionary to return the semantic labels retry and ok and map them to the draft node and the END constant.
The loop. The conditional edge from draft through the review function back to draft forms a cycle. It is bounded twice: by the attempts < 2 condition in the logic itself and by the recursion_limit parameter at invocation.
Where to put limits
The documentation says the recursion limit sets the maximum number of graph steps in one run; when exceeded, GraphRecursionError is raised, and since version 1.0.6 the default is 1000. For a paid API that is too generous: a loop with a bug in its condition will make hundreds of model calls. So:
- Always pass
recursion_limitinconfig, as in the example. It is a separateconfigkey, not part ofconfigurable. - Keep your own attempts counter in the state, like
attempts. It is clearer and lets the graph finish normally instead of with an exception. - Give the API key a daily limit in $: if the in-code limits fail, spending stops on the gateway side. How to set it up is in the article on spend limits for agents.
Watching execution step by step
To see which node returned what, use update streaming:
for update in app.stream(
{"question": "The sign-in confirmation code never arrives", "attempts": 0},
config={"recursion_limit": 10},
stream_mode="updates",
):
print(update)Each item shows the node name and what it added to the state. This is the best debugger for a first graph. If a conversation must continue between runs, pass a checkpointer at compile time, for example InMemorySaver from langgraph.checkpoint.memory, and give a thread_id in configurable on invocation. For production you pick persistent storage; see the LangGraph documentation for details.
What one run costs
Prices as of October 18, 2026, $ per 1M tokens: deepseek-v4.1-flash input 0.013 / output 0.038, claude-sonnet-5-5 input 0.4 / output 2. Suppose classification and review together take 550 input and 8 output tokens, and the draft takes 200 input and 250 output tokens.
- cheap model: 550 × 0.013 / 1,000,000 + 8 × 0.038 / 1,000,000 ≈ $0.0000072 + $0.0000003;
- strong model: 200 × 0.4 / 1,000,000 + 250 × 2 / 1,000,000 = $0.00008 + $0.0005;
- total about $0.0006 per request.
If review rejects the first draft and the graph makes a second, the strong part doubles and the sum is about $0.0012. Note that nearly all the cost sits in the strong model, which is why a cascade pays off. The numbers show the order of magnitude; real texts will use more tokens.
For current prices and the model list: GET https://api.zukko.pro/v1/models or the Models page in the dashboard. Which model suits your requests best, test on your own data.
What next
If you are following a LangGraph course and reached part two, the next steps are usually tools (a node calls functions, as in the documentation quickstart), keeping state between runs and a human in the loop. All of it builds on the same three concepts: state, nodes, edges. The general way to connect LangChain to the gateway is in the article on LangChain and the gateway.
FAQ
Do I need LangChain to use LangGraph?
The graph itself doesn't depend on LangChain, but it is convenient to take models from langchain-openai. You can also call the plain openai SDK right inside a node: a node is just a function.
Why doesn't my graph finish?
Most likely a conditional edge always returns the loop node. Add a counter to the state and recursion_limit to config, then look at the stream_mode="updates" stream.
Which base_url should I use?
https://api.zukko.pro/v1. LangChain uses an OpenAI-compatible client, so the /v1 suffix is needed.
Does this work from Russia?
Payment from Russia: SBP, MasterCard or cryptocurrency. The graph code does not change.
Create a key in the dashboard, set ZUKKO_API_KEY and run the example: within a minute you will see the graph moving through its nodes.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Claude Code YOLO: Permission Modes and Working Without PromptsClaude Code has no mode literally called YOLO, but it has several permission modes and flexible rules. How to stop clicking Yes without losing files.
- Claude Code: a checklist of best practices for daily workHabits that make working with Claude Code predictable: a short CLAUDE.md, a plan before edits, small tasks, checking results and keeping spend under control.
- An MCP server in Java: a minimal example with one toolWe write a minimal MCP server in Java with one tool, register it in Claude Code, and learn to find the cause when the agent doesn't see it.