◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-04-26Intermediate

Measuring Classification Confidence with Gemini API Logprobs — A Practical Walkthrough

Use the Gemini API responseLogprobs option to extract per-token confidence scores, then turn them into an auto-vs-review gate for classification — with working Python code and the threshold thinking behind it.

gemini-api286logprobsclassificationconfidence-scoringpython108prompt-engineering16

Picture a simple classification task: you ask Gemini to label an incoming email as "inquiry", "complaint", or "other". You get back a single word. It looks fine on the surface — but you have no idea whether the model picked it with 0.99 confidence or with 0.42, neck-and-neck with another option. The first time I shipped this kind of pipeline as an indie developer, the lack of visibility into that confidence was the single biggest source of stress.

Turning on responseLogprobs puts that confidence number in your hands. Here I'll walk through how to enable logprobs in the Gemini API and, more importantly, how to turn the raw values into an "auto-process or send to a human reviewer" gate that actually holds up in production. The flag itself is one boolean; the design decisions around it are where almost all the leverage lives, and that's what I want to spend most of the words on.

The problem logprobs solves

When Gemini answers "inquiry", was that the safe top choice — or did "complaint" come in a close second? The downstream system you trust to act on the label looks very different in those two worlds.

Logprobs are the natural-log probabilities Gemini assigned to each generated token. With them you can:

  • Route only low-confidence cases to human review, like an active-learning loop
  • Compare prompt A vs. prompt B by looking at confidence on the same inputs
  • Detect "almost right" failures in A/B tests with a real number, not a hunch
  • Auto-process whatever clears your quality gate, and only spend human time on the rest

The implementation cost is surprisingly small for how much it changes downstream design — that was my honest first impression after a few days of using it.

Enabling logprobs in the Gemini API

Here's the smallest useful setup using the google-genai Python SDK. response_logprobs=True turns logprobs on, and logprobs=N asks for the top-N alternatives at each position (1–5).

# pip install google-genai
from google import genai
from google.genai import types
import os
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
 
config = types.GenerateContentConfig(
    response_logprobs=True,    # return logprobs for chosen tokens
    logprobs=3,                # also return top-3 alternatives per position
    max_output_tokens=4,       # one or two tokens is plenty for classification
    temperature=0,             # determinism on 2.x; silently ignored on 3.x (see below)
)
 
resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=(
        "Classify the following email into exactly one of "
        "{inquiry, complaint, other}.\n"
        "Email: My order hasn't arrived. Order number 12345.\n"
        "Answer (one word only):"
    ),
    config=config,
)
 
print(resp.text)
print(resp.candidates[0].logprobs_result)

Three things matter here. First, temperature=0. We want to compare confidence as "how strongly the model preferred this token", and randomness destroys that interpretation. That setting only lands on the 2.x line, though — the 3.x models handle it differently, which I cover further down. Second, cap max_output_tokens low. Most classification answers fit in one or two tokens, and longer outputs just add noise. Third, logprobs=3 is plenty — even when you ask for ten alternatives, you'll mostly look at the top two or three.

Reading the response

The logprobs_result field has two parts: chosen_candidates (the tokens actually emitted) and top_candidates (the top-N alternatives at each step). For classification, you usually only care about the first meaningful token.

import math
 
def label_confidence(resp):
    """Return the chosen label and its probability in [0, 1]."""
    chosen = resp.candidates[0].logprobs_result.chosen_candidates
    if not chosen:
        return None, 0.0
    first = chosen[0]
    prob = math.exp(first.log_probability)
    return first.token, prob
 
label, p = label_confidence(resp)
print(f"label={label!r}  confidence={p:.3f}")
# Example: label='inquiry'  confidence=0.987

log_probability is a natural log, so math.exp() gives you a probability between 0 and 1. Anything around 0.99 is essentially "the model is sure"; once you drop below 0.5, you're in territory where another label was a serious contender.

A practical gate: auto-process when confident, review when not

This is the pattern that paid for itself fastest in my own work. One threshold, one decision, and your automated share goes up or down accordingly.

