Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:57:11.306Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

anthropic.rs

635 lines · 22.0 KB · rust
1pub mod telemetry;
2
3use anthropic::{ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode};
4use anyhow::Result;
5use collections::BTreeMap;
6use credentials_provider::CredentialsProvider;
7use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
8use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task};
9use http_client::{CustomHeaders, HttpClient};
10use language_model::{
11    ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ApiKeyConfiguration, ApiKeyState,
12    AuthenticateError, EnvVar, FastModeConfirmation, IconOrSvg, LanguageModel,
13    LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
14    LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
15    LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
16    ProviderSettingsView, RateLimiter, env_var,
17};
18use settings::{Settings, SettingsStore};
19use std::sync::{Arc, LazyLock};
20use ui::IconName;
21
22pub use anthropic::completion::{AnthropicEventMapper, AnthropicPromptCacheMode, into_anthropic};
23pub use settings::AnthropicAvailableModel as AvailableModel;
24
25const PROVIDER_ID: LanguageModelProviderId = ANTHROPIC_PROVIDER_ID;
26const PROVIDER_NAME: LanguageModelProviderName = ANTHROPIC_PROVIDER_NAME;
27
28#[derive(Default, Clone, Debug, PartialEq)]
29pub struct AnthropicSettings {
30    pub api_url: String,
31    /// Extend Zed's list of Anthropic models.
32    pub available_models: Vec<AvailableModel>,
33    /// User-configured headers added to every Anthropic request.
34    pub custom_headers: CustomHeaders,
35}
36
37pub struct AnthropicLanguageModelProvider {
38    http_client: Arc<dyn HttpClient>,
39    state: Entity<State>,
40}
41
42const API_KEY_ENV_VAR_NAME: &str = "ANTHROPIC_API_KEY";
43static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
44
45pub(crate) const RESERVED_HEADER_NAMES: &[&str] =
46    &["X-Api-Key", "Anthropic-Version", "Anthropic-Beta"];
47
48pub struct State {
49    api_key_state: ApiKeyState,
50    credentials_provider: Arc<dyn CredentialsProvider>,
51    http_client: Arc<dyn HttpClient>,
52    fetched_models: Vec<anthropic::Model>,
53    fetch_models_task: Option<Task<Result<()>>>,
54}
55
56impl State {
57    fn is_authenticated(&self) -> bool {
58        self.api_key_state.has_key()
59    }
60
61    fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
62        let credentials_provider = self.credentials_provider.clone();
63        let api_url = AnthropicLanguageModelProvider::api_url(cx);
64        let should_fetch_models = api_key.is_some();
65        let task = self.api_key_state.store(
66            api_url,
67            api_key,
68            |this| &mut this.api_key_state,
69            credentials_provider,
70            cx,
71        );
72        self.fetched_models.clear();
73        cx.spawn(async move |this, cx| {
74            let result = task.await;
75            if result.is_ok() && should_fetch_models {
76                this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
77                    .ok();
78            }
79            result
80        })
81    }
82
83    fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
84        let credentials_provider = self.credentials_provider.clone();
85        let api_url = AnthropicLanguageModelProvider::api_url(cx);
86        let task = self.api_key_state.load_if_needed(
87            api_url,
88            |this| &mut this.api_key_state,
89            credentials_provider,
90            cx,
91        );
92
93        cx.spawn(async move |this, cx| {
94            let result = task.await;
95            if result.is_ok() {
96                this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
97                    .ok();
98            }
99            result
100        })
101    }
102
103    fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
104        let http_client = self.http_client.clone();
105        let api_url = AnthropicLanguageModelProvider::api_url(cx);
106        let Some(api_key) = self.api_key_state.key(&api_url) else {
107            return Task::ready(Err(anyhow::anyhow!(
108                "cannot fetch Anthropic models without an API key"
109            )));
110        };
111        let extra_headers = AnthropicLanguageModelProvider::settings(cx)
112            .custom_headers
113            .clone();
114
115        cx.spawn(async move |this, cx| {
116            let models = anthropic::list_models(
117                http_client.as_ref(),
118                &api_url,
119                api_key.as_ref(),
120                &extra_headers,
121            )
122            .await?;
123
124            this.update(cx, |this, cx| {
125                this.fetched_models = models;
126                cx.notify();
127            })
128        })
129    }
130
131    fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) {
132        let task = self.fetch_models(cx);
133        self.fetch_models_task.replace(task);
134    }
135}
136
137impl AnthropicLanguageModelProvider {
138    pub fn new(
139        http_client: Arc<dyn HttpClient>,
140        credentials_provider: Arc<dyn CredentialsProvider>,
141        cx: &mut App,
142    ) -> Self {
143        let state = cx.new(|cx| {
144            cx.observe_global::<SettingsStore>({
145                let mut last_api_url = Self::api_url(cx);
146                move |this: &mut State, cx| {
147                    let credentials_provider = this.credentials_provider.clone();
148                    let api_url = Self::api_url(cx);
149                    let url_changed = api_url != last_api_url;
150                    last_api_url = api_url.clone();
151                    this.api_key_state.handle_url_change(
152                        api_url,
153                        |this| &mut this.api_key_state,
154                        credentials_provider,
155                        cx,
156                    );
157                    if url_changed {
158                        this.fetched_models.clear();
159                        this.authenticate(cx).detach();
160                    }
161                    cx.notify();
162                }
163            })
164            .detach();
165            State {
166                api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
167                credentials_provider,
168                http_client: http_client.clone(),
169                fetched_models: Vec::new(),
170                fetch_models_task: None,
171            }
172        });
173
174        Self { http_client, state }
175    }
176
177    fn create_language_model(&self, model: anthropic::Model) -> Arc<dyn LanguageModel> {
178        Arc::new(AnthropicModel {
179            id: LanguageModelId::from(model.id.to_string()),
180            model,
181            state: self.state.clone(),
182            http_client: self.http_client.clone(),
183            request_limiter: RateLimiter::new(4),
184        })
185    }
186
187    fn settings(cx: &App) -> &AnthropicSettings {
188        &crate::AllLanguageModelSettings::get_global(cx).anthropic
189    }
190
191    fn api_url(cx: &App) -> SharedString {
192        let api_url = &Self::settings(cx).api_url;
193        if api_url.is_empty() {
194            ANTHROPIC_API_URL.into()
195        } else {
196            SharedString::new(api_url.as_str())
197        }
198    }
199}
200
201impl LanguageModelProviderState for AnthropicLanguageModelProvider {
202    type ObservableEntity = State;
203
204    fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
205        Some(self.state.clone())
206    }
207}
208
209impl LanguageModelProvider for AnthropicLanguageModelProvider {
210    fn id(&self) -> LanguageModelProviderId {
211        PROVIDER_ID
212    }
213
214    fn name(&self) -> LanguageModelProviderName {
215        PROVIDER_NAME
216    }
217
218    fn icon(&self) -> IconOrSvg {
219        IconOrSvg::Icon(IconName::AiAnthropic)
220    }
221
222    fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
223        let fetched = self.state.read(cx).fetched_models.clone();
224        // Pick the highest-version Sonnet we know about; otherwise the first
225        // Claude model returned. Returning `None` until the fetch completes
226        // matches the Ollama provider's behavior.
227        pick_preferred_model(&fetched, &["claude-sonnet-", "claude-opus-", "claude-"])
228            .map(|model| self.create_language_model(model))
229    }
230
231    fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
232        let fetched = self.state.read(cx).fetched_models.clone();
233        pick_preferred_model(&fetched, &["claude-haiku-", "claude-"])
234            .map(|model| self.create_language_model(model))
235    }
236
237    fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
238        let fetched = self.state.read(cx).fetched_models.clone();
239        pick_preferred_model(&fetched, &["claude-sonnet-"])
240            .map(|model| vec![self.create_language_model(model)])
241            .unwrap_or_default()
242    }
243
244    fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
245        let mut models: BTreeMap<String, anthropic::Model> = BTreeMap::default();
246
247        // Models reported by Anthropic's `/v1/models` endpoint are the
248        // primary source. The list will be empty until authentication has
249        // succeeded and the first fetch completes.
250        for model in &self.state.read(cx).fetched_models {
251            models.insert(model.id.to_string(), model.clone());
252        }
253
254        // User-defined `available_models` from settings can either add
255        // entirely new entries or override fields on a fetched model with
256        // the same id (e.g. enable Fast mode or set a tool override).
257        for available in &AnthropicLanguageModelProvider::settings(cx).available_models {
258            let model = available_model_to_anthropic_model(available);
259            models.insert(model.id.to_string(), model);
260        }
261
262        models
263            .into_values()
264            .map(|model| self.create_language_model(model))
265            .collect()
266    }
267
268    fn is_authenticated(&self, cx: &App) -> bool {
269        self.state.read(cx).is_authenticated()
270    }
271
272    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
273        self.state.update(cx, |state, cx| state.authenticate(cx))
274    }
275
276    fn settings_view(&self, cx: &mut App) -> Option<ProviderSettingsView> {
277        let state = self.state.read(cx);
278        Some(ProviderSettingsView::ApiKey(ApiKeyConfiguration::new(
279            state.api_key_state.has_key(),
280            state.api_key_state.is_from_env_var(),
281            state.api_key_state.env_var_name().clone(),
282            "https://console.anthropic.com/settings/keys".into(),
283        )))
284    }
285
286    fn set_api_key(&self, api_key: Option<String>, cx: &mut App) -> Task<Result<()>> {
287        self.state
288            .update(cx, |state, cx| state.set_api_key(api_key, cx))
289    }
290
291    fn fast_mode_confirmation(&self, _cx: &App) -> Option<FastModeConfirmation> {
292        Some(FastModeConfirmation {
293            title: "Enable Fast Mode for Anthropic?".into(),
294            message: "Fast mode lets requests use your Anthropic Priority Tier capacity, which \
295                Anthropic prioritizes over standard requests during peak load. Requires a \
296                Priority Tier commitment with Anthropic; without one, requests behave the same \
297                as the standard tier."
298                .into(),
299        })
300    }
301}
302
303/// Pick the model from `models` whose id starts with the earliest matching
304/// prefix in `preferred_prefixes`. Within a single prefix bucket the model
305/// with the lexicographically greatest id wins, which roughly corresponds to
306/// the highest version since Anthropic ids embed dated suffixes.
307fn pick_preferred_model(
308    models: &[anthropic::Model],
309    preferred_prefixes: &[&str],
310) -> Option<anthropic::Model> {
311    for prefix in preferred_prefixes {
312        let candidate = models
313            .iter()
314            .filter(|m| m.id.starts_with(prefix))
315            .max_by(|a, b| a.id.cmp(&b.id));
316        if let Some(model) = candidate {
317            return Some(model.clone());
318        }
319    }
320    None
321}
322
323/// Convert a settings-defined `available_models` entry into an `anthropic::Model`.
324fn available_model_to_anthropic_model(available: &AvailableModel) -> anthropic::Model {
325    let mode = match available.mode.unwrap_or_default() {
326        settings::ModelMode::Default => AnthropicModelMode::Default,
327        settings::ModelMode::Thinking { budget_tokens } => {
328            AnthropicModelMode::Thinking { budget_tokens }
329        }
330        settings::ModelMode::Adaptive => AnthropicModelMode::AdaptiveThinking,
331    };
332    let supports_thinking = matches!(
333        mode,
334        AnthropicModelMode::Thinking { .. } | AnthropicModelMode::AdaptiveThinking
335    );
336    let supports_adaptive_thinking = matches!(mode, AnthropicModelMode::AdaptiveThinking);
337    let supports_speed = available
338        .supports_fast_mode
339        .unwrap_or_else(|| anthropic::supports_fast_mode(&available.name));
340    let mut extra_beta_headers = available.extra_beta_headers.clone();
341    if supports_speed
342        && !extra_beta_headers
343            .iter()
344            .any(|header| header.trim() == anthropic::FAST_MODE_BETA_HEADER)
345    {
346        extra_beta_headers.push(anthropic::FAST_MODE_BETA_HEADER.to_string());
347    }
348
349    anthropic::Model {
350        display_name: available
351            .display_name
352            .clone()
353            .unwrap_or_else(|| available.name.clone()),
354        id: available.name.clone(),
355        max_input_tokens: available.max_tokens,
356        max_output_tokens: available.max_output_tokens.unwrap_or(4_096),
357        default_temperature: available.default_temperature.unwrap_or(1.0),
358        mode,
359        supports_thinking,
360        supports_adaptive_thinking,
361        supports_images: true,
362        supports_speed,
363        supports_compaction: false,
364        supported_effort_levels: if supports_adaptive_thinking {
365            vec![
366                anthropic::Effort::Low,
367                anthropic::Effort::Medium,
368                anthropic::Effort::High,
369                anthropic::Effort::XHigh,
370                anthropic::Effort::Max,
371            ]
372        } else {
373            vec![]
374        },
375        tool_override: available.tool_override.clone(),
376        extra_beta_headers,
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    fn parse_available_model(json: &str) -> AvailableModel {
385        serde_json::from_str(json).expect("test fixture should parse")
386    }
387
388    #[test]
389    fn adaptive_mode_maps_to_adaptive_thinking_with_all_effort_levels() {
390        let available = parse_available_model(
391            r#"{
392                "name": "claude-opus-4-7",
393                "max_tokens": 1000000,
394                "max_output_tokens": 128000,
395                "mode": { "type": "adaptive" }
396            }"#,
397        );
398        let model = available_model_to_anthropic_model(&available);
399
400        assert_eq!(model.mode, AnthropicModelMode::AdaptiveThinking);
401        assert!(model.supports_thinking);
402        assert!(model.supports_adaptive_thinking);
403        assert_eq!(
404            model.supported_effort_levels,
405            vec![
406                anthropic::Effort::Low,
407                anthropic::Effort::Medium,
408                anthropic::Effort::High,
409                anthropic::Effort::XHigh,
410                anthropic::Effort::Max,
411            ]
412        );
413    }
414
415    #[test]
416    fn thinking_mode_does_not_enable_adaptive() {
417        let available = parse_available_model(
418            r#"{
419                "name": "claude-sonnet-4-5",
420                "max_tokens": 200000,
421                "mode": { "type": "thinking", "budget_tokens": 4096 }
422            }"#,
423        );
424        let model = available_model_to_anthropic_model(&available);
425
426        assert!(matches!(model.mode, AnthropicModelMode::Thinking { .. }));
427        assert!(model.supports_thinking);
428        assert!(!model.supports_adaptive_thinking);
429        assert!(model.supported_effort_levels.is_empty());
430    }
431
432    #[test]
433    fn default_mode_disables_thinking() {
434        let available = parse_available_model(
435            r#"{
436                "name": "claude-3-5-haiku",
437                "max_tokens": 200000
438            }"#,
439        );
440        let model = available_model_to_anthropic_model(&available);
441
442        assert_eq!(model.mode, AnthropicModelMode::Default);
443        assert!(!model.supports_thinking);
444        assert!(!model.supports_adaptive_thinking);
445        assert!(model.supported_effort_levels.is_empty());
446    }
447}
448
449pub struct AnthropicModel {
450    id: LanguageModelId,
451    model: anthropic::Model,
452    state: Entity<State>,
453    http_client: Arc<dyn HttpClient>,
454    request_limiter: RateLimiter,
455}
456
457impl AnthropicModel {
458    fn stream_completion(
459        &self,
460        request: anthropic::Request,
461        cx: &AsyncApp,
462    ) -> BoxFuture<
463        'static,
464        Result<
465            BoxStream<'static, Result<anthropic::Event, AnthropicError>>,
466            LanguageModelCompletionError,
467        >,
468    > {
469        let http_client = self.http_client.clone();
470
471        let (api_key, api_url, extra_headers) = self.state.read_with(cx, |state, cx| {
472            let api_url = AnthropicLanguageModelProvider::api_url(cx);
473            let extra_headers = AnthropicLanguageModelProvider::settings(cx)
474                .custom_headers
475                .clone();
476            (state.api_key_state.key(&api_url), api_url, extra_headers)
477        });
478
479        let beta_headers = self.model.beta_headers();
480
481        async move {
482            let Some(api_key) = api_key else {
483                return Err(LanguageModelCompletionError::NoApiKey {
484                    provider: PROVIDER_NAME,
485                });
486            };
487            let request = anthropic::stream_completion(
488                http_client.as_ref(),
489                &api_url,
490                &api_key,
491                request,
492                beta_headers,
493                &extra_headers,
494            );
495            request.await.map_err(Into::into)
496        }
497        .boxed()
498    }
499}
500
501impl LanguageModel for AnthropicModel {
502    fn id(&self) -> LanguageModelId {
503        self.id.clone()
504    }
505
506    fn name(&self) -> LanguageModelName {
507        LanguageModelName::from(self.model.display_name.clone())
508    }
509
510    fn provider_id(&self) -> LanguageModelProviderId {
511        PROVIDER_ID
512    }
513
514    fn provider_name(&self) -> LanguageModelProviderName {
515        PROVIDER_NAME
516    }
517
518    fn supports_tools(&self) -> bool {
519        true
520    }
521
522    fn supports_images(&self) -> bool {
523        self.model.supports_images
524    }
525
526    fn supports_streaming_tools(&self) -> bool {
527        true
528    }
529
530    fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
531        match choice {
532            LanguageModelToolChoice::Auto
533            | LanguageModelToolChoice::Any
534            | LanguageModelToolChoice::None => true,
535        }
536    }
537
538    fn supports_thinking(&self) -> bool {
539        self.model.supports_thinking
540    }
541
542    fn supports_fast_mode(&self) -> bool {
543        self.model.supports_speed
544    }
545
546    fn refusal_fallback_model_id(&self) -> Option<&'static str> {
547        if self.model.id.starts_with(anthropic::FABLE_MODEL_ID_PREFIX) {
548            Some(anthropic::FABLE_FALLBACK_MODEL_ID)
549        } else {
550            None
551        }
552    }
553
554    fn supports_server_side_compaction(&self) -> bool {
555        self.model.supports_compaction
556    }
557
558    fn supported_effort_levels(&self) -> Vec<language_model::LanguageModelEffortLevel> {
559        self.model
560            .supported_effort_levels
561            .iter()
562            .map(|e| {
563                let is_default = matches!(e, anthropic::Effort::High);
564                let (name, value) = match e {
565                    anthropic::Effort::Low => ("Low".into(), "low".into()),
566                    anthropic::Effort::Medium => ("Medium".into(), "medium".into()),
567                    anthropic::Effort::High => ("High".into(), "high".into()),
568                    anthropic::Effort::XHigh => ("XHigh".into(), "xhigh".into()),
569                    anthropic::Effort::Max => ("Max".into(), "max".into()),
570                };
571                language_model::LanguageModelEffortLevel {
572                    name,
573                    value,
574                    is_default,
575                }
576            })
577            .collect::<Vec<_>>()
578    }
579
580    fn telemetry_id(&self) -> String {
581        format!("anthropic/{}", self.model.id)
582    }
583
584    fn api_key(&self, cx: &App) -> Option<String> {
585        self.state.read_with(cx, |state, cx| {
586            let api_url = AnthropicLanguageModelProvider::api_url(cx);
587            state.api_key_state.key(&api_url).map(|key| key.to_string())
588        })
589    }
590
591    fn max_token_count(&self) -> u64 {
592        self.model.max_input_tokens
593    }
594
595    fn max_output_tokens(&self) -> Option<u64> {
596        Some(self.model.max_output_tokens)
597    }
598
599    fn stream_completion(
600        &self,
601        request: LanguageModelRequest,
602        cx: &AsyncApp,
603    ) -> BoxFuture<
604        'static,
605        Result<
606            BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
607            LanguageModelCompletionError,
608        >,
609    > {
610        let has_tools = !request.tools.is_empty();
611        let request_id = self.model.request_id(has_tools).to_string();
612        let mut request = match into_anthropic(
613            request,
614            request_id,
615            self.model.default_temperature,
616            self.model.max_output_tokens,
617            self.model.mode.clone(),
618            AnthropicPromptCacheMode::Automatic,
619            &PROVIDER_ID,
620        ) {
621            Ok(request) => request,
622            Err(error) => return async move { Err(error.into()) }.boxed(),
623        };
624        if !self.model.supports_speed {
625            request.speed = None;
626        }
627        let request = self.stream_completion(request, cx);
628        let future = self.request_limiter.stream(async move {
629            let response = request.await?;
630            Ok(AnthropicEventMapper::new(PROVIDER_NAME, PROVIDER_ID).map_stream(response))
631        });
632        async move { Ok(future.await?.boxed()) }.boxed()
633    }
634}
635
Served at tenant.openagents/omega Member data and write actions are omitted.