Skip to content

Overhaul: align with current Google API surface (thinking_level, model defaults, config hygiene) - #42

Merged
Brian Krabach (bkrabach) merged 7 commits into
mainfrom
gemini-api-overhaul-thinking-level
Aug 29, 2026
Merged

Overhaul: align with current Google API surface (thinking_level, model defaults, config hygiene)#42
Brian Krabach (bkrabach) merged 7 commits into
mainfrom
gemini-api-overhaul-thinking-level

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

Summary

Overhauls this provider to align with Google's current API surface and the ecosystem's config-surface conventions. Self-authored by an Amplifier maintainer session, working at the direction of a repo owner with admin access -- see merge note.

Seven ordered commits, each independently tested (fail-before/pass-after where a behavior actually changed):

  1. fix(thinking) -- Replace the lossy reasoning_effort -> {4096, -1} thinking_budget mapping with a thinking_level mapping (Google's current thinking control; thinking_budget is now legacy). Clamped per-model against a small maintained table, with a one-line INFO log on every clamp. Bumps the google-genai floor to >=1.56.0 (verified by probing the SDK's own installed types: 1.46.0 has no thinking_level field at all; 1.51.0 adds it with only LOW/HIGH; 1.56.0 completes the four-level enum). Also fixes a pre-existing bug this same code path surfaced: thinking_budget=0 was never actually reaching the API.

  2. fix(thought-signatures) -- Audited the stateless full-resend round-trip. Found TextBlock.signature / ToolCallBlock.signature / ToolCall.signature were captured as raw SDK bytes (unlike ThinkingBlock.signature, already correctly base64-encoded). Raw bytes containing non-UTF-8 sequences -- the normal case for an opaque signature -- crash model_dump(mode="json") outright. Fixed both capture sites; added a regression test using a deliberately non-UTF-8 byte sequence.

  3. feat(model) -- Default model gemini-2.5-flash -> gemini-3.7-flash (current flagship Flash, verified present via live list_models()). No new ConfigField added (app-cli model picker already handles this).

  4. fix(config) -- Bool/numeric config coercion (_parse_config_bool / _parse_config_number, ported from the established provider-openai/provider-github-copilot pattern): warn-and-default instead of silently-wrong-truthiness or mount-time ValueError. Unknown-key sweep with difflib "did you mean" suggestions. Targeted "this key is inert" messages for the three README ghost keys (debug, raw_debug, debug_truncate_length -- verified by grep that no code path reads them).

  5. feat(config) -- max_tokens -> max_output_tokens (Google's actual parameter name), with a deprecated back-compat alias and one-shot deprecation warning.

  6. feat(config) -- extra_request_params: settings-only (never a ConfigField) escape hatch merged LAST into GenerateContentConfig, reaching safety_settings/top_p/top_k/etc. Caller always wins, loudly (warns on override).

  7. docs -- README overhaul: removes the three ghost keys, fixes timeout default drift (300 -> 600), adds a key-reference table naming each key's real Google API parameter (or "Amplifier-only"), documents the thinking_level support/clamping table and extra_request_params contract.

Vendor facts verified LIVE against the real API (2026-08-29, not all documented by Google)

  • gemini-2.5-flash/gemini-2.5-pro/gemini-2.5-flash-lite reject thinking_level outright (400: "Thinking level is not supported for this model") -- not a smaller subset of levels, no support at all. Legacy thinking_budget remains their only control.
  • gemini-3.7-flash accepts low/medium/high but rejects minimal (400: "Thinking level MINIMAL is not supported for this model...").
  • gemini-3.5-flash/gemini-3.5-flash-lite accept the full minimal..high range.
  • Gemini 3.x thinking cannot be disabled at all: thinking_budget=0 on gemini-3.7-flash still produced ~26 thinking tokens; there is no "off" thinking_level.
  • Omitting thinking_config entirely is not equivalent to sending an explicit thinking_budget=0 on gemini-2.5-flash: the former still thinks by default (verified thoughts_token_count populated); only the latter genuinely disables it.

Testing

  • Full suite: 263 passed (was ~204 pre-existing baseline before this PR's new tests), 0 failures, 0 skips beyond the pre-existing 2 live-marked tests.
  • python_check on every touched file matches the pre-existing baseline error/warning counts exactly -- no new lint/type issues introduced.
  • Live verification: ran probe_gemini.py (mount -> list_models -> complete contract) against this branch installed into a real venv with a real GOOGLE_API_KEY -- list_models() returned 39 models including gemini-3.7-flash; a plain completion against gemini-2.5-flash succeeded. A supplementary live check confirmed reasoning_effort={low,medium,high} against gemini-3.7-flash all succeed end-to-end via the real thinking_level path (no 400s, no thinking_budget+thinking_level conflict). Total live spend across all verification: a fraction of a cent.

🤖 Generated with Amplifier

Google's thinking_budget (an approximate output-token budget) is now the
LEGACY thinking control. The current control -- and the only one some
Gemini 3.x models accept at all -- is thinking_level, an enum
(minimal|low|medium|high). Sending both on one request is a 400.

Replaces the old lossy reasoning_effort -> {4096, -1} mapping with a
thinking_level mapping, clamped per-model against a small maintained table
(_THINKING_LEVEL_TABLE), with a one-line INFO log whenever a clamp changes
the requested level -- never silent.

Verified LIVE against the real API on 2026-08-29 (not documented by Google
as of this writing):
- gemini-2.5-flash and gemini-2.5-pro REJECT thinking_level outright: 400
  INVALID_ARGUMENT "Thinking level is not supported for this model." These
  models keep the legacy thinking_budget path as their only control.
- gemini-3.7-flash accepts low/medium/high but rejects MINIMAL ("Thinking
  level MINIMAL is not supported for this model...").
- gemini-3.5-flash(-lite) accept the full minimal..high range.
- gemini-3.x thinking is mandatory: thinking_budget=0 on gemini-3.7-flash
  still produced ~26 thinking tokens -- there is no way to disable
  thinking on a Gemini 3.x model, and thinking_level has no "off" value.

Also fixes a pre-existing bug surfaced by this same code path: explicit
thinking_budget=0 was never actually reaching the API (the old code
omitted thinking_config entirely instead of sending the zero). Verified
live that omitting the config is NOT equivalent to sending an explicit
zero -- gemini-2.5-flash with no thinking_config still reports a populated
thoughts_token_count (still thinking), while an explicit thinking_budget=0
correctly reports thoughts_token_count=None (thinking genuinely disabled).
The fix always sends the explicit value instead of omitting it.

Explicit thinking_budget via kwargs/request.metadata still wins outright
(legacy override, honored ALONE, never combined with thinking_level).
When no directive is given at all, thinking_config now carries only
include_thoughts -- omitting both budget and level lets the model apply
its own default amount (verified live this still returns thought
summaries), rather than forcing an explicit dynamic budget that forecloses
the thinking_level path for level-only models.

Bumps the google-genai floor from >=1.40.0 to >=1.56.0 -- verified by
probing the installed SDK's own types directly: 1.46.0 has no
thinking_level field at all; 1.51.0 adds it with only LOW/HIGH; 1.56.0
adds MINIMAL and MEDIUM, completing the four-level enum this module needs.

Updates the two existing tests whose assertions encoded the old (buggy)
behavior, with comments explaining what changed and why. Adds
tests/test_thinking_level.py: per-model support table lookups, clamping
(including the INFO log), the full reasoning_effort matrix on a
level-supporting model, the legacy-mapping fallback on a
level-rejecting model, and the never-both-together guarantee.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…w bytes

Audited how assistant thought/tool-call parts are replayed in multi-turn
conversation building, per this module's stateless full-resend design
(the entire Message list is rebuilt from stored history and resent on
every turn -- thought signatures MUST survive that unmodified or Gemini
returns FinishReason MISSING_THOUGHT_SIGNATURE).

Found a load-bearing gap: TextBlock.signature and ToolCallBlock/
ToolCall.signature were captured as the SDK's raw bytes, while
ThinkingBlock.signature was already correctly encoded to a base64 str at
capture time. Verified directly against amplifier_core's own models that
this is not cosmetic: model_dump(mode="json") on a ToolCallBlock/TextBlock
carrying a non-UTF-8 raw-bytes signature raises UnicodeDecodeError /
PydanticSerializationError outright -- and an opaque cryptographic
signature is essentially never valid UTF-8. Any code path that JSON-
serializes the message history (session persistence, event logging, an
orchestrator's model_dump(mode="json")) would crash or silently drop the
signature the next time thinking needs to be resent.

Fixes both capture sites to encode via the existing _encode_sig() helper,
matching ThinkingBlock's contract exactly (str | None, base64 ASCII). The
outbound path (_convert_messages) already called _encode_sig() again at
send time for defense-in-depth -- that call is a no-op for an
already-encoded str, so no outbound changes were needed; the gap was
capture-side only.

Updates the 3 existing tests whose assertions encoded raw-bytes equality
into base64-str equality (documented inline). Adds
test_inbound_signature_is_json_safe_not_raw_bytes, which uses a
deliberately non-UTF-8 byte sequence and asserts model_dump(mode="json")
succeeds and round-trips exactly -- the regression guard for the bug this
audit found.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
gemini-3.7-flash is Google's current flagship Flash model (ai.google.dev
describes it as "the latest and most capable" Flash) -- verified present
in list_models() against this account's live key on 2026-08-29 (40
gemini-* models served). gemini-2.5-flash is documented as two generations
back; gemini-3.5-flash is documented as legacy.

Updates GeminiProvider.default_model's fallback and get_info()'s
defaults["model"]. Deliberately does NOT add a default_model ConfigField --
the app-cli model picker phase already collects this from the user.

Existing tests that depend on the OLD default's specific legacy
thinking_budget mapping (test_reasoning_effort.py) now pin
default_model="gemini-2.5-flash" explicitly in their provider fixture,
with a module docstring note pointing to test_thinking_level.py for the
comprehensive reasoning_effort -> thinking_level/budget matrix across both
model families. No test asserted get_info()'s previous default model
value directly.

README default-model documentation is updated in the README-overhaul
commit rather than here, to avoid two commits touching the same
prose.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…rt ones

Config commonly arrives as strings: the app-cli wizard writes
field_type="boolean" values as the literal strings "true"/"false" (not
Python bools), and hand-edited YAML often quotes both booleans and
numbers. This provider had no coercion at all for several config keys:

- raw / use_streaming / retry_jitter: naive `bool(raw)` (or a bare
  truthiness check) is wrong for a string -- `bool("false")` is True,
  since any non-empty string is truthy, silently inverting the flag.
- timeout / max_tokens / temperature / priority: no coercion at all. A
  string timeout survived uncoerced all the way to
  asyncio.wait_for(timeout=...), failing confusingly on the first real
  API call instead of at mount.
- max_retries / min_retry_delay / max_retry_delay: bare int()/float()
  RAISED ValueError at mount time for any unparseable string, crashing
  the whole provider mount over one bad config value.

Adds _parse_config_bool and _parse_config_number (ported from the
established pattern in amplifier-module-provider-openai /
amplifier-module-provider-github-copilot): warn-and-default, never raise,
for both bool and numeric config coercion. Every config-reading line in
GeminiProvider.__init__ now goes through one of these two helpers.

Adds an unknown-config-key sweep (_sweep_unknown_config_keys), run at
mount, with three tiers of message:
  1. A known documentation ghost (debug / raw_debug /
     debug_truncate_length -- verified by grep that no
     self.config.get(...) call site reads any of them) gets a specific,
     helpful explanation instead of a generic warning.
  2. A likely typo of a real key gets a difflib "did you mean X?" suggestion.
  3. Anything else gets a generic "unrecognized, ignored" plus the full
     list of keys this provider actually reads.

The allowlist (_CONSUMED_CONFIG_KEYS) is exactly the 13 keys this module's
__init__ reads via self.config.get(...), including `priority` -- which
this module only stores for the orchestrator's own provider-selection
logic to read, never used internally, but is a real consumed key, not a
typo to flag.

tests/test_config_hygiene.py: unit tests for both coercion helpers
(including the exact "false" string bug, the bool-is-an-int-subclass
guard, and warn-vs-raise for garbage input) plus end-to-end
GeminiProvider.__init__ tests proving string config values are coerced
and a bad numeric string no longer raises at mount, and coverage for all
three sweep message tiers.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ram name)

max_output_tokens is the actual field name in Google's API (already used
directly in this module's own generate_content call:
GenerateContentConfig(max_output_tokens=...)) -- the provider's config
surface previously called the same knob "max_tokens", a needless mismatch
for anyone cross-referencing Google's docs while writing a bundle config.

