use crate::{
Dialect,
transform::{
Converted, Report, TransformError,
identity::{IdentityFlow, IdentityRole, TargetIdPolicy},
},
wire::{DeclaredFields, claude::generate_content as c, openai::responses as r},
};
mod content;
mod facts;
pub(crate) mod mcp;
pub(crate) mod usage;
pub use facts::ClaudeResponseContext;
pub use usage::ResponsesUsageFacts;
pub fn claude_to_responses_response(
input: c::GenerateContentResponseBody,
context: ClaudeResponseContext,
flow: &mut IdentityFlow,
policy: &TargetIdPolicy,
) -> Result<Converted<r::GenerateContentResponseBody>, TransformError> {
if policy.dialect != Dialect::OpenAi {
return Err(TransformError::shape(
"identity.policy",
"expected Responses policy",
));
}
let input = input.into_declared();
if context.created_at < 0 {
return Err(TransformError::shape(
"created_at",
"negative creation time",
));
}
let incomplete = match input.stop_reason {
c::StopReason::MaxTokens => true,
c::StopReason::EndTurn
| c::StopReason::ToolUse
| c::StopReason::StopSequence
| c::StopReason::Refusal => false,
c::StopReason::ModelContextWindowExceeded => true,
c::StopReason::PauseTurn | c::StopReason::Compaction => true,
};
let mut ids = flow.clone();
let mut report = Report::default();
let response_id = content::id(
&mut ids,
policy,
Dialect::Claude,
IdentityRole::Response,
IdentityRole::Response,
Some(input.id),
0,
)?;
let bindings =
super::super::client_tools::Bindings::for_target(&context.request, Dialect::Claude)?;
let output = content::to_responses(
input.content,
incomplete,
input.stop_reason == c::StopReason::Refusal,
&mut ids,
policy,
&mut report,
&bindings,
)?;
let tier = input
.usage
.service_tier
.flatten()
.and_then(|value| match value {
c::ResponseServiceTier::Standard => Some(r::ServiceTier::Default),
c::ResponseServiceTier::Priority => Some(r::ServiceTier::Priority),
c::ResponseServiceTier::Batch => {
report.omitted("usage.service_tier", "Responses has no batch tier");
None
}
});
let usage = usage::to_responses(input.usage, context.usage, &mut report)?;
let created_at = context.created_at;
let mut target = context.into_response(response_id, created_at, input.model)?;
target.output = output;
target.usage = Some(Some(usage));
target.service_tier = tier.map(Some);
target.status = Some(if incomplete {
r::ResponseStatus::Incomplete
} else {
r::ResponseStatus::Completed
});
target.incomplete_details =
(input.stop_reason == c::StopReason::MaxTokens).then(|| r::ResponseIncompleteDetails {
reason: Some(r::ResponseIncompleteReason::MaxOutputTokens),
rest: Default::default(),
});
for (present, field) in [
(input.container.is_some(), "container"),
(input.context_management.is_some(), "context_management"),
(input.diagnostics.is_some(), "diagnostics"),
(input.stop_details.is_some(), "stop_details"),
(input.stop_sequence.is_some(), "stop_sequence"),
] {
if present {
report.omitted(
field,
"Responses has no equivalent Claude response metadata field",
);
}
}
*flow = ids;
Ok(Converted {
value: target,
report,
})
}
pub fn responses_to_claude_response(
input: r::GenerateContentResponseBody,
flow: &mut IdentityFlow,
policy: &TargetIdPolicy,
) -> Result<Converted<c::GenerateContentResponseBody>, TransformError> {
convert_to_claude(input, flow, policy)
}
fn convert_to_claude(
input: r::GenerateContentResponseBody,
flow: &mut IdentityFlow,
policy: &TargetIdPolicy,
) -> Result<Converted<c::GenerateContentResponseBody>, TransformError> {
if policy.dialect != Dialect::Claude {
return Err(TransformError::shape(
"identity.policy",
"expected Claude policy",
));
}
let input = input.into_declared();
if input.error.is_some() {
return Err(TransformError::invalid_result(
"response.error",
"upstream response contains an error",
));
}
let completed = match input.status {
Some(r::ResponseStatus::Completed) => true,
Some(r::ResponseStatus::Incomplete) => false,
Some(
r::ResponseStatus::Failed
| r::ResponseStatus::Cancelled
| r::ResponseStatus::Queued
| r::ResponseStatus::InProgress,
) => {
return Err(TransformError::invalid_result(
"response.status",
"not a successful terminal response",
));
}
None => return Err(TransformError::missing_metadata("response.status")),
};
let steered = input.incomplete_details.as_ref().and_then(|v| v.reason)
== Some(r::ResponseIncompleteReason::Steered);
let reason = if completed || steered {
if completed && input.incomplete_details.is_some() {
return Err(TransformError::invalid_result(
"incomplete_details",
"completed response has incomplete details",
));
}
None
} else {
Some(
match input
.incomplete_details
.and_then(|v| v.reason)
.ok_or_else(|| TransformError::missing_metadata("incomplete_details.reason"))?
{
r::ResponseIncompleteReason::MaxOutputTokens
| r::ResponseIncompleteReason::MaxMessages => c::StopReason::MaxTokens,
r::ResponseIncompleteReason::Steered | r::ResponseIncompleteReason::Interrupted => {
c::StopReason::EndTurn
}
r::ResponseIncompleteReason::ContentFilter => c::StopReason::Refusal,
},
)
};
let mut ids = flow.clone();
let mut report = Report::default();
let content = content::to_claude(input.output, completed, &mut ids, policy, &mut report)?;
let id = content::id(
&mut ids,
policy,
Dialect::OpenAi,
IdentityRole::Response,
IdentityRole::Response,
Some(input.id),
0,
)?;
let mut usage = usage::to_claude(
input
.usage
.flatten()
.ok_or_else(|| TransformError::missing_metadata("response.usage"))?,
)?;
usage.service_tier = input.service_tier.flatten().and_then(|tier| match tier {
r::ServiceTier::Default => Some(Some(c::ResponseServiceTier::Standard)),
r::ServiceTier::Priority => Some(Some(c::ResponseServiceTier::Priority)),
r::ServiceTier::Auto
| r::ServiceTier::Flex
| r::ServiceTier::Scale
| r::ServiceTier::Fast
| r::ServiceTier::Ultrafast => {
report.omitted(
"service_tier",
"Claude has no equivalent effective service tier",
);
None
}
});
let stop_reason = reason.unwrap_or(if content.tools {
c::StopReason::ToolUse
} else if content.refusal {
c::StopReason::Refusal
} else {
c::StopReason::EndTurn
});
for (present, field) in [
(input.instructions.is_some(), "instructions"),
(input.metadata.is_some(), "metadata"),
(!input.tools.is_empty(), "tools"),
(input.temperature.is_some(), "temperature"),
(input.top_p.is_some(), "top_p"),
(input.conversation.is_some(), "conversation"),
(input.completed_at.is_some(), "completed_at"),
(input.max_output_tokens.is_some(), "max_output_tokens"),
(input.max_tool_calls.is_some(), "max_tool_calls"),
(input.previous_response_id.is_some(), "previous_response_id"),
(input.prompt.is_some(), "prompt"),
(input.prompt_cache_key.is_some(), "prompt_cache_key"),
(input.prompt_cache_options.is_some(), "prompt_cache_options"),
(
input.prompt_cache_retention.is_some(),
"prompt_cache_retention",
),
(input.reasoning.is_some(), "reasoning"),
(input.safety_identifier.is_some(), "safety_identifier"),
(input.text.is_some(), "text"),
(input.top_logprobs.is_some(), "top_logprobs"),
(input.truncation.is_some(), "truncation"),
(input.user.is_some(), "user"),
(input.moderation.is_some(), "moderation"),
(input.background.is_some(), "background"),
] {
if present {
report.omitted(
field,
"Claude does not echo this Responses request or lifecycle metadata",
);
}
}
let value = c::GenerateContentResponseBody {
input_transformations: None,
type_: c::GenerateContentResponseBodyType::Tag,
id,
container: None,
content: content.blocks,
context_management: None,
diagnostics: None,
model: input.model,
role: c::ResponseRole::Assistant,
stop_details: None,
stop_reason,
stop_sequence: None,
usage,
rest: Default::default(),
};
*flow = ids;
Ok(Converted { value, report })
}