Overhaul: align with current Google API surface (thinking_level, model defaults, config hygiene) - #42
Merged
Brian Krabach (bkrabach) merged 7 commits intoAug 29, 2026
Conversation
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>
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
fix(thinking)-- Replace the lossyreasoning_effort->{4096, -1}thinking_budgetmapping with athinking_levelmapping (Google's current thinking control;thinking_budgetis now legacy). Clamped per-model against a small maintained table, with a one-line INFO log on every clamp. Bumps thegoogle-genaifloor to>=1.56.0(verified by probing the SDK's own installed types: 1.46.0 has nothinking_levelfield 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=0was never actually reaching the API.fix(thought-signatures)-- Audited the stateless full-resend round-trip. FoundTextBlock.signature/ToolCallBlock.signature/ToolCall.signaturewere captured as raw SDK bytes (unlikeThinkingBlock.signature, already correctly base64-encoded). Raw bytes containing non-UTF-8 sequences -- the normal case for an opaque signature -- crashmodel_dump(mode="json")outright. Fixed both capture sites; added a regression test using a deliberately non-UTF-8 byte sequence.feat(model)-- Default modelgemini-2.5-flash->gemini-3.7-flash(current flagship Flash, verified present via livelist_models()). No newConfigFieldadded (app-cli model picker already handles this).fix(config)-- Bool/numeric config coercion (_parse_config_bool/_parse_config_number, ported from the establishedprovider-openai/provider-github-copilotpattern): warn-and-default instead of silently-wrong-truthiness or mount-timeValueError. 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).feat(config)--max_tokens->max_output_tokens(Google's actual parameter name), with a deprecated back-compat alias and one-shot deprecation warning.feat(config)--extra_request_params: settings-only (never aConfigField) escape hatch merged LAST intoGenerateContentConfig, reachingsafety_settings/top_p/top_k/etc. Caller always wins, loudly (warns on override).docs-- README overhaul: removes the three ghost keys, fixestimeoutdefault drift (300 -> 600), adds a key-reference table naming each key's real Google API parameter (or "Amplifier-only"), documents thethinking_levelsupport/clamping table andextra_request_paramscontract.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-literejectthinking_leveloutright (400: "Thinking level is not supported for this model") -- not a smaller subset of levels, no support at all. Legacythinking_budgetremains their only control.gemini-3.7-flashacceptslow/medium/highbut rejectsminimal(400: "Thinking level MINIMAL is not supported for this model...").gemini-3.5-flash/gemini-3.5-flash-liteaccept the fullminimal..highrange.thinking_budget=0ongemini-3.7-flashstill produced ~26 thinking tokens; there is no "off"thinking_level.thinking_configentirely is not equivalent to sending an explicitthinking_budget=0ongemini-2.5-flash: the former still thinks by default (verifiedthoughts_token_countpopulated); only the latter genuinely disables it.Testing
live-marked tests.python_checkon every touched file matches the pre-existing baseline error/warning counts exactly -- no new lint/type issues introduced.probe_gemini.py(mount -> list_models -> complete contract) against this branch installed into a real venv with a realGOOGLE_API_KEY--list_models()returned 39 models includinggemini-3.7-flash; a plain completion againstgemini-2.5-flashsucceeded. A supplementary live check confirmedreasoning_effort={low,medium,high}againstgemini-3.7-flashall succeed end-to-end via the realthinking_levelpath (no 400s, no thinking_budget+thinking_level conflict). Total live spend across all verification: a fraction of a cent.🤖 Generated with Amplifier