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

bash
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.

bash
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:

  1. Always pass recursion_limit in config, as in the example. It is a separate config key, not part of configurable.
  2. Keep your own attempts counter in the state, like attempts. It is clearer and lets the graph finish normally instead of with an exception.
  3. 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:

bash
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.

Get API keyDocumentation

api.zukko.pro · fetch_*

LangGraph Python: first graph with a model via gateway · Zukko