Skip to content

[bug] OpenAI content_filter is silently translated to Anthropic end_turn #369

Description

@Atharva-Kanherkar

Symptom

When an Anthropic Messages request is served by an OpenAI-compatible backend, an upstream finish_reason of content_filter is translated to an Anthropic stop_reason of end_turn.

A response blocked or truncated by content filtering is therefore presented as a normal completion. The translation also emits no diagnostic indicating that the stop reason was lost.

Reproduction

Translate an OpenAI Chat response to Anthropic Messages with the following values:

  • Response ID: chatcmpl-probe
  • Model: gpt-4o
  • Assistant content: partial answer before the block
  • Finish reason: content_filter
  • Prompt tokens: 10
  • Completion tokens: 5
  • Total tokens: 15

Call translate_response with WireFormat::OpenAiChat as the source, WireFormat::AnthropicMessages as the target, and the default translation policy.

The translated response reports stop_reason: "end_turn".

A control response with finish_reason: "stop" also reports stop_reason: "end_turn", making the filtered response indistinguishable from a normal completion.

Expected vs. actual

  • Expected: Translation should not silently make a content-filter stop indistinguishable from a normal completion. It should preserve the distinction, emit a lossy-conversion diagnostic and honor the configured lossy-conversion policy, or translate the event as a failure.
  • Actual: The response contains stop_reason: "end_turn" and no translation diagnostic.

Environment

  • Switchyard commit SHA: fb3fc30
  • Python: python3 --version reports 3.9.6, though this path is Rust-only
  • OS / arch: macOS 26.2, Darwin 25.2.0, arm64
  • Install path: source build
  • Inbound format: Anthropic Messages
  • Backend: OpenAI or another OpenAI-compatible upstream

Additional context

The buffered OpenAI decoder preserves content_filter as StopReason::ContentFilter in crates/switchyard-translation/src/codecs/openai_chat/buffered.rs:1133. The OpenAI encoder maps that normalized variant back to content_filter at line 1148.

The loss occurs in the Anthropic buffered encoder at crates/switchyard-translation/src/codecs/anthropic/buffered.rs:986, where StopReason::ContentFilter is mapped to end_turn.

The streaming path has the same behavior. The OpenAI stream decoder copies the upstream finish reason into the normalized stop event at crates/switchyard-translation/src/codecs/openai_chat/stream.rs:142. The Anthropic stream encoder stores that reason at crates/switchyard-translation/src/codecs/anthropic/stream.rs:206, then maps unrecognized values, including content_filter, to end_turn at line 571.

The stream accumulator also recognizes content_filter as StopReason::ContentFilter at crates/protocol/src/stream.rs:432.

Because the Anthropic vocabulary handled by this codec has no direct content-filter equivalent, the wire behavior needs a design decision. Protocol-safe options include:

  1. Emit a lossy-conversion diagnostic and honor LossyConversionPolicy.
  2. Translate a filtered generation into an error response or event.
  3. Preserve the original reason in an explicitly supported extension, if one exists or is introduced.

I can provide the local probe as a regression test once the desired behavior is chosen.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions