magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

pub(super) fn build_vision_request(
    config: &VisionConfig,
    system_prompt: &str,
    prompt: &str,
    media_type: &str,
    base64: String,
) -> VisionHttpRequest {
    match &config.provider_kind {
        VisionProvider::OpenAiCompatible {
            base_url,
            api_key,
            use_responses_endpoint,
        } => {
            let mut headers = json_headers();
            if let Some(api_key) = api_key {
                headers.insert("authorization".to_string(), format!("Bearer {api_key}"));
            }
            if *use_responses_endpoint {
                return VisionHttpRequest {
                    url: format!("{base_url}/responses"),
                    headers,
                    body: responses_view_image_body(
                        &config.model,
                        system_prompt,
                        prompt,
                        media_type,
                        base64,
                        config.text_verbosity,
                    ),
                };
            }
            VisionHttpRequest {
                url: format!("{base_url}/chat/completions"),
                headers,
                body: chat_completions_view_image_body(
                    &config.model,
                    system_prompt,
                    prompt,
                    media_type,
                    base64,
                ),
            }
        }
        VisionProvider::OpenAiCodex {
            access_token,
            account_id,
        } => {
            let model = normalize_codex_model(&config.model);
            VisionHttpRequest {
                url: CODEX_RESPONSES_URL.to_string(),
                headers: codex_sse_headers_with_routing_hint(access_token, account_id, model, None),
                body: codex_view_image_body(
                    model,
                    system_prompt,
                    prompt,
                    media_type,
                    base64,
                    config.text_verbosity,
                ),
            }
        }
        VisionProvider::OpenAiResponses {
            responses_url,
            api_key,
        } => {
            let mut headers = json_headers();
            headers.insert("authorization".to_string(), format!("Bearer {api_key}"));
            VisionHttpRequest {
                url: responses_url.clone(),
                headers,
                body: responses_view_image_body(
                    &config.model,
                    system_prompt,
                    prompt,
                    media_type,
                    base64,
                    config.text_verbosity,
                ),
            }
        }
        VisionProvider::Anthropic { api_key } => {
            let mut headers = json_headers();
            headers.insert("x-api-key".to_string(), api_key.clone());
            headers.insert(
                "anthropic-version".to_string(),
                ANTHROPIC_VERSION.to_string(),
            );
            VisionHttpRequest {
                url: ANTHROPIC_MESSAGES_URL.to_string(),
                headers,
                body: anthropic_view_image_body(
                    &config.model,
                    Some(system_prompt),
                    prompt,
                    media_type,
                    base64,
                ),
            }
        }
    }
}

pub(super) fn encode_image_base64(bytes: Vec<u8>) -> String {
    let mut base64 = String::with_capacity(bytes.len().div_ceil(3) * 4);
    STANDARD.encode_string(bytes, &mut base64);
    base64
}

