vercel.rs

  1use anyhow::{Context as _, Result, anyhow};
  2use collections::BTreeMap;
  3use credentials_provider::CredentialsProvider;
  4use futures::{FutureExt, StreamExt, future::BoxFuture};
  5use gpui::{AnyView, App, AsyncApp, Context, Entity, Subscription, Task, Window};
  6use http_client::HttpClient;
  7use language_model::{
  8    AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
  9    LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
 10    LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
 11    LanguageModelToolChoice, RateLimiter, Role,
 12};
 13use menu;
 14use open_ai::ResponseStreamEvent;
 15use settings::{Settings, SettingsStore};
 16use std::sync::Arc;
 17use strum::IntoEnumIterator;
 18use vercel::Model;
 19
 20pub use settings::VercelAvailableModel as AvailableModel;
 21use ui::{ElevationIndex, List, Tooltip, prelude::*};
 22use ui_input::SingleLineInput;
 23use util::ResultExt;
 24
 25use crate::{AllLanguageModelSettings, ui::InstructionListItem};
 26
 27const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel");
 28const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel");
 29
 30// todo!() -> Remove default implementation
 31#[derive(Default, Clone, Debug, PartialEq)]
 32pub struct VercelSettings {
 33    pub api_url: String,
 34    pub available_models: Vec<AvailableModel>,
 35}
 36
 37pub struct VercelLanguageModelProvider {
 38    http_client: Arc<dyn HttpClient>,
 39    state: gpui::Entity<State>,
 40}
 41
 42pub struct State {
 43    api_key: Option<String>,
 44    api_key_from_env: bool,
 45    _subscription: Subscription,
 46}
 47
 48const VERCEL_API_KEY_VAR: &str = "VERCEL_API_KEY";
 49
 50impl State {
 51    fn is_authenticated(&self) -> bool {
 52        self.api_key.is_some()
 53    }
 54
 55    fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
 56        let credentials_provider = <dyn CredentialsProvider>::global(cx);
 57        let settings = &AllLanguageModelSettings::get_global(cx).vercel;
 58        let api_url = if settings.api_url.is_empty() {
 59            vercel::VERCEL_API_URL.to_string()
 60        } else {
 61            settings.api_url.clone()
 62        };
 63        cx.spawn(async move |this, cx| {
 64            credentials_provider
 65                .delete_credentials(&api_url, cx)
 66                .await
 67                .log_err();
 68            this.update(cx, |this, cx| {
 69                this.api_key = None;
 70                this.api_key_from_env = false;
 71                cx.notify();
 72            })
 73        })
 74    }
 75
 76    fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
 77        let credentials_provider = <dyn CredentialsProvider>::global(cx);
 78        let settings = &AllLanguageModelSettings::get_global(cx).vercel;
 79        let api_url = if settings.api_url.is_empty() {
 80            vercel::VERCEL_API_URL.to_string()
 81        } else {
 82            settings.api_url.clone()
 83        };
 84        cx.spawn(async move |this, cx| {
 85            credentials_provider
 86                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
 87                .await
 88                .log_err();
 89            this.update(cx, |this, cx| {
 90                this.api_key = Some(api_key);
 91                cx.notify();
 92            })
 93        })
 94    }
 95
 96    fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
 97        if self.is_authenticated() {
 98            return Task::ready(Ok(()));
 99        }
