logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
co-authored by Claude Fable 5
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
[package]
name = "language_models"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/language_models.rs"
[dependencies]
ai_onboarding.workspace = true
async-lock.workspace = true
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
aws-config = { workspace = true, features = ["behavior-version-latest"] }
aws-credential-types = { workspace = true, features = ["hardcoded-credentials"] }
aws_http_client.workspace = true
base64.workspace = true
bedrock = { workspace = true, features = ["schemars"] }
client.workspace = true
cloud_api_client.workspace = true
cloud_api_types.workspace = true
collections.workspace = true
component.workspace = true
convert_case.workspace = true
copilot.workspace = true
copilot_chat.workspace = true
copilot_ui.workspace = true
credentials_provider.workspace = true
deepseek = { workspace = true, features = ["schemars"] }
extension.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
google_ai = { workspace = true, features = ["schemars"] }
gpui.workspace = true
gpui_tokio.workspace = true
http_client.workspace = true
language.workspace = true
language_model.workspace = true
language_models_cloud.workspace = true
lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true
menu.workspace = true
mistral = { workspace = true, features = ["schemars"] }
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
opencode = { workspace = true, features = ["schemars"] }
open_router = { workspace = true, features = ["schemars"] }
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
strum.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
ui.workspace = true
ui_input.workspace = true
util.workspace = true
x_ai = { workspace = true, features = ["schemars"] }
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
db = { workspace = true, features = ["test-support"] }
feature_flags.workspace = true
gpui = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
settings = { workspace = true, features = ["test-support"] }
+1
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
+67
View File
@@ -0,0 +1,67 @@
use collections::HashMap;
use extension::{
ExtensionHostProxy, ExtensionLanguageModelProviderProxy, LanguageModelProviderRegistration,
};
use gpui::{App, Entity};
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
use std::sync::{Arc, LazyLock};
/// Maps built-in provider IDs to their corresponding extension IDs.
/// When an extension with this ID is installed, the built-in provider should be hidden.
static BUILTIN_TO_EXTENSION_MAP: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(|| {
let mut map = HashMap::default();
map.insert("anthropic", "anthropic");
map.insert("openai", "openai");
map.insert("google", "google-ai");
map.insert("openrouter", "openrouter");
map.insert("copilot_chat", "copilot-chat");
map
});
/// Returns the extension ID that should hide the given built-in provider.
pub fn extension_for_builtin_provider(provider_id: &str) -> Option<&'static str> {
BUILTIN_TO_EXTENSION_MAP.get(provider_id).copied()
}
/// Proxy that registers extension language model providers with the LanguageModelRegistry.
pub struct LanguageModelProviderRegistryProxy {
registry: Entity<LanguageModelRegistry>,
}
impl LanguageModelProviderRegistryProxy {
pub fn new(registry: Entity<LanguageModelRegistry>) -> Self {
Self { registry }
}
}
impl ExtensionLanguageModelProviderProxy for LanguageModelProviderRegistryProxy {
fn register_language_model_provider(
&self,
_provider_id: Arc<str>,
register_fn: LanguageModelProviderRegistration,
cx: &mut App,
) {
register_fn(cx);
}
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
self.registry.update(cx, |registry, cx| {
registry.unregister_provider(LanguageModelProviderId::from(provider_id), cx);
});
}
}
/// Initialize the extension language model provider proxy.
/// This must be called BEFORE extension_host::init to ensure the proxy is available
/// when extensions try to register their language model providers.
pub fn init_proxy(cx: &mut App) {
let proxy = ExtensionHostProxy::default_global(cx);
let registry = LanguageModelRegistry::global(cx);
registry.update(cx, |registry, _cx| {
registry.set_builtin_provider_hiding_fn(Box::new(extension_for_builtin_provider));
});
proxy.register_language_model_provider_proxy(LanguageModelProviderRegistryProxy::new(registry));
}
@@ -0,0 +1,333 @@
use std::sync::Arc;
use ::settings::{Settings, SettingsStore};
use client::{Client, UserStore};
use collections::HashSet;
use credentials_provider::CredentialsProvider;
use gpui::{App, Context, Entity};
use language_model::{
ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use provider::deepseek::DeepSeekLanguageModelProvider;
pub mod extension;
pub mod provider;
mod settings;
pub use crate::extension::init_proxy as init_extension_proxy;
use crate::provider::anthropic::AnthropicLanguageModelProvider;
use crate::provider::bedrock::BedrockLanguageModelProvider;
use crate::provider::cloud::CloudLanguageModelProvider;
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
use crate::provider::google::GoogleLanguageModelProvider;
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
pub use crate::provider::mistral::MistralLanguageModelProvider;
use crate::provider::ollama::OllamaLanguageModelProvider;
use crate::provider::open_ai::OpenAiLanguageModelProvider;
use crate::provider::open_ai_compatible::OpenAiCompatibleLanguageModelProvider;
use crate::provider::open_router::OpenRouterLanguageModelProvider;
use crate::provider::opencode::OpenCodeLanguageModelProvider;
use crate::provider::vercel_ai_gateway::VercelAiGatewayLanguageModelProvider;
use crate::provider::x_ai::XAiLanguageModelProvider;
pub use crate::settings::*;
pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
let credentials_provider = client.credentials_provider();
let registry = LanguageModelRegistry::global(cx);
registry.update(cx, |registry, cx| {
register_language_model_providers(
registry,
user_store,
client.clone(),
credentials_provider.clone(),
cx,
);
});
// Subscribe to extension store events to track LLM extension installations
if let Some(extension_store) = extension_host::ExtensionStore::try_global(cx) {
cx.subscribe(&extension_store, {
let registry = registry.downgrade();
move |extension_store, event, cx| {
let Some(registry) = registry.upgrade() else {
return;
};
match event {
extension_host::Event::ExtensionInstalled(extension_id) => {
if let Some(manifest) = extension_store
.read(cx)
.extension_manifest_for_id(extension_id)
{
if !manifest.language_model_providers.is_empty() {
registry.update(cx, |registry, cx| {
registry.extension_installed(extension_id.clone(), cx);
});
}
}
}
extension_host::Event::ExtensionUninstalled(extension_id) => {
registry.update(cx, |registry, cx| {
registry.extension_uninstalled(extension_id, cx);
});
}
extension_host::Event::ExtensionsUpdated => {
let mut new_ids = HashSet::default();
for (extension_id, entry) in extension_store.read(cx).installed_extensions()
{
if !entry.manifest.language_model_providers.is_empty() {
new_ids.insert(extension_id.clone());
}
}
registry.update(cx, |registry, cx| {
registry.sync_installed_llm_extensions(new_ids, cx);
});
}
_ => {}
}
}
})
.detach();
// Initialize with currently installed extensions
registry.update(cx, |registry, cx| {
let mut initial_ids = HashSet::default();
for (extension_id, entry) in extension_store.read(cx).installed_extensions() {
if !entry.manifest.language_model_providers.is_empty() {
initial_ids.insert(extension_id.clone());
}
}
registry.sync_installed_llm_extensions(initial_ids, cx);
});
}
let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx)
.openai_compatible
.keys()
.cloned()
.collect::<HashSet<_>>();
registry.update(cx, |registry, cx| {
register_openai_compatible_providers(
registry,
&HashSet::default(),
&openai_compatible_providers,
client.clone(),
credentials_provider.clone(),
cx,
);
});
let registry = registry.downgrade();
cx.observe_global::<SettingsStore>(move |cx| {
let Some(registry) = registry.upgrade() else {
return;
};
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
.openai_compatible
.keys()
.cloned()
.collect::<HashSet<_>>();
if openai_compatible_providers_new != openai_compatible_providers {
registry.update(cx, |registry, cx| {
register_openai_compatible_providers(
registry,
&openai_compatible_providers,
&openai_compatible_providers_new,
client.clone(),
credentials_provider.clone(),
cx,
);
});
openai_compatible_providers = openai_compatible_providers_new;
}
})
.detach();
}
/// Recomputes and sets the [`LanguageModelRegistry`]'s environment fallback
/// model based on currently authenticated providers.
///
/// Prefers the Zed cloud provider so that, once the user is signed in, we
/// always pick a Zed-hosted model over models from other authenticated
/// providers in the environment. If the Zed cloud provider is authenticated
/// but hasn't finished loading its models yet, we don't fall back to another
/// provider to avoid flickering between providers during sign in.
pub fn update_environment_fallback_model(cx: &mut App) {
let registry = LanguageModelRegistry::global(cx);
let fallback_model = {
let registry = registry.read(cx);
let cloud_provider = registry.provider(&ZED_CLOUD_PROVIDER_ID);
if cloud_provider
.as_ref()
.is_some_and(|provider| provider.is_authenticated(cx))
{
cloud_provider.and_then(|provider| {
let model = provider
.default_model(cx)
.or_else(|| provider.recommended_models(cx).first().cloned())?;
Some(ConfiguredModel { provider, model })
})
} else {
registry
.providers()
.iter()
.filter(|provider| provider.is_authenticated(cx))
.find_map(|provider| {
let model = provider
.default_model(cx)
.or_else(|| provider.recommended_models(cx).first().cloned())?;
Some(ConfiguredModel {
provider: provider.clone(),
model,
})
})
}
};
registry.update(cx, |registry, cx| {
registry.set_environment_fallback_model(fallback_model, cx);
});
}
fn register_openai_compatible_providers(
registry: &mut LanguageModelRegistry,
old: &HashSet<Arc<str>>,
new: &HashSet<Arc<str>>,
client: Arc<Client>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut Context<LanguageModelRegistry>,
) {
for provider_id in old {
if !new.contains(provider_id) {
registry.unregister_provider(LanguageModelProviderId::from(provider_id.clone()), cx);
}
}
for provider_id in new {
if !old.contains(provider_id) {
registry.register_provider(
Arc::new(OpenAiCompatibleLanguageModelProvider::new(
provider_id.clone(),
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
}
}
}
fn register_language_model_providers(
registry: &mut LanguageModelRegistry,
user_store: Entity<UserStore>,
client: Arc<Client>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut Context<LanguageModelRegistry>,
) {
registry.register_provider(
Arc::new(CloudLanguageModelProvider::new(
user_store,
client.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(AnthropicLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(OpenAiLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(OllamaLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(LmStudioLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(DeepSeekLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(GoogleLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
MistralLanguageModelProvider::global(
client.http_client(),
credentials_provider.clone(),
cx,
),
cx,
);
registry.register_provider(
Arc::new(BedrockLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(OpenRouterLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(VercelAiGatewayLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(XAiLanguageModelProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(OpenCodeLanguageModelProvider::new(
client.http_client(),
credentials_provider,
cx,
)),
cx,
);
registry.register_provider(Arc::new(CopilotChatLanguageModelProvider::new(cx)), cx);
}
+16
View File
@@ -0,0 +1,16 @@
pub mod anthropic;
pub mod bedrock;
pub mod cloud;
pub mod copilot_chat;
pub mod deepseek;
pub mod google;
pub mod lmstudio;
pub mod mistral;
pub mod ollama;
pub mod open_ai;
pub mod open_ai_compatible;
pub mod open_router;
pub mod opencode;
pub mod vercel_ai_gateway;
pub mod x_ai;
@@ -0,0 +1,655 @@
pub mod telemetry;
use anthropic::{ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode};
use anyhow::Result;
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt};
use http_client::HttpClient;
use language_model::{
ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ApiKeyState, AuthenticateError,
ConfigurationViewTargetAgent, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelToolChoice, RateLimiter, env_var,
};
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
pub use anthropic::completion::{AnthropicEventMapper, AnthropicPromptCacheMode, into_anthropic};
pub use settings::AnthropicAvailableModel as AvailableModel;
const PROVIDER_ID: LanguageModelProviderId = ANTHROPIC_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = ANTHROPIC_PROVIDER_NAME;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct AnthropicSettings {
pub api_url: String,
/// Extend Zed's list of Anthropic models.
pub available_models: Vec<AvailableModel>,
}
pub struct AnthropicLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
state: Entity<State>,
}
const API_KEY_ENV_VAR_NAME: &str = "ANTHROPIC_API_KEY";
static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
pub struct State {
api_key_state: ApiKeyState,
credentials_provider: Arc<dyn CredentialsProvider>,
http_client: Arc<dyn HttpClient>,
fetched_models: Vec<anthropic::Model>,
fetch_models_task: Option<Task<Result<()>>>,
}
impl State {
fn is_authenticated(&self) -> bool {
self.api_key_state.has_key()
}
fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = AnthropicLanguageModelProvider::api_url(cx);
let should_fetch_models = api_key.is_some();
let task = self.api_key_state.store(
api_url,
api_key,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
self.fetched_models.clear();
cx.spawn(async move |this, cx| {
let result = task.await;
if result.is_ok() && should_fetch_models {
this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
.ok();
}
result
})
}
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = AnthropicLanguageModelProvider::api_url(cx);
let task = self.api_key_state.load_if_needed(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
cx.spawn(async move |this, cx| {
let result = task.await;
if result.is_ok() {
this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
.ok();
}
result
})
}
fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let http_client = self.http_client.clone();
let api_url = AnthropicLanguageModelProvider::api_url(cx);
let Some(api_key) = self.api_key_state.key(&api_url) else {
return Task::ready(Err(anyhow::anyhow!(
"cannot fetch Anthropic models without an API key"
)));
};
cx.spawn(async move |this, cx| {
let models =
anthropic::list_models(http_client.as_ref(), &api_url, api_key.as_ref()).await?;
this.update(cx, |this, cx| {
this.fetched_models = models;
cx.notify();
})
})
}
fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) {
let task = self.fetch_models(cx);
self.fetch_models_task.replace(task);
}
}
impl AnthropicLanguageModelProvider {
pub fn new(
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>({
let mut last_api_url = Self::api_url(cx);
move |this: &mut State, cx| {
let credentials_provider = this.credentials_provider.clone();
let api_url = Self::api_url(cx);
let url_changed = api_url != last_api_url;
last_api_url = api_url.clone();
this.api_key_state.handle_url_change(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
if url_changed {
this.fetched_models.clear();
this.authenticate(cx).detach();
}
cx.notify();
}
})
.detach();
State {
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
credentials_provider,
http_client: http_client.clone(),
fetched_models: Vec::new(),
fetch_models_task: None,
}
});
Self { http_client, state }
}
fn create_language_model(&self, model: anthropic::Model) -> Arc<dyn LanguageModel> {
Arc::new(AnthropicModel {
id: LanguageModelId::from(model.id.to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
})
}
fn settings(cx: &App) -> &AnthropicSettings {
&crate::AllLanguageModelSettings::get_global(cx).anthropic
}
fn api_url(cx: &App) -> SharedString {
let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
ANTHROPIC_API_URL.into()
} else {
SharedString::new(api_url.as_str())
}
}
}
impl LanguageModelProviderState for AnthropicLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for AnthropicLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiAnthropic)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
let fetched = self.state.read(cx).fetched_models.clone();
// Pick the highest-version Sonnet we know about; otherwise the first
// Claude model returned. Returning `None` until the fetch completes
// matches the Ollama provider's behavior.
pick_preferred_model(&fetched, &["claude-sonnet-", "claude-opus-", "claude-"])
.map(|model| self.create_language_model(model))
}
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
let fetched = self.state.read(cx).fetched_models.clone();
pick_preferred_model(&fetched, &["claude-haiku-", "claude-"])
.map(|model| self.create_language_model(model))
}
fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let fetched = self.state.read(cx).fetched_models.clone();
pick_preferred_model(&fetched, &["claude-sonnet-"])
.map(|model| vec![self.create_language_model(model)])
.unwrap_or_default()
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models: BTreeMap<String, anthropic::Model> = BTreeMap::default();
// Models reported by Anthropic's `/v1/models` endpoint are the
// primary source. The list will be empty until authentication has
// succeeded and the first fetch completes.
for model in &self.state.read(cx).fetched_models {
models.insert(model.id.to_string(), model.clone());
}
// User-defined `available_models` from settings can either add
// entirely new entries or override fields on a fetched model with
// the same id (e.g. enable Fast mode or set a tool override).
for available in &AnthropicLanguageModelProvider::settings(cx).available_models {
let model = available_model_to_anthropic_model(available);
models.insert(model.id.to_string(), model);
}
models
.into_values()
.map(|model| self.create_language_model(model))
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(
&self,
target_agent: ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
.into()
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(None, cx))
}
}
/// Pick the model from `models` whose id starts with the earliest matching
/// prefix in `preferred_prefixes`. Within a single prefix bucket the model
/// with the lexicographically greatest id wins, which roughly corresponds to
/// the highest version since Anthropic ids embed dated suffixes.
fn pick_preferred_model(
models: &[anthropic::Model],
preferred_prefixes: &[&str],
) -> Option<anthropic::Model> {
for prefix in preferred_prefixes {
let candidate = models
.iter()
.filter(|m| m.id.starts_with(prefix))
.max_by(|a, b| a.id.cmp(&b.id));
if let Some(model) = candidate {
return Some(model.clone());
}
}
None
}
/// Convert a settings-defined `available_models` entry into an `anthropic::Model`.
fn available_model_to_anthropic_model(available: &AvailableModel) -> anthropic::Model {
let mode = match available.mode.unwrap_or_default() {
settings::ModelMode::Default => AnthropicModelMode::Default,
settings::ModelMode::Thinking { budget_tokens } => {
AnthropicModelMode::Thinking { budget_tokens }
}
};
let supports_thinking = matches!(
mode,
AnthropicModelMode::Thinking { .. } | AnthropicModelMode::AdaptiveThinking
);
let supports_adaptive_thinking = matches!(mode, AnthropicModelMode::AdaptiveThinking);
anthropic::Model {
display_name: available
.display_name
.clone()
.unwrap_or_else(|| available.name.clone()),
id: available.name.clone(),
max_input_tokens: available.max_tokens,
max_output_tokens: available.max_output_tokens.unwrap_or(4_096),
default_temperature: available.default_temperature.unwrap_or(1.0),
mode,
supports_thinking,
supports_adaptive_thinking,
supports_images: true,
supports_speed: false,
supported_effort_levels: if supports_adaptive_thinking {
vec![
anthropic::Effort::Low,
anthropic::Effort::Medium,
anthropic::Effort::High,
anthropic::Effort::XHigh,
anthropic::Effort::Max,
]
} else {
vec![]
},
tool_override: available.tool_override.clone(),
extra_beta_headers: available.extra_beta_headers.clone(),
}
}
pub struct AnthropicModel {
id: LanguageModelId,
model: anthropic::Model,
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
impl AnthropicModel {
fn stream_completion(
&self,
request: anthropic::Request,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
BoxStream<'static, Result<anthropic::Event, AnthropicError>>,
LanguageModelCompletionError,
>,
> {
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
let api_url = AnthropicLanguageModelProvider::api_url(cx);
(state.api_key_state.key(&api_url), api_url)
});
let beta_headers = self.model.beta_headers();
async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey {
provider: PROVIDER_NAME,
});
};
let request = anthropic::stream_completion(
http_client.as_ref(),
&api_url,
&api_key,
request,
beta_headers,
);
request.await.map_err(Into::into)
}
.boxed()
}
}
impl LanguageModel for AnthropicModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(self.model.display_name.clone())
}
fn provider_id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
true
}
fn supports_images(&self) -> bool {
self.model.supports_images
}
fn supports_streaming_tools(&self) -> bool {
true
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
match choice {
LanguageModelToolChoice::Auto
| LanguageModelToolChoice::Any
| LanguageModelToolChoice::None => true,
}
}
fn supports_thinking(&self) -> bool {
self.model.supports_thinking
}
fn supports_fast_mode(&self) -> bool {
self.model.supports_speed
}
fn supported_effort_levels(&self) -> Vec<language_model::LanguageModelEffortLevel> {
self.model
.supported_effort_levels
.iter()
.map(|e| {
let is_default = matches!(e, anthropic::Effort::High);
let (name, value) = match e {
anthropic::Effort::Low => ("Low".into(), "low".into()),
anthropic::Effort::Medium => ("Medium".into(), "medium".into()),
anthropic::Effort::High => ("High".into(), "high".into()),
anthropic::Effort::XHigh => ("XHigh".into(), "xhigh".into()),
anthropic::Effort::Max => ("Max".into(), "max".into()),
};
language_model::LanguageModelEffortLevel {
name,
value,
is_default,
}
})
.collect::<Vec<_>>()
}
fn telemetry_id(&self) -> String {
format!("anthropic/{}", self.model.id)
}
fn api_key(&self, cx: &App) -> Option<String> {
self.state.read_with(cx, |state, cx| {
let api_url = AnthropicLanguageModelProvider::api_url(cx);
state.api_key_state.key(&api_url).map(|key| key.to_string())
})
}
fn max_token_count(&self) -> u64 {
self.model.max_input_tokens
}
fn max_output_tokens(&self) -> Option<u64> {
Some(self.model.max_output_tokens)
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let has_tools = !request.tools.is_empty();
let request_id = self.model.request_id(has_tools).to_string();
let mut request = into_anthropic(
request,
request_id,
self.model.default_temperature,
self.model.max_output_tokens,
self.model.mode.clone(),
AnthropicPromptCacheMode::Automatic,
);
if !self.model.supports_speed {
request.speed = None;
}
let request = self.stream_completion(request, cx);
let future = self.request_limiter.stream(async move {
let response = request.await?;
Ok(AnthropicEventMapper::new().map_stream(response))
});
async move { Ok(future.await?.boxed()) }.boxed()
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
None
}
}
struct ConfigurationView {
api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
target_agent: ConfigurationViewTargetAgent,
}
impl ConfigurationView {
const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
fn new(
state: Entity<State>,
target_agent: ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let load_credentials_task = Some(cx.spawn({
let state = state.clone();
async move |this, cx| {
let task = state.update(cx, |state, cx| state.authenticate(cx));
// We don't log an error, because "not signed in" is also an error.
let _ = task.await;
this.update(cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor: cx.new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_TEXT)),
state,
load_credentials_task,
target_agent,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx);
if api_key.is_empty() {
return;
}
// url changes can cause the editor to be displayed again
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
.await
})
.detach_and_log_err(cx);
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(None, cx))
.await
})
.detach_and_log_err(cx);
}
fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
let configured_card_label = if env_var_set {
format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
} else {
let api_url = AnthropicLanguageModelProvider::api_url(cx);
if api_url == ANTHROPIC_API_URL {
"API key configured".to_string()
} else {
format!("API key configured for {}", api_url)
}
};
if self.load_credentials_task.is_some() {
div()
.child(Label::new("Loading credentials..."))
.into_any_element()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(),
ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
})))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Create one by visiting"))
.child(ButtonLink::new("Anthropic's settings", "https://console.anthropic.com/settings/keys"))
)
.child(
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
)
)
.child(self.api_key_editor.clone())
.child(
Label::new(
format!("You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
)
.size(LabelSize::Small)
.color(Color::Muted)
.mt_0p5(),
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
this.tooltip_label(format!(
"To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."
))
})
.into_any_element()
}
}
}
@@ -0,0 +1,136 @@
use anthropic::ANTHROPIC_API_URL;
use anyhow::{Context as _, anyhow};
use gpui::BackgroundExecutor;
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use language_model::{ANTHROPIC_PROVIDER_ID, LanguageModel};
use std::env;
use std::sync::Arc;
use util::ResultExt;
#[derive(Clone, Debug)]
pub struct AnthropicEventData {
pub completion_type: AnthropicCompletionType,
pub event: AnthropicEventType,
pub language_name: Option<String>,
pub message_id: Option<String>,
}
#[derive(Clone, Debug)]
pub enum AnthropicCompletionType {
Editor,
Terminal,
Panel,
}
#[derive(Clone, Debug)]
pub enum AnthropicEventType {
Invoked,
Response,
Accept,
Reject,
}
impl AnthropicCompletionType {
fn as_str(&self) -> &'static str {
match self {
Self::Editor => "natural_language_completion_in_editor",
Self::Terminal => "natural_language_completion_in_terminal",
Self::Panel => "conversation_message",
}
}
}
impl AnthropicEventType {
fn as_str(&self) -> &'static str {
match self {
Self::Invoked => "invoke",
Self::Response => "response",
Self::Accept => "accept",
Self::Reject => "reject",
}
}
}
pub fn report_anthropic_event(
model: &Arc<dyn LanguageModel>,
event: AnthropicEventData,
cx: &gpui::App,
) {
let reporter = AnthropicEventReporter::new(model, cx);
reporter.report(event);
}
#[derive(Clone)]
pub struct AnthropicEventReporter {
http_client: Arc<dyn HttpClient>,
executor: BackgroundExecutor,
api_key: Option<String>,
is_anthropic: bool,
}
impl AnthropicEventReporter {
pub fn new(model: &Arc<dyn LanguageModel>, cx: &gpui::App) -> Self {
Self {
http_client: cx.http_client(),
executor: cx.background_executor().clone(),
api_key: model.api_key(cx),
is_anthropic: model.provider_id() == ANTHROPIC_PROVIDER_ID,
}
}
pub fn report(&self, event: AnthropicEventData) {
if !self.is_anthropic {
return;
}
let Some(api_key) = self.api_key.clone() else {
return;
};
let client = self.http_client.clone();
self.executor
.spawn(async move {
send_anthropic_event(event, client, api_key).await.log_err();
})
.detach();
}
}
async fn send_anthropic_event(
event: AnthropicEventData,
client: Arc<dyn HttpClient>,
api_key: String,
) -> anyhow::Result<()> {
let uri = format!("{ANTHROPIC_API_URL}/v1/log/zed");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
let serialized_event = serde_json::json!({
"completion_type": event.completion_type.as_str(),
"event": event.event.as_str(),
"metadata": {
"language_name": event.language_name,
"message_id": event.message_id,
"platform": env::consts::OS,
}
});
let request = request_builder
.body(AsyncBody::from(serialized_event.to_string()))
.context("Failed to construct Anthropic telemetry HTTP request body")?;
let response = client
.send(request)
.await
.context("Failed to send telemetry HTTP request to Anthropic")?;
if response.status().is_success() {
Ok(())
} else {
Err(anyhow!(
"Anthropic telemetry logging failed with HTTP status: {}",
response.status()
))
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,778 @@
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::FutureExt;
use futures::StreamExt;
use futures::future::BoxFuture;
use gpui::{AnyElement, AnyView, App, AppContext, Context, Entity, Subscription, Task, TaskExt};
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: &impl AppContext) -> Self::AuthContext {
self.user_store.read_with(cx, |user_store, _| {
user_store
.current_organization()
.map(|organization| organization.id.clone())
})
}
fn cached_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
.cached_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 sign_in(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let client = self.client.clone();
let mut current_user = self.user_store.read(cx).watch_current_user();
cx.spawn(async move |state, cx| {
client.sign_in_with_optional_connect(true, cx).await?;
while current_user.borrow().is_none() {
current_user.next().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>> {
if self.is_authenticated(cx) {
return Task::ready(Ok(()));
}
let mut status = self.state.read(cx).client.status();
let mut current_user = self.state.read(cx).user_store.read(cx).watch_current_user();
if !status.borrow().is_signing_in() {
return Task::ready(Ok(()));
}
cx.background_spawn(async move {
while status.borrow().is_signing_in() {
status.next().await;
}
while current_user.borrow().is_none() {
let current_status = *status.borrow();
if !matches!(
current_status,
client::Status::Authenticated
| client::Status::Reauthenticated
| client::Status::Connected { .. }
) {
return Err(AuthenticateError::Other(anyhow::anyhow!(
"sign-in did not complete: {current_status:?}"
)));
}
futures::select_biased! {
_ = current_user.next().fuse() => {},
_ = status.next().fuse() => {},
}
}
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>,
is_zed_model_provider_enabled: bool,
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) => (
if self.is_zed_model_provider_enabled {
"You have access to Zed's hosted models through your organization."
} else {
"Zed's hosted models are disabled by your organization's configuration."
},
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.sign_in(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);
let is_zed_model_provider_enabled = user_store
.current_organization_configuration()
.map_or(true, |config| config.is_zed_model_provider_enabled);
ZedAiConfiguration {
is_connected: !state.is_signed_out(cx),
plan: user_store.plan(),
is_zed_model_provider_enabled,
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(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use client::{Credentials, test::make_get_authenticated_user_response};
use clock::FakeSystemClock;
use feature_flags::FeatureFlagAppExt as _;
use gpui::TestAppContext;
use http_client::{FakeHttpClient, Method, Response};
use std::sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
};
const TEST_USER_ID: u64 = 42;
fn init_test(cx: &mut App) -> (Arc<Client>, Entity<UserStore>, CloudLanguageModelProvider) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
cx.set_global(db::AppDatabase::test_new());
let app_version = AppVersion::global(cx);
release_channel::init_test(app_version, release_channel::ReleaseChannel::Dev, cx);
gpui_tokio::init(cx);
cx.update_flags(false, Vec::new());
let client = Client::new(
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_404_response(),
cx,
);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
let provider = CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx);
(client, user_store, provider)
}
fn override_authenticate(
client: &Arc<Client>,
authenticate_rx: futures::channel::oneshot::Receiver<anyhow::Result<Credentials>>,
) {
let authenticate_rx = Arc::new(Mutex::new(Some(authenticate_rx)));
client.override_authenticate(move |cx| {
let authenticate_rx = authenticate_rx.clone();
cx.background_spawn(async move {
let authenticate_rx = authenticate_rx
.lock()
.expect("authenticate receiver lock poisoned")
.take()
.expect("authenticate receiver already used");
authenticate_rx.await?
})
});
}
fn respond_to_authenticated_user_after(
client: &Arc<Client>,
authenticated_user_rx: futures::channel::oneshot::Receiver<()>,
) {
let authenticated_user_rx = Arc::new(Mutex::new(Some(authenticated_user_rx)));
client
.http_client()
.as_fake()
.replace_handler(move |old_handler, request| {
let authenticated_user_rx = authenticated_user_rx.clone();
async move {
if request.method() == Method::GET && request.uri().path() == "/client/users/me"
{
let authenticated_user_rx = authenticated_user_rx
.lock()
.expect("authenticated user receiver lock poisoned")
.take();
if let Some(authenticated_user_rx) = authenticated_user_rx {
authenticated_user_rx.await.ok();
}
return Ok(Response::builder()
.status(200)
.body(
serde_json::to_string(&make_get_authenticated_user_response(
TEST_USER_ID as i32,
format!("user-{TEST_USER_ID}"),
))
.expect("failed to serialize authenticated user response")
.into(),
)
.expect("failed to build authenticated user response"));
}
old_handler(request).await
}
});
}
async fn sign_in_until_authenticating(
client: Arc<Client>,
cx: &mut TestAppContext,
) -> Task<anyhow::Result<Credentials>> {
let mut status = client.status();
let sign_in_task = cx.update(|cx| {
cx.spawn({
let client = client.clone();
async move |cx| client.sign_in(false, cx).await
})
});
while !status.borrow().is_signing_in() {
status.next().await;
}
sign_in_task
}
#[gpui::test]
async fn provider_authenticate_does_not_start_sign_in_when_signed_out(cx: &mut TestAppContext) {
let (client, _user_store, provider) = cx.update(init_test);
let authenticate_calls = Arc::new(AtomicUsize::new(0));
client.override_authenticate({
let authenticate_calls = authenticate_calls.clone();
move |_| {
authenticate_calls.fetch_add(1, Ordering::SeqCst);
Task::ready(Err(anyhow::anyhow!(
"provider authenticate should not start sign-in"
)))
}
});
assert!(!cx.read(|cx| provider.is_authenticated(cx)));
assert!(matches!(
*client.status().borrow(),
client::Status::SignedOut
));
cx.update(|cx| provider.authenticate(cx))
.now_or_never()
.expect("authenticate should return immediately when signed out")
.expect("authenticate should not fail when no sign-in is in progress");
cx.executor().run_until_parked();
assert_eq!(authenticate_calls.load(Ordering::SeqCst), 0);
assert!(matches!(
*client.status().borrow(),
client::Status::SignedOut
));
assert!(!cx.read(|cx| provider.is_authenticated(cx)));
}
#[gpui::test]
async fn provider_authenticate_waits_for_current_user(cx: &mut TestAppContext) {
let (client, _user_store, provider) = cx.update(init_test);
let (authenticate_tx, authenticate_rx) = futures::channel::oneshot::channel();
let (authenticated_user_tx, authenticated_user_rx) = futures::channel::oneshot::channel();
override_authenticate(&client, authenticate_rx);
respond_to_authenticated_user_after(&client, authenticated_user_rx);
let sign_in_task = sign_in_until_authenticating(client.clone(), cx).await;
let authenticate_task = cx.update(|cx| provider.authenticate(cx));
authenticate_tx
.send(Ok(Credentials {
user_id: TEST_USER_ID,
access_token: "token".to_string(),
}))
.expect("authenticate receiver dropped");
cx.executor().run_until_parked();
assert!(!cx.read(|cx| provider.is_authenticated(cx)));
authenticated_user_tx
.send(())
.expect("authenticated user receiver dropped");
sign_in_task
.await
.expect("sign-in should complete after user response");
authenticate_task
.await
.expect("provider authentication should complete after current user is populated");
assert!(cx.read(|cx| provider.is_authenticated(cx)));
cx.update(|cx| provider.authenticate(cx))
.now_or_never()
.expect("already-authenticated provider should authenticate immediately")
.unwrap();
}
#[gpui::test]
async fn provider_authenticate_returns_error_when_sign_in_fails(cx: &mut TestAppContext) {
let (client, _user_store, provider) = cx.update(init_test);
let (authenticate_tx, authenticate_rx) = futures::channel::oneshot::channel();
override_authenticate(&client, authenticate_rx);
let sign_in_task = sign_in_until_authenticating(client.clone(), cx).await;
let authenticate_task = cx.update(|cx| provider.authenticate(cx));
authenticate_tx
.send(Err(anyhow::anyhow!("test authentication failed")))
.expect("authenticate receiver dropped");
sign_in_task
.await
.expect_err("sign-in should report authentication failure");
let error = authenticate_task
.await
.expect_err("provider authentication should fail when sign-in fails");
assert!(error.to_string().contains("AuthenticationError"));
}
}
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> {
struct PreviewConfiguration {
plan: Option<Plan>,
is_connected: bool,
is_zed_model_provider_enabled: bool,
eligible_for_trial: bool,
}
let configuration = |config: PreviewConfiguration| -> AnyElement {
ZedAiConfiguration {
is_connected: config.is_connected,
plan: config.plan,
is_zed_model_provider_enabled: config.is_zed_model_provider_enabled,
eligible_for_trial: config.eligible_for_trial,
account_too_young: false,
sign_in_callback: Arc::new(|_, _| {}),
}
.into_any_element()
};
Some(
v_flex()
.p_4()
.gap_4()
.children(vec![
single_example(
"Not connected",
configuration(PreviewConfiguration {
plan: None,
is_connected: false,
is_zed_model_provider_enabled: true,
eligible_for_trial: false,
}),
),
single_example(
"Accept Terms of Service",
configuration(PreviewConfiguration {
plan: None,
is_connected: true,
is_zed_model_provider_enabled: true,
eligible_for_trial: true,
}),
),
single_example(
"No Plan - Not eligible for trial",
configuration(PreviewConfiguration {
plan: None,
is_connected: true,
is_zed_model_provider_enabled: true,
eligible_for_trial: false,
}),
),
single_example(
"No Plan - Eligible for trial",
configuration(PreviewConfiguration {
plan: None,
is_connected: true,
is_zed_model_provider_enabled: true,
eligible_for_trial: true,
}),
),
single_example(
"Free Plan",
configuration(PreviewConfiguration {
plan: Some(Plan::ZedFree),
is_connected: true,
is_zed_model_provider_enabled: true,
eligible_for_trial: true,
}),
),
single_example(
"Zed Pro Trial Plan",
configuration(PreviewConfiguration {
plan: Some(Plan::ZedProTrial),
is_connected: true,
is_zed_model_provider_enabled: true,
eligible_for_trial: true,
}),
),
single_example(
"Zed Pro Plan",
configuration(PreviewConfiguration {
plan: Some(Plan::ZedPro),
is_connected: true,
is_zed_model_provider_enabled: true,
eligible_for_trial: true,
}),
),
single_example(
"Business Plan - Zed models enabled",
configuration(PreviewConfiguration {
plan: Some(Plan::ZedBusiness),
is_connected: true,
is_zed_model_provider_enabled: true,
eligible_for_trial: false,
}),
),
single_example(
"Business Plan - Zed models disabled",
configuration(PreviewConfiguration {
plan: Some(Plan::ZedBusiness),
is_connected: true,
is_zed_model_provider_enabled: false,
eligible_for_trial: false,
}),
),
])
.into_any_element(),
)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,755 @@
use anyhow::{Result, anyhow};
use collections::{HashMap, IndexMap};
use credentials_provider::CredentialsProvider;
use deepseek::DEEPSEEK_API_URL;
use futures::Stream;
use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, RateLimiter, Role,
StopReason, TokenUsage, env_var,
};
pub use settings::DeepseekAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
use std::pin::Pin;
use std::sync::{Arc, LazyLock};
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
use language_model::util::{fix_streamed_json, parse_tool_arguments};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("DeepSeek");
const API_KEY_ENV_VAR_NAME: &str = "DEEPSEEK_API_KEY";
static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
#[derive(Default)]
struct RawToolCall {
id: String,
name: String,
arguments: String,
}
#[derive(Default, Clone, Debug, PartialEq)]
pub struct DeepSeekSettings {
pub api_url: String,
pub available_models: Vec<AvailableModel>,
}
pub struct DeepSeekLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
state: Entity<State>,
}
pub struct State {
api_key_state: ApiKeyState,
credentials_provider: Arc<dyn CredentialsProvider>,
}
impl State {
fn is_authenticated(&self) -> bool {
self.api_key_state.has_key()
}
fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = DeepSeekLanguageModelProvider::api_url(cx);
self.api_key_state.store(
api_url,
api_key,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = DeepSeekLanguageModelProvider::api_url(cx);
self.api_key_state.load_if_needed(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
}
impl DeepSeekLanguageModelProvider {
pub fn new(
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let credentials_provider = this.credentials_provider.clone();
let api_url = Self::api_url(cx);
this.api_key_state.handle_url_change(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
cx.notify();
})
.detach();
State {
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
credentials_provider,
}
});
Self { http_client, state }
}
fn create_language_model(&self, model: deepseek::Model) -> Arc<dyn LanguageModel> {
Arc::new(DeepSeekLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
})
}
fn settings(cx: &App) -> &DeepSeekSettings {
&crate::AllLanguageModelSettings::get_global(cx).deepseek
}
fn api_url(cx: &App) -> SharedString {
let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
DEEPSEEK_API_URL.into()
} else {
SharedString::new(api_url.as_str())
}
}
}
impl LanguageModelProviderState for DeepSeekLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for DeepSeekLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiDeepSeek)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(deepseek::Model::default()))
}
fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(deepseek::Model::default_fast()))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = IndexMap::default();
models.insert("deepseek-v4-flash", deepseek::Model::V4Flash);
models.insert("deepseek-v4-pro", deepseek::Model::V4Pro);
for available_model in &Self::settings(cx).available_models {
models.insert(
&available_model.name,
deepseek::Model::Custom {
name: available_model.name.clone(),
display_name: available_model.display_name.clone(),
max_tokens: available_model.max_tokens,
max_output_tokens: available_model.max_output_tokens,
},
);
}
models
.into_values()
.map(|model| self.create_language_model(model))
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(None, cx))
}
}
pub struct DeepSeekLanguageModel {
id: LanguageModelId,
model: deepseek::Model,
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
impl DeepSeekLanguageModel {
fn stream_completion(
&self,
request: deepseek::Request,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<deepseek::StreamResponse>>>> {
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
let api_url = DeepSeekLanguageModelProvider::api_url(cx);
(state.api_key_state.key(&api_url), api_url)
});
let future = self.request_limiter.stream(async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey {
provider: PROVIDER_NAME,
});
};
let request =
deepseek::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
}
impl LanguageModel for DeepSeekLanguageModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(self.model.display_name().to_string())
}
fn provider_id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
true
}
fn supports_streaming_tools(&self) -> bool {
true
}
fn supports_thinking(&self) -> bool {
matches!(
self.model,
deepseek::Model::V4Flash | deepseek::Model::V4Pro
)
}
fn supported_effort_levels(&self) -> Vec<LanguageModelEffortLevel> {
if !self.supports_thinking() {
return Vec::new();
}
vec![
LanguageModelEffortLevel {
name: "High".into(),
value: "high".into(),
is_default: true,
},
LanguageModelEffortLevel {
name: "Max".into(),
value: "max".into(),
is_default: false,
},
]
}
fn supports_tool_choice(&self, _choice: LanguageModelToolChoice) -> bool {
true
}
fn supports_images(&self) -> bool {
false
}
fn telemetry_id(&self) -> String {
format!("deepseek/{}", self.model.id())
}
fn max_token_count(&self) -> u64 {
self.model.max_token_count()
}
fn max_output_tokens(&self) -> Option<u64> {
self.model.max_output_tokens()
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = into_deepseek(request, &self.model, self.max_output_tokens());
let stream = self.stream_completion(request, cx);
async move {
let mapper = DeepSeekEventMapper::new();
Ok(mapper.map_stream(stream.await?).boxed())
}
.boxed()
}
}
pub fn into_deepseek(
request: LanguageModelRequest,
model: &deepseek::Model,
max_output_tokens: Option<u64>,
) -> deepseek::Request {
let thinking = deepseek_thinking(model, request.thinking_allowed);
let thinking_enabled = thinking
.as_ref()
.is_some_and(|thinking| thinking.kind == deepseek::ThinkingType::Enabled);
let mut messages = Vec::new();
let mut current_reasoning: Option<String> = None;
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) => {
let should_add = if message.role == Role::User {
!text.trim().is_empty()
} else {
!text.is_empty()
};
if should_add {
messages.push(match message.role {
Role::User => deepseek::RequestMessage::User { content: text },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
reasoning_content: current_reasoning.take(),
},
Role::System => deepseek::RequestMessage::System { content: text },
});
}
}
MessageContent::Thinking { text, .. } => {
// Accumulate reasoning content for next assistant message
current_reasoning.get_or_insert_default().push_str(&text);
}
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
let tool_call = deepseek::ToolCall {
id: tool_use.id.to_string(),
content: deepseek::ToolCallContent::Function {
function: deepseek::FunctionContent {
name: tool_use.name.to_string(),
arguments: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
},
},
};
if let Some(deepseek::RequestMessage::Assistant { tool_calls, .. }) =
messages.last_mut()
{
tool_calls.push(tool_call);
} else {
messages.push(deepseek::RequestMessage::Assistant {
content: None,
tool_calls: vec![tool_call],
reasoning_content: current_reasoning.take(),
});
}
}
MessageContent::ToolResult(tool_result) => {
let mut text_parts: Vec<String> = Vec::new();
for part in &tool_result.content {
match part {
LanguageModelToolResultContent::Text(text) => {
text_parts.push(text.to_string());
}
LanguageModelToolResultContent::Image(_) => {
text_parts.push("[Tool responded with an image]".to_string());
}
}
}
let content = if text_parts.is_empty() {
"<Tool returned an empty string>".to_string()
} else {
text_parts.join("\n")
};
messages.push(deepseek::RequestMessage::Tool {
content,
tool_call_id: tool_result.tool_use_id.to_string(),
});
}
}
}
}
deepseek::Request {
model: model.id().to_string(),
messages,
stream: true,
max_tokens: max_output_tokens,
temperature: if thinking_enabled {
None
} else {
request.temperature
},
thinking,
reasoning_effort: if thinking_enabled {
into_deepseek_reasoning_effort(request.thinking_effort.as_deref())
} else {
None
},
response_format: None,
tool_choice: request.tool_choice.map(|choice| match choice {
LanguageModelToolChoice::Auto => deepseek::ToolChoice::Auto,
LanguageModelToolChoice::Any => deepseek::ToolChoice::Required,
LanguageModelToolChoice::None => deepseek::ToolChoice::None,
}),
tools: request
.tools
.into_iter()
.map(|tool| deepseek::ToolDefinition::Function {
function: deepseek::FunctionDefinition {
name: tool.name,
description: Some(tool.description),
parameters: Some(tool.input_schema),
},
})
.collect(),
}
}
fn deepseek_thinking(
model: &deepseek::Model,
thinking_allowed: bool,
) -> Option<deepseek::Thinking> {
let kind = match model {
deepseek::Model::V4Flash | deepseek::Model::V4Pro => {
if thinking_allowed {
deepseek::ThinkingType::Enabled
} else {
deepseek::ThinkingType::Disabled
}
}
deepseek::Model::Custom { .. } => return None,
};
Some(deepseek::Thinking { kind })
}
fn into_deepseek_reasoning_effort(effort: Option<&str>) -> Option<deepseek::ReasoningEffort> {
match effort {
Some("high") => Some(deepseek::ReasoningEffort::High),
Some("max") => Some(deepseek::ReasoningEffort::Max),
_ => None,
}
}
pub struct DeepSeekEventMapper {
tool_calls_by_index: HashMap<usize, RawToolCall>,
}
impl DeepSeekEventMapper {
pub fn new() -> Self {
Self {
tool_calls_by_index: HashMap::default(),
}
}
pub fn map_stream(
mut self,
events: Pin<Box<dyn Send + Stream<Item = Result<deepseek::StreamResponse>>>>,
) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
{
events.flat_map(move |event| {
futures::stream::iter(match event {
Ok(event) => self.map_event(event),
Err(error) => vec![Err(LanguageModelCompletionError::from(error))],
})
})
}
pub fn map_event(
&mut self,
event: deepseek::StreamResponse,
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
let Some(choice) = event.choices.first() else {
return vec![Err(LanguageModelCompletionError::from(anyhow!(
"Response contained no choices"
)))];
};
let mut events = Vec::new();
if let Some(content) = choice.delta.content.clone()
&& !content.is_empty()
{
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
if let Some(reasoning_content) = choice.delta.reasoning_content.clone() {
events.push(Ok(LanguageModelCompletionEvent::Thinking {
text: reasoning_content,
signature: None,
}));
}
if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
for tool_call in tool_calls {
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
if let Some(tool_id) = tool_call.id.clone() {
entry.id = tool_id;
}
if let Some(function) = tool_call.function.as_ref() {
if let Some(name) = function.name.clone() {
entry.name = name;
}
if let Some(arguments) = function.arguments.clone() {
entry.arguments.push_str(&arguments);
}
}
if !entry.id.is_empty() && !entry.name.is_empty() {
if let Ok(input) = serde_json::from_str::<serde_json::Value>(
&fix_streamed_json(&entry.arguments),
) {
events.push(Ok(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: entry.id.clone().into(),
name: entry.name.as_str().into(),
is_input_complete: false,
input,
raw_input: entry.arguments.clone(),
thought_signature: None,
},
)));
}
}
}
}
if let Some(usage) = event.usage {
events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
input_tokens: usage.prompt_tokens,
output_tokens: usage.completion_tokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
})));
}
match choice.finish_reason.as_deref() {
Some("stop") => {
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
}
Some("tool_calls") => {
events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
match parse_tool_arguments(&tool_call.arguments) {
Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: tool_call.id.clone().into(),
name: tool_call.name.as_str().into(),
is_input_complete: true,
input,
raw_input: tool_call.arguments.clone(),
thought_signature: None,
},
)),
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
id: tool_call.id.clone().into(),
tool_name: tool_call.name.as_str().into(),
raw_input: tool_call.arguments.into(),
json_parse_error: error.to_string(),
}),
}
}));
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
}
Some(stop_reason) => {
log::error!("Unexpected DeepSeek stop_reason: {stop_reason:?}",);
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
}
None => {}
}
events
}
}
struct ConfigurationView {
api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor =
cx.new(|cx| InputField::new(window, cx, "sk-00000000000000000000000000000000"));
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let load_credentials_task = Some(cx.spawn({
let state = state.clone();
async move |this, cx| {
if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
let _ = task.await;
}
this.update(cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor,
state,
load_credentials_task,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
if api_key.is_empty() {
return;
}
let state = self.state.clone();
cx.spawn(async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
.await
})
.detach_and_log_err(cx);
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn(async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(None, cx))
.await
})
.detach_and_log_err(cx);
}
fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
let configured_card_label = if env_var_set {
format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
} else {
let api_url = DeepSeekLanguageModelProvider::api_url(cx);
if api_url == DEEPSEEK_API_URL {
"API key configured".to_string()
} else {
format!("API key configured for {}", api_url)
}
};
if self.load_credentials_task.is_some() {
div()
.child(Label::new("Loading credentials..."))
.into_any_element()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new("To use DeepSeek in Zed, you need an API key:"))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Get your API key from the"))
.child(ButtonLink::new(
"DeepSeek console",
"https://platform.deepseek.com/api_keys",
)),
)
.child(ListBulletItem::new(
"Paste your API key below and hit enter to start using the assistant",
)),
)
.child(self.api_key_editor.clone())
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.into_any_element()
}
}
}
@@ -0,0 +1,495 @@
use anyhow::{Context as _, Result};
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use futures::{FutureExt, StreamExt, future::BoxFuture};
pub use google_ai::completion::{GoogleEventMapper, into_google};
use google_ai::{GenerateContentResponse, GoogleModelMode};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::HttpClient;
use language_model::{
AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat,
};
use language_model::{
GOOGLE_PROVIDER_ID, GOOGLE_PROVIDER_NAME, IconOrSvg, LanguageModel, LanguageModelId,
LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use settings::GoogleAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
use language_model::ApiKeyState;
const PROVIDER_ID: LanguageModelProviderId = GOOGLE_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = GOOGLE_PROVIDER_NAME;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct GoogleSettings {
pub api_url: String,
pub available_models: Vec<AvailableModel>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ModelMode {
#[default]
Default,
Thinking {
/// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`.
budget_tokens: Option<u32>,
},
}
pub struct GoogleLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
state: Entity<State>,
}
pub struct State {
api_key_state: ApiKeyState,
credentials_provider: Arc<dyn CredentialsProvider>,
}
const GEMINI_API_KEY_VAR_NAME: &str = "GEMINI_API_KEY";
const GOOGLE_AI_API_KEY_VAR_NAME: &str = "GOOGLE_AI_API_KEY";
static API_KEY_ENV_VAR: LazyLock<EnvVar> = LazyLock::new(|| {
// Try GEMINI_API_KEY first as primary, fallback to GOOGLE_AI_API_KEY
EnvVar::new(GEMINI_API_KEY_VAR_NAME.into()).or(EnvVar::new(GOOGLE_AI_API_KEY_VAR_NAME.into()))
});
impl State {
fn is_authenticated(&self) -> bool {
self.api_key_state.has_key()
}
fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = GoogleLanguageModelProvider::api_url(cx);
self.api_key_state.store(
api_url,
api_key,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = GoogleLanguageModelProvider::api_url(cx);
self.api_key_state.load_if_needed(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
}
impl GoogleLanguageModelProvider {
pub fn new(
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let credentials_provider = this.credentials_provider.clone();
let api_url = Self::api_url(cx);
this.api_key_state.handle_url_change(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
cx.notify();
})
.detach();
State {
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
credentials_provider,
}
});
Self { http_client, state }
}
fn create_language_model(&self, model: google_ai::Model) -> Arc<dyn LanguageModel> {
Arc::new(GoogleLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
})
}
fn settings(cx: &App) -> &GoogleSettings {
&crate::AllLanguageModelSettings::get_global(cx).google
}
fn api_url(cx: &App) -> SharedString {
let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
google_ai::API_URL.into()
} else {
SharedString::new(api_url.as_str())
}
}
}
impl LanguageModelProviderState for GoogleLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for GoogleLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiGoogle)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(google_ai::Model::default()))
}
fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(google_ai::Model::default_fast()))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
// Add base models from google_ai::Model::iter()
for model in google_ai::Model::iter() {
if !matches!(model, google_ai::Model::Custom { .. }) {
models.insert(model.id().to_string(), model);
}
}
// Override with available models from settings
for model in &GoogleLanguageModelProvider::settings(cx).available_models {
models.insert(
model.name.clone(),
google_ai::Model::Custom {
name: model.name.clone(),
display_name: model.display_name.clone(),
max_tokens: model.max_tokens,
mode: model.mode.unwrap_or_default(),
},
);
}
models
.into_values()
.map(|model| {
Arc::new(GoogleLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
}) as Arc<dyn LanguageModel>
})
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(
&self,
target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
.into()
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(None, cx))
}
}
pub struct GoogleLanguageModel {
id: LanguageModelId,
model: google_ai::Model,
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
impl GoogleLanguageModel {
fn stream_completion(
&self,
request: google_ai::GenerateContentRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<futures::stream::BoxStream<'static, Result<GenerateContentResponse>>>,
> {
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
let api_url = GoogleLanguageModelProvider::api_url(cx);
(state.api_key_state.key(&api_url), api_url)
});
async move {
let api_key = api_key.context("Missing Google API key")?;
let request = google_ai::stream_generate_content(
http_client.as_ref(),
&api_url,
&api_key,
request,
);
request.await.context("failed to stream completion")
}
.boxed()
}
}
impl LanguageModel for GoogleLanguageModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(self.model.display_name().to_string())
}
fn provider_id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
self.model.supports_tools()
}
fn supports_images(&self) -> bool {
self.model.supports_images()
}
fn supports_thinking(&self) -> bool {
matches!(self.model.mode(), GoogleModelMode::Thinking { .. })
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
match choice {
LanguageModelToolChoice::Auto
| LanguageModelToolChoice::Any
| LanguageModelToolChoice::None => true,
}
}
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
LanguageModelToolSchemaFormat::JsonSchemaSubset
}
fn telemetry_id(&self) -> String {
format!("google/{}", self.model.request_id())
}
fn max_token_count(&self) -> u64 {
self.model.max_token_count()
}
fn max_output_tokens(&self) -> Option<u64> {
self.model.max_output_tokens()
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = into_google(
request,
self.model.request_id().to_string(),
self.model.mode(),
);
let request = self.stream_completion(request, cx);
let future = self.request_limiter.stream(async move {
let response = request.await.map_err(LanguageModelCompletionError::from)?;
Ok(GoogleEventMapper::new().map_stream(response))
});
async move { Ok(future.await?.boxed()) }.boxed()
}
}
struct ConfigurationView {
api_key_editor: Entity<InputField>,
state: Entity<State>,
target_agent: language_model::ConfigurationViewTargetAgent,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(
state: Entity<State>,
target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let load_credentials_task = Some(cx.spawn_in(window, {
let state = state.clone();
async move |this, cx| {
if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
// We don't log an error, because "not signed in" is also an error.
let _ = task.await;
}
this.update(cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor: cx.new(|cx| InputField::new(window, cx, "AIzaSy...")),
target_agent,
state,
load_credentials_task,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
if api_key.is_empty() {
return;
}
// url changes can cause the editor to be displayed again
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
.await
})
.detach_and_log_err(cx);
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(None, cx))
.await
})
.detach_and_log_err(cx);
}
fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
let configured_card_label = if env_var_set {
format!(
"API key set in {} environment variable",
API_KEY_ENV_VAR.name
)
} else {
let api_url = GoogleLanguageModelProvider::api_url(cx);
if api_url == google_ai::API_URL {
"API key configured".to_string()
} else {
format!("API key configured for {}", api_url)
}
};
if self.load_credentials_task.is_some() {
div()
.child(Label::new("Loading credentials..."))
.into_any_element()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI".into(),
ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
})))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Create one by visiting"))
.child(ButtonLink::new("Google AI's console", "https://aistudio.google.com/app/apikey"))
)
.child(
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
)
)
.child(self.api_key_editor.clone())
.child(
Label::new(
format!("You can also set the {GEMINI_API_KEY_VAR_NAME} environment variable and restart Zed."),
)
.size(LabelSize::Small).color(Color::Muted),
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
this.tooltip_label(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR_NAME} and {GOOGLE_AI_API_KEY_VAR_NAME} environment variables are unset."))
})
.into_any_element()
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,706 @@
use anyhow::Result;
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use futures::{FutureExt, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, OPEN_AI_PROVIDER_ID,
OPEN_AI_PROVIDER_NAME, RateLimiter, env_var,
};
use menu;
use open_ai::{
OPEN_AI_API_URL, ResponseStreamEvent,
responses::{Request as ResponseRequest, StreamEvent as ResponsesStreamEvent, stream_response},
stream_completion,
};
use settings::{OpenAiAvailableModel as AvailableModel, Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
pub use open_ai::completion::{
OpenAiEventMapper, OpenAiResponseEventMapper, into_open_ai, into_open_ai_response,
};
const PROVIDER_ID: LanguageModelProviderId = OPEN_AI_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = OPEN_AI_PROVIDER_NAME;
const API_KEY_ENV_VAR_NAME: &str = "OPENAI_API_KEY";
static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
#[derive(Default, Clone, Debug, PartialEq)]
pub struct OpenAiSettings {
pub api_url: String,
pub available_models: Vec<AvailableModel>,
}
pub struct OpenAiLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
state: Entity<State>,
}
pub struct State {
api_key_state: ApiKeyState,
credentials_provider: Arc<dyn CredentialsProvider>,
}
impl State {
fn is_authenticated(&self) -> bool {
self.api_key_state.has_key()
}
fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = OpenAiLanguageModelProvider::api_url(cx);
self.api_key_state.store(
api_url,
api_key,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = OpenAiLanguageModelProvider::api_url(cx);
self.api_key_state.load_if_needed(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
}
impl OpenAiLanguageModelProvider {
pub fn new(
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let credentials_provider = this.credentials_provider.clone();
let api_url = Self::api_url(cx);
this.api_key_state.handle_url_change(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
cx.notify();
})
.detach();
State {
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
credentials_provider,
}
});
Self { http_client, state }
}
fn create_language_model(&self, model: open_ai::Model) -> Arc<dyn LanguageModel> {
Arc::new(OpenAiLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
})
}
fn settings(cx: &App) -> &OpenAiSettings {
&crate::AllLanguageModelSettings::get_global(cx).openai
}
fn api_url(cx: &App) -> SharedString {
let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
open_ai::OPEN_AI_API_URL.into()
} else {
SharedString::new(api_url.as_str())
}
}
}
impl LanguageModelProviderState for OpenAiLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for OpenAiLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenAi)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(open_ai::Model::default()))
}
fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(open_ai::Model::default_fast()))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
// Add base models from open_ai::Model::iter()
for model in open_ai::Model::iter() {
if !matches!(model, open_ai::Model::Custom { .. }) {
models.insert(model.id().to_string(), model);
}
}
// Override with available models from settings
for model in &OpenAiLanguageModelProvider::settings(cx).available_models {
models.insert(
model.name.clone(),
open_ai::Model::Custom {
name: model.name.clone(),
display_name: model.display_name.clone(),
max_tokens: model.max_tokens,
max_output_tokens: model.max_output_tokens,
max_completion_tokens: model.max_completion_tokens,
reasoning_effort: model.reasoning_effort,
supports_chat_completions: model.capabilities.chat_completions,
supports_images: model.capabilities.images,
},
);
}
models
.into_values()
.map(|model| self.create_language_model(model))
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(None, cx))
}
}
fn default_thinking_reasoning_effort(model: &open_ai::Model) -> Option<open_ai::ReasoningEffort> {
use open_ai::ReasoningEffort;
model
.reasoning_effort()
.filter(|effort| *effort != ReasoningEffort::None)
.or_else(|| {
let supported_efforts = model.supported_reasoning_efforts();
if supported_efforts.contains(&ReasoningEffort::Medium) {
Some(ReasoningEffort::Medium)
} else {
supported_efforts
.iter()
.copied()
.find(|effort| *effort != ReasoningEffort::None)
}
})
}
fn supports_selectable_thinking_effort(model: &open_ai::Model) -> bool {
model.uses_responses_api()
&& model
.supported_reasoning_efforts()
.iter()
.any(|effort| *effort != open_ai::ReasoningEffort::None)
}
fn supported_thinking_effort_levels(model: &open_ai::Model) -> Vec<LanguageModelEffortLevel> {
if !supports_selectable_thinking_effort(model) {
return Vec::new();
}
let default_effort = default_thinking_reasoning_effort(model);
model
.supported_reasoning_efforts()
.iter()
.copied()
.filter_map(|effort| {
let (name, value) = match effort {
open_ai::ReasoningEffort::None => return None,
open_ai::ReasoningEffort::Minimal => ("Minimal", "minimal"),
open_ai::ReasoningEffort::Low => ("Low", "low"),
open_ai::ReasoningEffort::Medium => ("Medium", "medium"),
open_ai::ReasoningEffort::High => ("High", "high"),
open_ai::ReasoningEffort::XHigh => ("Extra High", "xhigh"),
};
Some(LanguageModelEffortLevel {
name: name.into(),
value: value.into(),
is_default: Some(effort) == default_effort,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn supported_thinking_effort_levels_hide_none() {
let effort_levels = supported_thinking_effort_levels(&open_ai::Model::FivePointTwo);
let values = effort_levels
.iter()
.map(|level| level.value.as_ref())
.collect::<Vec<_>>();
assert_eq!(values, ["low", "medium", "high", "xhigh"]);
assert_eq!(
effort_levels
.iter()
.find(|level| level.is_default)
.map(|level| level.value.as_ref()),
Some("medium")
);
}
#[test]
fn models_supporting_only_none_have_no_selectable_thinking_effort() {
let model = open_ai::Model::Custom {
name: "custom-model".to_string(),
display_name: None,
max_tokens: 128_000,
max_output_tokens: None,
max_completion_tokens: None,
reasoning_effort: Some(open_ai::ReasoningEffort::None),
supports_chat_completions: false,
supports_images: true,
};
assert!(!supports_selectable_thinking_effort(&model));
assert!(supported_thinking_effort_levels(&model).is_empty());
assert!(
model
.supported_reasoning_efforts()
.contains(&open_ai::ReasoningEffort::None)
);
}
}
pub struct OpenAiLanguageModel {
id: LanguageModelId,
model: open_ai::Model,
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
impl OpenAiLanguageModel {
fn stream_completion(
&self,
request: open_ai::Request,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<ResponseStreamEvent>>>>
{
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
let api_url = OpenAiLanguageModelProvider::api_url(cx);
(state.api_key_state.key(&api_url), api_url)
});
let future = self.request_limiter.stream(async move {
let provider = PROVIDER_NAME;
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey { provider });
};
let request = stream_completion(
http_client.as_ref(),
provider.0.as_str(),
&api_url,
&api_key,
request,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
fn stream_response(
&self,
request: ResponseRequest,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<ResponsesStreamEvent>>>>
{
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
let api_url = OpenAiLanguageModelProvider::api_url(cx);
(state.api_key_state.key(&api_url), api_url)
});
let provider = PROVIDER_NAME;
let future = self.request_limiter.stream(async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey { provider });
};
let request = stream_response(
http_client.as_ref(),
provider.0.as_str(),
&api_url,
&api_key,
request,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
}
impl LanguageModel for OpenAiLanguageModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(self.model.display_name().to_string())
}
fn provider_id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
true
}
fn supports_images(&self) -> bool {
use open_ai::Model;
match &self.model {
Model::FourOmniMini
| Model::Five
| Model::FiveMini
| Model::FiveNano
| Model::FivePointOne
| Model::FivePointTwo
| Model::FivePointThreeCodex
| Model::FivePointFour
| Model::FivePointFourMini
| Model::FivePointFourNano
| Model::FivePointFourPro
| Model::FivePointFive
| Model::FivePointFivePro
| Model::O3 => true,
Model::Four => false,
Model::Custom {
supports_images, ..
} => *supports_images,
}
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
match choice {
LanguageModelToolChoice::Auto => true,
LanguageModelToolChoice::Any => true,
LanguageModelToolChoice::None => true,
}
}
fn supports_streaming_tools(&self) -> bool {
true
}
fn supports_thinking(&self) -> bool {
supports_selectable_thinking_effort(&self.model)
}
fn supported_effort_levels(&self) -> Vec<LanguageModelEffortLevel> {
supported_thinking_effort_levels(&self.model)
}
fn supports_split_token_display(&self) -> bool {
true
}
fn telemetry_id(&self) -> String {
format!("openai/{}", self.model.id())
}
fn max_token_count(&self) -> u64 {
self.model.max_token_count()
}
fn max_output_tokens(&self) -> Option<u64> {
self.model.max_output_tokens()
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
if self.model.uses_responses_api() {
let request = into_open_ai_response(
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
default_thinking_reasoning_effort(&self.model),
self.model
.supported_reasoning_efforts()
.contains(&open_ai::ReasoningEffort::None),
);
let completions = self.stream_response(request, cx);
async move {
let mapper = OpenAiResponseEventMapper::new();
Ok(mapper.map_stream(completions.await?).boxed())
}
.boxed()
} else {
let request = into_open_ai(
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
None,
false,
);
let completions = self.stream_completion(request, cx);
async move {
let mapper = OpenAiEventMapper::new();
Ok(mapper.map_stream(completions.await?).boxed())
}
.boxed()
}
}
}
struct ConfigurationView {
api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor = cx.new(|cx| {
InputField::new(
window,
cx,
"sk-000000000000000000000000000000000000000000000000",
)
});
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let load_credentials_task = Some(cx.spawn_in(window, {
let state = state.clone();
async move |this, cx| {
if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
// We don't log an error, because "not signed in" is also an error.
let _ = task.await;
}
this.update(cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor,
state,
load_credentials_task,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
if api_key.is_empty() {
return;
}
// url changes can cause the editor to be displayed again
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
.await
})
.detach_and_log_err(cx);
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor
.update(cx, |input, cx| input.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(None, cx))
.await
})
.detach_and_log_err(cx);
}
fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
let configured_card_label = if env_var_set {
format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
} else {
let api_url = OpenAiLanguageModelProvider::api_url(cx);
if api_url == OPEN_AI_API_URL {
"API key configured".to_string()
} else {
format!("API key configured for {}", api_url)
}
};
let api_key_section = if self.should_render_editor(cx) {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new("To use Zed's agent with OpenAI, you need to add an API key. Follow these steps:"))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Create one by visiting"))
.child(ButtonLink::new("OpenAI's console", "https://platform.openai.com/api-keys"))
)
.child(
ListBulletItem::new("Ensure your OpenAI account has credits")
)
.child(
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
),
)
.child(self.api_key_editor.clone())
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new(
"Note that having a subscription for another service like GitHub Copilot won't work.",
)
.size(LabelSize::Small).color(Color::Muted),
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
})
.into_any_element()
};
let compatible_api_section = h_flex()
.mt_1p5()
.gap_0p5()
.flex_wrap()
.when(self.should_render_editor(cx), |this| {
this.pt_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
})
.child(
h_flex()
.gap_2()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new("Zed also supports OpenAI-compatible models.")),
)
.child(
Button::new("docs", "Learn More")
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::Small)
.color(Color::Muted),
)
.on_click(move |_, _window, cx| {
cx.open_url("https://zed.dev/docs/ai/llm-providers#openai-api-compatible")
}),
);
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials…")).into_any()
} else {
v_flex()
.size_full()
.child(api_key_section)
.child(compatible_api_section)
.into_any()
}
}
}
@@ -0,0 +1,569 @@
use anyhow::Result;
use convert_case::{Case, Casing};
use credentials_provider::CredentialsProvider;
use futures::{FutureExt, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
};
use menu;
use open_ai::{
ResponseStreamEvent,
responses::{Request as ResponseRequest, StreamEvent as ResponsesStreamEvent, stream_response},
stream_completion,
};
use settings::{Settings, SettingsStore};
use std::sync::Arc;
use ui::{ElevationIndex, Tooltip, prelude::*};
use ui_input::InputField;
use util::ResultExt;
use crate::provider::open_ai::{
OpenAiEventMapper, OpenAiResponseEventMapper, into_open_ai, into_open_ai_response,
};
pub use settings::OpenAiCompatibleAvailableModel as AvailableModel;
pub use settings::OpenAiCompatibleModelCapabilities as ModelCapabilities;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct OpenAiCompatibleSettings {
pub api_url: String,
pub available_models: Vec<AvailableModel>,
}
pub struct OpenAiCompatibleLanguageModelProvider {
id: LanguageModelProviderId,
name: LanguageModelProviderName,
http_client: Arc<dyn HttpClient>,
state: Entity<State>,
}
pub struct State {
id: Arc<str>,
api_key_state: ApiKeyState,
settings: OpenAiCompatibleSettings,
credentials_provider: Arc<dyn CredentialsProvider>,
}
impl State {
fn is_authenticated(&self) -> bool {
self.api_key_state.has_key()
}
fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = SharedString::new(self.settings.api_url.as_str());
self.api_key_state.store(
api_url,
api_key,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = SharedString::new(self.settings.api_url.clone());
self.api_key_state.load_if_needed(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
}
impl OpenAiCompatibleLanguageModelProvider {
pub fn new(
id: Arc<str>,
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
fn resolve_settings<'a>(id: &'a str, cx: &'a App) -> Option<&'a OpenAiCompatibleSettings> {
crate::AllLanguageModelSettings::get_global(cx)
.openai_compatible
.get(id)
}
let api_key_env_var_name = format!("{}_API_KEY", id).to_case(Case::UpperSnake).into();
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let Some(settings) = resolve_settings(&this.id, cx).cloned() else {
return;
};
if &this.settings != &settings {
let credentials_provider = this.credentials_provider.clone();
let api_url = SharedString::new(settings.api_url.as_str());
this.api_key_state.handle_url_change(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
this.settings = settings;
cx.notify();
}
})
.detach();
let settings = resolve_settings(&id, cx).cloned().unwrap_or_default();
State {
id: id.clone(),
api_key_state: ApiKeyState::new(
SharedString::new(settings.api_url.as_str()),
EnvVar::new(api_key_env_var_name),
),
settings,
credentials_provider,
}
});
Self {
id: id.clone().into(),
name: id.into(),
http_client,
state,
}
}
fn create_language_model(&self, model: AvailableModel) -> Arc<dyn LanguageModel> {
Arc::new(OpenAiCompatibleLanguageModel {
id: LanguageModelId::from(model.name.clone()),
provider_id: self.id.clone(),
provider_name: self.name.clone(),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
})
}
}
impl LanguageModelProviderState for OpenAiCompatibleLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
self.id.clone()
}
fn name(&self) -> LanguageModelProviderName {
self.name.clone()
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenAiCompat)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
self.state
.read(cx)
.settings
.available_models
.first()
.map(|model| self.create_language_model(model.clone()))
}
fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
None
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
self.state
.read(cx)
.settings
.available_models
.iter()
.map(|model| self.create_language_model(model.clone()))
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(None, cx))
}
}
pub struct OpenAiCompatibleLanguageModel {
id: LanguageModelId,
provider_id: LanguageModelProviderId,
provider_name: LanguageModelProviderName,
model: AvailableModel,
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
impl OpenAiCompatibleLanguageModel {
fn stream_completion(
&self,
request: open_ai::Request,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<'static, Result<ResponseStreamEvent>>,
LanguageModelCompletionError,
>,
> {
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, _cx| {
let api_url = &state.settings.api_url;
(
state.api_key_state.key(api_url),
state.settings.api_url.clone(),
)
});
let provider = self.provider_name.clone();
let future = self.request_limiter.stream(async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey { provider });
};
let request = stream_completion(
http_client.as_ref(),
provider.0.as_str(),
&api_url,
&api_key,
request,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
fn stream_response(
&self,
request: ResponseRequest,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<ResponsesStreamEvent>>>>
{
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, _cx| {
let api_url = &state.settings.api_url;
(
state.api_key_state.key(api_url),
state.settings.api_url.clone(),
)
});
let provider = self.provider_name.clone();
let future = self.request_limiter.stream(async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey { provider });
};
let request = stream_response(
http_client.as_ref(),
provider.0.as_str(),
&api_url,
&api_key,
request,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
}
impl LanguageModel for OpenAiCompatibleLanguageModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(
self.model
.display_name
.clone()
.unwrap_or_else(|| self.model.name.clone()),
)
}
fn provider_id(&self) -> LanguageModelProviderId {
self.provider_id.clone()
}
fn provider_name(&self) -> LanguageModelProviderName {
self.provider_name.clone()
}
fn supports_tools(&self) -> bool {
self.model.capabilities.tools
}
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
LanguageModelToolSchemaFormat::JsonSchemaSubset
}
fn supports_images(&self) -> bool {
self.model.capabilities.images
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
match choice {
LanguageModelToolChoice::Auto => self.model.capabilities.tools,
LanguageModelToolChoice::Any => self.model.capabilities.tools,
LanguageModelToolChoice::None => true,
}
}
fn supports_streaming_tools(&self) -> bool {
true
}
fn supports_split_token_display(&self) -> bool {
true
}
fn telemetry_id(&self) -> String {
format!("openai/{}", self.model.name)
}
fn max_token_count(&self) -> u64 {
self.model.max_tokens
}
fn max_output_tokens(&self) -> Option<u64> {
self.model.max_output_tokens
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
if self.model.capabilities.chat_completions {
let request = into_open_ai(
request,
&self.model.name,
self.model.capabilities.parallel_tool_calls,
self.model.capabilities.prompt_cache_key,
self.max_output_tokens(),
self.model.reasoning_effort,
self.model.capabilities.interleaved_reasoning,
);
let completions = self.stream_completion(request, cx);
async move {
let mapper = OpenAiEventMapper::new();
Ok(mapper.map_stream(completions.await?).boxed())
}
.boxed()
} else {
let request = into_open_ai_response(
request,
&self.model.name,
self.model.capabilities.parallel_tool_calls,
self.model.capabilities.prompt_cache_key,
self.max_output_tokens(),
self.model
.reasoning_effort
.filter(|effort| *effort != open_ai::ReasoningEffort::None),
self.model.reasoning_effort == Some(open_ai::ReasoningEffort::None),
);
let completions = self.stream_response(request, cx);
async move {
let mapper = OpenAiResponseEventMapper::new();
Ok(mapper.map_stream(completions.await?).boxed())
}
.boxed()
}
}
}
struct ConfigurationView {
api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor = cx.new(|cx| {
InputField::new(
window,
cx,
"000000000000000000000000000000000000000000000000000",
)
});
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let load_credentials_task = Some(cx.spawn_in(window, {
let state = state.clone();
async move |this, cx| {
if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
// We don't log an error, because "not signed in" is also an error.
let _ = task.await;
}
this.update(cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor,
state,
load_credentials_task,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
if api_key.is_empty() {
return;
}
// url changes can cause the editor to be displayed again
self.api_key_editor
.update(cx, |input, cx| input.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
.await
})
.detach_and_log_err(cx);
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor
.update(cx, |input, cx| input.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(None, cx))
.await
})
.detach_and_log_err(cx);
}
fn should_render_editor(&self, cx: &Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let state = self.state.read(cx);
let env_var_set = state.api_key_state.is_from_env_var();
let env_var_name = state.api_key_state.env_var_name();
let api_key_section = if self.should_render_editor(cx) {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new("To use Zed's agent with an OpenAI-compatible provider, you need to add an API key."))
.child(
div()
.pt(DynamicSpacing::Base04.rems(cx))
.child(self.api_key_editor.clone())
)
.child(
Label::new(
format!("You can also set the {env_var_name} environment variable and restart Zed."),
)
.size(LabelSize::Small).color(Color::Muted),
)
.into_any()
} else {
h_flex()
.mt_1()
.p_1()
.justify_between()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().background)
.child(
h_flex()
.flex_1()
.min_w_0()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(
div()
.w_full()
.overflow_x_hidden()
.text_ellipsis()
.child(Label::new(
if env_var_set {
format!("API key set in {env_var_name} environment variable")
} else {
format!("API key configured for {}", &state.settings.api_url)
}
))
),
)
.child(
h_flex()
.flex_shrink_0()
.child(
Button::new("reset-api-key", "Reset API Key")
.label_size(LabelSize::Small)
.start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
.layer(ElevationIndex::ModalSurface)
.when(env_var_set, |this| {
this.tooltip(Tooltip::text(format!("To reset your API key, unset the {env_var_name} environment variable.")))
})
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
),
)
.into_any()
};
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials…")).into_any()
} else {
v_flex().size_full().child(api_key_section).into_any()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,943 @@
use anyhow::Result;
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use fs::Fs;
use futures::{FutureExt, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::{AsyncBody, HttpClient, http};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, RateLimiter,
ReasoningEffort, env_var,
};
use opencode::{ApiProtocol, OPENCODE_API_URL, OpenCodeSubscription};
pub use settings::OpenCodeAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore, update_settings_file};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
use ui::{
Banner, ButtonLink, ConfiguredApiCard, List, ListBulletItem, Severity, Switch,
SwitchLabelPosition, ToggleState, prelude::*,
};
use ui_input::InputField;
use util::ResultExt;
use crate::provider::anthropic::{AnthropicEventMapper, into_anthropic};
use crate::provider::google::{GoogleEventMapper, into_google};
use crate::provider::open_ai::{
OpenAiEventMapper, OpenAiResponseEventMapper, into_open_ai, into_open_ai_response,
};
fn normalize_reasoning_effort(effort: &str) -> Option<ReasoningEffort> {
match effort.trim().to_ascii_lowercase().as_str() {
"none" => Some(ReasoningEffort::None),
"minimal" => Some(ReasoningEffort::Minimal),
"low" => Some(ReasoningEffort::Low),
"medium" => Some(ReasoningEffort::Medium),
"high" => Some(ReasoningEffort::High),
"max" | "xhigh" => Some(ReasoningEffort::XHigh),
_ => None,
}
}
fn reasoning_effort_display(effort: ReasoningEffort) -> (&'static str, &'static str) {
match effort {
ReasoningEffort::None => ("None", "none"),
ReasoningEffort::Minimal => ("Minimal", "minimal"),
ReasoningEffort::Low => ("Low", "low"),
ReasoningEffort::Medium => ("Medium", "medium"),
ReasoningEffort::High => ("High", "high"),
ReasoningEffort::XHigh => ("Max", "max"),
}
}
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("opencode");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenCode");
const API_KEY_ENV_VAR_NAME: &str = "OPENCODE_API_KEY";
static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
#[derive(Default, Clone, Debug, PartialEq)]
pub struct OpenCodeSettings {
pub api_url: String,
pub available_models: Vec<AvailableModel>,
pub show_zen_models: bool,
pub show_go_models: bool,
pub show_free_models: bool,
}
pub struct OpenCodeLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
state: Entity<State>,
}
pub struct State {
api_key_state: ApiKeyState,
credentials_provider: Arc<dyn CredentialsProvider>,
}
impl State {
fn is_authenticated(&self) -> bool {
self.api_key_state.has_key()
}
fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = OpenCodeLanguageModelProvider::api_url(cx);
self.api_key_state.store(
api_url,
api_key,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = OpenCodeLanguageModelProvider::api_url(cx);
self.api_key_state.load_if_needed(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
}
impl OpenCodeLanguageModelProvider {
pub fn new(
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let credentials_provider = this.credentials_provider.clone();
let api_url = Self::api_url(cx);
this.api_key_state.handle_url_change(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
cx.notify();
})
.detach();
State {
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
credentials_provider,
}
});
Self { http_client, state }
}
fn create_language_model(
&self,
model: opencode::Model,
subscription: OpenCodeSubscription,
) -> Arc<dyn LanguageModel> {
let id_str = format!("{}/{}", subscription.id_prefix(), model.id());
Arc::new(OpenCodeLanguageModel {
id: LanguageModelId::from(id_str),
model,
subscription,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
})
}
pub fn settings(cx: &App) -> &OpenCodeSettings {
&crate::AllLanguageModelSettings::get_global(cx).opencode
}
fn subscription_enabled(subscription: OpenCodeSubscription, cx: &App) -> bool {
let settings = Self::settings(cx);
match subscription {
OpenCodeSubscription::Zen => settings.show_zen_models,
OpenCodeSubscription::Go => settings.show_go_models,
OpenCodeSubscription::Free => settings.show_free_models,
}
}
fn api_url(cx: &App) -> SharedString {
let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
OPENCODE_API_URL.into()
} else {
SharedString::new(api_url.as_str())
}
}
}
impl LanguageModelProviderState for OpenCodeLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for OpenCodeLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenCode)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
if Self::subscription_enabled(OpenCodeSubscription::Go, cx) {
// If both Go and Zen are enabled, prefer Go since it's not pay-as-you-go
Some(
self.create_language_model(opencode::Model::default_go(), OpenCodeSubscription::Go),
)
} else if Self::subscription_enabled(OpenCodeSubscription::Zen, cx) {
Some(self.create_language_model(opencode::Model::default(), OpenCodeSubscription::Zen))
} else if Self::subscription_enabled(OpenCodeSubscription::Free, cx) {
Some(
self.create_language_model(
opencode::Model::default_free(),
OpenCodeSubscription::Free,
),
)
} else {
None
}
}
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
if Self::subscription_enabled(OpenCodeSubscription::Go, cx) {
// If both Go and Zen are enabled, prefer Go since it's not pay-as-you-go
Some(self.create_language_model(
opencode::Model::default_go_fast(),
OpenCodeSubscription::Go,
))
} else if Self::subscription_enabled(OpenCodeSubscription::Zen, cx) {
Some(
self.create_language_model(
opencode::Model::default_fast(),
OpenCodeSubscription::Zen,
),
)
} else if Self::subscription_enabled(OpenCodeSubscription::Free, cx) {
Some(self.create_language_model(
opencode::Model::default_free_fast(),
OpenCodeSubscription::Free,
))
} else {
None
}
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models: BTreeMap<String, (opencode::Model, OpenCodeSubscription)> =
BTreeMap::default();
let settings = Self::settings(cx);
for model in opencode::Model::iter() {
if matches!(model, opencode::Model::Custom { .. }) {
continue;
}
for &subscription in model.available_subscriptions() {
if Self::subscription_enabled(subscription, cx) {
let key = format!("{}/{}", subscription.id_prefix(), model.id());
models.insert(key, (model.clone(), subscription));
}
}
}
for model in &settings.available_models {
let protocol = match model.protocol.as_str() {
"anthropic" => ApiProtocol::Anthropic,
"openai_responses" => ApiProtocol::OpenAiResponses,
"openai_chat" => ApiProtocol::OpenAiChat,
"google" => ApiProtocol::Google,
_ => ApiProtocol::OpenAiChat, // default fallback
};
let subscription = match model.subscription {
Some(settings::OpenCodeModelSubscription::Go) => OpenCodeSubscription::Go,
Some(settings::OpenCodeModelSubscription::Free) => OpenCodeSubscription::Free,
Some(settings::OpenCodeModelSubscription::Zen) | None => OpenCodeSubscription::Zen,
};
if !Self::subscription_enabled(subscription, cx) {
continue;
}
let custom_model = opencode::Model::Custom {
name: model.name.clone(),
display_name: model.display_name.clone(),
max_tokens: model.max_tokens,
max_output_tokens: model.max_output_tokens,
protocol,
reasoning_effort_levels: model.reasoning_effort_levels.clone(),
custom_model_api_url: model.custom_model_api_url.clone(),
interleaved_reasoning: model.interleaved_reasoning,
};
let key = format!("{}/{}", subscription.id_prefix(), model.name);
models.insert(key, (custom_model, subscription));
}
models
.into_values()
.map(|(model, subscription)| self.create_language_model(model, subscription))
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(None, cx))
}
}
pub struct OpenCodeLanguageModel {
id: LanguageModelId,
model: opencode::Model,
subscription: OpenCodeSubscription,
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
struct InjectHeaderClient {
inner: Arc<dyn HttpClient>,
name: http::HeaderName,
value: http::HeaderValue,
}
impl HttpClient for InjectHeaderClient {
fn user_agent(&self) -> Option<&http::HeaderValue> {
self.inner.user_agent()
}
fn proxy(&self) -> Option<&http_client::Url> {
self.inner.proxy()
}
fn send(
&self,
mut req: http::Request<AsyncBody>,
) -> futures::future::BoxFuture<'static, anyhow::Result<http::Response<AsyncBody>>> {
req.headers_mut()
.insert(self.name.clone(), self.value.clone());
self.inner.send(req)
}
}
impl OpenCodeLanguageModel {
fn base_api_url(&self, cx: &AsyncApp) -> SharedString {
// Custom models can override the API URL
if let opencode::Model::Custom {
custom_model_api_url: Some(url),
..
} = &self.model
{
if !url.is_empty() {
return url.clone().into();
}
}
// Combine base URL with subscription path suffix
let base = self
.state
.read_with(cx, |_, cx| OpenCodeLanguageModelProvider::api_url(cx));
let suffix = self.subscription.api_path_suffix();
let base_str = base.as_ref().trim_end_matches('/');
format!("{}{}", base_str, suffix).into()
}
fn api_key(&self, cx: &AsyncApp) -> Option<Arc<str>> {
self.state.read_with(cx, |state, cx| {
let api_url = OpenCodeLanguageModelProvider::api_url(cx);
state.api_key_state.key(&api_url)
})
}
fn stream_anthropic(
&self,
request: anthropic::Request,
http_client: Arc<dyn HttpClient>,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<anthropic::Event, anthropic::AnthropicError>,
>,
LanguageModelCompletionError,
>,
> {
// Anthropic crate appends /v1/messages to api_url
let api_url = self.base_api_url(cx);
let api_key = self.api_key(cx);
let future = self.request_limiter.stream(async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey {
provider: PROVIDER_NAME,
});
};
let request = anthropic::stream_completion(
http_client.as_ref(),
&api_url,
&api_key,
request,
None,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
fn stream_openai_chat(
&self,
request: open_ai::Request,
http_client: Arc<dyn HttpClient>,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<futures::stream::BoxStream<'static, Result<open_ai::ResponseStreamEvent>>>,
> {
// OpenAI crate appends /chat/completions to api_url, so we pass base + "/v1"
let base_url = self.base_api_url(cx);
let api_url: SharedString = format!("{base_url}/v1").into();
let api_key = self.api_key(cx);
let provider_name = PROVIDER_NAME.0.to_string();
let future = self.request_limiter.stream(async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey {
provider: PROVIDER_NAME,
});
};
let request = open_ai::stream_completion(
http_client.as_ref(),
&provider_name,
&api_url,
&api_key,
request,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
fn stream_openai_response(
&self,
request: open_ai::responses::Request,
http_client: Arc<dyn HttpClient>,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<futures::stream::BoxStream<'static, Result<open_ai::responses::StreamEvent>>>,
> {
// Responses crate appends /responses to api_url, so we pass base + "/v1"
let base_url = self.base_api_url(cx);
let api_url: SharedString = format!("{base_url}/v1").into();
let api_key = self.api_key(cx);
let provider_name = PROVIDER_NAME.0.to_string();
let future = self.request_limiter.stream(async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey {
provider: PROVIDER_NAME,
});
};
let request = open_ai::responses::stream_response(
http_client.as_ref(),
&provider_name,
&api_url,
&api_key,
request,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
fn stream_google(
&self,
request: google_ai::GenerateContentRequest,
http_client: Arc<dyn HttpClient>,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<futures::stream::BoxStream<'static, Result<google_ai::GenerateContentResponse>>>,
> {
let api_url = self.base_api_url(cx);
let api_key = self.api_key(cx);
let future = self.request_limiter.stream(async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey {
provider: PROVIDER_NAME,
});
};
let request = opencode::stream_generate_content(
http_client.as_ref(),
&api_url,
&api_key,
request,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
}
impl LanguageModel for OpenCodeLanguageModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(format!(
"{}: {}",
self.subscription.display_name(),
self.model.display_name()
))
}
fn provider_id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
self.model.supports_tools()
}
fn supports_images(&self) -> bool {
self.model.supports_images()
}
fn supports_thinking(&self) -> bool {
self.model
.supported_reasoning_effort_levels()
.is_some_and(|levels| levels.iter().any(|effort| *effort != ReasoningEffort::None))
}
fn supported_effort_levels(&self) -> Vec<LanguageModelEffortLevel> {
self.model
.supported_reasoning_effort_levels()
.map(|levels| {
let levels = levels
.into_iter()
.filter(|effort| *effort != ReasoningEffort::None)
.collect::<Vec<_>>();
if levels.is_empty() {
return Vec::new();
}
let default_index = levels.len() - 1;
levels
.into_iter()
.enumerate()
.map(|(i, effort)| {
let (name, value) = reasoning_effort_display(effort);
LanguageModelEffortLevel {
name: name.into(),
value: value.into(),
is_default: i == default_index,
}
})
.collect()
})
.unwrap_or_default()
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
match choice {
LanguageModelToolChoice::Auto | LanguageModelToolChoice::Any => true,
LanguageModelToolChoice::None => {
// Google models don't support None tool choice
self.model.protocol(self.subscription) != ApiProtocol::Google
}
}
}
fn telemetry_id(&self) -> String {
format!(
"opencode/{}/{}",
self.subscription.id_prefix(),
self.model.id()
)
}
fn max_token_count(&self) -> u64 {
self.model.max_token_count()
}
fn max_output_tokens(&self) -> Option<u64> {
self.model.max_output_tokens()
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let http_client = if let Some(ref thread_id) = request.thread_id
&& let Ok(value) = http::HeaderValue::from_str(thread_id)
{
Arc::new(InjectHeaderClient {
inner: self.http_client.clone(),
name: http::HeaderName::from_static("x-opencode-session"),
value,
})
} else {
self.http_client.clone()
};
match self.model.protocol(self.subscription) {
ApiProtocol::Anthropic => {
let mode = if self.supports_thinking() && request.thinking_allowed {
anthropic::AnthropicModelMode::AdaptiveThinking
} else {
anthropic::AnthropicModelMode::Default
};
let anthropic_request = into_anthropic(
request,
self.model.id().to_string(),
1.0,
self.model.max_output_tokens().unwrap_or(8192),
mode,
anthropic::completion::AnthropicPromptCacheMode::Automatic,
);
let stream = self.stream_anthropic(anthropic_request, http_client, cx);
async move {
let mapper = AnthropicEventMapper::new();
Ok(mapper.map_stream(stream.await?).boxed())
}
.boxed()
}
ApiProtocol::OpenAiChat => {
let reasoning_effort = if request.thinking_allowed {
request
.thinking_effort
.as_deref()
.and_then(normalize_reasoning_effort)
} else {
None
};
let openai_request = into_open_ai(
request,
self.model.id(),
false,
false,
self.model.max_output_tokens(),
reasoning_effort,
self.model.interleaved_reasoning(),
);
let stream = self.stream_openai_chat(openai_request, http_client, cx);
async move {
let mapper = OpenAiEventMapper::new();
Ok(mapper.map_stream(stream.await?).boxed())
}
.boxed()
}
ApiProtocol::OpenAiResponses => {
let supports_none_reasoning_effort = self
.model
.supported_reasoning_effort_levels()
.is_some_and(|levels| levels.contains(&ReasoningEffort::None));
let response_request = into_open_ai_response(
request,
self.model.id(),
false,
false,
self.model.max_output_tokens(),
None,
supports_none_reasoning_effort,
);
let stream = self.stream_openai_response(response_request, http_client, cx);
async move {
let mapper = OpenAiResponseEventMapper::new();
Ok(mapper.map_stream(stream.await?).boxed())
}
.boxed()
}
ApiProtocol::Google => {
let google_request = into_google(
request,
self.model.id().to_string(),
google_ai::GoogleModelMode::Default,
);
let stream = self.stream_google(google_request, http_client, cx);
async move {
let mapper = GoogleEventMapper::new();
Ok(mapper.map_stream(stream.await?.boxed()).boxed())
}
.boxed()
}
}
}
}
struct ConfigurationView {
api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor = cx.new(|cx| {
InputField::new(window, cx, "sk-00000000000000000000000000000000").label("API key")
});
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let load_credentials_task = Some(cx.spawn_in(window, {
let state = state.clone();
async move |this, cx| {
if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
let _ = task.await;
}
this.update(cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor,
state,
load_credentials_task,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
if api_key.is_empty() {
return;
}
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
.await
})
.detach_and_log_err(cx);
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(None, cx))
.await
})
.detach_and_log_err(cx);
}
fn set_subscription_enabled(
&mut self,
subscription: OpenCodeSubscription,
is_enabled: bool,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let fs = <dyn Fs>::global(cx);
update_settings_file(fs, cx, move |settings, _| {
let opencode_settings = settings
.language_models
.get_or_insert_default()
.opencode
.get_or_insert_default();
match subscription {
OpenCodeSubscription::Zen => opencode_settings.show_zen_models = Some(is_enabled),
OpenCodeSubscription::Go => opencode_settings.show_go_models = Some(is_enabled),
OpenCodeSubscription::Free => opencode_settings.show_free_models = Some(is_enabled),
}
});
}
fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
let configured_card_label = if env_var_set {
format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
} else {
let api_url = OpenCodeLanguageModelProvider::api_url(cx);
if api_url == OPENCODE_API_URL {
"API key configured".to_string()
} else {
format!("API key configured for {}", api_url)
}
};
let api_key_section = if self.should_render_editor(cx) {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new(
"To use OpenCode models in Zed, you need an API key:",
))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Sign in and get your key at"))
.child(ButtonLink::new(
"OpenCode Console",
"https://opencode.ai/auth",
)),
)
.child(ListBulletItem::new(
"Paste your API key below and hit enter to start using OpenCode",
)),
)
.child(self.api_key_editor.clone())
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip_label(format!(
"To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."
))
})
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.into_any_element()
};
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any()
} else {
let settings = OpenCodeLanguageModelProvider::settings(cx);
let show_zen = settings.show_zen_models;
let show_go = settings.show_go_models;
let show_free = settings.show_free_models;
let subscription_toggles = v_flex()
.gap_1()
.child(Label::new("Subscriptions:").color(Color::Muted))
.child(
Switch::new("opencode-show-zen-models", show_zen.into())
.label("Show Zen models")
.label_position(SwitchLabelPosition::End)
.on_click(cx.listener(|this, state, window, cx| {
this.set_subscription_enabled(
OpenCodeSubscription::Zen,
matches!(state, ToggleState::Selected),
window,
cx,
);
})),
)
.child(
Switch::new("opencode-show-go-models", show_go.into())
.label("Show Go models")
.label_position(SwitchLabelPosition::End)
.on_click(cx.listener(|this, state, window, cx| {
this.set_subscription_enabled(
OpenCodeSubscription::Go,
matches!(state, ToggleState::Selected),
window,
cx,
);
})),
)
.child(
Switch::new("opencode-show-free-models", show_free.into())
.label("Show Free models")
.label_position(SwitchLabelPosition::End)
.on_click(cx.listener(|this, state, window, cx| {
this.set_subscription_enabled(
OpenCodeSubscription::Free,
matches!(state, ToggleState::Selected),
window,
cx,
);
})),
);
let no_subscriptions_warning = if !show_zen && !show_go && !show_free {
Some(Banner::new().severity(Severity::Warning).child(Label::new(
"No subscriptions enabled. Enable at least one subscription to use OpenCode.",
)))
} else {
None
};
v_flex()
.size_full()
.gap_2()
.child(api_key_section)
.child(subscription_toggles)
.children(no_subscriptions_warning)
.into_any()
}
}
}
@@ -0,0 +1,711 @@
use anyhow::Result;
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use futures::{AsyncReadExt, FutureExt, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
env_var,
};
use open_ai::ResponseStreamEvent;
use serde::Deserialize;
pub use settings::OpenAiCompatibleModelCapabilities as ModelCapabilities;
pub use settings::VercelAiGatewayAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel_ai_gateway");
const PROVIDER_NAME: LanguageModelProviderName =
LanguageModelProviderName::new("Vercel AI Gateway");
const API_URL: &str = "https://ai-gateway.vercel.sh/v1";
const API_KEY_ENV_VAR_NAME: &str = "VERCEL_AI_GATEWAY_API_KEY";
static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
#[derive(Default, Clone, Debug, PartialEq)]
pub struct VercelAiGatewaySettings {
pub api_url: String,
pub available_models: Vec<AvailableModel>,
}
pub struct VercelAiGatewayLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
state: Entity<State>,
}
pub struct State {
api_key_state: ApiKeyState,
credentials_provider: Arc<dyn CredentialsProvider>,
http_client: Arc<dyn HttpClient>,
available_models: Vec<AvailableModel>,
fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>,
}
impl State {
fn is_authenticated(&self) -> bool {
self.api_key_state.has_key()
}
fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = VercelAiGatewayLanguageModelProvider::api_url(cx);
self.api_key_state.store(
api_url,
api_key,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = VercelAiGatewayLanguageModelProvider::api_url(cx);
let task = self.api_key_state.load_if_needed(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
cx.spawn(async move |this, cx| {
let result = task.await;
this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
.ok();
result
})
}
fn fetch_models(
&mut self,
cx: &mut Context<Self>,
) -> Task<Result<(), LanguageModelCompletionError>> {
let http_client = self.http_client.clone();
let api_url = VercelAiGatewayLanguageModelProvider::api_url(cx);
let api_key = self.api_key_state.key(&api_url);
cx.spawn(async move |this, cx| {
let models = list_models(http_client.as_ref(), &api_url, api_key.as_deref()).await?;
this.update(cx, |this, cx| {
this.available_models = models;
cx.notify();
})
.map_err(|e| LanguageModelCompletionError::Other(e))?;
Ok(())
})
}
fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) {
if self.is_authenticated() {
let task = self.fetch_models(cx);
self.fetch_models_task.replace(task);
} else {
self.available_models = Vec::new();
}
}
}
impl VercelAiGatewayLanguageModelProvider {
pub fn new(
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>({
let mut last_settings = VercelAiGatewayLanguageModelProvider::settings(cx).clone();
move |this: &mut State, cx| {
let current_settings = VercelAiGatewayLanguageModelProvider::settings(cx);
if current_settings != &last_settings {
last_settings = current_settings.clone();
this.authenticate(cx).detach();
cx.notify();
}
}
})
.detach();
State {
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
credentials_provider,
http_client: http_client.clone(),
available_models: Vec::new(),
fetch_models_task: None,
}
});
Self { http_client, state }
}
fn settings(cx: &App) -> &VercelAiGatewaySettings {
&crate::AllLanguageModelSettings::get_global(cx).vercel_ai_gateway
}
fn api_url(cx: &App) -> SharedString {
let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
API_URL.into()
} else {
SharedString::new(api_url.as_str())
}
}
fn default_available_model() -> AvailableModel {
AvailableModel {
name: "openai/gpt-5.3-codex".to_string(),
display_name: Some("GPT 5.3 Codex".to_string()),
max_tokens: 400_000,
max_output_tokens: Some(128_000),
max_completion_tokens: None,
capabilities: ModelCapabilities::default(),
}
}
fn create_language_model(&self, model: AvailableModel) -> Arc<dyn LanguageModel> {
Arc::new(VercelAiGatewayLanguageModel {
id: LanguageModelId::from(model.name.clone()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
})
}
}
impl LanguageModelProviderState for VercelAiGatewayLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for VercelAiGatewayLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiVercel)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(Self::default_available_model()))
}
fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
None
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
let default_model = Self::default_available_model();
models.insert(default_model.name.clone(), default_model);
for model in self.state.read(cx).available_models.clone() {
models.insert(model.name.clone(), model);
}
for model in &Self::settings(cx).available_models {
models.insert(model.name.clone(), model.clone());
}
models
.into_values()
.map(|model| self.create_language_model(model))
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(None, cx))
}
}
pub struct VercelAiGatewayLanguageModel {
id: LanguageModelId,
model: AvailableModel,
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
impl VercelAiGatewayLanguageModel {
fn stream_open_ai(
&self,
request: open_ai::Request,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<'static, Result<ResponseStreamEvent>>,
LanguageModelCompletionError,
>,
> {
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
let api_url = VercelAiGatewayLanguageModelProvider::api_url(cx);
(state.api_key_state.key(&api_url), api_url)
});
let future = self.request_limiter.stream(async move {
let provider = PROVIDER_NAME;
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey { provider });
};
let request = open_ai::stream_completion(
http_client.as_ref(),
provider.0.as_str(),
&api_url,
&api_key,
request,
);
let response = request.await.map_err(map_open_ai_error)?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
}
fn map_open_ai_error(error: open_ai::RequestError) -> LanguageModelCompletionError {
match error {
open_ai::RequestError::HttpResponseError {
status_code,
body,
headers,
..
} => {
let retry_after = headers
.get(http::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok()?.parse::<u64>().ok())
.map(std::time::Duration::from_secs);
LanguageModelCompletionError::from_http_status(
PROVIDER_NAME,
status_code,
extract_error_message(&body),
retry_after,
)
}
open_ai::RequestError::Other(error) => LanguageModelCompletionError::Other(error),
}
}
fn extract_error_message(body: &str) -> String {
let json = match serde_json::from_str::<serde_json::Value>(body) {
Ok(json) => json,
Err(_) => return body.to_string(),
};
let message = json
.get("error")
.and_then(|value| {
value
.get("message")
.and_then(serde_json::Value::as_str)
.or_else(|| value.as_str())
})
.or_else(|| json.get("message").and_then(serde_json::Value::as_str))
.map(ToString::to_string)
.unwrap_or_else(|| body.to_string());
clean_error_message(&message)
}
fn clean_error_message(message: &str) -> String {
let lower = message.to_lowercase();
if lower.contains("vercel_oidc_token") && lower.contains("oidc token") {
return "Authentication failed for Vercel AI Gateway. Use a Vercel AI Gateway key (vck_...).\nCreate or manage keys in Vercel AI Gateway console.\nIf this persists, regenerate the key and update it in Vercel AI Gateway provider settings in Zed.".to_string();
}
if lower.contains("invalid api key") || lower.contains("invalid_api_key") {
return "Authentication failed for Vercel AI Gateway. Check that your Vercel AI Gateway key starts with vck_ and is active.".to_string();
}
message.to_string()
}
fn has_tag(tags: &[String], expected: &str) -> bool {
tags.iter()
.any(|tag| tag.trim().eq_ignore_ascii_case(expected))
}
impl LanguageModel for VercelAiGatewayLanguageModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(
self.model
.display_name
.clone()
.unwrap_or_else(|| self.model.name.clone()),
)
}
fn provider_id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
self.model.capabilities.tools
}
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
LanguageModelToolSchemaFormat::JsonSchemaSubset
}
fn supports_images(&self) -> bool {
self.model.capabilities.images
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
match choice {
LanguageModelToolChoice::Auto => self.model.capabilities.tools,
LanguageModelToolChoice::Any => self.model.capabilities.tools,
LanguageModelToolChoice::None => true,
}
}
fn supports_streaming_tools(&self) -> bool {
true
}
fn supports_split_token_display(&self) -> bool {
true
}
fn telemetry_id(&self) -> String {
format!("vercel_ai_gateway/{}", self.model.name)
}
fn max_token_count(&self) -> u64 {
self.model.max_tokens
}
fn max_output_tokens(&self) -> Option<u64> {
self.model.max_output_tokens
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = crate::provider::open_ai::into_open_ai(
request,
&self.model.name,
self.model.capabilities.parallel_tool_calls,
self.model.capabilities.prompt_cache_key,
self.max_output_tokens(),
None,
false,
);
let completions = self.stream_open_ai(request, cx);
async move {
let mapper = crate::provider::open_ai::OpenAiEventMapper::new();
Ok(mapper.map_stream(completions.await?).boxed())
}
.boxed()
}
}
#[derive(Deserialize)]
struct ModelsResponse {
data: Vec<ApiModel>,
}
#[derive(Deserialize)]
struct ApiModel {
id: String,
name: Option<String>,
context_window: Option<u64>,
max_tokens: Option<u64>,
#[serde(default)]
r#type: Option<String>,
#[serde(default)]
supported_parameters: Vec<String>,
#[serde(default)]
tags: Vec<String>,
architecture: Option<ApiModelArchitecture>,
}
#[derive(Deserialize)]
struct ApiModelArchitecture {
#[serde(default)]
input_modalities: Vec<String>,
}
async fn list_models(
client: &dyn HttpClient,
api_url: &str,
api_key: Option<&str>,
) -> Result<Vec<AvailableModel>, LanguageModelCompletionError> {
let uri = format!("{api_url}/models?include_mappings=true");
let mut request_builder = HttpRequest::builder()
.method(Method::GET)
.uri(uri)
.header("Accept", "application/json");
if let Some(api_key) = api_key {
request_builder = request_builder.header("Authorization", format!("Bearer {}", api_key));
}
let request = request_builder
.body(AsyncBody::default())
.map_err(|error| LanguageModelCompletionError::BuildRequestBody {
provider: PROVIDER_NAME,
error,
})?;
let mut response =
client
.send(request)
.await
.map_err(|error| LanguageModelCompletionError::HttpSend {
provider: PROVIDER_NAME,
error,
})?;
let mut body = String::new();
response
.body_mut()
.read_to_string(&mut body)
.await
.map_err(|error| LanguageModelCompletionError::ApiReadResponseError {
provider: PROVIDER_NAME,
error,
})?;
if !response.status().is_success() {
return Err(LanguageModelCompletionError::from_http_status(
PROVIDER_NAME,
response.status(),
extract_error_message(&body),
None,
));
}
let response: ModelsResponse = serde_json::from_str(&body).map_err(|error| {
LanguageModelCompletionError::DeserializeResponse {
provider: PROVIDER_NAME,
error,
}
})?;
let mut models = Vec::new();
for model in response.data {
if let Some(model_type) = model.r#type.as_deref()
&& model_type != "language"
{
continue;
}
let supports_tools = model
.supported_parameters
.iter()
.any(|parameter| parameter == "tools")
|| has_tag(&model.tags, "tool-use")
|| has_tag(&model.tags, "tools");
let supports_images = model.architecture.is_some_and(|architecture| {
architecture
.input_modalities
.iter()
.any(|modality| modality == "image")
}) || has_tag(&model.tags, "vision")
|| has_tag(&model.tags, "image-input");
let parallel_tool_calls = model
.supported_parameters
.iter()
.any(|parameter| parameter == "parallel_tool_calls");
let prompt_cache_key = model
.supported_parameters
.iter()
.any(|parameter| parameter == "prompt_cache_key" || parameter == "cache_control");
models.push(AvailableModel {
name: model.id.clone(),
display_name: model.name.or(Some(model.id)),
max_tokens: model.context_window.or(model.max_tokens).unwrap_or(128_000),
max_output_tokens: model.max_tokens,
max_completion_tokens: None,
capabilities: ModelCapabilities {
tools: supports_tools,
images: supports_images,
parallel_tool_calls,
prompt_cache_key,
chat_completions: true,
interleaved_reasoning: false,
},
});
}
Ok(models)
}
struct ConfigurationView {
api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor =
cx.new(|cx| InputField::new(window, cx, "vck_000000000000000000000000000"));
cx.observe(&state, |_, _, cx| cx.notify()).detach();
let load_credentials_task = Some(cx.spawn_in(window, {
let state = state.clone();
async move |this, cx| {
if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
let _ = task.await;
}
this.update(cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor,
state,
load_credentials_task,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
if api_key.is_empty() {
return;
}
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
.await
})
.detach_and_log_err(cx);
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(None, cx))
.await
})
.detach_and_log_err(cx);
}
fn should_render_editor(&self, cx: &Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
let configured_card_label = if env_var_set {
format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
} else {
let api_url = VercelAiGatewayLanguageModelProvider::api_url(cx);
if api_url == API_URL {
"API key configured".to_string()
} else {
format!("API key configured for {}", api_url)
}
};
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new(
"To use Zed's agent with Vercel AI Gateway, you need to add an API key. Follow these steps:",
))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Create an API key in"))
.child(ButtonLink::new(
"Vercel AI Gateway's console",
"https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys&title=Go+to+AI+Gateway",
)),
)
.child(ListBulletItem::new(
"Paste your API key below and hit enter to start using the assistant",
)),
)
.child(self.api_key_editor.clone())
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed.",
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
})
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.into_any_element()
}
}
}
+490
View File
@@ -0,0 +1,490 @@
use anyhow::Result;
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use futures::{FutureExt, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
env_var,
};
use open_ai::ResponseStreamEvent;
pub use settings::XaiAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
use x_ai::XAI_API_URL;
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("x_ai");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("xAI");
const API_KEY_ENV_VAR_NAME: &str = "XAI_API_KEY";
static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
#[derive(Default, Clone, Debug, PartialEq)]
pub struct XAiSettings {
pub api_url: String,
pub available_models: Vec<AvailableModel>,
}
pub struct XAiLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
state: Entity<State>,
}
pub struct State {
api_key_state: ApiKeyState,
credentials_provider: Arc<dyn CredentialsProvider>,
}
impl State {
fn is_authenticated(&self) -> bool {
self.api_key_state.has_key()
}
fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = XAiLanguageModelProvider::api_url(cx);
self.api_key_state.store(
api_url,
api_key,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = self.credentials_provider.clone();
let api_url = XAiLanguageModelProvider::api_url(cx);
self.api_key_state.load_if_needed(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
)
}
}
impl XAiLanguageModelProvider {
pub fn new(
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let credentials_provider = this.credentials_provider.clone();
let api_url = Self::api_url(cx);
this.api_key_state.handle_url_change(
api_url,
|this| &mut this.api_key_state,
credentials_provider,
cx,
);
cx.notify();
})
.detach();
State {
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
credentials_provider,
}
});
Self { http_client, state }
}
fn create_language_model(&self, model: x_ai::Model) -> Arc<dyn LanguageModel> {
Arc::new(XAiLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
})
}
fn settings(cx: &App) -> &XAiSettings {
&crate::AllLanguageModelSettings::get_global(cx).x_ai
}
fn api_url(cx: &App) -> SharedString {
let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
XAI_API_URL.into()
} else {
SharedString::new(api_url.as_str())
}
}
}
impl LanguageModelProviderState for XAiLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for XAiLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiXAi)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(x_ai::Model::default()))
}
fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(x_ai::Model::default_fast()))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
for model in x_ai::Model::iter() {
if !matches!(model, x_ai::Model::Custom { .. }) {
models.insert(model.id().to_string(), model);
}
}
for model in &Self::settings(cx).available_models {
models.insert(
model.name.clone(),
x_ai::Model::Custom {
name: model.name.clone(),
display_name: model.display_name.clone(),
max_tokens: model.max_tokens,
max_output_tokens: model.max_output_tokens,
max_completion_tokens: model.max_completion_tokens,
supports_images: model.supports_images,
supports_tools: model.supports_tools,
parallel_tool_calls: model.parallel_tool_calls,
},
);
}
models
.into_values()
.map(|model| self.create_language_model(model))
.collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(None, cx))
}
}
pub struct XAiLanguageModel {
id: LanguageModelId,
model: x_ai::Model,
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
impl XAiLanguageModel {
fn stream_completion(
&self,
request: open_ai::Request,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<'static, Result<ResponseStreamEvent>>,
LanguageModelCompletionError,
>,
> {
let http_client = self.http_client.clone();
let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
let api_url = XAiLanguageModelProvider::api_url(cx);
(state.api_key_state.key(&api_url), api_url)
});
let future = self.request_limiter.stream(async move {
let provider = PROVIDER_NAME;
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey { provider });
};
let request = open_ai::stream_completion(
http_client.as_ref(),
provider.0.as_str(),
&api_url,
&api_key,
request,
);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
}
impl LanguageModel for XAiLanguageModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(self.model.display_name().to_string())
}
fn provider_id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
self.model.supports_tool()
}
fn supports_images(&self) -> bool {
self.model.supports_images()
}
fn supports_streaming_tools(&self) -> bool {
true
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
match choice {
LanguageModelToolChoice::Auto
| LanguageModelToolChoice::Any
| LanguageModelToolChoice::None => true,
}
}
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
if self.model.requires_json_schema_subset() {
LanguageModelToolSchemaFormat::JsonSchemaSubset
} else {
LanguageModelToolSchemaFormat::JsonSchema
}
}
fn telemetry_id(&self) -> String {
format!("x_ai/{}", self.model.id())
}
fn max_token_count(&self) -> u64 {
self.model.max_token_count()
}
fn max_output_tokens(&self) -> Option<u64> {
self.model.max_output_tokens()
}
fn supports_split_token_display(&self) -> bool {
true
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = crate::provider::open_ai::into_open_ai(
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
None,
false,
);
let completions = self.stream_completion(request, cx);
async move {
let mapper = crate::provider::open_ai::OpenAiEventMapper::new();
Ok(mapper.map_stream(completions.await?).boxed())
}
.boxed()
}
}
struct ConfigurationView {
api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor = cx.new(|cx| {
InputField::new(
window,
cx,
"xai-0000000000000000000000000000000000000000000000000",
)
.label("API key")
});
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let load_credentials_task = Some(cx.spawn_in(window, {
let state = state.clone();
async move |this, cx| {
if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
// We don't log an error, because "not signed in" is also an error.
let _ = task.await;
}
this.update(cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor,
state,
load_credentials_task,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
if api_key.is_empty() {
return;
}
// url changes can cause the editor to be displayed again
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
.await
})
.detach_and_log_err(cx);
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor
.update(cx, |input, cx| input.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
.update(cx, |state, cx| state.set_api_key(None, cx))
.await
})
.detach_and_log_err(cx);
}
fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
let configured_card_label = if env_var_set {
format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
} else {
let api_url = XAiLanguageModelProvider::api_url(cx);
if api_url == XAI_API_URL {
"API key configured".to_string()
} else {
format!("API key configured for {}", api_url)
}
};
let api_key_section = if self.should_render_editor(cx) {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new("To use Zed's agent with xAI, you need to add an API key. Follow these steps:"))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Create one by visiting"))
.child(ButtonLink::new("xAI console", "https://console.x.ai/team/default/api-keys"))
)
.child(
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
),
)
.child(self.api_key_editor.clone())
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new("Note that xAI is a custom OpenAI-compatible provider.")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
})
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.into_any_element()
};
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials…")).into_any()
} else {
v_flex().size_full().child(api_key_section).into_any()
}
}
}
+129
View File
@@ -0,0 +1,129 @@
use std::sync::Arc;
use collections::HashMap;
use settings::RegisterSetting;
use crate::provider::{
anthropic::AnthropicSettings, bedrock::AmazonBedrockSettings, cloud::ZedDotDevSettings,
deepseek::DeepSeekSettings, google::GoogleSettings, lmstudio::LmStudioSettings,
mistral::MistralSettings, ollama::OllamaSettings, open_ai::OpenAiSettings,
open_ai_compatible::OpenAiCompatibleSettings, open_router::OpenRouterSettings,
opencode::OpenCodeSettings, vercel_ai_gateway::VercelAiGatewaySettings, x_ai::XAiSettings,
};
#[derive(Debug, RegisterSetting)]
pub struct AllLanguageModelSettings {
pub anthropic: AnthropicSettings,
pub bedrock: AmazonBedrockSettings,
pub deepseek: DeepSeekSettings,
pub google: GoogleSettings,
pub lmstudio: LmStudioSettings,
pub mistral: MistralSettings,
pub ollama: OllamaSettings,
pub opencode: OpenCodeSettings,
pub open_router: OpenRouterSettings,
pub openai: OpenAiSettings,
pub openai_compatible: HashMap<Arc<str>, OpenAiCompatibleSettings>,
pub vercel_ai_gateway: VercelAiGatewaySettings,
pub x_ai: XAiSettings,
pub zed_dot_dev: ZedDotDevSettings,
}
impl settings::Settings for AllLanguageModelSettings {
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
fn from_settings(content: &settings::SettingsContent) -> Self {
let language_models = content.language_models.clone().unwrap();
let anthropic = language_models.anthropic.unwrap();
let bedrock = language_models.bedrock.unwrap();
let deepseek = language_models.deepseek.unwrap();
let google = language_models.google.unwrap();
let lmstudio = language_models.lmstudio.unwrap();
let mistral = language_models.mistral.unwrap();
let ollama = language_models.ollama.unwrap();
let opencode = language_models.opencode.unwrap();
let open_router = language_models.open_router.unwrap();
let openai = language_models.openai.unwrap();
let openai_compatible = language_models.openai_compatible.unwrap();
let vercel_ai_gateway = language_models.vercel_ai_gateway.unwrap();
let x_ai = language_models.x_ai.unwrap();
let zed_dot_dev = language_models.zed_dot_dev.unwrap();
Self {
anthropic: AnthropicSettings {
api_url: anthropic.api_url.unwrap(),
available_models: anthropic.available_models.unwrap_or_default(),
},
bedrock: AmazonBedrockSettings {
available_models: bedrock.available_models.unwrap_or_default(),
region: bedrock.region,
endpoint: bedrock.endpoint_url, // todo(should be api_url)
profile_name: bedrock.profile,
role_arn: None, // todo(was never a setting for this...)
authentication_method: bedrock.authentication_method.map(Into::into),
allow_global: bedrock.allow_global,
guardrail_identifier: bedrock.guardrail_identifier,
guardrail_version: bedrock.guardrail_version,
},
deepseek: DeepSeekSettings {
api_url: deepseek.api_url.unwrap(),
available_models: deepseek.available_models.unwrap_or_default(),
},
google: GoogleSettings {
api_url: google.api_url.unwrap(),
available_models: google.available_models.unwrap_or_default(),
},
lmstudio: LmStudioSettings {
api_url: lmstudio.api_url.unwrap(),
available_models: lmstudio.available_models.unwrap_or_default(),
},
mistral: MistralSettings {
api_url: mistral.api_url.unwrap(),
available_models: mistral.available_models.unwrap_or_default(),
},
ollama: OllamaSettings {
api_url: ollama.api_url.unwrap(),
auto_discover: ollama.auto_discover.unwrap_or(true),
available_models: ollama.available_models.unwrap_or_default(),
context_window: ollama.context_window,
},
opencode: OpenCodeSettings {
api_url: opencode.api_url.unwrap(),
available_models: opencode.available_models.unwrap_or_default(),
show_zen_models: opencode.show_zen_models.unwrap_or(true),
show_go_models: opencode.show_go_models.unwrap_or(true),
show_free_models: opencode.show_free_models.unwrap_or(true),
},
open_router: OpenRouterSettings {
api_url: open_router.api_url.unwrap(),
available_models: open_router.available_models.unwrap_or_default(),
},
openai: OpenAiSettings {
api_url: openai.api_url.unwrap(),
available_models: openai.available_models.unwrap_or_default(),
},
openai_compatible: openai_compatible
.into_iter()
.map(|(key, value)| {
(
key,
OpenAiCompatibleSettings {
api_url: value.api_url,
available_models: value.available_models,
},
)
})
.collect(),
vercel_ai_gateway: VercelAiGatewaySettings {
api_url: vercel_ai_gateway.api_url.unwrap(),
available_models: vercel_ai_gateway.available_models.unwrap_or_default(),
},
x_ai: XAiSettings {
api_url: x_ai.api_url.unwrap(),
available_models: x_ai.available_models.unwrap_or_default(),
},
zed_dot_dev: ZedDotDevSettings {
available_models: zed_dot_dev.available_models.unwrap_or_default(),
},
}
}
}