Description
#7105 gave chat clients a fallback so an unmapped provider finish reason value passes through as the raw string instead of vanishing to None. It touched ag, bedrock, claude, core, github_copilot, ollama, and openai. It did not touch gemini.
RawGeminiChatClient._map_finish_reason in python/packages/gemini/agent_framework_gemini/_chat_client.py still has no fallback:
def _map_finish_reason(self, reason: str | None) -> FinishReasonLiteral | None:
if not reason:
return None
return _FINISH_REASON_MAP.get(reason)
_FINISH_REASON_MAP covers 13 of the 18 members of google.genai.types.FinishReason in the package's own pinned dependency range (google-genai>=1.69.0,<3.0.0). Missing: OTHER, TOO_MANY_TOOL_CALLS, NO_IMAGE, IMAGE_OTHER (plus FINISH_REASON_UNSPECIFIED, which is a legitimate absent case and should keep mapping to None).
This cascades into two more silent losses. In _process_chunk:
if finish_reason and (usage := self._parse_usage(chunk.usage_metadata)):
Usage is attached to a streamed chunk only when finish_reason is truthy, so an unmapped reason drops the finish reason and the whole turn's token/billing accounting. ChatTelemetryLayer in observability.py skips recording the terminal OTel span state the same way when finish_reason is falsy.
Concretely: a Gemini call in an agentic tool loop trips Gemini's own tool-call-count guardrail and returns finish_reason=TOO_MANY_TOOL_CALLS. The caller sees finish_reason=None, no usage, and has no way to tell the run stopped abnormally instead of completing normally.
Code Sample
from google.genai import types
from agent_framework_gemini._chat_client import RawGeminiChatClient
chunk = types.GenerateContentResponse(
candidates=[
types.Candidate(
content=types.Content(role="model", parts=[]),
finish_reason=types.FinishReason.TOO_MANY_TOOL_CALLS,
)
],
model_version="gemini-2.5-flash",
usage_metadata=types.GenerateContentResponseUsageMetadata(
prompt_token_count=1200, candidates_token_count=340, total_token_count=1540,
),
)
update = RawGeminiChatClient._process_chunk(object.__new__(RawGeminiChatClient), chunk)
print(update.finish_reason) # None -- should be "TOO_MANY_TOOL_CALLS"
print(update.contents) # [] -- usage silently dropped
Error Messages / Stack Traces
No exception is raised; the failure is silent (finish reason and usage both become absent).
Package Versions
agent-framework-gemini: 1.0.0b260813 (current main), google-genai: 2.17.0
Python Version
Python 3.13
Additional Context
Bedrock's version of the same method is the canonical shape after #7105:
def _map_finish_reason(self, reason: str | None) -> str | None:
if not reason:
return None
return FINISH_REASON_MAP.get(reason.lower(), reason)
I have a minimal fix ready (mirrors the ollama/openai pattern of wrapping the raw fallback in FinishReason(...) so it type-checks under this package's strict Pyright config, since bedrock's file carries a blanket # type: ignore that gemini's does not) plus new unit tests covering the four newly-mapped values, the usage-cascade case, and regressions for the 13 already-mapped values and the None/FINISH_REASON_UNSPECIFIED absent case. Opening a PR for it.
Description
#7105 gave chat clients a fallback so an unmapped provider finish reason value passes through as the raw string instead of vanishing to
None. It touchedag,bedrock,claude,core,github_copilot,ollama, andopenai. It did not touchgemini.RawGeminiChatClient._map_finish_reasoninpython/packages/gemini/agent_framework_gemini/_chat_client.pystill has no fallback:_FINISH_REASON_MAPcovers 13 of the 18 members ofgoogle.genai.types.FinishReasonin the package's own pinned dependency range (google-genai>=1.69.0,<3.0.0). Missing:OTHER,TOO_MANY_TOOL_CALLS,NO_IMAGE,IMAGE_OTHER(plusFINISH_REASON_UNSPECIFIED, which is a legitimate absent case and should keep mapping toNone).This cascades into two more silent losses. In
_process_chunk:Usage is attached to a streamed chunk only when
finish_reasonis truthy, so an unmapped reason drops the finish reason and the whole turn's token/billing accounting.ChatTelemetryLayerinobservability.pyskips recording the terminal OTel span state the same way whenfinish_reasonis falsy.Concretely: a Gemini call in an agentic tool loop trips Gemini's own tool-call-count guardrail and returns
finish_reason=TOO_MANY_TOOL_CALLS. The caller seesfinish_reason=None, no usage, and has no way to tell the run stopped abnormally instead of completing normally.Code Sample
Error Messages / Stack Traces
No exception is raised; the failure is silent (finish reason and usage both become absent).
Package Versions
agent-framework-gemini: 1.0.0b260813 (current main), google-genai: 2.17.0
Python Version
Python 3.13
Additional Context
Bedrock's version of the same method is the canonical shape after #7105:
I have a minimal fix ready (mirrors the
ollama/openaipattern of wrapping the raw fallback inFinishReason(...)so it type-checks under this package's strict Pyright config, sincebedrock's file carries a blanket# type: ignorethatgemini's does not) plus new unit tests covering the four newly-mapped values, the usage-cascade case, and regressions for the 13 already-mapped values and theNone/FINISH_REASON_UNSPECIFIEDabsent case. Opening a PR for it.