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()),
])
}