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