use choreo_ai_protocols::openai::{AssistantToolCall, AssistantToolFunction, ChatRequestMessage};
use choreo_ai_protocols::{ReasoningPassback, model_reasoning_passback};
use choreo_proto::{ReasoningArtifact, Turn};
use tracing::warn;
use crate::sessions::SessionState;
pub fn build_chat_request_messages(
session: &SessionState,
system_prompt: Option<&str>,
provider_slug: &str,
model: &str,
) -> Vec<ChatRequestMessage> {
let mut messages = Vec::new();
if let Some(prompt) = system_prompt {
messages.push(ChatRequestMessage::simple("system", prompt.to_string()));
}
let passback = model_reasoning_passback(provider_slug, model);
for turn in session.turns.values() {
if turn.undone {
continue;
}
if let Some(text) = &turn.user_text {
messages.push(ChatRequestMessage::simple("user", text.clone()));
}
let same_model = turn
.reasoning_producer
.as_ref()
.map(|p| (p.provider_slug.as_str(), p.model.as_str()))
== Some((provider_slug, model));
let include_artifact = same_model
&& match passback {
ReasoningPassback::None => false,
ReasoningPassback::ToolLoop => turn_has_tool_involvement(turn),
ReasoningPassback::AllTurns => true,
ReasoningPassback::Signature => true,
ReasoningPassback::ResponseId => false,
};
let has_tool_calls = !turn.tool_calls.is_empty();
if turn.assistant_text.is_some() || has_tool_calls {
let tool_calls = if has_tool_calls {
Some(
turn.tool_calls
.iter()
.map(|tc| AssistantToolCall {
id: tc.call_id.clone(),
kind: "function".to_string(),
function: AssistantToolFunction {
name: tc.name.clone(),
arguments: tc.arguments_json.clone(),
},
})
.collect(),
)
} else {
None
};
messages.push(ChatRequestMessage {
role: "assistant",
content: turn.assistant_text.clone(),
tool_call_id: None,
tool_calls,
reasoning_content: None,
reasoning: None,
reasoning_text: None,
reasoning_artifact: if include_artifact {
turn.reasoning_artifact.clone()
} else {
None
},
});
}
for tr in &turn.tool_results {
messages.push(ChatRequestMessage {
role: "tool",
content: Some(tr.content.clone()),
tool_call_id: Some(tr.call_id.clone()),
tool_calls: None,
reasoning_content: None,
reasoning: None,
reasoning_text: None,
reasoning_artifact: None,
});
}
}
messages
}
fn turn_has_tool_involvement(turn: &Turn) -> bool {
!turn.tool_calls.is_empty() || !turn.tool_results.is_empty()
}
pub(crate) fn initial_prev_resp_id(
session: &SessionState,
provider_slug: &str,
model: &str,
) -> Option<String> {
let same_producer = session
.config
.last_response_id_producer
.as_ref()
.map(|p| (p.provider_slug.as_str(), p.model.as_str()))
== Some((provider_slug, model));
if same_producer
&& model_reasoning_passback(provider_slug, model) == ReasoningPassback::ResponseId
{
session.config.last_response_id.clone()
} else {
None
}
}
pub(crate) fn warn_on_missing_reasoning_artifacts(
session: &SessionState,
session_id: u64,
provider_slug: &str,
model: &str,
) -> usize {
let passback = model_reasoning_passback(provider_slug, model);
if matches!(
passback,
ReasoningPassback::None | ReasoningPassback::ResponseId
) {
return 0;
}
let check_all_turns = matches!(
passback,
ReasoningPassback::AllTurns | ReasoningPassback::Signature
);
let mut problems = 0;
for (turn_id, turn) in session.turns.iter() {
if turn.undone {
continue;
}
let has_assistant_message = turn.assistant_text.is_some() || !turn.tool_calls.is_empty();
if !has_assistant_message {
continue;
}
if !check_all_turns && !turn_has_tool_involvement(turn) {
continue;
}
let same_producer = turn
.reasoning_producer
.as_ref()
.map(|p| (p.provider_slug.as_str(), p.model.as_str()))
== Some((provider_slug, model));
match (&turn.reasoning_artifact, turn.reasoning_producer.as_ref()) {
(None, _) => {
problems += 1;
warn!(
session_id,
turn_id,
provider_slug,
model,
passback = ?passback,
"reasoning artifact missing for turn; provider may reject this request",
);
}
(Some(_), None) => {
problems += 1;
warn!(
session_id,
turn_id,
provider_slug,
model,
passback = ?passback,
"reasoning artifact present but its producer is unrecorded; it will not be replayed and the provider may reject this request",
);
}
(Some(_), Some(_)) if !same_producer => {
problems += 1;
warn!(
session_id,
turn_id,
provider_slug,
model,
passback = ?passback,
"reasoning artifact produced by a different model; it will not be replayed and the provider may reject this request",
);
}
(Some(_), Some(_)) => {}
}
}
problems
}
pub(crate) fn reasoning_artifact_tokens(
enc: &tiktoken::CoreBpe,
artifact: &ReasoningArtifact,
) -> u32 {
let bytes = match artifact {
ReasoningArtifact::ChatReasoning { bytes: b, .. }
| ReasoningArtifact::AnthropicThinking(b)
| ReasoningArtifact::GoogleSignatures(b)
| ReasoningArtifact::ResponsesItems(b) => b,
};
match std::str::from_utf8(bytes) {
Ok(text) => enc.count(text) as u32,
Err(_) => (bytes.len() / 4) as u32,
}
}