LangChain.js: connect to an OpenAI-compatible API in Node.js

How to point ChatOpenAI from LangChain.js at the Zukko gateway: one baseURL setting, streaming, tool calls and a spend check.

4 min · · development · API keys

If you write in LangChain.js and want to call more than OpenAI models through it, you don't need a separate package per vendor. The ChatOpenAI class works with any server that speaks the Chat Completions format, and the server address is set in one field. Below is how to connect LangChain.js to the Zukko gateway in Node.js, from installation to streaming and tool calls.

What you need

  • Node.js 18 or newer and npm.
  • A key like fetch_...: create it in the dashboard; the full key is shown once.
  • A positive balance. Top-up starts at $1: SBP (the payment page adds an 8.5% fee on top), cryptocurrency or MasterCard cards.

Install the packages:

bash
npm install @langchain/openai @langchain/core zod
export ZUKKO_API_KEY="fetch_your_key"

@langchain/openai provides ChatOpenAI, @langchain/core supplies messages and tools, and zod describes argument schemas.

LangChain.js: basic connection

In the LangChain.js documentation the server address is changed through the configuration field, which accepts the same options as the official OpenAI SDK. For Zukko the address ends in /v1, because this is a client of the OpenAI-compatible family.

bash
import { ChatOpenAI } from "@langchain/openai";

const llm = new ChatOpenAI({
  model: "claude-sonnet-5-5",
  apiKey: process.env.ZUKKO_API_KEY,
  configuration: {
    baseURL: "https://api.zukko.pro/v1",
  },
});

const answer = await llm.invoke([
  { role: "system", content: "Answer briefly and to the point." },
  { role: "user", content: "How does Promise.all differ from Promise.allSettled?" },
]);
console.log(answer.content);

Save the code as chat.ts in an ES-modules project ("type": "module" in package.json) and run it your usual way, for example npx tsx chat.ts.

If your package version does not accept apiKey in the constructor, set the OPENAI_API_KEY environment variable to the same value. There is also an OPENAI_BASE_URL variable for the base address, read by the underlying SDK. An explicit configuration.baseURL is clearer when reading the code and does not depend on the server environment.

You can swap the model for any in the catalog: gpt-6.1-sol, deepseek-v4.1-flash, glm-5.3-flash, claude-opus-5-5. The current list and prices come from GET https://api.zukko.pro/v1/models and from the Models page in the dashboard.

Streaming

Streaming in LangChain.js needs no extra settings: stream() returns an async iterator.

bash
const stream = await llm.stream("Explain closures in JavaScript in three sentences");
for await (const chunk of stream) {
  process.stdout.write(String(chunk.content));
}

At the end of the stream the gateway sends a service chunk with usage and an empty choices list. LangChain handles that itself. If you parse the stream by hand with the official SDK, check that the array is non-empty before reading chunk.choices[0].

Tools (tool calling)

To let the model call your code, describe a function with tool and attach it with bindTools.

bash
import { tool } from "@langchain/core/tools";
import { HumanMessage, ToolMessage } from "@langchain/core/messages";
import * as z from "zod";

const getWeather = tool(
  async ({ city }) => `In ${city} it is +12 with light rain`,
  {
    name: "get_weather",
    description: "Returns the current weather in a city",
    schema: z.object({ city: z.string().describe("City name") }),
  },
);

const withTools = llm.bindTools([getWeather]);

The loop is written by hand so you can see what happens: the model returns not text but a call request, you run the function and hand the result back. When you need several steps in a row, LangChain's agent wrappers take over the same logic; the approach itself is covered in the note on tool use. How well each model handles tool calls depends on the model, so test it on your own tasks.

What it costs

Prices as of October 18, 2026, $ per 1M tokens. For claude-sonnet-5-5: input 0.4, output 2. Say an agent makes three model calls of 2,000 input and 300 output tokens each:

  • input: 6,000 × 0.4 / 1,000,000 = $0.0024;
  • output: 900 × 2 / 1,000,000 = $0.0018;
  • total $0.0042 per run.

An important caveat about caching. For Claude, the gateway applies the prompt cache discount only in the Anthropic format (/v1/messages). In the OpenAI format that ChatOpenAI uses, all input is billed at the input price. If you have a long repeated context and a Claude model, see the format comparison. For GPT models, cache reads are visible in both formats.

Without LangChain: the official openai SDK

If you don't need a framework, the same address works with the plain openai package:

bash
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.zukko.pro/v1",
  apiKey: process.env.ZUKKO_API_KEY,
});

const r = await client.chat.completions.create({
  model: "gpt-6.1-sol",
  messages: [{ role: "user", content: "Hello" }],
});
console.log(r.choices[0].message.content);

More on the Node.js SDK: the full example. A general walkthrough of LangChain setup for Python and JS is in the article on connecting LangChain to the gateway.

Common errors

  • 404 model_not_found: the model name differs from the one in /v1/models.
  • 401: the key was copied with a space or passed in the wrong field.
  • 402: not enough balance.
  • 403 model_not_allowed_for_key: the key's model list is restricted in the dashboard.
  • 429: the key's daily or monthly limit is exhausted, or the model provider is limiting you.
  • Requests go to the wrong place: check that baseURL is inside configuration, not at the top level of the constructor.

For safe experiments, create a separate key with a daily limit in $ and a list of allowed models.

FAQ

Do I need a separate package for Claude in LangChain.js?

Not for calls through Zukko: Claude is available in the OpenAI-compatible format, so ChatOpenAI with the right baseURL is enough. A dedicated Anthropic class makes sense if you specifically need the Anthropic format and the cache discount; then the base address has no /v1.

Where do I put baseURL if I use environment variables?

You can set OPENAI_BASE_URL=https://api.zukko.pro/v1 and OPENAI_API_KEY=fetch_.... An explicit setting in configuration is more reliable, especially on servers where variables may differ.

Does this work from Russia?

There are three ways to pay from Russia: SBP, MasterCard cards and cryptocurrency. Your application code does not change.

How do I avoid overspending while debugging?

Create a separate key with a daily limit, start with a cheap model (deepseek-v4.1-flash or glm-5.3-flash) and watch spend under "Usage" in the dashboard.

To try it, create a key in the dashboard, put it into the example above and run your first request.

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_*

LangChain.js: connect to an OpenAI-compatible API · Zukko