Python: [Bug]: Gemini non-streaming responses report the client default model instead of the requested one · Issue #9548 · microsoft/agent-framework · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Python: [Bug]: Gemini non-streaming responses report the client default model instead of the requested one #9548
Since #9139 the streaming path falls back to the resolved model instead (:497-501, :1123). So for a gemini-2.5-flash client called with options={"model": "gemini-2.5-pro"}, a response without model_version is reported as gemini-2.5-pro when streamed and gemini-2.5-flash when not.
Expected Behavior
Both paths report gemini-2.5-pro, the model the request was sent to.
Steps to Reproduce
Create the client with model="gemini-2.5-flash".
Call get_response(messages, options={"model": "gemini-2.5-pro"}) without streaming.
Return a response with no model_version (it is Optional[str] in google-genai).
No API key or network needed.
Minimal Reproduction
With the helpers of python/packages/gemini/tests/test_gemini_client.py:
🤖 Automated triage reproduction notes(agent-authored — trust but verify)
Agent analysis
Repro: python/packages/gemini/agent_framework_gemini/_chat_client.py::RawGeminiChatClient._inner_get_response resolves the per-request model but does not pass it to _process_generate_response, whose line 108 fallback uses self.model. This triggers when a non-streaming call overrides options["model"] and the SDK response has no model_version. Minimal repro: create a gemini-2.5-flash client, mock a response with model_version=None, request gemini-2.5-pro, and assert response.model equals the requested model.
Read at main 08851e69d722b0e0961c08eecf8575db995f0172: the mismatch you describe is real, and it is one of three sites with the same shape. In all three the requested model is already in hand where the response is built.
:433model = options.get("model") or self.model → :447run_options["modelId"] = model
:773model = response.get("modelId") or output.get("modelId") or self.model
:408 passes request["modelId"] into the event processor
mistral
:543model = options.get("model") or self.model → :553request["model"] = model
:805model=response.model or self.model
_parse_chunk reads chunk.model (:835)
That suggests the axis is not streaming vs non-streaming but which of the two values the processor receives. Bedrock shows it within one screen: _process_converse_stream_event(event, request["modelId"], ...) at :407, against _process_converse_response(raw_response, options) at :423, whose fallback at :773 reaches for self.model although request["modelId"] is two frames up. Mistral is the same picture from the other side: both paths build request before dispatching (:448, :477), so request["model"] is available at :489 with no new plumbing.
Two qualifications, so the scope is not over-read:
I read the ten provider chat-client modules in that tree. These three carry an ... or self.model on a response-derived model. A fourth, same-shaped seed is openai/.../_chat_client.py:3849 (model = self.model), overwritten at :4150 from event.response.model; I did not measure whether an event can arrive without one.
Reachability is not uniform and I have no credentials here. Gemini's field is Optional[str] in the SDK, which is what made the mid-stream case real. Whether a non-streaming Mistral or Bedrock body can omit model / modelId I did not test, so for those two the fallback may be defence rather than a live bug.
If the fix is written as "the reported model comes from the request, and the provider's own value is an override only when present", it is the same one-line change at three processors plus threading the resolved value in — no signature grows beyond what each streaming twin already takes. Whether to do all three in one change or just the filed one is of course your call; my point is only that the filed site is not unique, so the PR description will be read against the other two either way.
Two of the three are already settled Bedrock has #9399 which threads request["modelId"] into _process_converse_response the same way.
Mistral can't reach its fallback, because ChatCompletionResponse.model is a required str in mistralai 2.10.1. The OpenAI seed at :3849 only reaches intermediate updates; the final response takes model from response.completed, where it's required too. Gemini is the only live one
Re-checked both at main today (openai _chat_client.py, blob a5c18ebda8), and your reading holds: :4146 matches the terminal cases and :4150 runs model = event.response.model, so whatever :3849 seeded is overwritten before the final response is built — the seed can only surface on intermediate updates.
On requiredness I can only agree, not confirm: neither openai nor mistralai==2.10.1 is installed on this box, so I cannot assert from here that ChatCompletionResponse.model / event.response.model are non-optional. Taking your word for those two.
One correction, because it changes what the PR itself has to say: Bedrock is not settled in the tree. #9399 is still open (opened 2026-10-14), and at main today _process_converse_response is reached at :423 with no model argument and ends at :773 with model = response.get("modelId") or output.get("modelId") or self.model. The site stays live until that PR lands, so a Gemini PR written as "two of three already handled" will get Bedrock re-filed by the next reader.
What #9399 is worth reusing is its sentence — "Converse does not return the model id, so model should be the modelId the request was sent with" — and the shape that follows from it: thread the resolved value into the non-streaming processor instead of re-deriving it there. Same change works for Gemini, and matching it means the three processors read alike once both PRs land.
Fair point settled was too strong: #9399 is still open so Bedrock stays live until it lands and the PR will point to it rather than call it done.
The Gemini fix already has that shape: _get_response passes the resolved model to _process_generate_response as a keyword-only model, used before self.model. For Mistral, model is declared model: str on ChatCompletionResponse in mistralai/client/models/chatcompletionresponse.py (2.10.1).
Confirmed, and thank you — I checked the annotation instead of taking your word for it: src/mistralai/client/models/chatcompletionresponse.py at tag v2.10.1, line 26 in the class (model: str) and line 15 in the TypedDict. Required, no default. My "I can only take your word on those two" from the earlier comment is withdrawn.
One caveat on what that buys, because it is the difference between "settled" and "hard to reach": str rules out None but not "", and response.model or self.model fires on both. So if the PR says Mistral needs nothing, the sentence should rest on the API always returning a non-empty model version, not on the annotation — otherwise it is the same shape as the two live ones, just harder to reach.
Since this thread has now twice argued about the count, I listed the sites instead of reasoning about them: every *_chat_client.py under python/packages/ at main (18 files), searched for a fallback to self.model.
Response-side, with no model threaded in — the bug:
bedrock/agent_framework_bedrock/_chat_client.py:773 — response.get("modelId") or output.get("modelId") or self.model
gemini/agent_framework_gemini/_chat_client.py:1088 — response.model_version or self.model
mistral/agent_framework_mistral/_chat_client.py:805 — response.model or self.model
Request-side, resolving the model correctly and not the bug:
bedrock:433, gemini:562, mistral:543 — options.get("model") or self.model
So three, not one, and all three are one shape: the request builder resolves the model, and the response processor cannot see it. Your keyword-only model is the right shape for the reason you gave — once #9399 threads request["modelId"], all three read alike — but note the three processors are not alike today: Bedrock's takes options, Gemini's takes response_format only, Mistral's takes response_format too. "Alike afterwards" therefore means giving all three the same explicit model argument, not reusing options in one and not the others.
Also, so nobody duplicates work: no open PR references #9548 as of now (searched a moment ago), so the issue is still the record and there is nothing to review yet.
Activity
github-actions commentedon Oct 20, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro:
python/packages/gemini/agent_framework_gemini/_chat_client.py::RawGeminiChatClient._inner_get_responseresolves the per-request model but does not pass it to_process_generate_response, whose line 108 fallback usesself.model. This triggers when a non-streaming call overridesoptions["model"]and the SDK response has nomodel_version. Minimal repro: create agemini-2.5-flashclient, mock a response withmodel_version=None, requestgemini-2.5-pro, and assertresponse.modelequals the requested model.python/packages/gemini/tests/test_gemini_client.py::test_get_response_model_falls_back_to_requested_model1.0.0b2610151.0.0b261015Ayoubhm07 commentedon Oct 20, 2026
Eduard van Valkenburg (@eavanvalkenburg) happy to take this one: it's the follow-up I offered in #9139 and the fix and tests are ready !
pm25coder commentedon Oct 21, 2026
Read at main
08851e69d722b0e0961c08eecf8575db995f0172: the mismatch you describe is real, and it is one of three sites with the same shape. In all three the requested model is already in hand where the response is built.:515model, contents, config = self._prepare_request(...):1088model=response.model_version or self.model:497fallback_model = model(from #9139):433model = options.get("model") or self.model→:447run_options["modelId"] = model:773model = response.get("modelId") or output.get("modelId") or self.model:408passesrequest["modelId"]into the event processor:543model = options.get("model") or self.model→:553request["model"] = model:805model=response.model or self.model_parse_chunkreadschunk.model(:835)That suggests the axis is not streaming vs non-streaming but which of the two values the processor receives. Bedrock shows it within one screen:
_process_converse_stream_event(event, request["modelId"], ...)at:407, against_process_converse_response(raw_response, options)at:423, whose fallback at:773reaches forself.modelalthoughrequest["modelId"]is two frames up. Mistral is the same picture from the other side: both paths buildrequestbefore dispatching (:448,:477), sorequest["model"]is available at:489with no new plumbing.Two qualifications, so the scope is not over-read:
... or self.modelon a response-derived model. A fourth, same-shaped seed isopenai/.../_chat_client.py:3849(model = self.model), overwritten at:4150fromevent.response.model; I did not measure whether an event can arrive without one.Optional[str]in the SDK, which is what made the mid-stream case real. Whether a non-streaming Mistral or Bedrock body can omitmodel/modelIdI did not test, so for those two the fallback may be defence rather than a live bug.If the fix is written as "the reported model comes from the request, and the provider's own value is an override only when present", it is the same one-line change at three processors plus threading the resolved value in — no signature grows beyond what each streaming twin already takes. Whether to do all three in one change or just the filed one is of course your call; my point is only that the filed site is not unique, so the PR description will be read against the other two either way.
Ayoubhm07 commentedon Oct 21, 2026
Two of the three are already settled Bedrock has #9399 which threads
request["modelId"]into_process_converse_responsethe same way.Mistral can't reach its fallback, because
ChatCompletionResponse.modelis a requiredstrin mistralai 2.10.1. The OpenAI seed at:3849only reaches intermediate updates; the final response takesmodelfromresponse.completed, where it's required too. Gemini is the only live onepm25coder commentedon Oct 21, 2026
Re-checked both at
maintoday (openai_chat_client.py, bloba5c18ebda8), and your reading holds::4146matches the terminal cases and:4150runsmodel = event.response.model, so whatever:3849seeded is overwritten before the final response is built — the seed can only surface on intermediate updates.On requiredness I can only agree, not confirm: neither
openainormistralai==2.10.1is installed on this box, so I cannot assert from here thatChatCompletionResponse.model/event.response.modelare non-optional. Taking your word for those two.One correction, because it changes what the PR itself has to say: Bedrock is not settled in the tree. #9399 is still open (opened 2026-10-14), and at
maintoday_process_converse_responseis reached at:423with no model argument and ends at:773withmodel = response.get("modelId") or output.get("modelId") or self.model. The site stays live until that PR lands, so a Gemini PR written as "two of three already handled" will get Bedrock re-filed by the next reader.What #9399 is worth reusing is its sentence — "Converse does not return the model id, so
modelshould be themodelIdthe request was sent with" — and the shape that follows from it: thread the resolved value into the non-streaming processor instead of re-deriving it there. Same change works for Gemini, and matching it means the three processors read alike once both PRs land.Ayoubhm07 commentedon Oct 21, 2026
Fair point settled was too strong: #9399 is still open so Bedrock stays live until it lands and the PR will point to it rather than call it done.
The Gemini fix already has that shape:
_get_responsepasses the resolved model to_process_generate_responseas a keyword-onlymodel, used beforeself.model. For Mistral,modelis declaredmodel: stronChatCompletionResponseinmistralai/client/models/chatcompletionresponse.py(2.10.1).pm25coder commentedon Oct 21, 2026
Confirmed, and thank you — I checked the annotation instead of taking your word for it:
src/mistralai/client/models/chatcompletionresponse.pyat tagv2.10.1, line 26 in the class (model: str) and line 15 in the TypedDict. Required, no default. My "I can only take your word on those two" from the earlier comment is withdrawn.One caveat on what that buys, because it is the difference between "settled" and "hard to reach":
strrules outNonebut not"", andresponse.model or self.modelfires on both. So if the PR says Mistral needs nothing, the sentence should rest on the API always returning a non-empty model version, not on the annotation — otherwise it is the same shape as the two live ones, just harder to reach.Since this thread has now twice argued about the count, I listed the sites instead of reasoning about them: every
*_chat_client.pyunderpython/packages/atmain(18 files), searched for a fallback toself.model.Response-side, with no model threaded in — the bug:
bedrock/agent_framework_bedrock/_chat_client.py:773—response.get("modelId") or output.get("modelId") or self.modelgemini/agent_framework_gemini/_chat_client.py:1088—response.model_version or self.modelmistral/agent_framework_mistral/_chat_client.py:805—response.model or self.modelRequest-side, resolving the model correctly and not the bug:
bedrock:433,gemini:562,mistral:543—options.get("model") or self.modelSo three, not one, and all three are one shape: the request builder resolves the model, and the response processor cannot see it. Your keyword-only
modelis the right shape for the reason you gave — once #9399 threadsrequest["modelId"], all three read alike — but note the three processors are not alike today: Bedrock's takesoptions, Gemini's takesresponse_formatonly, Mistral's takesresponse_formattoo. "Alike afterwards" therefore means giving all three the same explicitmodelargument, not reusingoptionsin one and not the others.Also, so nobody duplicates work: no open PR references #9548 as of now (searched a moment ago), so the issue is still the record and there is nothing to review yet.