Skip to repository content635 lines · 22.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:57:11.306Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
anthropic.rs
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