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.

Activity

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

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

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions