Basics

Common first errors when calling an LLM API from Go

The errors a first agent usually runs into, what each one looks like, and the fix. A missing key, no credit, a bad model ID, rate limits and server errors, a reply cut off partway, a reasoning model that spent its whole reply thinking, and two Go traps.

Updated

This page works on one stage of the loop

  1. 1Build the request
  2. 2Call the model (this page)
  3. 3Run the tools
  4. 4Decide

Most problems with a first agent are problems with a single call. They come in two kinds: the API refuses the request and says so with an HTTP status, or it accepts the request and the reply isn’t what your code expects. Both are easy to handle once you’ve seen them.

When the API refuses

A refused request comes back with a status other than 200 and a small JSON body that says what went wrong. This one is a real response, recorded with a key that didn’t belong to any account:

A 401, as the API sent it
"response": {
	"error": {
		"message": "User not found.",
		"code": 401
	}
}

Read the status first; it tells you whether trying again can help.

Status Usually means Fix Retry?
401 The key is missing, mistyped, or revoked Check echo $OPENROUTER_API_KEY in the same shell that runs the program; an export in another terminal doesn’t carry over No
402 No credit left, or the key reached its own credit limit Add credit or raise the key’s limit No, with one exception below
400 The request itself is wrong: a misspelled model ID, a malformed message, or more text than the model can take Fix the request; for a model ID, copy it from the model’s page, in company/model form No
403 A moderation check blocked the input Change the input No
408, 429 A timeout, or too many requests too fast Wait, then try again, waiting longer each time Yes
500, 502, 503 A failure on the API’s or a model host’s side Wait briefly and try again Yes

OpenRouter also sends a 402 for a temporary limit on how much a key can have in flight at once, with a header saying how long to wait; that one is worth retrying. Retries and observability covers waiting well, and why the agent stopped turns these statuses into Go errors your code can check.

A model that can’t use tools is a separate case. If you send tools to a model that doesn’t support them, the request fails; check that the model’s page lists tool support before you choose it, as choosing a model describes.

When the reply is cut off

A request can succeed and still bring back half an answer. This reply came from a request that allowed the model only 20 tokens:

finish_reason: length
"choices": [
  {
    "index": 0,
    "logprobs": null,
    "finish_reason": "length",
    "native_finish_reason": "max_tokens",
    "message": {
      "role": "assistant",
      "content": "Go, also known as Golang, was conceived in September 2007 ",
      "refusal": null,
      "reasoning": null
    }
  }
],

The text simply stops, mid-sentence. Nothing in the content says so; only finish_reason does. An agent that treats this as an answer will act on half of one, so check finish_reason on every reply: stop is a finished answer, tool_calls is a request for tools, and length means the reply needs more room. Raise the reply limit (max_tokens), or ask for something shorter.

When a reasoning model spends the reply thinking

Some models think before they answer, writing reasoning that counts against the same reply limit. Given only 12 tokens, this model spent all of them reasoning and returned no answer at all:

All 12 tokens went to reasoning
"finish_reason": "length",
"native_finish_reason": "max_tokens",
"message": {
	"role": "assistant",
	"content": null,
	"refusal": null,
	"reasoning": null,

content is null, and finish_reason is length again. With a reasoning model, leave a generous reply limit, and treat an empty answer with length the same way as a cut-off one. Choosing a model has more on reasoning models and what they cost.

When the program hangs

A call to a model can take many seconds, and once in a while it doesn’t come back at all. Give every call a deadline with context.WithTimeout, so a stuck request fails with an error instead of holding the whole agent. Calling a model from Go builds a client that does this and records why a call was cancelled.

Two Go traps

A field that never fills in. encoding/json/v2 matches JSON names to struct fields exactly, case included. A field declared as FinishReason string without a tag never matches "finish_reason", and it stays empty with no error, so the check above never fires. Give every field you decode from an API an explicit tag, as the companion code’s client does:

Every field tagged with its JSON name
type chatChoice struct {
	FinishReason string      `json:"finish_reason"`
	Message      chatMessage `json:"message"`
	Error        *apiError   `json:"error"` // a failure partway through
}

A Go that’s too old. The companion code needs Go 1.27. Go 1.21 and later download it automatically when they read the module’s go.mod; an older go stops with an error naming the version the module requires. Getting set up covers installing it.