GEMINI LABJP
DESKTOP — Gemini now has a Windows desktop app. It runs on Windows 10 and later, and Alt + Space brings it up from anywhere0.59.0 — This is the current stable Gemini CLI. Versions 0.60 and 0.61 exist only as nightly and preview builds, with no stable tag yetSEPT 30 — Sixteen days left before gemini-omni-flash-preview shuts down. Its successor adds a resolution field to video_config, so swapping the model ID may not be enough429 — Passing an external image or PDF URL through fileData keeps returning 429 for some users at 0.03% of quota. The same request succeeds with text aloneNEW — We edited a draft in Drive, asked again, and Gemini answered from the version before the edit. Here is how its file intake behaves and when to re-attachSAFETY — When translating fiction keeps hitting PROHIBITED_CONTENT, some cases clear with a lower threshold and some never will. Telling the two apart first saves the afternoonDESKTOP — Gemini now has a Windows desktop app. It runs on Windows 10 and later, and Alt + Space brings it up from anywhere0.59.0 — This is the current stable Gemini CLI. Versions 0.60 and 0.61 exist only as nightly and preview builds, with no stable tag yetSEPT 30 — Sixteen days left before gemini-omni-flash-preview shuts down. Its successor adds a resolution field to video_config, so swapping the model ID may not be enough429 — Passing an external image or PDF URL through fileData keeps returning 429 for some users at 0.03% of quota. The same request succeeds with text aloneNEW — We edited a draft in Drive, asked again, and Gemini answered from the version before the edit. Here is how its file intake behaves and when to re-attachSAFETY — When translating fiction keeps hitting PROHIBITED_CONTENT, some cases clear with a lower threshold and some never will. Telling the two apart first saves the afternoon
Articles/Advanced
Advanced/2026-09-14Intermediate

When BLOCK_NONE Changes Nothing: Telling Adjustable Gemini Blocks from the Ones You Can't Move

Some safety blocks go away when you loosen a threshold. Others never will. Here is how to read the response and decide which kind you are looking at before you touch a single setting.

Gemini API240safetySettingserror diagnosiscontent filtersoperations18

I was widening the short descriptions for a ukiyo-e wallpaper app into more languages when a handful of entries came back empty. Every one of them was a warrior print — a battle scene.

My first thought was that the safety filters were tuned too tight. So I listed all four categories in safetySettings, set every one of them to BLOCK_NONE, and ran the same batch again.

Not a single character changed. The same entries came back empty in exactly the same way.

Only then did I open the response object and actually read it. The place where things had stopped was not the place I thought I had loosened.

Separate the prompt side from the response side first

A Gemini response carries block information in two different places, and reading them as one thing is what keeps you circling.

The first is promptFeedback. If blockReason is set there, the input itself was rejected and no candidates come back at all. The reference text for that field reads: "If set, the prompt was blocked and no candidates are returned. Rephrase the prompt." Notice what the documentation recommends first — rewriting the prompt, not adjusting a threshold.

The second is the candidate's finishReason together with its safetyRatings. If generation started and was cut off, the reason lands here. The safety settings guide says that when response content was blocked and finishReason was SAFETY, you can inspect safetyRatings for details.

I put a small function in front of everything else whose only job is to answer that one question.

from google import genai
from google.genai import types
 
client = genai.Client()
 
# SAFETY is the only block reason that comes from the adjustable filters.
ADJUSTABLE = {"SAFETY"}
NORMAL_FINISH = {"STOP", "MAX_TOKENS"}
 
 
def classify_block(response):
    """Return (movability, where it stopped, reason)."""
    fb = getattr(response, "prompt_feedback", None)
 
    if fb is not None and getattr(fb, "block_reason", None) is not None:
        reason = fb.block_reason.name          # e.g. PROHIBITED_CONTENT
        where = "prompt"
    else:
        candidates = response.candidates or []
        if not candidates:
            return ("unknown", "response", "NO_CANDIDATES")
        finish = candidates[0].finish_reason
        reason = finish.name if finish is not None else "UNSPECIFIED"
        where = "response"
        if reason in NORMAL_FINISH:
            return ("not_blocked", where, reason)
 
    movable = "adjustable" if reason in ADJUSTABLE else "core"
    return (movable, where, reason)
 
 
