fix(thinking): keep client thinking config for uncatalogued models - #182
Open
warelik wants to merge 2 commits into
Open
fix(thinking): keep client thinking config for uncatalogued models#182warelik wants to merge 2 commits into
warelik wants to merge 2 commits into
Conversation
A configured model that the bundled catalog does not carry resolved to an
empty capability snapshot: Thinking == nil and UserDefined == false. The
thinking applier reads that pair as "this model provably cannot reason" and
strips generationConfig.thinkingConfig before dispatch, so includeThoughts
never reaches the upstream.
The effect is a silent capability loss for every model newer than the
embedded models.json. Verified live against Google Gemini through a
configured gemini-api-key entry for gemini-3.7-flash:
direct -> 2 parts (thought=true, 1192 chars + answer), thoughtsTokenCount 302
via CPA -> 1 part (answer only), thoughtsTokenCount 188
The upstream still runs and bills the reasoning pass, it just never returns
the thought parts, because Gemini only emits them when includeThoughts is
set. The same request was logged upstream as "generationConfig": {} — the
whole thinkingConfig removed.
Distinguish unknown capability from proven-absent capability in
ResolveModelInfo: a snapshot is marked user-defined only when the catalog
has no entry for the model and no explicit thinking block was configured.
Such a request is then forwarded for the upstream to validate, matching the
documented contract for config-declared models. A catalog-known model whose
entry records no thinking support keeps its authoritative absence and is
still stripped, and an explicitly configured thinking block still wins.
Adds model_info_unknown_thinking_test.go covering both directions: the
uncatalogued model must retain includeThoughts and thinkingBudget through
ApplyThinkingWithModelInfoAndSummary, and a catalog model documented without
reasoning must stay authoritative. Two existing tests asserted the previous
contract and are updated with the rationale.
TestExecuteStream_PublishesUsageRecordFromStreamUsage failed once in CI on PR kaitranntt#182 after exactly 2.000s while the PR diff touches only modelconfig. Local reproduction: 30/30 passes at both PR head and base commit, and 100/100 passes with the timeout raised to 5s. The wait helper polls for a usage.Record published by the executor goroutine; on a loaded CI runner the goroutine can be scheduled past the tight 2s budget. The deadline is a failure accelerator, not part of the assertion — widening it does not weaken what the test verifies.
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
A model declared in config but missing from the bundled
internal/registry/models/models.jsonsilently loses reasoning: the client'sgenerationConfig.thinkingConfigis stripped before dispatch, soincludeThoughtsnever reaches the upstream. The provider still runs and bills the reasoning pass, it just returns no thought parts.This affects every model newer than the embedded catalog — which is the normal case for a freshly released Gemini model added through
gemini-api-key.models[].Root cause
internal/modelconfig/model_info.gobuilt the capability snapshot for a configured model and unconditionally cleared the user-defined marker:So an uncatalogued model resolves to
Thinking == nilandUserDefined == false.internal/thinking/apply.goreads exactly that pair as an authoritative "this model cannot reason":The two states are not the same: an absent catalog entry means the capability is unknown, not proven absent. Only the second justifies stripping. This also contradicts the documented contract on
IsUserDefinedModel, which states that models configured via*-api-key.models[]should have their thinking configuration applied directly and validated by the upstream.Live evidence
Same prompt, same key, same model (
gemini-3.7-flashbehind a configured alias), before the fix:thoughtsTokenCountgenerativelanguage.googleapis.comThe upstream request log confirms the cause — the whole block was removed:
Non-zero
thoughtsTokenCountalongside a missing thought part is the signature of the bug: Gemini only emits thought parts whenincludeThoughtsis set, but it reasons (and bills) regardless.Change
ResolveModelInfonow distinguishes unknown capability from proven-absent capability:thinking:block → unknown capability → snapshot is user-defined → the caller's configuration is forwarded and the upstream validates it.thinking:block in config → still wins, unchanged.After the fix (same live setup)
Test plan
internal/modelconfig/model_info_unknown_thinking_test.gocovers both directions: an uncatalogued model must retainincludeThoughtsandthinkingBudgetthroughApplyThinkingWithModelInfoAndSummary, and a catalog model documented without reasoning must keep its authoritative absence.TestResolveModelInfoUnknownModelKeepsMissingCapability,TestAttachResolvedAPIKeyModelInfoBindsUnknownConfiguredCapability) and are updated with the rationale inline.go build ./...— cleango test ./... -count=1— 0 failuresgofmt -l— cleanMirror
CPA: router-for-me/CLIProxyAPI#4991 (identical changes)