def classify_with_gate(text: str, threshold: float = 0.85):
    """Auto-accept if confidence >= threshold; otherwise queue for review."""
    resp = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=(
            "Classify the following email into exactly one of "
            "{inquiry, complaint, other}.\n"
            f"Email: {text}\nAnswer (one word only):"
        ),
        config=config,
    )
    label, prob = label_confidence(resp)
 
    if label is None:
        return {"status": "ERROR", "reason": "empty response"}
 
    if prob >= threshold:
        return {"status": "AUTO", "label": label, "confidence": prob}
 
    # Surface the alternatives so the review UI has something useful to show
    top = resp.candidates[0].logprobs_result.top_candidates
    alternatives = [
        (c.token, math.exp(c.log_probability))
        for c in (top[0].candidates if top else [])
    ]
    return {
        "status": "REVIEW",
        "label": label,
        "confidence": prob,
        "alternatives": alternatives,
    }

I'd recommend not picking threshold out of thin air. Run a week or two of real traffic, log every (label, confidence) pair, and look at the distribution. Once you can say "at threshold 0.85 we auto-accept 78% and humans correct 4% of those", every threshold tweak becomes a number, not a vibe.

This pairs nicely with type-safe structured output via Pydantic and the patterns in production rate-limiting for the Gemini API. Logprobs give you the gate; structured output gives you the schema; rate-limit design keeps the lights on.

Calibrating the threshold with 100 examples

"Should I just set the threshold to 0.85?" — the honest answer is that it depends on your domain. What I actually do is pull 100 historical inputs and build a small spreadsheet:

  • Ground truth: the label a human eventually confirmed
  • Model label: what classify_with_gate returned
  • Confidence: the probability of the chosen token

Sort by confidence descending and you'll usually see something like "above 0.95 the error rate is zero", "0.85–0.94 has 2% errors", "0.70–0.84 has 9% errors". Pick the highest confidence band where the error rate is still acceptable for your use case (say, 2%) and you have a defensible threshold backed by your own data — not a number you copied from a blog post.

If no threshold gives you an acceptable error rate, the issue is almost never logprobs. It's usually that the prompt is ambiguous about where one label ends and the next begins. Tighten the label definitions, add a few canonical examples, and retest. Logprobs are a useful instrument on top of a well-defined prompt, not a fix for a fuzzy one.

When logprobs aren't reliable

Logprobs are useful, but they're not a universal "trust this number" knob. Three places I've personally been burned:

First, long generations. Logprobs are per-token, so once your output is ten or more tokens, no single number captures "confident at the start, drifting later". In practice, lean on logprobs only when the answer fits in one to a few tokens.

Second, loose prompts. If your prompt says "classify this casually", the model often emits filler words and your label slips to token two or three. End the prompt with a tight constraint like Answer (one word only): so the first chosen token actually is the label.

Third, temperature behaves differently depending on the model generation. On the 2.x line, any sampling means the chosen token can change between runs and the probability you read reflects sampling rather than the model's stable preference — so keep temperature=0 if a threshold depends on it. On the 3.x line the failure runs the other way: you can set it, and it won't land. That one gets its own section.

A fourth pitfall worth flagging: tokenizer surprises. Gemini's tokenizer doesn't always split on the boundaries you expect. A label like "complaint" might actually be one token in English but split into multiple subword tokens for Japanese labels such as "苦情". When that happens, the "first chosen token" is a fragment, not the full label. The fix is straightforward — read more than the first chosen token if your label can span subwords, or constrain output to a small set of single-token labels (e.g., A/B/C with a legend) so that one position equals one label.

Before you move to 3.x, measure whether temperature and logprobs still work

