Skip to main content
  1. Articles/

LLM and Go: Investigating Anthropic Messages API

·2276 words·11 mins· loading · loading ·
Marko Milojevic
Author
Marko Milojevic
Software engineer and architect. Golang and LLM enthusiast. Awful chess player, gym rat, harmonica newbie and cat lover.
LLM and Go - This article is part of a series.
Part 5: This Article

In the previous article I covered the fundamentals of Anthropic’s Messages API: setting up a client, maintaining conversation history, and integrating tools. That was enough to build a working conversational agent. This article goes a level deeper — into the API parameters that shape what the model returns and how it thinks.

Two parameters stand out as particularly useful in production: output_config.format and output_config.effort. The first gives you control over the structure of the model’s output. The second controls how much the model reasons before responding — which turns out to matter more than you might expect once you start caring about latency and cost.

Messages API details
#

The Messages API endpoint accepts a rich set of parameters. Most have sensible defaults and you will rarely touch them, but understanding what is available saves you from reaching for workarounds that already exist in the API. The table below covers a selection of the current parameters from the API reference:

Parameter Type Description
model string ID of the model to use
messages array Conversation history as an ordered list of messages
system string/array System prompt that sets the model’s behaviour, kept separate from messages
max_tokens integer Maximum tokens the model may generate — required on every request
output_config.format object Constrains the response to a JSON Schema
output_config.effort string Reasoning depth: low, medium, high, xhigh, max
thinking object Enables and configures extended or adaptive thinking
temperature number Sampling temperature from 0 to 1; higher values produce more random output
top_p number Alternative to temperature; nucleus sampling probability mass
top_k integer Restricts sampling to the top K most likely tokens
stop_sequences array Custom sequences at which the API stops generating
stream boolean Stream partial responses as server-sent events
tools array List of tools the model may call
tool_choice object Controls which tool the model calls
metadata object Arbitrary metadata about the request, such as an end-user ID

In this article we focus on output_config.format and output_config.effort — two parameters with a direct, visible impact on production systems.

Information extraction with output_config.format
#

The format field inside output_config controls how the model structures its output. By default, Claude replies with plain text. Setting output_config.format to a json_schema document constrains the response to conform to that schema — Anthropic calls this structured outputs.

Structured output matters whenever downstream code needs to parse the model’s response. Without it, you are parsing free text — which works until the model changes a field name or adds a sentence before the JSON block. With it, you get a contract. The model’s output either matches the schema or the call fails with an error you can handle, rather than silently producing malformed data.

A good production case for this is extracting company details from a website. I have used exactly this approach: given a company URL, extract name, description, and address as structured data, regardless of the language or layout of the page.

package main

import (
	// ...

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/option"
)

var client anthropic.Client

func extractWebsiteContent(websiteURL string) (string, error) {
	// ...
}

func main() {
	apiKey := os.Getenv("ANTHROPIC_API_KEY")
	if apiKey == "" {
		fmt.Fprintln(os.Stderr, "error: ANTHROPIC_API_KEY environment variable is not set")
		os.Exit(1)
	}

	client = anthropic.NewClient(option.WithAPIKey(apiKey))

	scanner := bufio.NewScanner(os.Stdin)
	fmt.Println("Please provide the company's website URL:")
	fmt.Println()
	if !scanner.Scan() {
		return
	}

	website := strings.TrimSpace(scanner.Text())
	if website == "" {
		return
	}

	companyInformation, err := extractWebsiteContent(website)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		return
	}
	
	// ...
}

The structure is familiar from the previous article. We initialize the client, read a URL from standard input, and pass it to extractWebsiteContent. That function is the first meaningful piece — it fetches the page and converts it into a form the model can work with efficiently.

import (
	// ...

	"github.com/k3a/html2text"
)

func extractWebsiteContent(websiteURL string) (string, error) {
	req, err := http.NewRequest("GET", websiteURL, nil)
	if err != nil {
		return "", err
	}

	httpClient := &http.Client{}
	response, err := httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()

	data, err := io.ReadAll(response.Body)
	if err != nil {
		return "", err
	}

	return html2text.HTML2Text(string(data)), nil
}

