1use anthropic::{
2 ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode, ContentDelta, Event, ResponseContent,
3 ToolResultContent, ToolResultPart, Usage,
4};
5use anyhow::{Result, anyhow};
6use collections::{BTreeMap, HashMap};
7use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::BoxStream};
8use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
9use http_client::HttpClient;
10use language_model::{
11 AuthenticateError, ConfigurationViewTargetAgent, LanguageModel,
12 LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId,
13 LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
14 LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
15 LanguageModelToolResultContent, MessageContent, RateLimiter, Role,
16};
17use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
18use settings::{Settings, SettingsStore};
19use std::pin::Pin;
20use std::str::FromStr;
21use std::sync::{Arc, LazyLock};
22use strum::IntoEnumIterator;
23use ui::{Icon, IconName, List, Tooltip, prelude::*};
24use ui_input::SingleLineInput;
25use util::{ResultExt, truncate_and_trailoff};
26use zed_env_vars::{EnvVar, env_var};
27
28use crate::api_key::ApiKeyState;
29use crate::ui::InstructionListItem;
30
31pub use settings::AnthropicAvailableModel as AvailableModel;
32
33const PROVIDER_ID: LanguageModelProviderId = language_model::ANTHROPIC_PROVIDER_ID;
34const PROVIDER_NAME: LanguageModelProviderName = language_model::ANTHROPIC_PROVIDER_NAME;
35
36#[derive(Default, Clone, Debug, PartialEq)]
37pub struct AnthropicSettings {
38 pub api_url: String,
39 /// Extend Zed's list of Anthropic models.
40 pub available_models: Vec<AvailableModel>,
41}
42
43pub struct AnthropicLanguageModelProvider {
44 http_client: Arc<dyn HttpClient>,
45 state: Entity<State>,
46}
47
48const API_KEY_ENV_VAR_NAME: &str = "ANTHROPIC_API_KEY";
49static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
50
51pub struct State {
52 api_key_state: ApiKeyState,
53}
54
55impl State {
56 fn is_authenticated(&self) -> bool {
57 self.api_key_state.has_key()
58 }
59
60 fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
61 let api_url = AnthropicLanguageModelProvider::api_url(cx);
62 self.api_key_state
63 .store(api_url, api_key, |this| &mut this.api_key_state, cx)
64 }
65
66 fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
67 let api_url = AnthropicLanguageModelProvider::api_url(cx);
68 self.api_key_state.load_if_needed(
69 api_url,
70 &API_KEY_ENV_VAR,
71 |this| &mut this.api_key_state,
72 cx,
73 )
74 }
75}
76
77impl AnthropicLanguageModelProvider {
78 pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
79 let state = cx.new(|cx| {
80 cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
81 let api_url = Self::api_url(cx);
82 this.api_key_state.handle_url_change(
83 api_url,
84 &API_KEY_ENV_VAR,
85 |this| &mut this.api_key_state,
86 cx,
87 );
88 cx.notify();
89 })
90 .detach();
91 State {
92 api_key_state: ApiKeyState::new(Self::api_url(cx)),
93 }
94 });
95
96 Self { http_client, state }
97 }
98
99 fn create_language_model(&self, model: anthropic::Model) -> Arc<dyn LanguageModel> {
100 Arc::new(AnthropicModel {
101 id: LanguageModelId::from(model.id().to_string()),
102 model,
103 state: self.state.clone(),
104 http_client: self.http_client.clone(),
105 request_limiter: RateLimiter::new(4),
106 })
107 }
108
109 fn settings(cx: &App) -> &AnthropicSettings {
110 &crate::AllLanguageModelSettings::get_global(cx).anthropic
111 }
112
113 fn api_url(cx: &App) -> SharedString {
114 let api_url = &Self::settings(cx).api_url;
115 if api_url.is_empty() {
116 ANTHROPIC_API_URL.into()
117 } else {
118 SharedString::new(api_url.as_str())
119 }
120 }
121}
122
123impl LanguageModelProviderState for AnthropicLanguageModelProvider {
124 type ObservableEntity = State;
125
126 fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
127 Some(self.state.clone())
128 }
129}
130
131impl LanguageModelProvider for AnthropicLanguageModelProvider {
132 fn id(&self) -> LanguageModelProviderId {
133 PROVIDER_ID
134 }
135
136 fn name(&self) -> LanguageModelProviderName {
137 PROVIDER_NAME
138 }
139
140 fn icon(&self) -> IconName {
141 IconName::AiAnthropic
142 }
143
144 fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
145 Some(self.create_language_model(anthropic::Model::default()))
146 }
147
148 fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
149 Some(self.create_language_model(anthropic::Model::default_fast()))
150 }
151
152 fn recommended_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
153 [
154 anthropic::Model::ClaudeSonnet4_5,
155 anthropic::Model::ClaudeSonnet4_5Thinking,
156 ]
157 .into_iter()
158 .map(|model| self.create_language_model(model))
159 .collect()
160 }
161
162 fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
163 let mut models = BTreeMap::default();
164
165 // Add base models from anthropic::Model::iter()
166 for model in anthropic::Model::iter() {
167 if !matches!(model, anthropic::Model::Custom { .. }) {
168 models.insert(model.id().to_string(), model);
169 }
170 }
171
172 // Override with available models from settings
173 for model in &AnthropicLanguageModelProvider::settings(cx).available_models {
174 models.insert(
175 model.name.clone(),
176 anthropic::Model::Custom {
177 name: model.name.clone(),
178 display_name: model.display_name.clone(),
179 max_tokens: model.max_tokens,
180 tool_override: model.tool_override.clone(),
181 cache_configuration: model.cache_configuration.as_ref().map(|config| {
182 anthropic::AnthropicModelCacheConfiguration {
183 max_cache_anchors: config.max_cache_anchors,
184 should_speculate: config.should_speculate,
185 min_total_token: config.min_total_token,
186 }
187 }),
188 max_output_tokens: model.max_output_tokens,
189 default_temperature: model.default_temperature,
190 extra_beta_headers: model.extra_beta_headers.clone(),
191 mode: model.mode.unwrap_or_default().into(),
192 },
193 );
194 }
195
196 models
197 .into_values()
198 .map(|model| self.create_language_model(model))
199 .collect()
200 }
201
202 fn is_authenticated(&self, cx: &App) -> bool {
203 self.state.read(cx).is_authenticated()
204 }
205
206 fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
207 self.state.update(cx, |state, cx| state.authenticate(cx))
208 }
209
210 fn configuration_view(
211 &self,
212 target_agent: ConfigurationViewTargetAgent,
213 window: &mut Window,
214 cx: &mut App,
215 ) -> AnyView {
216 cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
217 .into()
218 }
219
220 fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
221 self.state
222 .update(cx, |state, cx| state.set_api_key(None, cx))
223 }
224}
225
226pub struct AnthropicModel {
227 id: LanguageModelId,
228 model: anthropic::Model,
229 state: Entity<State>,
230 http_client: Arc<dyn HttpClient>,
231 request_limiter: RateLimiter,
232}
233
234pub fn count_anthropic_tokens(
235 request: LanguageModelRequest,
236 cx: &App,
237) -> BoxFuture<'static, Result<u64>> {
238 cx.background_spawn(async move {
239 let messages = request.messages;
240 let mut tokens_from_images = 0;
241 let mut string_messages = Vec::with_capacity(messages.len());
242
243 for message in messages {
244 use language_model::MessageContent;
245
246 let mut string_contents = String::new();
247
248 for content in message.content {
249 match content {
250 MessageContent::Text(text) => {
251 string_contents.push_str(&text);
252 }
253 MessageContent::Thinking { .. } => {
254 // Thinking blocks are not included in the input token count.
255 }
256 MessageContent::RedactedThinking(_) => {
257 // Thinking blocks are not included in the input token count.
258 }
259 MessageContent::Image(image) => {
260 tokens_from_images += image.estimate_tokens();
261 }
262 MessageContent::ToolUse(_tool_use) => {
263 // TODO: Estimate token usage from tool uses.
264 }
265 MessageContent::ToolResult(tool_result) => match &tool_result.content {
266 LanguageModelToolResultContent::Text(text) => {
267 string_contents.push_str(text);
268 }
269 LanguageModelToolResultContent::Image(image) => {
270 tokens_from_images += image.estimate_tokens();
271 }
272 },
273 }
274 }
275
276 if !string_contents.is_empty() {
277 string_messages.push(tiktoken_rs::ChatCompletionRequestMessage {
278 role: match message.role {
279 Role::User => "user".into(),
280 Role::Assistant => "assistant".into(),
281 Role::System => "system".into(),
282 },
283 content: Some(string_contents),
284 name: None,
285 function_call: None,
286 });
287 }
288 }
289
290 // Tiktoken doesn't yet support these models, so we manually use the
291 // same tokenizer as GPT-4.
292 tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages)
293 .map(|tokens| (tokens + tokens_from_images) as u64)
294 })
295 .boxed()
296}
297
298impl AnthropicModel {
299 fn stream_completion(
300 &self,
301 request: anthropic::Request,
302 cx: &AsyncApp,
303 ) -> BoxFuture<
304 'static,
305 Result<
306 BoxStream<'static, Result<anthropic::Event, AnthropicError>>,
307 LanguageModelCompletionError,
308 >,
309 > {
310 let http_client = self.http_client.clone();
311
312 let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
313 let api_url = AnthropicLanguageModelProvider::api_url(cx);
314 (state.api_key_state.key(&api_url), api_url)
315 }) else {
316 return future::ready(Err(anyhow!("App state dropped").into())).boxed();
317 };
318
319 let beta_headers = self.model.beta_headers();
320
321 async move {
322 let Some(api_key) = api_key else {
323 return Err(LanguageModelCompletionError::NoApiKey {
324 provider: PROVIDER_NAME,
325 });
326 };
327 let request = anthropic::stream_completion(
328 http_client.as_ref(),
329 &api_url,
330 &api_key,
331 request,
332 beta_headers,
333 );
334 request.await.map_err(Into::into)
335 }
336 .boxed()
337 }
338}
339
340impl LanguageModel for AnthropicModel {
341 fn id(&self) -> LanguageModelId {
342 self.id.clone()
343 }
344
345 fn name(&self) -> LanguageModelName {
346 LanguageModelName::from(self.model.display_name().to_string())
347 }
348
349 fn provider_id(&self) -> LanguageModelProviderId {
350 PROVIDER_ID
351 }
352
353 fn provider_name(&self) -> LanguageModelProviderName {
354 PROVIDER_NAME
355 }
356
357 fn supports_tools(&self) -> bool {
358 true
359 }
360
361 fn supports_images(&self) -> bool {
362 true
363 }
364
365 fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
366 match choice {
367 LanguageModelToolChoice::Auto
368 | LanguageModelToolChoice::Any
369 | LanguageModelToolChoice::None => true,
370 }
371 }
372
373 fn telemetry_id(&self) -> String {
374 format!("anthropic/{}", self.model.id())
375 }
376
377 fn api_key(&self, cx: &App) -> Option<String> {
378 self.state.read_with(cx, |state, cx| {
379 let api_url = AnthropicLanguageModelProvider::api_url(cx);
380 state.api_key_state.key(&api_url).map(|key| key.to_string())
381 })
382 }
383
384 fn max_token_count(&self) -> u64 {
385 self.model.max_token_count()
386 }
387
388 fn max_output_tokens(&self) -> Option<u64> {
389 Some(self.model.max_output_tokens())
390 }
391
392 fn count_tokens(
393 &self,
394 request: LanguageModelRequest,
395 cx: &App,
396 ) -> BoxFuture<'static, Result<u64>> {
397 count_anthropic_tokens(request, cx)
398 }
399
400 fn stream_completion(
401 &self,
402 request: LanguageModelRequest,
403 cx: &AsyncApp,
404 ) -> BoxFuture<
405 'static,
406 Result<
407 BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
408 LanguageModelCompletionError,
409 >,
410 > {
411 let request = into_anthropic(
412 request,
413 self.model.request_id().into(),
414 self.model.default_temperature(),
415 self.model.max_output_tokens(),
416 self.model.mode(),
417 );
418 let request = self.stream_completion(request, cx);
419 let future = self.request_limiter.stream(async move {
420 let response = request.await?;
421 Ok(AnthropicEventMapper::new().map_stream(response))
422 });
423 async move { Ok(future.await?.boxed()) }.boxed()
424 }
425
426 fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
427 self.model
428 .cache_configuration()
429 .map(|config| LanguageModelCacheConfiguration {
430 max_cache_anchors: config.max_cache_anchors,
431 should_speculate: config.should_speculate,
432 min_total_token: config.min_total_token,
433 })
434 }
435}
436
437pub fn into_anthropic(
438 request: LanguageModelRequest,
439 model: String,
440 default_temperature: f32,
441 max_output_tokens: u64,
442 mode: AnthropicModelMode,
443) -> anthropic::Request {
444 let mut new_messages: Vec<anthropic::Message> = Vec::new();
445 let mut system_message = String::new();
446
447 for message in request.messages {
448 if message.contents_empty() {
449 continue;
450 }
451
452 match message.role {
453 Role::User | Role::Assistant => {
454 let mut anthropic_message_content: Vec<anthropic::RequestContent> = message
455 .content
456 .into_iter()
457 .filter_map(|content| match content {
458 MessageContent::Text(text) => {
459 let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) {
460 text.trim_end().to_string()
461 } else {
462 text
463 };
464 if !text.is_empty() {
465 Some(anthropic::RequestContent::Text {
466 text,
467 cache_control: None,
468 })
469 } else {
470 None
471 }
472 }
473 MessageContent::Thinking {
474 text: thinking,
475 signature,
476 } => {
477 if !thinking.is_empty() {
478 Some(anthropic::RequestContent::Thinking {
479 thinking,
480 signature: signature.unwrap_or_default(),
481 cache_control: None,
482 })
483 } else {
484 None
485 }
486 }
487 MessageContent::RedactedThinking(data) => {
488 if !data.is_empty() {
489 Some(anthropic::RequestContent::RedactedThinking { data })
490 } else {
491 None
492 }
493 }
494 MessageContent::Image(image) => Some(anthropic::RequestContent::Image {
495 source: anthropic::ImageSource {
496 source_type: "base64".to_string(),
497 media_type: "image/png".to_string(),
498 data: image.source.to_string(),
499 },
500 cache_control: None,
501 }),
502 MessageContent::ToolUse(tool_use) => {
503 Some(anthropic::RequestContent::ToolUse {
504 id: tool_use.id.to_string(),
505 name: tool_use.name.to_string(),
506 input: tool_use.input,
507 cache_control: None,
508 })
509 }
510 MessageContent::ToolResult(tool_result) => {
511 Some(anthropic::RequestContent::ToolResult {
512 tool_use_id: tool_result.tool_use_id.to_string(),
513 is_error: tool_result.is_error,
514 content: match tool_result.content {
515 LanguageModelToolResultContent::Text(text) => {
516 ToolResultContent::Plain(text.to_string())
517 }
518 LanguageModelToolResultContent::Image(image) => {
519 ToolResultContent::Multipart(vec![ToolResultPart::Image {
520 source: anthropic::ImageSource {
521 source_type: "base64".to_string(),
522 media_type: "image/png".to_string(),
523 data: image.source.to_string(),
524 },
525 }])
526 }
527 },
528 cache_control: None,
529 })
530 }
531 })
532 .collect();
533 let anthropic_role = match message.role {
534 Role::User => anthropic::Role::User,
535 Role::Assistant => anthropic::Role::Assistant,
536 Role::System => unreachable!("System role should never occur here"),
537 };
538 if let Some(last_message) = new_messages.last_mut()
539 && last_message.role == anthropic_role
540 {
541 last_message.content.extend(anthropic_message_content);
542 continue;
543 }
544
545 // Mark the last segment of the message as cached
546 if message.cache {
547 let cache_control_value = Some(anthropic::CacheControl {
548 cache_type: anthropic::CacheControlType::Ephemeral,
549 });
550 for message_content in anthropic_message_content.iter_mut().rev() {
551 match message_content {
552 anthropic::RequestContent::RedactedThinking { .. } => {
553 // Caching is not possible, fallback to next message
554 }
555 anthropic::RequestContent::Text { cache_control, .. }
556 | anthropic::RequestContent::Thinking { cache_control, .. }
557 | anthropic::RequestContent::Image { cache_control, .. }
558 | anthropic::RequestContent::ToolUse { cache_control, .. }
559 | anthropic::RequestContent::ToolResult { cache_control, .. } => {
560 *cache_control = cache_control_value;
561 break;
562 }
563 }
564 }
565 }
566
567 new_messages.push(anthropic::Message {
568 role: anthropic_role,
569 content: anthropic_message_content,
570 });
571 }
572 Role::System => {
573 if !system_message.is_empty() {
574 system_message.push_str("\n\n");
575 }
576 system_message.push_str(&message.string_contents());
577 }
578 }
579 }
580
581 anthropic::Request {
582 model,
583 messages: new_messages,
584 max_tokens: max_output_tokens,
585 system: if system_message.is_empty() {
586 None
587 } else {
588 Some(anthropic::StringOrContents::String(system_message))
589 },
590 thinking: if request.thinking_allowed
591 && let AnthropicModelMode::Thinking { budget_tokens } = mode
592 {
593 Some(anthropic::Thinking::Enabled { budget_tokens })
594 } else {
595 None
596 },
597 tools: request
598 .tools
599 .into_iter()
600 .map(|tool| anthropic::Tool {
601 name: tool.name,
602 description: tool.description,
603 input_schema: tool.input_schema,
604 })
605 .collect(),
606 tool_choice: request.tool_choice.map(|choice| match choice {
607 LanguageModelToolChoice::Auto => anthropic::ToolChoice::Auto,
608 LanguageModelToolChoice::Any => anthropic::ToolChoice::Any,
609 LanguageModelToolChoice::None => anthropic::ToolChoice::None,
610 }),
611 metadata: None,
612 stop_sequences: Vec::new(),
613 temperature: request.temperature.or(Some(default_temperature)),
614 top_k: None,
615 top_p: None,
616 }
617}
618
619pub struct AnthropicEventMapper {
620 tool_uses_by_index: HashMap<usize, RawToolUse>,
621 usage: Usage,
622 stop_reason: StopReason,
623}
624
625impl AnthropicEventMapper {
626 pub fn new() -> Self {
627 Self {
628 tool_uses_by_index: HashMap::default(),
629 usage: Usage::default(),
630 stop_reason: StopReason::EndTurn,
631 }
632 }
633
634 pub fn map_stream(
635 mut self,
636 events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
637 ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
638 {
639 events.flat_map(move |event| {
640 futures::stream::iter(match event {
641 Ok(event) => self.map_event(event),
642 Err(error) => vec![Err(error.into())],
643 })
644 })
645 }
646
647 pub fn map_event(
648 &mut self,
649 event: Event,
650 ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
651 match event {
652 Event::ContentBlockStart {
653 index,
654 content_block,
655 } => match content_block {
656 ResponseContent::Text { text } => {
657 vec![Ok(LanguageModelCompletionEvent::Text(text))]
658 }
659 ResponseContent::Thinking { thinking } => {
660 vec![Ok(LanguageModelCompletionEvent::Thinking {
661 text: thinking,
662 signature: None,
663 })]
664 }
665 ResponseContent::RedactedThinking { data } => {
666 vec![Ok(LanguageModelCompletionEvent::RedactedThinking { data })]
667 }
668 ResponseContent::ToolUse { id, name, .. } => {
669 self.tool_uses_by_index.insert(
670 index,
671 RawToolUse {
672 id,
673 name,
674 input_json: String::new(),
675 },
676 );
677 Vec::new()
678 }
679 },
680 Event::ContentBlockDelta { index, delta } => match delta {
681 ContentDelta::TextDelta { text } => {
682 vec![Ok(LanguageModelCompletionEvent::Text(text))]
683 }
684 ContentDelta::ThinkingDelta { thinking } => {
685 vec![Ok(LanguageModelCompletionEvent::Thinking {
686 text: thinking,
687 signature: None,
688 })]
689 }
690 ContentDelta::SignatureDelta { signature } => {
691 vec![Ok(LanguageModelCompletionEvent::Thinking {
692 text: "".to_string(),
693 signature: Some(signature),
694 })]
695 }
696 ContentDelta::InputJsonDelta { partial_json } => {
697 if let Some(tool_use) = self.tool_uses_by_index.get_mut(&index) {
698 tool_use.input_json.push_str(&partial_json);
699
700 // Try to convert invalid (incomplete) JSON into
701 // valid JSON that serde can accept, e.g. by closing
702 // unclosed delimiters. This way, we can update the
703 // UI with whatever has been streamed back so far.
704 if let Ok(input) = serde_json::Value::from_str(
705 &partial_json_fixer::fix_json(&tool_use.input_json),
706 ) {
707 return vec![Ok(LanguageModelCompletionEvent::ToolUse(
708 LanguageModelToolUse {
709 id: tool_use.id.clone().into(),
710 name: tool_use.name.clone().into(),
711 is_input_complete: false,
712 raw_input: tool_use.input_json.clone(),
713 input,
714 },
715 ))];
716 }
717 }
718 vec![]
719 }
720 },
721 Event::ContentBlockStop { index } => {
722 if let Some(tool_use) = self.tool_uses_by_index.remove(&index) {
723 let input_json = tool_use.input_json.trim();
724 let input_value = if input_json.is_empty() {
725 Ok(serde_json::Value::Object(serde_json::Map::default()))
726 } else {
727 serde_json::Value::from_str(input_json)
728 };
729 let event_result = match input_value {
730 Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
731 LanguageModelToolUse {
732 id: tool_use.id.into(),
733 name: tool_use.name.into(),
734 is_input_complete: true,
735 input,
736 raw_input: tool_use.input_json.clone(),
737 },
738 )),
739 Err(json_parse_err) => {
740 Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
741 id: tool_use.id.into(),
742 tool_name: tool_use.name.into(),
743 raw_input: input_json.into(),
744 json_parse_error: json_parse_err.to_string(),
745 })
746 }
747 };
748
749 vec![event_result]
750 } else {
751 Vec::new()
752 }
753 }
754 Event::MessageStart { message } => {
755 update_usage(&mut self.usage, &message.usage);
756 vec![
757 Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
758 &self.usage,
759 ))),
760 Ok(LanguageModelCompletionEvent::StartMessage {
761 message_id: message.id,
762 }),
763 ]
764 }
765 Event::MessageDelta { delta, usage } => {
766 update_usage(&mut self.usage, &usage);
767 if let Some(stop_reason) = delta.stop_reason.as_deref() {
768 self.stop_reason = match stop_reason {
769 "end_turn" => StopReason::EndTurn,
770 "max_tokens" => StopReason::MaxTokens,
771 "tool_use" => StopReason::ToolUse,
772 "refusal" => StopReason::Refusal,
773 _ => {
774 log::error!("Unexpected anthropic stop_reason: {stop_reason}");
775 StopReason::EndTurn
776 }
777 };
778 }
779 vec![Ok(LanguageModelCompletionEvent::UsageUpdate(
780 convert_usage(&self.usage),
781 ))]
782 }
783 Event::MessageStop => {
784 vec![Ok(LanguageModelCompletionEvent::Stop(self.stop_reason))]
785 }
786 Event::Error { error } => {
787 vec![Err(error.into())]
788 }
789 _ => Vec::new(),
790 }
791 }
792}
793
794struct RawToolUse {
795 id: String,
796 name: String,
797 input_json: String,
798}
799
800/// Updates usage data by preferring counts from `new`.
801fn update_usage(usage: &mut Usage, new: &Usage) {
802 if let Some(input_tokens) = new.input_tokens {
803 usage.input_tokens = Some(input_tokens);
804 }
805 if let Some(output_tokens) = new.output_tokens {
806 usage.output_tokens = Some(output_tokens);
807 }
808 if let Some(cache_creation_input_tokens) = new.cache_creation_input_tokens {
809 usage.cache_creation_input_tokens = Some(cache_creation_input_tokens);
810 }
811 if let Some(cache_read_input_tokens) = new.cache_read_input_tokens {
812 usage.cache_read_input_tokens = Some(cache_read_input_tokens);
813 }
814}
815
816fn convert_usage(usage: &Usage) -> language_model::TokenUsage {
817 language_model::TokenUsage {
818 input_tokens: usage.input_tokens.unwrap_or(0),
819 output_tokens: usage.output_tokens.unwrap_or(0),
820 cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0),
821 cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0),
822 }
823}
824
825struct ConfigurationView {
826 api_key_editor: Entity<SingleLineInput>,
827 state: Entity<State>,
828 load_credentials_task: Option<Task<()>>,
829 target_agent: ConfigurationViewTargetAgent,
830}
831
832impl ConfigurationView {
833 const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
834
835 fn new(
836 state: Entity<State>,
837 target_agent: ConfigurationViewTargetAgent,
838 window: &mut Window,
839 cx: &mut Context<Self>,
840 ) -> Self {
841 cx.observe(&state, |_, _, cx| {
842 cx.notify();
843 })
844 .detach();
845
846 let load_credentials_task = Some(cx.spawn({
847 let state = state.clone();
848 async move |this, cx| {
849 if let Some(task) = state
850 .update(cx, |state, cx| state.authenticate(cx))
851 .log_err()
852 {
853 // We don't log an error, because "not signed in" is also an error.
854 let _ = task.await;
855 }
856 this.update(cx, |this, cx| {
857 this.load_credentials_task = None;
858 cx.notify();
859 })
860 .log_err();
861 }
862 }));
863
864 Self {
865 api_key_editor: cx.new(|cx| SingleLineInput::new(window, cx, Self::PLACEHOLDER_TEXT)),
866 state,
867 load_credentials_task,
868 target_agent,
869 }
870 }
871
872 fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
873 let api_key = self.api_key_editor.read(cx).text(cx);
874 if api_key.is_empty() {
875 return;
876 }
877
878 // url changes can cause the editor to be displayed again
879 self.api_key_editor
880 .update(cx, |editor, cx| editor.set_text("", window, cx));
881
882 let state = self.state.clone();
883 cx.spawn_in(window, async move |_, cx| {
884 state
885 .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))?
886 .await
887 })
888 .detach_and_log_err(cx);
889 }
890
891 fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
892 self.api_key_editor
893 .update(cx, |editor, cx| editor.set_text("", window, cx));
894
895 let state = self.state.clone();
896 cx.spawn_in(window, async move |_, cx| {
897 state
898 .update(cx, |state, cx| state.set_api_key(None, cx))?
899 .await
900 })
901 .detach_and_log_err(cx);
902 }
903
904 fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
905 !self.state.read(cx).is_authenticated()
906 }
907}
908
909impl Render for ConfigurationView {
910 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
911 let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
912
913 if self.load_credentials_task.is_some() {
914 div().child(Label::new("Loading credentials...")).into_any()
915 } else if self.should_render_editor(cx) {
916 v_flex()
917 .size_full()
918 .on_action(cx.listener(Self::save_api_key))
919 .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
920 ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(),
921 ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
922 })))
923 .child(
924 List::new()
925 .child(
926 InstructionListItem::new(
927 "Create one by visiting",
928 Some("Anthropic's settings"),
929 Some("https://console.anthropic.com/settings/keys")
930 )
931 )
932 .child(
933 InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent")
934 )
935 )
936 .child(self.api_key_editor.clone())
937 .child(
938 Label::new(
939 format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
940 )
941 .size(LabelSize::Small)
942 .color(Color::Muted),
943 )
944 .into_any()
945 } else {
946 h_flex()
947 .mt_1()
948 .p_1()
949 .justify_between()
950 .rounded_md()
951 .border_1()
952 .border_color(cx.theme().colors().border)
953 .bg(cx.theme().colors().background)
954 .child(
955 h_flex()
956 .gap_1()
957 .child(Icon::new(IconName::Check).color(Color::Success))
958 .child(Label::new(if env_var_set {
959 format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
960 } else {
961 let api_url = AnthropicLanguageModelProvider::api_url(cx);
962 if api_url == ANTHROPIC_API_URL {
963 "API key configured".to_string()
964 } else {
965 format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
966 }
967 })),
968 )
969 .child(
970 Button::new("reset-key", "Reset Key")
971 .label_size(LabelSize::Small)
972 .icon(Some(IconName::Trash))
973 .icon_size(IconSize::Small)
974 .icon_position(IconPosition::Start)
975 .disabled(env_var_set)
976 .when(env_var_set, |this| {
977 this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")))
978 })
979 .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
980 )
981 .into_any()
982 }
983 }
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989 use anthropic::AnthropicModelMode;
990 use language_model::{LanguageModelRequestMessage, MessageContent};
991
992 #[test]
993 fn test_cache_control_only_on_last_segment() {
994 let request = LanguageModelRequest {
995 messages: vec![LanguageModelRequestMessage {
996 role: Role::User,
997 content: vec![
998 MessageContent::Text("Some prompt".to_string()),
999 MessageContent::Image(language_model::LanguageModelImage::empty()),
1000 MessageContent::Image(language_model::LanguageModelImage::empty()),
1001 MessageContent::Image(language_model::LanguageModelImage::empty()),
1002 MessageContent::Image(language_model::LanguageModelImage::empty()),
1003 ],
1004 cache: true,
1005 }],
1006 thread_id: None,
1007 prompt_id: None,
1008 intent: None,
1009 mode: None,
1010 stop: vec![],
1011 temperature: None,
1012 tools: vec![],
1013 tool_choice: None,
1014 thinking_allowed: true,
1015 };
1016
1017 let anthropic_request = into_anthropic(
1018 request,
1019 "claude-3-5-sonnet".to_string(),
1020 0.7,
1021 4096,
1022 AnthropicModelMode::Default,
1023 );
1024
1025 assert_eq!(anthropic_request.messages.len(), 1);
1026
1027 let message = &anthropic_request.messages[0];
1028 assert_eq!(message.content.len(), 5);
1029
1030 assert!(matches!(
1031 message.content[0],
1032 anthropic::RequestContent::Text {
1033 cache_control: None,
1034 ..
1035 }
1036 ));
1037 for i in 1..3 {
1038 assert!(matches!(
1039 message.content[i],
1040 anthropic::RequestContent::Image {
1041 cache_control: None,
1042 ..
1043 }
1044 ));
1045 }
1046
1047 assert!(matches!(
1048 message.content[4],
1049 anthropic::RequestContent::Image {
1050 cache_control: Some(anthropic::CacheControl {
1051 cache_type: anthropic::CacheControlType::Ephemeral,
1052 }),
1053 ..
1054 }
1055 ));
1056 }
1057}