use crate::canonical::{CanonicalError, CanonicalRequest, Content, ErrorKind, ToolChoice};
use crate::protocol::{Envelope, ExecSpec, ProviderCtx, WireRequest};
pub(super) fn encode(
req: &CanonicalRequest,
ctx: &ProviderCtx,
) -> Result<WireRequest, CanonicalError> {
let program = ctx.exec.ok_or_else(|| CanonicalError {
kind: ErrorKind::Config,
message: "claude_code row carries no `exec` (the subprocess program); \
add `exec = \"claude\"` to the provider row"
.to_owned(),
provider_detail: None,
retry_after_seconds: None,
})?;
if !req.tools.is_empty() {
return Err(reject(
"claude_code carries no tool declarations; use the `anthropic` row for tools",
));
}
if !matches!(req.tool_choice, ToolChoice::Auto) {
return Err(reject(
"claude_code cannot express a tool_choice (no tools reach the CLI)",
));
}
let system = text_only(req.system.as_deref().unwrap_or(&[]), "system")?;
let prompt = prompt_text(req)?;
let mut spec = exec_spec(program, &system, ctx.model);
if let Some(effort) = req.reasoning {
spec.args.push("--effort".to_owned());
spec.args.push(effort.as_str().to_owned());
}
let mut wire = WireRequest::new(String::new(), prompt.into_bytes());
wire.exec = Some(spec);
Ok(wire)
}
pub(super) fn exec_spec(program: &str, system: &str, model: &str) -> ExecSpec {
let args = [
"-p",
"--output-format",
"stream-json",
"--include-partial-messages",
"--verbose", "--setting-sources",
"", "--tools",
"", "--disable-slash-commands",
"--strict-mcp-config", "--no-session-persistence",
"--system-prompt",
system, "--model",
model,
];
ExecSpec {
program: program.to_owned(),
args: args.iter().map(|s| (*s).to_owned()).collect(),
envelope: Envelope::Body,
}
}
fn prompt_text(req: &CanonicalRequest) -> Result<String, CanonicalError> {
match req.messages.as_slice() {
[one] if one.role == crate::canonical::Role::User => text_only(&one.content, "message"),
_ => Err(reject(
"claude_code is single-turn: the request must carry exactly one user \
message (multi-turn replay cannot ride the CLI; use the `anthropic` row)",
)),
}
}
fn text_only(blocks: &[Content], slot: &str) -> Result<String, CanonicalError> {
let mut texts = Vec::with_capacity(blocks.len());
for block in blocks {
match block {
Content::Text(t) => texts.push(t.as_str()),
_ => {
return Err(reject(&format!(
"claude_code accepts only text content in {slot} \
(images/documents/tool blocks cannot ride the CLI)"
)))
}
}
}
Ok(texts.join("\n\n"))
}
fn reject(message: &str) -> CanonicalError {
CanonicalError {
kind: ErrorKind::ParseInput,
message: message.to_owned(),
provider_detail: None,
retry_after_seconds: None,
}
}