summarylogtreecommitdiffstats
path: root/feature-local-image.patch
blob: ec0bc058745037a22644ebdd676b315b8b1fd066 (plain)
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 fbb83406..e5579202 100644
--- a/crates/librefang-runtime-media/src/media_understanding.rs
+++ b/crates/librefang-runtime-media/src/media_understanding.rs
@@ -151,7 +151,8 @@ impl MediaEngine {
             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,
@@ -460,14 +461,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!(
@@ -1500,19 +1508,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 7f86b88f..ee149af5 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,
@@ -930,6 +933,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]