1pub mod telemetry;
2
3use anthropic::{
4 ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode, ContentDelta, CountTokensRequest, Event,
5 ResponseContent, ToolResultContent, ToolResultPart, Usage,
6};
7use anyhow::Result;
8use collections::{BTreeMap, HashMap};
9use credentials_provider::CredentialsProvider;
10use futures::{FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream};
11use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
12use http_client::HttpClient;
13use language_model::{
14 ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ApiKeyState, AuthenticateError,
15 ConfigurationViewTargetAgent, EnvVar, IconOrSvg, LanguageModel,
16 LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
17 LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
18 LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
19 LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
20 RateLimiter, Role, StopReason, env_var,
21};
22use settings::{Settings, SettingsStore};
23use std::pin::Pin;
24use std::str::FromStr;
25use std::sync::{Arc, LazyLock};
26use strum::IntoEnumIterator;
27use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
28use ui_input::InputField;
29use util::ResultExt;
30
31use crate::provider::util::{fix_streamed_json, parse_tool_arguments};
32
33pub use settings::AnthropicAvailableModel as AvailableModel;
34
35const PROVIDER_ID: LanguageModelProviderId = ANTHROPIC_PROVIDER_ID;
36const PROVIDER_NAME: LanguageModelProviderName = ANTHROPIC_PROVIDER_NAME;
37
38#[derive(Default, Clone, Debug, PartialEq)]
39pub struct AnthropicSettings {
40 pub api_url: String,
41 /// Extend Zed's list of Anthropic models.
42 pub available_models: Vec<AvailableModel>,
43}
44
45pub struct AnthropicLanguageModelProvider {
46 http_client: Arc<dyn HttpClient>,
47 state: Entity<State>,
48}
49
50const API_KEY_ENV_VAR_NAME: &str = "ANTHROPIC_API_KEY";
51static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
52
53pub struct State {
54 api_key_state: ApiKeyState,
55 credentials_provider: Arc<dyn CredentialsProvider>,
56}
57
58impl State {
59 fn is_authenticated(&self) -> bool {
60 self.api_key_state.has_key()
61 }
62
63 fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
64 let credentials_provider = self.credentials_provider.clone();
65 let api_url = AnthropicLanguageModelProvider::api_url(cx);
66 self.api_key_state.store(
67 api_url,
68 api_key,
69 |this| &mut this.api_key_state,
70 credentials_provider,
71 cx,
72 )
73 }
74
75 fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
76 let credentials_provider = self.credentials_provider.clone();
77 let api_url = AnthropicLanguageModelProvider::api_url(cx);
78 self.api_key_state.load_if_needed(
79 api_url,
80 |this| &mut this.api_key_state,
81 credentials_provider,
82 cx,
83 )
84 }
85}
86
87impl AnthropicLanguageModelProvider {
88 pub fn new(
89 http_client: Arc<dyn HttpClient>,
90 credentials_provider: Arc<dyn CredentialsProvider>,
91 cx: &mut App,
92 ) -> Self {
93 let state = cx.new(|cx| {
94 cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
95 let credentials_provider = this.credentials_provider.clone();
96 let api_url = Self::api_url(cx);
97 this.api_key_state.handle_url_change(
98 api_url,
99 |this| &mut this.api_key_state,
100 credentials_provider,
101 cx,
102 );
103 cx.notify();
104 })
105 .detach();
106 State {
107 api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
108 credentials_provider,
109 }
110 });
111
112 Self { http_client, state }
113 }
114
115 fn create_language_model(&self, model: anthropic::Model) -> Arc<dyn LanguageModel> {
116 Arc::new(AnthropicModel {
117 id: LanguageModelId::from(model.id().to_string()),
118 model,
119 state: self.state.clone(),
120 http_client: self.http_client.clone(),
121 request_limiter: RateLimiter::new(4),
122 })
123 }
124
125 fn settings(cx: &App) -> &AnthropicSettings {
126 &crate::AllLanguageModelSettings::get_global(cx).anthropic
127 }
128
129 fn api_url(cx: &App) -> SharedString {
130 let api_url = &Self::settings(cx).api_url;
131 if api_url.is_empty() {
132 ANTHROPIC_API_URL.into()
133 } else {
134 SharedString::new(api_url.as_str())
135 }
136 }
137}
138
139impl LanguageModelProviderState for AnthropicLanguageModelProvider {
140 type ObservableEntity = State;
141
142 fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
143 Some(self.state.clone())
144 }
145}
146
147impl LanguageModelProvider for AnthropicLanguageModelProvider {
148 fn id(&self) -> LanguageModelProviderId {
149 PROVIDER_ID
150 }
151
152 fn name(&self) -> LanguageModelProviderName {
153 PROVIDER_NAME
154 }
155
156 fn icon(&self) -> IconOrSvg {
157 IconOrSvg::Icon(IconName::AiAnthropic)
158 }
159
160 fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
161 Some(self.create_language_model(anthropic::Model::default()))
162 }
163
164 fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
165 Some(self.create_language_model(anthropic::Model::default_fast()))
166 }
167
168 fn recommended_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
169 [anthropic::Model::ClaudeSonnet4_6]
170 .into_iter()
171 .map(|model| self.create_language_model(model))
172 .collect()
173 }
174
175 fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
176 let mut models = BTreeMap::default();
177
178 // Add base models from anthropic::Model::iter()
179 for model in anthropic::Model::iter() {
180 if !matches!(model, anthropic::Model::Custom { .. }) {
181 models.insert(model.id().to_string(), model);
182 }
183 }
184
185 // Override with available models from settings
186 for model in &AnthropicLanguageModelProvider::settings(cx).available_models {
187 models.insert(
188 model.name.clone(),
189 anthropic::Model::Custom {
190 name: model.name.clone(),
191 display_name: model.display_name.clone(),
192 max_tokens: model.max_tokens,
193 tool_override: model.tool_override.clone(),
194 cache_configuration: model.cache_configuration.as_ref().map(|config| {
195 anthropic::AnthropicModelCacheConfiguration {
196 max_cache_anchors: config.max_cache_anchors,
197 should_speculate: config.should_speculate,
198 min_total_token: config.min_total_token,
199 }
200 }),
201 max_output_tokens: model.max_output_tokens,
202 default_temperature: model.default_temperature,
203 extra_beta_headers: model.extra_beta_headers.clone(),
204 mode: match model.mode.unwrap_or_default() {
205 settings::ModelMode::Default => AnthropicModelMode::Default,
206 settings::ModelMode::Thinking { budget_tokens } => {
207 AnthropicModelMode::Thinking { budget_tokens }
208 }
209 },
210 },
211 );
212 }
213
214 models
215 .into_values()
216 .map(|model| self.create_language_model(model))
217 .collect()
218 }
219
220 fn is_authenticated(&self, cx: &App) -> bool {
221 self.state.read(cx).is_authenticated()
222 }
223
224 fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
225 self.state.update(cx, |state, cx| state.authenticate(cx))
226 }
227
228 fn configuration_view(
229 &self,
230 target_agent: ConfigurationViewTargetAgent,
231 window: &mut Window,
232 cx: &mut App,
233 ) -> AnyView {
234 cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
235 .into()
236 }
237
238 fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
239 self.state
240 .update(cx, |state, cx| state.set_api_key(None, cx))
241 }
242}
243
244pub struct AnthropicModel {
245 id: LanguageModelId,
246 model: anthropic::Model,
247 state: Entity<State>,
248 http_client: Arc<dyn HttpClient>,
249 request_limiter: RateLimiter,
250}
251
252fn to_anthropic_content(content: MessageContent) -> Option<anthropic::RequestContent> {
253 match content {
254 MessageContent::Text(text) => {
255 let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) {
256 text.trim_end().to_string()
257 } else {
258 text
259 };
260 if !text.is_empty() {
261 Some(anthropic::RequestContent::Text {
262 text,
263 cache_control: None,
264 })
265 } else {
266 None
267 }
268 }
269 MessageContent::Thinking {
270 text: thinking,
271 signature,
272 } => {
273 if let Some(signature) = signature
274 && !thinking.is_empty()
275 {
276 Some(anthropic::RequestContent::Thinking {
277 thinking,
278 signature,
279 cache_control: None,
280 })
281 } else {
282 None
283 }
284 }
285 MessageContent::RedactedThinking(data) => {
286 if !data.is_empty() {
287 Some(anthropic::RequestContent::RedactedThinking { data })
288 } else {
289 None
290 }
291 }
292 MessageContent::Image(image) => Some(anthropic::RequestContent::Image {
293 source: anthropic::ImageSource {
294 source_type: "base64".to_string(),
295 media_type: "image/png".to_string(),
296 data: image.source.to_string(),
297 },
298 cache_control: None,
299 }),
300 MessageContent::ToolUse(tool_use) => Some(anthropic::RequestContent::ToolUse {
301 id: tool_use.id.to_string(),
302 name: tool_use.name.to_string(),
303 input: tool_use.input,
304 cache_control: None,
305 }),
306 MessageContent::ToolResult(tool_result) => Some(anthropic::RequestContent::ToolResult {
307 tool_use_id: tool_result.tool_use_id.to_string(),
308 is_error: tool_result.is_error,
309 content: match tool_result.content {
310 LanguageModelToolResultContent::Text(text) => {
311 ToolResultContent::Plain(text.to_string())
312 }
313 LanguageModelToolResultContent::Image(image) => {
314 ToolResultContent::Multipart(vec![ToolResultPart::Image {
315 source: anthropic::ImageSource {
316 source_type: "base64".to_string(),
317 media_type: "image/png".to_string(),
318 data: image.source.to_string(),
319 },
320 }])
321 }
322 },
323 cache_control: None,
324 }),
325 }
326}
327
328/// Convert a LanguageModelRequest to an Anthropic CountTokensRequest.
329pub fn into_anthropic_count_tokens_request(
330 request: LanguageModelRequest,
331 model: String,
332 mode: AnthropicModelMode,
333) -> CountTokensRequest {
334 let mut new_messages: Vec<anthropic::Message> = Vec::new();
335 let mut system_message = String::new();
336
337 for message in request.messages {
338 if message.contents_empty() {
339 continue;
340 }
341
342 match message.role {
343 Role::User | Role::Assistant => {
344 let anthropic_message_content: Vec<anthropic::RequestContent> = message
345 .content
346 .into_iter()
347 .filter_map(to_anthropic_content)
348 .collect();
349 let anthropic_role = match message.role {
350 Role::User => anthropic::Role::User,
351 Role::Assistant => anthropic::Role::Assistant,
352 Role::System => unreachable!("System role should never occur here"),
353 };
354 if anthropic_message_content.is_empty() {
355 continue;
356 }
357
358 if let Some(last_message) = new_messages.last_mut()
359 && last_message.role == anthropic_role
360 {
361 last_message.content.extend(anthropic_message_content);
362 continue;
363 }
364
365 new_messages.push(anthropic::Message {
366 role: anthropic_role,
367 content: anthropic_message_content,
368 });
369 }
370 Role::System => {
371 if !system_message.is_empty() {
372 system_message.push_str("\n\n");
373 }
374 system_message.push_str(&message.string_contents());
375 }
376 }
377 }
378
379 CountTokensRequest {
380 model,
381 messages: new_messages,
382 system: if system_message.is_empty() {
383 None
384 } else {
385 Some(anthropic::StringOrContents::String(system_message))
386 },
387 thinking: if request.thinking_allowed {
388 match mode {
389 AnthropicModelMode::Thinking { budget_tokens } => {
390 Some(anthropic::Thinking::Enabled { budget_tokens })
391 }
392 AnthropicModelMode::AdaptiveThinking => Some(anthropic::Thinking::Adaptive),
393 AnthropicModelMode::Default => None,
394 }
395 } else {
396 None
397 },
398 tools: request
399 .tools
400 .into_iter()
401 .map(|tool| anthropic::Tool {
402 name: tool.name,
403 description: tool.description,
404 input_schema: tool.input_schema,
405 eager_input_streaming: tool.use_input_streaming,
406 })
407 .collect(),
408 tool_choice: request.tool_choice.map(|choice| match choice {
409 LanguageModelToolChoice::Auto => anthropic::ToolChoice::Auto,
410 LanguageModelToolChoice::Any => anthropic::ToolChoice::Any,
411 LanguageModelToolChoice::None => anthropic::ToolChoice::None,
412 }),
413 }
414}
415
416/// Estimate tokens using tiktoken. Used as a fallback when the API is unavailable,
417/// or by providers (like Zed Cloud) that don't have direct Anthropic API access.
418pub fn count_anthropic_tokens_with_tiktoken(request: LanguageModelRequest) -> Result<u64> {
419 let messages = request.messages;
420 let mut tokens_from_images = 0;
421 let mut string_messages = Vec::with_capacity(messages.len());
422
423 for message in messages {
424 let mut string_contents = String::new();
425
426 for content in message.content {
427 match content {
428 MessageContent::Text(text) => {
429 string_contents.push_str(&text);
430 }
431 MessageContent::Thinking { .. } => {
432 // Thinking blocks are not included in the input token count.
433 }
434 MessageContent::RedactedThinking(_) => {
435 // Thinking blocks are not included in the input token count.
436 }
437 MessageContent::Image(image) => {
438 tokens_from_images += image.estimate_tokens();
439 }
440 MessageContent::ToolUse(_tool_use) => {
441 // TODO: Estimate token usage from tool uses.
442 }
443 MessageContent::ToolResult(tool_result) => match &tool_result.content {
444 LanguageModelToolResultContent::Text(text) => {
445 string_contents.push_str(text);
446 }
447 LanguageModelToolResultContent::Image(image) => {
448 tokens_from_images += image.estimate_tokens();
449 }
450 },
451 }
452 }
453
454 if !string_contents.is_empty() {
455 string_messages.push(tiktoken_rs::ChatCompletionRequestMessage {
456 role: match message.role {
457 Role::User => "user".into(),
458 Role::Assistant => "assistant".into(),
459 Role::System => "system".into(),
460 },
461 content: Some(string_contents),
462 name: None,
463 function_call: None,
464 });
465 }
466 }
467
468 // Tiktoken doesn't yet support these models, so we manually use the
469 // same tokenizer as GPT-4.
470 tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages)
471 .map(|tokens| (tokens + tokens_from_images) as u64)
472}
473
474impl AnthropicModel {
475 fn stream_completion(
476 &self,
477 request: anthropic::Request,
478 cx: &AsyncApp,
479 ) -> BoxFuture<
480 'static,
481 Result<
482 BoxStream<'static, Result<anthropic::Event, AnthropicError>>,
483 LanguageModelCompletionError,
484 >,
485 > {
486 let http_client = self.http_client.clone();
487
488 let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
489 let api_url = AnthropicLanguageModelProvider::api_url(cx);
490 (state.api_key_state.key(&api_url), api_url)
491 });
492
493 let beta_headers = self.model.beta_headers();
494
495 async move {
496 let Some(api_key) = api_key else {
497 return Err(LanguageModelCompletionError::NoApiKey {
498 provider: PROVIDER_NAME,
499 });
500 };
501 let request = anthropic::stream_completion(
502 http_client.as_ref(),
503 &api_url,
504 &api_key,
505 request,
506 beta_headers,
507 );
508 request.await.map_err(Into::into)
509 }
510 .boxed()
511 }
512}
513
514impl LanguageModel for AnthropicModel {
515 fn id(&self) -> LanguageModelId {
516 self.id.clone()
517 }
518
519 fn name(&self) -> LanguageModelName {
520 LanguageModelName::from(self.model.display_name().to_string())
521 }
522
523 fn provider_id(&self) -> LanguageModelProviderId {
524 PROVIDER_ID
525 }
526
527 fn provider_name(&self) -> LanguageModelProviderName {
528 PROVIDER_NAME
529 }
530
531 fn supports_tools(&self) -> bool {
532 true
533 }
534
535 fn supports_images(&self) -> bool {
536 true
537 }
538
539 fn supports_streaming_tools(&self) -> bool {
540 true
541 }
542
543 fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
544 match choice {
545 LanguageModelToolChoice::Auto
546 | LanguageModelToolChoice::Any
547 | LanguageModelToolChoice::None => true,
548 }
549 }
550
551 fn supports_thinking(&self) -> bool {
552 self.model.supports_thinking()
553 }
554
555 fn supported_effort_levels(&self) -> Vec<language_model::LanguageModelEffortLevel> {
556 if self.model.supports_adaptive_thinking() {
557 vec![
558 language_model::LanguageModelEffortLevel {
559 name: "Low".into(),
560 value: "low".into(),
561 is_default: false,
562 },
563 language_model::LanguageModelEffortLevel {
564 name: "Medium".into(),
565 value: "medium".into(),
566 is_default: false,
567 },
568 language_model::LanguageModelEffortLevel {
569 name: "High".into(),
570 value: "high".into(),
571 is_default: true,
572 },
573 language_model::LanguageModelEffortLevel {
574 name: "Max".into(),
575 value: "max".into(),
576 is_default: false,
577 },
578 ]
579 } else {
580 Vec::new()
581 }
582 }
583
584 fn telemetry_id(&self) -> String {
585 format!("anthropic/{}", self.model.id())
586 }
587
588 fn api_key(&self, cx: &App) -> Option<String> {
589 self.state.read_with(cx, |state, cx| {
590 let api_url = AnthropicLanguageModelProvider::api_url(cx);
591 state.api_key_state.key(&api_url).map(|key| key.to_string())
592 })
593 }
594
595 fn max_token_count(&self) -> u64 {
596 self.model.max_token_count()
597 }
598
599 fn max_output_tokens(&self) -> Option<u64> {
600 Some(self.model.max_output_tokens())
601 }
602
603 fn count_tokens(
604 &self,
605 request: LanguageModelRequest,
606 cx: &App,
607 ) -> BoxFuture<'static, Result<u64>> {
608 let http_client = self.http_client.clone();
609 let model_id = self.model.request_id().to_string();
610 let mode = self.model.mode();
611
612 let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
613 let api_url = AnthropicLanguageModelProvider::api_url(cx);
614 (
615 state.api_key_state.key(&api_url).map(|k| k.to_string()),
616 api_url.to_string(),
617 )
618 });
619
620 async move {
621 // If no API key, fall back to tiktoken estimation
622 let Some(api_key) = api_key else {
623 return count_anthropic_tokens_with_tiktoken(request);
624 };
625
626 let count_request =
627 into_anthropic_count_tokens_request(request.clone(), model_id, mode);
628
629 match anthropic::count_tokens(http_client.as_ref(), &api_url, &api_key, count_request)
630 .await
631 {
632 Ok(response) => Ok(response.input_tokens),
633 Err(err) => {
634 log::error!(
635 "Anthropic count_tokens API failed, falling back to tiktoken: {err:?}"
636 );
637 count_anthropic_tokens_with_tiktoken(request)
638 }
639 }
640 }
641 .boxed()
642 }
643
644 fn stream_completion(
645 &self,
646 request: LanguageModelRequest,
647 cx: &AsyncApp,
648 ) -> BoxFuture<
649 'static,
650 Result<
651 BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
652 LanguageModelCompletionError,
653 >,
654 > {
655 let request = into_anthropic(
656 request,
657 self.model.request_id().into(),
658 self.model.default_temperature(),
659 self.model.max_output_tokens(),
660 self.model.mode(),
661 );
662 let request = self.stream_completion(request, cx);
663 let future = self.request_limiter.stream(async move {
664 let response = request.await?;
665 Ok(AnthropicEventMapper::new().map_stream(response))
666 });
667 async move { Ok(future.await?.boxed()) }.boxed()
668 }
669
670 fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
671 self.model
672 .cache_configuration()
673 .map(|config| LanguageModelCacheConfiguration {
674 max_cache_anchors: config.max_cache_anchors,
675 should_speculate: config.should_speculate,
676 min_total_token: config.min_total_token,
677 })
678 }
679}
680
681pub fn into_anthropic(
682 request: LanguageModelRequest,
683 model: String,
684 default_temperature: f32,
685 max_output_tokens: u64,
686 mode: AnthropicModelMode,
687) -> anthropic::Request {
688 let mut new_messages: Vec<anthropic::Message> = Vec::new();
689 let mut system_message = String::new();
690
691 for message in request.messages {
692 if message.contents_empty() {
693 continue;
694 }
695
696 match message.role {
697 Role::User | Role::Assistant => {
698 let mut anthropic_message_content: Vec<anthropic::RequestContent> = message
699 .content
700 .into_iter()
701 .filter_map(to_anthropic_content)
702 .collect();
703 let anthropic_role = match message.role {
704 Role::User => anthropic::Role::User,
705 Role::Assistant => anthropic::Role::Assistant,
706 Role::System => unreachable!("System role should never occur here"),
707 };
708 if anthropic_message_content.is_empty() {
709 continue;
710 }
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 match mode {
766 AnthropicModelMode::Thinking { budget_tokens } => {
767 Some(anthropic::Thinking::Enabled { budget_tokens })
768 }
769 AnthropicModelMode::AdaptiveThinking => Some(anthropic::Thinking::Adaptive),
770 AnthropicModelMode::Default => None,
771 }
772 } else {
773 None
774 },
775 tools: request
776 .tools
777 .into_iter()
778 .map(|tool| anthropic::Tool {
779 name: tool.name,
780 description: tool.description,
781 input_schema: tool.input_schema,
782 eager_input_streaming: tool.use_input_streaming,
783 })
784 .collect(),
785 tool_choice: request.tool_choice.map(|choice| match choice {
786 LanguageModelToolChoice::Auto => anthropic::ToolChoice::Auto,
787 LanguageModelToolChoice::Any => anthropic::ToolChoice::Any,
788 LanguageModelToolChoice::None => anthropic::ToolChoice::None,
789 }),
790 metadata: None,
791 output_config: if request.thinking_allowed
792 && matches!(mode, AnthropicModelMode::AdaptiveThinking)
793 {
794 request.thinking_effort.as_deref().and_then(|effort| {
795 let effort = match effort {
796 "low" => Some(anthropic::Effort::Low),
797 "medium" => Some(anthropic::Effort::Medium),
798 "high" => Some(anthropic::Effort::High),
799 "max" => Some(anthropic::Effort::Max),
800 _ => None,
801 };
802 effort.map(|effort| anthropic::OutputConfig {
803 effort: Some(effort),
804 })
805 })
806 } else {
807 None
808 },
809 stop_sequences: Vec::new(),
810 speed: request.speed.map(From::from),
811 temperature: request.temperature.or(Some(default_temperature)),
812 top_k: None,
813 top_p: None,
814 }
815}
816
817pub struct AnthropicEventMapper {
818 tool_uses_by_index: HashMap<usize, RawToolUse>,
819 usage: Usage,
820 stop_reason: StopReason,
821}
822
823impl AnthropicEventMapper {
824 pub fn new() -> Self {
825 Self {
826 tool_uses_by_index: HashMap::default(),
827 usage: Usage::default(),
828 stop_reason: StopReason::EndTurn,
829 }
830 }
831
832 pub fn map_stream(
833 mut self,
834 events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
835 ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
836 {
837 events.flat_map(move |event| {
838 futures::stream::iter(match event {
839 Ok(event) => self.map_event(event),
840 Err(error) => vec![Err(error.into())],
841 })
842 })
843 }
844
845 pub fn map_event(
846 &mut self,
847 event: Event,
848 ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
849 match event {
850 Event::ContentBlockStart {
851 index,
852 content_block,
853 } => match content_block {
854 ResponseContent::Text { text } => {
855 vec![Ok(LanguageModelCompletionEvent::Text(text))]
856 }
857 ResponseContent::Thinking { thinking } => {
858 vec![Ok(LanguageModelCompletionEvent::Thinking {
859 text: thinking,
860 signature: None,
861 })]
862 }
863 ResponseContent::RedactedThinking { data } => {
864 vec![Ok(LanguageModelCompletionEvent::RedactedThinking { data })]
865 }
866 ResponseContent::ToolUse { id, name, .. } => {
867 self.tool_uses_by_index.insert(
868 index,
869 RawToolUse {
870 id,
871 name,
872 input_json: String::new(),
873 },
874 );
875 Vec::new()
876 }
877 },
878 Event::ContentBlockDelta { index, delta } => match delta {
879 ContentDelta::TextDelta { text } => {
880 vec![Ok(LanguageModelCompletionEvent::Text(text))]
881 }
882 ContentDelta::ThinkingDelta { thinking } => {
883 vec![Ok(LanguageModelCompletionEvent::Thinking {
884 text: thinking,
885 signature: None,
886 })]
887 }
888 ContentDelta::SignatureDelta { signature } => {
889 vec![Ok(LanguageModelCompletionEvent::Thinking {
890 text: "".to_string(),
891 signature: Some(signature),
892 })]
893 }
894 ContentDelta::InputJsonDelta { partial_json } => {
895 if let Some(tool_use) = self.tool_uses_by_index.get_mut(&index) {
896 tool_use.input_json.push_str(&partial_json);
897
898 // Try to convert invalid (incomplete) JSON into
899 // valid JSON that serde can accept, e.g. by closing
900 // unclosed delimiters. This way, we can update the
901 // UI with whatever has been streamed back so far.
902 if let Ok(input) =
903 serde_json::Value::from_str(&fix_streamed_json(&tool_use.input_json))
904 {
905 return vec![Ok(LanguageModelCompletionEvent::ToolUse(
906 LanguageModelToolUse {
907 id: tool_use.id.clone().into(),
908 name: tool_use.name.clone().into(),
909 is_input_complete: false,
910 raw_input: tool_use.input_json.clone(),
911 input,
912 thought_signature: None,
913 },
914 ))];
915 }
916 }
917 vec![]
918 }
919 },
920 Event::ContentBlockStop { index } => {
921 if let Some(tool_use) = self.tool_uses_by_index.remove(&index) {
922 let input_json = tool_use.input_json.trim();
923 let event_result = match parse_tool_arguments(input_json) {
924 Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
925 LanguageModelToolUse {
926 id: tool_use.id.into(),
927 name: tool_use.name.into(),
928 is_input_complete: true,
929 input,
930 raw_input: tool_use.input_json.clone(),
931 thought_signature: None,
932 },
933 )),
934 Err(json_parse_err) => {
935 Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
936 id: tool_use.id.into(),
937 tool_name: tool_use.name.into(),
938 raw_input: input_json.into(),
939 json_parse_error: json_parse_err.to_string(),
940 })
941 }
942 };
943
944 vec![event_result]
945 } else {
946 Vec::new()
947 }
948 }
949 Event::MessageStart { message } => {
950 update_usage(&mut self.usage, &message.usage);
951 vec![
952 Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
953 &self.usage,
954 ))),
955 Ok(LanguageModelCompletionEvent::StartMessage {
956 message_id: message.id,
957 }),
958 ]
959 }
960 Event::MessageDelta { delta, usage } => {
961 update_usage(&mut self.usage, &usage);
962 if let Some(stop_reason) = delta.stop_reason.as_deref() {
963 self.stop_reason = match stop_reason {
964 "end_turn" => StopReason::EndTurn,
965 "max_tokens" => StopReason::MaxTokens,
966 "tool_use" => StopReason::ToolUse,
967 "refusal" => StopReason::Refusal,
968 _ => {
969 log::error!("Unexpected anthropic stop_reason: {stop_reason}");
970 StopReason::EndTurn
971 }
972 };
973 }
974 vec![Ok(LanguageModelCompletionEvent::UsageUpdate(
975 convert_usage(&self.usage),
976 ))]
977 }
978 Event::MessageStop => {
979 vec![Ok(LanguageModelCompletionEvent::Stop(self.stop_reason))]
980 }
981 Event::Error { error } => {
982 vec![Err(error.into())]
983 }
984 _ => Vec::new(),
985 }
986 }
987}
988
989struct RawToolUse {
990 id: String,
991 name: String,
992 input_json: String,
993}
994
995/// Updates usage data by preferring counts from `new`.
996fn update_usage(usage: &mut Usage, new: &Usage) {
997 if let Some(input_tokens) = new.input_tokens {
998 usage.input_tokens = Some(input_tokens);
999 }
1000 if let Some(output_tokens) = new.output_tokens {
1001 usage.output_tokens = Some(output_tokens);
1002 }
1003 if let Some(cache_creation_input_tokens) = new.cache_creation_input_tokens {
1004 usage.cache_creation_input_tokens = Some(cache_creation_input_tokens);
1005 }
1006 if let Some(cache_read_input_tokens) = new.cache_read_input_tokens {
1007 usage.cache_read_input_tokens = Some(cache_read_input_tokens);
1008 }
1009}
1010
1011fn convert_usage(usage: &Usage) -> language_model::TokenUsage {
1012 language_model::TokenUsage {
1013 input_tokens: usage.input_tokens.unwrap_or(0),
1014 output_tokens: usage.output_tokens.unwrap_or(0),
1015 cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0),
1016 cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0),
1017 }
1018}
1019
1020struct ConfigurationView {
1021 api_key_editor: Entity<InputField>,
1022 state: Entity<State>,
1023 load_credentials_task: Option<Task<()>>,
1024 target_agent: ConfigurationViewTargetAgent,
1025}
1026
1027impl ConfigurationView {
1028 const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
1029
1030 fn new(
1031 state: Entity<State>,
1032 target_agent: ConfigurationViewTargetAgent,
1033 window: &mut Window,
1034 cx: &mut Context<Self>,
1035 ) -> Self {
1036 cx.observe(&state, |_, _, cx| {
1037 cx.notify();
1038 })
1039 .detach();
1040
1041 let load_credentials_task = Some(cx.spawn({
1042 let state = state.clone();
1043 async move |this, cx| {
1044 let task = state.update(cx, |state, cx| state.authenticate(cx));
1045 // We don't log an error, because "not signed in" is also an error.
1046 let _ = task.await;
1047 this.update(cx, |this, cx| {
1048 this.load_credentials_task = None;
1049 cx.notify();
1050 })
1051 .log_err();
1052 }
1053 }));
1054
1055 Self {
1056 api_key_editor: cx.new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_TEXT)),
1057 state,
1058 load_credentials_task,
1059 target_agent,
1060 }
1061 }
1062
1063 fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
1064 let api_key = self.api_key_editor.read(cx).text(cx);
1065 if api_key.is_empty() {
1066 return;
1067 }
1068
1069 // url changes can cause the editor to be displayed again
1070 self.api_key_editor
1071 .update(cx, |editor, cx| editor.set_text("", window, cx));
1072
1073 let state = self.state.clone();
1074 cx.spawn_in(window, async move |_, cx| {
1075 state
1076 .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
1077 .await
1078 })
1079 .detach_and_log_err(cx);
1080 }
1081
1082 fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1083 self.api_key_editor
1084 .update(cx, |editor, cx| editor.set_text("", window, cx));
1085
1086 let state = self.state.clone();
1087 cx.spawn_in(window, async move |_, cx| {
1088 state
1089 .update(cx, |state, cx| state.set_api_key(None, cx))
1090 .await
1091 })
1092 .detach_and_log_err(cx);
1093 }
1094
1095 fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
1096 !self.state.read(cx).is_authenticated()
1097 }
1098}
1099
1100impl Render for ConfigurationView {
1101 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1102 let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
1103 let configured_card_label = if env_var_set {
1104 format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
1105 } else {
1106 let api_url = AnthropicLanguageModelProvider::api_url(cx);
1107 if api_url == ANTHROPIC_API_URL {
1108 "API key configured".to_string()
1109 } else {
1110 format!("API key configured for {}", api_url)
1111 }
1112 };
1113
1114 if self.load_credentials_task.is_some() {
1115 div()
1116 .child(Label::new("Loading credentials..."))
1117 .into_any_element()
1118 } else if self.should_render_editor(cx) {
1119 v_flex()
1120 .size_full()
1121 .on_action(cx.listener(Self::save_api_key))
1122 .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
1123 ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(),
1124 ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
1125 })))
1126 .child(
1127 List::new()
1128 .child(
1129 ListBulletItem::new("")
1130 .child(Label::new("Create one by visiting"))
1131 .child(ButtonLink::new("Anthropic's settings", "https://console.anthropic.com/settings/keys"))
1132 )
1133 .child(
1134 ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
1135 )
1136 )
1137 .child(self.api_key_editor.clone())
1138 .child(
1139 Label::new(
1140 format!("You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
1141 )
1142 .size(LabelSize::Small)
1143 .color(Color::Muted)
1144 .mt_0p5(),
1145 )
1146 .into_any_element()
1147 } else {
1148 ConfiguredApiCard::new(configured_card_label)
1149 .disabled(env_var_set)
1150 .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
1151 .when(env_var_set, |this| {
1152 this.tooltip_label(format!(
1153 "To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."
1154 ))
1155 })
1156 .into_any_element()
1157 }
1158 }
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163 use super::*;
1164 use anthropic::AnthropicModelMode;
1165 use language_model::{LanguageModelRequestMessage, MessageContent};
1166
1167 #[test]
1168 fn test_cache_control_only_on_last_segment() {
1169 let request = LanguageModelRequest {
1170 messages: vec![LanguageModelRequestMessage {
1171 role: Role::User,
1172 content: vec![
1173 MessageContent::Text("Some prompt".to_string()),
1174 MessageContent::Image(language_model::LanguageModelImage::empty()),
1175 MessageContent::Image(language_model::LanguageModelImage::empty()),
1176 MessageContent::Image(language_model::LanguageModelImage::empty()),
1177 MessageContent::Image(language_model::LanguageModelImage::empty()),
1178 ],
1179 cache: true,
1180 reasoning_details: None,
1181 }],
1182 thread_id: None,
1183 prompt_id: None,
1184 intent: None,
1185 stop: vec![],
1186 temperature: None,
1187 tools: vec![],
1188 tool_choice: None,
1189 thinking_allowed: true,
1190 thinking_effort: None,
1191 speed: None,
1192 };
1193
1194 let anthropic_request = into_anthropic(
1195 request,
1196 "claude-3-5-sonnet".to_string(),
1197 0.7,
1198 4096,
1199 AnthropicModelMode::Default,
1200 );
1201
1202 assert_eq!(anthropic_request.messages.len(), 1);
1203
1204 let message = &anthropic_request.messages[0];
1205 assert_eq!(message.content.len(), 5);
1206
1207 assert!(matches!(
1208 message.content[0],
1209 anthropic::RequestContent::Text {
1210 cache_control: None,
1211 ..
1212 }
1213 ));
1214 for i in 1..3 {
1215 assert!(matches!(
1216 message.content[i],
1217 anthropic::RequestContent::Image {
1218 cache_control: None,
1219 ..
1220 }
1221 ));
1222 }
1223
1224 assert!(matches!(
1225 message.content[4],
1226 anthropic::RequestContent::Image {
1227 cache_control: Some(anthropic::CacheControl {
1228 cache_type: anthropic::CacheControlType::Ephemeral,
1229 }),
1230 ..
1231 }
1232 ));
1233 }
1234
1235 fn request_with_assistant_content(
1236 assistant_content: Vec<MessageContent>,
1237 ) -> anthropic::Request {
1238 let mut request = LanguageModelRequest {
1239 messages: vec![LanguageModelRequestMessage {
1240 role: Role::User,
1241 content: vec![MessageContent::Text("Hello".to_string())],
1242 cache: false,
1243 reasoning_details: None,
1244 }],
1245 thinking_effort: None,
1246 thread_id: None,
1247 prompt_id: None,
1248 intent: None,
1249 stop: vec![],
1250 temperature: None,
1251 tools: vec![],
1252 tool_choice: None,
1253 thinking_allowed: true,
1254 speed: None,
1255 };
1256 request.messages.push(LanguageModelRequestMessage {
1257 role: Role::Assistant,
1258 content: assistant_content,
1259 cache: false,
1260 reasoning_details: None,
1261 });
1262 into_anthropic(
1263 request,
1264 "claude-sonnet-4-5".to_string(),
1265 1.0,
1266 16000,
1267 AnthropicModelMode::Thinking {
1268 budget_tokens: Some(10000),
1269 },
1270 )
1271 }
1272
1273 #[test]
1274 fn test_unsigned_thinking_blocks_stripped() {
1275 let result = request_with_assistant_content(vec![
1276 MessageContent::Thinking {
1277 text: "Cancelled mid-think, no signature".to_string(),
1278 signature: None,
1279 },
1280 MessageContent::Text("Some response text".to_string()),
1281 ]);
1282
1283 let assistant_message = result
1284 .messages
1285 .iter()
1286 .find(|m| m.role == anthropic::Role::Assistant)
1287 .expect("assistant message should still exist");
1288
1289 assert_eq!(
1290 assistant_message.content.len(),
1291 1,
1292 "Only the text content should remain; unsigned thinking block should be stripped"
1293 );
1294 assert!(matches!(
1295 &assistant_message.content[0],
1296 anthropic::RequestContent::Text { text, .. } if text == "Some response text"
1297 ));
1298 }
1299
1300 #[test]
1301 fn test_signed_thinking_blocks_preserved() {
1302 let result = request_with_assistant_content(vec![
1303 MessageContent::Thinking {
1304 text: "Completed thinking".to_string(),
1305 signature: Some("valid-signature".to_string()),
1306 },
1307 MessageContent::Text("Response".to_string()),
1308 ]);
1309
1310 let assistant_message = result
1311 .messages
1312 .iter()
1313 .find(|m| m.role == anthropic::Role::Assistant)
1314 .expect("assistant message should exist");
1315
1316 assert_eq!(
1317 assistant_message.content.len(),
1318 2,
1319 "Both the signed thinking block and text should be preserved"
1320 );
1321 assert!(matches!(
1322 &assistant_message.content[0],
1323 anthropic::RequestContent::Thinking { thinking, signature, .. }
1324 if thinking == "Completed thinking" && signature == "valid-signature"
1325 ));
1326 }
1327
1328 #[test]
1329 fn test_only_unsigned_thinking_block_omits_entire_message() {
1330 let result = request_with_assistant_content(vec![MessageContent::Thinking {
1331 text: "Cancelled before any text or signature".to_string(),
1332 signature: None,
1333 }]);
1334
1335 let assistant_messages: Vec<_> = result
1336 .messages
1337 .iter()
1338 .filter(|m| m.role == anthropic::Role::Assistant)
1339 .collect();
1340
1341 assert_eq!(
1342 assistant_messages.len(),
1343 0,
1344 "An assistant message whose only content was an unsigned thinking block \
1345 should be omitted entirely"
1346 );
1347 }
1348}