Adds _read_renamed_config(config, new, old): reads the new key, falling
back to the deprecated old key with exactly one warning -- only when the
old key is the value actually used, so a fully-migrated config stays
silent. The new key always wins when both are set, with its own distinct
warning naming the winner.

Both 'max_output_tokens' and the deprecated 'max_tokens' alias are in
_CONSUMED_CONFIG_KEYS (neither trips the unknown-key sweep from the
previous commit). String values still coerce correctly through either
name, since _read_renamed_config's result flows into the same
_parse_config_number this module already uses.

tests/test_config_hygiene.py: the new key alone, the old key alone (with
its deprecation warning), both set together (new wins, with its own
warning), a string value through the old alias, and neither set (default
unchanged at 8192). Updates the existing "recognized keys produce no
warnings" test to use the new canonical key name.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…nfig escape hatch

Adds extra_request_params: a dict of arbitrary GenerateContentConfig
fields (safety_settings, top_p, top_k, seed, stop_sequences,
presence_penalty/frequency_penalty, response_mime_type, labels, and any
other field google-genai's GenerateContentConfig defines) merged LAST,
after this provider's own computed values (temperature, max_output_tokens,
thinking_config, tools). Deliberately settings-only -- never a
ConfigField / interactive wizard prompt -- since it's an owner-beware
power-user knob, not something to walk a user through.

