use super::ResponsesRequestContext;
use super::model_policy::{
is_gpt5_codex_model, is_gpt6_model, is_gpt56_model, is_openai_gpt_responses_model, supports_assistant_phase_replay,
};
use crate::provider;
use crate::providers::openai::responses_api::build_standard_responses_payload;
use crate::providers::openai::types::{InstructionSegmentKind, OpenAIResponsesPayload};
use crate::providers::shared::split_dynamic_prompt_suffix;
use crate::system_prompt::{default_system_prompt, openai_gpt6_contract_addendum, openai_gpt56_contract_addendum};
use serde_json::{Value, json};
const DYNAMIC_RUNTIME_REMINDER_LABEL: &str = "[System reminder — runtime context, not a user request]";
fn strip_non_native_assistant_phase(input: &mut [Value]) {
for item in input {
if let Some(map) = item.as_object_mut() {
map.remove("phase");
}
}
}
fn default_replay_instructions(model: &str) -> Option<String> {
if is_gpt5_codex_model(model) {
Some(format!("You are Codex, based on GPT-5. {}", default_system_prompt()))
} else if is_gpt56_model(model)
|| is_gpt6_model(model)
|| vtcode_config::models::model_catalog_entry("openai", model)
.is_some_and(|entry| entry.prompt_contract.is_some())
{
Some(default_system_prompt())
} else {
None
}
}
pub(super) fn augment_openai_instructions(model: &str, instructions: String) -> String {
let addendum =
match vtcode_config::models::model_catalog_entry("openai", model).and_then(|entry| entry.prompt_contract) {
Some("gpt6") => Some(openai_gpt6_contract_addendum()),
Some("gpt56") => Some(openai_gpt56_contract_addendum()),
_ if is_gpt6_model(model) => Some(openai_gpt6_contract_addendum()),
_ if is_gpt56_model(model) => Some(openai_gpt56_contract_addendum()),
_ => None,
};
let Some(addendum) = addendum else {
return instructions;
};
let trimmed_addendum = addendum.trim();
if instructions.contains(trimmed_addendum) {
instructions
} else if instructions.trim().is_empty() {
addendum
} else {
format!("{instructions}\n\n{addendum}")
}
}
fn is_explicit_cache_breakpoint_model(model: &str) -> bool {
super::supports_prompt_cache_options(model)
}
fn is_eligible_cache_breakpoint_block(block: &Value) -> bool {
matches!(block.get("type").and_then(Value::as_str), Some("input_text" | "input_image" | "input_file"))
}
fn is_runtime_reminder_item(item: &Value) -> bool {
item.get("content").and_then(Value::as_array).is_some_and(|blocks| {
blocks.iter().any(|block| {
block
.get("text")
.and_then(Value::as_str)
.is_some_and(|text| text.starts_with(DYNAMIC_RUNTIME_REMINDER_LABEL))
})
})
}
fn apply_explicit_cache_breakpoints(input: &mut [Value], model: &str, instruction_anchor: bool) {
if !is_explicit_cache_breakpoint_model(model) || input.is_empty() {
return;
}
let stable_end = input.len().saturating_sub(1);
let mut marked = u8::from(instruction_anchor);
for item in input[..stable_end].iter_mut().rev() {
if marked >= 3 {
break;
}
if is_runtime_reminder_item(item) {
continue;
}
let Some(content) = item.get_mut("content").and_then(Value::as_array_mut) else {
continue;
};
let Some(block) = content.iter_mut().rev().find(|block| is_eligible_cache_breakpoint_block(block)) else {
continue;
};
if block.get("prompt_cache_breakpoint").is_some() {
continue;
}
block["prompt_cache_breakpoint"] = json!({ "mode": "explicit" });
marked += 1;
}
if marked == 0 {
let fallback_end = if input.len() > 1 { input.len() - 1 } else { input.len() };
for item in input[..fallback_end].iter_mut().rev() {
if is_runtime_reminder_item(item) {
continue;
}
let Some(content) = item.get_mut("content").and_then(Value::as_array_mut) else {
continue;
};
if let Some(block) = content.iter_mut().rev().find(|block| is_eligible_cache_breakpoint_block(block))
&& block.get("prompt_cache_breakpoint").is_none()
{
block["prompt_cache_breakpoint"] = json!({ "mode": "explicit" });
break;
}
}
}
}
pub(super) fn build_responses_item_history(
request: &provider::LLMRequest,
ctx: &ResponsesRequestContext<'_>,
) -> Result<OpenAIResponsesPayload, provider::LLMError> {
let preserve_structured_history = ctx.include_structured_history_in_input
|| (ctx.preserve_structured_history_on_replay
&& (is_openai_gpt_responses_model(&request.model) || is_gpt5_codex_model(&request.model)));
let mut responses_payload = build_standard_responses_payload(request, preserve_structured_history)?;
if responses_payload.instructions.is_none()
&& preserve_structured_history
&& let Some(instructions) = default_replay_instructions(&request.model)
{
responses_payload.instructions = Some(instructions);
}
separate_dynamic_instructions(request, &mut responses_payload);
if !(ctx.include_assistant_phase
|| ctx.preserve_assistant_phase_on_replay && supports_assistant_phase_replay(&request.model))
{
strip_non_native_assistant_phase(&mut responses_payload.input);
}
if ctx.is_responses_api_model && ctx.include_explicit_cache_breakpoints {
let instruction_anchor = is_explicit_cache_breakpoint_model(&request.model)
&& request.system_prompt.is_some()
&& responses_payload
.instructions
.as_ref()
.is_some_and(|text| !text.trim().is_empty());
if instruction_anchor && let Some(instructions) = responses_payload.instructions.take() {
responses_payload.input.insert(
0,
json!({
"role": "developer",
"content": [{"type": "input_text", "text": instructions,
"prompt_cache_breakpoint": {"mode": "explicit"}}]
}),
);
}
apply_explicit_cache_breakpoints(&mut responses_payload.input, &request.model, instruction_anchor);
}
Ok(responses_payload)
}
fn separate_dynamic_instructions(request: &provider::LLMRequest, payload: &mut OpenAIResponsesPayload) {
let Some(built) = payload.instructions.take() else {
return;
};
let segments = match payload.instruction_segments.take() {
Some(segments) => segments,
None => {
payload.instructions = Some(augment_openai_instructions(&request.model, built));
return;
}
};
let mut stable_parts: Vec<String> = Vec::with_capacity(segments.len());
let mut volatile_parts: Vec<String> = Vec::new();
for (kind, text) in &segments {
match kind {
InstructionSegmentKind::SystemPrompt => {
let (stable, dynamic) = split_dynamic_prompt_suffix(text);
if !stable.is_empty() {
stable_parts.push(stable);
}
if let Some(dynamic) = dynamic.filter(|text| !text.trim().is_empty()) {
volatile_parts.push(dynamic);
}
}
InstructionSegmentKind::HistorySystem | InstructionSegmentKind::FoldedHistory => {
volatile_parts.push(text.clone());
}
}
}
if volatile_parts.is_empty() {
payload.instructions = Some(augment_openai_instructions(&request.model, built));
return;
}
let stable_joined = stable_parts.join("\n\n");
let stable_instructions = if stable_joined.trim().is_empty() {
let had_system_prompt = segments.iter().any(|(kind, _)| *kind == InstructionSegmentKind::SystemPrompt);
let addendum_only = augment_openai_instructions(&request.model, String::new());
(had_system_prompt && !addendum_only.trim().is_empty()).then_some(addendum_only)
} else {
Some(augment_openai_instructions(&request.model, stable_joined))
};
payload.instructions = stable_instructions;
let tail = volatile_parts.join("\n\n");
let mut reminder = String::with_capacity(DYNAMIC_RUNTIME_REMINDER_LABEL.len() + 1 + tail.len());
reminder.push_str(DYNAMIC_RUNTIME_REMINDER_LABEL);
reminder.push('\n');
reminder.push_str(&tail);
payload.input.push(json!({
"role": "user",
"content": [{ "type": "input_text", "text": reminder }]
}));
}