fn chat_completions_view_image_body(
    model: &str,
    system_prompt: &str,
    prompt: &str,
    media_type: &str,
    base64: String,
) -> String {
    let model = json_string(model);
    let system_prompt = json_string(system_prompt);
    let prompt = json_string(prompt);
    let mut body = String::with_capacity(
        180 + model.len() + system_prompt.len() + prompt.len() + media_type.len() + base64.len(),
    );
    body.push_str("{\"model\":");
    body.push_str(&model);
    body.push_str(",\"stream\":false,\"messages\":[{\"role\":\"system\",\"content\":");
    body.push_str(&system_prompt);
    body.push_str("},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":");
    body.push_str(&prompt);
    body.push_str("},{\"type\":\"image_url\",\"image_url\":{\"url\":");
    push_data_url_json_string(&mut body, media_type, &base64);
    body.push_str("}}]}]}");
    body
}

fn codex_view_image_body(
    model: &str,
    system_prompt: &str,
    prompt: &str,
    media_type: &str,
    base64: String,
    text_verbosity: Option<crate::config::TextVerbosity>,
) -> String {
    let model = json_string(model);
    let system_prompt = json_string(system_prompt);
    let prompt = json_string(prompt);
    let mut body = String::with_capacity(
        240 + model.len() + system_prompt.len() + prompt.len() + media_type.len() + base64.len(),
    );
    body.push_str("{\"model\":");
    body.push_str(&model);
    body.push_str(",\"store\":false,\"stream\":true,\"instructions\":");
    body.push_str(&system_prompt);
    body.push_str(",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":");
    body.push_str(&prompt);
    body.push_str("},{\"type\":\"input_image\",\"image_url\":");
    push_data_url_json_string(&mut body, media_type, &base64);
    body.push_str("}]}]");
    if let Some(text_verbosity) = text_verbosity {
        body.push_str(",\"text\":{\"verbosity\":");
        body.push_str(&json_string(text_verbosity.as_api_str()));
        body.push('}');
    }
    body.push_str(",\"include\":[\"reasoning.encrypted_content\"]}");
    body
}

pub(super) fn responses_view_image_body(
    model: &str,
    system_prompt: &str,
    prompt: &str,
    media_type: &str,
    base64: String,
    text_verbosity: Option<crate::config::TextVerbosity>,
) -> String {
    let model = json_string(model);
    let system_prompt = json_string(system_prompt);
    let prompt = json_string(prompt);
    let mut body = String::with_capacity(
        180 + model.len() + system_prompt.len() + prompt.len() + media_type.len() + base64.len(),
    );
    body.push_str("{\"model\":");
    body.push_str(&model);
    body.push_str(",\"stream\":false,\"instructions\":");
    body.push_str(&system_prompt);
    body.push_str(",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":");
    body.push_str(&prompt);
    body.push_str("},{\"type\":\"input_image\",\"image_url\":");
    push_data_url_json_string(&mut body, media_type, &base64);
    body.push_str("}]}]");
    if let Some(text_verbosity) = text_verbosity {
        body.push_str(",\"text\":{\"verbosity\":");
        body.push_str(&json_string(text_verbosity.as_api_str()));
        body.push('}');
    }
    body.push('}');
    body
}

fn anthropic_view_image_body(
    model: &str,
    system_prompt: Option<&str>,
    prompt: &str,
    media_type: &str,
    base64: String,
) -> String {
    let model = json_string(model);
    let prompt = json_string(prompt);
    let system_prompt = system_prompt.map(json_string);
    let mut body = String::with_capacity(
        220 + model.len()
            + system_prompt.as_ref().map_or(0, String::len)
            + prompt.len()
            + media_type.len()
            + base64.len(),
    );
    body.push_str("{\"model\":");
    body.push_str(&model);
    body.push_str(",\"max_tokens\":4096,\"stream\":false");
    if let Some(system_prompt) = system_prompt {
        body.push_str(",\"system\":");
        body.push_str(&system_prompt);
    }
    body.push_str(",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"image\",\"source\":{\"type\":\"base64\",\"media_type\":");
    push_json_string_unescaped(&mut body, media_type);
    body.push_str(",\"data\":");
    push_json_string_unescaped(&mut body, &base64);
    body.push_str("}},{\"type\":\"text\",\"text\":");
    body.push_str(&prompt);
    body.push_str("}]}]}");
    body
}

fn json_string(value: &str) -> String {
    serde_json::to_string(value).expect("serializing string to JSON cannot fail")
}

fn push_data_url_json_string(body: &mut String, media_type: &str, base64: &str) {
    body.push('"');
    body.push_str("data:");
    body.push_str(media_type);
    body.push_str(";base64,");
    body.push_str(base64);
    body.push('"');
}

fn push_json_string_unescaped(body: &mut String, value: &str) {
    body.push('"');
    body.push_str(value);
    body.push('"');
}

pub(super) fn json_headers() -> BTreeMap<String, String> {
    BTreeMap::from([
        ("accept".to_string(), "application/json".to_string()),
        ("content-type".to_string(), "application/json".to_string()),
        ("user-agent".to_string(), USER_AGENT.to_string()),
    ])
}