Skip to content

Python: Normalize chat finish reasons#7105

Open
eavanvalkenburg wants to merge 2 commits into
microsoft:mainfrom
eavanvalkenburg:fix_7025
Open

Python: Normalize chat finish reasons#7105
eavanvalkenburg wants to merge 2 commits into
microsoft:mainfrom
eavanvalkenburg:fix_7025

Conversation

@eavanvalkenburg

@eavanvalkenburg eavanvalkenburg commented Jul 14, 2026

Copy link
Copy Markdown
Member

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

  • What are the major changes? Normalize documented provider finish reasons, preserve unknown provider strings, populate the OpenAI Responses API terminal reason, and guard OTel output serialization against non-canonical values.
  • What is the impact of these changes? Consumers can distinguish normal stops, length limits, tool calls, and content filtering across supported clients without losing provider-specific terminal information.
  • What do you want reviewers to focus on? @moonbox3, please double-check the AG-UI RUN_ERROR change: generic run errors now leave finish_reason unset rather than being labeled content_filter.

Related Issue

Fixes #7025
Fixes #7051
Fixes #4622

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 45b65bfa-8e36-47b0-99d9-ec58ec60ace1
@giles17 giles17 added the python Usage: [Issues, PRs], Target: Python label Jul 14, 2026
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/ag-ui/agent_framework_ag_ui
   _event_converters.py110199%169
packages/bedrock/agent_framework_bedrock
   _chat_client.py4499678%305–306, 322–331, 337, 405, 414, 425, 427, 429, 434, 453–454, 478, 491, 503, 506, 514–515, 518–519, 521–522, 527–529, 531, 541–542, 564, 571, 580–581, 583–584, 586–588, 590, 592–593, 599–601, 604–605, 611–614, 620–630, 633, 652, 657, 723, 749, 761, 766, 794, 798–799, 802, 820, 844, 856, 860, 874, 882–883, 887, 889–896
packages/claude/agent_framework_claude
   _agent.py3282891%418–419, 423, 435, 443–445, 447–448, 478–480, 499, 503, 505, 509, 518, 564, 567, 607, 612–613, 687, 773, 799–802
packages/core/agent_framework
   observability.py10128791%407, 409–410, 413, 416, 419–420, 425–426, 432–433, 439–440, 447, 449–451, 454–456, 461–462, 468–469, 475–476, 483, 660–661, 871, 875–877, 879, 883–884, 888, 926, 928, 939–941, 943–945, 949, 957, 1081–1082, 1317, 1596–1597, 1708, 1855, 1894–1895, 2077, 2362–2365, 2371, 2397–2398, 2426–2437, 2556, 2559, 2571, 2588, 2592–2593, 2596, 2602, 2711, 2928, 2930
packages/github_copilot/agent_framework_github_copilot
   _agent.py4202195%61–62, 104, 113–115, 119, 538, 553–554, 633, 646, 780, 784, 936–937, 976, 979, 1102, 1125, 1170
packages/ollama/agent_framework_ollama
   _chat_client.py227996%399, 401, 412, 416–417, 432, 443, 522, 548
packages/openai/agent_framework_openai
   _chat_client.py129212790%308, 321, 671–675, 683–686, 692–696, 746–753, 755–757, 764–766, 824, 832, 855, 973, 1072, 1131, 1133, 1135, 1137, 1203, 1217, 1297, 1307, 1312, 1355, 1466–1467, 1482, 1706, 1711, 1715–1717, 1721–1722, 1805, 1815, 1842, 1848, 1858, 1864, 1869, 1875, 1880–1881, 1961, 2005, 2008–2011, 2025, 2027, 2035–2036, 2048, 2090, 2155, 2172, 2175, 2202–2204, 2243, 2260, 2263, 2325, 2332, 2369–2370, 2405, 2443–2444, 2462–2463, 2506, 2633–2634, 2652, 2738–2746, 2776, 2922, 2937, 2986–2989, 2999–3001, 3028–3030, 3040–3041, 3047, 3062, 3184–3185
   _chat_completion_client.py3731895%440, 536–537, 541, 782, 784, 789, 792, 883, 885, 902, 923, 931, 955, 968, 992, 1012, 1327
TOTAL44440524588% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8972 33 💤 0 ❌ 0 🔥 2m 8s ⏱️

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 86% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Failure Modes, Design Approach


Automated review by eavanvalkenburg's agents

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_reason unset.

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_intervenedcontent_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.

Comment thread python/packages/github_copilot/agent_framework_github_copilot/_agent.py Outdated
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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

4 participants