Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:33:34.104Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

llama_cpp.rs

2121 lines · 84.4 KB · rust
1use anyhow::Result;
2use collections::{HashMap, HashSet};
3use credentials_provider::CredentialsProvider;
4use fs::Fs;
5use futures::Stream;
6use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
7use gpui::{App, AsyncApp, Context, Entity, Task, TaskExt};
8use http_client::{CustomHeaders, HttpClient};
9use language_model::util::parse_tool_arguments;
10use language_model::{
11    ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, InlineDescription, LanguageModel,
12    LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
13    LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
14    LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
15    LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, ProviderSettingsView,
16    RateLimiter, Role, StopReason, SubPageProviderSettings, TokenUsage, env_var,
17};
18use llama_cpp::{
19    LLAMA_CPP_API_URL, ModelEntry, Props, get_models, get_props, stream_chat_completion,
20    stream_model_events,
21};
22pub use settings::LlamaCppAvailableModel as AvailableModel;
23use settings::{Settings, SettingsStore, update_settings_file};
24use std::pin::Pin;
25use std::sync::LazyLock;
26use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
27use std::time::Duration;
28use ui::{
29    ButtonLike, ButtonLink, ConfiguredApiCard, Divider, List, ListBulletItem, Tooltip, prelude::*,
30};
31use ui_input::InputField;
32use util::ResultExt;
33
34use crate::AllLanguageModelSettings;
35
36const LLAMA_CPP_DOWNLOAD_URL: &str = "https://llama.app";
37const LLAMA_CPP_MODELS_URL: &str = "https://huggingface.co/models?library=gguf&sort=trending";
38
39const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("llama.cpp");
40const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("llama.cpp");
41
42const API_KEY_ENV_VAR_NAME: &str = "LLAMACPP_API_KEY";
43static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
44
45/// How long to wait before reconnecting to `/models/sse` after the stream ends.
46const MODEL_EVENT_RECONNECT_INTERVAL: Duration = Duration::from_secs(5);
47
48/// Context length assumed for an unloaded router model (it can't be probed
49/// without loading it). Generous so early messages work; re-discovery refines
50/// it once the model loads.
51const ASSUMED_UNLOADED_CONTEXT: u64 = 131_072;
52
53#[derive(Default, Debug, Clone, PartialEq)]
54pub struct LlamaCppSettings {
55    pub api_url: String,
56    pub auto_discover: bool,
57    pub available_models: Vec<AvailableModel>,
58    pub context_window: Option<u64>,
59    pub custom_headers: CustomHeaders,
60}
61
62pub struct LlamaCppLanguageModelProvider {
63    http_client: Arc<dyn HttpClient>,
64    state: Entity<State>,
65    /// Live capabilities shared with the agent's models (see [`LiveCapabilities`]).
66    capability_cells: CapabilityCells,
67    /// Live model-load progress shared with the models (see [`LoadingProgress`]).
68    loading_progress: LoadingProgress,
69}
70
71pub struct State {
72    api_key_state: ApiKeyState,
73    credentials_provider: Arc<dyn CredentialsProvider>,
74    http_client: Arc<dyn HttpClient>,
75    fetched_models: Vec<llama_cpp::Model>,
76    fetch_model_task: Option<Task<Result<()>>>,
77    /// Router-mode task on `/models/sse`; re-runs discovery as models load/unload.
78    model_event_task: Option<Task<()>>,
79    /// Same `Arc` as the provider's; re-discovery keeps these cells in sync.
80    capability_cells: CapabilityCells,
81    /// Same `Arc` as the provider's; the event stream updates it as a model loads.
82    loading_progress: LoadingProgress,
83}
84
85impl State {
86    fn is_authenticated(&self) -> bool {
87        !self.fetched_models.is_empty()
88    }
89
90    fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
91        let credentials_provider = self.credentials_provider.clone();
92        let api_url = LlamaCppLanguageModelProvider::api_url(cx);
93        let task = self.api_key_state.store(
94            api_url,
95            api_key,
96            |this| &mut this.api_key_state,
97            credentials_provider,
98            cx,
99        );
100
101        self.fetched_models.clear();
102        // Drop the event stream so it reconnects with the new key (re-fetch
103        // below restarts it).
104        self.model_event_task = None;
105        write_recover(&self.loading_progress).clear();
106        cx.spawn(async move |this, cx| {
107            let result = task.await;
108            this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
109                .ok();
110            result
111        })
112    }
113
114    fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
115        let credentials_provider = self.credentials_provider.clone();
116        let api_url = LlamaCppLanguageModelProvider::api_url(cx);
117        let load_key_task = self.api_key_state.load_if_needed(
118            api_url,
119            |this| &mut this.api_key_state,
120            credentials_provider,
121            cx,
122        );
123
124        if self.is_authenticated() {
125            return Task::ready(Ok(()));
126        }
127
128        cx.spawn(async move |this, cx| {
129            match load_key_task.await {
130                Ok(()) | Err(AuthenticateError::CredentialsNotFound) => {}
131                Err(error) => {
132                    log::warn!("failed to load llama.cpp API key: {error}");
133                }
134            }
135            let fetch_models_task = this.update(cx, |this, cx| this.fetch_models(cx))?;
136            match fetch_models_task.await {
137                Ok(()) => Ok(()),
138                Err(err) => {
139                    // A refused connection means the server isn't running yet, not an error.
140                    let connection_refused = err.chain().any(|cause| {
141                        cause
142                            .downcast_ref::<std::io::Error>()
143                            .is_some_and(|io_err| {
144                                io_err.kind() == std::io::ErrorKind::ConnectionRefused
145                            })
146                    });
147                    if connection_refused {
148                        Err(AuthenticateError::ConnectionRefused)
149                    } else {
150                        Err(AuthenticateError::Other(err))
151                    }
152                }
153            }
154        })
155    }
156
157    fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
158        let http_client = Arc::clone(&self.http_client);
159        let settings = LlamaCppLanguageModelProvider::settings(cx);
160        let api_url = LlamaCppLanguageModelProvider::api_url(cx);
161        let api_key = self.api_key_state.key(&api_url);
162        let extra_headers = settings.custom_headers.clone();
163
164        cx.spawn(async move |this, cx| {
165            let entries = get_models(
166                http_client.as_ref(),
167                &api_url,
168                api_key.as_deref(),
169                &extra_headers,
170            )
171            .await?;
172
173            let is_router = entries.iter().any(ModelEntry::is_router_entry);
174
175            // Models the server reports as loading, used below to prune stale
176            // progress labels by reconciling against the live listing (a preempted
177            // load or missed SSE event can skip the terminal event).
178            let loading_ids: HashSet<String> = entries
179                .iter()
180                .filter(|entry| entry.is_loading())
181                .map(|entry| entry.id.clone())
182                .collect();
183
184            let models: Vec<llama_cpp::Model> = if is_router {
185                // Router mode: metadata comes from `/v1/models`. We probe
186                // `/props` only for loaded models so listing never triggers a
187                // load; unloaded models use the listing's hints and overrides.
188                let tasks = entries.into_iter().map(|entry| {
189                    let http_client = Arc::clone(&http_client);
190                    let api_url = api_url.clone();
191                    let api_key = api_key.clone();
192                    let extra_headers = extra_headers.clone();
193                    async move {
194                        let props = if entry.is_loaded() {
195                            get_props(
196                                http_client.as_ref(),
197                                &api_url,
198                                api_key.as_deref(),
199                                Some(&entry.id),
200                                &extra_headers,
201                            )
202                            .await
203                            .log_err()
204                        } else {
205                            None
206                        };
207                        model_from_entry(&entry, props.as_ref())
208                    }
209                });
210                futures::stream::iter(tasks)
211                    .buffer_unordered(5)
212                    .collect()
213                    .await
214            } else {
215                // Single-model mode: one `/props` call describes the loaded model.
216                let props = get_props(
217                    http_client.as_ref(),
218                    &api_url,
219                    api_key.as_deref(),
220                    None,
221                    &extra_headers,
222                )
223                .await
224                .log_err();
225                entries
226                    .iter()
227                    .map(|entry| model_from_entry(entry, props.as_ref()))
228                    .collect()
229            };
230
231            this.update(cx, |this, cx| {
232                this.fetched_models = models;
233                let effective = compute_effective_models(
234                    &this.fetched_models,
235                    LlamaCppLanguageModelProvider::settings(cx),
236                );
237                sync_capability_cells(&this.capability_cells, &effective);
238                // Drop progress labels for models no longer loading, so a stale
239                // "Loading …" can't stick after a preempted load or missed event.
240                write_recover(&this.loading_progress).retain(|id, _| loading_ids.contains(id));
241                // Router mode loads models on demand: subscribe so capabilities
242                // self-correct as they load/unload. Start it once (events trigger
243                // re-discovery, not a re-spawn); single-model mode needs no stream.
244                if is_router {
245                    if this.model_event_task.is_none() {
246                        this.start_model_event_stream(cx);
247                    }
248                } else {
249                    this.model_event_task = None;
250                }
251                cx.notify();
252            })
253        })
254    }
255
256    /// Subscribes to `/models/sse` and re-runs discovery as models load, unload,
257    /// or the list changes, so capabilities stay current. Reconnects if the stream
258    /// drops; on builds without `/models/sse` the refresh is simply skipped.
259    fn start_model_event_stream(&mut self, cx: &mut Context<Self>) {
260        let http_client = Arc::clone(&self.http_client);
261        let api_url = LlamaCppLanguageModelProvider::api_url(cx);
262        let api_key = self.api_key_state.key(&api_url);
263        let extra_headers = LlamaCppLanguageModelProvider::settings(cx)
264            .custom_headers
265            .clone();
266
267        self.model_event_task = Some(cx.spawn(async move |this, cx| {
268            loop {
269                match stream_model_events(
270                    http_client.as_ref(),
271                    &api_url,
272                    api_key.as_deref(),
273                    &extra_headers,
274                )
275                .await
276                {
277                    Ok(mut events) => {
278                        while let Some(event) = events.next().await {
279                            let Some(event) = event.log_err() else {
280                                continue;
281                            };
282                            if let Some(exit_code) = event.load_failure() {
283                                log::error!(
284                                    "llama.cpp model {} failed to load (exit code {exit_code})",
285                                    event.model
286                                );
287                            }
288                            // Loading-progress tick: record it for the selector (no
289                            // re-discovery). `cx.notify()` drives `ProviderStateChanged`.
290                            if let Some(progress) = event.load_progress() {
291                                let label = SharedString::from(progress.progress_label());
292                                if this
293                                    .update(cx, |this, cx| {
294                                        write_recover(&this.loading_progress)
295                                            .insert(event.model.clone(), label);
296                                        cx.notify();
297                                    })
298                                    .is_err()
299                                {
300                                    return;
301                                }
302                                continue;
303                            }
304                            if !event.changes_model_state() {
305                                continue;
306                            }
307                            // Terminal load/unload (or list change): drop the
308                            // progress label and re-discover to refresh capabilities.
309                            if this
310                                .update(cx, |this, cx| {
311                                    write_recover(&this.loading_progress).remove(&event.model);
312                                    this.restart_fetch_models_task(cx);
313                                })
314                                .is_err()
315                            {
316                                return;
317                            }
318                        }
319                    }
320                    // Endpoint missing (older build) or connection failed; retry after a backoff.
321                    Err(error) => {
322                        log::warn!("llama.cpp model event stream unavailable: {error:#}");
323                    }
324                }
325
326                cx.background_executor()
327                    .timer(MODEL_EVENT_RECONNECT_INTERVAL)
328                    .await;
329                if this.update(cx, |_, _| ()).is_err() {
330                    return;
331                }
332            }
333        }));
334    }
335
336    fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) {
337        let task = self.fetch_models(cx);
338        self.fetch_model_task.replace(task);
339    }
340}
341
342/// Capabilities that only become known once a router model loads. `LanguageModel`'s
343/// methods take no `cx`, yet the agent reads them live each turn, so we share
344/// them through a map that re-discovery updates — an already-selected model
345/// picks up real values without re-selection. Image support is available from
346/// `/v1/models` hints, so it stays a field.
347#[derive(Clone, Copy, Debug, PartialEq)]
348struct LiveCapabilities {
349    max_tokens: u64,
350    supports_tools: bool,
351    supports_thinking: bool,
352}
353
354impl LiveCapabilities {
355    fn of(model: &llama_cpp::Model) -> Self {
356        Self {
357            max_tokens: model.max_tokens,
358            supports_tools: model.supports_tools,
359            supports_thinking: model.supports_thinking,
360        }
361    }
362}
363
364/// Live capabilities keyed by model name, shared by the provider and its models.
365type CapabilityCells = Arc<RwLock<HashMap<String, LiveCapabilities>>>;
366
367/// Model name → load-status label (e.g. `"Loading weights 42%"`) while a router
368/// model loads, shared so the model selector can show progress. Absent once loaded.
369type LoadingProgress = Arc<RwLock<HashMap<String, SharedString>>>;
370
371/// Locks for reading, recovering instead of panicking on a poisoned lock. The
372/// critical sections are infallible map ops, so poisoning is unreachable anyway.
373fn read_recover<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
374    lock.read().unwrap_or_else(|poisoned| poisoned.into_inner())
375}
376
377/// Locks for writing; see [`read_recover`].
378fn write_recover<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
379    lock.write()
380        .unwrap_or_else(|poisoned| poisoned.into_inner())
381}
382
383/// The exact set of models `provided_models` exposes: discovery merged with the
384/// `available_models` and `context_window` overrides. Shared with re-discovery.
385fn compute_effective_models(
386    fetched_models: &[llama_cpp::Model],
387    settings: &LlamaCppSettings,
388) -> HashMap<String, llama_cpp::Model> {
389    let mut models: HashMap<String, llama_cpp::Model> = HashMap::default();
390    if settings.auto_discover {
391        for model in fetched_models {
392            let mut model = model.clone();
393            if let Some(context_window) = settings.context_window {
394                model.max_tokens = context_window;
395            }
396            models.insert(model.name.clone(), model);
397        }
398    }
399    merge_settings_into_models(
400        &mut models,
401        &settings.available_models,
402        settings.context_window,
403    );
404    models
405}
406
407/// Updates the shared capability map from the effective models, so a model held
408/// by an open conversation observes the new values (it reads the map by name).
409fn sync_capability_cells(cells: &CapabilityCells, effective: &HashMap<String, llama_cpp::Model>) {
410    let mut cells = write_recover(cells);
411    for model in effective.values() {
412        cells.insert(model.name.clone(), LiveCapabilities::of(model));
413    }
414}
415
416/// Builds a model from a `/v1/models` entry, refined by `/props` when the model
417/// is loaded. An unloaded router model can't be probed, so we assume optimistic
418/// capabilities and let re-discovery refine them on load.
419fn model_from_entry(entry: &ModelEntry, props: Option<&Props>) -> llama_cpp::Model {
420    let max_tokens = props
421        .and_then(Props::context_length)
422        .or_else(|| entry.meta.as_ref().and_then(|meta| meta.n_ctx))
423        .or_else(|| entry.meta.as_ref().and_then(|meta| meta.n_ctx_train))
424        .unwrap_or(ASSUMED_UNLOADED_CONTEXT);
425    // Trust `/props` when present. Without it, assume tools for an unloaded model
426    // (re-discovery corrects on load) but not for a loaded model whose probe failed.
427    let supports_tools = match props {
428        Some(props) => props.supports_tools(),
429        None => !entry.is_loaded(),
430    };
431    let supports_images = props.is_some_and(Props::supports_images) || entry.supports_images_hint();
432    let supports_thinking = props.is_some_and(Props::supports_thinking);
433
434    llama_cpp::Model::new(
435        &entry.id,
436        Some(&display_name_for(&entry.id)),
437        Some(max_tokens),
438        supports_tools,
439        supports_images,
440        supports_thinking,
441    )
442}
443
444/// Friendly display name from a model id, which is often a `.gguf` file path.
445fn display_name_for(id: &str) -> String {
446    let base = id.rsplit(['/', '\\']).next().unwrap_or(id);
447    base.strip_suffix(".gguf").unwrap_or(base).to_string()
448}
449
450fn telemetry_id_for(id: &str) -> String {
451    format!("{PROVIDER_ID}/{}", display_name_for(id))
452}
453
454impl LlamaCppLanguageModelProvider {
455    pub fn new(
456        http_client: Arc<dyn HttpClient>,
457        credentials_provider: Arc<dyn CredentialsProvider>,
458        cx: &mut App,
459    ) -> Self {
460        let capability_cells: CapabilityCells = Arc::new(RwLock::new(HashMap::default()));
461        let loading_progress: LoadingProgress = Arc::new(RwLock::new(HashMap::default()));
462        let this = Self {
463            http_client: http_client.clone(),
464            capability_cells: capability_cells.clone(),
465            loading_progress: loading_progress.clone(),
466            state: cx.new(|cx| {
467                cx.observe_global::<SettingsStore>({
468                    let mut last_settings = LlamaCppLanguageModelProvider::settings(cx).clone();
469                    move |this: &mut State, cx| {
470                        let current_settings = LlamaCppLanguageModelProvider::settings(cx);
471                        let settings_changed = current_settings != &last_settings;
472                        if settings_changed {
473                            let url_changed = last_settings.api_url != current_settings.api_url;
474                            last_settings = current_settings.clone();
475                            if url_changed {
476                                let credentials_provider = this.credentials_provider.clone();
477                                let api_url = Self::api_url(cx);
478                                this.api_key_state.handle_url_change(
479                                    api_url,
480                                    |this| &mut this.api_key_state,
481                                    credentials_provider,
482                                    cx,
483                                );
484                                this.fetched_models.clear();
485                                // Drop the event stream so it reconnects against
486                                // the new URL (re-auth below restarts it).
487                                this.model_event_task = None;
488                                write_recover(&this.loading_progress).clear();
489                                this.authenticate(cx).detach();
490                            }
491                            cx.notify();
492                        }
493                    }
494                })
495                .detach();
496
497                State {
498                    http_client,
499                    fetched_models: Default::default(),
500                    fetch_model_task: None,
501                    model_event_task: None,
502                    capability_cells,
503                    loading_progress,
504                    api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
505                    credentials_provider,
506                }
507            }),
508        };
509        // Discover eagerly so a running server is picked up without opening settings.
510        this.state
511            .update(cx, |state, cx| state.restart_fetch_models_task(cx));
512        this
513    }
514
515    fn settings(cx: &App) -> &LlamaCppSettings {
516        &AllLanguageModelSettings::get_global(cx).llama_cpp
517    }
518
519    fn api_url(cx: &App) -> SharedString {
520        let api_url = &Self::settings(cx).api_url;
521        if api_url.is_empty() {
522            LLAMA_CPP_API_URL.into()
523        } else {
524            SharedString::new(api_url.as_str())
525        }
526    }
527
528    fn has_custom_url(cx: &App) -> bool {
529        Self::settings(cx).api_url != LLAMA_CPP_API_URL
530    }
531}
532
533impl LanguageModelProviderState for LlamaCppLanguageModelProvider {
534    type ObservableEntity = State;
535
536    fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
537        Some(self.state.clone())
538    }
539}
540
541impl LanguageModelProvider for LlamaCppLanguageModelProvider {
542    fn id(&self) -> LanguageModelProviderId {
543        PROVIDER_ID
544    }
545
546    fn name(&self) -> LanguageModelProviderName {
547        PROVIDER_NAME
548    }
549
550    fn icon(&self) -> IconOrSvg {
551        IconOrSvg::Icon(IconName::AiLlamaCpp)
552    }
553
554    fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
555        // No default model: in router mode it could trigger an expensive load of
556        // an unloaded model on a constrained machine.
557        None
558    }
559
560    fn default_fast_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
561        // See explanation for default_model.
562        None
563    }
564
565    fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
566        let settings = LlamaCppLanguageModelProvider::settings(cx);
567        let effective = compute_effective_models(&self.state.read(cx).fetched_models, settings);
568
569        // Refresh the shared capability map so open conversations pick up settings changes.
570        sync_capability_cells(&self.capability_cells, &effective);
571        let mut models = effective
572            .into_values()
573            .map(|model| {
574                Arc::new(LlamaCppLanguageModel {
575                    id: LanguageModelId::from(model.name.clone()),
576                    name: model.name.clone(),
577                    display_name: model.display_name().to_string(),
578                    fallback_capabilities: LiveCapabilities::of(&model),
579                    supports_images: model.supports_images,
580                    capability_cells: self.capability_cells.clone(),
581                    loading_progress: self.loading_progress.clone(),
582                    http_client: self.http_client.clone(),
583                    request_limiter: RateLimiter::new(4),
584                    state: self.state.clone(),
585                }) as Arc<dyn LanguageModel>
586            })
587            .collect::<Vec<_>>();
588        models.sort_by_key(|model| model.name());
589        models
590    }
591
592    fn is_authenticated(&self, cx: &App) -> bool {
593        self.state.read(cx).is_authenticated()
594    }
595
596    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
597        self.state.update(cx, |state, cx| state.authenticate(cx))
598    }
599
600    fn settings_view(&self, _cx: &mut App) -> Option<ProviderSettingsView> {
601        let state = self.state.clone();
602        Some(ProviderSettingsView::SubPage(
603            SubPageProviderSettings::new(move |window, cx| {
604                cx.new(|cx| ConfigurationView::new(state.clone(), window, cx))
605                    .into()
606            })
607            .description(InlineDescription::Text(
608                "Run local models on your machine with LlamaCpp.".into(),
609            )),
610        ))
611    }
612}
613
614pub struct LlamaCppLanguageModel {
615    id: LanguageModelId,
616    /// The model id sent to the server.
617    name: String,
618    display_name: String,
619    /// Live capabilities shared with the provider, read fresh on each access so an
620    /// open conversation reflects the model's real values once it has loaded.
621    capability_cells: CapabilityCells,
622    /// Used when `capability_cells` has no entry (e.g. model removed mid-conversation).
623    fallback_capabilities: LiveCapabilities,
624    /// Available from `/v1/models` hints, so captured at build time.
625    supports_images: bool,
626    /// Shared with the provider; this model's load progress, read by `name` so the
627    /// selector can show a loading indicator.
628    loading_progress: LoadingProgress,
629    http_client: Arc<dyn HttpClient>,
630    request_limiter: RateLimiter,
631    state: Entity<State>,
632}
633
634impl LlamaCppLanguageModel {
635    /// The model's live capabilities, or the build-time fallback if the map lacks it.
636    fn capabilities(&self) -> LiveCapabilities {
637        read_recover(&self.capability_cells)
638            .get(&self.name)
639            .copied()
640            .unwrap_or(self.fallback_capabilities)
641    }
642
643    /// This model's load-status label while loading, read live from the shared map.
644    fn loading_label(&self) -> Option<SharedString> {
645        read_recover(&self.loading_progress)
646            .get(&self.name)
647            .cloned()
648    }
649
650    fn to_llama_cpp_request(
651        &self,
652        request: LanguageModelRequest,
653    ) -> Result<llama_cpp::ChatCompletionRequest> {
654        build_llama_cpp_request(
655            &self.name,
656            self.supports_images,
657            self.capabilities(),
658            request,
659        )
660    }
661
662    fn stream_completion(
663        &self,
664        request: llama_cpp::ChatCompletionRequest,
665        cx: &AsyncApp,
666    ) -> BoxFuture<
667        'static,
668        Result<futures::stream::BoxStream<'static, Result<llama_cpp::ResponseStreamEvent>>>,
669    > {
670        let http_client = self.http_client.clone();
671        let (api_key, api_url, extra_headers) = self.state.read_with(cx, |state, cx| {
672            let api_url = LlamaCppLanguageModelProvider::api_url(cx);
673            let extra_headers = LlamaCppLanguageModelProvider::settings(cx)
674                .custom_headers
675                .clone();
676            (state.api_key_state.key(&api_url), api_url, extra_headers)
677        });
678
679        let future = self.request_limiter.stream(async move {
680            let stream = stream_chat_completion(
681                http_client.as_ref(),
682                &api_url,
683                api_key.as_deref(),
684                request,
685                &extra_headers,
686            )
687            .await?;
688            Ok(stream)
689        });
690
691        async move { Ok(future.await?.boxed()) }.boxed()
692    }
693}
694
695fn build_llama_cpp_request(
696    model_name: &str,
697    supports_images: bool,
698    capabilities: LiveCapabilities,
699    request: LanguageModelRequest,
700) -> Result<llama_cpp::ChatCompletionRequest> {
701    if request.contains_custom_tool_input() {
702        anyhow::bail!("llama.cpp does not support custom tools");
703    }
704
705    let supports_tools = capabilities.supports_tools;
706    let supports_thinking = capabilities.supports_thinking;
707    let mut messages = Vec::new();
708
709    for message in request.messages {
710        let mut reasoning_content: Option<String> = None;
711        for content in message.content {
712            match content {
713                MessageContent::Text(text) => add_message_content_part(
714                    llama_cpp::MessagePart::Text { text },
715                    message.role,
716                    &mut messages,
717                    if supports_thinking && message.role == Role::Assistant {
718                        reasoning_content.take()
719                    } else {
720                        None
721                    },
722                ),
723                MessageContent::Thinking { text, .. } => {
724                    if supports_thinking && message.role == Role::Assistant && !text.is_empty() {
725                        reasoning_content.get_or_insert_default().push_str(&text);
726                    }
727                }
728                MessageContent::RedactedThinking(_) => {}
729                MessageContent::Compaction(_) => {}
730                MessageContent::Image(image) => {
731                    if supports_images {
732                        add_message_content_part(
733                            llama_cpp::MessagePart::Image {
734                                image_url: llama_cpp::ImageUrl {
735                                    url: image.to_base64_url(),
736                                    detail: None,
737                                },
738                            },
739                            message.role,
740                            &mut messages,
741                            if supports_thinking && message.role == Role::Assistant {
742                                reasoning_content.take()
743                            } else {
744                                None
745                            },
746                        );
747                    }
748                }
749                MessageContent::ToolUse(tool_use) => {
750                    let input = tool_use.input.as_json().ok_or_else(|| {
751                        anyhow::anyhow!("llama.cpp does not support custom tool calls")
752                    })?;
753                    let tool_call = llama_cpp::ToolCall {
754                        id: tool_use.id.to_string(),
755                        content: llama_cpp::ToolCallContent::Function {
756                            function: llama_cpp::FunctionContent {
757                                name: tool_use.name.to_string(),
758                                arguments: serde_json::to_string(input).unwrap_or_default(),
759                            },
760                        },
761                    };
762
763                    if let Some(llama_cpp::ChatMessage::Assistant {
764                        tool_calls,
765                        reasoning_content: message_reasoning_content,
766                        ..
767                    }) = messages.last_mut()
768                    {
769                        append_reasoning_content(
770                            message_reasoning_content,
771                            reasoning_content.take(),
772                        );
773                        tool_calls.push(tool_call);
774                    } else {
775                        messages.push(llama_cpp::ChatMessage::Assistant {
776                            content: None,
777                            reasoning_content: reasoning_content.take(),
778                            tool_calls: vec![tool_call],
779                        });
780                    }
781                }
782                MessageContent::ToolResult(tool_result) => {
783                    let content: Vec<llama_cpp::MessagePart> = tool_result
784                        .content
785                        .iter()
786                        .filter_map(|part| match part {
787                            LanguageModelToolResultContent::Text(text) => {
788                                Some(llama_cpp::MessagePart::Text {
789                                    text: text.to_string(),
790                                })
791                            }
792                            LanguageModelToolResultContent::Image(image) => {
793                                if supports_images {
794                                    Some(llama_cpp::MessagePart::Image {
795                                        image_url: llama_cpp::ImageUrl {
796                                            url: image.to_base64_url(),
797                                            detail: None,
798                                        },
799                                    })
800                                } else {
801                                    None
802                                }
803                            }
804                        })
805                        .collect();
806
807                    messages.push(llama_cpp::ChatMessage::Tool {
808                        content: content.into(),
809                        tool_call_id: tool_result.tool_use_id.to_string(),
810                    });
811                }
812            }
813        }
814    }
815
816    let tools: Vec<llama_cpp::ToolDefinition> = if supports_tools {
817        request
818            .tools
819            .into_iter()
820            .map(|tool| {
821                let input_schema = match tool.input {
822                    language_model::LanguageModelRequestToolInput::Function {
823                        input_schema,
824                        ..
825                    } => input_schema,
826                    language_model::LanguageModelRequestToolInput::Custom { .. } => {
827                        return Err(anyhow::anyhow!("llama.cpp does not support custom tools"));
828                    }
829                };
830                Ok(llama_cpp::ToolDefinition::Function {
831                    function: llama_cpp::FunctionDefinition {
832                        name: tool.name,
833                        description: Some(tool.description),
834                        parameters: Some(input_schema),
835                    },
836                })
837            })
838            .collect::<Result<_>>()?
839    } else {
840        Vec::new()
841    };
842    // Only send `tool_choice` with actual tools; some OpenAI-compatible servers
843    // reject it otherwise.
844    let tool_choice = if tools.is_empty() {
845        None
846    } else {
847        request.tool_choice.map(|choice| match choice {
848            LanguageModelToolChoice::Auto => llama_cpp::ToolChoice::Auto,
849            LanguageModelToolChoice::Any => llama_cpp::ToolChoice::Required,
850            LanguageModelToolChoice::None => llama_cpp::ToolChoice::None,
851        })
852    };
853
854    Ok(llama_cpp::ChatCompletionRequest {
855        model: model_name.to_string(),
856        messages,
857        stream: true,
858        // Let the server decide the output length (its `n_predict` default).
859        max_tokens: None,
860        stop: if request.stop.is_empty() {
861            None
862        } else {
863            Some(request.stop)
864        },
865        // llama.cpp models often ship recommended sampler settings, so override
866        // temperature only when the request sets one.
867        temperature: request.temperature,
868        tools,
869        tool_choice,
870        stream_options: Some(llama_cpp::StreamOptions {
871            include_usage: true,
872        }),
873    })
874}
875
876impl LanguageModel for LlamaCppLanguageModel {
877    fn id(&self) -> LanguageModelId {
878        self.id.clone()
879    }
880
881    fn name(&self) -> LanguageModelName {
882        match self.loading_label() {
883            // Surface load progress in the display name so it shows wherever the
884            // model is named, without provider-agnostic UI changes. The agent
885            // rebuilds the name on `ProviderStateChanged`, which our ticks emit.
886            Some(label) => LanguageModelName::from(format!("{} · {}", self.display_name, label)),
887            None => LanguageModelName::from(self.display_name.clone()),
888        }
889    }
890
891    fn provider_id(&self) -> LanguageModelProviderId {
892        PROVIDER_ID
893    }
894
895    fn provider_name(&self) -> LanguageModelProviderName {
896        PROVIDER_NAME
897    }
898
899    fn supports_tools(&self) -> bool {
900        self.capabilities().supports_tools
901    }
902
903    fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
904        self.supports_tools()
905            && match choice {
906                LanguageModelToolChoice::Auto => true,
907                LanguageModelToolChoice::Any => true,
908                LanguageModelToolChoice::None => true,
909            }
910    }
911
912    fn supports_images(&self) -> bool {
913        self.supports_images
914    }
915
916    fn supports_thinking(&self) -> bool {
917        self.capabilities().supports_thinking
918    }
919
920    fn telemetry_id(&self) -> String {
921        telemetry_id_for(&self.name)
922    }
923
924    fn max_token_count(&self) -> u64 {
925        self.capabilities().max_tokens
926    }
927
928    fn stream_completion(
929        &self,
930        request: LanguageModelRequest,
931        cx: &AsyncApp,
932    ) -> BoxFuture<
933        'static,
934        Result<
935            BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
936            LanguageModelCompletionError,
937        >,
938    > {
939        let request = match self.to_llama_cpp_request(request) {
940            Ok(request) => request,
941            Err(error) => return async move { Err(error.into()) }.boxed(),
942        };
943        let completions = self.stream_completion(request, cx);
944        async move {
945            let mapper = LlamaCppEventMapper::new();
946            Ok(mapper.map_stream(completions.await?).boxed())
947        }
948        .boxed()
949    }
950}
951
952struct LlamaCppEventMapper {
953    tool_calls_by_index: HashMap<usize, RawToolCall>,
954}
955
956impl LlamaCppEventMapper {
957    fn new() -> Self {
958        Self {
959            tool_calls_by_index: HashMap::default(),
960        }
961    }
962
963    pub fn map_stream(
964        mut self,
965        events: Pin<Box<dyn Send + Stream<Item = Result<llama_cpp::ResponseStreamEvent>>>>,
966    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
967    {
968        events.flat_map(move |event| {
969            futures::stream::iter(match event {
970                Ok(event) => self.map_event(event),
971                Err(error) => vec![Err(LanguageModelCompletionError::from(error))],
972            })
973        })
974    }
975
976    pub fn map_event(
977        &mut self,
978        event: llama_cpp::ResponseStreamEvent,
979    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
980        let mut events = Vec::new();
981
982        if let Some(usage) = event.usage {
983            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
984                input_tokens: usage.prompt_tokens,
985                output_tokens: usage.completion_tokens,
986                cache_creation_input_tokens: 0,
987                cache_read_input_tokens: 0,
988            })));
989        }
990
991        if let Some(choice) = event.choices.into_iter().next() {
992            if let Some(reasoning_content) = choice.delta.reasoning_content {
993                events.push(Ok(LanguageModelCompletionEvent::Thinking {
994                    text: reasoning_content,
995                    signature: None,
996                }));
997            }
998
999            if let Some(content) = choice.delta.content {
1000                if !content.is_empty() {
1001                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
1002                }
1003            }
1004
1005            if let Some(tool_calls) = choice.delta.tool_calls {
1006                for tool_call in tool_calls {
1007                    let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
1008
1009                    if let Some(tool_id) = tool_call.id {
1010                        entry.id = tool_id;
1011                    }
1012
1013                    if let Some(function) = tool_call.function {
1014                        if let Some(name) = function.name {
1015                            // Only the first chunk carries the function name;
1016                            // later chunks send an empty name with arguments.
1017                            if !name.is_empty() {
1018                                entry.name = name;
1019                            }
1020                        }
1021
1022                        if let Some(arguments) = function.arguments {
1023                            entry.arguments.push_str(&arguments);
1024                        }
1025                    }
1026                }
1027            }
1028
1029            if let Some(finish_reason) = choice.finish_reason.as_deref() {
1030                match finish_reason {
1031                    "stop" => {
1032                        events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
1033                    }
1034                    "tool_calls" => {
1035                        events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
1036                            match parse_tool_arguments(&tool_call.arguments) {
1037                                Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
1038                                    LanguageModelToolUse {
1039                                        id: tool_call.id.into(),
1040                                        name: tool_call.name.into(),
1041                                        is_input_complete: true,
1042                                        input: language_model::LanguageModelToolUseInput::Json(
1043                                            input,
1044                                        ),
1045                                        raw_input: tool_call.arguments,
1046                                        thought_signature: None,
1047                                    },
1048                                )),
1049                                Err(error) => {
1050                                    Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
1051                                        id: tool_call.id.into(),
1052                                        tool_name: tool_call.name.into(),
1053                                        raw_input: tool_call.arguments.into(),
1054                                        json_parse_error: error.to_string(),
1055                                    })
1056                                }
1057                            }
1058                        }));
1059
1060                        events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
1061                    }
1062                    "length" => {
1063                        events.push(Ok(LanguageModelCompletionEvent::Stop(
1064                            StopReason::MaxTokens,
1065                        )));
1066                    }
1067                    unexpected => {
1068                        log::warn!("Unexpected llama.cpp finish_reason: {unexpected:?}");
1069                        events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
1070                    }
1071                }
1072            }
1073        }
1074
1075        events
1076    }
1077}
1078
1079#[derive(Default)]
1080struct RawToolCall {
1081    id: String,
1082    name: String,
1083    arguments: String,
1084}
1085
1086fn add_message_content_part(
1087    new_part: llama_cpp::MessagePart,
1088    role: Role,
1089    messages: &mut Vec<llama_cpp::ChatMessage>,
1090    reasoning_content: Option<String>,
1091) {
1092    match (role, messages.last_mut()) {
1093        (Role::User, Some(llama_cpp::ChatMessage::User { content }))
1094        | (Role::System, Some(llama_cpp::ChatMessage::System { content })) => {
1095            content.push_part(new_part);
1096        }
1097        (
1098            Role::Assistant,
1099            Some(llama_cpp::ChatMessage::Assistant {
1100                content: Some(content),
1101                reasoning_content: message_reasoning_content,
1102                ..
1103            }),
1104        ) => {
1105            append_reasoning_content(message_reasoning_content, reasoning_content);
1106            content.push_part(new_part);
1107        }
1108        _ => {
1109            messages.push(match role {
1110                Role::User => llama_cpp::ChatMessage::User {
1111                    content: llama_cpp::MessageContent::from(vec![new_part]),
1112                },
1113                Role::Assistant => llama_cpp::ChatMessage::Assistant {
1114                    content: Some(llama_cpp::MessageContent::from(vec![new_part])),
1115                    reasoning_content,
1116                    tool_calls: Vec::new(),
1117                },
1118                Role::System => llama_cpp::ChatMessage::System {
1119                    content: llama_cpp::MessageContent::from(vec![new_part]),
1120                },
1121            });
1122        }
1123    }
1124}
1125
1126fn append_reasoning_content(target: &mut Option<String>, content: Option<String>) {
1127    let Some(content) = content else {
1128        return;
1129    };
1130    if content.is_empty() {
1131        return;
1132    }
1133    target.get_or_insert_default().push_str(&content);
1134}
1135
1136fn merge_settings_into_models(
1137    models: &mut HashMap<String, llama_cpp::Model>,
1138    available_models: &[AvailableModel],
1139    context_window: Option<u64>,
1140) {
1141    for setting_model in available_models {
1142        if let Some(model) = models.get_mut(&setting_model.name) {
1143            if context_window.is_none() {
1144                model.max_tokens = setting_model.max_tokens;
1145            }
1146            if setting_model.display_name.is_some() {
1147                model.display_name = setting_model.display_name.clone();
1148            }
1149            if let Some(supports_tools) = setting_model.supports_tools {
1150                model.supports_tools = supports_tools;
1151            }
1152            if let Some(supports_images) = setting_model.supports_images {
1153                model.supports_images = supports_images;
1154            }
1155            if let Some(supports_thinking) = setting_model.supports_thinking {
1156                model.supports_thinking = supports_thinking;
1157            }
1158        } else {
1159            models.insert(
1160                setting_model.name.clone(),
1161                llama_cpp::Model {
1162                    name: setting_model.name.clone(),
1163                    display_name: setting_model.display_name.clone(),
1164                    max_tokens: context_window.unwrap_or(setting_model.max_tokens),
1165                    supports_tools: setting_model.supports_tools.unwrap_or(false),
1166                    supports_images: setting_model.supports_images.unwrap_or(false),
1167                    supports_thinking: setting_model.supports_thinking.unwrap_or(false),
1168                },
1169            );
1170        }
1171    }
1172}
1173
1174struct ConfigurationView {
1175    api_key_editor: Entity<InputField>,
1176    api_url_editor: Entity<InputField>,
1177    context_window_editor: Entity<InputField>,
1178    state: Entity<State>,
1179}
1180
1181impl ConfigurationView {
1182    pub fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1183        let api_key_editor = cx.new(|cx| InputField::new(window, cx, "sk-...").label("API key"));
1184
1185        let api_url_editor = cx.new(|cx| {
1186            let input = InputField::new(window, cx, LLAMA_CPP_API_URL).label("API URL");
1187            input.set_text(&LlamaCppLanguageModelProvider::api_url(cx), window, cx);
1188            input
1189        });
1190
1191        let context_window_editor = cx.new(|cx| {
1192            let input = InputField::new(window, cx, "8192").label("Context Window");
1193            if let Some(context_window) = LlamaCppLanguageModelProvider::settings(cx).context_window
1194            {
1195                input.set_text(&context_window.to_string(), window, cx);
1196            }
1197            input
1198        });
1199
1200        cx.observe(&state, |_, _, cx| {
1201            cx.notify();
1202        })
1203        .detach();
1204
1205        Self {
1206            api_key_editor,
1207            api_url_editor,
1208            context_window_editor,
1209            state,
1210        }
1211    }
1212
1213    fn retry_connection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1214        let has_api_url = LlamaCppLanguageModelProvider::has_custom_url(cx);
1215        let has_api_key = self
1216            .state
1217            .read_with(cx, |state, _| state.api_key_state.has_key());
1218        if !has_api_url {
1219            self.save_api_url(cx);
1220        }
1221        if !has_api_key {
1222            self.save_api_key(&Default::default(), window, cx);
1223        }
1224
1225        self.state.update(cx, |state, cx| {
1226            state.restart_fetch_models_task(cx);
1227        });
1228    }
1229
1230    fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
1231        let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
1232        if api_key.is_empty() {
1233            return;
1234        }
1235
1236        // A URL change can cause the editor to be shown again.
1237        self.api_key_editor
1238            .update(cx, |input, cx| input.set_text("", window, cx));
1239
1240        let state = self.state.clone();
1241        cx.spawn_in(window, async move |_, cx| {
1242            state
1243                .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
1244                .await
1245        })
1246        .detach_and_log_err(cx);
1247    }
1248
1249    fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1250        self.api_key_editor
1251            .update(cx, |input, cx| input.set_text("", window, cx));
1252
1253        let state = self.state.clone();
1254        cx.spawn_in(window, async move |_, cx| {
1255            state
1256                .update(cx, |state, cx| state.set_api_key(None, cx))
1257                .await
1258        })
1259        .detach_and_log_err(cx);
1260
1261        cx.notify();
1262    }
1263
1264    fn save_api_url(&self, cx: &mut Context<Self>) {
1265        let api_url = self.api_url_editor.read(cx).text(cx).trim().to_string();
1266        let current_url = LlamaCppLanguageModelProvider::api_url(cx);
1267        if !api_url.is_empty() && &api_url != &current_url {
1268            let fs = <dyn Fs>::global(cx);
1269            update_settings_file(fs, cx, move |settings, _| {
1270                settings
1271                    .language_models
1272                    .get_or_insert_default()
1273                    .llama_cpp
1274                    .get_or_insert_default()
1275                    .api_url = Some(api_url);
1276            });
1277        }
1278    }
1279
1280    fn reset_api_url(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1281        self.api_url_editor
1282            .update(cx, |input, cx| input.set_text("", window, cx));
1283        let fs = <dyn Fs>::global(cx);
1284        update_settings_file(fs, cx, |settings, _cx| {
1285            if let Some(settings) = settings
1286                .language_models
1287                .as_mut()
1288                .and_then(|models| models.llama_cpp.as_mut())
1289            {
1290                settings.api_url = Some(LLAMA_CPP_API_URL.into());
1291            }
1292        });
1293        cx.notify();
1294    }
1295
1296    fn save_context_window(&mut self, cx: &mut Context<Self>) {
1297        let context_window_str = self
1298            .context_window_editor
1299            .read(cx)
1300            .text(cx)
1301            .trim()
1302            .to_string();
1303        let current_context_window = LlamaCppLanguageModelProvider::settings(cx).context_window;
1304
1305        if let Ok(context_window) = context_window_str.parse::<u64>() {
1306            if Some(context_window) != current_context_window {
1307                let fs = <dyn Fs>::global(cx);
1308                update_settings_file(fs, cx, move |settings, _| {
1309                    settings
1310                        .language_models
1311                        .get_or_insert_default()
1312                        .llama_cpp
1313                        .get_or_insert_default()
1314                        .context_window = Some(context_window);
1315                });
1316            }
1317        } else if context_window_str.is_empty() && current_context_window.is_some() {
1318            let fs = <dyn Fs>::global(cx);
1319            update_settings_file(fs, cx, move |settings, _| {
1320                settings
1321                    .language_models
1322                    .get_or_insert_default()
1323                    .llama_cpp
1324                    .get_or_insert_default()
1325                    .context_window = None;
1326            });
1327        }
1328    }
1329
1330    fn reset_context_window(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1331        self.context_window_editor
1332            .update(cx, |input, cx| input.set_text("", window, cx));
1333        let fs = <dyn Fs>::global(cx);
1334        update_settings_file(fs, cx, |settings, _cx| {
1335            if let Some(settings) = settings
1336                .language_models
1337                .as_mut()
1338                .and_then(|models| models.llama_cpp.as_mut())
1339            {
1340                settings.context_window = None;
1341            }
1342        });
1343        cx.notify();
1344    }
1345
1346    fn render_instructions(cx: &App) -> Div {
1347        v_flex()
1348            .gap_2()
1349            .child(
1350                Label::new(
1351                    "Run open models locally with llama.cpp's built-in server, or connect to a \
1352                remote llama.cpp server.",
1353                )
1354                .color(Color::Muted),
1355            )
1356            .child(Label::new("To use a local llama.cpp server:").color(Color::Muted))
1357            .child(
1358                List::new()
1359                    .child(
1360                        ListBulletItem::new("")
1361                            .child(Label::new("Install llama.cpp from").color(Color::Muted))
1362                            .child(ButtonLink::new("llama.app", LLAMA_CPP_DOWNLOAD_URL)),
1363                    )
1364                    .child(
1365                        ListBulletItem::new("")
1366                            .child(
1367                                Label::new("Start the server in router mode:").color(Color::Muted),
1368                            )
1369                            .child(Label::new("llama serve").inline_code(cx)),
1370                    )
1371                    .child(
1372                        ListBulletItem::new(
1373                            "Click 'Connect' below to start using llama.cpp in Omega",
1374                        )
1375                        .label_color(Color::Muted),
1376                    ),
1377            )
1378            .child(
1379                Label::new(
1380                    "Alternatively, you can connect to a remote llama.cpp server by specifying its \
1381                URL and API key (set with --api-key, may not be required):",
1382                )
1383                .color(Color::Muted),
1384            )
1385    }
1386
1387    fn render_api_key_editor(&self, cx: &Context<Self>) -> impl IntoElement {
1388        let state = self.state.read(cx);
1389        let env_var_set = state.api_key_state.is_from_env_var();
1390        let configured_card_label = if env_var_set {
1391            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
1392        } else {
1393            "API key configured".to_string()
1394        };
1395
1396        let api_key_control = if !state.api_key_state.has_key() {
1397            self.api_key_editor.clone().into_any_element()
1398        } else {
1399            ConfiguredApiCard::new("llama-cpp-reset-key", configured_card_label)
1400                .disabled(env_var_set)
1401                .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
1402                .when(env_var_set, |this| {
1403                    this.tooltip_label(format!(
1404                        "To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."
1405                    ))
1406                })
1407                .into_any_element()
1408        };
1409
1410        v_flex()
1411            .on_action(cx.listener(Self::save_api_key))
1412            .child(api_key_control)
1413            .gap_1p5()
1414            .mb_2()
1415            .child(
1416                Label::new(format!(
1417                    "You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Omega."
1418                ))
1419                .size(LabelSize::Small)
1420                .color(Color::Muted),
1421            )
1422    }
1423
1424    fn render_context_window_editor(&self, cx: &Context<Self>) -> Div {
1425        let settings = LlamaCppLanguageModelProvider::settings(cx);
1426        let custom_context_window_set = settings.context_window.is_some();
1427
1428        if custom_context_window_set {
1429            h_flex()
1430                .p_1()
1431                .justify_between()
1432                .rounded_md()
1433                .border_1()
1434                .border_color(cx.theme().colors().border_variant)
1435                .bg(cx.theme().colors().background.opacity(0.5))
1436                .child(
1437                    h_flex()
1438                        .gap_1()
1439                        .child(Icon::new(IconName::Check).color(Color::Success))
1440                        .child(Label::new(format!(
1441                            "Context Window: {}",
1442                            settings.context_window.unwrap_or_default()
1443                        ))),
1444                )
1445                .child(
1446                    Button::new("reset-context-window", "Reset")
1447                        .style(ButtonStyle::Outlined)
1448                        .label_size(LabelSize::Small)
1449                        .start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
1450                        .on_click(
1451                            cx.listener(|this, _, window, cx| {
1452                                this.reset_context_window(window, cx)
1453                            }),
1454                        ),
1455                )
1456        } else {
1457            v_flex()
1458                .on_action(
1459                    cx.listener(|this, _: &menu::Confirm, _window, cx| {
1460                        this.save_context_window(cx)
1461                    }),
1462                )
1463                .child(self.context_window_editor.clone())
1464                .gap_1p5()
1465                .child(
1466                    Label::new("Default: Discovered from the server")
1467                        .size(LabelSize::Small)
1468                        .color(Color::Muted),
1469                )
1470        }
1471    }
1472
1473    fn render_api_url_editor(&self, cx: &Context<Self>) -> Div {
1474        let api_url = LlamaCppLanguageModelProvider::api_url(cx);
1475        let custom_api_url_set = api_url != LLAMA_CPP_API_URL;
1476
1477        if custom_api_url_set {
1478            h_flex()
1479                .p_1()
1480                .justify_between()
1481                .rounded_md()
1482                .border_1()
1483                .border_color(cx.theme().colors().border_variant)
1484                .bg(cx.theme().colors().background.opacity(0.5))
1485                .child(
1486                    h_flex()
1487                        .gap_1()
1488                        .child(Icon::new(IconName::Check).color(Color::Success))
1489                        .child(Label::new(api_url)),
1490                )
1491                .child(
1492                    Button::new("reset-api-url", "Reset API URL")
1493                        .style(ButtonStyle::Outlined)
1494                        .label_size(LabelSize::Small)
1495                        .start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
1496                        .on_click(
1497                            cx.listener(|this, _, window, cx| this.reset_api_url(window, cx)),
1498                        ),
1499                )
1500        } else {
1501            v_flex()
1502                .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| {
1503                    this.save_api_url(cx);
1504                    cx.notify();
1505                }))
1506                .gap_1p5()
1507                .child(self.api_url_editor.clone())
1508        }
1509    }
1510}
1511
1512impl Render for ConfigurationView {
1513    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1514        let is_authenticated = self.state.read(cx).is_authenticated();
1515
1516        v_flex()
1517            .gap_2()
1518            .child(Headline::new("llama.cpp").size(HeadlineSize::Small))
1519            .child(Self::render_instructions(cx))
1520            .child(self.render_api_url_editor(cx))
1521            .child(self.render_context_window_editor(cx))
1522            .child(self.render_api_key_editor(cx))
1523            .child(Divider::horizontal())
1524            .child(
1525                h_flex()
1526                    .pt_2()
1527                    .w_full()
1528                    .justify_between()
1529                    .gap_2()
1530                    .child(
1531                        h_flex()
1532                            .w_full()
1533                            .gap_2()
1534                            .map(|this| {
1535                                if is_authenticated {
1536                                    this.child(
1537                                        Button::new("llama-cpp-webui", "Open WebUI")
1538                                            .style(ButtonStyle::OutlinedGhost)
1539                                            .size(ButtonSize::Medium)
1540                                            .end_icon(
1541                                                Icon::new(IconName::ArrowUpRight)
1542                                                    .size(IconSize::XSmall)
1543                                                    .color(Color::Muted),
1544                                            )
1545                                            .on_click(move |_, _, cx| {
1546                                                let url =
1547                                                    LlamaCppLanguageModelProvider::api_url(cx);
1548                                                cx.open_url(&url);
1549                                            })
1550                                            .into_any_element(),
1551                                    )
1552                                    .child(
1553                                        Button::new("llama-cpp-site", "llama.cpp")
1554                                            .style(ButtonStyle::OutlinedGhost)
1555                                            .size(ButtonSize::Medium)
1556                                            .end_icon(
1557                                                Icon::new(IconName::ArrowUpRight)
1558                                                    .size(IconSize::XSmall)
1559                                                    .color(Color::Muted),
1560                                            )
1561                                            .on_click(move |_, _, cx| {
1562                                                cx.open_url(LLAMA_CPP_DOWNLOAD_URL)
1563                                            })
1564                                            .into_any_element(),
1565                                    )
1566                                } else {
1567                                    this.child(
1568                                        Button::new("download_llama_cpp_button", "Get llama.cpp")
1569                                            .style(ButtonStyle::OutlinedGhost)
1570                                            .size(ButtonSize::Medium)
1571                                            .end_icon(
1572                                                Icon::new(IconName::ArrowUpRight)
1573                                                    .size(IconSize::XSmall)
1574                                                    .color(Color::Muted),
1575                                            )
1576                                            .on_click(move |_, _, cx| {
1577                                                cx.open_url(LLAMA_CPP_DOWNLOAD_URL)
1578                                            })
1579                                            .into_any_element(),
1580                                    )
1581                                }
1582                            })
1583                            .child(
1584                                Button::new("view-models", "Browse GGUF Models")
1585                                    .style(ButtonStyle::OutlinedGhost)
1586                                    .size(ButtonSize::Medium)
1587                                    .end_icon(
1588                                        Icon::new(IconName::ArrowUpRight)
1589                                            .size(IconSize::XSmall)
1590                                            .color(Color::Muted),
1591                                    )
1592                                    .on_click(move |_, _, cx| cx.open_url(LLAMA_CPP_MODELS_URL)),
1593                            ),
1594                    )
1595                    .map(|this| {
1596                        if is_authenticated {
1597                            this.child(
1598                                ButtonLike::new("connected")
1599                                    .size(ButtonSize::Medium)
1600                                    .child(
1601                                        h_flex()
1602                                            .gap_1()
1603                                            .child(Icon::new(IconName::Check).color(Color::Success))
1604                                            .child(Label::new("Connected")),
1605                                    )
1606                                    .child(
1607                                        IconButton::new("refresh-models", IconName::RotateCcw)
1608                                            .icon_size(IconSize::Small)
1609                                            .tooltip(Tooltip::text("Refresh Models"))
1610                                            .on_click(cx.listener(|this, _, window, cx| {
1611                                                this.state.update(cx, |state, _| {
1612                                                    state.fetched_models.clear();
1613                                                });
1614                                                this.retry_connection(window, cx);
1615                                            })),
1616                                    ),
1617                            )
1618                        } else {
1619                            this.child(
1620                                Button::new("retry_llama_cpp_models", "Connect")
1621                                    .style(ButtonStyle::Outlined)
1622                                    .size(ButtonSize::Medium)
1623                                    .start_icon(
1624                                        Icon::new(IconName::PlayOutlined).size(IconSize::XSmall),
1625                                    )
1626                                    .on_click(cx.listener(move |this, _, window, cx| {
1627                                        this.retry_connection(window, cx)
1628                                    })),
1629                            )
1630                        }
1631                    }),
1632            )
1633    }
1634}
1635
1636#[cfg(test)]
1637mod tests {
1638    use super::*;
1639    use gpui::TestAppContext;
1640    use http_client::FakeHttpClient;
1641    use parking_lot::Mutex;
1642    use std::sync::atomic::{AtomicUsize, Ordering};
1643
1644    struct FakeCredentialsProvider {
1645        api_key: Vec<u8>,
1646    }
1647
1648    impl CredentialsProvider for FakeCredentialsProvider {
1649        fn read_credentials<'a>(
1650            &'a self,
1651            _url: &'a str,
1652            _cx: &'a AsyncApp,
1653        ) -> std::pin::Pin<
1654            Box<dyn std::future::Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>,
1655        > {
1656            let api_key = self.api_key.clone();
1657            Box::pin(async move { Ok(Some(("Bearer".to_string(), api_key))) })
1658        }
1659
1660        fn write_credentials<'a>(
1661            &'a self,
1662            _url: &'a str,
1663            _username: &'a str,
1664            _password: &'a [u8],
1665            _cx: &'a AsyncApp,
1666        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
1667            Box::pin(async { Ok(()) })
1668        }
1669
1670        fn delete_credentials<'a>(
1671            &'a self,
1672            _url: &'a str,
1673            _cx: &'a AsyncApp,
1674        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
1675            Box::pin(async { Ok(()) })
1676        }
1677    }
1678
1679    fn entry(id: &str, n_ctx: Option<u64>, n_ctx_train: Option<u64>) -> ModelEntry {
1680        ModelEntry {
1681            id: id.to_string(),
1682            meta: Some(llama_cpp::ModelMeta { n_ctx, n_ctx_train }),
1683            architecture: None,
1684            status: None,
1685        }
1686    }
1687
1688    #[test]
1689    fn display_name_strips_path_and_extension() {
1690        assert_eq!(
1691            display_name_for("../models/Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf"),
1692            "Qwen2.5-Coder-7B-Instruct-Q4_K_M"
1693        );
1694        assert_eq!(display_name_for("my-alias"), "my-alias");
1695    }
1696
1697    #[test]
1698    fn telemetry_id_strips_local_model_paths() {
1699        assert_eq!(
1700            telemetry_id_for("/Users/alice/models/Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf"),
1701            "llama.cpp/Qwen2.5-Coder-7B-Instruct-Q4_K_M"
1702        );
1703        assert_eq!(
1704            telemetry_id_for(r"C:\Users\alice\models\Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf"),
1705            "llama.cpp/Qwen2.5-Coder-7B-Instruct-Q4_K_M"
1706        );
1707        assert_eq!(telemetry_id_for("my-alias"), "llama.cpp/my-alias");
1708    }
1709
1710    #[test]
1711    fn model_uses_props_then_meta_for_context() {
1712        let props = Props {
1713            default_generation_settings: Some(llama_cpp::GenerationSettings { n_ctx: Some(8192) }),
1714            modalities: Some(llama_cpp::Modalities { vision: true }),
1715            chat_template_caps: Some(llama_cpp::ChatTemplateCaps {
1716                supports_tool_calls: true,
1717                supports_preserve_reasoning: true,
1718                ..Default::default()
1719            }),
1720        };
1721        // /props wins when present.
1722        let model = model_from_entry(&entry("m", Some(4096), Some(131072)), Some(&props));
1723        assert_eq!(model.max_tokens, 8192);
1724        assert!(model.supports_tools);
1725        assert!(model.supports_images);
1726        assert!(model.supports_thinking);
1727
1728        // Unprobed: falls back to the listing's runtime context, then trained
1729        // context. Tools are assumed supported until the model loads.
1730        let model = model_from_entry(&entry("m", Some(4096), Some(131072)), None);
1731        assert_eq!(model.max_tokens, 4096);
1732        assert!(model.supports_tools);
1733        assert!(!model.supports_thinking);
1734
1735        let model = model_from_entry(&entry("m", None, Some(131072)), None);
1736        assert_eq!(model.max_tokens, 131072);
1737
1738        // Nothing reported -> the optimistic unloaded-context assumption.
1739        let model = model_from_entry(&entry("m", None, None), None);
1740        assert_eq!(model.max_tokens, ASSUMED_UNLOADED_CONTEXT);
1741        assert!(model.supports_tools);
1742    }
1743
1744    #[test]
1745    fn router_entry_detects_vision_from_modalities() {
1746        let router_entry = ModelEntry {
1747            id: "vlm".to_string(),
1748            meta: None,
1749            architecture: Some(llama_cpp::Architecture {
1750                input_modalities: vec!["text".to_string(), "image".to_string()],
1751            }),
1752            status: Some(llama_cpp::ModelStatus {
1753                value: "unloaded".to_string(),
1754            }),
1755        };
1756        let model = model_from_entry(&router_entry, None);
1757        assert!(model.supports_images);
1758        // Unprobed router models optimistically advertise tools until loaded.
1759        assert!(model.supports_tools);
1760    }
1761
1762    #[test]
1763    fn settings_override_capabilities_and_context() {
1764        let mut models: HashMap<String, llama_cpp::Model> = HashMap::default();
1765        models.insert(
1766            "qwen".to_string(),
1767            llama_cpp::Model::new("qwen", Some("qwen"), Some(8192), false, false, false),
1768        );
1769
1770        let available = vec![AvailableModel {
1771            name: "qwen".to_string(),
1772            display_name: Some("Qwen Coder".to_string()),
1773            max_tokens: 16384,
1774            supports_tools: Some(true),
1775            supports_images: None,
1776            supports_thinking: Some(true),
1777        }];
1778
1779        merge_settings_into_models(&mut models, &available, None);
1780
1781        let model = models.get("qwen").unwrap();
1782        assert_eq!(model.display_name.as_deref(), Some("Qwen Coder"));
1783        assert_eq!(model.max_tokens, 16384);
1784        assert!(model.supports_tools);
1785        assert!(model.supports_thinking);
1786        // Unspecified capability keeps the discovered value.
1787        assert!(!model.supports_images);
1788    }
1789
1790    #[test]
1791    fn capability_cells_update_when_a_model_loads() {
1792        let cells: CapabilityCells = Arc::new(RwLock::new(HashMap::default()));
1793        let settings = LlamaCppSettings {
1794            auto_discover: true,
1795            ..Default::default()
1796        };
1797
1798        // Cold: the optimistic unloaded-context assumption.
1799        let cold = vec![llama_cpp::Model::new(
1800            "m",
1801            Some("m"),
1802            Some(ASSUMED_UNLOADED_CONTEXT),
1803            true,
1804            false,
1805            false,
1806        )];
1807        sync_capability_cells(&cells, &compute_effective_models(&cold, &settings));
1808        assert_eq!(
1809            cells.read().unwrap().get("m").unwrap().max_tokens,
1810            ASSUMED_UNLOADED_CONTEXT
1811        );
1812
1813        // The model loads and reports its real context. The shared map must
1814        // reflect the new value so a model reading it by name (an open
1815        // conversation) is no longer stuck on the cold-start assumption.
1816        let loaded = vec![llama_cpp::Model::new(
1817            "m",
1818            Some("m"),
1819            Some(262_144),
1820            true,
1821            false,
1822            true,
1823        )];
1824        sync_capability_cells(&cells, &compute_effective_models(&loaded, &settings));
1825        assert_eq!(cells.read().unwrap().get("m").unwrap().max_tokens, 262_144);
1826        assert!(cells.read().unwrap().get("m").unwrap().supports_thinking);
1827    }
1828
1829    #[test]
1830    fn request_preserves_assistant_thinking_when_supported() {
1831        let request = build_llama_cpp_request(
1832            "test-model",
1833            false,
1834            LiveCapabilities {
1835                max_tokens: 8192,
1836                supports_tools: false,
1837                supports_thinking: true,
1838            },
1839            LanguageModelRequest {
1840                messages: vec![language_model::LanguageModelRequestMessage {
1841                    role: Role::Assistant,
1842                    content: vec![
1843                        MessageContent::Thinking {
1844                            text: "reasoning".to_string(),
1845                            signature: None,
1846                        },
1847                        MessageContent::Text("answer".to_string()),
1848                    ],
1849                    cache: false,
1850                    reasoning_details: None,
1851                }],
1852                ..Default::default()
1853            },
1854        )
1855        .unwrap();
1856
1857        assert_eq!(request.messages.len(), 1);
1858        match &request.messages[0] {
1859            llama_cpp::ChatMessage::Assistant {
1860                content: Some(llama_cpp::MessageContent::Plain(content)),
1861                reasoning_content: Some(reasoning_content),
1862                tool_calls,
1863            } => {
1864                assert_eq!(content, "answer");
1865                assert_eq!(reasoning_content, "reasoning");
1866                assert!(tool_calls.is_empty());
1867            }
1868            message => panic!("unexpected message: {message:?}"),
1869        }
1870    }
1871
1872    #[test]
1873    fn request_skips_assistant_thinking_when_unsupported() {
1874        let request = build_llama_cpp_request(
1875            "test-model",
1876            false,
1877            LiveCapabilities {
1878                max_tokens: 8192,
1879                supports_tools: false,
1880                supports_thinking: false,
1881            },
1882            LanguageModelRequest {
1883                messages: vec![language_model::LanguageModelRequestMessage {
1884                    role: Role::Assistant,
1885                    content: vec![
1886                        MessageContent::Thinking {
1887                            text: "reasoning".to_string(),
1888                            signature: None,
1889                        },
1890                        MessageContent::RedactedThinking("encrypted".to_string()),
1891                        MessageContent::Text("answer".to_string()),
1892                    ],
1893                    cache: false,
1894                    reasoning_details: None,
1895                }],
1896                ..Default::default()
1897            },
1898        )
1899        .unwrap();
1900
1901        assert_eq!(request.messages.len(), 1);
1902        match &request.messages[0] {
1903            llama_cpp::ChatMessage::Assistant {
1904                content: Some(llama_cpp::MessageContent::Plain(content)),
1905                reasoning_content,
1906                tool_calls,
1907            } => {
1908                assert_eq!(content, "answer");
1909                assert!(reasoning_content.is_none());
1910                assert!(tool_calls.is_empty());
1911            }
1912            message => panic!("unexpected message: {message:?}"),
1913        }
1914    }
1915
1916    #[test]
1917    fn request_preserves_thinking_for_assistant_tool_calls_when_supported() {
1918        let request = build_llama_cpp_request(
1919            "test-model",
1920            false,
1921            LiveCapabilities {
1922                max_tokens: 8192,
1923                supports_tools: true,
1924                supports_thinking: true,
1925            },
1926            LanguageModelRequest {
1927                messages: vec![language_model::LanguageModelRequestMessage {
1928                    role: Role::Assistant,
1929                    content: vec![
1930                        MessageContent::Thinking {
1931                            text: "reasoning".to_string(),
1932                            signature: None,
1933                        },
1934                        MessageContent::ToolUse(LanguageModelToolUse {
1935                            id: "call_1".into(),
1936                            name: "weather".into(),
1937                            raw_input: r#"{"city":"Oslo"}"#.to_string(),
1938                            input: language_model::LanguageModelToolUseInput::Json(
1939                                serde_json::json!({ "city": "Oslo" }),
1940                            ),
1941                            is_input_complete: true,
1942                            thought_signature: None,
1943                        }),
1944                    ],
1945                    cache: false,
1946                    reasoning_details: None,
1947                }],
1948                ..Default::default()
1949            },
1950        )
1951        .unwrap();
1952
1953        assert_eq!(request.messages.len(), 1);
1954        match &request.messages[0] {
1955            llama_cpp::ChatMessage::Assistant {
1956                content: None,
1957                reasoning_content: Some(reasoning_content),
1958                tool_calls,
1959            } => {
1960                assert_eq!(reasoning_content, "reasoning");
1961                assert_eq!(tool_calls.len(), 1);
1962            }
1963            message => panic!("unexpected message: {message:?}"),
1964        }
1965    }
1966
1967    #[test]
1968    fn usage_event_precedes_stop_event() {
1969        let mut mapper = LlamaCppEventMapper::new();
1970        let events = mapper.map_event(llama_cpp::ResponseStreamEvent {
1971            model: "test-model".to_string(),
1972            object: "chat.completion.chunk".to_string(),
1973            choices: vec![llama_cpp::ChoiceDelta {
1974                index: 0,
1975                delta: llama_cpp::ResponseMessageDelta {
1976                    content: None,
1977                    reasoning_content: None,
1978                    tool_calls: None,
1979                },
1980                finish_reason: Some("stop".to_string()),
1981            }],
1982            usage: Some(llama_cpp::Usage {
1983                prompt_tokens: 11,
1984                completion_tokens: 7,
1985                total_tokens: 18,
1986            }),
1987        });
1988
1989        assert!(matches!(
1990            events.as_slice(),
1991            [
1992                Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1993                    input_tokens: 11,
1994                    output_tokens: 7,
1995                    cache_creation_input_tokens: 0,
1996                    cache_read_input_tokens: 0,
1997                })),
1998                Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)),
1999            ]
2000        ));
2001    }
2002
2003    #[test]
2004    fn usage_event_precedes_tool_use_stop_event() {
2005        let mut mapper = LlamaCppEventMapper::new();
2006        let events = mapper.map_event(llama_cpp::ResponseStreamEvent {
2007            model: "test-model".to_string(),
2008            object: "chat.completion.chunk".to_string(),
2009            choices: vec![llama_cpp::ChoiceDelta {
2010                index: 0,
2011                delta: llama_cpp::ResponseMessageDelta {
2012                    content: None,
2013                    reasoning_content: None,
2014                    tool_calls: Some(vec![llama_cpp::ToolCallChunk {
2015                        index: 0,
2016                        id: Some("tool-call-id".to_string()),
2017                        function: Some(llama_cpp::FunctionChunk {
2018                            name: Some("test_tool".to_string()),
2019                            arguments: Some(r#"{"value":1}"#.to_string()),
2020                        }),
2021                    }]),
2022                },
2023                finish_reason: Some("tool_calls".to_string()),
2024            }],
2025            usage: Some(llama_cpp::Usage {
2026                prompt_tokens: 13,
2027                completion_tokens: 5,
2028                total_tokens: 18,
2029            }),
2030        });
2031
2032        assert!(matches!(
2033            events.as_slice(),
2034            [
2035                Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
2036                    input_tokens: 13,
2037                    output_tokens: 5,
2038                    cache_creation_input_tokens: 0,
2039                    cache_read_input_tokens: 0,
2040                })),
2041                Ok(LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
2042                    id,
2043                    name,
2044                    ..
2045                })),
2046                Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)),
2047            ] if id.to_string() == "tool-call-id" && name.as_ref() == "test_tool"
2048        ));
2049    }
2050
2051    #[gpui::test]
2052    async fn authenticate_fetches_models_after_loading_api_key(cx: &mut TestAppContext) {
2053        cx.update(|cx| {
2054            let settings_store = SettingsStore::test(cx);
2055            cx.set_global(settings_store);
2056        });
2057
2058        let model_request_authorizations = Arc::new(Mutex::new(Vec::new()));
2059        let model_request_count = Arc::new(AtomicUsize::new(0));
2060        let http_client = FakeHttpClient::create({
2061            let model_request_authorizations = model_request_authorizations.clone();
2062            let model_request_count = model_request_count.clone();
2063            move |request| {
2064                let model_request_authorizations = model_request_authorizations.clone();
2065                let model_request_count = model_request_count.clone();
2066                async move {
2067                    let path = request.uri().path();
2068                    let authorization = request
2069                        .headers()
2070                        .get("Authorization")
2071                        .and_then(|value| value.to_str().ok())
2072                        .map(str::to_string);
2073
2074                    if path == "/v1/models" {
2075                        model_request_authorizations.lock().push(authorization);
2076                        let request_index = model_request_count.fetch_add(1, Ordering::SeqCst);
2077                        if request_index == 0 {
2078                            return Ok(http_client::Response::builder()
2079                                .status(503)
2080                                .body(http_client::AsyncBody::from("not ready"))?);
2081                        }
2082
2083                        return Ok(http_client::Response::builder().status(200).body(
2084                            http_client::AsyncBody::from(
2085                                r#"{"data":[{"id":"test-model","meta":{"n_ctx":4096}}]}"#,
2086                            ),
2087                        )?);
2088                    }
2089
2090                    if path == "/props" {
2091                        return Ok(http_client::Response::builder()
2092                            .status(200)
2093                            .body(http_client::AsyncBody::from("{}"))?);
2094                    }
2095
2096                    Ok(http_client::Response::builder()
2097                        .status(404)
2098                        .body(http_client::AsyncBody::default())?)
2099                }
2100            }
2101        });
2102        let credentials_provider = Arc::new(FakeCredentialsProvider {
2103            api_key: b"loaded-key".to_vec(),
2104        });
2105        let provider = cx
2106            .update(|cx| LlamaCppLanguageModelProvider::new(http_client, credentials_provider, cx));
2107
2108        cx.run_until_parked();
2109
2110        let result = cx.update(|cx| provider.authenticate(cx)).await;
2111        assert!(
2112            result.is_ok(),
2113            "authenticate should discover models after loading credentials"
2114        );
2115        assert_eq!(
2116            &*model_request_authorizations.lock(),
2117            &[None, Some("Bearer loaded-key".to_string())]
2118        );
2119    }
2120}
2121
Served at tenant.openagents/omega Member data and write actions are omitted.