_apply_extra_request_params(config, extra_request_params):
- The caller's value always wins over anything this provider already
  computed, and wins LOUDLY: overriding a non-None field logs a warning
  naming the field, the provider's own value, and the override.
- An unrecognized field name (checked against GenerateContentConfig's own
  model_fields, not a hardcoded list) warns and is skipped -- never
  raises, since a typo in settings.yaml shouldn't crash the provider mount.
- Single merge site: both the streaming and non-streaming call paths in
  _complete_chat_request share the same `config` object this mutates, so
  there is nowhere for the two paths to drift.

tests/test_extra_request_params.py: unit tests for the merge helper
(unexposed-field merge, no-op on empty/None, loud override with the exact
warning content, unknown field skipped not raised) plus end-to-end tests
through complete() proving extra_request_params reaches the real
GenerateContentConfig sent to the API, and that get_info() does not list
it as a ConfigField.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…level/extra_request_params

Removes the three ghost config keys documented in prior README revisions
but never actually implemented (verified by grep: no
self.config.get(...) call site reads any of them):
- `debug` / `raw_debug` -- no llm:request:debug/llm:response:debug or
  llm:request:raw/llm:response:raw events exist in this provider.
- `debug_truncate_length` -- no debug-log truncation path exists.
The entire "Debug Configuration" section describing these is removed.
(Config still setting them now gets a specific "this key is inert"
warning at mount time, from the previous hygiene commit.)

