Skip to content

Normalize finish_reason across providers#321

Merged
cpsievert merged 8 commits into
mainfrom
feat/finish-reason-normalization
Jul 21, 2026
Merged

Normalize finish_reason across providers#321
cpsievert merged 8 commits into
mainfrom
feat/finish-reason-normalization

Conversation

@cpsievert

@cpsievert cpsievert commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Mirrors tidyverse/ellmer#1015 (and #1039 for the tool_use addition below).

What's now possible

turn.finish_reason returns a consistent vocabulary regardless of which provider you're using:

Value Meaning
"success" Normal completion
"tool_use" Model stopped to call a tool
"max_tokens" Hit token limit
"content_filter" Blocked by safety filter
"stop_sequence" Hit a stop sequence
"context_window" Context window exceeded

Unknown provider-specific reasons pass through as-is so callers can still inspect them.

Changes

  • Each provider module gains a normalize_finish_reason() function with a mapping dict keyed to the upstream API docs
  • AssistantTurn.finish_reason is now set by calling that function rather than storing the raw provider string
  • OpenAI streaming fix: response.incomplete events are now handled in stream_merge_chunks so truncated streaming responses are returned rather than silently dropped
  • All finish_reason == "stop" / "end_turn" / "STOP" assertions in provider tests updated to "success"
  • Unit tests added for each provider's normalization function covering known mappings, unknown pass-through, and None handling
  • Anthropic's tool_use and OpenAI completions' tool_calls now both normalize to "tool_use" instead of leaking through as provider-specific strings, per thisisnic's comment

@thisisnic

Copy link
Copy Markdown

FYI, I've extended the list of standard values in tidyverse/ellmer#1039 to also include "tool_use" as it was raising unnecessary warnings.

Maps provider-specific stop reasons to a common vocabulary:
"success", "max_tokens", "content_filter", "stop_sequence",
"context_window". Unknown reasons pass through as-is.

Also fixes OpenAI streaming to handle `response.incomplete` events
so truncated responses are returned rather than silently dropped.

Mirrors tidyverse/ellmer#1015.
@cpsievert
cpsievert force-pushed the feat/finish-reason-normalization branch from 1615e61 to b9b9dd4 Compare July 21, 2026 18:07
Anthropic's stop_reason and OpenAI completions' finish_reason both use
provider-specific spellings for "the model wants to call a tool"
(tool_use / tool_calls). Map both explicitly to "tool_use" so callers
get a consistent value instead of a provider-specific string leaking
through unnormalized.

Mirrors tidyverse/ellmer#1039.

This comment was marked as spam.

Now that finish_reason is normalized to a shared vocabulary across
providers, type it as a Literal (with a str fallback for reasons
chatlas doesn't yet recognize) instead of a bare str.
Each provider's _*_FINISH_REASON_MAP now declares its value type as
FinishReason, documenting that every mapped value is one of the known
reasons. normalize_finish_reason()'s return type drops the redundant
`FinishReason |` prefix (pyright collapses `Literal[...] | str` down to
plain `str` anyway) since these functions pass unrecognized reasons
through unchanged, matching ellmer's value_finish_reason() behavior.

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Comment thread chatlas/_provider_openai.py
Comment thread chatlas/_turn.py Outdated
Comment thread CHANGELOG.md Outdated
- Normalize OpenAI Responses API finish_reason to "tool_use" when the
  output contains a function_call, since status alone is "completed"
  even for tool-calling turns.
- Fix line length in test_provider_google.py import.
- Correct misleading comment on FinishReason type alias.
- Soften changelog wording since SnowflakeProvider doesn't yet set
  finish_reason.
@cpsievert
cpsievert merged commit b7a01c2 into main Jul 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants