Skip to content

Commit b81aa52

Browse files
zmanianclaude
andcommitted
fix(llm): add image detail field, auto-append /v1 to base URL (#2378, #1934)
Set detail: "auto" on ImageUrl construction so providers requiring the field (e.g. MiniMax) no longer reject vision requests. Normalize OpenAI-compatible base URLs by appending /v1 when missing, fixing 404s for local model servers (MLX, vLLM, llama.cpp) using bare URLs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 88b87c0 commit b81aa52

File tree

2 files changed

+79
-2
lines changed

2 files changed

+79
-2
lines changed

src/agent/attachments.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub fn augment_with_attachments(
4343
image_parts.push(ContentPart::ImageUrl {
4444
image_url: ImageUrl {
4545
url: data_url,
46-
detail: None,
46+
detail: Some("auto".to_string()),
4747
},
4848
});
4949
}
@@ -242,6 +242,26 @@ mod tests {
242242
}
243243
}
244244

245+
#[test]
246+
fn image_url_includes_detail_auto() {
247+
let mut att = make_attachment(AttachmentKind::Image);
248+
att.mime_type = "image/png".to_string();
249+
att.data = vec![0x89, 0x50, 0x4E, 0x47]; // fake PNG header
250+
251+
let result = augment_with_attachments("check", &[att]).unwrap();
252+
assert_eq!(result.image_parts.len(), 1);
253+
match &result.image_parts[0] {
254+
ContentPart::ImageUrl { image_url } => {
255+
assert_eq!(
256+
image_url.detail.as_deref(),
257+
Some("auto"),
258+
"detail field must be set to 'auto' for provider compatibility"
259+
);
260+
}
261+
other => panic!("Expected ImageUrl, got: {:?}", other),
262+
}
263+
}
264+
245265
#[test]
246266
fn document_with_extracted_text() {
247267
let mut att = make_attachment(AttachmentKind::Document);

src/llm/mod.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ fn create_openai_compat_from_registry(
285285

286286
let mut builder = openai::Client::builder().api_key(&api_key);
287287
if !config.base_url.is_empty() {
288-
builder = builder.base_url(&config.base_url);
288+
let base_url = normalize_openai_base_url(&config.base_url);
289+
builder = builder.base_url(&base_url);
289290
}
290291
if !extra_headers.is_empty() {
291292
builder = builder.http_headers(extra_headers);
@@ -707,6 +708,21 @@ pub fn create_gemini_oauth_provider(config: &LlmConfig) -> Result<Arc<dyn LlmPro
707708
Ok(Arc::new(provider))
708709
}
709710

711+
/// Normalize an OpenAI-compatible base URL by appending `/v1` if missing.
712+
///
713+
/// rig-core's `openai::Client` does not auto-append `/v1/` to the base URL,
714+
/// so local model servers (MLX, vLLM, llama.cpp) using bare URLs like
715+
/// `http://localhost:8080` get 404s. This mirrors the old `NearAiChatProvider::api_url()`
716+
/// behavior.
717+
fn normalize_openai_base_url(url: &str) -> String {
718+
let trimmed = url.trim_end_matches('/');
719+
if trimmed.ends_with("/v1") {
720+
trimmed.to_string()
721+
} else {
722+
format!("{trimmed}/v1")
723+
}
724+
}
725+
710726
#[cfg(test)]
711727
mod tests {
712728
use super::*;
@@ -867,4 +883,45 @@ mod tests {
867883
let config = test_llm_config();
868884
assert_eq!(config.cheap_model_name(), None);
869885
}
886+
887+
#[test]
888+
fn test_normalize_openai_base_url_appends_v1_when_missing() {
889+
assert_eq!(
890+
normalize_openai_base_url("http://localhost:8080"),
891+
"http://localhost:8080/v1"
892+
);
893+
assert_eq!(
894+
normalize_openai_base_url("http://localhost:8080/"),
895+
"http://localhost:8080/v1"
896+
);
897+
assert_eq!(
898+
normalize_openai_base_url("https://my-server.example.com"),
899+
"https://my-server.example.com/v1"
900+
);
901+
}
902+
903+
#[test]
904+
fn test_normalize_openai_base_url_leaves_v1_alone() {
905+
assert_eq!(
906+
normalize_openai_base_url("http://localhost:8080/v1"),
907+
"http://localhost:8080/v1"
908+
);
909+
assert_eq!(
910+
normalize_openai_base_url("http://localhost:8080/v1/"),
911+
"http://localhost:8080/v1"
912+
);
913+
assert_eq!(
914+
normalize_openai_base_url("https://api.openai.com/v1"),
915+
"https://api.openai.com/v1"
916+
);
917+
}
918+
919+
#[test]
920+
fn test_normalize_openai_base_url_preserves_subpaths() {
921+
// A URL with a different path should get /v1 appended
922+
assert_eq!(
923+
normalize_openai_base_url("https://api.example.com/custom"),
924+
"https://api.example.com/custom/v1"
925+
);
926+
}
870927
}

0 commit comments

Comments
 (0)