use gproxy_protocol::{claude, openai};
use crate::TransformError;
use crate::common::{responses, tools};
mod items;
#[allow(deprecated)] pub(crate) fn transform(
body: bytes::Bytes,
model: &str,
stream: bool,
) -> Result<bytes::Bytes, TransformError> {
let input: openai::ResponseCreateRequest = serde_json::from_slice(&body)?;
let output = transform_typed(input, model, stream)?;
Ok(bytes::Bytes::from(serde_json::to_vec(&output)?))
}
#[allow(deprecated)] pub(crate) fn transform_typed(
input: openai::ResponseCreateRequest,
model: &str,
stream: bool,
) -> Result<claude::CreateMessageRequestBody, TransformError> {
let max_tokens = input
.max_output_tokens
.map(u64::from)
.unwrap_or(crate::common::DEFAULT_CLAUDE_MAX_TOKENS);
let (messages, system) = promote_system(
input_messages(input.input)?,
input.instructions.map(claude::StringOrArray::String),
)?;
let output = crate::wire!(claude::CreateMessageRequestBody {
model: model.to_owned().into(),
messages,
max_tokens,
cache_control: None,
container: None,
context_management: None,
diagnostics: input
.previous_response_id
.map(|id| claude::DiagnosticsParam {
previous_message_id: Some(Some(id)),
rest: Default::default(),
}),
fallback_credit_token: None,
fallbacks: None,
inference_geo: None,
mcp_servers: None,
metadata: input.metadata.and_then(|metadata| {
metadata
.get("user_id")
.cloned()
.map(|user_id| claude::Metadata {
user_id: Some(user_id),
rest: Default::default(),
})
}),
output_config: output_config(input.reasoning, input.text)?,
output_format: None,
service_tier: service_tier(input.service_tier)?,
speed: None,
stop_sequences: None,
stream: Some(stream),
system,
temperature: input.temperature,
thinking: None,
tool_choice: tool_choice(input.tool_choice, input.parallel_tool_calls)?,
tools: tools::responses_to_claude(input.tools)?,
top_k: None,
top_p: input.top_p,
user_profile_id: None,
rest: Default::default(),
});
Ok(output)
}
fn promote_system(
messages: Vec<claude::MessageParam>,
initial: Option<claude::SystemPrompt>,
) -> Result<(Vec<claude::MessageParam>, Option<claude::SystemPrompt>), TransformError> {
let mut system = match initial {
Some(claude::StringOrArray::String(text)) => text,
None => String::new(),
Some(_) => {
return Err(TransformError::unsupported(
"Responses instructions",
"future system shape",
));
}
};
let mut retained = Vec::new();
for message in messages {
if message.role != claude::MessageRole::Known(claude::MessageRoleKnown::System) {
retained.push(message);
continue;
}
let text = match message.content {
claude::StringOrArray::String(text) => text,
claude::StringOrArray::Array(blocks) => blocks
.into_iter()
.filter_map(|block| match block {
claude::ContentBlockParam::Text(block) => Some(block.text),
_ => None,
})
.collect::<Vec<_>>()
.join("\n"),
claude::StringOrArray::Raw(raw) => {
return Err(TransformError::unsupported(
"Responses system message",
raw.to_string(),
));
}
_ => {
return Err(TransformError::unsupported(
"Responses system message",
"future content shape",
));
}
};
if !system.is_empty() && !text.is_empty() {
system.push('\n');
}
system.push_str(&text);
}
Ok((
retained,
(!system.is_empty()).then_some(claude::StringOrArray::String(system)),
))
}
#[allow(deprecated)]
pub(crate) fn count_tokens(
input: openai::ResponseInputTokensRequest,
model: &str,
) -> Result<claude::CountTokensRequestBody, TransformError> {
Ok(crate::wire!(claude::CountTokensRequestBody {
model: model.to_owned().into(),
messages: input_messages(input.input)?,
cache_control: None,
context_management: None,
diagnostics: input
.previous_response_id
.map(|id| claude::DiagnosticsParam {
previous_message_id: Some(Some(id)),
rest: Default::default(),
}),
mcp_servers: None,
output_config: output_config(input.reasoning, input.text)?,
output_format: None,
service_tier: service_tier(input.service_tier)?,
speed: None,
system: input.instructions.map(claude::StringOrArray::String),
thinking: None,
tool_choice: tool_choice(input.tool_choice, input.parallel_tool_calls)?,
tools: tools::responses_to_claude(input.tools)?,
rest: Default::default(),
}))
}
fn input_messages(
input: Option<openai::ResponseInput>,
) -> Result<Vec<claude::MessageParam>, TransformError> {
match input {
None => Ok(Vec::new()),
Some(openai::ResponseInput::Text(text)) => Ok(vec![message(
claude::MessageRoleKnown::User,
vec![text_block(text)],
)]),
Some(openai::ResponseInput::Items(items)) => items
.into_iter()
.filter_map(|item| input_item(item).transpose())
.collect(),
Some(openai::ResponseInput::Unknown(_)) => Ok(Vec::new()),
}
}
fn input_item(item: openai::ResponseItem) -> Result<Option<claude::MessageParam>, TransformError> {
match item {
openai::ResponseItem::Message(message_item) => Ok(Some(match message_item {
openai::ResponseMessageItem::EasyInput(message_item) => {
let role = match message_item.role {
openai::ResponseEasyInputMessageRole::Assistant => {
claude::MessageRoleKnown::Assistant
}
openai::ResponseEasyInputMessageRole::System
| openai::ResponseEasyInputMessageRole::Developer => {
claude::MessageRoleKnown::System
}
openai::ResponseEasyInputMessageRole::User => claude::MessageRoleKnown::User,
#[cfg(not(feature = "exhaustive"))]
_ => {
return Err(crate::TransformError::unsupported(
"protocol enum",
"unrecognized external variant",
));
}
};
let blocks = match message_item.content {
openai::ResponseEasyInputContent::Text(text) => vec![text_block(text)],
openai::ResponseEasyInputContent::Parts(parts) => {
responses::input_to_claude(parts)?
}
openai::ResponseEasyInputContent::OutputParts(parts) => {
responses::output_to_claude(parts)?
}
openai::ResponseEasyInputContent::Unknown(_) => return Ok(None),
#[cfg(not(feature = "exhaustive"))]
_ => {
return Err(crate::TransformError::unsupported(
"protocol enum",
"unrecognized external variant",
));
}
};
message(role, blocks)
}
openai::ResponseMessageItem::Input(message_item) => {
let role = match message_item.role {
openai::ResponseInputMessageRole::User => claude::MessageRoleKnown::User,
openai::ResponseInputMessageRole::System
| openai::ResponseInputMessageRole::Developer => {
claude::MessageRoleKnown::System
}
#[cfg(not(feature = "exhaustive"))]
_ => {
return Err(crate::TransformError::unsupported(
"protocol enum",
"unrecognized external variant",
));
}
};
message(role, responses::input_to_claude(message_item.content)?)
}
openai::ResponseMessageItem::Output(message_item) => message(
claude::MessageRoleKnown::Assistant,
responses::output_to_claude(message_item.content)?,
),
openai::ResponseMessageItem::Unknown(_) => return Ok(None),
#[cfg(not(feature = "exhaustive"))]
_ => {
return Err(crate::TransformError::unsupported(
"protocol enum",
"unrecognized external variant",
));
}
})),
openai::ResponseItem::Typed(item) => match items::typed_item(*item) {
Ok(message) => Ok(Some(message)),
Err(TransformError::Unsupported { .. }) => Ok(None),
Err(error) => Err(error),
},
openai::ResponseItem::Unknown(_) => Ok(None),
#[cfg(not(feature = "exhaustive"))]
_ => {
return Err(crate::TransformError::unsupported(
"protocol enum",
"unrecognized external variant",
));
}
}
}
fn message(
role: claude::MessageRoleKnown,
content: Vec<claude::ContentBlockParam>,
) -> claude::MessageParam {
crate::wire!(claude::MessageParam {
role: claude::MessageRole::Known(role),
content: claude::StringOrArray::Array(content),
clear_at: None,
output_config: None,
rest: Default::default(),
})
}
fn text_block(text: String) -> claude::ContentBlockParam {
claude::ContentBlockParam::Text(crate::wire!(claude::TextBlock {
text,
type_: claude::TextBlockType::Text,
cache_control: None,
citations: None,
rest: Default::default(),
}))
}
fn tool_choice(
choice: Option<openai::ResponseToolChoice>,
parallel: Option<bool>,
) -> Result<Option<claude::ToolChoice>, TransformError> {
let disable_parallel_tool_use = parallel.map(|parallel| !parallel);
Ok(match choice {
None => None,
Some(openai::ResponseToolChoice::Mode(openai::ToolChoiceMode::Auto)) => Some(
claude::ToolChoice::Auto(crate::wire!(claude::ToolChoiceAuto {
type_: claude::ToolChoiceAutoType::Auto,
disable_parallel_tool_use,
rest: Default::default(),
})),
),
Some(openai::ResponseToolChoice::Mode(openai::ToolChoiceMode::Required)) => Some(
claude::ToolChoice::Any(crate::wire!(claude::ToolChoiceAny {
type_: claude::ToolChoiceAnyType::Any,
disable_parallel_tool_use,
rest: Default::default(),
})),
),
Some(openai::ResponseToolChoice::Mode(openai::ToolChoiceMode::None)) => Some(
claude::ToolChoice::None(crate::wire!(claude::ToolChoiceNone {
type_: claude::ToolChoiceNoneType::None,
rest: Default::default(),
})),
),
Some(openai::ResponseToolChoice::Function(choice)) => Some(claude::ToolChoice::Tool(
crate::wire!(claude::ToolChoiceTool {
name: choice.name,
type_: claude::ToolChoiceToolType::Tool,
disable_parallel_tool_use,
rest: Default::default(),
}),
)),
Some(openai::ResponseToolChoice::Custom(choice)) => Some(claude::ToolChoice::Tool(
crate::wire!(claude::ToolChoiceTool {
name: choice.name,
type_: claude::ToolChoiceToolType::Tool,
disable_parallel_tool_use,
rest: Default::default(),
}),
)),
Some(openai::ResponseToolChoice::Unknown(_)) => None,
Some(other) => {
return Err(TransformError::unsupported(
"OpenAI Responses tool choice",
serde_json::to_string(&other)?,
));
}
})
}
fn output_config(
reasoning: Option<openai::ReasoningConfig>,
text: Option<openai::TextConfig>,
) -> Result<Option<claude::OutputConfig>, TransformError> {
let effort = reasoning
.and_then(|reasoning| reasoning.effort)
.map(|effort| serde_json::from_value(serde_json::to_value(effort)?))
.transpose()?;
let format = match text.and_then(|text| text.format) {
Some(openai::ResponseFormat::JsonSchema(format)) => {
Some(crate::wire!(claude::JsonSchemaFormat {
type_: claude::JsonSchemaFormatType::Known(
claude::JsonSchemaFormatTypeKnown::JsonSchema,
),
schema: format.schema,
rest: Default::default(),
}))
}
Some(openai::ResponseFormat::Text(_)) | None => None,
Some(other) => {
return Err(TransformError::unsupported(
"OpenAI Responses format",
serde_json::to_string(&other)?,
));
}
};
Ok(
(effort.is_some() || format.is_some()).then_some(crate::wire!(claude::OutputConfig {
effort,
format,
task_budget: None,
rest: Default::default(),
})),
)
}
fn service_tier(
tier: Option<openai::ServiceTier>,
) -> Result<Option<claude::RequestServiceTier>, TransformError> {
Ok(match tier {
Some(openai::ServiceTier::Auto | openai::ServiceTier::Default) => Some(
claude::RequestServiceTier::Known(claude::RequestServiceTierKnown::Auto),
),
Some(openai::ServiceTier::Unknown(_)) => None,
_ => None,
})
}