Python: [Bug]: Gemini non-streaming responses report the client default model instead of the requested one · Issue #9548 · microsoft/agent-framework · GitHub
Skip to content

Python: [Bug]: Gemini non-streaming responses report the client default model instead of the requested one #9548

Open

Description

@Ayoubhm07

Observed Behavior

_get_response resolves the request model, then never passes it to _process_generate_response:

which falls back to self.model, the client default:

model=response.model_version or self.model,

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

  1. Create the client with model="gemini-2.5-flash".
  2. Call get_response(messages, options={"model": "gemini-2.5-pro"}) without streaming.
  3. 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:

async def test_repro() -> None:
    client, mock = _make_gemini_client(model="gemini-2.5-flash")
    raw = _make_response([_make_part(text="Hi")])
    raw.model_version = None
    mock.aio.models.generate_content = AsyncMock(return_value=raw)

    response = await client.get_response(
        messages=[Message(role="user", contents=[Content.from_text("Hi")])],
        options={"model": "gemini-2.5-pro"},
    )

    assert response.model == "gemini-2.5-pro"

Error Messages and Stack Traces

AssertionError: assert 'gemini-2.5-flash' == 'gemini-2.5-pro'

Package Versions

agent-framework-core: 1.22.0, agent-framework-gemini: 1.0.0b261015 (main at 809cc04)

Python Version

Python 3.10

Operating System

Windows

Regression

No

Additional Context

Follow-up I offered in #9139: #9139 (comment)

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I personally verified this behavior and the reproduction details are authentic.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 20, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 20, 2026
  3. github-actions commented on Oct 20, 2026

    @github-actions
    Contributor

    🤖 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.

    • Failing test: python/packages/gemini/tests/test_gemini_client.py::test_get_response_model_falls_back_to_requested_model
    • Files examined: python/packages/gemini/agent_framework_gemini/_chat_client.py, python/packages/gemini/tests/test_gemini_client.py, python/packages/gemini/pyproject.toml
    • Tests run: test_get_response_model_falls_back_to_requested_model (failed), test_get_response_streaming_model_falls_back_to_requested_model (passed), test_get_response_uses_model_from_options (passed)
    • Reported version: 1.0.0b261015
    • Current version: 1.0.0b261015
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 20, 2026
  5. Ayoubhm07 commented on Oct 20, 2026

    @Ayoubhm07
    ContributorAuthor

    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 !

  6. pm25coder commented on Oct 21, 2026

    @pm25coder

    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.

    package resolved model non-streaming reports streaming twin
    gemini :515 model, contents, config = self._prepare_request(...) :1088 model=response.model_version or self.model :497 fallback_model = model (from #9139)
    bedrock :433 model = options.get("model") or self.model → :447 run_options["modelId"] = model :773 model = response.get("modelId") or output.get("modelId") or self.model :408 passes request["modelId"] into the event processor
    mistral :543 model = options.get("model") or self.model → :553 request["model"] = model :805 model=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.

  7. Ayoubhm07 commented on Oct 21, 2026

    @Ayoubhm07
    ContributorAuthor

    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

  8. pm25coder commented on Oct 21, 2026

    @pm25coder

    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.

  9. Ayoubhm07 commented on Oct 21, 2026

    @Ayoubhm07
    ContributorAuthor

    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).

  10. pm25coder commented on Oct 21, 2026

    @pm25coder

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions