GEMINI LABJP
ROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variantsVIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still runningDEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks leftAPIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service accountPRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figuresAUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 termsROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variantsVIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still runningDEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks leftAPIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service accountPRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figuresAUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 terms
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-api283logprobsclassification2confidence-scoringpython104prompt-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. In this article 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 in this article 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 $15 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-04-15
Designing a Production Prompt Management System for Gemini API — Versioning, A/B Testing, and Canary Rollouts
A complete implementation guide for solving the prompt versioning, attribution, and safety challenges in production Gemini API deployments — using FastAPI, PostgreSQL, Redis, A/B testing, and canary rollouts.
API / SDK2026-03-25
Building a Prompt Evaluation & Optimization Pipeline with Gemini API — Automated Quality Scoring with LLM-as-Judge
Learn how to build a prompt evaluation pipeline using Gemini API. Covers the LLM-as-Judge pattern, A/B testing prompts, automated quality scoring, and cost-quality optimization for production systems.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →