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