Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/protocol/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub enum WireFormat {
/// OpenAI Responses API.
#[serde(rename = "openai_responses")]
OpenAiResponses,
/// Amazon Bedrock Converse API; the host owns AWS transport framing.
#[serde(rename = "bedrock_converse")]
BedrockConverse,
}

impl WireFormat {
Expand All @@ -29,6 +32,7 @@ impl WireFormat {
Self::OpenAiChat => "openai_chat",
Self::AnthropicMessages => "anthropic_messages",
Self::OpenAiResponses => "openai_responses",
Self::BedrockConverse => "bedrock_converse",
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/protocol/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,10 @@ fn stop_reason_from_str(reason: Option<&str>) -> StopReason {
match reason {
Some("length" | "max_tokens") => StopReason::MaxTokens,
Some("tool_calls" | "function_call" | "tool_use") => StopReason::ToolUse,
Some("content_filter") => StopReason::ContentFilter,
Some("content_filter" | "content_filtered" | "guardrail_intervened") => {
StopReason::ContentFilter
}
Some("malformed_model_output" | "malformed_tool_use") => StopReason::Error,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Some("stop" | "end_turn" | "stop_sequence") | None => StopReason::EndTurn,
Some(_) => StopReason::Unknown,
}
Expand Down
16 changes: 9 additions & 7 deletions crates/switchyard-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1389,13 +1389,15 @@ impl ApiError {
"message": self.message.clone(),
}
}),
WireFormat::OpenAiChat | WireFormat::OpenAiResponses => json!({
"error": {
"message": self.message.clone(),
"type": self.error_type,
"code": self.code,
}
}),
WireFormat::OpenAiChat | WireFormat::OpenAiResponses | WireFormat::BedrockConverse => {
json!({
"error": {
"message": self.message.clone(),
"type": self.error_type,
"code": self.code,
}
})
}
};
let mut response = (self.status, Json(body)).into_response();
response
Expand Down
6 changes: 5 additions & 1 deletion crates/switchyard-server/src/sse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ fn frame_event(
let data = redactor.json(serde_json::to_string(&value)?);
match target_format {
WireFormat::OpenAiChat => Ok(Event::default().data(data)),
WireFormat::BedrockConverse => Err(serde_json::Error::io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Bedrock requires AWS EventStream framing, unavailable on SSE endpoints",
))),
WireFormat::AnthropicMessages | WireFormat::OpenAiResponses => {
let event_type = value
.get("type")
Expand All @@ -95,7 +99,7 @@ fn frame_event(

fn error_event(target_format: WireFormat, message: String, redactor: &Redactor) -> Event {
match target_format {
WireFormat::OpenAiChat => Event::default().data(
WireFormat::OpenAiChat | WireFormat::BedrockConverse => Event::default().data(
redactor.json(
json!({
"error": {
Expand Down
31 changes: 29 additions & 2 deletions crates/switchyard-translation/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
# switchyard-translation

Pure Rust translation between OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages
request, response, and streaming formats.
Pure Rust translation between OpenAI Chat Completions, OpenAI Responses, Anthropic Messages,
and Amazon Bedrock Converse request, response, and streaming formats.

The crate translates through provider-neutral LLM types from `switchyard-protocol` and does not
depend on provider SDKs, HTTP servers, Python, or FFI bindings.

## Bedrock Converse

Use `WireFormat::BedrockConverse` (serialized as `bedrock_converse`) with `TranslationEngine`
for buffered bodies. Text, system instructions, function tools and tool results, common inference
settings, inline images, stop reasons, and cache-token usage have neutral mappings. JSON tool results
become serialized JSON text when normalized; same-format preservation retains their original JSON.
Native controls such as guardrails and additional model fields survive same-format request encoding.
Cross-format projection diagnoses their loss and rejects it under strict loss policy.

ConverseStream codecs operate on the JSON union events after the host removes and validates AWS
EventStream framing. Feed those events to `decode_event_stream`, then use `encode_stream` for target
JSON events. `decode_stream` is for SSE bytes and rejects Bedrock. A Bedrock stream must include
`messageStart`, closed content blocks, `messageStop`, and terminal `metadata` with reported usage;
truncation, malformed events, or provider exceptions fail the stream. Encoding requires reported
input and output tokens; an absent total is derived from those counts and cache details.

Same-format preserved events replay unchanged, including native reasoning signatures. Aggregated
Bedrock reasoning preserves signature and redacted-content fragments. Foreign opaque reasoning
stream details, including cross-format Anthropic/Bedrock signatures, are rejected. Tool arguments
may precede their ID and name; parallel calls are serialized into Bedrock blocks. A tool block
cannot resume after other content closes it. Encoding Bedrock rejects unsupported foreign media and provider-specific built-in tool
history.

The host owns the model ID in the request URL, AWS credentials, SigV4 signing, regions, retries,
and binary EventStream framing. This crate adds no Bedrock HTTP client or server endpoint. Adding
the public `WireFormat` member requires downstream exhaustive matches to handle the new variant.

## License

Licensed under the Apache License, Version 2.0. See the
Expand Down
16 changes: 15 additions & 1 deletion crates/switchyard-translation/src/codecs/anthropic/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,21 @@ fn encode_anthropic_stream(
}));
out
}
LlmResponseChunk::ReasoningDetailsDelta { text, .. } => {
LlmResponseChunk::ReasoningDetailsDelta { text, details, .. } => {
if details.iter().any(|detail| {
detail
.get("type")
.and_then(Value::as_str)
.is_some_and(|kind| kind.starts_with("bedrock."))
}) {
return encode_anthropic_stream(
state,
LlmResponseChunk::DecodeError {
message: "Bedrock reasoning signatures have no Anthropic stream mapping"
.into(),
},
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if text.is_empty() {
return Vec::new();
}
Expand Down
12 changes: 12 additions & 0 deletions crates/switchyard-translation/src/codecs/bedrock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Bedrock Converse buffered bodies and ConverseStream event payloads.

mod buffered;
pub(crate) mod stream;

pub use buffered::BedrockConverseCodec;
pub use stream::BedrockConverseStreamCodec;

pub(crate) use buffered::request_projection_diagnostics;
Loading
Loading