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