LangChainGo: connecting a Go application to an OpenAI-compatible API
The langchaingo library accepts any OpenAI-compatible address. We show the client, a request, streaming and a templated chain in Go, and estimate call costs.
5 min · · development · API keys
LangChain for Go, known as langchaingo, talks to models over the same protocol as the official OpenAI SDK. That means you can point it at any OpenAI-compatible gateway by changing the address, key and model name. Below is a step-by-step langchain go setup: the client, a simple request, messages with a system role, streaming and a chain with a template. At the end we calculate what such calls cost.
The code is checked against the sources of github.com/tmc/langchaingo on 18 October 2026. The library's API keeps evolving, so before you start, compare with your version on pkg.go.dev. The library's go.mod at the time of checking specified Go 1.26, so make sure your toolchain fits.
Installation and key
go get github.com/tmc/langchaingoCreate the fetch_... key in the Zukko dashboard. The full key is shown once, so save it and pass it to the app through an environment variable:
export ZUKKO_API_KEY=fetch_your_keyThe client: base URL, key and model
The llms/openai package creates a client with the openai.New function and options. We need three: WithBaseURL, WithToken and WithModel.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/openai"
)
func main() {
ctx := context.Background()The address for the OpenAI format always ends in /v1: https://api.zukko.pro/v1. The library adds the /chat/completions path itself.
The same without code: per the sources, if no options are set the client reads the environment variables OPENAI_BASE_URL, OPENAI_API_KEY and OPENAI_MODEL. Then openai.New() is enough in the program, and the address, key and model are switched from outside.
Messages, the system role and token usage
For a dialogue with a system instruction use GenerateContent:
messages := []llms.MessageContent{
llms.TextParts(llms.ChatMessageTypeSystem, "You are a Go developer's assistant. Answer briefly."),
llms.TextParts(llms.ChatMessageTypeHuman, "How does context.WithTimeout differ from context.WithDeadline?"),
}
resp, err := llm.GenerateContent(ctx, messages, llms.WithTemperature(0.2))
if err != nil {
log.Fatal(err)
}
choice := resp.Choices[0]
fmt.Println(choice.Content)
fmt.Println("input:", choice.GenerationInfo["PromptTokens"],
"output:", choice.GenerationInfo["CompletionTokens"],
"cached:", choice.GenerationInfo["PromptCachedTokens"])The library puts token usage data into GenerationInfo: PromptTokens, CompletionTokens, TotalTokens, and also PromptCachedTokens. For GPT models the gateway sees cache reads, and they arrive in prompt_tokens_details.cached_tokens, which is where the library takes this field from. These numbers are handy to log: they let you reconcile with the charges in the dashboard.
About llms.WithMaxTokens: per the sources, the library sends it as max_completion_tokens. If a particular model or route answers 400 to that parameter, remove the option, check the request with curl, and consult the langchaingo documentation about its mode for the old max_tokens field.
Streaming
_, err = llm.GenerateContent(ctx, messages,
llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
fmt.Print(string(chunk))
return nil
}),
)In OpenAI-format streams, the Zukko server always sends a final chunk with usage and an empty choices list. If you write your own stream parser, check that choices is not empty before reading the first element. langchaingo handles such a chunk: the library skips it as a text piece and takes the usage statistics from it.
A chain with a template
In langchaingo, LLMChain ties a prompt template and a model together. In the current version the template is written in Go template syntax:
import (
"github.com/tmc/langchaingo/chains"
"github.com/tmc/langchaingo/prompts"
)
prompt := prompts.NewPromptTemplate(
"Suggest three names for a service that {{.description}}. Answer as a list.",
[]string{"description"},
)
chain := chains.NewLLMChain(llm, prompt)
out, err := chains.Run(ctx, chain, "turns voice messages into text")
if err != nil {
log.Fatal(err)
}
fmt.Println(out)chains.Run fits when the chain has one input parameter and one output. For several parameters use chains.Call and pass a map[string]any. If you port examples from old articles, check the substitution syntax: curly braces with a bare name and no dot may not work.
The official openai-go SDK
If you do not need LangChain and just want an OpenAI-format client in Go, the official github.com/openai/openai-go/v3 library fits. Per its README, the address is changed with the option.WithBaseURL option:
client := openai.NewClient(
option.WithBaseURL("https://api.zukko.pro/v1"),
option.WithAPIKey(os.Getenv("ZUKKO_API_KEY")),
)
resp, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Hi! Answer in one word."),
},
Model: "deepseek-v4.1-flash",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Choices[0].Message.Content)The openai and option packages are imported from github.com/openai/openai-go/v3 and github.com/openai/openai-go/v3/option. That library's README at the time of checking required Go 1.26 or newer. For a low-level option with net/http and SSE parsing see Claude API in Go.
What it costs
Prices as of 18 October 2026, dollars per 1M tokens. An example: a service handles 1,000 requests, each with 400 input tokens and 150 output tokens. That is 0.4M input tokens and 0.15M output tokens.
| Model | Input / output | Formula | Total |
|---|---|---|---|
deepseek-v4.1-flash | 0.013 / 0.038 | 0.4 × 0.013 + 0.15 × 0.038 | $0.0109 |
gpt-6.1-sol | 0.27 / 1.32 | 0.4 × 0.27 + 0.15 × 1.32 | $0.306 |
claude-sonnet-5-5 | 0.4 / 2 | 0.4 × 0.4 + 0.15 × 2 | $0.46 |
For Claude through langchaingo and the chat/completions format, all input is billed at the input price, and the cache discount works in the Anthropic format. For GPT, cache reads are visible in both formats. The current list of models and prices is GET https://api.zukko.pro/v1/models, and format details are in the chat/completions reference. So a test loop does not overspend, create a separate key with a daily limit.
If something does not work
Errors come from the library as a regular error with the server's response text. Go by the meaning:
- 401: wrong key. Check the
fetch_prefix and that the environment variable is set in the process that launches the program. - 402: not enough balance.
- 403
model_not_allowed_for_key: the model is not in the key's allowed list. - 404
model_not_found: wrong model name, compare withGET /v1/models. If 404 comes on every request, check the address: it must end in/v1. - 503
model_disabled: the model is turned off, switch to another from the list.
Access from Russia goes through the gateway, with payment by SBP (the payment page adds 8.5% on top), MasterCard, or crypto. Error breakdown: 401, 402, model not found.
FAQ
Can I use Claude or DeepSeek in langchaingo, not just GPT?
Yes. The gateway accepts any of its models in the OpenAI format, so changing the string in WithModel is enough. Before switching, check that the feature you need (tools, JSON mode) works with the chosen model.
Do I need /v1 in the address?
Yes, for the OpenAI format the address is https://api.zukko.pro/v1. Without /v1 requests go to the wrong path.
How do I find out how many tokens a request used?
Read GenerationInfo in the response: the PromptTokens, CompletionTokens and PromptCachedTokens fields. See spend history in the dashboard under "Usage" or via GET /v1/usage.
Does langchaingo have a ready-made agent with tools?
The library has agents and tools, but their set and API have changed, so look at the examples in the repository's examples folder for your version.
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
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Gemini API in Python: requests via the OpenAI SDK and OpenAI formatA working Gemini API example in Python with no extra library: change base_url in the OpenAI SDK, add streaming and JSON, and compare with google-genai.
- Anthropic SDK for Python: base_url, key and a first request via a gatewayThe official anthropic package for Python just needs a different address. We cover base_url, the key, streaming, token counting, caching and errors.