Skip to main content

Make Google Gemini structured output actually validate against your Pydantic models — fixes the anyOf/enum drop, ignored numeric & length bounds, and degraded array tails.

Project description

gemini-coax

Make Google Gemini structured output actually validate against your Pydantic models.

PyPI Python License: MIT

Gemini's response_json_schema promises structured output, then quietly breaks its own promise. It enforces shape (types, properties, required) but silently ignores value-level constraints — so the model hallucinates enum values, blows past your numeric bounds, and trails off into half-formed objects at the end of long arrays. Pydantic then rejects the entire response over one bad field.

gemini-coax coaxes the output back into shape. No retries, no extra LLM calls for the common cases — just targeted repair at the validation seam.

If you've hit any of these, this library is for you:

  • ValueError: AnyOf is not supported in the response schema for the Gemini API
  • Input should be 'a', 'b' or 'c' [type=literal_error] on a value the schema defined
  • A nullable Literal[...] | None field where Gemini invents values off-menu
  • ge/le/max_length/max_items constraints ignored, failing validation
  • Empty {} or truncated objects at the tail of a long list, killing the whole array

Install

pip install gemini-coax                  # core — pure, depends only on pydantic
pip install "gemini-coax[langchain]"     # + the drop-in ChatGoogleGenerativeAI

Use it — LangChain (langchain-google-genai)

Swap ChatGoogleGenerativeAI for GeminiSafe. That's the whole change. Every with_structured_output() call is now coaxed; no edits in your chains.

from typing import Literal
from pydantic import BaseModel, Field
from gemini_coax import GeminiSafe          # was: ChatGoogleGenerativeAI

class Finding(BaseModel):
    label: Literal["bug", "smell", "nit"] | None   # nullable enum — Gemini drops the enum
    severity: int = Field(ge=1, le=5)              # bounds Gemini ignores

class Report(BaseModel):
    findings: list[Finding]                        # long array → degraded tail

llm = GeminiSafe(model="gemini-2.5-flash", temperature=0)
report = llm.with_structured_output(Report).invoke("Review this diff: ...")
# Validates. The anyOf-enum is stripped before send, out-of-range
# severities are clamped, and a broken trailing finding is salvaged away.

It also retries transient transport faults (ConnectionResetError, aiohttp ClientOSError, ServerDisconnectedError) that the google-genai SDK leaves uncaught — at the single async seam every call funnels through.

Use it — raw google-genai SDK (no LangChain)

One call. Hand it the decoded dict and your model:

from gemini_coax import coax

raw = json.loads(response.text)     # whatever Gemini gave you
report = coax(raw, Report)          # clamp → fill nullables → validate → repair enums → salvage lists

Or compose the pieces yourself:

from gemini_coax import (
    strip_nullable_anyof,   # rewrite the schema BEFORE you send it
    clamp_to_constraints,   # clamp ignored numeric / length / array bounds
    fill_missing_nullables, # inject None for nullables Gemini omitted
    repair_enums,           # fuzzy-match close-but-wrong enum values
    salvage_lists,          # drop broken tail entries, keep the valid ones
)

schema = strip_nullable_anyof(Report.model_json_schema())   # send THIS to Gemini

What it does

Gemini misbehavior gemini-coax response
Drops enum inside anyOf (nullable Literal) → hallucinated values strip_nullable_anyof rewrites the schema to a plain enum + drops it from required before send
Ignores ge/le/gt/lt, max_length, max_items clamp_to_constraints clamps raw values to the model's field metadata
Omits a now-optional nullable field fill_missing_nullables injects None so re-validation passes
Close-but-wrong enum at the array tail ("defensiveness" vs "defensiveness-tone") repair_enums fuzzy-matches it back (zero-cost difflib)
Empty {} / truncated objects when the token budget runs out salvage_lists validates entries individually, keeps the good ones
Transient transport fault before any HTTP status GeminiSafe retries with exponential backoff + jitter

A full-chain retry is 100–300× more expensive than these repairs — and often makes things worse. Repair beats re-roll.

Design

Two layers, so the value isn't hostage to any framework's release notes:

  • Core (gemini_coax.schema, gemini_coax.repair, coax) — pure functions over dict + Pydantic. Only dependency is pydantic. Works with the raw SDK, Vertex AI, or anything that hands you a dict.
  • Adapter (gemini_coax.langchain.GeminiSafe) — the LangChain drop-in. Pulled in only by the [langchain] extra; pins langchain-google-genai>=4.2,<5.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gemini_coax-0.1.0.tar.gz (14.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gemini_coax-0.1.0-py3-none-any.whl (15.0 kB view details)

Uploaded Python 3

File details

Details for the file gemini_coax-0.1.0.tar.gz.

File metadata

  • Download URL: gemini_coax-0.1.0.tar.gz
  • Upload date:
  • Size: 14.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gemini_coax-0.1.0.tar.gz
Algorithm Hash digest
SHA256 df9f66f50d4eb3e18a48ce4c899096c9a5bf7419de2c9072c9ce105859742134
MD5 677a1ee4fcde142d972c246cf36e0e1c
BLAKE2b-256 8c43677848cda7c97a0c0cd0165b188cbe3921bc106f24b23cc25e4923ae9efb

See more details on using hashes here.

Provenance

The following attestation bundles were made for gemini_coax-0.1.0.tar.gz:

Publisher: release.yml on mreza0100/gemini-coax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gemini_coax-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: gemini_coax-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 15.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gemini_coax-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8c40d12ed442591d21043310dbd62db04487320b8e33681e0580e3af837344a8
MD5 105434c5dc4ad7b122d15c766ab3ffa5
BLAKE2b-256 3c96937499168742596de19f4c955011764e993002b87981b227121ba50a0733

See more details on using hashes here.

Provenance

The following attestation bundles were made for gemini_coax-0.1.0-py3-none-any.whl:

Publisher: release.yml on mreza0100/gemini-coax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page