rig_core/providers/
llamafile.rs1use crate::client::{
32 self, Capabilities, Capable, DebugExt, Nothing, Provider, ProviderBuilder, ProviderClient,
33 Transport,
34};
35use crate::http_client::{self, HttpClientExt};
36use crate::providers::openai;
37
38const LLAMAFILE_API_BASE_URL: &str = "http://localhost:8080";
42
43pub const LLAMA_CPP: &str = "LLaMA_CPP";
45
46#[derive(Debug, Default, Clone, Copy)]
47pub struct LlamafileExt;
48
49#[derive(Debug, Default, Clone, Copy)]
50pub struct LlamafileBuilder;
51
52impl Provider for LlamafileExt {
53 type Builder = LlamafileBuilder;
54 const VERIFY_PATH: &'static str = "/models";
55
56 fn build_uri(&self, base_url: &str, path: &str, _transport: Transport) -> String {
60 let base_url = base_url.trim_end_matches('/');
61 format!("{base_url}/v1/{}", path.trim_start_matches('/'))
62 }
63}
64
65impl openai::completion::OpenAICompatibleProvider for LlamafileExt {
66 const PROVIDER_NAME: &'static str = "llamafile";
67
68 type StreamingUsage = openai::Usage;
69
70 const EMITS_COMPLETE_SINGLE_CHUNK_TOOL_CALLS: bool = true;
72
73 type Response = openai::CompletionResponse;
74}
75
76impl openai::embedding::OpenAIEmbeddingsCompatible for LlamafileExt {
77 const PROVIDER_NAME: &'static str = "llamafile";
78}
79
80impl<H> Capabilities<H> for LlamafileExt {
81 type Completion = Capable<openai::completion::GenericCompletionModel<LlamafileExt, H>>;
82 type Embeddings = Capable<openai::embedding::GenericEmbeddingModel<LlamafileExt, H>>;
83 type Transcription = Nothing;
84 type ModelListing = Nothing;
85 #[cfg(feature = "image")]
86 type ImageGeneration = Nothing;
87 #[cfg(feature = "audio")]
88 type AudioGeneration = Nothing;
89 type Rerank = Nothing;
90}
91
92impl DebugExt for LlamafileExt {}
93
94impl ProviderBuilder for LlamafileBuilder {
95 type Extension<H>
96 = LlamafileExt
97 where
98 H: HttpClientExt;
99 type ApiKey = Nothing;
100
101 const BASE_URL: &'static str = LLAMAFILE_API_BASE_URL;
102
103 fn build<H>(
104 _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
105 ) -> http_client::Result<Self::Extension<H>>
106 where
107 H: HttpClientExt,
108 {
109 Ok(LlamafileExt)
110 }
111}
112
113pub type Client<H = reqwest::Client> = client::Client<LlamafileExt, H>;
114pub type ClientBuilder<H = crate::markers::Missing> =
115 client::ClientBuilder<LlamafileBuilder, Nothing, H>;
116
117pub type CompletionModel<H = reqwest::Client> =
119 openai::completion::GenericCompletionModel<LlamafileExt, H>;
120
121pub type EmbeddingModel<H = reqwest::Client> =
123 openai::embedding::GenericEmbeddingModel<LlamafileExt, H>;
124
125impl Client {
126 pub fn from_url(base_url: &str) -> crate::client::ProviderClientResult<Self> {
129 Self::builder()
130 .api_key(Nothing)
131 .base_url(base_url)
132 .build()
133 .map_err(Into::into)
134 }
135}
136
137impl ProviderClient for Client {
138 type Input = Nothing;
139 type Error = crate::client::ProviderClientError;
140
141 fn from_env() -> Result<Self, Self::Error> {
142 let api_base = crate::client::required_env_var("LLAMAFILE_API_BASE_URL")?;
143 Self::from_url(&api_base)
144 }
145
146 fn from_val(_: Self::Input) -> Result<Self, Self::Error> {
147 Self::builder().api_key(Nothing).build().map_err(Into::into)
148 }
149}
150
151#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::client::{EmbeddingsClient, Nothing};
158 use crate::embeddings::EmbeddingModel as _;
159 use crate::providers::openai::embedding::EncodingFormat;
160 use crate::test_utils::RecordingHttpClient;
161
162 #[test]
163 fn test_client_initialization() {
164 let _client =
165 crate::providers::llamafile::Client::new(Nothing).expect("Client::new() failed");
166 let _client_from_builder = crate::providers::llamafile::Client::builder()
167 .api_key(Nothing)
168 .build()
169 .expect("Client::builder() failed");
170 }
171
172 #[test]
173 fn test_client_from_url() {
174 let _client = crate::providers::llamafile::Client::from_url("http://localhost:8080");
175 }
176
177 #[test]
178 fn test_build_uri_routes_through_v1() {
179 let ext = LlamafileExt;
180 assert_eq!(
181 ext.build_uri(
182 "http://localhost:8080",
183 "/chat/completions",
184 Transport::Http
185 ),
186 "http://localhost:8080/v1/chat/completions"
187 );
188 assert_eq!(
189 ext.build_uri("http://localhost:8080/", "/embeddings", Transport::Http),
190 "http://localhost:8080/v1/embeddings"
191 );
192 assert_eq!(
193 ext.build_uri(
194 "http://localhost:8080",
195 LlamafileExt::VERIFY_PATH,
196 Transport::Http
197 ),
198 "http://localhost:8080/v1/models"
199 );
200 }
201
202 #[tokio::test]
203 async fn embedding_model_preserves_v1_path_and_usage() {
204 let response = r#"{
205 "object": "list",
206 "model": "LLaMA_CPP",
207 "usage": { "prompt_tokens": 2, "total_tokens": 2 },
208 "data": [{ "object": "embedding", "index": 0, "embedding": [0.1, 0.2] }]
209 }"#;
210 let http_client = RecordingHttpClient::new(response);
211 let client = Client::builder()
212 .api_key(Nothing)
213 .http_client(http_client.clone())
214 .build()
215 .expect("client should build");
216 let model = client.embedding_model(LLAMA_CPP);
217
218 let response = model
219 .embed_texts_with_usage(["hello".to_string()])
220 .await
221 .expect("embedding request should succeed");
222
223 assert_eq!(response.usage.total_tokens, 2);
224 assert_eq!(
225 http_client.requests()[0].uri,
226 "http://localhost:8080/v1/embeddings"
227 );
228 }
229
230 #[tokio::test]
231 async fn embedding_model_rejects_base64_before_sending() {
232 let http_client = RecordingHttpClient::new("{}");
233 let client = Client::builder()
234 .api_key(Nothing)
235 .http_client(http_client.clone())
236 .build()
237 .expect("client should build");
238 let model = client
239 .embedding_model(LLAMA_CPP)
240 .encoding_format(EncodingFormat::Base64);
241
242 let error = model
243 .embed_texts(["hello".to_string()])
244 .await
245 .expect_err("numeric response parser should reject base64");
246
247 assert!(matches!(
248 error,
249 crate::embeddings::EmbeddingError::UnsupportedResponseEncoding {
250 provider: "llamafile",
251 encoding_format: "base64"
252 }
253 ));
254 assert!(http_client.requests().is_empty());
255 }
256}