resp = client.models.generate_content(
    model="gemini-3.8-flash",
    contents="(your actual input here)",
)
print(classify_block(resp))

Mine printed ("core", "prompt", "PROHIBITED_CONTENT"). That is the side no threshold reaches.

One detail about safetyRatings is worth knowing before you build logging on top of it: the reference states there is at most one rating per category. You will never get two harassment entries for the same prompt, so a flat dictionary keyed by category is a safe shape to store, and you can compare two runs field by field without worrying about ordering.

The block reasons fall into two layers

The API reference lists six possible values for blockReason. Exactly one of them comes from the filters you can configure.

ValueWhat the docs sayMoves with safetySettings?
SAFETYBlocked for safety reasons; inspect safetyRatings for the categoryYes
PROHIBITED_CONTENTBlocked due to prohibited contentNo
BLOCKLISTBlocked due to terms from the terminology blocklistNo
IMAGE_SAFETYBlocked due to unsafe image generation contentNo
OTHERBlocked for unknown reasonsNo
BLOCK_REASON_UNSPECIFIEDDefault value, unusedNot applicable

The guide states the reason plainly: alongside the adjustable filters, the Gemini API has built-in protections against core harms, such as content that endangers child safety, and those are always blocked and cannot be adjusted.

So safetySettings is not a master switch over all filtering. Outside the four adjustable categories there is a second layer that your configuration simply does not reach.

If you do not know that, you can spend hours moving a dial that was never connected to anything. I spent a night that way.

The default is already Off on Gemini 2.5 and 3, so "loosening" does nothing

There is a line just under the threshold table in the guide that I had skimmed past:

If the threshold is not set, the default block threshold is Off for Gemini 2.5 and 3 models.

The same page explains that because of the model's inherent safety, the additional filters are off by default.

Follow that through and the conclusion is sharper than I expected. If you were blocked without having written any safetySettings at all, the adjustable filters were never engaged in the first place. Adding BLOCK_NONE or OFF on top of that changes nothing, because nothing was on.

That night, listing four categories and rerunning the batch amounted to switching off a switch that was already off.

Thresholds are a tool for tightening, not for loosening. Keeping that one sentence nearby changed the order I work in. When something stops now, I read blockReason before I touch a setting.

One more thing worth writing down, about which categories you can actually pass. The guide's table shows four adjustable filters — harassment, hate speech, sexually explicit, and dangerous content. But the safetySettings field description in the generateContent reference lists six supported harm categories.

CategoryIn the guide's table
HARM_CATEGORY_HARASSMENTYes
HARM_CATEGORY_HATE_SPEECHYes
HARM_CATEGORY_SEXUALLY_EXPLICITYes
HARM_CATEGORY_DANGEROUS_CONTENTYes
HARM_CATEGORY_CIVIC_INTEGRITYNo
HARM_CATEGORY_JAILBREAKNo

The same reference notes that there should not be more than one setting per category, and that any category you leave out falls back to its default. Since the last two are absent from the guide's table, I would verify their behavior in your own project before building on it.

There is also a detail about how blocking is decided that is easy to get backwards. Gemini blocks on the probability that content is unsafe, not on severity. The guide contrasts "The robot punched me" with "The robot slashed me up": the first may score a higher probability, while you would probably judge the second more severe. The instinct that harsher material gets stopped harder does not hold.

When you hit the immovable layer, work on the input

Once PROHIBITED_CONTENT or BLOCKLIST comes back, what is left to you is the input, not the configuration. These are the four moves I go through, in order.

First, stop sending everything at once. This was my real mistake. Thirty descriptions went into a single call, so I had no way of knowing which one was responsible. A binary split finds the offending item in a handful of requests.

def is_blocked(response):
    kind, _where, _reason = classify_block(response)
    return kind in ("adjustable", "core", "unknown")
 
 