100
101        let credentials_provider = <dyn CredentialsProvider>::global(cx);
102        let settings = &AllLanguageModelSettings::get_global(cx).vercel;
103        let api_url = if settings.api_url.is_empty() {
104            vercel::VERCEL_API_URL.to_string()
105        } else {
106            settings.api_url.clone()
107        };
108        cx.spawn(async move |this, cx| {
109            let (api_key, from_env) = if let Ok(api_key) = std::env::var(VERCEL_API_KEY_VAR) {
110                (api_key, true)
111            } else {
112                let (_, api_key) = credentials_provider
113                    .read_credentials(&api_url, cx)
114                    .await?
115                    .ok_or(AuthenticateError::CredentialsNotFound)?;
116                (
117                    String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
118                    false,
119                )
120            };
121            this.update(cx, |this, cx| {
122                this.api_key = Some(api_key);
123                this.api_key_from_env = from_env;
124                cx.notify();
125            })?;
126
127            Ok(())
128        })
129    }
130}
131
132impl VercelLanguageModelProvider {
133    pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
134        let state = cx.new(|cx| State {
135            api_key: None,
136            api_key_from_env: false,
137            _subscription: cx.observe_global::<SettingsStore>(|_this: &mut State, cx| {
138                cx.notify();
139            }),
140        });
141
142        Self { http_client, state }
143    }
144
145    fn create_language_model(&self, model: vercel::Model) -> Arc<dyn LanguageModel> {
146        Arc::new(VercelLanguageModel {
147            id: LanguageModelId::from(model.id().to_string()),
148            model,
149            state: self.state.clone(),
150            http_client: self.http_client.clone(),
151            request_limiter: RateLimiter::new(4),
152        })
153    }
154}
155
156impl LanguageModelProviderState for VercelLanguageModelProvider {
157    type ObservableEntity = State;
158
159    fn observable_entity(&self) -> Option<gpui::Entity<Self::ObservableEntity>> {
160        Some(self.state.clone())
161    }
162}
163
164impl LanguageModelProvider for VercelLanguageModelProvider {
165    fn id(&self) -> LanguageModelProviderId {
166        PROVIDER_ID
167    }
168
169    fn name(&self) -> LanguageModelProviderName {
170        PROVIDER_NAME
171    }
172
173    fn icon(&self) -> IconName {
174        IconName::AiVZero
175    }
176
177    fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
178        Some(self.create_language_model(vercel::Model::default()))
179    }
180
181    fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
182        Some(self.create_language_model(vercel::Model::default_fast()))
183    }
184
185    fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
186        let mut models = BTreeMap::default();
187
188        for model in vercel::Model::iter() {
189            if !matches!(model, vercel::Model::Custom { .. }) {
190                models.insert(model.id().to_string(), model);
191            }
192        }
193
194        for model in &AllLanguageModelSettings::get_global(cx)
195            .vercel
196            .available_models
197        {
198            models.insert(
199                model.name.clone(),
200                vercel::Model::Custom {
201                    name: model.name.clone(),
202                    display_name: model.display_name.clone(),
203                    max_tokens: model.max_tokens,
204                    max_output_tokens: model.max_output_tokens,
205                    max_completion_tokens: model.max_completion_tokens,
206                },
207            );
208        }
209
210        models
211            .into_values()
212            .map(|model| self.create_language_model(model))
213            .collect()
214    }
215
216    fn is_authenticated(&self, cx: &App) -> bool {
217        self.state.read(cx).is_authenticated()
218    }
219
220    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
221        self.state.update(cx, |state, cx| state.authenticate(cx))
222    }
223
224    fn configuration_view(
225        &self,
226        _target_agent: language_model::ConfigurationViewTargetAgent,
227        window: &mut Window,
228        cx: &mut App,
229    ) -> AnyView {
230        cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
231            .into()
232    }
233
234    fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
235        self.state.update(cx, |state, cx| state.reset_api_key(cx))
236    }
237}
238
239pub struct VercelLanguageModel {
240    id: LanguageModelId,
241    model: vercel::Model,
242    state: gpui::Entity<State>,
243    http_client: Arc<dyn HttpClient>,
244    request_limiter: RateLimiter,
245}
246
247impl VercelLanguageModel {
248    fn stream_completion(
249        &self,
250        request: open_ai::Request,
251        cx: &AsyncApp,
252    ) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<ResponseStreamEvent>>>>
253    {
254        let http_client = self.http_client.clone();
255        let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
256            let settings = &AllLanguageModelSettings::get_global(cx).vercel;
257            let api_url = if settings.api_url.is_empty() {
258                vercel::VERCEL_API_URL.to_string()
259            } else {
260                settings.api_url.clone()
261            };
262            (state.api_key.clone(), api_url)
263        }) else {
264            return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
265        };
266
267        let future = self.request_limiter.stream(async move {
268            let Some(api_key) = api_key else {
269                return Err(LanguageModelCompletionError::NoApiKey {
270                    provider: PROVIDER_NAME,
271                });
272            };
273            let request =
274                open_ai::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
275            let response = request.await?;
276            Ok(response)
277        });
278
279        async move { Ok(future.await?.boxed()) }.boxed()
280    }
281}
282
283impl LanguageModel for VercelLanguageModel {
284    fn id(&self) -> LanguageModelId {
285        self.id.clone()
286    }
287
288    fn name(&self) -> LanguageModelName {
289        LanguageModelName::from(self.model.display_name().to_string())
290    }
291
292    fn provider_id(&self) -> LanguageModelProviderId {
293        PROVIDER_ID
294    }
295
296    fn provider_name(&self) -> LanguageModelProviderName {
297        PROVIDER_NAME
298    }
299
300    fn supports_tools(&self) -> bool {
301        true
302    }
303
304    fn supports_images(&self) -> bool {
305        true
306    }
307
308    fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
309        match choice {
310            LanguageModelToolChoice::Auto
311            | LanguageModelToolChoice::Any
312            | LanguageModelToolChoice::None => true,
313        }
314    }
315
316    fn telemetry_id(&self) -> String {
317        format!("vercel/{}", self.model.id())
318    }
319
320    fn max_token_count(&self) -> u64 {
321        self.model.max_token_count()
322    }
323
324    fn max_output_tokens(&self) -> Option<u64> {
325        self.model.max_output_tokens()
326    }
327
328    fn count_tokens(
329        &self,
330        request: LanguageModelRequest,
331        cx: &App,
332    ) -> BoxFuture<'static, Result<u64>> {
333        count_vercel_tokens(request, self.model.clone(), cx)
334    }
335
336    fn stream_completion(
337        &self,
338        request: LanguageModelRequest,
339        cx: &AsyncApp,
340    ) -> BoxFuture<
341        'static,
342        Result<
343            futures::stream::BoxStream<
344                'static,
345                Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
346            >,
347            LanguageModelCompletionError,
348        >,
349    > {
350        let request = crate::provider::open_ai::into_open_ai(
351            request,
352            self.model.id(),
353            self.model.supports_parallel_tool_calls(),
354            self.model.supports_prompt_cache_key(),
355            self.max_output_tokens(),
356            None,
357        );
358        let completions = self.stream_completion(request, cx);
359        async move {
360            let mapper = crate::provider::open_ai::OpenAiEventMapper::new();
361            Ok(mapper.map_stream(completions.await?).boxed())
362        }
363        .boxed()
364    }
365}
366
367pub fn count_vercel_tokens(
368    request: LanguageModelRequest,
369    model: Model,
370    cx: &App,
371) -> BoxFuture<'static, Result<u64>> {
372    cx.background_spawn(async move {
373        let messages = request
374            .messages
375            .into_iter()
376            .map(|message| tiktoken_rs::ChatCompletionRequestMessage {
377                role: match message.role {
378                    Role::User => "user".into(),
379                    Role::Assistant => "assistant".into(),
380                    Role::System => "system".into(),
381                },
382                content: Some(message.string_contents()),
383                name: None,
384                function_call: None,
385            })
386            .collect::<Vec<_>>();
387
388        match model {
389            Model::Custom { max_tokens, .. } => {
390                let model = if max_tokens >= 100_000 {
391                    // If the max tokens is 100k or more, it is likely the o200k_base tokenizer from gpt4o
392                    "gpt-4o"
393                } else {
394                    // Otherwise fallback to gpt-4, since only cl100k_base and o200k_base are
395                    // supported with this tiktoken method
396                    "gpt-4"
397                };
398                tiktoken_rs::num_tokens_from_messages(model, &messages)
399            }
400            // Map Vercel models to appropriate OpenAI models for token counting
401            // since Vercel uses OpenAI-compatible API
402            Model::VZeroOnePointFiveMedium => {
403                // Vercel v0 is similar to GPT-4o, so use gpt-4o for token counting
404                tiktoken_rs::num_tokens_from_messages("gpt-4o", &messages)
405            }
406        }
407        .map(|tokens| tokens as u64)
408    })
409    .boxed()
410}
411
412struct ConfigurationView {
413    api_key_editor: Entity<SingleLineInput>,
414    state: gpui::Entity<State>,
415    load_credentials_task: Option<Task<()>>,
416}
417
418impl ConfigurationView {
419    fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
420        let api_key_editor = cx.new(|cx| {
421            SingleLineInput::new(
422                window,
423                cx,
424                "v1:0000000000000000000000000000000000000000000000000",
425            )
426            .label("API key")
427        });
428
429        cx.observe(&state, |_, _, cx| {
430            cx.notify();
431        })
432        .detach();
433
434        let load_credentials_task = Some(cx.spawn_in(window, {
435            let state = state.clone();
436            async move |this, cx| {
437                if let Some(task) = state
438                    .update(cx, |state, cx| state.authenticate(cx))
439                    .log_err()
440                {
441                    // We don't log an error, because "not signed in" is also an error.
442                    let _ = task.await;
443                }
444                this.update(cx, |this, cx| {
445                    this.load_credentials_task = None;
446                    cx.notify();
447                })
448                .log_err();
449            }
450        }));
451
452        Self {
453            api_key_editor,
454            state,
455            load_credentials_task,
456        }
457    }
458
459    fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
460        let api_key = self
461            .api_key_editor
462            .read(cx)
463            .editor()
464            .read(cx)
465            .text(cx)
466            .trim()
467            .to_string();
468
469        // Don't proceed if no API key is provided and we're not authenticated
470        if api_key.is_empty() && !self.state.read(cx).is_authenticated() {
471            return;
472        }
473
474        let state = self.state.clone();
475        cx.spawn_in(window, async move |_, cx| {
476            state
477                .update(cx, |state, cx| state.set_api_key(api_key, cx))?
478                .await
479        })
480        .detach_and_log_err(cx);
481
482        cx.notify();
483    }
484
485    fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
486        self.api_key_editor.update(cx, |input, cx| {
487            input.editor.update(cx, |editor, cx| {
488                editor.set_text("", window, cx);
489            });
490        });
491
492        let state = self.state.clone();
493        cx.spawn_in(window, async move |_, cx| {
494            state.update(cx, |state, cx| state.reset_api_key(cx))?.await
495        })
496        .detach_and_log_err(cx);
497
498        cx.notify();
499    }
500
501    fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
502        !self.state.read(cx).is_authenticated()
503    }
504}
505
506impl Render for ConfigurationView {
507    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
508        let env_var_set = self.state.read(cx).api_key_from_env;
509
510        let api_key_section = if self.should_render_editor(cx) {
511            v_flex()
512                .on_action(cx.listener(Self::save_api_key))
513                .child(Label::new("To use Zed's agent with Vercel v0, you need to add an API key. Follow these steps:"))
514                .child(
515                    List::new()
516                        .child(InstructionListItem::new(
517                            "Create one by visiting",
518                            Some("Vercel v0's console"),
519                            Some("https://v0.dev/chat/settings/keys"),
520                        ))
521                        .child(InstructionListItem::text_only(
522                            "Paste your API key below and hit enter to start using the agent",
523                        )),
524                )
525                .child(self.api_key_editor.clone())
526                .child(
527                    Label::new(format!(
528                        "You can also assign the {VERCEL_API_KEY_VAR} environment variable and restart Zed."
529                    ))
530                    .size(LabelSize::Small)
531                    .color(Color::Muted),
532                )
533                .child(
534                    Label::new("Note that Vercel v0 is a custom OpenAI-compatible provider.")
535                        .size(LabelSize::Small)
536                        .color(Color::Muted),
537                )
538                .into_any()
539        } else {
540            h_flex()
541                .mt_1()
542                .p_1()
543                .justify_between()
544                .rounded_md()
545                .border_1()
546                .border_color(cx.theme().colors().border)
547                .bg(cx.theme().colors().background)
548                .child(
549                    h_flex()
550                        .gap_1()
551                        .child(Icon::new(IconName::Check).color(Color::Success))
552                        .child(Label::new(if env_var_set {
553                            format!("API key set in {VERCEL_API_KEY_VAR} environment variable.")
554                        } else {
555                            "API key configured.".to_string()
556                        })),
557                )
558                .child(
559                    Button::new("reset-api-key", "Reset API Key")
560                        .label_size(LabelSize::Small)
561                        .icon(IconName::Undo)
562                        .icon_size(IconSize::Small)
563                        .icon_position(IconPosition::Start)
564                        .layer(ElevationIndex::ModalSurface)
565                        .when(env_var_set, |this| {
566                            this.tooltip(Tooltip::text(format!("To reset your API key, unset the {VERCEL_API_KEY_VAR} environment variable.")))
567                        })
568                        .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
569                )
570                .into_any()
571        };
572
573        if self.load_credentials_task.is_some() {
574            div().child(Label::new("Loading credentials…")).into_any()
575        } else {
576            v_flex().size_full().child(api_key_section).into_any()
577        }
578    }
579}