Files
zed/crates/language_models/src/provider/cloud.rs
T
Agus ZubiagaandJohn Tur 98c17ca160 language_models: Refactor deps and extract cloud (#53270)
- `language_model` no longer depends on provider-specific crates such as
`anthropic` and `open_ai` (inverted dependency)
- `language_model_core` was extracted from `language_model` which
contains the types for the provider-specific crates to convert to/from.
- `gpui::SharedString` has been extracted into its own crate (still
exposed by `gpui`), so `language_model_core` and provider API crates
don't have to depend on `gpui`.
- Removes some unnecessary `&'static str` | `SharedString` -> `String`
-> `SharedString` conversions across the codebase.
- Extracts the core logic of the cloud `LanguageModelProvider` into its
own crate with simpler dependencies.


Release Notes:

- N/A

---------

Co-authored-by: John Tur <john-tur@outlook.com>
2026-04-07 12:28:19 -03:00

469 lines
15 KiB
Rust

use ai_onboarding::YoungAccountBanner;
use anyhow::Result;
use client::{Client, RefreshLlmTokenListener, UserStore, global_llm_token, zed_urls};
use cloud_api_client::LlmApiToken;
use cloud_api_types::OrganizationId;
use cloud_api_types::Plan;
use futures::StreamExt;
use futures::future::BoxFuture;
use gpui::AsyncApp;
use gpui::{AnyElement, AnyView, App, Context, Entity, Subscription, Task};
use language_model::{
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, ZED_CLOUD_PROVIDER_ID,
ZED_CLOUD_PROVIDER_NAME,
};
use language_models_cloud::{CloudLlmTokenProvider, CloudModelProvider};
use release_channel::AppVersion;
use settings::SettingsStore;
pub use settings::ZedDotDevAvailableModel as AvailableModel;
pub use settings::ZedDotDevAvailableProvider as AvailableProvider;
use std::sync::Arc;
use ui::{TintColor, prelude::*};
const PROVIDER_ID: LanguageModelProviderId = ZED_CLOUD_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = ZED_CLOUD_PROVIDER_NAME;
struct ClientTokenProvider {
client: Arc<Client>,
llm_api_token: LlmApiToken,
user_store: Entity<UserStore>,
}
impl CloudLlmTokenProvider for ClientTokenProvider {
type AuthContext = Option<OrganizationId>;
fn auth_context(&self, cx: &AsyncApp) -> Self::AuthContext {
self.user_store.read_with(cx, |user_store, _| {
user_store
.current_organization()
.map(|organization| organization.id.clone())
})
}
fn acquire_token(
&self,
organization_id: Self::AuthContext,
) -> BoxFuture<'static, Result<String>> {
let client = self.client.clone();
let llm_api_token = self.llm_api_token.clone();
Box::pin(async move {
client
.acquire_llm_token(&llm_api_token, organization_id)
.await
})
}
fn refresh_token(
&self,
organization_id: Self::AuthContext,
) -> BoxFuture<'static, Result<String>> {
let client = self.client.clone();
let llm_api_token = self.llm_api_token.clone();
Box::pin(async move {
client
.refresh_llm_token(&llm_api_token, organization_id)
.await
})
}
}
#[derive(Default, Clone, Debug, PartialEq)]
pub struct ZedDotDevSettings {
pub available_models: Vec<AvailableModel>,
}
pub struct CloudLanguageModelProvider {
state: Entity<State>,
_maintain_client_status: Task<()>,
}
pub struct State {
client: Arc<Client>,
user_store: Entity<UserStore>,
status: client::Status,
provider: Entity<CloudModelProvider<ClientTokenProvider>>,
_user_store_subscription: Subscription,
_settings_subscription: Subscription,
_llm_token_subscription: Subscription,
_provider_subscription: Subscription,
}
impl State {
fn new(
client: Arc<Client>,
user_store: Entity<UserStore>,
status: client::Status,
cx: &mut Context<Self>,
) -> Self {
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
let token_provider = Arc::new(ClientTokenProvider {
client: client.clone(),
llm_api_token: global_llm_token(cx),
user_store: user_store.clone(),
});
let provider = cx.new(|cx| {
CloudModelProvider::new(
token_provider.clone(),
client.http_client(),
Some(AppVersion::global(cx)),
)
});
Self {
client: client.clone(),
user_store: user_store.clone(),
status,
_provider_subscription: cx.observe(&provider, |_, _, cx| cx.notify()),
provider,
_user_store_subscription: cx.subscribe(
&user_store,
move |this, _user_store, event, cx| match event {
client::user::Event::PrivateUserInfoUpdated => {
let status = *client.status().borrow();
if status.is_signed_out() {
return;
}
this.refresh_models(cx);
}
_ => {}
},
),
_settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
cx.notify();
}),
_llm_token_subscription: cx.subscribe(
&refresh_llm_token_listener,
move |this, _listener, _event, cx| {
this.refresh_models(cx);
},
),
}
}
fn is_signed_out(&self, cx: &App) -> bool {
self.user_store.read(cx).current_user().is_none()
}
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(async move |state, cx| {
client.sign_in_with_optional_connect(true, cx).await?;
state.update(cx, |_, cx| cx.notify())
})
}
fn refresh_models(&mut self, cx: &mut Context<Self>) {
self.provider.update(cx, |provider, cx| {
provider.refresh_models(cx).detach_and_log_err(cx);
});
}
}
impl CloudLanguageModelProvider {
pub fn new(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) -> Self {
let mut status_rx = client.status();
let status = *status_rx.borrow();
let state = cx.new(|cx| State::new(client.clone(), user_store.clone(), status, cx));
let state_ref = state.downgrade();
let maintain_client_status = cx.spawn(async move |cx| {
while let Some(status) = status_rx.next().await {
if let Some(this) = state_ref.upgrade() {
_ = this.update(cx, |this, cx| {
if this.status != status {
this.status = status;
cx.notify();
}
});
} else {
break;
}
}
});
Self {
state,
_maintain_client_status: maintain_client_status,
}
}
}
impl LanguageModelProviderState for CloudLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for CloudLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiZed)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
let state = self.state.read(cx);
let provider = state.provider.read(cx);
let model = provider.default_model()?;
Some(provider.create_model(model))
}
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
let state = self.state.read(cx);
let provider = state.provider.read(cx);
let model = provider.default_fast_model()?;
Some(provider.create_model(model))
}
fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let state = self.state.read(cx);
let provider = state.provider.read(cx);
provider
.recommended_models()
.iter()
.map(|model| provider.create_model(model))
.collect()
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let state = self.state.read(cx);
let provider = state.provider.read(cx);
provider
.models()
.iter()
.map(|model| provider.create_model(model))
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
let state = self.state.read(cx);
!state.is_signed_out(cx)
}
fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> {
Task::ready(Ok(()))
}
fn configuration_view(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
_: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|_| ConfigurationView::new(self.state.clone()))
.into()
}
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
}
#[derive(IntoElement, RegisterComponent)]
struct ZedAiConfiguration {
is_connected: bool,
plan: Option<Plan>,
eligible_for_trial: bool,
account_too_young: bool,
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
}
impl RenderOnce for ZedAiConfiguration {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let (subscription_text, has_paid_plan) = match self.plan {
Some(Plan::ZedPro) => (
"You have access to Zed's hosted models through your Pro subscription.",
true,
),
Some(Plan::ZedProTrial) => (
"You have access to Zed's hosted models through your Pro trial.",
false,
),
Some(Plan::ZedStudent) => (
"You have access to Zed's hosted models through your Student subscription.",
true,
),
Some(Plan::ZedBusiness) => (
"You have access to Zed's hosted models through your Organization.",
true,
),
Some(Plan::ZedFree) | None => (
if self.eligible_for_trial {
"Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
} else {
"Subscribe for access to Zed's hosted models."
},
false,
),
};
let manage_subscription_buttons = if has_paid_plan {
Button::new("manage_settings", "Manage Subscription")
.full_width()
.label_size(LabelSize::Small)
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
.into_any_element()
} else if self.plan.is_none() || self.eligible_for_trial {
Button::new("start_trial", "Start 14-day Free Pro Trial")
.full_width()
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
.into_any_element()
} else {
Button::new("upgrade", "Upgrade to Pro")
.full_width()
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
.into_any_element()
};
if !self.is_connected {
return v_flex()
.gap_2()
.child(Label::new("Sign in to have access to Zed's complete agentic experience with hosted models."))
.child(
Button::new("sign_in", "Sign In to use Zed AI")
.start_icon(Icon::new(IconName::Github).size(IconSize::Small).color(Color::Muted))
.full_width()
.on_click({
let callback = self.sign_in_callback.clone();
move |_, window, cx| (callback)(window, cx)
}),
);
}
v_flex().gap_2().w_full().map(|this| {
if self.account_too_young {
this.child(YoungAccountBanner).child(
Button::new("upgrade", "Upgrade to Pro")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
)
} else {
this.text_sm()
.child(subscription_text)
.child(manage_subscription_buttons)
}
})
}
}
struct ConfigurationView {
state: Entity<State>,
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
}
impl ConfigurationView {
fn new(state: Entity<State>) -> Self {
let sign_in_callback = Arc::new({
let state = state.clone();
move |_window: &mut Window, cx: &mut App| {
state.update(cx, |state, cx| {
state.authenticate(cx).detach_and_log_err(cx);
});
}
});
Self {
state,
sign_in_callback,
}
}
}
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let state = self.state.read(cx);
let user_store = state.user_store.read(cx);
ZedAiConfiguration {
is_connected: !state.is_signed_out(cx),
plan: user_store.plan(),
eligible_for_trial: user_store.trial_started_at().is_none(),
account_too_young: user_store.account_too_young(),
sign_in_callback: self.sign_in_callback.clone(),
}
}
}
impl Component for ZedAiConfiguration {
fn name() -> &'static str {
"AI Configuration Content"
}
fn sort_name() -> &'static str {
"AI Configuration Content"
}
fn scope() -> ComponentScope {
ComponentScope::Onboarding
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn configuration(
is_connected: bool,
plan: Option<Plan>,
eligible_for_trial: bool,
account_too_young: bool,
) -> AnyElement {
ZedAiConfiguration {
is_connected,
plan,
eligible_for_trial,
account_too_young,
sign_in_callback: Arc::new(|_, _| {}),
}
.into_any_element()
}
Some(
v_flex()
.p_4()
.gap_4()
.children(vec![
single_example("Not connected", configuration(false, None, false, false)),
single_example(
"Accept Terms of Service",
configuration(true, None, true, false),
),
single_example(
"No Plan - Not eligible for trial",
configuration(true, None, false, false),
),
single_example(
"No Plan - Eligible for trial",
configuration(true, None, true, false),
),
single_example(
"Free Plan",
configuration(true, Some(Plan::ZedFree), true, false),
),
single_example(
"Zed Pro Trial Plan",
configuration(true, Some(Plan::ZedProTrial), true, false),
),
single_example(
"Zed Pro Plan",
configuration(true, Some(Plan::ZedPro), true, false),
),
])
.into_any_element(),
)
}
}