def find_offending(items, call):
    """Narrow down which items trigger a block."""
    if not items:
        return []
    if len(items) == 1:
        return list(items) if is_blocked(call(items)) else []
 
    mid = len(items) // 2
    found = []
    for half in (items[:mid], items[mid:]):
        if is_blocked(call(half)):
            found.extend(find_offending(half, call))
    return found
 
 
# `call` takes a list of items and returns a GenerateContentResponse
offenders = find_offending(descriptions, call=run_batch)
print(f"candidates for the cause: {len(offenders)}")

Second, state the purpose in system_instruction. Explaining that this is a historical artwork description, with no intent to promote violence, sometimes gets you through. It will not help against the core layer, though, so I try it once and let it go if it fails.

Third, shift from generation to transformation. Instead of letting the model write freely, have it pick from a vocabulary you supply, or classify rather than compose. Narrowing the output space widens what gets through.

Fourth, and this may matter most: accept that some material will not pass, and hand it back to a person. That is what I decided for those warrior prints. If twenty-odd of thirty entries finished on their own, writing the rest by hand is a fair trade.

Burning a night trying to automate the last few costs more than marking the boundary and moving on. The boundary itself is useful information, too. Once I had the list of entries that would not pass, I could see the pattern in them within a minute of reading — it was not the language, and it was not the length. It was the subject.

Splitting the batch buys you something beyond diagnosis, as well. When one item out of thirty is rejected on the prompt side, you lose all thirty results, because no candidates come back at all. Smaller calls mean a single bad item costs you one result instead of the whole run, which matters a great deal once you are paying for the tokens.

In production, three values are enough to log

If this is going into something you run every day rather than a one-off investigation, you need three values in your logs: blockReason, finishReason, and the per-category safetyRatings.

import json
import logging
 
 
def log_block(response, request_id):
    kind, where, reason = classify_block(response)
    if kind == "not_blocked":
        return
 
    ratings = []
    candidates = response.candidates or []
    source = candidates[0] if candidates else getattr(response, "prompt_feedback", None)
    for rating in (getattr(source, "safety_ratings", None) or []):
        ratings.append({
            "category": rating.category.name,
            "probability": rating.probability.name,
        })
 
    logging.warning(json.dumps({
        "request_id": request_id,
        "movable": kind,      # adjustable / core / unknown
        "where": where,
        "reason": reason,
        "ratings": ratings,
    }))

What matters is reading this as a rate, not case by case. A steady share of core is a property of your subject matter, not a configuration problem. A rising share of adjustable, on the other hand, means there is still room to work on thresholds or prompts.

Splitting the counter by where is worth the extra field as well. Blocks on the prompt side and blocks on the response side call for different responses from you — the first points at your input, the second at how the model was drifting as it wrote. Watching them as one number hides which of the two is actually growing.

Since adding this to my multilingual batch, there is one screen fewer for me to check in the morning. Confirming the numbers are flat is enough.

If you want to go further and design moderation on both the input and output sides, I wrote that up in Designing Production-Grade Safety Controls for the Gemini API.

For now, if you are stuck on a block, start by logging blockReason on one line. Reaching for a threshold makes sense only after that value turns out to be SAFETY. I did it in the opposite order and lost a night to it.

Thank you for reading. I hope this shortens the time you spend narrowing things down.

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

Advanced2026-08-06
The Day the Knowledge Cutoff Moved Forward, the Stale Part Was My System Instruction
When a model's knowledge cutoff advances, the thing that goes stale is not the model — it is the dated assertions in your system instruction. Here is why only the lines written between the two cutoffs flip from helpful to contradictory, plus a working audit script and its measured results.
Advanced2026-07-03
Your Night Batch Is Causing the Morning 429s — Priority Admission Control for a Shared Gemini Quota
When bulk jobs and interactive features share one project's RPM/TPM, the bulk lane wins by default. A priority token bucket design with measurements: 429 rate 3.2% down to 0.03%.
Advanced2026-09-05
Two Kinds of Video Questions: Why I Send "Find It" and "Prove It Isn't There" Down Separate Paths
Agentic video understanding lets the model decide which parts of a video to watch. That works beautifully for finding things, and it quietly breaks when you need to prove something never appears. Here is how I split my questions, and the coverage check I now run first.
📚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