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