Python: Normalize chat finish reasons#7105
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 45b65bfa-8e36-47b0-99d9-ec58ec60ace1
There was a problem hiding this comment.
Pull request overview
This PR standardizes how Python integrations populate and propagate terminal finish_reason values across providers/clients, ensuring consumers can reliably distinguish normal stops, length truncation, tool calls, and content filtering while avoiding telemetry serialization issues for non-canonical values.
Changes:
- Adds Responses-API finish-reason mapping for
OpenAIChatClient(non-streaming + streaming) and corresponding unit tests. - Normalizes and/or preserves provider finish reason strings across several clients (Chat Completions function-call mapping; Ollama/Claude/Bedrock/Copilot adjustments).
- Updates OTel message capture to only emit canonical finish reasons, and updates AG-UI RUN_ERROR handling to leave
finish_reasonunset.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| python/packages/openai/tests/openai/test_openai_chat_client.py | Adds unit tests covering Responses API status/incomplete-details → finish_reason mapping (streaming + non-streaming). |
| python/packages/openai/agent_framework_openai/_chat_completion_client.py | Normalizes Chat Completions finish_reason "function_call" → "tool_calls". |
| python/packages/openai/agent_framework_openai/_chat_client.py | Implements finish-reason mapping for Responses API terminal states and wires it into parsing. |
| python/packages/ollama/tests/test_ollama_chat_client.py | Updates test to expect unknown provider finish reasons to be preserved. |
| python/packages/ollama/agent_framework_ollama/_chat_client.py | Preserves unknown done_reason values (and maps tool-call presence to "tool_calls"). |
| python/packages/github_copilot/tests/test_github_copilot_agent.py | Updates expectations for content-filter-triggered usage and preservation of provider-specific finish reasons. |
| python/packages/github_copilot/agent_framework_github_copilot/_agent.py | Adjusts how Copilot usage events map content_filter_triggered and finish_reason. |
| python/packages/core/agent_framework/observability.py | Guards OTel output-message finish-reason mapping to avoid non-canonical values causing issues. |
| python/packages/claude/tests/test_claude_agent.py | Updates tests to expect normalized Claude stop reasons (stop/length). |
| python/packages/claude/agent_framework_claude/_agent.py | Introduces a Claude stop-reason → framework finish-reason normalization map (while preserving unknowns). |
| python/packages/bedrock/tests/test_bedrock_settings.py | Adds coverage for Bedrock guardrail intervention mapping to content_filter. |
| python/packages/bedrock/agent_framework_bedrock/_chat_client.py | Maps guardrail_intervened → content_filter and preserves unknown finish-reason strings. |
| python/packages/ag-ui/tests/ag_ui/test_event_converters.py | Updates RUN_ERROR test expectation to leave finish_reason unset. |
| python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py | Stops labeling generic RUN_ERROR events as content_filter. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 45b65bfa-8e36-47b0-99d9-ec58ec60ace1
| FINISH_REASON_MAP: dict[str, str] = { | ||
| "end_turn": "stop", | ||
| "stop_sequence": "stop", | ||
| "pause_turn": "stop", |
There was a problem hiding this comment.
Shouldn't we preserve pause_turn instead of normalizing it to stop? Anthropic defines pause_turn as an unfinished server-tool loop that callers must continue by sending the assistant content back, while stop means normal completion. This also replaces the previously exposed raw value, so existing continuation logic keyed on finish_reason == "pause_turn" will silently stop early.
Motivation & Context
Responses-based clients and several other Python integrations either dropped terminal finish metadata or exposed incompatible provider-native values. This restores consistent finish-reason propagation and avoids incorrectly reporting generic AG-UI errors as content filtering.
Description & Review Guide
RUN_ERRORchange: generic run errors now leavefinish_reasonunset rather than being labeledcontent_filter.Related Issue
Fixes #7025
Fixes #7051
Fixes #4622
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.