Skip to content

title: Control sampling and reasoning effort description: Temperature, TopP and Effort: what each does, which models still accept them, and why a refused setting now surfaces at construction rather than in traffic. tags: [how-to, sampling, effort, capabilities]


Control sampling and reasoning effort

By default the model generates however the provider's defaults say. Three Config fields let you influence that: Temperature, TopP and Effort.

They are all nil/empty by default, and leaving them alone is the right choice for most work. Reach for them when you are doing something where variance itself is the problem: extraction against a fixed document, classification you intend to measure, or a judging pass whose consistency you need to defend.

Reduce variance for extraction

cfg := chat.Config{
    Provider:       chat.ProviderGemini,
    ResponseSchema: chat.GenerateSchema[Extraction](),
    Temperature:    ptr(0.0), // sample as narrowly as the provider allows
}

Spend more reasoning on a hard problem

cfg := chat.Config{
    Provider: chat.ProviderClaude,
    Effort:   chat.EffortHigh,
}

What temperature actually does

The model produces a score for every token in its vocabulary, and temperature rescales those scores before they become probabilities:

p(i) = exp(z_i / T) / Σ exp(z_j / T)
  • T below 1 exaggerates the gaps, so likely tokens become likelier and the tail collapses. Output gets focused, and repetitive at the extreme.
  • T above 1 compresses the gaps, so unlikely tokens get a real chance. Output gets varied, then incoherent.
  • T approaching 0 approaches always taking the top token.

TopP is the sibling control: sort tokens by probability, keep adding until the cumulative mass reaches p, discard the rest. It truncates the tail rather than reshaping the curve. Providers recommend adjusting one or the other, not both, because they interact in ways that are hard to reason about.

Low temperature is not determinism

This matters more than anything else on this page.

Even at T=0 the same prompt can produce different output across runs. Floating-point addition is not associative, so a request batched alongside different traffic can reduce the same scores in a different order and differ in the last bits. Where two candidate tokens are nearly tied that is enough to flip which one wins, and a single flipped token early in a response changes everything after it.

So pinning the sampler narrows the distribution you are drawing from. It does not collapse it to a point.

That still buys you something specific and useful. It separates two causes of instability that otherwise look identical:

  • Sampling variance. The model was genuinely uncertain and the sampler chose differently each time. Lowering temperature shrinks this.
  • Genuine ambiguity. Your prompt or your document does not determine one answer. Lowering temperature does not fix this. It makes the same near-tie land more consistently on whichever side is marginally ahead, which looks like a fix while hiding the real problem.

If output is still unstable at T=0, the prompt is the thing to change.

What effort does

Effort selects how much reasoning the model does before answering. It is an ordinal ladder:

chat.EffortLow      // least the provider will accept
chat.EffortMedium
chat.EffortHigh
chat.EffortXHigh
chat.EffortMax      // most the provider will accept

An ordinal is genuinely portable in a way a number is not: low means the same thing on every provider, whereas 0.5 means different things on a 0–1 scale and a 0–2 one.

Effort is the most direct cost dial in Config. Reasoning tokens are billed, and Max on a reasoning model can multiply output substantially. Watch it with the field that already reports it:

_, _ = client.Chat(ctx, prompt)
fmt.Println(client.Usage().ReasoningTokens)

Measure rather than assume. The multiplier varies by provider, by model, and by how hard your prompt actually is.

Which providers take which

Provider Temperature / TopP Effort
gemini ✅ 0–2 / 0–1 xhigh and max clamp to its highest level
openai ⚠️ 0–2 / 0–1, model-dependent ✅ all five
claude ⚠️ 0–1, model-dependent ⚠️ model-dependent
claude-local ❌ never ✅ all five

None of the default models accepts temperature or top-p

The defaults are claude-opus-5, gpt-5.6-sol and gemini-3.7-flash, and only Gemini's still takes sampling controls. claude-opus-5 deprecates temperature; gpt-5.6-sol permits only its default of 1 and rejects top-p outright, where the previous default gpt-5.4 accepted both.

So if you set Config.Temperature or Config.TopP without also setting Config.Model, chat.New reports it: the setting is dropped and named in the returned error, and you still get a working client. Use Config.Effort instead (it is supported everywhere and is what these models expect to be steered with) or name an older model explicitly.

Support is per model, not per provider

The awkward part, and worth understanding before you hit it.

Whether a model accepts these controls is a property of the model, not the provider. Two models from the same vendor can differ completely:

  • claude-sonnet-4-5 accepts temperature and rejects effort
  • claude-opus-5 accepts effort and rejects temperature
  • gpt-5.4 accepts temperature and top-p; gpt-5.6-sol rejects both

Vendors are replacing sampling controls with effort controls on reasoning models. It is tempting to read that as a clean generational cutover you could predict from a version number, but the measured OpenAI data says otherwise:

Model Temperature / TopP
gpt-5, gpt-5-mini, gpt-5-nano
gpt-5.1gpt-5.4-pro
gpt-5.5
gpt-5.5-pro
gpt-5.6-sol / -terra / -luna

Support goes away, comes back for three generations, then goes away again, and gpt-5.5-pro accepts what plain gpt-5.5 refuses. There is no rule here to infer, which is why the module measures each model rather than guessing from its name. So there are two different failures, and they want different fixes:

Construction error: the provider has no such concept at all.

_, err := chat.New(ctx, chat.Settings{Config: chat.Config{
    Provider:    chat.ProviderClaudeLocal,
    Temperature: ptr(0.2),
}})
// error: provider claude-local cannot carry Config.Temperature or Config.TopP

The claude CLI has no temperature flag at any version, so this can never work. Fix: change provider, or drop the field.

Construction error: this model is known not to accept it.

Each provider module ships a generated table of what its models accept. When that table says no for the model you selected, chat.New drops the setting and tells you, rather than letting the request fail later:

client, err := chat.New(ctx, chat.Settings{Config: chat.Config{
    Provider:    chat.ProviderOpenAI,
    Temperature: ptr(0.2),
}})
// client is non-nil and usable; Temperature was not applied
// error: model gpt-5.6-sol (the openai default) does not accept Config.Temperature

You still get a client. Only the unsupported setting is dropped; everything else you configured is applied. Inspect what went, and act on it or don't:

var cfgErr *chat.ConfigErrors
if errors.As(err, &cfgErr) {
    for _, d := range cfgErr.Dropped() {
        log.Warn("setting not applied", "fields", d.Fields, "why", d.Reason)
    }
}

Request error: the model is not in the table.

A model too new to have been measured reports unknown, not no. The module does not guess: it applies your setting and lets the provider answer.

_, err := client.Chat(ctx, "…")
if errors.Is(err, chat.ErrModelRejectedParameter) {
    // this model will not take the value that was set
}

Fix: change model, or drop the field. The underlying provider error is still reachable by unwrapping if you want the vendor's own wording.

That split is what keeps a stale table from doing harm. A table that is merely behind costs you nothing. An unmeasured model behaves exactly as it did before the tables existed. Only a confident no changes anything.

Nothing is dropped silently. A setting is either applied, or it is named in the error chat.New returns. A control that is silently ignored is indistinguishable from one that does nothing, and you would have no way to tell which you were looking at.