prodex 0.53.0

OpenAI profile pooling and safe auto-rotate for Codex CLI and Claude Code
Documentation
use super::*;

pub(crate) fn translate_runtime_anthropic_messages_request(
    request: &RuntimeProxyRequest,
) -> Result<RuntimeAnthropicMessagesRequest> {
    let value = serde_json::from_slice::<serde_json::Value>(&request.body)
        .context("failed to parse Anthropic request body as JSON")?;
    let requested_model = value
        .get("model")
        .and_then(serde_json::Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .context("Anthropic request requires a non-empty model")?
        .to_string();
    let messages = value
        .get("messages")
        .and_then(serde_json::Value::as_array)
        .context("Anthropic request requires a messages array")?;
    if messages.is_empty() {
        bail!("Anthropic request requires at least one message");
    }
    let carried_server_tool_usage = runtime_proxy_anthropic_carried_server_tool_usage(messages);
    let target_model = runtime_proxy_claude_target_model(&requested_model);
    let native_shell_enabled = runtime_proxy_anthropic_native_shell_enabled_for_request(&value);
    let native_computer_enabled =
        runtime_proxy_anthropic_native_computer_enabled_for_request(&value, &target_model);

    let mut input = Vec::new();
    let mut native_client_tool_calls = BTreeMap::new();
    for message in messages {
        let role = message
            .get("role")
            .and_then(serde_json::Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .context("Anthropic message is missing a non-empty role")?;
        let content = message
            .get("content")
            .context("Anthropic message is missing content")?;
        input.extend(runtime_proxy_translate_anthropic_message_content(
            role,
            content,
            native_shell_enabled,
            native_computer_enabled,
            &mut native_client_tool_calls,
        )?);
    }
    if input.is_empty() {
        input.push(serde_json::json!({
            "role": "user",
            "content": "",
        }));
    }

    let mut translated_body = serde_json::Map::new();
    translated_body.insert(
        "model".to_string(),
        serde_json::Value::String(target_model.clone()),
    );
    translated_body.insert("input".to_string(), serde_json::Value::Array(input));
    translated_body.insert("stream".to_string(), serde_json::Value::Bool(true));
    translated_body.insert("store".to_string(), serde_json::Value::Bool(false));
    let base_instructions = runtime_proxy_anthropic_system_instructions(&value)?;
    let translated_tools = runtime_proxy_translate_anthropic_tools(
        &value,
        native_shell_enabled,
        native_computer_enabled,
    )?;
    let mut translated_tools = translated_tools;
    runtime_proxy_anthropic_register_server_tools_from_messages(
        messages,
        &mut translated_tools.server_tools,
    );
    if let Some(instructions) =
        runtime_proxy_anthropic_append_tool_instructions(base_instructions, translated_tools.memory)
    {
        translated_body.insert(
            "instructions".to_string(),
            serde_json::Value::String(instructions),
        );
    }
    if !translated_tools.tools.is_empty() {
        translated_body.insert(
            "tools".to_string(),
            serde_json::Value::Array(translated_tools.tools.clone()),
        );
    }
    if translated_tools.server_tools.web_search {
        translated_body.insert(
            "include".to_string(),
            serde_json::json!(["web_search_call.action.sources"]),
        );
    }
    if let Some(tool_choice) = runtime_proxy_translate_anthropic_tool_choice(
        &value,
        &translated_tools.server_tools,
        &translated_tools.tool_name_aliases,
        &translated_tools.native_tool_names,
    )?
    .or_else(|| translated_tools.implicit_tool_choice())
    {
        translated_body.insert("tool_choice".to_string(), tool_choice);
    }
    if let Some(effort) = runtime_proxy_anthropic_reasoning_effort(&value, &target_model) {
        translated_body.insert(
            "reasoning".to_string(),
            serde_json::json!({
                "summary": "auto",
                "effort": effort,
            }),
        );
    } else if runtime_proxy_anthropic_wants_thinking(&value) {
        translated_body.insert(
            "reasoning".to_string(),
            serde_json::json!({
                "summary": "auto",
            }),
        );
    }

    let mut translated_headers = vec![("Content-Type".to_string(), "application/json".to_string())];
    if let Some(user_agent) = runtime_proxy_request_header_value(&request.headers, "User-Agent") {
        translated_headers.push(("User-Agent".to_string(), user_agent.to_string()));
    }
    if let Some(session_id) = runtime_proxy_claude_session_id(request) {
        translated_headers.push(("session_id".to_string(), session_id));
    }
    translated_headers.push((
        PRODEX_INTERNAL_REQUEST_ORIGIN_HEADER.to_string(),
        PRODEX_INTERNAL_REQUEST_ORIGIN_ANTHROPIC_MESSAGES.to_string(),
    ));

    Ok(RuntimeAnthropicMessagesRequest {
        translated_request: RuntimeProxyRequest {
            method: request.method.clone(),
            path_and_query: format!("{RUNTIME_PROXY_OPENAI_UPSTREAM_PATH}/responses"),
            headers: translated_headers,
            body: serde_json::to_vec(&serde_json::Value::Object(translated_body))
                .context("failed to serialize translated Anthropic request")?,
        },
        requested_model,
        stream: value
            .get("stream")
            .and_then(serde_json::Value::as_bool)
            .unwrap_or(false),
        want_thinking: runtime_proxy_anthropic_wants_thinking(&value),
        server_tools: translated_tools.server_tools,
        carried_web_search_requests: carried_server_tool_usage.web_search_requests,
        carried_web_fetch_requests: carried_server_tool_usage.web_fetch_requests,
        carried_code_execution_requests: carried_server_tool_usage.code_execution_requests,
        carried_tool_search_requests: carried_server_tool_usage.tool_search_requests,
    })
}