Fixes documentation drift: `timeout` default was documented as 300.0 but
the code default has been 600.0. Renames `max_tokens` to
`max_output_tokens` throughout (keeping `max_tokens` documented as the
deprecated alias). Updates `default_model` references to gemini-3.7-flash
and the google-genai floor to >=1.56.0, matching the code changes in
earlier commits of this PR.

Adds the house-style key-reference table: every config key now states
either the real Google API parameter name it maps to (e.g. `temperature`
-> API: `temperature`) or "Amplifier-only" for glue with no Google
equivalent, plus one plain-language sentence -- replacing the old
table's bare type/default/description columns with an explicit
provenance column.

Documents:
- The full thinking_level per-model support table and the
  reasoning_effort -> thinking_level mapping (including the legacy
  thinking_budget fallback for gemini-2.5-* models, and the
  cannot-disable-thinking limitation on gemini-3.x), with the exact live
  error messages that back each row.
- extra_request_params as a dedicated section: contract (merged last,
  loud override warnings, unknown fields skipped not raised), the
  settings-only/no-ConfigField guarantee, an example, and the
  safety-filters-default-off vendor fact.
- Thought-signature round-trip and why this provider's stateless
  full-resend design makes it load-bearing.

Rewrites the Supported Models section for the current lineup (3.7-flash
flagship, 3.5-flash/-lite legacy, 2.5-* two generations back but still
served and thinking_level-rejecting, 2.0 shut down) with a note to prefer
live list_models() over this table since availability changes frequently.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@bkrabach

Copy link
Copy Markdown
Collaborator Author

Maintainer admin-merge note: This PR is self-authored by an Amplifier agent session acting at the direction of a repository owner/admin (task explicitly authorized this merge pattern). All CI checks are green (license/cla, pytest py3.11, pytest py3.12) -- merging via the documented maintainer admin pattern, squash, branch retained for reference.

@bkrabach
Brian Krabach (bkrabach) merged commit a7c5e0c into main Aug 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants