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:
- Emit a lossy-conversion diagnostic and honor
LossyConversionPolicy.
- Translate a filtered generation into an error response or event.
- 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.
Symptom
When an Anthropic Messages request is served by an OpenAI-compatible backend, an upstream
finish_reasonofcontent_filteris translated to an Anthropicstop_reasonofend_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:
chatcmpl-probegpt-4opartial answer before the blockcontent_filterCall
translate_responsewithWireFormat::OpenAiChatas the source,WireFormat::AnthropicMessagesas the target, and the default translation policy.The translated response reports
stop_reason: "end_turn".A control response with
finish_reason: "stop"also reportsstop_reason: "end_turn", making the filtered response indistinguishable from a normal completion.Expected vs. actual
stop_reason: "end_turn"and no translation diagnostic.Environment
fb3fc30python3 --versionreports 3.9.6, though this path is Rust-onlyAdditional context
The buffered OpenAI decoder preserves
content_filterasStopReason::ContentFilterincrates/switchyard-translation/src/codecs/openai_chat/buffered.rs:1133. The OpenAI encoder maps that normalized variant back tocontent_filterat line 1148.The loss occurs in the Anthropic buffered encoder at
crates/switchyard-translation/src/codecs/anthropic/buffered.rs:986, whereStopReason::ContentFilteris mapped toend_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 atcrates/switchyard-translation/src/codecs/anthropic/stream.rs:206, then maps unrecognized values, includingcontent_filter, toend_turnat line 571.The stream accumulator also recognizes
content_filterasStopReason::ContentFilteratcrates/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:
LossyConversionPolicy.I can provide the local probe as a regression test once the desired behavior is chosen.