We fetch the page using Go’s standard http package and convert the HTML body to plain text using github.com/k3a/html2text. That conversion step is more important than it looks. Raw HTML sent to the model is full of tags, scripts, and attributes that add tokens without adding meaning. Stripping them down to plain text significantly reduces the size of the input, which lowers cost and reduces the chance of the model getting distracted by irrelevant markup. Cleaner input tends to produce more accurate output.

With the content in hand, we define the schema we want the model to conform to.

var jsonSchema = map[string]any{
	"type": "object",
	"properties": map[string]any{
		"company": map[string]any{
			"type":        "object",
			"description": "The company information.",
			"properties": map[string]any{
				"name": map[string]any{
					"type":        "string",
					"description": "The name of the company.",
				},
				"description": map[string]any{
					"type":        "string",
					"description": "The description of the company.",
				},
				"address": map[string]any{
					"type":        "object",
					"description": "The address of the company.",
					"properties": map[string]any{
						"streetName": map[string]any{
							"type":        []string{"string", "null"},
							"description": "The street name of the company.",
						},
						"streetNumber": map[string]any{
							"type":        []string{"string", "null"},
							"description": "The street number of the company.",
						},
						"city": map[string]any{
							"type":        []string{"string", "null"},
							"description": "The city of the company.",
						},
					},
					"required": []string{
						"streetName",
						"streetNumber",
						"city",
					},
					"additionalProperties": false,
				},
			},
			"required": []string{
				"name",
				"description",
				"address",
			},
			"additionalProperties": false,
		},
	},
	"required": []string{
		"company",
	},
	"additionalProperties": false,
}

This is a standard JSON Schema document expressed as a Go map, passed as the Schema field on JSONOutputFormatParam. Address fields like streetName and city use a union type of string or null because not every company website publishes a full postal address — and Anthropic’s structured outputs require every property to be listed in required, so optional fields have to be modelled as nullable rather than left out. The additionalProperties: false constraint at each object level prevents the model from adding fields outside the schema.

func extractCompanyInformation(companyInformation string) (string, error) {
	start := time.Now()

	resp, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeOpus5,
		MaxTokens: 4096,
		System: []anthropic.TextBlockParam{
			{Text: "You are a legal advisor. " +
				"Given a company's information, extract the company's name, description, and address." +
				"You must provide company description in English"},
		},
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(companyInformation)),
		},
		OutputConfig: anthropic.OutputConfigParam{
			Effort: anthropic.OutputConfigEffortHigh,
			Format: anthropic.JSONOutputFormatParam{
				Schema: jsonSchema,
			},
		},
	})
	duration := time.Since(start)
	fmt.Println("Duration: " + duration.String())

	if err != nil {
		return "", fmt.Errorf("API call failed: %w", err)
	}

	var text strings.Builder
	for _, block := range resp.Content {
		if variant, ok := block.AsAny().(anthropic.TextBlock); ok {
			text.WriteString(variant.Text)
		}
	}

	return text.String(), nil
}

The OutputConfig field carries both Effort and Format for this request. Format is set to the JSONOutputFormatParam we defined above, and Effort is set to High here — effort is only supported on Claude’s Opus and Sonnet models, not Haiku, which is why this example and the ones that follow stick to claude-opus-5 and claude-sonnet-4-6. The system prompt defines the persona — "You are a legal advisor." — which primes the model to treat the task with the seriousness a legal context implies, rather than producing casual summaries. We also instruct the model to respond in English regardless of the source page language, because company websites can be in any language and we want consistent output. As in the previous article, we walk resp.Content for TextBlock values rather than indexing into a single fixed field — a Messages API response is a list of content blocks, not one message.

func main() {
	// ...

	companyInformation, err := extractWebsiteContent(website)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		return
	}

	result, err := extractCompanyInformation(companyInformation)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		return
	}

	fmt.Println("Result: " + result)
}

The main function sequences the two calls: fetch and convert the page, then extract the structured data. Each step returns an error that terminates the process cleanly. The result is printed directly — at this stage it is already valid JSON conforming to our schema, ready to be unmarshalled into a Go struct by any consuming code.

Please provide the company's website URL:

https://thinksurance.de/kontakt/

Duration: 6.91785975s

