anthropic.rs

  1use crate::ui::InstructionListItem;
  2use crate::AllLanguageModelSettings;
  3use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent};
  4use anyhow::{anyhow, Context as _, Result};
  5use collections::{BTreeMap, HashMap};
  6use credentials_provider::CredentialsProvider;
  7use editor::{Editor, EditorElement, EditorStyle};
  8use futures::Stream;
  9use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryStreamExt as _};
 10use gpui::{
 11    AnyView, App, AsyncApp, Context, Entity, FontStyle, Subscription, Task, TextStyle, WhiteSpace,
 12};
 13use http_client::HttpClient;
 14use language_model::{
 15    AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId,
 16    LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
 17    LanguageModelProviderState, LanguageModelRequest, MessageContent, RateLimiter, Role,
 18};
 19use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
 20use schemars::JsonSchema;
 21use serde::{Deserialize, Serialize};
 22use settings::{Settings, SettingsStore};
 23use std::pin::Pin;
 24use std::str::FromStr;
 25use std::sync::Arc;
 26use strum::IntoEnumIterator;
 27use theme::ThemeSettings;
 28use ui::{prelude::*, Icon, IconName, List, Tooltip};
 29use util::{maybe, ResultExt};
 30
 31const PROVIDER_ID: &str = language_model::ANTHROPIC_PROVIDER_ID;
 32const PROVIDER_NAME: &str = "Anthropic";
 33
 34#[derive(Default, Clone, Debug, PartialEq)]
 35pub struct AnthropicSettings {
 36    pub api_url: String,
 37    /// Extend Zed's list of Anthropic models.
 38    pub available_models: Vec<AvailableModel>,
 39    pub needs_setting_migration: bool,
 40}
 41
 42#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
 43pub struct AvailableModel {
 44    /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc
 45    pub name: String,
 46    /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel.
 47    pub display_name: Option<String>,
 48    /// The model's context window size.
 49    pub max_tokens: usize,
 50    /// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling.
 51    pub tool_override: Option<String>,
 52    /// Configuration of Anthropic's caching API.
 53    pub cache_configuration: Option<LanguageModelCacheConfiguration>,
 54    pub max_output_tokens: Option<u32>,
 55    pub default_temperature: Option<f32>,
 56    #[serde(default)]
 57    pub extra_beta_headers: Vec<String>,
 58}
 59
 60pub struct AnthropicLanguageModelProvider {
 61    http_client: Arc<dyn HttpClient>,
 62    state: gpui::Entity<State>,
 63}
 64
 65const ANTHROPIC_API_KEY_VAR: &str = "ANTHROPIC_API_KEY";
 66
 67pub struct State {
 68    api_key: Option<String>,
 69    api_key_from_env: bool,
 70    _subscription: Subscription,
 71}
 72
 73impl State {
 74    fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
 75        let credentials_provider = <dyn CredentialsProvider>::global(cx);
 76        let api_url = AllLanguageModelSettings::get_global(cx)
 77            .anthropic
 78            .api_url
 79            .clone();
 80        cx.spawn(|this, mut cx| async move {
 81            credentials_provider
 82                .delete_credentials(&api_url, &cx)
 83                .await
 84                .ok();
 85            this.update(&mut cx, |this, cx| {
 86                this.api_key = None;
 87                this.api_key_from_env = false;
 88                cx.notify();
 89            })
 90        })
 91    }
 92
 93    fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
 94        let credentials_provider = <dyn CredentialsProvider>::global(cx);
 95        let api_url = AllLanguageModelSettings::get_global(cx)
 96            .anthropic
 97            .api_url
 98            .clone();
 99        cx.spawn(|this, mut cx| async move {
100            credentials_provider
101                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
102                .await
103                .ok();
104
105            this.update(&mut cx, |this, cx| {
106                this.api_key = Some(api_key);
107                cx.notify();
108            })
109        })
110    }
111
112    fn is_authenticated(&self) -> bool {
113        self.api_key.is_some()
114    }
115
116    fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
117        if self.is_authenticated() {
118            return Task::ready(Ok(()));
119        }
120
121        let credentials_provider = <dyn CredentialsProvider>::global(cx);
122        let api_url = AllLanguageModelSettings::get_global(cx)
123            .anthropic
124            .api_url
125            .clone();
126
127        cx.spawn(|this, mut cx| async move {
128            let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR) {
129                (api_key, true)
130            } else {
131                let (_, api_key) = credentials_provider
132                    .read_credentials(&api_url, &cx)
133                    .await?
134                    .ok_or(AuthenticateError::CredentialsNotFound)?;
135                (
136                    String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
137                    false,
138                )
139            };
140
141            this.update(&mut cx, |this, cx| {
142                this.api_key = Some(api_key);
143                this.api_key_from_env = from_env;
144                cx.notify();
145            })?;
146
147            Ok(())
148        })
149    }
150}
151
152impl AnthropicLanguageModelProvider {
153    pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
154        let state = cx.new(|cx| State {
155            api_key: None,
156            api_key_from_env: false,
157            _subscription: cx.observe_global::<SettingsStore>(|_, cx| {
158                cx.notify();
159            }),
160        });
161
162        Self { http_client, state }
163    }
164}
165
166impl LanguageModelProviderState for AnthropicLanguageModelProvider {
167    type ObservableEntity = State;
168
169    fn observable_entity(&self) -> Option<gpui::Entity<Self::ObservableEntity>> {
170        Some(self.state.clone())
171    }
172}
173
174impl LanguageModelProvider for AnthropicLanguageModelProvider {
175    fn id(&self) -> LanguageModelProviderId {
176        LanguageModelProviderId(PROVIDER_ID.into())
177    }
178
179    fn name(&self) -> LanguageModelProviderName {
180        LanguageModelProviderName(PROVIDER_NAME.into())
181    }
182
183    fn icon(&self) -> IconName {
184        IconName::AiAnthropic
185    }
186
187    fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
188        let model = anthropic::Model::default();
189        Some(Arc::new(AnthropicModel {
190            id: LanguageModelId::from(model.id().to_string()),
191            model,
192            state: self.state.clone(),
193            http_client: self.http_client.clone(),
194            request_limiter: RateLimiter::new(4),
195        }))
196    }
197
198    fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
199        let mut models = BTreeMap::default();
200
201        // Add base models from anthropic::Model::iter()
202        for model in anthropic::Model::iter() {
203            if !matches!(model, anthropic::Model::Custom { .. }) {
204                models.insert(model.id().to_string(), model);
205            }
206        }
207
208        // Override with available models from settings
209        for model in AllLanguageModelSettings::get_global(cx)
210            .anthropic
211            .available_models
212            .iter()
213        {
214            models.insert(
215                model.name.clone(),
216                anthropic::Model::Custom {
217                    name: model.name.clone(),
218                    display_name: model.display_name.clone(),
219                    max_tokens: model.max_tokens,
220                    tool_override: model.tool_override.clone(),
221                    cache_configuration: model.cache_configuration.as_ref().map(|config| {
222                        anthropic::AnthropicModelCacheConfiguration {
223                            max_cache_anchors: config.max_cache_anchors,
224                            should_speculate: config.should_speculate,
225                            min_total_token: config.min_total_token,
226                        }
227                    }),
228                    max_output_tokens: model.max_output_tokens,
229                    default_temperature: model.default_temperature,
230                    extra_beta_headers: model.extra_beta_headers.clone(),
231                },
232            );
233        }
234
235        models
236            .into_values()
237            .map(|model| {
238                Arc::new(AnthropicModel {
239                    id: LanguageModelId::from(model.id().to_string()),
240                    model,
241                    state: self.state.clone(),
242                    http_client: self.http_client.clone(),
243                    request_limiter: RateLimiter::new(4),
244                }) as Arc<dyn LanguageModel>
245            })
246            .collect()
247    }
248
249    fn is_authenticated(&self, cx: &App) -> bool {
250        self.state.read(cx).is_authenticated()
251    }
252
253    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
254        self.state.update(cx, |state, cx| state.authenticate(cx))
255    }
256
257    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
258        cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
259            .into()
260    }
261
262    fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
263        self.state.update(cx, |state, cx| state.reset_api_key(cx))
264    }
265}
266
267pub struct AnthropicModel {
268    id: LanguageModelId,
269    model: anthropic::Model,
270    state: gpui::Entity<State>,
271    http_client: Arc<dyn HttpClient>,
272    request_limiter: RateLimiter,
273}
274
275pub fn count_anthropic_tokens(
276    request: LanguageModelRequest,
277    cx: &App,
278) -> BoxFuture<'static, Result<usize>> {
279    cx.background_spawn(async move {
280        let messages = request.messages;
281        let mut tokens_from_images = 0;
282        let mut string_messages = Vec::with_capacity(messages.len());
283
284        for message in messages {
285            use language_model::MessageContent;
286
287            let mut string_contents = String::new();
288
289            for content in message.content {
290                match content {
291                    MessageContent::Text(text) => {
292                        string_contents.push_str(&text);
293                    }
294                    MessageContent::Image(image) => {
295                        tokens_from_images += image.estimate_tokens();
296                    }
297                    MessageContent::ToolUse(_tool_use) => {
298                        // TODO: Estimate token usage from tool uses.
299                    }
300                    MessageContent::ToolResult(tool_result) => {
301                        string_contents.push_str(&tool_result.content);
302                    }
303                }
304            }
305
306            if !string_contents.is_empty() {
307                string_messages.push(tiktoken_rs::ChatCompletionRequestMessage {
308                    role: match message.role {
309                        Role::User => "user".into(),
310                        Role::Assistant => "assistant".into(),
311                        Role::System => "system".into(),
312                    },
313                    content: Some(string_contents),
314                    name: None,
315                    function_call: None,
316                });
317            }
318        }
319
320        // Tiktoken doesn't yet support these models, so we manually use the
321        // same tokenizer as GPT-4.
322        tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages)
323            .map(|tokens| tokens + tokens_from_images)
324    })
325    .boxed()
326}
327
328impl AnthropicModel {
329    fn stream_completion(
330        &self,
331        request: anthropic::Request,
332        cx: &AsyncApp,
333    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<anthropic::Event, AnthropicError>>>>
334    {
335        let http_client = self.http_client.clone();
336
337        let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
338            let settings = &AllLanguageModelSettings::get_global(cx).anthropic;
339            (state.api_key.clone(), settings.api_url.clone())
340        }) else {
341            return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
342        };
343
344        async move {
345            let api_key = api_key.ok_or_else(|| anyhow!("Missing Anthropic API Key"))?;
346            let request =
347                anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
348            request.await.context("failed to stream completion")
349        }
350        .boxed()
351    }
352}
353
354impl LanguageModel for AnthropicModel {
355    fn id(&self) -> LanguageModelId {
356        self.id.clone()
357    }
358
359    fn name(&self) -> LanguageModelName {
360        LanguageModelName::from(self.model.display_name().to_string())
361    }
362
363    fn provider_id(&self) -> LanguageModelProviderId {
364        LanguageModelProviderId(PROVIDER_ID.into())
365    }
366
367    fn provider_name(&self) -> LanguageModelProviderName {
368        LanguageModelProviderName(PROVIDER_NAME.into())
369    }
370
371    fn telemetry_id(&self) -> String {
372        format!("anthropic/{}", self.model.id())
373    }
374
375    fn api_key(&self, cx: &App) -> Option<String> {
376        self.state.read(cx).api_key.clone()
377    }
378
379    fn max_token_count(&self) -> usize {
380        self.model.max_token_count()
381    }
382
383    fn max_output_tokens(&self) -> Option<u32> {
384        Some(self.model.max_output_tokens())
385    }
386
387    fn count_tokens(
388        &self,
389        request: LanguageModelRequest,
390        cx: &App,
391    ) -> BoxFuture<'static, Result<usize>> {
392        count_anthropic_tokens(request, cx)
393    }
394
395    fn stream_completion(
396        &self,
397        request: LanguageModelRequest,
398        cx: &AsyncApp,
399    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
400        let request = into_anthropic(
401            request,
402            self.model.id().into(),
403            self.model.default_temperature(),
404            self.model.max_output_tokens(),
405        );
406        let request = self.stream_completion(request, cx);
407        let future = self.request_limiter.stream(async move {
408            let response = request.await.map_err(|err| anyhow!(err))?;
409            Ok(map_to_language_model_completion_events(response))
410        });
411        async move { Ok(future.await?.boxed()) }.boxed()
412    }
413
414    fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
415        self.model
416            .cache_configuration()
417            .map(|config| LanguageModelCacheConfiguration {
418                max_cache_anchors: config.max_cache_anchors,
419                should_speculate: config.should_speculate,
420                min_total_token: config.min_total_token,
421            })
422    }
423
424    fn use_any_tool(
425        &self,
426        request: LanguageModelRequest,
427        tool_name: String,
428        tool_description: String,
429        input_schema: serde_json::Value,
430        cx: &AsyncApp,
431    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
432        let mut request = into_anthropic(
433            request,
434            self.model.tool_model_id().into(),
435            self.model.default_temperature(),
436            self.model.max_output_tokens(),
437        );
438        request.tool_choice = Some(anthropic::ToolChoice::Tool {
439            name: tool_name.clone(),
440        });
441        request.tools = vec![anthropic::Tool {
442            name: tool_name.clone(),
443            description: tool_description,
444            input_schema,
445        }];
446
447        let response = self.stream_completion(request, cx);
448        self.request_limiter
449            .run(async move {
450                let response = response.await?;
451                Ok(anthropic::extract_tool_args_from_events(
452                    tool_name,
453                    Box::pin(response.map_err(|e| anyhow!(e))),
454                )
455                .await?
456                .boxed())
457            })
458            .boxed()
459    }
460}
461
462pub fn into_anthropic(
463    request: LanguageModelRequest,
464    model: String,
465    default_temperature: f32,
466    max_output_tokens: u32,
467) -> anthropic::Request {
468    let mut new_messages: Vec<anthropic::Message> = Vec::new();
469    let mut system_message = String::new();
470
471    for message in request.messages {
472        if message.contents_empty() {
473            continue;
474        }
475
476        match message.role {
477            Role::User | Role::Assistant => {
478                let cache_control = if message.cache {
479                    Some(anthropic::CacheControl {
480                        cache_type: anthropic::CacheControlType::Ephemeral,
481                    })
482                } else {
483                    None
484                };
485                let anthropic_message_content: Vec<anthropic::RequestContent> = message
486                    .content
487                    .into_iter()
488                    .filter_map(|content| match content {
489                        MessageContent::Text(text) => {
490                            if !text.is_empty() {
491                                Some(anthropic::RequestContent::Text {
492                                    text,
493                                    cache_control,
494                                })
495                            } else {
496                                None
497                            }
498                        }
499                        MessageContent::Image(image) => Some(anthropic::RequestContent::Image {
500                            source: anthropic::ImageSource {
501                                source_type: "base64".to_string(),
502                                media_type: "image/png".to_string(),
503                                data: image.source.to_string(),
504                            },
505                            cache_control,
506                        }),
507                        MessageContent::ToolUse(tool_use) => {
508                            Some(anthropic::RequestContent::ToolUse {
509                                id: tool_use.id.to_string(),
510                                name: tool_use.name.to_string(),
511                                input: tool_use.input,
512                                cache_control,
513                            })
514                        }
515                        MessageContent::ToolResult(tool_result) => {
516                            Some(anthropic::RequestContent::ToolResult {
517                                tool_use_id: tool_result.tool_use_id.to_string(),
518                                is_error: tool_result.is_error,
519                                content: tool_result.content.to_string(),
520                                cache_control,
521                            })
522                        }
523                    })
524                    .collect();
525                let anthropic_role = match message.role {
526                    Role::User => anthropic::Role::User,
527                    Role::Assistant => anthropic::Role::Assistant,
528                    Role::System => unreachable!("System role should never occur here"),
529                };
530                if let Some(last_message) = new_messages.last_mut() {
531                    if last_message.role == anthropic_role {
532                        last_message.content.extend(anthropic_message_content);
533                        continue;
534                    }
535                }
536                new_messages.push(anthropic::Message {
537                    role: anthropic_role,
538                    content: anthropic_message_content,
539                });
540            }
541            Role::System => {
542                if !system_message.is_empty() {
543                    system_message.push_str("\n\n");
544                }
545                system_message.push_str(&message.string_contents());
546            }
547        }
548    }
549
550    anthropic::Request {
551        model,
552        messages: new_messages,
553        max_tokens: max_output_tokens,
554        system: Some(system_message),
555        tools: request
556            .tools
557            .into_iter()
558            .map(|tool| anthropic::Tool {
559                name: tool.name,
560                description: tool.description,
561                input_schema: tool.input_schema,
562            })
563            .collect(),
564        tool_choice: None,
565        metadata: None,
566        stop_sequences: Vec::new(),
567        temperature: request.temperature.or(Some(default_temperature)),
568        top_k: None,
569        top_p: None,
570    }
571}
572
573pub fn map_to_language_model_completion_events(
574    events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
575) -> impl Stream<Item = Result<LanguageModelCompletionEvent>> {
576    struct RawToolUse {
577        id: String,
578        name: String,
579        input_json: String,
580    }
581
582    struct State {
583        events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
584        tool_uses_by_index: HashMap<usize, RawToolUse>,
585    }
586
587    futures::stream::unfold(
588        State {
589            events,
590            tool_uses_by_index: HashMap::default(),
591        },
592        |mut state| async move {
593            while let Some(event) = state.events.next().await {
594                match event {
595                    Ok(event) => match event {
596                        Event::ContentBlockStart {
597                            index,
598                            content_block,
599                        } => match content_block {
600                            ResponseContent::Text { text } => {
601                                return Some((
602                                    Some(Ok(LanguageModelCompletionEvent::Text(text))),
603                                    state,
604                                ));
605                            }
606                            ResponseContent::ToolUse { id, name, .. } => {
607                                state.tool_uses_by_index.insert(
608                                    index,
609                                    RawToolUse {
610                                        id,
611                                        name,
612                                        input_json: String::new(),
613                                    },
614                                );
615
616                                return Some((None, state));
617                            }
618                        },
619                        Event::ContentBlockDelta { index, delta } => match delta {
620                            ContentDelta::TextDelta { text } => {
621                                return Some((
622                                    Some(Ok(LanguageModelCompletionEvent::Text(text))),
623                                    state,
624                                ));
625                            }
626                            ContentDelta::InputJsonDelta { partial_json } => {
627                                if let Some(tool_use) = state.tool_uses_by_index.get_mut(&index) {
628                                    tool_use.input_json.push_str(&partial_json);
629                                    return Some((None, state));
630                                }
631                            }
632                        },
633                        Event::ContentBlockStop { index } => {
634                            if let Some(tool_use) = state.tool_uses_by_index.remove(&index) {
635                                return Some((
636                                    Some(maybe!({
637                                        Ok(LanguageModelCompletionEvent::ToolUse(
638                                            LanguageModelToolUse {
639                                                id: tool_use.id.into(),
640                                                name: tool_use.name.into(),
641                                                input: if tool_use.input_json.is_empty() {
642                                                    serde_json::Value::Null
643                                                } else {
644                                                    serde_json::Value::from_str(
645                                                        &tool_use.input_json,
646                                                    )
647                                                    .map_err(|err| anyhow!(err))?
648                                                },
649                                            },
650                                        ))
651                                    })),
652                                    state,
653                                ));
654                            }
655                        }
656                        Event::MessageStart { message } => {
657                            return Some((
658                                Some(Ok(LanguageModelCompletionEvent::StartMessage {
659                                    message_id: message.id,
660                                })),
661                                state,
662                            ))
663                        }
664                        Event::MessageDelta { delta, .. } => {
665                            if let Some(stop_reason) = delta.stop_reason.as_deref() {
666                                let stop_reason = match stop_reason {
667                                    "end_turn" => StopReason::EndTurn,
668                                    "max_tokens" => StopReason::MaxTokens,
669                                    "tool_use" => StopReason::ToolUse,
670                                    _ => StopReason::EndTurn,
671                                };
672
673                                return Some((
674                                    Some(Ok(LanguageModelCompletionEvent::Stop(stop_reason))),
675                                    state,
676                                ));
677                            }
678                        }
679                        Event::Error { error } => {
680                            return Some((
681                                Some(Err(anyhow!(AnthropicError::ApiError(error)))),
682                                state,
683                            ));
684                        }
685                        _ => {}
686                    },
687                    Err(err) => {
688                        return Some((Some(Err(anyhow!(err))), state));
689                    }
690                }
691            }
692
693            None
694        },
695    )
696    .filter_map(|event| async move { event })
697}
698
699struct ConfigurationView {
700    api_key_editor: Entity<Editor>,
701    state: gpui::Entity<State>,
702    load_credentials_task: Option<Task<()>>,
703}
704
705impl ConfigurationView {
706    const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
707
708    fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
709        cx.observe(&state, |_, _, cx| {
710            cx.notify();
711        })
712        .detach();
713
714        let load_credentials_task = Some(cx.spawn({
715            let state = state.clone();
716            |this, mut cx| async move {
717                if let Some(task) = state
718                    .update(&mut cx, |state, cx| state.authenticate(cx))
719                    .log_err()
720                {
721                    // We don't log an error, because "not signed in" is also an error.
722                    let _ = task.await;
723                }
724                this.update(&mut cx, |this, cx| {
725                    this.load_credentials_task = None;
726                    cx.notify();
727                })
728                .log_err();
729            }
730        }));
731
732        Self {
733            api_key_editor: cx.new(|cx| {
734                let mut editor = Editor::single_line(window, cx);
735                editor.set_placeholder_text(Self::PLACEHOLDER_TEXT, cx);
736                editor
737            }),
738            state,
739            load_credentials_task,
740        }
741    }
742
743    fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
744        let api_key = self.api_key_editor.read(cx).text(cx);
745        if api_key.is_empty() {
746            return;
747        }
748
749        let state = self.state.clone();
750        cx.spawn_in(window, |_, mut cx| async move {
751            state
752                .update(&mut cx, |state, cx| state.set_api_key(api_key, cx))?
753                .await
754        })
755        .detach_and_log_err(cx);
756
757        cx.notify();
758    }
759
760    fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
761        self.api_key_editor
762            .update(cx, |editor, cx| editor.set_text("", window, cx));
763
764        let state = self.state.clone();
765        cx.spawn_in(window, |_, mut cx| async move {
766            state
767                .update(&mut cx, |state, cx| state.reset_api_key(cx))?
768                .await
769        })
770        .detach_and_log_err(cx);
771
772        cx.notify();
773    }
774
775    fn render_api_key_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
776        let settings = ThemeSettings::get_global(cx);
777        let text_style = TextStyle {
778            color: cx.theme().colors().text,
779            font_family: settings.ui_font.family.clone(),
780            font_features: settings.ui_font.features.clone(),
781            font_fallbacks: settings.ui_font.fallbacks.clone(),
782            font_size: rems(0.875).into(),
783            font_weight: settings.ui_font.weight,
784            font_style: FontStyle::Normal,
785            line_height: relative(1.3),
786            white_space: WhiteSpace::Normal,
787            ..Default::default()
788        };
789        EditorElement::new(
790            &self.api_key_editor,
791            EditorStyle {
792                background: cx.theme().colors().editor_background,
793                local_player: cx.theme().players().local(),
794                text: text_style,
795                ..Default::default()
796            },
797        )
798    }
799
800    fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
801        !self.state.read(cx).is_authenticated()
802    }
803}
804
805impl Render for ConfigurationView {
806    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
807        let env_var_set = self.state.read(cx).api_key_from_env;
808
809        if self.load_credentials_task.is_some() {
810            div().child(Label::new("Loading credentials...")).into_any()
811        } else if self.should_render_editor(cx) {
812            v_flex()
813                .size_full()
814                .on_action(cx.listener(Self::save_api_key))
815                .child(Label::new("To use Zed's assistant with Anthropic, you need to add an API key. Follow these steps:"))
816                .child(
817                    List::new()
818                        .child(
819                            InstructionListItem::new(
820                                "Create one by visiting",
821                                Some("Anthropic's settings"),
822                                Some("https://console.anthropic.com/settings/keys")
823                            )
824                        )
825                        .child(
826                            InstructionListItem::text_only("Paste your API key below and hit enter to start using the assistant")
827                        )
828                )
829                .child(
830                    h_flex()
831                        .w_full()
832                        .my_2()
833                        .px_2()
834                        .py_1()
835                        .bg(cx.theme().colors().editor_background)
836                        .border_1()
837                        .border_color(cx.theme().colors().border_variant)
838                        .rounded_md()
839                        .child(self.render_api_key_editor(cx)),
840                )
841                .child(
842                    Label::new(
843                        format!("You can also assign the {ANTHROPIC_API_KEY_VAR} environment variable and restart Zed."),
844                    )
845                    .size(LabelSize::Small)
846                    .color(Color::Muted),
847                )
848                .into_any()
849        } else {
850            h_flex()
851                .size_full()
852                .justify_between()
853                .child(
854                    h_flex()
855                        .gap_1()
856                        .child(Icon::new(IconName::Check).color(Color::Success))
857                        .child(Label::new(if env_var_set {
858                            format!("API key set in {ANTHROPIC_API_KEY_VAR} environment variable.")
859                        } else {
860                            "API key configured.".to_string()
861                        })),
862                )
863                .child(
864                    Button::new("reset-key", "Reset key")
865                        .icon(Some(IconName::Trash))
866                        .icon_size(IconSize::Small)
867                        .icon_position(IconPosition::Start)
868                        .disabled(env_var_set)
869                        .when(env_var_set, |this| {
870                            this.tooltip(Tooltip::text(format!("To reset your API key, unset the {ANTHROPIC_API_KEY_VAR} environment variable.")))
871                        })
872                        .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
873                )
874                .into_any()
875        }
876    }
877}