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