1use anthropic::{
2 ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode, ContentDelta, CountTokensRequest, Event,
3 ResponseContent, 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, IconOrSvg, 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) -> IconOrSvg {
129 IconOrSvg::Icon(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
222/// Convert a LanguageModelRequest to an Anthropic CountTokensRequest.
223pub fn into_anthropic_count_tokens_request(
224 request: LanguageModelRequest,
225 model: String,
226 mode: AnthropicModelMode,
227) -> CountTokensRequest {
228 let mut new_messages: Vec<anthropic::Message> = Vec::new();
229 let mut system_message = String::new();
230
231 for message in request.messages {
232 if message.contents_empty() {
233 continue;
234 }
235
236 match message.role {
237 Role::User | Role::Assistant => {
238 let anthropic_message_content: Vec<anthropic::RequestContent> = message
239 .content
240 .into_iter()
241 .filter_map(|content| match content {
242 MessageContent::Text(text) => {
243 let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) {
244 text.trim_end().to_string()
245 } else {
246 text
247 };
248 if !text.is_empty() {
249 Some(anthropic::RequestContent::Text {
250 text,
251 cache_control: None,
252 })
253 } else {
254 None
255 }
256 }
257 MessageContent::Thinking {
258 text: thinking,
259 signature,
260 } => {
261 if !thinking.is_empty() {
262 Some(anthropic::RequestContent::Thinking {
263 thinking,
264 signature: signature.unwrap_or_default(),
265 cache_control: None,
266 })
267 } else {
268 None
269 }
270 }
271 MessageContent::RedactedThinking(data) => {
272 if !data.is_empty() {
273 Some(anthropic::RequestContent::RedactedThinking { data })
274 } else {
275 None
276 }
277 }
278 MessageContent::Image(image) => Some(anthropic::RequestContent::Image {
279 source: anthropic::ImageSource {
280 source_type: "base64".to_string(),
281 media_type: "image/png".to_string(),
282 data: image.source.to_string(),
283 },
284 cache_control: None,
285 }),
286 MessageContent::ToolUse(tool_use) => {
287 Some(anthropic::RequestContent::ToolUse {
288 id: tool_use.id.to_string(),
289 name: tool_use.name.to_string(),
290 input: tool_use.input,
291 cache_control: None,
292 })
293 }
294 MessageContent::ToolResult(tool_result) => {
295 Some(anthropic::RequestContent::ToolResult {
296 tool_use_id: tool_result.tool_use_id.to_string(),
297 is_error: tool_result.is_error,
298 content: match tool_result.content {
299 LanguageModelToolResultContent::Text(text) => {
300 ToolResultContent::Plain(text.to_string())
301 }
302 LanguageModelToolResultContent::Image(image) => {
303 ToolResultContent::Multipart(vec![ToolResultPart::Image {
304 source: anthropic::ImageSource {
305 source_type: "base64".to_string(),
306 media_type: "image/png".to_string(),
307 data: image.source.to_string(),
308 },
309 }])
310 }
311 },
312 cache_control: None,
313 })
314 }
315 })
316 .collect();
317 let anthropic_role = match message.role {
318 Role::User => anthropic::Role::User,
319 Role::Assistant => anthropic::Role::Assistant,
320 Role::System => unreachable!("System role should never occur here"),
321 };
322 if let Some(last_message) = new_messages.last_mut()
323 && last_message.role == anthropic_role
324 {
325 last_message.content.extend(anthropic_message_content);
326 continue;
327 }
328
329 new_messages.push(anthropic::Message {
330 role: anthropic_role,
331 content: anthropic_message_content,
332 });
333 }
334 Role::System => {
335 if !system_message.is_empty() {
336 system_message.push_str("\n\n");
337 }
338 system_message.push_str(&message.string_contents());
339 }
340 }
341 }
342
343 CountTokensRequest {
344 model,
345 messages: new_messages,
346 system: if system_message.is_empty() {
347 None
348 } else {
349 Some(anthropic::StringOrContents::String(system_message))
350 },
351 thinking: if request.thinking_allowed
352 && let AnthropicModelMode::Thinking { budget_tokens } = mode
353 {
354 Some(anthropic::Thinking::Enabled { budget_tokens })
355 } else {
356 None
357 },
358 tools: request
359 .tools
360 .into_iter()
361 .map(|tool| anthropic::Tool {
362 name: tool.name,
363 description: tool.description,
364 input_schema: tool.input_schema,
365 })
366 .collect(),
367 tool_choice: request.tool_choice.map(|choice| match choice {
368 LanguageModelToolChoice::Auto => anthropic::ToolChoice::Auto,
369 LanguageModelToolChoice::Any => anthropic::ToolChoice::Any,
370 LanguageModelToolChoice::None => anthropic::ToolChoice::None,
371 }),
372 }
373}
374
375/// Estimate tokens using tiktoken. Used as a fallback when the API is unavailable,
376/// or by providers (like Zed Cloud) that don't have direct Anthropic API access.
377pub fn count_anthropic_tokens_with_tiktoken(request: LanguageModelRequest) -> Result<u64> {
378 let messages = request.messages;
379 let mut tokens_from_images = 0;
380 let mut string_messages = Vec::with_capacity(messages.len());
381
382 for message in messages {
383 let mut string_contents = String::new();
384
385 for content in message.content {
386 match content {
387 MessageContent::Text(text) => {
388 string_contents.push_str(&text);
389 }
390 MessageContent::Thinking { .. } => {
391 // Thinking blocks are not included in the input token count.
392 }
393 MessageContent::RedactedThinking(_) => {
394 // Thinking blocks are not included in the input token count.
395 }
396 MessageContent::Image(image) => {
397 tokens_from_images += image.estimate_tokens();
398 }
399 MessageContent::ToolUse(_tool_use) => {
400 // TODO: Estimate token usage from tool uses.
401 }
402 MessageContent::ToolResult(tool_result) => match &tool_result.content {
403 LanguageModelToolResultContent::Text(text) => {
404 string_contents.push_str(text);
405 }
406 LanguageModelToolResultContent::Image(image) => {
407 tokens_from_images += image.estimate_tokens();
408 }
409 },
410 }
411 }
412
413 if !string_contents.is_empty() {
414 string_messages.push(tiktoken_rs::ChatCompletionRequestMessage {
415 role: match message.role {
416 Role::User => "user".into(),
417 Role::Assistant => "assistant".into(),
418 Role::System => "system".into(),
419 },
420 content: Some(string_contents),
421 name: None,
422 function_call: None,
423 });
424 }
425 }
426
427 // Tiktoken doesn't yet support these models, so we manually use the
428 // same tokenizer as GPT-4.
429 tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages)
430 .map(|tokens| (tokens + tokens_from_images) as u64)
431}
432
433impl AnthropicModel {
434 fn stream_completion(
435 &self,
436 request: anthropic::Request,
437 cx: &AsyncApp,
438 ) -> BoxFuture<
439 'static,
440 Result<
441 BoxStream<'static, Result<anthropic::Event, AnthropicError>>,
442 LanguageModelCompletionError,
443 >,
444 > {
445 let http_client = self.http_client.clone();
446
447 let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
448 let api_url = AnthropicLanguageModelProvider::api_url(cx);
449 (state.api_key_state.key(&api_url), api_url)
450 }) else {
451 return future::ready(Err(anyhow!("App state dropped").into())).boxed();
452 };
453
454 let beta_headers = self.model.beta_headers();
455
456 async move {
457 let Some(api_key) = api_key else {
458 return Err(LanguageModelCompletionError::NoApiKey {
459 provider: PROVIDER_NAME,
460 });
461 };
462 let request = anthropic::stream_completion(
463 http_client.as_ref(),
464 &api_url,
465 &api_key,
466 request,
467 beta_headers,
468 );
469 request.await.map_err(Into::into)
470 }
471 .boxed()
472 }
473}
474
475impl LanguageModel for AnthropicModel {
476 fn id(&self) -> LanguageModelId {
477 self.id.clone()
478 }
479
480 fn name(&self) -> LanguageModelName {
481 LanguageModelName::from(self.model.display_name().to_string())
482 }
483
484 fn provider_id(&self) -> LanguageModelProviderId {
485 PROVIDER_ID
486 }
487
488 fn provider_name(&self) -> LanguageModelProviderName {
489 PROVIDER_NAME
490 }
491
492 fn supports_tools(&self) -> bool {
493 true
494 }
495
496 fn supports_images(&self) -> bool {
497 true
498 }
499
500 fn supports_streaming_tools(&self) -> bool {
501 true
502 }
503
504 fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
505 match choice {
506 LanguageModelToolChoice::Auto
507 | LanguageModelToolChoice::Any
508 | LanguageModelToolChoice::None => true,
509 }
510 }
511
512 fn telemetry_id(&self) -> String {
513 format!("anthropic/{}", self.model.id())
514 }
515
516 fn api_key(&self, cx: &App) -> Option<String> {
517 self.state.read_with(cx, |state, cx| {
518 let api_url = AnthropicLanguageModelProvider::api_url(cx);
519 state.api_key_state.key(&api_url).map(|key| key.to_string())
520 })
521 }
522
523 fn max_token_count(&self) -> u64 {
524 self.model.max_token_count()
525 }
526
527 fn max_output_tokens(&self) -> Option<u64> {
528 Some(self.model.max_output_tokens())
529 }
530
531 fn count_tokens(
532 &self,
533 request: LanguageModelRequest,
534 cx: &App,
535 ) -> BoxFuture<'static, Result<u64>> {
536 let http_client = self.http_client.clone();
537 let model_id = self.model.request_id().to_string();
538 let mode = self.model.mode();
539
540 let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
541 let api_url = AnthropicLanguageModelProvider::api_url(cx);
542 (
543 state.api_key_state.key(&api_url).map(|k| k.to_string()),
544 api_url.to_string(),
545 )
546 });
547
548 async move {
549 // If no API key, fall back to tiktoken estimation
550 let Some(api_key) = api_key else {
551 return count_anthropic_tokens_with_tiktoken(request);
552 };
553
554 let count_request =
555 into_anthropic_count_tokens_request(request.clone(), model_id, mode);
556
557 match anthropic::count_tokens(http_client.as_ref(), &api_url, &api_key, count_request)
558 .await
559 {
560 Ok(response) => Ok(response.input_tokens),
561 Err(err) => {
562 log::error!(
563 "Anthropic count_tokens API failed, falling back to tiktoken: {err:?}"
564 );
565 count_anthropic_tokens_with_tiktoken(request)
566 }
567 }
568 }
569 .boxed()
570 }
571
572 fn stream_completion(
573 &self,
574 request: LanguageModelRequest,
575 cx: &AsyncApp,
576 ) -> BoxFuture<
577 'static,
578 Result<
579 BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
580 LanguageModelCompletionError,
581 >,
582 > {
583 let request = into_anthropic(
584 request,
585 self.model.request_id().into(),
586 self.model.default_temperature(),
587 self.model.max_output_tokens(),
588 self.model.mode(),
589 );
590 let request = self.stream_completion(request, cx);
591 let future = self.request_limiter.stream(async move {
592 let response = request.await?;
593 Ok(AnthropicEventMapper::new().map_stream(response))
594 });
595 async move { Ok(future.await?.boxed()) }.boxed()
596 }
597
598 fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
599 self.model
600 .cache_configuration()
601 .map(|config| LanguageModelCacheConfiguration {
602 max_cache_anchors: config.max_cache_anchors,
603 should_speculate: config.should_speculate,
604 min_total_token: config.min_total_token,
605 })
606 }
607}
608
609pub fn into_anthropic(
610 request: LanguageModelRequest,
611 model: String,
612 default_temperature: f32,
613 max_output_tokens: u64,
614 mode: AnthropicModelMode,
615) -> anthropic::Request {
616 let mut new_messages: Vec<anthropic::Message> = Vec::new();
617 let mut system_message = String::new();
618
619 for message in request.messages {
620 if message.contents_empty() {
621 continue;
622 }
623
624 match message.role {
625 Role::User | Role::Assistant => {
626 let mut anthropic_message_content: Vec<anthropic::RequestContent> = message
627 .content
628 .into_iter()
629 .filter_map(|content| match content {
630 MessageContent::Text(text) => {
631 let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) {
632 text.trim_end().to_string()
633 } else {
634 text
635 };
636 if !text.is_empty() {
637 Some(anthropic::RequestContent::Text {
638 text,
639 cache_control: None,
640 })
641 } else {
642 None
643 }
644 }
645 MessageContent::Thinking {
646 text: thinking,
647 signature,
648 } => {
649 if !thinking.is_empty() {
650 Some(anthropic::RequestContent::Thinking {
651 thinking,
652 signature: signature.unwrap_or_default(),
653 cache_control: None,
654 })
655 } else {
656 None
657 }
658 }
659 MessageContent::RedactedThinking(data) => {
660 if !data.is_empty() {
661 Some(anthropic::RequestContent::RedactedThinking { data })
662 } else {
663 None
664 }
665 }
666 MessageContent::Image(image) => Some(anthropic::RequestContent::Image {
667 source: anthropic::ImageSource {
668 source_type: "base64".to_string(),
669 media_type: "image/png".to_string(),
670 data: image.source.to_string(),
671 },
672 cache_control: None,
673 }),
674 MessageContent::ToolUse(tool_use) => {
675 Some(anthropic::RequestContent::ToolUse {
676 id: tool_use.id.to_string(),
677 name: tool_use.name.to_string(),
678 input: tool_use.input,
679 cache_control: None,
680 })
681 }
682 MessageContent::ToolResult(tool_result) => {
683 Some(anthropic::RequestContent::ToolResult {
684 tool_use_id: tool_result.tool_use_id.to_string(),
685 is_error: tool_result.is_error,
686 content: match tool_result.content {
687 LanguageModelToolResultContent::Text(text) => {
688 ToolResultContent::Plain(text.to_string())
689 }
690 LanguageModelToolResultContent::Image(image) => {
691 ToolResultContent::Multipart(vec![ToolResultPart::Image {
692 source: anthropic::ImageSource {
693 source_type: "base64".to_string(),
694 media_type: "image/png".to_string(),
695 data: image.source.to_string(),
696 },
697 }])
698 }
699 },
700 cache_control: None,
701 })
702 }
703 })
704 .collect();
705 let anthropic_role = match message.role {
706 Role::User => anthropic::Role::User,
707 Role::Assistant => anthropic::Role::Assistant,
708 Role::System => unreachable!("System role should never occur here"),
709 };
710 if let Some(last_message) = new_messages.last_mut()
711 && last_message.role == anthropic_role
712 {
713 last_message.content.extend(anthropic_message_content);
714 continue;
715 }
716
717 // Mark the last segment of the message as cached
718 if message.cache {
719 let cache_control_value = Some(anthropic::CacheControl {
720 cache_type: anthropic::CacheControlType::Ephemeral,
721 });
722 for message_content in anthropic_message_content.iter_mut().rev() {
723 match message_content {
724 anthropic::RequestContent::RedactedThinking { .. } => {
725 // Caching is not possible, fallback to next message
726 }
727 anthropic::RequestContent::Text { cache_control, .. }
728 | anthropic::RequestContent::Thinking { cache_control, .. }
729 | anthropic::RequestContent::Image { cache_control, .. }
730 | anthropic::RequestContent::ToolUse { cache_control, .. }
731 | anthropic::RequestContent::ToolResult { cache_control, .. } => {
732 *cache_control = cache_control_value;
733 break;
734 }
735 }
736 }
737 }
738
739 new_messages.push(anthropic::Message {
740 role: anthropic_role,
741 content: anthropic_message_content,
742 });
743 }
744 Role::System => {
745 if !system_message.is_empty() {
746 system_message.push_str("\n\n");
747 }
748 system_message.push_str(&message.string_contents());
749 }
750 }
751 }
752
753 anthropic::Request {
754 model,
755 messages: new_messages,
756 max_tokens: max_output_tokens,
757 system: if system_message.is_empty() {
758 None
759 } else {
760 Some(anthropic::StringOrContents::String(system_message))
761 },
762 thinking: if request.thinking_allowed
763 && let AnthropicModelMode::Thinking { budget_tokens } = mode
764 {
765 Some(anthropic::Thinking::Enabled { budget_tokens })
766 } else {
767 None
768 },
769 tools: request
770 .tools
771 .into_iter()
772 .map(|tool| anthropic::Tool {
773 name: tool.name,
774 description: tool.description,
775 input_schema: tool.input_schema,
776 })
777 .collect(),
778 tool_choice: request.tool_choice.map(|choice| match choice {
779 LanguageModelToolChoice::Auto => anthropic::ToolChoice::Auto,
780 LanguageModelToolChoice::Any => anthropic::ToolChoice::Any,
781 LanguageModelToolChoice::None => anthropic::ToolChoice::None,
782 }),
783 metadata: None,
784 stop_sequences: Vec::new(),
785 temperature: request.temperature.or(Some(default_temperature)),
786 top_k: None,
787 top_p: None,
788 }
789}
790
791pub struct AnthropicEventMapper {
792 tool_uses_by_index: HashMap<usize, RawToolUse>,
793 usage: Usage,
794 stop_reason: StopReason,
795}
796
797impl AnthropicEventMapper {
798 pub fn new() -> Self {
799 Self {
800 tool_uses_by_index: HashMap::default(),
801 usage: Usage::default(),
802 stop_reason: StopReason::EndTurn,
803 }
804 }
805
806 pub fn map_stream(
807 mut self,
808 events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
809 ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
810 {
811 events.flat_map(move |event| {
812 futures::stream::iter(match event {
813 Ok(event) => self.map_event(event),
814 Err(error) => vec![Err(error.into())],
815 })
816 })
817 }
818
819 pub fn map_event(
820 &mut self,
821 event: Event,
822 ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
823 match event {
824 Event::ContentBlockStart {
825 index,
826 content_block,
827 } => match content_block {
828 ResponseContent::Text { text } => {
829 vec![Ok(LanguageModelCompletionEvent::Text(text))]
830 }
831 ResponseContent::Thinking { thinking } => {
832 vec![Ok(LanguageModelCompletionEvent::Thinking {
833 text: thinking,
834 signature: None,
835 })]
836 }
837 ResponseContent::RedactedThinking { data } => {
838 vec![Ok(LanguageModelCompletionEvent::RedactedThinking { data })]
839 }
840 ResponseContent::ToolUse { id, name, .. } => {
841 self.tool_uses_by_index.insert(
842 index,
843 RawToolUse {
844 id,
845 name,
846 input_json: String::new(),
847 },
848 );
849 Vec::new()
850 }
851 },
852 Event::ContentBlockDelta { index, delta } => match delta {
853 ContentDelta::TextDelta { text } => {
854 vec![Ok(LanguageModelCompletionEvent::Text(text))]
855 }
856 ContentDelta::ThinkingDelta { thinking } => {
857 vec![Ok(LanguageModelCompletionEvent::Thinking {
858 text: thinking,
859 signature: None,
860 })]
861 }
862 ContentDelta::SignatureDelta { signature } => {
863 vec![Ok(LanguageModelCompletionEvent::Thinking {
864 text: "".to_string(),
865 signature: Some(signature),
866 })]
867 }
868 ContentDelta::InputJsonDelta { partial_json } => {
869 if let Some(tool_use) = self.tool_uses_by_index.get_mut(&index) {
870 tool_use.input_json.push_str(&partial_json);
871
872 // Try to convert invalid (incomplete) JSON into
873 // valid JSON that serde can accept, e.g. by closing
874 // unclosed delimiters. This way, we can update the
875 // UI with whatever has been streamed back so far.
876 if let Ok(input) = serde_json::Value::from_str(
877 &partial_json_fixer::fix_json(&tool_use.input_json),
878 ) {
879 return vec![Ok(LanguageModelCompletionEvent::ToolUse(
880 LanguageModelToolUse {
881 id: tool_use.id.clone().into(),
882 name: tool_use.name.clone().into(),
883 is_input_complete: false,
884 raw_input: tool_use.input_json.clone(),
885 input,
886 thought_signature: None,
887 },
888 ))];
889 }
890 }
891 vec![]
892 }
893 },
894 Event::ContentBlockStop { index } => {
895 if let Some(tool_use) = self.tool_uses_by_index.remove(&index) {
896 let input_json = tool_use.input_json.trim();
897 let input_value = if input_json.is_empty() {
898 Ok(serde_json::Value::Object(serde_json::Map::default()))
899 } else {
900 serde_json::Value::from_str(input_json)
901 };
902 let event_result = match input_value {
903 Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
904 LanguageModelToolUse {
905 id: tool_use.id.into(),
906 name: tool_use.name.into(),
907 is_input_complete: true,
908 input,
909 raw_input: tool_use.input_json.clone(),
910 thought_signature: None,
911 },
912 )),
913 Err(json_parse_err) => {
914 Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
915 id: tool_use.id.into(),
916 tool_name: tool_use.name.into(),
917 raw_input: input_json.into(),
918 json_parse_error: json_parse_err.to_string(),
919 })
920 }
921 };
922
923 vec![event_result]
924 } else {
925 Vec::new()
926 }
927 }
928 Event::MessageStart { message } => {
929 update_usage(&mut self.usage, &message.usage);
930 vec![
931 Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
932 &self.usage,
933 ))),
934 Ok(LanguageModelCompletionEvent::StartMessage {
935 message_id: message.id,
936 }),
937 ]
938 }
939 Event::MessageDelta { delta, usage } => {
940 update_usage(&mut self.usage, &usage);
941 if let Some(stop_reason) = delta.stop_reason.as_deref() {
942 self.stop_reason = match stop_reason {
943 "end_turn" => StopReason::EndTurn,
944 "max_tokens" => StopReason::MaxTokens,
945 "tool_use" => StopReason::ToolUse,
946 "refusal" => StopReason::Refusal,
947 _ => {
948 log::error!("Unexpected anthropic stop_reason: {stop_reason}");
949 StopReason::EndTurn
950 }
951 };
952 }
953 vec![Ok(LanguageModelCompletionEvent::UsageUpdate(
954 convert_usage(&self.usage),
955 ))]
956 }
957 Event::MessageStop => {
958 vec![Ok(LanguageModelCompletionEvent::Stop(self.stop_reason))]
959 }
960 Event::Error { error } => {
961 vec![Err(error.into())]
962 }
963 _ => Vec::new(),
964 }
965 }
966}
967
968struct RawToolUse {
969 id: String,
970 name: String,
971 input_json: String,
972}
973
974/// Updates usage data by preferring counts from `new`.
975fn update_usage(usage: &mut Usage, new: &Usage) {
976 if let Some(input_tokens) = new.input_tokens {
977 usage.input_tokens = Some(input_tokens);
978 }
979 if let Some(output_tokens) = new.output_tokens {
980 usage.output_tokens = Some(output_tokens);
981 }
982 if let Some(cache_creation_input_tokens) = new.cache_creation_input_tokens {
983 usage.cache_creation_input_tokens = Some(cache_creation_input_tokens);
984 }
985 if let Some(cache_read_input_tokens) = new.cache_read_input_tokens {
986 usage.cache_read_input_tokens = Some(cache_read_input_tokens);
987 }
988}
989
990fn convert_usage(usage: &Usage) -> language_model::TokenUsage {
991 language_model::TokenUsage {
992 input_tokens: usage.input_tokens.unwrap_or(0),
993 output_tokens: usage.output_tokens.unwrap_or(0),
994 cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0),
995 cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0),
996 }
997}
998
999struct ConfigurationView {
1000 api_key_editor: Entity<InputField>,
1001 state: Entity<State>,
1002 load_credentials_task: Option<Task<()>>,
1003 target_agent: ConfigurationViewTargetAgent,
1004}
1005
1006impl ConfigurationView {
1007 const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
1008
1009 fn new(
1010 state: Entity<State>,
1011 target_agent: ConfigurationViewTargetAgent,
1012 window: &mut Window,
1013 cx: &mut Context<Self>,
1014 ) -> Self {
1015 cx.observe(&state, |_, _, cx| {
1016 cx.notify();
1017 })
1018 .detach();
1019
1020 let load_credentials_task = Some(cx.spawn({
1021 let state = state.clone();
1022 async move |this, cx| {
1023 if let Some(task) = state
1024 .update(cx, |state, cx| state.authenticate(cx))
1025 .log_err()
1026 {
1027 // We don't log an error, because "not signed in" is also an error.
1028 let _ = task.await;
1029 }
1030 this.update(cx, |this, cx| {
1031 this.load_credentials_task = None;
1032 cx.notify();
1033 })
1034 .log_err();
1035 }
1036 }));
1037
1038 Self {
1039 api_key_editor: cx.new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_TEXT)),
1040 state,
1041 load_credentials_task,
1042 target_agent,
1043 }
1044 }
1045
1046 fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
1047 let api_key = self.api_key_editor.read(cx).text(cx);
1048 if api_key.is_empty() {
1049 return;
1050 }
1051
1052 // url changes can cause the editor to be displayed again
1053 self.api_key_editor
1054 .update(cx, |editor, cx| editor.set_text("", window, cx));
1055
1056 let state = self.state.clone();
1057 cx.spawn_in(window, async move |_, cx| {
1058 state
1059 .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))?
1060 .await
1061 })
1062 .detach_and_log_err(cx);
1063 }
1064
1065 fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1066 self.api_key_editor
1067 .update(cx, |editor, cx| editor.set_text("", window, cx));
1068
1069 let state = self.state.clone();
1070 cx.spawn_in(window, async move |_, cx| {
1071 state
1072 .update(cx, |state, cx| state.set_api_key(None, cx))?
1073 .await
1074 })
1075 .detach_and_log_err(cx);
1076 }
1077
1078 fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
1079 !self.state.read(cx).is_authenticated()
1080 }
1081}
1082
1083impl Render for ConfigurationView {
1084 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1085 let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
1086 let configured_card_label = if env_var_set {
1087 format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
1088 } else {
1089 let api_url = AnthropicLanguageModelProvider::api_url(cx);
1090 if api_url == ANTHROPIC_API_URL {
1091 "API key configured".to_string()
1092 } else {
1093 format!("API key configured for {}", api_url)
1094 }
1095 };
1096
1097 if self.load_credentials_task.is_some() {
1098 div()
1099 .child(Label::new("Loading credentials..."))
1100 .into_any_element()
1101 } else if self.should_render_editor(cx) {
1102 v_flex()
1103 .size_full()
1104 .on_action(cx.listener(Self::save_api_key))
1105 .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
1106 ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(),
1107 ConfigurationViewTargetAgent::EditPrediction => "Anthropic for edit predictions".into(),
1108 ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
1109 })))
1110 .child(
1111 List::new()
1112 .child(
1113 ListBulletItem::new("")
1114 .child(Label::new("Create one by visiting"))
1115 .child(ButtonLink::new("Anthropic's settings", "https://console.anthropic.com/settings/keys"))
1116 )
1117 .child(
1118 ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
1119 )
1120 )
1121 .child(self.api_key_editor.clone())
1122 .child(
1123 Label::new(
1124 format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
1125 )
1126 .size(LabelSize::Small)
1127 .color(Color::Muted)
1128 .mt_0p5(),
1129 )
1130 .into_any_element()
1131 } else {
1132 ConfiguredApiCard::new(configured_card_label)
1133 .disabled(env_var_set)
1134 .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
1135 .when(env_var_set, |this| {
1136 this.tooltip_label(format!(
1137 "To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."
1138 ))
1139 })
1140 .into_any_element()
1141 }
1142 }
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147 use super::*;
1148 use anthropic::AnthropicModelMode;
1149 use language_model::{LanguageModelRequestMessage, MessageContent};
1150
1151 #[test]
1152 fn test_cache_control_only_on_last_segment() {
1153 let request = LanguageModelRequest {
1154 messages: vec![LanguageModelRequestMessage {
1155 role: Role::User,
1156 content: vec![
1157 MessageContent::Text("Some prompt".to_string()),
1158 MessageContent::Image(language_model::LanguageModelImage::empty()),
1159 MessageContent::Image(language_model::LanguageModelImage::empty()),
1160 MessageContent::Image(language_model::LanguageModelImage::empty()),
1161 MessageContent::Image(language_model::LanguageModelImage::empty()),
1162 ],
1163 cache: true,
1164 reasoning_details: None,
1165 }],
1166 thread_id: None,
1167 prompt_id: None,
1168 intent: None,
1169 mode: None,
1170 stop: vec![],
1171 temperature: None,
1172 tools: vec![],
1173 tool_choice: None,
1174 thinking_allowed: true,
1175 };
1176
1177 let anthropic_request = into_anthropic(
1178 request,
1179 "claude-3-5-sonnet".to_string(),
1180 0.7,
1181 4096,
1182 AnthropicModelMode::Default,
1183 );
1184
1185 assert_eq!(anthropic_request.messages.len(), 1);
1186
1187 let message = &anthropic_request.messages[0];
1188 assert_eq!(message.content.len(), 5);
1189
1190 assert!(matches!(
1191 message.content[0],
1192 anthropic::RequestContent::Text {
1193 cache_control: None,
1194 ..
1195 }
1196 ));
1197 for i in 1..3 {
1198 assert!(matches!(
1199 message.content[i],
1200 anthropic::RequestContent::Image {
1201 cache_control: None,
1202 ..
1203 }
1204 ));
1205 }
1206
1207 assert!(matches!(
1208 message.content[4],
1209 anthropic::RequestContent::Image {
1210 cache_control: Some(anthropic::CacheControl {
1211 cache_type: anthropic::CacheControlType::Ephemeral,
1212 }),
1213 ..
1214 }
1215 ));
1216 }
1217}