1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
diff --git a/crates/librefang-runtime-media/src/media_understanding.rs b/crates/librefang-runtime-media/src/media_understanding.rs
index 887275d9a..321ef66ef 100644
--- a/crates/librefang-runtime-media/src/media_understanding.rs
+++ b/crates/librefang-runtime-media/src/media_understanding.rs
@@ -113,7 +113,8 @@ impl MediaEngine {
let description = match provider {
"anthropic" => anthropic_describe_image(model, &image_bytes, mime_type).await?,
"openai" | "groq" => {
- let (api_url, api_key) = openai_vision_provider_config(provider)?;
+ let custom_url = self.config.image_base_url.as_deref();
+ let (api_url, api_key) = openai_vision_provider_config(provider, custom_url)?;
openai_describe_image(&api_url, &api_key, model, &image_bytes, mime_type).await?
}
"gemini" => gemini_describe_image(model, &image_bytes, mime_type).await?,
@@ -394,14 +395,21 @@ fn detect_vision_provider() -> Option<&'static str> {
// ── Vision provider helpers ───────────────────────────────────────────────
/// Resolve OpenAI-compatible vision API URL and key for a provider.
-fn openai_vision_provider_config(provider: &str) -> Result<(String, String), String> {
+fn openai_vision_provider_config(
+ provider: &str,
+ custom_base_url: Option<&str>,
+) -> Result<(String, String), String> {
match provider {
"openai" => Ok((
- "https://api.openai.com/v1/chat/completions".into(),
+ custom_base_url
+ .map(|u| format!("{}/chat/completions", u.trim_end_matches('/')))
+ .unwrap_or_else(|| "https://api.openai.com/v1/chat/completions".into()),
std::env::var("OPENAI_API_KEY").map_err(|_| "OPENAI_API_KEY not set")?,
)),
"groq" => Ok((
- "https://api.groq.com/openai/v1/chat/completions".into(),
+ custom_base_url
+ .map(|u| format!("{}/chat/completions", u.trim_end_matches('/')))
+ .unwrap_or_else(|| "https://api.groq.com/openai/v1/chat/completions".into()),
std::env::var("GROQ_API_KEY").map_err(|_| "GROQ_API_KEY not set")?,
)),
other => Err(format!(
@@ -1385,19 +1393,35 @@ mod tests {
#[test]
fn openai_vision_provider_config_resolves_known_providers() {
// With no env set these return Err("not set"), but the URL/structure is stable
- let groq = openai_vision_provider_config("groq");
+ let groq = openai_vision_provider_config("groq", None);
// Either Ok with the right URL, or Err because the key is absent
match groq {
Ok((url, _)) => assert!(url.contains("groq.com")),
Err(e) => assert!(e.contains("GROQ_API_KEY")),
}
- let openai = openai_vision_provider_config("openai");
+ let openai = openai_vision_provider_config("openai", None);
match openai {
Ok((url, _)) => assert!(url.contains("openai.com")),
Err(e) => assert!(e.contains("OPENAI_API_KEY")),
}
// Unknown provider must error
- assert!(openai_vision_provider_config("unknown_provider").is_err());
+ assert!(openai_vision_provider_config("unknown_provider", None).is_err());
+ }
+
+ #[test]
+ fn openai_vision_provider_config_resolves_with_custom_url() {
+ let custom_url = Some("http://localhost:50100/v1");
+ std::env::set_var("OPENAI_API_KEY", "test-key");
+ std::env::set_var("GROQ_API_KEY", "test-key");
+
+ let openai = openai_vision_provider_config("openai", custom_url);
+ assert_eq!(openai.unwrap().0, "http://localhost:50100/v1/chat/completions");
+
+ let groq = openai_vision_provider_config("groq", custom_url);
+ assert_eq!(groq.unwrap().0, "http://localhost:50100/v1/chat/completions");
+
+ std::env::remove_var("OPENAI_API_KEY");
+ std::env::remove_var("GROQ_API_KEY");
}
#[test]
diff --git a/crates/librefang-types/src/media.rs b/crates/librefang-types/src/media.rs
index 4223a6058..c58d2983d 100644
--- a/crates/librefang-types/src/media.rs
+++ b/crates/librefang-types/src/media.rs
@@ -117,6 +117,8 @@ pub struct MediaConfig {
pub image_provider: Option<String>,
/// Preferred image description model (provider default if None).
pub image_model: Option<String>,
+ /// Custom base URL override for image description.
+ pub image_base_url: Option<String>,
/// Preferred audio transcription provider (auto-detect if None).
///
/// Set to any string (e.g. `"local-whisper"`) to use the custom STT
@@ -146,6 +148,7 @@ impl Default for MediaConfig {
max_concurrency: 2,
image_provider: None,
image_model: None,
+ image_base_url: None,
audio_provider: None,
audio_model: None,
audio_base_url: None,
@@ -792,6 +795,7 @@ mod tests {
assert_eq!(config.max_concurrency, 2);
assert!(config.image_provider.is_none());
assert!(config.image_model.is_none());
+ assert!(config.image_base_url.is_none());
}
#[test]
|