On a machine running google-genai 2.30.1, I broke one letter in a config on purpose. I wrote temprature instead of temperature, the kind of typo your fingers make without asking.
What came back was not an auth error or a network error. I was using a dummy API key, and the call still stopped with a pydantic.ValidationError before any request left the machine.
The first thing I'd like you to take away: that red error is not a bug. It is the SDK telling you, early and cheaply, that it does not know a name you gave it. Finding the cause means comparing one string with the SDK's own list of fields.
The full error, and where it appears
Pass a name that GenerateContentConfig does not define and you get this:
1 validation error for GenerateContentConfig
max_output_token
Extra inputs are not permitted [type=extra_forbidden, input_value=256, input_type=int]The name on the second line (max_output_token here) is the key the SDK has never heard of. One missing s at the end.
Passing a plain dict as config= behaves the same way. With the dummy key, client.models.generate_content(...) raised extra_forbidden before any network call, so nothing is billed and no quota is touched.
When I write small scripts as an indie developer, I tend to keep settings in a dict. A dict gets no editor completion, which means spelling is checked only by my own eyes.
The cause: undefined names are rejected, not ignored
GenerateContentConfig refuses keys it does not define instead of silently dropping them. In my tests it behaved like this:
| Name passed | Result | Correct name |
|---|---|---|
max_output_token | stops with extra_forbidden | max_output_tokens |
temprature | stops with extra_forbidden | temperature |
system_instructions | stops with extra_forbidden | system_instruction |
stop_sequence | stops with extra_forbidden | stop_sequences |
maxOutputTokens (camelCase) | accepted (lands in max_output_tokens) | fine as is |
The last row is the easy one to miss. A camelCase alias is accepted, while a misspelling is not: case may differ, spelling may not. That is probably why a key name you copied works and the one you typed yourself fails.
Check suspicious keys before you call
Instead of scanning the spelling by eye, compare against the field list the SDK already knows. Adding a close match from difflib tells you what to change it to.
import difflib
from google.genai import types
def check_config(cfg: dict) -> list[str]:
"""Return keys in a config dict the SDK doesn't know, with a close match."""
fields = types.GenerateContentConfig.model_fields
known = set(fields)
# camelCase aliases (maxOutputTokens etc.) are accepted, so allow them
aliases = {f.alias for f in fields.values() if f.alias}
problems = []
for key in cfg:
if key in known | aliases:
continue
near = difflib.get_close_matches(key, sorted(known), n=1, cutoff=0.6)
hint = f" (did you mean {near[0]!r}?)" if near else ""
problems.append(f"{key!r} is not defined{hint}")
return problems
cfg = {"temprature": 0.1, "system_instructions": "Answer briefly", "stop_sequence": ["END"]}
print(check_config(cfg) or "OK")Running it here printed:
["'temprature' is not defined (did you mean 'temperature'?)", "'system_instructions' is not defined (did you mean 'system_instruction'?)", "'stop_sequence' is not defined (did you mean 'stop_sequences'?)"]All three typos surfaced in one run. The pydantic message also lists several errors at once, but it won't suggest the nearest valid name, so checking first keeps your hands moving.
I use model_fields because the list updates itself when a new SDK version adds fields, and I never have to maintain it. On 2.30.1 it held 37 fields.
The other symptom: text is None, and nothing turns red
A misspelled key is the friendlier failure, because it stops loudly. The trickier case is response.text being None with no exception at all.
What I checked was SDK behavior on responses built from the SDK's own types, not live API responses. Even so, the same None came from three different situations:
| State of the response | text | Where to look |
|---|---|---|
| Parts contain only a function call | None (a warning is logged too) | response.function_calls |
| finish_reason is MAX_TOKENS and there are no parts | None | finish_reason and your output token limit |
| candidates is empty and a block_reason is set | None | prompt_feedback.block_reason |
For the function-call case the log says non-text parts in the response: ['function_call']. Don't read that as "the text is empty." Go look at function_calls.
Here is a function that tells the three apart in one line.
def explain_empty(resp) -> str:
"""When text is None, return the reason in one line."""
if not resp.candidates:
fb = resp.prompt_feedback
return f"no candidates / block_reason={getattr(fb, 'block_reason', None)}"
c = resp.candidates[0]
parts = (c.content.parts if c.content else None) or []
kinds = [k for p in parts
for k in ("function_call", "thought", "inline_data", "executable_code")
if getattr(p, k, None)]
return f"finish_reason={c.finish_reason.name} / parts={len(parts)} / non-text={kinds or 'none'}"For the three states above, the output was:
tool -> finish_reason=STOP / parts=1 / non-text=['function_call']
max -> finish_reason=MAX_TOKENS / parts=0 / non-text=none
block -> no candidates / block_reason=SAFETYIf it still fails: the next steps
If you fix the named key and it still fails at the same place, a fixed order keeps you from wandering:
- Run
pip show google-genaiand note the version. Field names can change between releases, so readmodel_fieldson the same version that raised the error. - Stop using a dict and write
types.GenerateContentConfig(...)instead. Your editor can complete the names, and the typo shows up before you run anything. - If a framework wrapper sits in between,
printthe dict right before it reaches the SDK.
A typo that stops the run is a kindness; a None that doesn't is the one that costs you. I let the SDK own the names and keep my own job to checking that they match.
Today, try running one of your real configs through check_config. And if you want to go further and measure what happens when structured output drifts from its schema in production, I wrote down where I place validation and retries in this operations note.