Result: {
  "company":{
    "address":{
      "city":"Frankfurt am Main",
      "streetName":"Niddastraße",
      "streetNumber":"91"
    },
    "description":"Thinksurance is a digital platform for commercial insurance, providing software solutions (Advisory Suite, ConsultDirect, Data Suite) that support brokers, industrial brokers, pools and sales networks, insurers, underwriting agencies, banks and savings banks, and digital business models in advising, comparing, and placing commercial and industrial insurance policies.",
    "name":"Thinksurance GmbH"
  }
}

Process finished with the exit code 0

The output is a clean JSON object with the company information extracted from a German-language page — delivered in English, as instructed. The model correctly identified that website is in German, followed the language instruction in the system message, and returned the address in the exact structure the schema required.

What is output_config.effort?
#

The effort field inside output_config controls how much internal reasoning — thinking — the model does before generating its response. It is available on Claude’s Opus and Sonnet models; Haiku does not support it. The options are low, medium, high, and on the newest models xhigh and max. Higher effort means the model thinks longer, produces more thorough analysis, and handles complex or ambiguous tasks better. Lower effort is faster and cheaper. For straightforward extraction tasks with a well-defined schema, high effort is wasted compute.

This trade-off becomes concrete when you add timing to the same extraction request we built above.

func extractCompanyInformation(companyInformation string) (string, error) {
	start := time.Now()

	resp, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeOpus5,
		MaxTokens: 4096,
		System: []anthropic.TextBlockParam{
			{Text: "You are a legal advisor. " +
				"Given a company's information, extract the company's name, description, and address." +
				"You must provide company description in English"},
		},
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(companyInformation)),
		},
		OutputConfig: anthropic.OutputConfigParam{
			Effort: anthropic.OutputConfigEffortLow,
			Format: anthropic.JSONOutputFormatParam{
				Schema: jsonSchema,
			},
		},
	})
	duration := time.Since(start)
	// ...
}

Adding time.Now() before the call and time.Since(start) after gives us the wall-clock duration of the API round-trip. With claude-opus-5 and Effort set to Low, the baseline looks like this:

Baseline Duration

Please provide the company's website URL:

https://thinksurance.de/kontakt/

Duration: 5.514082916s

Result: {
  "company":{
    "address":{
      "city":"Frankfurt am Main",
      "streetName":"Niddastraße",
      "streetNumber":"91"
    },
    "description":"Thinksurance is a digital platform for commercial insurance, providing software solutions (Advisory Suite, ConsultDirect, Data Suite) that support brokers, industrial brokers, pools and sales networks, insurers, underwriting agencies, banks and savings banks, and digital business models in advising, comparing, and placing commercial and industrial insurance policies.",
    "name":"Thinksurance GmbH"
  }
}
Process finished with the exit code 0

Around 5.5 seconds for a simple extraction — reasonable.

Now let’s switch to claude-sonnet-4-6 and drop the explicit Effort override, leaving it at its default.

func extractCompanyInformation(companyInformation string) (string, error) {
	start := time.Now()

	resp, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeSonnet4_6,
		MaxTokens: 4096,
		System: []anthropic.TextBlockParam{
			{Text: "You are a legal advisor. " +
				"Given a company's information, extract the company's name, description, and address." +
				"You must provide company description in English"},
		},
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(companyInformation)),
		},
		OutputConfig: anthropic.OutputConfigParam{
			Format: anthropic.JSONOutputFormatParam{
				Schema: jsonSchema,
			},
		},
	})
	duration := time.Since(start)
	// ...
}

The only change is the model and dropping the Effort override. Same task, same schema, same system prompt. The result is noticeably slower — around fourteen seconds.

Please provide the company's website URL:

https://thinksurance.de/kontakt/

Duration: 13.965781239s

Result: {
  "company": {
    "address": {
      "city": "Frankfurt am Main",
      "streetName": "Niddastraße",
      "streetNumber": "91"
    },
    "description": "Thinksurance GmbH is a company offering...",
    "name": "Thinksurance GmbH"
  }
}
Process finished with the exit code 0

Effort defaults to High when the field is left unset, and for a task as clear-cut as extracting a company name and address from a schema, the model is doing far more internal work than the task requires. The output quality is no better; the time cost is far higher.

The fix is one line: set Effort to Low.

