use super::{Endpoint, GenerationIdentity, GenerationOutcome, GenerationProgress, transport};
use crate::transform::generate::chat_responses as p;
use crate::wire::openai::chat as h;
use crate::wire::openai::responses as r;
use crate::{
Dialect,
capability::Upstream,
codec::CodecLimits,
transform::{Converted, Report, TransformError},
wire::DeclaredFields,
};
#[derive(Debug)]
pub struct ChatViaResponses {
original_request: h::GenerateContentRequestBody,
target_request: r::GenerateContentRequestBody,
selected_model: String,
endpoint: Endpoint,
identities: GenerationIdentity,
report: Report,
}
impl ChatViaResponses {
pub fn prepare(
input: h::GenerateContentRequestBody,
selected_model: impl Into<String>,
endpoint: Endpoint,
identities: GenerationIdentity,
) -> Result<Self, TransformError> {
Self::prepare_with_calls(
input,
selected_model,
endpoint,
identities,
&Default::default(),
)
}
fn prepare_with_calls(
input: h::GenerateContentRequestBody,
selected_model: impl Into<String>,
endpoint: Endpoint,
mut identities: GenerationIdentity,
prior_calls: &std::collections::BTreeMap<String, p::ToolCallKind>,
) -> Result<Self, TransformError> {
let selected_model = selected_model.into();
if selected_model.trim().is_empty() {
return Err(TransformError::missing_metadata("selected_model"));
}
let original_request = input.into_declared();
if original_request.stream.flatten() == Some(true) {
return Err(TransformError::unsupported(
"stream",
"use the incremental invocation entrypoint",
));
}
let converted = p::chat_to_responses_request_with_calls(
original_request.clone(),
&selected_model,
prior_calls,
&mut identities.request,
&identities.request_policy,
)?;
Ok(Self {
original_request,
target_request: converted.value.into_declared(),
selected_model,
endpoint,
identities,
report: converted.report,
})
}
pub async fn prepare_with_state<S: crate::capability::StateStore>(
input: h::GenerateContentRequestBody,
endpoint: Endpoint,
identities: GenerationIdentity,
state: &super::GenerationStateAccess<'_, S>,
) -> Result<Self, TransformError> {
let selected_model = state.target.model.clone();
let original = input.into_declared();
let (restored, names) = super::history::chat(original.clone(), state).await?;
let mut prepared =
Self::prepare_with_calls(restored, selected_model, endpoint, identities, &names.kinds)?;
prepared.original_request = original;
Ok(prepared)
}
pub async fn prepare_with_capabilities<
S: crate::capability::StateStore,
R: crate::capability::ResourceAccess,
>(
input: h::GenerateContentRequestBody,
endpoint: Endpoint,
identities: GenerationIdentity,
state: &super::GenerationStateAccess<'_, S>,
resources: &super::GenerationResources<'_, R>,
) -> Result<Self, TransformError> {
let original = input.into_declared();
let materialized = resources.chat(original.clone()).await?;
let mut prepared =
Self::prepare_with_state(materialized, endpoint, identities, state).await?;
prepared.original_request = original;
Ok(prepared)
}
pub fn original_request(&self) -> &h::GenerateContentRequestBody {
&self.original_request
}
pub fn target_request(&self) -> &r::GenerateContentRequestBody {
&self.target_request
}
pub fn identities(&self) -> &GenerationIdentity {
&self.identities
}
pub fn report(&self) -> &Report {
&self.report
}
pub fn selected_model(&self) -> &str {
&self.selected_model
}
pub fn convert_response(
&mut self,
native: r::GenerateContentResponseBody,
_facts: (),
) -> Result<Converted<h::GenerateContentResponseBody>, TransformError> {
let native = native.into_declared();
p::responses_to_chat_response(
native,
&mut self.identities.response,
&self.identities.response_policy,
)
}
pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
&mut self,
upstream: &U,
target: &U::Target,
limits: CodecLimits,
state: &super::GenerationStateAccess<'_, S>,
progress: &mut GenerationProgress<r::GenerateContentResponseBody>,
facts: impl FnOnce(&r::GenerateContentResponseBody) -> Result<(), TransformError>,
) -> Result<GenerationOutcome<h::GenerateContentResponseBody>, TransformError> {
transport::bind(
(&self.endpoint, &self.selected_model),
&self.identities,
(&self.original_request, &self.target_request),
state,
limits,
progress,
false,
)?;
if !transport::send(
upstream,
target,
self.endpoint.clone(),
self.target_request.clone(),
limits,
progress,
)
.await?
{
return Ok(transport::rejected(progress));
}
self.recover(limits, state, progress, facts).await
}
pub async fn recover<S: crate::capability::StateStore>(
&mut self,
limits: CodecLimits,
state: &super::GenerationStateAccess<'_, S>,
progress: &mut GenerationProgress<r::GenerateContentResponseBody>,
facts: impl FnOnce(&r::GenerateContentResponseBody) -> Result<(), TransformError>,
) -> Result<GenerationOutcome<h::GenerateContentResponseBody>, TransformError> {
transport::bind(
(&self.endpoint, &self.selected_model),
&self.identities,
(&self.original_request, &self.target_request),
state,
limits,
progress,
true,
)?;
if progress
.raw_response
.as_ref()
.is_none_or(|r| !r.status.is_success())
{
return Err(TransformError::invalid_result(
"generation.recovery",
"successful native response required",
));
}
let native = transport::recover_native(progress, limits)?;
facts(&native)?;
let converted = self.convert_response(native, ())?;
transport::finish(progress, converted, self.report.clone(), limits)
}
}
#[derive(Debug)]
pub struct ChatReturnFacts {
pub parallel_tool_calls: bool,
pub tool_choice: r::input::ToolChoice,
pub prompt_cache_options: Option<r::response::ResponsePromptCacheOptions>,
pub usage: p::ChatUsageSupplement,
}
#[derive(Debug)]
pub struct ResponsesViaChat {
original_request: r::GenerateContentRequestBody,
target_request: h::GenerateContentRequestBody,
selected_model: String,
endpoint: Endpoint,
identities: GenerationIdentity,
report: Report,
}
impl ResponsesViaChat {
pub fn prepare(
input: r::GenerateContentRequestBody,
selected_model: impl Into<String>,
endpoint: Endpoint,
identities: GenerationIdentity,
) -> Result<Self, TransformError> {
Self::prepare_with_replay(
input,
selected_model,
endpoint,
identities,
&Default::default(),
)
}
fn prepare_with_replay(
input: r::GenerateContentRequestBody,
selected_model: impl Into<String>,
endpoint: Endpoint,
mut identities: GenerationIdentity,
replay: &super::GenerationToolReplay,
) -> Result<Self, TransformError> {
let selected_model = selected_model.into();
if selected_model.trim().is_empty() {
return Err(TransformError::missing_metadata("selected_model"));
}
let original_request = input.into_declared();
if original_request.stream.flatten() == Some(true) {
return Err(TransformError::unsupported(
"stream",
"use the incremental invocation entrypoint",
));
}
let mut converted =
p::responses_to_chat_request(original_request.clone(), &selected_model)?;
super::legacy_chat::restore(&mut converted.value, replay, &mut converted.report)?;
super::claude_chat_ids::chat_request(
&mut converted.value,
&mut identities,
Dialect::OpenAi,
)?;
Ok(Self {
original_request,
target_request: converted.value.into_declared(),
selected_model,
endpoint,
identities,
report: converted.report,
})
}
pub async fn prepare_with_state<S: crate::capability::StateStore>(
input: r::GenerateContentRequestBody,
endpoint: Endpoint,
identities: GenerationIdentity,
state: &super::GenerationStateAccess<'_, S>,
) -> Result<Self, TransformError> {
let selected_model = state.target.model.clone();
let original = input.into_declared();
let mut lowered = original.clone();
let mut tool_report = Report::default();
p::client_tools::Bindings::new(&original)?.lower(&mut lowered, &mut tool_report)?;
let (restored, names) = super::history::responses(lowered, state).await?;
let mut prepared =
Self::prepare_with_replay(restored, selected_model, endpoint, identities, &names)?;
prepared.original_request = original;
prepared.report.diagnostics.extend(tool_report.diagnostics);
Ok(prepared)
}
pub async fn prepare_with_capabilities<
S: crate::capability::StateStore,
R: crate::capability::ResourceAccess,
>(
input: r::GenerateContentRequestBody,
endpoint: Endpoint,
identities: GenerationIdentity,
state: &super::GenerationStateAccess<'_, S>,
resources: &super::GenerationResources<'_, R>,
) -> Result<Self, TransformError> {
let original = input.into_declared();
let materialized = resources.responses(original.clone()).await?;
let mut prepared =
Self::prepare_with_state(materialized, endpoint, identities, state).await?;
prepared.original_request = original;
Ok(prepared)
}
pub fn original_request(&self) -> &r::GenerateContentRequestBody {
&self.original_request
}
pub fn target_request(&self) -> &h::GenerateContentRequestBody {
&self.target_request
}
pub fn identities(&self) -> &GenerationIdentity {
&self.identities
}
pub fn report(&self) -> &Report {
&self.report
}
pub fn selected_model(&self) -> &str {
&self.selected_model
}
pub fn convert_response(
&mut self,
native: h::GenerateContentResponseBody,
facts: ChatReturnFacts,
) -> Result<Converted<r::GenerateContentResponseBody>, TransformError> {
let native = native.into_declared();
p::chat_to_responses_response(
native,
p::ResponsesResponseContext {
request: self.original_request.clone(),
effective_parallel_tool_calls: facts.parallel_tool_calls,
effective_tool_choice: facts.tool_choice,
usage: facts.usage,
effective_prompt_cache_options: facts.prompt_cache_options,
},
&mut self.identities.response,
&self.identities.response_policy,
)
}
pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
&mut self,
upstream: &U,
target: &U::Target,
limits: CodecLimits,
state: &super::GenerationStateAccess<'_, S>,
progress: &mut GenerationProgress<h::GenerateContentResponseBody>,
facts: impl FnOnce(&h::GenerateContentResponseBody) -> Result<ChatReturnFacts, TransformError>,
) -> Result<GenerationOutcome<r::GenerateContentResponseBody>, TransformError> {
transport::bind(
(&self.endpoint, &self.selected_model),
&self.identities,
(&self.original_request, &self.target_request),
state,
limits,
progress,
false,
)?;
if !transport::send(
upstream,
target,
self.endpoint.clone(),
self.target_request.clone(),
limits,
progress,
)
.await?
{
return Ok(transport::rejected(progress));
}
self.recover(limits, state, progress, facts).await
}
pub async fn recover<S: crate::capability::StateStore>(
&mut self,
limits: CodecLimits,
state: &super::GenerationStateAccess<'_, S>,
progress: &mut GenerationProgress<h::GenerateContentResponseBody>,
facts: impl FnOnce(&h::GenerateContentResponseBody) -> Result<ChatReturnFacts, TransformError>,
) -> Result<GenerationOutcome<r::GenerateContentResponseBody>, TransformError> {
transport::bind(
(&self.endpoint, &self.selected_model),
&self.identities,
(&self.original_request, &self.target_request),
state,
limits,
progress,
true,
)?;
if progress
.raw_response
.as_ref()
.is_none_or(|r| !r.status.is_success())
{
return Err(TransformError::invalid_result(
"generation.recovery",
"successful native response required",
));
}
let native = transport::recover_native(progress, limits)?;
let facts = facts(&native)?;
let converted = self.convert_response(native, facts)?;
transport::finish(progress, converted, self.report.clone(), limits)
}
}
super::prepared::prepared_generation!(ChatViaResponses, h::GenerateContentRequestBody => r::GenerateContentRequestBody);
super::prepared::stream_synthesis!(ChatViaResponses, h::GenerateContentRequestBody);
super::prepared::prepared_generation!(ResponsesViaChat, r::GenerateContentRequestBody => h::GenerateContentRequestBody);
super::prepared::stream_synthesis!(ResponsesViaChat, r::GenerateContentRequestBody);