use crate::providers::{
ChatMessage, ProviderConversationItem, ProviderRequest, anthropic_messages_body,
claude_code_messages_body, codex_responses_body, openai_compatible_chat_completions_body,
};
use anyhow::Context;
use serde::Serialize;
use serde_json::Value;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PromptDumpProviderTarget {
Anthropic,
OpenAi,
OpenAiCompatible,
OpenAiCodex,
ClaudeCode,
}
impl FromStr for PromptDumpProviderTarget {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"anthropic" => Ok(Self::Anthropic),
"openai" => Ok(Self::OpenAi),
"openai-compatible" => Ok(Self::OpenAiCompatible),
"openai-codex" => Ok(Self::OpenAiCodex),
"claude-code" => Ok(Self::ClaudeCode),
other => anyhow::bail!(
"unsupported --provider target '{other}' for --dump-prompt; expected one of: anthropic, openai, openai-compatible, openai-codex, claude-code"
),
}
}
}
#[derive(Debug, Serialize)]
pub(crate) struct PromptDumpFixture {
pub(crate) system_prompt: String,
pub(crate) tools: Value,
}
pub(crate) fn provider_prompt_dump(
target: PromptDumpProviderTarget,
system_prompt: String,
) -> anyhow::Result<PromptDumpFixture> {
let request = ProviderRequest::from_conversation(
dump_model_for_target(target),
vec![
ProviderConversationItem::Message(ChatMessage::system(system_prompt)),
ProviderConversationItem::Message(ChatMessage::user(" ")),
],
);
match target {
PromptDumpProviderTarget::Anthropic => dump_from_anthropic_body(
anthropic_messages_body(&request.model, &request, None),
"system",
),
PromptDumpProviderTarget::ClaudeCode => dump_from_anthropic_body(
claude_code_messages_body(&request.model, &request, false, None, None),
"system",
),
PromptDumpProviderTarget::OpenAi | PromptDumpProviderTarget::OpenAiCompatible => {
dump_from_chat_completions_body(openai_compatible_chat_completions_body(
&request.model,
&request,
))
}
PromptDumpProviderTarget::OpenAiCodex => {
dump_from_responses_body(codex_responses_body(&request.model, &request))
}
}
}
#[cfg(test)]
pub(crate) fn provider_prompt_dump_with_openai_compatible_responses(
system_prompt: String,
) -> anyhow::Result<PromptDumpFixture> {
let request = ProviderRequest::from_conversation(
dump_model_for_target(PromptDumpProviderTarget::OpenAiCompatible),
vec![
ProviderConversationItem::Message(ChatMessage::system(system_prompt)),
ProviderConversationItem::Message(ChatMessage::user(" ")),
],
);
dump_from_responses_body(crate::providers::openai_compatible_responses_body(
&request.model,
&request,
))
}
fn dump_model_for_target(target: PromptDumpProviderTarget) -> &'static str {
match target {
PromptDumpProviderTarget::Anthropic => crate::providers::DEFAULT_ANTHROPIC_MODEL,
PromptDumpProviderTarget::ClaudeCode => crate::providers::DEFAULT_CLAUDE_CODE_MODEL,
PromptDumpProviderTarget::OpenAi
| PromptDumpProviderTarget::OpenAiCompatible
| PromptDumpProviderTarget::OpenAiCodex => crate::providers::DEFAULT_CODEX_MODEL,
}
}
fn dump_from_anthropic_body(body: Value, system_key: &str) -> anyhow::Result<PromptDumpFixture> {
Ok(PromptDumpFixture {
system_prompt: body
.get(system_key)
.and_then(Value::as_str)
.context("provider body missing system prompt")?
.to_string(),
tools: body
.get("tools")
.cloned()
.context("provider body missing tools")?,
})
}
fn dump_from_chat_completions_body(body: Value) -> anyhow::Result<PromptDumpFixture> {
let system_prompt = body
.get("messages")
.and_then(Value::as_array)
.and_then(|messages| {
messages.iter().find_map(|message| {
(message.get("role").and_then(Value::as_str) == Some("system"))
.then(|| message.get("content").and_then(Value::as_str))
.flatten()
})
})
.context("provider body missing system message")?
.to_string();
Ok(PromptDumpFixture {
system_prompt,
tools: body
.get("tools")
.cloned()
.context("provider body missing tools")?,
})
}
fn dump_from_responses_body(body: Value) -> anyhow::Result<PromptDumpFixture> {
Ok(PromptDumpFixture {
system_prompt: body
.get("instructions")
.and_then(Value::as_str)
.context("provider body missing instructions")?
.to_string(),
tools: body
.get("tools")
.cloned()
.context("provider body missing tools")?,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn request_with_system_prompt(system_prompt: &str) -> ProviderRequest {
ProviderRequest::from_conversation(
crate::providers::DEFAULT_CODEX_MODEL,
vec![
ProviderConversationItem::Message(ChatMessage::system(system_prompt)),
ProviderConversationItem::Message(ChatMessage::user(" ")),
],
)
}
#[test]
fn dump_prompt_provider_payload_parity_anthropic() {
let request = request_with_system_prompt("runtime prompt");
let body = anthropic_messages_body(&request.model, &request, None);
let dump = provider_prompt_dump(
PromptDumpProviderTarget::Anthropic,
"runtime prompt".to_string(),
)
.unwrap();
assert_eq!(dump.system_prompt, body["system"]);
assert_eq!(dump.tools, body["tools"]);
}
#[test]
fn dump_prompt_provider_payload_parity_openai_family() {
let request = request_with_system_prompt("runtime prompt");
let body = openai_compatible_chat_completions_body(&request.model, &request);
let dump = provider_prompt_dump(PromptDumpProviderTarget::OpenAi, "runtime prompt".into())
.unwrap();
assert_eq!(dump.system_prompt, body["messages"][0]["content"]);
assert_eq!(dump.tools, body["tools"]);
}
#[test]
fn dump_uses_anthropic_body_system_and_tools_shape() {
let dump = provider_prompt_dump(
PromptDumpProviderTarget::Anthropic,
"runtime prompt".to_string(),
)
.unwrap();
assert_eq!(dump.system_prompt, "runtime prompt");
let first = dump.tools.as_array().unwrap().first().unwrap();
assert_eq!(
first
.as_object()
.unwrap()
.keys()
.cloned()
.collect::<Vec<_>>(),
vec!["description", "input_schema", "name"]
);
assert!(first.get("input_schema").is_some());
assert!(first.get("parameters").is_none());
}
#[test]
fn dump_uses_openai_chat_completions_wrapper_shape() {
let dump =
provider_prompt_dump(PromptDumpProviderTarget::OpenAi, "runtime".to_string()).unwrap();
let first = dump.tools.as_array().unwrap().first().unwrap();
assert_eq!(first["type"], json!("function"));
assert!(first["function"].get("name").is_some());
assert!(first["function"].get("description").is_some());
assert!(first["function"].get("parameters").is_some());
}
#[test]
fn dump_uses_codex_responses_direct_tool_shape() {
let dump =
provider_prompt_dump(PromptDumpProviderTarget::OpenAiCodex, "runtime".to_string())
.unwrap();
let first = dump.tools.as_array().unwrap().first().unwrap();
assert_eq!(first["type"], json!("function"));
assert!(first.get("name").is_some());
assert!(first.get("parameters").is_some());
assert!(first.get("function").is_none());
}
#[test]
fn openai_compatible_responses_parity_helper_uses_direct_tool_shape() {
let dump =
provider_prompt_dump_with_openai_compatible_responses("runtime".to_string()).unwrap();
let first = dump.tools.as_array().unwrap().first().unwrap();
assert_eq!(first["type"], json!("function"));
assert!(first.get("name").is_some());
assert!(first.get("function").is_none());
}
}