跳到正文
DEV Community· pm25coder·· 2 小時前AI 評分54

agent-framework-gemini 非串流呼叫送出 gemini-2.5-pro,卻回報 gemini-2.5-flash

It sent gemini-2.5-pro and reported gemini-2.5-flash

AI 導讀

在 agent-framework-gemini 1.0.0b261015 中,非串流呼叫按請求選項送出 gemini-2.5-pro,但回應缺少 model_version 時,卻回報客戶端預設的 gemini-2.5-flash。

正文

A response's model field is not decoration. It is what your logs record, what your cost attribution divides by, what a router uses to pick the next call, and what a UI puts under the message. When it is wrong it is wrong quietly, and every downstream reader inherits it.

Here is a case where two call paths of the same client disagree about that field, for the same request, on the same server reply.

The measurement

Two calls. Same client, same per-request model override, same reply shape — a reply that carries no model_version. One call streams, the other does not.

call reported model requested model
get_response(stream=True) gemini-2.5-pro gemini-2.5-pro
get_response() gemini-2.5-flash gemini-2.5-pro

The second row is the client's configured model, not the one this request resolved to. The client was built with model="gemini-2.5-flash" and asked for options={"model": "gemini-2.5-pro"}.

I did not invent the streaming test; that one is theirs. I wrote its non-streaming twin — same fixture factory, same options dict, same absent model_version, only the call path differs:

test_streaming_falls_back_to_requested_model      PASSED
test_non_streaming_falls_back_to_requested_model  FAILED
E   AssertionError: assert 'gemini-2.5-flash' == 'gemini-2.5-pro'

Run against agent-framework-gemini 1.0.0b261015 from PyPI, and the same two lines are still on main today.

Where it lives

Three lines in the non-streaming path:

model, contents, config = self._prepare_request(messages, validated)        # the model this request resolved to
raw = await generate_content(model=model, contents=contents, config=config) # sent correctly
return self._process_generate_response(raw, response_format=...)            # ...and not handed on

and inside _process_generate_response:

model=response.model_version or self.model,

Two different models, one frame apart. self.model is the client's configured default; the local model is what this request resolved to after the options override. When the reply omits model_version, or falls through to the attribute — so the label reports whatever the client was constructed with, no matter what was actually asked for or sent.

The streaming path does not have this problem, because it takes the resolved model as a parameter, and its docstring says exactly the right thing:

the last version the server reported in this stream, or the model resolved for this request

The part that makes it interesting

This is not an oversight, and I want to be careful about that, because the thread tells a better story than "someone missed a path".

The person who fixed the streaming case named the sibling himself, on the pull request, before it merged:

_process_generate_response has the same client-default fallback at :1058. I left it alone to keep this PR on the streaming path — happy to align it here or in a follow-up, whichever you prefer.

microsoft/agent-framework#9139 merged on 2026-10-13. Six days before that he had flagged the sibling; four days before, another comment reported the conflict resolved and the diff as "4 lines in _chat_client.py and the 48 test lines". The follow-up was never asked for, never refused, and never tracked. Today the sibling still reads self.model.

So the defect's history is: found, described precisely, offered as a choice, and then closed by the sweep of an issue going green. Not hidden — unanswered. That is a much more ordinary way for a bug to survive, and a much more useful one to learn from, because the same thing happens in every repository where a fixer is being polite about scope.

Why a green suite does not see it

The suite has eleven tests with "model" in the name. All eleven pass on this install. What separates them is not count but which boundary they assert:

  • streaming: updates[0].model == "gemini-2.5-pro" — the delivery boundary.
  • non-streaming: call_kwargs["model"] == "gemini-2.5-pro" — the request boundary.

Both sentences are true about this code. They are just not the same sentence: one asks what did we report, the other asks what did we send. And the single non-streaming test that does look at response.model always hands the mock a model_version, so the or self.model branch is unreachable from the suite.

Tallied: four streaming tests about the reported model, one non-streaming, zero non-streaming tests about the fallback.

The shape worth carrying

Not really about Gemini, or about this package. Four checkable parts:

  1. Ask the assertion at the delivery boundary. For each pair of sibling call paths, the test that matters asserts what the caller receives. A request-boundary assertion still earns its place — it catches a different class of bug — but it cannot stand in for the other one.
  2. A fallback that reads self.X is a fallback to the wrong value whenever the resolved value is a local variable. Here the resolved model is in scope one frame up. The attribute wins by accident of where the code sits, not by decision.
  3. Write the case with the field absent. A test that supplies model_version exercises the happy path and says nothing about the branch that only runs when the server stays quiet.
  4. "Happy to align it here or in a follow-up, whichever you prefer" is a defect with a timer on it. Every reviewer reads it as diligence, and it is — but the choice lives in a comment, and comments do not survive a closing issue. If you write that sentence, the follow-up needs an issue number in it, or the polite version of leaving the bug in is the version that keeps it.

The fix here is one argument: pass the resolved model into _process_generate_response and use response.model_version or model. Then write the twin — the streaming test already exists, so the twin is a copy with stream=True deleted.

What I did not measure

All of this runs on mocks; I did not call the live Gemini API. So the honest scope is narrow and sufficient: given a reply with no model_version, the two call paths report different things. Whether Gemini's API omits model_version in practice is not something I measured — but a fallback branch exists because it can, and there is already a test for exactly that case on the other leg.

What I can add is that the streaming half is correct and tested. This is not a broken fix. It is a fix that did what its author said it would do, on the path it was aiming at.

The reading you want, when a fix lands on one of two siblings, is not "did the fix work" but "what did its author leave for later — and did anyone come back?"

來源:DEV Community · dev.to