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