Description
#7105 gave the chat clients a fallback so an unmapped provider finish reason passes through as the raw string instead of vanishing to None. It touched ag-ui, bedrock, claude, core, github_copilot, ollama, and openai. It did not touch anthropic or mistral, and both still drop unmapped values. (gemini has the same gap and is already covered by #7836 / #7837, so it is out of scope here.)
Anthropic (python/packages/anthropic/agent_framework_anthropic/_chat_client.py) looks up the map with no default, in three places (non-streaming _process_message, and the message_start and message_delta branches of _process_stream_event):
finish_reason=FINISH_REASON_MAP.get(message.stop_reason) if message.stop_reason else None,
FINISH_REASON_MAP covers end_turn, stop_sequence, max_tokens, tool_use, refusal, and pause_turn. model_context_window_exceeded, a documented Anthropic stop_reason, is not covered, so a turn that ends because the context window was exceeded is reported to the caller as finish_reason=None, indistinguishable from a response that carries no stop reason at all.
Mistral (python/packages/mistral/agent_framework_mistral/_chat_client.py) does the same in _parse_response and _parse_chunk:
if reason := choice.get("finish_reason"):
finish_reason = _FINISH_REASON_MAP.get(str(reason))
_FINISH_REASON_MAP covers stop, length, model_length, and tool_calls. Mistral's error finish reason is not covered, so a failed generation also arrives as finish_reason=None.
In both packages this is a silent loss: nothing is logged and no exception is raised. A caller cannot tell an abnormal ending from a normal one, and OTel gen_ai spans record no finish reason for those turns.
FinishReason is a NewType("FinishReason", str) documented as accepting any string for extensibility, and bedrock/claude/ollama already pass unmapped provider values straight through, so the fallback is the framework's established behavior — these two packages were simply missed.
Code Sample
from unittest.mock import MagicMock
from agent_framework_anthropic import AnthropicClient
message = MagicMock()
message.id = "msg_123"
message.model = "claude-sonnet-4-5"
message.content = []
message.usage = None
message.stop_reason = "model_context_window_exceeded"
client = AnthropicClient(api_key="...", model_id="claude-sonnet-4-5")
print(client._process_message(message, {}).finish_reason)
# None -- expected "model_context_window_exceeded"
The Mistral equivalent: a chat-completion payload whose choice carries "finish_reason": "error" yields ChatResponse.finish_reason is None, both for get_response and for the streamed final response.
Error Messages / Stack Traces
No exception is raised. The failure is silent: the finish reason becomes None.
Package Versions
agent-framework-anthropic, agent-framework-mistral, agent-framework-core (current main, a2a8635)
Python Version
Python 3.12
Additional Context
The fix mirrors the shape already used by bedrock and claude: fall back to the raw provider value, wrapped in FinishReason(...) so it type-checks under each package's strict Pyright config. It is additive — every value in the two maps keeps mapping exactly as it does today, and None/absent stays None.
I am going to take this on. I have the change and unit tests ready (unmapped value preserved, non-streaming and streaming, plus regressions for the already-mapped values and the absent case); the new tests fail on main and pass with the fix, and poe syntax, poe pyright, poe test-typing, and poe test are clean for both packages.
Description
#7105 gave the chat clients a fallback so an unmapped provider finish reason passes through as the raw string instead of vanishing to
None. It touchedag-ui,bedrock,claude,core,github_copilot,ollama, andopenai. It did not touchanthropicormistral, and both still drop unmapped values. (geminihas the same gap and is already covered by #7836 / #7837, so it is out of scope here.)Anthropic (
python/packages/anthropic/agent_framework_anthropic/_chat_client.py) looks up the map with no default, in three places (non-streaming_process_message, and themessage_startandmessage_deltabranches of_process_stream_event):FINISH_REASON_MAPcoversend_turn,stop_sequence,max_tokens,tool_use,refusal, andpause_turn.model_context_window_exceeded, a documented Anthropicstop_reason, is not covered, so a turn that ends because the context window was exceeded is reported to the caller asfinish_reason=None, indistinguishable from a response that carries no stop reason at all.Mistral (
python/packages/mistral/agent_framework_mistral/_chat_client.py) does the same in_parse_responseand_parse_chunk:_FINISH_REASON_MAPcoversstop,length,model_length, andtool_calls. Mistral'serrorfinish reason is not covered, so a failed generation also arrives asfinish_reason=None.In both packages this is a silent loss: nothing is logged and no exception is raised. A caller cannot tell an abnormal ending from a normal one, and OTel
gen_aispans record no finish reason for those turns.FinishReasonis aNewType("FinishReason", str)documented as accepting any string for extensibility, andbedrock/claude/ollamaalready pass unmapped provider values straight through, so the fallback is the framework's established behavior — these two packages were simply missed.Code Sample
The Mistral equivalent: a chat-completion payload whose choice carries
"finish_reason": "error"yieldsChatResponse.finish_reason is None, both forget_responseand for the streamed final response.Error Messages / Stack Traces
No exception is raised. The failure is silent: the finish reason becomes
None.Package Versions
agent-framework-anthropic, agent-framework-mistral, agent-framework-core (current
main, a2a8635)Python Version
Python 3.12
Additional Context
The fix mirrors the shape already used by
bedrockandclaude: fall back to the raw provider value, wrapped inFinishReason(...)so it type-checks under each package's strict Pyright config. It is additive — every value in the two maps keeps mapping exactly as it does today, andNone/absent staysNone.I am going to take this on. I have the change and unit tests ready (unmapped value preserved, non-streaming and streaming, plus regressions for the already-mapped values and the absent case); the new tests fail on
mainand pass with the fix, andpoe syntax,poe pyright,poe test-typing, andpoe testare clean for both packages.