This is the part that needs the most care right now. The migration checklist for Gemini 3.7 Flash says plainly to strip temperature, top_p, and top_k from your generation configs, and notes that Gemini 3.6 Flash already stopped supporting them (What's new in Gemini 3.7 Flash).

The way it fails is what makes this awkward. Per the Gemini 3.6 Flash model reference, custom values for temperature, top-K, and top-P are simply ignored. Frequency penalty and presence penalty are also unsupported — but those raise an error. Same category of "not supported", two completely different signals: one breaks loudly, the other passes quietly.

Everything here rests on temperature=0. Swap the model ID to a 3.x one and your requests still return 200, and your (label, confidence) CSV still fills up exactly as before. Nothing in the output says the determinism assumption is gone. A config key that gets silently dropped is the kind of bug that eats an afternoon, so it's worth spending five minutes on it at migration time instead.

SettingBehavior on 3.xImpact on threshold gating
temperature / top_p / top_kAccepted and ignored (no error)Determinism assumption quietly disappears. Remove from config
frequency_penalty / presence_penaltyRaises an errorYou will notice this one during migration
candidate_countUnsupported on 3.xRemove from config
response_logprobs / logprobsVaries by modelVerify empirically per target model

Reports on whether response_logprobs comes back at all differ from model to model. Rather than reading specs and hoping, it is faster and more reliable to fire one request at the model ID you plan to move to. The whole premise of this article is deciding by measurement rather than by feel, so the capability check should work the same way.

def probe_model(model_id: str, sample_text: str, runs: int = 8):
    """Measure whether logprobs come back and whether output is stable."""
    prompt = (
        "Classify the following email into exactly one of "
        "{inquiry, complaint, other}.\n"
        f"Email: {sample_text}\nAnswer (one word only):"
    )
 
    # 1. Do we get logprobs at all?
    try:
        resp = client.models.generate_content(
            model=model_id,
            contents=prompt,
            config=types.GenerateContentConfig(
                response_logprobs=True, logprobs=3, max_output_tokens=4
            ),
        )
        has_logprobs = resp.candidates[0].logprobs_result is not None
    except Exception as e:
        return {
            "model": model_id,
            "logprobs": f"ERROR: {type(e).__name__}",
            "deterministic": None,
        }
 
    # 2. Is temperature=0 actually landing? Count distinct outputs for one input.
    outputs = set()
    for _ in range(runs):
        r = client.models.generate_content(
            model=model_id,
            contents=prompt,
            config=types.GenerateContentConfig(temperature=0, max_output_tokens=4),
        )
        outputs.add((r.text or "").strip())
 
    return {
        "model": model_id,
        "logprobs": has_logprobs,
        "deterministic": len(outputs) == 1,
        "distinct_outputs": sorted(outputs),
    }

I keep this probe on the same branch as the commit that rewrites the model ID. A deterministic of False means temperature=0 never reached the model. Be careful with the sample you feed it, though: on a task with few label options, outputs can line up by coincidence even when the parameter is being dropped. Pick a borderline input — a row from your own logs where confidence landed between 0.6 and 0.8 makes an excellent probe. Test with something the model is sure about and you will get a pass that tells you nothing.

If you accept that temperature is off the table on 3.x, determinism has to come from the system instruction instead: spell out the output format and the boundary between labels in words. That is unglamorous work, and it happens to be the same fix as when no threshold gives you an acceptable error rate. Tightening the prompt definition pays off in both directions.

A concrete next step

Before designing anything elaborate, run response_logprobs=True over 100 representative inputs from your actual domain and dump (label, confidence) to a CSV. Plot the histogram. The point where "below this is risky" lives is usually obvious within a few minutes of looking. Once you have that number, drop it into a classify_with_gate-style function and you've meaningfully raised your pipeline's quality without changing models. Logprobs are quiet and unglamorous, but in my experience they're one of the highest-leverage flags in the Gemini API.

One last note from experience: don't try to interpret confidence in absolute terms across models. A 0.92 from gemini-2.5-flash and a 0.92 from gemini-2.5-pro are not directly comparable — different models have different temperature-zero distributions. If you upgrade the model under your gate, recalibrate the threshold against the same 100-example set — and if the upgrade crosses a model generation, run probe_model first. Confirm you can still take the measurement before you spend time re-taking it.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◈ API / SDK2026-04-21
Don't Let Your Gemini Prompts Silently Rot — A Practical Regression Testing Playbook with Pytest
Ever tweaked a prompt and watched production quality quietly degrade? This article walks through testing Gemini API prompts with Pytest, combining snapshot tests and LLM-as-Judge to catch regressions automatically — all from the perspective of an individual developer running things solo.
◈ API / SDK2026-10-16
The Day /models Returns 200 but generateContent Returns 403 — Decide Your Triage Order Before It Happens
A triage order for the case where listing models succeeds with 200 but generation fails with 403 permission_denied. Includes a small diagnostic script that checks key, project, and billing in turn, why you should not retry a 403, and how to assemble what support needs.
◈ API / SDK2026-09-04
Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini
An ambient loop that clicks only at the wrap point. Here is the numeric prescreen I run before sending anything to audio understanding, why an absolute threshold fails, and how AAC encoding quietly rebuilt the seam I had just repaired.