func extractCompanyInformation(companyInformation string) (string, error) {
	start := time.Now()

	resp, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeSonnet4_6,
		MaxTokens: 4096,
		System: []anthropic.TextBlockParam{
			{Text: "You are a legal advisor. " +
				"Given a company's information, extract the company's name, description, and address." +
				"You must provide company description in English"},
		},
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(companyInformation)),
		},
		OutputConfig: anthropic.OutputConfigParam{
			Effort: anthropic.OutputConfigEffortLow,
			Format: anthropic.JSONOutputFormatParam{
				Schema: jsonSchema,
			},
		},
	})
	duration := time.Since(start)
	// ...
}

Here Effort is set to Low, telling the model to spend less on internal reasoning for this request.

Please provide the company's website URL:

https://thinksurance.de/kontakt/

Duration: 4.161644584s

Result: {
  "company": {
    "address": {
      "city": "Frankfurt am Main",
      "streetName": "Niddastraße",
      "streetNumber": "91"
    },
    "description": "Thinksurance GmbH is a company offering...",
    "name": "Thinksurance GmbH"
  }
}
Process finished with the exit code 0

Back to 4.1 seconds, with identical output quality. To be clear, the reasoning is not bad — it is that reasoning should match the task. For complex analysis, multi-step problem solving, or ambiguous requirements, higher output_config.effort earns its cost. For deterministic extraction with a well-defined schema, Low is the right setting. Matching the effort level to the task is one of the more impactful tuning decisions you can make when running LLMs in production at volume.

Conclusion
#

The Messages API gives you more control than most developers use. output_config.format with a JSON Schema turns the model into a reliable data extraction tool — no parsing hacks, no fragile string matching, just structured output you can trust. output_config.effort lets you tune the cost-latency trade-off for models that think by default, and the difference between the wrong and right setting can be substantial. Neither parameter requires complex code changes; both have an immediate effect on what the model produces and how quickly it produces it.

Useful Resources
#

LLM and Go - This article is part of a series.
Part 5: This Article

Related

LLM and Go: Anthropic Integration via Messages API

·2757 words·13 mins· loading · loading
The previous two articles in this series covered OpenAI’s side of this problem: Chat Completions, where the client owns the entire conversation history, and Responses, where OpenAI’s servers do. Anthropic’s Claude models are built on a third set of API decisions — close enough to Chat Completions in shape that the same agent design carries over almost directly, but different enough in the details, like how the system prompt is passed and how a response is structured, that it is worth building the same agent again to see exactly where. This article rebuilds that agent on Anthropic’s Messages API — the stateless, request-based interface behind Claude. By the end, you will have a working conversational agent that can call external tools to answer questions it otherwise could not, and a clear picture of what changes when you swap providers. A short introduction to Claude and Anthropic # The path to large language models runs through a decade of incremental progress in deep learning. Early models like word2vec and GloVe learned to embed words into dense vector spaces, capturing semantic relationships between terms. The transformer architecture, introduced by Google in 2017, changed the trajectory of the field — it processes sequences in parallel using attention mechanisms that capture long-range dependencies far more effectively than recurrent networks. This architectural shift made it practical to train models on orders of magnitude more data. GPT-1 in 2018 showed that large-scale unsupervised pre-training followed by fine-tuning could match or beat purpose-built models across a range of language tasks. Understanding what these models actually do removes a lot of the mysticism around them. An LLM is, at its core, a next-token predictor. It takes a sequence of tokens as input and outputs a probability distribution over the vocabulary for the next token. The transformer’s attention mechanism allows every token in the input to attend to every other token, building a rich contextual representation before making that prediction. Training adjusts billions of parameters to minimise prediction error across enormous text corpora. What emerges is a model with broad world knowledge encoded in its weights — not because it was taught facts directly, but because predicting text well requires internalising the structure of the world that produced that text. Claude is Anthropic’s model family, and the company itself was founded in 2021 by a group of former OpenAI researchers, including Dario Amodei and Daniela Amodei, with AI safety as its founding focus. What differentiates Claude’s training pipeline from a pure RLHF approach is Constitutional AI — a technique where the model critiques and revises its own outputs against a written set of principles, rather than relying solely on human raters to judge every response. The goal is a model that behaves predictably even in situations no human rater explicitly labelled. For developers, the way to reach any Claude model programmatically is the Messages API — the stateless, request-based interface used throughout this article.

