●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Don't make Gemini judge your AdMob report — confine structured output to extraction
When deciding AdMob floors (eCPM thresholds), letting Gemini make the decision itself is dangerous. Confine structured output to 'extracting a messy report into typed data,' and keep the threshold judgment in deterministic code — here is the reasoning and implementation, with the actual decision rules from running 42 groups.
When automating the job of setting AdMob floors (eCPM thresholds) from a report, the first idea most people reach for is "hand the whole report to Gemini and have it answer the new floor for each group." I started down that path too, and stopped almost immediately. Letting an LLM do the threshold arithmetic and judgment makes that judgment unauditable. Setting a floor is an irreversible operation tied directly to revenue, and any mechanism where you cannot later reproduce "why this value" must not be put into operation.
When you run apps solo, most of your revenue ends up resting on AdMob mediation, which makes reviewing floors an unavoidable monthly chore. I keep 42 mediation groups across iOS and Android, and since you cannot move those by gut feel, I have gradually pushed the judgment into a mechanism. What settled it for me through that process is that Gemini's structured output is strong not at "judging" but at "extracting." This article shows the design that confines structured output to the extraction step and separates the judgment into code, together with the actual decision rules.
Why you must not let the LLM do the judging
The floor decision rules themselves are, in fact, fully deterministic. The rules I use are these. On iOS, the floor is based on actual eCPM × 55%; if the ratio of current floor / eCPM exceeds 65%, lower it; if the ratio is under 40% and the match rate exceeds 95%, raise it; the middle (40–65%) holds. The minimum unit is $0.50. Android is a flat $0.50, not a ratio of actuals.
These rules complete with arithmetic and threshold comparisons alone. That is, there is no room for an LLM to "decide." And yet, passing the whole ruleset to an LLM via prompt produces three problems. First, output wavers at the threshold edges (e.g., when the ratio is exactly 65.0%). Second, even on the same input it may return subtly different values per run, breaking reproducibility. Third, and biggest: you cannot verify, from the output, the basis for a judgment like "hold because ratio is 64%." With code, anyone can trace the single line ratio = floor / ecpm; an LLM's internal reasoning cannot be traced.
A mis-set floor causes no-fill if too high (fill rate drops) and collapses eCPM if too low. In fact, I once had Android INT floors set excessively high relative to actuals, which capped the match rate in the low 70% range. To verify and fix this kind of "too strong / too weak" afterward, the judgment must be deterministic.
So what do you let Gemini do — only "extraction"
On the other hand, when you receive an AdMob report as CSV or pasted text, the data is astonishingly messy. Inconsistent group-name notation, currency symbols present or absent, a match rate written as "47.53%" one time and "0.4753" another, column order shifting with export settings. Tidying this messy input into uniformly typed rows is exactly where Gemini's structured output is overwhelmingly strong.
The trick here is to include no "decision result" whatsoever in the output schema. Define only the raw observations in the schema (group name, group ID, actual eCPM, match rate, current floor), and never put the new floor or a "raise/lower" judgment into it. You confine the LLM's responsibility to "messy input → typed observations."
import osfrom google import genaifrom pydantic import BaseModelclass GroupRow(BaseModel): group_name: str group_id: str ecpm_usd: float # actual eCPM (observation only) match_rate: float # normalized to 0.0-1.0 current_floor_usd: floatclass ExtractedReport(BaseModel): rows: list[GroupRow]client = genai.Client(api_key=os.environ["YOUR_GEMINI_API_KEY"])raw_report = open("admob_mediation_report.txt", encoding="utf-8").read()resp = client.models.generate_content( model="gemini-2.5-pro", contents=( "Extract the following AdMob mediation report, normalized to one row per group. " "Convert match_rate to 0.0-1.0. Do NOT output any judgment or recommended value. " "Observations only.\n\n" + raw_report ), config={ "response_mime_type": "application/json", "response_schema": ExtractedReport, },)report: ExtractedReport = resp.parsed
The key is stating in the prompt, too, "Do NOT output any judgment or recommended value." Bound by the schema and limited in natural language as well, Gemini works stably as an "extractor."
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Why and how to split: confine Gemini structured output to 'extraction' and keep threshold judgment in code
✦A deterministic implementation of the real 42-group floor decision rules (eCPM×55%, ratio thresholds)
✦How letting an LLM compute thresholds becomes unauditable, and the typed-schema design that avoids it
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Put judgment in code — an auditable decision function
Against the extracted, typed data, judgment happens in an ordinary Python function. This is the crux of auditability.
from dataclasses import dataclass@dataclassclass FloorDecision: group_id: str current: float proposed: float action: str # "raise" / "lower" / "hold" reason: str # keep the basis as a string (for audit)def decide_floor_ios(row) -> FloorDecision: ratio = row.current_floor_usd / row.ecpm_usd if row.ecpm_usd else 0 target = round(row.ecpm_usd * 0.55 / 0.5) * 0.5 # 55% basis, $0.50 steps if ratio > 0.65: return FloorDecision(row.group_id, row.current_floor_usd, max(target, 0.5), "lower", f"ratio {ratio:.0%} > 65%") if ratio < 0.40 and row.match_rate > 0.95: return FloorDecision(row.group_id, row.current_floor_usd, max(target, 0.5), "raise", f"ratio {ratio:.0%} < 40% & mr {row.match_rate:.0%} > 95%") return FloorDecision(row.group_id, row.current_floor_usd, row.current_floor_usd, "hold", f"ratio {ratio:.0%} in 40-65% band")decisions = [decide_floor_ios(r) for r in report.rows]
Keeping the basis as a string in the reason field is the point. If you are later asked "why was this group a hold," a mechanical basis like ratio 64% in 40-65% band comes out instantly. Had you let an LLM do it, you would never get that one line. When you want to change the logic, you just write a test and fix the function — no need to "grow" a prompt.
How to guarantee the extraction is correct
Even with judgment shifted into code, it is all for nothing if the extraction is wrong. So pass the extracted result through light sanity checks on the code side.
def sanity_check(row) -> list[str]: issues = [] if not (0.0 <= row.match_rate <= 1.0): issues.append(f"{row.group_id}: abnormal match_rate {row.match_rate}") if row.ecpm_usd < 0 or row.ecpm_usd > 200: issues.append(f"{row.group_id}: abnormal eCPM {row.ecpm_usd}") if row.current_floor_usd < 0: issues.append(f"{row.group_id}: negative floor") return issues
If the match rate exceeds 1.0, Gemini likely put "47.53%" in as 47.53. An abnormal eCPM suggests a misread currency symbol. Extraction by the LLM, validation by code — the same division as the previous section: the LLM for fuzzy input, code for deterministic verification. Operational filters, like excluding groups with fewer than 20 impressions from judgment, are also made explicit here as code.
Reconcile whether the extracted rows cover every group
Alongside per-row sanity checks, the one I added after getting burned in real operation is reconciling the row count itself. Gemini's extraction fails not only on value correctness but on dropping groups. If the tail of the report is cut off, or two similarly named groups get merged into one row, the 42 groups that should exist come back as 41 rows. Every value looks plausible, so the previous section's sanity check waves it through.
So after extraction, reconcile against "the set of group IDs I expect."
EXPECTED_GROUP_IDS = {...} # hold the live group IDs as a constantdef reconcile_rows(report) -> list[str]: got = {r.group_id for r in report.rows} missing = EXPECTED_GROUP_IDS - got unexpected = got - EXPECTED_GROUP_IDS issues = [] if missing: issues.append(f"dropped: {sorted(missing)}") if unexpected: issues.append(f"unknown group_id: {sorted(unexpected)}") if len(report.rows) != len({r.group_id for r in report.rows}): issues.append("duplicate group_id (one group may be split into two rows)") return issues
missing is a drop, unexpected is the trace of a notation variant mistaken for a different group, and a duplicate is the sign of one group split into two rows. Group composition barely changes month to month, so simply holding the known set as a constant catches almost any anomaly in the extraction "count." Only with both stages — value verification (previous section) and count reconciliation (this section) — is the extracted result ready to hand to judgment.
Leave the write to the human — designing the pipeline's terminus
Even after extraction and judgment, the final move of typing the floor into AdMob's edit screen I leave to the human. AdMob's edit screen has dynamic button IDs, and auto-entry is prone to misfiring into the wrong field. The pipeline's output stops at "a list of proposed values and bases per group"; the human reads it and enters them. After entry, the actual value on the edit screen is read back by the same mechanism as the extraction and reconciled against the proposed value.
This terminus design yields a pipeline where each step's responsibility is cleanly separated: "Gemini extracts → code judges → human writes → code reconciles the actual value." The LLM is involved only in the first extraction, while the revenue-critical judgment and the irreversible write are held by code and the human respectively. This is the basic form I recommend for automation that handles revenue data.
Where this design applies
Finally, this "separate extraction from judgment" design applies beyond AdMob floor decisions. Any task that meets the condition — the decision rules can be written deterministically, while the input data is messy — becomes a fast, auditable automation when you extract with structured output and place judgment in code. Tasks where the judgment itself is fuzzy, like review sentiment classification, lean to the LLM; judgments decided by thresholds or arithmetic lean to code. Deciding this line first is, I think, the starting point of design when embedding an LLM into operations.
Start by reviewing one prompt you are sending to an LLM and separating "is this extraction, or judgment?" Just moving the judgment part into code should sharply raise the reproducibility and auditability of the output.
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.