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