LLM and Go: Investigating OpenAI Chat Completions API

·2132 words·11 mins· loading · loading
In the previous article I covered the fundamentals of the Chat Completions API: setting up a client, maintaining conversation history, and integrating tools. That was enough to build a working conversational agent. This article goes a level deeper — into the API parameters that shape what the model returns and how it thinks. Two parameters stand out as particularly useful in production: response_format and reasoning_effort. The first gives you control over the structure of the model’s output. The second controls how much the model reasons before responding — which turns out to matter more than you might expect once you start caring about latency and cost. Chat Completions API details # The Chat Completions API endpoint accepts a rich set of parameters. Most have sensible defaults and you will rarely touch them, but understanding what is available saves you from reaching for workarounds that already exist in the API. The table below covers the current non-deprecated parameters from the API reference: Parameter Type Description model string ID of the model to use messages array Conversation history as an ordered list of messages response_format object Output format: text, json_object, or json_schema reasoning_effort string Reasoning intensity for reasoning models: low, medium, high temperature number Sampling temperature from 0 to 2; higher values produce more random output top_p number Alternative to temperature; nucleus sampling probability mass max_completion_tokens integer Maximum tokens the model may generate in the response n integer Number of completion choices to return stream boolean Stream partial responses as server-sent events stop string/array Sequences at which the API stops generating presence_penalty number Penalises new tokens based on whether they appear in the text so far frequency_penalty number Penalises new tokens based on their frequency in the text so far tools array List of tools (functions) the model may call tool_choice string/object Controls which tool the model calls seed integer Seed for deterministic sampling user string Unique identifier for the end user In this article we focus on response_format and reasoning_effort — two parameters with a direct, visible impact on production systems. Information extraction with response_format # The response_format parameter controls how the model structures its output. The default is plain text. Setting it to json_object tells the model to return valid JSON, but gives you no control over the schema. Setting it to json_schema goes further: you provide a JSON Schema document and the model guarantees its output will conform to it. OpenAI calls this structured output.

LLM and Go: OpenAI Integration via Chat Completions API

·2721 words·13 mins· loading · loading
For most of my career, integrating external intelligence into an application meant calling a rules engine, training a custom classifier, or encoding business logic that someone had painfully documented in a spreadsheet. The idea that I could describe a task in plain language and have a model respond with genuine reasoning was not something I expected to become production-ready in my working life. Then GPT happened, and it changed what backend developers need to know. This article is the first in a series on using LLMs in Go. We start with the OpenAI Chat Completions API — the stateless, request-based interface that gives you direct control over every aspect of the conversation. By the end, you will have a working conversational agent that can call external tools to answer questions it otherwise could not. A short introduction to ChatGPT and OpenAI # The path to large language models runs through a decade of incremental progress in deep learning. Early models like word2vec and GloVe learned to embed words into dense vector spaces, capturing semantic relationships between terms. The transformer architecture, introduced by Google in 2017, changed the trajectory of the field — it processes sequences in parallel using attention mechanisms that capture long-range dependencies far more effectively than recurrent networks. This architectural shift made it practical to train models on orders of magnitude more data. GPT-1 in 2018 showed that large-scale unsupervised pre-training followed by fine-tuning could match or beat purpose-built models across a range of language tasks. Understanding what these models actually do removes a lot of the mysticism around them. An LLM is, at its core, a next-token predictor. It takes a sequence of tokens as input and outputs a probability distribution over the vocabulary for the next token. The transformer’s attention mechanism allows every token in the input to attend to every other token, building a rich contextual representation before making that prediction. Training adjusts billions of parameters to minimise prediction error across enormous text corpora. What emerges is a model with broad world knowledge encoded in its weights — not because it was taught facts directly, but because predicting text well requires internalising the structure of the world that produced that text. ChatGPT is OpenAI’s conversational product built on the GPT model series. What set it apart from raw GPT-3 was the addition of reinforcement learning from human feedback (RLHF) — a technique that fine-tunes the base model to follow instructions and produce responses that human raters judge as helpful and safe. When ChatGPT launched in late 2022, it became one of the fastest-adopted consumer products in history. For developers, the more relevant artefact is the API behind it — specifically the Chat Completions API, which gives programmatic access to the same models powering the product.