Skip to content

Python: [Bug]: Anthropic and Mistral chat clients drop unmapped finish reasons #7849

Description

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonUsage: [Issues, PRs], Target: PythontriageUsage: [Issues], Target: All issues that still need to be triaged

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions