anthropic.rs

   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}