From c7e77674a1923825f2e28083b7008326c2928dc7 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 2 Dec 2025 13:50:00 -0500 Subject: [PATCH 01/45] Initial Claude Opus 4.5 implementation of Provider Extensions --- Cargo.lock | 3 + crates/extension/src/extension_host_proxy.rs | 54 + crates/extension/src/extension_manifest.rs | 55 + crates/extension_api/src/extension_api.rs | 146 +- .../extension_api/wit/since_v0.7.0/common.wit | 12 + .../wit/since_v0.7.0/context-server.wit | 11 + crates/extension_api/wit/since_v0.7.0/dap.wit | 123 ++ .../wit/since_v0.7.0/extension.wit | 248 +++ .../extension_api/wit/since_v0.7.0/github.wit | 35 + .../wit/since_v0.7.0/http-client.wit | 67 + .../wit/since_v0.7.0/llm-provider.wit | 255 +++ crates/extension_api/wit/since_v0.7.0/lsp.wit | 90 ++ .../extension_api/wit/since_v0.7.0/nodejs.wit | 13 + .../wit/since_v0.7.0/platform.wit | 24 + .../wit/since_v0.7.0/process.wit | 29 + .../wit/since_v0.7.0/settings.rs | 40 + .../wit/since_v0.7.0/slash-command.wit | 41 + crates/extension_host/Cargo.toml | 2 + .../extension_compilation_benchmark.rs | 1 + .../extension_host/src/capability_granter.rs | 1 + crates/extension_host/src/extension_host.rs | 119 +- .../src/extension_store_test.rs | 3 + crates/extension_host/src/wasm_host.rs | 1 + .../src/wasm_host/llm_provider.rs | 624 ++++++++ crates/extension_host/src/wasm_host/wit.rs | 307 +++- .../src/wasm_host/wit/since_v0_6_0.rs | 1016 +----------- .../src/wasm_host/wit/since_v0_7_0.rs | 1196 ++++++++++++++ crates/language_models/Cargo.toml | 1 + crates/language_models/src/extension.rs | 33 + crates/language_models/src/language_models.rs | 8 + ...odel_provider_extensions_implementation.md | 689 +++++++++ ...language_model_provider_extensions_plan.md | 1368 +++++++++++++++++ extensions/test-extension/extension.toml | 2 +- 33 files changed, 5650 insertions(+), 967 deletions(-) create mode 100644 crates/extension_api/wit/since_v0.7.0/common.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/context-server.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/dap.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/extension.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/github.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/http-client.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/llm-provider.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/lsp.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/nodejs.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/platform.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/process.wit create mode 100644 crates/extension_api/wit/since_v0.7.0/settings.rs create mode 100644 crates/extension_api/wit/since_v0.7.0/slash-command.wit create mode 100644 crates/extension_host/src/wasm_host/llm_provider.rs create mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs create mode 100644 crates/language_models/src/extension.rs create mode 100644 docs/language_model_provider_extensions_implementation.md create mode 100644 docs/language_model_provider_extensions_plan.md diff --git a/Cargo.lock b/Cargo.lock index 6e558cbf395866..4a1cf2518639b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5916,6 +5916,7 @@ dependencies = [ "http_client", "language", "language_extension", + "language_model", "log", "lsp", "moka", @@ -5938,6 +5939,7 @@ dependencies = [ "theme", "theme_extension", "toml 0.8.23", + "ui", "url", "util", "wasmparser 0.221.3", @@ -8911,6 +8913,7 @@ dependencies = [ "credentials_provider", "deepseek", "editor", + "extension", "fs", "futures 0.3.31", "google_ai", diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 6a24e3ba3f496b..dc395d6a937c7b 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -29,6 +29,7 @@ pub struct ExtensionHostProxy { slash_command_proxy: RwLock>>, context_server_proxy: RwLock>>, debug_adapter_provider_proxy: RwLock>>, + language_model_provider_proxy: RwLock>>, } impl ExtensionHostProxy { @@ -54,6 +55,7 @@ impl ExtensionHostProxy { slash_command_proxy: RwLock::default(), context_server_proxy: RwLock::default(), debug_adapter_provider_proxy: RwLock::default(), + language_model_provider_proxy: RwLock::default(), } } @@ -90,6 +92,15 @@ impl ExtensionHostProxy { .write() .replace(Arc::new(proxy)); } + + pub fn register_language_model_provider_proxy( + &self, + proxy: impl ExtensionLanguageModelProviderProxy, + ) { + self.language_model_provider_proxy + .write() + .replace(Arc::new(proxy)); + } } pub trait ExtensionThemeProxy: Send + Sync + 'static { @@ -375,6 +386,49 @@ pub trait ExtensionContextServerProxy: Send + Sync + 'static { fn unregister_context_server(&self, server_id: Arc, cx: &mut App); } +/// A boxed function that registers a language model provider with the registry. +/// This allows extension_host to create the provider (which requires WasmExtension) +/// and pass a registration closure to the language_models crate. +pub type LanguageModelProviderRegistration = Box; + +pub trait ExtensionLanguageModelProviderProxy: Send + Sync + 'static { + /// Register an LLM provider from an extension. + /// The `register_fn` closure will be called with the App context and should + /// register the provider with the LanguageModelRegistry. + fn register_language_model_provider( + &self, + provider_id: Arc, + register_fn: LanguageModelProviderRegistration, + cx: &mut App, + ); + + /// Unregister an LLM provider when an extension is unloaded. + fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App); +} + +impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy { + fn register_language_model_provider( + &self, + provider_id: Arc, + register_fn: LanguageModelProviderRegistration, + cx: &mut App, + ) { + let Some(proxy) = self.language_model_provider_proxy.read().clone() else { + return; + }; + + proxy.register_language_model_provider(provider_id, register_fn, cx) + } + + fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App) { + let Some(proxy) = self.language_model_provider_proxy.read().clone() else { + return; + }; + + proxy.unregister_language_model_provider(provider_id, cx) + } +} + impl ExtensionContextServerProxy for ExtensionHostProxy { fn register_context_server( &self, diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 4ecdd378ca86db..02a6c18be7ae0b 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -93,6 +93,8 @@ pub struct ExtensionManifest { pub debug_adapters: BTreeMap, DebugAdapterManifestEntry>, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub debug_locators: BTreeMap, DebugLocatorManifestEntry>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub language_model_providers: BTreeMap, LanguageModelProviderManifestEntry>, } impl ExtensionManifest { @@ -288,6 +290,57 @@ pub struct DebugAdapterManifestEntry { #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct DebugLocatorManifestEntry {} +/// Manifest entry for a language model provider. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LanguageModelProviderManifestEntry { + /// Display name for the provider. + pub name: String, + /// Icon name from Zed's icon set (optional). + #[serde(default)] + pub icon: Option, + /// Default models to show even before API connection. + #[serde(default)] + pub models: Vec, + /// Authentication configuration. + #[serde(default)] + pub auth: Option, +} + +/// Manifest entry for a language model. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LanguageModelManifestEntry { + /// Unique identifier for the model. + pub id: String, + /// Display name for the model. + pub name: String, + /// Maximum input token count. + #[serde(default)] + pub max_token_count: u64, + /// Maximum output tokens (optional). + #[serde(default)] + pub max_output_tokens: Option, + /// Whether the model supports image inputs. + #[serde(default)] + pub supports_images: bool, + /// Whether the model supports tool/function calling. + #[serde(default)] + pub supports_tools: bool, + /// Whether the model supports extended thinking/reasoning. + #[serde(default)] + pub supports_thinking: bool, +} + +/// Authentication configuration for a language model provider. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LanguageModelAuthConfig { + /// Environment variable name for the API key. + #[serde(default)] + pub env_var: Option, + /// Label to show when prompting for credentials. + #[serde(default)] + pub credential_label: Option, +} + impl ExtensionManifest { pub async fn load(fs: Arc, extension_dir: &Path) -> Result { let extension_name = extension_dir @@ -358,6 +411,7 @@ fn manifest_from_old_manifest( capabilities: Vec::new(), debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: Default::default(), } } @@ -391,6 +445,7 @@ mod tests { capabilities: vec![], debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: BTreeMap::default(), } } diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 723e5442098f1a..5b548b8e45f282 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -17,7 +17,8 @@ pub use serde_json; pub use wit::{ CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars, KeyValueStore, LanguageServerInstallationStatus, Project, Range, Worktree, download_file, - make_file_executable, + llm_delete_credential, llm_get_credential, llm_get_env_var, llm_request_credential, + llm_store_credential, make_file_executable, zed::extension::context_server::ContextServerConfiguration, zed::extension::dap::{ AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, BuildTaskTemplate, @@ -29,6 +30,19 @@ pub use wit::{ GithubRelease, GithubReleaseAsset, GithubReleaseOptions, github_release_by_tag_name, latest_github_release, }, + zed::extension::llm_provider::{ + CacheConfiguration as LlmCacheConfiguration, CompletionEvent as LlmCompletionEvent, + CompletionRequest as LlmCompletionRequest, CredentialType as LlmCredentialType, + ImageData as LlmImageData, MessageContent as LlmMessageContent, + MessageRole as LlmMessageRole, ModelCapabilities as LlmModelCapabilities, + ModelInfo as LlmModelInfo, ProviderInfo as LlmProviderInfo, + RequestMessage as LlmRequestMessage, StopReason as LlmStopReason, + ThinkingContent as LlmThinkingContent, TokenUsage as LlmTokenUsage, + ToolChoice as LlmToolChoice, ToolDefinition as LlmToolDefinition, + ToolInputFormat as LlmToolInputFormat, ToolResult as LlmToolResult, + ToolResultContent as LlmToolResultContent, ToolUse as LlmToolUse, + ToolUseJsonParseError as LlmToolUseJsonParseError, + }, zed::extension::nodejs::{ node_binary_path, npm_install_package, npm_package_installed_version, npm_package_latest_version, @@ -259,6 +273,79 @@ pub trait Extension: Send + Sync { ) -> Result { Err("`run_dap_locator` not implemented".to_string()) } + + // ========================================================================= + // Language Model Provider Methods + // ========================================================================= + + /// Returns information about language model providers offered by this extension. + fn llm_providers(&self) -> Vec { + Vec::new() + } + + /// Returns the models available for a provider. + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(Vec::new()) + } + + /// Check if the provider is authenticated. + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + false + } + + /// Attempt to authenticate the provider. + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + Err("`llm_provider_authenticate` not implemented".to_string()) + } + + /// Reset credentials for the provider. + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + Err("`llm_provider_reset_credentials` not implemented".to_string()) + } + + /// Count tokens for a request. + fn llm_count_tokens( + &self, + _provider_id: &str, + _model_id: &str, + _request: &LlmCompletionRequest, + ) -> Result { + Err("`llm_count_tokens` not implemented".to_string()) + } + + /// Start streaming a completion from the model. + /// Returns a stream ID that can be used with `llm_stream_completion_next` and `llm_stream_completion_close`. + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + _model_id: &str, + _request: &LlmCompletionRequest, + ) -> Result { + Err("`llm_stream_completion_start` not implemented".to_string()) + } + + /// Get the next event from a completion stream. + /// Returns `Ok(None)` when the stream is complete. + fn llm_stream_completion_next( + &mut self, + _stream_id: &str, + ) -> Result, String> { + Err("`llm_stream_completion_next` not implemented".to_string()) + } + + /// Close a completion stream and release its resources. + fn llm_stream_completion_close(&mut self, _stream_id: &str) { + // Default implementation does nothing + } + + /// Get cache configuration for a model (if prompt caching is supported). + fn llm_cache_configuration( + &self, + _provider_id: &str, + _model_id: &str, + ) -> Option { + None + } } /// Registers the provided type as a Zed extension. @@ -334,7 +421,7 @@ mod wit { wit_bindgen::generate!({ skip: ["init-extension"], - path: "./wit/since_v0.6.0", + path: "./wit/since_v0.7.0", }); } @@ -518,6 +605,61 @@ impl wit::Guest for Component { ) -> Result { extension().run_dap_locator(locator_name, build_task) } + + // ========================================================================= + // Language Model Provider Methods + // ========================================================================= + + fn llm_providers() -> Vec { + extension().llm_providers() + } + + fn llm_provider_models(provider_id: String) -> Result, String> { + extension().llm_provider_models(&provider_id) + } + + fn llm_provider_is_authenticated(provider_id: String) -> bool { + extension().llm_provider_is_authenticated(&provider_id) + } + + fn llm_provider_authenticate(provider_id: String) -> Result<(), String> { + extension().llm_provider_authenticate(&provider_id) + } + + fn llm_provider_reset_credentials(provider_id: String) -> Result<(), String> { + extension().llm_provider_reset_credentials(&provider_id) + } + + fn llm_count_tokens( + provider_id: String, + model_id: String, + request: LlmCompletionRequest, + ) -> Result { + extension().llm_count_tokens(&provider_id, &model_id, &request) + } + + fn llm_stream_completion_start( + provider_id: String, + model_id: String, + request: LlmCompletionRequest, + ) -> Result { + extension().llm_stream_completion_start(&provider_id, &model_id, &request) + } + + fn llm_stream_completion_next(stream_id: String) -> Result, String> { + extension().llm_stream_completion_next(&stream_id) + } + + fn llm_stream_completion_close(stream_id: String) { + extension().llm_stream_completion_close(&stream_id) + } + + fn llm_cache_configuration( + provider_id: String, + model_id: String, + ) -> Option { + extension().llm_cache_configuration(&provider_id, &model_id) + } } /// The ID of a language server. diff --git a/crates/extension_api/wit/since_v0.7.0/common.wit b/crates/extension_api/wit/since_v0.7.0/common.wit new file mode 100644 index 00000000000000..139e7ba0ca4d1c --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/common.wit @@ -0,0 +1,12 @@ +interface common { + /// A (half-open) range (`[start, end)`). + record range { + /// The start of the range (inclusive). + start: u32, + /// The end of the range (exclusive). + end: u32, + } + + /// A list of environment variables. + type env-vars = list>; +} diff --git a/crates/extension_api/wit/since_v0.7.0/context-server.wit b/crates/extension_api/wit/since_v0.7.0/context-server.wit new file mode 100644 index 00000000000000..7234e0e6d0f6d4 --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/context-server.wit @@ -0,0 +1,11 @@ +interface context-server { + /// Configuration for context server setup and installation. + record context-server-configuration { + /// Installation instructions in Markdown format. + installation-instructions: string, + /// JSON schema for settings validation. + settings-schema: string, + /// Default settings template. + default-settings: string, + } +} diff --git a/crates/extension_api/wit/since_v0.7.0/dap.wit b/crates/extension_api/wit/since_v0.7.0/dap.wit new file mode 100644 index 00000000000000..693befe02f9c31 --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/dap.wit @@ -0,0 +1,123 @@ +interface dap { + use common.{env-vars}; + + /// Resolves a specified TcpArgumentsTemplate into TcpArguments + resolve-tcp-template: func(template: tcp-arguments-template) -> result; + + record launch-request { + program: string, + cwd: option, + args: list, + envs: env-vars, + } + + record attach-request { + process-id: option, + } + + variant debug-request { + launch(launch-request), + attach(attach-request) + } + + record tcp-arguments { + port: u16, + host: u32, + timeout: option, + } + + record tcp-arguments-template { + port: option, + host: option, + timeout: option, + } + + /// Debug Config is the "highest-level" configuration for a debug session. + /// It comes from a new process modal UI; thus, it is essentially debug-adapter-agnostic. + /// It is expected of the extension to translate this generic configuration into something that can be debugged by the adapter (debug scenario). + record debug-config { + /// Name of the debug task + label: string, + /// The debug adapter to use + adapter: string, + request: debug-request, + stop-on-entry: option, + } + + record task-template { + /// Human readable name of the task to display in the UI. + label: string, + /// Executable command to spawn. + command: string, + args: list, + env: env-vars, + cwd: option, + } + + /// A task template with substituted task variables. + type resolved-task = task-template; + + /// A task template for building a debug target. + type build-task-template = task-template; + + variant build-task-definition { + by-name(string), + template(build-task-definition-template-payload ) + } + record build-task-definition-template-payload { + locator-name: option, + template: build-task-template + } + + /// Debug Scenario is the user-facing configuration type (used in debug.json). It is still concerned with what to debug and not necessarily how to do it (except for any + /// debug-adapter-specific configuration options). + record debug-scenario { + /// Unsubstituted label for the task.DebugAdapterBinary + label: string, + /// Name of the Debug Adapter this configuration is intended for. + adapter: string, + /// An optional build step to be ran prior to starting a debug session. Build steps are used by Zed's locators to locate the executable to debug. + build: option, + /// JSON-encoded configuration for a given debug adapter. + config: string, + /// TCP connection parameters (if they were specified by user) + tcp-connection: option, + } + + enum start-debugging-request-arguments-request { + launch, + attach, + } + + record debug-task-definition { + /// Unsubstituted label for the task.DebugAdapterBinary + label: string, + /// Name of the Debug Adapter this configuration is intended for. + adapter: string, + /// JSON-encoded configuration for a given debug adapter. + config: string, + /// TCP connection parameters (if they were specified by user) + tcp-connection: option, + } + + record start-debugging-request-arguments { + /// JSON-encoded configuration for a given debug adapter. It is specific to each debug adapter. + /// `configuration` will have it's Zed variable references substituted prior to being passed to the debug adapter. + configuration: string, + request: start-debugging-request-arguments-request, + } + + /// The lowest-level representation of a debug session, which specifies: + /// - How to start a debug adapter process + /// - How to start a debug session with it (using DAP protocol) + /// for a given debug scenario. + record debug-adapter-binary { + command: option, + arguments: list, + envs: env-vars, + cwd: option, + /// Zed will use TCP transport if `connection` is specified. + connection: option, + request-args: start-debugging-request-arguments + } +} diff --git a/crates/extension_api/wit/since_v0.7.0/extension.wit b/crates/extension_api/wit/since_v0.7.0/extension.wit new file mode 100644 index 00000000000000..265bb922a43c03 --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/extension.wit @@ -0,0 +1,248 @@ +package zed:extension; + +world extension { + import context-server; + import dap; + import github; + import http-client; + import platform; + import process; + import nodejs; + import llm-provider; + + use common.{env-vars, range}; + use context-server.{context-server-configuration}; + use dap.{attach-request, build-task-template, debug-config, debug-adapter-binary, debug-task-definition, debug-request, debug-scenario, launch-request, resolved-task, start-debugging-request-arguments-request}; + use lsp.{completion, symbol}; + use process.{command}; + use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; + use llm-provider.{ + provider-info, model-info, completion-request, + credential-type, cache-configuration, completion-event, token-usage + }; + + /// Initializes the extension. + export init-extension: func(); + + /// The type of a downloaded file. + enum downloaded-file-type { + /// A gzipped file (`.gz`). + gzip, + /// A gzipped tar archive (`.tar.gz`). + gzip-tar, + /// A ZIP file (`.zip`). + zip, + /// An uncompressed file. + uncompressed, + } + + /// The installation status for a language server. + variant language-server-installation-status { + /// The language server has no installation status. + none, + /// The language server is being downloaded. + downloading, + /// The language server is checking for updates. + checking-for-update, + /// The language server installation failed for specified reason. + failed(string), + } + + record settings-location { + worktree-id: u64, + path: string, + } + + import get-settings: func(path: option, category: string, key: option) -> result; + + /// Downloads a file from the given URL and saves it to the given path within the extension's + /// working directory. + /// + /// The file will be extracted according to the given file type. + import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; + + /// Makes the file at the given path executable. + import make-file-executable: func(filepath: string) -> result<_, string>; + + /// Updates the installation status for the given language server. + import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); + + /// A Zed worktree. + resource worktree { + /// Returns the ID of the worktree. + id: func() -> u64; + /// Returns the root path of the worktree. + root-path: func() -> string; + /// Returns the textual contents of the specified file in the worktree. + read-text-file: func(path: string) -> result; + /// Returns the path to the given binary name, if one is present on the `$PATH`. + which: func(binary-name: string) -> option; + /// Returns the current shell environment. + shell-env: func() -> env-vars; + } + + /// A Zed project. + resource project { + /// Returns the IDs of all of the worktrees in this project. + worktree-ids: func() -> list; + } + + /// A key-value store. + resource key-value-store { + /// Inserts an entry under the specified key. + insert: func(key: string, value: string) -> result<_, string>; + } + + /// Returns the command used to start up the language server. + export language-server-command: func(language-server-id: string, worktree: borrow) -> result; + + /// Returns the initialization options to pass to the language server on startup. + /// + /// The initialization options are represented as a JSON string. + export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; + + /// Returns the workspace configuration options to pass to the language server. + export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; + + /// Returns the initialization options to pass to the other language server. + export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; + + /// Returns the workspace configuration options to pass to the other language server. + export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; + + /// A label containing some code. + record code-label { + /// The source code to parse with Tree-sitter. + code: string, + /// The spans to display in the label. + spans: list, + /// The range of the displayed label to include when filtering. + filter-range: range, + } + + /// A span within a code label. + variant code-label-span { + /// A range into the parsed code. + code-range(range), + /// A span containing a code literal. + literal(code-label-span-literal), + } + + /// A span containing a code literal. + record code-label-span-literal { + /// The literal text. + text: string, + /// The name of the highlight to use for this literal. + highlight-name: option, + } + + export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; + export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; + + + /// Returns the completions that should be shown when completing the provided slash command with the given query. + export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; + + /// Returns the output from running the provided slash command. + export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; + + /// Returns the command used to start up a context server. + export context-server-command: func(context-server-id: string, project: borrow) -> result; + + /// Returns the configuration for a context server. + export context-server-configuration: func(context-server-id: string, project: borrow) -> result, string>; + + /// Returns a list of packages as suggestions to be included in the `/docs` + /// search results. + /// + /// This can be used to provide completions for known packages (e.g., from the + /// local project or a registry) before a package has been indexed. + export suggest-docs-packages: func(provider-name: string) -> result, string>; + + /// Indexes the docs for the specified package. + export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; + + /// Returns a configured debug adapter binary for a given debug task. + export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option, worktree: borrow) -> result; + /// Returns the kind of a debug scenario (launch or attach). + export dap-request-kind: func(adapter-name: string, config: string) -> result; + export dap-config-to-scenario: func(config: debug-config) -> result; + export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option; + export run-dap-locator: func(locator-name: string, config: resolved-task) -> result; + + // ========================================================================= + // Language Model Provider Extension API + // ========================================================================= + + /// Returns information about language model providers offered by this extension. + export llm-providers: func() -> list; + + /// Returns the models available for a provider. + export llm-provider-models: func(provider-id: string) -> result, string>; + + /// Check if the provider is authenticated. + export llm-provider-is-authenticated: func(provider-id: string) -> bool; + + /// Attempt to authenticate the provider. + export llm-provider-authenticate: func(provider-id: string) -> result<_, string>; + + /// Reset credentials for the provider. + export llm-provider-reset-credentials: func(provider-id: string) -> result<_, string>; + + /// Count tokens for a request. + export llm-count-tokens: func( + provider-id: string, + model-id: string, + request: completion-request + ) -> result; + + /// Start streaming a completion from the model. + /// Returns a stream ID that can be used with llm-stream-next and llm-stream-close. + export llm-stream-completion-start: func( + provider-id: string, + model-id: string, + request: completion-request + ) -> result; + + /// Get the next event from a completion stream. + /// Returns None when the stream is complete. + export llm-stream-completion-next: func( + stream-id: string + ) -> result, string>; + + /// Close a completion stream and release its resources. + export llm-stream-completion-close: func( + stream-id: string + ); + + /// Get cache configuration for a model (if prompt caching is supported). + export llm-cache-configuration: func( + provider-id: string, + model-id: string + ) -> option; + + // ========================================================================= + // Language Model Provider Imports (callable by extensions) + // ========================================================================= + + /// Request a credential from the user. + /// Returns true if the credential was provided, false if the user cancelled. + import llm-request-credential: func( + provider-id: string, + credential-type: credential-type, + label: string, + placeholder: string + ) -> result; + + /// Get a stored credential for this provider. + import llm-get-credential: func(provider-id: string) -> option; + + /// Store a credential for this provider. + import llm-store-credential: func(provider-id: string, value: string) -> result<_, string>; + + /// Delete a stored credential for this provider. + import llm-delete-credential: func(provider-id: string) -> result<_, string>; + + /// Read an environment variable. + import llm-get-env-var: func(name: string) -> option; +} diff --git a/crates/extension_api/wit/since_v0.7.0/github.wit b/crates/extension_api/wit/since_v0.7.0/github.wit new file mode 100644 index 00000000000000..21cd5d48056af0 --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/github.wit @@ -0,0 +1,35 @@ +interface github { + /// A GitHub release. + record github-release { + /// The version of the release. + version: string, + /// The list of assets attached to the release. + assets: list, + } + + /// An asset from a GitHub release. + record github-release-asset { + /// The name of the asset. + name: string, + /// The download URL for the asset. + download-url: string, + } + + /// The options used to filter down GitHub releases. + record github-release-options { + /// Whether releases without assets should be included. + require-assets: bool, + /// Whether pre-releases should be included. + pre-release: bool, + } + + /// Returns the latest release for the given GitHub repository. + /// + /// Takes repo as a string in the form "/", for example: "zed-industries/zed". + latest-github-release: func(repo: string, options: github-release-options) -> result; + + /// Returns the GitHub release with the specified tag name for the given GitHub repository. + /// + /// Returns an error if a release with the given tag name does not exist. + github-release-by-tag-name: func(repo: string, tag: string) -> result; +} diff --git a/crates/extension_api/wit/since_v0.7.0/http-client.wit b/crates/extension_api/wit/since_v0.7.0/http-client.wit new file mode 100644 index 00000000000000..bb0206c17a52d4 --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/http-client.wit @@ -0,0 +1,67 @@ +interface http-client { + /// An HTTP request. + record http-request { + /// The HTTP method for the request. + method: http-method, + /// The URL to which the request should be made. + url: string, + /// The headers for the request. + headers: list>, + /// The request body. + body: option>, + /// The policy to use for redirects. + redirect-policy: redirect-policy, + } + + /// HTTP methods. + enum http-method { + /// `GET` + get, + /// `HEAD` + head, + /// `POST` + post, + /// `PUT` + put, + /// `DELETE` + delete, + /// `OPTIONS` + options, + /// `PATCH` + patch, + } + + /// The policy for dealing with redirects received from the server. + variant redirect-policy { + /// Redirects from the server will not be followed. + /// + /// This is the default behavior. + no-follow, + /// Redirects from the server will be followed up to the specified limit. + follow-limit(u32), + /// All redirects from the server will be followed. + follow-all, + } + + /// An HTTP response. + record http-response { + /// The response headers. + headers: list>, + /// The response body. + body: list, + } + + /// Performs an HTTP request and returns the response. + fetch: func(req: http-request) -> result; + + /// An HTTP response stream. + resource http-response-stream { + /// Retrieves the next chunk of data from the response stream. + /// + /// Returns `Ok(None)` if the stream has ended. + next-chunk: func() -> result>, string>; + } + + /// Performs an HTTP request and returns a response stream. + fetch-stream: func(req: http-request) -> result; +} diff --git a/crates/extension_api/wit/since_v0.7.0/llm-provider.wit b/crates/extension_api/wit/since_v0.7.0/llm-provider.wit new file mode 100644 index 00000000000000..31f19b90769fdf --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/llm-provider.wit @@ -0,0 +1,255 @@ +interface llm-provider { + /// Information about a language model provider. + record provider-info { + /// Unique identifier for the provider (e.g., "my-extension.my-provider"). + id: string, + /// Display name for the provider. + name: string, + /// Icon name from Zed's icon set (optional). + icon: option, + } + + /// Capabilities of a language model. + record model-capabilities { + /// Whether the model supports image inputs. + supports-images: bool, + /// Whether the model supports tool/function calling. + supports-tools: bool, + /// Whether the model supports the "auto" tool choice. + supports-tool-choice-auto: bool, + /// Whether the model supports the "any" tool choice. + supports-tool-choice-any: bool, + /// Whether the model supports the "none" tool choice. + supports-tool-choice-none: bool, + /// Whether the model supports extended thinking/reasoning. + supports-thinking: bool, + /// The format for tool input schemas. + tool-input-format: tool-input-format, + } + + /// Format for tool input schemas. + enum tool-input-format { + /// Standard JSON Schema format. + json-schema, + /// Simplified schema format for certain providers. + simplified, + } + + /// Information about a specific model. + record model-info { + /// Unique identifier for the model. + id: string, + /// Display name for the model. + name: string, + /// Maximum input token count. + max-token-count: u64, + /// Maximum output tokens (optional). + max-output-tokens: option, + /// Model capabilities. + capabilities: model-capabilities, + /// Whether this is the default model for the provider. + is-default: bool, + /// Whether this is the default fast model. + is-default-fast: bool, + } + + /// The role of a message participant. + enum message-role { + /// User message. + user, + /// Assistant message. + assistant, + /// System message. + system, + } + + /// A message in a completion request. + record request-message { + /// The role of the message sender. + role: message-role, + /// The content of the message. + content: list, + /// Whether to cache this message for prompt caching. + cache: bool, + } + + /// Content within a message. + variant message-content { + /// Plain text content. + text(string), + /// Image content. + image(image-data), + /// A tool use request from the assistant. + tool-use(tool-use), + /// A tool result from the user. + tool-result(tool-result), + /// Thinking/reasoning content. + thinking(thinking-content), + /// Redacted/encrypted thinking content. + redacted-thinking(string), + } + + /// Image data for vision models. + record image-data { + /// Base64-encoded image data. + source: string, + /// Image width in pixels (optional). + width: option, + /// Image height in pixels (optional). + height: option, + } + + /// A tool use request from the model. + record tool-use { + /// Unique identifier for this tool use. + id: string, + /// The name of the tool being used. + name: string, + /// JSON string of the tool input arguments. + input: string, + /// Thought signature for providers that support it (e.g., Anthropic). + thought-signature: option, + } + + /// A tool result to send back to the model. + record tool-result { + /// The ID of the tool use this is a result for. + tool-use-id: string, + /// The name of the tool. + tool-name: string, + /// Whether this result represents an error. + is-error: bool, + /// The content of the result. + content: tool-result-content, + } + + /// Content of a tool result. + variant tool-result-content { + /// Text result. + text(string), + /// Image result. + image(image-data), + } + + /// Thinking/reasoning content from models that support extended thinking. + record thinking-content { + /// The thinking text. + text: string, + /// Signature for the thinking block (provider-specific). + signature: option, + } + + /// A tool definition for function calling. + record tool-definition { + /// The name of the tool. + name: string, + /// Description of what the tool does. + description: string, + /// JSON Schema for input parameters. + input-schema: string, + } + + /// Tool choice preference for the model. + enum tool-choice { + /// Let the model decide whether to use tools. + auto, + /// Force the model to use at least one tool. + any, + /// Prevent the model from using tools. + none, + } + + /// A completion request to send to the model. + record completion-request { + /// The messages in the conversation. + messages: list, + /// Available tools for the model to use. + tools: list, + /// Tool choice preference. + tool-choice: option, + /// Stop sequences to end generation. + stop-sequences: list, + /// Temperature for sampling (0.0-1.0). + temperature: option, + /// Whether thinking/reasoning is allowed. + thinking-allowed: bool, + /// Maximum tokens to generate. + max-tokens: option, + } + + /// Events emitted during completion streaming. + variant completion-event { + /// Completion has started. + started, + /// Text content chunk. + text(string), + /// Thinking/reasoning content chunk. + thinking(thinking-content), + /// Redacted thinking (encrypted) chunk. + redacted-thinking(string), + /// Tool use request from the model. + tool-use(tool-use), + /// JSON parse error when parsing tool input. + tool-use-json-parse-error(tool-use-json-parse-error), + /// Completion stopped. + stop(stop-reason), + /// Token usage update. + usage(token-usage), + /// Reasoning details (provider-specific JSON). + reasoning-details(string), + } + + /// Error information when tool use JSON parsing fails. + record tool-use-json-parse-error { + /// The tool use ID. + id: string, + /// The tool name. + tool-name: string, + /// The raw input that failed to parse. + raw-input: string, + /// The parse error message. + error: string, + } + + /// Reason the completion stopped. + enum stop-reason { + /// The model finished generating. + end-turn, + /// Maximum tokens reached. + max-tokens, + /// The model wants to use a tool. + tool-use, + /// The model refused to respond. + refusal, + } + + /// Token usage statistics. + record token-usage { + /// Number of input tokens used. + input-tokens: u64, + /// Number of output tokens generated. + output-tokens: u64, + /// Tokens used for cache creation (if supported). + cache-creation-input-tokens: option, + /// Tokens read from cache (if supported). + cache-read-input-tokens: option, + } + + /// Credential types that can be requested. + enum credential-type { + /// An API key. + api-key, + /// An OAuth token. + oauth-token, + } + + /// Cache configuration for prompt caching. + record cache-configuration { + /// Maximum number of cache anchors. + max-cache-anchors: u32, + /// Whether caching should be applied to tool definitions. + should-cache-tool-definitions: bool, + /// Minimum token count for a message to be cached. + min-total-token-count: u64, + } +} diff --git a/crates/extension_api/wit/since_v0.7.0/lsp.wit b/crates/extension_api/wit/since_v0.7.0/lsp.wit new file mode 100644 index 00000000000000..91a36c93a66467 --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/lsp.wit @@ -0,0 +1,90 @@ +interface lsp { + /// An LSP completion. + record completion { + label: string, + label-details: option, + detail: option, + kind: option, + insert-text-format: option, + } + + /// The kind of an LSP completion. + variant completion-kind { + text, + method, + function, + %constructor, + field, + variable, + class, + %interface, + module, + property, + unit, + value, + %enum, + keyword, + snippet, + color, + file, + reference, + folder, + enum-member, + constant, + struct, + event, + operator, + type-parameter, + other(s32), + } + + /// Label details for an LSP completion. + record completion-label-details { + detail: option, + description: option, + } + + /// Defines how to interpret the insert text in a completion item. + variant insert-text-format { + plain-text, + snippet, + other(s32), + } + + /// An LSP symbol. + record symbol { + kind: symbol-kind, + name: string, + } + + /// The kind of an LSP symbol. + variant symbol-kind { + file, + module, + namespace, + %package, + class, + method, + property, + field, + %constructor, + %enum, + %interface, + function, + variable, + constant, + %string, + number, + boolean, + array, + object, + key, + null, + enum-member, + struct, + event, + operator, + type-parameter, + other(s32), + } +} diff --git a/crates/extension_api/wit/since_v0.7.0/nodejs.wit b/crates/extension_api/wit/since_v0.7.0/nodejs.wit new file mode 100644 index 00000000000000..c814548314162c --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/nodejs.wit @@ -0,0 +1,13 @@ +interface nodejs { + /// Returns the path to the Node binary used by Zed. + node-binary-path: func() -> result; + + /// Returns the latest version of the given NPM package. + npm-package-latest-version: func(package-name: string) -> result; + + /// Returns the installed version of the given NPM package, if it exists. + npm-package-installed-version: func(package-name: string) -> result, string>; + + /// Installs the specified NPM package. + npm-install-package: func(package-name: string, version: string) -> result<_, string>; +} diff --git a/crates/extension_api/wit/since_v0.7.0/platform.wit b/crates/extension_api/wit/since_v0.7.0/platform.wit new file mode 100644 index 00000000000000..48472a99bc175f --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/platform.wit @@ -0,0 +1,24 @@ +interface platform { + /// An operating system. + enum os { + /// macOS. + mac, + /// Linux. + linux, + /// Windows. + windows, + } + + /// A platform architecture. + enum architecture { + /// AArch64 (e.g., Apple Silicon). + aarch64, + /// x86. + x86, + /// x86-64. + x8664, + } + + /// Gets the current operating system and architecture. + current-platform: func() -> tuple; +} diff --git a/crates/extension_api/wit/since_v0.7.0/process.wit b/crates/extension_api/wit/since_v0.7.0/process.wit new file mode 100644 index 00000000000000..d9a5728a3d8f5b --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/process.wit @@ -0,0 +1,29 @@ +interface process { + use common.{env-vars}; + + /// A command. + record command { + /// The command to execute. + command: string, + /// The arguments to pass to the command. + args: list, + /// The environment variables to set for the command. + env: env-vars, + } + + /// The output of a finished process. + record output { + /// The status (exit code) of the process. + /// + /// On Unix, this will be `None` if the process was terminated by a signal. + status: option, + /// The data that the process wrote to stdout. + stdout: list, + /// The data that the process wrote to stderr. + stderr: list, + } + + /// Executes the given command as a child process, waiting for it to finish + /// and collecting all of its output. + run-command: func(command: command) -> result; +} diff --git a/crates/extension_api/wit/since_v0.7.0/settings.rs b/crates/extension_api/wit/since_v0.7.0/settings.rs new file mode 100644 index 00000000000000..19e28c1ba955a9 --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/settings.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, num::NonZeroU32}; + +/// The settings for a particular language. +#[derive(Debug, Serialize, Deserialize)] +pub struct LanguageSettings { + /// How many columns a tab should occupy. + pub tab_size: NonZeroU32, +} + +/// The settings for a particular language server. +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct LspSettings { + /// The settings for the language server binary. + pub binary: Option, + /// The initialization options to pass to the language server. + pub initialization_options: Option, + /// The settings to pass to language server. + pub settings: Option, +} + +/// The settings for a particular context server. +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ContextServerSettings { + /// The settings for the context server binary. + pub command: Option, + /// The settings to pass to the context server. + pub settings: Option, +} + +/// The settings for a command. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct CommandSettings { + /// The path to the command. + pub path: Option, + /// The arguments to pass to the command. + pub arguments: Option>, + /// The environment variables. + pub env: Option>, +} diff --git a/crates/extension_api/wit/since_v0.7.0/slash-command.wit b/crates/extension_api/wit/since_v0.7.0/slash-command.wit new file mode 100644 index 00000000000000..f52561c2ef412b --- /dev/null +++ b/crates/extension_api/wit/since_v0.7.0/slash-command.wit @@ -0,0 +1,41 @@ +interface slash-command { + use common.{range}; + + /// A slash command for use in the Assistant. + record slash-command { + /// The name of the slash command. + name: string, + /// The description of the slash command. + description: string, + /// The tooltip text to display for the run button. + tooltip-text: string, + /// Whether this slash command requires an argument. + requires-argument: bool, + } + + /// The output of a slash command. + record slash-command-output { + /// The text produced by the slash command. + text: string, + /// The list of sections to show in the slash command placeholder. + sections: list, + } + + /// A section in the slash command output. + record slash-command-output-section { + /// The range this section occupies. + range: range, + /// The label to display in the placeholder for this section. + label: string, + } + + /// A completion for a slash command argument. + record slash-command-argument-completion { + /// The label to display for this completion. + label: string, + /// The new text that should be inserted into the command when this completion is accepted. + new-text: string, + /// Whether the command should be run when accepting this completion. + run-command: bool, + } +} diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 328b808b1310e3..46c481ee53babe 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -30,6 +30,7 @@ gpui.workspace = true gpui_tokio.workspace = true http_client.workspace = true language.workspace = true +language_model.workspace = true log.workspace = true lsp.workspace = true moka.workspace = true @@ -47,6 +48,7 @@ task.workspace = true telemetry.workspace = true tempfile.workspace = true toml.workspace = true +ui.workspace = true url.workspace = true util.workspace = true wasmparser.workspace = true diff --git a/crates/extension_host/benches/extension_compilation_benchmark.rs b/crates/extension_host/benches/extension_compilation_benchmark.rs index c3459cf116b551..2a77be06a88d5d 100644 --- a/crates/extension_host/benches/extension_compilation_benchmark.rs +++ b/crates/extension_host/benches/extension_compilation_benchmark.rs @@ -143,6 +143,7 @@ fn manifest() -> ExtensionManifest { )], debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: BTreeMap::default(), } } diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index 9f27b5e480bc3c..6278deef0a7d41 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -113,6 +113,7 @@ mod tests { capabilities: vec![], debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: BTreeMap::default(), } } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index c1c598f1895f68..f399354ecec6a1 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -16,9 +16,9 @@ pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents, - ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy, - ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, - ExtensionThemeProxy, + ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageModelProviderProxy, + ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, + ExtensionSnippetProxy, ExtensionThemeProxy, }; use fs::{Fs, RemoveOptions}; use futures::future::join_all; @@ -57,9 +57,10 @@ use std::{ }; use url::Url; use util::{ResultExt, paths::RemotePathBuf}; +use wasm_host::llm_provider::ExtensionLanguageModelProvider; use wasm_host::{ WasmExtension, WasmHost, - wit::{is_supported_wasm_api_version, wasm_api_version_range}, + wit::{LlmModelInfo, LlmProviderInfo, is_supported_wasm_api_version, wasm_api_version_range}, }; pub use extension::{ @@ -1217,6 +1218,11 @@ impl ExtensionStore { for command_name in extension.manifest.slash_commands.keys() { self.proxy.unregister_slash_command(command_name.clone()); } + for provider_id in extension.manifest.language_model_providers.keys() { + let full_provider_id: Arc = format!("{}:{}", extension_id, provider_id).into(); + self.proxy + .unregister_language_model_provider(full_provider_id, cx); + } } self.wasm_extensions @@ -1355,7 +1361,11 @@ impl ExtensionStore { }) .await; - let mut wasm_extensions = Vec::new(); + let mut wasm_extensions: Vec<( + Arc, + WasmExtension, + Vec<(LlmProviderInfo, Vec)>, + )> = Vec::new(); for extension in extension_entries { if extension.manifest.lib.kind.is_none() { continue; @@ -1373,7 +1383,71 @@ impl ExtensionStore { match wasm_extension { Ok(wasm_extension) => { - wasm_extensions.push((extension.manifest.clone(), wasm_extension)) + // Query for LLM providers if the manifest declares any + let mut llm_providers_with_models = Vec::new(); + if !extension.manifest.language_model_providers.is_empty() { + let providers_result = wasm_extension + .call(|ext, store| { + async move { ext.call_llm_providers(store).await }.boxed() + }) + .await; + + if let Ok(Ok(providers)) = providers_result { + for provider_info in providers { + let models_result = wasm_extension + .call({ + let provider_id = provider_info.id.clone(); + |ext, store| { + async move { + ext.call_llm_provider_models(store, &provider_id) + .await + } + .boxed() + } + }) + .await; + + let models: Vec = match models_result { + Ok(Ok(Ok(models))) => models, + Ok(Ok(Err(e))) => { + log::error!( + "Failed to get models for LLM provider {} in extension {}: {}", + provider_info.id, + extension.manifest.id, + e + ); + Vec::new() + } + Ok(Err(e)) => { + log::error!( + "Wasm error calling llm_provider_models for {} in extension {}: {:?}", + provider_info.id, + extension.manifest.id, + e + ); + Vec::new() + } + Err(e) => { + log::error!( + "Extension call failed for llm_provider_models {} in extension {}: {:?}", + provider_info.id, + extension.manifest.id, + e + ); + Vec::new() + } + }; + + llm_providers_with_models.push((provider_info, models)); + } + } + } + + wasm_extensions.push(( + extension.manifest.clone(), + wasm_extension, + llm_providers_with_models, + )) } Err(e) => { log::error!( @@ -1392,7 +1466,7 @@ impl ExtensionStore { this.update(cx, |this, cx| { this.reload_complete_senders.clear(); - for (manifest, wasm_extension) in &wasm_extensions { + for (manifest, wasm_extension, llm_providers_with_models) in &wasm_extensions { let extension = Arc::new(wasm_extension.clone()); for (language_server_id, language_server_config) in &manifest.language_servers { @@ -1446,9 +1520,38 @@ impl ExtensionStore { this.proxy .register_debug_locator(extension.clone(), debug_adapter.clone()); } + + // Register LLM providers + for (provider_info, models) in llm_providers_with_models { + let provider_id: Arc = + format!("{}:{}", manifest.id, provider_info.id).into(); + let wasm_ext = wasm_extension.clone(); + let pinfo = provider_info.clone(); + let mods = models.clone(); + + this.proxy.register_language_model_provider( + provider_id, + Box::new(move |cx: &mut App| { + let provider = Arc::new(ExtensionLanguageModelProvider::new( + wasm_ext, pinfo, mods, cx, + )); + language_model::LanguageModelRegistry::global(cx).update( + cx, + |registry, cx| { + registry.register_provider(provider, cx); + }, + ); + }), + cx, + ); + } } - this.wasm_extensions.extend(wasm_extensions); + let wasm_extensions_without_llm: Vec<_> = wasm_extensions + .into_iter() + .map(|(manifest, ext, _)| (manifest, ext)) + .collect(); + this.wasm_extensions.extend(wasm_extensions_without_llm); this.proxy.set_extensions_loaded(); this.proxy.reload_current_theme(cx); this.proxy.reload_current_icon_theme(cx); diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 85a3a720ce8c62..b3275ff52ff7fe 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -165,6 +165,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { capabilities: Vec::new(), debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: BTreeMap::default(), }), dev: false, }, @@ -196,6 +197,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { capabilities: Vec::new(), debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: BTreeMap::default(), }), dev: false, }, @@ -376,6 +378,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { capabilities: Vec::new(), debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: BTreeMap::default(), }), dev: false, }, diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index cecaf2039bc6dc..cd0b99cc02499b 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -1,3 +1,4 @@ +pub mod llm_provider; pub mod wit; use crate::capability_granter::CapabilityGranter; diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs new file mode 100644 index 00000000000000..7e98e0b400ef4d --- /dev/null +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -0,0 +1,624 @@ +use crate::wasm_host::WasmExtension; + +use crate::wasm_host::wit::{ + LlmCompletionEvent, LlmCompletionRequest, LlmImageData, LlmMessageContent, LlmMessageRole, + LlmModelInfo, LlmProviderInfo, LlmRequestMessage, LlmStopReason, LlmThinkingContent, + LlmToolChoice, LlmToolDefinition, LlmToolInputFormat, LlmToolResult, LlmToolResultContent, + LlmToolUse, +}; +use anyhow::{Result, anyhow}; +use futures::future::BoxFuture; +use futures::stream::BoxStream; +use futures::{FutureExt, StreamExt}; +use gpui::{AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, Window}; +use language_model::tool_schema::LanguageModelToolSchemaFormat; +use language_model::{ + AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, + LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, + LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, + LanguageModelToolChoice, LanguageModelToolUse, LanguageModelToolUseId, StopReason, TokenUsage, +}; +use std::sync::Arc; + +/// An extension-based language model provider. +pub struct ExtensionLanguageModelProvider { + pub extension: WasmExtension, + pub provider_info: LlmProviderInfo, + state: Entity, +} + +pub struct ExtensionLlmProviderState { + is_authenticated: bool, + available_models: Vec, +} + +impl EventEmitter<()> for ExtensionLlmProviderState {} + +impl ExtensionLanguageModelProvider { + pub fn new( + extension: WasmExtension, + provider_info: LlmProviderInfo, + models: Vec, + cx: &mut App, + ) -> Self { + let state = cx.new(|_| ExtensionLlmProviderState { + is_authenticated: false, + available_models: models, + }); + + Self { + extension, + provider_info, + state, + } + } + + fn provider_id_string(&self) -> String { + format!("{}:{}", self.extension.manifest.id, self.provider_info.id) + } +} + +impl LanguageModelProvider for ExtensionLanguageModelProvider { + fn id(&self) -> LanguageModelProviderId { + LanguageModelProviderId::from(self.provider_id_string()) + } + + fn name(&self) -> LanguageModelProviderName { + LanguageModelProviderName::from(self.provider_info.name.clone()) + } + + fn icon(&self) -> ui::IconName { + ui::IconName::ZedAssistant + } + + fn default_model(&self, cx: &App) -> Option> { + let state = self.state.read(cx); + state + .available_models + .iter() + .find(|m| m.is_default) + .or_else(|| state.available_models.first()) + .map(|model_info| { + Arc::new(ExtensionLanguageModel { + extension: self.extension.clone(), + model_info: model_info.clone(), + provider_id: self.id(), + provider_name: self.name(), + provider_info: self.provider_info.clone(), + }) as Arc + }) + } + + fn default_fast_model(&self, cx: &App) -> Option> { + let state = self.state.read(cx); + state + .available_models + .iter() + .find(|m| m.is_default_fast) + .or_else(|| state.available_models.iter().find(|m| m.is_default)) + .or_else(|| state.available_models.first()) + .map(|model_info| { + Arc::new(ExtensionLanguageModel { + extension: self.extension.clone(), + model_info: model_info.clone(), + provider_id: self.id(), + provider_name: self.name(), + provider_info: self.provider_info.clone(), + }) as Arc + }) + } + + fn provided_models(&self, cx: &App) -> Vec> { + let state = self.state.read(cx); + state + .available_models + .iter() + .map(|model_info| { + Arc::new(ExtensionLanguageModel { + extension: self.extension.clone(), + model_info: model_info.clone(), + provider_id: self.id(), + provider_name: self.name(), + provider_info: self.provider_info.clone(), + }) as Arc + }) + .collect() + } + + fn is_authenticated(&self, cx: &App) -> bool { + self.state.read(cx).is_authenticated + } + + fn authenticate(&self, cx: &mut App) -> Task> { + let extension = self.extension.clone(); + let provider_id = self.provider_info.id.clone(); + let state = self.state.clone(); + + cx.spawn(async move |cx| { + let result = extension + .call(|extension, store| { + async move { + extension + .call_llm_provider_authenticate(store, &provider_id) + .await + } + .boxed() + }) + .await; + + match result { + Ok(Ok(Ok(()))) => { + cx.update(|cx| { + state.update(cx, |state, _| { + state.is_authenticated = true; + }); + })?; + Ok(()) + } + Ok(Ok(Err(e))) => Err(AuthenticateError::Other(anyhow!("{}", e))), + Ok(Err(e)) => Err(AuthenticateError::Other(e)), + Err(e) => Err(AuthenticateError::Other(e)), + } + }) + } + + fn configuration_view( + &self, + _target_agent: ConfigurationViewTargetAgent, + _window: &mut Window, + cx: &mut App, + ) -> AnyView { + cx.new(|_| EmptyConfigView).into() + } + + fn reset_credentials(&self, cx: &mut App) -> Task> { + let extension = self.extension.clone(); + let provider_id = self.provider_info.id.clone(); + let state = self.state.clone(); + + cx.spawn(async move |cx| { + let result = extension + .call(|extension, store| { + async move { + extension + .call_llm_provider_reset_credentials(store, &provider_id) + .await + } + .boxed() + }) + .await; + + match result { + Ok(Ok(Ok(()))) => { + cx.update(|cx| { + state.update(cx, |state, _| { + state.is_authenticated = false; + }); + })?; + Ok(()) + } + Ok(Ok(Err(e))) => Err(anyhow!("{}", e)), + Ok(Err(e)) => Err(e), + Err(e) => Err(e), + } + }) + } +} + +impl LanguageModelProviderState for ExtensionLanguageModelProvider { + type ObservableEntity = ExtensionLlmProviderState; + + fn observable_entity(&self) -> Option> { + Some(self.state.clone()) + } + + fn subscribe( + &self, + cx: &mut Context, + callback: impl Fn(&mut T, &mut Context) + 'static, + ) -> Option { + Some(cx.subscribe(&self.state, move |this, _, _, cx| callback(this, cx))) + } +} + +struct EmptyConfigView; + +impl gpui::Render for EmptyConfigView { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + gpui::Empty + } +} + +/// An extension-based language model. +pub struct ExtensionLanguageModel { + extension: WasmExtension, + model_info: LlmModelInfo, + provider_id: LanguageModelProviderId, + provider_name: LanguageModelProviderName, + provider_info: LlmProviderInfo, +} + +impl LanguageModel for ExtensionLanguageModel { + fn id(&self) -> LanguageModelId { + LanguageModelId::from(format!("{}:{}", self.provider_id.0, self.model_info.id)) + } + + fn name(&self) -> LanguageModelName { + LanguageModelName::from(self.model_info.name.clone()) + } + + fn provider_id(&self) -> LanguageModelProviderId { + self.provider_id.clone() + } + + fn provider_name(&self) -> LanguageModelProviderName { + self.provider_name.clone() + } + + fn telemetry_id(&self) -> String { + format!("extension:{}", self.model_info.id) + } + + fn supports_images(&self) -> bool { + self.model_info.capabilities.supports_images + } + + fn supports_tools(&self) -> bool { + self.model_info.capabilities.supports_tools + } + + fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { + match choice { + LanguageModelToolChoice::Auto => self.model_info.capabilities.supports_tool_choice_auto, + LanguageModelToolChoice::Any => self.model_info.capabilities.supports_tool_choice_any, + LanguageModelToolChoice::None => self.model_info.capabilities.supports_tool_choice_none, + } + } + + fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { + match self.model_info.capabilities.tool_input_format { + LlmToolInputFormat::JsonSchema => LanguageModelToolSchemaFormat::JsonSchema, + LlmToolInputFormat::Simplified => LanguageModelToolSchemaFormat::JsonSchema, + } + } + + fn max_token_count(&self) -> u64 { + self.model_info.max_token_count + } + + fn max_output_tokens(&self) -> Option { + self.model_info.max_output_tokens + } + + fn count_tokens( + &self, + request: LanguageModelRequest, + _cx: &App, + ) -> BoxFuture<'static, Result> { + let extension = self.extension.clone(); + let provider_id = self.provider_info.id.clone(); + let model_id = self.model_info.id.clone(); + + async move { + let wit_request = convert_request_to_wit(&request); + + let result = extension + .call(|ext, store| { + async move { + ext.call_llm_count_tokens(store, &provider_id, &model_id, &wit_request) + .await + } + .boxed() + }) + .await?; + + match result { + Ok(Ok(count)) => Ok(count), + Ok(Err(e)) => Err(anyhow!("{}", e)), + Err(e) => Err(e), + } + } + .boxed() + } + + fn stream_completion( + &self, + request: LanguageModelRequest, + _cx: &AsyncApp, + ) -> BoxFuture< + 'static, + Result< + BoxStream<'static, Result>, + LanguageModelCompletionError, + >, + > { + let extension = self.extension.clone(); + let provider_id = self.provider_info.id.clone(); + let model_id = self.model_info.id.clone(); + + async move { + let wit_request = convert_request_to_wit(&request); + + // Start the stream and get a stream ID + let outer_result = extension + .call(|ext, store| { + async move { + ext.call_llm_stream_completion_start( + store, + &provider_id, + &model_id, + &wit_request, + ) + .await + } + .boxed() + }) + .await + .map_err(|e| LanguageModelCompletionError::Other(e))?; + + // Unwrap the inner Result> + let inner_result = + outer_result.map_err(|e| LanguageModelCompletionError::Other(anyhow!("{}", e)))?; + + // Get the stream ID + let stream_id = + inner_result.map_err(|e| LanguageModelCompletionError::Other(anyhow!("{}", e)))?; + + // Create a stream that polls for events + let stream = futures::stream::unfold( + (extension, stream_id, false), + |(ext, stream_id, done)| async move { + if done { + return None; + } + + let result = ext + .call({ + let stream_id = stream_id.clone(); + move |ext, store| { + async move { + ext.call_llm_stream_completion_next(store, &stream_id).await + } + .boxed() + } + }) + .await; + + match result { + Ok(Ok(Ok(Some(event)))) => { + let converted = convert_completion_event(event); + Some((Ok(converted), (ext, stream_id, false))) + } + Ok(Ok(Ok(None))) => { + // Stream complete - close it + let _ = ext + .call({ + let stream_id = stream_id.clone(); + move |ext, store| { + async move { + ext.call_llm_stream_completion_close(store, &stream_id) + .await + } + .boxed() + } + }) + .await; + None + } + Ok(Ok(Err(e))) => { + // Extension returned an error - close stream and return error + let _ = ext + .call({ + let stream_id = stream_id.clone(); + move |ext, store| { + async move { + ext.call_llm_stream_completion_close(store, &stream_id) + .await + } + .boxed() + } + }) + .await; + Some(( + Err(LanguageModelCompletionError::Other(anyhow!("{}", e))), + (ext, stream_id, true), + )) + } + Ok(Err(e)) => { + // WASM call error - close stream and return error + let _ = ext + .call({ + let stream_id = stream_id.clone(); + move |ext, store| { + async move { + ext.call_llm_stream_completion_close(store, &stream_id) + .await + } + .boxed() + } + }) + .await; + Some(( + Err(LanguageModelCompletionError::Other(e)), + (ext, stream_id, true), + )) + } + Err(e) => { + // Channel error - close stream and return error + let _ = ext + .call({ + let stream_id = stream_id.clone(); + move |ext, store| { + async move { + ext.call_llm_stream_completion_close(store, &stream_id) + .await + } + .boxed() + } + }) + .await; + Some(( + Err(LanguageModelCompletionError::Other(e)), + (ext, stream_id, true), + )) + } + } + }, + ); + + Ok(stream.boxed()) + } + .boxed() + } + + fn cache_configuration(&self) -> Option { + None + } +} + +fn convert_request_to_wit(request: &LanguageModelRequest) -> LlmCompletionRequest { + let messages = request + .messages + .iter() + .map(|msg| LlmRequestMessage { + role: match msg.role { + language_model::Role::User => LlmMessageRole::User, + language_model::Role::Assistant => LlmMessageRole::Assistant, + language_model::Role::System => LlmMessageRole::System, + }, + content: msg + .content + .iter() + .map(|content| match content { + language_model::MessageContent::Text(text) => { + LlmMessageContent::Text(text.clone()) + } + language_model::MessageContent::Image(image) => { + LlmMessageContent::Image(LlmImageData { + source: image.source.to_string(), + width: Some(image.size.width.0 as u32), + height: Some(image.size.height.0 as u32), + }) + } + language_model::MessageContent::ToolUse(tool_use) => { + LlmMessageContent::ToolUse(LlmToolUse { + id: tool_use.id.to_string(), + name: tool_use.name.to_string(), + input: tool_use.raw_input.clone(), + thought_signature: tool_use.thought_signature.clone(), + }) + } + language_model::MessageContent::ToolResult(result) => { + LlmMessageContent::ToolResult(LlmToolResult { + tool_use_id: result.tool_use_id.to_string(), + tool_name: result.tool_name.to_string(), + is_error: result.is_error, + content: match &result.content { + language_model::LanguageModelToolResultContent::Text(t) => { + LlmToolResultContent::Text(t.to_string()) + } + language_model::LanguageModelToolResultContent::Image(img) => { + LlmToolResultContent::Image(LlmImageData { + source: img.source.to_string(), + width: Some(img.size.width.0 as u32), + height: Some(img.size.height.0 as u32), + }) + } + }, + }) + } + language_model::MessageContent::Thinking { text, signature } => { + LlmMessageContent::Thinking(LlmThinkingContent { + text: text.clone(), + signature: signature.clone(), + }) + } + language_model::MessageContent::RedactedThinking(data) => { + LlmMessageContent::RedactedThinking(data.clone()) + } + }) + .collect(), + cache: msg.cache, + }) + .collect(); + + let tools = request + .tools + .iter() + .map(|tool| LlmToolDefinition { + name: tool.name.clone(), + description: tool.description.clone(), + input_schema: serde_json::to_string(&tool.input_schema).unwrap_or_default(), + }) + .collect(); + + let tool_choice = request.tool_choice.as_ref().map(|choice| match choice { + LanguageModelToolChoice::Auto => LlmToolChoice::Auto, + LanguageModelToolChoice::Any => LlmToolChoice::Any, + LanguageModelToolChoice::None => LlmToolChoice::None, + }); + + LlmCompletionRequest { + messages, + tools, + tool_choice, + stop_sequences: request.stop.clone(), + temperature: request.temperature, + thinking_allowed: request.thinking_allowed, + max_tokens: None, + } +} + +fn convert_completion_event(event: LlmCompletionEvent) -> LanguageModelCompletionEvent { + match event { + LlmCompletionEvent::Started => LanguageModelCompletionEvent::Started, + LlmCompletionEvent::Text(text) => LanguageModelCompletionEvent::Text(text), + LlmCompletionEvent::Thinking(thinking) => LanguageModelCompletionEvent::Thinking { + text: thinking.text, + signature: thinking.signature, + }, + LlmCompletionEvent::RedactedThinking(data) => { + LanguageModelCompletionEvent::RedactedThinking { data } + } + LlmCompletionEvent::ToolUse(tool_use) => { + LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { + id: LanguageModelToolUseId::from(tool_use.id), + name: tool_use.name.into(), + raw_input: tool_use.input.clone(), + input: serde_json::from_str(&tool_use.input).unwrap_or(serde_json::Value::Null), + is_input_complete: true, + thought_signature: tool_use.thought_signature, + }) + } + LlmCompletionEvent::ToolUseJsonParseError(error) => { + LanguageModelCompletionEvent::ToolUseJsonParseError { + id: LanguageModelToolUseId::from(error.id), + tool_name: error.tool_name.into(), + raw_input: error.raw_input.into(), + json_parse_error: error.error, + } + } + LlmCompletionEvent::Stop(reason) => LanguageModelCompletionEvent::Stop(match reason { + LlmStopReason::EndTurn => StopReason::EndTurn, + LlmStopReason::MaxTokens => StopReason::MaxTokens, + LlmStopReason::ToolUse => StopReason::ToolUse, + LlmStopReason::Refusal => StopReason::Refusal, + }), + LlmCompletionEvent::Usage(usage) => LanguageModelCompletionEvent::UsageUpdate(TokenUsage { + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0), + cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0), + }), + LlmCompletionEvent::ReasoningDetails(json) => { + LanguageModelCompletionEvent::ReasoningDetails( + serde_json::from_str(&json).unwrap_or(serde_json::Value::Null), + ) + } + } +} diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 4c88af1b0a0234..a18ad1a10803bb 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -7,6 +7,7 @@ mod since_v0_3_0; mod since_v0_4_0; mod since_v0_5_0; mod since_v0_6_0; +mod since_v0_7_0; use dap::DebugRequest; use extension::{DebugTaskDefinition, KeyValueStoreDelegate, WorktreeDelegate}; use gpui::BackgroundExecutor; @@ -15,12 +16,12 @@ use lsp::LanguageServerName; use release_channel::ReleaseChannel; use task::{DebugScenario, SpawnInTerminal, TaskTemplate, ZedDebugConfig}; -use crate::wasm_host::wit::since_v0_6_0::dap::StartDebuggingRequestArgumentsRequest; +use crate::wasm_host::wit::since_v0_7_0::dap::StartDebuggingRequestArgumentsRequest; use super::{WasmState, wasm_engine}; use anyhow::{Context as _, Result, anyhow}; use semver::Version; -use since_v0_6_0 as latest; +use since_v0_7_0 as latest; use std::{ops::RangeInclusive, path::PathBuf, sync::Arc}; use wasmtime::{ Store, @@ -32,6 +33,19 @@ pub use latest::CodeLabelSpanLiteral; pub use latest::{ CodeLabel, CodeLabelSpan, Command, DebugAdapterBinary, ExtensionProject, Range, SlashCommand, zed::extension::context_server::ContextServerConfiguration, + zed::extension::llm_provider::{ + CacheConfiguration as LlmCacheConfiguration, CompletionEvent as LlmCompletionEvent, + CompletionRequest as LlmCompletionRequest, CredentialType as LlmCredentialType, + ImageData as LlmImageData, MessageContent as LlmMessageContent, + MessageRole as LlmMessageRole, ModelCapabilities as LlmModelCapabilities, + ModelInfo as LlmModelInfo, ProviderInfo as LlmProviderInfo, + RequestMessage as LlmRequestMessage, StopReason as LlmStopReason, + ThinkingContent as LlmThinkingContent, TokenUsage as LlmTokenUsage, + ToolChoice as LlmToolChoice, ToolDefinition as LlmToolDefinition, + ToolInputFormat as LlmToolInputFormat, ToolResult as LlmToolResult, + ToolResultContent as LlmToolResultContent, ToolUse as LlmToolUse, + ToolUseJsonParseError as LlmToolUseJsonParseError, + }, zed::extension::lsp::{ Completion, CompletionKind, CompletionLabelDetails, InsertTextFormat, Symbol, SymbolKind, }, @@ -95,6 +109,7 @@ pub fn authorize_access_to_unreleased_wasm_api_version( } pub enum Extension { + V0_7_0(since_v0_7_0::Extension), V0_6_0(since_v0_6_0::Extension), V0_5_0(since_v0_5_0::Extension), V0_4_0(since_v0_4_0::Extension), @@ -122,6 +137,15 @@ impl Extension { latest::Extension::instantiate_async(store, component, latest::linker(executor)) .await .context("failed to instantiate wasm extension")?; + Ok(Self::V0_7_0(extension)) + } else if version >= since_v0_6_0::MIN_VERSION { + let extension = since_v0_6_0::Extension::instantiate_async( + store, + component, + since_v0_6_0::linker(executor), + ) + .await + .context("failed to instantiate wasm extension")?; Ok(Self::V0_6_0(extension)) } else if version >= since_v0_5_0::MIN_VERSION { let extension = since_v0_5_0::Extension::instantiate_async( @@ -200,6 +224,7 @@ impl Extension { pub async fn call_init_extension(&self, store: &mut Store) -> Result<()> { match self { + Extension::V0_7_0(ext) => ext.call_init_extension(store).await, Extension::V0_6_0(ext) => ext.call_init_extension(store).await, Extension::V0_5_0(ext) => ext.call_init_extension(store).await, Extension::V0_4_0(ext) => ext.call_init_extension(store).await, @@ -220,6 +245,10 @@ impl Extension { resource: Resource>, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + ext.call_language_server_command(store, &language_server_id.0, resource) + .await + } Extension::V0_6_0(ext) => { ext.call_language_server_command(store, &language_server_id.0, resource) .await @@ -282,6 +311,14 @@ impl Extension { resource: Resource>, ) -> Result, String>> { match self { + Extension::V0_7_0(ext) => { + ext.call_language_server_initialization_options( + store, + &language_server_id.0, + resource, + ) + .await + } Extension::V0_6_0(ext) => { ext.call_language_server_initialization_options( store, @@ -371,6 +408,14 @@ impl Extension { resource: Resource>, ) -> Result, String>> { match self { + Extension::V0_7_0(ext) => { + ext.call_language_server_workspace_configuration( + store, + &language_server_id.0, + resource, + ) + .await + } Extension::V0_6_0(ext) => { ext.call_language_server_workspace_configuration( store, @@ -439,6 +484,15 @@ impl Extension { resource: Resource>, ) -> Result, String>> { match self { + Extension::V0_7_0(ext) => { + ext.call_language_server_additional_initialization_options( + store, + &language_server_id.0, + &target_language_server_id.0, + resource, + ) + .await + } Extension::V0_6_0(ext) => { ext.call_language_server_additional_initialization_options( store, @@ -483,6 +537,15 @@ impl Extension { resource: Resource>, ) -> Result, String>> { match self { + Extension::V0_7_0(ext) => { + ext.call_language_server_additional_workspace_configuration( + store, + &language_server_id.0, + &target_language_server_id.0, + resource, + ) + .await + } Extension::V0_6_0(ext) => { ext.call_language_server_additional_workspace_configuration( store, @@ -526,10 +589,23 @@ impl Extension { completions: Vec, ) -> Result>, String>> { match self { - Extension::V0_6_0(ext) => { + Extension::V0_7_0(ext) => { ext.call_labels_for_completions(store, &language_server_id.0, &completions) .await } + Extension::V0_6_0(ext) => Ok(ext + .call_labels_for_completions( + store, + &language_server_id.0, + &completions.into_iter().collect::>(), + ) + .await? + .map(|labels| { + labels + .into_iter() + .map(|label| label.map(Into::into)) + .collect() + })), Extension::V0_5_0(ext) => Ok(ext .call_labels_for_completions( store, @@ -619,10 +695,23 @@ impl Extension { symbols: Vec, ) -> Result>, String>> { match self { - Extension::V0_6_0(ext) => { + Extension::V0_7_0(ext) => { ext.call_labels_for_symbols(store, &language_server_id.0, &symbols) .await } + Extension::V0_6_0(ext) => Ok(ext + .call_labels_for_symbols( + store, + &language_server_id.0, + &symbols.into_iter().collect::>(), + ) + .await? + .map(|labels| { + labels + .into_iter() + .map(|label| label.map(Into::into)) + .collect() + })), Extension::V0_5_0(ext) => Ok(ext .call_labels_for_symbols( store, @@ -712,6 +801,10 @@ impl Extension { arguments: &[String], ) -> Result, String>> { match self { + Extension::V0_7_0(ext) => { + ext.call_complete_slash_command_argument(store, command, arguments) + .await + } Extension::V0_6_0(ext) => { ext.call_complete_slash_command_argument(store, command, arguments) .await @@ -750,6 +843,10 @@ impl Extension { resource: Option>>, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + ext.call_run_slash_command(store, command, arguments, resource) + .await + } Extension::V0_6_0(ext) => { ext.call_run_slash_command(store, command, arguments, resource) .await @@ -787,6 +884,10 @@ impl Extension { project: Resource, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + ext.call_context_server_command(store, &context_server_id, project) + .await + } Extension::V0_6_0(ext) => { ext.call_context_server_command(store, &context_server_id, project) .await @@ -823,6 +924,10 @@ impl Extension { project: Resource, ) -> Result, String>> { match self { + Extension::V0_7_0(ext) => { + ext.call_context_server_configuration(store, &context_server_id, project) + .await + } Extension::V0_6_0(ext) => { ext.call_context_server_configuration(store, &context_server_id, project) .await @@ -849,6 +954,7 @@ impl Extension { provider: &str, ) -> Result, String>> { match self { + Extension::V0_7_0(ext) => ext.call_suggest_docs_packages(store, provider).await, Extension::V0_6_0(ext) => ext.call_suggest_docs_packages(store, provider).await, Extension::V0_5_0(ext) => ext.call_suggest_docs_packages(store, provider).await, Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await, @@ -869,6 +975,10 @@ impl Extension { kv_store: Resource>, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + ext.call_index_docs(store, provider, package_name, kv_store) + .await + } Extension::V0_6_0(ext) => { ext.call_index_docs(store, provider, package_name, kv_store) .await @@ -907,6 +1017,20 @@ impl Extension { resource: Resource>, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + let dap_binary = ext + .call_get_dap_binary( + store, + &adapter_name, + &task.try_into()?, + user_installed_path.as_ref().and_then(|p| p.to_str()), + resource, + ) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + + Ok(Ok(dap_binary)) + } Extension::V0_6_0(ext) => { let dap_binary = ext .call_get_dap_binary( @@ -931,6 +1055,16 @@ impl Extension { config: serde_json::Value, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + let config = + serde_json::to_string(&config).context("Adapter config is not a valid JSON")?; + let dap_binary = ext + .call_dap_request_kind(store, &adapter_name, &config) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + + Ok(Ok(dap_binary)) + } Extension::V0_6_0(ext) => { let config = serde_json::to_string(&config).context("Adapter config is not a valid JSON")?; @@ -950,6 +1084,15 @@ impl Extension { config: ZedDebugConfig, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + let config = config.into(); + let dap_binary = ext + .call_dap_config_to_scenario(store, &config) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + + Ok(Ok(dap_binary.try_into()?)) + } Extension::V0_6_0(ext) => { let config = config.into(); let dap_binary = ext @@ -971,6 +1114,20 @@ impl Extension { debug_adapter_name: String, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + let build_config_template = build_config_template.into(); + let dap_binary = ext + .call_dap_locator_create_scenario( + store, + &locator_name, + &build_config_template, + &resolved_label, + &debug_adapter_name, + ) + .await?; + + Ok(dap_binary.map(TryInto::try_into).transpose()?) + } Extension::V0_6_0(ext) => { let build_config_template = build_config_template.into(); let dap_binary = ext @@ -995,6 +1152,15 @@ impl Extension { resolved_build_task: SpawnInTerminal, ) -> Result> { match self { + Extension::V0_7_0(ext) => { + let build_config_template = resolved_build_task.try_into()?; + let dap_request = ext + .call_run_dap_locator(store, &locator_name, &build_config_template) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + + Ok(Ok(dap_request.into())) + } Extension::V0_6_0(ext) => { let build_config_template = resolved_build_task.try_into()?; let dap_request = ext @@ -1007,6 +1173,139 @@ impl Extension { _ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"), } } + + // ========================================================================= + // LLM Provider Methods (v0.7.0+) + // ========================================================================= + + pub async fn call_llm_providers( + &self, + store: &mut Store, + ) -> Result> { + match self { + Extension::V0_7_0(ext) => ext.call_llm_providers(store).await, + _ => Ok(Vec::new()), + } + } + + pub async fn call_llm_provider_models( + &self, + store: &mut Store, + provider_id: &str, + ) -> Result, String>> { + match self { + Extension::V0_7_0(ext) => ext.call_llm_provider_models(store, provider_id).await, + _ => anyhow::bail!("`llm_provider_models` not available prior to v0.7.0"), + } + } + + pub async fn call_llm_provider_is_authenticated( + &self, + store: &mut Store, + provider_id: &str, + ) -> Result { + match self { + Extension::V0_7_0(ext) => { + ext.call_llm_provider_is_authenticated(store, provider_id) + .await + } + _ => Ok(false), + } + } + + pub async fn call_llm_provider_authenticate( + &self, + store: &mut Store, + provider_id: &str, + ) -> Result> { + match self { + Extension::V0_7_0(ext) => ext.call_llm_provider_authenticate(store, provider_id).await, + _ => anyhow::bail!("`llm_provider_authenticate` not available prior to v0.7.0"), + } + } + + pub async fn call_llm_provider_reset_credentials( + &self, + store: &mut Store, + provider_id: &str, + ) -> Result> { + match self { + Extension::V0_7_0(ext) => { + ext.call_llm_provider_reset_credentials(store, provider_id) + .await + } + _ => anyhow::bail!("`llm_provider_reset_credentials` not available prior to v0.7.0"), + } + } + + pub async fn call_llm_count_tokens( + &self, + store: &mut Store, + provider_id: &str, + model_id: &str, + request: &latest::llm_provider::CompletionRequest, + ) -> Result> { + match self { + Extension::V0_7_0(ext) => { + ext.call_llm_count_tokens(store, provider_id, model_id, request) + .await + } + _ => anyhow::bail!("`llm_count_tokens` not available prior to v0.7.0"), + } + } + + pub async fn call_llm_stream_completion_start( + &self, + store: &mut Store, + provider_id: &str, + model_id: &str, + request: &latest::llm_provider::CompletionRequest, + ) -> Result> { + match self { + Extension::V0_7_0(ext) => { + ext.call_llm_stream_completion_start(store, provider_id, model_id, request) + .await + } + _ => anyhow::bail!("`llm_stream_completion_start` not available prior to v0.7.0"), + } + } + + pub async fn call_llm_stream_completion_next( + &self, + store: &mut Store, + stream_id: &str, + ) -> Result, String>> { + match self { + Extension::V0_7_0(ext) => ext.call_llm_stream_completion_next(store, stream_id).await, + _ => anyhow::bail!("`llm_stream_completion_next` not available prior to v0.7.0"), + } + } + + pub async fn call_llm_stream_completion_close( + &self, + store: &mut Store, + stream_id: &str, + ) -> Result<()> { + match self { + Extension::V0_7_0(ext) => ext.call_llm_stream_completion_close(store, stream_id).await, + _ => anyhow::bail!("`llm_stream_completion_close` not available prior to v0.7.0"), + } + } + + pub async fn call_llm_cache_configuration( + &self, + store: &mut Store, + provider_id: &str, + model_id: &str, + ) -> Result> { + match self { + Extension::V0_7_0(ext) => { + ext.call_llm_cache_configuration(store, provider_id, model_id) + .await + } + _ => Ok(None), + } + } } trait ToWasmtimeResult { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index c96e5216c4703d..4df9174362749c 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -1,42 +1,15 @@ -use crate::wasm_host::wit::since_v0_6_0::{ - dap::{ - AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest, - StartDebuggingRequestArguments, TcpArguments, TcpArgumentsTemplate, - }, - slash_command::SlashCommandOutputSection, -}; -use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind}; -use crate::wasm_host::{WasmState, wit::ToWasmtimeResult}; -use ::http_client::{AsyncBody, HttpRequestExt}; -use ::settings::{Settings, WorktreeId}; -use anyhow::{Context as _, Result, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use async_trait::async_trait; -use extension::{ - ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, -}; -use futures::{AsyncReadExt, lock::Mutex}; -use futures::{FutureExt as _, io::BufReader}; -use gpui::{BackgroundExecutor, SharedString}; -use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings}; -use project::project_settings::ProjectSettings; +use crate::wasm_host::WasmState; +use anyhow::Result; +use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; +use gpui::BackgroundExecutor; use semver::Version; -use std::{ - env, - net::Ipv4Addr, - path::{Path, PathBuf}, - str::FromStr, - sync::{Arc, OnceLock}, -}; -use task::{SpawnInTerminal, ZedDebugConfig}; -use url::Url; -use util::{ - archive::extract_zip, fs::make_file_executable, maybe, paths::PathStyle, rel_path::RelPath, -}; +use std::sync::{Arc, OnceLock}; use wasmtime::component::{Linker, Resource}; +use super::latest; + pub const MIN_VERSION: Version = Version::new(0, 6, 0); +#[allow(dead_code)] pub const MAX_VERSION: Version = Version::new(0, 7, 0); wasmtime::component::bindgen!({ @@ -44,15 +17,22 @@ wasmtime::component::bindgen!({ trappable_imports: true, path: "../extension_api/wit/since_v0.6.0", with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream + "worktree": ExtensionWorktree, + "project": ExtensionProject, + "key-value-store": ExtensionKeyValueStore, + "zed:extension/common": latest::zed::extension::common, + "zed:extension/github": latest::zed::extension::github, + "zed:extension/http-client": latest::zed::extension::http_client, + "zed:extension/lsp": latest::zed::extension::lsp, + "zed:extension/nodejs": latest::zed::extension::nodejs, + "zed:extension/platform": latest::zed::extension::platform, + "zed:extension/process": latest::zed::extension::process, + "zed:extension/slash-command": latest::zed::extension::slash_command, + "zed:extension/context-server": latest::zed::extension::context_server, + "zed:extension/dap": latest::zed::extension::dap, }, }); -pub use self::zed::extension::*; - mod settings { #![allow(dead_code)] include!(concat!(env!("OUT_DIR"), "/since_v0.6.0/settings.rs")); @@ -61,289 +41,32 @@ mod settings { pub type ExtensionWorktree = Arc; pub type ExtensionProject = Arc; pub type ExtensionKeyValueStore = Arc; -pub type ExtensionHttpResponseStream = Arc>>; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) } -impl From for std::ops::Range { - fn from(range: Range) -> Self { - let start = range.start as usize; - let end = range.end as usize; - start..end - } -} - -impl From for extension::Command { - fn from(value: Command) -> Self { - Self { - command: value.command.into(), - args: value.args, - env: value.env, - } - } -} - -impl From - for extension::StartDebuggingRequestArgumentsRequest -{ - fn from(value: StartDebuggingRequestArgumentsRequest) -> Self { - match value { - StartDebuggingRequestArgumentsRequest::Launch => Self::Launch, - StartDebuggingRequestArgumentsRequest::Attach => Self::Attach, - } - } -} -impl TryFrom for extension::StartDebuggingRequestArguments { - type Error = anyhow::Error; - - fn try_from(value: StartDebuggingRequestArguments) -> Result { - Ok(Self { - configuration: serde_json::from_str(&value.configuration)?, - request: value.request.into(), - }) - } -} -impl From for extension::TcpArguments { - fn from(value: TcpArguments) -> Self { - Self { - host: value.host.into(), - port: value.port, - timeout: value.timeout, - } - } -} - -impl From for TcpArgumentsTemplate { - fn from(value: extension::TcpArgumentsTemplate) -> Self { - Self { - host: value.host.map(Ipv4Addr::to_bits), - port: value.port, - timeout: value.timeout, - } - } -} - -impl From for extension::TcpArgumentsTemplate { - fn from(value: TcpArgumentsTemplate) -> Self { - Self { - host: value.host.map(Ipv4Addr::from_bits), - port: value.port, - timeout: value.timeout, - } - } -} - -impl TryFrom for DebugTaskDefinition { - type Error = anyhow::Error; - fn try_from(value: extension::DebugTaskDefinition) -> Result { - Ok(Self { - label: value.label.to_string(), - adapter: value.adapter.to_string(), - config: value.config.to_string(), - tcp_connection: value.tcp_connection.map(Into::into), - }) - } -} - -impl From for DebugRequest { - fn from(value: task::DebugRequest) -> Self { - match value { - task::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), - task::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), - } - } -} - -impl From for task::DebugRequest { - fn from(value: DebugRequest) -> Self { - match value { - DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), - DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), - } - } -} - -impl From for LaunchRequest { - fn from(value: task::LaunchRequest) -> Self { - Self { - program: value.program, - cwd: value.cwd.map(|p| p.to_string_lossy().into_owned()), - args: value.args, - envs: value.env.into_iter().collect(), - } - } -} - -impl From for AttachRequest { - fn from(value: task::AttachRequest) -> Self { - Self { - process_id: value.process_id, - } - } -} - -impl From for task::LaunchRequest { - fn from(value: LaunchRequest) -> Self { - Self { - program: value.program, - cwd: value.cwd.map(|p| p.into()), - args: value.args, - env: value.envs.into_iter().collect(), - } - } -} -impl From for task::AttachRequest { - fn from(value: AttachRequest) -> Self { - Self { - process_id: value.process_id, - } - } -} - -impl From for DebugConfig { - fn from(value: ZedDebugConfig) -> Self { - Self { - label: value.label.into(), - adapter: value.adapter.into(), - request: value.request.into(), - stop_on_entry: value.stop_on_entry, - } - } -} -impl TryFrom for extension::DebugAdapterBinary { - type Error = anyhow::Error; - fn try_from(value: DebugAdapterBinary) -> Result { - Ok(Self { - command: value.command, - arguments: value.arguments, - envs: value.envs.into_iter().collect(), - cwd: value.cwd.map(|s| s.into()), - connection: value.connection.map(Into::into), - request_args: value.request_args.try_into()?, - }) - } -} - -impl From for extension::BuildTaskDefinition { - fn from(value: BuildTaskDefinition) -> Self { - match value { - BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), - BuildTaskDefinition::Template(build_task_template) => Self::Template { - task_template: build_task_template.template.into(), - locator_name: build_task_template.locator_name.map(SharedString::from), - }, - } - } -} - -impl From for BuildTaskDefinition { - fn from(value: extension::BuildTaskDefinition) -> Self { - match value { - extension::BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), - extension::BuildTaskDefinition::Template { - task_template, - locator_name, - } => Self::Template(BuildTaskDefinitionTemplatePayload { - template: task_template.into(), - locator_name: locator_name.map(String::from), - }), - } - } -} -impl From for extension::BuildTaskTemplate { - fn from(value: BuildTaskTemplate) -> Self { - Self { - label: value.label, - command: value.command, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd, - ..Default::default() - } - } -} -impl From for BuildTaskTemplate { - fn from(value: extension::BuildTaskTemplate) -> Self { - Self { - label: value.label, - command: value.command, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd, - } - } -} - -impl TryFrom for extension::DebugScenario { - type Error = anyhow::Error; - - fn try_from(value: DebugScenario) -> std::result::Result { - Ok(Self { - adapter: value.adapter.into(), - label: value.label.into(), - build: value.build.map(Into::into), - config: serde_json::Value::from_str(&value.config)?, - tcp_connection: value.tcp_connection.map(Into::into), - }) - } -} - -impl From for DebugScenario { - fn from(value: extension::DebugScenario) -> Self { - Self { - adapter: value.adapter.into(), - label: value.label.into(), - build: value.build.map(Into::into), - config: value.config.to_string(), - tcp_connection: value.tcp_connection.map(Into::into), - } - } -} - -impl TryFrom for ResolvedTask { - type Error = anyhow::Error; - - fn try_from(value: SpawnInTerminal) -> Result { - Ok(Self { - label: value.label, - command: value.command.context("missing command")?, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd.map(|s| { - let s = s.to_string_lossy(); - if cfg!(target_os = "windows") { - s.replace('\\', "/") - } else { - s.into_owned() - } - }), - }) - } -} - -impl From for extension::CodeLabel { +impl From for latest::CodeLabel { fn from(value: CodeLabel) -> Self { Self { code: value.code, spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), + filter_range: value.filter_range, } } } -impl From for extension::CodeLabelSpan { +impl From for latest::CodeLabelSpan { fn from(value: CodeLabelSpan) -> Self { match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), + CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), } } } -impl From for extension::CodeLabelSpanLiteral { +impl From for latest::CodeLabelSpanLiteral { fn from(value: CodeLabelSpanLiteral) -> Self { Self { text: value.text, @@ -352,167 +75,37 @@ impl From for extension::CodeLabelSpanLiteral { } } -impl From for Completion { - fn from(value: extension::Completion) -> Self { - Self { - label: value.label, - label_details: value.label_details.map(Into::into), - detail: value.detail, - kind: value.kind.map(Into::into), - insert_text_format: value.insert_text_format.map(Into::into), - } - } -} - -impl From for CompletionLabelDetails { - fn from(value: extension::CompletionLabelDetails) -> Self { +impl From for latest::SettingsLocation { + fn from(value: SettingsLocation) -> Self { Self { - detail: value.detail, - description: value.description, + worktree_id: value.worktree_id, + path: value.path, } } } -impl From for CompletionKind { - fn from(value: extension::CompletionKind) -> Self { +impl From for latest::LanguageServerInstallationStatus { + fn from(value: LanguageServerInstallationStatus) -> Self { match value { - extension::CompletionKind::Text => Self::Text, - extension::CompletionKind::Method => Self::Method, - extension::CompletionKind::Function => Self::Function, - extension::CompletionKind::Constructor => Self::Constructor, - extension::CompletionKind::Field => Self::Field, - extension::CompletionKind::Variable => Self::Variable, - extension::CompletionKind::Class => Self::Class, - extension::CompletionKind::Interface => Self::Interface, - extension::CompletionKind::Module => Self::Module, - extension::CompletionKind::Property => Self::Property, - extension::CompletionKind::Unit => Self::Unit, - extension::CompletionKind::Value => Self::Value, - extension::CompletionKind::Enum => Self::Enum, - extension::CompletionKind::Keyword => Self::Keyword, - extension::CompletionKind::Snippet => Self::Snippet, - extension::CompletionKind::Color => Self::Color, - extension::CompletionKind::File => Self::File, - extension::CompletionKind::Reference => Self::Reference, - extension::CompletionKind::Folder => Self::Folder, - extension::CompletionKind::EnumMember => Self::EnumMember, - extension::CompletionKind::Constant => Self::Constant, - extension::CompletionKind::Struct => Self::Struct, - extension::CompletionKind::Event => Self::Event, - extension::CompletionKind::Operator => Self::Operator, - extension::CompletionKind::TypeParameter => Self::TypeParameter, - extension::CompletionKind::Other(value) => Self::Other(value), + LanguageServerInstallationStatus::None => Self::None, + LanguageServerInstallationStatus::Downloading => Self::Downloading, + LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, + LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), } } } -impl From for InsertTextFormat { - fn from(value: extension::InsertTextFormat) -> Self { +impl From for latest::DownloadedFileType { + fn from(value: DownloadedFileType) -> Self { match value { - extension::InsertTextFormat::PlainText => Self::PlainText, - extension::InsertTextFormat::Snippet => Self::Snippet, - extension::InsertTextFormat::Other(value) => Self::Other(value), - } - } -} - -impl From for Symbol { - fn from(value: extension::Symbol) -> Self { - Self { - kind: value.kind.into(), - name: value.name, - } - } -} - -impl From for SymbolKind { - fn from(value: extension::SymbolKind) -> Self { - match value { - extension::SymbolKind::File => Self::File, - extension::SymbolKind::Module => Self::Module, - extension::SymbolKind::Namespace => Self::Namespace, - extension::SymbolKind::Package => Self::Package, - extension::SymbolKind::Class => Self::Class, - extension::SymbolKind::Method => Self::Method, - extension::SymbolKind::Property => Self::Property, - extension::SymbolKind::Field => Self::Field, - extension::SymbolKind::Constructor => Self::Constructor, - extension::SymbolKind::Enum => Self::Enum, - extension::SymbolKind::Interface => Self::Interface, - extension::SymbolKind::Function => Self::Function, - extension::SymbolKind::Variable => Self::Variable, - extension::SymbolKind::Constant => Self::Constant, - extension::SymbolKind::String => Self::String, - extension::SymbolKind::Number => Self::Number, - extension::SymbolKind::Boolean => Self::Boolean, - extension::SymbolKind::Array => Self::Array, - extension::SymbolKind::Object => Self::Object, - extension::SymbolKind::Key => Self::Key, - extension::SymbolKind::Null => Self::Null, - extension::SymbolKind::EnumMember => Self::EnumMember, - extension::SymbolKind::Struct => Self::Struct, - extension::SymbolKind::Event => Self::Event, - extension::SymbolKind::Operator => Self::Operator, - extension::SymbolKind::TypeParameter => Self::TypeParameter, - extension::SymbolKind::Other(value) => Self::Other(value), - } - } -} - -impl From for SlashCommand { - fn from(value: extension::SlashCommand) -> Self { - Self { - name: value.name, - description: value.description, - tooltip_text: value.tooltip_text, - requires_argument: value.requires_argument, - } - } -} - -impl From for extension::SlashCommandOutput { - fn from(value: SlashCommandOutput) -> Self { - Self { - text: value.text, - sections: value.sections.into_iter().map(Into::into).collect(), - } - } -} - -impl From for extension::SlashCommandOutputSection { - fn from(value: SlashCommandOutputSection) -> Self { - Self { - range: value.range.start as usize..value.range.end as usize, - label: value.label, - } - } -} - -impl From for extension::SlashCommandArgumentCompletion { - fn from(value: SlashCommandArgumentCompletion) -> Self { - Self { - label: value.label, - new_text: value.new_text, - run_command: value.run_command, + DownloadedFileType::Gzip => Self::Gzip, + DownloadedFileType::GzipTar => Self::GzipTar, + DownloadedFileType::Zip => Self::Zip, + DownloadedFileType::Uncompressed => Self::Uncompressed, } } } -impl TryFrom for extension::ContextServerConfiguration { - type Error = anyhow::Error; - - fn try_from(value: ContextServerConfiguration) -> Result { - let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema) - .context("Failed to parse settings_schema")?; - - Ok(Self { - installation_instructions: value.installation_instructions, - default_settings: value.default_settings, - settings_schema, - }) - } -} - impl HostKeyValueStore for WasmState { async fn insert( &mut self, @@ -520,8 +113,7 @@ impl HostKeyValueStore for WasmState { key: String, value: String, ) -> wasmtime::Result> { - let kv_store = self.table.get(&kv_store)?; - kv_store.insert(key, value).await.to_wasmtime_result() + latest::HostKeyValueStore::insert(self, kv_store, key, value).await } async fn drop(&mut self, _worktree: Resource) -> Result<()> { @@ -535,8 +127,7 @@ impl HostProject for WasmState { &mut self, project: Resource, ) -> wasmtime::Result> { - let project = self.table.get(&project)?; - Ok(project.worktree_ids()) + latest::HostProject::worktree_ids(self, project).await } async fn drop(&mut self, _project: Resource) -> Result<()> { @@ -547,16 +138,14 @@ impl HostProject for WasmState { impl HostWorktree for WasmState { async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.id()) + latest::HostWorktree::id(self, delegate).await } async fn root_path( &mut self, delegate: Resource>, ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.root_path()) + latest::HostWorktree::root_path(self, delegate).await } async fn read_text_file( @@ -564,19 +153,14 @@ impl HostWorktree for WasmState { delegate: Resource>, path: String, ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?) - .await - .map_err(|error| error.to_string())) + latest::HostWorktree::read_text_file(self, delegate, path).await } async fn shell_env( &mut self, delegate: Resource>, ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.shell_env().await.into_iter().collect()) + latest::HostWorktree::shell_env(self, delegate).await } async fn which( @@ -584,8 +168,7 @@ impl HostWorktree for WasmState { delegate: Resource>, binary_name: String, ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate.which(binary_name).await) + latest::HostWorktree::which(self, delegate, binary_name).await } async fn drop(&mut self, _worktree: Resource) -> Result<()> { @@ -594,319 +177,6 @@ impl HostWorktree for WasmState { } } -impl common::Host for WasmState {} - -impl http_client::Host for WasmState { - async fn fetch( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result> { - maybe!(async { - let url = &request.url; - let request = convert_request(&request)?; - let mut response = self.host.http_client.send(request).await?; - - if response.status().is_client_error() || response.status().is_server_error() { - bail!("failed to fetch '{url}': status code {}", response.status()) - } - convert_response(&mut response).await - }) - .await - .to_wasmtime_result() - } - - async fn fetch_stream( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result, String>> { - let request = convert_request(&request)?; - let response = self.host.http_client.send(request); - maybe!(async { - let response = response.await?; - let stream = Arc::new(Mutex::new(response)); - let resource = self.table.push(stream)?; - Ok(resource) - }) - .await - .to_wasmtime_result() - } -} - -impl http_client::HostHttpResponseStream for WasmState { - async fn next_chunk( - &mut self, - resource: Resource, - ) -> wasmtime::Result>, String>> { - let stream = self.table.get(&resource)?.clone(); - maybe!(async move { - let mut response = stream.lock().await; - let mut buffer = vec![0; 8192]; // 8KB buffer - let bytes_read = response.body_mut().read(&mut buffer).await?; - if bytes_read == 0 { - Ok(None) - } else { - buffer.truncate(bytes_read); - Ok(Some(buffer)) - } - }) - .await - .to_wasmtime_result() - } - - async fn drop(&mut self, _resource: Resource) -> Result<()> { - Ok(()) - } -} - -impl From for ::http_client::Method { - fn from(value: http_client::HttpMethod) -> Self { - match value { - http_client::HttpMethod::Get => Self::GET, - http_client::HttpMethod::Post => Self::POST, - http_client::HttpMethod::Put => Self::PUT, - http_client::HttpMethod::Delete => Self::DELETE, - http_client::HttpMethod::Head => Self::HEAD, - http_client::HttpMethod::Options => Self::OPTIONS, - http_client::HttpMethod::Patch => Self::PATCH, - } - } -} - -fn convert_request( - extension_request: &http_client::HttpRequest, -) -> anyhow::Result<::http_client::Request> { - let mut request = ::http_client::Request::builder() - .method(::http_client::Method::from(extension_request.method)) - .uri(&extension_request.url) - .follow_redirects(match extension_request.redirect_policy { - http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow, - http_client::RedirectPolicy::FollowLimit(limit) => { - ::http_client::RedirectPolicy::FollowLimit(limit) - } - http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll, - }); - for (key, value) in &extension_request.headers { - request = request.header(key, value); - } - let body = extension_request - .body - .clone() - .map(AsyncBody::from) - .unwrap_or_default(); - request.body(body).map_err(anyhow::Error::from) -} - -async fn convert_response( - response: &mut ::http_client::Response, -) -> anyhow::Result { - let mut extension_response = http_client::HttpResponse { - body: Vec::new(), - headers: Vec::new(), - }; - - for (key, value) in response.headers() { - extension_response - .headers - .push((key.to_string(), value.to_str().unwrap_or("").to_string())); - } - - response - .body_mut() - .read_to_end(&mut extension_response.body) - .await?; - - Ok(extension_response) -} - -impl nodejs::Host for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - self.host - .node_runtime - .binary_path() - .await - .map(|path| path.to_string_lossy().into_owned()) - .to_wasmtime_result() - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_package_latest_version(&package_name) - .await - .to_wasmtime_result() - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - self.host - .node_runtime - .npm_package_installed_version(&self.work_dir(), &package_name) - .await - .to_wasmtime_result() - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - self.capability_granter - .grant_npm_install_package(&package_name)?; - - self.host - .node_runtime - .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl lsp::Host for WasmState {} - -impl From<::http_client::github::GithubRelease> for github::GithubRelease { - fn from(value: ::http_client::github::GithubRelease) -> Self { - Self { - version: value.tag_name, - assets: value.assets.into_iter().map(Into::into).collect(), - } - } -} - -impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset { - fn from(value: ::http_client::github::GithubReleaseAsset) -> Self { - Self { - name: value.name, - download_url: value.browser_download_url, - } - } -} - -impl github::Host for WasmState { - async fn latest_github_release( - &mut self, - repo: String, - options: github::GithubReleaseOptions, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_client::github::latest_github_release( - &repo, - options.require_assets, - options.pre_release, - self.host.http_client.clone(), - ) - .await?; - Ok(release.into()) - }) - .await - .to_wasmtime_result() - } - - async fn github_release_by_tag_name( - &mut self, - repo: String, - tag: String, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_client::github::get_release_by_tag_name( - &repo, - &tag, - self.host.http_client.clone(), - ) - .await?; - Ok(release.into()) - }) - .await - .to_wasmtime_result() - } -} - -impl platform::Host for WasmState { - async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { - Ok(( - match env::consts::OS { - "macos" => platform::Os::Mac, - "linux" => platform::Os::Linux, - "windows" => platform::Os::Windows, - _ => panic!("unsupported os"), - }, - match env::consts::ARCH { - "aarch64" => platform::Architecture::Aarch64, - "x86" => platform::Architecture::X86, - "x86_64" => platform::Architecture::X8664, - _ => panic!("unsupported architecture"), - }, - )) - } -} - -impl From for process::Output { - fn from(output: std::process::Output) -> Self { - Self { - status: output.status.code(), - stdout: output.stdout, - stderr: output.stderr, - } - } -} - -impl process::Host for WasmState { - async fn run_command( - &mut self, - command: process::Command, - ) -> wasmtime::Result> { - maybe!(async { - self.capability_granter - .grant_exec(&command.command, &command.args)?; - - let output = util::command::new_smol_command(command.command.as_str()) - .args(&command.args) - .envs(command.env) - .output() - .await?; - - Ok(output.into()) - }) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl slash_command::Host for WasmState {} - -#[async_trait] -impl context_server::Host for WasmState {} - -impl dap::Host for WasmState { - async fn resolve_tcp_template( - &mut self, - template: TcpArgumentsTemplate, - ) -> wasmtime::Result> { - maybe!(async { - let (host, port, timeout) = - ::dap::configure_tcp_connection(task::TcpArgumentsTemplate { - port: template.port, - host: template.host.map(Ipv4Addr::from_bits), - timeout: template.timeout, - }) - .await?; - Ok(TcpArguments { - port, - host: host.to_bits(), - timeout, - }) - }) - .await - .to_wasmtime_result() - } -} - impl ExtensionImports for WasmState { async fn get_settings( &mut self, @@ -914,96 +184,13 @@ impl ExtensionImports for WasmState { category: String, key: Option, ) -> wasmtime::Result> { - self.on_main_thread(|cx| { - async move { - let path = location.as_ref().and_then(|location| { - RelPath::new(Path::new(&location.path), PathStyle::Posix).ok() - }); - let location = path - .as_ref() - .zip(location.as_ref()) - .map(|(path, location)| ::settings::SettingsLocation { - worktree_id: WorktreeId::from_proto(location.worktree_id), - path, - }); - - cx.update(|cx| match category.as_str() { - "language" => { - let key = key.map(|k| LanguageName::new(&k)); - let settings = AllLanguageSettings::get(location, cx).language( - location, - key.as_ref(), - cx, - ); - Ok(serde_json::to_string(&settings::LanguageSettings { - tab_size: settings.tab_size, - })?) - } - "lsp" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .lsp - .get(&::lsp::LanguageServerName::from_proto(key)) - }) - .cloned() - .unwrap_or_default(); - Ok(serde_json::to_string(&settings::LspSettings { - binary: settings.binary.map(|binary| settings::CommandSettings { - path: binary.path, - arguments: binary.arguments, - env: binary.env.map(|env| env.into_iter().collect()), - }), - settings: settings.settings, - initialization_options: settings.initialization_options, - })?) - } - "context_servers" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .context_servers - .get(key.as_str()) - }) - .cloned() - .unwrap_or_else(|| { - project::project_settings::ContextServerSettings::default_extension( - ) - }); - - match settings { - project::project_settings::ContextServerSettings::Stdio { - enabled: _, - command, - } => Ok(serde_json::to_string(&settings::ContextServerSettings { - command: Some(settings::CommandSettings { - path: command.path.to_str().map(|path| path.to_string()), - arguments: Some(command.args), - env: command.env.map(|env| env.into_iter().collect()), - }), - settings: None, - })?), - project::project_settings::ContextServerSettings::Extension { - enabled: _, - settings, - } => Ok(serde_json::to_string(&settings::ContextServerSettings { - command: None, - settings: Some(settings), - })?), - project::project_settings::ContextServerSettings::Http { .. } => { - bail!("remote context server settings not supported in 0.6.0") - } - } - } - _ => { - bail!("Unknown settings category: {}", category); - } - }) - } - .boxed_local() - }) - .await? - .to_wasmtime_result() + latest::ExtensionImports::get_settings( + self, + location.map(|location| location.into()), + category, + key, + ) + .await } async fn set_language_server_installation_status( @@ -1011,18 +198,12 @@ impl ExtensionImports for WasmState { server_name: String, status: LanguageServerInstallationStatus, ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, - LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, - LanguageServerInstallationStatus::None => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, - }; - - self.host - .proxy - .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); - - Ok(()) + latest::ExtensionImports::set_language_server_installation_status( + self, + server_name, + status.into(), + ) + .await } async fn download_file( @@ -1031,79 +212,10 @@ impl ExtensionImports for WasmState { path: String, file_type: DownloadedFileType, ) -> wasmtime::Result> { - maybe!(async { - let parsed_url = Url::parse(&url)?; - self.capability_granter.grant_download_file(&parsed_url)?; - - let path = PathBuf::from(path); - let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); - - self.host.fs.create_dir(&extension_work_dir).await?; - - let destination_path = self - .host - .writeable_path_from_extension(&self.manifest.id, &path)?; - - let mut response = self - .host - .http_client - .get(&url, Default::default(), true) - .await - .context("downloading release")?; - - anyhow::ensure!( - response.status().is_success(), - "download failed with status {}", - response.status() - ); - let body = BufReader::new(response.body_mut()); - - match file_type { - DownloadedFileType::Uncompressed => { - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::Gzip => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::GzipTar => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .extract_tar_file(&destination_path, Archive::new(body)) - .await?; - } - DownloadedFileType::Zip => { - futures::pin_mut!(body); - extract_zip(&destination_path, body) - .await - .with_context(|| format!("unzipping {path:?} archive"))?; - } - } - - Ok(()) - }) - .await - .to_wasmtime_result() + latest::ExtensionImports::download_file(self, url, path, file_type.into()).await } async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - make_file_executable(&path) - .await - .with_context(|| format!("setting permissions for path {path:?}")) - .to_wasmtime_result() + latest::ExtensionImports::make_file_executable(self, path).await } } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs new file mode 100644 index 00000000000000..9b40c22a5595d6 --- /dev/null +++ b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs @@ -0,0 +1,1196 @@ +use crate::wasm_host::wit::since_v0_7_0::{ + dap::{ + AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest, + StartDebuggingRequestArguments, TcpArguments, TcpArgumentsTemplate, + }, + lsp::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind}, + slash_command::SlashCommandOutputSection, +}; +use crate::wasm_host::{WasmState, wit::ToWasmtimeResult}; +use ::http_client::{AsyncBody, HttpRequestExt}; +use ::settings::{Settings, WorktreeId}; +use anyhow::{Context as _, Result, bail}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use extension::{ + ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, +}; +use futures::{AsyncReadExt, lock::Mutex}; +use futures::{FutureExt as _, io::BufReader}; +use gpui::{BackgroundExecutor, SharedString}; +use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings}; +use project::project_settings::ProjectSettings; +use semver::Version; +use std::{ + env, + net::Ipv4Addr, + path::{Path, PathBuf}, + str::FromStr, + sync::{Arc, OnceLock}, +}; +use task::{SpawnInTerminal, ZedDebugConfig}; +use url::Url; +use util::{ + archive::extract_zip, fs::make_file_executable, maybe, paths::PathStyle, rel_path::RelPath, +}; +use wasmtime::component::{Linker, Resource}; + +pub const MIN_VERSION: Version = Version::new(0, 7, 0); +pub const MAX_VERSION: Version = Version::new(0, 8, 0); + +wasmtime::component::bindgen!({ + async: true, + trappable_imports: true, + path: "../extension_api/wit/since_v0.7.0", + with: { + "worktree": ExtensionWorktree, + "project": ExtensionProject, + "key-value-store": ExtensionKeyValueStore, + "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream, + }, +}); + +// This is the latest version, so we pub use to make types available to parent module. +// Note: The parent wit.rs module re-exports specific types from here as the "latest" types. +pub use self::zed::extension::*; + +mod settings { + #![allow(dead_code)] + include!(concat!(env!("OUT_DIR"), "/since_v0.7.0/settings.rs")); +} + +pub type ExtensionWorktree = Arc; +pub type ExtensionProject = Arc; +pub type ExtensionKeyValueStore = Arc; +pub type ExtensionHttpResponseStream = Arc>>; + +pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { + static LINKER: OnceLock> = OnceLock::new(); + LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) +} + +impl From for std::ops::Range { + fn from(range: Range) -> Self { + let start = range.start as usize; + let end = range.end as usize; + start..end + } +} + +impl From for extension::Command { + fn from(value: Command) -> Self { + Self { + command: value.command.into(), + args: value.args, + env: value.env, + } + } +} + +impl From + for extension::StartDebuggingRequestArgumentsRequest +{ + fn from(value: StartDebuggingRequestArgumentsRequest) -> Self { + match value { + StartDebuggingRequestArgumentsRequest::Launch => Self::Launch, + StartDebuggingRequestArgumentsRequest::Attach => Self::Attach, + } + } +} +impl TryFrom for extension::StartDebuggingRequestArguments { + type Error = anyhow::Error; + + fn try_from(value: StartDebuggingRequestArguments) -> Result { + Ok(Self { + configuration: serde_json::from_str(&value.configuration)?, + request: value.request.into(), + }) + } +} +impl From for extension::TcpArguments { + fn from(value: TcpArguments) -> Self { + Self { + host: value.host.into(), + port: value.port, + timeout: value.timeout, + } + } +} + +impl From for TcpArgumentsTemplate { + fn from(value: extension::TcpArgumentsTemplate) -> Self { + Self { + host: value.host.map(Ipv4Addr::to_bits), + port: value.port, + timeout: value.timeout, + } + } +} + +impl From for extension::TcpArgumentsTemplate { + fn from(value: TcpArgumentsTemplate) -> Self { + Self { + host: value.host.map(Ipv4Addr::from_bits), + port: value.port, + timeout: value.timeout, + } + } +} + +impl TryFrom for DebugTaskDefinition { + type Error = anyhow::Error; + fn try_from(value: extension::DebugTaskDefinition) -> Result { + Ok(Self { + label: value.label.to_string(), + adapter: value.adapter.to_string(), + config: value.config.to_string(), + tcp_connection: value.tcp_connection.map(Into::into), + }) + } +} + +impl From for DebugRequest { + fn from(value: task::DebugRequest) -> Self { + match value { + task::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), + task::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), + } + } +} + +impl From for task::DebugRequest { + fn from(value: DebugRequest) -> Self { + match value { + DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), + DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), + } + } +} + +impl From for LaunchRequest { + fn from(value: task::LaunchRequest) -> Self { + Self { + program: value.program, + cwd: value.cwd.map(|p| p.to_string_lossy().into_owned()), + args: value.args, + envs: value.env.into_iter().collect(), + } + } +} + +impl From for AttachRequest { + fn from(value: task::AttachRequest) -> Self { + Self { + process_id: value.process_id, + } + } +} + +impl From for task::LaunchRequest { + fn from(value: LaunchRequest) -> Self { + Self { + program: value.program, + cwd: value.cwd.map(|p| p.into()), + args: value.args, + env: value.envs.into_iter().collect(), + } + } +} +impl From for task::AttachRequest { + fn from(value: AttachRequest) -> Self { + Self { + process_id: value.process_id, + } + } +} + +impl From for DebugConfig { + fn from(value: ZedDebugConfig) -> Self { + Self { + label: value.label.into(), + adapter: value.adapter.into(), + request: value.request.into(), + stop_on_entry: value.stop_on_entry, + } + } +} +impl TryFrom for extension::DebugAdapterBinary { + type Error = anyhow::Error; + fn try_from(value: DebugAdapterBinary) -> Result { + Ok(Self { + command: value.command, + arguments: value.arguments, + envs: value.envs.into_iter().collect(), + cwd: value.cwd.map(|s| s.into()), + connection: value.connection.map(Into::into), + request_args: value.request_args.try_into()?, + }) + } +} + +impl From for extension::BuildTaskDefinition { + fn from(value: BuildTaskDefinition) -> Self { + match value { + BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), + BuildTaskDefinition::Template(build_task_template) => Self::Template { + task_template: build_task_template.template.into(), + locator_name: build_task_template.locator_name.map(SharedString::from), + }, + } + } +} + +impl From for BuildTaskDefinition { + fn from(value: extension::BuildTaskDefinition) -> Self { + match value { + extension::BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), + extension::BuildTaskDefinition::Template { + task_template, + locator_name, + } => Self::Template(BuildTaskDefinitionTemplatePayload { + template: task_template.into(), + locator_name: locator_name.map(String::from), + }), + } + } +} +impl From for extension::BuildTaskTemplate { + fn from(value: BuildTaskTemplate) -> Self { + Self { + label: value.label, + command: value.command, + args: value.args, + env: value.env.into_iter().collect(), + cwd: value.cwd, + ..Default::default() + } + } +} +impl From for BuildTaskTemplate { + fn from(value: extension::BuildTaskTemplate) -> Self { + Self { + label: value.label, + command: value.command, + args: value.args, + env: value.env.into_iter().collect(), + cwd: value.cwd, + } + } +} + +impl TryFrom for extension::DebugScenario { + type Error = anyhow::Error; + + fn try_from(value: DebugScenario) -> std::result::Result { + Ok(Self { + adapter: value.adapter.into(), + label: value.label.into(), + build: value.build.map(Into::into), + config: serde_json::Value::from_str(&value.config)?, + tcp_connection: value.tcp_connection.map(Into::into), + }) + } +} + +impl From for DebugScenario { + fn from(value: extension::DebugScenario) -> Self { + Self { + adapter: value.adapter.into(), + label: value.label.into(), + build: value.build.map(Into::into), + config: value.config.to_string(), + tcp_connection: value.tcp_connection.map(Into::into), + } + } +} + +impl TryFrom for ResolvedTask { + type Error = anyhow::Error; + + fn try_from(value: SpawnInTerminal) -> Result { + Ok(Self { + label: value.label, + command: value.command.context("missing command")?, + args: value.args, + env: value.env.into_iter().collect(), + cwd: value.cwd.map(|s| { + let s = s.to_string_lossy(); + if cfg!(target_os = "windows") { + s.replace('\\', "/") + } else { + s.into_owned() + } + }), + }) + } +} + +impl From for extension::CodeLabel { + fn from(value: CodeLabel) -> Self { + Self { + code: value.code, + spans: value.spans.into_iter().map(Into::into).collect(), + filter_range: value.filter_range.into(), + } + } +} + +impl From for extension::CodeLabelSpan { + fn from(value: CodeLabelSpan) -> Self { + match value { + CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), + CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), + } + } +} + +impl From for extension::CodeLabelSpanLiteral { + fn from(value: CodeLabelSpanLiteral) -> Self { + Self { + text: value.text, + highlight_name: value.highlight_name, + } + } +} + +impl From for Completion { + fn from(value: extension::Completion) -> Self { + Self { + label: value.label, + label_details: value.label_details.map(Into::into), + detail: value.detail, + kind: value.kind.map(Into::into), + insert_text_format: value.insert_text_format.map(Into::into), + } + } +} + +impl From for CompletionLabelDetails { + fn from(value: extension::CompletionLabelDetails) -> Self { + Self { + detail: value.detail, + description: value.description, + } + } +} + +impl From for CompletionKind { + fn from(value: extension::CompletionKind) -> Self { + match value { + extension::CompletionKind::Text => Self::Text, + extension::CompletionKind::Method => Self::Method, + extension::CompletionKind::Function => Self::Function, + extension::CompletionKind::Constructor => Self::Constructor, + extension::CompletionKind::Field => Self::Field, + extension::CompletionKind::Variable => Self::Variable, + extension::CompletionKind::Class => Self::Class, + extension::CompletionKind::Interface => Self::Interface, + extension::CompletionKind::Module => Self::Module, + extension::CompletionKind::Property => Self::Property, + extension::CompletionKind::Unit => Self::Unit, + extension::CompletionKind::Value => Self::Value, + extension::CompletionKind::Enum => Self::Enum, + extension::CompletionKind::Keyword => Self::Keyword, + extension::CompletionKind::Snippet => Self::Snippet, + extension::CompletionKind::Color => Self::Color, + extension::CompletionKind::File => Self::File, + extension::CompletionKind::Reference => Self::Reference, + extension::CompletionKind::Folder => Self::Folder, + extension::CompletionKind::EnumMember => Self::EnumMember, + extension::CompletionKind::Constant => Self::Constant, + extension::CompletionKind::Struct => Self::Struct, + extension::CompletionKind::Event => Self::Event, + extension::CompletionKind::Operator => Self::Operator, + extension::CompletionKind::TypeParameter => Self::TypeParameter, + extension::CompletionKind::Other(value) => Self::Other(value), + } + } +} + +impl From for InsertTextFormat { + fn from(value: extension::InsertTextFormat) -> Self { + match value { + extension::InsertTextFormat::PlainText => Self::PlainText, + extension::InsertTextFormat::Snippet => Self::Snippet, + extension::InsertTextFormat::Other(value) => Self::Other(value), + } + } +} + +impl From for Symbol { + fn from(value: extension::Symbol) -> Self { + Self { + kind: value.kind.into(), + name: value.name, + } + } +} + +impl From for SymbolKind { + fn from(value: extension::SymbolKind) -> Self { + match value { + extension::SymbolKind::File => Self::File, + extension::SymbolKind::Module => Self::Module, + extension::SymbolKind::Namespace => Self::Namespace, + extension::SymbolKind::Package => Self::Package, + extension::SymbolKind::Class => Self::Class, + extension::SymbolKind::Method => Self::Method, + extension::SymbolKind::Property => Self::Property, + extension::SymbolKind::Field => Self::Field, + extension::SymbolKind::Constructor => Self::Constructor, + extension::SymbolKind::Enum => Self::Enum, + extension::SymbolKind::Interface => Self::Interface, + extension::SymbolKind::Function => Self::Function, + extension::SymbolKind::Variable => Self::Variable, + extension::SymbolKind::Constant => Self::Constant, + extension::SymbolKind::String => Self::String, + extension::SymbolKind::Number => Self::Number, + extension::SymbolKind::Boolean => Self::Boolean, + extension::SymbolKind::Array => Self::Array, + extension::SymbolKind::Object => Self::Object, + extension::SymbolKind::Key => Self::Key, + extension::SymbolKind::Null => Self::Null, + extension::SymbolKind::EnumMember => Self::EnumMember, + extension::SymbolKind::Struct => Self::Struct, + extension::SymbolKind::Event => Self::Event, + extension::SymbolKind::Operator => Self::Operator, + extension::SymbolKind::TypeParameter => Self::TypeParameter, + extension::SymbolKind::Other(value) => Self::Other(value), + } + } +} + +impl From for SlashCommand { + fn from(value: extension::SlashCommand) -> Self { + Self { + name: value.name, + description: value.description, + tooltip_text: value.tooltip_text, + requires_argument: value.requires_argument, + } + } +} + +impl From for extension::SlashCommandOutput { + fn from(value: SlashCommandOutput) -> Self { + Self { + text: value.text, + sections: value.sections.into_iter().map(Into::into).collect(), + } + } +} + +impl From for extension::SlashCommandOutputSection { + fn from(value: SlashCommandOutputSection) -> Self { + Self { + range: value.range.start as usize..value.range.end as usize, + label: value.label, + } + } +} + +impl From for extension::SlashCommandArgumentCompletion { + fn from(value: SlashCommandArgumentCompletion) -> Self { + Self { + label: value.label, + new_text: value.new_text, + run_command: value.run_command, + } + } +} + +impl TryFrom for extension::ContextServerConfiguration { + type Error = anyhow::Error; + + fn try_from(value: ContextServerConfiguration) -> Result { + let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema) + .context("Failed to parse settings_schema")?; + + Ok(Self { + installation_instructions: value.installation_instructions, + default_settings: value.default_settings, + settings_schema, + }) + } +} + +impl HostKeyValueStore for WasmState { + async fn insert( + &mut self, + kv_store: Resource, + key: String, + value: String, + ) -> wasmtime::Result> { + let kv_store = self.table.get(&kv_store)?; + kv_store.insert(key, value).await.to_wasmtime_result() + } + + async fn drop(&mut self, _worktree: Resource) -> Result<()> { + // We only ever hand out borrows of key-value stores. + Ok(()) + } +} + +impl HostProject for WasmState { + async fn worktree_ids( + &mut self, + project: Resource, + ) -> wasmtime::Result> { + let project = self.table.get(&project)?; + Ok(project.worktree_ids()) + } + + async fn drop(&mut self, _project: Resource) -> Result<()> { + // We only ever hand out borrows of projects. + Ok(()) + } +} + +impl HostWorktree for WasmState { + async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.id()) + } + + async fn root_path( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.root_path()) + } + + async fn read_text_file( + &mut self, + delegate: Resource>, + path: String, + ) -> wasmtime::Result> { + let delegate = self.table.get(&delegate)?; + Ok(delegate + .read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?) + .await + .map_err(|error| error.to_string())) + } + + async fn shell_env( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.shell_env().await.into_iter().collect()) + } + + async fn which( + &mut self, + delegate: Resource>, + binary_name: String, + ) -> wasmtime::Result> { + let delegate = self.table.get(&delegate)?; + Ok(delegate.which(binary_name).await) + } + + async fn drop(&mut self, _worktree: Resource) -> Result<()> { + // We only ever hand out borrows of worktrees. + Ok(()) + } +} + +impl common::Host for WasmState {} + +impl http_client::Host for WasmState { + async fn fetch( + &mut self, + request: http_client::HttpRequest, + ) -> wasmtime::Result> { + maybe!(async { + let url = &request.url; + let request = convert_request(&request)?; + let mut response = self.host.http_client.send(request).await?; + + if response.status().is_client_error() || response.status().is_server_error() { + bail!("failed to fetch '{url}': status code {}", response.status()) + } + convert_response(&mut response).await + }) + .await + .to_wasmtime_result() + } + + async fn fetch_stream( + &mut self, + request: http_client::HttpRequest, + ) -> wasmtime::Result, String>> { + let request = convert_request(&request)?; + let response = self.host.http_client.send(request); + maybe!(async { + let response = response.await?; + let stream = Arc::new(Mutex::new(response)); + let resource = self.table.push(stream)?; + Ok(resource) + }) + .await + .to_wasmtime_result() + } +} + +impl http_client::HostHttpResponseStream for WasmState { + async fn next_chunk( + &mut self, + resource: Resource, + ) -> wasmtime::Result>, String>> { + let stream = self.table.get(&resource)?.clone(); + maybe!(async move { + let mut response = stream.lock().await; + let mut buffer = vec![0; 8192]; // 8KB buffer + let bytes_read = response.body_mut().read(&mut buffer).await?; + if bytes_read == 0 { + Ok(None) + } else { + buffer.truncate(bytes_read); + Ok(Some(buffer)) + } + }) + .await + .to_wasmtime_result() + } + + async fn drop(&mut self, _resource: Resource) -> Result<()> { + Ok(()) + } +} + +impl From for ::http_client::Method { + fn from(value: http_client::HttpMethod) -> Self { + match value { + http_client::HttpMethod::Get => Self::GET, + http_client::HttpMethod::Post => Self::POST, + http_client::HttpMethod::Put => Self::PUT, + http_client::HttpMethod::Delete => Self::DELETE, + http_client::HttpMethod::Head => Self::HEAD, + http_client::HttpMethod::Options => Self::OPTIONS, + http_client::HttpMethod::Patch => Self::PATCH, + } + } +} + +fn convert_request( + extension_request: &http_client::HttpRequest, +) -> anyhow::Result<::http_client::Request> { + let mut request = ::http_client::Request::builder() + .method(::http_client::Method::from(extension_request.method)) + .uri(&extension_request.url) + .follow_redirects(match extension_request.redirect_policy { + http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow, + http_client::RedirectPolicy::FollowLimit(limit) => { + ::http_client::RedirectPolicy::FollowLimit(limit) + } + http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll, + }); + for (key, value) in &extension_request.headers { + request = request.header(key, value); + } + let body = extension_request + .body + .clone() + .map(AsyncBody::from) + .unwrap_or_default(); + request.body(body).map_err(anyhow::Error::from) +} + +async fn convert_response( + response: &mut ::http_client::Response, +) -> anyhow::Result { + let mut extension_response = http_client::HttpResponse { + body: Vec::new(), + headers: Vec::new(), + }; + + for (key, value) in response.headers() { + extension_response + .headers + .push((key.to_string(), value.to_str().unwrap_or("").to_string())); + } + + response + .body_mut() + .read_to_end(&mut extension_response.body) + .await?; + + Ok(extension_response) +} + +impl nodejs::Host for WasmState { + async fn node_binary_path(&mut self) -> wasmtime::Result> { + self.host + .node_runtime + .binary_path() + .await + .map(|path| path.to_string_lossy().into_owned()) + .to_wasmtime_result() + } + + async fn npm_package_latest_version( + &mut self, + package_name: String, + ) -> wasmtime::Result> { + self.host + .node_runtime + .npm_package_latest_version(&package_name) + .await + .to_wasmtime_result() + } + + async fn npm_package_installed_version( + &mut self, + package_name: String, + ) -> wasmtime::Result, String>> { + self.host + .node_runtime + .npm_package_installed_version(&self.work_dir(), &package_name) + .await + .to_wasmtime_result() + } + + async fn npm_install_package( + &mut self, + package_name: String, + version: String, + ) -> wasmtime::Result> { + self.capability_granter + .grant_npm_install_package(&package_name)?; + + self.host + .node_runtime + .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) + .await + .to_wasmtime_result() + } +} + +#[async_trait] +impl lsp::Host for WasmState {} + +impl From<::http_client::github::GithubRelease> for github::GithubRelease { + fn from(value: ::http_client::github::GithubRelease) -> Self { + Self { + version: value.tag_name, + assets: value.assets.into_iter().map(Into::into).collect(), + } + } +} + +impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset { + fn from(value: ::http_client::github::GithubReleaseAsset) -> Self { + Self { + name: value.name, + download_url: value.browser_download_url, + } + } +} + +impl github::Host for WasmState { + async fn latest_github_release( + &mut self, + repo: String, + options: github::GithubReleaseOptions, + ) -> wasmtime::Result> { + maybe!(async { + let release = ::http_client::github::latest_github_release( + &repo, + options.require_assets, + options.pre_release, + self.host.http_client.clone(), + ) + .await?; + Ok(release.into()) + }) + .await + .to_wasmtime_result() + } + + async fn github_release_by_tag_name( + &mut self, + repo: String, + tag: String, + ) -> wasmtime::Result> { + maybe!(async { + let release = ::http_client::github::get_release_by_tag_name( + &repo, + &tag, + self.host.http_client.clone(), + ) + .await?; + Ok(release.into()) + }) + .await + .to_wasmtime_result() + } +} + +impl platform::Host for WasmState { + async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { + Ok(( + match env::consts::OS { + "macos" => platform::Os::Mac, + "linux" => platform::Os::Linux, + "windows" => platform::Os::Windows, + _ => panic!("unsupported os"), + }, + match env::consts::ARCH { + "aarch64" => platform::Architecture::Aarch64, + "x86" => platform::Architecture::X86, + "x86_64" => platform::Architecture::X8664, + _ => panic!("unsupported architecture"), + }, + )) + } +} + +impl From for process::Output { + fn from(output: std::process::Output) -> Self { + Self { + status: output.status.code(), + stdout: output.stdout, + stderr: output.stderr, + } + } +} + +impl process::Host for WasmState { + async fn run_command( + &mut self, + command: process::Command, + ) -> wasmtime::Result> { + maybe!(async { + self.capability_granter + .grant_exec(&command.command, &command.args)?; + + let output = util::command::new_smol_command(command.command.as_str()) + .args(&command.args) + .envs(command.env) + .output() + .await?; + + Ok(output.into()) + }) + .await + .to_wasmtime_result() + } +} + +#[async_trait] +impl slash_command::Host for WasmState {} + +#[async_trait] +impl context_server::Host for WasmState {} + +impl dap::Host for WasmState { + async fn resolve_tcp_template( + &mut self, + template: TcpArgumentsTemplate, + ) -> wasmtime::Result> { + maybe!(async { + let (host, port, timeout) = + ::dap::configure_tcp_connection(task::TcpArgumentsTemplate { + port: template.port, + host: template.host.map(Ipv4Addr::from_bits), + timeout: template.timeout, + }) + .await?; + Ok(TcpArguments { + port, + host: host.to_bits(), + timeout, + }) + }) + .await + .to_wasmtime_result() + } +} + +impl ExtensionImports for WasmState { + async fn get_settings( + &mut self, + location: Option, + category: String, + key: Option, + ) -> wasmtime::Result> { + self.on_main_thread(|cx| { + async move { + let path = location.as_ref().and_then(|location| { + RelPath::new(Path::new(&location.path), PathStyle::Posix).ok() + }); + let location = path + .as_ref() + .zip(location.as_ref()) + .map(|(path, location)| ::settings::SettingsLocation { + worktree_id: WorktreeId::from_proto(location.worktree_id), + path, + }); + + cx.update(|cx| match category.as_str() { + "language" => { + let key = key.map(|k| LanguageName::new(&k)); + let settings = AllLanguageSettings::get(location, cx).language( + location, + key.as_ref(), + cx, + ); + Ok(serde_json::to_string(&settings::LanguageSettings { + tab_size: settings.tab_size, + })?) + } + "lsp" => { + let settings = key + .and_then(|key| { + ProjectSettings::get(location, cx) + .lsp + .get(&::lsp::LanguageServerName::from_proto(key)) + }) + .cloned() + .unwrap_or_default(); + Ok(serde_json::to_string(&settings::LspSettings { + binary: settings.binary.map(|binary| settings::CommandSettings { + path: binary.path, + arguments: binary.arguments, + env: binary.env.map(|env| env.into_iter().collect()), + }), + settings: settings.settings, + initialization_options: settings.initialization_options, + })?) + } + "context_servers" => { + let settings = key + .and_then(|key| { + ProjectSettings::get(location, cx) + .context_servers + .get(key.as_str()) + }) + .cloned() + .unwrap_or_else(|| { + project::project_settings::ContextServerSettings::default_extension( + ) + }); + + match settings { + project::project_settings::ContextServerSettings::Stdio { + enabled: _, + command, + } => Ok(serde_json::to_string(&settings::ContextServerSettings { + command: Some(settings::CommandSettings { + path: command.path.to_str().map(|path| path.to_string()), + arguments: Some(command.args), + env: command.env.map(|env| env.into_iter().collect()), + }), + settings: None, + })?), + project::project_settings::ContextServerSettings::Extension { + enabled: _, + settings, + } => Ok(serde_json::to_string(&settings::ContextServerSettings { + command: None, + settings: Some(settings), + })?), + project::project_settings::ContextServerSettings::Http { .. } => { + bail!("remote context server settings not supported in 0.6.0") + } + } + } + _ => { + bail!("Unknown settings category: {}", category); + } + }) + } + .boxed_local() + }) + .await? + .to_wasmtime_result() + } + + async fn set_language_server_installation_status( + &mut self, + server_name: String, + status: LanguageServerInstallationStatus, + ) -> wasmtime::Result<()> { + let status = match status { + LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, + LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, + LanguageServerInstallationStatus::None => BinaryStatus::None, + LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, + }; + + self.host + .proxy + .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); + + Ok(()) + } + + async fn download_file( + &mut self, + url: String, + path: String, + file_type: DownloadedFileType, + ) -> wasmtime::Result> { + maybe!(async { + let parsed_url = Url::parse(&url)?; + self.capability_granter.grant_download_file(&parsed_url)?; + + let path = PathBuf::from(path); + let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); + + self.host.fs.create_dir(&extension_work_dir).await?; + + let destination_path = self + .host + .writeable_path_from_extension(&self.manifest.id, &path)?; + + let mut response = self + .host + .http_client + .get(&url, Default::default(), true) + .await + .context("downloading release")?; + + anyhow::ensure!( + response.status().is_success(), + "download failed with status {}", + response.status() + ); + let body = BufReader::new(response.body_mut()); + + match file_type { + DownloadedFileType::Uncompressed => { + futures::pin_mut!(body); + self.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + DownloadedFileType::Gzip => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + self.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + DownloadedFileType::GzipTar => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + self.host + .fs + .extract_tar_file(&destination_path, Archive::new(body)) + .await?; + } + DownloadedFileType::Zip => { + futures::pin_mut!(body); + extract_zip(&destination_path, body) + .await + .with_context(|| format!("unzipping {path:?} archive"))?; + } + } + + Ok(()) + }) + .await + .to_wasmtime_result() + } + + async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { + let path = self + .host + .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; + + make_file_executable(&path) + .await + .with_context(|| format!("setting permissions for path {path:?}")) + .to_wasmtime_result() + } + + // ========================================================================= + // LLM Provider Import Implementations + // ========================================================================= + + async fn llm_request_credential( + &mut self, + _provider_id: String, + _credential_type: llm_provider::CredentialType, + _label: String, + _placeholder: String, + ) -> wasmtime::Result> { + // For now, credential requests return false (not provided) + // Extensions should use llm_get_env_var to check for env vars first, + // then llm_store_credential/llm_get_credential for manual storage + // Full UI credential prompting will be added in a future phase + Ok(Ok(false)) + } + + async fn llm_get_credential( + &mut self, + provider_id: String, + ) -> wasmtime::Result> { + let extension_id = self.manifest.id.clone(); + let credential_key = format!("{}:{}", extension_id, provider_id); + + self.on_main_thread(move |cx| { + async move { + let task = cx.update(|cx| cx.read_credentials(&credential_key))?; + let result = task.await.ok().flatten(); + Ok(result.map(|(_, password)| String::from_utf8_lossy(&password).to_string())) + } + .boxed_local() + }) + .await + } + + async fn llm_store_credential( + &mut self, + provider_id: String, + value: String, + ) -> wasmtime::Result> { + let extension_id = self.manifest.id.clone(); + let credential_key = format!("{}:{}", extension_id, provider_id); + + self.on_main_thread(move |cx| { + async move { + let task = cx.update(|cx| { + cx.write_credentials(&credential_key, "api_key", value.as_bytes()) + })?; + task.await.map_err(|e| anyhow::anyhow!("{}", e)) + } + .boxed_local() + }) + .await + .to_wasmtime_result() + } + + async fn llm_delete_credential( + &mut self, + provider_id: String, + ) -> wasmtime::Result> { + let extension_id = self.manifest.id.clone(); + let credential_key = format!("{}:{}", extension_id, provider_id); + + self.on_main_thread(move |cx| { + async move { + let task = cx.update(|cx| cx.delete_credentials(&credential_key))?; + task.await.map_err(|e| anyhow::anyhow!("{}", e)) + } + .boxed_local() + }) + .await + .to_wasmtime_result() + } + + async fn llm_get_env_var(&mut self, name: String) -> wasmtime::Result> { + Ok(env::var(&name).ok()) + } +} + +// ============================================================================= +// LLM Provider Host Implementations +// ============================================================================= + +impl llm_provider::Host for WasmState {} diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 6c5704312d94e2..decb32c5aa4000 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -28,6 +28,7 @@ convert_case.workspace = true copilot.workspace = true credentials_provider.workspace = true deepseek = { workspace = true, features = ["schemars"] } +extension.workspace = true fs.workspace = true futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } diff --git a/crates/language_models/src/extension.rs b/crates/language_models/src/extension.rs new file mode 100644 index 00000000000000..139ff632657cd8 --- /dev/null +++ b/crates/language_models/src/extension.rs @@ -0,0 +1,33 @@ +use extension::{ExtensionLanguageModelProviderProxy, LanguageModelProviderRegistration}; +use gpui::{App, Entity}; +use language_model::{LanguageModelProviderId, LanguageModelRegistry}; +use std::sync::Arc; + +/// Proxy implementation that registers extension-based language model providers +/// with the LanguageModelRegistry. +pub struct ExtensionLanguageModelProxy { + registry: Entity, +} + +impl ExtensionLanguageModelProxy { + pub fn new(registry: Entity) -> Self { + Self { registry } + } +} + +impl ExtensionLanguageModelProviderProxy for ExtensionLanguageModelProxy { + fn register_language_model_provider( + &self, + _provider_id: Arc, + register_fn: LanguageModelProviderRegistration, + cx: &mut App, + ) { + register_fn(cx); + } + + fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App) { + self.registry.update(cx, |registry, cx| { + registry.unregister_provider(LanguageModelProviderId::from(provider_id), cx); + }); + } +} diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index d771dba3733540..445c6e11daa849 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use ::extension::ExtensionHostProxy; use ::settings::{Settings, SettingsStore}; use client::{Client, UserStore}; use collections::HashSet; @@ -8,6 +9,7 @@ use language_model::{LanguageModelProviderId, LanguageModelRegistry}; use provider::deepseek::DeepSeekLanguageModelProvider; mod api_key; +mod extension; pub mod provider; mod settings; pub mod ui; @@ -33,6 +35,12 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { register_language_model_providers(registry, user_store, client.clone(), cx); }); + // Register the extension language model provider proxy + let extension_proxy = ExtensionHostProxy::default_global(cx); + extension_proxy.register_language_model_provider_proxy( + extension::ExtensionLanguageModelProxy::new(registry.clone()), + ); + let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx) .openai_compatible .keys() diff --git a/docs/language_model_provider_extensions_implementation.md b/docs/language_model_provider_extensions_implementation.md new file mode 100644 index 00000000000000..f77fd569df2c59 --- /dev/null +++ b/docs/language_model_provider_extensions_implementation.md @@ -0,0 +1,689 @@ +# Language Model Provider Extensions - Implementation Guide + +## Purpose + +This document provides a detailed guide for completing the implementation of Language Model Provider Extensions in Zed. It explains what has been done, what remains, and how to complete the work. + +For the full design and rationale, see [language_model_provider_extensions_plan.md](./language_model_provider_extensions_plan.md). + +## Core Design Principle + +**Extensions handle ALL provider-specific logic.** This means: +- Thought signatures (Anthropic) +- Reasoning effort parameters (OpenAI o-series) +- Cache control markers +- Parallel tool calls +- SSE/streaming format parsing +- Any other provider-specific features + +Zed's core should have **zero knowledge** of these details. The extension API must be generic enough that extensions can implement any provider without Zed changes. + +--- + +## Current Status: STREAMING API COMPLETE ✅ + +The core plumbing and streaming API are now complete. Extensions can: +1. Declare LLM providers in their manifest +2. Be queried for providers and models at load time +3. Have their providers registered with the `LanguageModelRegistry` +4. Have their providers unregistered when the extension is unloaded +5. Stream completions using the new polling-based API + +**What's NOT done yet:** +- Credential UI prompt support (`llm_request_credential` returns false) +- Model refresh mechanism +- A working test extension that demonstrates the feature (requires WASM build) +- End-to-end testing with a real extension + +--- + +## What Has Been Completed + +### 1. WIT Interface Definition ✅ + +**Location:** `crates/extension_api/wit/since_v0.7.0/` + +Created all WIT files for v0.7.0: +- `llm-provider.wit` - Core LLM types (ProviderInfo, ModelInfo, CompletionRequest, CompletionEvent, etc.) +- `extension.wit` - Updated with LLM exports/imports + +Key types in `llm-provider.wit`: +```wit +record provider-info { + id: string, + name: string, + icon: option, +} + +record model-info { + id: string, + name: string, + max-token-count: u64, + max-output-tokens: option, + capabilities: model-capabilities, + is-default: bool, + is-default-fast: bool, +} + +variant completion-event { + started, + text(string), + thinking(thinking-content), + redacted-thinking(string), + tool-use(tool-use), + tool-use-json-parse-error(tool-use-json-parse-error), + stop(stop-reason), + usage(token-usage), + reasoning-details(string), +} +``` + +Key exports in `extension.wit`: +```wit +export llm-providers: func() -> list; +export llm-provider-models: func(provider-id: string) -> result, string>; +export llm-provider-is-authenticated: func(provider-id: string) -> bool; +export llm-provider-authenticate: func(provider-id: string) -> result<_, string>; +export llm-stream-completion-start: func(provider-id: string, model-id: string, request: completion-request) -> result; +export llm-stream-completion-next: func(stream-id: string) -> result, string>; +export llm-stream-completion-close: func(stream-id: string); +``` + +Note: The streaming API uses a polling-based approach with explicit stream IDs instead of a resource handle. +This avoids complexity with cross-boundary resource ownership in the WASM component model. + +Key imports in `extension.wit`: +```wit +import llm-get-credential: func(provider-id: string) -> option; +import llm-store-credential: func(provider-id: string, value: string) -> result<_, string>; +import llm-delete-credential: func(provider-id: string) -> result<_, string>; +import llm-get-env-var: func(name: string) -> option; +``` + +### 2. Extension Manifest Changes ✅ + +**Location:** `crates/extension/src/extension_manifest.rs` + +Added these types: +```rust +pub struct LanguageModelProviderManifestEntry { + pub name: String, + pub icon: Option, + pub models: Vec, + pub auth: Option, +} + +pub struct LanguageModelManifestEntry { + pub id: String, + pub name: String, + pub max_token_count: u64, + pub max_output_tokens: Option, + pub supports_images: bool, + pub supports_tools: bool, + pub supports_thinking: bool, +} + +pub struct LanguageModelAuthConfig { + pub env_var: Option, + pub credential_label: Option, +} +``` + +Added to `ExtensionManifest`: +```rust +pub language_model_providers: BTreeMap, LanguageModelProviderManifestEntry>, +``` + +### 3. Host-Side Provider/Model Structs ✅ + +**Location:** `crates/extension_host/src/wasm_host/llm_provider.rs` + +Created `ExtensionLanguageModelProvider` implementing `LanguageModelProvider`: +- Wraps a `WasmExtension` and `LlmProviderInfo` +- Delegates to extension calls for authentication, model listing, etc. +- Returns `ExtensionLanguageModel` instances +- Implements `LanguageModelProviderState` for UI observation + +Created `ExtensionLanguageModel` implementing `LanguageModel`: +- Wraps extension + model info +- Implements `stream_completion` by calling extension's `llm-stream-completion` +- Converts between Zed's `LanguageModelRequest` and WIT's `CompletionRequest` +- Handles streaming via polling-based approach with explicit stream IDs + +**Key implementation details:** +- The `stream_completion` method uses a polling loop that calls `llm_stream_completion_start`, then repeatedly calls `llm_stream_completion_next` until the stream is complete, and finally calls `llm_stream_completion_close` to clean up +- Credential storage uses gpui's `cx.read_credentials()`, `cx.write_credentials()`, and `cx.delete_credentials()` APIs +- The `new()` method now accepts a `models: Vec` parameter to populate available models at registration time + +### 4. Extension Host Proxy ✅ + +**Location:** `crates/extension/src/extension_host_proxy.rs` + +Added `ExtensionLanguageModelProviderProxy` trait: +```rust +pub type LanguageModelProviderRegistration = Box; + +pub trait ExtensionLanguageModelProviderProxy: Send + Sync + 'static { + fn register_language_model_provider( + &self, + provider_id: Arc, + register_fn: LanguageModelProviderRegistration, + cx: &mut App, + ); + + fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App); +} +``` + +The proxy uses a boxed closure pattern. This allows `extension_host` to create the `ExtensionLanguageModelProvider` (which requires `WasmExtension`), while letting `language_models` handle the actual registry registration. + +### 5. Proxy Implementation ✅ + +**Location:** `crates/language_models/src/extension.rs` + +```rust +pub struct ExtensionLanguageModelProxy { + registry: Entity, +} + +impl ExtensionLanguageModelProviderProxy for ExtensionLanguageModelProxy { + fn register_language_model_provider( + &self, + _provider_id: Arc, + register_fn: LanguageModelProviderRegistration, + cx: &mut App, + ) { + register_fn(cx); + } + + fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App) { + self.registry.update(cx, |registry, cx| { + registry.unregister_provider(LanguageModelProviderId::from(provider_id), cx); + }); + } +} +``` + +The proxy is registered during `language_models::init()`. + +### 6. Extension Loading Wiring ✅ + +**Location:** `crates/extension_host/src/extension_host.rs` + +In `extensions_updated()`: + +**Unloading (around line 1217):** +```rust +for provider_id in extension.manifest.language_model_providers.keys() { + let full_provider_id: Arc = format!("{}:{}", extension_id, provider_id).into(); + self.proxy.unregister_language_model_provider(full_provider_id, cx); +} +``` + +**Loading (around line 1383):** +After loading a wasm extension, we query for LLM providers and models: +```rust +if !extension.manifest.language_model_providers.is_empty() { + let providers_result = wasm_extension + .call(|ext, store| { + async move { ext.call_llm_providers(store).await }.boxed() + }) + .await; + + if let Ok(Ok(providers)) = providers_result { + for provider_info in providers { + // Query for models... + let models_result = wasm_extension.call(...).await; + // Store provider_info and models for registration + } + } +} +``` + +Then during registration (around line 1511): +```rust +for (provider_info, models) in llm_providers_with_models { + let provider_id: Arc = format!("{}:{}", manifest.id, provider_info.id).into(); + this.proxy.register_language_model_provider( + provider_id, + Box::new(move |cx: &mut App| { + let provider = Arc::new(ExtensionLanguageModelProvider::new( + wasm_ext, pinfo, mods, cx, + )); + language_model::LanguageModelRegistry::global(cx).update( + cx, + |registry, cx| { + registry.register_provider(provider, cx); + }, + ); + }), + cx, + ); +} +``` + +### 7. Extension API Updates ✅ + +**Location:** `crates/extension_api/src/extension_api.rs` + +- Updated `wit_bindgen::generate!` to use `./wit/since_v0.7.0` +- Added LLM type re-exports (prefixed with `Llm` for clarity) +- Added LLM methods to `Extension` trait with default implementations +- Added `wit::Guest` implementations for LLM functions + +The default implementations ensure backward compatibility: +```rust +fn llm_providers(&self) -> Vec { + Vec::new() // Extensions without LLM providers return empty +} + +fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(Vec::new()) +} + +fn llm_stream_completion_start(...) -> Result { + Err("`llm_stream_completion_start` not implemented".to_string()) +} +fn llm_stream_completion_next(stream_id: &str) -> Result, String> { + Err("`llm_stream_completion_next` not implemented".to_string()) +} +fn llm_stream_completion_close(stream_id: &str) { /* cleanup */ } +``` + +### 8. Test Files Updated ✅ + +Added `language_model_providers: BTreeMap::default()` to all test manifests: +- `crates/extension/src/extension_manifest.rs` (test module) +- `crates/extension_host/src/extension_store_test.rs` +- `crates/extension_host/src/capability_granter.rs` (test module) +- `crates/extension_host/benches/extension_compilation_benchmark.rs` + +--- + +## What Remains To Be Done + +### Task 1: Test the Streaming Completion Flow (HIGH PRIORITY) - ARCHITECTURE UPDATED ✅ + +The streaming API has been updated to use a polling-based approach instead of a resource handle pattern. +This was necessary because the original design had a fundamental issue: the `completion-stream` resource +was defined in an imported interface but returned from an exported function, creating ownership ambiguity. + +**New API:** +- `llm-stream-completion-start` - Returns a stream ID (string) +- `llm-stream-completion-next` - Poll for the next event using the stream ID +- `llm-stream-completion-close` - Clean up the stream when done + +**Still needs testing:** +1. Create a test extension that implements a simple LLM provider +2. Verify the polling-based streaming works correctly through the WASM boundary +3. Test error handling and edge cases + +**Location to test:** `crates/extension_host/src/wasm_host/llm_provider.rs` - the `stream_completion` method on `ExtensionLanguageModel`. + +### Task 2: Credential UI Prompt Support (MEDIUM PRIORITY) + +**Location:** `crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs` + +The `llm_request_credential` host function currently returns `Ok(Ok(false))`: +```rust +async fn llm_request_credential( + &mut self, + _provider_id: String, + _credential_type: llm_provider::CredentialType, + _label: String, + _placeholder: String, +) -> wasmtime::Result> { + // TODO: Implement actual UI prompting + Ok(Ok(false)) +} +``` + +**What needs to happen:** +1. Show a dialog to the user asking for the credential +2. Wait for user input +3. Return `true` if provided, `false` if cancelled +4. The extension can then use `llm_store_credential` to save it + +This requires UI work and async coordination with gpui windows. + +### Task 3: Handle Model Refresh (LOW PRIORITY - can be follow-up) + +Currently models are only queried once at registration time. Options for improvement: + +1. Add a refresh mechanism that re-queries `call_llm_provider_models` +2. Add a notification mechanism where extensions can signal that models have changed +3. Automatic refresh on authentication + +**Recommendation:** Start with refresh-on-authentication as a fast-follow. + +### Task 4: Create a Test Extension (LOW PRIORITY - but very useful) + +**Note:** Creating a working test extension requires building a WASM component, which needs: +1. The `wasm32-wasip1` Rust target: `rustup target add wasm32-wasip1` +2. Building with: `cargo build --target wasm32-wasip1 --release` +3. The resulting `.wasm` file must be placed in the extension directory + +The existing `extensions/test-extension` has a pre-built WASM file checked in. To test LLM +provider functionality, either: +- Rebuild the test-extension WASM with LLM provider code +- Create a new extension and build it locally + +Example test extension that demonstrates the LLM provider API: + +``` +extensions/test-llm-provider/ +├── extension.toml +├── Cargo.toml +└── src/ + └── lib.rs +``` + +**extension.toml:** +```toml +id = "test-llm-provider" +name = "Test LLM Provider" +version = "0.1.0" +schema_version = 1 + +[language_model_providers.test-provider] +name = "Test Provider" +``` + +**src/lib.rs:** +```rust +use zed_extension_api::{self as zed, *}; + +use std::collections::HashMap; +use std::sync::Mutex; + +struct TestExtension { + streams: Mutex>>, + next_stream_id: Mutex, +} + +impl zed::Extension for TestExtension { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "test-provider".into(), + name: "Test Provider".into(), + icon: None, + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(vec![LlmModelInfo { + id: "test-model".into(), + name: "Test Model".into(), + max_token_count: 4096, + max_output_tokens: Some(1024), + capabilities: LlmModelCapabilities { + supports_images: false, + supports_tools: false, + supports_tool_choice_auto: false, + supports_tool_choice_any: false, + supports_tool_choice_none: false, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: true, + is_default_fast: true, + }]) + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + _model_id: &str, + _request: &LlmCompletionRequest, + ) -> Result { + // Create a simple response with test events + let events = vec![ + LlmCompletionEvent::Started, + LlmCompletionEvent::Text("Hello, ".into()), + LlmCompletionEvent::Text("world!".into()), + LlmCompletionEvent::Stop(LlmStopReason::EndTurn), + ]; + + let mut id = self.next_stream_id.lock().unwrap(); + let stream_id = format!("stream-{}", *id); + *id += 1; + + self.streams.lock().unwrap().insert(stream_id.clone(), events); + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + if let Some(events) = streams.get_mut(stream_id) { + if events.is_empty() { + Ok(None) + } else { + Ok(Some(events.remove(0))) + } + } else { + Err(format!("Unknown stream: {}", stream_id)) + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(TestExtension); +``` + +--- + +## File-by-File Checklist + +### Completed ✅ + +- [x] `crates/extension_api/wit/since_v0.7.0/llm-provider.wit` - LLM types defined +- [x] `crates/extension_api/wit/since_v0.7.0/extension.wit` - LLM exports/imports added +- [x] `crates/extension_api/src/extension_api.rs` - Extension trait + Guest impl updated for v0.7.0 +- [x] `crates/extension/src/extension_manifest.rs` - Manifest types added +- [x] `crates/extension/src/extension_host_proxy.rs` - Proxy trait added +- [x] `crates/extension_host/src/wasm_host/llm_provider.rs` - Provider/Model structs created +- [x] `crates/extension_host/src/wasm_host/wit.rs` - LLM types exported, Extension enum updated +- [x] `crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs` - Host trait implementations +- [x] `crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs` - Rewritten to use latest types +- [x] `crates/extension_host/src/extension_host.rs` - Wired up LLM provider registration/unregistration +- [x] `crates/extension_host/Cargo.toml` - Dependencies added +- [x] `crates/language_models/src/extension.rs` - Proxy implementation +- [x] `crates/language_models/src/language_models.rs` - Proxy registration +- [x] `crates/language_models/Cargo.toml` - Extension dependency added + +### Should Implement (Follow-up PRs) + +- [ ] `llm_request_credential` UI implementation +- [ ] Model refresh mechanism +- [ ] Test extension for validation +- [ ] Documentation for extension authors + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Extension Host │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ extensions_updated() │ │ +│ │ │ │ +│ │ 1. Load WasmExtension │ │ +│ │ 2. Query llm_providers() and llm_provider_models() │ │ +│ │ 3. Call proxy.register_language_model_provider() │ │ +│ └───────────────────────────┬───────────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────────▼───────────────────────────────────┐ │ +│ │ ExtensionLanguageModelProvider │ │ +│ │ - Wraps WasmExtension │ │ +│ │ - Implements LanguageModelProvider │ │ +│ │ - Creates ExtensionLanguageModel instances │ │ +│ └───────────────────────────┬───────────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────────▼───────────────────────────────────┐ │ +│ │ ExtensionLanguageModel │ │ +│ │ - Implements LanguageModel │ │ +│ │ - stream_completion() calls extension via WASM │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + │ Proxy (boxed closure) + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Language Models Crate │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ ExtensionLanguageModelProxy │ │ +│ │ - Implements ExtensionLanguageModelProviderProxy │ │ +│ │ - Calls register_fn closure │ │ +│ │ - Unregisters from LanguageModelRegistry │ │ +│ └───────────────────────────┬───────────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────────▼───────────────────────────────────┐ │ +│ │ LanguageModelRegistry │ │ +│ │ - Stores all providers (built-in + extension) │ │ +│ │ - Provides models to UI │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Key Code Patterns + +### 1. Provider ID Format + +Provider IDs are formatted as `{extension_id}:{provider_id}` to ensure uniqueness: + +```rust +let provider_id: Arc = format!("{}:{}", manifest.id, provider_info.id).into(); +``` + +### 2. Triple-Nested Result Handling + +When calling extension methods, results are nested: +- Outer `Result`: from channel operations (anyhow error) +- Middle `Result`: from WASM call (anyhow error) +- Inner `Result`: from extension logic + +```rust +let models_result = wasm_extension.call(...).await; + +let models: Vec = match models_result { + Ok(Ok(Ok(models))) => models, + Ok(Ok(Err(e))) => { /* extension returned error */ } + Ok(Err(e)) => { /* WASM call failed */ } + Err(e) => { /* channel operation failed */ } +}; +``` + +### 3. Polling-Based Streaming Pattern + +The streaming API uses explicit stream IDs with polling instead of resource handles: + +```rust +// Start the stream and get an ID +let stream_id = ext.call_llm_stream_completion_start(store, provider_id, model_id, request).await?; + +// Poll for events in a loop +loop { + match ext.call_llm_stream_completion_next(store, &stream_id).await? { + Ok(Some(event)) => { /* process event */ } + Ok(None) => break, // Stream complete + Err(e) => { /* handle error */ } + } +} + +// Clean up +ext.call_llm_stream_completion_close(store, &stream_id).await; +``` + +This pattern avoids the complexity of cross-boundary resource ownership in the WASM component model. + +### 4. Default Trait Implementations + +All LLM methods in the `Extension` trait have defaults so existing extensions continue to work: + +```rust +fn llm_providers(&self) -> Vec { + Vec::new() // No providers by default +} +``` + +--- + +## Common Pitfalls + +1. **Type confusion:** WIT bindgen creates NEW types for each version. `Completion` from v0.6.0 bindgen is different from v0.7.0. This is why we map older interfaces to `latest::`. + +2. **Import paths:** After `pub use self::zed::extension::*;`, types are available without prefix. Types in sub-interfaces (like `lsp::CompletionKind`) need explicit imports. + +3. **Async closures:** Extension calls use `extension.call(|ext, store| async move { ... }.boxed())` pattern. The closure must be `'static + Send`. + +4. **Stream ID management:** Extensions must track their active streams using the stream IDs returned from `llm_stream_completion_start`. The host will call `llm_stream_completion_close` when done. + +5. **Result nesting:** `extension.call(...)` wraps the closure's return type in `Result`, so if the closure returns `Result>`, you get `Result>>`. Unwrap carefully! + +6. **Proxy type boundaries:** The `extension` crate shouldn't depend on `extension_host`. The proxy trait uses a boxed closure to pass the registration logic without needing to share types. + +7. **Resource ownership in WIT:** Be careful when defining resources in imported interfaces but returning them from exported functions. This creates ownership ambiguity. The streaming API was changed to use polling to avoid this issue. + +--- + +## Testing + +All existing tests pass: +```bash +cargo test -p extension_host --lib +# 3 tests pass + +./script/clippy +# No warnings +``` + +To test the full flow manually: +1. Create a test extension with LLM provider +2. Build and install it +3. Check if it appears in the model selector +4. Try making a completion request + +--- + +## Relevant Files for Reference + +### How providers are registered +- `crates/language_model/src/registry.rs` - `LanguageModelRegistry::register_provider` + +### How other extension proxies work +- `crates/extension/src/extension_host_proxy.rs` - the proxy pattern +- `crates/project/src/context_server_store/extension.rs` - context server proxy implementation + +### How extensions are loaded +- `crates/extension_host/src/extension_host.rs` - `extensions_updated` method + +### WasmExtension call pattern +- `crates/extension_host/src/wasm_host.rs` - `WasmExtension::call` method + +--- + +## Questions for Follow-up + +1. **Where should configuration UI live?** The current implementation uses an empty config view. Should extension providers have configurable settings? + +2. **How to handle extension reload?** Currently, in-flight completions will fail if the extension is unloaded. Should we add graceful handling? + +3. **Should there be rate limiting?** If an extension's provider misbehaves, should Zed throttle or disable it? + +4. **Icon support:** The `provider_info.icon` field exists but `icon()` on the provider returns `ui::IconName::ZedAssistant`. Should we add custom icon support? \ No newline at end of file diff --git a/docs/language_model_provider_extensions_plan.md b/docs/language_model_provider_extensions_plan.md new file mode 100644 index 00000000000000..14be166180a634 --- /dev/null +++ b/docs/language_model_provider_extensions_plan.md @@ -0,0 +1,1368 @@ +# Language Model Provider Extensions Plan + +## Executive Summary + +This document outlines a comprehensive plan to introduce **Language Model Provider Extensions** to Zed. This feature will allow third-party developers to create extensions that register new language model providers, enabling users to select and use custom language models in Zed's AI features (Agent, inline assist, commit message generation, etc.). + +## Table of Contents + +1. [Current Architecture Overview](#current-architecture-overview) +2. [Goals and Requirements](#goals-and-requirements) +3. [Proposed Architecture](#proposed-architecture) +4. [Implementation Phases](#implementation-phases) +5. [WIT Interface Design](#wit-interface-design) +6. [Extension Manifest Changes](#extension-manifest-changes) +7. [Migration Plan for Built-in Providers](#migration-plan-for-built-in-providers) +8. [Testing Strategy](#testing-strategy) +9. [Security Considerations](#security-considerations) +10. [Appendix: Provider-Specific Requirements](#appendix-provider-specific-requirements) + +--- + +## Current Architecture Overview + +### Key Components + +#### `language_model` crate (`crates/language_model/`) +- **`LanguageModel` trait** (`src/language_model.rs:580-718`): Core trait defining model capabilities + - `id()`, `name()`, `provider_id()`, `provider_name()` + - `supports_images()`, `supports_tools()`, `supports_tool_choice()` + - `max_token_count()`, `max_output_tokens()` + - `count_tokens()` - async token counting + - `stream_completion()` - the main completion streaming method + - `cache_configuration()` - optional prompt caching config + +- **`LanguageModelProvider` trait** (`src/language_model.rs:743-764`): Provider registration + - `id()`, `name()`, `icon()` + - `default_model()`, `default_fast_model()` + - `provided_models()`, `recommended_models()` + - `is_authenticated()`, `authenticate()` + - `configuration_view()` - UI for provider configuration + - `reset_credentials()` + +- **`LanguageModelRegistry`** (`src/registry.rs`): Global registry for providers + - `register_provider()` / `unregister_provider()` + - Model selection and configuration + - Event emission for UI updates + +#### `language_models` crate (`crates/language_models/`) +Contains all built-in provider implementations: +- `provider/anthropic.rs` - Anthropic Claude models +- `provider/cloud.rs` - Zed Cloud (proxied models) +- `provider/google.rs` - Google Gemini models +- `provider/open_ai.rs` - OpenAI GPT models +- `provider/ollama.rs` - Local Ollama models +- `provider/deepseek.rs` - DeepSeek models +- `provider/open_router.rs` - OpenRouter aggregator +- `provider/bedrock.rs` - AWS Bedrock +- And more... + +#### Extension System (`crates/extension_host/`, `crates/extension_api/`) +- **WIT interface** (`extension_api/wit/since_v0.6.0/`): WebAssembly Interface Types definitions +- **WASM host** (`extension_host/src/wasm_host.rs`): Executes extension WASM modules +- **Extension trait** (`extension/src/extension.rs`): Rust trait for extensions +- **HTTP client** (`extension_api/src/http_client.rs`): Existing HTTP capability for extensions + +### Request/Response Flow + +``` +User Request + ↓ +LanguageModelRequest (crates/language_model/src/request.rs) + ↓ +Provider-specific conversion (e.g., into_anthropic(), into_open_ai()) + ↓ +HTTP API call (provider-specific crate) + ↓ +Stream of provider-specific events + ↓ +Event mapping to LanguageModelCompletionEvent + ↓ +Consumer (Agent, Inline Assist, etc.) +``` + +### Key Data Structures + +```rust +// Request +pub struct LanguageModelRequest { + pub thread_id: Option, + pub prompt_id: Option, + pub intent: Option, + pub mode: Option, + pub messages: Vec, + pub tools: Vec, + pub tool_choice: Option, + pub stop: Vec, + pub temperature: Option, + pub thinking_allowed: bool, +} + +// Completion Events +pub enum LanguageModelCompletionEvent { + Queued { position: usize }, + Started, + UsageUpdated { amount: usize, limit: usize }, + ToolUseLimitReached, + Stop(StopReason), + Text(String), + Thinking { text: String, signature: Option }, + RedactedThinking { data: String }, + ToolUse(LanguageModelToolUse), + ToolUseJsonParseError { ... }, + StartMessage { message_id: Option }, + ReasoningDetails(serde_json::Value), + UsageUpdate(TokenUsage), +} +``` + +--- + +## Goals and Requirements + +### Primary Goals + +1. **Extensibility**: Allow any developer to add new LLM providers via extensions +2. **Parity**: Extension-based providers should have feature parity with built-in providers +3. **Performance**: Minimize overhead from WASM boundary crossings during streaming +4. **Security**: Sandbox API key handling and network access appropriately +5. **User Experience**: Seamless integration with existing model selectors and configuration UI + +### Functional Requirements + +1. Extensions can register one or more language model providers +2. Extensions can define multiple models per provider +3. Extensions handle authentication (API keys, OAuth, etc.) +4. Extensions implement the streaming completion API +5. Extensions can specify model capabilities (tools, images, thinking, etc.) +6. Extensions can provide token counting logic +7. Extensions can provide configuration UI components +8. Extensions receive full request context for API customization + +### Non-Functional Requirements + +1. Streaming should feel as responsive as built-in providers +2. Extension crashes should not crash Zed +3. API keys should never be logged or exposed +4. Extensions should be able to make arbitrary HTTP requests +5. Settings should persist across sessions + +--- + +## Proposed Architecture + +### High-Level Design + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Zed Application │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ LanguageModelRegistry ││ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ ││ +│ │ │ Built-in │ │ Extension │ │ Extension │ ││ +│ │ │ Providers │ │ Provider A │ │ Provider B │ ││ +│ │ │ (Anthropic, │ │ (WASM) │ │ (WASM) │ ││ +│ │ │ OpenAI...) │ │ │ │ │ ││ +│ │ └──────────────┘ └──────────────┘ └──────────────────┘ ││ +│ └─────────────────────────────────────────────────────────────┘│ +│ ↑ │ +│ │ │ +│ ┌───────────────────────────┴─────────────────────────────────┐│ +│ │ ExtensionLanguageModelProvider ││ +│ │ ┌─────────────────────────────────────────────────────────┐││ +│ │ │ • Bridges WASM extension to LanguageModelProvider trait │││ +│ │ │ • Manages streaming across WASM boundary │││ +│ │ │ • Handles credential storage via credentials_provider │││ +│ │ │ • Provides configuration UI scaffolding │││ +│ │ └─────────────────────────────────────────────────────────┘││ +│ └─────────────────────────────────────────────────────────────┘│ +│ ↑ │ +│ ┌───────────────────────────┴─────────────────────────────────┐│ +│ │ WasmHost / WasmExtension ││ +│ │ • Executes WASM module ││ +│ │ • Provides WIT interface for LLM operations ││ +│ │ • HTTP client for API calls ││ +│ └─────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────┘ +``` + +### New Components + +#### 1. `ExtensionLanguageModelProvider` + +A new struct in `extension_host` that implements `LanguageModelProvider` and wraps a WASM extension: + +```rust +pub struct ExtensionLanguageModelProvider { + extension: WasmExtension, + provider_info: ExtensionLlmProviderInfo, + state: Entity, +} + +struct ExtensionLlmProviderState { + is_authenticated: bool, + available_models: Vec, +} +``` + +#### 2. `ExtensionLanguageModel` + +Implements `LanguageModel` trait, delegating to WASM calls: + +```rust +pub struct ExtensionLanguageModel { + extension: WasmExtension, + model_info: ExtensionLlmModelInfo, + provider_id: LanguageModelProviderId, +} +``` + +#### 3. WIT Interface Extensions + +New WIT definitions for LLM provider functionality (see [WIT Interface Design](#wit-interface-design)). + +--- + +## Implementation Phases + +### Phase 1: Foundation (2-3 weeks) + +**Goal**: Establish the core infrastructure for extension-based LLM providers. + +#### Tasks + +1. **Define WIT interface for LLM providers** (`extension_api/wit/since_v0.7.0/llm-provider.wit`) + - Provider metadata (id, name, icon) + - Model definitions (id, name, capabilities, limits) + - Credential management hooks + - Completion request/response types + +2. **Create `ExtensionLanguageModelProvider`** (`extension_host/src/wasm_host/llm_provider.rs`) + - Implement `LanguageModelProvider` trait + - Handle provider registration/unregistration + - Basic authentication state management + +3. **Create `ExtensionLanguageModel`** (`extension_host/src/wasm_host/llm_model.rs`) + - Implement `LanguageModel` trait + - Simple synchronous completion (non-streaming initially) + +4. **Update `ExtensionManifest`** (`extension/src/extension_manifest.rs`) + - Add `language_model_providers` field + - Parse provider configuration from `extension.toml` + +5. **Update extension loading** (`extension_host/src/extension_host.rs`) + - Detect LLM provider declarations in manifest + - Register providers with `LanguageModelRegistry` + +#### Deliverables +- Extensions can register a provider that appears in model selector +- Basic (non-streaming) completions work +- Manual testing with a test extension + +### Phase 2: Streaming Support (2-3 weeks) + +**Goal**: Enable efficient streaming completions across the WASM boundary. + +#### Tasks + +1. **Design streaming protocol** + - Option A: Chunked responses via repeated WASM calls + - Option B: Callback-based streaming (preferred) + - Option C: Shared memory buffer with polling + +2. **Implement streaming in WIT** + ```wit + resource completion-stream { + next-event: func() -> result, string>; + } + + export stream-completion: func( + provider-id: string, + model-id: string, + request: completion-request + ) -> result; + ``` + +3. **Implement `http-response-stream` integration** + - Extensions already have access to `fetch-stream` + - Need to parse SSE/chunked responses in WASM + - Map to completion events + +4. **Update `ExtensionLanguageModel::stream_completion`** + - Bridge WASM completion-stream to Rust BoxStream + - Handle backpressure and cancellation + +5. **Performance optimization** + - Batch small events to reduce WASM boundary crossings + - Consider using shared memory for large payloads + +#### Deliverables +- Streaming completions work with acceptable latency +- Performance benchmarks vs built-in providers + +### Phase 3: Full Feature Parity (2-3 weeks) + +**Goal**: Support all advanced features that built-in providers have. + +#### Tasks + +1. **Tool/Function calling support** + - Add tool definitions to request + - Parse tool use events from response + - Handle tool results in follow-up requests + +2. **Image support** + - Pass image data in messages + - Handle base64 encoding/size limits + +3. **Thinking/reasoning support** (for Claude-like models) + - `Thinking` and `RedactedThinking` events + - Thought signatures for tool calls + +4. **Token counting** + - WIT interface for `count_tokens` + - Allow extensions to provide custom tokenizers or call API + +5. **Prompt caching configuration** + - Cache control markers in messages + - Cache configuration reporting + +6. **Rate limiting and error handling** + - Standard error types in WIT + - Retry-after headers + - Rate limit events + +#### Deliverables +- Extension providers can use tools +- Extension providers can process images +- Full error handling parity + +### Phase 4: Credential Management & Configuration UI (1-2 weeks) + +**Goal**: Secure credential storage and user-friendly configuration. + +#### Tasks + +1. **Credential storage integration** + - Use existing `credentials_provider` crate + - Extensions request credentials via WIT + - Credentials never exposed to WASM directly (only "is_authenticated" status) + +2. **API key input flow** + ```wit + import request-credential: func( + credential-type: credential-type, + label: string, + placeholder: string + ) -> result; + ``` + +3. **Configuration view scaffolding** + - Generic configuration view that works for most providers + - Extensions can provide additional settings via JSON schema + - Settings stored in extension-specific namespace + +4. **Environment variable support** + - Allow specifying env var names for API keys + - Read from environment on startup + +#### Deliverables +- Secure API key storage +- Configuration UI for extension providers +- Environment variable fallback + +### Phase 5: Testing & Documentation (1-2 weeks) + +**Goal**: Comprehensive testing and developer documentation. + +#### Tasks + +1. **Integration tests** + - Test extension loading and registration + - Test streaming completions + - Test error handling + - Test credential management + +2. **Performance tests** + - Latency benchmarks + - Memory usage under load + - Comparison with built-in providers + +3. **Example extensions** + - Simple OpenAI-compatible provider + - Provider with custom authentication + - Provider with tool support + +4. **Documentation** + - Extension developer guide + - API reference + - Migration guide for custom providers + +#### Deliverables +- Full test coverage +- Published documentation +- Example extensions in `extensions/` directory + +### Phase 6: Migration of Built-in Providers (Optional, Long-term) + +**Goal**: Prove the extension system by migrating one or more built-in providers. + +#### Tasks + +1. **Select candidate provider** (suggest: Ollama or LM Studio - simplest API) +2. **Create extension version** +3. **Feature parity testing** +4. **Performance comparison** +5. **Gradual rollout (feature flag) + +--- + +## WIT Interface Design + +### New File: `extension_api/wit/since_v0.7.0/llm-provider.wit` + +```wit +interface llm-provider { + /// Information about a language model provider + record provider-info { + /// Unique identifier for the provider (e.g., "my-extension.my-provider") + id: string, + /// Display name for the provider + name: string, + /// Icon name from Zed's icon set (optional) + icon: option, + } + + /// Capabilities of a language model + record model-capabilities { + /// Whether the model supports image inputs + supports-images: bool, + /// Whether the model supports tool/function calling + supports-tools: bool, + /// Whether the model supports tool choice (auto/any/none) + supports-tool-choice-auto: bool, + supports-tool-choice-any: bool, + supports-tool-choice-none: bool, + /// Whether the model supports extended thinking + supports-thinking: bool, + /// The format for tool input schemas + tool-input-format: tool-input-format, + } + + /// Format for tool input schemas + enum tool-input-format { + json-schema, + simplified, + } + + /// Information about a specific model + record model-info { + /// Unique identifier for the model + id: string, + /// Display name for the model + name: string, + /// Maximum input token count + max-token-count: u64, + /// Maximum output tokens (optional) + max-output-tokens: option, + /// Model capabilities + capabilities: model-capabilities, + /// Whether this is the default model for the provider + is-default: bool, + /// Whether this is the default fast model + is-default-fast: bool, + } + + /// A message in a completion request + record request-message { + role: message-role, + content: list, + cache: bool, + } + + enum message-role { + user, + assistant, + system, + } + + /// Content within a message + variant message-content { + text(string), + image(image-data), + tool-use(tool-use), + tool-result(tool-result), + thinking(thinking-content), + redacted-thinking(string), + } + + record image-data { + /// Base64-encoded image data + source: string, + /// Estimated dimensions + width: option, + height: option, + } + + record tool-use { + id: string, + name: string, + input: string, // JSON string + thought-signature: option, + } + + record tool-result { + tool-use-id: string, + tool-name: string, + is-error: bool, + content: tool-result-content, + } + + variant tool-result-content { + text(string), + image(image-data), + } + + record thinking-content { + text: string, + signature: option, + } + + /// A tool definition + record tool-definition { + name: string, + description: string, + /// JSON Schema for input parameters + input-schema: string, + } + + /// Tool choice preference + enum tool-choice { + auto, + any, + none, + } + + /// A completion request + record completion-request { + messages: list, + tools: list, + tool-choice: option, + stop-sequences: list, + temperature: option, + thinking-allowed: bool, + /// Maximum tokens to generate + max-tokens: option, + } + + /// Events emitted during completion streaming + variant completion-event { + /// Completion has started + started, + /// Text content + text(string), + /// Thinking/reasoning content + thinking(thinking-content), + /// Redacted thinking (encrypted) + redacted-thinking(string), + /// Tool use request + tool-use(tool-use), + /// Completion stopped + stop(stop-reason), + /// Token usage update + usage(token-usage), + } + + enum stop-reason { + end-turn, + max-tokens, + tool-use, + } + + record token-usage { + input-tokens: u64, + output-tokens: u64, + cache-creation-input-tokens: option, + cache-read-input-tokens: option, + } + + /// A streaming completion response + resource completion-stream { + /// Get the next event from the stream. + /// Returns None when the stream is complete. + next-event: func() -> result, string>; + } + + /// Credential types that can be requested + enum credential-type { + api-key, + oauth-token, + } +} +``` + +### Updates to `extension_api/wit/since_v0.7.0/extension.wit` + +```wit +world extension { + // ... existing imports ... + import llm-provider; + + use llm-provider.{ + provider-info, model-info, completion-request, + completion-stream, credential-type + }; + + /// Returns information about language model providers offered by this extension + export llm-providers: func() -> list; + + /// Returns the models available for a provider + export llm-provider-models: func(provider-id: string) -> result, string>; + + /// Check if the provider is authenticated + export llm-provider-is-authenticated: func(provider-id: string) -> bool; + + /// Attempt to authenticate the provider + export llm-provider-authenticate: func(provider-id: string) -> result<_, string>; + + /// Reset credentials for the provider + export llm-provider-reset-credentials: func(provider-id: string) -> result<_, string>; + + /// Count tokens for a request + export llm-count-tokens: func( + provider-id: string, + model-id: string, + request: completion-request + ) -> result; + + /// Stream a completion + export llm-stream-completion: func( + provider-id: string, + model-id: string, + request: completion-request + ) -> result; + + /// Request a credential from the user + import llm-request-credential: func( + provider-id: string, + credential-type: credential-type, + label: string, + placeholder: string + ) -> result; + + /// Get a stored credential + import llm-get-credential: func(provider-id: string) -> option; + + /// Store a credential + import llm-store-credential: func(provider-id: string, value: string) -> result<_, string>; + + /// Delete a stored credential + import llm-delete-credential: func(provider-id: string) -> result<_, string>; +} +``` + +--- + +## Extension Manifest Changes + +### Updated `extension.toml` Schema + +```toml +id = "my-llm-extension" +name = "My LLM Provider" +description = "Adds support for My LLM API" +version = "1.0.0" +schema_version = 1 +authors = ["Developer "] +repository = "https://github.com/example/my-llm-extension" + +[lib] +kind = "rust" +version = "0.7.0" + +# New section for LLM providers +[language_model_providers.my-provider] +name = "My LLM" +icon = "sparkle" # Optional, from Zed's icon set + +# Optional: Default models to show even before API connection +[[language_model_providers.my-provider.models]] +id = "my-model-large" +name = "My Model Large" +max_token_count = 200000 +max_output_tokens = 8192 +supports_images = true +supports_tools = true + +[[language_model_providers.my-provider.models]] +id = "my-model-small" +name = "My Model Small" +max_token_count = 100000 +max_output_tokens = 4096 +supports_images = false +supports_tools = true + +# Optional: Environment variable for API key +[language_model_providers.my-provider.auth] +env_var = "MY_LLM_API_KEY" +credential_label = "API Key" +``` + +### `ExtensionManifest` Changes + +```rust +// In extension/src/extension_manifest.rs + +#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LanguageModelProviderManifestEntry { + pub name: String, + #[serde(default)] + pub icon: Option, + #[serde(default)] + pub models: Vec, + #[serde(default)] + pub auth: Option, +} + +#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LanguageModelManifestEntry { + pub id: String, + pub name: String, + #[serde(default)] + pub max_token_count: u64, + #[serde(default)] + pub max_output_tokens: Option, + #[serde(default)] + pub supports_images: bool, + #[serde(default)] + pub supports_tools: bool, + #[serde(default)] + pub supports_thinking: bool, +} + +#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LanguageModelAuthConfig { + pub env_var: Option, + pub credential_label: Option, +} + +// Add to ExtensionManifest struct: +pub struct ExtensionManifest { + // ... existing fields ... + #[serde(default)] + pub language_model_providers: BTreeMap, LanguageModelProviderManifestEntry>, +} +``` + +--- + +## Migration Plan for Built-in Providers + +This section analyzes each built-in provider and what would be required to implement them as extensions. + +### Provider Comparison Matrix + +| Provider | API Style | Auth Method | Special Features | Migration Complexity | +|----------|-----------|-------------|------------------|---------------------| +| Anthropic | REST/SSE | API Key | Thinking, Caching, Tool signatures | High | +| OpenAI | REST/SSE | API Key | Reasoning effort, Prompt caching | Medium | +| Google | REST/SSE | API Key | Thinking, Tool signatures | High | +| Ollama | REST/SSE | None (local) | Dynamic model discovery | Low | +| DeepSeek | REST/SSE | API Key | Reasoning mode | Medium | +| OpenRouter | REST/SSE | API Key | Reasoning details, Model routing | Medium | +| LM Studio | REST/SSE | None (local) | OpenAI-compatible | Low | +| Bedrock | AWS SDK | AWS Credentials | Multiple underlying providers | High | +| Zed Cloud | Zed Auth | Zed Account | Proxied providers | N/A (keep built-in) | + +### Provider-by-Provider Analysis + +#### Anthropic (`provider/anthropic.rs`) + +**Current Implementation Highlights:** +- Uses `anthropic` crate for API types and streaming +- Custom event mapper (`AnthropicEventMapper`) for SSE → completion events +- Supports thinking/reasoning with thought signatures +- Prompt caching with cache control markers +- Beta headers for experimental features + +**Extension Requirements:** +- Full SSE parsing in WASM +- Complex event mapping logic +- Thinking content with signatures +- Cache configuration reporting + +**Unique Challenges:** +```rust +// Thought signatures in tool use +pub struct LanguageModelToolUse { + pub thought_signature: Option, // Anthropic-specific +} + +// Thinking events with signatures +Thinking { text: String, signature: Option } +``` + +**Migration Approach:** +1. Port `anthropic` crate types to extension-compatible structures +2. Implement SSE parser in extension (can use existing `fetch-stream`) +3. Map Anthropic events to generic completion events +4. Handle beta headers via custom HTTP headers + +#### OpenAI (`provider/open_ai.rs`) + +**Current Implementation Highlights:** +- Uses `open_ai` crate for API types +- Tiktoken-based token counting +- Parallel tool calls support +- Reasoning effort parameter (o1/o3 models) + +**Extension Requirements:** +- SSE parsing (standard format) +- Token counting (could call API or use simplified estimate) +- Tool call aggregation across chunks + +**Unique Challenges:** +```rust +// Reasoning effort for o-series models +pub reasoning_effort: Option, // "low", "medium", "high" + +// Prompt cache key (preview feature) +pub prompt_cache_key: Option, +``` + +**Migration Approach:** +1. Standard SSE parsing +2. Token counting via API or tiktoken WASM port +3. Support reasoning_effort as model-specific config + +#### Google/Gemini (`provider/google.rs`) + +**Current Implementation Highlights:** +- Uses `google_ai` crate +- Different API structure from OpenAI/Anthropic +- Thinking support similar to Anthropic +- Tool signatures in function calls + +**Extension Requirements:** +- Different request/response format +- Thinking content handling +- Tool signature preservation + +**Unique Challenges:** +```rust +// Google uses different content structure +enum ContentPart { + Text { text: String }, + InlineData { mime_type: String, data: String }, + FunctionCall { name: String, args: Value }, + FunctionResponse { name: String, response: Value }, +} +``` + +**Migration Approach:** +1. Implement Google-specific request building +2. Map Google events to generic completion events +3. Handle thinking/function call signatures + +#### Ollama (`provider/ollama.rs`) + +**Current Implementation Highlights:** +- Local-only, no authentication needed +- Dynamic model discovery via API +- OpenAI-compatible chat endpoint +- Simple streaming format + +**Extension Requirements:** +- API URL configuration +- Model list fetching +- Basic streaming + +**Why This is a Good First Migration Target:** +- No authentication complexity +- Simple API format +- Dynamic model discovery is isolated +- Good test case for local provider pattern + +**Migration Approach:** +1. Configuration for API URL +2. Model discovery endpoint call +3. OpenAI-compatible streaming + +#### DeepSeek (`provider/deepseek.rs`) + +**Current Implementation Highlights:** +- OpenAI-compatible API with extensions +- Reasoner model support +- Different handling for reasoning vs standard models + +**Extension Requirements:** +- API key authentication +- Model-specific request modifications +- Reasoning content handling + +**Migration Approach:** +1. Standard OpenAI-compatible base +2. Special handling for reasoner model +3. Temperature disabled for reasoning + +#### OpenRouter (`provider/open_router.rs`) + +**Current Implementation Highlights:** +- Aggregates multiple providers +- Dynamic model fetching +- Reasoning details preservation +- Tool call signatures + +**Extension Requirements:** +- API key authentication +- Model list from API +- Reasoning details in responses + +**Migration Approach:** +1. Model discovery from API +2. Standard OpenAI-compatible streaming +3. Preserve reasoning_details in events + +#### LM Studio (`provider/lmstudio.rs`) + +**Current Implementation Highlights:** +- Local-only, OpenAI-compatible +- Model discovery from API +- Simple configuration + +**Why This is a Good First Migration Target:** +- No authentication +- OpenAI-compatible (reusable streaming code) +- Similar to Ollama + +#### Bedrock (`provider/bedrock.rs`) + +**Current Implementation Highlights:** +- AWS SDK-based authentication +- Multiple authentication methods (IAM, Profile, etc.) +- Proxies to Claude, Llama, etc. + +**Extension Requirements:** +- AWS credential handling (complex) +- AWS Signature V4 signing +- Region configuration + +**Why This Should Stay Built-in (Initially):** +- AWS credential management is complex +- SDK dependency not easily portable to WASM +- Security implications of AWS credentials in extensions + +--- + +## Testing Strategy + +### Unit Tests + +```rust +// extension_host/src/wasm_host/llm_provider_tests.rs + +#[gpui::test] +async fn test_extension_provider_registration(cx: &mut TestAppContext) { + // Load test extension with LLM provider + // Verify provider appears in registry + // Verify models are listed correctly +} + +#[gpui::test] +async fn test_extension_streaming_completion(cx: &mut TestAppContext) { + // Create mock HTTP server + // Load extension + // Send completion request + // Verify streaming events received correctly +} + +#[gpui::test] +async fn test_extension_tool_calling(cx: &mut TestAppContext) { + // Test tool definitions are passed correctly + // Test tool use events are parsed + // Test tool results can be sent back +} + +#[gpui::test] +async fn test_extension_credential_management(cx: &mut TestAppContext) { + // Test credential storage + // Test credential retrieval + // Test authentication state +} + +#[gpui::test] +async fn test_extension_error_handling(cx: &mut TestAppContext) { + // Test API errors are propagated correctly + // Test rate limiting is handled + // Test network errors are handled +} +``` + +### Integration Tests + +```rust +// crates/extension_host/src/extension_store_test.rs (additions) + +#[gpui::test] +async fn test_llm_extension_lifecycle(cx: &mut TestAppContext) { + // Install extension with LLM provider + // Verify provider registered + // Configure credentials + // Make completion request + // Uninstall extension + // Verify provider unregistered +} +``` + +### Manual Testing Checklist + +1. **Provider Discovery** + - [ ] Extension provider appears in model selector + - [ ] Provider icon displays correctly + - [ ] Models list correctly + +2. **Authentication** + - [ ] API key prompt appears when not authenticated + - [ ] API key is stored securely + - [ ] Environment variable fallback works + - [ ] "Reset credentials" works + +3. **Completions** + - [ ] Basic text completion works + - [ ] Streaming is smooth (no jank) + - [ ] Long responses complete successfully + - [ ] Cancellation works + +4. **Advanced Features** + - [ ] Tool calling works (Agent panel) + - [ ] Image inputs work (if supported) + - [ ] Thinking/reasoning displays correctly + +5. **Error Handling** + - [ ] Invalid API key shows error + - [ ] Rate limiting shows appropriate message + - [ ] Network errors are handled gracefully + +6. **Performance** + - [ ] First token latency acceptable (<500ms overhead) + - [ ] Memory usage reasonable + - [ ] No memory leaks on repeated requests + +--- + +## Security Considerations + +### Credential Handling + +1. **Never expose raw credentials to WASM** + - Extensions request credentials via import function + - Zed stores credentials in secure storage (keychain/credential manager) + - Extensions receive only "authenticated: true/false" status + +2. **Credential scope isolation** + - Each extension has its own credential namespace + - Extensions cannot access other extensions' credentials + - Provider ID is prefixed with extension ID + +3. **Audit logging** + - Log when credentials are accessed (not the values) + - Log when credentials are modified + +### Network Access + +1. **HTTP request validation** + - Extensions already have HTTP access via `fetch` / `fetch-stream` + - Consider domain allowlisting for LLM providers + - Log outbound requests for debugging + +2. **Request/Response inspection** + - API keys in headers should be redacted in logs + - Response bodies may contain sensitive data + +### Extension Sandbox + +1. **WASM isolation** + - Extensions run in WASM sandbox + - Cannot access filesystem outside work directory + - Cannot access other extensions' data + +2. **Resource limits** + - Memory limits per extension + - CPU time limits (epoch-based interruption already exists) + - Concurrent request limits + +### Capability Requirements + +```toml +# Extensions with LLM providers should declare: +[[capabilities]] +kind = "network:http" +domains = ["api.example.com"] # Optional domain restriction + +[[capabilities]] +kind = "credential:store" +``` + +--- + +## Appendix: Provider-Specific Requirements + +### A. Anthropic Implementation Details + +**Request Format:** +```json +{ + "model": "claude-sonnet-4-20250514", + "max_tokens": 8192, + "messages": [ + {"role": "user", "content": [{"type": "text", "text": "Hello"}]} + ], + "system": [{"type": "text", "text": "You are helpful"}], + "tools": [...], + "thinking": {"type": "enabled", "budget_tokens": 10000} +} +``` + +**SSE Events:** +- `message_start` - Contains message ID, model, usage +- `content_block_start` - Starts text/tool_use/thinking block +- `content_block_delta` - Incremental content (text_delta, input_json_delta, thinking_delta) +- `content_block_stop` - Block complete +- `message_delta` - Stop reason, final usage +- `message_stop` - End of message + +**Special Considerations:** +- Beta headers for thinking: `anthropic-beta: interleaved-thinking-2025-05-14` +- Cache control markers in messages +- Thought signatures on tool uses + +### B. OpenAI Implementation Details + +**Request Format:** +```json +{ + "model": "gpt-4o", + "messages": [ + {"role": "system", "content": "You are helpful"}, + {"role": "user", "content": "Hello"} + ], + "stream": true, + "tools": [...], + "max_completion_tokens": 4096 +} +``` + +**SSE Events:** +``` +data: {"choices":[{"delta":{"content":"Hello"}}]} +data: {"choices":[{"delta":{"tool_calls":[...]}}]} +data: [DONE] +``` + +**Special Considerations:** +- `reasoning_effort` for o-series models +- `parallel_tool_calls` option +- Token counting via tiktoken + +### C. Google/Gemini Implementation Details + +**Request Format:** +```json +{ + "contents": [ + {"role": "user", "parts": [{"text": "Hello"}]} + ], + "generationConfig": { + "maxOutputTokens": 8192, + "temperature": 0.7 + }, + "tools": [...] +} +``` + +**Response Format:** +```json +{ + "candidates": [{ + "content": { + "parts": [ + {"text": "Response"}, + {"functionCall": {"name": "...", "args": {...}}} + ] + } + }] +} +``` + +**Special Considerations:** +- Different streaming format (not SSE, line-delimited JSON) +- Tool signatures in function calls +- Thinking support similar to Anthropic + +### D. OpenAI-Compatible Providers (Ollama, LM Studio, DeepSeek) + +These providers can share common implementation: + +**Shared Code:** +```rust +// In extension +fn stream_openai_compatible( + api_url: &str, + api_key: Option<&str>, + request: CompletionRequest, +) -> Result { + let request_body = build_openai_request(request); + let stream = http_client::fetch_stream(HttpRequest { + method: HttpMethod::Post, + url: format!("{}/v1/chat/completions", api_url), + headers: build_headers(api_key), + body: Some(serde_json::to_vec(&request_body)?), + redirect_policy: RedirectPolicy::NoFollow, + })?; + + Ok(OpenAiStreamParser::new(stream)) +} +``` + +### E. Example Extension: Simple OpenAI-Compatible Provider + +```rust +// src/my_provider.rs +use zed_extension_api::{self as zed, Result}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, RedirectPolicy}; + +struct MyLlmExtension { + api_key: Option, +} + +impl zed::Extension for MyLlmExtension { + fn new() -> Self { + Self { api_key: None } + } + + fn llm_providers(&self) -> Vec { + vec![zed::LlmProviderInfo { + id: "my-provider".into(), + name: "My LLM Provider".into(), + icon: Some("sparkle".into()), + }] + } + + fn llm_provider_models(&self, provider_id: &str) -> Result> { + Ok(vec![ + zed::LlmModelInfo { + id: "my-model".into(), + name: "My Model".into(), + max_token_count: 128000, + max_output_tokens: Some(4096), + capabilities: zed::LlmModelCapabilities { + supports_images: true, + supports_tools: true, + ..Default::default() + }, + is_default: true, + is_default_fast: false, + } + ]) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + self.api_key.is_some() || std::env::var("MY_API_KEY").is_ok() + } + + fn llm_provider_authenticate(&mut self, provider_id: &str) -> Result<()> { + if let Some(key) = zed::llm_get_credential(provider_id)? { + self.api_key = Some(key); + return Ok(()); + } + + if zed::llm_request_credential( + provider_id, + zed::CredentialType::ApiKey, + "API Key", + "Enter your API key", + )? { + self.api_key = zed::llm_get_credential(provider_id)?; + } + + Ok(()) + } + + fn llm_stream_completion( + &self, + provider_id: &str, + model_id: &str, + request: zed::LlmCompletionRequest, + ) -> Result { + let api_key = self.api_key.as_ref() + .or_else(|| std::env::var("MY_API_KEY").ok().as_ref()) + .ok_or("Not authenticated")?; + + let body = serde_json::json!({ + "model": model_id, + "messages": self.convert_messages(&request.messages), + "stream": true, + "max_tokens": request.max_tokens.unwrap_or(4096), + }); + + let stream = HttpRequest::builder() + .method(HttpMethod::Post) + .url("https://api.my-provider.com/v1/chat/completions") + .header("Authorization", format!("Bearer {}", api_key)) + .header("Content-Type", "application/json") + .body(serde_json::to_vec(&body)?) + .build()? + .fetch_stream()?; + + Ok(zed::LlmCompletionStream::new(OpenAiStreamParser::new(stream))) + } +} + +zed::register_extension!(MyLlmExtension); +``` + +--- + +## Timeline Summary + +| Phase | Duration | Key Deliverables | +|-------|----------|------------------| +| 1. Foundation | 2-3 weeks | WIT interface, basic provider registration | +| 2. Streaming | 2-3 weeks | Efficient streaming across WASM boundary | +| 3. Full Features | 2-3 weeks | Tools, images, thinking support | +| 4. Credentials & UI | 1-2 weeks | Secure credentials, configuration UI | +| 5. Testing & Docs | 1-2 weeks | Tests, documentation, examples | +| 6. Migration (optional) | Ongoing | Migrate built-in providers | + +**Total estimated time: 8-13 weeks** + +--- + +## Open Questions + +1. **Streaming efficiency**: Is callback-based streaming feasible in WASM, or should we use polling? + +2. **Token counting**: Should we require extensions to implement token counting, or provide a fallback estimation? + +3. **Configuration UI**: Should extensions be able to provide custom UI components, or just JSON schema-driven forms? + +4. **Provider priorities**: Should extension providers appear before or after built-in providers in the selector? + +5. **Backward compatibility**: How do we handle extensions built against older WIT versions when adding new LLM features? + +6. **Rate limiting**: Should the host help with rate limiting, or leave it entirely to extensions? + +--- + +## Conclusion + +This plan provides a comprehensive roadmap for implementing Language Model Provider Extensions in Zed. The phased approach allows for incremental delivery of value while building toward full feature parity with built-in providers. + +The key architectural decisions are: +1. **WIT-based interface** for WASM interop, consistent with existing extension patterns +2. **Streaming via resources** to minimize WASM boundary crossing overhead +3. **Host-managed credentials** for security +4. **Manifest-based discovery** for static model information + +The migration analysis shows that simpler providers (Ollama, LM Studio) can be migrated first as proof of concept, while more complex providers (Anthropic, Bedrock) may remain built-in initially. \ No newline at end of file diff --git a/extensions/test-extension/extension.toml b/extensions/test-extension/extension.toml index 0cb5afac7f7031..3c359ade72adad 100644 --- a/extensions/test-extension/extension.toml +++ b/extensions/test-extension/extension.toml @@ -22,4 +22,4 @@ args = ["hello from a child process!"] [[capabilities]] kind = "process:exec" command = "cmd" -args = ["/C", "echo", "hello from a child process!"] +args = ["/C", "echo", "hello from a child process!"] \ No newline at end of file From d7caae30de9520b8110e01f13e0c66de9d7e7eff Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 3 Dec 2025 13:00:53 -0500 Subject: [PATCH 02/45] Fix auth and subscriptions for provider extensions --- crates/agent/src/agent.rs | 27 ++++ crates/agent_ui/src/agent_ui.rs | 34 +++- .../agent_ui/src/language_model_selector.rs | 153 +++++++++++++++--- crates/extension/src/extension_host_proxy.rs | 5 + crates/extension_host/src/extension_host.rs | 67 ++++++-- crates/extension_host/src/wasm_host.rs | 6 - .../src/wasm_host/llm_provider.rs | 13 +- crates/language_model/src/registry.rs | 31 +++- crates/language_models/src/extension.rs | 10 +- 9 files changed, 298 insertions(+), 48 deletions(-) diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 404cd6549e5786..fdfcf8ca5863ff 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -103,12 +103,22 @@ impl LanguageModels { } fn refresh_list(&mut self, cx: &App) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!("[{}ms] LanguageModels::refresh_list called", now); let providers = LanguageModelRegistry::global(cx) .read(cx) .providers() .into_iter() .filter(|provider| provider.is_authenticated(cx)) .collect::>(); + eprintln!( + "[{}ms] LanguageModels::refresh_list got {} authenticated providers", + now, + providers.len() + ); let mut language_model_list = IndexMap::default(); let mut recommended_models = HashSet::default(); @@ -146,6 +156,15 @@ impl LanguageModels { self.models = models; self.model_list = acp_thread::AgentModelList::Grouped(language_model_list); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] LanguageModels::refresh_list completed with {} models in list", + now, + self.models.len() + ); self.refresh_models_tx.send(()).ok(); } @@ -603,6 +622,14 @@ impl NativeAgent { _event: &language_model::Event, cx: &mut Context, ) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] NativeAgent::handle_models_updated_event called", + now + ); self.models.refresh_list(cx); let registry = LanguageModelRegistry::read_global(cx); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 5f5682b7dcc90d..f5fd35f5a7fe0b 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -344,9 +344,37 @@ fn init_language_model_settings(cx: &mut App) { cx.subscribe( &LanguageModelRegistry::global(cx), |_, event: &language_model::Event, cx| match event { - language_model::Event::ProviderStateChanged(_) - | language_model::Event::AddedProvider(_) - | language_model::Event::RemovedProvider(_) => { + language_model::Event::ProviderStateChanged(id) => { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] agent_ui global subscription: ProviderStateChanged for {:?}", + now, id + ); + update_active_language_model_from_settings(cx); + } + language_model::Event::AddedProvider(id) => { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] agent_ui global subscription: AddedProvider for {:?}", + now, id + ); + update_active_language_model_from_settings(cx); + } + language_model::Event::RemovedProvider(id) => { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] agent_ui global subscription: RemovedProvider for {:?}", + now, id + ); update_active_language_model_from_settings(cx); } _ => {} diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 5b5a4513c6dca3..05384b4a93083b 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -1,10 +1,9 @@ use std::{cmp::Reverse, sync::Arc}; use collections::IndexMap; +use futures::{StreamExt, channel::mpsc}; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task, -}; +use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Task}; use language_model::{ AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, @@ -47,7 +46,13 @@ pub fn language_model_selector( } fn all_models(cx: &App) -> GroupedModels { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!("[{}ms] all_models() called", now); let providers = LanguageModelRegistry::global(cx).read(cx).providers(); + eprintln!("[{}ms] all_models: got {} providers", now, providers.len()); let recommended = providers .iter() @@ -62,19 +67,41 @@ fn all_models(cx: &App) -> GroupedModels { }) .collect(); - let all = providers + let all: Vec = providers .iter() .flat_map(|provider| { - provider - .provided_models(cx) - .into_iter() - .map(|model| ModelInfo { - model, - icon: provider.icon(), - }) + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] all_models: calling provided_models for {:?}", + now, + provider.id() + ); + let models = provider.provided_models(cx); + eprintln!( + "[{}ms] all_models: provider {:?} returned {} models", + now, + provider.id(), + models.len() + ); + models.into_iter().map(|model| ModelInfo { + model, + icon: provider.icon(), + }) }) .collect(); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] all_models: returning {} total models", + now, + all.len() + ); GroupedModels::new(all, recommended) } @@ -91,7 +118,7 @@ pub struct LanguageModelPickerDelegate { filtered_entries: Vec, selected_index: usize, _authenticate_all_providers_task: Task<()>, - _subscriptions: Vec, + _refresh_models_task: Task<()>, popover_styles: bool, focus_handle: FocusHandle, } @@ -105,8 +132,18 @@ impl LanguageModelPickerDelegate { window: &mut Window, cx: &mut Context>, ) -> Self { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!("[{}ms] LanguageModelPickerDelegate::new() called", now); let on_model_changed = Arc::new(on_model_changed); let models = all_models(cx); + eprintln!( + "[{}ms] LanguageModelPickerDelegate::new() got {} models from all_models()", + now, + models.all.len() + ); let entries = models.entries(); Self { @@ -116,24 +153,88 @@ impl LanguageModelPickerDelegate { filtered_entries: entries, get_active_model: Arc::new(get_active_model), _authenticate_all_providers_task: Self::authenticate_all_providers(cx), - _subscriptions: vec![cx.subscribe_in( - &LanguageModelRegistry::global(cx), - window, - |picker, _, event, window, cx| { + _refresh_models_task: { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] LanguageModelPickerDelegate::new() setting up refresh task for LanguageModelRegistry", + now + ); + + // Create a channel to signal when models need refreshing + let (refresh_tx, mut refresh_rx) = mpsc::unbounded::<()>(); + + // Subscribe to registry events and send refresh signals through the channel + let registry = LanguageModelRegistry::global(cx); + eprintln!( + "[{}ms] LanguageModelPickerDelegate::new() subscribing to registry entity_id: {:?}", + now, + registry.entity_id() + ); + cx.subscribe(®istry, move |_picker, _, event, _cx| { match event { - language_model::Event::ProviderStateChanged(_) - | language_model::Event::AddedProvider(_) - | language_model::Event::RemovedProvider(_) => { - let query = picker.query(cx); - picker.delegate.all_models = Arc::new(all_models(cx)); - // Update matches will automatically drop the previous task - // if we get a provider event again - picker.update_matches(query, window, cx) + language_model::Event::ProviderStateChanged(id) => { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] LanguageModelSelector: ProviderStateChanged event for {:?}, sending refresh signal", + now, id + ); + refresh_tx.unbounded_send(()).ok(); + } + language_model::Event::AddedProvider(id) => { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] LanguageModelSelector: AddedProvider event for {:?}, sending refresh signal", + now, id + ); + refresh_tx.unbounded_send(()).ok(); + } + language_model::Event::RemovedProvider(id) => { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] LanguageModelSelector: RemovedProvider event for {:?}, sending refresh signal", + now, id + ); + refresh_tx.unbounded_send(()).ok(); } _ => {} } - }, - )], + }) + .detach(); + + // Spawn a task that listens for refresh signals and updates the picker + cx.spawn_in(window, async move |this, cx| { + while let Some(()) = refresh_rx.next().await { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] LanguageModelSelector: refresh signal received, updating models", + now + ); + let result = this.update_in(cx, |picker, window, cx| { + picker.delegate.all_models = Arc::new(all_models(cx)); + picker.refresh(window, cx); + }); + if result.is_err() { + // Picker was dropped, exit the loop + break; + } + } + }) + }, popover_styles, focus_handle, } diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index dc395d6a937c7b..6fe9e1f8084cf5 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -414,9 +414,14 @@ impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy { cx: &mut App, ) { let Some(proxy) = self.language_model_provider_proxy.read().clone() else { + eprintln!( + "Failed to register LLM provider '{}': language_model_provider_proxy not yet initialized", + provider_id + ); return; }; + eprintln!("Registering LLM provider: {}", provider_id); proxy.register_language_model_provider(provider_id, register_fn, cx) } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index f399354ecec6a1..1fd568e9063817 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1364,7 +1364,7 @@ impl ExtensionStore { let mut wasm_extensions: Vec<( Arc, WasmExtension, - Vec<(LlmProviderInfo, Vec)>, + Vec<(LlmProviderInfo, Vec, bool)>, )> = Vec::new(); for extension in extension_entries { if extension.manifest.lib.kind.is_none() { @@ -1384,8 +1384,14 @@ impl ExtensionStore { match wasm_extension { Ok(wasm_extension) => { // Query for LLM providers if the manifest declares any + // Tuple is (provider_info, models, is_authenticated) let mut llm_providers_with_models = Vec::new(); if !extension.manifest.language_model_providers.is_empty() { + eprintln!( + "Extension {} declares {} LLM providers in manifest, querying...", + extension.manifest.id, + extension.manifest.language_model_providers.len() + ); let providers_result = wasm_extension .call(|ext, store| { async move { ext.call_llm_providers(store).await }.boxed() @@ -1393,6 +1399,11 @@ impl ExtensionStore { .await; if let Ok(Ok(providers)) = providers_result { + eprintln!( + "Extension {} returned {} LLM providers", + extension.manifest.id, + providers.len() + ); for provider_info in providers { let models_result = wasm_extension .call({ @@ -1410,7 +1421,7 @@ impl ExtensionStore { let models: Vec = match models_result { Ok(Ok(Ok(models))) => models, Ok(Ok(Err(e))) => { - log::error!( + eprintln!( "Failed to get models for LLM provider {} in extension {}: {}", provider_info.id, extension.manifest.id, @@ -1419,7 +1430,7 @@ impl ExtensionStore { Vec::new() } Ok(Err(e)) => { - log::error!( + eprintln!( "Wasm error calling llm_provider_models for {} in extension {}: {:?}", provider_info.id, extension.manifest.id, @@ -1428,7 +1439,7 @@ impl ExtensionStore { Vec::new() } Err(e) => { - log::error!( + eprintln!( "Extension call failed for llm_provider_models {} in extension {}: {:?}", provider_info.id, extension.manifest.id, @@ -1438,8 +1449,40 @@ impl ExtensionStore { } }; - llm_providers_with_models.push((provider_info, models)); + // Query initial authentication state + let is_authenticated = wasm_extension + .call({ + let provider_id = provider_info.id.clone(); + |ext, store| { + async move { + ext.call_llm_provider_is_authenticated( + store, + &provider_id, + ) + .await + } + .boxed() + } + }) + .await + .unwrap_or(Ok(false)) + .unwrap_or(false); + + eprintln!( + "LLM provider {} has {} models, is_authenticated={}", + provider_info.id, + models.len(), + is_authenticated + ); + llm_providers_with_models + .push((provider_info, models, is_authenticated)); } + } else { + eprintln!( + "Failed to get LLM providers from extension {}: {:?}", + extension.manifest.id, + providers_result + ); } } @@ -1522,28 +1565,34 @@ impl ExtensionStore { } // Register LLM providers - for (provider_info, models) in llm_providers_with_models { + for (provider_info, models, is_authenticated) in llm_providers_with_models { let provider_id: Arc = format!("{}:{}", manifest.id, provider_info.id).into(); - let wasm_ext = wasm_extension.clone(); + let wasm_ext = extension.as_ref().clone(); let pinfo = provider_info.clone(); let mods = models.clone(); + let auth = *is_authenticated; this.proxy.register_language_model_provider( - provider_id, + provider_id.clone(), Box::new(move |cx: &mut App| { + eprintln!("register_fn closure called, creating provider"); let provider = Arc::new(ExtensionLanguageModelProvider::new( - wasm_ext, pinfo, mods, cx, + wasm_ext, pinfo, mods, auth, cx, )); + eprintln!("Provider created, registering with registry"); language_model::LanguageModelRegistry::global(cx).update( cx, |registry, cx| { + eprintln!("Inside registry.register_provider"); registry.register_provider(provider, cx); }, ); + eprintln!("Provider registered"); }), cx, ); + eprintln!("register_language_model_provider call completed for {}", provider_id); } } diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index cd0b99cc02499b..93b8a1de9b723e 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -73,12 +73,6 @@ pub struct WasmExtension { _task: Arc>>, } -impl Drop for WasmExtension { - fn drop(&mut self) { - self.tx.close_channel(); - } -} - #[async_trait] impl extension::Extension for WasmExtension { fn manifest(&self) -> Arc { diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 7e98e0b400ef4d..02cc9722f3ca8a 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -40,10 +40,11 @@ impl ExtensionLanguageModelProvider { extension: WasmExtension, provider_info: LlmProviderInfo, models: Vec, + is_authenticated: bool, cx: &mut App, ) -> Self { let state = cx.new(|_| ExtensionLlmProviderState { - is_authenticated: false, + is_authenticated, available_models: models, }); @@ -61,7 +62,9 @@ impl ExtensionLanguageModelProvider { impl LanguageModelProvider for ExtensionLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { - LanguageModelProviderId::from(self.provider_id_string()) + let id = LanguageModelProviderId::from(self.provider_id_string()); + eprintln!("ExtensionLanguageModelProvider::id() -> {:?}", id); + id } fn name(&self) -> LanguageModelProviderName { @@ -111,10 +114,16 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider { fn provided_models(&self, cx: &App) -> Vec> { let state = self.state.read(cx); + eprintln!( + "ExtensionLanguageModelProvider::provided_models called for {}, returning {} models", + self.provider_info.name, + state.available_models.len() + ); state .available_models .iter() .map(|model_info| { + eprintln!(" - model: {}", model_info.name); Arc::new(ExtensionLanguageModel { extension: self.extension.clone(), model_info: model_info.clone(), diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 6ed8bf07c4e976..e7b511e8d9de08 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -153,7 +153,19 @@ impl LanguageModelRegistry { } self.providers.insert(id.clone(), provider); - cx.emit(Event::AddedProvider(id)); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] LanguageModelRegistry: About to emit AddedProvider event for {:?}", + now, id + ); + cx.emit(Event::AddedProvider(id.clone())); + eprintln!( + "[{}ms] LanguageModelRegistry: Emitted AddedProvider event for {:?}", + now, id + ); } pub fn unregister_provider(&mut self, id: LanguageModelProviderId, cx: &mut Context) { @@ -163,6 +175,18 @@ impl LanguageModelRegistry { } pub fn providers(&self) -> Vec> { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + eprintln!( + "[{}ms] LanguageModelRegistry::providers() called, {} providers in registry", + now, + self.providers.len() + ); + for (id, _) in &self.providers { + eprintln!(" - provider: {:?}", id); + } let zed_provider_id = LanguageModelProviderId("zed.dev".into()); let mut providers = Vec::with_capacity(self.providers.len()); if let Some(provider) = self.providers.get(&zed_provider_id) { @@ -175,6 +199,11 @@ impl LanguageModelRegistry { None } })); + eprintln!( + "[{}ms] LanguageModelRegistry::providers() returning {} providers", + now, + providers.len() + ); providers } diff --git a/crates/language_models/src/extension.rs b/crates/language_models/src/extension.rs index 139ff632657cd8..e17e581fef6baf 100644 --- a/crates/language_models/src/extension.rs +++ b/crates/language_models/src/extension.rs @@ -18,11 +18,19 @@ impl ExtensionLanguageModelProxy { impl ExtensionLanguageModelProviderProxy for ExtensionLanguageModelProxy { fn register_language_model_provider( &self, - _provider_id: Arc, + provider_id: Arc, register_fn: LanguageModelProviderRegistration, cx: &mut App, ) { + eprintln!( + "ExtensionLanguageModelProxy::register_language_model_provider called for {}", + provider_id + ); register_fn(cx); + eprintln!( + "ExtensionLanguageModelProxy::register_language_model_provider completed for {}", + provider_id + ); } fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App) { From e5ce32e93665b306f164f9b216fdbbe48c898227 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 3 Dec 2025 14:41:39 -0500 Subject: [PATCH 03/45] Add provider extension API key in settings --- Cargo.lock | 4 + crates/extension_api/src/extension_api.rs | 10 + .../wit/since_v0.7.0/extension.wit | 4 + crates/extension_host/Cargo.toml | 5 + .../src/wasm_host/llm_provider.rs | 757 ++++++++++++------ crates/extension_host/src/wasm_host/wit.rs | 14 + 6 files changed, 558 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a1cf2518639b2..8ac867d8a78cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5905,9 +5905,11 @@ dependencies = [ "async-trait", "client", "collections", + "credentials_provider", "criterion", "ctor", "dap", + "editor", "extension", "fs", "futures 0.3.31", @@ -5919,6 +5921,8 @@ dependencies = [ "language_model", "log", "lsp", + "markdown", + "menu", "moka", "node_runtime", "parking_lot", diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 5b548b8e45f282..daafb63c278cac 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -288,6 +288,12 @@ pub trait Extension: Send + Sync { Ok(Vec::new()) } + /// Returns markdown content to display in the provider's settings UI. + /// This can include setup instructions, links to documentation, etc. + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + None + } + /// Check if the provider is authenticated. fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { false @@ -618,6 +624,10 @@ impl wit::Guest for Component { extension().llm_provider_models(&provider_id) } + fn llm_provider_settings_markdown(provider_id: String) -> Option { + extension().llm_provider_settings_markdown(&provider_id) + } + fn llm_provider_is_authenticated(provider_id: String) -> bool { extension().llm_provider_is_authenticated(&provider_id) } diff --git a/crates/extension_api/wit/since_v0.7.0/extension.wit b/crates/extension_api/wit/since_v0.7.0/extension.wit index 265bb922a43c03..92979a87800397 100644 --- a/crates/extension_api/wit/since_v0.7.0/extension.wit +++ b/crates/extension_api/wit/since_v0.7.0/extension.wit @@ -180,6 +180,10 @@ world extension { /// Returns the models available for a provider. export llm-provider-models: func(provider-id: string) -> result, string>; + /// Returns markdown content to display in the provider's settings UI. + /// This can include setup instructions, links to documentation, etc. + export llm-provider-settings-markdown: func(provider-id: string) -> option; + /// Check if the provider is authenticated. export llm-provider-is-authenticated: func(provider-id: string) -> bool; diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 46c481ee53babe..a5c9357b9c80b7 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -22,7 +22,9 @@ async-tar.workspace = true async-trait.workspace = true client.workspace = true collections.workspace = true +credentials_provider.workspace = true dap.workspace = true +editor.workspace = true extension.workspace = true fs.workspace = true futures.workspace = true @@ -32,7 +34,9 @@ http_client.workspace = true language.workspace = true language_model.workspace = true log.workspace = true +markdown.workspace = true lsp.workspace = true +menu.workspace = true moka.workspace = true node_runtime.workspace = true paths.workspace = true @@ -47,6 +51,7 @@ settings.workspace = true task.workspace = true telemetry.workspace = true tempfile.workspace = true +theme.workspace = true toml.workspace = true ui.workspace = true url.workspace = true diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 02cc9722f3ca8a..e55e8b05938f4a 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -7,10 +7,16 @@ use crate::wasm_host::wit::{ LlmToolUse, }; use anyhow::{Result, anyhow}; +use credentials_provider::CredentialsProvider; +use editor::Editor; use futures::future::BoxFuture; use futures::stream::BoxStream; use futures::{FutureExt, StreamExt}; -use gpui::{AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, Window}; +use gpui::Focusable; +use gpui::{ + AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, + TextStyleRefinement, UnderlineStyle, Window, px, +}; use language_model::tool_schema::LanguageModelToolSchemaFormat; use language_model::{ AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, @@ -19,7 +25,12 @@ use language_model::{ LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolUse, LanguageModelToolUseId, StopReason, TokenUsage, }; +use markdown::{Markdown, MarkdownElement, MarkdownStyle}; +use settings::Settings; use std::sync::Arc; +use theme::ThemeSettings; +use ui::{Label, LabelSize, prelude::*}; +use util::ResultExt as _; /// An extension-based language model provider. pub struct ExtensionLanguageModelProvider { @@ -58,13 +69,16 @@ impl ExtensionLanguageModelProvider { fn provider_id_string(&self) -> String { format!("{}:{}", self.extension.manifest.id, self.provider_info.id) } + + /// The credential key used for storing the API key in the system keychain. + fn credential_key(&self) -> String { + format!("extension-llm-{}", self.provider_id_string()) + } } impl LanguageModelProvider for ExtensionLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { - let id = LanguageModelProviderId::from(self.provider_id_string()); - eprintln!("ExtensionLanguageModelProvider::id() -> {:?}", id); - id + LanguageModelProviderId::from(self.provider_id_string()) } fn name(&self) -> LanguageModelProviderName { @@ -99,8 +113,6 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider { .available_models .iter() .find(|m| m.is_default_fast) - .or_else(|| state.available_models.iter().find(|m| m.is_default)) - .or_else(|| state.available_models.first()) .map(|model_info| { Arc::new(ExtensionLanguageModel { extension: self.extension.clone(), @@ -114,16 +126,10 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider { fn provided_models(&self, cx: &App) -> Vec> { let state = self.state.read(cx); - eprintln!( - "ExtensionLanguageModelProvider::provided_models called for {}, returning {} models", - self.provider_info.name, - state.available_models.len() - ); state .available_models .iter() .map(|model_info| { - eprintln!(" - model: {}", model_info.name); Arc::new(ExtensionLanguageModel { extension: self.extension.clone(), model_info: model_info.clone(), @@ -175,18 +181,43 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider { fn configuration_view( &self, _target_agent: ConfigurationViewTargetAgent, - _window: &mut Window, + window: &mut Window, cx: &mut App, ) -> AnyView { - cx.new(|_| EmptyConfigView).into() + let credential_key = self.credential_key(); + let extension = self.extension.clone(); + let extension_provider_id = self.provider_info.id.clone(); + let state = self.state.clone(); + + cx.new(|cx| { + ExtensionProviderConfigurationView::new( + credential_key, + extension, + extension_provider_id, + state, + window, + cx, + ) + }) + .into() } fn reset_credentials(&self, cx: &mut App) -> Task> { let extension = self.extension.clone(); let provider_id = self.provider_info.id.clone(); let state = self.state.clone(); + let credential_key = self.credential_key(); + + let credentials_provider = ::global(cx); cx.spawn(async move |cx| { + // Delete from system keychain + credentials_provider + .delete_credentials(&credential_key, cx) + .await + .log_err(); + + // Call extension's reset_credentials let result = extension .call(|extension, store| { async move { @@ -198,15 +229,15 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider { }) .await; + // Update state + cx.update(|cx| { + state.update(cx, |state, _| { + state.is_authenticated = false; + }); + })?; + match result { - Ok(Ok(Ok(()))) => { - cx.update(|cx| { - state.update(cx, |state, _| { - state.is_authenticated = false; - }); - })?; - Ok(()) - } + Ok(Ok(Ok(()))) => Ok(()), Ok(Ok(Err(e))) => Err(anyhow!("{}", e)), Ok(Err(e)) => Err(e), Err(e) => Err(e), @@ -226,20 +257,302 @@ impl LanguageModelProviderState for ExtensionLanguageModelProvider { &self, cx: &mut Context, callback: impl Fn(&mut T, &mut Context) + 'static, - ) -> Option { + ) -> Option { Some(cx.subscribe(&self.state, move |this, _, _, cx| callback(this, cx))) } } -struct EmptyConfigView; +/// Configuration view for extension-based LLM providers. +struct ExtensionProviderConfigurationView { + credential_key: String, + extension: WasmExtension, + extension_provider_id: String, + state: Entity, + settings_markdown: Option>, + api_key_editor: Entity, + loading_settings: bool, + loading_credentials: bool, + _subscriptions: Vec, +} + +impl ExtensionProviderConfigurationView { + fn new( + credential_key: String, + extension: WasmExtension, + extension_provider_id: String, + state: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + // Subscribe to state changes + let state_subscription = cx.subscribe(&state, |_, _, _, cx| { + cx.notify(); + }); + + // Create API key editor + let api_key_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_placeholder_text("Enter API key...", window, cx); + editor + }); + + let mut this = Self { + credential_key, + extension, + extension_provider_id, + state, + settings_markdown: None, + api_key_editor, + loading_settings: true, + loading_credentials: true, + _subscriptions: vec![state_subscription], + }; + + // Load settings text from extension + this.load_settings_text(cx); + + // Load existing credentials + this.load_credentials(cx); + + this + } + + fn load_settings_text(&mut self, cx: &mut Context) { + let extension = self.extension.clone(); + let provider_id = self.extension_provider_id.clone(); + + cx.spawn(async move |this, cx| { + let result = extension + .call({ + let provider_id = provider_id.clone(); + |ext, store| { + async move { + ext.call_llm_provider_settings_markdown(store, &provider_id) + .await + } + .boxed() + } + }) + .await; + + let settings_text = result.ok().and_then(|inner| inner.ok()).flatten(); + + this.update(cx, |this, cx| { + this.loading_settings = false; + if let Some(text) = settings_text { + let markdown = cx.new(|cx| Markdown::new(text.into(), None, None, cx)); + this.settings_markdown = Some(markdown); + } + cx.notify(); + }) + .log_err(); + }) + .detach(); + } + + fn load_credentials(&mut self, cx: &mut Context) { + let credential_key = self.credential_key.clone(); + let credentials_provider = ::global(cx); + let state = self.state.clone(); + + cx.spawn(async move |this, cx| { + let credentials = credentials_provider + .read_credentials(&credential_key, cx) + .await + .log_err() + .flatten(); + + let has_credentials = credentials.is_some(); + + // Update authentication state based on stored credentials + let _ = cx.update(|cx| { + state.update(cx, |state, cx| { + state.is_authenticated = has_credentials; + cx.notify(); + }); + }); + + this.update(cx, |this, cx| { + this.loading_credentials = false; + cx.notify(); + }) + .log_err(); + }) + .detach(); + } + + fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { + let api_key = self.api_key_editor.read(cx).text(cx); + if api_key.is_empty() { + return; + } + + // Clear the editor + self.api_key_editor + .update(cx, |editor, cx| editor.set_text("", window, cx)); + + let credential_key = self.credential_key.clone(); + let credentials_provider = ::global(cx); + let state = self.state.clone(); + + cx.spawn(async move |_this, cx| { + // Store in system keychain + credentials_provider + .write_credentials(&credential_key, "Bearer", api_key.as_bytes(), cx) + .await + .log_err(); + + // Update state to authenticated + let _ = cx.update(|cx| { + state.update(cx, |state, cx| { + state.is_authenticated = true; + cx.notify(); + }); + }); + }) + .detach(); + } + + fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context) { + // Clear the editor + self.api_key_editor + .update(cx, |editor, cx| editor.set_text("", window, cx)); + + let credential_key = self.credential_key.clone(); + let credentials_provider = ::global(cx); + let state = self.state.clone(); + + cx.spawn(async move |_this, cx| { + // Delete from system keychain + credentials_provider + .delete_credentials(&credential_key, cx) + .await + .log_err(); + + // Update state to unauthenticated + let _ = cx.update(|cx| { + state.update(cx, |state, cx| { + state.is_authenticated = false; + cx.notify(); + }); + }); + }) + .detach(); + } + + fn is_authenticated(&self, cx: &Context) -> bool { + self.state.read(cx).is_authenticated + } +} + +impl gpui::Render for ExtensionProviderConfigurationView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_loading = self.loading_settings || self.loading_credentials; + let is_authenticated = self.is_authenticated(cx); + + if is_loading { + return v_flex() + .gap_2() + .child(Label::new("Loading...").color(Color::Muted)) + .into_any_element(); + } + + let mut content = v_flex().gap_4().size_full(); + + // Render settings markdown if available + if let Some(markdown) = &self.settings_markdown { + let style = settings_markdown_style(_window, cx); + content = content.child( + div() + .p_2() + .rounded_md() + .bg(cx.theme().colors().surface_background) + .child(MarkdownElement::new(markdown.clone(), style)), + ); + } -impl gpui::Render for EmptyConfigView { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut gpui::Context, - ) -> impl gpui::IntoElement { - gpui::Empty + // Render API key section + if is_authenticated { + content = content.child( + v_flex() + .gap_2() + .child( + h_flex() + .gap_2() + .child( + ui::Icon::new(ui::IconName::Check) + .color(Color::Success) + .size(ui::IconSize::Small), + ) + .child(Label::new("API key configured").color(Color::Success)), + ) + .child( + ui::Button::new("reset-api-key", "Reset API Key") + .style(ui::ButtonStyle::Subtle) + .on_click(cx.listener(|this, _, window, cx| { + this.reset_api_key(window, cx); + })), + ), + ); + } else { + content = content.child( + v_flex() + .gap_2() + .on_action(cx.listener(Self::save_api_key)) + .child( + Label::new("API Key") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(self.api_key_editor.clone()) + .child( + Label::new("Enter your API key and press Enter to save") + .size(LabelSize::Small) + .color(Color::Muted), + ), + ); + } + + content.into_any_element() + } +} + +impl Focusable for ExtensionProviderConfigurationView { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.api_key_editor.focus_handle(cx) + } +} + +fn settings_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { + let theme_settings = ThemeSettings::get_global(cx); + let colors = cx.theme().colors(); + let mut text_style = window.text_style(); + text_style.refine(&TextStyleRefinement { + font_family: Some(theme_settings.ui_font.family.clone()), + font_fallbacks: theme_settings.ui_font.fallbacks.clone(), + font_features: Some(theme_settings.ui_font.features.clone()), + color: Some(colors.text), + ..Default::default() + }); + + MarkdownStyle { + base_text_style: text_style, + selection_background_color: colors.element_selection_background, + inline_code: TextStyleRefinement { + background_color: Some(colors.editor_background), + ..Default::default() + }, + link: TextStyleRefinement { + color: Some(colors.text_accent), + underline: Some(UnderlineStyle { + color: Some(colors.text_accent.opacity(0.5)), + thickness: px(1.), + ..Default::default() + }), + ..Default::default() + }, + syntax: cx.theme().syntax().clone(), + ..Default::default() } } @@ -254,7 +567,7 @@ pub struct ExtensionLanguageModel { impl LanguageModel for ExtensionLanguageModel { fn id(&self) -> LanguageModelId { - LanguageModelId::from(format!("{}:{}", self.provider_id.0, self.model_info.id)) + LanguageModelId::from(self.model_info.id.clone()) } fn name(&self) -> LanguageModelName { @@ -270,7 +583,7 @@ impl LanguageModel for ExtensionLanguageModel { } fn telemetry_id(&self) -> String { - format!("extension:{}", self.model_info.id) + format!("extension-{}", self.model_info.id) } fn supports_images(&self) -> bool { @@ -307,31 +620,33 @@ impl LanguageModel for ExtensionLanguageModel { fn count_tokens( &self, request: LanguageModelRequest, - _cx: &App, + cx: &App, ) -> BoxFuture<'static, Result> { let extension = self.extension.clone(); let provider_id = self.provider_info.id.clone(); let model_id = self.model_info.id.clone(); - async move { - let wit_request = convert_request_to_wit(&request); - - let result = extension - .call(|ext, store| { - async move { - ext.call_llm_count_tokens(store, &provider_id, &model_id, &wit_request) - .await + let wit_request = convert_request_to_wit(request); + + cx.background_spawn(async move { + extension + .call({ + let provider_id = provider_id.clone(); + let model_id = model_id.clone(); + let wit_request = wit_request.clone(); + |ext, store| { + async move { + let count = ext + .call_llm_count_tokens(store, &provider_id, &model_id, &wit_request) + .await? + .map_err(|e| anyhow!("{}", e))?; + Ok(count) + } + .boxed() } - .boxed() }) - .await?; - - match result { - Ok(Ok(count)) => Ok(count), - Ok(Err(e)) => Err(anyhow!("{}", e)), - Err(e) => Err(e), - } - } + .await? + }) .boxed() } @@ -350,68 +665,77 @@ impl LanguageModel for ExtensionLanguageModel { let provider_id = self.provider_info.id.clone(); let model_id = self.model_info.id.clone(); - async move { - let wit_request = convert_request_to_wit(&request); + let wit_request = convert_request_to_wit(request); - // Start the stream and get a stream ID - let outer_result = extension - .call(|ext, store| { - async move { - ext.call_llm_stream_completion_start( - store, - &provider_id, - &model_id, - &wit_request, - ) - .await + async move { + // Start the stream + let stream_id = extension + .call({ + let provider_id = provider_id.clone(); + let model_id = model_id.clone(); + let wit_request = wit_request.clone(); + |ext, store| { + async move { + let id = ext + .call_llm_stream_completion_start( + store, + &provider_id, + &model_id, + &wit_request, + ) + .await? + .map_err(|e| anyhow!("{}", e))?; + Ok(id) + } + .boxed() } - .boxed() }) .await - .map_err(|e| LanguageModelCompletionError::Other(e))?; - - // Unwrap the inner Result> - let inner_result = - outer_result.map_err(|e| LanguageModelCompletionError::Other(anyhow!("{}", e)))?; - - // Get the stream ID - let stream_id = - inner_result.map_err(|e| LanguageModelCompletionError::Other(anyhow!("{}", e)))?; + .map_err(LanguageModelCompletionError::Other)? + .map_err(LanguageModelCompletionError::Other)?; // Create a stream that polls for events let stream = futures::stream::unfold( - (extension, stream_id, false), - |(ext, stream_id, done)| async move { + (extension.clone(), stream_id, false), + move |(extension, stream_id, done)| async move { if done { return None; } - let result = ext + let result = extension .call({ let stream_id = stream_id.clone(); - move |ext, store| { + |ext, store| { async move { - ext.call_llm_stream_completion_next(store, &stream_id).await + let event = ext + .call_llm_stream_completion_next(store, &stream_id) + .await? + .map_err(|e| anyhow!("{}", e))?; + Ok(event) } .boxed() } }) - .await; + .await + .and_then(|inner| inner); match result { - Ok(Ok(Ok(Some(event)))) => { + Ok(Some(event)) => { let converted = convert_completion_event(event); - Some((Ok(converted), (ext, stream_id, false))) + let is_done = + matches!(&converted, Ok(LanguageModelCompletionEvent::Stop(_))); + Some((converted, (extension, stream_id, is_done))) } - Ok(Ok(Ok(None))) => { - // Stream complete - close it - let _ = ext + Ok(None) => { + // Stream complete, close it + let _ = extension .call({ let stream_id = stream_id.clone(); - move |ext, store| { + |ext, store| { async move { ext.call_llm_stream_completion_close(store, &stream_id) - .await + .await?; + Ok::<(), anyhow::Error>(()) } .boxed() } @@ -419,63 +743,10 @@ impl LanguageModel for ExtensionLanguageModel { .await; None } - Ok(Ok(Err(e))) => { - // Extension returned an error - close stream and return error - let _ = ext - .call({ - let stream_id = stream_id.clone(); - move |ext, store| { - async move { - ext.call_llm_stream_completion_close(store, &stream_id) - .await - } - .boxed() - } - }) - .await; - Some(( - Err(LanguageModelCompletionError::Other(anyhow!("{}", e))), - (ext, stream_id, true), - )) - } - Ok(Err(e)) => { - // WASM call error - close stream and return error - let _ = ext - .call({ - let stream_id = stream_id.clone(); - move |ext, store| { - async move { - ext.call_llm_stream_completion_close(store, &stream_id) - .await - } - .boxed() - } - }) - .await; - Some(( - Err(LanguageModelCompletionError::Other(e)), - (ext, stream_id, true), - )) - } - Err(e) => { - // Channel error - close stream and return error - let _ = ext - .call({ - let stream_id = stream_id.clone(); - move |ext, store| { - async move { - ext.call_llm_stream_completion_close(store, &stream_id) - .await - } - .boxed() - } - }) - .await; - Some(( - Err(LanguageModelCompletionError::Other(e)), - (ext, stream_id, true), - )) - } + Err(e) => Some(( + Err(LanguageModelCompletionError::Other(e)), + (extension, stream_id, true), + )), } }, ); @@ -486,87 +757,88 @@ impl LanguageModel for ExtensionLanguageModel { } fn cache_configuration(&self) -> Option { + // Extensions can implement this via llm_cache_configuration None } } -fn convert_request_to_wit(request: &LanguageModelRequest) -> LlmCompletionRequest { - let messages = request +fn convert_request_to_wit(request: LanguageModelRequest) -> LlmCompletionRequest { + use language_model::{MessageContent, Role}; + + let messages: Vec = request .messages - .iter() - .map(|msg| LlmRequestMessage { - role: match msg.role { - language_model::Role::User => LlmMessageRole::User, - language_model::Role::Assistant => LlmMessageRole::Assistant, - language_model::Role::System => LlmMessageRole::System, - }, - content: msg + .into_iter() + .map(|msg| { + let role = match msg.role { + Role::User => LlmMessageRole::User, + Role::Assistant => LlmMessageRole::Assistant, + Role::System => LlmMessageRole::System, + }; + + let content: Vec = msg .content - .iter() - .map(|content| match content { - language_model::MessageContent::Text(text) => { - LlmMessageContent::Text(text.clone()) - } - language_model::MessageContent::Image(image) => { - LlmMessageContent::Image(LlmImageData { - source: image.source.to_string(), - width: Some(image.size.width.0 as u32), - height: Some(image.size.height.0 as u32), - }) - } - language_model::MessageContent::ToolUse(tool_use) => { - LlmMessageContent::ToolUse(LlmToolUse { - id: tool_use.id.to_string(), - name: tool_use.name.to_string(), - input: tool_use.raw_input.clone(), - thought_signature: tool_use.thought_signature.clone(), - }) - } - language_model::MessageContent::ToolResult(result) => { + .into_iter() + .map(|c| match c { + MessageContent::Text(text) => LlmMessageContent::Text(text), + MessageContent::Image(image) => LlmMessageContent::Image(LlmImageData { + source: image.source.to_string(), + width: Some(image.size.width.0 as u32), + height: Some(image.size.height.0 as u32), + }), + MessageContent::ToolUse(tool_use) => LlmMessageContent::ToolUse(LlmToolUse { + id: tool_use.id.to_string(), + name: tool_use.name.to_string(), + input: serde_json::to_string(&tool_use.input).unwrap_or_default(), + thought_signature: tool_use.thought_signature, + }), + MessageContent::ToolResult(tool_result) => { + let content = match tool_result.content { + language_model::LanguageModelToolResultContent::Text(text) => { + LlmToolResultContent::Text(text.to_string()) + } + language_model::LanguageModelToolResultContent::Image(image) => { + LlmToolResultContent::Image(LlmImageData { + source: image.source.to_string(), + width: Some(image.size.width.0 as u32), + height: Some(image.size.height.0 as u32), + }) + } + }; LlmMessageContent::ToolResult(LlmToolResult { - tool_use_id: result.tool_use_id.to_string(), - tool_name: result.tool_name.to_string(), - is_error: result.is_error, - content: match &result.content { - language_model::LanguageModelToolResultContent::Text(t) => { - LlmToolResultContent::Text(t.to_string()) - } - language_model::LanguageModelToolResultContent::Image(img) => { - LlmToolResultContent::Image(LlmImageData { - source: img.source.to_string(), - width: Some(img.size.width.0 as u32), - height: Some(img.size.height.0 as u32), - }) - } - }, + tool_use_id: tool_result.tool_use_id.to_string(), + tool_name: tool_result.tool_name.to_string(), + is_error: tool_result.is_error, + content, }) } - language_model::MessageContent::Thinking { text, signature } => { - LlmMessageContent::Thinking(LlmThinkingContent { - text: text.clone(), - signature: signature.clone(), - }) + MessageContent::Thinking { text, signature } => { + LlmMessageContent::Thinking(LlmThinkingContent { text, signature }) } - language_model::MessageContent::RedactedThinking(data) => { - LlmMessageContent::RedactedThinking(data.clone()) + MessageContent::RedactedThinking(data) => { + LlmMessageContent::RedactedThinking(data) } }) - .collect(), - cache: msg.cache, + .collect(); + + LlmRequestMessage { + role, + content, + cache: msg.cache, + } }) .collect(); - let tools = request + let tools: Vec = request .tools - .iter() + .into_iter() .map(|tool| LlmToolDefinition { - name: tool.name.clone(), - description: tool.description.clone(), + name: tool.name, + description: tool.description, input_schema: serde_json::to_string(&tool.input_schema).unwrap_or_default(), }) .collect(); - let tool_choice = request.tool_choice.as_ref().map(|choice| match choice { + let tool_choice = request.tool_choice.map(|tc| match tc { LanguageModelToolChoice::Auto => LlmToolChoice::Auto, LanguageModelToolChoice::Any => LlmToolChoice::Any, LanguageModelToolChoice::None => LlmToolChoice::None, @@ -576,58 +848,71 @@ fn convert_request_to_wit(request: &LanguageModelRequest) -> LlmCompletionReques messages, tools, tool_choice, - stop_sequences: request.stop.clone(), + stop_sequences: request.stop, temperature: request.temperature, - thinking_allowed: request.thinking_allowed, + thinking_allowed: false, max_tokens: None, } } -fn convert_completion_event(event: LlmCompletionEvent) -> LanguageModelCompletionEvent { +fn convert_completion_event( + event: LlmCompletionEvent, +) -> Result { match event { - LlmCompletionEvent::Started => LanguageModelCompletionEvent::Started, - LlmCompletionEvent::Text(text) => LanguageModelCompletionEvent::Text(text), - LlmCompletionEvent::Thinking(thinking) => LanguageModelCompletionEvent::Thinking { + LlmCompletionEvent::Started => Ok(LanguageModelCompletionEvent::StartMessage { + message_id: String::new(), + }), + LlmCompletionEvent::Text(text) => Ok(LanguageModelCompletionEvent::Text(text)), + LlmCompletionEvent::Thinking(thinking) => Ok(LanguageModelCompletionEvent::Thinking { text: thinking.text, signature: thinking.signature, - }, + }), LlmCompletionEvent::RedactedThinking(data) => { - LanguageModelCompletionEvent::RedactedThinking { data } + Ok(LanguageModelCompletionEvent::RedactedThinking { data }) } LlmCompletionEvent::ToolUse(tool_use) => { - LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { - id: LanguageModelToolUseId::from(tool_use.id), - name: tool_use.name.into(), - raw_input: tool_use.input.clone(), - input: serde_json::from_str(&tool_use.input).unwrap_or(serde_json::Value::Null), - is_input_complete: true, - thought_signature: tool_use.thought_signature, - }) + let raw_input = tool_use.input.clone(); + let input = serde_json::from_str(&tool_use.input).unwrap_or(serde_json::Value::Null); + Ok(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: LanguageModelToolUseId::from(tool_use.id), + name: tool_use.name.into(), + raw_input, + input, + is_input_complete: true, + thought_signature: tool_use.thought_signature, + }, + )) } LlmCompletionEvent::ToolUseJsonParseError(error) => { - LanguageModelCompletionEvent::ToolUseJsonParseError { + Ok(LanguageModelCompletionEvent::ToolUseJsonParseError { id: LanguageModelToolUseId::from(error.id), tool_name: error.tool_name.into(), raw_input: error.raw_input.into(), json_parse_error: error.error, - } + }) + } + LlmCompletionEvent::Stop(reason) => { + let stop_reason = match reason { + LlmStopReason::EndTurn => StopReason::EndTurn, + LlmStopReason::MaxTokens => StopReason::MaxTokens, + LlmStopReason::ToolUse => StopReason::ToolUse, + LlmStopReason::Refusal => StopReason::Refusal, + }; + Ok(LanguageModelCompletionEvent::Stop(stop_reason)) + } + LlmCompletionEvent::Usage(usage) => { + Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage { + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0), + cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0), + })) } - LlmCompletionEvent::Stop(reason) => LanguageModelCompletionEvent::Stop(match reason { - LlmStopReason::EndTurn => StopReason::EndTurn, - LlmStopReason::MaxTokens => StopReason::MaxTokens, - LlmStopReason::ToolUse => StopReason::ToolUse, - LlmStopReason::Refusal => StopReason::Refusal, - }), - LlmCompletionEvent::Usage(usage) => LanguageModelCompletionEvent::UsageUpdate(TokenUsage { - input_tokens: usage.input_tokens, - output_tokens: usage.output_tokens, - cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0), - cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0), - }), LlmCompletionEvent::ReasoningDetails(json) => { - LanguageModelCompletionEvent::ReasoningDetails( + Ok(LanguageModelCompletionEvent::ReasoningDetails( serde_json::from_str(&json).unwrap_or(serde_json::Value::Null), - ) + )) } } } diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index a18ad1a10803bb..ec178b035c50e5 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -1199,6 +1199,20 @@ impl Extension { } } + pub async fn call_llm_provider_settings_markdown( + &self, + store: &mut Store, + provider_id: &str, + ) -> Result> { + match self { + Extension::V0_7_0(ext) => { + ext.call_llm_provider_settings_markdown(store, provider_id) + .await + } + _ => Ok(None), + } + } + pub async fn call_llm_provider_is_authenticated( &self, store: &mut Store, From 04de4563739e60d6ebe694599ef5e35c7e8102c2 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 3 Dec 2025 15:55:10 -0500 Subject: [PATCH 04/45] Use extension-llm- prefix for credential keys --- .../src/wasm_host/wit/since_v0_7_0.rs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs index 9b40c22a5595d6..c502f29228c9a6 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs @@ -13,6 +13,7 @@ use anyhow::{Context as _, Result, bail}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; +use credentials_provider::CredentialsProvider; use extension::{ ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, }; @@ -1132,12 +1133,16 @@ impl ExtensionImports for WasmState { provider_id: String, ) -> wasmtime::Result> { let extension_id = self.manifest.id.clone(); - let credential_key = format!("{}:{}", extension_id, provider_id); + let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id); self.on_main_thread(move |cx| { async move { - let task = cx.update(|cx| cx.read_credentials(&credential_key))?; - let result = task.await.ok().flatten(); + let credentials_provider = cx.update(|cx| ::global(cx))?; + let result = credentials_provider + .read_credentials(&credential_key, cx) + .await + .ok() + .flatten(); Ok(result.map(|(_, password)| String::from_utf8_lossy(&password).to_string())) } .boxed_local() @@ -1151,14 +1156,15 @@ impl ExtensionImports for WasmState { value: String, ) -> wasmtime::Result> { let extension_id = self.manifest.id.clone(); - let credential_key = format!("{}:{}", extension_id, provider_id); + let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id); self.on_main_thread(move |cx| { async move { - let task = cx.update(|cx| { - cx.write_credentials(&credential_key, "api_key", value.as_bytes()) - })?; - task.await.map_err(|e| anyhow::anyhow!("{}", e)) + let credentials_provider = cx.update(|cx| ::global(cx))?; + credentials_provider + .write_credentials(&credential_key, "api_key", value.as_bytes(), cx) + .await + .map_err(|e| anyhow::anyhow!("{}", e)) } .boxed_local() }) @@ -1171,12 +1177,15 @@ impl ExtensionImports for WasmState { provider_id: String, ) -> wasmtime::Result> { let extension_id = self.manifest.id.clone(); - let credential_key = format!("{}:{}", extension_id, provider_id); + let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id); self.on_main_thread(move |cx| { async move { - let task = cx.update(|cx| cx.delete_credentials(&credential_key))?; - task.await.map_err(|e| anyhow::anyhow!("{}", e)) + let credentials_provider = cx.update(|cx| ::global(cx))?; + credentials_provider + .delete_credentials(&credential_key, cx) + .await + .map_err(|e| anyhow::anyhow!("{}", e)) } .boxed_local() }) From 948905d9163d007be6115d5ce4de6333ee06f35b Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 3 Dec 2025 20:22:10 -0500 Subject: [PATCH 05/45] Revise provider extensions for Gemini API --- crates/extension_host/src/extension_host.rs | 29 ++------- .../src/wasm_host/llm_provider.rs | 60 +++++++++++++++++-- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 1fd568e9063817..d929ae9ef3c9f0 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1387,11 +1387,6 @@ impl ExtensionStore { // Tuple is (provider_info, models, is_authenticated) let mut llm_providers_with_models = Vec::new(); if !extension.manifest.language_model_providers.is_empty() { - eprintln!( - "Extension {} declares {} LLM providers in manifest, querying...", - extension.manifest.id, - extension.manifest.language_model_providers.len() - ); let providers_result = wasm_extension .call(|ext, store| { async move { ext.call_llm_providers(store).await }.boxed() @@ -1399,11 +1394,6 @@ impl ExtensionStore { .await; if let Ok(Ok(providers)) = providers_result { - eprintln!( - "Extension {} returned {} LLM providers", - extension.manifest.id, - providers.len() - ); for provider_info in providers { let models_result = wasm_extension .call({ @@ -1421,7 +1411,7 @@ impl ExtensionStore { let models: Vec = match models_result { Ok(Ok(Ok(models))) => models, Ok(Ok(Err(e))) => { - eprintln!( + log::error!( "Failed to get models for LLM provider {} in extension {}: {}", provider_info.id, extension.manifest.id, @@ -1430,7 +1420,7 @@ impl ExtensionStore { Vec::new() } Ok(Err(e)) => { - eprintln!( + log::error!( "Wasm error calling llm_provider_models for {} in extension {}: {:?}", provider_info.id, extension.manifest.id, @@ -1439,7 +1429,7 @@ impl ExtensionStore { Vec::new() } Err(e) => { - eprintln!( + log::error!( "Extension call failed for llm_provider_models {} in extension {}: {:?}", provider_info.id, extension.manifest.id, @@ -1468,17 +1458,11 @@ impl ExtensionStore { .unwrap_or(Ok(false)) .unwrap_or(false); - eprintln!( - "LLM provider {} has {} models, is_authenticated={}", - provider_info.id, - models.len(), - is_authenticated - ); llm_providers_with_models .push((provider_info, models, is_authenticated)); } } else { - eprintln!( + log::error!( "Failed to get LLM providers from extension {}: {:?}", extension.manifest.id, providers_result @@ -1576,23 +1560,18 @@ impl ExtensionStore { this.proxy.register_language_model_provider( provider_id.clone(), Box::new(move |cx: &mut App| { - eprintln!("register_fn closure called, creating provider"); let provider = Arc::new(ExtensionLanguageModelProvider::new( wasm_ext, pinfo, mods, auth, cx, )); - eprintln!("Provider created, registering with registry"); language_model::LanguageModelRegistry::global(cx).update( cx, |registry, cx| { - eprintln!("Inside registry.register_provider"); registry.register_provider(provider, cx); }, ); - eprintln!("Provider registered"); }), cx, ); - eprintln!("register_language_model_provider call completed for {}", provider_id); } } diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index e55e8b05938f4a..6ef02c16a6a34f 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -665,11 +665,22 @@ impl LanguageModel for ExtensionLanguageModel { let provider_id = self.provider_info.id.clone(); let model_id = self.model_info.id.clone(); + eprintln!( + "[EXT LLM DEBUG] stream_completion called for provider={}, model={}", + provider_id, model_id + ); + let wit_request = convert_request_to_wit(request); + eprintln!( + "[EXT LLM DEBUG] Converted request: {} messages, {} tools", + wit_request.messages.len(), + wit_request.tools.len() + ); async move { // Start the stream - let stream_id = extension + eprintln!("[EXT LLM DEBUG] Calling llm_stream_completion_start..."); + let stream_id_result = extension .call({ let provider_id = provider_id.clone(); let model_id = model_id.clone(); @@ -690,10 +701,19 @@ impl LanguageModel for ExtensionLanguageModel { .boxed() } }) - .await + .await; + + eprintln!( + "[EXT LLM DEBUG] llm_stream_completion_start result: {:?}", + stream_id_result + ); + + let stream_id = stream_id_result .map_err(LanguageModelCompletionError::Other)? .map_err(LanguageModelCompletionError::Other)?; + eprintln!("[EXT LLM DEBUG] Got stream_id: {}", stream_id); + // Create a stream that polls for events let stream = futures::stream::unfold( (extension.clone(), stream_id, false), @@ -721,12 +741,37 @@ impl LanguageModel for ExtensionLanguageModel { match result { Ok(Some(event)) => { + let event_desc = match &event { + LlmCompletionEvent::Started => "Started".to_string(), + LlmCompletionEvent::Text(t) => format!("Text: {:?}", t), + LlmCompletionEvent::Thinking(th) => { + format!("Thinking: {:?}", th.text) + } + LlmCompletionEvent::RedactedThinking(r) => { + format!("RedactedThinking: {:?}", r) + } + LlmCompletionEvent::ToolUse(tu) => { + format!("ToolUse: name={}, input={}", tu.name, tu.input) + } + LlmCompletionEvent::ToolUseJsonParseError(e) => { + format!("ToolUseJsonParseError: {:?}", e.error) + } + LlmCompletionEvent::Stop(r) => format!("Stop({:?})", r), + LlmCompletionEvent::Usage(u) => { + format!("Usage: in={}, out={}", u.input_tokens, u.output_tokens) + } + LlmCompletionEvent::ReasoningDetails(d) => { + format!("ReasoningDetails: {:?}", d) + } + }; + eprintln!("[EXT LLM DEBUG] Got event: {}", event_desc); let converted = convert_completion_event(event); let is_done = matches!(&converted, Ok(LanguageModelCompletionEvent::Stop(_))); Some((converted, (extension, stream_id, is_done))) } Ok(None) => { + eprintln!("[EXT LLM DEBUG] Stream returned None, closing"); // Stream complete, close it let _ = extension .call({ @@ -743,10 +788,13 @@ impl LanguageModel for ExtensionLanguageModel { .await; None } - Err(e) => Some(( - Err(LanguageModelCompletionError::Other(e)), - (extension, stream_id, true), - )), + Err(e) => { + eprintln!("[EXT LLM DEBUG] Stream error: {:?}", e); + Some(( + Err(LanguageModelCompletionError::Other(e)), + (extension, stream_id, true), + )) + } } }, ); From b200e10bc40433da664d070c232ac80eb8795655 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 11:30:44 -0500 Subject: [PATCH 06/45] Clean up debug statements --- crates/extension/src/extension_host_proxy.rs | 2 +- .../src/wasm_host/llm_provider.rs | 54 ++----------------- 2 files changed, 5 insertions(+), 51 deletions(-) diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 6fe9e1f8084cf5..2a67726f420469 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -386,7 +386,7 @@ pub trait ExtensionContextServerProxy: Send + Sync + 'static { fn unregister_context_server(&self, server_id: Arc, cx: &mut App); } -/// A boxed function that registers a language model provider with the registry. +/// A function that registers a language model provider with the registry. /// This allows extension_host to create the provider (which requires WasmExtension) /// and pass a registration closure to the language_models crate. pub type LanguageModelProviderRegistration = Box; diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 6ef02c16a6a34f..ce80432fd513ee 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -665,21 +665,10 @@ impl LanguageModel for ExtensionLanguageModel { let provider_id = self.provider_info.id.clone(); let model_id = self.model_info.id.clone(); - eprintln!( - "[EXT LLM DEBUG] stream_completion called for provider={}, model={}", - provider_id, model_id - ); - let wit_request = convert_request_to_wit(request); - eprintln!( - "[EXT LLM DEBUG] Converted request: {} messages, {} tools", - wit_request.messages.len(), - wit_request.tools.len() - ); async move { // Start the stream - eprintln!("[EXT LLM DEBUG] Calling llm_stream_completion_start..."); let stream_id_result = extension .call({ let provider_id = provider_id.clone(); @@ -703,17 +692,10 @@ impl LanguageModel for ExtensionLanguageModel { }) .await; - eprintln!( - "[EXT LLM DEBUG] llm_stream_completion_start result: {:?}", - stream_id_result - ); - let stream_id = stream_id_result .map_err(LanguageModelCompletionError::Other)? .map_err(LanguageModelCompletionError::Other)?; - eprintln!("[EXT LLM DEBUG] Got stream_id: {}", stream_id); - // Create a stream that polls for events let stream = futures::stream::unfold( (extension.clone(), stream_id, false), @@ -741,37 +723,12 @@ impl LanguageModel for ExtensionLanguageModel { match result { Ok(Some(event)) => { - let event_desc = match &event { - LlmCompletionEvent::Started => "Started".to_string(), - LlmCompletionEvent::Text(t) => format!("Text: {:?}", t), - LlmCompletionEvent::Thinking(th) => { - format!("Thinking: {:?}", th.text) - } - LlmCompletionEvent::RedactedThinking(r) => { - format!("RedactedThinking: {:?}", r) - } - LlmCompletionEvent::ToolUse(tu) => { - format!("ToolUse: name={}, input={}", tu.name, tu.input) - } - LlmCompletionEvent::ToolUseJsonParseError(e) => { - format!("ToolUseJsonParseError: {:?}", e.error) - } - LlmCompletionEvent::Stop(r) => format!("Stop({:?})", r), - LlmCompletionEvent::Usage(u) => { - format!("Usage: in={}, out={}", u.input_tokens, u.output_tokens) - } - LlmCompletionEvent::ReasoningDetails(d) => { - format!("ReasoningDetails: {:?}", d) - } - }; - eprintln!("[EXT LLM DEBUG] Got event: {}", event_desc); let converted = convert_completion_event(event); let is_done = matches!(&converted, Ok(LanguageModelCompletionEvent::Stop(_))); Some((converted, (extension, stream_id, is_done))) } Ok(None) => { - eprintln!("[EXT LLM DEBUG] Stream returned None, closing"); // Stream complete, close it let _ = extension .call({ @@ -788,13 +745,10 @@ impl LanguageModel for ExtensionLanguageModel { .await; None } - Err(e) => { - eprintln!("[EXT LLM DEBUG] Stream error: {:?}", e); - Some(( - Err(LanguageModelCompletionError::Other(e)), - (extension, stream_id, true), - )) - } + Err(e) => Some(( + Err(LanguageModelCompletionError::Other(e)), + (extension, stream_id, true), + )), } }, ); From a95f3f33a491435d3abaad995ddbf25f902236d6 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 12:38:06 -0500 Subject: [PATCH 07/45] Clean up debug logging --- crates/agent/src/agent.rs | 27 ---- crates/agent_ui/src/agent_ui.rs | 30 +---- .../agent_ui/src/language_model_selector.rs | 120 +++--------------- crates/extension/src/extension_host_proxy.rs | 5 - crates/language_model/src/registry.rs | 31 +---- crates/language_models/src/extension.rs | 9 +- 6 files changed, 22 insertions(+), 200 deletions(-) diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 256c8c2462e848..aec0767c25422d 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -103,22 +103,12 @@ impl LanguageModels { } fn refresh_list(&mut self, cx: &App) { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!("[{}ms] LanguageModels::refresh_list called", now); let providers = LanguageModelRegistry::global(cx) .read(cx) .providers() .into_iter() .filter(|provider| provider.is_authenticated(cx)) .collect::>(); - eprintln!( - "[{}ms] LanguageModels::refresh_list got {} authenticated providers", - now, - providers.len() - ); let mut language_model_list = IndexMap::default(); let mut recommended_models = HashSet::default(); @@ -156,15 +146,6 @@ impl LanguageModels { self.models = models; self.model_list = acp_thread::AgentModelList::Grouped(language_model_list); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] LanguageModels::refresh_list completed with {} models in list", - now, - self.models.len() - ); self.refresh_models_tx.send(()).ok(); } @@ -622,14 +603,6 @@ impl NativeAgent { _event: &language_model::Event, cx: &mut Context, ) { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] NativeAgent::handle_models_updated_event called", - now - ); self.models.refresh_list(cx); let registry = LanguageModelRegistry::read_global(cx); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index ce1e540a671541..9e5d6cbe0d5ac0 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -346,37 +346,13 @@ fn init_language_model_settings(cx: &mut App) { cx.subscribe( &LanguageModelRegistry::global(cx), |_, event: &language_model::Event, cx| match event { - language_model::Event::ProviderStateChanged(id) => { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] agent_ui global subscription: ProviderStateChanged for {:?}", - now, id - ); + language_model::Event::ProviderStateChanged(_) => { update_active_language_model_from_settings(cx); } - language_model::Event::AddedProvider(id) => { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] agent_ui global subscription: AddedProvider for {:?}", - now, id - ); + language_model::Event::AddedProvider(_) => { update_active_language_model_from_settings(cx); } - language_model::Event::RemovedProvider(id) => { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] agent_ui global subscription: RemovedProvider for {:?}", - now, id - ); + language_model::Event::RemovedProvider(_) => { update_active_language_model_from_settings(cx); } _ => {} diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 05384b4a93083b..6d8c20963876c5 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -46,13 +46,7 @@ pub fn language_model_selector( } fn all_models(cx: &App) -> GroupedModels { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!("[{}ms] all_models() called", now); let providers = LanguageModelRegistry::global(cx).read(cx).providers(); - eprintln!("[{}ms] all_models: got {} providers", now, providers.len()); let recommended = providers .iter() @@ -70,38 +64,16 @@ fn all_models(cx: &App) -> GroupedModels { let all: Vec = providers .iter() .flat_map(|provider| { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] all_models: calling provided_models for {:?}", - now, - provider.id() - ); - let models = provider.provided_models(cx); - eprintln!( - "[{}ms] all_models: provider {:?} returned {} models", - now, - provider.id(), - models.len() - ); - models.into_iter().map(|model| ModelInfo { - model, - icon: provider.icon(), - }) + provider + .provided_models(cx) + .into_iter() + .map(|model| ModelInfo { + model, + icon: provider.icon(), + }) }) .collect(); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] all_models: returning {} total models", - now, - all.len() - ); GroupedModels::new(all, recommended) } @@ -132,18 +104,8 @@ impl LanguageModelPickerDelegate { window: &mut Window, cx: &mut Context>, ) -> Self { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!("[{}ms] LanguageModelPickerDelegate::new() called", now); let on_model_changed = Arc::new(on_model_changed); let models = all_models(cx); - eprintln!( - "[{}ms] LanguageModelPickerDelegate::new() got {} models from all_models()", - now, - models.all.len() - ); let entries = models.entries(); Self { @@ -154,76 +116,28 @@ impl LanguageModelPickerDelegate { get_active_model: Arc::new(get_active_model), _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _refresh_models_task: { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] LanguageModelPickerDelegate::new() setting up refresh task for LanguageModelRegistry", - now - ); - // Create a channel to signal when models need refreshing let (refresh_tx, mut refresh_rx) = mpsc::unbounded::<()>(); // Subscribe to registry events and send refresh signals through the channel let registry = LanguageModelRegistry::global(cx); - eprintln!( - "[{}ms] LanguageModelPickerDelegate::new() subscribing to registry entity_id: {:?}", - now, - registry.entity_id() - ); - cx.subscribe(®istry, move |_picker, _, event, _cx| { - match event { - language_model::Event::ProviderStateChanged(id) => { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] LanguageModelSelector: ProviderStateChanged event for {:?}, sending refresh signal", - now, id - ); - refresh_tx.unbounded_send(()).ok(); - } - language_model::Event::AddedProvider(id) => { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] LanguageModelSelector: AddedProvider event for {:?}, sending refresh signal", - now, id - ); - refresh_tx.unbounded_send(()).ok(); - } - language_model::Event::RemovedProvider(id) => { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] LanguageModelSelector: RemovedProvider event for {:?}, sending refresh signal", - now, id - ); - refresh_tx.unbounded_send(()).ok(); - } - _ => {} + cx.subscribe(®istry, move |_picker, _, event, _cx| match event { + language_model::Event::ProviderStateChanged(_) => { + refresh_tx.unbounded_send(()).ok(); + } + language_model::Event::AddedProvider(_) => { + refresh_tx.unbounded_send(()).ok(); + } + language_model::Event::RemovedProvider(_) => { + refresh_tx.unbounded_send(()).ok(); } + _ => {} }) .detach(); // Spawn a task that listens for refresh signals and updates the picker cx.spawn_in(window, async move |this, cx| { while let Some(()) = refresh_rx.next().await { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] LanguageModelSelector: refresh signal received, updating models", - now - ); let result = this.update_in(cx, |picker, window, cx| { picker.delegate.all_models = Arc::new(all_models(cx)); picker.refresh(window, cx); diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 2a67726f420469..70b2da14b47d3a 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -414,14 +414,9 @@ impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy { cx: &mut App, ) { let Some(proxy) = self.language_model_provider_proxy.read().clone() else { - eprintln!( - "Failed to register LLM provider '{}': language_model_provider_proxy not yet initialized", - provider_id - ); return; }; - eprintln!("Registering LLM provider: {}", provider_id); proxy.register_language_model_provider(provider_id, register_fn, cx) } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index c41c9b76e264e3..27b83098109629 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -158,19 +158,7 @@ impl LanguageModelRegistry { } self.providers.insert(id.clone(), provider); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] LanguageModelRegistry: About to emit AddedProvider event for {:?}", - now, id - ); - cx.emit(Event::AddedProvider(id.clone())); - eprintln!( - "[{}ms] LanguageModelRegistry: Emitted AddedProvider event for {:?}", - now, id - ); + cx.emit(Event::AddedProvider(id)); } pub fn unregister_provider(&mut self, id: LanguageModelProviderId, cx: &mut Context) { @@ -180,18 +168,6 @@ impl LanguageModelRegistry { } pub fn providers(&self) -> Vec> { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - eprintln!( - "[{}ms] LanguageModelRegistry::providers() called, {} providers in registry", - now, - self.providers.len() - ); - for (id, _) in &self.providers { - eprintln!(" - provider: {:?}", id); - } let zed_provider_id = LanguageModelProviderId("zed.dev".into()); let mut providers = Vec::with_capacity(self.providers.len()); if let Some(provider) = self.providers.get(&zed_provider_id) { @@ -204,11 +180,6 @@ impl LanguageModelRegistry { None } })); - eprintln!( - "[{}ms] LanguageModelRegistry::providers() returning {} providers", - now, - providers.len() - ); providers } diff --git a/crates/language_models/src/extension.rs b/crates/language_models/src/extension.rs index e17e581fef6baf..9af6f41bd59955 100644 --- a/crates/language_models/src/extension.rs +++ b/crates/language_models/src/extension.rs @@ -22,15 +22,8 @@ impl ExtensionLanguageModelProviderProxy for ExtensionLanguageModelProxy { register_fn: LanguageModelProviderRegistration, cx: &mut App, ) { - eprintln!( - "ExtensionLanguageModelProxy::register_language_model_provider called for {}", - provider_id - ); + let _ = provider_id; register_fn(cx); - eprintln!( - "ExtensionLanguageModelProxy::register_language_model_provider completed for {}", - provider_id - ); } fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App) { From e08ab99e8df97a9bc55e48c9e79cb3d064e9d440 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 15:59:53 -0500 Subject: [PATCH 08/45] Add extensions for LLM providers --- extensions/anthropic/Cargo.lock | 823 +++++++++++++++++ extensions/anthropic/Cargo.toml | 17 + extensions/anthropic/extension.toml | 10 + extensions/anthropic/src/anthropic.rs | 803 +++++++++++++++++ extensions/copilot_chat/Cargo.lock | 823 +++++++++++++++++ extensions/copilot_chat/Cargo.toml | 17 + extensions/copilot_chat/extension.toml | 10 + extensions/copilot_chat/src/copilot_chat.rs | 696 +++++++++++++++ extensions/example-provider/Cargo.lock | 821 +++++++++++++++++ extensions/example-provider/Cargo.toml | 15 + extensions/example-provider/extension.toml | 10 + .../example-provider/src/example_provider.rs | 181 ++++ extensions/google-ai/Cargo.lock | 823 +++++++++++++++++ extensions/google-ai/Cargo.toml | 17 + extensions/google-ai/extension.toml | 10 + extensions/google-ai/src/google_ai.rs | 840 ++++++++++++++++++ extensions/open_router/Cargo.lock | 823 +++++++++++++++++ extensions/open_router/Cargo.toml | 17 + extensions/open_router/extension.toml | 10 + extensions/open_router/src/open_router.rs | 830 +++++++++++++++++ extensions/openai/Cargo.lock | 823 +++++++++++++++++ extensions/openai/Cargo.toml | 17 + extensions/openai/extension.toml | 10 + extensions/openai/src/openai.rs | 727 +++++++++++++++ 24 files changed, 9173 insertions(+) create mode 100644 extensions/anthropic/Cargo.lock create mode 100644 extensions/anthropic/Cargo.toml create mode 100644 extensions/anthropic/extension.toml create mode 100644 extensions/anthropic/src/anthropic.rs create mode 100644 extensions/copilot_chat/Cargo.lock create mode 100644 extensions/copilot_chat/Cargo.toml create mode 100644 extensions/copilot_chat/extension.toml create mode 100644 extensions/copilot_chat/src/copilot_chat.rs create mode 100644 extensions/example-provider/Cargo.lock create mode 100644 extensions/example-provider/Cargo.toml create mode 100644 extensions/example-provider/extension.toml create mode 100644 extensions/example-provider/src/example_provider.rs create mode 100644 extensions/google-ai/Cargo.lock create mode 100644 extensions/google-ai/Cargo.toml create mode 100644 extensions/google-ai/extension.toml create mode 100644 extensions/google-ai/src/google_ai.rs create mode 100644 extensions/open_router/Cargo.lock create mode 100644 extensions/open_router/Cargo.toml create mode 100644 extensions/open_router/extension.toml create mode 100644 extensions/open_router/src/open_router.rs create mode 100644 extensions/openai/Cargo.lock create mode 100644 extensions/openai/Cargo.toml create mode 100644 extensions/openai/extension.toml create mode 100644 extensions/openai/src/openai.rs diff --git a/extensions/anthropic/Cargo.lock b/extensions/anthropic/Cargo.lock new file mode 100644 index 00000000000000..bd558d1ce1a11c --- /dev/null +++ b/extensions/anthropic/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fanthropic" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.7.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/anthropic/Cargo.toml b/extensions/anthropic/Cargo.toml new file mode 100644 index 00000000000000..89c54229513ca1 --- /dev/null +++ b/extensions/anthropic/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "anthropic" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/anthropic.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/extensions/anthropic/extension.toml b/extensions/anthropic/extension.toml new file mode 100644 index 00000000000000..0f5973f7a9bd36 --- /dev/null +++ b/extensions/anthropic/extension.toml @@ -0,0 +1,10 @@ +id = "anthropic" +name = "Anthropic" +description = "Anthropic Claude LLM provider for Zed." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.anthropic] +name = "Anthropic" \ No newline at end of file diff --git a/extensions/anthropic/src/anthropic.rs b/extensions/anthropic/src/anthropic.rs new file mode 100644 index 00000000000000..78bf1735bcd5f2 --- /dev/null +++ b/extensions/anthropic/src/anthropic.rs @@ -0,0 +1,803 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +struct AnthropicProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + current_tool_use: Option, + stop_reason: Option, + pending_signature: Option, +} + +struct ToolUseState { + id: String, + name: String, + input_json: String, +} + +struct ModelDefinition { + real_id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: u64, + supports_images: bool, + supports_thinking: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + ModelDefinition { + real_id: "claude-opus-4-5-20251101", + display_name: "Claude Opus 4.5", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-opus-4-5-20251101", + display_name: "Claude Opus 4.5 Thinking", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-sonnet-4-5-20250929", + display_name: "Claude Sonnet 4.5", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-sonnet-4-5-20250929", + display_name: "Claude Sonnet 4.5 Thinking", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-sonnet-4-20250514", + display_name: "Claude Sonnet 4", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-sonnet-4-20250514", + display_name: "Claude Sonnet 4 Thinking", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-haiku-4-5-20251001", + display_name: "Claude Haiku 4.5", + max_tokens: 200_000, + max_output_tokens: 64_000, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + real_id: "claude-haiku-4-5-20251001", + display_name: "Claude Haiku 4.5 Thinking", + max_tokens: 200_000, + max_output_tokens: 64_000, + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-3-5-sonnet-latest", + display_name: "Claude 3.5 Sonnet", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-3-5-haiku-latest", + display_name: "Claude 3.5 Haiku", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_model_definition(display_name: &str) -> Option<&'static ModelDefinition> { + MODELS.iter().find(|m| m.display_name == display_name) +} + +// Anthropic API Request Types + +#[derive(Serialize)] +struct AnthropicRequest { + model: String, + max_tokens: u64, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + system: Option, + #[serde(skip_serializing_if = "Option::is_none")] + thinking: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop_sequences: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, +} + +#[derive(Serialize)] +struct AnthropicThinking { + #[serde(rename = "type")] + thinking_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + budget_tokens: Option, +} + +#[derive(Serialize)] +struct AnthropicMessage { + role: String, + content: Vec, +} + +#[derive(Serialize, Clone)] +#[serde(tag = "type")] +enum AnthropicContent { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "thinking")] + Thinking { thinking: String, signature: String }, + #[serde(rename = "redacted_thinking")] + RedactedThinking { data: String }, + #[serde(rename = "image")] + Image { source: AnthropicImageSource }, + #[serde(rename = "tool_use")] + ToolUse { + id: String, + name: String, + input: serde_json::Value, + }, + #[serde(rename = "tool_result")] + ToolResult { + tool_use_id: String, + is_error: bool, + content: String, + }, +} + +#[derive(Serialize, Clone)] +struct AnthropicImageSource { + #[serde(rename = "type")] + source_type: String, + media_type: String, + data: String, +} + +#[derive(Serialize)] +struct AnthropicTool { + name: String, + description: String, + input_schema: serde_json::Value, +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +enum AnthropicToolChoice { + Auto, + Any, + None, +} + +// Anthropic API Response Types + +#[derive(Deserialize, Debug)] +#[serde(tag = "type")] +#[allow(dead_code)] +enum AnthropicEvent { + #[serde(rename = "message_start")] + MessageStart { message: AnthropicMessageResponse }, + #[serde(rename = "content_block_start")] + ContentBlockStart { + index: usize, + content_block: AnthropicContentBlock, + }, + #[serde(rename = "content_block_delta")] + ContentBlockDelta { index: usize, delta: AnthropicDelta }, + #[serde(rename = "content_block_stop")] + ContentBlockStop { index: usize }, + #[serde(rename = "message_delta")] + MessageDelta { + delta: AnthropicMessageDelta, + usage: AnthropicUsage, + }, + #[serde(rename = "message_stop")] + MessageStop, + #[serde(rename = "ping")] + Ping, + #[serde(rename = "error")] + Error { error: AnthropicApiError }, +} + +#[derive(Deserialize, Debug)] +struct AnthropicMessageResponse { + #[allow(dead_code)] + id: String, + #[allow(dead_code)] + role: String, + #[serde(default)] + usage: AnthropicUsage, +} + +#[derive(Deserialize, Debug)] +#[serde(tag = "type")] +enum AnthropicContentBlock { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "thinking")] + Thinking { thinking: String }, + #[serde(rename = "redacted_thinking")] + RedactedThinking { data: String }, + #[serde(rename = "tool_use")] + ToolUse { id: String, name: String }, +} + +#[derive(Deserialize, Debug)] +#[serde(tag = "type")] +enum AnthropicDelta { + #[serde(rename = "text_delta")] + TextDelta { text: String }, + #[serde(rename = "thinking_delta")] + ThinkingDelta { thinking: String }, + #[serde(rename = "signature_delta")] + SignatureDelta { signature: String }, + #[serde(rename = "input_json_delta")] + InputJsonDelta { partial_json: String }, +} + +#[derive(Deserialize, Debug)] +struct AnthropicMessageDelta { + stop_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct AnthropicUsage { + #[serde(default)] + input_tokens: Option, + #[serde(default)] + output_tokens: Option, + #[serde(default)] + cache_creation_input_tokens: Option, + #[serde(default)] + cache_read_input_tokens: Option, +} + +#[derive(Deserialize, Debug)] +struct AnthropicApiError { + #[serde(rename = "type")] + #[allow(dead_code)] + error_type: String, + message: String, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let model_def = + get_model_definition(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; + + let mut messages: Vec = Vec::new(); + let mut system_message = String::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + for content in &msg.content { + if let LlmMessageContent::Text(text) = content { + if !system_message.is_empty() { + system_message.push('\n'); + } + system_message.push_str(text); + } + } + } + LlmMessageRole::User => { + let mut contents: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + contents.push(AnthropicContent::Text { text: text.clone() }); + } + } + LlmMessageContent::Image(img) => { + contents.push(AnthropicContent::Image { + source: AnthropicImageSource { + source_type: "base64".to_string(), + media_type: "image/png".to_string(), + data: img.source.clone(), + }, + }); + } + LlmMessageContent::ToolResult(result) => { + let content_text = match &result.content { + LlmToolResultContent::Text(t) => t.clone(), + LlmToolResultContent::Image(_) => "[Image]".to_string(), + }; + contents.push(AnthropicContent::ToolResult { + tool_use_id: result.tool_use_id.clone(), + is_error: result.is_error, + content: content_text, + }); + } + _ => {} + } + } + + if !contents.is_empty() { + messages.push(AnthropicMessage { + role: "user".to_string(), + content: contents, + }); + } + } + LlmMessageRole::Assistant => { + let mut contents: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + contents.push(AnthropicContent::Text { text: text.clone() }); + } + } + LlmMessageContent::ToolUse(tool_use) => { + let input: serde_json::Value = + serde_json::from_str(&tool_use.input).unwrap_or_default(); + contents.push(AnthropicContent::ToolUse { + id: tool_use.id.clone(), + name: tool_use.name.clone(), + input, + }); + } + LlmMessageContent::Thinking(thinking) => { + if !thinking.text.is_empty() { + contents.push(AnthropicContent::Thinking { + thinking: thinking.text.clone(), + signature: thinking.signature.clone().unwrap_or_default(), + }); + } + } + LlmMessageContent::RedactedThinking(data) => { + if !data.is_empty() { + contents.push(AnthropicContent::RedactedThinking { + data: data.clone(), + }); + } + } + _ => {} + } + } + + if !contents.is_empty() { + messages.push(AnthropicMessage { + role: "assistant".to_string(), + content: contents, + }); + } + } + } + } + + let tools: Vec = request + .tools + .iter() + .map(|t| AnthropicTool { + name: t.name.clone(), + description: t.description.clone(), + input_schema: serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())), + }) + .collect(); + + let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { + LlmToolChoice::Auto => AnthropicToolChoice::Auto, + LlmToolChoice::Any => AnthropicToolChoice::Any, + LlmToolChoice::None => AnthropicToolChoice::None, + }); + + let thinking = if model_def.supports_thinking && request.thinking_allowed { + Some(AnthropicThinking { + thinking_type: "enabled".to_string(), + budget_tokens: Some(4096), + }) + } else { + None + }; + + Ok(AnthropicRequest { + model: model_def.real_id.to_string(), + max_tokens: model_def.max_output_tokens, + messages, + system: if system_message.is_empty() { + None + } else { + Some(system_message) + }, + thinking, + tools, + tool_choice, + stop_sequences: request.stop_sequences.clone(), + temperature: request.temperature, + stream: true, + }) +} + +fn parse_sse_line(line: &str) -> Option { + let data = line.strip_prefix("data: ")?; + serde_json::from_str(data).ok() +} + +impl zed::Extension for AnthropicProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "anthropic".into(), + name: "Anthropic".into(), + icon: Some("anthropic".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.display_name.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: Some(m.max_output_tokens), + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: m.supports_thinking, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("anthropic").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# Anthropic Setup + +Welcome to **Anthropic**! This extension provides access to Claude models. + +## Configuration + +Enter your Anthropic API key below. You can get your API key at [console.anthropic.com](https://console.anthropic.com/). + +## Available Models + +| Display Name | Real Model | Context | Output | +|--------------|------------|---------|--------| +| Claude Opus 4.5 | claude-opus-4-5 | 200K | 8K | +| Claude Opus 4.5 Thinking | claude-opus-4-5 | 200K | 8K | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | 200K | 8K | +| Claude Sonnet 4.5 Thinking | claude-sonnet-4-5 | 200K | 8K | +| Claude Sonnet 4 | claude-sonnet-4 | 200K | 8K | +| Claude Sonnet 4 Thinking | claude-sonnet-4 | 200K | 8K | +| Claude Haiku 4.5 | claude-haiku-4-5 | 200K | 64K | +| Claude Haiku 4.5 Thinking | claude-haiku-4-5 | 200K | 64K | +| Claude 3.5 Sonnet | claude-3-5-sonnet | 200K | 8K | +| Claude 3.5 Haiku | claude-3-5-haiku | 200K | 8K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling +- ✅ Vision (image inputs) +- ✅ Extended thinking support +- ✅ All Claude models + +## Pricing + +Uses your Anthropic API credits. See [Anthropic pricing](https://www.anthropic.com/pricing) for details. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "anthropic", + LlmCredentialType::ApiKey, + "Anthropic API Key", + "sk-ant-...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("anthropic") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("anthropic").ok_or_else(|| { + "No API key configured. Please add your Anthropic API key in settings.".to_string() + })?; + + let anthropic_request = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&anthropic_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let http_request = HttpRequest { + method: HttpMethod::Post, + url: "https://api.anthropic.com/v1/messages".to_string(), + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("x-api-key".to_string(), api_key), + ("anthropic-version".to_string(), "2023-06-01".to_string()), + ], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("anthropic-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + current_tool_use: None, + stop_reason: None, + pending_signature: None, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if line.trim().is_empty() || line.starts_with("event:") { + continue; + } + + if let Some(event) = parse_sse_line(&line) { + match event { + AnthropicEvent::MessageStart { message } => { + if let (Some(input), Some(output)) = + (message.usage.input_tokens, message.usage.output_tokens) + { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: input, + output_tokens: output, + cache_creation_input_tokens: message + .usage + .cache_creation_input_tokens, + cache_read_input_tokens: message.usage.cache_read_input_tokens, + }))); + } + } + AnthropicEvent::ContentBlockStart { content_block, .. } => { + match content_block { + AnthropicContentBlock::Text { text } => { + if !text.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(text))); + } + } + AnthropicContentBlock::Thinking { thinking } => { + return Ok(Some(LlmCompletionEvent::Thinking( + LlmThinkingContent { + text: thinking, + signature: None, + }, + ))); + } + AnthropicContentBlock::RedactedThinking { data } => { + return Ok(Some(LlmCompletionEvent::RedactedThinking(data))); + } + AnthropicContentBlock::ToolUse { id, name } => { + state.current_tool_use = Some(ToolUseState { + id, + name, + input_json: String::new(), + }); + } + } + } + AnthropicEvent::ContentBlockDelta { delta, .. } => match delta { + AnthropicDelta::TextDelta { text } => { + if !text.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(text))); + } + } + AnthropicDelta::ThinkingDelta { thinking } => { + return Ok(Some(LlmCompletionEvent::Thinking( + LlmThinkingContent { + text: thinking, + signature: None, + }, + ))); + } + AnthropicDelta::SignatureDelta { signature } => { + state.pending_signature = Some(signature.clone()); + return Ok(Some(LlmCompletionEvent::Thinking( + LlmThinkingContent { + text: String::new(), + signature: Some(signature), + }, + ))); + } + AnthropicDelta::InputJsonDelta { partial_json } => { + if let Some(ref mut tool_use) = state.current_tool_use { + tool_use.input_json.push_str(&partial_json); + } + } + }, + AnthropicEvent::ContentBlockStop { .. } => { + if let Some(tool_use) = state.current_tool_use.take() { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tool_use.id, + name: tool_use.name, + input: tool_use.input_json, + thought_signature: state.pending_signature.take(), + }))); + } + } + AnthropicEvent::MessageDelta { delta, usage } => { + if let Some(reason) = delta.stop_reason { + state.stop_reason = Some(match reason.as_str() { + "end_turn" => LlmStopReason::EndTurn, + "max_tokens" => LlmStopReason::MaxTokens, + "tool_use" => LlmStopReason::ToolUse, + _ => LlmStopReason::EndTurn, + }); + } + if let Some(output) = usage.output_tokens { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.input_tokens.unwrap_or(0), + output_tokens: output, + cache_creation_input_tokens: usage.cache_creation_input_tokens, + cache_read_input_tokens: usage.cache_read_input_tokens, + }))); + } + } + AnthropicEvent::MessageStop => { + if let Some(stop_reason) = state.stop_reason.take() { + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + return Ok(Some(LlmCompletionEvent::Stop(LlmStopReason::EndTurn))); + } + AnthropicEvent::Ping => {} + AnthropicEvent::Error { error } => { + return Err(format!("API error: {}", error.message)); + } + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + if let Some(stop_reason) = state.stop_reason.take() { + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + return Ok(None); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(AnthropicProvider); diff --git a/extensions/copilot_chat/Cargo.lock b/extensions/copilot_chat/Cargo.lock new file mode 100644 index 00000000000000..1ba4a97d7291c7 --- /dev/null +++ b/extensions/copilot_chat/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "copilot_chat" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.8.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/copilot_chat/Cargo.toml b/extensions/copilot_chat/Cargo.toml new file mode 100644 index 00000000000000..c611e38a8451bc --- /dev/null +++ b/extensions/copilot_chat/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "copilot_chat" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/copilot_chat.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/extensions/copilot_chat/extension.toml b/extensions/copilot_chat/extension.toml new file mode 100644 index 00000000000000..213a13b463caec --- /dev/null +++ b/extensions/copilot_chat/extension.toml @@ -0,0 +1,10 @@ +id = "copilot_chat" +name = "Copilot Chat" +description = "GitHub Copilot Chat LLM provider for Zed." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.copilot_chat] +name = "Copilot Chat" \ No newline at end of file diff --git a/extensions/copilot_chat/src/copilot_chat.rs b/extensions/copilot_chat/src/copilot_chat.rs new file mode 100644 index 00000000000000..482e61101f6164 --- /dev/null +++ b/extensions/copilot_chat/src/copilot_chat.rs @@ -0,0 +1,696 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +struct CopilotChatProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + tool_calls_emitted: bool, +} + +#[derive(Clone, Default)] +struct AccumulatedToolCall { + id: String, + name: String, + arguments: String, +} + +struct ModelDefinition { + id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + supports_images: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + ModelDefinition { + id: "gpt-4o", + display_name: "GPT-4o", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + id: "gpt-4o-mini", + display_name: "GPT-4o Mini", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + id: "gpt-4.1", + display_name: "GPT-4.1", + max_tokens: 1_000_000, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "o1", + display_name: "o1", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "o3-mini", + display_name: "o3-mini", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "claude-3.5-sonnet", + display_name: "Claude 3.5 Sonnet", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "claude-3.7-sonnet", + display_name: "Claude 3.7 Sonnet", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "gemini-2.0-flash-001", + display_name: "Gemini 2.0 Flash", + max_tokens: 1_000_000, + max_output_tokens: Some(8_192), + supports_images: true, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> { + MODELS.iter().find(|m| m.id == model_id) +} + +#[derive(Serialize)] +struct OpenAiRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + stream_options: Option, +} + +#[derive(Serialize)] +struct StreamOptions { + include_usage: bool, +} + +#[derive(Serialize)] +struct OpenAiMessage { + role: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, +} + +#[derive(Serialize, Clone)] +#[serde(untagged)] +enum OpenAiContent { + Text(String), + Parts(Vec), +} + +#[derive(Serialize, Clone)] +#[serde(tag = "type")] +enum OpenAiContentPart { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image_url")] + ImageUrl { image_url: ImageUrl }, +} + +#[derive(Serialize, Clone)] +struct ImageUrl { + url: String, +} + +#[derive(Serialize, Clone)] +struct OpenAiToolCall { + id: String, + #[serde(rename = "type")] + call_type: String, + function: OpenAiFunctionCall, +} + +#[derive(Serialize, Clone)] +struct OpenAiFunctionCall { + name: String, + arguments: String, +} + +#[derive(Serialize)] +struct OpenAiTool { + #[serde(rename = "type")] + tool_type: String, + function: OpenAiFunctionDef, +} + +#[derive(Serialize)] +struct OpenAiFunctionDef { + name: String, + description: String, + parameters: serde_json::Value, +} + +#[derive(Deserialize, Debug)] +struct OpenAiStreamResponse { + choices: Vec, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiStreamChoice { + delta: OpenAiDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenAiToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiUsage { + prompt_tokens: u64, + completion_tokens: u64, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let mut messages: Vec = Vec::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + let mut text_content = String::new(); + for content in &msg.content { + if let LlmMessageContent::Text(text) = content { + if !text_content.is_empty() { + text_content.push('\n'); + } + text_content.push_str(text); + } + } + if !text_content.is_empty() { + messages.push(OpenAiMessage { + role: "system".to_string(), + content: Some(OpenAiContent::Text(text_content)), + tool_calls: None, + tool_call_id: None, + }); + } + } + LlmMessageRole::User => { + let mut parts: Vec = Vec::new(); + let mut tool_result_messages: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + parts.push(OpenAiContentPart::Text { text: text.clone() }); + } + } + LlmMessageContent::Image(img) => { + let data_url = format!("data:image/png;base64,{}", img.source); + parts.push(OpenAiContentPart::ImageUrl { + image_url: ImageUrl { url: data_url }, + }); + } + LlmMessageContent::ToolResult(result) => { + let content_text = match &result.content { + LlmToolResultContent::Text(t) => t.clone(), + LlmToolResultContent::Image(_) => "[Image]".to_string(), + }; + tool_result_messages.push(OpenAiMessage { + role: "tool".to_string(), + content: Some(OpenAiContent::Text(content_text)), + tool_calls: None, + tool_call_id: Some(result.tool_use_id.clone()), + }); + } + _ => {} + } + } + + if !parts.is_empty() { + let content = if parts.len() == 1 { + if let OpenAiContentPart::Text { text } = &parts[0] { + OpenAiContent::Text(text.clone()) + } else { + OpenAiContent::Parts(parts) + } + } else { + OpenAiContent::Parts(parts) + }; + + messages.push(OpenAiMessage { + role: "user".to_string(), + content: Some(content), + tool_calls: None, + tool_call_id: None, + }); + } + + messages.extend(tool_result_messages); + } + LlmMessageRole::Assistant => { + let mut text_content = String::new(); + let mut tool_calls: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + if !text_content.is_empty() { + text_content.push('\n'); + } + text_content.push_str(text); + } + } + LlmMessageContent::ToolUse(tool_use) => { + tool_calls.push(OpenAiToolCall { + id: tool_use.id.clone(), + call_type: "function".to_string(), + function: OpenAiFunctionCall { + name: tool_use.name.clone(), + arguments: tool_use.input.clone(), + }, + }); + } + _ => {} + } + } + + messages.push(OpenAiMessage { + role: "assistant".to_string(), + content: if text_content.is_empty() { + None + } else { + Some(OpenAiContent::Text(text_content)) + }, + tool_calls: if tool_calls.is_empty() { + None + } else { + Some(tool_calls) + }, + tool_call_id: None, + }); + } + } + } + + let tools: Vec = request + .tools + .iter() + .map(|t| OpenAiTool { + tool_type: "function".to_string(), + function: OpenAiFunctionDef { + name: t.name.clone(), + description: t.description.clone(), + parameters: serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())), + }, + }) + .collect(); + + let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { + LlmToolChoice::Auto => "auto".to_string(), + LlmToolChoice::Any => "required".to_string(), + LlmToolChoice::None => "none".to_string(), + }); + + let model_def = get_model_definition(model_id); + let max_tokens = request + .max_tokens + .or(model_def.and_then(|m| m.max_output_tokens)); + + Ok(OpenAiRequest { + model: model_id.to_string(), + messages, + max_tokens, + tools, + tool_choice, + stop: request.stop_sequences.clone(), + temperature: request.temperature, + stream: true, + stream_options: Some(StreamOptions { + include_usage: true, + }), + }) +} + +fn parse_sse_line(line: &str) -> Option { + let data = line.strip_prefix("data: ")?; + if data.trim() == "[DONE]" { + return None; + } + serde_json::from_str(data).ok() +} + +impl zed::Extension for CopilotChatProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "copilot_chat".into(), + name: "Copilot Chat".into(), + icon: None, + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.id.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: m.max_output_tokens, + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("copilot_chat").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# Copilot Chat Setup + +Welcome to **Copilot Chat**! This extension provides access to GitHub Copilot's chat models. + +## Configuration + +Enter your GitHub Copilot token below. You need an active GitHub Copilot subscription. + +To get your token: +1. Ensure you have a GitHub Copilot subscription +2. Generate a token from your GitHub Copilot settings + +## Available Models + +| Model | Context | Output | +|-------|---------|--------| +| GPT-4o | 128K | 16K | +| GPT-4o Mini | 128K | 16K | +| GPT-4.1 | 1M | 32K | +| o1 | 200K | 100K | +| o3-mini | 200K | 100K | +| Claude 3.5 Sonnet | 200K | 8K | +| Claude 3.7 Sonnet | 200K | 8K | +| Gemini 2.0 Flash | 1M | 8K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling +- ✅ Vision (image inputs) +- ✅ Multiple model providers via Copilot + +## Note + +This extension requires an active GitHub Copilot subscription. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "copilot_chat", + LlmCredentialType::ApiKey, + "GitHub Copilot Token", + "ghu_...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("copilot_chat") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("copilot_chat").ok_or_else(|| { + "No token configured. Please add your GitHub Copilot token in settings.".to_string() + })?; + + let openai_request = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&openai_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let http_request = HttpRequest { + method: HttpMethod::Post, + url: "https://api.githubcopilot.com/chat/completions".to_string(), + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("Authorization".to_string(), format!("Bearer {}", api_key)), + ( + "Copilot-Integration-Id".to_string(), + "vscode-chat".to_string(), + ), + ("Editor-Version".to_string(), "Zed/1.0.0".to_string()), + ], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("copilot-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + tool_calls: HashMap::new(), + tool_calls_emitted: false, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if line.trim().is_empty() { + continue; + } + + if let Some(response) = parse_sse_line(&line) { + if let Some(choice) = response.choices.first() { + if let Some(content) = &choice.delta.content { + if !content.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(content.clone()))); + } + } + + if let Some(tool_calls) = &choice.delta.tool_calls { + for tc in tool_calls { + let entry = state + .tool_calls + .entry(tc.index) + .or_insert_with(AccumulatedToolCall::default); + + if let Some(id) = &tc.id { + entry.id = id.clone(); + } + if let Some(func) = &tc.function { + if let Some(name) = &func.name { + entry.name = name.clone(); + } + if let Some(args) = &func.arguments { + entry.arguments.push_str(args); + } + } + } + } + + if let Some(finish_reason) = &choice.finish_reason { + if !state.tool_calls.is_empty() && !state.tool_calls_emitted { + state.tool_calls_emitted = true; + let mut tool_calls: Vec<_> = state.tool_calls.drain().collect(); + tool_calls.sort_by_key(|(idx, _)| *idx); + + if let Some((_, tc)) = tool_calls.into_iter().next() { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tc.id, + name: tc.name, + input: tc.arguments, + thought_signature: None, + }))); + } + } + + let stop_reason = match finish_reason.as_str() { + "stop" => LlmStopReason::EndTurn, + "length" => LlmStopReason::MaxTokens, + "tool_calls" => LlmStopReason::ToolUse, + "content_filter" => LlmStopReason::Refusal, + _ => LlmStopReason::EndTurn, + }; + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + } + + if let Some(usage) = response.usage { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_tokens, + output_tokens: usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + return Ok(None); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(CopilotChatProvider); diff --git a/extensions/example-provider/Cargo.lock b/extensions/example-provider/Cargo.lock new file mode 100644 index 00000000000000..08b2f0c343b1d3 --- /dev/null +++ b/extensions/example-provider/Cargo.lock @@ -0,0 +1,821 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "example_provider" +version = "0.1.0" +dependencies = [ + "zed_extension_api", +] + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.7.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/example-provider/Cargo.toml b/extensions/example-provider/Cargo.toml new file mode 100644 index 00000000000000..604d4fa6446a7a --- /dev/null +++ b/extensions/example-provider/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "example_provider" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/example_provider.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } \ No newline at end of file diff --git a/extensions/example-provider/extension.toml b/extensions/example-provider/extension.toml new file mode 100644 index 00000000000000..5dc58e8d73e8da --- /dev/null +++ b/extensions/example-provider/extension.toml @@ -0,0 +1,10 @@ +id = "example-provider" +name = "Example Provider" +description = "An example LLM provider extension for testing." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.example] +name = "Example Provider" \ No newline at end of file diff --git a/extensions/example-provider/src/example_provider.rs b/extensions/example-provider/src/example_provider.rs new file mode 100644 index 00000000000000..190251e4d54095 --- /dev/null +++ b/extensions/example-provider/src/example_provider.rs @@ -0,0 +1,181 @@ +use std::collections::HashMap; +use std::sync::Mutex; +use zed_extension_api::{self as zed, *}; + +struct ExampleProvider { + /// Active completion streams, keyed by stream ID + streams: Mutex>>, + /// Counter for generating unique stream IDs + next_stream_id: Mutex, +} + +impl zed::Extension for ExampleProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "example".into(), + name: "Example Provider".into(), + icon: None, + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(vec![ + LlmModelInfo { + id: "example-fast".into(), + name: "Example Fast".into(), + max_token_count: 8192, + max_output_tokens: Some(4096), + capabilities: LlmModelCapabilities { + supports_images: false, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: false, + is_default_fast: true, + }, + LlmModelInfo { + id: "example-smart".into(), + name: "Example Smart".into(), + max_token_count: 32768, + max_output_tokens: Some(8192), + capabilities: LlmModelCapabilities { + supports_images: true, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: true, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: true, + is_default_fast: false, + }, + ]) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + // Example provider is always authenticated for testing + true + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some(r#"# Example Provider Setup + +Welcome to the **Example Provider**! This is a demonstration LLM provider for testing purposes. + +## Features + +- 🚀 **Fast responses** - Instant echo responses for testing +- 🛠️ **Tool support** - Full function calling capabilities +- 🖼️ **Image support** - Vision model available (Example Smart) + +## Configuration + +No API key is required for this example provider. It echoes back your messages for testing purposes. + +## Models + +- **Example Fast** - Quick responses, 8K context +- **Example Smart** - Extended features, 32K context, supports images and thinking + +## Usage + +Simply select this provider and start chatting! Your messages will be echoed back with the model name. +"#.to_string()) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + // Example provider doesn't need authentication + Ok(()) + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + // Get the last user message to echo back + let user_message = request + .messages + .iter() + .filter(|m| matches!(m.role, LlmMessageRole::User)) + .last() + .and_then(|m| { + m.content.iter().find_map(|c| { + if let LlmMessageContent::Text(text) = c { + Some(text.clone()) + } else { + None + } + }) + }) + .unwrap_or_else(|| "Hello!".to_string()); + + // Create a response based on the model + let response_text = format!("Hello from {}! You said: \"{}\"", model_id, user_message); + + // Create events for the stream - simulate streaming by breaking into chunks + let mut events = vec![LlmCompletionEvent::Started]; + + // Stream the response in chunks + for chunk in response_text.chars().collect::>().chunks(10) { + let text: String = chunk.iter().collect(); + events.push(LlmCompletionEvent::Text(text)); + } + + events.push(LlmCompletionEvent::Stop(LlmStopReason::EndTurn)); + events.push(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: 10, + output_tokens: response_text.len() as u64 / 4, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + })); + + // Generate a unique stream ID + let mut id_counter = self.next_stream_id.lock().unwrap(); + let stream_id = format!("example-stream-{}", *id_counter); + *id_counter += 1; + + // Store the events + self.streams + .lock() + .unwrap() + .insert(stream_id.clone(), events); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + if let Some(events) = streams.get_mut(stream_id) { + if events.is_empty() { + Ok(None) + } else { + Ok(Some(events.remove(0))) + } + } else { + Err(format!("Unknown stream: {}", stream_id)) + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(ExampleProvider); diff --git a/extensions/google-ai/Cargo.lock b/extensions/google-ai/Cargo.lock new file mode 100644 index 00000000000000..2389ff51da0c24 --- /dev/null +++ b/extensions/google-ai/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foogle" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.7.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/google-ai/Cargo.toml b/extensions/google-ai/Cargo.toml new file mode 100644 index 00000000000000..2028c191dddf6f --- /dev/null +++ b/extensions/google-ai/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "google-ai" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/google_ai.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/extensions/google-ai/extension.toml b/extensions/google-ai/extension.toml new file mode 100644 index 00000000000000..e9c1318b54ee87 --- /dev/null +++ b/extensions/google-ai/extension.toml @@ -0,0 +1,10 @@ +id = "google-ai" +name = "Google AI" +description = "Google Gemini LLM provider for Zed." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.google-ai] +name = "Google AI" \ No newline at end of file diff --git a/extensions/google-ai/src/google_ai.rs b/extensions/google-ai/src/google_ai.rs new file mode 100644 index 00000000000000..37990951581e9b --- /dev/null +++ b/extensions/google-ai/src/google_ai.rs @@ -0,0 +1,840 @@ +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +static TOOL_CALL_COUNTER: AtomicU64 = AtomicU64::new(0); + +struct GoogleAiProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + stop_reason: Option, + wants_tool_use: bool, +} + +struct ModelDefinition { + real_id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + supports_images: bool, + supports_thinking: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + ModelDefinition { + real_id: "gemini-2.5-flash-lite", + display_name: "Gemini 2.5 Flash-Lite", + max_tokens: 1_048_576, + max_output_tokens: Some(65_536), + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + real_id: "gemini-2.5-flash", + display_name: "Gemini 2.5 Flash", + max_tokens: 1_048_576, + max_output_tokens: Some(65_536), + supports_images: true, + supports_thinking: true, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gemini-2.5-pro", + display_name: "Gemini 2.5 Pro", + max_tokens: 1_048_576, + max_output_tokens: Some(65_536), + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gemini-3-pro-preview", + display_name: "Gemini 3 Pro", + max_tokens: 1_048_576, + max_output_tokens: Some(65_536), + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_real_model_id(display_name: &str) -> Option<&'static str> { + MODELS + .iter() + .find(|m| m.display_name == display_name) + .map(|m| m.real_id) +} + +fn get_model_supports_thinking(display_name: &str) -> bool { + MODELS + .iter() + .find(|m| m.display_name == display_name) + .map(|m| m.supports_thinking) + .unwrap_or(false) +} + +/// Adapts a JSON schema to be compatible with Google's API subset. +/// Google only supports a specific subset of JSON Schema fields. +/// See: https://ai.google.dev/api/caching#Schema +fn adapt_schema_for_google(json: &mut serde_json::Value) { + adapt_schema_for_google_impl(json, true); +} + +fn adapt_schema_for_google_impl(json: &mut serde_json::Value, is_schema: bool) { + if let serde_json::Value::Object(obj) = json { + // Google's Schema only supports these fields: + // type, format, title, description, nullable, enum, maxItems, minItems, + // properties, required, minProperties, maxProperties, minLength, maxLength, + // pattern, example, anyOf, propertyOrdering, default, items, minimum, maximum + const ALLOWED_KEYS: &[&str] = &[ + "type", + "format", + "title", + "description", + "nullable", + "enum", + "maxItems", + "minItems", + "properties", + "required", + "minProperties", + "maxProperties", + "minLength", + "maxLength", + "pattern", + "example", + "anyOf", + "propertyOrdering", + "default", + "items", + "minimum", + "maximum", + ]; + + // Convert oneOf to anyOf before filtering keys + if let Some(one_of) = obj.remove("oneOf") { + obj.insert("anyOf".to_string(), one_of); + } + + // If type is an array (e.g., ["string", "null"]), take just the first type + if let Some(type_field) = obj.get_mut("type") { + if let serde_json::Value::Array(types) = type_field { + if let Some(first_type) = types.first().cloned() { + *type_field = first_type; + } + } + } + + // Only filter keys if this is a schema object, not a properties map + if is_schema { + obj.retain(|key, _| ALLOWED_KEYS.contains(&key.as_str())); + } + + // Recursively process nested values + // "properties" contains a map of property names -> schemas + // "items" and "anyOf" contain schemas directly + for (key, value) in obj.iter_mut() { + if key == "properties" { + // properties is a map of property_name -> schema + if let serde_json::Value::Object(props) = value { + for (_, prop_schema) in props.iter_mut() { + adapt_schema_for_google_impl(prop_schema, true); + } + } + } else if key == "items" { + // items is a schema + adapt_schema_for_google_impl(value, true); + } else if key == "anyOf" { + // anyOf is an array of schemas + if let serde_json::Value::Array(arr) = value { + for item in arr.iter_mut() { + adapt_schema_for_google_impl(item, true); + } + } + } + } + } else if let serde_json::Value::Array(arr) = json { + for item in arr.iter_mut() { + adapt_schema_for_google_impl(item, true); + } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleRequest { + contents: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + system_instruction: Option, + #[serde(skip_serializing_if = "Option::is_none")] + generation_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_config: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleSystemInstruction { + parts: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleContent { + parts: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + role: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +enum GooglePart { + Text(GoogleTextPart), + InlineData(GoogleInlineDataPart), + FunctionCall(GoogleFunctionCallPart), + FunctionResponse(GoogleFunctionResponsePart), + Thought(GoogleThoughtPart), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleTextPart { + text: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleInlineDataPart { + inline_data: GoogleBlob, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleBlob { + mime_type: String, + data: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionCallPart { + function_call: GoogleFunctionCall, + #[serde(skip_serializing_if = "Option::is_none")] + thought_signature: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionCall { + name: String, + args: serde_json::Value, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionResponsePart { + function_response: GoogleFunctionResponse, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionResponse { + name: String, + response: serde_json::Value, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleThoughtPart { + thought: bool, + thought_signature: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleGenerationConfig { + #[serde(skip_serializing_if = "Option::is_none")] + candidate_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stop_sequences: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + max_output_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + thinking_config: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleThinkingConfig { + thinking_budget: u32, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleTool { + function_declarations: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionDeclaration { + name: String, + description: String, + parameters: serde_json::Value, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleToolConfig { + function_calling_config: GoogleFunctionCallingConfig, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionCallingConfig { + mode: String, + #[serde(skip_serializing_if = "Option::is_none")] + allowed_function_names: Option>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GoogleStreamResponse { + #[serde(default)] + candidates: Vec, + #[serde(default)] + usage_metadata: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GoogleCandidate { + #[serde(default)] + content: Option, + #[serde(default)] + finish_reason: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GoogleUsageMetadata { + #[serde(default)] + prompt_token_count: u64, + #[serde(default)] + candidates_token_count: u64, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result<(GoogleRequest, String), String> { + let real_model_id = + get_real_model_id(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; + + let supports_thinking = get_model_supports_thinking(model_id); + + let mut contents: Vec = Vec::new(); + let mut system_parts: Vec = Vec::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + for content in &msg.content { + if let LlmMessageContent::Text(text) = content { + if !text.is_empty() { + system_parts + .push(GooglePart::Text(GoogleTextPart { text: text.clone() })); + } + } + } + } + LlmMessageRole::User => { + let mut parts: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + parts.push(GooglePart::Text(GoogleTextPart { text: text.clone() })); + } + } + LlmMessageContent::Image(img) => { + parts.push(GooglePart::InlineData(GoogleInlineDataPart { + inline_data: GoogleBlob { + mime_type: "image/png".to_string(), + data: img.source.clone(), + }, + })); + } + LlmMessageContent::ToolResult(result) => { + let response_value = match &result.content { + LlmToolResultContent::Text(t) => { + serde_json::json!({ "output": t }) + } + LlmToolResultContent::Image(_) => { + serde_json::json!({ "output": "Tool responded with an image" }) + } + }; + parts.push(GooglePart::FunctionResponse(GoogleFunctionResponsePart { + function_response: GoogleFunctionResponse { + name: result.tool_name.clone(), + response: response_value, + }, + })); + } + _ => {} + } + } + + if !parts.is_empty() { + contents.push(GoogleContent { + parts, + role: Some("user".to_string()), + }); + } + } + LlmMessageRole::Assistant => { + let mut parts: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + parts.push(GooglePart::Text(GoogleTextPart { text: text.clone() })); + } + } + LlmMessageContent::ToolUse(tool_use) => { + let thought_signature = + tool_use.thought_signature.clone().filter(|s| !s.is_empty()); + + let args: serde_json::Value = + serde_json::from_str(&tool_use.input).unwrap_or_default(); + + parts.push(GooglePart::FunctionCall(GoogleFunctionCallPart { + function_call: GoogleFunctionCall { + name: tool_use.name.clone(), + args, + }, + thought_signature, + })); + } + LlmMessageContent::Thinking(thinking) => { + if let Some(ref signature) = thinking.signature { + if !signature.is_empty() { + parts.push(GooglePart::Thought(GoogleThoughtPart { + thought: true, + thought_signature: signature.clone(), + })); + } + } + } + _ => {} + } + } + + if !parts.is_empty() { + contents.push(GoogleContent { + parts, + role: Some("model".to_string()), + }); + } + } + } + } + + let system_instruction = if system_parts.is_empty() { + None + } else { + Some(GoogleSystemInstruction { + parts: system_parts, + }) + }; + + let tools: Option> = if request.tools.is_empty() { + None + } else { + let declarations: Vec = request + .tools + .iter() + .map(|t| { + let mut parameters: serde_json::Value = serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())); + adapt_schema_for_google(&mut parameters); + GoogleFunctionDeclaration { + name: t.name.clone(), + description: t.description.clone(), + parameters, + } + }) + .collect(); + Some(vec![GoogleTool { + function_declarations: declarations, + }]) + }; + + let tool_config = request.tool_choice.as_ref().map(|tc| { + let mode = match tc { + LlmToolChoice::Auto => "AUTO", + LlmToolChoice::Any => "ANY", + LlmToolChoice::None => "NONE", + }; + GoogleToolConfig { + function_calling_config: GoogleFunctionCallingConfig { + mode: mode.to_string(), + allowed_function_names: None, + }, + } + }); + + let thinking_config = if supports_thinking && request.thinking_allowed { + Some(GoogleThinkingConfig { + thinking_budget: 8192, + }) + } else { + None + }; + + let generation_config = Some(GoogleGenerationConfig { + candidate_count: Some(1), + stop_sequences: if request.stop_sequences.is_empty() { + None + } else { + Some(request.stop_sequences.clone()) + }, + max_output_tokens: None, + temperature: request.temperature.map(|t| t as f64).or(Some(1.0)), + thinking_config, + }); + + Ok(( + GoogleRequest { + contents, + system_instruction, + generation_config, + tools, + tool_config, + }, + real_model_id.to_string(), + )) +} + +fn parse_stream_line(line: &str) -> Option { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed == "[" || trimmed == "]" || trimmed == "," { + return None; + } + + let json_str = trimmed.strip_prefix("data: ").unwrap_or(trimmed); + let json_str = json_str.trim_start_matches(',').trim(); + + if json_str.is_empty() { + return None; + } + + serde_json::from_str(json_str).ok() +} + +impl zed::Extension for GoogleAiProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "google-ai".into(), + name: "Google AI".into(), + icon: Some("google-ai".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.display_name.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: m.max_output_tokens, + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: m.supports_thinking, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("google-ai").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# Google AI Setup + +Welcome to **Google AI**! This extension provides access to Google Gemini models. + +## Configuration + +Enter your Google AI API key below. You can get your API key at [aistudio.google.com/apikey](https://aistudio.google.com/apikey). + +## Available Models + +| Display Name | Real Model | Context | Output | +|--------------|------------|---------|--------| +| Gemini 2.5 Flash-Lite | gemini-2.5-flash-lite | 1M | 65K | +| Gemini 2.5 Flash | gemini-2.5-flash | 1M | 65K | +| Gemini 2.5 Pro | gemini-2.5-pro | 1M | 65K | +| Gemini 3 Pro | gemini-3-pro-preview | 1M | 65K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling with thought signatures +- ✅ Vision (image inputs) +- ✅ Extended thinking support +- ✅ All Gemini models + +## Pricing + +Uses your Google AI API credits. See [Google AI pricing](https://ai.google.dev/pricing) for details. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "google-ai", + LlmCredentialType::ApiKey, + "Google AI API Key", + "AIza...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("google-ai") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("google-ai").ok_or_else(|| { + "No API key configured. Please add your Google AI API key in settings.".to_string() + })?; + + let (google_request, real_model_id) = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&google_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let url = format!( + "https://generativelanguage.googleapis.com/v1beta/models/{}:streamGenerateContent?alt=sse&key={}", + real_model_id, api_key + ); + + let http_request = HttpRequest { + method: HttpMethod::Post, + url, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("google-ai-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + stop_reason: None, + wants_tool_use: false, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if let Some(response) = parse_stream_line(&line) { + for candidate in response.candidates { + if let Some(finish_reason) = &candidate.finish_reason { + state.stop_reason = Some(match finish_reason.as_str() { + "STOP" => { + if state.wants_tool_use { + LlmStopReason::ToolUse + } else { + LlmStopReason::EndTurn + } + } + "MAX_TOKENS" => LlmStopReason::MaxTokens, + "SAFETY" => LlmStopReason::Refusal, + _ => LlmStopReason::EndTurn, + }); + } + + if let Some(content) = candidate.content { + for part in content.parts { + match part { + GooglePart::Text(text_part) => { + if !text_part.text.is_empty() { + return Ok(Some(LlmCompletionEvent::Text( + text_part.text, + ))); + } + } + GooglePart::FunctionCall(fc_part) => { + state.wants_tool_use = true; + let next_tool_id = + TOOL_CALL_COUNTER.fetch_add(1, Ordering::SeqCst); + let id = format!( + "{}-{}", + fc_part.function_call.name, next_tool_id + ); + + let thought_signature = + fc_part.thought_signature.filter(|s| !s.is_empty()); + + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id, + name: fc_part.function_call.name, + input: fc_part.function_call.args.to_string(), + thought_signature, + }))); + } + GooglePart::Thought(thought_part) => { + return Ok(Some(LlmCompletionEvent::Thinking( + LlmThinkingContent { + text: "(Encrypted thought)".to_string(), + signature: Some(thought_part.thought_signature), + }, + ))); + } + _ => {} + } + } + } + } + + if let Some(usage) = response.usage_metadata { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_token_count, + output_tokens: usage.candidates_token_count, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + // Stream ended - check if we have a stop reason + if let Some(stop_reason) = state.stop_reason.take() { + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + + // No stop reason - this is unexpected. Check if buffer contains error info + let mut error_msg = String::from("Stream ended unexpectedly."); + + // Try to parse remaining buffer as potential error response + if !state.buffer.is_empty() { + error_msg.push_str(&format!( + "\nRemaining buffer: {}", + &state.buffer[..state.buffer.len().min(1000)] + )); + } + + return Err(error_msg); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(GoogleAiProvider); diff --git a/extensions/open_router/Cargo.lock b/extensions/open_router/Cargo.lock new file mode 100644 index 00000000000000..4dea7c7a8a9cd8 --- /dev/null +++ b/extensions/open_router/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open_router" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.8.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/open_router/Cargo.toml b/extensions/open_router/Cargo.toml new file mode 100644 index 00000000000000..e02e5b6d4faba9 --- /dev/null +++ b/extensions/open_router/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "open_router" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/open_router.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/extensions/open_router/extension.toml b/extensions/open_router/extension.toml new file mode 100644 index 00000000000000..f2d48e07fcd42c --- /dev/null +++ b/extensions/open_router/extension.toml @@ -0,0 +1,10 @@ +id = "open_router" +name = "OpenRouter" +description = "OpenRouter LLM provider - access multiple AI models through a unified API." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.open_router] +name = "OpenRouter" \ No newline at end of file diff --git a/extensions/open_router/src/open_router.rs b/extensions/open_router/src/open_router.rs new file mode 100644 index 00000000000000..0c34bb794f3777 --- /dev/null +++ b/extensions/open_router/src/open_router.rs @@ -0,0 +1,830 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +struct OpenRouterProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + tool_calls_emitted: bool, +} + +#[derive(Clone, Default)] +struct AccumulatedToolCall { + id: String, + name: String, + arguments: String, +} + +struct ModelDefinition { + id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + supports_images: bool, + supports_tools: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + // Anthropic Models + ModelDefinition { + id: "anthropic/claude-sonnet-4", + display_name: "Claude Sonnet 4", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + id: "anthropic/claude-opus-4", + display_name: "Claude Opus 4", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "anthropic/claude-haiku-4", + display_name: "Claude Haiku 4", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + id: "anthropic/claude-3.5-sonnet", + display_name: "Claude 3.5 Sonnet", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + // OpenAI Models + ModelDefinition { + id: "openai/gpt-4o", + display_name: "GPT-4o", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "openai/gpt-4o-mini", + display_name: "GPT-4o Mini", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "openai/o1", + display_name: "o1", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + supports_tools: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "openai/o3-mini", + display_name: "o3-mini", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: false, + supports_tools: false, + is_default: false, + is_default_fast: false, + }, + // Google Models + ModelDefinition { + id: "google/gemini-2.0-flash-001", + display_name: "Gemini 2.0 Flash", + max_tokens: 1_000_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "google/gemini-2.5-pro-preview", + display_name: "Gemini 2.5 Pro", + max_tokens: 1_000_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + // Meta Models + ModelDefinition { + id: "meta-llama/llama-3.3-70b-instruct", + display_name: "Llama 3.3 70B", + max_tokens: 128_000, + max_output_tokens: Some(4_096), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "meta-llama/llama-4-maverick", + display_name: "Llama 4 Maverick", + max_tokens: 128_000, + max_output_tokens: Some(4_096), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + // Mistral Models + ModelDefinition { + id: "mistralai/mistral-large-2411", + display_name: "Mistral Large", + max_tokens: 128_000, + max_output_tokens: Some(4_096), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "mistralai/codestral-latest", + display_name: "Codestral", + max_tokens: 32_000, + max_output_tokens: Some(4_096), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + // DeepSeek Models + ModelDefinition { + id: "deepseek/deepseek-chat-v3-0324", + display_name: "DeepSeek V3", + max_tokens: 64_000, + max_output_tokens: Some(8_192), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "deepseek/deepseek-r1", + display_name: "DeepSeek R1", + max_tokens: 64_000, + max_output_tokens: Some(8_192), + supports_images: false, + supports_tools: false, + is_default: false, + is_default_fast: false, + }, + // Qwen Models + ModelDefinition { + id: "qwen/qwen3-235b-a22b", + display_name: "Qwen 3 235B", + max_tokens: 40_000, + max_output_tokens: Some(8_192), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> { + MODELS.iter().find(|m| m.id == model_id) +} + +#[derive(Serialize)] +struct OpenRouterRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, +} + +#[derive(Serialize)] +struct OpenRouterMessage { + role: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, +} + +#[derive(Serialize, Clone)] +#[serde(untagged)] +enum OpenRouterContent { + Text(String), + Parts(Vec), +} + +#[derive(Serialize, Clone)] +#[serde(tag = "type")] +enum OpenRouterContentPart { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image_url")] + ImageUrl { image_url: ImageUrl }, +} + +#[derive(Serialize, Clone)] +struct ImageUrl { + url: String, +} + +#[derive(Serialize, Clone)] +struct OpenRouterToolCall { + id: String, + #[serde(rename = "type")] + call_type: String, + function: OpenRouterFunctionCall, +} + +#[derive(Serialize, Clone)] +struct OpenRouterFunctionCall { + name: String, + arguments: String, +} + +#[derive(Serialize)] +struct OpenRouterTool { + #[serde(rename = "type")] + tool_type: String, + function: OpenRouterFunctionDef, +} + +#[derive(Serialize)] +struct OpenRouterFunctionDef { + name: String, + description: String, + parameters: serde_json::Value, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterStreamResponse { + choices: Vec, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterStreamChoice { + delta: OpenRouterDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenRouterDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenRouterFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterUsage { + prompt_tokens: u64, + completion_tokens: u64, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let mut messages: Vec = Vec::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + let mut text_content = String::new(); + for content in &msg.content { + if let LlmMessageContent::Text(text) = content { + if !text_content.is_empty() { + text_content.push('\n'); + } + text_content.push_str(text); + } + } + if !text_content.is_empty() { + messages.push(OpenRouterMessage { + role: "system".to_string(), + content: Some(OpenRouterContent::Text(text_content)), + tool_calls: None, + tool_call_id: None, + }); + } + } + LlmMessageRole::User => { + let mut parts: Vec = Vec::new(); + let mut tool_result_messages: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + parts.push(OpenRouterContentPart::Text { text: text.clone() }); + } + } + LlmMessageContent::Image(img) => { + let data_url = format!("data:image/png;base64,{}", img.source); + parts.push(OpenRouterContentPart::ImageUrl { + image_url: ImageUrl { url: data_url }, + }); + } + LlmMessageContent::ToolResult(result) => { + let content_text = match &result.content { + LlmToolResultContent::Text(t) => t.clone(), + LlmToolResultContent::Image(_) => "[Image]".to_string(), + }; + tool_result_messages.push(OpenRouterMessage { + role: "tool".to_string(), + content: Some(OpenRouterContent::Text(content_text)), + tool_calls: None, + tool_call_id: Some(result.tool_use_id.clone()), + }); + } + _ => {} + } + } + + if !parts.is_empty() { + let content = if parts.len() == 1 { + if let OpenRouterContentPart::Text { text } = &parts[0] { + OpenRouterContent::Text(text.clone()) + } else { + OpenRouterContent::Parts(parts) + } + } else { + OpenRouterContent::Parts(parts) + }; + + messages.push(OpenRouterMessage { + role: "user".to_string(), + content: Some(content), + tool_calls: None, + tool_call_id: None, + }); + } + + messages.extend(tool_result_messages); + } + LlmMessageRole::Assistant => { + let mut text_content = String::new(); + let mut tool_calls: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + if !text_content.is_empty() { + text_content.push('\n'); + } + text_content.push_str(text); + } + } + LlmMessageContent::ToolUse(tool_use) => { + tool_calls.push(OpenRouterToolCall { + id: tool_use.id.clone(), + call_type: "function".to_string(), + function: OpenRouterFunctionCall { + name: tool_use.name.clone(), + arguments: tool_use.input.clone(), + }, + }); + } + _ => {} + } + } + + messages.push(OpenRouterMessage { + role: "assistant".to_string(), + content: if text_content.is_empty() { + None + } else { + Some(OpenRouterContent::Text(text_content)) + }, + tool_calls: if tool_calls.is_empty() { + None + } else { + Some(tool_calls) + }, + tool_call_id: None, + }); + } + } + } + + let model_def = get_model_definition(model_id); + let supports_tools = model_def.map(|m| m.supports_tools).unwrap_or(true); + + let tools: Vec = if supports_tools { + request + .tools + .iter() + .map(|t| OpenRouterTool { + tool_type: "function".to_string(), + function: OpenRouterFunctionDef { + name: t.name.clone(), + description: t.description.clone(), + parameters: serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())), + }, + }) + .collect() + } else { + Vec::new() + }; + + let tool_choice = if supports_tools { + request.tool_choice.as_ref().map(|tc| match tc { + LlmToolChoice::Auto => "auto".to_string(), + LlmToolChoice::Any => "required".to_string(), + LlmToolChoice::None => "none".to_string(), + }) + } else { + None + }; + + let max_tokens = request + .max_tokens + .or(model_def.and_then(|m| m.max_output_tokens)); + + Ok(OpenRouterRequest { + model: model_id.to_string(), + messages, + max_tokens, + tools, + tool_choice, + stop: request.stop_sequences.clone(), + temperature: request.temperature, + stream: true, + }) +} + +fn parse_sse_line(line: &str) -> Option { + let data = line.strip_prefix("data: ")?; + if data.trim() == "[DONE]" { + return None; + } + serde_json::from_str(data).ok() +} + +impl zed::Extension for OpenRouterProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "open_router".into(), + name: "OpenRouter".into(), + icon: None, + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.id.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: m.max_output_tokens, + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: m.supports_tools, + supports_tool_choice_auto: m.supports_tools, + supports_tool_choice_any: m.supports_tools, + supports_tool_choice_none: m.supports_tools, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("open_router").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# OpenRouter Setup + +Welcome to **OpenRouter**! Access multiple AI models through a single API. + +## Configuration + +Enter your OpenRouter API key below. Get your API key at [openrouter.ai/keys](https://openrouter.ai/keys). + +## Available Models + +### Anthropic +| Model | Context | Output | +|-------|---------|--------| +| Claude Sonnet 4 | 200K | 8K | +| Claude Opus 4 | 200K | 8K | +| Claude Haiku 4 | 200K | 8K | +| Claude 3.5 Sonnet | 200K | 8K | + +### OpenAI +| Model | Context | Output | +|-------|---------|--------| +| GPT-4o | 128K | 16K | +| GPT-4o Mini | 128K | 16K | +| o1 | 200K | 100K | +| o3-mini | 200K | 100K | + +### Google +| Model | Context | Output | +|-------|---------|--------| +| Gemini 2.0 Flash | 1M | 8K | +| Gemini 2.5 Pro | 1M | 8K | + +### Meta +| Model | Context | Output | +|-------|---------|--------| +| Llama 3.3 70B | 128K | 4K | +| Llama 4 Maverick | 128K | 4K | + +### Mistral +| Model | Context | Output | +|-------|---------|--------| +| Mistral Large | 128K | 4K | +| Codestral | 32K | 4K | + +### DeepSeek +| Model | Context | Output | +|-------|---------|--------| +| DeepSeek V3 | 64K | 8K | +| DeepSeek R1 | 64K | 8K | + +### Qwen +| Model | Context | Output | +|-------|---------|--------| +| Qwen 3 235B | 40K | 8K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling (model dependent) +- ✅ Vision (model dependent) +- ✅ Access to 200+ models +- ✅ Unified billing + +## Pricing + +Pay-per-use based on model. See [openrouter.ai/models](https://openrouter.ai/models) for pricing. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "open_router", + LlmCredentialType::ApiKey, + "OpenRouter API Key", + "sk-or-v1-...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("open_router") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("open_router").ok_or_else(|| { + "No API key configured. Please add your OpenRouter API key in settings.".to_string() + })?; + + let openrouter_request = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&openrouter_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let http_request = HttpRequest { + method: HttpMethod::Post, + url: "https://openrouter.ai/api/v1/chat/completions".to_string(), + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("Authorization".to_string(), format!("Bearer {}", api_key)), + ("HTTP-Referer".to_string(), "https://zed.dev".to_string()), + ("X-Title".to_string(), "Zed Editor".to_string()), + ], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("openrouter-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + tool_calls: HashMap::new(), + tool_calls_emitted: false, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if line.trim().is_empty() { + continue; + } + + if let Some(response) = parse_sse_line(&line) { + if let Some(choice) = response.choices.first() { + if let Some(content) = &choice.delta.content { + if !content.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(content.clone()))); + } + } + + if let Some(tool_calls) = &choice.delta.tool_calls { + for tc in tool_calls { + let entry = state + .tool_calls + .entry(tc.index) + .or_insert_with(AccumulatedToolCall::default); + + if let Some(id) = &tc.id { + entry.id = id.clone(); + } + if let Some(func) = &tc.function { + if let Some(name) = &func.name { + entry.name = name.clone(); + } + if let Some(args) = &func.arguments { + entry.arguments.push_str(args); + } + } + } + } + + if let Some(finish_reason) = &choice.finish_reason { + if !state.tool_calls.is_empty() && !state.tool_calls_emitted { + state.tool_calls_emitted = true; + let mut tool_calls: Vec<_> = state.tool_calls.drain().collect(); + tool_calls.sort_by_key(|(idx, _)| *idx); + + if let Some((_, tc)) = tool_calls.into_iter().next() { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tc.id, + name: tc.name, + input: tc.arguments, + thought_signature: None, + }))); + } + } + + let stop_reason = match finish_reason.as_str() { + "stop" => LlmStopReason::EndTurn, + "length" => LlmStopReason::MaxTokens, + "tool_calls" => LlmStopReason::ToolUse, + "content_filter" => LlmStopReason::Refusal, + _ => LlmStopReason::EndTurn, + }; + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + } + + if let Some(usage) = response.usage { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_tokens, + output_tokens: usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + return Ok(None); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(OpenRouterProvider); diff --git a/extensions/openai/Cargo.lock b/extensions/openai/Cargo.lock new file mode 100644 index 00000000000000..2ef354a2892b23 --- /dev/null +++ b/extensions/openai/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fopenai" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.7.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/openai/Cargo.toml b/extensions/openai/Cargo.toml new file mode 100644 index 00000000000000..9477f9b9862f48 --- /dev/null +++ b/extensions/openai/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "openai" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/openai.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/extensions/openai/extension.toml b/extensions/openai/extension.toml new file mode 100644 index 00000000000000..ca77fa684bcff8 --- /dev/null +++ b/extensions/openai/extension.toml @@ -0,0 +1,10 @@ +id = "openai" +name = "OpenAI" +description = "OpenAI GPT LLM provider for Zed." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.openai] +name = "OpenAI" \ No newline at end of file diff --git a/extensions/openai/src/openai.rs b/extensions/openai/src/openai.rs new file mode 100644 index 00000000000000..d464066d13f54f --- /dev/null +++ b/extensions/openai/src/openai.rs @@ -0,0 +1,727 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +struct OpenAiProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + tool_calls_emitted: bool, +} + +#[derive(Clone, Default)] +struct AccumulatedToolCall { + id: String, + name: String, + arguments: String, +} + +struct ModelDefinition { + real_id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + supports_images: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + ModelDefinition { + real_id: "gpt-4o", + display_name: "GPT-4o", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-4o-mini", + display_name: "GPT-4o-mini", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + real_id: "gpt-4.1", + display_name: "GPT-4.1", + max_tokens: 1_047_576, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-4.1-mini", + display_name: "GPT-4.1-mini", + max_tokens: 1_047_576, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-4.1-nano", + display_name: "GPT-4.1-nano", + max_tokens: 1_047_576, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-5", + display_name: "GPT-5", + max_tokens: 272_000, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-5-mini", + display_name: "GPT-5-mini", + max_tokens: 272_000, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "o1", + display_name: "o1", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "o3", + display_name: "o3", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "o3-mini", + display_name: "o3-mini", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "o4-mini", + display_name: "o4-mini", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_real_model_id(display_name: &str) -> Option<&'static str> { + MODELS + .iter() + .find(|m| m.display_name == display_name) + .map(|m| m.real_id) +} + +#[derive(Serialize)] +struct OpenAiRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + stream: bool, + stream_options: Option, +} + +#[derive(Serialize)] +struct StreamOptions { + include_usage: bool, +} + +#[derive(Serialize)] +#[serde(tag = "role")] +enum OpenAiMessage { + #[serde(rename = "system")] + System { content: String }, + #[serde(rename = "user")] + User { content: Vec }, + #[serde(rename = "assistant")] + Assistant { + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + }, + #[serde(rename = "tool")] + Tool { + tool_call_id: String, + content: String, + }, +} + +#[derive(Serialize)] +#[serde(tag = "type")] +enum OpenAiContentPart { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image_url")] + ImageUrl { image_url: ImageUrl }, +} + +#[derive(Serialize)] +struct ImageUrl { + url: String, +} + +#[derive(Serialize, Deserialize, Clone)] +struct OpenAiToolCall { + id: String, + #[serde(rename = "type")] + call_type: String, + function: OpenAiFunctionCall, +} + +#[derive(Serialize, Deserialize, Clone)] +struct OpenAiFunctionCall { + name: String, + arguments: String, +} + +#[derive(Serialize)] +struct OpenAiTool { + #[serde(rename = "type")] + tool_type: String, + function: OpenAiFunctionDef, +} + +#[derive(Serialize)] +struct OpenAiFunctionDef { + name: String, + description: String, + parameters: serde_json::Value, +} + +#[derive(Deserialize, Debug)] +struct OpenAiStreamEvent { + choices: Vec, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiChoice { + delta: OpenAiDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenAiToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiUsage { + prompt_tokens: u64, + completion_tokens: u64, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct OpenAiError { + error: OpenAiErrorDetail, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct OpenAiErrorDetail { + message: String, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let real_model_id = + get_real_model_id(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; + + let mut messages = Vec::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + let text: String = msg + .content + .iter() + .filter_map(|c| match c { + LlmMessageContent::Text(t) => Some(t.as_str()), + _ => None, + }) + .collect::>() + .join("\n"); + if !text.is_empty() { + messages.push(OpenAiMessage::System { content: text }); + } + } + LlmMessageRole::User => { + let parts: Vec = msg + .content + .iter() + .filter_map(|c| match c { + LlmMessageContent::Text(t) => { + Some(OpenAiContentPart::Text { text: t.clone() }) + } + LlmMessageContent::Image(img) => Some(OpenAiContentPart::ImageUrl { + image_url: ImageUrl { + url: format!("data:image/png;base64,{}", img.source), + }, + }), + LlmMessageContent::ToolResult(_) => None, + _ => None, + }) + .collect(); + + for content in &msg.content { + if let LlmMessageContent::ToolResult(result) = content { + let content_text = match &result.content { + LlmToolResultContent::Text(t) => t.clone(), + LlmToolResultContent::Image(_) => "[Image]".to_string(), + }; + messages.push(OpenAiMessage::Tool { + tool_call_id: result.tool_use_id.clone(), + content: content_text, + }); + } + } + + if !parts.is_empty() { + messages.push(OpenAiMessage::User { content: parts }); + } + } + LlmMessageRole::Assistant => { + let mut content_text: Option = None; + let mut tool_calls: Vec = Vec::new(); + + for c in &msg.content { + match c { + LlmMessageContent::Text(t) => { + content_text = Some(t.clone()); + } + LlmMessageContent::ToolUse(tool_use) => { + tool_calls.push(OpenAiToolCall { + id: tool_use.id.clone(), + call_type: "function".to_string(), + function: OpenAiFunctionCall { + name: tool_use.name.clone(), + arguments: tool_use.input.clone(), + }, + }); + } + _ => {} + } + } + + messages.push(OpenAiMessage::Assistant { + content: content_text, + tool_calls: if tool_calls.is_empty() { + None + } else { + Some(tool_calls) + }, + }); + } + } + } + + let tools: Option> = if request.tools.is_empty() { + None + } else { + Some( + request + .tools + .iter() + .map(|t| OpenAiTool { + tool_type: "function".to_string(), + function: OpenAiFunctionDef { + name: t.name.clone(), + description: t.description.clone(), + parameters: serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())), + }, + }) + .collect(), + ) + }; + + let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { + LlmToolChoice::Auto => "auto".to_string(), + LlmToolChoice::Any => "required".to_string(), + LlmToolChoice::None => "none".to_string(), + }); + + Ok(OpenAiRequest { + model: real_model_id.to_string(), + messages, + tools, + tool_choice, + temperature: request.temperature, + max_tokens: request.max_tokens, + stop: request.stop_sequences.clone(), + stream: true, + stream_options: Some(StreamOptions { + include_usage: true, + }), + }) +} + +fn parse_sse_line(line: &str) -> Option { + if let Some(data) = line.strip_prefix("data: ") { + if data == "[DONE]" { + return None; + } + serde_json::from_str(data).ok() + } else { + None + } +} + +impl zed::Extension for OpenAiProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "openai".into(), + name: "OpenAI".into(), + icon: Some("openai".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.display_name.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: m.max_output_tokens, + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("openai").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# OpenAI Setup + +Welcome to **OpenAI**! This extension provides access to OpenAI GPT models. + +## Configuration + +Enter your OpenAI API key below. You can find your API key at [platform.openai.com/api-keys](https://platform.openai.com/api-keys). + +## Available Models + +| Display Name | Real Model | Context | Output | +|--------------|------------|---------|--------| +| GPT-4o | gpt-4o | 128K | 16K | +| GPT-4o-mini | gpt-4o-mini | 128K | 16K | +| GPT-4.1 | gpt-4.1 | 1M | 32K | +| GPT-4.1-mini | gpt-4.1-mini | 1M | 32K | +| GPT-5 | gpt-5 | 272K | 32K | +| GPT-5-mini | gpt-5-mini | 272K | 32K | +| o1 | o1 | 200K | 100K | +| o3 | o3 | 200K | 100K | +| o3-mini | o3-mini | 200K | 100K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling +- ✅ Vision (image inputs) +- ✅ All OpenAI models + +## Pricing + +Uses your OpenAI API credits. See [OpenAI pricing](https://openai.com/pricing) for details. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "openai", + LlmCredentialType::ApiKey, + "OpenAI API Key", + "sk-...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("openai") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("openai").ok_or_else(|| { + "No API key configured. Please add your OpenAI API key in settings.".to_string() + })?; + + let openai_request = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&openai_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let http_request = HttpRequest { + method: HttpMethod::Post, + url: "https://api.openai.com/v1/chat/completions".to_string(), + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("Authorization".to_string(), format!("Bearer {}", api_key)), + ], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("openai-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + tool_calls: HashMap::new(), + tool_calls_emitted: false, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].trim().to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if line.is_empty() { + continue; + } + + if let Some(event) = parse_sse_line(&line) { + if let Some(choice) = event.choices.first() { + if let Some(tool_calls) = &choice.delta.tool_calls { + for tc in tool_calls { + let entry = state.tool_calls.entry(tc.index).or_default(); + + if let Some(id) = &tc.id { + entry.id = id.clone(); + } + + if let Some(func) = &tc.function { + if let Some(name) = &func.name { + entry.name = name.clone(); + } + if let Some(args) = &func.arguments { + entry.arguments.push_str(args); + } + } + } + } + + if let Some(reason) = &choice.finish_reason { + if reason == "tool_calls" && !state.tool_calls_emitted { + state.tool_calls_emitted = true; + if let Some((&index, _)) = state.tool_calls.iter().next() { + if let Some(tool_call) = state.tool_calls.remove(&index) { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tool_call.id, + name: tool_call.name, + input: tool_call.arguments, + thought_signature: None, + }))); + } + } + } + + let stop_reason = match reason.as_str() { + "stop" => LlmStopReason::EndTurn, + "length" => LlmStopReason::MaxTokens, + "tool_calls" => LlmStopReason::ToolUse, + "content_filter" => LlmStopReason::Refusal, + _ => LlmStopReason::EndTurn, + }; + + if let Some(usage) = event.usage { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_tokens, + output_tokens: usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + + if let Some(content) = &choice.delta.content { + if !content.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(content.clone()))); + } + } + } + + if event.choices.is_empty() { + if let Some(usage) = event.usage { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_tokens, + output_tokens: usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + if !state.tool_calls.is_empty() && !state.tool_calls_emitted { + state.tool_calls_emitted = true; + let keys: Vec = state.tool_calls.keys().copied().collect(); + if let Some(&key) = keys.first() { + if let Some(tool_call) = state.tool_calls.remove(&key) { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tool_call.id, + name: tool_call.name, + input: tool_call.arguments, + thought_signature: None, + }))); + } + } + } + return Ok(None); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(OpenAiProvider); From 58207325e2322bded0052e89c5ccd3f7321f0936 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:12:21 -0500 Subject: [PATCH 09/45] restore impl Drop for WasmExtension --- crates/extension_host/src/wasm_host.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 93b8a1de9b723e..cd0b99cc02499b 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -73,6 +73,12 @@ pub struct WasmExtension { _task: Arc>>, } +impl Drop for WasmExtension { + fn drop(&mut self) { + self.tx.close_channel(); + } +} + #[async_trait] impl extension::Extension for WasmExtension { fn manifest(&self) -> Arc { From 2a89529d7fb47ccf1519fe2ac38d2114c3aefb58 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:17:50 -0500 Subject: [PATCH 10/45] Use named fields --- crates/extension_host/src/extension_host.rs | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index d929ae9ef3c9f0..dfbf1ae3c7e9ea 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -63,6 +63,12 @@ use wasm_host::{ wit::{LlmModelInfo, LlmProviderInfo, is_supported_wasm_api_version, wasm_api_version_range}, }; +struct LlmProviderWithModels { + provider_info: LlmProviderInfo, + models: Vec, + is_authenticated: bool, +} + pub use extension::{ ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion, }; @@ -1364,7 +1370,7 @@ impl ExtensionStore { let mut wasm_extensions: Vec<( Arc, WasmExtension, - Vec<(LlmProviderInfo, Vec, bool)>, + Vec, )> = Vec::new(); for extension in extension_entries { if extension.manifest.lib.kind.is_none() { @@ -1384,7 +1390,6 @@ impl ExtensionStore { match wasm_extension { Ok(wasm_extension) => { // Query for LLM providers if the manifest declares any - // Tuple is (provider_info, models, is_authenticated) let mut llm_providers_with_models = Vec::new(); if !extension.manifest.language_model_providers.is_empty() { let providers_result = wasm_extension @@ -1458,8 +1463,11 @@ impl ExtensionStore { .unwrap_or(Ok(false)) .unwrap_or(false); - llm_providers_with_models - .push((provider_info, models, is_authenticated)); + llm_providers_with_models.push(LlmProviderWithModels { + provider_info, + models, + is_authenticated, + }); } } else { log::error!( @@ -1549,13 +1557,13 @@ impl ExtensionStore { } // Register LLM providers - for (provider_info, models, is_authenticated) in llm_providers_with_models { + for llm_provider in llm_providers_with_models { let provider_id: Arc = - format!("{}:{}", manifest.id, provider_info.id).into(); + format!("{}:{}", manifest.id, llm_provider.provider_info.id).into(); let wasm_ext = extension.as_ref().clone(); - let pinfo = provider_info.clone(); - let mods = models.clone(); - let auth = *is_authenticated; + let pinfo = llm_provider.provider_info.clone(); + let mods = llm_provider.models.clone(); + let auth = llm_provider.is_authenticated; this.proxy.register_language_model_provider( provider_id.clone(), From f54e7f8c9df33d16f2b57d2c510c811800facb51 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:18:43 -0500 Subject: [PATCH 11/45] Add trailing newlines --- extensions/anthropic/Cargo.toml | 2 +- extensions/anthropic/extension.toml | 2 +- extensions/copilot_chat/Cargo.toml | 2 +- extensions/copilot_chat/extension.toml | 2 +- extensions/example-provider/Cargo.toml | 2 +- extensions/example-provider/extension.toml | 2 +- extensions/google-ai/Cargo.toml | 2 +- extensions/google-ai/extension.toml | 2 +- extensions/open_router/Cargo.toml | 2 +- extensions/open_router/extension.toml | 2 +- extensions/openai/Cargo.toml | 2 +- extensions/openai/extension.toml | 2 +- extensions/test-extension/extension.toml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/extensions/anthropic/Cargo.toml b/extensions/anthropic/Cargo.toml index 89c54229513ca1..25dfe72b0e92ca 100644 --- a/extensions/anthropic/Cargo.toml +++ b/extensions/anthropic/Cargo.toml @@ -14,4 +14,4 @@ crate-type = ["cdylib"] [dependencies] zed_extension_api = { path = "../../crates/extension_api" } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" diff --git a/extensions/anthropic/extension.toml b/extensions/anthropic/extension.toml index 0f5973f7a9bd36..ab27ecda7869c2 100644 --- a/extensions/anthropic/extension.toml +++ b/extensions/anthropic/extension.toml @@ -7,4 +7,4 @@ authors = ["Zed Team"] repository = "https://github.com/zed-industries/zed" [language_model_providers.anthropic] -name = "Anthropic" \ No newline at end of file +name = "Anthropic" diff --git a/extensions/copilot_chat/Cargo.toml b/extensions/copilot_chat/Cargo.toml index c611e38a8451bc..6daae73229776d 100644 --- a/extensions/copilot_chat/Cargo.toml +++ b/extensions/copilot_chat/Cargo.toml @@ -14,4 +14,4 @@ crate-type = ["cdylib"] [dependencies] zed_extension_api = { path = "../../crates/extension_api" } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" diff --git a/extensions/copilot_chat/extension.toml b/extensions/copilot_chat/extension.toml index 213a13b463caec..321a17df003e02 100644 --- a/extensions/copilot_chat/extension.toml +++ b/extensions/copilot_chat/extension.toml @@ -7,4 +7,4 @@ authors = ["Zed Team"] repository = "https://github.com/zed-industries/zed" [language_model_providers.copilot_chat] -name = "Copilot Chat" \ No newline at end of file +name = "Copilot Chat" diff --git a/extensions/example-provider/Cargo.toml b/extensions/example-provider/Cargo.toml index 604d4fa6446a7a..62d6783382e9e9 100644 --- a/extensions/example-provider/Cargo.toml +++ b/extensions/example-provider/Cargo.toml @@ -12,4 +12,4 @@ path = "src/example_provider.rs" crate-type = ["cdylib"] [dependencies] -zed_extension_api = { path = "../../crates/extension_api" } \ No newline at end of file +zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/example-provider/extension.toml b/extensions/example-provider/extension.toml index 5dc58e8d73e8da..3b89b7bf8d5241 100644 --- a/extensions/example-provider/extension.toml +++ b/extensions/example-provider/extension.toml @@ -7,4 +7,4 @@ authors = ["Zed Team"] repository = "https://github.com/zed-industries/zed" [language_model_providers.example] -name = "Example Provider" \ No newline at end of file +name = "Example Provider" diff --git a/extensions/google-ai/Cargo.toml b/extensions/google-ai/Cargo.toml index 2028c191dddf6f..f6de35d4066938 100644 --- a/extensions/google-ai/Cargo.toml +++ b/extensions/google-ai/Cargo.toml @@ -14,4 +14,4 @@ crate-type = ["cdylib"] [dependencies] zed_extension_api = { path = "../../crates/extension_api" } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" diff --git a/extensions/google-ai/extension.toml b/extensions/google-ai/extension.toml index e9c1318b54ee87..a9ed706c1f4d5e 100644 --- a/extensions/google-ai/extension.toml +++ b/extensions/google-ai/extension.toml @@ -7,4 +7,4 @@ authors = ["Zed Team"] repository = "https://github.com/zed-industries/zed" [language_model_providers.google-ai] -name = "Google AI" \ No newline at end of file +name = "Google AI" diff --git a/extensions/open_router/Cargo.toml b/extensions/open_router/Cargo.toml index e02e5b6d4faba9..47a87b5d52571a 100644 --- a/extensions/open_router/Cargo.toml +++ b/extensions/open_router/Cargo.toml @@ -14,4 +14,4 @@ crate-type = ["cdylib"] [dependencies] zed_extension_api = { path = "../../crates/extension_api" } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" diff --git a/extensions/open_router/extension.toml b/extensions/open_router/extension.toml index f2d48e07fcd42c..41c8c8458ae016 100644 --- a/extensions/open_router/extension.toml +++ b/extensions/open_router/extension.toml @@ -7,4 +7,4 @@ authors = ["Zed Team"] repository = "https://github.com/zed-industries/zed" [language_model_providers.open_router] -name = "OpenRouter" \ No newline at end of file +name = "OpenRouter" diff --git a/extensions/openai/Cargo.toml b/extensions/openai/Cargo.toml index 9477f9b9862f48..f81809e426ef69 100644 --- a/extensions/openai/Cargo.toml +++ b/extensions/openai/Cargo.toml @@ -14,4 +14,4 @@ crate-type = ["cdylib"] [dependencies] zed_extension_api = { path = "../../crates/extension_api" } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" diff --git a/extensions/openai/extension.toml b/extensions/openai/extension.toml index ca77fa684bcff8..b688cd108e91b4 100644 --- a/extensions/openai/extension.toml +++ b/extensions/openai/extension.toml @@ -7,4 +7,4 @@ authors = ["Zed Team"] repository = "https://github.com/zed-industries/zed" [language_model_providers.openai] -name = "OpenAI" \ No newline at end of file +name = "OpenAI" diff --git a/extensions/test-extension/extension.toml b/extensions/test-extension/extension.toml index 3c359ade72adad..0cb5afac7f7031 100644 --- a/extensions/test-extension/extension.toml +++ b/extensions/test-extension/extension.toml @@ -22,4 +22,4 @@ args = ["hello from a child process!"] [[capabilities]] kind = "process:exec" command = "cmd" -args = ["/C", "echo", "hello from a child process!"] \ No newline at end of file +args = ["/C", "echo", "hello from a child process!"] From fcb3d3dec6f07267b877f6ba3ee4f482d7ffaee2 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:28:29 -0500 Subject: [PATCH 12/45] Update a comment --- crates/extension/src/extension_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 02a6c18be7ae0b..bfffe9bb87f2ac 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -336,7 +336,7 @@ pub struct LanguageModelAuthConfig { /// Environment variable name for the API key. #[serde(default)] pub env_var: Option, - /// Label to show when prompting for credentials. + /// Human-readable name for the credential shown in the UI input field (e.g., "API Key", "Access Token"). #[serde(default)] pub credential_label: Option, } From 1396c680109366de405286d7626bff5533cd9ed2 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:43:49 -0500 Subject: [PATCH 13/45] Add svg icons to llm provider extensions --- crates/agent_ui/src/agent_configuration.rs | 10 +++-- crates/agent_ui/src/agent_model_selector.rs | 16 ++++++-- .../agent_ui/src/language_model_selector.rs | 39 ++++++++++++++----- crates/agent_ui/src/text_thread_editor.rs | 15 ++++++- .../src/agent_api_keys_onboarding.rs | 38 +++++++++++++++--- .../src/agent_panel_onboarding_content.rs | 16 +++----- crates/extension/src/extension_manifest.rs | 2 +- .../wit/since_v0.7.0/llm-provider.wit | 2 +- .../wit/since_v0.8.0/llm-provider.wit | 2 +- crates/extension_cli/src/main.rs | 15 +++++++ crates/extension_host/src/extension_host.rs | 21 ++++++++-- .../src/wasm_host/llm_provider.rs | 7 ++++ crates/language_model/src/language_model.rs | 5 +++ extensions/anthropic/src/anthropic.rs | 2 +- extensions/google-ai/src/google_ai.rs | 2 +- extensions/openai/src/openai.rs | 2 +- 16 files changed, 153 insertions(+), 41 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index f831329e2cde40..3533c28caa93f8 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -260,11 +260,15 @@ impl AgentConfiguration { h_flex() .w_full() .gap_1p5() - .child( + .child(if let Some(icon_path) = provider.icon_path() { + Icon::from_external_svg(icon_path) + .size(IconSize::Small) + .color(Color::Muted) + } else { Icon::new(provider.icon()) .size(IconSize::Small) - .color(Color::Muted), - ) + .color(Color::Muted) + }) .child( h_flex() .w_full() diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index 43982cdda7bd88..924f37db0440dd 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -73,7 +73,8 @@ impl Render for AgentModelSelector { .map(|model| model.model.name().0) .unwrap_or_else(|| SharedString::from("Select a Model")); - let provider_icon = model.as_ref().map(|model| model.provider.icon()); + let provider_icon_path = model.as_ref().and_then(|model| model.provider.icon_path()); + let provider_icon_name = model.as_ref().map(|model| model.provider.icon()); let color = if self.menu_handle.is_deployed() { Color::Accent } else { @@ -85,8 +86,17 @@ impl Render for AgentModelSelector { PickerPopoverMenu::new( self.selector.clone(), ButtonLike::new("active-model") - .when_some(provider_icon, |this, icon| { - this.child(Icon::new(icon).color(color).size(IconSize::XSmall)) + .when_some(provider_icon_path.clone(), |this, icon_path| { + this.child( + Icon::from_external_svg(icon_path) + .color(color) + .size(IconSize::XSmall), + ) + }) + .when(provider_icon_path.is_none(), |this| { + this.when_some(provider_icon_name, |this, icon| { + this.child(Icon::new(icon).color(color).size(IconSize::XSmall)) + }) }) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) .child( diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 6d8c20963876c5..9fd717a597e149 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -5,8 +5,8 @@ use futures::{StreamExt, channel::mpsc}; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Task}; use language_model::{ - AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, - LanguageModelRegistry, + AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProvider, + LanguageModelProviderId, LanguageModelRegistry, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -56,7 +56,7 @@ fn all_models(cx: &App) -> GroupedModels { .into_iter() .map(|model| ModelInfo { model, - icon: provider.icon(), + icon: ProviderIcon::from_provider(provider.as_ref()), }) }) .collect(); @@ -69,7 +69,7 @@ fn all_models(cx: &App) -> GroupedModels { .into_iter() .map(|model| ModelInfo { model, - icon: provider.icon(), + icon: ProviderIcon::from_provider(provider.as_ref()), }) }) .collect(); @@ -77,10 +77,26 @@ fn all_models(cx: &App) -> GroupedModels { GroupedModels::new(all, recommended) } +#[derive(Clone)] +enum ProviderIcon { + Name(IconName), + Path(SharedString), +} + +impl ProviderIcon { + fn from_provider(provider: &dyn LanguageModelProvider) -> Self { + if let Some(path) = provider.icon_path() { + Self::Path(path) + } else { + Self::Name(provider.icon()) + } + } +} + #[derive(Clone)] struct ModelInfo { model: Arc, - icon: IconName, + icon: ProviderIcon, } pub struct LanguageModelPickerDelegate { @@ -519,11 +535,16 @@ impl PickerDelegate for LanguageModelPickerDelegate { h_flex() .w_full() .gap_1p5() - .child( - Icon::new(model_info.icon) + .child(match &model_info.icon { + ProviderIcon::Name(icon_name) => Icon::new(*icon_name) .color(model_icon_color) .size(IconSize::Small), - ) + ProviderIcon::Path(icon_path) => { + Icon::from_external_svg(icon_path.clone()) + .color(model_icon_color) + .size(IconSize::Small) + } + }) .child(Label::new(model_info.model.name().0).truncate()), ) .end_slot(div().pr_3().when(is_selected, |this| { @@ -672,7 +693,7 @@ mod tests { .into_iter() .map(|(provider, name)| ModelInfo { model: Arc::new(TestLanguageModel::new(name, provider)), - icon: IconName::Ai, + icon: ProviderIcon::Name(IconName::Ai), }) .collect() } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 161fad95e68c01..30538898b28a1d 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -2097,7 +2097,8 @@ impl TextThreadEditor { .default_model() .map(|default| default.provider); - let provider_icon = match active_provider { + let provider_icon_path = active_provider.as_ref().and_then(|p| p.icon_path()); + let provider_icon_name = match &active_provider { Some(provider) => provider.icon(), None => IconName::Ai, }; @@ -2109,6 +2110,16 @@ impl TextThreadEditor { (Color::Muted, IconName::ChevronDown) }; + let provider_icon_element = if let Some(icon_path) = provider_icon_path { + Icon::from_external_svg(icon_path) + .color(color) + .size(IconSize::XSmall) + } else { + Icon::new(provider_icon_name) + .color(color) + .size(IconSize::XSmall) + }; + PickerPopoverMenu::new( self.language_model_selector.clone(), ButtonLike::new("active-model") @@ -2116,7 +2127,7 @@ impl TextThreadEditor { .child( h_flex() .gap_0p5() - .child(Icon::new(provider_icon).color(color).size(IconSize::XSmall)) + .child(provider_icon_element) .child( Label::new(model_name) .color(color) diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index fadc4222ae44f3..bdf1ce3640bf50 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -1,9 +1,25 @@ use gpui::{Action, IntoElement, ParentElement, RenderOnce, point}; -use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; +use language_model::{LanguageModelProvider, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; use ui::{Divider, List, ListBulletItem, prelude::*}; +#[derive(Clone)] +enum ProviderIcon { + Name(IconName), + Path(SharedString), +} + +impl ProviderIcon { + fn from_provider(provider: &dyn LanguageModelProvider) -> Self { + if let Some(path) = provider.icon_path() { + Self::Path(path) + } else { + Self::Name(provider.icon()) + } + } +} + pub struct ApiKeysWithProviders { - configured_providers: Vec<(IconName, SharedString)>, + configured_providers: Vec<(ProviderIcon, SharedString)>, } impl ApiKeysWithProviders { @@ -26,14 +42,19 @@ impl ApiKeysWithProviders { } } - fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> { + fn compute_configured_providers(cx: &App) -> Vec<(ProviderIcon, SharedString)> { LanguageModelRegistry::read_global(cx) .providers() .iter() .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0)) + .map(|provider| { + ( + ProviderIcon::from_provider(provider.as_ref()), + provider.name().0, + ) + }) .collect() } } @@ -47,7 +68,14 @@ impl Render for ApiKeysWithProviders { .map(|(icon, name)| { h_flex() .gap_1p5() - .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)) + .child(match icon { + ProviderIcon::Name(icon_name) => Icon::new(icon_name) + .size(IconSize::XSmall) + .color(Color::Muted), + ProviderIcon::Path(icon_path) => Icon::from_external_svg(icon_path) + .size(IconSize::XSmall) + .color(Color::Muted), + }) .child(Label::new(name)) }); div() diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 3c8ffc1663e066..ae92268ff4db45 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding}; pub struct AgentPanelOnboarding { user_store: Entity, client: Arc, - configured_providers: Vec<(IconName, SharedString)>, + has_configured_providers: bool, continue_with_zed_ai: Arc, } @@ -28,7 +28,7 @@ impl AgentPanelOnboarding { language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { - this.configured_providers = Self::compute_available_providers(cx) + this.has_configured_providers = Self::has_configured_providers(cx) } _ => {} }, @@ -38,20 +38,16 @@ impl AgentPanelOnboarding { Self { user_store, client, - configured_providers: Self::compute_available_providers(cx), + has_configured_providers: Self::has_configured_providers(cx), continue_with_zed_ai: Arc::new(continue_with_zed_ai), } } - fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> { + fn has_configured_providers(cx: &App) -> bool { LanguageModelRegistry::read_global(cx) .providers() .iter() - .filter(|provider| { - provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID - }) - .map(|provider| (provider.icon(), provider.name().0)) - .collect() + .any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID) } } @@ -81,7 +77,7 @@ impl Render for AgentPanelOnboarding { }), ) .map(|this| { - if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() { + if enrolled_in_trial || is_pro_user || self.has_configured_providers { this } else { this.child(ApiKeysWithoutProviders::new()) diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index bfffe9bb87f2ac..73747c2997a28a 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -295,7 +295,7 @@ pub struct DebugLocatorManifestEntry {} pub struct LanguageModelProviderManifestEntry { /// Display name for the provider. pub name: String, - /// Icon name from Zed's icon set (optional). + /// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg"). #[serde(default)] pub icon: Option, /// Default models to show even before API connection. diff --git a/crates/extension_api/wit/since_v0.7.0/llm-provider.wit b/crates/extension_api/wit/since_v0.7.0/llm-provider.wit index 31f19b90769fdf..5912654ebcf9e5 100644 --- a/crates/extension_api/wit/since_v0.7.0/llm-provider.wit +++ b/crates/extension_api/wit/since_v0.7.0/llm-provider.wit @@ -5,7 +5,7 @@ interface llm-provider { id: string, /// Display name for the provider. name: string, - /// Icon name from Zed's icon set (optional). + /// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg"). icon: option, } diff --git a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit index 31f19b90769fdf..5912654ebcf9e5 100644 --- a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit +++ b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit @@ -5,7 +5,7 @@ interface llm-provider { id: string, /// Display name for the provider. name: string, - /// Icon name from Zed's icon set (optional). + /// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg"). icon: option, } diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index 524e14b0cedceb..24eb696b1dcba8 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -254,6 +254,21 @@ async fn copy_extension_resources( } } + for (_, provider_entry) in &manifest.language_model_providers { + if let Some(icon_path) = &provider_entry.icon { + let source_icon = extension_path.join(icon_path); + let dest_icon = output_dir.join(icon_path); + + // Create parent directory if needed + if let Some(parent) = dest_icon.parent() { + fs::create_dir_all(parent)?; + } + + fs::copy(&source_icon, &dest_icon) + .with_context(|| format!("failed to copy LLM provider icon '{}'", icon_path))?; + } + } + if !manifest.languages.is_empty() { let output_languages_dir = output_dir.join("languages"); fs::create_dir_all(&output_languages_dir)?; diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index dfbf1ae3c7e9ea..869319ded5419d 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -32,8 +32,8 @@ use futures::{ select_biased, }; use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, - actions, + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, SharedString, Task, + WeakEntity, actions, }; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::{ @@ -67,6 +67,7 @@ struct LlmProviderWithModels { provider_info: LlmProviderInfo, models: Vec, is_authenticated: bool, + icon_path: Option, } pub use extension::{ @@ -1463,10 +1464,23 @@ impl ExtensionStore { .unwrap_or(Ok(false)) .unwrap_or(false); + // Resolve icon path if provided + let icon_path = provider_info.icon.as_ref().map(|icon| { + let icon_file_path = extension_path.join(icon); + // Canonicalize to resolve symlinks (dev extensions are symlinked) + let absolute_icon_path = icon_file_path + .canonicalize() + .unwrap_or(icon_file_path) + .to_string_lossy() + .to_string(); + SharedString::from(absolute_icon_path) + }); + llm_providers_with_models.push(LlmProviderWithModels { provider_info, models, is_authenticated, + icon_path, }); } } else { @@ -1564,12 +1578,13 @@ impl ExtensionStore { let pinfo = llm_provider.provider_info.clone(); let mods = llm_provider.models.clone(); let auth = llm_provider.is_authenticated; + let icon = llm_provider.icon_path.clone(); this.proxy.register_language_model_provider( provider_id.clone(), Box::new(move |cx: &mut App| { let provider = Arc::new(ExtensionLanguageModelProvider::new( - wasm_ext, pinfo, mods, auth, cx, + wasm_ext, pinfo, mods, auth, icon, cx, )); language_model::LanguageModelRegistry::global(cx).update( cx, diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index ce80432fd513ee..0ae833080a59a5 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -36,6 +36,7 @@ use util::ResultExt as _; pub struct ExtensionLanguageModelProvider { pub extension: WasmExtension, pub provider_info: LlmProviderInfo, + icon_path: Option, state: Entity, } @@ -52,6 +53,7 @@ impl ExtensionLanguageModelProvider { provider_info: LlmProviderInfo, models: Vec, is_authenticated: bool, + icon_path: Option, cx: &mut App, ) -> Self { let state = cx.new(|_| ExtensionLlmProviderState { @@ -62,6 +64,7 @@ impl ExtensionLanguageModelProvider { Self { extension, provider_info, + icon_path, state, } } @@ -89,6 +92,10 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider { ui::IconName::ZedAssistant } + fn icon_path(&self) -> Option { + self.icon_path.clone() + } + fn default_model(&self, cx: &App) -> Option> { let state = self.state.read(cx); state diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index c9b6391136da1a..60fe4226ac3b45 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -746,6 +746,11 @@ pub trait LanguageModelProvider: 'static { fn icon(&self) -> IconName { IconName::ZedAssistant } + /// Returns the path to an external SVG icon for this provider, if any. + /// When present, this takes precedence over `icon()`. + fn icon_path(&self) -> Option { + None + } fn default_model(&self, cx: &App) -> Option>; fn default_fast_model(&self, cx: &App) -> Option>; fn provided_models(&self, cx: &App) -> Vec>; diff --git a/extensions/anthropic/src/anthropic.rs b/extensions/anthropic/src/anthropic.rs index 78bf1735bcd5f2..95765ebdab8a99 100644 --- a/extensions/anthropic/src/anthropic.rs +++ b/extensions/anthropic/src/anthropic.rs @@ -490,7 +490,7 @@ impl zed::Extension for AnthropicProvider { vec![LlmProviderInfo { id: "anthropic".into(), name: "Anthropic".into(), - icon: Some("anthropic".into()), + icon: None, }] } diff --git a/extensions/google-ai/src/google_ai.rs b/extensions/google-ai/src/google_ai.rs index 37990951581e9b..010c5a278077f2 100644 --- a/extensions/google-ai/src/google_ai.rs +++ b/extensions/google-ai/src/google_ai.rs @@ -567,7 +567,7 @@ impl zed::Extension for GoogleAiProvider { vec![LlmProviderInfo { id: "google-ai".into(), name: "Google AI".into(), - icon: Some("google-ai".into()), + icon: None, }] } diff --git a/extensions/openai/src/openai.rs b/extensions/openai/src/openai.rs index d464066d13f54f..e596c5c2d6aa8a 100644 --- a/extensions/openai/src/openai.rs +++ b/extensions/openai/src/openai.rs @@ -445,7 +445,7 @@ impl zed::Extension for OpenAiProvider { vec![LlmProviderInfo { id: "openai".into(), name: "OpenAI".into(), - icon: Some("openai".into()), + icon: None, }] } From 63c35d2b00928b7da19227624adab52b9f806947 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:48:25 -0500 Subject: [PATCH 14/45] Use local icons in llm extensions --- extensions/anthropic/icons/anthropic.svg | 11 +++++++++++ extensions/anthropic/src/anthropic.rs | 2 +- extensions/copilot_chat/icons/copilot.svg | 9 +++++++++ extensions/copilot_chat/src/copilot_chat.rs | 2 +- extensions/google-ai/icons/google-ai.svg | 3 +++ extensions/google-ai/src/google_ai.rs | 2 +- extensions/open_router/icons/open-router.svg | 8 ++++++++ extensions/open_router/src/open_router.rs | 2 +- extensions/openai/icons/openai.svg | 3 +++ extensions/openai/src/openai.rs | 2 +- 10 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 extensions/anthropic/icons/anthropic.svg create mode 100644 extensions/copilot_chat/icons/copilot.svg create mode 100644 extensions/google-ai/icons/google-ai.svg create mode 100644 extensions/open_router/icons/open-router.svg create mode 100644 extensions/openai/icons/openai.svg diff --git a/extensions/anthropic/icons/anthropic.svg b/extensions/anthropic/icons/anthropic.svg new file mode 100644 index 00000000000000..07ffd9ac026238 --- /dev/null +++ b/extensions/anthropic/icons/anthropic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/extensions/anthropic/src/anthropic.rs b/extensions/anthropic/src/anthropic.rs index 95765ebdab8a99..26d364cf90acbc 100644 --- a/extensions/anthropic/src/anthropic.rs +++ b/extensions/anthropic/src/anthropic.rs @@ -490,7 +490,7 @@ impl zed::Extension for AnthropicProvider { vec![LlmProviderInfo { id: "anthropic".into(), name: "Anthropic".into(), - icon: None, + icon: Some("icons/anthropic.svg".into()), }] } diff --git a/extensions/copilot_chat/icons/copilot.svg b/extensions/copilot_chat/icons/copilot.svg new file mode 100644 index 00000000000000..e3800e823402e7 --- /dev/null +++ b/extensions/copilot_chat/icons/copilot.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extensions/copilot_chat/src/copilot_chat.rs b/extensions/copilot_chat/src/copilot_chat.rs index 482e61101f6164..2c64d430689f94 100644 --- a/extensions/copilot_chat/src/copilot_chat.rs +++ b/extensions/copilot_chat/src/copilot_chat.rs @@ -429,7 +429,7 @@ impl zed::Extension for CopilotChatProvider { vec![LlmProviderInfo { id: "copilot_chat".into(), name: "Copilot Chat".into(), - icon: None, + icon: Some("icons/copilot.svg".into()), }] } diff --git a/extensions/google-ai/icons/google-ai.svg b/extensions/google-ai/icons/google-ai.svg new file mode 100644 index 00000000000000..03bf85c376fd65 --- /dev/null +++ b/extensions/google-ai/icons/google-ai.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/google-ai/src/google_ai.rs b/extensions/google-ai/src/google_ai.rs index 010c5a278077f2..61baca80b19d9d 100644 --- a/extensions/google-ai/src/google_ai.rs +++ b/extensions/google-ai/src/google_ai.rs @@ -567,7 +567,7 @@ impl zed::Extension for GoogleAiProvider { vec![LlmProviderInfo { id: "google-ai".into(), name: "Google AI".into(), - icon: None, + icon: Some("icons/google-ai.svg".into()), }] } diff --git a/extensions/open_router/icons/open-router.svg b/extensions/open_router/icons/open-router.svg new file mode 100644 index 00000000000000..9139a1c0ef4293 --- /dev/null +++ b/extensions/open_router/icons/open-router.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/extensions/open_router/src/open_router.rs b/extensions/open_router/src/open_router.rs index 0c34bb794f3777..8d8b143cd70a3c 100644 --- a/extensions/open_router/src/open_router.rs +++ b/extensions/open_router/src/open_router.rs @@ -535,7 +535,7 @@ impl zed::Extension for OpenRouterProvider { vec![LlmProviderInfo { id: "open_router".into(), name: "OpenRouter".into(), - icon: None, + icon: Some("icons/open-router.svg".into()), }] } diff --git a/extensions/openai/icons/openai.svg b/extensions/openai/icons/openai.svg new file mode 100644 index 00000000000000..3abc45ce1ffaa2 --- /dev/null +++ b/extensions/openai/icons/openai.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/openai/src/openai.rs b/extensions/openai/src/openai.rs index e596c5c2d6aa8a..40a99352abd5da 100644 --- a/extensions/openai/src/openai.rs +++ b/extensions/openai/src/openai.rs @@ -445,7 +445,7 @@ impl zed::Extension for OpenAiProvider { vec![LlmProviderInfo { id: "openai".into(), name: "OpenAI".into(), - icon: None, + icon: Some("icons/openai.svg".into()), }] } From bf2b8e999e5dd8dc3d27a3f40d7d2d117fa7eca2 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:51:47 -0500 Subject: [PATCH 15/45] use fill=black over fill=currentColor --- extensions/anthropic/icons/anthropic.svg | 4 ++-- extensions/copilot_chat/icons/copilot.svg | 14 +++++++------- extensions/google-ai/icons/google-ai.svg | 2 +- extensions/open_router/icons/open-router.svg | 12 ++++++------ extensions/openai/icons/openai.svg | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/anthropic/icons/anthropic.svg b/extensions/anthropic/icons/anthropic.svg index 07ffd9ac026238..75c1a7e0014e7d 100644 --- a/extensions/anthropic/icons/anthropic.svg +++ b/extensions/anthropic/icons/anthropic.svg @@ -1,7 +1,7 @@ - - + + diff --git a/extensions/copilot_chat/icons/copilot.svg b/extensions/copilot_chat/icons/copilot.svg index e3800e823402e7..2584cd631006c1 100644 --- a/extensions/copilot_chat/icons/copilot.svg +++ b/extensions/copilot_chat/icons/copilot.svg @@ -1,9 +1,9 @@ - - - - - - - + + + + + + + diff --git a/extensions/google-ai/icons/google-ai.svg b/extensions/google-ai/icons/google-ai.svg index 03bf85c376fd65..bdde44ed247531 100644 --- a/extensions/google-ai/icons/google-ai.svg +++ b/extensions/google-ai/icons/google-ai.svg @@ -1,3 +1,3 @@ - + diff --git a/extensions/open_router/icons/open-router.svg b/extensions/open_router/icons/open-router.svg index 9139a1c0ef4293..b6f5164e0b385f 100644 --- a/extensions/open_router/icons/open-router.svg +++ b/extensions/open_router/icons/open-router.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/extensions/openai/icons/openai.svg b/extensions/openai/icons/openai.svg index 3abc45ce1ffaa2..e45ac315a01185 100644 --- a/extensions/openai/icons/openai.svg +++ b/extensions/openai/icons/openai.svg @@ -1,3 +1,3 @@ - + From fec9525be4a50ce0ef66b7bd56baa6c138dff55b Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 17:12:23 -0500 Subject: [PATCH 16/45] Add env var checkbox --- crates/extension_host/src/extension_host.rs | 13 +- .../extension_host/src/extension_settings.rs | 13 +- .../src/wasm_host/llm_provider.rs | 212 +++++++++++++++++- .../src/wasm_host/wit/since_v0_7_0.rs | 50 +++++ .../src/wasm_host/wit/since_v0_8_0.rs | 50 +++++ .../src/settings_content/extension.rs | 6 + 6 files changed, 339 insertions(+), 5 deletions(-) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 869319ded5419d..689224dda0e92a 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -68,6 +68,7 @@ struct LlmProviderWithModels { models: Vec, is_authenticated: bool, icon_path: Option, + auth_config: Option, } pub use extension::{ @@ -1476,11 +1477,20 @@ impl ExtensionStore { SharedString::from(absolute_icon_path) }); + let provider_id_arc: Arc = + provider_info.id.as_str().into(); + let auth_config = extension + .manifest + .language_model_providers + .get(&provider_id_arc) + .and_then(|entry| entry.auth.clone()); + llm_providers_with_models.push(LlmProviderWithModels { provider_info, models, is_authenticated, icon_path, + auth_config, }); } } else { @@ -1579,12 +1589,13 @@ impl ExtensionStore { let mods = llm_provider.models.clone(); let auth = llm_provider.is_authenticated; let icon = llm_provider.icon_path.clone(); + let auth_config = llm_provider.auth_config.clone(); this.proxy.register_language_model_provider( provider_id.clone(), Box::new(move |cx: &mut App| { let provider = Arc::new(ExtensionLanguageModelProvider::new( - wasm_ext, pinfo, mods, auth, icon, cx, + wasm_ext, pinfo, mods, auth, icon, auth_config, cx, )); language_model::LanguageModelRegistry::global(cx).update( cx, diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs index 736dd6b87ae53a..3322ea4068cc08 100644 --- a/crates/extension_host/src/extension_settings.rs +++ b/crates/extension_host/src/extension_settings.rs @@ -1,4 +1,4 @@ -use collections::HashMap; +use collections::{HashMap, HashSet}; use extension::{ DownloadFileCapability, ExtensionCapability, NpmInstallPackageCapability, ProcessExecCapability, }; @@ -16,6 +16,10 @@ pub struct ExtensionSettings { pub auto_install_extensions: HashMap, bool>, pub auto_update_extensions: HashMap, bool>, pub granted_capabilities: Vec, + /// The extension language model providers that are allowed to read API keys + /// from environment variables. Each entry is a provider ID in the format + /// "extension_id:provider_id". + pub allowed_env_var_providers: HashSet>, } impl ExtensionSettings { @@ -60,6 +64,13 @@ impl Settings for ExtensionSettings { } }) .collect(), + allowed_env_var_providers: content + .extension + .allowed_env_var_providers + .clone() + .unwrap_or_default() + .into_iter() + .collect(), } } } diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 0ae833080a59a5..3f16fb31cd11d5 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -1,3 +1,4 @@ +use crate::ExtensionSettings; use crate::wasm_host::WasmExtension; use crate::wasm_host::wit::{ @@ -9,6 +10,7 @@ use crate::wasm_host::wit::{ use anyhow::{Result, anyhow}; use credentials_provider::CredentialsProvider; use editor::Editor; +use extension::LanguageModelAuthConfig; use futures::future::BoxFuture; use futures::stream::BoxStream; use futures::{FutureExt, StreamExt}; @@ -37,12 +39,15 @@ pub struct ExtensionLanguageModelProvider { pub extension: WasmExtension, pub provider_info: LlmProviderInfo, icon_path: Option, + auth_config: Option, state: Entity, } pub struct ExtensionLlmProviderState { is_authenticated: bool, available_models: Vec, + env_var_allowed: bool, + api_key_from_env: bool, } impl EventEmitter<()> for ExtensionLlmProviderState {} @@ -54,17 +59,42 @@ impl ExtensionLanguageModelProvider { models: Vec, is_authenticated: bool, icon_path: Option, + auth_config: Option, cx: &mut App, ) -> Self { + let provider_id_string = format!("{}:{}", extension.manifest.id, provider_info.id); + let env_var_allowed = ExtensionSettings::get_global(cx) + .allowed_env_var_providers + .contains(provider_id_string.as_str()); + + let (is_authenticated, api_key_from_env) = + if env_var_allowed && auth_config.as_ref().is_some_and(|c| c.env_var.is_some()) { + let env_var_name = auth_config.as_ref().unwrap().env_var.as_ref().unwrap(); + if let Ok(value) = std::env::var(env_var_name) { + if !value.is_empty() { + (true, true) + } else { + (is_authenticated, false) + } + } else { + (is_authenticated, false) + } + } else { + (is_authenticated, false) + }; + let state = cx.new(|_| ExtensionLlmProviderState { is_authenticated, available_models: models, + env_var_allowed, + api_key_from_env, }); Self { extension, provider_info, icon_path, + auth_config, state, } } @@ -194,13 +224,17 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider { let credential_key = self.credential_key(); let extension = self.extension.clone(); let extension_provider_id = self.provider_info.id.clone(); + let full_provider_id = self.provider_id_string(); let state = self.state.clone(); + let auth_config = self.auth_config.clone(); cx.new(|cx| { ExtensionProviderConfigurationView::new( credential_key, extension, extension_provider_id, + full_provider_id, + auth_config, state, window, cx, @@ -274,6 +308,8 @@ struct ExtensionProviderConfigurationView { credential_key: String, extension: WasmExtension, extension_provider_id: String, + full_provider_id: String, + auth_config: Option, state: Entity, settings_markdown: Option>, api_key_editor: Entity, @@ -287,6 +323,8 @@ impl ExtensionProviderConfigurationView { credential_key: String, extension: WasmExtension, extension_provider_id: String, + full_provider_id: String, + auth_config: Option, state: Entity, window: &mut Window, cx: &mut Context, @@ -307,6 +345,8 @@ impl ExtensionProviderConfigurationView { credential_key, extension, extension_provider_id, + full_provider_id, + auth_config, state, settings_markdown: None, api_key_editor, @@ -362,7 +402,20 @@ impl ExtensionProviderConfigurationView { let credentials_provider = ::global(cx); let state = self.state.clone(); + // Check if we should use env var (already set in state during provider construction) + let api_key_from_env = self.state.read(cx).api_key_from_env; + cx.spawn(async move |this, cx| { + // If using env var, we're already authenticated + if api_key_from_env { + this.update(cx, |this, cx| { + this.loading_credentials = false; + cx.notify(); + }) + .log_err(); + return; + } + let credentials = credentials_provider .read_credentials(&credential_key, cx) .await @@ -388,6 +441,92 @@ impl ExtensionProviderConfigurationView { .detach(); } + fn toggle_env_var_permission(&mut self, cx: &mut Context) { + let full_provider_id: Arc = self.full_provider_id.clone().into(); + let env_var_name = match &self.auth_config { + Some(config) => config.env_var.clone(), + None => return, + }; + + let state = self.state.clone(); + let currently_allowed = self.state.read(cx).env_var_allowed; + + // Update settings file + settings::update_settings_file(::global(cx), cx, move |settings, _| { + let providers = settings + .extension + .allowed_env_var_providers + .get_or_insert_with(Vec::new); + + if currently_allowed { + providers.retain(|id| id.as_ref() != full_provider_id.as_ref()); + } else { + if !providers + .iter() + .any(|id| id.as_ref() == full_provider_id.as_ref()) + { + providers.push(full_provider_id.clone()); + } + } + }); + + // Update local state + let new_allowed = !currently_allowed; + let new_from_env = if new_allowed { + if let Some(var_name) = &env_var_name { + if let Ok(value) = std::env::var(var_name) { + !value.is_empty() + } else { + false + } + } else { + false + } + } else { + false + }; + + state.update(cx, |state, cx| { + state.env_var_allowed = new_allowed; + state.api_key_from_env = new_from_env; + if new_from_env { + state.is_authenticated = true; + } + cx.notify(); + }); + + // If env var is being disabled, reload credentials from keychain + if !new_allowed { + self.reload_keychain_credentials(cx); + } + + cx.notify(); + } + + fn reload_keychain_credentials(&mut self, cx: &mut Context) { + let credential_key = self.credential_key.clone(); + let credentials_provider = ::global(cx); + let state = self.state.clone(); + + cx.spawn(async move |_this, cx| { + let credentials = credentials_provider + .read_credentials(&credential_key, cx) + .await + .log_err() + .flatten(); + + let has_credentials = credentials.is_some(); + + let _ = cx.update(|cx| { + state.update(cx, |state, cx| { + state.is_authenticated = has_credentials; + cx.notify(); + }); + }); + }) + .detach(); + } + fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { let api_key = self.api_key_editor.read(cx).text(cx); if api_key.is_empty() { @@ -456,6 +595,8 @@ impl gpui::Render for ExtensionProviderConfigurationView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_loading = self.loading_settings || self.loading_credentials; let is_authenticated = self.is_authenticated(cx); + let env_var_allowed = self.state.read(cx).env_var_allowed; + let api_key_from_env = self.state.read(cx).api_key_from_env; if is_loading { return v_flex() @@ -478,8 +619,67 @@ impl gpui::Render for ExtensionProviderConfigurationView { ); } + // Render env var checkbox if the extension specifies an env var + if let Some(auth_config) = &self.auth_config { + if let Some(env_var_name) = &auth_config.env_var { + let env_var_name = env_var_name.clone(); + let checkbox_label = + format!("Read API key from {} environment variable", env_var_name); + + content = content.child( + h_flex() + .gap_2() + .child( + ui::Checkbox::new("env-var-permission", env_var_allowed.into()) + .on_click(cx.listener(|this, _, _window, cx| { + this.toggle_env_var_permission(cx); + })), + ) + .child(Label::new(checkbox_label).size(LabelSize::Small)), + ); + + // Show status if env var is allowed + if env_var_allowed { + if api_key_from_env { + content = content.child( + h_flex() + .gap_2() + .child( + ui::Icon::new(ui::IconName::Check) + .color(Color::Success) + .size(ui::IconSize::Small), + ) + .child( + Label::new(format!("API key loaded from {}", env_var_name)) + .color(Color::Success), + ), + ); + return content.into_any_element(); + } else { + content = content.child( + h_flex() + .gap_2() + .child( + ui::Icon::new(ui::IconName::Warning) + .color(Color::Warning) + .size(ui::IconSize::Small), + ) + .child( + Label::new(format!( + "{} is not set or empty. You can set it and restart Zed, or enter an API key below.", + env_var_name + )) + .color(Color::Warning) + .size(LabelSize::Small), + ), + ); + } + } + } + } + // Render API key section - if is_authenticated { + if is_authenticated && !api_key_from_env { content = content.child( v_flex() .gap_2() @@ -501,13 +701,19 @@ impl gpui::Render for ExtensionProviderConfigurationView { })), ), ); - } else { + } else if !api_key_from_env { + let credential_label = self + .auth_config + .as_ref() + .and_then(|c| c.credential_label.clone()) + .unwrap_or_else(|| "API Key".to_string()); + content = content.child( v_flex() .gap_2() .on_action(cx.listener(Self::save_api_key)) .child( - Label::new("API Key") + Label::new(credential_label) .size(LabelSize::Small) .color(Color::Muted), ) diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs index 20ae6945b69d48..b2a6cc8315849d 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs @@ -1,3 +1,4 @@ +use crate::ExtensionSettings; use crate::wasm_host::wit::since_v0_7_0::{ dap::{ AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest, @@ -1195,6 +1196,55 @@ impl ExtensionImports for WasmState { } async fn llm_get_env_var(&mut self, name: String) -> wasmtime::Result> { + let extension_id = self.manifest.id.clone(); + + // Find which provider (if any) declares this env var in its auth config + let mut allowed_provider_id: Option> = None; + for (provider_id, provider_entry) in &self.manifest.language_model_providers { + if let Some(auth_config) = &provider_entry.auth { + if auth_config.env_var.as_deref() == Some(&name) { + allowed_provider_id = Some(provider_id.clone()); + break; + } + } + } + + // If no provider declares this env var, deny access + let Some(provider_id) = allowed_provider_id else { + log::warn!( + "Extension {} attempted to read env var {} which is not declared in any provider auth config", + extension_id, + name + ); + return Ok(None); + }; + + // Check if the user has allowed this provider to read env vars + let full_provider_id = format!("{}:{}", extension_id, provider_id); + let is_allowed = self + .on_main_thread(move |cx| { + async move { + cx.update(|cx| { + ExtensionSettings::get_global(cx) + .allowed_env_var_providers + .contains(full_provider_id.as_str()) + }) + .unwrap_or(false) + } + .boxed_local() + }) + .await; + + if !is_allowed { + log::debug!( + "Extension {} provider {} is not allowed to read env var {}", + extension_id, + provider_id, + name + ); + return Ok(None); + } + Ok(env::var(&name).ok()) } } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 77a46118d2bfab..b5984d7a19a462 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -1,3 +1,4 @@ +use crate::ExtensionSettings; use crate::wasm_host::wit::since_v0_8_0::{ dap::{ AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest, @@ -1192,6 +1193,55 @@ impl ExtensionImports for WasmState { } async fn llm_get_env_var(&mut self, name: String) -> wasmtime::Result> { + let extension_id = self.manifest.id.clone(); + + // Find which provider (if any) declares this env var in its auth config + let mut allowed_provider_id: Option> = None; + for (provider_id, provider_entry) in &self.manifest.language_model_providers { + if let Some(auth_config) = &provider_entry.auth { + if auth_config.env_var.as_deref() == Some(&name) { + allowed_provider_id = Some(provider_id.clone()); + break; + } + } + } + + // If no provider declares this env var, deny access + let Some(provider_id) = allowed_provider_id else { + log::warn!( + "Extension {} attempted to read env var {} which is not declared in any provider auth config", + extension_id, + name + ); + return Ok(None); + }; + + // Check if the user has allowed this provider to read env vars + let full_provider_id = format!("{}:{}", extension_id, provider_id); + let is_allowed = self + .on_main_thread(move |cx| { + async move { + cx.update(|cx| { + ExtensionSettings::get_global(cx) + .allowed_env_var_providers + .contains(full_provider_id.as_str()) + }) + .unwrap_or(false) + } + .boxed_local() + }) + .await; + + if !is_allowed { + log::debug!( + "Extension {} provider {} is not allowed to read env var {}", + extension_id, + provider_id, + name + ); + return Ok(None); + } + Ok(env::var(&name).ok()) } } diff --git a/crates/settings/src/settings_content/extension.rs b/crates/settings/src/settings_content/extension.rs index 2fefd4ef38aeb9..64df163f4ec961 100644 --- a/crates/settings/src/settings_content/extension.rs +++ b/crates/settings/src/settings_content/extension.rs @@ -20,6 +20,12 @@ pub struct ExtensionSettingsContent { pub auto_update_extensions: HashMap, bool>, /// The capabilities granted to extensions. pub granted_extension_capabilities: Option>, + /// Extension language model providers that are allowed to read API keys from + /// environment variables. Each entry is a provider ID in the format + /// "extension_id:provider_id" (e.g., "openai:openai"). + /// + /// Default: [] + pub allowed_env_var_providers: Option>>, } /// A capability for an extension. From a48bd10da07cdec1d0462c1451ac05a3e9f31db6 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:11:59 -0500 Subject: [PATCH 17/45] Add llm extensions to auto_install_extensions --- assets/settings/default.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f687778d7bd7fc..ce498dd6172c5a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1721,7 +1721,12 @@ // If you don't want any of these extensions, add this field to your settings // and change the value to `false`. "auto_install_extensions": { - "html": true + "anthropic": true, + "copilot_chat": true, + "google-ai": true, + "html": true, + "openai": true, + "open_router": true }, // The capabilities granted to extensions. // From 2d3a3521baaedcd2e5ce34f3883d3b59e37aaa2b Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:50:44 -0500 Subject: [PATCH 18/45] Add OAuth Web Flow auth option for llm provider extensions --- .../wit/since_v0.7.0/extension.wit | 32 +++ .../wit/since_v0.7.0/llm-provider.wit | 47 +++++ .../wit/since_v0.8.0/extension.wit | 32 +++ .../wit/since_v0.8.0/llm-provider.wit | 47 +++++ .../src/wasm_host/wit/since_v0_8_0.rs | 188 ++++++++++++++++++ 5 files changed, 346 insertions(+) diff --git a/crates/extension_api/wit/since_v0.7.0/extension.wit b/crates/extension_api/wit/since_v0.7.0/extension.wit index 92979a87800397..f95dfa04dac25f 100644 --- a/crates/extension_api/wit/since_v0.7.0/extension.wit +++ b/crates/extension_api/wit/since_v0.7.0/extension.wit @@ -249,4 +249,36 @@ world extension { /// Read an environment variable. import llm-get-env-var: func(name: string) -> option; + + // ========================================================================= + // OAuth Web Auth Flow Imports + // ========================================================================= + + use llm-provider.{oauth-web-auth-config, oauth-web-auth-result, oauth-http-request, oauth-http-response}; + + /// Start an OAuth web authentication flow. + /// + /// This will: + /// 1. Start a localhost server to receive the OAuth callback + /// 2. Open the auth URL in the user's default browser + /// 3. Wait for the callback (up to the timeout) + /// 4. Return the callback URL with query parameters + /// + /// The extension is responsible for: + /// - Constructing the auth URL with client_id, redirect_uri, scope, state, etc. + /// - Parsing the callback URL to extract the authorization code + /// - Exchanging the code for tokens using llm-oauth-http-request + import llm-oauth-start-web-auth: func(config: oauth-web-auth-config) -> result; + + /// Make an HTTP request for OAuth token exchange. + /// + /// This is a simple HTTP client for OAuth flows, allowing the extension + /// to handle token exchange with full control over serialization. + import llm-oauth-http-request: func(request: oauth-http-request) -> result; + + /// Open a URL in the user's default browser. + /// + /// Useful for OAuth flows that need to open a browser but handle the + /// callback differently (e.g., polling-based flows). + import llm-oauth-open-browser: func(url: string) -> result<_, string>; } diff --git a/crates/extension_api/wit/since_v0.7.0/llm-provider.wit b/crates/extension_api/wit/since_v0.7.0/llm-provider.wit index 5912654ebcf9e5..aec6569c2efda7 100644 --- a/crates/extension_api/wit/since_v0.7.0/llm-provider.wit +++ b/crates/extension_api/wit/since_v0.7.0/llm-provider.wit @@ -252,4 +252,51 @@ interface llm-provider { /// Minimum token count for a message to be cached. min-total-token-count: u64, } + + // ========================================================================= + // OAuth Web Auth Flow Types + // ========================================================================= + + /// Configuration for starting an OAuth web authentication flow. + record oauth-web-auth-config { + /// The URL to open in the user's browser to start authentication. + /// This should include client_id, redirect_uri, scope, state, etc. + auth-url: string, + /// The path to listen on for the OAuth callback (e.g., "/callback"). + /// A localhost server will be started to receive the redirect. + callback-path: string, + /// Timeout in seconds to wait for the callback (default: 300 = 5 minutes). + timeout-secs: option, + } + + /// Result of an OAuth web authentication flow. + record oauth-web-auth-result { + /// The full callback URL that was received, including query parameters. + /// The extension is responsible for parsing the code, state, etc. + callback-url: string, + /// The port that was used for the localhost callback server. + port: u32, + } + + /// A generic HTTP request for OAuth token exchange. + record oauth-http-request { + /// The URL to request. + url: string, + /// HTTP method (e.g., "POST", "GET"). + method: string, + /// Request headers as key-value pairs. + headers: list>, + /// Request body as a string (for form-encoded or JSON bodies). + body: string, + } + + /// Response from an OAuth HTTP request. + record oauth-http-response { + /// HTTP status code. + status: u16, + /// Response headers as key-value pairs. + headers: list>, + /// Response body as a string. + body: string, + } } diff --git a/crates/extension_api/wit/since_v0.8.0/extension.wit b/crates/extension_api/wit/since_v0.8.0/extension.wit index 92979a87800397..f95dfa04dac25f 100644 --- a/crates/extension_api/wit/since_v0.8.0/extension.wit +++ b/crates/extension_api/wit/since_v0.8.0/extension.wit @@ -249,4 +249,36 @@ world extension { /// Read an environment variable. import llm-get-env-var: func(name: string) -> option; + + // ========================================================================= + // OAuth Web Auth Flow Imports + // ========================================================================= + + use llm-provider.{oauth-web-auth-config, oauth-web-auth-result, oauth-http-request, oauth-http-response}; + + /// Start an OAuth web authentication flow. + /// + /// This will: + /// 1. Start a localhost server to receive the OAuth callback + /// 2. Open the auth URL in the user's default browser + /// 3. Wait for the callback (up to the timeout) + /// 4. Return the callback URL with query parameters + /// + /// The extension is responsible for: + /// - Constructing the auth URL with client_id, redirect_uri, scope, state, etc. + /// - Parsing the callback URL to extract the authorization code + /// - Exchanging the code for tokens using llm-oauth-http-request + import llm-oauth-start-web-auth: func(config: oauth-web-auth-config) -> result; + + /// Make an HTTP request for OAuth token exchange. + /// + /// This is a simple HTTP client for OAuth flows, allowing the extension + /// to handle token exchange with full control over serialization. + import llm-oauth-http-request: func(request: oauth-http-request) -> result; + + /// Open a URL in the user's default browser. + /// + /// Useful for OAuth flows that need to open a browser but handle the + /// callback differently (e.g., polling-based flows). + import llm-oauth-open-browser: func(url: string) -> result<_, string>; } diff --git a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit index 5912654ebcf9e5..aec6569c2efda7 100644 --- a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit +++ b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit @@ -252,4 +252,51 @@ interface llm-provider { /// Minimum token count for a message to be cached. min-total-token-count: u64, } + + // ========================================================================= + // OAuth Web Auth Flow Types + // ========================================================================= + + /// Configuration for starting an OAuth web authentication flow. + record oauth-web-auth-config { + /// The URL to open in the user's browser to start authentication. + /// This should include client_id, redirect_uri, scope, state, etc. + auth-url: string, + /// The path to listen on for the OAuth callback (e.g., "/callback"). + /// A localhost server will be started to receive the redirect. + callback-path: string, + /// Timeout in seconds to wait for the callback (default: 300 = 5 minutes). + timeout-secs: option, + } + + /// Result of an OAuth web authentication flow. + record oauth-web-auth-result { + /// The full callback URL that was received, including query parameters. + /// The extension is responsible for parsing the code, state, etc. + callback-url: string, + /// The port that was used for the localhost callback server. + port: u32, + } + + /// A generic HTTP request for OAuth token exchange. + record oauth-http-request { + /// The URL to request. + url: string, + /// HTTP method (e.g., "POST", "GET"). + method: string, + /// Request headers as key-value pairs. + headers: list>, + /// Request body as a string (for form-encoded or JSON bodies). + body: string, + } + + /// Response from an OAuth HTTP request. + record oauth-http-response { + /// HTTP status code. + status: u16, + /// Response headers as key-value pairs. + headers: list>, + /// Response body as a string. + body: string, + } } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index b5984d7a19a462..213a677687a9c2 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -24,12 +24,15 @@ use gpui::{BackgroundExecutor, SharedString}; use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings}; use project::project_settings::ProjectSettings; use semver::Version; +use smol::net::TcpListener; use std::{ env, + io::{BufRead, Write}, net::Ipv4Addr, path::{Path, PathBuf}, str::FromStr, sync::{Arc, OnceLock}, + time::Duration, }; use task::{SpawnInTerminal, ZedDebugConfig}; use url::Url; @@ -1244,6 +1247,191 @@ impl ExtensionImports for WasmState { Ok(env::var(&name).ok()) } + + async fn llm_oauth_start_web_auth( + &mut self, + config: llm_provider::OauthWebAuthConfig, + ) -> wasmtime::Result> { + let auth_url = config.auth_url; + let callback_path = config.callback_path; + let timeout_secs = config.timeout_secs.unwrap_or(300); + + self.on_main_thread(move |cx| { + async move { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .map_err(|e| anyhow::anyhow!("Failed to bind localhost server: {}", e))?; + let port = listener + .local_addr() + .map_err(|e| anyhow::anyhow!("Failed to get local address: {}", e))? + .port(); + + cx.update(|cx| { + cx.open_url(&auth_url); + })?; + + let accept_future = async { + let (stream, _) = listener + .accept() + .await + .map_err(|e| anyhow::anyhow!("Failed to accept connection: {}", e))?; + + let mut reader = smol::io::BufReader::new(&stream); + let mut request_line = String::new(); + smol::io::AsyncBufReadExt::read_line(&mut reader, &mut request_line) + .await + .map_err(|e| anyhow::anyhow!("Failed to read request: {}", e))?; + + let callback_url = if let Some(path_start) = request_line.find(' ') { + if let Some(path_end) = request_line[path_start + 1..].find(' ') { + let path = &request_line[path_start + 1..path_start + 1 + path_end]; + if path.starts_with(&callback_path) || path.starts_with(&format!("/{}", callback_path.trim_start_matches('/'))) { + format!("http://localhost:{}{}", port, path) + } else { + return Err(anyhow::anyhow!( + "Unexpected callback path: {}", + path + )); + } + } else { + return Err(anyhow::anyhow!("Malformed HTTP request")); + } + } else { + return Err(anyhow::anyhow!("Malformed HTTP request")); + }; + + let response = "HTTP/1.1 200 OK\r\n\ + Content-Type: text/html\r\n\ + Connection: close\r\n\ + \r\n\ + \ + Authentication Complete\ + \ +
\ +

Authentication Complete

\ +

You can close this window and return to Zed.

\ +
"; + + let mut writer = &stream; + smol::io::AsyncWriteExt::write_all(&mut writer, response.as_bytes()) + .await + .ok(); + smol::io::AsyncWriteExt::flush(&mut writer).await.ok(); + + Ok(callback_url) + }; + + let timeout_duration = Duration::from_secs(timeout_secs as u64); + let callback_url = smol::future::or( + accept_future, + async { + smol::Timer::after(timeout_duration).await; + Err(anyhow::anyhow!( + "OAuth callback timed out after {} seconds", + timeout_secs + )) + }, + ) + .await?; + + Ok(llm_provider::OauthWebAuthResult { + callback_url, + port: port as u32, + }) + } + .boxed_local() + }) + .await + .to_wasmtime_result() + } + + async fn llm_oauth_http_request( + &mut self, + request: llm_provider::OauthHttpRequest, + ) -> wasmtime::Result> { + let http_client = self.http_client.clone(); + + self.on_main_thread(move |_cx| { + async move { + let method = match request.method.to_uppercase().as_str() { + "GET" => ::http_client::Method::GET, + "POST" => ::http_client::Method::POST, + "PUT" => ::http_client::Method::PUT, + "DELETE" => ::http_client::Method::DELETE, + "PATCH" => ::http_client::Method::PATCH, + _ => { + return Err(anyhow::anyhow!( + "Unsupported HTTP method: {}", + request.method + )); + } + }; + + let mut builder = ::http_client::HttpRequest::builder() + .method(method) + .uri(&request.url); + + for (key, value) in &request.headers { + builder = builder.header(key.as_str(), value.as_str()); + } + + let body = if request.body.is_empty() { + AsyncBody::empty() + } else { + AsyncBody::from(request.body.into_bytes()) + }; + + let http_request = builder + .body(body) + .map_err(|e| anyhow::anyhow!("Failed to build request: {}", e))?; + + let mut response = http_client + .send(http_request) + .await + .map_err(|e| anyhow::anyhow!("HTTP request failed: {}", e))?; + + let status = response.status().as_u16(); + let headers: Vec<(String, String)> = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + let mut body_bytes = Vec::new(); + futures::AsyncReadExt::read_to_end(response.body_mut(), &mut body_bytes) + .await + .map_err(|e| anyhow::anyhow!("Failed to read response body: {}", e))?; + + let body = String::from_utf8_lossy(&body_bytes).to_string(); + + Ok(llm_provider::OauthHttpResponse { + status, + headers, + body, + }) + } + .boxed_local() + }) + .await + .to_wasmtime_result() + } + + async fn llm_oauth_open_browser( + &mut self, + url: String, + ) -> wasmtime::Result> { + self.on_main_thread(move |cx| { + async move { + cx.update(|cx| { + cx.open_url(&url); + })?; + Ok(()) + } + .boxed_local() + }) + .await + .to_wasmtime_result() + } } // ============================================================================= From aabed9497064573e9f45cfbd293cb9e99e10d6f8 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 16:59:49 -0500 Subject: [PATCH 19/45] Add OAuth via web authentication to llm extensions, migrate copilot --- Cargo.lock | 2 + crates/extension_api/src/extension_api.rs | 9 +- crates/extension_host/Cargo.toml | 2 + .../extension_host/src/copilot_migration.rs | 161 +++++++++++++++ crates/extension_host/src/extension_host.rs | 4 + .../src/wasm_host/wit/since_v0_7_0.rs | 188 ++++++++++++++++++ .../src/wasm_host/wit/since_v0_8_0.rs | 22 +- 7 files changed, 374 insertions(+), 14 deletions(-) create mode 100644 crates/extension_host/src/copilot_migration.rs diff --git a/Cargo.lock b/Cargo.lock index 8ec0db929036d5..854a74f25adc13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5908,6 +5908,7 @@ dependencies = [ "criterion", "ctor", "dap", + "dirs 4.0.0", "editor", "extension", "fs", @@ -5936,6 +5937,7 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", + "smol", "task", "telemetry", "tempfile", diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index ac0827cd014d77..1bb2a84059fb30 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -17,8 +17,9 @@ pub use serde_json; pub use wit::{ CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars, KeyValueStore, LanguageServerInstallationStatus, Project, Range, Worktree, download_file, - llm_delete_credential, llm_get_credential, llm_get_env_var, llm_request_credential, - llm_store_credential, make_file_executable, + llm_delete_credential, llm_get_credential, llm_get_env_var, llm_oauth_http_request, + llm_oauth_open_browser, llm_oauth_start_web_auth, llm_request_credential, llm_store_credential, + make_file_executable, zed::extension::context_server::ContextServerConfiguration, zed::extension::dap::{ AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, BuildTaskTemplate, @@ -35,7 +36,9 @@ pub use wit::{ CompletionRequest as LlmCompletionRequest, CredentialType as LlmCredentialType, ImageData as LlmImageData, MessageContent as LlmMessageContent, MessageRole as LlmMessageRole, ModelCapabilities as LlmModelCapabilities, - ModelInfo as LlmModelInfo, ProviderInfo as LlmProviderInfo, + ModelInfo as LlmModelInfo, OauthHttpRequest as LlmOauthHttpRequest, + OauthHttpResponse as LlmOauthHttpResponse, OauthWebAuthConfig as LlmOauthWebAuthConfig, + OauthWebAuthResult as LlmOauthWebAuthResult, ProviderInfo as LlmProviderInfo, RequestMessage as LlmRequestMessage, StopReason as LlmStopReason, ThinkingContent as LlmThinkingContent, TokenUsage as LlmTokenUsage, ToolChoice as LlmToolChoice, ToolDefinition as LlmToolDefinition, diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index a5c9357b9c80b7..0f3d1eefee9e04 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -24,6 +24,7 @@ client.workspace = true collections.workspace = true credentials_provider.workspace = true dap.workspace = true +dirs.workspace = true editor.workspace = true extension.workspace = true fs.workspace = true @@ -48,6 +49,7 @@ serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true +smol.workspace = true task.workspace = true telemetry.workspace = true tempfile.workspace = true diff --git a/crates/extension_host/src/copilot_migration.rs b/crates/extension_host/src/copilot_migration.rs new file mode 100644 index 00000000000000..90fdf48c0de69c --- /dev/null +++ b/crates/extension_host/src/copilot_migration.rs @@ -0,0 +1,161 @@ +use credentials_provider::CredentialsProvider; +use gpui::App; +use std::path::PathBuf; + +const COPILOT_CHAT_EXTENSION_ID: &str = "copilot_chat"; +const COPILOT_CHAT_PROVIDER_ID: &str = "copilot_chat"; + +pub fn migrate_copilot_credentials_if_needed(extension_id: &str, cx: &mut App) { + if extension_id != COPILOT_CHAT_EXTENSION_ID { + return; + } + + let credential_key = format!( + "extension-llm-{}:{}", + COPILOT_CHAT_EXTENSION_ID, COPILOT_CHAT_PROVIDER_ID + ); + + let credentials_provider = ::global(cx); + + cx.spawn(async move |cx| { + let existing_credential = credentials_provider + .read_credentials(&credential_key, &cx) + .await + .ok() + .flatten(); + + if existing_credential.is_some() { + log::debug!("Copilot Chat extension already has credentials, skipping migration"); + return; + } + + let oauth_token = match read_copilot_oauth_token().await { + Some(token) => token, + None => { + log::debug!("No existing Copilot OAuth token found to migrate"); + return; + } + }; + + log::info!("Migrating existing Copilot OAuth token to Copilot Chat extension"); + + match credentials_provider + .write_credentials(&credential_key, "api_key", oauth_token.as_bytes(), &cx) + .await + { + Ok(()) => { + log::info!("Successfully migrated Copilot OAuth token to Copilot Chat extension"); + } + Err(err) => { + log::error!("Failed to migrate Copilot OAuth token: {}", err); + } + } + }) + .detach(); +} + +async fn read_copilot_oauth_token() -> Option { + let config_paths = copilot_config_paths(); + + for path in config_paths { + if let Some(token) = read_oauth_token_from_file(&path).await { + return Some(token); + } + } + + None +} + +fn copilot_config_paths() -> Vec { + let config_dir = if cfg!(target_os = "windows") { + dirs::data_local_dir() + } else { + std::env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .ok() + .or_else(|| dirs::home_dir().map(|h| h.join(".config"))) + }; + + let Some(config_dir) = config_dir else { + return Vec::new(); + }; + + let copilot_dir = config_dir.join("github-copilot"); + + vec![ + copilot_dir.join("hosts.json"), + copilot_dir.join("apps.json"), + ] +} + +async fn read_oauth_token_from_file(path: &PathBuf) -> Option { + let contents = match smol::fs::read_to_string(path).await { + Ok(contents) => contents, + Err(_) => return None, + }; + + extract_oauth_token(&contents, "github.com") +} + +fn extract_oauth_token(contents: &str, domain: &str) -> Option { + let value: serde_json::Value = serde_json::from_str(contents).ok()?; + let obj = value.as_object()?; + + for (key, value) in obj.iter() { + if key.starts_with(domain) { + if let Some(token) = value.get("oauth_token").and_then(|v| v.as_str()) { + return Some(token.to_string()); + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_oauth_token() { + let contents = r#"{ + "github.com": { + "oauth_token": "ghu_test_token_12345" + } + }"#; + + let token = extract_oauth_token(contents, "github.com"); + assert_eq!(token, Some("ghu_test_token_12345".to_string())); + } + + #[test] + fn test_extract_oauth_token_with_prefix() { + let contents = r#"{ + "github.com:user": { + "oauth_token": "ghu_another_token" + } + }"#; + + let token = extract_oauth_token(contents, "github.com"); + assert_eq!(token, Some("ghu_another_token".to_string())); + } + + #[test] + fn test_extract_oauth_token_missing() { + let contents = r#"{ + "gitlab.com": { + "oauth_token": "some_token" + } + }"#; + + let token = extract_oauth_token(contents, "github.com"); + assert_eq!(token, None); + } + + #[test] + fn test_extract_oauth_token_invalid_json() { + let contents = "not valid json"; + let token = extract_oauth_token(contents, "github.com"); + assert_eq!(token, None); + } +} diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 689224dda0e92a..f2feb4e8657b05 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1,4 +1,5 @@ mod capability_granter; +mod copilot_migration; pub mod extension_settings; pub mod headless_host; pub mod wasm_host; @@ -788,6 +789,9 @@ impl ExtensionStore { this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) }); } + + // Run extension-specific migrations + copilot_migration::migrate_copilot_credentials_if_needed(&extension_id, cx); }) .ok(); } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs index b2a6cc8315849d..6d1457bebd4fb1 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs @@ -24,12 +24,14 @@ use gpui::{BackgroundExecutor, SharedString}; use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings}; use project::project_settings::ProjectSettings; use semver::Version; +use smol::net::TcpListener; use std::{ env, net::Ipv4Addr, path::{Path, PathBuf}, str::FromStr, sync::{Arc, OnceLock}, + time::Duration, }; use task::{SpawnInTerminal, ZedDebugConfig}; use url::Url; @@ -1247,6 +1249,192 @@ impl ExtensionImports for WasmState { Ok(env::var(&name).ok()) } + + async fn llm_oauth_start_web_auth( + &mut self, + config: llm_provider::OauthWebAuthConfig, + ) -> wasmtime::Result> { + let auth_url = config.auth_url; + let callback_path = config.callback_path; + let timeout_secs = config.timeout_secs.unwrap_or(300); + + self.on_main_thread(move |cx| { + async move { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .map_err(|e| anyhow::anyhow!("Failed to bind localhost server: {}", e))?; + let port = listener + .local_addr() + .map_err(|e| anyhow::anyhow!("Failed to get local address: {}", e))? + .port(); + + cx.update(|cx| { + cx.open_url(&auth_url); + })?; + + let accept_future = async { + let (mut stream, _) = listener + .accept() + .await + .map_err(|e| anyhow::anyhow!("Failed to accept connection: {}", e))?; + + let mut request_line = String::new(); + { + let mut reader = smol::io::BufReader::new(&mut stream); + smol::io::AsyncBufReadExt::read_line(&mut reader, &mut request_line) + .await + .map_err(|e| anyhow::anyhow!("Failed to read request: {}", e))?; + } + + let callback_url = if let Some(path_start) = request_line.find(' ') { + if let Some(path_end) = request_line[path_start + 1..].find(' ') { + let path = &request_line[path_start + 1..path_start + 1 + path_end]; + if path.starts_with(&callback_path) || path.starts_with(&format!("/{}", callback_path.trim_start_matches('/'))) { + format!("http://localhost:{}{}", port, path) + } else { + return Err(anyhow::anyhow!( + "Unexpected callback path: {}", + path + )); + } + } else { + return Err(anyhow::anyhow!("Malformed HTTP request")); + } + } else { + return Err(anyhow::anyhow!("Malformed HTTP request")); + }; + + let response = "HTTP/1.1 200 OK\r\n\ + Content-Type: text/html\r\n\ + Connection: close\r\n\ + \r\n\ + \ + Authentication Complete\ + \ +
\ +

Authentication Complete

\ +

You can close this window and return to Zed.

\ +
"; + + smol::io::AsyncWriteExt::write_all(&mut stream, response.as_bytes()) + .await + .ok(); + smol::io::AsyncWriteExt::flush(&mut stream).await.ok(); + + Ok(callback_url) + }; + + let timeout_duration = Duration::from_secs(timeout_secs as u64); + let callback_url = smol::future::or( + accept_future, + async { + smol::Timer::after(timeout_duration).await; + Err(anyhow::anyhow!( + "OAuth callback timed out after {} seconds", + timeout_secs + )) + }, + ) + .await?; + + Ok(llm_provider::OauthWebAuthResult { + callback_url, + port: port as u32, + }) + } + .boxed_local() + }) + .await + .to_wasmtime_result() + } + + async fn llm_oauth_http_request( + &mut self, + request: llm_provider::OauthHttpRequest, + ) -> wasmtime::Result> { + let http_client = self.host.http_client.clone(); + + self.on_main_thread(move |_cx| { + async move { + let method = match request.method.to_uppercase().as_str() { + "GET" => ::http_client::Method::GET, + "POST" => ::http_client::Method::POST, + "PUT" => ::http_client::Method::PUT, + "DELETE" => ::http_client::Method::DELETE, + "PATCH" => ::http_client::Method::PATCH, + _ => { + return Err(anyhow::anyhow!( + "Unsupported HTTP method: {}", + request.method + )); + } + }; + + let mut builder = ::http_client::Request::builder() + .method(method) + .uri(&request.url); + + for (key, value) in &request.headers { + builder = builder.header(key.as_str(), value.as_str()); + } + + let body = if request.body.is_empty() { + AsyncBody::empty() + } else { + AsyncBody::from(request.body.into_bytes()) + }; + + let http_request = builder + .body(body) + .map_err(|e| anyhow::anyhow!("Failed to build request: {}", e))?; + + let mut response = http_client + .send(http_request) + .await + .map_err(|e| anyhow::anyhow!("HTTP request failed: {}", e))?; + + let status = response.status().as_u16(); + let headers: Vec<(String, String)> = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + let mut body_bytes = Vec::new(); + futures::AsyncReadExt::read_to_end(response.body_mut(), &mut body_bytes) + .await + .map_err(|e| anyhow::anyhow!("Failed to read response body: {}", e))?; + + let body = String::from_utf8_lossy(&body_bytes).to_string(); + + Ok(llm_provider::OauthHttpResponse { + status, + headers, + body, + }) + } + .boxed_local() + }) + .await + .to_wasmtime_result() + } + + async fn llm_oauth_open_browser( + &mut self, + url: String, + ) -> wasmtime::Result> { + self.on_main_thread(move |cx| { + async move { + cx.update(|cx| { + cx.open_url(&url); + })?; + Ok(()) + } + .boxed_local() + }) + .await + .to_wasmtime_result() + } } // ============================================================================= diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 213a677687a9c2..714caa05ff1301 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -27,7 +27,6 @@ use semver::Version; use smol::net::TcpListener; use std::{ env, - io::{BufRead, Write}, net::Ipv4Addr, path::{Path, PathBuf}, str::FromStr, @@ -1271,16 +1270,18 @@ impl ExtensionImports for WasmState { })?; let accept_future = async { - let (stream, _) = listener + let (mut stream, _) = listener .accept() .await .map_err(|e| anyhow::anyhow!("Failed to accept connection: {}", e))?; - let mut reader = smol::io::BufReader::new(&stream); let mut request_line = String::new(); - smol::io::AsyncBufReadExt::read_line(&mut reader, &mut request_line) - .await - .map_err(|e| anyhow::anyhow!("Failed to read request: {}", e))?; + { + let mut reader = smol::io::BufReader::new(&mut stream); + smol::io::AsyncBufReadExt::read_line(&mut reader, &mut request_line) + .await + .map_err(|e| anyhow::anyhow!("Failed to read request: {}", e))?; + } let callback_url = if let Some(path_start) = request_line.find(' ') { if let Some(path_end) = request_line[path_start + 1..].find(' ') { @@ -1312,11 +1313,10 @@ impl ExtensionImports for WasmState {

You can close this window and return to Zed.

\ "; - let mut writer = &stream; - smol::io::AsyncWriteExt::write_all(&mut writer, response.as_bytes()) + smol::io::AsyncWriteExt::write_all(&mut stream, response.as_bytes()) .await .ok(); - smol::io::AsyncWriteExt::flush(&mut writer).await.ok(); + smol::io::AsyncWriteExt::flush(&mut stream).await.ok(); Ok(callback_url) }; @@ -1349,7 +1349,7 @@ impl ExtensionImports for WasmState { &mut self, request: llm_provider::OauthHttpRequest, ) -> wasmtime::Result> { - let http_client = self.http_client.clone(); + let http_client = self.host.http_client.clone(); self.on_main_thread(move |_cx| { async move { @@ -1367,7 +1367,7 @@ impl ExtensionImports for WasmState { } }; - let mut builder = ::http_client::HttpRequest::builder() + let mut builder = ::http_client::Request::builder() .method(method) .uri(&request.url); From 3b6b3ff504986f3ba805841d2c6df19e8b0b2e78 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 17:17:51 -0500 Subject: [PATCH 20/45] Specify env vars for the builtin extensions --- crates/extension_host/src/extension_host.rs | 79 +++++++++++++++++++++ extensions/anthropic/extension.toml | 3 + extensions/copilot_chat/extension.toml | 3 + extensions/google-ai/extension.toml | 3 + extensions/open_router/extension.toml | 3 + extensions/openai/extension.toml | 3 + 6 files changed, 94 insertions(+) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index f2feb4e8657b05..3b2f1586a62ea8 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -80,6 +80,80 @@ pub use extension_settings::ExtensionSettings; pub const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200); const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); +/// Extension IDs that are being migrated from hardcoded LLM providers. +/// For backwards compatibility, if the user has the corresponding env var set, +/// we automatically enable env var reading for these extensions. +const LEGACY_LLM_EXTENSION_IDS: &[&str] = &[ + "anthropic", + "copilot_chat", + "google-ai", + "open_router", + "openai", +]; + +/// Migrates legacy LLM provider extensions by auto-enabling env var reading +/// if the env var is currently present in the environment. +fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut App) { + // Only apply migration to known legacy LLM extensions + if !LEGACY_LLM_EXTENSION_IDS.contains(&manifest.id.as_ref()) { + return; + } + + // Check each provider in the manifest + for (provider_id, provider_entry) in &manifest.language_model_providers { + let Some(auth_config) = &provider_entry.auth else { + continue; + }; + let Some(env_var_name) = &auth_config.env_var else { + continue; + }; + + // Check if the env var is present and non-empty + let env_var_is_set = std::env::var(env_var_name) + .map(|v| !v.is_empty()) + .unwrap_or(false); + + if !env_var_is_set { + continue; + } + + let full_provider_id: Arc = format!("{}:{}", manifest.id, provider_id).into(); + + // Check if already in settings + let already_allowed = ExtensionSettings::get_global(cx) + .allowed_env_var_providers + .contains(full_provider_id.as_ref()); + + if already_allowed { + continue; + } + + // Auto-enable env var reading for this provider + log::info!( + "Migrating legacy LLM provider {}: auto-enabling {} env var reading", + full_provider_id, + env_var_name + ); + + settings::update_settings_file(::global(cx), cx, { + let full_provider_id = full_provider_id.clone(); + move |settings, _| { + let providers = settings + .extension + .allowed_env_var_providers + .get_or_insert_with(Vec::new); + + if !providers + .iter() + .any(|id| id.as_ref() == full_provider_id.as_ref()) + { + providers.push(full_provider_id); + } + } + }); + } +} + /// The current extension [`SchemaVersion`] supported by Zed. const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); @@ -781,6 +855,11 @@ impl ExtensionStore { if let ExtensionOperation::Install = operation { this.update(cx, |this, cx| { + // Check for legacy LLM provider migration + if let Some(manifest) = this.extension_manifest_for_id(&extension_id) { + migrate_legacy_llm_provider_env_var(&manifest, cx); + } + cx.emit(Event::ExtensionInstalled(extension_id.clone())); if let Some(events) = ExtensionEvents::try_global(cx) && let Some(manifest) = this.extension_manifest_for_id(&extension_id) diff --git a/extensions/anthropic/extension.toml b/extensions/anthropic/extension.toml index ab27ecda7869c2..c37b8aca34f6cf 100644 --- a/extensions/anthropic/extension.toml +++ b/extensions/anthropic/extension.toml @@ -8,3 +8,6 @@ repository = "https://github.com/zed-industries/zed" [language_model_providers.anthropic] name = "Anthropic" + +[language_model_providers.anthropic.auth] +env_var = "ANTHROPIC_API_KEY" \ No newline at end of file diff --git a/extensions/copilot_chat/extension.toml b/extensions/copilot_chat/extension.toml index 321a17df003e02..9afa188462fcb6 100644 --- a/extensions/copilot_chat/extension.toml +++ b/extensions/copilot_chat/extension.toml @@ -8,3 +8,6 @@ repository = "https://github.com/zed-industries/zed" [language_model_providers.copilot_chat] name = "Copilot Chat" + +[language_model_providers.copilot_chat.auth] +env_var = "GH_COPILOT_TOKEN" \ No newline at end of file diff --git a/extensions/google-ai/extension.toml b/extensions/google-ai/extension.toml index a9ed706c1f4d5e..1b1cb382a7835d 100644 --- a/extensions/google-ai/extension.toml +++ b/extensions/google-ai/extension.toml @@ -8,3 +8,6 @@ repository = "https://github.com/zed-industries/zed" [language_model_providers.google-ai] name = "Google AI" + +[language_model_providers.google-ai.auth] +env_var = "GEMINI_API_KEY" \ No newline at end of file diff --git a/extensions/open_router/extension.toml b/extensions/open_router/extension.toml index 41c8c8458ae016..d321b3d9620c23 100644 --- a/extensions/open_router/extension.toml +++ b/extensions/open_router/extension.toml @@ -8,3 +8,6 @@ repository = "https://github.com/zed-industries/zed" [language_model_providers.open_router] name = "OpenRouter" + +[language_model_providers.open_router.auth] +env_var = "OPENROUTER_API_KEY" \ No newline at end of file diff --git a/extensions/openai/extension.toml b/extensions/openai/extension.toml index b688cd108e91b4..94788688716f1d 100644 --- a/extensions/openai/extension.toml +++ b/extensions/openai/extension.toml @@ -8,3 +8,6 @@ repository = "https://github.com/zed-industries/zed" [language_model_providers.openai] name = "OpenAI" + +[language_model_providers.openai.auth] +env_var = "OPENAI_API_KEY" \ No newline at end of file From e1a9269921d7dff969103f4b9c8b381d286551a3 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 17:20:32 -0500 Subject: [PATCH 21/45] Delete example provider extension --- extensions/example-provider/Cargo.lock | 821 ------------------ extensions/example-provider/Cargo.toml | 15 - extensions/example-provider/extension.toml | 10 - .../example-provider/src/example_provider.rs | 181 ---- 4 files changed, 1027 deletions(-) delete mode 100644 extensions/example-provider/Cargo.lock delete mode 100644 extensions/example-provider/Cargo.toml delete mode 100644 extensions/example-provider/extension.toml delete mode 100644 extensions/example-provider/src/example_provider.rs diff --git a/extensions/example-provider/Cargo.lock b/extensions/example-provider/Cargo.lock deleted file mode 100644 index 08b2f0c343b1d3..00000000000000 --- a/extensions/example-provider/Cargo.lock +++ /dev/null @@ -1,821 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "auditable-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" -dependencies = [ - "semver", - "serde", - "serde_json", - "topological-sort", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "example_provider" -version = "0.1.0" -dependencies = [ - "zed_extension_api", -] - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "topological-sort" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "wasm-encoder" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" -dependencies = [ - "anyhow", - "auditable-serde", - "flate2", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "url", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" -dependencies = [ - "wit-bindgen-rt", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" -dependencies = [ - "bitflags", - "futures", - "once_cell", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zed_extension_api" -version = "0.7.0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/extensions/example-provider/Cargo.toml b/extensions/example-provider/Cargo.toml deleted file mode 100644 index 62d6783382e9e9..00000000000000 --- a/extensions/example-provider/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "example_provider" -version = "0.1.0" -edition = "2021" -publish = false -license = "Apache-2.0" - -[workspace] - -[lib] -path = "src/example_provider.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/example-provider/extension.toml b/extensions/example-provider/extension.toml deleted file mode 100644 index 3b89b7bf8d5241..00000000000000 --- a/extensions/example-provider/extension.toml +++ /dev/null @@ -1,10 +0,0 @@ -id = "example-provider" -name = "Example Provider" -description = "An example LLM provider extension for testing." -version = "0.1.0" -schema_version = 1 -authors = ["Zed Team"] -repository = "https://github.com/zed-industries/zed" - -[language_model_providers.example] -name = "Example Provider" diff --git a/extensions/example-provider/src/example_provider.rs b/extensions/example-provider/src/example_provider.rs deleted file mode 100644 index 190251e4d54095..00000000000000 --- a/extensions/example-provider/src/example_provider.rs +++ /dev/null @@ -1,181 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; -use zed_extension_api::{self as zed, *}; - -struct ExampleProvider { - /// Active completion streams, keyed by stream ID - streams: Mutex>>, - /// Counter for generating unique stream IDs - next_stream_id: Mutex, -} - -impl zed::Extension for ExampleProvider { - fn new() -> Self { - Self { - streams: Mutex::new(HashMap::new()), - next_stream_id: Mutex::new(0), - } - } - - fn llm_providers(&self) -> Vec { - vec![LlmProviderInfo { - id: "example".into(), - name: "Example Provider".into(), - icon: None, - }] - } - - fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { - Ok(vec![ - LlmModelInfo { - id: "example-fast".into(), - name: "Example Fast".into(), - max_token_count: 8192, - max_output_tokens: Some(4096), - capabilities: LlmModelCapabilities { - supports_images: false, - supports_tools: true, - supports_tool_choice_auto: true, - supports_tool_choice_any: true, - supports_tool_choice_none: true, - supports_thinking: false, - tool_input_format: LlmToolInputFormat::JsonSchema, - }, - is_default: false, - is_default_fast: true, - }, - LlmModelInfo { - id: "example-smart".into(), - name: "Example Smart".into(), - max_token_count: 32768, - max_output_tokens: Some(8192), - capabilities: LlmModelCapabilities { - supports_images: true, - supports_tools: true, - supports_tool_choice_auto: true, - supports_tool_choice_any: true, - supports_tool_choice_none: true, - supports_thinking: true, - tool_input_format: LlmToolInputFormat::JsonSchema, - }, - is_default: true, - is_default_fast: false, - }, - ]) - } - - fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { - // Example provider is always authenticated for testing - true - } - - fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { - Some(r#"# Example Provider Setup - -Welcome to the **Example Provider**! This is a demonstration LLM provider for testing purposes. - -## Features - -- 🚀 **Fast responses** - Instant echo responses for testing -- 🛠️ **Tool support** - Full function calling capabilities -- 🖼️ **Image support** - Vision model available (Example Smart) - -## Configuration - -No API key is required for this example provider. It echoes back your messages for testing purposes. - -## Models - -- **Example Fast** - Quick responses, 8K context -- **Example Smart** - Extended features, 32K context, supports images and thinking - -## Usage - -Simply select this provider and start chatting! Your messages will be echoed back with the model name. -"#.to_string()) - } - - fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { - // Example provider doesn't need authentication - Ok(()) - } - - fn llm_stream_completion_start( - &mut self, - _provider_id: &str, - model_id: &str, - request: &LlmCompletionRequest, - ) -> Result { - // Get the last user message to echo back - let user_message = request - .messages - .iter() - .filter(|m| matches!(m.role, LlmMessageRole::User)) - .last() - .and_then(|m| { - m.content.iter().find_map(|c| { - if let LlmMessageContent::Text(text) = c { - Some(text.clone()) - } else { - None - } - }) - }) - .unwrap_or_else(|| "Hello!".to_string()); - - // Create a response based on the model - let response_text = format!("Hello from {}! You said: \"{}\"", model_id, user_message); - - // Create events for the stream - simulate streaming by breaking into chunks - let mut events = vec![LlmCompletionEvent::Started]; - - // Stream the response in chunks - for chunk in response_text.chars().collect::>().chunks(10) { - let text: String = chunk.iter().collect(); - events.push(LlmCompletionEvent::Text(text)); - } - - events.push(LlmCompletionEvent::Stop(LlmStopReason::EndTurn)); - events.push(LlmCompletionEvent::Usage(LlmTokenUsage { - input_tokens: 10, - output_tokens: response_text.len() as u64 / 4, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - })); - - // Generate a unique stream ID - let mut id_counter = self.next_stream_id.lock().unwrap(); - let stream_id = format!("example-stream-{}", *id_counter); - *id_counter += 1; - - // Store the events - self.streams - .lock() - .unwrap() - .insert(stream_id.clone(), events); - - Ok(stream_id) - } - - fn llm_stream_completion_next( - &mut self, - stream_id: &str, - ) -> Result, String> { - let mut streams = self.streams.lock().unwrap(); - if let Some(events) = streams.get_mut(stream_id) { - if events.is_empty() { - Ok(None) - } else { - Ok(Some(events.remove(0))) - } - } else { - Err(format!("Unknown stream: {}", stream_id)) - } - } - - fn llm_stream_completion_close(&mut self, stream_id: &str) { - self.streams.lock().unwrap().remove(stream_id); - } -} - -zed::register_extension!(ExampleProvider); From 5559726fd7cf9d0cd28fc76ca0f3b869939b1fb8 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 17:20:08 -0500 Subject: [PATCH 22/45] Remove builtin extensions for now --- extensions/anthropic/Cargo.lock | 823 ------------------ extensions/anthropic/Cargo.toml | 17 - extensions/anthropic/extension.toml | 13 - extensions/anthropic/icons/anthropic.svg | 11 - extensions/anthropic/src/anthropic.rs | 803 ------------------ extensions/copilot_chat/Cargo.lock | 823 ------------------ extensions/copilot_chat/Cargo.toml | 17 - extensions/copilot_chat/extension.toml | 13 - extensions/copilot_chat/icons/copilot.svg | 9 - extensions/copilot_chat/src/copilot_chat.rs | 696 --------------- extensions/google-ai/Cargo.lock | 823 ------------------ extensions/google-ai/Cargo.toml | 17 - extensions/google-ai/extension.toml | 13 - extensions/google-ai/icons/google-ai.svg | 3 - extensions/google-ai/src/google_ai.rs | 840 ------------------- extensions/open_router/Cargo.lock | 823 ------------------ extensions/open_router/Cargo.toml | 17 - extensions/open_router/extension.toml | 13 - extensions/open_router/icons/open-router.svg | 8 - extensions/open_router/src/open_router.rs | 830 ------------------ extensions/openai/Cargo.lock | 823 ------------------ extensions/openai/Cargo.toml | 17 - extensions/openai/extension.toml | 13 - extensions/openai/icons/openai.svg | 3 - extensions/openai/src/openai.rs | 727 ---------------- 25 files changed, 8195 deletions(-) delete mode 100644 extensions/anthropic/Cargo.lock delete mode 100644 extensions/anthropic/Cargo.toml delete mode 100644 extensions/anthropic/extension.toml delete mode 100644 extensions/anthropic/icons/anthropic.svg delete mode 100644 extensions/anthropic/src/anthropic.rs delete mode 100644 extensions/copilot_chat/Cargo.lock delete mode 100644 extensions/copilot_chat/Cargo.toml delete mode 100644 extensions/copilot_chat/extension.toml delete mode 100644 extensions/copilot_chat/icons/copilot.svg delete mode 100644 extensions/copilot_chat/src/copilot_chat.rs delete mode 100644 extensions/google-ai/Cargo.lock delete mode 100644 extensions/google-ai/Cargo.toml delete mode 100644 extensions/google-ai/extension.toml delete mode 100644 extensions/google-ai/icons/google-ai.svg delete mode 100644 extensions/google-ai/src/google_ai.rs delete mode 100644 extensions/open_router/Cargo.lock delete mode 100644 extensions/open_router/Cargo.toml delete mode 100644 extensions/open_router/extension.toml delete mode 100644 extensions/open_router/icons/open-router.svg delete mode 100644 extensions/open_router/src/open_router.rs delete mode 100644 extensions/openai/Cargo.lock delete mode 100644 extensions/openai/Cargo.toml delete mode 100644 extensions/openai/extension.toml delete mode 100644 extensions/openai/icons/openai.svg delete mode 100644 extensions/openai/src/openai.rs diff --git a/extensions/anthropic/Cargo.lock b/extensions/anthropic/Cargo.lock deleted file mode 100644 index bd558d1ce1a11c..00000000000000 --- a/extensions/anthropic/Cargo.lock +++ /dev/null @@ -1,823 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "auditable-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" -dependencies = [ - "semver", - "serde", - "serde_json", - "topological-sort", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fanthropic" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "zed_extension_api", -] - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "topological-sort" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "wasm-encoder" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" -dependencies = [ - "anyhow", - "auditable-serde", - "flate2", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "url", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" -dependencies = [ - "wit-bindgen-rt", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" -dependencies = [ - "bitflags", - "futures", - "once_cell", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zed_extension_api" -version = "0.7.0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/extensions/anthropic/Cargo.toml b/extensions/anthropic/Cargo.toml deleted file mode 100644 index 25dfe72b0e92ca..00000000000000 --- a/extensions/anthropic/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "anthropic" -version = "0.1.0" -edition = "2021" -publish = false -license = "Apache-2.0" - -[workspace] - -[lib] -path = "src/anthropic.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = { path = "../../crates/extension_api" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" diff --git a/extensions/anthropic/extension.toml b/extensions/anthropic/extension.toml deleted file mode 100644 index c37b8aca34f6cf..00000000000000 --- a/extensions/anthropic/extension.toml +++ /dev/null @@ -1,13 +0,0 @@ -id = "anthropic" -name = "Anthropic" -description = "Anthropic Claude LLM provider for Zed." -version = "0.1.0" -schema_version = 1 -authors = ["Zed Team"] -repository = "https://github.com/zed-industries/zed" - -[language_model_providers.anthropic] -name = "Anthropic" - -[language_model_providers.anthropic.auth] -env_var = "ANTHROPIC_API_KEY" \ No newline at end of file diff --git a/extensions/anthropic/icons/anthropic.svg b/extensions/anthropic/icons/anthropic.svg deleted file mode 100644 index 75c1a7e0014e7d..00000000000000 --- a/extensions/anthropic/icons/anthropic.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/extensions/anthropic/src/anthropic.rs b/extensions/anthropic/src/anthropic.rs deleted file mode 100644 index 26d364cf90acbc..00000000000000 --- a/extensions/anthropic/src/anthropic.rs +++ /dev/null @@ -1,803 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; - -use serde::{Deserialize, Serialize}; -use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; -use zed_extension_api::{self as zed, *}; - -struct AnthropicProvider { - streams: Mutex>, - next_stream_id: Mutex, -} - -struct StreamState { - response_stream: Option, - buffer: String, - started: bool, - current_tool_use: Option, - stop_reason: Option, - pending_signature: Option, -} - -struct ToolUseState { - id: String, - name: String, - input_json: String, -} - -struct ModelDefinition { - real_id: &'static str, - display_name: &'static str, - max_tokens: u64, - max_output_tokens: u64, - supports_images: bool, - supports_thinking: bool, - is_default: bool, - is_default_fast: bool, -} - -const MODELS: &[ModelDefinition] = &[ - ModelDefinition { - real_id: "claude-opus-4-5-20251101", - display_name: "Claude Opus 4.5", - max_tokens: 200_000, - max_output_tokens: 8_192, - supports_images: true, - supports_thinking: false, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "claude-opus-4-5-20251101", - display_name: "Claude Opus 4.5 Thinking", - max_tokens: 200_000, - max_output_tokens: 8_192, - supports_images: true, - supports_thinking: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "claude-sonnet-4-5-20250929", - display_name: "Claude Sonnet 4.5", - max_tokens: 200_000, - max_output_tokens: 8_192, - supports_images: true, - supports_thinking: false, - is_default: true, - is_default_fast: false, - }, - ModelDefinition { - real_id: "claude-sonnet-4-5-20250929", - display_name: "Claude Sonnet 4.5 Thinking", - max_tokens: 200_000, - max_output_tokens: 8_192, - supports_images: true, - supports_thinking: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "claude-sonnet-4-20250514", - display_name: "Claude Sonnet 4", - max_tokens: 200_000, - max_output_tokens: 8_192, - supports_images: true, - supports_thinking: false, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "claude-sonnet-4-20250514", - display_name: "Claude Sonnet 4 Thinking", - max_tokens: 200_000, - max_output_tokens: 8_192, - supports_images: true, - supports_thinking: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "claude-haiku-4-5-20251001", - display_name: "Claude Haiku 4.5", - max_tokens: 200_000, - max_output_tokens: 64_000, - supports_images: true, - supports_thinking: false, - is_default: false, - is_default_fast: true, - }, - ModelDefinition { - real_id: "claude-haiku-4-5-20251001", - display_name: "Claude Haiku 4.5 Thinking", - max_tokens: 200_000, - max_output_tokens: 64_000, - supports_images: true, - supports_thinking: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "claude-3-5-sonnet-latest", - display_name: "Claude 3.5 Sonnet", - max_tokens: 200_000, - max_output_tokens: 8_192, - supports_images: true, - supports_thinking: false, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "claude-3-5-haiku-latest", - display_name: "Claude 3.5 Haiku", - max_tokens: 200_000, - max_output_tokens: 8_192, - supports_images: true, - supports_thinking: false, - is_default: false, - is_default_fast: false, - }, -]; - -fn get_model_definition(display_name: &str) -> Option<&'static ModelDefinition> { - MODELS.iter().find(|m| m.display_name == display_name) -} - -// Anthropic API Request Types - -#[derive(Serialize)] -struct AnthropicRequest { - model: String, - max_tokens: u64, - messages: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - system: Option, - #[serde(skip_serializing_if = "Option::is_none")] - thinking: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - tools: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - tool_choice: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - stop_sequences: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - stream: bool, -} - -#[derive(Serialize)] -struct AnthropicThinking { - #[serde(rename = "type")] - thinking_type: String, - #[serde(skip_serializing_if = "Option::is_none")] - budget_tokens: Option, -} - -#[derive(Serialize)] -struct AnthropicMessage { - role: String, - content: Vec, -} - -#[derive(Serialize, Clone)] -#[serde(tag = "type")] -enum AnthropicContent { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "thinking")] - Thinking { thinking: String, signature: String }, - #[serde(rename = "redacted_thinking")] - RedactedThinking { data: String }, - #[serde(rename = "image")] - Image { source: AnthropicImageSource }, - #[serde(rename = "tool_use")] - ToolUse { - id: String, - name: String, - input: serde_json::Value, - }, - #[serde(rename = "tool_result")] - ToolResult { - tool_use_id: String, - is_error: bool, - content: String, - }, -} - -#[derive(Serialize, Clone)] -struct AnthropicImageSource { - #[serde(rename = "type")] - source_type: String, - media_type: String, - data: String, -} - -#[derive(Serialize)] -struct AnthropicTool { - name: String, - description: String, - input_schema: serde_json::Value, -} - -#[derive(Serialize)] -#[serde(tag = "type", rename_all = "lowercase")] -enum AnthropicToolChoice { - Auto, - Any, - None, -} - -// Anthropic API Response Types - -#[derive(Deserialize, Debug)] -#[serde(tag = "type")] -#[allow(dead_code)] -enum AnthropicEvent { - #[serde(rename = "message_start")] - MessageStart { message: AnthropicMessageResponse }, - #[serde(rename = "content_block_start")] - ContentBlockStart { - index: usize, - content_block: AnthropicContentBlock, - }, - #[serde(rename = "content_block_delta")] - ContentBlockDelta { index: usize, delta: AnthropicDelta }, - #[serde(rename = "content_block_stop")] - ContentBlockStop { index: usize }, - #[serde(rename = "message_delta")] - MessageDelta { - delta: AnthropicMessageDelta, - usage: AnthropicUsage, - }, - #[serde(rename = "message_stop")] - MessageStop, - #[serde(rename = "ping")] - Ping, - #[serde(rename = "error")] - Error { error: AnthropicApiError }, -} - -#[derive(Deserialize, Debug)] -struct AnthropicMessageResponse { - #[allow(dead_code)] - id: String, - #[allow(dead_code)] - role: String, - #[serde(default)] - usage: AnthropicUsage, -} - -#[derive(Deserialize, Debug)] -#[serde(tag = "type")] -enum AnthropicContentBlock { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "thinking")] - Thinking { thinking: String }, - #[serde(rename = "redacted_thinking")] - RedactedThinking { data: String }, - #[serde(rename = "tool_use")] - ToolUse { id: String, name: String }, -} - -#[derive(Deserialize, Debug)] -#[serde(tag = "type")] -enum AnthropicDelta { - #[serde(rename = "text_delta")] - TextDelta { text: String }, - #[serde(rename = "thinking_delta")] - ThinkingDelta { thinking: String }, - #[serde(rename = "signature_delta")] - SignatureDelta { signature: String }, - #[serde(rename = "input_json_delta")] - InputJsonDelta { partial_json: String }, -} - -#[derive(Deserialize, Debug)] -struct AnthropicMessageDelta { - stop_reason: Option, -} - -#[derive(Deserialize, Debug, Default)] -struct AnthropicUsage { - #[serde(default)] - input_tokens: Option, - #[serde(default)] - output_tokens: Option, - #[serde(default)] - cache_creation_input_tokens: Option, - #[serde(default)] - cache_read_input_tokens: Option, -} - -#[derive(Deserialize, Debug)] -struct AnthropicApiError { - #[serde(rename = "type")] - #[allow(dead_code)] - error_type: String, - message: String, -} - -fn convert_request( - model_id: &str, - request: &LlmCompletionRequest, -) -> Result { - let model_def = - get_model_definition(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; - - let mut messages: Vec = Vec::new(); - let mut system_message = String::new(); - - for msg in &request.messages { - match msg.role { - LlmMessageRole::System => { - for content in &msg.content { - if let LlmMessageContent::Text(text) = content { - if !system_message.is_empty() { - system_message.push('\n'); - } - system_message.push_str(text); - } - } - } - LlmMessageRole::User => { - let mut contents: Vec = Vec::new(); - - for content in &msg.content { - match content { - LlmMessageContent::Text(text) => { - if !text.is_empty() { - contents.push(AnthropicContent::Text { text: text.clone() }); - } - } - LlmMessageContent::Image(img) => { - contents.push(AnthropicContent::Image { - source: AnthropicImageSource { - source_type: "base64".to_string(), - media_type: "image/png".to_string(), - data: img.source.clone(), - }, - }); - } - LlmMessageContent::ToolResult(result) => { - let content_text = match &result.content { - LlmToolResultContent::Text(t) => t.clone(), - LlmToolResultContent::Image(_) => "[Image]".to_string(), - }; - contents.push(AnthropicContent::ToolResult { - tool_use_id: result.tool_use_id.clone(), - is_error: result.is_error, - content: content_text, - }); - } - _ => {} - } - } - - if !contents.is_empty() { - messages.push(AnthropicMessage { - role: "user".to_string(), - content: contents, - }); - } - } - LlmMessageRole::Assistant => { - let mut contents: Vec = Vec::new(); - - for content in &msg.content { - match content { - LlmMessageContent::Text(text) => { - if !text.is_empty() { - contents.push(AnthropicContent::Text { text: text.clone() }); - } - } - LlmMessageContent::ToolUse(tool_use) => { - let input: serde_json::Value = - serde_json::from_str(&tool_use.input).unwrap_or_default(); - contents.push(AnthropicContent::ToolUse { - id: tool_use.id.clone(), - name: tool_use.name.clone(), - input, - }); - } - LlmMessageContent::Thinking(thinking) => { - if !thinking.text.is_empty() { - contents.push(AnthropicContent::Thinking { - thinking: thinking.text.clone(), - signature: thinking.signature.clone().unwrap_or_default(), - }); - } - } - LlmMessageContent::RedactedThinking(data) => { - if !data.is_empty() { - contents.push(AnthropicContent::RedactedThinking { - data: data.clone(), - }); - } - } - _ => {} - } - } - - if !contents.is_empty() { - messages.push(AnthropicMessage { - role: "assistant".to_string(), - content: contents, - }); - } - } - } - } - - let tools: Vec = request - .tools - .iter() - .map(|t| AnthropicTool { - name: t.name.clone(), - description: t.description.clone(), - input_schema: serde_json::from_str(&t.input_schema) - .unwrap_or(serde_json::Value::Object(Default::default())), - }) - .collect(); - - let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { - LlmToolChoice::Auto => AnthropicToolChoice::Auto, - LlmToolChoice::Any => AnthropicToolChoice::Any, - LlmToolChoice::None => AnthropicToolChoice::None, - }); - - let thinking = if model_def.supports_thinking && request.thinking_allowed { - Some(AnthropicThinking { - thinking_type: "enabled".to_string(), - budget_tokens: Some(4096), - }) - } else { - None - }; - - Ok(AnthropicRequest { - model: model_def.real_id.to_string(), - max_tokens: model_def.max_output_tokens, - messages, - system: if system_message.is_empty() { - None - } else { - Some(system_message) - }, - thinking, - tools, - tool_choice, - stop_sequences: request.stop_sequences.clone(), - temperature: request.temperature, - stream: true, - }) -} - -fn parse_sse_line(line: &str) -> Option { - let data = line.strip_prefix("data: ")?; - serde_json::from_str(data).ok() -} - -impl zed::Extension for AnthropicProvider { - fn new() -> Self { - Self { - streams: Mutex::new(HashMap::new()), - next_stream_id: Mutex::new(0), - } - } - - fn llm_providers(&self) -> Vec { - vec![LlmProviderInfo { - id: "anthropic".into(), - name: "Anthropic".into(), - icon: Some("icons/anthropic.svg".into()), - }] - } - - fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { - Ok(MODELS - .iter() - .map(|m| LlmModelInfo { - id: m.display_name.to_string(), - name: m.display_name.to_string(), - max_token_count: m.max_tokens, - max_output_tokens: Some(m.max_output_tokens), - capabilities: LlmModelCapabilities { - supports_images: m.supports_images, - supports_tools: true, - supports_tool_choice_auto: true, - supports_tool_choice_any: true, - supports_tool_choice_none: true, - supports_thinking: m.supports_thinking, - tool_input_format: LlmToolInputFormat::JsonSchema, - }, - is_default: m.is_default, - is_default_fast: m.is_default_fast, - }) - .collect()) - } - - fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { - llm_get_credential("anthropic").is_some() - } - - fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { - Some( - r#"# Anthropic Setup - -Welcome to **Anthropic**! This extension provides access to Claude models. - -## Configuration - -Enter your Anthropic API key below. You can get your API key at [console.anthropic.com](https://console.anthropic.com/). - -## Available Models - -| Display Name | Real Model | Context | Output | -|--------------|------------|---------|--------| -| Claude Opus 4.5 | claude-opus-4-5 | 200K | 8K | -| Claude Opus 4.5 Thinking | claude-opus-4-5 | 200K | 8K | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | 200K | 8K | -| Claude Sonnet 4.5 Thinking | claude-sonnet-4-5 | 200K | 8K | -| Claude Sonnet 4 | claude-sonnet-4 | 200K | 8K | -| Claude Sonnet 4 Thinking | claude-sonnet-4 | 200K | 8K | -| Claude Haiku 4.5 | claude-haiku-4-5 | 200K | 64K | -| Claude Haiku 4.5 Thinking | claude-haiku-4-5 | 200K | 64K | -| Claude 3.5 Sonnet | claude-3-5-sonnet | 200K | 8K | -| Claude 3.5 Haiku | claude-3-5-haiku | 200K | 8K | - -## Features - -- ✅ Full streaming support -- ✅ Tool/function calling -- ✅ Vision (image inputs) -- ✅ Extended thinking support -- ✅ All Claude models - -## Pricing - -Uses your Anthropic API credits. See [Anthropic pricing](https://www.anthropic.com/pricing) for details. -"# - .to_string(), - ) - } - - fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { - let provided = llm_request_credential( - "anthropic", - LlmCredentialType::ApiKey, - "Anthropic API Key", - "sk-ant-...", - )?; - if provided { - Ok(()) - } else { - Err("Authentication cancelled".to_string()) - } - } - - fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { - llm_delete_credential("anthropic") - } - - fn llm_stream_completion_start( - &mut self, - _provider_id: &str, - model_id: &str, - request: &LlmCompletionRequest, - ) -> Result { - let api_key = llm_get_credential("anthropic").ok_or_else(|| { - "No API key configured. Please add your Anthropic API key in settings.".to_string() - })?; - - let anthropic_request = convert_request(model_id, request)?; - - let body = serde_json::to_vec(&anthropic_request) - .map_err(|e| format!("Failed to serialize request: {}", e))?; - - let http_request = HttpRequest { - method: HttpMethod::Post, - url: "https://api.anthropic.com/v1/messages".to_string(), - headers: vec![ - ("Content-Type".to_string(), "application/json".to_string()), - ("x-api-key".to_string(), api_key), - ("anthropic-version".to_string(), "2023-06-01".to_string()), - ], - body: Some(body), - redirect_policy: RedirectPolicy::FollowAll, - }; - - let response_stream = http_request - .fetch_stream() - .map_err(|e| format!("HTTP request failed: {}", e))?; - - let stream_id = { - let mut id_counter = self.next_stream_id.lock().unwrap(); - let id = format!("anthropic-stream-{}", *id_counter); - *id_counter += 1; - id - }; - - self.streams.lock().unwrap().insert( - stream_id.clone(), - StreamState { - response_stream: Some(response_stream), - buffer: String::new(), - started: false, - current_tool_use: None, - stop_reason: None, - pending_signature: None, - }, - ); - - Ok(stream_id) - } - - fn llm_stream_completion_next( - &mut self, - stream_id: &str, - ) -> Result, String> { - let mut streams = self.streams.lock().unwrap(); - let state = streams - .get_mut(stream_id) - .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; - - if !state.started { - state.started = true; - return Ok(Some(LlmCompletionEvent::Started)); - } - - let response_stream = state - .response_stream - .as_mut() - .ok_or_else(|| "Stream already closed".to_string())?; - - loop { - if let Some(newline_pos) = state.buffer.find('\n') { - let line = state.buffer[..newline_pos].to_string(); - state.buffer = state.buffer[newline_pos + 1..].to_string(); - - if line.trim().is_empty() || line.starts_with("event:") { - continue; - } - - if let Some(event) = parse_sse_line(&line) { - match event { - AnthropicEvent::MessageStart { message } => { - if let (Some(input), Some(output)) = - (message.usage.input_tokens, message.usage.output_tokens) - { - return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { - input_tokens: input, - output_tokens: output, - cache_creation_input_tokens: message - .usage - .cache_creation_input_tokens, - cache_read_input_tokens: message.usage.cache_read_input_tokens, - }))); - } - } - AnthropicEvent::ContentBlockStart { content_block, .. } => { - match content_block { - AnthropicContentBlock::Text { text } => { - if !text.is_empty() { - return Ok(Some(LlmCompletionEvent::Text(text))); - } - } - AnthropicContentBlock::Thinking { thinking } => { - return Ok(Some(LlmCompletionEvent::Thinking( - LlmThinkingContent { - text: thinking, - signature: None, - }, - ))); - } - AnthropicContentBlock::RedactedThinking { data } => { - return Ok(Some(LlmCompletionEvent::RedactedThinking(data))); - } - AnthropicContentBlock::ToolUse { id, name } => { - state.current_tool_use = Some(ToolUseState { - id, - name, - input_json: String::new(), - }); - } - } - } - AnthropicEvent::ContentBlockDelta { delta, .. } => match delta { - AnthropicDelta::TextDelta { text } => { - if !text.is_empty() { - return Ok(Some(LlmCompletionEvent::Text(text))); - } - } - AnthropicDelta::ThinkingDelta { thinking } => { - return Ok(Some(LlmCompletionEvent::Thinking( - LlmThinkingContent { - text: thinking, - signature: None, - }, - ))); - } - AnthropicDelta::SignatureDelta { signature } => { - state.pending_signature = Some(signature.clone()); - return Ok(Some(LlmCompletionEvent::Thinking( - LlmThinkingContent { - text: String::new(), - signature: Some(signature), - }, - ))); - } - AnthropicDelta::InputJsonDelta { partial_json } => { - if let Some(ref mut tool_use) = state.current_tool_use { - tool_use.input_json.push_str(&partial_json); - } - } - }, - AnthropicEvent::ContentBlockStop { .. } => { - if let Some(tool_use) = state.current_tool_use.take() { - return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { - id: tool_use.id, - name: tool_use.name, - input: tool_use.input_json, - thought_signature: state.pending_signature.take(), - }))); - } - } - AnthropicEvent::MessageDelta { delta, usage } => { - if let Some(reason) = delta.stop_reason { - state.stop_reason = Some(match reason.as_str() { - "end_turn" => LlmStopReason::EndTurn, - "max_tokens" => LlmStopReason::MaxTokens, - "tool_use" => LlmStopReason::ToolUse, - _ => LlmStopReason::EndTurn, - }); - } - if let Some(output) = usage.output_tokens { - return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { - input_tokens: usage.input_tokens.unwrap_or(0), - output_tokens: output, - cache_creation_input_tokens: usage.cache_creation_input_tokens, - cache_read_input_tokens: usage.cache_read_input_tokens, - }))); - } - } - AnthropicEvent::MessageStop => { - if let Some(stop_reason) = state.stop_reason.take() { - return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); - } - return Ok(Some(LlmCompletionEvent::Stop(LlmStopReason::EndTurn))); - } - AnthropicEvent::Ping => {} - AnthropicEvent::Error { error } => { - return Err(format!("API error: {}", error.message)); - } - } - } - - continue; - } - - match response_stream.next_chunk() { - Ok(Some(chunk)) => { - let text = String::from_utf8_lossy(&chunk); - state.buffer.push_str(&text); - } - Ok(None) => { - if let Some(stop_reason) = state.stop_reason.take() { - return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); - } - return Ok(None); - } - Err(e) => { - return Err(format!("Stream error: {}", e)); - } - } - } - } - - fn llm_stream_completion_close(&mut self, stream_id: &str) { - self.streams.lock().unwrap().remove(stream_id); - } -} - -zed::register_extension!(AnthropicProvider); diff --git a/extensions/copilot_chat/Cargo.lock b/extensions/copilot_chat/Cargo.lock deleted file mode 100644 index 1ba4a97d7291c7..00000000000000 --- a/extensions/copilot_chat/Cargo.lock +++ /dev/null @@ -1,823 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "auditable-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" -dependencies = [ - "semver", - "serde", - "serde_json", - "topological-sort", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "copilot_chat" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "zed_extension_api", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "topological-sort" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "wasm-encoder" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" -dependencies = [ - "anyhow", - "auditable-serde", - "flate2", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "url", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" -dependencies = [ - "wit-bindgen-rt", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" -dependencies = [ - "bitflags", - "futures", - "once_cell", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zed_extension_api" -version = "0.8.0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/extensions/copilot_chat/Cargo.toml b/extensions/copilot_chat/Cargo.toml deleted file mode 100644 index 6daae73229776d..00000000000000 --- a/extensions/copilot_chat/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "copilot_chat" -version = "0.1.0" -edition = "2021" -publish = false -license = "Apache-2.0" - -[workspace] - -[lib] -path = "src/copilot_chat.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = { path = "../../crates/extension_api" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" diff --git a/extensions/copilot_chat/extension.toml b/extensions/copilot_chat/extension.toml deleted file mode 100644 index 9afa188462fcb6..00000000000000 --- a/extensions/copilot_chat/extension.toml +++ /dev/null @@ -1,13 +0,0 @@ -id = "copilot_chat" -name = "Copilot Chat" -description = "GitHub Copilot Chat LLM provider for Zed." -version = "0.1.0" -schema_version = 1 -authors = ["Zed Team"] -repository = "https://github.com/zed-industries/zed" - -[language_model_providers.copilot_chat] -name = "Copilot Chat" - -[language_model_providers.copilot_chat.auth] -env_var = "GH_COPILOT_TOKEN" \ No newline at end of file diff --git a/extensions/copilot_chat/icons/copilot.svg b/extensions/copilot_chat/icons/copilot.svg deleted file mode 100644 index 2584cd631006c1..00000000000000 --- a/extensions/copilot_chat/icons/copilot.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/extensions/copilot_chat/src/copilot_chat.rs b/extensions/copilot_chat/src/copilot_chat.rs deleted file mode 100644 index 2c64d430689f94..00000000000000 --- a/extensions/copilot_chat/src/copilot_chat.rs +++ /dev/null @@ -1,696 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; - -use serde::{Deserialize, Serialize}; -use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; -use zed_extension_api::{self as zed, *}; - -struct CopilotChatProvider { - streams: Mutex>, - next_stream_id: Mutex, -} - -struct StreamState { - response_stream: Option, - buffer: String, - started: bool, - tool_calls: HashMap, - tool_calls_emitted: bool, -} - -#[derive(Clone, Default)] -struct AccumulatedToolCall { - id: String, - name: String, - arguments: String, -} - -struct ModelDefinition { - id: &'static str, - display_name: &'static str, - max_tokens: u64, - max_output_tokens: Option, - supports_images: bool, - is_default: bool, - is_default_fast: bool, -} - -const MODELS: &[ModelDefinition] = &[ - ModelDefinition { - id: "gpt-4o", - display_name: "GPT-4o", - max_tokens: 128_000, - max_output_tokens: Some(16_384), - supports_images: true, - is_default: true, - is_default_fast: false, - }, - ModelDefinition { - id: "gpt-4o-mini", - display_name: "GPT-4o Mini", - max_tokens: 128_000, - max_output_tokens: Some(16_384), - supports_images: true, - is_default: false, - is_default_fast: true, - }, - ModelDefinition { - id: "gpt-4.1", - display_name: "GPT-4.1", - max_tokens: 1_000_000, - max_output_tokens: Some(32_768), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "o1", - display_name: "o1", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "o3-mini", - display_name: "o3-mini", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: false, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "claude-3.5-sonnet", - display_name: "Claude 3.5 Sonnet", - max_tokens: 200_000, - max_output_tokens: Some(8_192), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "claude-3.7-sonnet", - display_name: "Claude 3.7 Sonnet", - max_tokens: 200_000, - max_output_tokens: Some(8_192), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "gemini-2.0-flash-001", - display_name: "Gemini 2.0 Flash", - max_tokens: 1_000_000, - max_output_tokens: Some(8_192), - supports_images: true, - is_default: false, - is_default_fast: false, - }, -]; - -fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> { - MODELS.iter().find(|m| m.id == model_id) -} - -#[derive(Serialize)] -struct OpenAiRequest { - model: String, - messages: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - max_tokens: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - tools: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - tool_choice: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - stop: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - stream: bool, - #[serde(skip_serializing_if = "Option::is_none")] - stream_options: Option, -} - -#[derive(Serialize)] -struct StreamOptions { - include_usage: bool, -} - -#[derive(Serialize)] -struct OpenAiMessage { - role: String, - #[serde(skip_serializing_if = "Option::is_none")] - content: Option, - #[serde(skip_serializing_if = "Option::is_none")] - tool_calls: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - tool_call_id: Option, -} - -#[derive(Serialize, Clone)] -#[serde(untagged)] -enum OpenAiContent { - Text(String), - Parts(Vec), -} - -#[derive(Serialize, Clone)] -#[serde(tag = "type")] -enum OpenAiContentPart { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "image_url")] - ImageUrl { image_url: ImageUrl }, -} - -#[derive(Serialize, Clone)] -struct ImageUrl { - url: String, -} - -#[derive(Serialize, Clone)] -struct OpenAiToolCall { - id: String, - #[serde(rename = "type")] - call_type: String, - function: OpenAiFunctionCall, -} - -#[derive(Serialize, Clone)] -struct OpenAiFunctionCall { - name: String, - arguments: String, -} - -#[derive(Serialize)] -struct OpenAiTool { - #[serde(rename = "type")] - tool_type: String, - function: OpenAiFunctionDef, -} - -#[derive(Serialize)] -struct OpenAiFunctionDef { - name: String, - description: String, - parameters: serde_json::Value, -} - -#[derive(Deserialize, Debug)] -struct OpenAiStreamResponse { - choices: Vec, - #[serde(default)] - usage: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenAiStreamChoice { - delta: OpenAiDelta, - finish_reason: Option, -} - -#[derive(Deserialize, Debug, Default)] -struct OpenAiDelta { - #[serde(default)] - content: Option, - #[serde(default)] - tool_calls: Option>, -} - -#[derive(Deserialize, Debug)] -struct OpenAiToolCallDelta { - index: usize, - #[serde(default)] - id: Option, - #[serde(default)] - function: Option, -} - -#[derive(Deserialize, Debug, Default)] -struct OpenAiFunctionDelta { - #[serde(default)] - name: Option, - #[serde(default)] - arguments: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenAiUsage { - prompt_tokens: u64, - completion_tokens: u64, -} - -fn convert_request( - model_id: &str, - request: &LlmCompletionRequest, -) -> Result { - let mut messages: Vec = Vec::new(); - - for msg in &request.messages { - match msg.role { - LlmMessageRole::System => { - let mut text_content = String::new(); - for content in &msg.content { - if let LlmMessageContent::Text(text) = content { - if !text_content.is_empty() { - text_content.push('\n'); - } - text_content.push_str(text); - } - } - if !text_content.is_empty() { - messages.push(OpenAiMessage { - role: "system".to_string(), - content: Some(OpenAiContent::Text(text_content)), - tool_calls: None, - tool_call_id: None, - }); - } - } - LlmMessageRole::User => { - let mut parts: Vec = Vec::new(); - let mut tool_result_messages: Vec = Vec::new(); - - for content in &msg.content { - match content { - LlmMessageContent::Text(text) => { - if !text.is_empty() { - parts.push(OpenAiContentPart::Text { text: text.clone() }); - } - } - LlmMessageContent::Image(img) => { - let data_url = format!("data:image/png;base64,{}", img.source); - parts.push(OpenAiContentPart::ImageUrl { - image_url: ImageUrl { url: data_url }, - }); - } - LlmMessageContent::ToolResult(result) => { - let content_text = match &result.content { - LlmToolResultContent::Text(t) => t.clone(), - LlmToolResultContent::Image(_) => "[Image]".to_string(), - }; - tool_result_messages.push(OpenAiMessage { - role: "tool".to_string(), - content: Some(OpenAiContent::Text(content_text)), - tool_calls: None, - tool_call_id: Some(result.tool_use_id.clone()), - }); - } - _ => {} - } - } - - if !parts.is_empty() { - let content = if parts.len() == 1 { - if let OpenAiContentPart::Text { text } = &parts[0] { - OpenAiContent::Text(text.clone()) - } else { - OpenAiContent::Parts(parts) - } - } else { - OpenAiContent::Parts(parts) - }; - - messages.push(OpenAiMessage { - role: "user".to_string(), - content: Some(content), - tool_calls: None, - tool_call_id: None, - }); - } - - messages.extend(tool_result_messages); - } - LlmMessageRole::Assistant => { - let mut text_content = String::new(); - let mut tool_calls: Vec = Vec::new(); - - for content in &msg.content { - match content { - LlmMessageContent::Text(text) => { - if !text.is_empty() { - if !text_content.is_empty() { - text_content.push('\n'); - } - text_content.push_str(text); - } - } - LlmMessageContent::ToolUse(tool_use) => { - tool_calls.push(OpenAiToolCall { - id: tool_use.id.clone(), - call_type: "function".to_string(), - function: OpenAiFunctionCall { - name: tool_use.name.clone(), - arguments: tool_use.input.clone(), - }, - }); - } - _ => {} - } - } - - messages.push(OpenAiMessage { - role: "assistant".to_string(), - content: if text_content.is_empty() { - None - } else { - Some(OpenAiContent::Text(text_content)) - }, - tool_calls: if tool_calls.is_empty() { - None - } else { - Some(tool_calls) - }, - tool_call_id: None, - }); - } - } - } - - let tools: Vec = request - .tools - .iter() - .map(|t| OpenAiTool { - tool_type: "function".to_string(), - function: OpenAiFunctionDef { - name: t.name.clone(), - description: t.description.clone(), - parameters: serde_json::from_str(&t.input_schema) - .unwrap_or(serde_json::Value::Object(Default::default())), - }, - }) - .collect(); - - let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { - LlmToolChoice::Auto => "auto".to_string(), - LlmToolChoice::Any => "required".to_string(), - LlmToolChoice::None => "none".to_string(), - }); - - let model_def = get_model_definition(model_id); - let max_tokens = request - .max_tokens - .or(model_def.and_then(|m| m.max_output_tokens)); - - Ok(OpenAiRequest { - model: model_id.to_string(), - messages, - max_tokens, - tools, - tool_choice, - stop: request.stop_sequences.clone(), - temperature: request.temperature, - stream: true, - stream_options: Some(StreamOptions { - include_usage: true, - }), - }) -} - -fn parse_sse_line(line: &str) -> Option { - let data = line.strip_prefix("data: ")?; - if data.trim() == "[DONE]" { - return None; - } - serde_json::from_str(data).ok() -} - -impl zed::Extension for CopilotChatProvider { - fn new() -> Self { - Self { - streams: Mutex::new(HashMap::new()), - next_stream_id: Mutex::new(0), - } - } - - fn llm_providers(&self) -> Vec { - vec![LlmProviderInfo { - id: "copilot_chat".into(), - name: "Copilot Chat".into(), - icon: Some("icons/copilot.svg".into()), - }] - } - - fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { - Ok(MODELS - .iter() - .map(|m| LlmModelInfo { - id: m.id.to_string(), - name: m.display_name.to_string(), - max_token_count: m.max_tokens, - max_output_tokens: m.max_output_tokens, - capabilities: LlmModelCapabilities { - supports_images: m.supports_images, - supports_tools: true, - supports_tool_choice_auto: true, - supports_tool_choice_any: true, - supports_tool_choice_none: true, - supports_thinking: false, - tool_input_format: LlmToolInputFormat::JsonSchema, - }, - is_default: m.is_default, - is_default_fast: m.is_default_fast, - }) - .collect()) - } - - fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { - llm_get_credential("copilot_chat").is_some() - } - - fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { - Some( - r#"# Copilot Chat Setup - -Welcome to **Copilot Chat**! This extension provides access to GitHub Copilot's chat models. - -## Configuration - -Enter your GitHub Copilot token below. You need an active GitHub Copilot subscription. - -To get your token: -1. Ensure you have a GitHub Copilot subscription -2. Generate a token from your GitHub Copilot settings - -## Available Models - -| Model | Context | Output | -|-------|---------|--------| -| GPT-4o | 128K | 16K | -| GPT-4o Mini | 128K | 16K | -| GPT-4.1 | 1M | 32K | -| o1 | 200K | 100K | -| o3-mini | 200K | 100K | -| Claude 3.5 Sonnet | 200K | 8K | -| Claude 3.7 Sonnet | 200K | 8K | -| Gemini 2.0 Flash | 1M | 8K | - -## Features - -- ✅ Full streaming support -- ✅ Tool/function calling -- ✅ Vision (image inputs) -- ✅ Multiple model providers via Copilot - -## Note - -This extension requires an active GitHub Copilot subscription. -"# - .to_string(), - ) - } - - fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { - let provided = llm_request_credential( - "copilot_chat", - LlmCredentialType::ApiKey, - "GitHub Copilot Token", - "ghu_...", - )?; - if provided { - Ok(()) - } else { - Err("Authentication cancelled".to_string()) - } - } - - fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { - llm_delete_credential("copilot_chat") - } - - fn llm_stream_completion_start( - &mut self, - _provider_id: &str, - model_id: &str, - request: &LlmCompletionRequest, - ) -> Result { - let api_key = llm_get_credential("copilot_chat").ok_or_else(|| { - "No token configured. Please add your GitHub Copilot token in settings.".to_string() - })?; - - let openai_request = convert_request(model_id, request)?; - - let body = serde_json::to_vec(&openai_request) - .map_err(|e| format!("Failed to serialize request: {}", e))?; - - let http_request = HttpRequest { - method: HttpMethod::Post, - url: "https://api.githubcopilot.com/chat/completions".to_string(), - headers: vec![ - ("Content-Type".to_string(), "application/json".to_string()), - ("Authorization".to_string(), format!("Bearer {}", api_key)), - ( - "Copilot-Integration-Id".to_string(), - "vscode-chat".to_string(), - ), - ("Editor-Version".to_string(), "Zed/1.0.0".to_string()), - ], - body: Some(body), - redirect_policy: RedirectPolicy::FollowAll, - }; - - let response_stream = http_request - .fetch_stream() - .map_err(|e| format!("HTTP request failed: {}", e))?; - - let stream_id = { - let mut id_counter = self.next_stream_id.lock().unwrap(); - let id = format!("copilot-stream-{}", *id_counter); - *id_counter += 1; - id - }; - - self.streams.lock().unwrap().insert( - stream_id.clone(), - StreamState { - response_stream: Some(response_stream), - buffer: String::new(), - started: false, - tool_calls: HashMap::new(), - tool_calls_emitted: false, - }, - ); - - Ok(stream_id) - } - - fn llm_stream_completion_next( - &mut self, - stream_id: &str, - ) -> Result, String> { - let mut streams = self.streams.lock().unwrap(); - let state = streams - .get_mut(stream_id) - .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; - - if !state.started { - state.started = true; - return Ok(Some(LlmCompletionEvent::Started)); - } - - let response_stream = state - .response_stream - .as_mut() - .ok_or_else(|| "Stream already closed".to_string())?; - - loop { - if let Some(newline_pos) = state.buffer.find('\n') { - let line = state.buffer[..newline_pos].to_string(); - state.buffer = state.buffer[newline_pos + 1..].to_string(); - - if line.trim().is_empty() { - continue; - } - - if let Some(response) = parse_sse_line(&line) { - if let Some(choice) = response.choices.first() { - if let Some(content) = &choice.delta.content { - if !content.is_empty() { - return Ok(Some(LlmCompletionEvent::Text(content.clone()))); - } - } - - if let Some(tool_calls) = &choice.delta.tool_calls { - for tc in tool_calls { - let entry = state - .tool_calls - .entry(tc.index) - .or_insert_with(AccumulatedToolCall::default); - - if let Some(id) = &tc.id { - entry.id = id.clone(); - } - if let Some(func) = &tc.function { - if let Some(name) = &func.name { - entry.name = name.clone(); - } - if let Some(args) = &func.arguments { - entry.arguments.push_str(args); - } - } - } - } - - if let Some(finish_reason) = &choice.finish_reason { - if !state.tool_calls.is_empty() && !state.tool_calls_emitted { - state.tool_calls_emitted = true; - let mut tool_calls: Vec<_> = state.tool_calls.drain().collect(); - tool_calls.sort_by_key(|(idx, _)| *idx); - - if let Some((_, tc)) = tool_calls.into_iter().next() { - return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { - id: tc.id, - name: tc.name, - input: tc.arguments, - thought_signature: None, - }))); - } - } - - let stop_reason = match finish_reason.as_str() { - "stop" => LlmStopReason::EndTurn, - "length" => LlmStopReason::MaxTokens, - "tool_calls" => LlmStopReason::ToolUse, - "content_filter" => LlmStopReason::Refusal, - _ => LlmStopReason::EndTurn, - }; - return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); - } - } - - if let Some(usage) = response.usage { - return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { - input_tokens: usage.prompt_tokens, - output_tokens: usage.completion_tokens, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }))); - } - } - - continue; - } - - match response_stream.next_chunk() { - Ok(Some(chunk)) => { - let text = String::from_utf8_lossy(&chunk); - state.buffer.push_str(&text); - } - Ok(None) => { - return Ok(None); - } - Err(e) => { - return Err(format!("Stream error: {}", e)); - } - } - } - } - - fn llm_stream_completion_close(&mut self, stream_id: &str) { - self.streams.lock().unwrap().remove(stream_id); - } -} - -zed::register_extension!(CopilotChatProvider); diff --git a/extensions/google-ai/Cargo.lock b/extensions/google-ai/Cargo.lock deleted file mode 100644 index 2389ff51da0c24..00000000000000 --- a/extensions/google-ai/Cargo.lock +++ /dev/null @@ -1,823 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "auditable-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" -dependencies = [ - "semver", - "serde", - "serde_json", - "topological-sort", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foogle" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "zed_extension_api", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "topological-sort" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "wasm-encoder" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" -dependencies = [ - "anyhow", - "auditable-serde", - "flate2", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "url", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" -dependencies = [ - "wit-bindgen-rt", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" -dependencies = [ - "bitflags", - "futures", - "once_cell", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zed_extension_api" -version = "0.7.0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/extensions/google-ai/Cargo.toml b/extensions/google-ai/Cargo.toml deleted file mode 100644 index f6de35d4066938..00000000000000 --- a/extensions/google-ai/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "google-ai" -version = "0.1.0" -edition = "2021" -publish = false -license = "Apache-2.0" - -[workspace] - -[lib] -path = "src/google_ai.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = { path = "../../crates/extension_api" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" diff --git a/extensions/google-ai/extension.toml b/extensions/google-ai/extension.toml deleted file mode 100644 index 1b1cb382a7835d..00000000000000 --- a/extensions/google-ai/extension.toml +++ /dev/null @@ -1,13 +0,0 @@ -id = "google-ai" -name = "Google AI" -description = "Google Gemini LLM provider for Zed." -version = "0.1.0" -schema_version = 1 -authors = ["Zed Team"] -repository = "https://github.com/zed-industries/zed" - -[language_model_providers.google-ai] -name = "Google AI" - -[language_model_providers.google-ai.auth] -env_var = "GEMINI_API_KEY" \ No newline at end of file diff --git a/extensions/google-ai/icons/google-ai.svg b/extensions/google-ai/icons/google-ai.svg deleted file mode 100644 index bdde44ed247531..00000000000000 --- a/extensions/google-ai/icons/google-ai.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/extensions/google-ai/src/google_ai.rs b/extensions/google-ai/src/google_ai.rs deleted file mode 100644 index 61baca80b19d9d..00000000000000 --- a/extensions/google-ai/src/google_ai.rs +++ /dev/null @@ -1,840 +0,0 @@ -use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Mutex; - -use serde::{Deserialize, Serialize}; -use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; -use zed_extension_api::{self as zed, *}; - -static TOOL_CALL_COUNTER: AtomicU64 = AtomicU64::new(0); - -struct GoogleAiProvider { - streams: Mutex>, - next_stream_id: Mutex, -} - -struct StreamState { - response_stream: Option, - buffer: String, - started: bool, - stop_reason: Option, - wants_tool_use: bool, -} - -struct ModelDefinition { - real_id: &'static str, - display_name: &'static str, - max_tokens: u64, - max_output_tokens: Option, - supports_images: bool, - supports_thinking: bool, - is_default: bool, - is_default_fast: bool, -} - -const MODELS: &[ModelDefinition] = &[ - ModelDefinition { - real_id: "gemini-2.5-flash-lite", - display_name: "Gemini 2.5 Flash-Lite", - max_tokens: 1_048_576, - max_output_tokens: Some(65_536), - supports_images: true, - supports_thinking: true, - is_default: false, - is_default_fast: true, - }, - ModelDefinition { - real_id: "gemini-2.5-flash", - display_name: "Gemini 2.5 Flash", - max_tokens: 1_048_576, - max_output_tokens: Some(65_536), - supports_images: true, - supports_thinking: true, - is_default: true, - is_default_fast: false, - }, - ModelDefinition { - real_id: "gemini-2.5-pro", - display_name: "Gemini 2.5 Pro", - max_tokens: 1_048_576, - max_output_tokens: Some(65_536), - supports_images: true, - supports_thinking: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "gemini-3-pro-preview", - display_name: "Gemini 3 Pro", - max_tokens: 1_048_576, - max_output_tokens: Some(65_536), - supports_images: true, - supports_thinking: true, - is_default: false, - is_default_fast: false, - }, -]; - -fn get_real_model_id(display_name: &str) -> Option<&'static str> { - MODELS - .iter() - .find(|m| m.display_name == display_name) - .map(|m| m.real_id) -} - -fn get_model_supports_thinking(display_name: &str) -> bool { - MODELS - .iter() - .find(|m| m.display_name == display_name) - .map(|m| m.supports_thinking) - .unwrap_or(false) -} - -/// Adapts a JSON schema to be compatible with Google's API subset. -/// Google only supports a specific subset of JSON Schema fields. -/// See: https://ai.google.dev/api/caching#Schema -fn adapt_schema_for_google(json: &mut serde_json::Value) { - adapt_schema_for_google_impl(json, true); -} - -fn adapt_schema_for_google_impl(json: &mut serde_json::Value, is_schema: bool) { - if let serde_json::Value::Object(obj) = json { - // Google's Schema only supports these fields: - // type, format, title, description, nullable, enum, maxItems, minItems, - // properties, required, minProperties, maxProperties, minLength, maxLength, - // pattern, example, anyOf, propertyOrdering, default, items, minimum, maximum - const ALLOWED_KEYS: &[&str] = &[ - "type", - "format", - "title", - "description", - "nullable", - "enum", - "maxItems", - "minItems", - "properties", - "required", - "minProperties", - "maxProperties", - "minLength", - "maxLength", - "pattern", - "example", - "anyOf", - "propertyOrdering", - "default", - "items", - "minimum", - "maximum", - ]; - - // Convert oneOf to anyOf before filtering keys - if let Some(one_of) = obj.remove("oneOf") { - obj.insert("anyOf".to_string(), one_of); - } - - // If type is an array (e.g., ["string", "null"]), take just the first type - if let Some(type_field) = obj.get_mut("type") { - if let serde_json::Value::Array(types) = type_field { - if let Some(first_type) = types.first().cloned() { - *type_field = first_type; - } - } - } - - // Only filter keys if this is a schema object, not a properties map - if is_schema { - obj.retain(|key, _| ALLOWED_KEYS.contains(&key.as_str())); - } - - // Recursively process nested values - // "properties" contains a map of property names -> schemas - // "items" and "anyOf" contain schemas directly - for (key, value) in obj.iter_mut() { - if key == "properties" { - // properties is a map of property_name -> schema - if let serde_json::Value::Object(props) = value { - for (_, prop_schema) in props.iter_mut() { - adapt_schema_for_google_impl(prop_schema, true); - } - } - } else if key == "items" { - // items is a schema - adapt_schema_for_google_impl(value, true); - } else if key == "anyOf" { - // anyOf is an array of schemas - if let serde_json::Value::Array(arr) = value { - for item in arr.iter_mut() { - adapt_schema_for_google_impl(item, true); - } - } - } - } - } else if let serde_json::Value::Array(arr) = json { - for item in arr.iter_mut() { - adapt_schema_for_google_impl(item, true); - } - } -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GoogleRequest { - contents: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - system_instruction: Option, - #[serde(skip_serializing_if = "Option::is_none")] - generation_config: Option, - #[serde(skip_serializing_if = "Option::is_none")] - tools: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - tool_config: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GoogleSystemInstruction { - parts: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleContent { - parts: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - role: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(untagged)] -enum GooglePart { - Text(GoogleTextPart), - InlineData(GoogleInlineDataPart), - FunctionCall(GoogleFunctionCallPart), - FunctionResponse(GoogleFunctionResponsePart), - Thought(GoogleThoughtPart), -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleTextPart { - text: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleInlineDataPart { - inline_data: GoogleBlob, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleBlob { - mime_type: String, - data: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleFunctionCallPart { - function_call: GoogleFunctionCall, - #[serde(skip_serializing_if = "Option::is_none")] - thought_signature: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleFunctionCall { - name: String, - args: serde_json::Value, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleFunctionResponsePart { - function_response: GoogleFunctionResponse, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleFunctionResponse { - name: String, - response: serde_json::Value, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct GoogleThoughtPart { - thought: bool, - thought_signature: String, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GoogleGenerationConfig { - #[serde(skip_serializing_if = "Option::is_none")] - candidate_count: Option, - #[serde(skip_serializing_if = "Option::is_none")] - stop_sequences: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - max_output_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - thinking_config: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GoogleThinkingConfig { - thinking_budget: u32, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GoogleTool { - function_declarations: Vec, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GoogleFunctionDeclaration { - name: String, - description: String, - parameters: serde_json::Value, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GoogleToolConfig { - function_calling_config: GoogleFunctionCallingConfig, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GoogleFunctionCallingConfig { - mode: String, - #[serde(skip_serializing_if = "Option::is_none")] - allowed_function_names: Option>, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct GoogleStreamResponse { - #[serde(default)] - candidates: Vec, - #[serde(default)] - usage_metadata: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct GoogleCandidate { - #[serde(default)] - content: Option, - #[serde(default)] - finish_reason: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct GoogleUsageMetadata { - #[serde(default)] - prompt_token_count: u64, - #[serde(default)] - candidates_token_count: u64, -} - -fn convert_request( - model_id: &str, - request: &LlmCompletionRequest, -) -> Result<(GoogleRequest, String), String> { - let real_model_id = - get_real_model_id(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; - - let supports_thinking = get_model_supports_thinking(model_id); - - let mut contents: Vec = Vec::new(); - let mut system_parts: Vec = Vec::new(); - - for msg in &request.messages { - match msg.role { - LlmMessageRole::System => { - for content in &msg.content { - if let LlmMessageContent::Text(text) = content { - if !text.is_empty() { - system_parts - .push(GooglePart::Text(GoogleTextPart { text: text.clone() })); - } - } - } - } - LlmMessageRole::User => { - let mut parts: Vec = Vec::new(); - - for content in &msg.content { - match content { - LlmMessageContent::Text(text) => { - if !text.is_empty() { - parts.push(GooglePart::Text(GoogleTextPart { text: text.clone() })); - } - } - LlmMessageContent::Image(img) => { - parts.push(GooglePart::InlineData(GoogleInlineDataPart { - inline_data: GoogleBlob { - mime_type: "image/png".to_string(), - data: img.source.clone(), - }, - })); - } - LlmMessageContent::ToolResult(result) => { - let response_value = match &result.content { - LlmToolResultContent::Text(t) => { - serde_json::json!({ "output": t }) - } - LlmToolResultContent::Image(_) => { - serde_json::json!({ "output": "Tool responded with an image" }) - } - }; - parts.push(GooglePart::FunctionResponse(GoogleFunctionResponsePart { - function_response: GoogleFunctionResponse { - name: result.tool_name.clone(), - response: response_value, - }, - })); - } - _ => {} - } - } - - if !parts.is_empty() { - contents.push(GoogleContent { - parts, - role: Some("user".to_string()), - }); - } - } - LlmMessageRole::Assistant => { - let mut parts: Vec = Vec::new(); - - for content in &msg.content { - match content { - LlmMessageContent::Text(text) => { - if !text.is_empty() { - parts.push(GooglePart::Text(GoogleTextPart { text: text.clone() })); - } - } - LlmMessageContent::ToolUse(tool_use) => { - let thought_signature = - tool_use.thought_signature.clone().filter(|s| !s.is_empty()); - - let args: serde_json::Value = - serde_json::from_str(&tool_use.input).unwrap_or_default(); - - parts.push(GooglePart::FunctionCall(GoogleFunctionCallPart { - function_call: GoogleFunctionCall { - name: tool_use.name.clone(), - args, - }, - thought_signature, - })); - } - LlmMessageContent::Thinking(thinking) => { - if let Some(ref signature) = thinking.signature { - if !signature.is_empty() { - parts.push(GooglePart::Thought(GoogleThoughtPart { - thought: true, - thought_signature: signature.clone(), - })); - } - } - } - _ => {} - } - } - - if !parts.is_empty() { - contents.push(GoogleContent { - parts, - role: Some("model".to_string()), - }); - } - } - } - } - - let system_instruction = if system_parts.is_empty() { - None - } else { - Some(GoogleSystemInstruction { - parts: system_parts, - }) - }; - - let tools: Option> = if request.tools.is_empty() { - None - } else { - let declarations: Vec = request - .tools - .iter() - .map(|t| { - let mut parameters: serde_json::Value = serde_json::from_str(&t.input_schema) - .unwrap_or(serde_json::Value::Object(Default::default())); - adapt_schema_for_google(&mut parameters); - GoogleFunctionDeclaration { - name: t.name.clone(), - description: t.description.clone(), - parameters, - } - }) - .collect(); - Some(vec![GoogleTool { - function_declarations: declarations, - }]) - }; - - let tool_config = request.tool_choice.as_ref().map(|tc| { - let mode = match tc { - LlmToolChoice::Auto => "AUTO", - LlmToolChoice::Any => "ANY", - LlmToolChoice::None => "NONE", - }; - GoogleToolConfig { - function_calling_config: GoogleFunctionCallingConfig { - mode: mode.to_string(), - allowed_function_names: None, - }, - } - }); - - let thinking_config = if supports_thinking && request.thinking_allowed { - Some(GoogleThinkingConfig { - thinking_budget: 8192, - }) - } else { - None - }; - - let generation_config = Some(GoogleGenerationConfig { - candidate_count: Some(1), - stop_sequences: if request.stop_sequences.is_empty() { - None - } else { - Some(request.stop_sequences.clone()) - }, - max_output_tokens: None, - temperature: request.temperature.map(|t| t as f64).or(Some(1.0)), - thinking_config, - }); - - Ok(( - GoogleRequest { - contents, - system_instruction, - generation_config, - tools, - tool_config, - }, - real_model_id.to_string(), - )) -} - -fn parse_stream_line(line: &str) -> Option { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed == "[" || trimmed == "]" || trimmed == "," { - return None; - } - - let json_str = trimmed.strip_prefix("data: ").unwrap_or(trimmed); - let json_str = json_str.trim_start_matches(',').trim(); - - if json_str.is_empty() { - return None; - } - - serde_json::from_str(json_str).ok() -} - -impl zed::Extension for GoogleAiProvider { - fn new() -> Self { - Self { - streams: Mutex::new(HashMap::new()), - next_stream_id: Mutex::new(0), - } - } - - fn llm_providers(&self) -> Vec { - vec![LlmProviderInfo { - id: "google-ai".into(), - name: "Google AI".into(), - icon: Some("icons/google-ai.svg".into()), - }] - } - - fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { - Ok(MODELS - .iter() - .map(|m| LlmModelInfo { - id: m.display_name.to_string(), - name: m.display_name.to_string(), - max_token_count: m.max_tokens, - max_output_tokens: m.max_output_tokens, - capabilities: LlmModelCapabilities { - supports_images: m.supports_images, - supports_tools: true, - supports_tool_choice_auto: true, - supports_tool_choice_any: true, - supports_tool_choice_none: true, - supports_thinking: m.supports_thinking, - tool_input_format: LlmToolInputFormat::JsonSchema, - }, - is_default: m.is_default, - is_default_fast: m.is_default_fast, - }) - .collect()) - } - - fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { - llm_get_credential("google-ai").is_some() - } - - fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { - Some( - r#"# Google AI Setup - -Welcome to **Google AI**! This extension provides access to Google Gemini models. - -## Configuration - -Enter your Google AI API key below. You can get your API key at [aistudio.google.com/apikey](https://aistudio.google.com/apikey). - -## Available Models - -| Display Name | Real Model | Context | Output | -|--------------|------------|---------|--------| -| Gemini 2.5 Flash-Lite | gemini-2.5-flash-lite | 1M | 65K | -| Gemini 2.5 Flash | gemini-2.5-flash | 1M | 65K | -| Gemini 2.5 Pro | gemini-2.5-pro | 1M | 65K | -| Gemini 3 Pro | gemini-3-pro-preview | 1M | 65K | - -## Features - -- ✅ Full streaming support -- ✅ Tool/function calling with thought signatures -- ✅ Vision (image inputs) -- ✅ Extended thinking support -- ✅ All Gemini models - -## Pricing - -Uses your Google AI API credits. See [Google AI pricing](https://ai.google.dev/pricing) for details. -"# - .to_string(), - ) - } - - fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { - let provided = llm_request_credential( - "google-ai", - LlmCredentialType::ApiKey, - "Google AI API Key", - "AIza...", - )?; - if provided { - Ok(()) - } else { - Err("Authentication cancelled".to_string()) - } - } - - fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { - llm_delete_credential("google-ai") - } - - fn llm_stream_completion_start( - &mut self, - _provider_id: &str, - model_id: &str, - request: &LlmCompletionRequest, - ) -> Result { - let api_key = llm_get_credential("google-ai").ok_or_else(|| { - "No API key configured. Please add your Google AI API key in settings.".to_string() - })?; - - let (google_request, real_model_id) = convert_request(model_id, request)?; - - let body = serde_json::to_vec(&google_request) - .map_err(|e| format!("Failed to serialize request: {}", e))?; - - let url = format!( - "https://generativelanguage.googleapis.com/v1beta/models/{}:streamGenerateContent?alt=sse&key={}", - real_model_id, api_key - ); - - let http_request = HttpRequest { - method: HttpMethod::Post, - url, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: Some(body), - redirect_policy: RedirectPolicy::FollowAll, - }; - - let response_stream = http_request - .fetch_stream() - .map_err(|e| format!("HTTP request failed: {}", e))?; - - let stream_id = { - let mut id_counter = self.next_stream_id.lock().unwrap(); - let id = format!("google-ai-stream-{}", *id_counter); - *id_counter += 1; - id - }; - - self.streams.lock().unwrap().insert( - stream_id.clone(), - StreamState { - response_stream: Some(response_stream), - buffer: String::new(), - started: false, - stop_reason: None, - wants_tool_use: false, - }, - ); - - Ok(stream_id) - } - - fn llm_stream_completion_next( - &mut self, - stream_id: &str, - ) -> Result, String> { - let mut streams = self.streams.lock().unwrap(); - let state = streams - .get_mut(stream_id) - .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; - - if !state.started { - state.started = true; - return Ok(Some(LlmCompletionEvent::Started)); - } - - let response_stream = state - .response_stream - .as_mut() - .ok_or_else(|| "Stream already closed".to_string())?; - - loop { - if let Some(newline_pos) = state.buffer.find('\n') { - let line = state.buffer[..newline_pos].to_string(); - state.buffer = state.buffer[newline_pos + 1..].to_string(); - - if let Some(response) = parse_stream_line(&line) { - for candidate in response.candidates { - if let Some(finish_reason) = &candidate.finish_reason { - state.stop_reason = Some(match finish_reason.as_str() { - "STOP" => { - if state.wants_tool_use { - LlmStopReason::ToolUse - } else { - LlmStopReason::EndTurn - } - } - "MAX_TOKENS" => LlmStopReason::MaxTokens, - "SAFETY" => LlmStopReason::Refusal, - _ => LlmStopReason::EndTurn, - }); - } - - if let Some(content) = candidate.content { - for part in content.parts { - match part { - GooglePart::Text(text_part) => { - if !text_part.text.is_empty() { - return Ok(Some(LlmCompletionEvent::Text( - text_part.text, - ))); - } - } - GooglePart::FunctionCall(fc_part) => { - state.wants_tool_use = true; - let next_tool_id = - TOOL_CALL_COUNTER.fetch_add(1, Ordering::SeqCst); - let id = format!( - "{}-{}", - fc_part.function_call.name, next_tool_id - ); - - let thought_signature = - fc_part.thought_signature.filter(|s| !s.is_empty()); - - return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { - id, - name: fc_part.function_call.name, - input: fc_part.function_call.args.to_string(), - thought_signature, - }))); - } - GooglePart::Thought(thought_part) => { - return Ok(Some(LlmCompletionEvent::Thinking( - LlmThinkingContent { - text: "(Encrypted thought)".to_string(), - signature: Some(thought_part.thought_signature), - }, - ))); - } - _ => {} - } - } - } - } - - if let Some(usage) = response.usage_metadata { - return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { - input_tokens: usage.prompt_token_count, - output_tokens: usage.candidates_token_count, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }))); - } - } - - continue; - } - - match response_stream.next_chunk() { - Ok(Some(chunk)) => { - let text = String::from_utf8_lossy(&chunk); - state.buffer.push_str(&text); - } - Ok(None) => { - // Stream ended - check if we have a stop reason - if let Some(stop_reason) = state.stop_reason.take() { - return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); - } - - // No stop reason - this is unexpected. Check if buffer contains error info - let mut error_msg = String::from("Stream ended unexpectedly."); - - // Try to parse remaining buffer as potential error response - if !state.buffer.is_empty() { - error_msg.push_str(&format!( - "\nRemaining buffer: {}", - &state.buffer[..state.buffer.len().min(1000)] - )); - } - - return Err(error_msg); - } - Err(e) => { - return Err(format!("Stream error: {}", e)); - } - } - } - } - - fn llm_stream_completion_close(&mut self, stream_id: &str) { - self.streams.lock().unwrap().remove(stream_id); - } -} - -zed::register_extension!(GoogleAiProvider); diff --git a/extensions/open_router/Cargo.lock b/extensions/open_router/Cargo.lock deleted file mode 100644 index 4dea7c7a8a9cd8..00000000000000 --- a/extensions/open_router/Cargo.lock +++ /dev/null @@ -1,823 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "auditable-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" -dependencies = [ - "semver", - "serde", - "serde_json", - "topological-sort", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "open_router" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "zed_extension_api", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "topological-sort" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "wasm-encoder" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" -dependencies = [ - "anyhow", - "auditable-serde", - "flate2", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "url", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" -dependencies = [ - "wit-bindgen-rt", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" -dependencies = [ - "bitflags", - "futures", - "once_cell", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zed_extension_api" -version = "0.8.0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/extensions/open_router/Cargo.toml b/extensions/open_router/Cargo.toml deleted file mode 100644 index 47a87b5d52571a..00000000000000 --- a/extensions/open_router/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "open_router" -version = "0.1.0" -edition = "2021" -publish = false -license = "Apache-2.0" - -[workspace] - -[lib] -path = "src/open_router.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = { path = "../../crates/extension_api" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" diff --git a/extensions/open_router/extension.toml b/extensions/open_router/extension.toml deleted file mode 100644 index d321b3d9620c23..00000000000000 --- a/extensions/open_router/extension.toml +++ /dev/null @@ -1,13 +0,0 @@ -id = "open_router" -name = "OpenRouter" -description = "OpenRouter LLM provider - access multiple AI models through a unified API." -version = "0.1.0" -schema_version = 1 -authors = ["Zed Team"] -repository = "https://github.com/zed-industries/zed" - -[language_model_providers.open_router] -name = "OpenRouter" - -[language_model_providers.open_router.auth] -env_var = "OPENROUTER_API_KEY" \ No newline at end of file diff --git a/extensions/open_router/icons/open-router.svg b/extensions/open_router/icons/open-router.svg deleted file mode 100644 index b6f5164e0b385f..00000000000000 --- a/extensions/open_router/icons/open-router.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/extensions/open_router/src/open_router.rs b/extensions/open_router/src/open_router.rs deleted file mode 100644 index 8d8b143cd70a3c..00000000000000 --- a/extensions/open_router/src/open_router.rs +++ /dev/null @@ -1,830 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; - -use serde::{Deserialize, Serialize}; -use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; -use zed_extension_api::{self as zed, *}; - -struct OpenRouterProvider { - streams: Mutex>, - next_stream_id: Mutex, -} - -struct StreamState { - response_stream: Option, - buffer: String, - started: bool, - tool_calls: HashMap, - tool_calls_emitted: bool, -} - -#[derive(Clone, Default)] -struct AccumulatedToolCall { - id: String, - name: String, - arguments: String, -} - -struct ModelDefinition { - id: &'static str, - display_name: &'static str, - max_tokens: u64, - max_output_tokens: Option, - supports_images: bool, - supports_tools: bool, - is_default: bool, - is_default_fast: bool, -} - -const MODELS: &[ModelDefinition] = &[ - // Anthropic Models - ModelDefinition { - id: "anthropic/claude-sonnet-4", - display_name: "Claude Sonnet 4", - max_tokens: 200_000, - max_output_tokens: Some(8_192), - supports_images: true, - supports_tools: true, - is_default: true, - is_default_fast: false, - }, - ModelDefinition { - id: "anthropic/claude-opus-4", - display_name: "Claude Opus 4", - max_tokens: 200_000, - max_output_tokens: Some(8_192), - supports_images: true, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "anthropic/claude-haiku-4", - display_name: "Claude Haiku 4", - max_tokens: 200_000, - max_output_tokens: Some(8_192), - supports_images: true, - supports_tools: true, - is_default: false, - is_default_fast: true, - }, - ModelDefinition { - id: "anthropic/claude-3.5-sonnet", - display_name: "Claude 3.5 Sonnet", - max_tokens: 200_000, - max_output_tokens: Some(8_192), - supports_images: true, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - // OpenAI Models - ModelDefinition { - id: "openai/gpt-4o", - display_name: "GPT-4o", - max_tokens: 128_000, - max_output_tokens: Some(16_384), - supports_images: true, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "openai/gpt-4o-mini", - display_name: "GPT-4o Mini", - max_tokens: 128_000, - max_output_tokens: Some(16_384), - supports_images: true, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "openai/o1", - display_name: "o1", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: true, - supports_tools: false, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "openai/o3-mini", - display_name: "o3-mini", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: false, - supports_tools: false, - is_default: false, - is_default_fast: false, - }, - // Google Models - ModelDefinition { - id: "google/gemini-2.0-flash-001", - display_name: "Gemini 2.0 Flash", - max_tokens: 1_000_000, - max_output_tokens: Some(8_192), - supports_images: true, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "google/gemini-2.5-pro-preview", - display_name: "Gemini 2.5 Pro", - max_tokens: 1_000_000, - max_output_tokens: Some(8_192), - supports_images: true, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - // Meta Models - ModelDefinition { - id: "meta-llama/llama-3.3-70b-instruct", - display_name: "Llama 3.3 70B", - max_tokens: 128_000, - max_output_tokens: Some(4_096), - supports_images: false, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "meta-llama/llama-4-maverick", - display_name: "Llama 4 Maverick", - max_tokens: 128_000, - max_output_tokens: Some(4_096), - supports_images: true, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - // Mistral Models - ModelDefinition { - id: "mistralai/mistral-large-2411", - display_name: "Mistral Large", - max_tokens: 128_000, - max_output_tokens: Some(4_096), - supports_images: false, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "mistralai/codestral-latest", - display_name: "Codestral", - max_tokens: 32_000, - max_output_tokens: Some(4_096), - supports_images: false, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - // DeepSeek Models - ModelDefinition { - id: "deepseek/deepseek-chat-v3-0324", - display_name: "DeepSeek V3", - max_tokens: 64_000, - max_output_tokens: Some(8_192), - supports_images: false, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "deepseek/deepseek-r1", - display_name: "DeepSeek R1", - max_tokens: 64_000, - max_output_tokens: Some(8_192), - supports_images: false, - supports_tools: false, - is_default: false, - is_default_fast: false, - }, - // Qwen Models - ModelDefinition { - id: "qwen/qwen3-235b-a22b", - display_name: "Qwen 3 235B", - max_tokens: 40_000, - max_output_tokens: Some(8_192), - supports_images: false, - supports_tools: true, - is_default: false, - is_default_fast: false, - }, -]; - -fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> { - MODELS.iter().find(|m| m.id == model_id) -} - -#[derive(Serialize)] -struct OpenRouterRequest { - model: String, - messages: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - max_tokens: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - tools: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - tool_choice: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - stop: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - stream: bool, -} - -#[derive(Serialize)] -struct OpenRouterMessage { - role: String, - #[serde(skip_serializing_if = "Option::is_none")] - content: Option, - #[serde(skip_serializing_if = "Option::is_none")] - tool_calls: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - tool_call_id: Option, -} - -#[derive(Serialize, Clone)] -#[serde(untagged)] -enum OpenRouterContent { - Text(String), - Parts(Vec), -} - -#[derive(Serialize, Clone)] -#[serde(tag = "type")] -enum OpenRouterContentPart { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "image_url")] - ImageUrl { image_url: ImageUrl }, -} - -#[derive(Serialize, Clone)] -struct ImageUrl { - url: String, -} - -#[derive(Serialize, Clone)] -struct OpenRouterToolCall { - id: String, - #[serde(rename = "type")] - call_type: String, - function: OpenRouterFunctionCall, -} - -#[derive(Serialize, Clone)] -struct OpenRouterFunctionCall { - name: String, - arguments: String, -} - -#[derive(Serialize)] -struct OpenRouterTool { - #[serde(rename = "type")] - tool_type: String, - function: OpenRouterFunctionDef, -} - -#[derive(Serialize)] -struct OpenRouterFunctionDef { - name: String, - description: String, - parameters: serde_json::Value, -} - -#[derive(Deserialize, Debug)] -struct OpenRouterStreamResponse { - choices: Vec, - #[serde(default)] - usage: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenRouterStreamChoice { - delta: OpenRouterDelta, - finish_reason: Option, -} - -#[derive(Deserialize, Debug, Default)] -struct OpenRouterDelta { - #[serde(default)] - content: Option, - #[serde(default)] - tool_calls: Option>, -} - -#[derive(Deserialize, Debug)] -struct OpenRouterToolCallDelta { - index: usize, - #[serde(default)] - id: Option, - #[serde(default)] - function: Option, -} - -#[derive(Deserialize, Debug, Default)] -struct OpenRouterFunctionDelta { - #[serde(default)] - name: Option, - #[serde(default)] - arguments: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenRouterUsage { - prompt_tokens: u64, - completion_tokens: u64, -} - -fn convert_request( - model_id: &str, - request: &LlmCompletionRequest, -) -> Result { - let mut messages: Vec = Vec::new(); - - for msg in &request.messages { - match msg.role { - LlmMessageRole::System => { - let mut text_content = String::new(); - for content in &msg.content { - if let LlmMessageContent::Text(text) = content { - if !text_content.is_empty() { - text_content.push('\n'); - } - text_content.push_str(text); - } - } - if !text_content.is_empty() { - messages.push(OpenRouterMessage { - role: "system".to_string(), - content: Some(OpenRouterContent::Text(text_content)), - tool_calls: None, - tool_call_id: None, - }); - } - } - LlmMessageRole::User => { - let mut parts: Vec = Vec::new(); - let mut tool_result_messages: Vec = Vec::new(); - - for content in &msg.content { - match content { - LlmMessageContent::Text(text) => { - if !text.is_empty() { - parts.push(OpenRouterContentPart::Text { text: text.clone() }); - } - } - LlmMessageContent::Image(img) => { - let data_url = format!("data:image/png;base64,{}", img.source); - parts.push(OpenRouterContentPart::ImageUrl { - image_url: ImageUrl { url: data_url }, - }); - } - LlmMessageContent::ToolResult(result) => { - let content_text = match &result.content { - LlmToolResultContent::Text(t) => t.clone(), - LlmToolResultContent::Image(_) => "[Image]".to_string(), - }; - tool_result_messages.push(OpenRouterMessage { - role: "tool".to_string(), - content: Some(OpenRouterContent::Text(content_text)), - tool_calls: None, - tool_call_id: Some(result.tool_use_id.clone()), - }); - } - _ => {} - } - } - - if !parts.is_empty() { - let content = if parts.len() == 1 { - if let OpenRouterContentPart::Text { text } = &parts[0] { - OpenRouterContent::Text(text.clone()) - } else { - OpenRouterContent::Parts(parts) - } - } else { - OpenRouterContent::Parts(parts) - }; - - messages.push(OpenRouterMessage { - role: "user".to_string(), - content: Some(content), - tool_calls: None, - tool_call_id: None, - }); - } - - messages.extend(tool_result_messages); - } - LlmMessageRole::Assistant => { - let mut text_content = String::new(); - let mut tool_calls: Vec = Vec::new(); - - for content in &msg.content { - match content { - LlmMessageContent::Text(text) => { - if !text.is_empty() { - if !text_content.is_empty() { - text_content.push('\n'); - } - text_content.push_str(text); - } - } - LlmMessageContent::ToolUse(tool_use) => { - tool_calls.push(OpenRouterToolCall { - id: tool_use.id.clone(), - call_type: "function".to_string(), - function: OpenRouterFunctionCall { - name: tool_use.name.clone(), - arguments: tool_use.input.clone(), - }, - }); - } - _ => {} - } - } - - messages.push(OpenRouterMessage { - role: "assistant".to_string(), - content: if text_content.is_empty() { - None - } else { - Some(OpenRouterContent::Text(text_content)) - }, - tool_calls: if tool_calls.is_empty() { - None - } else { - Some(tool_calls) - }, - tool_call_id: None, - }); - } - } - } - - let model_def = get_model_definition(model_id); - let supports_tools = model_def.map(|m| m.supports_tools).unwrap_or(true); - - let tools: Vec = if supports_tools { - request - .tools - .iter() - .map(|t| OpenRouterTool { - tool_type: "function".to_string(), - function: OpenRouterFunctionDef { - name: t.name.clone(), - description: t.description.clone(), - parameters: serde_json::from_str(&t.input_schema) - .unwrap_or(serde_json::Value::Object(Default::default())), - }, - }) - .collect() - } else { - Vec::new() - }; - - let tool_choice = if supports_tools { - request.tool_choice.as_ref().map(|tc| match tc { - LlmToolChoice::Auto => "auto".to_string(), - LlmToolChoice::Any => "required".to_string(), - LlmToolChoice::None => "none".to_string(), - }) - } else { - None - }; - - let max_tokens = request - .max_tokens - .or(model_def.and_then(|m| m.max_output_tokens)); - - Ok(OpenRouterRequest { - model: model_id.to_string(), - messages, - max_tokens, - tools, - tool_choice, - stop: request.stop_sequences.clone(), - temperature: request.temperature, - stream: true, - }) -} - -fn parse_sse_line(line: &str) -> Option { - let data = line.strip_prefix("data: ")?; - if data.trim() == "[DONE]" { - return None; - } - serde_json::from_str(data).ok() -} - -impl zed::Extension for OpenRouterProvider { - fn new() -> Self { - Self { - streams: Mutex::new(HashMap::new()), - next_stream_id: Mutex::new(0), - } - } - - fn llm_providers(&self) -> Vec { - vec![LlmProviderInfo { - id: "open_router".into(), - name: "OpenRouter".into(), - icon: Some("icons/open-router.svg".into()), - }] - } - - fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { - Ok(MODELS - .iter() - .map(|m| LlmModelInfo { - id: m.id.to_string(), - name: m.display_name.to_string(), - max_token_count: m.max_tokens, - max_output_tokens: m.max_output_tokens, - capabilities: LlmModelCapabilities { - supports_images: m.supports_images, - supports_tools: m.supports_tools, - supports_tool_choice_auto: m.supports_tools, - supports_tool_choice_any: m.supports_tools, - supports_tool_choice_none: m.supports_tools, - supports_thinking: false, - tool_input_format: LlmToolInputFormat::JsonSchema, - }, - is_default: m.is_default, - is_default_fast: m.is_default_fast, - }) - .collect()) - } - - fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { - llm_get_credential("open_router").is_some() - } - - fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { - Some( - r#"# OpenRouter Setup - -Welcome to **OpenRouter**! Access multiple AI models through a single API. - -## Configuration - -Enter your OpenRouter API key below. Get your API key at [openrouter.ai/keys](https://openrouter.ai/keys). - -## Available Models - -### Anthropic -| Model | Context | Output | -|-------|---------|--------| -| Claude Sonnet 4 | 200K | 8K | -| Claude Opus 4 | 200K | 8K | -| Claude Haiku 4 | 200K | 8K | -| Claude 3.5 Sonnet | 200K | 8K | - -### OpenAI -| Model | Context | Output | -|-------|---------|--------| -| GPT-4o | 128K | 16K | -| GPT-4o Mini | 128K | 16K | -| o1 | 200K | 100K | -| o3-mini | 200K | 100K | - -### Google -| Model | Context | Output | -|-------|---------|--------| -| Gemini 2.0 Flash | 1M | 8K | -| Gemini 2.5 Pro | 1M | 8K | - -### Meta -| Model | Context | Output | -|-------|---------|--------| -| Llama 3.3 70B | 128K | 4K | -| Llama 4 Maverick | 128K | 4K | - -### Mistral -| Model | Context | Output | -|-------|---------|--------| -| Mistral Large | 128K | 4K | -| Codestral | 32K | 4K | - -### DeepSeek -| Model | Context | Output | -|-------|---------|--------| -| DeepSeek V3 | 64K | 8K | -| DeepSeek R1 | 64K | 8K | - -### Qwen -| Model | Context | Output | -|-------|---------|--------| -| Qwen 3 235B | 40K | 8K | - -## Features - -- ✅ Full streaming support -- ✅ Tool/function calling (model dependent) -- ✅ Vision (model dependent) -- ✅ Access to 200+ models -- ✅ Unified billing - -## Pricing - -Pay-per-use based on model. See [openrouter.ai/models](https://openrouter.ai/models) for pricing. -"# - .to_string(), - ) - } - - fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { - let provided = llm_request_credential( - "open_router", - LlmCredentialType::ApiKey, - "OpenRouter API Key", - "sk-or-v1-...", - )?; - if provided { - Ok(()) - } else { - Err("Authentication cancelled".to_string()) - } - } - - fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { - llm_delete_credential("open_router") - } - - fn llm_stream_completion_start( - &mut self, - _provider_id: &str, - model_id: &str, - request: &LlmCompletionRequest, - ) -> Result { - let api_key = llm_get_credential("open_router").ok_or_else(|| { - "No API key configured. Please add your OpenRouter API key in settings.".to_string() - })?; - - let openrouter_request = convert_request(model_id, request)?; - - let body = serde_json::to_vec(&openrouter_request) - .map_err(|e| format!("Failed to serialize request: {}", e))?; - - let http_request = HttpRequest { - method: HttpMethod::Post, - url: "https://openrouter.ai/api/v1/chat/completions".to_string(), - headers: vec![ - ("Content-Type".to_string(), "application/json".to_string()), - ("Authorization".to_string(), format!("Bearer {}", api_key)), - ("HTTP-Referer".to_string(), "https://zed.dev".to_string()), - ("X-Title".to_string(), "Zed Editor".to_string()), - ], - body: Some(body), - redirect_policy: RedirectPolicy::FollowAll, - }; - - let response_stream = http_request - .fetch_stream() - .map_err(|e| format!("HTTP request failed: {}", e))?; - - let stream_id = { - let mut id_counter = self.next_stream_id.lock().unwrap(); - let id = format!("openrouter-stream-{}", *id_counter); - *id_counter += 1; - id - }; - - self.streams.lock().unwrap().insert( - stream_id.clone(), - StreamState { - response_stream: Some(response_stream), - buffer: String::new(), - started: false, - tool_calls: HashMap::new(), - tool_calls_emitted: false, - }, - ); - - Ok(stream_id) - } - - fn llm_stream_completion_next( - &mut self, - stream_id: &str, - ) -> Result, String> { - let mut streams = self.streams.lock().unwrap(); - let state = streams - .get_mut(stream_id) - .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; - - if !state.started { - state.started = true; - return Ok(Some(LlmCompletionEvent::Started)); - } - - let response_stream = state - .response_stream - .as_mut() - .ok_or_else(|| "Stream already closed".to_string())?; - - loop { - if let Some(newline_pos) = state.buffer.find('\n') { - let line = state.buffer[..newline_pos].to_string(); - state.buffer = state.buffer[newline_pos + 1..].to_string(); - - if line.trim().is_empty() { - continue; - } - - if let Some(response) = parse_sse_line(&line) { - if let Some(choice) = response.choices.first() { - if let Some(content) = &choice.delta.content { - if !content.is_empty() { - return Ok(Some(LlmCompletionEvent::Text(content.clone()))); - } - } - - if let Some(tool_calls) = &choice.delta.tool_calls { - for tc in tool_calls { - let entry = state - .tool_calls - .entry(tc.index) - .or_insert_with(AccumulatedToolCall::default); - - if let Some(id) = &tc.id { - entry.id = id.clone(); - } - if let Some(func) = &tc.function { - if let Some(name) = &func.name { - entry.name = name.clone(); - } - if let Some(args) = &func.arguments { - entry.arguments.push_str(args); - } - } - } - } - - if let Some(finish_reason) = &choice.finish_reason { - if !state.tool_calls.is_empty() && !state.tool_calls_emitted { - state.tool_calls_emitted = true; - let mut tool_calls: Vec<_> = state.tool_calls.drain().collect(); - tool_calls.sort_by_key(|(idx, _)| *idx); - - if let Some((_, tc)) = tool_calls.into_iter().next() { - return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { - id: tc.id, - name: tc.name, - input: tc.arguments, - thought_signature: None, - }))); - } - } - - let stop_reason = match finish_reason.as_str() { - "stop" => LlmStopReason::EndTurn, - "length" => LlmStopReason::MaxTokens, - "tool_calls" => LlmStopReason::ToolUse, - "content_filter" => LlmStopReason::Refusal, - _ => LlmStopReason::EndTurn, - }; - return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); - } - } - - if let Some(usage) = response.usage { - return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { - input_tokens: usage.prompt_tokens, - output_tokens: usage.completion_tokens, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }))); - } - } - - continue; - } - - match response_stream.next_chunk() { - Ok(Some(chunk)) => { - let text = String::from_utf8_lossy(&chunk); - state.buffer.push_str(&text); - } - Ok(None) => { - return Ok(None); - } - Err(e) => { - return Err(format!("Stream error: {}", e)); - } - } - } - } - - fn llm_stream_completion_close(&mut self, stream_id: &str) { - self.streams.lock().unwrap().remove(stream_id); - } -} - -zed::register_extension!(OpenRouterProvider); diff --git a/extensions/openai/Cargo.lock b/extensions/openai/Cargo.lock deleted file mode 100644 index 2ef354a2892b23..00000000000000 --- a/extensions/openai/Cargo.lock +++ /dev/null @@ -1,823 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "auditable-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" -dependencies = [ - "semver", - "serde", - "serde_json", - "topological-sort", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "fopenai" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "zed_extension_api", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "topological-sort" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "wasm-encoder" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" -dependencies = [ - "anyhow", - "auditable-serde", - "flate2", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "url", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" -dependencies = [ - "wit-bindgen-rt", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" -dependencies = [ - "bitflags", - "futures", - "once_cell", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zed_extension_api" -version = "0.7.0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/extensions/openai/Cargo.toml b/extensions/openai/Cargo.toml deleted file mode 100644 index f81809e426ef69..00000000000000 --- a/extensions/openai/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "openai" -version = "0.1.0" -edition = "2021" -publish = false -license = "Apache-2.0" - -[workspace] - -[lib] -path = "src/openai.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = { path = "../../crates/extension_api" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" diff --git a/extensions/openai/extension.toml b/extensions/openai/extension.toml deleted file mode 100644 index 94788688716f1d..00000000000000 --- a/extensions/openai/extension.toml +++ /dev/null @@ -1,13 +0,0 @@ -id = "openai" -name = "OpenAI" -description = "OpenAI GPT LLM provider for Zed." -version = "0.1.0" -schema_version = 1 -authors = ["Zed Team"] -repository = "https://github.com/zed-industries/zed" - -[language_model_providers.openai] -name = "OpenAI" - -[language_model_providers.openai.auth] -env_var = "OPENAI_API_KEY" \ No newline at end of file diff --git a/extensions/openai/icons/openai.svg b/extensions/openai/icons/openai.svg deleted file mode 100644 index e45ac315a01185..00000000000000 --- a/extensions/openai/icons/openai.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/extensions/openai/src/openai.rs b/extensions/openai/src/openai.rs deleted file mode 100644 index 40a99352abd5da..00000000000000 --- a/extensions/openai/src/openai.rs +++ /dev/null @@ -1,727 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; - -use serde::{Deserialize, Serialize}; -use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; -use zed_extension_api::{self as zed, *}; - -struct OpenAiProvider { - streams: Mutex>, - next_stream_id: Mutex, -} - -struct StreamState { - response_stream: Option, - buffer: String, - started: bool, - tool_calls: HashMap, - tool_calls_emitted: bool, -} - -#[derive(Clone, Default)] -struct AccumulatedToolCall { - id: String, - name: String, - arguments: String, -} - -struct ModelDefinition { - real_id: &'static str, - display_name: &'static str, - max_tokens: u64, - max_output_tokens: Option, - supports_images: bool, - is_default: bool, - is_default_fast: bool, -} - -const MODELS: &[ModelDefinition] = &[ - ModelDefinition { - real_id: "gpt-4o", - display_name: "GPT-4o", - max_tokens: 128_000, - max_output_tokens: Some(16_384), - supports_images: true, - is_default: true, - is_default_fast: false, - }, - ModelDefinition { - real_id: "gpt-4o-mini", - display_name: "GPT-4o-mini", - max_tokens: 128_000, - max_output_tokens: Some(16_384), - supports_images: true, - is_default: false, - is_default_fast: true, - }, - ModelDefinition { - real_id: "gpt-4.1", - display_name: "GPT-4.1", - max_tokens: 1_047_576, - max_output_tokens: Some(32_768), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "gpt-4.1-mini", - display_name: "GPT-4.1-mini", - max_tokens: 1_047_576, - max_output_tokens: Some(32_768), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "gpt-4.1-nano", - display_name: "GPT-4.1-nano", - max_tokens: 1_047_576, - max_output_tokens: Some(32_768), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "gpt-5", - display_name: "GPT-5", - max_tokens: 272_000, - max_output_tokens: Some(32_768), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "gpt-5-mini", - display_name: "GPT-5-mini", - max_tokens: 272_000, - max_output_tokens: Some(32_768), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "o1", - display_name: "o1", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "o3", - display_name: "o3", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "o3-mini", - display_name: "o3-mini", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: false, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - real_id: "o4-mini", - display_name: "o4-mini", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: true, - is_default: false, - is_default_fast: false, - }, -]; - -fn get_real_model_id(display_name: &str) -> Option<&'static str> { - MODELS - .iter() - .find(|m| m.display_name == display_name) - .map(|m| m.real_id) -} - -#[derive(Serialize)] -struct OpenAiRequest { - model: String, - messages: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - tools: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - tool_choice: Option, - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - max_tokens: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - stop: Vec, - stream: bool, - stream_options: Option, -} - -#[derive(Serialize)] -struct StreamOptions { - include_usage: bool, -} - -#[derive(Serialize)] -#[serde(tag = "role")] -enum OpenAiMessage { - #[serde(rename = "system")] - System { content: String }, - #[serde(rename = "user")] - User { content: Vec }, - #[serde(rename = "assistant")] - Assistant { - #[serde(skip_serializing_if = "Option::is_none")] - content: Option, - #[serde(skip_serializing_if = "Option::is_none")] - tool_calls: Option>, - }, - #[serde(rename = "tool")] - Tool { - tool_call_id: String, - content: String, - }, -} - -#[derive(Serialize)] -#[serde(tag = "type")] -enum OpenAiContentPart { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "image_url")] - ImageUrl { image_url: ImageUrl }, -} - -#[derive(Serialize)] -struct ImageUrl { - url: String, -} - -#[derive(Serialize, Deserialize, Clone)] -struct OpenAiToolCall { - id: String, - #[serde(rename = "type")] - call_type: String, - function: OpenAiFunctionCall, -} - -#[derive(Serialize, Deserialize, Clone)] -struct OpenAiFunctionCall { - name: String, - arguments: String, -} - -#[derive(Serialize)] -struct OpenAiTool { - #[serde(rename = "type")] - tool_type: String, - function: OpenAiFunctionDef, -} - -#[derive(Serialize)] -struct OpenAiFunctionDef { - name: String, - description: String, - parameters: serde_json::Value, -} - -#[derive(Deserialize, Debug)] -struct OpenAiStreamEvent { - choices: Vec, - #[serde(default)] - usage: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenAiChoice { - delta: OpenAiDelta, - finish_reason: Option, -} - -#[derive(Deserialize, Debug, Default)] -struct OpenAiDelta { - #[serde(default)] - content: Option, - #[serde(default)] - tool_calls: Option>, -} - -#[derive(Deserialize, Debug)] -struct OpenAiToolCallDelta { - index: usize, - #[serde(default)] - id: Option, - #[serde(default)] - function: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenAiFunctionDelta { - #[serde(default)] - name: Option, - #[serde(default)] - arguments: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenAiUsage { - prompt_tokens: u64, - completion_tokens: u64, -} - -#[allow(dead_code)] -#[derive(Deserialize, Debug)] -struct OpenAiError { - error: OpenAiErrorDetail, -} - -#[allow(dead_code)] -#[derive(Deserialize, Debug)] -struct OpenAiErrorDetail { - message: String, -} - -fn convert_request( - model_id: &str, - request: &LlmCompletionRequest, -) -> Result { - let real_model_id = - get_real_model_id(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; - - let mut messages = Vec::new(); - - for msg in &request.messages { - match msg.role { - LlmMessageRole::System => { - let text: String = msg - .content - .iter() - .filter_map(|c| match c { - LlmMessageContent::Text(t) => Some(t.as_str()), - _ => None, - }) - .collect::>() - .join("\n"); - if !text.is_empty() { - messages.push(OpenAiMessage::System { content: text }); - } - } - LlmMessageRole::User => { - let parts: Vec = msg - .content - .iter() - .filter_map(|c| match c { - LlmMessageContent::Text(t) => { - Some(OpenAiContentPart::Text { text: t.clone() }) - } - LlmMessageContent::Image(img) => Some(OpenAiContentPart::ImageUrl { - image_url: ImageUrl { - url: format!("data:image/png;base64,{}", img.source), - }, - }), - LlmMessageContent::ToolResult(_) => None, - _ => None, - }) - .collect(); - - for content in &msg.content { - if let LlmMessageContent::ToolResult(result) = content { - let content_text = match &result.content { - LlmToolResultContent::Text(t) => t.clone(), - LlmToolResultContent::Image(_) => "[Image]".to_string(), - }; - messages.push(OpenAiMessage::Tool { - tool_call_id: result.tool_use_id.clone(), - content: content_text, - }); - } - } - - if !parts.is_empty() { - messages.push(OpenAiMessage::User { content: parts }); - } - } - LlmMessageRole::Assistant => { - let mut content_text: Option = None; - let mut tool_calls: Vec = Vec::new(); - - for c in &msg.content { - match c { - LlmMessageContent::Text(t) => { - content_text = Some(t.clone()); - } - LlmMessageContent::ToolUse(tool_use) => { - tool_calls.push(OpenAiToolCall { - id: tool_use.id.clone(), - call_type: "function".to_string(), - function: OpenAiFunctionCall { - name: tool_use.name.clone(), - arguments: tool_use.input.clone(), - }, - }); - } - _ => {} - } - } - - messages.push(OpenAiMessage::Assistant { - content: content_text, - tool_calls: if tool_calls.is_empty() { - None - } else { - Some(tool_calls) - }, - }); - } - } - } - - let tools: Option> = if request.tools.is_empty() { - None - } else { - Some( - request - .tools - .iter() - .map(|t| OpenAiTool { - tool_type: "function".to_string(), - function: OpenAiFunctionDef { - name: t.name.clone(), - description: t.description.clone(), - parameters: serde_json::from_str(&t.input_schema) - .unwrap_or(serde_json::Value::Object(Default::default())), - }, - }) - .collect(), - ) - }; - - let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { - LlmToolChoice::Auto => "auto".to_string(), - LlmToolChoice::Any => "required".to_string(), - LlmToolChoice::None => "none".to_string(), - }); - - Ok(OpenAiRequest { - model: real_model_id.to_string(), - messages, - tools, - tool_choice, - temperature: request.temperature, - max_tokens: request.max_tokens, - stop: request.stop_sequences.clone(), - stream: true, - stream_options: Some(StreamOptions { - include_usage: true, - }), - }) -} - -fn parse_sse_line(line: &str) -> Option { - if let Some(data) = line.strip_prefix("data: ") { - if data == "[DONE]" { - return None; - } - serde_json::from_str(data).ok() - } else { - None - } -} - -impl zed::Extension for OpenAiProvider { - fn new() -> Self { - Self { - streams: Mutex::new(HashMap::new()), - next_stream_id: Mutex::new(0), - } - } - - fn llm_providers(&self) -> Vec { - vec![LlmProviderInfo { - id: "openai".into(), - name: "OpenAI".into(), - icon: Some("icons/openai.svg".into()), - }] - } - - fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { - Ok(MODELS - .iter() - .map(|m| LlmModelInfo { - id: m.display_name.to_string(), - name: m.display_name.to_string(), - max_token_count: m.max_tokens, - max_output_tokens: m.max_output_tokens, - capabilities: LlmModelCapabilities { - supports_images: m.supports_images, - supports_tools: true, - supports_tool_choice_auto: true, - supports_tool_choice_any: true, - supports_tool_choice_none: true, - supports_thinking: false, - tool_input_format: LlmToolInputFormat::JsonSchema, - }, - is_default: m.is_default, - is_default_fast: m.is_default_fast, - }) - .collect()) - } - - fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { - llm_get_credential("openai").is_some() - } - - fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { - Some( - r#"# OpenAI Setup - -Welcome to **OpenAI**! This extension provides access to OpenAI GPT models. - -## Configuration - -Enter your OpenAI API key below. You can find your API key at [platform.openai.com/api-keys](https://platform.openai.com/api-keys). - -## Available Models - -| Display Name | Real Model | Context | Output | -|--------------|------------|---------|--------| -| GPT-4o | gpt-4o | 128K | 16K | -| GPT-4o-mini | gpt-4o-mini | 128K | 16K | -| GPT-4.1 | gpt-4.1 | 1M | 32K | -| GPT-4.1-mini | gpt-4.1-mini | 1M | 32K | -| GPT-5 | gpt-5 | 272K | 32K | -| GPT-5-mini | gpt-5-mini | 272K | 32K | -| o1 | o1 | 200K | 100K | -| o3 | o3 | 200K | 100K | -| o3-mini | o3-mini | 200K | 100K | - -## Features - -- ✅ Full streaming support -- ✅ Tool/function calling -- ✅ Vision (image inputs) -- ✅ All OpenAI models - -## Pricing - -Uses your OpenAI API credits. See [OpenAI pricing](https://openai.com/pricing) for details. -"# - .to_string(), - ) - } - - fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { - let provided = llm_request_credential( - "openai", - LlmCredentialType::ApiKey, - "OpenAI API Key", - "sk-...", - )?; - if provided { - Ok(()) - } else { - Err("Authentication cancelled".to_string()) - } - } - - fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { - llm_delete_credential("openai") - } - - fn llm_stream_completion_start( - &mut self, - _provider_id: &str, - model_id: &str, - request: &LlmCompletionRequest, - ) -> Result { - let api_key = llm_get_credential("openai").ok_or_else(|| { - "No API key configured. Please add your OpenAI API key in settings.".to_string() - })?; - - let openai_request = convert_request(model_id, request)?; - - let body = serde_json::to_vec(&openai_request) - .map_err(|e| format!("Failed to serialize request: {}", e))?; - - let http_request = HttpRequest { - method: HttpMethod::Post, - url: "https://api.openai.com/v1/chat/completions".to_string(), - headers: vec![ - ("Content-Type".to_string(), "application/json".to_string()), - ("Authorization".to_string(), format!("Bearer {}", api_key)), - ], - body: Some(body), - redirect_policy: RedirectPolicy::FollowAll, - }; - - let response_stream = http_request - .fetch_stream() - .map_err(|e| format!("HTTP request failed: {}", e))?; - - let stream_id = { - let mut id_counter = self.next_stream_id.lock().unwrap(); - let id = format!("openai-stream-{}", *id_counter); - *id_counter += 1; - id - }; - - self.streams.lock().unwrap().insert( - stream_id.clone(), - StreamState { - response_stream: Some(response_stream), - buffer: String::new(), - started: false, - tool_calls: HashMap::new(), - tool_calls_emitted: false, - }, - ); - - Ok(stream_id) - } - - fn llm_stream_completion_next( - &mut self, - stream_id: &str, - ) -> Result, String> { - let mut streams = self.streams.lock().unwrap(); - let state = streams - .get_mut(stream_id) - .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; - - if !state.started { - state.started = true; - return Ok(Some(LlmCompletionEvent::Started)); - } - - let response_stream = state - .response_stream - .as_mut() - .ok_or_else(|| "Stream already closed".to_string())?; - - loop { - if let Some(newline_pos) = state.buffer.find('\n') { - let line = state.buffer[..newline_pos].trim().to_string(); - state.buffer = state.buffer[newline_pos + 1..].to_string(); - - if line.is_empty() { - continue; - } - - if let Some(event) = parse_sse_line(&line) { - if let Some(choice) = event.choices.first() { - if let Some(tool_calls) = &choice.delta.tool_calls { - for tc in tool_calls { - let entry = state.tool_calls.entry(tc.index).or_default(); - - if let Some(id) = &tc.id { - entry.id = id.clone(); - } - - if let Some(func) = &tc.function { - if let Some(name) = &func.name { - entry.name = name.clone(); - } - if let Some(args) = &func.arguments { - entry.arguments.push_str(args); - } - } - } - } - - if let Some(reason) = &choice.finish_reason { - if reason == "tool_calls" && !state.tool_calls_emitted { - state.tool_calls_emitted = true; - if let Some((&index, _)) = state.tool_calls.iter().next() { - if let Some(tool_call) = state.tool_calls.remove(&index) { - return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { - id: tool_call.id, - name: tool_call.name, - input: tool_call.arguments, - thought_signature: None, - }))); - } - } - } - - let stop_reason = match reason.as_str() { - "stop" => LlmStopReason::EndTurn, - "length" => LlmStopReason::MaxTokens, - "tool_calls" => LlmStopReason::ToolUse, - "content_filter" => LlmStopReason::Refusal, - _ => LlmStopReason::EndTurn, - }; - - if let Some(usage) = event.usage { - return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { - input_tokens: usage.prompt_tokens, - output_tokens: usage.completion_tokens, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }))); - } - - return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); - } - - if let Some(content) = &choice.delta.content { - if !content.is_empty() { - return Ok(Some(LlmCompletionEvent::Text(content.clone()))); - } - } - } - - if event.choices.is_empty() { - if let Some(usage) = event.usage { - return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { - input_tokens: usage.prompt_tokens, - output_tokens: usage.completion_tokens, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }))); - } - } - } - - continue; - } - - match response_stream.next_chunk() { - Ok(Some(chunk)) => { - let text = String::from_utf8_lossy(&chunk); - state.buffer.push_str(&text); - } - Ok(None) => { - if !state.tool_calls.is_empty() && !state.tool_calls_emitted { - state.tool_calls_emitted = true; - let keys: Vec = state.tool_calls.keys().copied().collect(); - if let Some(&key) = keys.first() { - if let Some(tool_call) = state.tool_calls.remove(&key) { - return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { - id: tool_call.id, - name: tool_call.name, - input: tool_call.arguments, - thought_signature: None, - }))); - } - } - } - return Ok(None); - } - Err(e) => { - return Err(format!("Stream error: {}", e)); - } - } - } - } - - fn llm_stream_completion_close(&mut self, stream_id: &str) { - self.streams.lock().unwrap().remove(stream_id); - } -} - -zed::register_extension!(OpenAiProvider); From 8b1ce75a57fced515c861c196dd0bcf3038ba770 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 10:30:02 -0500 Subject: [PATCH 23/45] Move wit extensions into their own module --- crates/extension_api/src/extension_api.rs | 8 +- .../extension_api/wit/since_v0.7.0/common.wit | 12 - .../wit/since_v0.7.0/context-server.wit | 11 - crates/extension_api/wit/since_v0.7.0/dap.wit | 123 - .../wit/since_v0.7.0/extension.wit | 284 --- .../extension_api/wit/since_v0.7.0/github.wit | 35 - .../wit/since_v0.7.0/http-client.wit | 67 - .../wit/since_v0.7.0/llm-provider.wit | 302 --- crates/extension_api/wit/since_v0.7.0/lsp.wit | 90 - .../extension_api/wit/since_v0.7.0/nodejs.wit | 13 - .../wit/since_v0.7.0/platform.wit | 24 - .../wit/since_v0.7.0/process.wit | 29 - .../wit/since_v0.7.0/settings.rs | 40 - .../wit/since_v0.7.0/slash-command.wit | 41 - .../wit/since_v0.8.0/extension.wit | 56 - .../wit/since_v0.8.0/llm-provider.wit | 51 + crates/extension_host/src/wasm_host/wit.rs | 209 +- .../src/wasm_host/wit/since_v0_7_0.rs | 2022 ----------------- .../src/wasm_host/wit/since_v0_8_0.rs | 38 +- 19 files changed, 78 insertions(+), 3377 deletions(-) delete mode 100644 crates/extension_api/wit/since_v0.7.0/common.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/context-server.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/dap.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/extension.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/github.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/http-client.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/llm-provider.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/lsp.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/nodejs.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/platform.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/process.wit delete mode 100644 crates/extension_api/wit/since_v0.7.0/settings.rs delete mode 100644 crates/extension_api/wit/since_v0.7.0/slash-command.wit delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 1bb2a84059fb30..7764b13f19672f 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -17,8 +17,6 @@ pub use serde_json; pub use wit::{ CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars, KeyValueStore, LanguageServerInstallationStatus, Project, Range, Worktree, download_file, - llm_delete_credential, llm_get_credential, llm_get_env_var, llm_oauth_http_request, - llm_oauth_open_browser, llm_oauth_start_web_auth, llm_request_credential, llm_store_credential, make_file_executable, zed::extension::context_server::ContextServerConfiguration, zed::extension::dap::{ @@ -45,6 +43,12 @@ pub use wit::{ ToolInputFormat as LlmToolInputFormat, ToolResult as LlmToolResult, ToolResultContent as LlmToolResultContent, ToolUse as LlmToolUse, ToolUseJsonParseError as LlmToolUseJsonParseError, + delete_credential as llm_delete_credential, get_credential as llm_get_credential, + get_env_var as llm_get_env_var, oauth_open_browser as llm_oauth_open_browser, + oauth_start_web_auth as llm_oauth_start_web_auth, + request_credential as llm_request_credential, + send_oauth_http_request as llm_oauth_http_request, + store_credential as llm_store_credential, }, zed::extension::nodejs::{ node_binary_path, npm_install_package, npm_package_installed_version, diff --git a/crates/extension_api/wit/since_v0.7.0/common.wit b/crates/extension_api/wit/since_v0.7.0/common.wit deleted file mode 100644 index 139e7ba0ca4d1c..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/common.wit +++ /dev/null @@ -1,12 +0,0 @@ -interface common { - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } - - /// A list of environment variables. - type env-vars = list>; -} diff --git a/crates/extension_api/wit/since_v0.7.0/context-server.wit b/crates/extension_api/wit/since_v0.7.0/context-server.wit deleted file mode 100644 index 7234e0e6d0f6d4..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/context-server.wit +++ /dev/null @@ -1,11 +0,0 @@ -interface context-server { - /// Configuration for context server setup and installation. - record context-server-configuration { - /// Installation instructions in Markdown format. - installation-instructions: string, - /// JSON schema for settings validation. - settings-schema: string, - /// Default settings template. - default-settings: string, - } -} diff --git a/crates/extension_api/wit/since_v0.7.0/dap.wit b/crates/extension_api/wit/since_v0.7.0/dap.wit deleted file mode 100644 index 693befe02f9c31..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/dap.wit +++ /dev/null @@ -1,123 +0,0 @@ -interface dap { - use common.{env-vars}; - - /// Resolves a specified TcpArgumentsTemplate into TcpArguments - resolve-tcp-template: func(template: tcp-arguments-template) -> result; - - record launch-request { - program: string, - cwd: option, - args: list, - envs: env-vars, - } - - record attach-request { - process-id: option, - } - - variant debug-request { - launch(launch-request), - attach(attach-request) - } - - record tcp-arguments { - port: u16, - host: u32, - timeout: option, - } - - record tcp-arguments-template { - port: option, - host: option, - timeout: option, - } - - /// Debug Config is the "highest-level" configuration for a debug session. - /// It comes from a new process modal UI; thus, it is essentially debug-adapter-agnostic. - /// It is expected of the extension to translate this generic configuration into something that can be debugged by the adapter (debug scenario). - record debug-config { - /// Name of the debug task - label: string, - /// The debug adapter to use - adapter: string, - request: debug-request, - stop-on-entry: option, - } - - record task-template { - /// Human readable name of the task to display in the UI. - label: string, - /// Executable command to spawn. - command: string, - args: list, - env: env-vars, - cwd: option, - } - - /// A task template with substituted task variables. - type resolved-task = task-template; - - /// A task template for building a debug target. - type build-task-template = task-template; - - variant build-task-definition { - by-name(string), - template(build-task-definition-template-payload ) - } - record build-task-definition-template-payload { - locator-name: option, - template: build-task-template - } - - /// Debug Scenario is the user-facing configuration type (used in debug.json). It is still concerned with what to debug and not necessarily how to do it (except for any - /// debug-adapter-specific configuration options). - record debug-scenario { - /// Unsubstituted label for the task.DebugAdapterBinary - label: string, - /// Name of the Debug Adapter this configuration is intended for. - adapter: string, - /// An optional build step to be ran prior to starting a debug session. Build steps are used by Zed's locators to locate the executable to debug. - build: option, - /// JSON-encoded configuration for a given debug adapter. - config: string, - /// TCP connection parameters (if they were specified by user) - tcp-connection: option, - } - - enum start-debugging-request-arguments-request { - launch, - attach, - } - - record debug-task-definition { - /// Unsubstituted label for the task.DebugAdapterBinary - label: string, - /// Name of the Debug Adapter this configuration is intended for. - adapter: string, - /// JSON-encoded configuration for a given debug adapter. - config: string, - /// TCP connection parameters (if they were specified by user) - tcp-connection: option, - } - - record start-debugging-request-arguments { - /// JSON-encoded configuration for a given debug adapter. It is specific to each debug adapter. - /// `configuration` will have it's Zed variable references substituted prior to being passed to the debug adapter. - configuration: string, - request: start-debugging-request-arguments-request, - } - - /// The lowest-level representation of a debug session, which specifies: - /// - How to start a debug adapter process - /// - How to start a debug session with it (using DAP protocol) - /// for a given debug scenario. - record debug-adapter-binary { - command: option, - arguments: list, - envs: env-vars, - cwd: option, - /// Zed will use TCP transport if `connection` is specified. - connection: option, - request-args: start-debugging-request-arguments - } -} diff --git a/crates/extension_api/wit/since_v0.7.0/extension.wit b/crates/extension_api/wit/since_v0.7.0/extension.wit deleted file mode 100644 index f95dfa04dac25f..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/extension.wit +++ /dev/null @@ -1,284 +0,0 @@ -package zed:extension; - -world extension { - import context-server; - import dap; - import github; - import http-client; - import platform; - import process; - import nodejs; - import llm-provider; - - use common.{env-vars, range}; - use context-server.{context-server-configuration}; - use dap.{attach-request, build-task-template, debug-config, debug-adapter-binary, debug-task-definition, debug-request, debug-scenario, launch-request, resolved-task, start-debugging-request-arguments-request}; - use lsp.{completion, symbol}; - use process.{command}; - use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; - use llm-provider.{ - provider-info, model-info, completion-request, - credential-type, cache-configuration, completion-event, token-usage - }; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// A Zed project. - resource project { - /// Returns the IDs of all of the worktrees in this project. - worktree-ids: func() -> list; - } - - /// A key-value store. - resource key-value-store { - /// Inserts an entry under the specified key. - insert: func(key: string, value: string) -> result<_, string>; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the initialization options to pass to the other language server. - export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the other language server. - export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; - - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; - - /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; - - /// Returns the command used to start up a context server. - export context-server-command: func(context-server-id: string, project: borrow) -> result; - - /// Returns the configuration for a context server. - export context-server-configuration: func(context-server-id: string, project: borrow) -> result, string>; - - /// Returns a list of packages as suggestions to be included in the `/docs` - /// search results. - /// - /// This can be used to provide completions for known packages (e.g., from the - /// local project or a registry) before a package has been indexed. - export suggest-docs-packages: func(provider-name: string) -> result, string>; - - /// Indexes the docs for the specified package. - export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; - - /// Returns a configured debug adapter binary for a given debug task. - export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option, worktree: borrow) -> result; - /// Returns the kind of a debug scenario (launch or attach). - export dap-request-kind: func(adapter-name: string, config: string) -> result; - export dap-config-to-scenario: func(config: debug-config) -> result; - export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option; - export run-dap-locator: func(locator-name: string, config: resolved-task) -> result; - - // ========================================================================= - // Language Model Provider Extension API - // ========================================================================= - - /// Returns information about language model providers offered by this extension. - export llm-providers: func() -> list; - - /// Returns the models available for a provider. - export llm-provider-models: func(provider-id: string) -> result, string>; - - /// Returns markdown content to display in the provider's settings UI. - /// This can include setup instructions, links to documentation, etc. - export llm-provider-settings-markdown: func(provider-id: string) -> option; - - /// Check if the provider is authenticated. - export llm-provider-is-authenticated: func(provider-id: string) -> bool; - - /// Attempt to authenticate the provider. - export llm-provider-authenticate: func(provider-id: string) -> result<_, string>; - - /// Reset credentials for the provider. - export llm-provider-reset-credentials: func(provider-id: string) -> result<_, string>; - - /// Count tokens for a request. - export llm-count-tokens: func( - provider-id: string, - model-id: string, - request: completion-request - ) -> result; - - /// Start streaming a completion from the model. - /// Returns a stream ID that can be used with llm-stream-next and llm-stream-close. - export llm-stream-completion-start: func( - provider-id: string, - model-id: string, - request: completion-request - ) -> result; - - /// Get the next event from a completion stream. - /// Returns None when the stream is complete. - export llm-stream-completion-next: func( - stream-id: string - ) -> result, string>; - - /// Close a completion stream and release its resources. - export llm-stream-completion-close: func( - stream-id: string - ); - - /// Get cache configuration for a model (if prompt caching is supported). - export llm-cache-configuration: func( - provider-id: string, - model-id: string - ) -> option; - - // ========================================================================= - // Language Model Provider Imports (callable by extensions) - // ========================================================================= - - /// Request a credential from the user. - /// Returns true if the credential was provided, false if the user cancelled. - import llm-request-credential: func( - provider-id: string, - credential-type: credential-type, - label: string, - placeholder: string - ) -> result; - - /// Get a stored credential for this provider. - import llm-get-credential: func(provider-id: string) -> option; - - /// Store a credential for this provider. - import llm-store-credential: func(provider-id: string, value: string) -> result<_, string>; - - /// Delete a stored credential for this provider. - import llm-delete-credential: func(provider-id: string) -> result<_, string>; - - /// Read an environment variable. - import llm-get-env-var: func(name: string) -> option; - - // ========================================================================= - // OAuth Web Auth Flow Imports - // ========================================================================= - - use llm-provider.{oauth-web-auth-config, oauth-web-auth-result, oauth-http-request, oauth-http-response}; - - /// Start an OAuth web authentication flow. - /// - /// This will: - /// 1. Start a localhost server to receive the OAuth callback - /// 2. Open the auth URL in the user's default browser - /// 3. Wait for the callback (up to the timeout) - /// 4. Return the callback URL with query parameters - /// - /// The extension is responsible for: - /// - Constructing the auth URL with client_id, redirect_uri, scope, state, etc. - /// - Parsing the callback URL to extract the authorization code - /// - Exchanging the code for tokens using llm-oauth-http-request - import llm-oauth-start-web-auth: func(config: oauth-web-auth-config) -> result; - - /// Make an HTTP request for OAuth token exchange. - /// - /// This is a simple HTTP client for OAuth flows, allowing the extension - /// to handle token exchange with full control over serialization. - import llm-oauth-http-request: func(request: oauth-http-request) -> result; - - /// Open a URL in the user's default browser. - /// - /// Useful for OAuth flows that need to open a browser but handle the - /// callback differently (e.g., polling-based flows). - import llm-oauth-open-browser: func(url: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.7.0/github.wit b/crates/extension_api/wit/since_v0.7.0/github.wit deleted file mode 100644 index 21cd5d48056af0..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/github.wit +++ /dev/null @@ -1,35 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - /// - /// Takes repo as a string in the form "/", for example: "zed-industries/zed". - latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Returns the GitHub release with the specified tag name for the given GitHub repository. - /// - /// Returns an error if a release with the given tag name does not exist. - github-release-by-tag-name: func(repo: string, tag: string) -> result; -} diff --git a/crates/extension_api/wit/since_v0.7.0/http-client.wit b/crates/extension_api/wit/since_v0.7.0/http-client.wit deleted file mode 100644 index bb0206c17a52d4..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/http-client.wit +++ /dev/null @@ -1,67 +0,0 @@ -interface http-client { - /// An HTTP request. - record http-request { - /// The HTTP method for the request. - method: http-method, - /// The URL to which the request should be made. - url: string, - /// The headers for the request. - headers: list>, - /// The request body. - body: option>, - /// The policy to use for redirects. - redirect-policy: redirect-policy, - } - - /// HTTP methods. - enum http-method { - /// `GET` - get, - /// `HEAD` - head, - /// `POST` - post, - /// `PUT` - put, - /// `DELETE` - delete, - /// `OPTIONS` - options, - /// `PATCH` - patch, - } - - /// The policy for dealing with redirects received from the server. - variant redirect-policy { - /// Redirects from the server will not be followed. - /// - /// This is the default behavior. - no-follow, - /// Redirects from the server will be followed up to the specified limit. - follow-limit(u32), - /// All redirects from the server will be followed. - follow-all, - } - - /// An HTTP response. - record http-response { - /// The response headers. - headers: list>, - /// The response body. - body: list, - } - - /// Performs an HTTP request and returns the response. - fetch: func(req: http-request) -> result; - - /// An HTTP response stream. - resource http-response-stream { - /// Retrieves the next chunk of data from the response stream. - /// - /// Returns `Ok(None)` if the stream has ended. - next-chunk: func() -> result>, string>; - } - - /// Performs an HTTP request and returns a response stream. - fetch-stream: func(req: http-request) -> result; -} diff --git a/crates/extension_api/wit/since_v0.7.0/llm-provider.wit b/crates/extension_api/wit/since_v0.7.0/llm-provider.wit deleted file mode 100644 index aec6569c2efda7..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/llm-provider.wit +++ /dev/null @@ -1,302 +0,0 @@ -interface llm-provider { - /// Information about a language model provider. - record provider-info { - /// Unique identifier for the provider (e.g., "my-extension.my-provider"). - id: string, - /// Display name for the provider. - name: string, - /// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg"). - icon: option, - } - - /// Capabilities of a language model. - record model-capabilities { - /// Whether the model supports image inputs. - supports-images: bool, - /// Whether the model supports tool/function calling. - supports-tools: bool, - /// Whether the model supports the "auto" tool choice. - supports-tool-choice-auto: bool, - /// Whether the model supports the "any" tool choice. - supports-tool-choice-any: bool, - /// Whether the model supports the "none" tool choice. - supports-tool-choice-none: bool, - /// Whether the model supports extended thinking/reasoning. - supports-thinking: bool, - /// The format for tool input schemas. - tool-input-format: tool-input-format, - } - - /// Format for tool input schemas. - enum tool-input-format { - /// Standard JSON Schema format. - json-schema, - /// Simplified schema format for certain providers. - simplified, - } - - /// Information about a specific model. - record model-info { - /// Unique identifier for the model. - id: string, - /// Display name for the model. - name: string, - /// Maximum input token count. - max-token-count: u64, - /// Maximum output tokens (optional). - max-output-tokens: option, - /// Model capabilities. - capabilities: model-capabilities, - /// Whether this is the default model for the provider. - is-default: bool, - /// Whether this is the default fast model. - is-default-fast: bool, - } - - /// The role of a message participant. - enum message-role { - /// User message. - user, - /// Assistant message. - assistant, - /// System message. - system, - } - - /// A message in a completion request. - record request-message { - /// The role of the message sender. - role: message-role, - /// The content of the message. - content: list, - /// Whether to cache this message for prompt caching. - cache: bool, - } - - /// Content within a message. - variant message-content { - /// Plain text content. - text(string), - /// Image content. - image(image-data), - /// A tool use request from the assistant. - tool-use(tool-use), - /// A tool result from the user. - tool-result(tool-result), - /// Thinking/reasoning content. - thinking(thinking-content), - /// Redacted/encrypted thinking content. - redacted-thinking(string), - } - - /// Image data for vision models. - record image-data { - /// Base64-encoded image data. - source: string, - /// Image width in pixels (optional). - width: option, - /// Image height in pixels (optional). - height: option, - } - - /// A tool use request from the model. - record tool-use { - /// Unique identifier for this tool use. - id: string, - /// The name of the tool being used. - name: string, - /// JSON string of the tool input arguments. - input: string, - /// Thought signature for providers that support it (e.g., Anthropic). - thought-signature: option, - } - - /// A tool result to send back to the model. - record tool-result { - /// The ID of the tool use this is a result for. - tool-use-id: string, - /// The name of the tool. - tool-name: string, - /// Whether this result represents an error. - is-error: bool, - /// The content of the result. - content: tool-result-content, - } - - /// Content of a tool result. - variant tool-result-content { - /// Text result. - text(string), - /// Image result. - image(image-data), - } - - /// Thinking/reasoning content from models that support extended thinking. - record thinking-content { - /// The thinking text. - text: string, - /// Signature for the thinking block (provider-specific). - signature: option, - } - - /// A tool definition for function calling. - record tool-definition { - /// The name of the tool. - name: string, - /// Description of what the tool does. - description: string, - /// JSON Schema for input parameters. - input-schema: string, - } - - /// Tool choice preference for the model. - enum tool-choice { - /// Let the model decide whether to use tools. - auto, - /// Force the model to use at least one tool. - any, - /// Prevent the model from using tools. - none, - } - - /// A completion request to send to the model. - record completion-request { - /// The messages in the conversation. - messages: list, - /// Available tools for the model to use. - tools: list, - /// Tool choice preference. - tool-choice: option, - /// Stop sequences to end generation. - stop-sequences: list, - /// Temperature for sampling (0.0-1.0). - temperature: option, - /// Whether thinking/reasoning is allowed. - thinking-allowed: bool, - /// Maximum tokens to generate. - max-tokens: option, - } - - /// Events emitted during completion streaming. - variant completion-event { - /// Completion has started. - started, - /// Text content chunk. - text(string), - /// Thinking/reasoning content chunk. - thinking(thinking-content), - /// Redacted thinking (encrypted) chunk. - redacted-thinking(string), - /// Tool use request from the model. - tool-use(tool-use), - /// JSON parse error when parsing tool input. - tool-use-json-parse-error(tool-use-json-parse-error), - /// Completion stopped. - stop(stop-reason), - /// Token usage update. - usage(token-usage), - /// Reasoning details (provider-specific JSON). - reasoning-details(string), - } - - /// Error information when tool use JSON parsing fails. - record tool-use-json-parse-error { - /// The tool use ID. - id: string, - /// The tool name. - tool-name: string, - /// The raw input that failed to parse. - raw-input: string, - /// The parse error message. - error: string, - } - - /// Reason the completion stopped. - enum stop-reason { - /// The model finished generating. - end-turn, - /// Maximum tokens reached. - max-tokens, - /// The model wants to use a tool. - tool-use, - /// The model refused to respond. - refusal, - } - - /// Token usage statistics. - record token-usage { - /// Number of input tokens used. - input-tokens: u64, - /// Number of output tokens generated. - output-tokens: u64, - /// Tokens used for cache creation (if supported). - cache-creation-input-tokens: option, - /// Tokens read from cache (if supported). - cache-read-input-tokens: option, - } - - /// Credential types that can be requested. - enum credential-type { - /// An API key. - api-key, - /// An OAuth token. - oauth-token, - } - - /// Cache configuration for prompt caching. - record cache-configuration { - /// Maximum number of cache anchors. - max-cache-anchors: u32, - /// Whether caching should be applied to tool definitions. - should-cache-tool-definitions: bool, - /// Minimum token count for a message to be cached. - min-total-token-count: u64, - } - - // ========================================================================= - // OAuth Web Auth Flow Types - // ========================================================================= - - /// Configuration for starting an OAuth web authentication flow. - record oauth-web-auth-config { - /// The URL to open in the user's browser to start authentication. - /// This should include client_id, redirect_uri, scope, state, etc. - auth-url: string, - /// The path to listen on for the OAuth callback (e.g., "/callback"). - /// A localhost server will be started to receive the redirect. - callback-path: string, - /// Timeout in seconds to wait for the callback (default: 300 = 5 minutes). - timeout-secs: option, - } - - /// Result of an OAuth web authentication flow. - record oauth-web-auth-result { - /// The full callback URL that was received, including query parameters. - /// The extension is responsible for parsing the code, state, etc. - callback-url: string, - /// The port that was used for the localhost callback server. - port: u32, - } - - /// A generic HTTP request for OAuth token exchange. - record oauth-http-request { - /// The URL to request. - url: string, - /// HTTP method (e.g., "POST", "GET"). - method: string, - /// Request headers as key-value pairs. - headers: list>, - /// Request body as a string (for form-encoded or JSON bodies). - body: string, - } - - /// Response from an OAuth HTTP request. - record oauth-http-response { - /// HTTP status code. - status: u16, - /// Response headers as key-value pairs. - headers: list>, - /// Response body as a string. - body: string, - } -} diff --git a/crates/extension_api/wit/since_v0.7.0/lsp.wit b/crates/extension_api/wit/since_v0.7.0/lsp.wit deleted file mode 100644 index 91a36c93a66467..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/lsp.wit +++ /dev/null @@ -1,90 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - label-details: option, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Label details for an LSP completion. - record completion-label-details { - detail: option, - description: option, - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.7.0/nodejs.wit b/crates/extension_api/wit/since_v0.7.0/nodejs.wit deleted file mode 100644 index c814548314162c..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.7.0/platform.wit b/crates/extension_api/wit/since_v0.7.0/platform.wit deleted file mode 100644 index 48472a99bc175f..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.7.0/process.wit b/crates/extension_api/wit/since_v0.7.0/process.wit deleted file mode 100644 index d9a5728a3d8f5b..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/process.wit +++ /dev/null @@ -1,29 +0,0 @@ -interface process { - use common.{env-vars}; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// The output of a finished process. - record output { - /// The status (exit code) of the process. - /// - /// On Unix, this will be `None` if the process was terminated by a signal. - status: option, - /// The data that the process wrote to stdout. - stdout: list, - /// The data that the process wrote to stderr. - stderr: list, - } - - /// Executes the given command as a child process, waiting for it to finish - /// and collecting all of its output. - run-command: func(command: command) -> result; -} diff --git a/crates/extension_api/wit/since_v0.7.0/settings.rs b/crates/extension_api/wit/since_v0.7.0/settings.rs deleted file mode 100644 index 19e28c1ba955a9..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/settings.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, num::NonZeroU32}; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a particular context server. -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ContextServerSettings { - /// The settings for the context server binary. - pub command: Option, - /// The settings to pass to the context server. - pub settings: Option, -} - -/// The settings for a command. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct CommandSettings { - /// The path to the command. - pub path: Option, - /// The arguments to pass to the command. - pub arguments: Option>, - /// The environment variables. - pub env: Option>, -} diff --git a/crates/extension_api/wit/since_v0.7.0/slash-command.wit b/crates/extension_api/wit/since_v0.7.0/slash-command.wit deleted file mode 100644 index f52561c2ef412b..00000000000000 --- a/crates/extension_api/wit/since_v0.7.0/slash-command.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface slash-command { - use common.{range}; - - /// A slash command for use in the Assistant. - record slash-command { - /// The name of the slash command. - name: string, - /// The description of the slash command. - description: string, - /// The tooltip text to display for the run button. - tooltip-text: string, - /// Whether this slash command requires an argument. - requires-argument: bool, - } - - /// The output of a slash command. - record slash-command-output { - /// The text produced by the slash command. - text: string, - /// The list of sections to show in the slash command placeholder. - sections: list, - } - - /// A section in the slash command output. - record slash-command-output-section { - /// The range this section occupies. - range: range, - /// The label to display in the placeholder for this section. - label: string, - } - - /// A completion for a slash command argument. - record slash-command-argument-completion { - /// The label to display for this completion. - label: string, - /// The new text that should be inserted into the command when this completion is accepted. - new-text: string, - /// Whether the command should be run when accepting this completion. - run-command: bool, - } -} diff --git a/crates/extension_api/wit/since_v0.8.0/extension.wit b/crates/extension_api/wit/since_v0.8.0/extension.wit index f95dfa04dac25f..8724369ec0472f 100644 --- a/crates/extension_api/wit/since_v0.8.0/extension.wit +++ b/crates/extension_api/wit/since_v0.8.0/extension.wit @@ -225,60 +225,4 @@ world extension { model-id: string ) -> option; - // ========================================================================= - // Language Model Provider Imports (callable by extensions) - // ========================================================================= - - /// Request a credential from the user. - /// Returns true if the credential was provided, false if the user cancelled. - import llm-request-credential: func( - provider-id: string, - credential-type: credential-type, - label: string, - placeholder: string - ) -> result; - - /// Get a stored credential for this provider. - import llm-get-credential: func(provider-id: string) -> option; - - /// Store a credential for this provider. - import llm-store-credential: func(provider-id: string, value: string) -> result<_, string>; - - /// Delete a stored credential for this provider. - import llm-delete-credential: func(provider-id: string) -> result<_, string>; - - /// Read an environment variable. - import llm-get-env-var: func(name: string) -> option; - - // ========================================================================= - // OAuth Web Auth Flow Imports - // ========================================================================= - - use llm-provider.{oauth-web-auth-config, oauth-web-auth-result, oauth-http-request, oauth-http-response}; - - /// Start an OAuth web authentication flow. - /// - /// This will: - /// 1. Start a localhost server to receive the OAuth callback - /// 2. Open the auth URL in the user's default browser - /// 3. Wait for the callback (up to the timeout) - /// 4. Return the callback URL with query parameters - /// - /// The extension is responsible for: - /// - Constructing the auth URL with client_id, redirect_uri, scope, state, etc. - /// - Parsing the callback URL to extract the authorization code - /// - Exchanging the code for tokens using llm-oauth-http-request - import llm-oauth-start-web-auth: func(config: oauth-web-auth-config) -> result; - - /// Make an HTTP request for OAuth token exchange. - /// - /// This is a simple HTTP client for OAuth flows, allowing the extension - /// to handle token exchange with full control over serialization. - import llm-oauth-http-request: func(request: oauth-http-request) -> result; - - /// Open a URL in the user's default browser. - /// - /// Useful for OAuth flows that need to open a browser but handle the - /// callback differently (e.g., polling-based flows). - import llm-oauth-open-browser: func(url: string) -> result<_, string>; } diff --git a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit index aec6569c2efda7..abf113c3910279 100644 --- a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit +++ b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit @@ -299,4 +299,55 @@ interface llm-provider { /// Response body as a string. body: string, } + + // ========================================================================= + // Import Functions (callable by extensions) + // ========================================================================= + + /// Request a credential from the user. + /// Returns true if the credential was provided, false if the user cancelled. + request-credential: func( + provider-id: string, + credential-type: credential-type, + label: string, + placeholder: string + ) -> result; + + /// Get a stored credential for this provider. + get-credential: func(provider-id: string) -> option; + + /// Store a credential for this provider. + store-credential: func(provider-id: string, value: string) -> result<_, string>; + + /// Delete a stored credential for this provider. + delete-credential: func(provider-id: string) -> result<_, string>; + + /// Read an environment variable. + get-env-var: func(name: string) -> option; + + /// Start an OAuth web authentication flow. + /// + /// This will: + /// 1. Start a localhost server to receive the OAuth callback + /// 2. Open the auth URL in the user's default browser + /// 3. Wait for the callback (up to the timeout) + /// 4. Return the callback URL with query parameters + /// + /// The extension is responsible for: + /// - Constructing the auth URL with client_id, redirect_uri, scope, state, etc. + /// - Parsing the callback URL to extract the authorization code + /// - Exchanging the code for tokens using oauth-http-request + oauth-start-web-auth: func(config: oauth-web-auth-config) -> result; + + /// Make an HTTP request for OAuth token exchange. + /// + /// This is a simple HTTP client for OAuth flows, allowing the extension + /// to handle token exchange with full control over serialization. + send-oauth-http-request: func(request: oauth-http-request) -> result; + + /// Open a URL in the user's default browser. + /// + /// Useful for OAuth flows that need to open a browser but handle the + /// callback differently (e.g., polling-based flows). + oauth-open-browser: func(url: string) -> result<_, string>; } diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 5fa62b032d6bd7..aeefa72237175f 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -7,7 +7,6 @@ mod since_v0_3_0; mod since_v0_4_0; mod since_v0_5_0; mod since_v0_6_0; -mod since_v0_7_0; mod since_v0_8_0; use dap::DebugRequest; use extension::{DebugTaskDefinition, KeyValueStoreDelegate, WorktreeDelegate}; @@ -111,7 +110,6 @@ pub fn authorize_access_to_unreleased_wasm_api_version( pub enum Extension { V0_8_0(since_v0_8_0::Extension), - V0_7_0(since_v0_7_0::Extension), V0_6_0(since_v0_6_0::Extension), V0_5_0(since_v0_5_0::Extension), V0_4_0(since_v0_4_0::Extension), @@ -142,15 +140,6 @@ impl Extension { .await .context("failed to instantiate wasm extension")?; Ok(Self::V0_8_0(extension)) - } else if version >= since_v0_7_0::MIN_VERSION { - let extension = since_v0_7_0::Extension::instantiate_async( - store, - component, - since_v0_7_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_7_0(extension)) } else if version >= since_v0_6_0::MIN_VERSION { let extension = since_v0_6_0::Extension::instantiate_async( store, @@ -238,7 +227,6 @@ impl Extension { pub async fn call_init_extension(&self, store: &mut Store) -> Result<()> { match self { Extension::V0_8_0(ext) => ext.call_init_extension(store).await, - Extension::V0_7_0(ext) => ext.call_init_extension(store).await, Extension::V0_6_0(ext) => ext.call_init_extension(store).await, Extension::V0_5_0(ext) => ext.call_init_extension(store).await, Extension::V0_4_0(ext) => ext.call_init_extension(store).await, @@ -263,10 +251,6 @@ impl Extension { ext.call_language_server_command(store, &language_server_id.0, resource) .await } - Extension::V0_7_0(ext) => Ok(ext - .call_language_server_command(store, &language_server_id.0, resource) - .await? - .map(Into::into)), Extension::V0_6_0(ext) => { ext.call_language_server_command(store, &language_server_id.0, resource) .await @@ -337,14 +321,6 @@ impl Extension { ) .await } - Extension::V0_7_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } Extension::V0_6_0(ext) => { ext.call_language_server_initialization_options( store, @@ -442,14 +418,6 @@ impl Extension { ) .await } - Extension::V0_7_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } Extension::V0_6_0(ext) => { ext.call_language_server_workspace_configuration( store, @@ -527,15 +495,6 @@ impl Extension { ) .await } - Extension::V0_7_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } Extension::V0_6_0(ext) => { ext.call_language_server_additional_initialization_options( store, @@ -589,15 +548,6 @@ impl Extension { ) .await } - Extension::V0_7_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } Extension::V0_6_0(ext) => { ext.call_language_server_additional_workspace_configuration( store, @@ -645,20 +595,6 @@ impl Extension { ext.call_labels_for_completions(store, &language_server_id.0, &completions) .await } - Extension::V0_7_0(ext) => ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions - .iter() - .cloned() - .map(Into::into) - .collect::>(), - ) - .await - .map(|res| { - res.map(|labels| labels.into_iter().map(|l| l.map(Into::into)).collect()) - }), Extension::V0_6_0(ext) => Ok(ext .call_labels_for_completions( store, @@ -765,16 +701,6 @@ impl Extension { ext.call_labels_for_symbols(store, &language_server_id.0, &symbols) .await } - Extension::V0_7_0(ext) => ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.iter().cloned().map(Into::into).collect::>(), - ) - .await - .map(|res| { - res.map(|labels| labels.into_iter().map(|l| l.map(Into::into)).collect()) - }), Extension::V0_6_0(ext) => Ok(ext .call_labels_for_symbols( store, @@ -881,13 +807,6 @@ impl Extension { ext.call_complete_slash_command_argument(store, command, arguments) .await } - Extension::V0_7_0(ext) => { - let command: since_v0_7_0::slash_command::SlashCommand = command.into(); - Ok(ext - .call_complete_slash_command_argument(store, &command, arguments) - .await? - .map(|completions| completions.into_iter().map(Into::into).collect())) - } Extension::V0_6_0(ext) => { ext.call_complete_slash_command_argument(store, command, arguments) .await @@ -930,13 +849,6 @@ impl Extension { ext.call_run_slash_command(store, command, arguments, resource) .await } - Extension::V0_7_0(ext) => { - let command: since_v0_7_0::slash_command::SlashCommand = command.into(); - Ok(ext - .call_run_slash_command(store, &command, arguments, resource) - .await? - .map(Into::into)) - } Extension::V0_6_0(ext) => { ext.call_run_slash_command(store, command, arguments, resource) .await @@ -978,10 +890,6 @@ impl Extension { ext.call_context_server_command(store, &context_server_id, project) .await } - Extension::V0_7_0(ext) => Ok(ext - .call_context_server_command(store, &context_server_id, project) - .await? - .map(Into::into)), Extension::V0_6_0(ext) => { ext.call_context_server_command(store, &context_server_id, project) .await @@ -1022,10 +930,6 @@ impl Extension { ext.call_context_server_configuration(store, &context_server_id, project) .await } - Extension::V0_7_0(ext) => Ok(ext - .call_context_server_configuration(store, &context_server_id, project) - .await? - .map(|opt| opt.map(Into::into))), Extension::V0_6_0(ext) => { ext.call_context_server_configuration(store, &context_server_id, project) .await @@ -1053,7 +957,6 @@ impl Extension { ) -> Result, String>> { match self { Extension::V0_8_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_7_0(ext) => ext.call_suggest_docs_packages(store, provider).await, Extension::V0_6_0(ext) => ext.call_suggest_docs_packages(store, provider).await, Extension::V0_5_0(ext) => ext.call_suggest_docs_packages(store, provider).await, Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await, @@ -1078,10 +981,6 @@ impl Extension { ext.call_index_docs(store, provider, package_name, kv_store) .await } - Extension::V0_7_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } Extension::V0_6_0(ext) => { ext.call_index_docs(store, provider, package_name, kv_store) .await @@ -1134,20 +1033,6 @@ impl Extension { Ok(Ok(dap_binary)) } - Extension::V0_7_0(ext) => { - let dap_binary = ext - .call_get_dap_binary( - store, - &adapter_name, - &task.try_into()?, - user_installed_path.as_ref().and_then(|p| p.to_str()), - resource, - ) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_binary.into())) - } Extension::V0_6_0(ext) => { let dap_binary = ext .call_get_dap_binary( @@ -1182,16 +1067,6 @@ impl Extension { Ok(Ok(result)) } - Extension::V0_7_0(ext) => { - let config = - serde_json::to_string(&config).context("Adapter config is not a valid JSON")?; - let result = ext - .call_dap_request_kind(store, &adapter_name, &config) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(result.into())) - } Extension::V0_6_0(ext) => { let config = serde_json::to_string(&config).context("Adapter config is not a valid JSON")?; @@ -1220,15 +1095,6 @@ impl Extension { Ok(Ok(result.try_into()?)) } - Extension::V0_7_0(ext) => { - let config: since_v0_7_0::dap::DebugConfig = config.into(); - let result = ext - .call_dap_config_to_scenario(store, &config) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(result.try_into()?)) - } Extension::V0_6_0(ext) => { let config = config.into(); let dap_binary = ext @@ -1264,21 +1130,6 @@ impl Extension { Ok(result.map(TryInto::try_into).transpose()?) } - Extension::V0_7_0(ext) => { - let build_config_template: since_v0_7_0::dap::BuildTaskTemplate = - build_config_template.into(); - let result = ext - .call_dap_locator_create_scenario( - store, - &locator_name, - &build_config_template, - &resolved_label, - &debug_adapter_name, - ) - .await?; - - Ok(result.map(TryInto::try_into).transpose()?) - } Extension::V0_6_0(ext) => { let build_config_template = build_config_template.into(); let dap_binary = ext @@ -1303,7 +1154,7 @@ impl Extension { resolved_build_task: SpawnInTerminal, ) -> Result> { match self { - Extension::V0_7_0(ext) => { + Extension::V0_8_0(ext) => { let build_config_template = resolved_build_task.try_into()?; let dap_request = ext .call_run_dap_locator(store, &locator_name, &build_config_template) @@ -1326,7 +1177,7 @@ impl Extension { } // ========================================================================= - // LLM Provider Methods (v0.7.0+) + // LLM Provider Methods (v0.8.0+) // ========================================================================= pub async fn call_llm_providers( @@ -1335,12 +1186,6 @@ impl Extension { ) -> Result> { match self { Extension::V0_8_0(ext) => ext.call_llm_providers(store).await, - Extension::V0_7_0(ext) => Ok(ext - .call_llm_providers(store) - .await? - .into_iter() - .map(Into::into) - .collect()), _ => Ok(Vec::new()), } } @@ -1352,11 +1197,7 @@ impl Extension { ) -> Result, String>> { match self { Extension::V0_8_0(ext) => ext.call_llm_provider_models(store, provider_id).await, - Extension::V0_7_0(ext) => Ok(ext - .call_llm_provider_models(store, provider_id) - .await? - .map(|models| models.into_iter().map(Into::into).collect())), - _ => anyhow::bail!("`llm_provider_models` not available prior to v0.7.0"), + _ => anyhow::bail!("`llm_provider_models` not available prior to v0.8.0"), } } @@ -1370,10 +1211,6 @@ impl Extension { ext.call_llm_provider_settings_markdown(store, provider_id) .await } - Extension::V0_7_0(ext) => { - ext.call_llm_provider_settings_markdown(store, provider_id) - .await - } _ => Ok(None), } } @@ -1388,10 +1225,6 @@ impl Extension { ext.call_llm_provider_is_authenticated(store, provider_id) .await } - Extension::V0_7_0(ext) => { - ext.call_llm_provider_is_authenticated(store, provider_id) - .await - } _ => Ok(false), } } @@ -1403,8 +1236,7 @@ impl Extension { ) -> Result> { match self { Extension::V0_8_0(ext) => ext.call_llm_provider_authenticate(store, provider_id).await, - Extension::V0_7_0(ext) => ext.call_llm_provider_authenticate(store, provider_id).await, - _ => anyhow::bail!("`llm_provider_authenticate` not available prior to v0.7.0"), + _ => anyhow::bail!("`llm_provider_authenticate` not available prior to v0.8.0"), } } @@ -1418,11 +1250,7 @@ impl Extension { ext.call_llm_provider_reset_credentials(store, provider_id) .await } - Extension::V0_7_0(ext) => { - ext.call_llm_provider_reset_credentials(store, provider_id) - .await - } - _ => anyhow::bail!("`llm_provider_reset_credentials` not available prior to v0.7.0"), + _ => anyhow::bail!("`llm_provider_reset_credentials` not available prior to v0.8.0"), } } @@ -1438,12 +1266,7 @@ impl Extension { ext.call_llm_count_tokens(store, provider_id, model_id, request) .await } - Extension::V0_7_0(ext) => { - let request: since_v0_7_0::llm_provider::CompletionRequest = request.clone().into(); - ext.call_llm_count_tokens(store, provider_id, model_id, &request) - .await - } - _ => anyhow::bail!("`llm_count_tokens` not available prior to v0.7.0"), + _ => anyhow::bail!("`llm_count_tokens` not available prior to v0.8.0"), } } @@ -1459,12 +1282,7 @@ impl Extension { ext.call_llm_stream_completion_start(store, provider_id, model_id, request) .await } - Extension::V0_7_0(ext) => { - let request: since_v0_7_0::llm_provider::CompletionRequest = request.clone().into(); - ext.call_llm_stream_completion_start(store, provider_id, model_id, &request) - .await - } - _ => anyhow::bail!("`llm_stream_completion_start` not available prior to v0.7.0"), + _ => anyhow::bail!("`llm_stream_completion_start` not available prior to v0.8.0"), } } @@ -1475,11 +1293,7 @@ impl Extension { ) -> Result, String>> { match self { Extension::V0_8_0(ext) => ext.call_llm_stream_completion_next(store, stream_id).await, - Extension::V0_7_0(ext) => Ok(ext - .call_llm_stream_completion_next(store, stream_id) - .await? - .map(|opt| opt.map(Into::into))), - _ => anyhow::bail!("`llm_stream_completion_next` not available prior to v0.7.0"), + _ => anyhow::bail!("`llm_stream_completion_next` not available prior to v0.8.0"), } } @@ -1490,8 +1304,7 @@ impl Extension { ) -> Result<()> { match self { Extension::V0_8_0(ext) => ext.call_llm_stream_completion_close(store, stream_id).await, - Extension::V0_7_0(ext) => ext.call_llm_stream_completion_close(store, stream_id).await, - _ => anyhow::bail!("`llm_stream_completion_close` not available prior to v0.7.0"), + _ => anyhow::bail!("`llm_stream_completion_close` not available prior to v0.8.0"), } } @@ -1506,10 +1319,6 @@ impl Extension { ext.call_llm_cache_configuration(store, provider_id, model_id) .await } - Extension::V0_7_0(ext) => Ok(ext - .call_llm_cache_configuration(store, provider_id, model_id) - .await? - .map(Into::into)), _ => Ok(None), } } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs deleted file mode 100644 index 6d1457bebd4fb1..00000000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_7_0.rs +++ /dev/null @@ -1,2022 +0,0 @@ -use crate::ExtensionSettings; -use crate::wasm_host::wit::since_v0_7_0::{ - dap::{ - AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest, - StartDebuggingRequestArguments, TcpArguments, TcpArgumentsTemplate, - }, - lsp::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind}, - slash_command::SlashCommandOutputSection, -}; -use crate::wasm_host::{WasmState, wit::ToWasmtimeResult}; -use ::http_client::{AsyncBody, HttpRequestExt}; -use ::settings::{Settings, WorktreeId}; -use anyhow::{Context as _, Result, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use async_trait::async_trait; -use credentials_provider::CredentialsProvider; -use extension::{ - ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, -}; -use futures::{AsyncReadExt, lock::Mutex}; -use futures::{FutureExt as _, io::BufReader}; -use gpui::{BackgroundExecutor, SharedString}; -use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings}; -use project::project_settings::ProjectSettings; -use semver::Version; -use smol::net::TcpListener; -use std::{ - env, - net::Ipv4Addr, - path::{Path, PathBuf}, - str::FromStr, - sync::{Arc, OnceLock}, - time::Duration, -}; -use task::{SpawnInTerminal, ZedDebugConfig}; -use url::Url; -use util::{ - archive::extract_zip, fs::make_file_executable, maybe, paths::PathStyle, rel_path::RelPath, -}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 7, 0); -#[allow(dead_code)] -pub const MAX_VERSION: Version = Version::new(0, 8, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.7.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream, - }, -}); - -// This is the latest version, so we pub use to make types available to parent module. -// Note: The parent wit.rs module re-exports specific types from here as the "latest" types. -pub use self::zed::extension::*; - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.7.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; -pub type ExtensionHttpResponseStream = Arc>>; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for std::ops::Range { - fn from(range: Range) -> Self { - let start = range.start as usize; - let end = range.end as usize; - start..end - } -} - -impl From for extension::Command { - fn from(value: Command) -> Self { - Self { - command: value.command.into(), - args: value.args, - env: value.env, - } - } -} - -impl From - for extension::StartDebuggingRequestArgumentsRequest -{ - fn from(value: StartDebuggingRequestArgumentsRequest) -> Self { - match value { - StartDebuggingRequestArgumentsRequest::Launch => Self::Launch, - StartDebuggingRequestArgumentsRequest::Attach => Self::Attach, - } - } -} -impl TryFrom for extension::StartDebuggingRequestArguments { - type Error = anyhow::Error; - - fn try_from(value: StartDebuggingRequestArguments) -> Result { - Ok(Self { - configuration: serde_json::from_str(&value.configuration)?, - request: value.request.into(), - }) - } -} -impl From for extension::TcpArguments { - fn from(value: TcpArguments) -> Self { - Self { - host: value.host.into(), - port: value.port, - timeout: value.timeout, - } - } -} - -impl From for TcpArgumentsTemplate { - fn from(value: extension::TcpArgumentsTemplate) -> Self { - Self { - host: value.host.map(Ipv4Addr::to_bits), - port: value.port, - timeout: value.timeout, - } - } -} - -impl From for extension::TcpArgumentsTemplate { - fn from(value: TcpArgumentsTemplate) -> Self { - Self { - host: value.host.map(Ipv4Addr::from_bits), - port: value.port, - timeout: value.timeout, - } - } -} - -impl TryFrom for DebugTaskDefinition { - type Error = anyhow::Error; - fn try_from(value: extension::DebugTaskDefinition) -> Result { - Ok(Self { - label: value.label.to_string(), - adapter: value.adapter.to_string(), - config: value.config.to_string(), - tcp_connection: value.tcp_connection.map(Into::into), - }) - } -} - -impl From for DebugRequest { - fn from(value: task::DebugRequest) -> Self { - match value { - task::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), - task::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), - } - } -} - -impl From for task::DebugRequest { - fn from(value: DebugRequest) -> Self { - match value { - DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), - DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), - } - } -} - -impl From for LaunchRequest { - fn from(value: task::LaunchRequest) -> Self { - Self { - program: value.program, - cwd: value.cwd.map(|p| p.to_string_lossy().into_owned()), - args: value.args, - envs: value.env.into_iter().collect(), - } - } -} - -impl From for AttachRequest { - fn from(value: task::AttachRequest) -> Self { - Self { - process_id: value.process_id, - } - } -} - -impl From for task::LaunchRequest { - fn from(value: LaunchRequest) -> Self { - Self { - program: value.program, - cwd: value.cwd.map(|p| p.into()), - args: value.args, - env: value.envs.into_iter().collect(), - } - } -} -impl From for task::AttachRequest { - fn from(value: AttachRequest) -> Self { - Self { - process_id: value.process_id, - } - } -} - -impl From for DebugConfig { - fn from(value: ZedDebugConfig) -> Self { - Self { - label: value.label.into(), - adapter: value.adapter.into(), - request: value.request.into(), - stop_on_entry: value.stop_on_entry, - } - } -} -impl TryFrom for extension::DebugAdapterBinary { - type Error = anyhow::Error; - fn try_from(value: DebugAdapterBinary) -> Result { - Ok(Self { - command: value.command, - arguments: value.arguments, - envs: value.envs.into_iter().collect(), - cwd: value.cwd.map(|s| s.into()), - connection: value.connection.map(Into::into), - request_args: value.request_args.try_into()?, - }) - } -} - -impl From for extension::BuildTaskDefinition { - fn from(value: BuildTaskDefinition) -> Self { - match value { - BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), - BuildTaskDefinition::Template(build_task_template) => Self::Template { - task_template: build_task_template.template.into(), - locator_name: build_task_template.locator_name.map(SharedString::from), - }, - } - } -} - -impl From for BuildTaskDefinition { - fn from(value: extension::BuildTaskDefinition) -> Self { - match value { - extension::BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), - extension::BuildTaskDefinition::Template { - task_template, - locator_name, - } => Self::Template(BuildTaskDefinitionTemplatePayload { - template: task_template.into(), - locator_name: locator_name.map(String::from), - }), - } - } -} -impl From for extension::BuildTaskTemplate { - fn from(value: BuildTaskTemplate) -> Self { - Self { - label: value.label, - command: value.command, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd, - ..Default::default() - } - } -} -impl From for BuildTaskTemplate { - fn from(value: extension::BuildTaskTemplate) -> Self { - Self { - label: value.label, - command: value.command, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd, - } - } -} - -impl TryFrom for extension::DebugScenario { - type Error = anyhow::Error; - - fn try_from(value: DebugScenario) -> std::result::Result { - Ok(Self { - adapter: value.adapter.into(), - label: value.label.into(), - build: value.build.map(Into::into), - config: serde_json::Value::from_str(&value.config)?, - tcp_connection: value.tcp_connection.map(Into::into), - }) - } -} - -impl From for DebugScenario { - fn from(value: extension::DebugScenario) -> Self { - Self { - adapter: value.adapter.into(), - label: value.label.into(), - build: value.build.map(Into::into), - config: value.config.to_string(), - tcp_connection: value.tcp_connection.map(Into::into), - } - } -} - -impl TryFrom for ResolvedTask { - type Error = anyhow::Error; - - fn try_from(value: SpawnInTerminal) -> Result { - Ok(Self { - label: value.label, - command: value.command.context("missing command")?, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd.map(|s| { - let s = s.to_string_lossy(); - if cfg!(target_os = "windows") { - s.replace('\\', "/") - } else { - s.into_owned() - } - }), - }) - } -} - -impl From for extension::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl From for extension::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for extension::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for Completion { - fn from(value: extension::Completion) -> Self { - Self { - label: value.label, - label_details: value.label_details.map(Into::into), - detail: value.detail, - kind: value.kind.map(Into::into), - insert_text_format: value.insert_text_format.map(Into::into), - } - } -} - -impl From for CompletionLabelDetails { - fn from(value: extension::CompletionLabelDetails) -> Self { - Self { - detail: value.detail, - description: value.description, - } - } -} - -impl From for CompletionKind { - fn from(value: extension::CompletionKind) -> Self { - match value { - extension::CompletionKind::Text => Self::Text, - extension::CompletionKind::Method => Self::Method, - extension::CompletionKind::Function => Self::Function, - extension::CompletionKind::Constructor => Self::Constructor, - extension::CompletionKind::Field => Self::Field, - extension::CompletionKind::Variable => Self::Variable, - extension::CompletionKind::Class => Self::Class, - extension::CompletionKind::Interface => Self::Interface, - extension::CompletionKind::Module => Self::Module, - extension::CompletionKind::Property => Self::Property, - extension::CompletionKind::Unit => Self::Unit, - extension::CompletionKind::Value => Self::Value, - extension::CompletionKind::Enum => Self::Enum, - extension::CompletionKind::Keyword => Self::Keyword, - extension::CompletionKind::Snippet => Self::Snippet, - extension::CompletionKind::Color => Self::Color, - extension::CompletionKind::File => Self::File, - extension::CompletionKind::Reference => Self::Reference, - extension::CompletionKind::Folder => Self::Folder, - extension::CompletionKind::EnumMember => Self::EnumMember, - extension::CompletionKind::Constant => Self::Constant, - extension::CompletionKind::Struct => Self::Struct, - extension::CompletionKind::Event => Self::Event, - extension::CompletionKind::Operator => Self::Operator, - extension::CompletionKind::TypeParameter => Self::TypeParameter, - extension::CompletionKind::Other(value) => Self::Other(value), - } - } -} - -impl From for InsertTextFormat { - fn from(value: extension::InsertTextFormat) -> Self { - match value { - extension::InsertTextFormat::PlainText => Self::PlainText, - extension::InsertTextFormat::Snippet => Self::Snippet, - extension::InsertTextFormat::Other(value) => Self::Other(value), - } - } -} - -impl From for Symbol { - fn from(value: extension::Symbol) -> Self { - Self { - kind: value.kind.into(), - name: value.name, - } - } -} - -impl From for SymbolKind { - fn from(value: extension::SymbolKind) -> Self { - match value { - extension::SymbolKind::File => Self::File, - extension::SymbolKind::Module => Self::Module, - extension::SymbolKind::Namespace => Self::Namespace, - extension::SymbolKind::Package => Self::Package, - extension::SymbolKind::Class => Self::Class, - extension::SymbolKind::Method => Self::Method, - extension::SymbolKind::Property => Self::Property, - extension::SymbolKind::Field => Self::Field, - extension::SymbolKind::Constructor => Self::Constructor, - extension::SymbolKind::Enum => Self::Enum, - extension::SymbolKind::Interface => Self::Interface, - extension::SymbolKind::Function => Self::Function, - extension::SymbolKind::Variable => Self::Variable, - extension::SymbolKind::Constant => Self::Constant, - extension::SymbolKind::String => Self::String, - extension::SymbolKind::Number => Self::Number, - extension::SymbolKind::Boolean => Self::Boolean, - extension::SymbolKind::Array => Self::Array, - extension::SymbolKind::Object => Self::Object, - extension::SymbolKind::Key => Self::Key, - extension::SymbolKind::Null => Self::Null, - extension::SymbolKind::EnumMember => Self::EnumMember, - extension::SymbolKind::Struct => Self::Struct, - extension::SymbolKind::Event => Self::Event, - extension::SymbolKind::Operator => Self::Operator, - extension::SymbolKind::TypeParameter => Self::TypeParameter, - extension::SymbolKind::Other(value) => Self::Other(value), - } - } -} - -impl From for SlashCommand { - fn from(value: extension::SlashCommand) -> Self { - Self { - name: value.name, - description: value.description, - tooltip_text: value.tooltip_text, - requires_argument: value.requires_argument, - } - } -} - -impl From for extension::SlashCommandOutput { - fn from(value: SlashCommandOutput) -> Self { - Self { - text: value.text, - sections: value.sections.into_iter().map(Into::into).collect(), - } - } -} - -impl From for extension::SlashCommandOutputSection { - fn from(value: SlashCommandOutputSection) -> Self { - Self { - range: value.range.start as usize..value.range.end as usize, - label: value.label, - } - } -} - -impl From for extension::SlashCommandArgumentCompletion { - fn from(value: SlashCommandArgumentCompletion) -> Self { - Self { - label: value.label, - new_text: value.new_text, - run_command: value.run_command, - } - } -} - -impl TryFrom for extension::ContextServerConfiguration { - type Error = anyhow::Error; - - fn try_from(value: ContextServerConfiguration) -> Result { - let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema) - .context("Failed to parse settings_schema")?; - - Ok(Self { - installation_instructions: value.installation_instructions, - default_settings: value.default_settings, - settings_schema, - }) - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - let kv_store = self.table.get(&kv_store)?; - kv_store.insert(key, value).await.to_wasmtime_result() - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - let project = self.table.get(&project)?; - Ok(project.worktree_ids()) - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.id()) - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.root_path()) - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?) - .await - .map_err(|error| error.to_string())) - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.shell_env().await.into_iter().collect()) - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate.which(binary_name).await) - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl common::Host for WasmState {} - -impl http_client::Host for WasmState { - async fn fetch( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result> { - maybe!(async { - let url = &request.url; - let request = convert_request(&request)?; - let mut response = self.host.http_client.send(request).await?; - - if response.status().is_client_error() || response.status().is_server_error() { - bail!("failed to fetch '{url}': status code {}", response.status()) - } - convert_response(&mut response).await - }) - .await - .to_wasmtime_result() - } - - async fn fetch_stream( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result, String>> { - let request = convert_request(&request)?; - let response = self.host.http_client.send(request); - maybe!(async { - let response = response.await?; - let stream = Arc::new(Mutex::new(response)); - let resource = self.table.push(stream)?; - Ok(resource) - }) - .await - .to_wasmtime_result() - } -} - -impl http_client::HostHttpResponseStream for WasmState { - async fn next_chunk( - &mut self, - resource: Resource, - ) -> wasmtime::Result>, String>> { - let stream = self.table.get(&resource)?.clone(); - maybe!(async move { - let mut response = stream.lock().await; - let mut buffer = vec![0; 8192]; // 8KB buffer - let bytes_read = response.body_mut().read(&mut buffer).await?; - if bytes_read == 0 { - Ok(None) - } else { - buffer.truncate(bytes_read); - Ok(Some(buffer)) - } - }) - .await - .to_wasmtime_result() - } - - async fn drop(&mut self, _resource: Resource) -> Result<()> { - Ok(()) - } -} - -impl From for ::http_client::Method { - fn from(value: http_client::HttpMethod) -> Self { - match value { - http_client::HttpMethod::Get => Self::GET, - http_client::HttpMethod::Post => Self::POST, - http_client::HttpMethod::Put => Self::PUT, - http_client::HttpMethod::Delete => Self::DELETE, - http_client::HttpMethod::Head => Self::HEAD, - http_client::HttpMethod::Options => Self::OPTIONS, - http_client::HttpMethod::Patch => Self::PATCH, - } - } -} - -fn convert_request( - extension_request: &http_client::HttpRequest, -) -> anyhow::Result<::http_client::Request> { - let mut request = ::http_client::Request::builder() - .method(::http_client::Method::from(extension_request.method)) - .uri(&extension_request.url) - .follow_redirects(match extension_request.redirect_policy { - http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow, - http_client::RedirectPolicy::FollowLimit(limit) => { - ::http_client::RedirectPolicy::FollowLimit(limit) - } - http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll, - }); - for (key, value) in &extension_request.headers { - request = request.header(key, value); - } - let body = extension_request - .body - .clone() - .map(AsyncBody::from) - .unwrap_or_default(); - request.body(body).map_err(anyhow::Error::from) -} - -async fn convert_response( - response: &mut ::http_client::Response, -) -> anyhow::Result { - let mut extension_response = http_client::HttpResponse { - body: Vec::new(), - headers: Vec::new(), - }; - - for (key, value) in response.headers() { - extension_response - .headers - .push((key.to_string(), value.to_str().unwrap_or("").to_string())); - } - - response - .body_mut() - .read_to_end(&mut extension_response.body) - .await?; - - Ok(extension_response) -} - -impl nodejs::Host for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - self.host - .node_runtime - .binary_path() - .await - .map(|path| path.to_string_lossy().into_owned()) - .to_wasmtime_result() - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_package_latest_version(&package_name) - .await - .to_wasmtime_result() - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - self.host - .node_runtime - .npm_package_installed_version(&self.work_dir(), &package_name) - .await - .to_wasmtime_result() - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - self.capability_granter - .grant_npm_install_package(&package_name)?; - - self.host - .node_runtime - .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl lsp::Host for WasmState {} - -impl From<::http_client::github::GithubRelease> for github::GithubRelease { - fn from(value: ::http_client::github::GithubRelease) -> Self { - Self { - version: value.tag_name, - assets: value.assets.into_iter().map(Into::into).collect(), - } - } -} - -impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset { - fn from(value: ::http_client::github::GithubReleaseAsset) -> Self { - Self { - name: value.name, - download_url: value.browser_download_url, - } - } -} - -impl github::Host for WasmState { - async fn latest_github_release( - &mut self, - repo: String, - options: github::GithubReleaseOptions, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_client::github::latest_github_release( - &repo, - options.require_assets, - options.pre_release, - self.host.http_client.clone(), - ) - .await?; - Ok(release.into()) - }) - .await - .to_wasmtime_result() - } - - async fn github_release_by_tag_name( - &mut self, - repo: String, - tag: String, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_client::github::get_release_by_tag_name( - &repo, - &tag, - self.host.http_client.clone(), - ) - .await?; - Ok(release.into()) - }) - .await - .to_wasmtime_result() - } -} - -impl platform::Host for WasmState { - async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { - Ok(( - match env::consts::OS { - "macos" => platform::Os::Mac, - "linux" => platform::Os::Linux, - "windows" => platform::Os::Windows, - _ => panic!("unsupported os"), - }, - match env::consts::ARCH { - "aarch64" => platform::Architecture::Aarch64, - "x86" => platform::Architecture::X86, - "x86_64" => platform::Architecture::X8664, - _ => panic!("unsupported architecture"), - }, - )) - } -} - -impl From for process::Output { - fn from(output: std::process::Output) -> Self { - Self { - status: output.status.code(), - stdout: output.stdout, - stderr: output.stderr, - } - } -} - -impl process::Host for WasmState { - async fn run_command( - &mut self, - command: process::Command, - ) -> wasmtime::Result> { - maybe!(async { - self.capability_granter - .grant_exec(&command.command, &command.args)?; - - let output = util::command::new_smol_command(command.command.as_str()) - .args(&command.args) - .envs(command.env) - .output() - .await?; - - Ok(output.into()) - }) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl slash_command::Host for WasmState {} - -#[async_trait] -impl context_server::Host for WasmState {} - -impl dap::Host for WasmState { - async fn resolve_tcp_template( - &mut self, - template: TcpArgumentsTemplate, - ) -> wasmtime::Result> { - maybe!(async { - let (host, port, timeout) = - ::dap::configure_tcp_connection(task::TcpArgumentsTemplate { - port: template.port, - host: template.host.map(Ipv4Addr::from_bits), - timeout: template.timeout, - }) - .await?; - Ok(TcpArguments { - port, - host: host.to_bits(), - timeout, - }) - }) - .await - .to_wasmtime_result() - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - self.on_main_thread(|cx| { - async move { - let path = location.as_ref().and_then(|location| { - RelPath::new(Path::new(&location.path), PathStyle::Posix).ok() - }); - let location = path - .as_ref() - .zip(location.as_ref()) - .map(|(path, location)| ::settings::SettingsLocation { - worktree_id: WorktreeId::from_proto(location.worktree_id), - path, - }); - - cx.update(|cx| match category.as_str() { - "language" => { - let key = key.map(|k| LanguageName::new(&k)); - let settings = AllLanguageSettings::get(location, cx).language( - location, - key.as_ref(), - cx, - ); - Ok(serde_json::to_string(&settings::LanguageSettings { - tab_size: settings.tab_size, - })?) - } - "lsp" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .lsp - .get(&::lsp::LanguageServerName::from_proto(key)) - }) - .cloned() - .unwrap_or_default(); - Ok(serde_json::to_string(&settings::LspSettings { - binary: settings.binary.map(|binary| settings::CommandSettings { - path: binary.path, - arguments: binary.arguments, - env: binary.env.map(|env| env.into_iter().collect()), - }), - settings: settings.settings, - initialization_options: settings.initialization_options, - })?) - } - "context_servers" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .context_servers - .get(key.as_str()) - }) - .cloned() - .unwrap_or_else(|| { - project::project_settings::ContextServerSettings::default_extension( - ) - }); - - match settings { - project::project_settings::ContextServerSettings::Stdio { - enabled: _, - command, - } => Ok(serde_json::to_string(&settings::ContextServerSettings { - command: Some(settings::CommandSettings { - path: command.path.to_str().map(|path| path.to_string()), - arguments: Some(command.args), - env: command.env.map(|env| env.into_iter().collect()), - }), - settings: None, - })?), - project::project_settings::ContextServerSettings::Extension { - enabled: _, - settings, - } => Ok(serde_json::to_string(&settings::ContextServerSettings { - command: None, - settings: Some(settings), - })?), - project::project_settings::ContextServerSettings::Http { .. } => { - bail!("remote context server settings not supported in 0.6.0") - } - } - } - _ => { - bail!("Unknown settings category: {}", category); - } - }) - } - .boxed_local() - }) - .await? - .to_wasmtime_result() - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, - LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, - LanguageServerInstallationStatus::None => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, - }; - - self.host - .proxy - .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); - - Ok(()) - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - maybe!(async { - let parsed_url = Url::parse(&url)?; - self.capability_granter.grant_download_file(&parsed_url)?; - - let path = PathBuf::from(path); - let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); - - self.host.fs.create_dir(&extension_work_dir).await?; - - let destination_path = self - .host - .writeable_path_from_extension(&self.manifest.id, &path)?; - - let mut response = self - .host - .http_client - .get(&url, Default::default(), true) - .await - .context("downloading release")?; - - anyhow::ensure!( - response.status().is_success(), - "download failed with status {}", - response.status() - ); - let body = BufReader::new(response.body_mut()); - - match file_type { - DownloadedFileType::Uncompressed => { - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::Gzip => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::GzipTar => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .extract_tar_file(&destination_path, Archive::new(body)) - .await?; - } - DownloadedFileType::Zip => { - futures::pin_mut!(body); - extract_zip(&destination_path, body) - .await - .with_context(|| format!("unzipping {path:?} archive"))?; - } - } - - Ok(()) - }) - .await - .to_wasmtime_result() - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - make_file_executable(&path) - .await - .with_context(|| format!("setting permissions for path {path:?}")) - .to_wasmtime_result() - } - - // ========================================================================= - // LLM Provider Import Implementations - // ========================================================================= - - async fn llm_request_credential( - &mut self, - _provider_id: String, - _credential_type: llm_provider::CredentialType, - _label: String, - _placeholder: String, - ) -> wasmtime::Result> { - // For now, credential requests return false (not provided) - // Extensions should use llm_get_env_var to check for env vars first, - // then llm_store_credential/llm_get_credential for manual storage - // Full UI credential prompting will be added in a future phase - Ok(Ok(false)) - } - - async fn llm_get_credential( - &mut self, - provider_id: String, - ) -> wasmtime::Result> { - let extension_id = self.manifest.id.clone(); - let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id); - - self.on_main_thread(move |cx| { - async move { - let credentials_provider = cx.update(|cx| ::global(cx))?; - let result = credentials_provider - .read_credentials(&credential_key, cx) - .await - .ok() - .flatten(); - Ok(result.map(|(_, password)| String::from_utf8_lossy(&password).to_string())) - } - .boxed_local() - }) - .await - } - - async fn llm_store_credential( - &mut self, - provider_id: String, - value: String, - ) -> wasmtime::Result> { - let extension_id = self.manifest.id.clone(); - let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id); - - self.on_main_thread(move |cx| { - async move { - let credentials_provider = cx.update(|cx| ::global(cx))?; - credentials_provider - .write_credentials(&credential_key, "api_key", value.as_bytes(), cx) - .await - .map_err(|e| anyhow::anyhow!("{}", e)) - } - .boxed_local() - }) - .await - .to_wasmtime_result() - } - - async fn llm_delete_credential( - &mut self, - provider_id: String, - ) -> wasmtime::Result> { - let extension_id = self.manifest.id.clone(); - let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id); - - self.on_main_thread(move |cx| { - async move { - let credentials_provider = cx.update(|cx| ::global(cx))?; - credentials_provider - .delete_credentials(&credential_key, cx) - .await - .map_err(|e| anyhow::anyhow!("{}", e)) - } - .boxed_local() - }) - .await - .to_wasmtime_result() - } - - async fn llm_get_env_var(&mut self, name: String) -> wasmtime::Result> { - let extension_id = self.manifest.id.clone(); - - // Find which provider (if any) declares this env var in its auth config - let mut allowed_provider_id: Option> = None; - for (provider_id, provider_entry) in &self.manifest.language_model_providers { - if let Some(auth_config) = &provider_entry.auth { - if auth_config.env_var.as_deref() == Some(&name) { - allowed_provider_id = Some(provider_id.clone()); - break; - } - } - } - - // If no provider declares this env var, deny access - let Some(provider_id) = allowed_provider_id else { - log::warn!( - "Extension {} attempted to read env var {} which is not declared in any provider auth config", - extension_id, - name - ); - return Ok(None); - }; - - // Check if the user has allowed this provider to read env vars - let full_provider_id = format!("{}:{}", extension_id, provider_id); - let is_allowed = self - .on_main_thread(move |cx| { - async move { - cx.update(|cx| { - ExtensionSettings::get_global(cx) - .allowed_env_var_providers - .contains(full_provider_id.as_str()) - }) - .unwrap_or(false) - } - .boxed_local() - }) - .await; - - if !is_allowed { - log::debug!( - "Extension {} provider {} is not allowed to read env var {}", - extension_id, - provider_id, - name - ); - return Ok(None); - } - - Ok(env::var(&name).ok()) - } - - async fn llm_oauth_start_web_auth( - &mut self, - config: llm_provider::OauthWebAuthConfig, - ) -> wasmtime::Result> { - let auth_url = config.auth_url; - let callback_path = config.callback_path; - let timeout_secs = config.timeout_secs.unwrap_or(300); - - self.on_main_thread(move |cx| { - async move { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .map_err(|e| anyhow::anyhow!("Failed to bind localhost server: {}", e))?; - let port = listener - .local_addr() - .map_err(|e| anyhow::anyhow!("Failed to get local address: {}", e))? - .port(); - - cx.update(|cx| { - cx.open_url(&auth_url); - })?; - - let accept_future = async { - let (mut stream, _) = listener - .accept() - .await - .map_err(|e| anyhow::anyhow!("Failed to accept connection: {}", e))?; - - let mut request_line = String::new(); - { - let mut reader = smol::io::BufReader::new(&mut stream); - smol::io::AsyncBufReadExt::read_line(&mut reader, &mut request_line) - .await - .map_err(|e| anyhow::anyhow!("Failed to read request: {}", e))?; - } - - let callback_url = if let Some(path_start) = request_line.find(' ') { - if let Some(path_end) = request_line[path_start + 1..].find(' ') { - let path = &request_line[path_start + 1..path_start + 1 + path_end]; - if path.starts_with(&callback_path) || path.starts_with(&format!("/{}", callback_path.trim_start_matches('/'))) { - format!("http://localhost:{}{}", port, path) - } else { - return Err(anyhow::anyhow!( - "Unexpected callback path: {}", - path - )); - } - } else { - return Err(anyhow::anyhow!("Malformed HTTP request")); - } - } else { - return Err(anyhow::anyhow!("Malformed HTTP request")); - }; - - let response = "HTTP/1.1 200 OK\r\n\ - Content-Type: text/html\r\n\ - Connection: close\r\n\ - \r\n\ - \ - Authentication Complete\ - \ -
\ -

Authentication Complete

\ -

You can close this window and return to Zed.

\ -
"; - - smol::io::AsyncWriteExt::write_all(&mut stream, response.as_bytes()) - .await - .ok(); - smol::io::AsyncWriteExt::flush(&mut stream).await.ok(); - - Ok(callback_url) - }; - - let timeout_duration = Duration::from_secs(timeout_secs as u64); - let callback_url = smol::future::or( - accept_future, - async { - smol::Timer::after(timeout_duration).await; - Err(anyhow::anyhow!( - "OAuth callback timed out after {} seconds", - timeout_secs - )) - }, - ) - .await?; - - Ok(llm_provider::OauthWebAuthResult { - callback_url, - port: port as u32, - }) - } - .boxed_local() - }) - .await - .to_wasmtime_result() - } - - async fn llm_oauth_http_request( - &mut self, - request: llm_provider::OauthHttpRequest, - ) -> wasmtime::Result> { - let http_client = self.host.http_client.clone(); - - self.on_main_thread(move |_cx| { - async move { - let method = match request.method.to_uppercase().as_str() { - "GET" => ::http_client::Method::GET, - "POST" => ::http_client::Method::POST, - "PUT" => ::http_client::Method::PUT, - "DELETE" => ::http_client::Method::DELETE, - "PATCH" => ::http_client::Method::PATCH, - _ => { - return Err(anyhow::anyhow!( - "Unsupported HTTP method: {}", - request.method - )); - } - }; - - let mut builder = ::http_client::Request::builder() - .method(method) - .uri(&request.url); - - for (key, value) in &request.headers { - builder = builder.header(key.as_str(), value.as_str()); - } - - let body = if request.body.is_empty() { - AsyncBody::empty() - } else { - AsyncBody::from(request.body.into_bytes()) - }; - - let http_request = builder - .body(body) - .map_err(|e| anyhow::anyhow!("Failed to build request: {}", e))?; - - let mut response = http_client - .send(http_request) - .await - .map_err(|e| anyhow::anyhow!("HTTP request failed: {}", e))?; - - let status = response.status().as_u16(); - let headers: Vec<(String, String)> = response - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect(); - - let mut body_bytes = Vec::new(); - futures::AsyncReadExt::read_to_end(response.body_mut(), &mut body_bytes) - .await - .map_err(|e| anyhow::anyhow!("Failed to read response body: {}", e))?; - - let body = String::from_utf8_lossy(&body_bytes).to_string(); - - Ok(llm_provider::OauthHttpResponse { - status, - headers, - body, - }) - } - .boxed_local() - }) - .await - .to_wasmtime_result() - } - - async fn llm_oauth_open_browser( - &mut self, - url: String, - ) -> wasmtime::Result> { - self.on_main_thread(move |cx| { - async move { - cx.update(|cx| { - cx.open_url(&url); - })?; - Ok(()) - } - .boxed_local() - }) - .await - .to_wasmtime_result() - } -} - -// ============================================================================= -// LLM Provider Host Implementations -// ============================================================================= - -impl llm_provider::Host for WasmState {} - -// ============================================================================= -// LLM Provider Type Conversions (v0.7.0 -> latest/v0.8.0) -// ============================================================================= - -use super::since_v0_8_0 as latest; - -impl From for latest::llm_provider::ProviderInfo { - fn from(value: llm_provider::ProviderInfo) -> Self { - Self { - id: value.id, - name: value.name, - icon: value.icon, - } - } -} - -impl From for latest::llm_provider::ModelInfo { - fn from(value: llm_provider::ModelInfo) -> Self { - Self { - id: value.id, - name: value.name, - max_token_count: value.max_token_count, - max_output_tokens: value.max_output_tokens, - capabilities: value.capabilities.into(), - is_default: value.is_default, - is_default_fast: value.is_default_fast, - } - } -} - -impl From for latest::llm_provider::ModelCapabilities { - fn from(value: llm_provider::ModelCapabilities) -> Self { - Self { - supports_images: value.supports_images, - supports_tools: value.supports_tools, - supports_tool_choice_auto: value.supports_tool_choice_auto, - supports_tool_choice_any: value.supports_tool_choice_any, - supports_tool_choice_none: value.supports_tool_choice_none, - supports_thinking: value.supports_thinking, - tool_input_format: value.tool_input_format.into(), - } - } -} - -impl From for latest::llm_provider::ToolInputFormat { - fn from(value: llm_provider::ToolInputFormat) -> Self { - match value { - llm_provider::ToolInputFormat::JsonSchema => Self::JsonSchema, - llm_provider::ToolInputFormat::Simplified => Self::Simplified, - } - } -} - -impl From for latest::llm_provider::CompletionEvent { - fn from(value: llm_provider::CompletionEvent) -> Self { - match value { - llm_provider::CompletionEvent::Started => Self::Started, - llm_provider::CompletionEvent::Text(s) => Self::Text(s), - llm_provider::CompletionEvent::Thinking(t) => Self::Thinking(t.into()), - llm_provider::CompletionEvent::RedactedThinking(s) => Self::RedactedThinking(s), - llm_provider::CompletionEvent::ToolUse(t) => Self::ToolUse(t.into()), - llm_provider::CompletionEvent::ToolUseJsonParseError(e) => { - Self::ToolUseJsonParseError(e.into()) - } - llm_provider::CompletionEvent::Stop(r) => Self::Stop(r.into()), - llm_provider::CompletionEvent::Usage(u) => Self::Usage(u.into()), - llm_provider::CompletionEvent::ReasoningDetails(s) => Self::ReasoningDetails(s), - } - } -} - -impl From for latest::llm_provider::ThinkingContent { - fn from(value: llm_provider::ThinkingContent) -> Self { - Self { - text: value.text, - signature: value.signature, - } - } -} - -impl From for latest::llm_provider::ToolUse { - fn from(value: llm_provider::ToolUse) -> Self { - Self { - id: value.id, - name: value.name, - input: value.input, - thought_signature: value.thought_signature, - } - } -} - -impl From for latest::llm_provider::ToolUseJsonParseError { - fn from(value: llm_provider::ToolUseJsonParseError) -> Self { - Self { - id: value.id, - tool_name: value.tool_name, - raw_input: value.raw_input, - error: value.error, - } - } -} - -impl From for latest::llm_provider::StopReason { - fn from(value: llm_provider::StopReason) -> Self { - match value { - llm_provider::StopReason::EndTurn => Self::EndTurn, - llm_provider::StopReason::MaxTokens => Self::MaxTokens, - llm_provider::StopReason::ToolUse => Self::ToolUse, - llm_provider::StopReason::Refusal => Self::Refusal, - } - } -} - -impl From for latest::llm_provider::TokenUsage { - fn from(value: llm_provider::TokenUsage) -> Self { - Self { - input_tokens: value.input_tokens, - output_tokens: value.output_tokens, - cache_creation_input_tokens: value.cache_creation_input_tokens, - cache_read_input_tokens: value.cache_read_input_tokens, - } - } -} - -impl From for latest::llm_provider::CacheConfiguration { - fn from(value: llm_provider::CacheConfiguration) -> Self { - Self { - max_cache_anchors: value.max_cache_anchors, - should_cache_tool_definitions: value.should_cache_tool_definitions, - min_total_token_count: value.min_total_token_count, - } - } -} - -// Conversions from latest (v0.8.0) -> v0.7.0 for requests - -impl From for llm_provider::CompletionRequest { - fn from(value: latest::llm_provider::CompletionRequest) -> Self { - Self { - messages: value.messages.into_iter().map(Into::into).collect(), - tools: value.tools.into_iter().map(Into::into).collect(), - tool_choice: value.tool_choice.map(Into::into), - stop_sequences: value.stop_sequences, - temperature: value.temperature, - thinking_allowed: value.thinking_allowed, - max_tokens: value.max_tokens, - } - } -} - -impl From for llm_provider::RequestMessage { - fn from(value: latest::llm_provider::RequestMessage) -> Self { - Self { - role: value.role.into(), - content: value.content.into_iter().map(Into::into).collect(), - cache: value.cache, - } - } -} - -impl From for llm_provider::MessageRole { - fn from(value: latest::llm_provider::MessageRole) -> Self { - match value { - latest::llm_provider::MessageRole::User => Self::User, - latest::llm_provider::MessageRole::Assistant => Self::Assistant, - latest::llm_provider::MessageRole::System => Self::System, - } - } -} - -impl From for llm_provider::MessageContent { - fn from(value: latest::llm_provider::MessageContent) -> Self { - match value { - latest::llm_provider::MessageContent::Text(s) => Self::Text(s), - latest::llm_provider::MessageContent::Image(i) => Self::Image(i.into()), - latest::llm_provider::MessageContent::ToolUse(t) => Self::ToolUse(t.into()), - latest::llm_provider::MessageContent::ToolResult(t) => Self::ToolResult(t.into()), - latest::llm_provider::MessageContent::Thinking(t) => Self::Thinking(t.into()), - latest::llm_provider::MessageContent::RedactedThinking(s) => Self::RedactedThinking(s), - } - } -} - -impl From for llm_provider::ImageData { - fn from(value: latest::llm_provider::ImageData) -> Self { - Self { - source: value.source, - width: value.width, - height: value.height, - } - } -} - -impl From for llm_provider::ToolUse { - fn from(value: latest::llm_provider::ToolUse) -> Self { - Self { - id: value.id, - name: value.name, - input: value.input, - thought_signature: value.thought_signature, - } - } -} - -impl From for llm_provider::ToolResult { - fn from(value: latest::llm_provider::ToolResult) -> Self { - Self { - tool_use_id: value.tool_use_id, - tool_name: value.tool_name, - is_error: value.is_error, - content: value.content.into(), - } - } -} - -impl From for llm_provider::ToolResultContent { - fn from(value: latest::llm_provider::ToolResultContent) -> Self { - match value { - latest::llm_provider::ToolResultContent::Text(s) => Self::Text(s), - latest::llm_provider::ToolResultContent::Image(i) => Self::Image(i.into()), - } - } -} - -impl From for llm_provider::ThinkingContent { - fn from(value: latest::llm_provider::ThinkingContent) -> Self { - Self { - text: value.text, - signature: value.signature, - } - } -} - -impl From for llm_provider::ToolDefinition { - fn from(value: latest::llm_provider::ToolDefinition) -> Self { - Self { - name: value.name, - description: value.description, - input_schema: value.input_schema, - } - } -} - -impl From for llm_provider::ToolChoice { - fn from(value: latest::llm_provider::ToolChoice) -> Self { - match value { - latest::llm_provider::ToolChoice::Auto => Self::Auto, - latest::llm_provider::ToolChoice::Any => Self::Any, - latest::llm_provider::ToolChoice::None => Self::None, - } - } -} - -// ============================================================================= -// Command Type Conversions (v0.7.0 -> latest/v0.8.0) -// ============================================================================= - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -// ============================================================================= -// LSP Type Conversions (latest/v0.8.0 -> v0.7.0) -// ============================================================================= - -impl From for lsp::Completion { - fn from(value: latest::lsp::Completion) -> Self { - Self { - label: value.label, - label_details: value.label_details.map(Into::into), - detail: value.detail, - kind: value.kind.map(Into::into), - insert_text_format: value.insert_text_format.map(Into::into), - } - } -} - -impl From for lsp::CompletionLabelDetails { - fn from(value: latest::lsp::CompletionLabelDetails) -> Self { - Self { - detail: value.detail, - description: value.description, - } - } -} - -impl From for lsp::CompletionKind { - fn from(value: latest::lsp::CompletionKind) -> Self { - match value { - latest::lsp::CompletionKind::Text => Self::Text, - latest::lsp::CompletionKind::Method => Self::Method, - latest::lsp::CompletionKind::Function => Self::Function, - latest::lsp::CompletionKind::Constructor => Self::Constructor, - latest::lsp::CompletionKind::Field => Self::Field, - latest::lsp::CompletionKind::Variable => Self::Variable, - latest::lsp::CompletionKind::Class => Self::Class, - latest::lsp::CompletionKind::Interface => Self::Interface, - latest::lsp::CompletionKind::Module => Self::Module, - latest::lsp::CompletionKind::Property => Self::Property, - latest::lsp::CompletionKind::Unit => Self::Unit, - latest::lsp::CompletionKind::Value => Self::Value, - latest::lsp::CompletionKind::Enum => Self::Enum, - latest::lsp::CompletionKind::Keyword => Self::Keyword, - latest::lsp::CompletionKind::Snippet => Self::Snippet, - latest::lsp::CompletionKind::Color => Self::Color, - latest::lsp::CompletionKind::File => Self::File, - latest::lsp::CompletionKind::Reference => Self::Reference, - latest::lsp::CompletionKind::Folder => Self::Folder, - latest::lsp::CompletionKind::EnumMember => Self::EnumMember, - latest::lsp::CompletionKind::Constant => Self::Constant, - latest::lsp::CompletionKind::Struct => Self::Struct, - latest::lsp::CompletionKind::Event => Self::Event, - latest::lsp::CompletionKind::Operator => Self::Operator, - latest::lsp::CompletionKind::TypeParameter => Self::TypeParameter, - latest::lsp::CompletionKind::Other(n) => Self::Other(n), - } - } -} - -impl From for lsp::InsertTextFormat { - fn from(value: latest::lsp::InsertTextFormat) -> Self { - match value { - latest::lsp::InsertTextFormat::PlainText => Self::PlainText, - latest::lsp::InsertTextFormat::Snippet => Self::Snippet, - latest::lsp::InsertTextFormat::Other(n) => Self::Other(n), - } - } -} - -impl From for lsp::Symbol { - fn from(value: latest::lsp::Symbol) -> Self { - Self { - kind: value.kind.into(), - name: value.name, - } - } -} - -impl From for lsp::SymbolKind { - fn from(value: latest::lsp::SymbolKind) -> Self { - match value { - latest::lsp::SymbolKind::File => Self::File, - latest::lsp::SymbolKind::Module => Self::Module, - latest::lsp::SymbolKind::Namespace => Self::Namespace, - latest::lsp::SymbolKind::Package => Self::Package, - latest::lsp::SymbolKind::Class => Self::Class, - latest::lsp::SymbolKind::Method => Self::Method, - latest::lsp::SymbolKind::Property => Self::Property, - latest::lsp::SymbolKind::Field => Self::Field, - latest::lsp::SymbolKind::Constructor => Self::Constructor, - latest::lsp::SymbolKind::Enum => Self::Enum, - latest::lsp::SymbolKind::Interface => Self::Interface, - latest::lsp::SymbolKind::Function => Self::Function, - latest::lsp::SymbolKind::Variable => Self::Variable, - latest::lsp::SymbolKind::Constant => Self::Constant, - latest::lsp::SymbolKind::String => Self::String, - latest::lsp::SymbolKind::Number => Self::Number, - latest::lsp::SymbolKind::Boolean => Self::Boolean, - latest::lsp::SymbolKind::Array => Self::Array, - latest::lsp::SymbolKind::Object => Self::Object, - latest::lsp::SymbolKind::Key => Self::Key, - latest::lsp::SymbolKind::Null => Self::Null, - latest::lsp::SymbolKind::EnumMember => Self::EnumMember, - latest::lsp::SymbolKind::Struct => Self::Struct, - latest::lsp::SymbolKind::Event => Self::Event, - latest::lsp::SymbolKind::Operator => Self::Operator, - latest::lsp::SymbolKind::TypeParameter => Self::TypeParameter, - latest::lsp::SymbolKind::Other(n) => Self::Other(n), - } - } -} - -// ============================================================================= -// CodeLabel Type Conversions (v0.7.0 -> latest/v0.8.0) -// ============================================================================= - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(r) => Self::CodeRange(r.into()), - CodeLabelSpan::Literal(l) => Self::Literal(l.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -// ============================================================================= -// SlashCommand Type Conversions (latest/v0.8.0 -> v0.7.0) -// ============================================================================= - -impl From<&latest::SlashCommand> for slash_command::SlashCommand { - fn from(value: &latest::SlashCommand) -> Self { - Self { - name: value.name.clone(), - description: value.description.clone(), - tooltip_text: value.tooltip_text.clone(), - requires_argument: value.requires_argument, - } - } -} - -// ============================================================================= -// SlashCommand Type Conversions (v0.7.0 -> latest/v0.8.0) -// ============================================================================= - -impl From - for latest::SlashCommandArgumentCompletion -{ - fn from(value: slash_command::SlashCommandArgumentCompletion) -> Self { - Self { - label: value.label, - new_text: value.new_text, - run_command: value.run_command, - } - } -} - -impl From for latest::SlashCommandOutput { - fn from(value: slash_command::SlashCommandOutput) -> Self { - Self { - sections: value.sections.into_iter().map(Into::into).collect(), - text: value.text, - } - } -} - -impl From for latest::slash_command::SlashCommandOutputSection { - fn from(value: SlashCommandOutputSection) -> Self { - Self { - range: value.range.into(), - label: value.label, - } - } -} - -// ============================================================================= -// ContextServer Type Conversions (v0.7.0 -> latest/v0.8.0) -// ============================================================================= - -impl From - for latest::context_server::ContextServerConfiguration -{ - fn from(value: context_server::ContextServerConfiguration) -> Self { - Self { - installation_instructions: value.installation_instructions, - settings_schema: value.settings_schema, - default_settings: value.default_settings, - } - } -} - -// ============================================================================= -// DAP Type Conversions (v0.7.0 -> latest/v0.8.0) -// ============================================================================= - -impl From for latest::dap::DebugAdapterBinary { - fn from(value: dap::DebugAdapterBinary) -> Self { - Self { - command: value.command, - arguments: value.arguments, - envs: value.envs, - cwd: value.cwd, - connection: value.connection.map(|c| latest::dap::TcpArguments { - host: c.host, - port: c.port, - timeout: c.timeout, - }), - request_args: latest::dap::StartDebuggingRequestArguments { - configuration: value.request_args.configuration, - request: match value.request_args.request { - dap::StartDebuggingRequestArgumentsRequest::Launch => { - latest::dap::StartDebuggingRequestArgumentsRequest::Launch - } - dap::StartDebuggingRequestArgumentsRequest::Attach => { - latest::dap::StartDebuggingRequestArgumentsRequest::Attach - } - }, - }, - } - } -} - -impl From - for latest::dap::StartDebuggingRequestArgumentsRequest -{ - fn from(value: dap::StartDebuggingRequestArgumentsRequest) -> Self { - match value { - dap::StartDebuggingRequestArgumentsRequest::Launch => Self::Launch, - dap::StartDebuggingRequestArgumentsRequest::Attach => Self::Attach, - } - } -} - -impl From for latest::dap::DebugScenario { - fn from(value: dap::DebugScenario) -> Self { - Self { - adapter: value.adapter, - label: value.label, - build: value.build.map(|b| match b { - dap::BuildTaskDefinition::ByName(name) => { - latest::dap::BuildTaskDefinition::ByName(name) - } - dap::BuildTaskDefinition::Template(t) => { - latest::dap::BuildTaskDefinition::Template( - latest::dap::BuildTaskDefinitionTemplatePayload { - locator_name: t.locator_name, - template: latest::dap::BuildTaskTemplate { - label: t.template.label, - command: t.template.command, - args: t.template.args, - env: t.template.env, - cwd: t.template.cwd, - }, - }, - ) - } - }), - config: value.config, - tcp_connection: value - .tcp_connection - .map(|t| latest::dap::TcpArgumentsTemplate { - host: t.host, - port: t.port, - timeout: t.timeout, - }), - } - } -} - -impl From for latest::dap::DebugRequest { - fn from(value: dap::DebugRequest) -> Self { - match value { - dap::DebugRequest::Attach(a) => Self::Attach(latest::dap::AttachRequest { - process_id: a.process_id, - }), - dap::DebugRequest::Launch(l) => Self::Launch(latest::dap::LaunchRequest { - program: l.program, - cwd: l.cwd, - args: l.args, - envs: l.envs, - }), - } - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 714caa05ff1301..b469349fb8c8a8 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -1110,12 +1110,10 @@ impl ExtensionImports for WasmState { .with_context(|| format!("setting permissions for path {path:?}")) .to_wasmtime_result() } +} - // ========================================================================= - // LLM Provider Import Implementations - // ========================================================================= - - async fn llm_request_credential( +impl llm_provider::Host for WasmState { + async fn request_credential( &mut self, _provider_id: String, _credential_type: llm_provider::CredentialType, @@ -1123,16 +1121,13 @@ impl ExtensionImports for WasmState { _placeholder: String, ) -> wasmtime::Result> { // For now, credential requests return false (not provided) - // Extensions should use llm_get_env_var to check for env vars first, - // then llm_store_credential/llm_get_credential for manual storage + // Extensions should use get_env_var to check for env vars first, + // then store_credential/get_credential for manual storage // Full UI credential prompting will be added in a future phase Ok(Ok(false)) } - async fn llm_get_credential( - &mut self, - provider_id: String, - ) -> wasmtime::Result> { + async fn get_credential(&mut self, provider_id: String) -> wasmtime::Result> { let extension_id = self.manifest.id.clone(); let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id); @@ -1151,7 +1146,7 @@ impl ExtensionImports for WasmState { .await } - async fn llm_store_credential( + async fn store_credential( &mut self, provider_id: String, value: String, @@ -1173,7 +1168,7 @@ impl ExtensionImports for WasmState { .to_wasmtime_result() } - async fn llm_delete_credential( + async fn delete_credential( &mut self, provider_id: String, ) -> wasmtime::Result> { @@ -1194,7 +1189,7 @@ impl ExtensionImports for WasmState { .to_wasmtime_result() } - async fn llm_get_env_var(&mut self, name: String) -> wasmtime::Result> { + async fn get_env_var(&mut self, name: String) -> wasmtime::Result> { let extension_id = self.manifest.id.clone(); // Find which provider (if any) declares this env var in its auth config @@ -1247,7 +1242,7 @@ impl ExtensionImports for WasmState { Ok(env::var(&name).ok()) } - async fn llm_oauth_start_web_auth( + async fn oauth_start_web_auth( &mut self, config: llm_provider::OauthWebAuthConfig, ) -> wasmtime::Result> { @@ -1345,7 +1340,7 @@ impl ExtensionImports for WasmState { .to_wasmtime_result() } - async fn llm_oauth_http_request( + async fn send_oauth_http_request( &mut self, request: llm_provider::OauthHttpRequest, ) -> wasmtime::Result> { @@ -1416,10 +1411,7 @@ impl ExtensionImports for WasmState { .to_wasmtime_result() } - async fn llm_oauth_open_browser( - &mut self, - url: String, - ) -> wasmtime::Result> { + async fn oauth_open_browser(&mut self, url: String) -> wasmtime::Result> { self.on_main_thread(move |cx| { async move { cx.update(|cx| { @@ -1433,9 +1425,3 @@ impl ExtensionImports for WasmState { .to_wasmtime_result() } } - -// ============================================================================= -// LLM Provider Host Implementations -// ============================================================================= - -impl llm_provider::Host for WasmState {} From 2031ca17e507e8a361e620525d7ba1da4fa48778 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 11:06:12 -0500 Subject: [PATCH 24/45] Revert auto-install extensions for now --- assets/settings/default.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index ce498dd6172c5a..f687778d7bd7fc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1721,12 +1721,7 @@ // If you don't want any of these extensions, add this field to your settings // and change the value to `false`. "auto_install_extensions": { - "anthropic": true, - "copilot_chat": true, - "google-ai": true, - "html": true, - "openai": true, - "open_router": true + "html": true }, // The capabilities granted to extensions. // From 21de6d35dd900015dd6f740b07ebcecda10b9611 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 11:13:22 -0500 Subject: [PATCH 25/45] Revert "Revert auto-install extensions for now" This reverts commit 2031ca17e507e8a361e620525d7ba1da4fa48778. --- assets/settings/default.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f687778d7bd7fc..ce498dd6172c5a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1721,7 +1721,12 @@ // If you don't want any of these extensions, add this field to your settings // and change the value to `false`. "auto_install_extensions": { - "html": true + "anthropic": true, + "copilot_chat": true, + "google-ai": true, + "html": true, + "openai": true, + "open_router": true }, // The capabilities granted to extensions. // From ccd6672d1abb37af65fa43c0464c537f22800880 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 11:13:29 -0500 Subject: [PATCH 26/45] Revert "Remove builtin extensions for now" This reverts commit 5559726fd7cf9d0cd28fc76ca0f3b869939b1fb8. --- extensions/anthropic/Cargo.lock | 823 ++++++++++++++++++ extensions/anthropic/Cargo.toml | 17 + extensions/anthropic/extension.toml | 13 + extensions/anthropic/icons/anthropic.svg | 11 + extensions/anthropic/src/anthropic.rs | 803 ++++++++++++++++++ extensions/copilot_chat/Cargo.lock | 823 ++++++++++++++++++ extensions/copilot_chat/Cargo.toml | 17 + extensions/copilot_chat/extension.toml | 13 + extensions/copilot_chat/icons/copilot.svg | 9 + extensions/copilot_chat/src/copilot_chat.rs | 696 +++++++++++++++ extensions/google-ai/Cargo.lock | 823 ++++++++++++++++++ extensions/google-ai/Cargo.toml | 17 + extensions/google-ai/extension.toml | 13 + extensions/google-ai/icons/google-ai.svg | 3 + extensions/google-ai/src/google_ai.rs | 840 +++++++++++++++++++ extensions/open_router/Cargo.lock | 823 ++++++++++++++++++ extensions/open_router/Cargo.toml | 17 + extensions/open_router/extension.toml | 13 + extensions/open_router/icons/open-router.svg | 8 + extensions/open_router/src/open_router.rs | 830 ++++++++++++++++++ extensions/openai/Cargo.lock | 823 ++++++++++++++++++ extensions/openai/Cargo.toml | 17 + extensions/openai/extension.toml | 13 + extensions/openai/icons/openai.svg | 3 + extensions/openai/src/openai.rs | 727 ++++++++++++++++ 25 files changed, 8195 insertions(+) create mode 100644 extensions/anthropic/Cargo.lock create mode 100644 extensions/anthropic/Cargo.toml create mode 100644 extensions/anthropic/extension.toml create mode 100644 extensions/anthropic/icons/anthropic.svg create mode 100644 extensions/anthropic/src/anthropic.rs create mode 100644 extensions/copilot_chat/Cargo.lock create mode 100644 extensions/copilot_chat/Cargo.toml create mode 100644 extensions/copilot_chat/extension.toml create mode 100644 extensions/copilot_chat/icons/copilot.svg create mode 100644 extensions/copilot_chat/src/copilot_chat.rs create mode 100644 extensions/google-ai/Cargo.lock create mode 100644 extensions/google-ai/Cargo.toml create mode 100644 extensions/google-ai/extension.toml create mode 100644 extensions/google-ai/icons/google-ai.svg create mode 100644 extensions/google-ai/src/google_ai.rs create mode 100644 extensions/open_router/Cargo.lock create mode 100644 extensions/open_router/Cargo.toml create mode 100644 extensions/open_router/extension.toml create mode 100644 extensions/open_router/icons/open-router.svg create mode 100644 extensions/open_router/src/open_router.rs create mode 100644 extensions/openai/Cargo.lock create mode 100644 extensions/openai/Cargo.toml create mode 100644 extensions/openai/extension.toml create mode 100644 extensions/openai/icons/openai.svg create mode 100644 extensions/openai/src/openai.rs diff --git a/extensions/anthropic/Cargo.lock b/extensions/anthropic/Cargo.lock new file mode 100644 index 00000000000000..bd558d1ce1a11c --- /dev/null +++ b/extensions/anthropic/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fanthropic" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.7.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/anthropic/Cargo.toml b/extensions/anthropic/Cargo.toml new file mode 100644 index 00000000000000..25dfe72b0e92ca --- /dev/null +++ b/extensions/anthropic/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "anthropic" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/anthropic.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/extensions/anthropic/extension.toml b/extensions/anthropic/extension.toml new file mode 100644 index 00000000000000..c37b8aca34f6cf --- /dev/null +++ b/extensions/anthropic/extension.toml @@ -0,0 +1,13 @@ +id = "anthropic" +name = "Anthropic" +description = "Anthropic Claude LLM provider for Zed." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.anthropic] +name = "Anthropic" + +[language_model_providers.anthropic.auth] +env_var = "ANTHROPIC_API_KEY" \ No newline at end of file diff --git a/extensions/anthropic/icons/anthropic.svg b/extensions/anthropic/icons/anthropic.svg new file mode 100644 index 00000000000000..75c1a7e0014e7d --- /dev/null +++ b/extensions/anthropic/icons/anthropic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/extensions/anthropic/src/anthropic.rs b/extensions/anthropic/src/anthropic.rs new file mode 100644 index 00000000000000..26d364cf90acbc --- /dev/null +++ b/extensions/anthropic/src/anthropic.rs @@ -0,0 +1,803 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +struct AnthropicProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + current_tool_use: Option, + stop_reason: Option, + pending_signature: Option, +} + +struct ToolUseState { + id: String, + name: String, + input_json: String, +} + +struct ModelDefinition { + real_id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: u64, + supports_images: bool, + supports_thinking: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + ModelDefinition { + real_id: "claude-opus-4-5-20251101", + display_name: "Claude Opus 4.5", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-opus-4-5-20251101", + display_name: "Claude Opus 4.5 Thinking", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-sonnet-4-5-20250929", + display_name: "Claude Sonnet 4.5", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-sonnet-4-5-20250929", + display_name: "Claude Sonnet 4.5 Thinking", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-sonnet-4-20250514", + display_name: "Claude Sonnet 4", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-sonnet-4-20250514", + display_name: "Claude Sonnet 4 Thinking", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-haiku-4-5-20251001", + display_name: "Claude Haiku 4.5", + max_tokens: 200_000, + max_output_tokens: 64_000, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + real_id: "claude-haiku-4-5-20251001", + display_name: "Claude Haiku 4.5 Thinking", + max_tokens: 200_000, + max_output_tokens: 64_000, + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-3-5-sonnet-latest", + display_name: "Claude 3.5 Sonnet", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "claude-3-5-haiku-latest", + display_name: "Claude 3.5 Haiku", + max_tokens: 200_000, + max_output_tokens: 8_192, + supports_images: true, + supports_thinking: false, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_model_definition(display_name: &str) -> Option<&'static ModelDefinition> { + MODELS.iter().find(|m| m.display_name == display_name) +} + +// Anthropic API Request Types + +#[derive(Serialize)] +struct AnthropicRequest { + model: String, + max_tokens: u64, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + system: Option, + #[serde(skip_serializing_if = "Option::is_none")] + thinking: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop_sequences: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, +} + +#[derive(Serialize)] +struct AnthropicThinking { + #[serde(rename = "type")] + thinking_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + budget_tokens: Option, +} + +#[derive(Serialize)] +struct AnthropicMessage { + role: String, + content: Vec, +} + +#[derive(Serialize, Clone)] +#[serde(tag = "type")] +enum AnthropicContent { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "thinking")] + Thinking { thinking: String, signature: String }, + #[serde(rename = "redacted_thinking")] + RedactedThinking { data: String }, + #[serde(rename = "image")] + Image { source: AnthropicImageSource }, + #[serde(rename = "tool_use")] + ToolUse { + id: String, + name: String, + input: serde_json::Value, + }, + #[serde(rename = "tool_result")] + ToolResult { + tool_use_id: String, + is_error: bool, + content: String, + }, +} + +#[derive(Serialize, Clone)] +struct AnthropicImageSource { + #[serde(rename = "type")] + source_type: String, + media_type: String, + data: String, +} + +#[derive(Serialize)] +struct AnthropicTool { + name: String, + description: String, + input_schema: serde_json::Value, +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +enum AnthropicToolChoice { + Auto, + Any, + None, +} + +// Anthropic API Response Types + +#[derive(Deserialize, Debug)] +#[serde(tag = "type")] +#[allow(dead_code)] +enum AnthropicEvent { + #[serde(rename = "message_start")] + MessageStart { message: AnthropicMessageResponse }, + #[serde(rename = "content_block_start")] + ContentBlockStart { + index: usize, + content_block: AnthropicContentBlock, + }, + #[serde(rename = "content_block_delta")] + ContentBlockDelta { index: usize, delta: AnthropicDelta }, + #[serde(rename = "content_block_stop")] + ContentBlockStop { index: usize }, + #[serde(rename = "message_delta")] + MessageDelta { + delta: AnthropicMessageDelta, + usage: AnthropicUsage, + }, + #[serde(rename = "message_stop")] + MessageStop, + #[serde(rename = "ping")] + Ping, + #[serde(rename = "error")] + Error { error: AnthropicApiError }, +} + +#[derive(Deserialize, Debug)] +struct AnthropicMessageResponse { + #[allow(dead_code)] + id: String, + #[allow(dead_code)] + role: String, + #[serde(default)] + usage: AnthropicUsage, +} + +#[derive(Deserialize, Debug)] +#[serde(tag = "type")] +enum AnthropicContentBlock { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "thinking")] + Thinking { thinking: String }, + #[serde(rename = "redacted_thinking")] + RedactedThinking { data: String }, + #[serde(rename = "tool_use")] + ToolUse { id: String, name: String }, +} + +#[derive(Deserialize, Debug)] +#[serde(tag = "type")] +enum AnthropicDelta { + #[serde(rename = "text_delta")] + TextDelta { text: String }, + #[serde(rename = "thinking_delta")] + ThinkingDelta { thinking: String }, + #[serde(rename = "signature_delta")] + SignatureDelta { signature: String }, + #[serde(rename = "input_json_delta")] + InputJsonDelta { partial_json: String }, +} + +#[derive(Deserialize, Debug)] +struct AnthropicMessageDelta { + stop_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct AnthropicUsage { + #[serde(default)] + input_tokens: Option, + #[serde(default)] + output_tokens: Option, + #[serde(default)] + cache_creation_input_tokens: Option, + #[serde(default)] + cache_read_input_tokens: Option, +} + +#[derive(Deserialize, Debug)] +struct AnthropicApiError { + #[serde(rename = "type")] + #[allow(dead_code)] + error_type: String, + message: String, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let model_def = + get_model_definition(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; + + let mut messages: Vec = Vec::new(); + let mut system_message = String::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + for content in &msg.content { + if let LlmMessageContent::Text(text) = content { + if !system_message.is_empty() { + system_message.push('\n'); + } + system_message.push_str(text); + } + } + } + LlmMessageRole::User => { + let mut contents: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + contents.push(AnthropicContent::Text { text: text.clone() }); + } + } + LlmMessageContent::Image(img) => { + contents.push(AnthropicContent::Image { + source: AnthropicImageSource { + source_type: "base64".to_string(), + media_type: "image/png".to_string(), + data: img.source.clone(), + }, + }); + } + LlmMessageContent::ToolResult(result) => { + let content_text = match &result.content { + LlmToolResultContent::Text(t) => t.clone(), + LlmToolResultContent::Image(_) => "[Image]".to_string(), + }; + contents.push(AnthropicContent::ToolResult { + tool_use_id: result.tool_use_id.clone(), + is_error: result.is_error, + content: content_text, + }); + } + _ => {} + } + } + + if !contents.is_empty() { + messages.push(AnthropicMessage { + role: "user".to_string(), + content: contents, + }); + } + } + LlmMessageRole::Assistant => { + let mut contents: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + contents.push(AnthropicContent::Text { text: text.clone() }); + } + } + LlmMessageContent::ToolUse(tool_use) => { + let input: serde_json::Value = + serde_json::from_str(&tool_use.input).unwrap_or_default(); + contents.push(AnthropicContent::ToolUse { + id: tool_use.id.clone(), + name: tool_use.name.clone(), + input, + }); + } + LlmMessageContent::Thinking(thinking) => { + if !thinking.text.is_empty() { + contents.push(AnthropicContent::Thinking { + thinking: thinking.text.clone(), + signature: thinking.signature.clone().unwrap_or_default(), + }); + } + } + LlmMessageContent::RedactedThinking(data) => { + if !data.is_empty() { + contents.push(AnthropicContent::RedactedThinking { + data: data.clone(), + }); + } + } + _ => {} + } + } + + if !contents.is_empty() { + messages.push(AnthropicMessage { + role: "assistant".to_string(), + content: contents, + }); + } + } + } + } + + let tools: Vec = request + .tools + .iter() + .map(|t| AnthropicTool { + name: t.name.clone(), + description: t.description.clone(), + input_schema: serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())), + }) + .collect(); + + let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { + LlmToolChoice::Auto => AnthropicToolChoice::Auto, + LlmToolChoice::Any => AnthropicToolChoice::Any, + LlmToolChoice::None => AnthropicToolChoice::None, + }); + + let thinking = if model_def.supports_thinking && request.thinking_allowed { + Some(AnthropicThinking { + thinking_type: "enabled".to_string(), + budget_tokens: Some(4096), + }) + } else { + None + }; + + Ok(AnthropicRequest { + model: model_def.real_id.to_string(), + max_tokens: model_def.max_output_tokens, + messages, + system: if system_message.is_empty() { + None + } else { + Some(system_message) + }, + thinking, + tools, + tool_choice, + stop_sequences: request.stop_sequences.clone(), + temperature: request.temperature, + stream: true, + }) +} + +fn parse_sse_line(line: &str) -> Option { + let data = line.strip_prefix("data: ")?; + serde_json::from_str(data).ok() +} + +impl zed::Extension for AnthropicProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "anthropic".into(), + name: "Anthropic".into(), + icon: Some("icons/anthropic.svg".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.display_name.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: Some(m.max_output_tokens), + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: m.supports_thinking, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("anthropic").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# Anthropic Setup + +Welcome to **Anthropic**! This extension provides access to Claude models. + +## Configuration + +Enter your Anthropic API key below. You can get your API key at [console.anthropic.com](https://console.anthropic.com/). + +## Available Models + +| Display Name | Real Model | Context | Output | +|--------------|------------|---------|--------| +| Claude Opus 4.5 | claude-opus-4-5 | 200K | 8K | +| Claude Opus 4.5 Thinking | claude-opus-4-5 | 200K | 8K | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | 200K | 8K | +| Claude Sonnet 4.5 Thinking | claude-sonnet-4-5 | 200K | 8K | +| Claude Sonnet 4 | claude-sonnet-4 | 200K | 8K | +| Claude Sonnet 4 Thinking | claude-sonnet-4 | 200K | 8K | +| Claude Haiku 4.5 | claude-haiku-4-5 | 200K | 64K | +| Claude Haiku 4.5 Thinking | claude-haiku-4-5 | 200K | 64K | +| Claude 3.5 Sonnet | claude-3-5-sonnet | 200K | 8K | +| Claude 3.5 Haiku | claude-3-5-haiku | 200K | 8K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling +- ✅ Vision (image inputs) +- ✅ Extended thinking support +- ✅ All Claude models + +## Pricing + +Uses your Anthropic API credits. See [Anthropic pricing](https://www.anthropic.com/pricing) for details. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "anthropic", + LlmCredentialType::ApiKey, + "Anthropic API Key", + "sk-ant-...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("anthropic") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("anthropic").ok_or_else(|| { + "No API key configured. Please add your Anthropic API key in settings.".to_string() + })?; + + let anthropic_request = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&anthropic_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let http_request = HttpRequest { + method: HttpMethod::Post, + url: "https://api.anthropic.com/v1/messages".to_string(), + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("x-api-key".to_string(), api_key), + ("anthropic-version".to_string(), "2023-06-01".to_string()), + ], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("anthropic-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + current_tool_use: None, + stop_reason: None, + pending_signature: None, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if line.trim().is_empty() || line.starts_with("event:") { + continue; + } + + if let Some(event) = parse_sse_line(&line) { + match event { + AnthropicEvent::MessageStart { message } => { + if let (Some(input), Some(output)) = + (message.usage.input_tokens, message.usage.output_tokens) + { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: input, + output_tokens: output, + cache_creation_input_tokens: message + .usage + .cache_creation_input_tokens, + cache_read_input_tokens: message.usage.cache_read_input_tokens, + }))); + } + } + AnthropicEvent::ContentBlockStart { content_block, .. } => { + match content_block { + AnthropicContentBlock::Text { text } => { + if !text.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(text))); + } + } + AnthropicContentBlock::Thinking { thinking } => { + return Ok(Some(LlmCompletionEvent::Thinking( + LlmThinkingContent { + text: thinking, + signature: None, + }, + ))); + } + AnthropicContentBlock::RedactedThinking { data } => { + return Ok(Some(LlmCompletionEvent::RedactedThinking(data))); + } + AnthropicContentBlock::ToolUse { id, name } => { + state.current_tool_use = Some(ToolUseState { + id, + name, + input_json: String::new(), + }); + } + } + } + AnthropicEvent::ContentBlockDelta { delta, .. } => match delta { + AnthropicDelta::TextDelta { text } => { + if !text.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(text))); + } + } + AnthropicDelta::ThinkingDelta { thinking } => { + return Ok(Some(LlmCompletionEvent::Thinking( + LlmThinkingContent { + text: thinking, + signature: None, + }, + ))); + } + AnthropicDelta::SignatureDelta { signature } => { + state.pending_signature = Some(signature.clone()); + return Ok(Some(LlmCompletionEvent::Thinking( + LlmThinkingContent { + text: String::new(), + signature: Some(signature), + }, + ))); + } + AnthropicDelta::InputJsonDelta { partial_json } => { + if let Some(ref mut tool_use) = state.current_tool_use { + tool_use.input_json.push_str(&partial_json); + } + } + }, + AnthropicEvent::ContentBlockStop { .. } => { + if let Some(tool_use) = state.current_tool_use.take() { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tool_use.id, + name: tool_use.name, + input: tool_use.input_json, + thought_signature: state.pending_signature.take(), + }))); + } + } + AnthropicEvent::MessageDelta { delta, usage } => { + if let Some(reason) = delta.stop_reason { + state.stop_reason = Some(match reason.as_str() { + "end_turn" => LlmStopReason::EndTurn, + "max_tokens" => LlmStopReason::MaxTokens, + "tool_use" => LlmStopReason::ToolUse, + _ => LlmStopReason::EndTurn, + }); + } + if let Some(output) = usage.output_tokens { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.input_tokens.unwrap_or(0), + output_tokens: output, + cache_creation_input_tokens: usage.cache_creation_input_tokens, + cache_read_input_tokens: usage.cache_read_input_tokens, + }))); + } + } + AnthropicEvent::MessageStop => { + if let Some(stop_reason) = state.stop_reason.take() { + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + return Ok(Some(LlmCompletionEvent::Stop(LlmStopReason::EndTurn))); + } + AnthropicEvent::Ping => {} + AnthropicEvent::Error { error } => { + return Err(format!("API error: {}", error.message)); + } + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + if let Some(stop_reason) = state.stop_reason.take() { + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + return Ok(None); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(AnthropicProvider); diff --git a/extensions/copilot_chat/Cargo.lock b/extensions/copilot_chat/Cargo.lock new file mode 100644 index 00000000000000..1ba4a97d7291c7 --- /dev/null +++ b/extensions/copilot_chat/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "copilot_chat" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.8.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/copilot_chat/Cargo.toml b/extensions/copilot_chat/Cargo.toml new file mode 100644 index 00000000000000..6daae73229776d --- /dev/null +++ b/extensions/copilot_chat/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "copilot_chat" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/copilot_chat.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/extensions/copilot_chat/extension.toml b/extensions/copilot_chat/extension.toml new file mode 100644 index 00000000000000..9afa188462fcb6 --- /dev/null +++ b/extensions/copilot_chat/extension.toml @@ -0,0 +1,13 @@ +id = "copilot_chat" +name = "Copilot Chat" +description = "GitHub Copilot Chat LLM provider for Zed." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.copilot_chat] +name = "Copilot Chat" + +[language_model_providers.copilot_chat.auth] +env_var = "GH_COPILOT_TOKEN" \ No newline at end of file diff --git a/extensions/copilot_chat/icons/copilot.svg b/extensions/copilot_chat/icons/copilot.svg new file mode 100644 index 00000000000000..2584cd631006c1 --- /dev/null +++ b/extensions/copilot_chat/icons/copilot.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extensions/copilot_chat/src/copilot_chat.rs b/extensions/copilot_chat/src/copilot_chat.rs new file mode 100644 index 00000000000000..2c64d430689f94 --- /dev/null +++ b/extensions/copilot_chat/src/copilot_chat.rs @@ -0,0 +1,696 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +struct CopilotChatProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + tool_calls_emitted: bool, +} + +#[derive(Clone, Default)] +struct AccumulatedToolCall { + id: String, + name: String, + arguments: String, +} + +struct ModelDefinition { + id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + supports_images: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + ModelDefinition { + id: "gpt-4o", + display_name: "GPT-4o", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + id: "gpt-4o-mini", + display_name: "GPT-4o Mini", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + id: "gpt-4.1", + display_name: "GPT-4.1", + max_tokens: 1_000_000, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "o1", + display_name: "o1", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "o3-mini", + display_name: "o3-mini", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "claude-3.5-sonnet", + display_name: "Claude 3.5 Sonnet", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "claude-3.7-sonnet", + display_name: "Claude 3.7 Sonnet", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "gemini-2.0-flash-001", + display_name: "Gemini 2.0 Flash", + max_tokens: 1_000_000, + max_output_tokens: Some(8_192), + supports_images: true, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> { + MODELS.iter().find(|m| m.id == model_id) +} + +#[derive(Serialize)] +struct OpenAiRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + stream_options: Option, +} + +#[derive(Serialize)] +struct StreamOptions { + include_usage: bool, +} + +#[derive(Serialize)] +struct OpenAiMessage { + role: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, +} + +#[derive(Serialize, Clone)] +#[serde(untagged)] +enum OpenAiContent { + Text(String), + Parts(Vec), +} + +#[derive(Serialize, Clone)] +#[serde(tag = "type")] +enum OpenAiContentPart { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image_url")] + ImageUrl { image_url: ImageUrl }, +} + +#[derive(Serialize, Clone)] +struct ImageUrl { + url: String, +} + +#[derive(Serialize, Clone)] +struct OpenAiToolCall { + id: String, + #[serde(rename = "type")] + call_type: String, + function: OpenAiFunctionCall, +} + +#[derive(Serialize, Clone)] +struct OpenAiFunctionCall { + name: String, + arguments: String, +} + +#[derive(Serialize)] +struct OpenAiTool { + #[serde(rename = "type")] + tool_type: String, + function: OpenAiFunctionDef, +} + +#[derive(Serialize)] +struct OpenAiFunctionDef { + name: String, + description: String, + parameters: serde_json::Value, +} + +#[derive(Deserialize, Debug)] +struct OpenAiStreamResponse { + choices: Vec, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiStreamChoice { + delta: OpenAiDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenAiToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiUsage { + prompt_tokens: u64, + completion_tokens: u64, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let mut messages: Vec = Vec::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + let mut text_content = String::new(); + for content in &msg.content { + if let LlmMessageContent::Text(text) = content { + if !text_content.is_empty() { + text_content.push('\n'); + } + text_content.push_str(text); + } + } + if !text_content.is_empty() { + messages.push(OpenAiMessage { + role: "system".to_string(), + content: Some(OpenAiContent::Text(text_content)), + tool_calls: None, + tool_call_id: None, + }); + } + } + LlmMessageRole::User => { + let mut parts: Vec = Vec::new(); + let mut tool_result_messages: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + parts.push(OpenAiContentPart::Text { text: text.clone() }); + } + } + LlmMessageContent::Image(img) => { + let data_url = format!("data:image/png;base64,{}", img.source); + parts.push(OpenAiContentPart::ImageUrl { + image_url: ImageUrl { url: data_url }, + }); + } + LlmMessageContent::ToolResult(result) => { + let content_text = match &result.content { + LlmToolResultContent::Text(t) => t.clone(), + LlmToolResultContent::Image(_) => "[Image]".to_string(), + }; + tool_result_messages.push(OpenAiMessage { + role: "tool".to_string(), + content: Some(OpenAiContent::Text(content_text)), + tool_calls: None, + tool_call_id: Some(result.tool_use_id.clone()), + }); + } + _ => {} + } + } + + if !parts.is_empty() { + let content = if parts.len() == 1 { + if let OpenAiContentPart::Text { text } = &parts[0] { + OpenAiContent::Text(text.clone()) + } else { + OpenAiContent::Parts(parts) + } + } else { + OpenAiContent::Parts(parts) + }; + + messages.push(OpenAiMessage { + role: "user".to_string(), + content: Some(content), + tool_calls: None, + tool_call_id: None, + }); + } + + messages.extend(tool_result_messages); + } + LlmMessageRole::Assistant => { + let mut text_content = String::new(); + let mut tool_calls: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + if !text_content.is_empty() { + text_content.push('\n'); + } + text_content.push_str(text); + } + } + LlmMessageContent::ToolUse(tool_use) => { + tool_calls.push(OpenAiToolCall { + id: tool_use.id.clone(), + call_type: "function".to_string(), + function: OpenAiFunctionCall { + name: tool_use.name.clone(), + arguments: tool_use.input.clone(), + }, + }); + } + _ => {} + } + } + + messages.push(OpenAiMessage { + role: "assistant".to_string(), + content: if text_content.is_empty() { + None + } else { + Some(OpenAiContent::Text(text_content)) + }, + tool_calls: if tool_calls.is_empty() { + None + } else { + Some(tool_calls) + }, + tool_call_id: None, + }); + } + } + } + + let tools: Vec = request + .tools + .iter() + .map(|t| OpenAiTool { + tool_type: "function".to_string(), + function: OpenAiFunctionDef { + name: t.name.clone(), + description: t.description.clone(), + parameters: serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())), + }, + }) + .collect(); + + let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { + LlmToolChoice::Auto => "auto".to_string(), + LlmToolChoice::Any => "required".to_string(), + LlmToolChoice::None => "none".to_string(), + }); + + let model_def = get_model_definition(model_id); + let max_tokens = request + .max_tokens + .or(model_def.and_then(|m| m.max_output_tokens)); + + Ok(OpenAiRequest { + model: model_id.to_string(), + messages, + max_tokens, + tools, + tool_choice, + stop: request.stop_sequences.clone(), + temperature: request.temperature, + stream: true, + stream_options: Some(StreamOptions { + include_usage: true, + }), + }) +} + +fn parse_sse_line(line: &str) -> Option { + let data = line.strip_prefix("data: ")?; + if data.trim() == "[DONE]" { + return None; + } + serde_json::from_str(data).ok() +} + +impl zed::Extension for CopilotChatProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "copilot_chat".into(), + name: "Copilot Chat".into(), + icon: Some("icons/copilot.svg".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.id.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: m.max_output_tokens, + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("copilot_chat").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# Copilot Chat Setup + +Welcome to **Copilot Chat**! This extension provides access to GitHub Copilot's chat models. + +## Configuration + +Enter your GitHub Copilot token below. You need an active GitHub Copilot subscription. + +To get your token: +1. Ensure you have a GitHub Copilot subscription +2. Generate a token from your GitHub Copilot settings + +## Available Models + +| Model | Context | Output | +|-------|---------|--------| +| GPT-4o | 128K | 16K | +| GPT-4o Mini | 128K | 16K | +| GPT-4.1 | 1M | 32K | +| o1 | 200K | 100K | +| o3-mini | 200K | 100K | +| Claude 3.5 Sonnet | 200K | 8K | +| Claude 3.7 Sonnet | 200K | 8K | +| Gemini 2.0 Flash | 1M | 8K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling +- ✅ Vision (image inputs) +- ✅ Multiple model providers via Copilot + +## Note + +This extension requires an active GitHub Copilot subscription. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "copilot_chat", + LlmCredentialType::ApiKey, + "GitHub Copilot Token", + "ghu_...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("copilot_chat") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("copilot_chat").ok_or_else(|| { + "No token configured. Please add your GitHub Copilot token in settings.".to_string() + })?; + + let openai_request = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&openai_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let http_request = HttpRequest { + method: HttpMethod::Post, + url: "https://api.githubcopilot.com/chat/completions".to_string(), + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("Authorization".to_string(), format!("Bearer {}", api_key)), + ( + "Copilot-Integration-Id".to_string(), + "vscode-chat".to_string(), + ), + ("Editor-Version".to_string(), "Zed/1.0.0".to_string()), + ], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("copilot-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + tool_calls: HashMap::new(), + tool_calls_emitted: false, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if line.trim().is_empty() { + continue; + } + + if let Some(response) = parse_sse_line(&line) { + if let Some(choice) = response.choices.first() { + if let Some(content) = &choice.delta.content { + if !content.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(content.clone()))); + } + } + + if let Some(tool_calls) = &choice.delta.tool_calls { + for tc in tool_calls { + let entry = state + .tool_calls + .entry(tc.index) + .or_insert_with(AccumulatedToolCall::default); + + if let Some(id) = &tc.id { + entry.id = id.clone(); + } + if let Some(func) = &tc.function { + if let Some(name) = &func.name { + entry.name = name.clone(); + } + if let Some(args) = &func.arguments { + entry.arguments.push_str(args); + } + } + } + } + + if let Some(finish_reason) = &choice.finish_reason { + if !state.tool_calls.is_empty() && !state.tool_calls_emitted { + state.tool_calls_emitted = true; + let mut tool_calls: Vec<_> = state.tool_calls.drain().collect(); + tool_calls.sort_by_key(|(idx, _)| *idx); + + if let Some((_, tc)) = tool_calls.into_iter().next() { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tc.id, + name: tc.name, + input: tc.arguments, + thought_signature: None, + }))); + } + } + + let stop_reason = match finish_reason.as_str() { + "stop" => LlmStopReason::EndTurn, + "length" => LlmStopReason::MaxTokens, + "tool_calls" => LlmStopReason::ToolUse, + "content_filter" => LlmStopReason::Refusal, + _ => LlmStopReason::EndTurn, + }; + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + } + + if let Some(usage) = response.usage { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_tokens, + output_tokens: usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + return Ok(None); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(CopilotChatProvider); diff --git a/extensions/google-ai/Cargo.lock b/extensions/google-ai/Cargo.lock new file mode 100644 index 00000000000000..2389ff51da0c24 --- /dev/null +++ b/extensions/google-ai/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foogle" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.7.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/google-ai/Cargo.toml b/extensions/google-ai/Cargo.toml new file mode 100644 index 00000000000000..f6de35d4066938 --- /dev/null +++ b/extensions/google-ai/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "google-ai" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/google_ai.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/extensions/google-ai/extension.toml b/extensions/google-ai/extension.toml new file mode 100644 index 00000000000000..1b1cb382a7835d --- /dev/null +++ b/extensions/google-ai/extension.toml @@ -0,0 +1,13 @@ +id = "google-ai" +name = "Google AI" +description = "Google Gemini LLM provider for Zed." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.google-ai] +name = "Google AI" + +[language_model_providers.google-ai.auth] +env_var = "GEMINI_API_KEY" \ No newline at end of file diff --git a/extensions/google-ai/icons/google-ai.svg b/extensions/google-ai/icons/google-ai.svg new file mode 100644 index 00000000000000..bdde44ed247531 --- /dev/null +++ b/extensions/google-ai/icons/google-ai.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/google-ai/src/google_ai.rs b/extensions/google-ai/src/google_ai.rs new file mode 100644 index 00000000000000..61baca80b19d9d --- /dev/null +++ b/extensions/google-ai/src/google_ai.rs @@ -0,0 +1,840 @@ +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +static TOOL_CALL_COUNTER: AtomicU64 = AtomicU64::new(0); + +struct GoogleAiProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + stop_reason: Option, + wants_tool_use: bool, +} + +struct ModelDefinition { + real_id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + supports_images: bool, + supports_thinking: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + ModelDefinition { + real_id: "gemini-2.5-flash-lite", + display_name: "Gemini 2.5 Flash-Lite", + max_tokens: 1_048_576, + max_output_tokens: Some(65_536), + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + real_id: "gemini-2.5-flash", + display_name: "Gemini 2.5 Flash", + max_tokens: 1_048_576, + max_output_tokens: Some(65_536), + supports_images: true, + supports_thinking: true, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gemini-2.5-pro", + display_name: "Gemini 2.5 Pro", + max_tokens: 1_048_576, + max_output_tokens: Some(65_536), + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gemini-3-pro-preview", + display_name: "Gemini 3 Pro", + max_tokens: 1_048_576, + max_output_tokens: Some(65_536), + supports_images: true, + supports_thinking: true, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_real_model_id(display_name: &str) -> Option<&'static str> { + MODELS + .iter() + .find(|m| m.display_name == display_name) + .map(|m| m.real_id) +} + +fn get_model_supports_thinking(display_name: &str) -> bool { + MODELS + .iter() + .find(|m| m.display_name == display_name) + .map(|m| m.supports_thinking) + .unwrap_or(false) +} + +/// Adapts a JSON schema to be compatible with Google's API subset. +/// Google only supports a specific subset of JSON Schema fields. +/// See: https://ai.google.dev/api/caching#Schema +fn adapt_schema_for_google(json: &mut serde_json::Value) { + adapt_schema_for_google_impl(json, true); +} + +fn adapt_schema_for_google_impl(json: &mut serde_json::Value, is_schema: bool) { + if let serde_json::Value::Object(obj) = json { + // Google's Schema only supports these fields: + // type, format, title, description, nullable, enum, maxItems, minItems, + // properties, required, minProperties, maxProperties, minLength, maxLength, + // pattern, example, anyOf, propertyOrdering, default, items, minimum, maximum + const ALLOWED_KEYS: &[&str] = &[ + "type", + "format", + "title", + "description", + "nullable", + "enum", + "maxItems", + "minItems", + "properties", + "required", + "minProperties", + "maxProperties", + "minLength", + "maxLength", + "pattern", + "example", + "anyOf", + "propertyOrdering", + "default", + "items", + "minimum", + "maximum", + ]; + + // Convert oneOf to anyOf before filtering keys + if let Some(one_of) = obj.remove("oneOf") { + obj.insert("anyOf".to_string(), one_of); + } + + // If type is an array (e.g., ["string", "null"]), take just the first type + if let Some(type_field) = obj.get_mut("type") { + if let serde_json::Value::Array(types) = type_field { + if let Some(first_type) = types.first().cloned() { + *type_field = first_type; + } + } + } + + // Only filter keys if this is a schema object, not a properties map + if is_schema { + obj.retain(|key, _| ALLOWED_KEYS.contains(&key.as_str())); + } + + // Recursively process nested values + // "properties" contains a map of property names -> schemas + // "items" and "anyOf" contain schemas directly + for (key, value) in obj.iter_mut() { + if key == "properties" { + // properties is a map of property_name -> schema + if let serde_json::Value::Object(props) = value { + for (_, prop_schema) in props.iter_mut() { + adapt_schema_for_google_impl(prop_schema, true); + } + } + } else if key == "items" { + // items is a schema + adapt_schema_for_google_impl(value, true); + } else if key == "anyOf" { + // anyOf is an array of schemas + if let serde_json::Value::Array(arr) = value { + for item in arr.iter_mut() { + adapt_schema_for_google_impl(item, true); + } + } + } + } + } else if let serde_json::Value::Array(arr) = json { + for item in arr.iter_mut() { + adapt_schema_for_google_impl(item, true); + } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleRequest { + contents: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + system_instruction: Option, + #[serde(skip_serializing_if = "Option::is_none")] + generation_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_config: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleSystemInstruction { + parts: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleContent { + parts: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + role: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +enum GooglePart { + Text(GoogleTextPart), + InlineData(GoogleInlineDataPart), + FunctionCall(GoogleFunctionCallPart), + FunctionResponse(GoogleFunctionResponsePart), + Thought(GoogleThoughtPart), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleTextPart { + text: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleInlineDataPart { + inline_data: GoogleBlob, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleBlob { + mime_type: String, + data: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionCallPart { + function_call: GoogleFunctionCall, + #[serde(skip_serializing_if = "Option::is_none")] + thought_signature: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionCall { + name: String, + args: serde_json::Value, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionResponsePart { + function_response: GoogleFunctionResponse, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionResponse { + name: String, + response: serde_json::Value, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleThoughtPart { + thought: bool, + thought_signature: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleGenerationConfig { + #[serde(skip_serializing_if = "Option::is_none")] + candidate_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stop_sequences: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + max_output_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + thinking_config: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleThinkingConfig { + thinking_budget: u32, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleTool { + function_declarations: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionDeclaration { + name: String, + description: String, + parameters: serde_json::Value, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleToolConfig { + function_calling_config: GoogleFunctionCallingConfig, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleFunctionCallingConfig { + mode: String, + #[serde(skip_serializing_if = "Option::is_none")] + allowed_function_names: Option>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GoogleStreamResponse { + #[serde(default)] + candidates: Vec, + #[serde(default)] + usage_metadata: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GoogleCandidate { + #[serde(default)] + content: Option, + #[serde(default)] + finish_reason: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GoogleUsageMetadata { + #[serde(default)] + prompt_token_count: u64, + #[serde(default)] + candidates_token_count: u64, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result<(GoogleRequest, String), String> { + let real_model_id = + get_real_model_id(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; + + let supports_thinking = get_model_supports_thinking(model_id); + + let mut contents: Vec = Vec::new(); + let mut system_parts: Vec = Vec::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + for content in &msg.content { + if let LlmMessageContent::Text(text) = content { + if !text.is_empty() { + system_parts + .push(GooglePart::Text(GoogleTextPart { text: text.clone() })); + } + } + } + } + LlmMessageRole::User => { + let mut parts: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + parts.push(GooglePart::Text(GoogleTextPart { text: text.clone() })); + } + } + LlmMessageContent::Image(img) => { + parts.push(GooglePart::InlineData(GoogleInlineDataPart { + inline_data: GoogleBlob { + mime_type: "image/png".to_string(), + data: img.source.clone(), + }, + })); + } + LlmMessageContent::ToolResult(result) => { + let response_value = match &result.content { + LlmToolResultContent::Text(t) => { + serde_json::json!({ "output": t }) + } + LlmToolResultContent::Image(_) => { + serde_json::json!({ "output": "Tool responded with an image" }) + } + }; + parts.push(GooglePart::FunctionResponse(GoogleFunctionResponsePart { + function_response: GoogleFunctionResponse { + name: result.tool_name.clone(), + response: response_value, + }, + })); + } + _ => {} + } + } + + if !parts.is_empty() { + contents.push(GoogleContent { + parts, + role: Some("user".to_string()), + }); + } + } + LlmMessageRole::Assistant => { + let mut parts: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + parts.push(GooglePart::Text(GoogleTextPart { text: text.clone() })); + } + } + LlmMessageContent::ToolUse(tool_use) => { + let thought_signature = + tool_use.thought_signature.clone().filter(|s| !s.is_empty()); + + let args: serde_json::Value = + serde_json::from_str(&tool_use.input).unwrap_or_default(); + + parts.push(GooglePart::FunctionCall(GoogleFunctionCallPart { + function_call: GoogleFunctionCall { + name: tool_use.name.clone(), + args, + }, + thought_signature, + })); + } + LlmMessageContent::Thinking(thinking) => { + if let Some(ref signature) = thinking.signature { + if !signature.is_empty() { + parts.push(GooglePart::Thought(GoogleThoughtPart { + thought: true, + thought_signature: signature.clone(), + })); + } + } + } + _ => {} + } + } + + if !parts.is_empty() { + contents.push(GoogleContent { + parts, + role: Some("model".to_string()), + }); + } + } + } + } + + let system_instruction = if system_parts.is_empty() { + None + } else { + Some(GoogleSystemInstruction { + parts: system_parts, + }) + }; + + let tools: Option> = if request.tools.is_empty() { + None + } else { + let declarations: Vec = request + .tools + .iter() + .map(|t| { + let mut parameters: serde_json::Value = serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())); + adapt_schema_for_google(&mut parameters); + GoogleFunctionDeclaration { + name: t.name.clone(), + description: t.description.clone(), + parameters, + } + }) + .collect(); + Some(vec![GoogleTool { + function_declarations: declarations, + }]) + }; + + let tool_config = request.tool_choice.as_ref().map(|tc| { + let mode = match tc { + LlmToolChoice::Auto => "AUTO", + LlmToolChoice::Any => "ANY", + LlmToolChoice::None => "NONE", + }; + GoogleToolConfig { + function_calling_config: GoogleFunctionCallingConfig { + mode: mode.to_string(), + allowed_function_names: None, + }, + } + }); + + let thinking_config = if supports_thinking && request.thinking_allowed { + Some(GoogleThinkingConfig { + thinking_budget: 8192, + }) + } else { + None + }; + + let generation_config = Some(GoogleGenerationConfig { + candidate_count: Some(1), + stop_sequences: if request.stop_sequences.is_empty() { + None + } else { + Some(request.stop_sequences.clone()) + }, + max_output_tokens: None, + temperature: request.temperature.map(|t| t as f64).or(Some(1.0)), + thinking_config, + }); + + Ok(( + GoogleRequest { + contents, + system_instruction, + generation_config, + tools, + tool_config, + }, + real_model_id.to_string(), + )) +} + +fn parse_stream_line(line: &str) -> Option { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed == "[" || trimmed == "]" || trimmed == "," { + return None; + } + + let json_str = trimmed.strip_prefix("data: ").unwrap_or(trimmed); + let json_str = json_str.trim_start_matches(',').trim(); + + if json_str.is_empty() { + return None; + } + + serde_json::from_str(json_str).ok() +} + +impl zed::Extension for GoogleAiProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "google-ai".into(), + name: "Google AI".into(), + icon: Some("icons/google-ai.svg".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.display_name.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: m.max_output_tokens, + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: m.supports_thinking, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("google-ai").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# Google AI Setup + +Welcome to **Google AI**! This extension provides access to Google Gemini models. + +## Configuration + +Enter your Google AI API key below. You can get your API key at [aistudio.google.com/apikey](https://aistudio.google.com/apikey). + +## Available Models + +| Display Name | Real Model | Context | Output | +|--------------|------------|---------|--------| +| Gemini 2.5 Flash-Lite | gemini-2.5-flash-lite | 1M | 65K | +| Gemini 2.5 Flash | gemini-2.5-flash | 1M | 65K | +| Gemini 2.5 Pro | gemini-2.5-pro | 1M | 65K | +| Gemini 3 Pro | gemini-3-pro-preview | 1M | 65K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling with thought signatures +- ✅ Vision (image inputs) +- ✅ Extended thinking support +- ✅ All Gemini models + +## Pricing + +Uses your Google AI API credits. See [Google AI pricing](https://ai.google.dev/pricing) for details. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "google-ai", + LlmCredentialType::ApiKey, + "Google AI API Key", + "AIza...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("google-ai") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("google-ai").ok_or_else(|| { + "No API key configured. Please add your Google AI API key in settings.".to_string() + })?; + + let (google_request, real_model_id) = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&google_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let url = format!( + "https://generativelanguage.googleapis.com/v1beta/models/{}:streamGenerateContent?alt=sse&key={}", + real_model_id, api_key + ); + + let http_request = HttpRequest { + method: HttpMethod::Post, + url, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("google-ai-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + stop_reason: None, + wants_tool_use: false, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if let Some(response) = parse_stream_line(&line) { + for candidate in response.candidates { + if let Some(finish_reason) = &candidate.finish_reason { + state.stop_reason = Some(match finish_reason.as_str() { + "STOP" => { + if state.wants_tool_use { + LlmStopReason::ToolUse + } else { + LlmStopReason::EndTurn + } + } + "MAX_TOKENS" => LlmStopReason::MaxTokens, + "SAFETY" => LlmStopReason::Refusal, + _ => LlmStopReason::EndTurn, + }); + } + + if let Some(content) = candidate.content { + for part in content.parts { + match part { + GooglePart::Text(text_part) => { + if !text_part.text.is_empty() { + return Ok(Some(LlmCompletionEvent::Text( + text_part.text, + ))); + } + } + GooglePart::FunctionCall(fc_part) => { + state.wants_tool_use = true; + let next_tool_id = + TOOL_CALL_COUNTER.fetch_add(1, Ordering::SeqCst); + let id = format!( + "{}-{}", + fc_part.function_call.name, next_tool_id + ); + + let thought_signature = + fc_part.thought_signature.filter(|s| !s.is_empty()); + + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id, + name: fc_part.function_call.name, + input: fc_part.function_call.args.to_string(), + thought_signature, + }))); + } + GooglePart::Thought(thought_part) => { + return Ok(Some(LlmCompletionEvent::Thinking( + LlmThinkingContent { + text: "(Encrypted thought)".to_string(), + signature: Some(thought_part.thought_signature), + }, + ))); + } + _ => {} + } + } + } + } + + if let Some(usage) = response.usage_metadata { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_token_count, + output_tokens: usage.candidates_token_count, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + // Stream ended - check if we have a stop reason + if let Some(stop_reason) = state.stop_reason.take() { + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + + // No stop reason - this is unexpected. Check if buffer contains error info + let mut error_msg = String::from("Stream ended unexpectedly."); + + // Try to parse remaining buffer as potential error response + if !state.buffer.is_empty() { + error_msg.push_str(&format!( + "\nRemaining buffer: {}", + &state.buffer[..state.buffer.len().min(1000)] + )); + } + + return Err(error_msg); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(GoogleAiProvider); diff --git a/extensions/open_router/Cargo.lock b/extensions/open_router/Cargo.lock new file mode 100644 index 00000000000000..4dea7c7a8a9cd8 --- /dev/null +++ b/extensions/open_router/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open_router" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.8.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/open_router/Cargo.toml b/extensions/open_router/Cargo.toml new file mode 100644 index 00000000000000..47a87b5d52571a --- /dev/null +++ b/extensions/open_router/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "open_router" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/open_router.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/extensions/open_router/extension.toml b/extensions/open_router/extension.toml new file mode 100644 index 00000000000000..d321b3d9620c23 --- /dev/null +++ b/extensions/open_router/extension.toml @@ -0,0 +1,13 @@ +id = "open_router" +name = "OpenRouter" +description = "OpenRouter LLM provider - access multiple AI models through a unified API." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.open_router] +name = "OpenRouter" + +[language_model_providers.open_router.auth] +env_var = "OPENROUTER_API_KEY" \ No newline at end of file diff --git a/extensions/open_router/icons/open-router.svg b/extensions/open_router/icons/open-router.svg new file mode 100644 index 00000000000000..b6f5164e0b385f --- /dev/null +++ b/extensions/open_router/icons/open-router.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/extensions/open_router/src/open_router.rs b/extensions/open_router/src/open_router.rs new file mode 100644 index 00000000000000..8d8b143cd70a3c --- /dev/null +++ b/extensions/open_router/src/open_router.rs @@ -0,0 +1,830 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +struct OpenRouterProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + tool_calls_emitted: bool, +} + +#[derive(Clone, Default)] +struct AccumulatedToolCall { + id: String, + name: String, + arguments: String, +} + +struct ModelDefinition { + id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + supports_images: bool, + supports_tools: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + // Anthropic Models + ModelDefinition { + id: "anthropic/claude-sonnet-4", + display_name: "Claude Sonnet 4", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + id: "anthropic/claude-opus-4", + display_name: "Claude Opus 4", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "anthropic/claude-haiku-4", + display_name: "Claude Haiku 4", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + id: "anthropic/claude-3.5-sonnet", + display_name: "Claude 3.5 Sonnet", + max_tokens: 200_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + // OpenAI Models + ModelDefinition { + id: "openai/gpt-4o", + display_name: "GPT-4o", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "openai/gpt-4o-mini", + display_name: "GPT-4o Mini", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "openai/o1", + display_name: "o1", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + supports_tools: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "openai/o3-mini", + display_name: "o3-mini", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: false, + supports_tools: false, + is_default: false, + is_default_fast: false, + }, + // Google Models + ModelDefinition { + id: "google/gemini-2.0-flash-001", + display_name: "Gemini 2.0 Flash", + max_tokens: 1_000_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "google/gemini-2.5-pro-preview", + display_name: "Gemini 2.5 Pro", + max_tokens: 1_000_000, + max_output_tokens: Some(8_192), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + // Meta Models + ModelDefinition { + id: "meta-llama/llama-3.3-70b-instruct", + display_name: "Llama 3.3 70B", + max_tokens: 128_000, + max_output_tokens: Some(4_096), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "meta-llama/llama-4-maverick", + display_name: "Llama 4 Maverick", + max_tokens: 128_000, + max_output_tokens: Some(4_096), + supports_images: true, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + // Mistral Models + ModelDefinition { + id: "mistralai/mistral-large-2411", + display_name: "Mistral Large", + max_tokens: 128_000, + max_output_tokens: Some(4_096), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "mistralai/codestral-latest", + display_name: "Codestral", + max_tokens: 32_000, + max_output_tokens: Some(4_096), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + // DeepSeek Models + ModelDefinition { + id: "deepseek/deepseek-chat-v3-0324", + display_name: "DeepSeek V3", + max_tokens: 64_000, + max_output_tokens: Some(8_192), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + id: "deepseek/deepseek-r1", + display_name: "DeepSeek R1", + max_tokens: 64_000, + max_output_tokens: Some(8_192), + supports_images: false, + supports_tools: false, + is_default: false, + is_default_fast: false, + }, + // Qwen Models + ModelDefinition { + id: "qwen/qwen3-235b-a22b", + display_name: "Qwen 3 235B", + max_tokens: 40_000, + max_output_tokens: Some(8_192), + supports_images: false, + supports_tools: true, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> { + MODELS.iter().find(|m| m.id == model_id) +} + +#[derive(Serialize)] +struct OpenRouterRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, +} + +#[derive(Serialize)] +struct OpenRouterMessage { + role: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, +} + +#[derive(Serialize, Clone)] +#[serde(untagged)] +enum OpenRouterContent { + Text(String), + Parts(Vec), +} + +#[derive(Serialize, Clone)] +#[serde(tag = "type")] +enum OpenRouterContentPart { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image_url")] + ImageUrl { image_url: ImageUrl }, +} + +#[derive(Serialize, Clone)] +struct ImageUrl { + url: String, +} + +#[derive(Serialize, Clone)] +struct OpenRouterToolCall { + id: String, + #[serde(rename = "type")] + call_type: String, + function: OpenRouterFunctionCall, +} + +#[derive(Serialize, Clone)] +struct OpenRouterFunctionCall { + name: String, + arguments: String, +} + +#[derive(Serialize)] +struct OpenRouterTool { + #[serde(rename = "type")] + tool_type: String, + function: OpenRouterFunctionDef, +} + +#[derive(Serialize)] +struct OpenRouterFunctionDef { + name: String, + description: String, + parameters: serde_json::Value, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterStreamResponse { + choices: Vec, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterStreamChoice { + delta: OpenRouterDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenRouterDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenRouterFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterUsage { + prompt_tokens: u64, + completion_tokens: u64, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let mut messages: Vec = Vec::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + let mut text_content = String::new(); + for content in &msg.content { + if let LlmMessageContent::Text(text) = content { + if !text_content.is_empty() { + text_content.push('\n'); + } + text_content.push_str(text); + } + } + if !text_content.is_empty() { + messages.push(OpenRouterMessage { + role: "system".to_string(), + content: Some(OpenRouterContent::Text(text_content)), + tool_calls: None, + tool_call_id: None, + }); + } + } + LlmMessageRole::User => { + let mut parts: Vec = Vec::new(); + let mut tool_result_messages: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + parts.push(OpenRouterContentPart::Text { text: text.clone() }); + } + } + LlmMessageContent::Image(img) => { + let data_url = format!("data:image/png;base64,{}", img.source); + parts.push(OpenRouterContentPart::ImageUrl { + image_url: ImageUrl { url: data_url }, + }); + } + LlmMessageContent::ToolResult(result) => { + let content_text = match &result.content { + LlmToolResultContent::Text(t) => t.clone(), + LlmToolResultContent::Image(_) => "[Image]".to_string(), + }; + tool_result_messages.push(OpenRouterMessage { + role: "tool".to_string(), + content: Some(OpenRouterContent::Text(content_text)), + tool_calls: None, + tool_call_id: Some(result.tool_use_id.clone()), + }); + } + _ => {} + } + } + + if !parts.is_empty() { + let content = if parts.len() == 1 { + if let OpenRouterContentPart::Text { text } = &parts[0] { + OpenRouterContent::Text(text.clone()) + } else { + OpenRouterContent::Parts(parts) + } + } else { + OpenRouterContent::Parts(parts) + }; + + messages.push(OpenRouterMessage { + role: "user".to_string(), + content: Some(content), + tool_calls: None, + tool_call_id: None, + }); + } + + messages.extend(tool_result_messages); + } + LlmMessageRole::Assistant => { + let mut text_content = String::new(); + let mut tool_calls: Vec = Vec::new(); + + for content in &msg.content { + match content { + LlmMessageContent::Text(text) => { + if !text.is_empty() { + if !text_content.is_empty() { + text_content.push('\n'); + } + text_content.push_str(text); + } + } + LlmMessageContent::ToolUse(tool_use) => { + tool_calls.push(OpenRouterToolCall { + id: tool_use.id.clone(), + call_type: "function".to_string(), + function: OpenRouterFunctionCall { + name: tool_use.name.clone(), + arguments: tool_use.input.clone(), + }, + }); + } + _ => {} + } + } + + messages.push(OpenRouterMessage { + role: "assistant".to_string(), + content: if text_content.is_empty() { + None + } else { + Some(OpenRouterContent::Text(text_content)) + }, + tool_calls: if tool_calls.is_empty() { + None + } else { + Some(tool_calls) + }, + tool_call_id: None, + }); + } + } + } + + let model_def = get_model_definition(model_id); + let supports_tools = model_def.map(|m| m.supports_tools).unwrap_or(true); + + let tools: Vec = if supports_tools { + request + .tools + .iter() + .map(|t| OpenRouterTool { + tool_type: "function".to_string(), + function: OpenRouterFunctionDef { + name: t.name.clone(), + description: t.description.clone(), + parameters: serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())), + }, + }) + .collect() + } else { + Vec::new() + }; + + let tool_choice = if supports_tools { + request.tool_choice.as_ref().map(|tc| match tc { + LlmToolChoice::Auto => "auto".to_string(), + LlmToolChoice::Any => "required".to_string(), + LlmToolChoice::None => "none".to_string(), + }) + } else { + None + }; + + let max_tokens = request + .max_tokens + .or(model_def.and_then(|m| m.max_output_tokens)); + + Ok(OpenRouterRequest { + model: model_id.to_string(), + messages, + max_tokens, + tools, + tool_choice, + stop: request.stop_sequences.clone(), + temperature: request.temperature, + stream: true, + }) +} + +fn parse_sse_line(line: &str) -> Option { + let data = line.strip_prefix("data: ")?; + if data.trim() == "[DONE]" { + return None; + } + serde_json::from_str(data).ok() +} + +impl zed::Extension for OpenRouterProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "open_router".into(), + name: "OpenRouter".into(), + icon: Some("icons/open-router.svg".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.id.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: m.max_output_tokens, + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: m.supports_tools, + supports_tool_choice_auto: m.supports_tools, + supports_tool_choice_any: m.supports_tools, + supports_tool_choice_none: m.supports_tools, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("open_router").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# OpenRouter Setup + +Welcome to **OpenRouter**! Access multiple AI models through a single API. + +## Configuration + +Enter your OpenRouter API key below. Get your API key at [openrouter.ai/keys](https://openrouter.ai/keys). + +## Available Models + +### Anthropic +| Model | Context | Output | +|-------|---------|--------| +| Claude Sonnet 4 | 200K | 8K | +| Claude Opus 4 | 200K | 8K | +| Claude Haiku 4 | 200K | 8K | +| Claude 3.5 Sonnet | 200K | 8K | + +### OpenAI +| Model | Context | Output | +|-------|---------|--------| +| GPT-4o | 128K | 16K | +| GPT-4o Mini | 128K | 16K | +| o1 | 200K | 100K | +| o3-mini | 200K | 100K | + +### Google +| Model | Context | Output | +|-------|---------|--------| +| Gemini 2.0 Flash | 1M | 8K | +| Gemini 2.5 Pro | 1M | 8K | + +### Meta +| Model | Context | Output | +|-------|---------|--------| +| Llama 3.3 70B | 128K | 4K | +| Llama 4 Maverick | 128K | 4K | + +### Mistral +| Model | Context | Output | +|-------|---------|--------| +| Mistral Large | 128K | 4K | +| Codestral | 32K | 4K | + +### DeepSeek +| Model | Context | Output | +|-------|---------|--------| +| DeepSeek V3 | 64K | 8K | +| DeepSeek R1 | 64K | 8K | + +### Qwen +| Model | Context | Output | +|-------|---------|--------| +| Qwen 3 235B | 40K | 8K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling (model dependent) +- ✅ Vision (model dependent) +- ✅ Access to 200+ models +- ✅ Unified billing + +## Pricing + +Pay-per-use based on model. See [openrouter.ai/models](https://openrouter.ai/models) for pricing. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "open_router", + LlmCredentialType::ApiKey, + "OpenRouter API Key", + "sk-or-v1-...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("open_router") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("open_router").ok_or_else(|| { + "No API key configured. Please add your OpenRouter API key in settings.".to_string() + })?; + + let openrouter_request = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&openrouter_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let http_request = HttpRequest { + method: HttpMethod::Post, + url: "https://openrouter.ai/api/v1/chat/completions".to_string(), + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("Authorization".to_string(), format!("Bearer {}", api_key)), + ("HTTP-Referer".to_string(), "https://zed.dev".to_string()), + ("X-Title".to_string(), "Zed Editor".to_string()), + ], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("openrouter-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + tool_calls: HashMap::new(), + tool_calls_emitted: false, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if line.trim().is_empty() { + continue; + } + + if let Some(response) = parse_sse_line(&line) { + if let Some(choice) = response.choices.first() { + if let Some(content) = &choice.delta.content { + if !content.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(content.clone()))); + } + } + + if let Some(tool_calls) = &choice.delta.tool_calls { + for tc in tool_calls { + let entry = state + .tool_calls + .entry(tc.index) + .or_insert_with(AccumulatedToolCall::default); + + if let Some(id) = &tc.id { + entry.id = id.clone(); + } + if let Some(func) = &tc.function { + if let Some(name) = &func.name { + entry.name = name.clone(); + } + if let Some(args) = &func.arguments { + entry.arguments.push_str(args); + } + } + } + } + + if let Some(finish_reason) = &choice.finish_reason { + if !state.tool_calls.is_empty() && !state.tool_calls_emitted { + state.tool_calls_emitted = true; + let mut tool_calls: Vec<_> = state.tool_calls.drain().collect(); + tool_calls.sort_by_key(|(idx, _)| *idx); + + if let Some((_, tc)) = tool_calls.into_iter().next() { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tc.id, + name: tc.name, + input: tc.arguments, + thought_signature: None, + }))); + } + } + + let stop_reason = match finish_reason.as_str() { + "stop" => LlmStopReason::EndTurn, + "length" => LlmStopReason::MaxTokens, + "tool_calls" => LlmStopReason::ToolUse, + "content_filter" => LlmStopReason::Refusal, + _ => LlmStopReason::EndTurn, + }; + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + } + + if let Some(usage) = response.usage { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_tokens, + output_tokens: usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + return Ok(None); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(OpenRouterProvider); diff --git a/extensions/openai/Cargo.lock b/extensions/openai/Cargo.lock new file mode 100644 index 00000000000000..2ef354a2892b23 --- /dev/null +++ b/extensions/openai/Cargo.lock @@ -0,0 +1,823 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fopenai" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.7.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/openai/Cargo.toml b/extensions/openai/Cargo.toml new file mode 100644 index 00000000000000..f81809e426ef69 --- /dev/null +++ b/extensions/openai/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "openai" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace] + +[lib] +path = "src/openai.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/extensions/openai/extension.toml b/extensions/openai/extension.toml new file mode 100644 index 00000000000000..94788688716f1d --- /dev/null +++ b/extensions/openai/extension.toml @@ -0,0 +1,13 @@ +id = "openai" +name = "OpenAI" +description = "OpenAI GPT LLM provider for Zed." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Team"] +repository = "https://github.com/zed-industries/zed" + +[language_model_providers.openai] +name = "OpenAI" + +[language_model_providers.openai.auth] +env_var = "OPENAI_API_KEY" \ No newline at end of file diff --git a/extensions/openai/icons/openai.svg b/extensions/openai/icons/openai.svg new file mode 100644 index 00000000000000..e45ac315a01185 --- /dev/null +++ b/extensions/openai/icons/openai.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/openai/src/openai.rs b/extensions/openai/src/openai.rs new file mode 100644 index 00000000000000..40a99352abd5da --- /dev/null +++ b/extensions/openai/src/openai.rs @@ -0,0 +1,727 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; +use zed_extension_api::{self as zed, *}; + +struct OpenAiProvider { + streams: Mutex>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + tool_calls_emitted: bool, +} + +#[derive(Clone, Default)] +struct AccumulatedToolCall { + id: String, + name: String, + arguments: String, +} + +struct ModelDefinition { + real_id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + supports_images: bool, + is_default: bool, + is_default_fast: bool, +} + +const MODELS: &[ModelDefinition] = &[ + ModelDefinition { + real_id: "gpt-4o", + display_name: "GPT-4o", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + is_default: true, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-4o-mini", + display_name: "GPT-4o-mini", + max_tokens: 128_000, + max_output_tokens: Some(16_384), + supports_images: true, + is_default: false, + is_default_fast: true, + }, + ModelDefinition { + real_id: "gpt-4.1", + display_name: "GPT-4.1", + max_tokens: 1_047_576, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-4.1-mini", + display_name: "GPT-4.1-mini", + max_tokens: 1_047_576, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-4.1-nano", + display_name: "GPT-4.1-nano", + max_tokens: 1_047_576, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-5", + display_name: "GPT-5", + max_tokens: 272_000, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "gpt-5-mini", + display_name: "GPT-5-mini", + max_tokens: 272_000, + max_output_tokens: Some(32_768), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "o1", + display_name: "o1", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "o3", + display_name: "o3", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "o3-mini", + display_name: "o3-mini", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: false, + is_default: false, + is_default_fast: false, + }, + ModelDefinition { + real_id: "o4-mini", + display_name: "o4-mini", + max_tokens: 200_000, + max_output_tokens: Some(100_000), + supports_images: true, + is_default: false, + is_default_fast: false, + }, +]; + +fn get_real_model_id(display_name: &str) -> Option<&'static str> { + MODELS + .iter() + .find(|m| m.display_name == display_name) + .map(|m| m.real_id) +} + +#[derive(Serialize)] +struct OpenAiRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + stream: bool, + stream_options: Option, +} + +#[derive(Serialize)] +struct StreamOptions { + include_usage: bool, +} + +#[derive(Serialize)] +#[serde(tag = "role")] +enum OpenAiMessage { + #[serde(rename = "system")] + System { content: String }, + #[serde(rename = "user")] + User { content: Vec }, + #[serde(rename = "assistant")] + Assistant { + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + }, + #[serde(rename = "tool")] + Tool { + tool_call_id: String, + content: String, + }, +} + +#[derive(Serialize)] +#[serde(tag = "type")] +enum OpenAiContentPart { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image_url")] + ImageUrl { image_url: ImageUrl }, +} + +#[derive(Serialize)] +struct ImageUrl { + url: String, +} + +#[derive(Serialize, Deserialize, Clone)] +struct OpenAiToolCall { + id: String, + #[serde(rename = "type")] + call_type: String, + function: OpenAiFunctionCall, +} + +#[derive(Serialize, Deserialize, Clone)] +struct OpenAiFunctionCall { + name: String, + arguments: String, +} + +#[derive(Serialize)] +struct OpenAiTool { + #[serde(rename = "type")] + tool_type: String, + function: OpenAiFunctionDef, +} + +#[derive(Serialize)] +struct OpenAiFunctionDef { + name: String, + description: String, + parameters: serde_json::Value, +} + +#[derive(Deserialize, Debug)] +struct OpenAiStreamEvent { + choices: Vec, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiChoice { + delta: OpenAiDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenAiToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiUsage { + prompt_tokens: u64, + completion_tokens: u64, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct OpenAiError { + error: OpenAiErrorDetail, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct OpenAiErrorDetail { + message: String, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let real_model_id = + get_real_model_id(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; + + let mut messages = Vec::new(); + + for msg in &request.messages { + match msg.role { + LlmMessageRole::System => { + let text: String = msg + .content + .iter() + .filter_map(|c| match c { + LlmMessageContent::Text(t) => Some(t.as_str()), + _ => None, + }) + .collect::>() + .join("\n"); + if !text.is_empty() { + messages.push(OpenAiMessage::System { content: text }); + } + } + LlmMessageRole::User => { + let parts: Vec = msg + .content + .iter() + .filter_map(|c| match c { + LlmMessageContent::Text(t) => { + Some(OpenAiContentPart::Text { text: t.clone() }) + } + LlmMessageContent::Image(img) => Some(OpenAiContentPart::ImageUrl { + image_url: ImageUrl { + url: format!("data:image/png;base64,{}", img.source), + }, + }), + LlmMessageContent::ToolResult(_) => None, + _ => None, + }) + .collect(); + + for content in &msg.content { + if let LlmMessageContent::ToolResult(result) = content { + let content_text = match &result.content { + LlmToolResultContent::Text(t) => t.clone(), + LlmToolResultContent::Image(_) => "[Image]".to_string(), + }; + messages.push(OpenAiMessage::Tool { + tool_call_id: result.tool_use_id.clone(), + content: content_text, + }); + } + } + + if !parts.is_empty() { + messages.push(OpenAiMessage::User { content: parts }); + } + } + LlmMessageRole::Assistant => { + let mut content_text: Option = None; + let mut tool_calls: Vec = Vec::new(); + + for c in &msg.content { + match c { + LlmMessageContent::Text(t) => { + content_text = Some(t.clone()); + } + LlmMessageContent::ToolUse(tool_use) => { + tool_calls.push(OpenAiToolCall { + id: tool_use.id.clone(), + call_type: "function".to_string(), + function: OpenAiFunctionCall { + name: tool_use.name.clone(), + arguments: tool_use.input.clone(), + }, + }); + } + _ => {} + } + } + + messages.push(OpenAiMessage::Assistant { + content: content_text, + tool_calls: if tool_calls.is_empty() { + None + } else { + Some(tool_calls) + }, + }); + } + } + } + + let tools: Option> = if request.tools.is_empty() { + None + } else { + Some( + request + .tools + .iter() + .map(|t| OpenAiTool { + tool_type: "function".to_string(), + function: OpenAiFunctionDef { + name: t.name.clone(), + description: t.description.clone(), + parameters: serde_json::from_str(&t.input_schema) + .unwrap_or(serde_json::Value::Object(Default::default())), + }, + }) + .collect(), + ) + }; + + let tool_choice = request.tool_choice.as_ref().map(|tc| match tc { + LlmToolChoice::Auto => "auto".to_string(), + LlmToolChoice::Any => "required".to_string(), + LlmToolChoice::None => "none".to_string(), + }); + + Ok(OpenAiRequest { + model: real_model_id.to_string(), + messages, + tools, + tool_choice, + temperature: request.temperature, + max_tokens: request.max_tokens, + stop: request.stop_sequences.clone(), + stream: true, + stream_options: Some(StreamOptions { + include_usage: true, + }), + }) +} + +fn parse_sse_line(line: &str) -> Option { + if let Some(data) = line.strip_prefix("data: ") { + if data == "[DONE]" { + return None; + } + serde_json::from_str(data).ok() + } else { + None + } +} + +impl zed::Extension for OpenAiProvider { + fn new() -> Self { + Self { + streams: Mutex::new(HashMap::new()), + next_stream_id: Mutex::new(0), + } + } + + fn llm_providers(&self) -> Vec { + vec![LlmProviderInfo { + id: "openai".into(), + name: "OpenAI".into(), + icon: Some("icons/openai.svg".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { + Ok(MODELS + .iter() + .map(|m| LlmModelInfo { + id: m.display_name.to_string(), + name: m.display_name.to_string(), + max_token_count: m.max_tokens, + max_output_tokens: m.max_output_tokens, + capabilities: LlmModelCapabilities { + supports_images: m.supports_images, + supports_tools: true, + supports_tool_choice_auto: true, + supports_tool_choice_any: true, + supports_tool_choice_none: true, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_default, + is_default_fast: m.is_default_fast, + }) + .collect()) + } + + fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { + llm_get_credential("openai").is_some() + } + + fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { + Some( + r#"# OpenAI Setup + +Welcome to **OpenAI**! This extension provides access to OpenAI GPT models. + +## Configuration + +Enter your OpenAI API key below. You can find your API key at [platform.openai.com/api-keys](https://platform.openai.com/api-keys). + +## Available Models + +| Display Name | Real Model | Context | Output | +|--------------|------------|---------|--------| +| GPT-4o | gpt-4o | 128K | 16K | +| GPT-4o-mini | gpt-4o-mini | 128K | 16K | +| GPT-4.1 | gpt-4.1 | 1M | 32K | +| GPT-4.1-mini | gpt-4.1-mini | 1M | 32K | +| GPT-5 | gpt-5 | 272K | 32K | +| GPT-5-mini | gpt-5-mini | 272K | 32K | +| o1 | o1 | 200K | 100K | +| o3 | o3 | 200K | 100K | +| o3-mini | o3-mini | 200K | 100K | + +## Features + +- ✅ Full streaming support +- ✅ Tool/function calling +- ✅ Vision (image inputs) +- ✅ All OpenAI models + +## Pricing + +Uses your OpenAI API credits. See [OpenAI pricing](https://openai.com/pricing) for details. +"# + .to_string(), + ) + } + + fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + let provided = llm_request_credential( + "openai", + LlmCredentialType::ApiKey, + "OpenAI API Key", + "sk-...", + )?; + if provided { + Ok(()) + } else { + Err("Authentication cancelled".to_string()) + } + } + + fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + llm_delete_credential("openai") + } + + fn llm_stream_completion_start( + &mut self, + _provider_id: &str, + model_id: &str, + request: &LlmCompletionRequest, + ) -> Result { + let api_key = llm_get_credential("openai").ok_or_else(|| { + "No API key configured. Please add your OpenAI API key in settings.".to_string() + })?; + + let openai_request = convert_request(model_id, request)?; + + let body = serde_json::to_vec(&openai_request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let http_request = HttpRequest { + method: HttpMethod::Post, + url: "https://api.openai.com/v1/chat/completions".to_string(), + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("Authorization".to_string(), format!("Bearer {}", api_key)), + ], + body: Some(body), + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response_stream = http_request + .fetch_stream() + .map_err(|e| format!("HTTP request failed: {}", e))?; + + let stream_id = { + let mut id_counter = self.next_stream_id.lock().unwrap(); + let id = format!("openai-stream-{}", *id_counter); + *id_counter += 1; + id + }; + + self.streams.lock().unwrap().insert( + stream_id.clone(), + StreamState { + response_stream: Some(response_stream), + buffer: String::new(), + started: false, + tool_calls: HashMap::new(), + tool_calls_emitted: false, + }, + ); + + Ok(stream_id) + } + + fn llm_stream_completion_next( + &mut self, + stream_id: &str, + ) -> Result, String> { + let mut streams = self.streams.lock().unwrap(); + let state = streams + .get_mut(stream_id) + .ok_or_else(|| format!("Unknown stream: {}", stream_id))?; + + if !state.started { + state.started = true; + return Ok(Some(LlmCompletionEvent::Started)); + } + + let response_stream = state + .response_stream + .as_mut() + .ok_or_else(|| "Stream already closed".to_string())?; + + loop { + if let Some(newline_pos) = state.buffer.find('\n') { + let line = state.buffer[..newline_pos].trim().to_string(); + state.buffer = state.buffer[newline_pos + 1..].to_string(); + + if line.is_empty() { + continue; + } + + if let Some(event) = parse_sse_line(&line) { + if let Some(choice) = event.choices.first() { + if let Some(tool_calls) = &choice.delta.tool_calls { + for tc in tool_calls { + let entry = state.tool_calls.entry(tc.index).or_default(); + + if let Some(id) = &tc.id { + entry.id = id.clone(); + } + + if let Some(func) = &tc.function { + if let Some(name) = &func.name { + entry.name = name.clone(); + } + if let Some(args) = &func.arguments { + entry.arguments.push_str(args); + } + } + } + } + + if let Some(reason) = &choice.finish_reason { + if reason == "tool_calls" && !state.tool_calls_emitted { + state.tool_calls_emitted = true; + if let Some((&index, _)) = state.tool_calls.iter().next() { + if let Some(tool_call) = state.tool_calls.remove(&index) { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tool_call.id, + name: tool_call.name, + input: tool_call.arguments, + thought_signature: None, + }))); + } + } + } + + let stop_reason = match reason.as_str() { + "stop" => LlmStopReason::EndTurn, + "length" => LlmStopReason::MaxTokens, + "tool_calls" => LlmStopReason::ToolUse, + "content_filter" => LlmStopReason::Refusal, + _ => LlmStopReason::EndTurn, + }; + + if let Some(usage) = event.usage { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_tokens, + output_tokens: usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + + return Ok(Some(LlmCompletionEvent::Stop(stop_reason))); + } + + if let Some(content) = &choice.delta.content { + if !content.is_empty() { + return Ok(Some(LlmCompletionEvent::Text(content.clone()))); + } + } + } + + if event.choices.is_empty() { + if let Some(usage) = event.usage { + return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage { + input_tokens: usage.prompt_tokens, + output_tokens: usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }))); + } + } + } + + continue; + } + + match response_stream.next_chunk() { + Ok(Some(chunk)) => { + let text = String::from_utf8_lossy(&chunk); + state.buffer.push_str(&text); + } + Ok(None) => { + if !state.tool_calls.is_empty() && !state.tool_calls_emitted { + state.tool_calls_emitted = true; + let keys: Vec = state.tool_calls.keys().copied().collect(); + if let Some(&key) = keys.first() { + if let Some(tool_call) = state.tool_calls.remove(&key) { + return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse { + id: tool_call.id, + name: tool_call.name, + input: tool_call.arguments, + thought_signature: None, + }))); + } + } + } + return Ok(None); + } + Err(e) => { + return Err(format!("Stream error: {}", e)); + } + } + } + } + + fn llm_stream_completion_close(&mut self, stream_id: &str) { + self.streams.lock().unwrap().remove(stream_id); + } +} + +zed::register_extension!(OpenAiProvider); From a0d3bc31e9e1f50f637f1bc438ecb727c91d65a6 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 11:14:12 -0500 Subject: [PATCH 27/45] Rename copilot_chat to copilot-chat --- assets/settings/default.json | 2 +- extensions/{copilot_chat => copilot-chat}/Cargo.lock | 0 extensions/{copilot_chat => copilot-chat}/Cargo.toml | 2 +- extensions/{copilot_chat => copilot-chat}/extension.toml | 6 +++--- extensions/{copilot_chat => copilot-chat}/icons/copilot.svg | 0 .../{copilot_chat => copilot-chat}/src/copilot_chat.rs | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename extensions/{copilot_chat => copilot-chat}/Cargo.lock (100%) rename extensions/{copilot_chat => copilot-chat}/Cargo.toml (93%) rename extensions/{copilot_chat => copilot-chat}/extension.toml (70%) rename extensions/{copilot_chat => copilot-chat}/icons/copilot.svg (100%) rename extensions/{copilot_chat => copilot-chat}/src/copilot_chat.rs (100%) diff --git a/assets/settings/default.json b/assets/settings/default.json index ce498dd6172c5a..6ffb563de5eba6 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1722,7 +1722,7 @@ // and change the value to `false`. "auto_install_extensions": { "anthropic": true, - "copilot_chat": true, + "copilot-chat": true, "google-ai": true, "html": true, "openai": true, diff --git a/extensions/copilot_chat/Cargo.lock b/extensions/copilot-chat/Cargo.lock similarity index 100% rename from extensions/copilot_chat/Cargo.lock rename to extensions/copilot-chat/Cargo.lock diff --git a/extensions/copilot_chat/Cargo.toml b/extensions/copilot-chat/Cargo.toml similarity index 93% rename from extensions/copilot_chat/Cargo.toml rename to extensions/copilot-chat/Cargo.toml index 6daae73229776d..189c1db9fc37b9 100644 --- a/extensions/copilot_chat/Cargo.toml +++ b/extensions/copilot-chat/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "copilot_chat" +name = "copilot-chat" version = "0.1.0" edition = "2021" publish = false diff --git a/extensions/copilot_chat/extension.toml b/extensions/copilot-chat/extension.toml similarity index 70% rename from extensions/copilot_chat/extension.toml rename to extensions/copilot-chat/extension.toml index 9afa188462fcb6..c226a20f3a7724 100644 --- a/extensions/copilot_chat/extension.toml +++ b/extensions/copilot-chat/extension.toml @@ -1,4 +1,4 @@ -id = "copilot_chat" +id = "copilot-chat" name = "Copilot Chat" description = "GitHub Copilot Chat LLM provider for Zed." version = "0.1.0" @@ -6,8 +6,8 @@ schema_version = 1 authors = ["Zed Team"] repository = "https://github.com/zed-industries/zed" -[language_model_providers.copilot_chat] +[language_model_providers.copilot-chat] name = "Copilot Chat" -[language_model_providers.copilot_chat.auth] +[language_model_providers.copilot-chat.auth] env_var = "GH_COPILOT_TOKEN" \ No newline at end of file diff --git a/extensions/copilot_chat/icons/copilot.svg b/extensions/copilot-chat/icons/copilot.svg similarity index 100% rename from extensions/copilot_chat/icons/copilot.svg rename to extensions/copilot-chat/icons/copilot.svg diff --git a/extensions/copilot_chat/src/copilot_chat.rs b/extensions/copilot-chat/src/copilot_chat.rs similarity index 100% rename from extensions/copilot_chat/src/copilot_chat.rs rename to extensions/copilot-chat/src/copilot_chat.rs From 4464392e8e75228de730f4991a0b1dbc5b205688 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 11:19:10 -0500 Subject: [PATCH 28/45] Use kebab-case for open-router extension too. --- assets/settings/default.json | 2 +- extensions/{open_router => open-router}/Cargo.lock | 0 extensions/{open_router => open-router}/Cargo.toml | 2 +- extensions/{open_router => open-router}/extension.toml | 6 +++--- .../{open_router => open-router}/icons/open-router.svg | 0 extensions/{open_router => open-router}/src/open_router.rs | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename extensions/{open_router => open-router}/Cargo.lock (100%) rename extensions/{open_router => open-router}/Cargo.toml (93%) rename extensions/{open_router => open-router}/extension.toml (73%) rename extensions/{open_router => open-router}/icons/open-router.svg (100%) rename extensions/{open_router => open-router}/src/open_router.rs (100%) diff --git a/assets/settings/default.json b/assets/settings/default.json index 6ffb563de5eba6..bcefc98742b8c2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1726,7 +1726,7 @@ "google-ai": true, "html": true, "openai": true, - "open_router": true + "open-router": true }, // The capabilities granted to extensions. // diff --git a/extensions/open_router/Cargo.lock b/extensions/open-router/Cargo.lock similarity index 100% rename from extensions/open_router/Cargo.lock rename to extensions/open-router/Cargo.lock diff --git a/extensions/open_router/Cargo.toml b/extensions/open-router/Cargo.toml similarity index 93% rename from extensions/open_router/Cargo.toml rename to extensions/open-router/Cargo.toml index 47a87b5d52571a..5c5af5ad7ff9e7 100644 --- a/extensions/open_router/Cargo.toml +++ b/extensions/open-router/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "open_router" +name = "open-router" version = "0.1.0" edition = "2021" publish = false diff --git a/extensions/open_router/extension.toml b/extensions/open-router/extension.toml similarity index 73% rename from extensions/open_router/extension.toml rename to extensions/open-router/extension.toml index d321b3d9620c23..6c1b8c087d016e 100644 --- a/extensions/open_router/extension.toml +++ b/extensions/open-router/extension.toml @@ -1,4 +1,4 @@ -id = "open_router" +id = "open-router" name = "OpenRouter" description = "OpenRouter LLM provider - access multiple AI models through a unified API." version = "0.1.0" @@ -6,8 +6,8 @@ schema_version = 1 authors = ["Zed Team"] repository = "https://github.com/zed-industries/zed" -[language_model_providers.open_router] +[language_model_providers.open-router] name = "OpenRouter" -[language_model_providers.open_router.auth] +[language_model_providers.open-router.auth] env_var = "OPENROUTER_API_KEY" \ No newline at end of file diff --git a/extensions/open_router/icons/open-router.svg b/extensions/open-router/icons/open-router.svg similarity index 100% rename from extensions/open_router/icons/open-router.svg rename to extensions/open-router/icons/open-router.svg diff --git a/extensions/open_router/src/open_router.rs b/extensions/open-router/src/open_router.rs similarity index 100% rename from extensions/open_router/src/open_router.rs rename to extensions/open-router/src/open_router.rs From 8b5b2712c8ef879500fca7f409898da154d16232 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 11:32:58 -0500 Subject: [PATCH 29/45] Update Cargo.lock --- extensions/copilot-chat/Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/copilot-chat/Cargo.lock b/extensions/copilot-chat/Cargo.lock index 1ba4a97d7291c7..4b78fda143f8ec 100644 --- a/extensions/copilot-chat/Cargo.lock +++ b/extensions/copilot-chat/Cargo.lock @@ -39,7 +39,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "copilot_chat" +name = "copilot-chat" version = "0.1.0" dependencies = [ "serde", From a198b6c0d153c86b8217ffc96167a96f055560a7 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 11:48:11 -0500 Subject: [PATCH 30/45] Use icon in more places --- assets/settings/default.json | 7 +------ crates/acp_thread/src/connection.rs | 11 +++++++++- crates/agent/src/agent.rs | 11 +++++++--- crates/agent_ui/src/acp/model_selector.rs | 18 +++++++++++------ .../src/acp/model_selector_popover.rs | 13 ++++++++---- extensions/anthropic/Cargo.lock | 20 +++++++++---------- 6 files changed, 50 insertions(+), 30 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index bcefc98742b8c2..f687778d7bd7fc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1721,12 +1721,7 @@ // If you don't want any of these extensions, add this field to your settings // and change the value to `false`. "auto_install_extensions": { - "anthropic": true, - "copilot-chat": true, - "google-ai": true, - "html": true, - "openai": true, - "open-router": true + "html": true }, // The capabilities granted to extensions. // diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 8213786a182e1d..a188c0fbe88d5b 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -204,12 +204,21 @@ pub trait AgentModelSelector: 'static { } } +/// Icon for a model in the model selector. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AgentModelIcon { + /// A built-in icon from Zed's icon set. + Named(IconName), + /// Path to a custom SVG icon file. + Path(SharedString), +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentModelInfo { pub id: acp::ModelId, pub name: SharedString, pub description: Option, - pub icon: Option, + pub icon: Option, } impl From for AgentModelInfo { diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index aec0767c25422d..7ebcd79f13e0ee 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -18,7 +18,7 @@ pub use templates::*; pub use thread::*; pub use tools::*; -use acp_thread::{AcpThread, AgentModelSelector}; +use acp_thread::{AcpThread, AgentModelIcon, AgentModelSelector}; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; @@ -161,11 +161,16 @@ impl LanguageModels { model: &Arc, provider: &Arc, ) -> acp_thread::AgentModelInfo { + let icon = if let Some(path) = provider.icon_path() { + Some(AgentModelIcon::Path(path)) + } else { + Some(AgentModelIcon::Named(provider.icon())) + }; acp_thread::AgentModelInfo { id: Self::model_id(model), name: model.name().0, description: None, - icon: Some(provider.icon()), + icon, } } @@ -1356,7 +1361,7 @@ mod internal_tests { id: acp::ModelId::new("fake/fake"), name: "Fake".into(), description: None, - icon: Some(ui::IconName::ZedAssistant), + icon: Some(AgentModelIcon::Named(ui::IconName::ZedAssistant)), }] )]) ); diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index f9710ad9b3aac2..6b8e1d87a0934a 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -1,6 +1,6 @@ use std::{cmp::Reverse, rc::Rc, sync::Arc}; -use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector}; +use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector}; use agent_servers::AgentServer; use anyhow::Result; use collections::IndexMap; @@ -292,12 +292,18 @@ impl PickerDelegate for AcpModelPickerDelegate { h_flex() .w_full() .gap_1p5() - .when_some(model_info.icon, |this, icon| { - this.child( - Icon::new(icon) + .map(|this| match &model_info.icon { + Some(AgentModelIcon::Path(path)) => this.child( + Icon::from_path(path.clone()) .color(model_icon_color) - .size(IconSize::Small) - ) + .size(IconSize::Small), + ), + Some(AgentModelIcon::Named(icon)) => this.child( + Icon::new(*icon) + .color(model_icon_color) + .size(IconSize::Small), + ), + None => this, }) .child(Label::new(model_info.name.clone()).truncate()), ) diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs index e2393c11bd6c23..7fd808bb2059fd 100644 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ b/crates/agent_ui/src/acp/model_selector_popover.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use std::sync::Arc; -use acp_thread::{AgentModelInfo, AgentModelSelector}; +use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector}; use agent_servers::AgentServer; use fs::Fs; use gpui::{Entity, FocusHandle}; @@ -64,7 +64,7 @@ impl Render for AcpModelSelectorPopover { .map(|model| model.name.clone()) .unwrap_or_else(|| SharedString::from("Select a Model")); - let model_icon = model.as_ref().and_then(|model| model.icon); + let model_icon = model.as_ref().and_then(|model| model.icon.clone()); let focus_handle = self.focus_handle.clone(); @@ -78,8 +78,13 @@ impl Render for AcpModelSelectorPopover { self.selector.clone(), ButtonLike::new("active-model") .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .when_some(model_icon, |this, icon| { - this.child(Icon::new(icon).color(color).size(IconSize::XSmall)) + .when_some(model_icon, |this, icon| match icon { + AgentModelIcon::Path(path) => { + this.child(Icon::from_path(path).color(color).size(IconSize::XSmall)) + } + AgentModelIcon::Named(icon_name) => { + this.child(Icon::new(icon_name).color(color).size(IconSize::XSmall)) + } }) .child( Label::new(model_name) diff --git a/extensions/anthropic/Cargo.lock b/extensions/anthropic/Cargo.lock index bd558d1ce1a11c..8bd00ffdbe3292 100644 --- a/extensions/anthropic/Cargo.lock +++ b/extensions/anthropic/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "anthropic" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -64,15 +73,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "fanthropic" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "zed_extension_api", -] - [[package]] name = "flate2" version = "1.1.5" @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "zed_extension_api" -version = "0.7.0" +version = "0.8.0" dependencies = [ "serde", "serde_json", From b1934fb7125d2a216da13dd2d50289159d3423f9 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 12:11:47 -0500 Subject: [PATCH 31/45] Remove builtin Anthropic provider --- assets/icons/ai_anthropic.svg | 11 - assets/settings/default.json | 333 +++--- crates/anthropic/src/anthropic.rs | 2 +- crates/icons/src/icons.rs | 1 - crates/language_models/src/language_models.rs | 8 - crates/language_models/src/provider.rs | 1 - .../language_models/src/provider/anthropic.rs | 1045 ----------------- crates/language_models/src/provider/cloud.rs | 443 ++++++- crates/language_models/src/settings.rs | 15 +- crates/settings/src/settings_content/agent.rs | 1 - .../src/settings_content/language_model.rs | 30 - docs/src/configuring-zed.md | 3 - 12 files changed, 609 insertions(+), 1284 deletions(-) delete mode 100644 assets/icons/ai_anthropic.svg delete mode 100644 crates/language_models/src/provider/anthropic.rs diff --git a/assets/icons/ai_anthropic.svg b/assets/icons/ai_anthropic.svg deleted file mode 100644 index 12d731fb0b4438..00000000000000 --- a/assets/icons/ai_anthropic.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/settings/default.json b/assets/settings/default.json index f687778d7bd7fc..83c01434635ee4 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -12,7 +12,7 @@ "theme": { "mode": "system", "light": "One Light", - "dark": "One Dark" + "dark": "One Dark", }, "icon_theme": "Zed (Default)", // The name of a base set of key bindings to use. @@ -29,7 +29,7 @@ // Features that can be globally enabled or disabled "features": { // Which edit prediction provider to use. - "edit_prediction_provider": "zed" + "edit_prediction_provider": "zed", }, // The name of a font to use for rendering text in the editor // ".ZedMono" currently aliases to Lilex @@ -69,7 +69,7 @@ // The OpenType features to enable for text in the UI "ui_font_features": { // Disable ligatures: - "calt": false + "calt": false, }, // The weight of the UI font in standard CSS units from 100 to 900. "ui_font_weight": 400, @@ -87,7 +87,7 @@ "border_size": 0.0, // Opacity of the inactive panes. 0 means transparent, 1 means opaque. // Values are clamped to the [0.0, 1.0] range. - "inactive_opacity": 1.0 + "inactive_opacity": 1.0, }, // Layout mode of the bottom dock. Defaults to "contained" // choices: contained, full, left_aligned, right_aligned @@ -103,12 +103,12 @@ "left_padding": 0.2, // The relative width of the right padding of the central pane from the // workspace when the centered layout is used. - "right_padding": 0.2 + "right_padding": 0.2, }, // Image viewer settings "image_viewer": { // The unit for image file sizes: "binary" (KiB, MiB) or decimal (KB, MB) - "unit": "binary" + "unit": "binary", }, // Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. // @@ -296,7 +296,7 @@ // When true, enables drag and drop text selection in buffer. "enabled": true, // The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. - "delay": 300 + "delay": 300, }, // What to do when go to definition yields no results. // @@ -400,14 +400,14 @@ // Visible characters used to render whitespace when show_whitespaces is enabled. "whitespace_map": { "space": "•", - "tab": "→" + "tab": "→", }, // Settings related to calls in Zed "calls": { // Join calls with the microphone live by default "mute_on_join": false, // Share your project when you are the first to join a channel - "share_on_join": false + "share_on_join": false, }, // Toolbar related settings "toolbar": { @@ -420,7 +420,7 @@ // Whether to show agent review buttons in the editor toolbar. "agent_review": true, // Whether to show code action buttons in the editor toolbar. - "code_actions": false + "code_actions": false, }, // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). "use_system_window_tabs": false, @@ -439,7 +439,7 @@ // Whether to show the sign in button in the titlebar. "show_sign_in": true, // Whether to show the menus in the titlebar. - "show_menus": false + "show_menus": false, }, "audio": { // Opt into the new audio system. @@ -472,7 +472,7 @@ // the future we will migrate by setting this to false // // You need to rejoin a call for this setting to apply - "experimental.legacy_audio_compatible": true + "experimental.legacy_audio_compatible": true, }, // Scrollbar related settings "scrollbar": { @@ -511,8 +511,8 @@ // When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. "horizontal": true, // When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. - "vertical": true - } + "vertical": true, + }, }, // Minimap related settings "minimap": { @@ -560,7 +560,7 @@ // 3. "gutter" or "none" to not highlight the current line in the minimap. "current_line_highlight": null, // Maximum number of columns to display in the minimap. - "max_width_columns": 80 + "max_width_columns": 80, }, // Enable middle-click paste on Linux. "middle_click_paste": true, @@ -583,7 +583,7 @@ // Whether to show fold buttons in the gutter. "folds": true, // Minimum number of characters to reserve space for in the gutter. - "min_line_number_digits": 4 + "min_line_number_digits": 4, }, "indent_guides": { // Whether to show indent guides in the editor. @@ -604,7 +604,7 @@ // // 1. "disabled" // 2. "indent_aware" - "background_coloring": "disabled" + "background_coloring": "disabled", }, // Whether the editor will scroll beyond the last line. "scroll_beyond_last_line": "one_page", @@ -623,7 +623,7 @@ "fast_scroll_sensitivity": 4.0, "sticky_scroll": { // Whether to stick scopes to the top of the editor. - "enabled": false + "enabled": false, }, "relative_line_numbers": "disabled", // If 'search_wrap' is disabled, search result do not wrap around the end of the file. @@ -641,7 +641,7 @@ // Whether to interpret the search query as a regular expression. "regex": false, // Whether to center the cursor on each search match when navigating. - "center_on_match": false + "center_on_match": false, }, // When to populate a new search's query based on the text under the cursor. // This setting can take the following three values: @@ -684,8 +684,8 @@ "shift": false, "alt": false, "platform": false, - "function": false - } + "function": false, + }, }, // Whether to resize all the panels in a dock when resizing the dock. // Can be a combination of "left", "right" and "bottom". @@ -733,7 +733,7 @@ // "always" // 5. Never show the scrollbar: // "never" - "show": null + "show": null, }, // Which files containing diagnostic errors/warnings to mark in the project panel. // This setting can take the following three values: @@ -756,7 +756,7 @@ // "always" // 2. Never show indent guides: // "never" - "show": "always" + "show": "always", }, // Sort order for entries in the project panel. // This setting can take three values: @@ -781,8 +781,8 @@ // Whether to automatically open files after pasting or duplicating them. "on_paste": true, // Whether to automatically open files dropped from external sources. - "on_drop": true - } + "on_drop": true, + }, }, "outline_panel": { // Whether to show the outline panel button in the status bar @@ -815,7 +815,7 @@ // "always" // 2. Never show indent guides: // "never" - "show": "always" + "show": "always", }, // Scrollbar-related settings "scrollbar": { @@ -832,11 +832,11 @@ // "always" // 5. Never show the scrollbar: // "never" - "show": null + "show": null, }, // Default depth to expand outline items in the current file. // Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper. - "expand_outlines_with_depth": 100 + "expand_outlines_with_depth": 100, }, "collaboration_panel": { // Whether to show the collaboration panel button in the status bar. @@ -844,7 +844,7 @@ // Where to dock the collaboration panel. Can be 'left' or 'right'. "dock": "left", // Default width of the collaboration panel. - "default_width": 240 + "default_width": 240, }, "git_panel": { // Whether to show the git panel button in the status bar. @@ -876,12 +876,12 @@ // Choices: always, auto, never, system // Default: inherits editor scrollbar settings // "show": null - } + }, }, "message_editor": { // Whether to automatically replace emoji shortcodes with emoji characters. // For example: typing `:wave:` gets replaced with `👋`. - "auto_replace_emoji_shortcode": true + "auto_replace_emoji_shortcode": true, }, "notification_panel": { // Whether to show the notification panel button in the status bar. @@ -889,7 +889,7 @@ // Where to dock the notification panel. Can be 'left' or 'right'. "dock": "right", // Default width of the notification panel. - "default_width": 380 + "default_width": 380, }, "agent": { // Whether the agent is enabled. @@ -911,7 +911,7 @@ // The provider to use. "provider": "zed.dev", // The model to use. - "model": "claude-sonnet-4" + "model": "claude-sonnet-4", }, // Additional parameters for language model requests. When making a request to a model, parameters will be taken // from the last entry in this list that matches the model's provider and name. In each entry, both provider @@ -966,8 +966,8 @@ "grep": true, "terminal": true, "thinking": true, - "web_search": true - } + "web_search": true, + }, }, "ask": { "name": "Ask", @@ -984,14 +984,14 @@ "open": true, "grep": true, "thinking": true, - "web_search": true - } + "web_search": true, + }, }, "minimal": { "name": "Minimal", "enable_all_context_servers": false, - "tools": {} - } + "tools": {}, + }, }, // Where to show notifications when the agent has either completed // its response, or else needs confirmation before it can run a @@ -1020,7 +1020,7 @@ // Minimum number of lines to display in the agent message editor. // // Default: 4 - "message_editor_min_lines": 4 + "message_editor_min_lines": 4, }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, @@ -1055,7 +1055,7 @@ // Whether or not to show the navigation history buttons. "show_nav_history_buttons": true, // Whether or not to show the tab bar buttons. - "show_tab_bar_buttons": true + "show_tab_bar_buttons": true, }, // Settings related to the editor's tabs "tabs": { @@ -1094,7 +1094,7 @@ // "errors" // 3. Mark files with errors and warnings: // "all" - "show_diagnostics": "off" + "show_diagnostics": "off", }, // Settings related to preview tabs. "preview_tabs": { @@ -1115,7 +1115,7 @@ "enable_preview_file_from_code_navigation": true, // Whether to keep tabs in preview mode when code navigation is used to navigate away from them. // If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one. - "enable_keep_preview_on_code_navigation": false + "enable_keep_preview_on_code_navigation": false, }, // Settings related to the file finder. "file_finder": { @@ -1159,7 +1159,7 @@ // * "all": Use all gitignored files // * "indexed": Use only the files Zed had indexed // * "smart": Be smart and search for ignored when called from a gitignored worktree - "include_ignored": "smart" + "include_ignored": "smart", }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. @@ -1230,7 +1230,7 @@ // Send debug info like crash reports. "diagnostics": true, // Send anonymized usage data like what languages you're using Zed with. - "metrics": true + "metrics": true, }, // Whether to disable all AI features in Zed. // @@ -1264,7 +1264,7 @@ "enabled": true, // Minimum time to wait before pulling diagnostics from the language server(s). // 0 turns the debounce off. - "debounce_ms": 50 + "debounce_ms": 50, }, // Settings for inline diagnostics "inline": { @@ -1282,8 +1282,8 @@ "min_column": 0, // The minimum severity of the diagnostics to show inline. // Inherits editor's diagnostics' max severity settings when `null`. - "max_severity": null - } + "max_severity": null, + }, }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file // scans, file searches, and not be displayed in the project file tree. Takes precedence over `file_scan_inclusions`. @@ -1297,7 +1297,7 @@ "**/.DS_Store", "**/Thumbs.db", "**/.classpath", - "**/.settings" + "**/.settings", ], // Files or globs of files that will be included by Zed, even when ignored by git. This is useful // for files that are not tracked by git, but are still important to your project. Note that globs @@ -1332,14 +1332,14 @@ // Whether or not to display the git commit summary on the same line. "show_commit_summary": false, // The minimum column number to show the inline blame information at - "min_column": 0 + "min_column": 0, }, "blame": { - "show_avatar": true + "show_avatar": true, }, // Control which information is shown in the branch picker. "branch_picker": { - "show_author_name": true + "show_author_name": true, }, // How git hunks are displayed visually in the editor. // This setting can take two values: @@ -1351,7 +1351,7 @@ "hunk_style": "staged_hollow", // Should the name or path be displayed first in the git view. // "path_style": "file_name_first" or "file_path_first" - "path_style": "file_name_first" + "path_style": "file_name_first", }, // The list of custom Git hosting providers. "git_hosting_providers": [ @@ -1385,7 +1385,7 @@ "**/secrets.yml", "**/.zed/settings.json", // zed project settings "/**/zed/settings.json", // zed user settings - "/**/zed/keymap.json" + "/**/zed/keymap.json", ], // When to show edit predictions previews in buffer. // This setting takes two possible values: @@ -1403,15 +1403,15 @@ "copilot": { "enterprise_uri": null, "proxy": null, - "proxy_no_verify": null + "proxy_no_verify": null, }, "codestral": { "model": null, - "max_tokens": null + "max_tokens": null, }, // Whether edit predictions are enabled when editing text threads in the agent panel. // This setting has no effect if globally disabled. - "enabled_in_text_threads": true + "enabled_in_text_threads": true, }, // Settings specific to journaling "journal": { @@ -1421,7 +1421,7 @@ // May take 2 values: // 1. hour12 // 2. hour24 - "hour_format": "hour12" + "hour_format": "hour12", }, // Status bar-related settings. "status_bar": { @@ -1432,7 +1432,7 @@ // Whether to show the cursor position button in the status bar. "cursor_position_button": true, // Whether to show active line endings button in the status bar. - "line_endings_button": false + "line_endings_button": false, }, // Settings specific to the terminal "terminal": { @@ -1553,8 +1553,8 @@ // Preferred Conda manager to use when activating Conda environments. // Values: "auto", "conda", "mamba", "micromamba" // Default: "auto" - "conda_manager": "auto" - } + "conda_manager": "auto", + }, }, "toolbar": { // Whether to display the terminal title in its toolbar's breadcrumbs. @@ -1562,7 +1562,7 @@ // // The shell running in the terminal needs to be configured to emit the title. // Example: `echo -e "\e]2;New Title\007";` - "breadcrumbs": false + "breadcrumbs": false, }, // Scrollbar-related settings "scrollbar": { @@ -1579,7 +1579,7 @@ // "always" // 5. Never show the scrollbar: // "never" - "show": null + "show": null, }, // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. @@ -1660,12 +1660,12 @@ "# which may be followed by trailing punctuation", "[.,:)}\\]>]*", "# and always includes trailing whitespace or end of line", - "([ ]+|$)" - ] + "([ ]+|$)", + ], ], // Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a // timeout of `0` will disable path hyperlinking in terminal. - "path_hyperlink_timeout_ms": 1 + "path_hyperlink_timeout_ms": 1, }, "code_actions_on_format": {}, // Settings related to running tasks. @@ -1681,7 +1681,7 @@ // * Zed task from history (e.g. one-off task was spawned before) // // Default: true - "prefer_lsp": true + "prefer_lsp": true, }, // An object whose keys are language names, and whose values // are arrays of filenames or extensions of files that should @@ -1698,7 +1698,7 @@ "file_types": { "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"], "Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"], - "Shell Script": [".env.*"] + "Shell Script": [".env.*"], }, // Settings for which version of Node.js and NPM to use when installing // language servers and Copilot. @@ -1714,14 +1714,14 @@ // `path`, but not `npm_path`, Zed will assume that `npm` is located at // `${path}/../npm`. "path": null, - "npm_path": null + "npm_path": null, }, // The extensions that Zed should automatically install on startup. // // If you don't want any of these extensions, add this field to your settings // and change the value to `false`. "auto_install_extensions": { - "html": true + "html": true, }, // The capabilities granted to extensions. // @@ -1729,7 +1729,7 @@ "granted_extension_capabilities": [ { "kind": "process:exec", "command": "*", "args": ["**"] }, { "kind": "download_file", "host": "*", "path": ["**"] }, - { "kind": "npm:install", "package": "*" } + { "kind": "npm:install", "package": "*" }, ], // Controls how completions are processed for this language. "completions": { @@ -1780,7 +1780,7 @@ // 4. "replace_suffix" // Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like // `"insert"` otherwise. - "lsp_insert_mode": "replace_suffix" + "lsp_insert_mode": "replace_suffix", }, // Different settings for specific languages. "languages": { @@ -1788,113 +1788,113 @@ "language_servers": ["astro-language-server", "..."], "prettier": { "allowed": true, - "plugins": ["prettier-plugin-astro"] - } + "plugins": ["prettier-plugin-astro"], + }, }, "Blade": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "C": { "format_on_save": "off", "use_on_type_format": false, "prettier": { - "allowed": false - } + "allowed": false, + }, }, "C++": { "format_on_save": "off", "use_on_type_format": false, "prettier": { - "allowed": false - } + "allowed": false, + }, }, "CSS": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "Dart": { - "tab_size": 2 + "tab_size": 2, }, "Diff": { "show_edit_predictions": false, "remove_trailing_whitespace_on_save": false, - "ensure_final_newline_on_save": false + "ensure_final_newline_on_save": false, }, "Elixir": { - "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."] + "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."], }, "Elm": { - "tab_size": 4 + "tab_size": 4, }, "Erlang": { - "language_servers": ["erlang-ls", "!elp", "..."] + "language_servers": ["erlang-ls", "!elp", "..."], }, "Git Commit": { "allow_rewrap": "anywhere", "soft_wrap": "editor_width", - "preferred_line_length": 72 + "preferred_line_length": 72, }, "Go": { "hard_tabs": true, "code_actions_on_format": { - "source.organizeImports": true + "source.organizeImports": true, }, - "debuggers": ["Delve"] + "debuggers": ["Delve"], }, "GraphQL": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "HEEX": { - "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."] + "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."], }, "HTML": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "HTML+ERB": { - "language_servers": ["herb", "!ruby-lsp", "..."] + "language_servers": ["herb", "!ruby-lsp", "..."], }, "Java": { "prettier": { "allowed": true, - "plugins": ["prettier-plugin-java"] - } + "plugins": ["prettier-plugin-java"], + }, }, "JavaScript": { "language_servers": ["!typescript-language-server", "vtsls", "..."], "prettier": { - "allowed": true - } + "allowed": true, + }, }, "JSON": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "JSONC": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "JS+ERB": { - "language_servers": ["!ruby-lsp", "..."] + "language_servers": ["!ruby-lsp", "..."], }, "Kotlin": { - "language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."] + "language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."], }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], "prettier": { "allowed": true, - "plugins": ["prettier-plugin-latex"] - } + "plugins": ["prettier-plugin-latex"], + }, }, "Markdown": { "format_on_save": "off", @@ -1903,135 +1903,132 @@ "allow_rewrap": "anywhere", "soft_wrap": "editor_width", "prettier": { - "allowed": true - } + "allowed": true, + }, }, "PHP": { "language_servers": ["phpactor", "!intelephense", "!phptools", "..."], "prettier": { "allowed": true, "plugins": ["@prettier/plugin-php"], - "parser": "php" - } + "parser": "php", + }, }, "Plain Text": { "allow_rewrap": "anywhere", - "soft_wrap": "editor_width" + "soft_wrap": "editor_width", }, "Python": { "code_actions_on_format": { - "source.organizeImports.ruff": true + "source.organizeImports.ruff": true, }, "formatter": { "language_server": { - "name": "ruff" - } + "name": "ruff", + }, }, "debuggers": ["Debugpy"], - "language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."] + "language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."], }, "Ruby": { - "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."] + "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."], }, "Rust": { - "debuggers": ["CodeLLDB"] + "debuggers": ["CodeLLDB"], }, "SCSS": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "Starlark": { - "language_servers": ["starpls", "!buck2-lsp", "..."] + "language_servers": ["starpls", "!buck2-lsp", "..."], }, "Svelte": { "language_servers": ["svelte-language-server", "..."], "prettier": { "allowed": true, - "plugins": ["prettier-plugin-svelte"] - } + "plugins": ["prettier-plugin-svelte"], + }, }, "TSX": { "language_servers": ["!typescript-language-server", "vtsls", "..."], "prettier": { - "allowed": true - } + "allowed": true, + }, }, "Twig": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "TypeScript": { "language_servers": ["!typescript-language-server", "vtsls", "..."], "prettier": { - "allowed": true - } + "allowed": true, + }, }, "SystemVerilog": { "format_on_save": "off", "language_servers": ["!slang", "..."], - "use_on_type_format": false + "use_on_type_format": false, }, "Vue.js": { "language_servers": ["vue-language-server", "vtsls", "..."], "prettier": { - "allowed": true - } + "allowed": true, + }, }, "XML": { "prettier": { "allowed": true, - "plugins": ["@prettier/plugin-xml"] - } + "plugins": ["@prettier/plugin-xml"], + }, }, "YAML": { "prettier": { - "allowed": true - } + "allowed": true, + }, }, "YAML+ERB": { - "language_servers": ["!ruby-lsp", "..."] + "language_servers": ["!ruby-lsp", "..."], }, "Zig": { - "language_servers": ["zls", "..."] - } + "language_servers": ["zls", "..."], + }, }, // Different settings for specific language models. "language_models": { - "anthropic": { - "api_url": "https://api.anthropic.com" - }, "bedrock": {}, "google": { - "api_url": "https://generativelanguage.googleapis.com" + "api_url": "https://generativelanguage.googleapis.com", }, "ollama": { - "api_url": "http://localhost:11434" + "api_url": "http://localhost:11434", }, "openai": { - "api_url": "https://api.openai.com/v1" + "api_url": "https://api.openai.com/v1", }, "openai_compatible": {}, "open_router": { - "api_url": "https://openrouter.ai/api/v1" + "api_url": "https://openrouter.ai/api/v1", }, "lmstudio": { - "api_url": "http://localhost:1234/api/v0" + "api_url": "http://localhost:1234/api/v0", }, "deepseek": { - "api_url": "https://api.deepseek.com/v1" + "api_url": "https://api.deepseek.com/v1", }, "mistral": { - "api_url": "https://api.mistral.ai/v1" + "api_url": "https://api.mistral.ai/v1", }, "vercel": { - "api_url": "https://api.v0.dev/v1" + "api_url": "https://api.v0.dev/v1", }, "x_ai": { - "api_url": "https://api.x.ai/v1" + "api_url": "https://api.x.ai/v1", }, - "zed.dev": {} + "zed.dev": {}, }, "session": { // Whether or not to restore unsaved buffers on restart. @@ -2040,7 +2037,7 @@ // dirty files when closing the application. // // Default: true - "restore_unsaved_buffers": true + "restore_unsaved_buffers": true, }, // Zed's Prettier integration settings. // Allows to enable/disable formatting with Prettier @@ -2058,11 +2055,11 @@ // "singleQuote": true // Forces Prettier integration to use a specific parser name when formatting files with the language // when set to a non-empty string. - "parser": "" + "parser": "", }, // Settings for auto-closing of JSX tags. "jsx_tag_auto_close": { - "enabled": true + "enabled": true, }, // LSP Specific settings. "lsp": { @@ -2083,19 +2080,19 @@ // Specify the DAP name as a key here. "CodeLLDB": { "env": { - "RUST_LOG": "info" - } - } + "RUST_LOG": "info", + }, + }, }, // Common language server settings. "global_lsp_settings": { // Whether to show the LSP servers button in the status bar. - "button": true + "button": true, }, // Jupyter settings "jupyter": { "enabled": true, - "kernel_selections": {} + "kernel_selections": {}, // Specify the language name as the key and the kernel name as the value. // "kernel_selections": { // "python": "conda-base" @@ -2109,7 +2106,7 @@ "max_columns": 128, // Maximum number of lines to keep in REPL's scrollback buffer. // Clamped with [4, 256] range. - "max_lines": 32 + "max_lines": 32, }, // Vim settings "vim": { @@ -2123,7 +2120,7 @@ // Specify the mode as the key and the shape as the value. // The mode can be one of the following: "normal", "replace", "insert", "visual". // The shape can be one of the following: "block", "bar", "underline", "hollow". - "cursor_shape": {} + "cursor_shape": {}, }, // The server to connect to. If the environment variable // ZED_SERVER_URL is set, it will override this setting. @@ -2156,9 +2153,9 @@ "windows": { "languages": { "PHP": { - "language_servers": ["intelephense", "!phpactor", "!phptools", "..."] - } - } + "language_servers": ["intelephense", "!phpactor", "!phptools", "..."], + }, + }, }, // Whether to show full labels in line indicator or short ones // @@ -2217,7 +2214,7 @@ "dock": "bottom", "log_dap_communications": true, "format_dap_log_messages": true, - "button": true + "button": true, }, // Configures any number of settings profiles that are temporarily applied on // top of your existing user settings when selected from @@ -2244,5 +2241,5 @@ // Useful for filtering out noisy logs or enabling more verbose logging. // // Example: {"log": {"client": "warn"}} - "log": {} + "log": {}, } diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index 041401418c4272..06e25253ee626b 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -8,7 +8,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B use http_client::http::{self, HeaderMap, HeaderValue}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode}; use serde::{Deserialize, Serialize}; -pub use settings::{AnthropicAvailableModel as AvailableModel, ModelMode}; +pub use settings::ModelMode; use strum::{EnumIter, EnumString}; use thiserror::Error; diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index d28e2c1030c3c2..ce4ba4d3fa2aa3 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -9,7 +9,6 @@ use strum::{EnumIter, EnumString, IntoStaticStr}; #[strum(serialize_all = "snake_case")] pub enum IconName { Ai, - AiAnthropic, AiBedrock, AiClaude, AiDeepSeek, diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 445c6e11daa849..8b8ca1e2912e2a 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -14,7 +14,6 @@ pub mod provider; mod settings; pub mod ui; -use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; use crate::provider::cloud::CloudLanguageModelProvider; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; @@ -119,13 +118,6 @@ fn register_language_model_providers( )), cx, ); - registry.register_provider( - Arc::new(AnthropicLanguageModelProvider::new( - client.http_client(), - cx, - )), - cx, - ); registry.register_provider( Arc::new(OpenAiLanguageModelProvider::new(client.http_client(), cx)), cx, diff --git a/crates/language_models/src/provider.rs b/crates/language_models/src/provider.rs index d780195c66ec0d..e585fc06f6b523 100644 --- a/crates/language_models/src/provider.rs +++ b/crates/language_models/src/provider.rs @@ -1,4 +1,3 @@ -pub mod anthropic; pub mod bedrock; pub mod cloud; pub mod copilot_chat; diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs deleted file mode 100644 index 1affe38a08d22e..00000000000000 --- a/crates/language_models/src/provider/anthropic.rs +++ /dev/null @@ -1,1045 +0,0 @@ -use anthropic::{ - ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode, ContentDelta, Event, ResponseContent, - ToolResultContent, ToolResultPart, Usage, -}; -use anyhow::{Result, anyhow}; -use collections::{BTreeMap, HashMap}; -use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::BoxStream}; -use gpui::{AnyView, App, AsyncApp, Context, Entity, Task}; -use http_client::HttpClient; -use language_model::{ - AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, - LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId, - LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, - LanguageModelToolResultContent, MessageContent, RateLimiter, Role, -}; -use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; -use settings::{Settings, SettingsStore}; -use std::pin::Pin; -use std::str::FromStr; -use std::sync::{Arc, LazyLock}; -use strum::IntoEnumIterator; -use ui::{List, prelude::*}; -use ui_input::InputField; -use util::ResultExt; -use zed_env_vars::{EnvVar, env_var}; - -use crate::api_key::ApiKeyState; -use crate::ui::{ConfiguredApiCard, InstructionListItem}; - -pub use settings::AnthropicAvailableModel as AvailableModel; - -const PROVIDER_ID: LanguageModelProviderId = language_model::ANTHROPIC_PROVIDER_ID; -const PROVIDER_NAME: LanguageModelProviderName = language_model::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, -} - -pub struct AnthropicLanguageModelProvider { - http_client: Arc, - state: Entity, -} - -const API_KEY_ENV_VAR_NAME: &str = "ANTHROPIC_API_KEY"; -static API_KEY_ENV_VAR: LazyLock = env_var!(API_KEY_ENV_VAR_NAME); - -pub struct State { - api_key_state: ApiKeyState, -} - -impl State { - fn is_authenticated(&self) -> bool { - self.api_key_state.has_key() - } - - fn set_api_key(&mut self, api_key: Option, cx: &mut Context) -> Task> { - let api_url = AnthropicLanguageModelProvider::api_url(cx); - self.api_key_state - .store(api_url, api_key, |this| &mut this.api_key_state, cx) - } - - fn authenticate(&mut self, cx: &mut Context) -> Task> { - let api_url = AnthropicLanguageModelProvider::api_url(cx); - self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ) - } -} - -impl AnthropicLanguageModelProvider { - pub fn new(http_client: Arc, cx: &mut App) -> Self { - let state = cx.new(|cx| { - cx.observe_global::(|this: &mut State, cx| { - let api_url = Self::api_url(cx); - this.api_key_state.handle_url_change( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); - cx.notify(); - }) - .detach(); - State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), - } - }); - - Self { http_client, state } - } - - fn create_language_model(&self, model: anthropic::Model) -> Arc { - 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> { - Some(self.state.clone()) - } -} - -impl LanguageModelProvider for AnthropicLanguageModelProvider { - fn id(&self) -> LanguageModelProviderId { - PROVIDER_ID - } - - fn name(&self) -> LanguageModelProviderName { - PROVIDER_NAME - } - - fn icon(&self) -> IconName { - IconName::AiAnthropic - } - - fn default_model(&self, _cx: &App) -> Option> { - Some(self.create_language_model(anthropic::Model::default())) - } - - fn default_fast_model(&self, _cx: &App) -> Option> { - Some(self.create_language_model(anthropic::Model::default_fast())) - } - - fn recommended_models(&self, _cx: &App) -> Vec> { - [ - anthropic::Model::ClaudeSonnet4_5, - anthropic::Model::ClaudeSonnet4_5Thinking, - ] - .into_iter() - .map(|model| self.create_language_model(model)) - .collect() - } - - fn provided_models(&self, cx: &App) -> Vec> { - let mut models = BTreeMap::default(); - - // Add base models from anthropic::Model::iter() - for model in anthropic::Model::iter() { - if !matches!(model, anthropic::Model::Custom { .. }) { - models.insert(model.id().to_string(), model); - } - } - - // Override with available models from settings - for model in &AnthropicLanguageModelProvider::settings(cx).available_models { - models.insert( - model.name.clone(), - anthropic::Model::Custom { - name: model.name.clone(), - display_name: model.display_name.clone(), - max_tokens: model.max_tokens, - tool_override: model.tool_override.clone(), - cache_configuration: model.cache_configuration.as_ref().map(|config| { - anthropic::AnthropicModelCacheConfiguration { - max_cache_anchors: config.max_cache_anchors, - should_speculate: config.should_speculate, - min_total_token: config.min_total_token, - } - }), - max_output_tokens: model.max_output_tokens, - default_temperature: model.default_temperature, - extra_beta_headers: model.extra_beta_headers.clone(), - mode: model.mode.unwrap_or_default().into(), - }, - ); - } - - 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> { - 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> { - self.state - .update(cx, |state, cx| state.set_api_key(None, cx)) - } -} - -pub struct AnthropicModel { - id: LanguageModelId, - model: anthropic::Model, - state: Entity, - http_client: Arc, - request_limiter: RateLimiter, -} - -pub fn count_anthropic_tokens( - request: LanguageModelRequest, - cx: &App, -) -> BoxFuture<'static, Result> { - cx.background_spawn(async move { - let messages = request.messages; - let mut tokens_from_images = 0; - let mut string_messages = Vec::with_capacity(messages.len()); - - for message in messages { - use language_model::MessageContent; - - let mut string_contents = String::new(); - - for content in message.content { - match content { - MessageContent::Text(text) => { - string_contents.push_str(&text); - } - MessageContent::Thinking { .. } => { - // Thinking blocks are not included in the input token count. - } - MessageContent::RedactedThinking(_) => { - // Thinking blocks are not included in the input token count. - } - MessageContent::Image(image) => { - tokens_from_images += image.estimate_tokens(); - } - MessageContent::ToolUse(_tool_use) => { - // TODO: Estimate token usage from tool uses. - } - MessageContent::ToolResult(tool_result) => match &tool_result.content { - LanguageModelToolResultContent::Text(text) => { - string_contents.push_str(text); - } - LanguageModelToolResultContent::Image(image) => { - tokens_from_images += image.estimate_tokens(); - } - }, - } - } - - if !string_contents.is_empty() { - string_messages.push(tiktoken_rs::ChatCompletionRequestMessage { - role: match message.role { - Role::User => "user".into(), - Role::Assistant => "assistant".into(), - Role::System => "system".into(), - }, - content: Some(string_contents), - name: None, - function_call: None, - }); - } - } - - // Tiktoken doesn't yet support these models, so we manually use the - // same tokenizer as GPT-4. - tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages) - .map(|tokens| (tokens + tokens_from_images) as u64) - }) - .boxed() -} - -impl AnthropicModel { - fn stream_completion( - &self, - request: anthropic::Request, - cx: &AsyncApp, - ) -> BoxFuture< - 'static, - Result< - BoxStream<'static, Result>, - LanguageModelCompletionError, - >, - > { - let http_client = self.http_client.clone(); - - let Ok((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) - }) else { - return future::ready(Err(anyhow!("App state dropped").into())).boxed(); - }; - - 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().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 { - true - } - - fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { - match choice { - LanguageModelToolChoice::Auto - | LanguageModelToolChoice::Any - | LanguageModelToolChoice::None => true, - } - } - - fn telemetry_id(&self) -> String { - format!("anthropic/{}", self.model.id()) - } - - fn api_key(&self, cx: &App) -> Option { - 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_token_count() - } - - fn max_output_tokens(&self) -> Option { - Some(self.model.max_output_tokens()) - } - - fn count_tokens( - &self, - request: LanguageModelRequest, - cx: &App, - ) -> BoxFuture<'static, Result> { - count_anthropic_tokens(request, cx) - } - - fn stream_completion( - &self, - request: LanguageModelRequest, - cx: &AsyncApp, - ) -> BoxFuture< - 'static, - Result< - BoxStream<'static, Result>, - LanguageModelCompletionError, - >, - > { - let request = into_anthropic( - request, - self.model.request_id().into(), - self.model.default_temperature(), - self.model.max_output_tokens(), - self.model.mode(), - ); - 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 { - self.model - .cache_configuration() - .map(|config| LanguageModelCacheConfiguration { - max_cache_anchors: config.max_cache_anchors, - should_speculate: config.should_speculate, - min_total_token: config.min_total_token, - }) - } -} - -pub fn into_anthropic( - request: LanguageModelRequest, - model: String, - default_temperature: f32, - max_output_tokens: u64, - mode: AnthropicModelMode, -) -> anthropic::Request { - let mut new_messages: Vec = Vec::new(); - let mut system_message = String::new(); - - for message in request.messages { - if message.contents_empty() { - continue; - } - - match message.role { - Role::User | Role::Assistant => { - let mut anthropic_message_content: Vec = message - .content - .into_iter() - .filter_map(|content| match content { - MessageContent::Text(text) => { - let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) { - text.trim_end().to_string() - } else { - text - }; - if !text.is_empty() { - Some(anthropic::RequestContent::Text { - text, - cache_control: None, - }) - } else { - None - } - } - MessageContent::Thinking { - text: thinking, - signature, - } => { - if !thinking.is_empty() { - Some(anthropic::RequestContent::Thinking { - thinking, - signature: signature.unwrap_or_default(), - cache_control: None, - }) - } else { - None - } - } - MessageContent::RedactedThinking(data) => { - if !data.is_empty() { - Some(anthropic::RequestContent::RedactedThinking { data }) - } else { - None - } - } - MessageContent::Image(image) => Some(anthropic::RequestContent::Image { - source: anthropic::ImageSource { - source_type: "base64".to_string(), - media_type: "image/png".to_string(), - data: image.source.to_string(), - }, - cache_control: None, - }), - MessageContent::ToolUse(tool_use) => { - Some(anthropic::RequestContent::ToolUse { - id: tool_use.id.to_string(), - name: tool_use.name.to_string(), - input: tool_use.input, - cache_control: None, - }) - } - MessageContent::ToolResult(tool_result) => { - Some(anthropic::RequestContent::ToolResult { - tool_use_id: tool_result.tool_use_id.to_string(), - is_error: tool_result.is_error, - content: match tool_result.content { - LanguageModelToolResultContent::Text(text) => { - ToolResultContent::Plain(text.to_string()) - } - LanguageModelToolResultContent::Image(image) => { - ToolResultContent::Multipart(vec![ToolResultPart::Image { - source: anthropic::ImageSource { - source_type: "base64".to_string(), - media_type: "image/png".to_string(), - data: image.source.to_string(), - }, - }]) - } - }, - cache_control: None, - }) - } - }) - .collect(); - let anthropic_role = match message.role { - Role::User => anthropic::Role::User, - Role::Assistant => anthropic::Role::Assistant, - Role::System => unreachable!("System role should never occur here"), - }; - if let Some(last_message) = new_messages.last_mut() - && last_message.role == anthropic_role - { - last_message.content.extend(anthropic_message_content); - continue; - } - - // Mark the last segment of the message as cached - if message.cache { - let cache_control_value = Some(anthropic::CacheControl { - cache_type: anthropic::CacheControlType::Ephemeral, - }); - for message_content in anthropic_message_content.iter_mut().rev() { - match message_content { - anthropic::RequestContent::RedactedThinking { .. } => { - // Caching is not possible, fallback to next message - } - anthropic::RequestContent::Text { cache_control, .. } - | anthropic::RequestContent::Thinking { cache_control, .. } - | anthropic::RequestContent::Image { cache_control, .. } - | anthropic::RequestContent::ToolUse { cache_control, .. } - | anthropic::RequestContent::ToolResult { cache_control, .. } => { - *cache_control = cache_control_value; - break; - } - } - } - } - - new_messages.push(anthropic::Message { - role: anthropic_role, - content: anthropic_message_content, - }); - } - Role::System => { - if !system_message.is_empty() { - system_message.push_str("\n\n"); - } - system_message.push_str(&message.string_contents()); - } - } - } - - anthropic::Request { - model, - messages: new_messages, - max_tokens: max_output_tokens, - system: if system_message.is_empty() { - None - } else { - Some(anthropic::StringOrContents::String(system_message)) - }, - thinking: if request.thinking_allowed - && let AnthropicModelMode::Thinking { budget_tokens } = mode - { - Some(anthropic::Thinking::Enabled { budget_tokens }) - } else { - None - }, - tools: request - .tools - .into_iter() - .map(|tool| anthropic::Tool { - name: tool.name, - description: tool.description, - input_schema: tool.input_schema, - }) - .collect(), - tool_choice: request.tool_choice.map(|choice| match choice { - LanguageModelToolChoice::Auto => anthropic::ToolChoice::Auto, - LanguageModelToolChoice::Any => anthropic::ToolChoice::Any, - LanguageModelToolChoice::None => anthropic::ToolChoice::None, - }), - metadata: None, - stop_sequences: Vec::new(), - temperature: request.temperature.or(Some(default_temperature)), - top_k: None, - top_p: None, - } -} - -pub struct AnthropicEventMapper { - tool_uses_by_index: HashMap, - usage: Usage, - stop_reason: StopReason, -} - -impl AnthropicEventMapper { - pub fn new() -> Self { - Self { - tool_uses_by_index: HashMap::default(), - usage: Usage::default(), - stop_reason: StopReason::EndTurn, - } - } - - pub fn map_stream( - mut self, - events: Pin>>>, - ) -> impl Stream> - { - events.flat_map(move |event| { - futures::stream::iter(match event { - Ok(event) => self.map_event(event), - Err(error) => vec![Err(error.into())], - }) - }) - } - - pub fn map_event( - &mut self, - event: Event, - ) -> Vec> { - match event { - Event::ContentBlockStart { - index, - content_block, - } => match content_block { - ResponseContent::Text { text } => { - vec![Ok(LanguageModelCompletionEvent::Text(text))] - } - ResponseContent::Thinking { thinking } => { - vec![Ok(LanguageModelCompletionEvent::Thinking { - text: thinking, - signature: None, - })] - } - ResponseContent::RedactedThinking { data } => { - vec![Ok(LanguageModelCompletionEvent::RedactedThinking { data })] - } - ResponseContent::ToolUse { id, name, .. } => { - self.tool_uses_by_index.insert( - index, - RawToolUse { - id, - name, - input_json: String::new(), - }, - ); - Vec::new() - } - }, - Event::ContentBlockDelta { index, delta } => match delta { - ContentDelta::TextDelta { text } => { - vec![Ok(LanguageModelCompletionEvent::Text(text))] - } - ContentDelta::ThinkingDelta { thinking } => { - vec![Ok(LanguageModelCompletionEvent::Thinking { - text: thinking, - signature: None, - })] - } - ContentDelta::SignatureDelta { signature } => { - vec![Ok(LanguageModelCompletionEvent::Thinking { - text: "".to_string(), - signature: Some(signature), - })] - } - ContentDelta::InputJsonDelta { partial_json } => { - if let Some(tool_use) = self.tool_uses_by_index.get_mut(&index) { - tool_use.input_json.push_str(&partial_json); - - // Try to convert invalid (incomplete) JSON into - // valid JSON that serde can accept, e.g. by closing - // unclosed delimiters. This way, we can update the - // UI with whatever has been streamed back so far. - if let Ok(input) = serde_json::Value::from_str( - &partial_json_fixer::fix_json(&tool_use.input_json), - ) { - return vec![Ok(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: tool_use.id.clone().into(), - name: tool_use.name.clone().into(), - is_input_complete: false, - raw_input: tool_use.input_json.clone(), - input, - thought_signature: None, - }, - ))]; - } - } - vec![] - } - }, - Event::ContentBlockStop { index } => { - if let Some(tool_use) = self.tool_uses_by_index.remove(&index) { - let input_json = tool_use.input_json.trim(); - let input_value = if input_json.is_empty() { - Ok(serde_json::Value::Object(serde_json::Map::default())) - } else { - serde_json::Value::from_str(input_json) - }; - let event_result = match input_value { - Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: tool_use.id.into(), - name: tool_use.name.into(), - is_input_complete: true, - input, - raw_input: tool_use.input_json.clone(), - thought_signature: None, - }, - )), - Err(json_parse_err) => { - Ok(LanguageModelCompletionEvent::ToolUseJsonParseError { - id: tool_use.id.into(), - tool_name: tool_use.name.into(), - raw_input: input_json.into(), - json_parse_error: json_parse_err.to_string(), - }) - } - }; - - vec![event_result] - } else { - Vec::new() - } - } - Event::MessageStart { message } => { - update_usage(&mut self.usage, &message.usage); - vec![ - Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage( - &self.usage, - ))), - Ok(LanguageModelCompletionEvent::StartMessage { - message_id: message.id, - }), - ] - } - Event::MessageDelta { delta, usage } => { - update_usage(&mut self.usage, &usage); - if let Some(stop_reason) = delta.stop_reason.as_deref() { - self.stop_reason = match stop_reason { - "end_turn" => StopReason::EndTurn, - "max_tokens" => StopReason::MaxTokens, - "tool_use" => StopReason::ToolUse, - "refusal" => StopReason::Refusal, - _ => { - log::error!("Unexpected anthropic stop_reason: {stop_reason}"); - StopReason::EndTurn - } - }; - } - vec![Ok(LanguageModelCompletionEvent::UsageUpdate( - convert_usage(&self.usage), - ))] - } - Event::MessageStop => { - vec![Ok(LanguageModelCompletionEvent::Stop(self.stop_reason))] - } - Event::Error { error } => { - vec![Err(error.into())] - } - _ => Vec::new(), - } - } -} - -struct RawToolUse { - id: String, - name: String, - input_json: String, -} - -/// Updates usage data by preferring counts from `new`. -fn update_usage(usage: &mut Usage, new: &Usage) { - if let Some(input_tokens) = new.input_tokens { - usage.input_tokens = Some(input_tokens); - } - if let Some(output_tokens) = new.output_tokens { - usage.output_tokens = Some(output_tokens); - } - if let Some(cache_creation_input_tokens) = new.cache_creation_input_tokens { - usage.cache_creation_input_tokens = Some(cache_creation_input_tokens); - } - if let Some(cache_read_input_tokens) = new.cache_read_input_tokens { - usage.cache_read_input_tokens = Some(cache_read_input_tokens); - } -} - -fn convert_usage(usage: &Usage) -> language_model::TokenUsage { - language_model::TokenUsage { - input_tokens: usage.input_tokens.unwrap_or(0), - output_tokens: usage.output_tokens.unwrap_or(0), - cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0), - cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0), - } -} - -struct ConfigurationView { - api_key_editor: Entity, - state: Entity, - load_credentials_task: Option>, - target_agent: ConfigurationViewTargetAgent, -} - -impl ConfigurationView { - const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; - - fn new( - state: Entity, - target_agent: ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut Context, - ) -> Self { - 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) = state - .update(cx, |state, cx| state.authenticate(cx)) - .log_err() - { - // 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) { - 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.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) -> bool { - !self.state.read(cx).is_authenticated() - } -} - -impl Render for ConfigurationView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> 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( - InstructionListItem::new( - "Create one by visiting", - Some("Anthropic's settings"), - Some("https://console.anthropic.com/settings/keys") - ) - ) - .child( - InstructionListItem::text_only("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 assign 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))) - .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() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use anthropic::AnthropicModelMode; - use language_model::{LanguageModelRequestMessage, MessageContent}; - - #[test] - fn test_cache_control_only_on_last_segment() { - let request = LanguageModelRequest { - messages: vec![LanguageModelRequestMessage { - role: Role::User, - content: vec![ - MessageContent::Text("Some prompt".to_string()), - MessageContent::Image(language_model::LanguageModelImage::empty()), - MessageContent::Image(language_model::LanguageModelImage::empty()), - MessageContent::Image(language_model::LanguageModelImage::empty()), - MessageContent::Image(language_model::LanguageModelImage::empty()), - ], - cache: true, - reasoning_details: None, - }], - thread_id: None, - prompt_id: None, - intent: None, - mode: None, - stop: vec![], - temperature: None, - tools: vec![], - tool_choice: None, - thinking_allowed: true, - }; - - let anthropic_request = into_anthropic( - request, - "claude-3-5-sonnet".to_string(), - 0.7, - 4096, - AnthropicModelMode::Default, - ); - - assert_eq!(anthropic_request.messages.len(), 1); - - let message = &anthropic_request.messages[0]; - assert_eq!(message.content.len(), 5); - - assert!(matches!( - message.content[0], - anthropic::RequestContent::Text { - cache_control: None, - .. - } - )); - for i in 1..3 { - assert!(matches!( - message.content[i], - anthropic::RequestContent::Image { - cache_control: None, - .. - } - )); - } - - assert!(matches!( - message.content[4], - anthropic::RequestContent::Image { - cache_control: Some(anthropic::CacheControl { - cache_type: anthropic::CacheControlType::Ephemeral, - }), - .. - } - )); - } -} diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index a19a427dbacb32..8f08ddacee91e1 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -1,5 +1,8 @@ use ai_onboarding::YoungAccountBanner; -use anthropic::AnthropicModelMode; +use anthropic::{ + AnthropicModelMode, ContentDelta, Event, ResponseContent, ToolResultContent, ToolResultPart, + Usage, +}; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use client::{Client, ModelRequestUsage, UserStore, zed_urls}; @@ -23,8 +26,9 @@ use language_model::{ LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, - LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, - PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, + LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, + LanguageModelToolUseId, LlmApiToken, MessageContent, ModelRequestLimitReachedError, + PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, Role, StopReason, }; use release_channel::AppVersion; use schemars::JsonSchema; @@ -42,7 +46,6 @@ use thiserror::Error; use ui::{TintColor, prelude::*}; use util::{ResultExt as _, maybe}; -use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, into_anthropic}; use crate::provider::google::{GoogleEventMapper, into_google}; use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai}; use crate::provider::x_ai::count_xai_tokens; @@ -1394,3 +1397,435 @@ mod tests { } } } + +fn count_anthropic_tokens( + request: LanguageModelRequest, + cx: &App, +) -> BoxFuture<'static, Result> { + use gpui::AppContext as _; + cx.background_spawn(async move { + let messages = request.messages; + let mut tokens_from_images = 0; + let mut string_messages = Vec::with_capacity(messages.len()); + + for message in messages { + let mut string_contents = String::new(); + + for content in message.content { + match content { + MessageContent::Text(text) => { + string_contents.push_str(&text); + } + MessageContent::Thinking { .. } => {} + MessageContent::RedactedThinking(_) => {} + MessageContent::Image(image) => { + tokens_from_images += image.estimate_tokens(); + } + MessageContent::ToolUse(_tool_use) => {} + MessageContent::ToolResult(tool_result) => match &tool_result.content { + LanguageModelToolResultContent::Text(text) => { + string_contents.push_str(text); + } + LanguageModelToolResultContent::Image(image) => { + tokens_from_images += image.estimate_tokens(); + } + }, + } + } + + if !string_contents.is_empty() { + string_messages.push(tiktoken_rs::ChatCompletionRequestMessage { + role: match message.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: Some(string_contents), + name: None, + function_call: None, + }); + } + } + + tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages) + .map(|tokens| (tokens + tokens_from_images) as u64) + }) + .boxed() +} + +fn into_anthropic( + request: LanguageModelRequest, + model: String, + default_temperature: f32, + max_output_tokens: u64, + mode: AnthropicModelMode, +) -> anthropic::Request { + let mut new_messages: Vec = Vec::new(); + let mut system_message = String::new(); + + for message in request.messages { + if message.contents_empty() { + continue; + } + + match message.role { + Role::User | Role::Assistant => { + let mut anthropic_message_content: Vec = message + .content + .into_iter() + .filter_map(|content| match content { + MessageContent::Text(text) => { + let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) { + text.trim_end().to_string() + } else { + text + }; + if !text.is_empty() { + Some(anthropic::RequestContent::Text { + text, + cache_control: None, + }) + } else { + None + } + } + MessageContent::Thinking { + text: thinking, + signature, + } => { + if !thinking.is_empty() { + Some(anthropic::RequestContent::Thinking { + thinking, + signature: signature.unwrap_or_default(), + cache_control: None, + }) + } else { + None + } + } + MessageContent::RedactedThinking(data) => { + if !data.is_empty() { + Some(anthropic::RequestContent::RedactedThinking { data }) + } else { + None + } + } + MessageContent::Image(image) => Some(anthropic::RequestContent::Image { + source: anthropic::ImageSource { + source_type: "base64".to_string(), + media_type: "image/png".to_string(), + data: image.source.to_string(), + }, + cache_control: None, + }), + MessageContent::ToolUse(tool_use) => { + Some(anthropic::RequestContent::ToolUse { + id: tool_use.id.to_string(), + name: tool_use.name.to_string(), + input: tool_use.input, + cache_control: None, + }) + } + MessageContent::ToolResult(tool_result) => { + Some(anthropic::RequestContent::ToolResult { + tool_use_id: tool_result.tool_use_id.to_string(), + is_error: tool_result.is_error, + content: match tool_result.content { + LanguageModelToolResultContent::Text(text) => { + ToolResultContent::Plain(text.to_string()) + } + LanguageModelToolResultContent::Image(image) => { + ToolResultContent::Multipart(vec![ToolResultPart::Image { + source: anthropic::ImageSource { + source_type: "base64".to_string(), + media_type: "image/png".to_string(), + data: image.source.to_string(), + }, + }]) + } + }, + cache_control: None, + }) + } + }) + .collect(); + let anthropic_role = match message.role { + Role::User => anthropic::Role::User, + Role::Assistant => anthropic::Role::Assistant, + Role::System => unreachable!("System role should never occur here"), + }; + if let Some(last_message) = new_messages.last_mut() + && last_message.role == anthropic_role + { + last_message.content.extend(anthropic_message_content); + continue; + } + + if message.cache { + let cache_control_value = Some(anthropic::CacheControl { + cache_type: anthropic::CacheControlType::Ephemeral, + }); + for message_content in anthropic_message_content.iter_mut().rev() { + match message_content { + anthropic::RequestContent::RedactedThinking { .. } => {} + anthropic::RequestContent::Text { cache_control, .. } + | anthropic::RequestContent::Thinking { cache_control, .. } + | anthropic::RequestContent::Image { cache_control, .. } + | anthropic::RequestContent::ToolUse { cache_control, .. } + | anthropic::RequestContent::ToolResult { cache_control, .. } => { + *cache_control = cache_control_value; + break; + } + } + } + } + + new_messages.push(anthropic::Message { + role: anthropic_role, + content: anthropic_message_content, + }); + } + Role::System => { + if !system_message.is_empty() { + system_message.push_str("\n\n"); + } + system_message.push_str(&message.string_contents()); + } + } + } + + anthropic::Request { + model, + messages: new_messages, + max_tokens: max_output_tokens, + system: if system_message.is_empty() { + None + } else { + Some(anthropic::StringOrContents::String(system_message)) + }, + thinking: if request.thinking_allowed + && let AnthropicModelMode::Thinking { budget_tokens } = mode + { + Some(anthropic::Thinking::Enabled { budget_tokens }) + } else { + None + }, + tools: request + .tools + .into_iter() + .map(|tool| anthropic::Tool { + name: tool.name, + description: tool.description, + input_schema: tool.input_schema, + }) + .collect(), + tool_choice: request.tool_choice.map(|choice| match choice { + LanguageModelToolChoice::Auto => anthropic::ToolChoice::Auto, + LanguageModelToolChoice::Any => anthropic::ToolChoice::Any, + LanguageModelToolChoice::None => anthropic::ToolChoice::None, + }), + metadata: None, + stop_sequences: Vec::new(), + temperature: request.temperature.or(Some(default_temperature)), + top_k: None, + top_p: None, + } +} + +struct AnthropicEventMapper { + tool_uses_by_index: collections::HashMap, + usage: Usage, + stop_reason: StopReason, +} + +impl AnthropicEventMapper { + fn new() -> Self { + Self { + tool_uses_by_index: collections::HashMap::default(), + usage: Usage::default(), + stop_reason: StopReason::EndTurn, + } + } + + fn map_event( + &mut self, + event: Event, + ) -> Vec> { + match event { + Event::ContentBlockStart { + index, + content_block, + } => match content_block { + ResponseContent::Text { text } => { + vec![Ok(LanguageModelCompletionEvent::Text(text))] + } + ResponseContent::Thinking { thinking } => { + vec![Ok(LanguageModelCompletionEvent::Thinking { + text: thinking, + signature: None, + })] + } + ResponseContent::RedactedThinking { data } => { + vec![Ok(LanguageModelCompletionEvent::RedactedThinking { data })] + } + ResponseContent::ToolUse { id, name, .. } => { + self.tool_uses_by_index.insert( + index, + RawToolUse { + id, + name, + input_json: String::new(), + }, + ); + Vec::new() + } + }, + Event::ContentBlockDelta { index, delta } => match delta { + ContentDelta::TextDelta { text } => { + vec![Ok(LanguageModelCompletionEvent::Text(text))] + } + ContentDelta::ThinkingDelta { thinking } => { + vec![Ok(LanguageModelCompletionEvent::Thinking { + text: thinking, + signature: None, + })] + } + ContentDelta::SignatureDelta { signature } => { + vec![Ok(LanguageModelCompletionEvent::Thinking { + text: "".to_string(), + signature: Some(signature), + })] + } + ContentDelta::InputJsonDelta { partial_json } => { + if let Some(tool_use) = self.tool_uses_by_index.get_mut(&index) { + tool_use.input_json.push_str(&partial_json); + + let event = serde_json::from_str::(&tool_use.input_json) + .ok() + .and_then(|input| { + let input_json_roundtripped = + serde_json::to_string(&input).ok()?.to_string(); + + if !tool_use.input_json.starts_with(&input_json_roundtripped) { + return None; + } + + Some(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: LanguageModelToolUseId::from(tool_use.id.clone()), + name: tool_use.name.clone().into(), + raw_input: tool_use.input_json.clone(), + input, + is_input_complete: false, + thought_signature: None, + }, + )) + }); + + if let Some(event) = event { + vec![Ok(event)] + } else { + Vec::new() + } + } else { + Vec::new() + } + } + }, + Event::ContentBlockStop { index } => { + if let Some(tool_use) = self.tool_uses_by_index.remove(&index) { + let event_result = match serde_json::from_str(&tool_use.input_json) { + Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: LanguageModelToolUseId::from(tool_use.id), + name: tool_use.name.into(), + raw_input: tool_use.input_json, + input, + is_input_complete: true, + thought_signature: None, + }, + )), + Err(json_parse_err) => { + Ok(LanguageModelCompletionEvent::ToolUseJsonParseError { + id: LanguageModelToolUseId::from(tool_use.id), + tool_name: tool_use.name.into(), + raw_input: tool_use.input_json.into(), + json_parse_error: json_parse_err.to_string(), + }) + } + }; + + vec![event_result] + } else { + Vec::new() + } + } + Event::MessageStart { message } => { + update_anthropic_usage(&mut self.usage, &message.usage); + vec![ + Ok(LanguageModelCompletionEvent::UsageUpdate( + convert_anthropic_usage(&self.usage), + )), + Ok(LanguageModelCompletionEvent::StartMessage { + message_id: message.id, + }), + ] + } + Event::MessageDelta { delta, usage } => { + update_anthropic_usage(&mut self.usage, &usage); + if let Some(stop_reason) = delta.stop_reason.as_deref() { + self.stop_reason = match stop_reason { + "end_turn" => StopReason::EndTurn, + "max_tokens" => StopReason::MaxTokens, + "tool_use" => StopReason::ToolUse, + "refusal" => StopReason::Refusal, + _ => { + log::error!("Unexpected anthropic stop_reason: {stop_reason}"); + StopReason::EndTurn + } + }; + } + vec![Ok(LanguageModelCompletionEvent::UsageUpdate( + convert_anthropic_usage(&self.usage), + ))] + } + Event::MessageStop => { + vec![Ok(LanguageModelCompletionEvent::Stop(self.stop_reason))] + } + Event::Error { error } => { + vec![Err(error.into())] + } + _ => Vec::new(), + } + } +} + +struct RawToolUse { + id: String, + name: String, + input_json: String, +} + +fn update_anthropic_usage(usage: &mut Usage, new: &Usage) { + if let Some(input_tokens) = new.input_tokens { + usage.input_tokens = Some(input_tokens); + } + if let Some(output_tokens) = new.output_tokens { + usage.output_tokens = Some(output_tokens); + } + if let Some(cache_creation_input_tokens) = new.cache_creation_input_tokens { + usage.cache_creation_input_tokens = Some(cache_creation_input_tokens); + } + if let Some(cache_read_input_tokens) = new.cache_read_input_tokens { + usage.cache_read_input_tokens = Some(cache_read_input_tokens); + } +} + +fn convert_anthropic_usage(usage: &Usage) -> language_model::TokenUsage { + language_model::TokenUsage { + input_tokens: usage.input_tokens.unwrap_or(0), + output_tokens: usage.output_tokens.unwrap_or(0), + cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0), + cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0), + } +} diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index 43a8e7334a744c..15a3c936705194 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -4,16 +4,14 @@ 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, - vercel::VercelSettings, x_ai::XAiSettings, + bedrock::AmazonBedrockSettings, cloud::ZedDotDevSettings, deepseek::DeepSeekSettings, + google::GoogleSettings, lmstudio::LmStudioSettings, mistral::MistralSettings, + ollama::OllamaSettings, open_ai::OpenAiSettings, open_ai_compatible::OpenAiCompatibleSettings, + open_router::OpenRouterSettings, vercel::VercelSettings, x_ai::XAiSettings, }; #[derive(Debug, RegisterSetting)] pub struct AllLanguageModelSettings { - pub anthropic: AnthropicSettings, pub bedrock: AmazonBedrockSettings, pub deepseek: DeepSeekSettings, pub google: GoogleSettings, @@ -33,7 +31,6 @@ impl settings::Settings for AllLanguageModelSettings { 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(); @@ -47,10 +44,6 @@ impl settings::Settings for AllLanguageModelSettings { 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, diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index 2ea9f0cd5788f3..e875c95f1c89b6 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -255,7 +255,6 @@ impl JsonSchema for LanguageModelProviderSetting { "type": "string", "enum": [ "amazon-bedrock", - "anthropic", "copilot_chat", "deepseek", "google", diff --git a/crates/settings/src/settings_content/language_model.rs b/crates/settings/src/settings_content/language_model.rs index 48f5a463a4b8d8..f99e1687130d80 100644 --- a/crates/settings/src/settings_content/language_model.rs +++ b/crates/settings/src/settings_content/language_model.rs @@ -8,7 +8,6 @@ use std::sync::Arc; #[with_fallible_options] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] pub struct AllLanguageModelSettingsContent { - pub anthropic: Option, pub bedrock: Option, pub deepseek: Option, pub google: Option, @@ -24,35 +23,6 @@ pub struct AllLanguageModelSettingsContent { pub zed_dot_dev: Option, } -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct AnthropicSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct AnthropicAvailableModel { - /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc - pub name: String, - /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. - pub display_name: Option, - /// The model's context window size. - pub max_tokens: u64, - /// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling. - pub tool_override: Option, - /// Configuration of Anthropic's caching API. - pub cache_configuration: Option, - pub max_output_tokens: Option, - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_temperature: Option, - #[serde(default)] - pub extra_beta_headers: Vec, - /// The model's mode (e.g. thinking) - pub mode: Option, -} - #[with_fallible_options] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] pub struct AmazonBedrockSettingsContent { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 477885a4537580..63ad054bfae1a2 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2626,9 +2626,6 @@ These values take in the same options as the root-level settings with the same n ```json [settings] { "language_models": { - "anthropic": { - "api_url": "https://api.anthropic.com" - }, "google": { "api_url": "https://generativelanguage.googleapis.com" }, From 7183b8a1cdbb1efcaabe96514bc44150803fb55e Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 12:59:19 -0500 Subject: [PATCH 32/45] Fix API key bug --- crates/extension_host/src/wasm_host.rs | 10 ++-- .../src/wasm_host/wit/since_v0_8_0.rs | 46 +++++++++++++------ crates/language_models/src/provider/cloud.rs | 3 +- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index cd0b99cc02499b..b9cb265ec59de6 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -5,6 +5,7 @@ use crate::capability_granter::CapabilityGranter; use crate::{ExtensionManifest, ExtensionSettings}; use anyhow::{Context as _, Result, anyhow, bail}; use async_trait::async_trait; +use collections::HashSet; use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest}; use extension::{ CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary, @@ -59,6 +60,8 @@ pub struct WasmHost { pub work_dir: PathBuf, /// The capabilities granted to extensions running on the host. pub(crate) granted_capabilities: Vec, + /// Extension LLM providers allowed to read API keys from environment variables. + pub(crate) allowed_env_var_providers: HashSet>, _main_thread_message_task: Task<()>, main_thread_message_tx: mpsc::UnboundedSender, } @@ -73,12 +76,6 @@ pub struct WasmExtension { _task: Arc>>, } -impl Drop for WasmExtension { - fn drop(&mut self) { - self.tx.close_channel(); - } -} - #[async_trait] impl extension::Extension for WasmExtension { fn manifest(&self) -> Arc { @@ -591,6 +588,7 @@ impl WasmHost { proxy, release_channel: ReleaseChannel::global(cx), granted_capabilities: extension_settings.granted_capabilities.clone(), + allowed_env_var_providers: extension_settings.allowed_env_var_providers.clone(), _main_thread_message_task: task, main_thread_message_tx: tx, }) diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index b469349fb8c8a8..1b6d5c4d4ea964 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -1,4 +1,3 @@ -use crate::ExtensionSettings; use crate::wasm_host::wit::since_v0_8_0::{ dap::{ AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest, @@ -1129,6 +1128,33 @@ impl llm_provider::Host for WasmState { async fn get_credential(&mut self, provider_id: String) -> wasmtime::Result> { let extension_id = self.manifest.id.clone(); + + // Check if this provider has an env var configured and if the user has allowed it + let env_var_name = self + .manifest + .language_model_providers + .get(&Arc::::from(provider_id.as_str())) + .and_then(|entry| entry.auth.as_ref()) + .and_then(|auth| auth.env_var.clone()); + + if let Some(env_var_name) = env_var_name { + let full_provider_id: Arc = format!("{}:{}", extension_id, provider_id).into(); + // Use cached settings from WasmHost instead of going to main thread + let is_allowed = self + .host + .allowed_env_var_providers + .contains(&full_provider_id); + + if is_allowed { + if let Ok(value) = env::var(&env_var_name) { + if !value.is_empty() { + return Ok(Some(value)); + } + } + } + } + + // Fall back to credential store let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id); self.on_main_thread(move |cx| { @@ -1214,20 +1240,12 @@ impl llm_provider::Host for WasmState { }; // Check if the user has allowed this provider to read env vars - let full_provider_id = format!("{}:{}", extension_id, provider_id); + // Use cached settings from WasmHost instead of going to main thread + let full_provider_id: Arc = format!("{}:{}", extension_id, provider_id).into(); let is_allowed = self - .on_main_thread(move |cx| { - async move { - cx.update(|cx| { - ExtensionSettings::get_global(cx) - .allowed_env_var_providers - .contains(full_provider_id.as_str()) - }) - .unwrap_or(false) - } - .boxed_local() - }) - .await; + .host + .allowed_env_var_providers + .contains(&full_provider_id); if !is_allowed { log::debug!( diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 8f08ddacee91e1..3730db2b42654b 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -1703,8 +1703,7 @@ impl AnthropicEventMapper { let event = serde_json::from_str::(&tool_use.input_json) .ok() .and_then(|input| { - let input_json_roundtripped = - serde_json::to_string(&input).ok()?.to_string(); + let input_json_roundtripped = serde_json::to_string(&input).ok()?; if !tool_use.input_json.starts_with(&input_json_roundtripped) { return None; From cc5f5e35e4d2daa5bf8d9255a839455e01677012 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 13:00:19 -0500 Subject: [PATCH 33/45] Clean up some comments --- crates/extension_api/wit/since_v0.8.0/extension.wit | 4 ---- crates/extension_api/wit/since_v0.8.0/llm-provider.wit | 8 -------- crates/extension_host/src/wasm_host/wit.rs | 4 ---- 3 files changed, 16 deletions(-) diff --git a/crates/extension_api/wit/since_v0.8.0/extension.wit b/crates/extension_api/wit/since_v0.8.0/extension.wit index 8724369ec0472f..b972b78a80ed56 100644 --- a/crates/extension_api/wit/since_v0.8.0/extension.wit +++ b/crates/extension_api/wit/since_v0.8.0/extension.wit @@ -170,10 +170,6 @@ world extension { export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option; export run-dap-locator: func(locator-name: string, config: resolved-task) -> result; - // ========================================================================= - // Language Model Provider Extension API - // ========================================================================= - /// Returns information about language model providers offered by this extension. export llm-providers: func() -> list; diff --git a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit index abf113c3910279..c0d6b371a88e9f 100644 --- a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit +++ b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit @@ -253,10 +253,6 @@ interface llm-provider { min-total-token-count: u64, } - // ========================================================================= - // OAuth Web Auth Flow Types - // ========================================================================= - /// Configuration for starting an OAuth web authentication flow. record oauth-web-auth-config { /// The URL to open in the user's browser to start authentication. @@ -300,10 +296,6 @@ interface llm-provider { body: string, } - // ========================================================================= - // Import Functions (callable by extensions) - // ========================================================================= - /// Request a credential from the user. /// Returns true if the credential was provided, false if the user cancelled. request-credential: func( diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index aeefa72237175f..eef2e2c79ea148 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -1176,10 +1176,6 @@ impl Extension { } } - // ========================================================================= - // LLM Provider Methods (v0.8.0+) - // ========================================================================= - pub async fn call_llm_providers( &self, store: &mut Store, From d1e77397c6de33b9fdeb6aec32386dbd2d48f72e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 5 Dec 2025 13:07:36 -0500 Subject: [PATCH 34/45] Don't make v0.8.0 available on Stable/Preview yet --- crates/extension_host/src/wasm_host/wit.rs | 2 +- crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index eef2e2c79ea148..3b77c7b178709e 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -80,7 +80,7 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive let max_version = match release_channel { ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION, - ReleaseChannel::Stable | ReleaseChannel::Preview => latest::MAX_VERSION, + ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_6_0::MAX_VERSION, }; since_v0_0_1::MIN_VERSION..=max_version diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index 4df9174362749c..45bec57ee376aa 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -9,7 +9,6 @@ use wasmtime::component::{Linker, Resource}; use super::latest; pub const MIN_VERSION: Version = Version::new(0, 6, 0); -#[allow(dead_code)] pub const MAX_VERSION: Version = Version::new(0, 7, 0); wasmtime::component::bindgen!({ From e2b49b3cd3407c6ca0f4bf783a35fb8e185cdd1d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 5 Dec 2025 13:08:30 -0500 Subject: [PATCH 35/45] Restore blank lines from `main` --- crates/extension_host/src/wasm_host/wit.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 3b77c7b178709e..946b4864f4f694 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -1010,6 +1010,7 @@ impl Extension { } } } + pub async fn call_get_dap_binary( &self, store: &mut Store, @@ -1050,6 +1051,7 @@ impl Extension { _ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"), } } + pub async fn call_dap_request_kind( &self, store: &mut Store, @@ -1080,6 +1082,7 @@ impl Extension { _ => anyhow::bail!("`dap_request_kind` not available prior to v0.6.0"), } } + pub async fn call_dap_config_to_scenario( &self, store: &mut Store, @@ -1107,6 +1110,7 @@ impl Extension { _ => anyhow::bail!("`dap_config_to_scenario` not available prior to v0.6.0"), } } + pub async fn call_dap_locator_create_scenario( &self, store: &mut Store, @@ -1147,6 +1151,7 @@ impl Extension { _ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"), } } + pub async fn call_run_dap_locator( &self, store: &mut Store, From c9998541f0c7f29c8677cd30eb128bb19263eb18 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 5 Dec 2025 13:25:03 -0500 Subject: [PATCH 36/45] Revert spurious changes to `default.json` --- assets/settings/default.json | 333 ++++++++++++++++++----------------- 1 file changed, 168 insertions(+), 165 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 83c01434635ee4..f687778d7bd7fc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -12,7 +12,7 @@ "theme": { "mode": "system", "light": "One Light", - "dark": "One Dark", + "dark": "One Dark" }, "icon_theme": "Zed (Default)", // The name of a base set of key bindings to use. @@ -29,7 +29,7 @@ // Features that can be globally enabled or disabled "features": { // Which edit prediction provider to use. - "edit_prediction_provider": "zed", + "edit_prediction_provider": "zed" }, // The name of a font to use for rendering text in the editor // ".ZedMono" currently aliases to Lilex @@ -69,7 +69,7 @@ // The OpenType features to enable for text in the UI "ui_font_features": { // Disable ligatures: - "calt": false, + "calt": false }, // The weight of the UI font in standard CSS units from 100 to 900. "ui_font_weight": 400, @@ -87,7 +87,7 @@ "border_size": 0.0, // Opacity of the inactive panes. 0 means transparent, 1 means opaque. // Values are clamped to the [0.0, 1.0] range. - "inactive_opacity": 1.0, + "inactive_opacity": 1.0 }, // Layout mode of the bottom dock. Defaults to "contained" // choices: contained, full, left_aligned, right_aligned @@ -103,12 +103,12 @@ "left_padding": 0.2, // The relative width of the right padding of the central pane from the // workspace when the centered layout is used. - "right_padding": 0.2, + "right_padding": 0.2 }, // Image viewer settings "image_viewer": { // The unit for image file sizes: "binary" (KiB, MiB) or decimal (KB, MB) - "unit": "binary", + "unit": "binary" }, // Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. // @@ -296,7 +296,7 @@ // When true, enables drag and drop text selection in buffer. "enabled": true, // The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. - "delay": 300, + "delay": 300 }, // What to do when go to definition yields no results. // @@ -400,14 +400,14 @@ // Visible characters used to render whitespace when show_whitespaces is enabled. "whitespace_map": { "space": "•", - "tab": "→", + "tab": "→" }, // Settings related to calls in Zed "calls": { // Join calls with the microphone live by default "mute_on_join": false, // Share your project when you are the first to join a channel - "share_on_join": false, + "share_on_join": false }, // Toolbar related settings "toolbar": { @@ -420,7 +420,7 @@ // Whether to show agent review buttons in the editor toolbar. "agent_review": true, // Whether to show code action buttons in the editor toolbar. - "code_actions": false, + "code_actions": false }, // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). "use_system_window_tabs": false, @@ -439,7 +439,7 @@ // Whether to show the sign in button in the titlebar. "show_sign_in": true, // Whether to show the menus in the titlebar. - "show_menus": false, + "show_menus": false }, "audio": { // Opt into the new audio system. @@ -472,7 +472,7 @@ // the future we will migrate by setting this to false // // You need to rejoin a call for this setting to apply - "experimental.legacy_audio_compatible": true, + "experimental.legacy_audio_compatible": true }, // Scrollbar related settings "scrollbar": { @@ -511,8 +511,8 @@ // When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. "horizontal": true, // When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. - "vertical": true, - }, + "vertical": true + } }, // Minimap related settings "minimap": { @@ -560,7 +560,7 @@ // 3. "gutter" or "none" to not highlight the current line in the minimap. "current_line_highlight": null, // Maximum number of columns to display in the minimap. - "max_width_columns": 80, + "max_width_columns": 80 }, // Enable middle-click paste on Linux. "middle_click_paste": true, @@ -583,7 +583,7 @@ // Whether to show fold buttons in the gutter. "folds": true, // Minimum number of characters to reserve space for in the gutter. - "min_line_number_digits": 4, + "min_line_number_digits": 4 }, "indent_guides": { // Whether to show indent guides in the editor. @@ -604,7 +604,7 @@ // // 1. "disabled" // 2. "indent_aware" - "background_coloring": "disabled", + "background_coloring": "disabled" }, // Whether the editor will scroll beyond the last line. "scroll_beyond_last_line": "one_page", @@ -623,7 +623,7 @@ "fast_scroll_sensitivity": 4.0, "sticky_scroll": { // Whether to stick scopes to the top of the editor. - "enabled": false, + "enabled": false }, "relative_line_numbers": "disabled", // If 'search_wrap' is disabled, search result do not wrap around the end of the file. @@ -641,7 +641,7 @@ // Whether to interpret the search query as a regular expression. "regex": false, // Whether to center the cursor on each search match when navigating. - "center_on_match": false, + "center_on_match": false }, // When to populate a new search's query based on the text under the cursor. // This setting can take the following three values: @@ -684,8 +684,8 @@ "shift": false, "alt": false, "platform": false, - "function": false, - }, + "function": false + } }, // Whether to resize all the panels in a dock when resizing the dock. // Can be a combination of "left", "right" and "bottom". @@ -733,7 +733,7 @@ // "always" // 5. Never show the scrollbar: // "never" - "show": null, + "show": null }, // Which files containing diagnostic errors/warnings to mark in the project panel. // This setting can take the following three values: @@ -756,7 +756,7 @@ // "always" // 2. Never show indent guides: // "never" - "show": "always", + "show": "always" }, // Sort order for entries in the project panel. // This setting can take three values: @@ -781,8 +781,8 @@ // Whether to automatically open files after pasting or duplicating them. "on_paste": true, // Whether to automatically open files dropped from external sources. - "on_drop": true, - }, + "on_drop": true + } }, "outline_panel": { // Whether to show the outline panel button in the status bar @@ -815,7 +815,7 @@ // "always" // 2. Never show indent guides: // "never" - "show": "always", + "show": "always" }, // Scrollbar-related settings "scrollbar": { @@ -832,11 +832,11 @@ // "always" // 5. Never show the scrollbar: // "never" - "show": null, + "show": null }, // Default depth to expand outline items in the current file. // Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper. - "expand_outlines_with_depth": 100, + "expand_outlines_with_depth": 100 }, "collaboration_panel": { // Whether to show the collaboration panel button in the status bar. @@ -844,7 +844,7 @@ // Where to dock the collaboration panel. Can be 'left' or 'right'. "dock": "left", // Default width of the collaboration panel. - "default_width": 240, + "default_width": 240 }, "git_panel": { // Whether to show the git panel button in the status bar. @@ -876,12 +876,12 @@ // Choices: always, auto, never, system // Default: inherits editor scrollbar settings // "show": null - }, + } }, "message_editor": { // Whether to automatically replace emoji shortcodes with emoji characters. // For example: typing `:wave:` gets replaced with `👋`. - "auto_replace_emoji_shortcode": true, + "auto_replace_emoji_shortcode": true }, "notification_panel": { // Whether to show the notification panel button in the status bar. @@ -889,7 +889,7 @@ // Where to dock the notification panel. Can be 'left' or 'right'. "dock": "right", // Default width of the notification panel. - "default_width": 380, + "default_width": 380 }, "agent": { // Whether the agent is enabled. @@ -911,7 +911,7 @@ // The provider to use. "provider": "zed.dev", // The model to use. - "model": "claude-sonnet-4", + "model": "claude-sonnet-4" }, // Additional parameters for language model requests. When making a request to a model, parameters will be taken // from the last entry in this list that matches the model's provider and name. In each entry, both provider @@ -966,8 +966,8 @@ "grep": true, "terminal": true, "thinking": true, - "web_search": true, - }, + "web_search": true + } }, "ask": { "name": "Ask", @@ -984,14 +984,14 @@ "open": true, "grep": true, "thinking": true, - "web_search": true, - }, + "web_search": true + } }, "minimal": { "name": "Minimal", "enable_all_context_servers": false, - "tools": {}, - }, + "tools": {} + } }, // Where to show notifications when the agent has either completed // its response, or else needs confirmation before it can run a @@ -1020,7 +1020,7 @@ // Minimum number of lines to display in the agent message editor. // // Default: 4 - "message_editor_min_lines": 4, + "message_editor_min_lines": 4 }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, @@ -1055,7 +1055,7 @@ // Whether or not to show the navigation history buttons. "show_nav_history_buttons": true, // Whether or not to show the tab bar buttons. - "show_tab_bar_buttons": true, + "show_tab_bar_buttons": true }, // Settings related to the editor's tabs "tabs": { @@ -1094,7 +1094,7 @@ // "errors" // 3. Mark files with errors and warnings: // "all" - "show_diagnostics": "off", + "show_diagnostics": "off" }, // Settings related to preview tabs. "preview_tabs": { @@ -1115,7 +1115,7 @@ "enable_preview_file_from_code_navigation": true, // Whether to keep tabs in preview mode when code navigation is used to navigate away from them. // If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one. - "enable_keep_preview_on_code_navigation": false, + "enable_keep_preview_on_code_navigation": false }, // Settings related to the file finder. "file_finder": { @@ -1159,7 +1159,7 @@ // * "all": Use all gitignored files // * "indexed": Use only the files Zed had indexed // * "smart": Be smart and search for ignored when called from a gitignored worktree - "include_ignored": "smart", + "include_ignored": "smart" }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. @@ -1230,7 +1230,7 @@ // Send debug info like crash reports. "diagnostics": true, // Send anonymized usage data like what languages you're using Zed with. - "metrics": true, + "metrics": true }, // Whether to disable all AI features in Zed. // @@ -1264,7 +1264,7 @@ "enabled": true, // Minimum time to wait before pulling diagnostics from the language server(s). // 0 turns the debounce off. - "debounce_ms": 50, + "debounce_ms": 50 }, // Settings for inline diagnostics "inline": { @@ -1282,8 +1282,8 @@ "min_column": 0, // The minimum severity of the diagnostics to show inline. // Inherits editor's diagnostics' max severity settings when `null`. - "max_severity": null, - }, + "max_severity": null + } }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file // scans, file searches, and not be displayed in the project file tree. Takes precedence over `file_scan_inclusions`. @@ -1297,7 +1297,7 @@ "**/.DS_Store", "**/Thumbs.db", "**/.classpath", - "**/.settings", + "**/.settings" ], // Files or globs of files that will be included by Zed, even when ignored by git. This is useful // for files that are not tracked by git, but are still important to your project. Note that globs @@ -1332,14 +1332,14 @@ // Whether or not to display the git commit summary on the same line. "show_commit_summary": false, // The minimum column number to show the inline blame information at - "min_column": 0, + "min_column": 0 }, "blame": { - "show_avatar": true, + "show_avatar": true }, // Control which information is shown in the branch picker. "branch_picker": { - "show_author_name": true, + "show_author_name": true }, // How git hunks are displayed visually in the editor. // This setting can take two values: @@ -1351,7 +1351,7 @@ "hunk_style": "staged_hollow", // Should the name or path be displayed first in the git view. // "path_style": "file_name_first" or "file_path_first" - "path_style": "file_name_first", + "path_style": "file_name_first" }, // The list of custom Git hosting providers. "git_hosting_providers": [ @@ -1385,7 +1385,7 @@ "**/secrets.yml", "**/.zed/settings.json", // zed project settings "/**/zed/settings.json", // zed user settings - "/**/zed/keymap.json", + "/**/zed/keymap.json" ], // When to show edit predictions previews in buffer. // This setting takes two possible values: @@ -1403,15 +1403,15 @@ "copilot": { "enterprise_uri": null, "proxy": null, - "proxy_no_verify": null, + "proxy_no_verify": null }, "codestral": { "model": null, - "max_tokens": null, + "max_tokens": null }, // Whether edit predictions are enabled when editing text threads in the agent panel. // This setting has no effect if globally disabled. - "enabled_in_text_threads": true, + "enabled_in_text_threads": true }, // Settings specific to journaling "journal": { @@ -1421,7 +1421,7 @@ // May take 2 values: // 1. hour12 // 2. hour24 - "hour_format": "hour12", + "hour_format": "hour12" }, // Status bar-related settings. "status_bar": { @@ -1432,7 +1432,7 @@ // Whether to show the cursor position button in the status bar. "cursor_position_button": true, // Whether to show active line endings button in the status bar. - "line_endings_button": false, + "line_endings_button": false }, // Settings specific to the terminal "terminal": { @@ -1553,8 +1553,8 @@ // Preferred Conda manager to use when activating Conda environments. // Values: "auto", "conda", "mamba", "micromamba" // Default: "auto" - "conda_manager": "auto", - }, + "conda_manager": "auto" + } }, "toolbar": { // Whether to display the terminal title in its toolbar's breadcrumbs. @@ -1562,7 +1562,7 @@ // // The shell running in the terminal needs to be configured to emit the title. // Example: `echo -e "\e]2;New Title\007";` - "breadcrumbs": false, + "breadcrumbs": false }, // Scrollbar-related settings "scrollbar": { @@ -1579,7 +1579,7 @@ // "always" // 5. Never show the scrollbar: // "never" - "show": null, + "show": null }, // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. @@ -1660,12 +1660,12 @@ "# which may be followed by trailing punctuation", "[.,:)}\\]>]*", "# and always includes trailing whitespace or end of line", - "([ ]+|$)", - ], + "([ ]+|$)" + ] ], // Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a // timeout of `0` will disable path hyperlinking in terminal. - "path_hyperlink_timeout_ms": 1, + "path_hyperlink_timeout_ms": 1 }, "code_actions_on_format": {}, // Settings related to running tasks. @@ -1681,7 +1681,7 @@ // * Zed task from history (e.g. one-off task was spawned before) // // Default: true - "prefer_lsp": true, + "prefer_lsp": true }, // An object whose keys are language names, and whose values // are arrays of filenames or extensions of files that should @@ -1698,7 +1698,7 @@ "file_types": { "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"], "Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"], - "Shell Script": [".env.*"], + "Shell Script": [".env.*"] }, // Settings for which version of Node.js and NPM to use when installing // language servers and Copilot. @@ -1714,14 +1714,14 @@ // `path`, but not `npm_path`, Zed will assume that `npm` is located at // `${path}/../npm`. "path": null, - "npm_path": null, + "npm_path": null }, // The extensions that Zed should automatically install on startup. // // If you don't want any of these extensions, add this field to your settings // and change the value to `false`. "auto_install_extensions": { - "html": true, + "html": true }, // The capabilities granted to extensions. // @@ -1729,7 +1729,7 @@ "granted_extension_capabilities": [ { "kind": "process:exec", "command": "*", "args": ["**"] }, { "kind": "download_file", "host": "*", "path": ["**"] }, - { "kind": "npm:install", "package": "*" }, + { "kind": "npm:install", "package": "*" } ], // Controls how completions are processed for this language. "completions": { @@ -1780,7 +1780,7 @@ // 4. "replace_suffix" // Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like // `"insert"` otherwise. - "lsp_insert_mode": "replace_suffix", + "lsp_insert_mode": "replace_suffix" }, // Different settings for specific languages. "languages": { @@ -1788,113 +1788,113 @@ "language_servers": ["astro-language-server", "..."], "prettier": { "allowed": true, - "plugins": ["prettier-plugin-astro"], - }, + "plugins": ["prettier-plugin-astro"] + } }, "Blade": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "C": { "format_on_save": "off", "use_on_type_format": false, "prettier": { - "allowed": false, - }, + "allowed": false + } }, "C++": { "format_on_save": "off", "use_on_type_format": false, "prettier": { - "allowed": false, - }, + "allowed": false + } }, "CSS": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "Dart": { - "tab_size": 2, + "tab_size": 2 }, "Diff": { "show_edit_predictions": false, "remove_trailing_whitespace_on_save": false, - "ensure_final_newline_on_save": false, + "ensure_final_newline_on_save": false }, "Elixir": { - "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."], + "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."] }, "Elm": { - "tab_size": 4, + "tab_size": 4 }, "Erlang": { - "language_servers": ["erlang-ls", "!elp", "..."], + "language_servers": ["erlang-ls", "!elp", "..."] }, "Git Commit": { "allow_rewrap": "anywhere", "soft_wrap": "editor_width", - "preferred_line_length": 72, + "preferred_line_length": 72 }, "Go": { "hard_tabs": true, "code_actions_on_format": { - "source.organizeImports": true, + "source.organizeImports": true }, - "debuggers": ["Delve"], + "debuggers": ["Delve"] }, "GraphQL": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "HEEX": { - "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."], + "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."] }, "HTML": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "HTML+ERB": { - "language_servers": ["herb", "!ruby-lsp", "..."], + "language_servers": ["herb", "!ruby-lsp", "..."] }, "Java": { "prettier": { "allowed": true, - "plugins": ["prettier-plugin-java"], - }, + "plugins": ["prettier-plugin-java"] + } }, "JavaScript": { "language_servers": ["!typescript-language-server", "vtsls", "..."], "prettier": { - "allowed": true, - }, + "allowed": true + } }, "JSON": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "JSONC": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "JS+ERB": { - "language_servers": ["!ruby-lsp", "..."], + "language_servers": ["!ruby-lsp", "..."] }, "Kotlin": { - "language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."], + "language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."] }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], "prettier": { "allowed": true, - "plugins": ["prettier-plugin-latex"], - }, + "plugins": ["prettier-plugin-latex"] + } }, "Markdown": { "format_on_save": "off", @@ -1903,132 +1903,135 @@ "allow_rewrap": "anywhere", "soft_wrap": "editor_width", "prettier": { - "allowed": true, - }, + "allowed": true + } }, "PHP": { "language_servers": ["phpactor", "!intelephense", "!phptools", "..."], "prettier": { "allowed": true, "plugins": ["@prettier/plugin-php"], - "parser": "php", - }, + "parser": "php" + } }, "Plain Text": { "allow_rewrap": "anywhere", - "soft_wrap": "editor_width", + "soft_wrap": "editor_width" }, "Python": { "code_actions_on_format": { - "source.organizeImports.ruff": true, + "source.organizeImports.ruff": true }, "formatter": { "language_server": { - "name": "ruff", - }, + "name": "ruff" + } }, "debuggers": ["Debugpy"], - "language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."], + "language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."] }, "Ruby": { - "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."], + "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."] }, "Rust": { - "debuggers": ["CodeLLDB"], + "debuggers": ["CodeLLDB"] }, "SCSS": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "Starlark": { - "language_servers": ["starpls", "!buck2-lsp", "..."], + "language_servers": ["starpls", "!buck2-lsp", "..."] }, "Svelte": { "language_servers": ["svelte-language-server", "..."], "prettier": { "allowed": true, - "plugins": ["prettier-plugin-svelte"], - }, + "plugins": ["prettier-plugin-svelte"] + } }, "TSX": { "language_servers": ["!typescript-language-server", "vtsls", "..."], "prettier": { - "allowed": true, - }, + "allowed": true + } }, "Twig": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "TypeScript": { "language_servers": ["!typescript-language-server", "vtsls", "..."], "prettier": { - "allowed": true, - }, + "allowed": true + } }, "SystemVerilog": { "format_on_save": "off", "language_servers": ["!slang", "..."], - "use_on_type_format": false, + "use_on_type_format": false }, "Vue.js": { "language_servers": ["vue-language-server", "vtsls", "..."], "prettier": { - "allowed": true, - }, + "allowed": true + } }, "XML": { "prettier": { "allowed": true, - "plugins": ["@prettier/plugin-xml"], - }, + "plugins": ["@prettier/plugin-xml"] + } }, "YAML": { "prettier": { - "allowed": true, - }, + "allowed": true + } }, "YAML+ERB": { - "language_servers": ["!ruby-lsp", "..."], + "language_servers": ["!ruby-lsp", "..."] }, "Zig": { - "language_servers": ["zls", "..."], - }, + "language_servers": ["zls", "..."] + } }, // Different settings for specific language models. "language_models": { + "anthropic": { + "api_url": "https://api.anthropic.com" + }, "bedrock": {}, "google": { - "api_url": "https://generativelanguage.googleapis.com", + "api_url": "https://generativelanguage.googleapis.com" }, "ollama": { - "api_url": "http://localhost:11434", + "api_url": "http://localhost:11434" }, "openai": { - "api_url": "https://api.openai.com/v1", + "api_url": "https://api.openai.com/v1" }, "openai_compatible": {}, "open_router": { - "api_url": "https://openrouter.ai/api/v1", + "api_url": "https://openrouter.ai/api/v1" }, "lmstudio": { - "api_url": "http://localhost:1234/api/v0", + "api_url": "http://localhost:1234/api/v0" }, "deepseek": { - "api_url": "https://api.deepseek.com/v1", + "api_url": "https://api.deepseek.com/v1" }, "mistral": { - "api_url": "https://api.mistral.ai/v1", + "api_url": "https://api.mistral.ai/v1" }, "vercel": { - "api_url": "https://api.v0.dev/v1", + "api_url": "https://api.v0.dev/v1" }, "x_ai": { - "api_url": "https://api.x.ai/v1", + "api_url": "https://api.x.ai/v1" }, - "zed.dev": {}, + "zed.dev": {} }, "session": { // Whether or not to restore unsaved buffers on restart. @@ -2037,7 +2040,7 @@ // dirty files when closing the application. // // Default: true - "restore_unsaved_buffers": true, + "restore_unsaved_buffers": true }, // Zed's Prettier integration settings. // Allows to enable/disable formatting with Prettier @@ -2055,11 +2058,11 @@ // "singleQuote": true // Forces Prettier integration to use a specific parser name when formatting files with the language // when set to a non-empty string. - "parser": "", + "parser": "" }, // Settings for auto-closing of JSX tags. "jsx_tag_auto_close": { - "enabled": true, + "enabled": true }, // LSP Specific settings. "lsp": { @@ -2080,19 +2083,19 @@ // Specify the DAP name as a key here. "CodeLLDB": { "env": { - "RUST_LOG": "info", - }, - }, + "RUST_LOG": "info" + } + } }, // Common language server settings. "global_lsp_settings": { // Whether to show the LSP servers button in the status bar. - "button": true, + "button": true }, // Jupyter settings "jupyter": { "enabled": true, - "kernel_selections": {}, + "kernel_selections": {} // Specify the language name as the key and the kernel name as the value. // "kernel_selections": { // "python": "conda-base" @@ -2106,7 +2109,7 @@ "max_columns": 128, // Maximum number of lines to keep in REPL's scrollback buffer. // Clamped with [4, 256] range. - "max_lines": 32, + "max_lines": 32 }, // Vim settings "vim": { @@ -2120,7 +2123,7 @@ // Specify the mode as the key and the shape as the value. // The mode can be one of the following: "normal", "replace", "insert", "visual". // The shape can be one of the following: "block", "bar", "underline", "hollow". - "cursor_shape": {}, + "cursor_shape": {} }, // The server to connect to. If the environment variable // ZED_SERVER_URL is set, it will override this setting. @@ -2153,9 +2156,9 @@ "windows": { "languages": { "PHP": { - "language_servers": ["intelephense", "!phpactor", "!phptools", "..."], - }, - }, + "language_servers": ["intelephense", "!phpactor", "!phptools", "..."] + } + } }, // Whether to show full labels in line indicator or short ones // @@ -2214,7 +2217,7 @@ "dock": "bottom", "log_dap_communications": true, "format_dap_log_messages": true, - "button": true, + "button": true }, // Configures any number of settings profiles that are temporarily applied on // top of your existing user settings when selected from @@ -2241,5 +2244,5 @@ // Useful for filtering out noisy logs or enabling more verbose logging. // // Example: {"log": {"client": "warn"}} - "log": {}, + "log": {} } From b90ac2dc075b7151c26c0d821c3fe7c3b39ca196 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 16:20:50 -0500 Subject: [PATCH 37/45] Fix Drop impl for WasmExtension --- crates/extension_host/src/wasm_host.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index b9cb265ec59de6..cfd75f7a10fce0 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -68,7 +68,7 @@ pub struct WasmHost { #[derive(Clone, Debug)] pub struct WasmExtension { - tx: UnboundedSender, + tx: Arc>, pub manifest: Arc, pub work_dir: Arc, #[allow(unused)] @@ -76,6 +76,15 @@ pub struct WasmExtension { _task: Arc>>, } +impl Drop for WasmExtension { + fn drop(&mut self) { + // Only close the channel when this is the last clone holding the sender + if Arc::strong_count(&self.tx) == 1 { + self.tx.close_channel(); + } + } +} + #[async_trait] impl extension::Extension for WasmExtension { fn manifest(&self) -> Arc { @@ -670,7 +679,7 @@ impl WasmHost { Ok(WasmExtension { manifest, work_dir, - tx, + tx: Arc::new(tx), zed_api_version, _task: task, }) From c89653bd073845e6056d8f400c9172b41336cb9b Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 17:07:25 -0500 Subject: [PATCH 38/45] Fix bugs around logging out from provider extensions --- crates/agent_ui/src/agent_ui.rs | 27 +++++++- crates/extension_host/src/extension_host.rs | 66 ++++++++++++------- .../extension_host/src/extension_settings.rs | 10 +++ crates/extension_host/src/wasm_host.rs | 5 +- .../src/wasm_host/llm_provider.rs | 14 ++++ .../src/wasm_host/wit/since_v0_8_0.rs | 40 ++++++++--- .../src/settings_content/extension.rs | 3 + 7 files changed, 127 insertions(+), 38 deletions(-) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 9e5d6cbe0d5ac0..4a5382c9e4d67e 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -371,26 +371,49 @@ fn update_active_language_model_from_settings(cx: &mut App) { } } - let default = settings.default_model.as_ref().map(to_selected_model); + // Filter out models from providers that are not authenticated + fn is_provider_authenticated( + selection: &LanguageModelSelection, + registry: &LanguageModelRegistry, + cx: &App, + ) -> bool { + let provider_id = LanguageModelProviderId::from(selection.provider.0.clone()); + registry + .provider(&provider_id) + .map_or(false, |provider| provider.is_authenticated(cx)) + } + + let registry = LanguageModelRegistry::global(cx); + let registry_ref = registry.read(cx); + + let default = settings + .default_model + .as_ref() + .filter(|s| is_provider_authenticated(s, registry_ref, cx)) + .map(to_selected_model); let inline_assistant = settings .inline_assistant_model .as_ref() + .filter(|s| is_provider_authenticated(s, registry_ref, cx)) .map(to_selected_model); let commit_message = settings .commit_message_model .as_ref() + .filter(|s| is_provider_authenticated(s, registry_ref, cx)) .map(to_selected_model); let thread_summary = settings .thread_summary_model .as_ref() + .filter(|s| is_provider_authenticated(s, registry_ref, cx)) .map(to_selected_model); let inline_alternatives = settings .inline_alternatives .iter() + .filter(|s| is_provider_authenticated(s, registry_ref, cx)) .map(to_selected_model) .collect::>(); - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + registry.update(cx, |registry, cx| { registry.select_default_model(default.as_ref(), cx); registry.select_inline_assistant_model(inline_assistant.as_ref(), cx); registry.select_commit_message_model(commit_message.as_ref(), cx); diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 3b2f1586a62ea8..ea6d52418fe693 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -82,7 +82,7 @@ const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); /// Extension IDs that are being migrated from hardcoded LLM providers. /// For backwards compatibility, if the user has the corresponding env var set, -/// we automatically enable env var reading for these extensions. +/// we automatically enable env var reading for these extensions on first install. const LEGACY_LLM_EXTENSION_IDS: &[&str] = &[ "anthropic", "copilot_chat", @@ -93,6 +93,9 @@ const LEGACY_LLM_EXTENSION_IDS: &[&str] = &[ /// Migrates legacy LLM provider extensions by auto-enabling env var reading /// if the env var is currently present in the environment. +/// +/// This migration only runs once per provider - we track which providers have been +/// migrated in `migrated_llm_providers` to avoid overriding user preferences. fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut App) { // Only apply migration to known legacy LLM extensions if !LEGACY_LLM_EXTENSION_IDS.contains(&manifest.id.as_ref()) { @@ -108,49 +111,64 @@ fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut Ap continue; }; - // Check if the env var is present and non-empty - let env_var_is_set = std::env::var(env_var_name) - .map(|v| !v.is_empty()) - .unwrap_or(false); - - if !env_var_is_set { - continue; - } - let full_provider_id: Arc = format!("{}:{}", manifest.id, provider_id).into(); - // Check if already in settings - let already_allowed = ExtensionSettings::get_global(cx) - .allowed_env_var_providers + // Check if we've already run migration for this provider (regardless of outcome) + let already_migrated = ExtensionSettings::get_global(cx) + .migrated_llm_providers .contains(full_provider_id.as_ref()); - if already_allowed { + if already_migrated { continue; } - // Auto-enable env var reading for this provider - log::info!( - "Migrating legacy LLM provider {}: auto-enabling {} env var reading", - full_provider_id, - env_var_name - ); + // Check if the env var is present and non-empty + let env_var_is_set = std::env::var(env_var_name) + .map(|v| !v.is_empty()) + .unwrap_or(false); + // Mark as migrated regardless of whether we enable env var reading settings::update_settings_file(::global(cx), cx, { let full_provider_id = full_provider_id.clone(); + let env_var_is_set = env_var_is_set; move |settings, _| { - let providers = settings + // Always mark as migrated + let migrated = settings .extension - .allowed_env_var_providers + .migrated_llm_providers .get_or_insert_with(Vec::new); - if !providers + if !migrated .iter() .any(|id| id.as_ref() == full_provider_id.as_ref()) { - providers.push(full_provider_id); + migrated.push(full_provider_id.clone()); + } + + // Only enable env var reading if the env var is set + if env_var_is_set { + let providers = settings + .extension + .allowed_env_var_providers + .get_or_insert_with(Vec::new); + + if !providers + .iter() + .any(|id| id.as_ref() == full_provider_id.as_ref()) + { + providers.push(full_provider_id); + } } } }); + + if env_var_is_set { + log::info!( + "Migrating legacy LLM provider {}: auto-enabling {} env var reading", + full_provider_id, + env_var_name + ); + } } } diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs index 3322ea4068cc08..36777cb1727c95 100644 --- a/crates/extension_host/src/extension_settings.rs +++ b/crates/extension_host/src/extension_settings.rs @@ -20,6 +20,9 @@ pub struct ExtensionSettings { /// from environment variables. Each entry is a provider ID in the format /// "extension_id:provider_id". pub allowed_env_var_providers: HashSet>, + /// Tracks which legacy LLM providers have been migrated. + /// This prevents the migration from running multiple times and overriding user preferences. + pub migrated_llm_providers: HashSet>, } impl ExtensionSettings { @@ -71,6 +74,13 @@ impl Settings for ExtensionSettings { .unwrap_or_default() .into_iter() .collect(), + migrated_llm_providers: content + .extension + .migrated_llm_providers + .clone() + .unwrap_or_default() + .into_iter() + .collect(), } } } diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index cfd75f7a10fce0..5194cafec2601d 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -5,7 +5,7 @@ use crate::capability_granter::CapabilityGranter; use crate::{ExtensionManifest, ExtensionSettings}; use anyhow::{Context as _, Result, anyhow, bail}; use async_trait::async_trait; -use collections::HashSet; + use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest}; use extension::{ CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary, @@ -60,8 +60,6 @@ pub struct WasmHost { pub work_dir: PathBuf, /// The capabilities granted to extensions running on the host. pub(crate) granted_capabilities: Vec, - /// Extension LLM providers allowed to read API keys from environment variables. - pub(crate) allowed_env_var_providers: HashSet>, _main_thread_message_task: Task<()>, main_thread_message_tx: mpsc::UnboundedSender, } @@ -597,7 +595,6 @@ impl WasmHost { proxy, release_channel: ReleaseChannel::global(cx), granted_capabilities: extension_settings.granted_capabilities.clone(), - allowed_env_var_providers: extension_settings.allowed_env_var_providers.clone(), _main_thread_message_task: task, main_thread_message_tx: tx, }) diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 3f16fb31cd11d5..b9650d5715909a 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -495,6 +495,20 @@ impl ExtensionProviderConfigurationView { cx.notify(); }); + // If env var is being enabled, clear any stored keychain credentials + // so there's only one source of truth for the API key + if new_allowed { + let credential_key = self.credential_key.clone(); + let credentials_provider = ::global(cx); + cx.spawn(async move |_this, cx| { + credentials_provider + .delete_credentials(&credential_key, cx) + .await + .log_err(); + }) + .detach(); + } + // If env var is being disabled, reload credentials from keychain if !new_allowed { self.reload_keychain_credentials(cx); diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 1b6d5c4d4ea964..82a662db9a28d8 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -1139,11 +1139,23 @@ impl llm_provider::Host for WasmState { if let Some(env_var_name) = env_var_name { let full_provider_id: Arc = format!("{}:{}", extension_id, provider_id).into(); - // Use cached settings from WasmHost instead of going to main thread + // Read settings dynamically to get current allowed_env_var_providers let is_allowed = self - .host - .allowed_env_var_providers - .contains(&full_provider_id); + .on_main_thread({ + let full_provider_id = full_provider_id.clone(); + move |cx| { + async move { + cx.update(|cx| { + crate::extension_settings::ExtensionSettings::get_global(cx) + .allowed_env_var_providers + .contains(&full_provider_id) + }) + } + .boxed_local() + } + }) + .await + .unwrap_or(false); if is_allowed { if let Ok(value) = env::var(&env_var_name) { @@ -1240,12 +1252,24 @@ impl llm_provider::Host for WasmState { }; // Check if the user has allowed this provider to read env vars - // Use cached settings from WasmHost instead of going to main thread + // Read settings dynamically to get current allowed_env_var_providers let full_provider_id: Arc = format!("{}:{}", extension_id, provider_id).into(); let is_allowed = self - .host - .allowed_env_var_providers - .contains(&full_provider_id); + .on_main_thread({ + let full_provider_id = full_provider_id.clone(); + move |cx| { + async move { + cx.update(|cx| { + crate::extension_settings::ExtensionSettings::get_global(cx) + .allowed_env_var_providers + .contains(&full_provider_id) + }) + } + .boxed_local() + } + }) + .await + .unwrap_or(false); if !is_allowed { log::debug!( diff --git a/crates/settings/src/settings_content/extension.rs b/crates/settings/src/settings_content/extension.rs index 64df163f4ec961..b405103e8c311d 100644 --- a/crates/settings/src/settings_content/extension.rs +++ b/crates/settings/src/settings_content/extension.rs @@ -26,6 +26,9 @@ pub struct ExtensionSettingsContent { /// /// Default: [] pub allowed_env_var_providers: Option>>, + /// Tracks which legacy LLM providers have been migrated. This is an internal + /// setting used to prevent the migration from running multiple times. + pub migrated_llm_providers: Option>>, } /// A capability for an extension. From f326b0000337fb52edc61a575ce7babdf7e63077 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 16:36:39 -0500 Subject: [PATCH 39/45] Sign in button for Copilot --- crates/extension/src/extension_manifest.rs | 11 + .../extension_host/src/copilot_migration.rs | 4 +- .../src/wasm_host/llm_provider.rs | 190 +++++++++++++++--- extensions/copilot-chat/extension.toml | 5 +- extensions/copilot-chat/src/copilot_chat.rs | 10 +- 5 files changed, 185 insertions(+), 35 deletions(-) diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 73747c2997a28a..3a09a602d5b461 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -339,6 +339,17 @@ pub struct LanguageModelAuthConfig { /// Human-readable name for the credential shown in the UI input field (e.g., "API Key", "Access Token"). #[serde(default)] pub credential_label: Option, + /// OAuth configuration for web-based authentication flows. + #[serde(default)] + pub oauth: Option, +} + +/// OAuth configuration for web-based authentication. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct OAuthConfig { + /// The text to display on the sign-in button (e.g., "Sign in with GitHub"). + #[serde(default)] + pub sign_in_button_label: Option, } impl ExtensionManifest { diff --git a/crates/extension_host/src/copilot_migration.rs b/crates/extension_host/src/copilot_migration.rs index 90fdf48c0de69c..27d78f6db3ed0c 100644 --- a/crates/extension_host/src/copilot_migration.rs +++ b/crates/extension_host/src/copilot_migration.rs @@ -2,8 +2,8 @@ use credentials_provider::CredentialsProvider; use gpui::App; use std::path::PathBuf; -const COPILOT_CHAT_EXTENSION_ID: &str = "copilot_chat"; -const COPILOT_CHAT_PROVIDER_ID: &str = "copilot_chat"; +const COPILOT_CHAT_EXTENSION_ID: &str = "copilot-chat"; +const COPILOT_CHAT_PROVIDER_ID: &str = "copilot-chat"; pub fn migrate_copilot_credentials_if_needed(extension_id: &str, cx: &mut App) { if extension_id != COPILOT_CHAT_EXTENSION_ID { diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index b9650d5715909a..8c08e2c22aa827 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -10,7 +10,7 @@ use crate::wasm_host::wit::{ use anyhow::{Result, anyhow}; use credentials_provider::CredentialsProvider; use editor::Editor; -use extension::LanguageModelAuthConfig; +use extension::{LanguageModelAuthConfig, OAuthConfig}; use futures::future::BoxFuture; use futures::stream::BoxStream; use futures::{FutureExt, StreamExt}; @@ -315,6 +315,7 @@ struct ExtensionProviderConfigurationView { api_key_editor: Entity, loading_settings: bool, loading_credentials: bool, + oauth_in_progress: bool, _subscriptions: Vec, } @@ -352,6 +353,7 @@ impl ExtensionProviderConfigurationView { api_key_editor, loading_settings: true, loading_credentials: true, + oauth_in_progress: false, _subscriptions: vec![state_subscription], }; @@ -600,9 +602,80 @@ impl ExtensionProviderConfigurationView { .detach(); } + fn start_oauth_sign_in(&mut self, cx: &mut Context) { + if self.oauth_in_progress { + return; + } + + self.oauth_in_progress = true; + cx.notify(); + + let extension = self.extension.clone(); + let provider_id = self.extension_provider_id.clone(); + let state = self.state.clone(); + + cx.spawn(async move |this, cx| { + let result = extension + .call({ + let provider_id = provider_id.clone(); + |ext, store| { + async move { + ext.call_llm_provider_authenticate(store, &provider_id) + .await + } + .boxed() + } + }) + .await; + + this.update(cx, |this, cx| { + this.oauth_in_progress = false; + cx.notify(); + }) + .log_err(); + + match result { + Ok(Ok(Ok(()))) => { + let _ = cx.update(|cx| { + state.update(cx, |state, cx| { + state.is_authenticated = true; + cx.notify(); + }); + }); + } + Ok(Ok(Err(e))) => { + log::error!("OAuth authentication failed: {}", e); + } + Ok(Err(e)) => { + log::error!("OAuth authentication error: {}", e); + } + Err(e) => { + log::error!("OAuth authentication error: {}", e); + } + } + }) + .detach(); + } + fn is_authenticated(&self, cx: &Context) -> bool { self.state.read(cx).is_authenticated } + + fn has_oauth_config(&self) -> bool { + self.auth_config.as_ref().is_some_and(|c| c.oauth.is_some()) + } + + fn oauth_config(&self) -> Option<&OAuthConfig> { + self.auth_config.as_ref().and_then(|c| c.oauth.as_ref()) + } + + fn has_api_key_config(&self) -> bool { + // API key is available if there's a credential_label or no oauth-only config + self.auth_config + .as_ref() + .map(|c| c.credential_label.is_some() || c.oauth.is_none()) + .unwrap_or(true) + } } impl gpui::Render for ExtensionProviderConfigurationView { @@ -611,6 +684,8 @@ impl gpui::Render for ExtensionProviderConfigurationView { let is_authenticated = self.is_authenticated(cx); let env_var_allowed = self.state.read(cx).env_var_allowed; let api_key_from_env = self.state.read(cx).api_key_from_env; + let has_oauth = self.has_oauth_config(); + let has_api_key = self.has_api_key_config(); if is_loading { return v_flex() @@ -680,7 +755,7 @@ impl gpui::Render for ExtensionProviderConfigurationView { ) .child( Label::new(format!( - "{} is not set or empty. You can set it and restart Zed, or enter an API key below.", + "{} is not set or empty. You can set it and restart Zed, or use another authentication method below.", env_var_name )) .color(Color::Warning) @@ -692,8 +767,20 @@ impl gpui::Render for ExtensionProviderConfigurationView { } } - // Render API key section + // If authenticated, show success state with sign out option if is_authenticated && !api_key_from_env { + let reset_label = if has_oauth && !has_api_key { + "Sign Out" + } else { + "Reset Credentials" + }; + + let status_label = if has_oauth && !has_api_key { + "Signed in" + } else { + "Authenticated" + }; + content = content.child( v_flex() .gap_2() @@ -705,39 +792,88 @@ impl gpui::Render for ExtensionProviderConfigurationView { .color(Color::Success) .size(ui::IconSize::Small), ) - .child(Label::new("API key configured").color(Color::Success)), + .child(Label::new(status_label).color(Color::Success)), ) .child( - ui::Button::new("reset-api-key", "Reset API Key") + ui::Button::new("reset-credentials", reset_label) .style(ui::ButtonStyle::Subtle) .on_click(cx.listener(|this, _, window, cx| { this.reset_api_key(window, cx); })), ), ); - } else if !api_key_from_env { - let credential_label = self - .auth_config - .as_ref() - .and_then(|c| c.credential_label.clone()) - .unwrap_or_else(|| "API Key".to_string()); - content = content.child( - v_flex() - .gap_2() - .on_action(cx.listener(Self::save_api_key)) - .child( - Label::new(credential_label) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(self.api_key_editor.clone()) - .child( - Label::new("Enter your API key and press Enter to save") - .size(LabelSize::Small) - .color(Color::Muted), - ), - ); + return content.into_any_element(); + } + + // Not authenticated - show available auth options + if !api_key_from_env { + // Render OAuth sign-in button if configured + if has_oauth { + let oauth_config = self.oauth_config(); + let button_label = oauth_config + .and_then(|c| c.sign_in_button_label.clone()) + .unwrap_or_else(|| "Sign In".to_string()); + + let oauth_in_progress = self.oauth_in_progress; + + content = content.child( + v_flex() + .gap_2() + .child( + ui::Button::new("oauth-sign-in", button_label) + .style(ui::ButtonStyle::Filled) + .disabled(oauth_in_progress) + .on_click(cx.listener(|this, _, _window, cx| { + this.start_oauth_sign_in(cx); + })), + ) + .when(oauth_in_progress, |this| { + this.child( + Label::new("Waiting for authentication...") + .size(LabelSize::Small) + .color(Color::Muted), + ) + }), + ); + } + + // Render API key input if configured (and we have both options, show a separator) + if has_api_key { + if has_oauth { + content = content.child( + h_flex() + .gap_2() + .items_center() + .child(div().h_px().flex_1().bg(cx.theme().colors().border)) + .child(Label::new("or").size(LabelSize::Small).color(Color::Muted)) + .child(div().h_px().flex_1().bg(cx.theme().colors().border)), + ); + } + + let credential_label = self + .auth_config + .as_ref() + .and_then(|c| c.credential_label.clone()) + .unwrap_or_else(|| "API Key".to_string()); + + content = content.child( + v_flex() + .gap_2() + .on_action(cx.listener(Self::save_api_key)) + .child( + Label::new(credential_label) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(self.api_key_editor.clone()) + .child( + Label::new("Enter your API key and press Enter to save") + .size(LabelSize::Small) + .color(Color::Muted), + ), + ); + } } content.into_any_element() diff --git a/extensions/copilot-chat/extension.toml b/extensions/copilot-chat/extension.toml index c226a20f3a7724..5e77c6dda4144f 100644 --- a/extensions/copilot-chat/extension.toml +++ b/extensions/copilot-chat/extension.toml @@ -10,4 +10,7 @@ repository = "https://github.com/zed-industries/zed" name = "Copilot Chat" [language_model_providers.copilot-chat.auth] -env_var = "GH_COPILOT_TOKEN" \ No newline at end of file +env_var = "GH_COPILOT_TOKEN" + +[language_model_providers.copilot-chat.auth.oauth] +sign_in_button_label = "Sign in with GitHub" \ No newline at end of file diff --git a/extensions/copilot-chat/src/copilot_chat.rs b/extensions/copilot-chat/src/copilot_chat.rs index 2c64d430689f94..c87346cbe101ae 100644 --- a/extensions/copilot-chat/src/copilot_chat.rs +++ b/extensions/copilot-chat/src/copilot_chat.rs @@ -427,7 +427,7 @@ impl zed::Extension for CopilotChatProvider { fn llm_providers(&self) -> Vec { vec![LlmProviderInfo { - id: "copilot_chat".into(), + id: "copilot-chat".into(), name: "Copilot Chat".into(), icon: Some("icons/copilot.svg".into()), }] @@ -457,7 +457,7 @@ impl zed::Extension for CopilotChatProvider { } fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { - llm_get_credential("copilot_chat").is_some() + llm_get_credential("copilot-chat").is_some() } fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option { @@ -504,7 +504,7 @@ This extension requires an active GitHub Copilot subscription. fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { let provided = llm_request_credential( - "copilot_chat", + "copilot-chat", LlmCredentialType::ApiKey, "GitHub Copilot Token", "ghu_...", @@ -517,7 +517,7 @@ This extension requires an active GitHub Copilot subscription. } fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { - llm_delete_credential("copilot_chat") + llm_delete_credential("copilot-chat") } fn llm_stream_completion_start( @@ -526,7 +526,7 @@ This extension requires an active GitHub Copilot subscription. model_id: &str, request: &LlmCompletionRequest, ) -> Result { - let api_key = llm_get_credential("copilot_chat").ok_or_else(|| { + let api_key = llm_get_credential("copilot-chat").ok_or_else(|| { "No token configured. Please add your GitHub Copilot token in settings.".to_string() })?; From 592985a35dce23abb2c421b756569dccc93bdfa1 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 17:11:27 -0500 Subject: [PATCH 40/45] More Copilot Chat auth --- extensions/copilot-chat/Cargo.lock | 1 + extensions/copilot-chat/Cargo.toml | 1 + extensions/copilot-chat/src/copilot_chat.rs | 100 +++++++++++++++++--- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/extensions/copilot-chat/Cargo.lock b/extensions/copilot-chat/Cargo.lock index 4b78fda143f8ec..f26c3b43ba9690 100644 --- a/extensions/copilot-chat/Cargo.lock +++ b/extensions/copilot-chat/Cargo.lock @@ -44,6 +44,7 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", + "url", "zed_extension_api", ] diff --git a/extensions/copilot-chat/Cargo.toml b/extensions/copilot-chat/Cargo.toml index 189c1db9fc37b9..620637aa04a156 100644 --- a/extensions/copilot-chat/Cargo.toml +++ b/extensions/copilot-chat/Cargo.toml @@ -15,3 +15,4 @@ crate-type = ["cdylib"] zed_extension_api = { path = "../../crates/extension_api" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +url = "2" diff --git a/extensions/copilot-chat/src/copilot_chat.rs b/extensions/copilot-chat/src/copilot_chat.rs index c87346cbe101ae..3089ea807f0278 100644 --- a/extensions/copilot-chat/src/copilot_chat.rs +++ b/extensions/copilot-chat/src/copilot_chat.rs @@ -2,9 +2,12 @@ use std::collections::HashMap; use std::sync::Mutex; use serde::{Deserialize, Serialize}; +use url::Url; use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; use zed_extension_api::{self as zed, *}; +const GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98"; + struct CopilotChatProvider { streams: Mutex>, next_stream_id: Mutex, @@ -466,13 +469,11 @@ impl zed::Extension for CopilotChatProvider { Welcome to **Copilot Chat**! This extension provides access to GitHub Copilot's chat models. -## Configuration +## Authentication -Enter your GitHub Copilot token below. You need an active GitHub Copilot subscription. +Click **Sign in with GitHub** to authenticate with your GitHub account. You'll be redirected to GitHub to authorize access. This requires an active GitHub Copilot subscription. -To get your token: -1. Ensure you have a GitHub Copilot subscription -2. Generate a token from your GitHub Copilot settings +Alternatively, you can set the `GH_COPILOT_TOKEN` environment variable with your token. ## Available Models @@ -503,17 +504,78 @@ This extension requires an active GitHub Copilot subscription. } fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { - let provided = llm_request_credential( - "copilot-chat", - LlmCredentialType::ApiKey, - "GitHub Copilot Token", - "ghu_...", - )?; - if provided { - Ok(()) - } else { - Err("Authentication cancelled".to_string()) + let state = generate_random_state(); + + let result = llm_oauth_start_web_auth(&LlmOauthWebAuthConfig { + auth_url: format!( + "https://github.com/login/oauth/authorize?client_id={}&scope=read:user&state={}", + GITHUB_COPILOT_CLIENT_ID, state + ), + callback_path: "/callback".to_string(), + timeout_secs: Some(300), + })?; + + let callback_url = Url::parse(&result.callback_url) + .map_err(|e| format!("Failed to parse callback URL: {}", e))?; + + let params: HashMap<_, _> = callback_url.query_pairs().collect(); + + let returned_state = params + .get("state") + .ok_or("Missing state parameter in callback")?; + if returned_state != &state { + return Err("State mismatch - possible CSRF attack".to_string()); + } + + let code = params + .get("code") + .ok_or("Missing authorization code in callback")?; + + let token_response = llm_oauth_http_request(&LlmOauthHttpRequest { + url: "https://github.com/login/oauth/access_token".to_string(), + method: "POST".to_string(), + headers: vec![ + ("Accept".to_string(), "application/json".to_string()), + ( + "Content-Type".to_string(), + "application/x-www-form-urlencoded".to_string(), + ), + ], + body: format!( + "client_id={}&code={}&redirect_uri=http://localhost:{}/callback", + GITHUB_COPILOT_CLIENT_ID, code, result.port + ), + })?; + + if token_response.status != 200 { + return Err(format!( + "Token exchange failed: HTTP {}", + token_response.status + )); + } + + #[derive(Deserialize)] + struct TokenResponse { + access_token: Option, + error: Option, + error_description: Option, + } + + let token_json: TokenResponse = serde_json::from_str(&token_response.body) + .map_err(|e| format!("Failed to parse token response: {}", e))?; + + if let Some(error) = token_json.error { + let description = token_json.error_description.unwrap_or_default(); + return Err(format!("OAuth error: {} - {}", error, description)); } + + let access_token = token_json + .access_token + .ok_or("No access_token in response")?; + + llm_store_credential("copilot-chat", &access_token)?; + + Ok(()) } fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { @@ -693,4 +755,12 @@ This extension requires an active GitHub Copilot subscription. } } +fn generate_random_state() -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + format!("{:x}{:x}", duration.as_secs(), duration.subsec_nanos()) +} + zed::register_extension!(CopilotChatProvider); From 3a2010fedb1db5d9d8af4ec59a407d8d865bd0e6 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 22:14:25 -0500 Subject: [PATCH 41/45] Attempt web-based oauth flow for Copilot --- crates/extension_api/src/extension_api.rs | 12 ++ .../wit/since_v0.8.0/extension.wit | 9 + .../wit/since_v0.8.0/llm-provider.wit | 3 + .../src/wasm_host/llm_provider.rs | 55 +++++- crates/extension_host/src/wasm_host/wit.rs | 14 ++ .../src/wasm_host/wit/since_v0_8_0.rs | 3 +- extensions/copilot-chat/Cargo.lock | 162 ++++++++++++++++++ extensions/copilot-chat/Cargo.toml | 3 + extensions/copilot-chat/src/copilot_chat.rs | 119 ++++++++++++- 9 files changed, 361 insertions(+), 19 deletions(-) diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 7764b13f19672f..19b0cfbbcd469b 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -303,10 +303,18 @@ pub trait Extension: Send + Sync { } /// Attempt to authenticate the provider. + /// This is called for background credential checks - it should check for + /// existing credentials and return Ok if found, or an error if not. fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { Err("`llm_provider_authenticate` not implemented".to_string()) } + /// Start an interactive OAuth sign-in flow. + /// This is called when the user explicitly clicks "Sign in with GitHub" or similar. + fn llm_provider_start_oauth_sign_in(&mut self, _provider_id: &str) -> Result<(), String> { + Err("`llm_provider_start_oauth_sign_in` not implemented".to_string()) + } + /// Reset credentials for the provider. fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { Err("`llm_provider_reset_credentials` not implemented".to_string()) @@ -635,6 +643,10 @@ impl wit::Guest for Component { extension().llm_provider_authenticate(&provider_id) } + fn llm_provider_start_oauth_sign_in(provider_id: String) -> Result<(), String> { + extension().llm_provider_start_oauth_sign_in(&provider_id) + } + fn llm_provider_reset_credentials(provider_id: String) -> Result<(), String> { extension().llm_provider_reset_credentials(&provider_id) } diff --git a/crates/extension_api/wit/since_v0.8.0/extension.wit b/crates/extension_api/wit/since_v0.8.0/extension.wit index b972b78a80ed56..08d1194cf1f79f 100644 --- a/crates/extension_api/wit/since_v0.8.0/extension.wit +++ b/crates/extension_api/wit/since_v0.8.0/extension.wit @@ -184,8 +184,17 @@ world extension { export llm-provider-is-authenticated: func(provider-id: string) -> bool; /// Attempt to authenticate the provider. + /// This is called for background credential checks - it should check for + /// existing credentials and return Ok if found, or an error if not. + /// For interactive OAuth flows, use llm-provider-start-oauth-sign-in instead. export llm-provider-authenticate: func(provider-id: string) -> result<_, string>; + /// Start an interactive OAuth sign-in flow. + /// This is called when the user explicitly clicks "Sign in with GitHub" or similar. + /// Unlike llm-provider-authenticate, this should open a browser and perform + /// the full interactive OAuth flow. + export llm-provider-start-oauth-sign-in: func(provider-id: string) -> result<_, string>; + /// Reset credentials for the provider. export llm-provider-reset-credentials: func(provider-id: string) -> result<_, string>; diff --git a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit index c0d6b371a88e9f..a3f1258fc78850 100644 --- a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit +++ b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit @@ -257,6 +257,9 @@ interface llm-provider { record oauth-web-auth-config { /// The URL to open in the user's browser to start authentication. /// This should include client_id, redirect_uri, scope, state, etc. + /// Use `{port}` as a placeholder in the URL - it will be replaced with + /// the actual localhost port before opening the browser. + /// Example: "https://example.com/oauth?redirect_uri=http://127.0.0.1:{port}/callback" auth-url: string, /// The path to listen on for the OAuth callback (e.g., "/callback"). /// A localhost server will be started to receive the redirect. diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 8c08e2c22aa827..69b48e3bba52e9 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -316,6 +316,7 @@ struct ExtensionProviderConfigurationView { loading_settings: bool, loading_credentials: bool, oauth_in_progress: bool, + oauth_error: Option, _subscriptions: Vec, } @@ -354,6 +355,7 @@ impl ExtensionProviderConfigurationView { loading_settings: true, loading_credentials: true, oauth_in_progress: false, + oauth_error: None, _subscriptions: vec![state_subscription], }; @@ -608,6 +610,7 @@ impl ExtensionProviderConfigurationView { } self.oauth_in_progress = true; + self.oauth_error = None; cx.notify(); let extension = self.extension.clone(); @@ -620,7 +623,7 @@ impl ExtensionProviderConfigurationView { let provider_id = provider_id.clone(); |ext, store| { async move { - ext.call_llm_provider_authenticate(store, &provider_id) + ext.call_llm_provider_start_oauth_sign_in(store, &provider_id) .await } .boxed() @@ -628,13 +631,7 @@ impl ExtensionProviderConfigurationView { }) .await; - this.update(cx, |this, cx| { - this.oauth_in_progress = false; - cx.notify(); - }) - .log_err(); - - match result { + let error_message = match &result { Ok(Ok(Ok(()))) => { let _ = cx.update(|cx| { state.update(cx, |state, cx| { @@ -642,17 +639,28 @@ impl ExtensionProviderConfigurationView { cx.notify(); }); }); + None } Ok(Ok(Err(e))) => { log::error!("OAuth authentication failed: {}", e); + Some(e.clone()) } Ok(Err(e)) => { log::error!("OAuth authentication error: {}", e); + Some(e.to_string()) } Err(e) => { log::error!("OAuth authentication error: {}", e); + Some(e.to_string()) } - } + }; + + this.update(cx, |this, cx| { + this.oauth_in_progress = false; + this.oauth_error = error_message; + cx.notify(); + }) + .log_err(); }) .detach(); } @@ -817,6 +825,8 @@ impl gpui::Render for ExtensionProviderConfigurationView { let oauth_in_progress = self.oauth_in_progress; + let oauth_error = self.oauth_error.clone(); + content = content.child( v_flex() .gap_2() @@ -834,6 +844,33 @@ impl gpui::Render for ExtensionProviderConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) + }) + .when_some(oauth_error, |this, error| { + this.child( + v_flex() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + ui::Icon::new(ui::IconName::Warning) + .color(Color::Error) + .size(ui::IconSize::Small), + ) + .child( + Label::new("Authentication failed") + .color(Color::Error) + .size(LabelSize::Small), + ), + ) + .child( + div().pl_6().child( + Label::new(error) + .color(Color::Error) + .size(LabelSize::Small), + ), + ), + ) }), ); } diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 946b4864f4f694..d3fafbe28c280a 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -1241,6 +1241,20 @@ impl Extension { } } + pub async fn call_llm_provider_start_oauth_sign_in( + &self, + store: &mut Store, + provider_id: &str, + ) -> Result> { + match self { + Extension::V0_8_0(ext) => { + ext.call_llm_provider_start_oauth_sign_in(store, provider_id) + .await + } + _ => anyhow::bail!("`llm_provider_start_oauth_sign_in` not available prior to v0.8.0"), + } + } + pub async fn call_llm_provider_reset_credentials( &self, store: &mut Store, diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 82a662db9a28d8..a7fc76ffb6d489 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -1302,8 +1302,9 @@ impl llm_provider::Host for WasmState { .map_err(|e| anyhow::anyhow!("Failed to get local address: {}", e))? .port(); + let auth_url_with_port = auth_url.replace("{port}", &port.to_string()); cx.update(|cx| { - cx.open_url(&auth_url); + cx.open_url(&auth_url_with_port); })?; let accept_future = async { diff --git a/extensions/copilot-chat/Cargo.lock b/extensions/copilot-chat/Cargo.lock index f26c3b43ba9690..1bc2721bd34248 100644 --- a/extensions/copilot-chat/Cargo.lock +++ b/extensions/copilot-chat/Cargo.lock @@ -26,12 +26,27 @@ dependencies = [ "topological-sort", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -42,12 +57,24 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "copilot-chat" version = "0.1.0" dependencies = [ + "base64", + "rand", "serde", "serde_json", + "sha2", "url", "zed_extension_api", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -57,6 +84,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -188,6 +235,27 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -341,6 +409,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + [[package]] name = "litemap" version = "0.8.1" @@ -402,6 +476,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -430,6 +513,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ryu" version = "1.0.20" @@ -489,6 +602,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -560,6 +684,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -590,6 +720,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-encoder" version = "0.227.1" @@ -769,6 +911,26 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/extensions/copilot-chat/Cargo.toml b/extensions/copilot-chat/Cargo.toml index 620637aa04a156..cabe3dd4ed62d3 100644 --- a/extensions/copilot-chat/Cargo.toml +++ b/extensions/copilot-chat/Cargo.toml @@ -16,3 +16,6 @@ zed_extension_api = { path = "../../crates/extension_api" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" url = "2" +sha2 = "0.10" +base64 = "0.22" +rand = "0.8" diff --git a/extensions/copilot-chat/src/copilot_chat.rs b/extensions/copilot-chat/src/copilot_chat.rs index 3089ea807f0278..3a479f4bdf3585 100644 --- a/extensions/copilot-chat/src/copilot_chat.rs +++ b/extensions/copilot-chat/src/copilot_chat.rs @@ -1,7 +1,10 @@ use std::collections::HashMap; use std::sync::Mutex; +use base64::Engine; +use rand::Rng; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use url::Url; use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; use zed_extension_api::{self as zed, *}; @@ -504,12 +507,26 @@ This extension requires an active GitHub Copilot subscription. } fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> { + // Check if we have existing credentials + if llm_get_credential("copilot-chat").is_some() { + return Ok(()); + } + + // No credentials found - return error for background auth checks. + // The OAuth flow will be triggered by the host when the user clicks + // the "Sign in with GitHub" button, which calls llm_provider_start_oauth_sign_in. + Err("CredentialsNotFound".to_string()) + } + + fn llm_provider_start_oauth_sign_in(&mut self, _provider_id: &str) -> Result<(), String> { let state = generate_random_state(); + let code_verifier = generate_pkce_verifier(); + let code_challenge = generate_pkce_challenge(&code_verifier); let result = llm_oauth_start_web_auth(&LlmOauthWebAuthConfig { auth_url: format!( - "https://github.com/login/oauth/authorize?client_id={}&scope=read:user&state={}", - GITHUB_COPILOT_CLIENT_ID, state + "https://github.com/login/oauth/authorize?client_id={}&redirect_uri=http://127.0.0.1:{{port}}/callback&scope=read:user&state={}&code_challenge={}&code_challenge_method=S256", + GITHUB_COPILOT_CLIENT_ID, state, code_challenge ), callback_path: "/callback".to_string(), timeout_secs: Some(300), @@ -542,8 +559,8 @@ This extension requires an active GitHub Copilot subscription. ), ], body: format!( - "client_id={}&code={}&redirect_uri=http://localhost:{}/callback", - GITHUB_COPILOT_CLIENT_ID, code, result.port + "client_id={}&code={}&redirect_uri=http://127.0.0.1:{}/callback&code_verifier={}", + GITHUB_COPILOT_CLIENT_ID, code, result.port, code_verifier ), })?; @@ -756,11 +773,95 @@ This extension requires an active GitHub Copilot subscription. } fn generate_random_state() -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - let duration = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); - format!("{:x}{:x}", duration.as_secs(), duration.subsec_nanos()) + let mut rng = rand::thread_rng(); + let bytes: [u8; 32] = rng.gen(); + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) +} + +fn generate_pkce_verifier() -> String { + let mut rng = rand::thread_rng(); + let bytes: [u8; 32] = rng.gen(); + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) +} + +fn generate_pkce_challenge(verifier: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(verifier.as_bytes()); + let hash = hasher.finalize(); + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pkce_challenge_generation() { + let verifier = generate_pkce_verifier(); + let challenge = generate_pkce_challenge(&verifier); + + // Verifier should be base64url encoded 32 bytes = 43 characters + assert_eq!(verifier.len(), 43); + + // Challenge should be base64url encoded SHA256 hash = 43 characters + assert_eq!(challenge.len(), 43); + + // Challenge should be deterministic for same verifier + let challenge2 = generate_pkce_challenge(&verifier); + assert_eq!(challenge, challenge2); + + // Different verifiers should produce different challenges + let verifier2 = generate_pkce_verifier(); + let challenge3 = generate_pkce_challenge(&verifier2); + assert_ne!(challenge, challenge3); + } + + #[test] + fn test_auth_url_template_with_pkce() { + let state = "test_state"; + let code_challenge = "test_challenge_abc123"; + let template = format!( + "https://github.com/login/oauth/authorize?client_id={}&redirect_uri=http://127.0.0.1:{{port}}/callback&scope=read:user&state={}&code_challenge={}&code_challenge_method=S256", + GITHUB_COPILOT_CLIENT_ID, state, code_challenge + ); + + // Verify the template contains {port} placeholder + assert!( + template.contains("{port}"), + "Template should contain {{port}} placeholder, got: {}", + template + ); + + // Simulate what the host does + let port = 54321; + let final_url = template.replace("{port}", &port.to_string()); + + // Verify it's a valid URL with all expected params + let parsed = Url::parse(&final_url).expect("should be a valid URL"); + assert_eq!(parsed.scheme(), "https"); + assert_eq!(parsed.host_str(), Some("github.com")); + assert_eq!(parsed.path(), "/login/oauth/authorize"); + + let params: HashMap<_, _> = parsed.query_pairs().collect(); + assert_eq!( + params.get("client_id").map(|s| s.as_ref()), + Some("Iv1.b507a08c87ecfe98") + ); + assert_eq!( + params.get("redirect_uri").map(|s| s.as_ref()), + Some("http://127.0.0.1:54321/callback") + ); + assert_eq!(params.get("scope").map(|s| s.as_ref()), Some("read:user")); + assert_eq!(params.get("state").map(|s| s.as_ref()), Some("test_state")); + assert_eq!( + params.get("code_challenge").map(|s| s.as_ref()), + Some("test_challenge_abc123") + ); + assert_eq!( + params.get("code_challenge_method").map(|s| s.as_ref()), + Some("S256") + ); + } } zed::register_extension!(CopilotChatProvider); From ef0b0ed984d4aff0080746dc9b1b1b1bf43dd675 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 22:20:00 -0500 Subject: [PATCH 42/45] Switch to device flow oauth for copilot --- extensions/copilot-chat/Cargo.lock | 163 -------------- extensions/copilot-chat/Cargo.toml | 4 - extensions/copilot-chat/src/copilot_chat.rs | 238 ++++++++------------ 3 files changed, 100 insertions(+), 305 deletions(-) diff --git a/extensions/copilot-chat/Cargo.lock b/extensions/copilot-chat/Cargo.lock index 1bc2721bd34248..4b78fda143f8ec 100644 --- a/extensions/copilot-chat/Cargo.lock +++ b/extensions/copilot-chat/Cargo.lock @@ -26,27 +26,12 @@ dependencies = [ "topological-sort", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -57,24 +42,11 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "copilot-chat" version = "0.1.0" dependencies = [ - "base64", - "rand", "serde", "serde_json", - "sha2", - "url", "zed_extension_api", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -84,26 +56,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -235,27 +187,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -409,12 +340,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - [[package]] name = "litemap" version = "0.8.1" @@ -476,15 +401,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -513,36 +429,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - [[package]] name = "ryu" version = "1.0.20" @@ -602,17 +488,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -684,12 +559,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - [[package]] name = "unicode-ident" version = "1.0.22" @@ -720,18 +589,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasm-encoder" version = "0.227.1" @@ -911,26 +768,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "zerocopy" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.6" diff --git a/extensions/copilot-chat/Cargo.toml b/extensions/copilot-chat/Cargo.toml index cabe3dd4ed62d3..189c1db9fc37b9 100644 --- a/extensions/copilot-chat/Cargo.toml +++ b/extensions/copilot-chat/Cargo.toml @@ -15,7 +15,3 @@ crate-type = ["cdylib"] zed_extension_api = { path = "../../crates/extension_api" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -url = "2" -sha2 = "0.10" -base64 = "0.22" -rand = "0.8" diff --git a/extensions/copilot-chat/src/copilot_chat.rs b/extensions/copilot-chat/src/copilot_chat.rs index 3a479f4bdf3585..d096130b016115 100644 --- a/extensions/copilot-chat/src/copilot_chat.rs +++ b/extensions/copilot-chat/src/copilot_chat.rs @@ -1,14 +1,14 @@ use std::collections::HashMap; use std::sync::Mutex; +use std::thread; +use std::time::Duration; -use base64::Engine; -use rand::Rng; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use url::Url; use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy}; use zed_extension_api::{self as zed, *}; +const GITHUB_DEVICE_CODE_URL: &str = "https://github.com/login/device/code"; +const GITHUB_ACCESS_TOKEN_URL: &str = "https://github.com/login/oauth/access_token"; const GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98"; struct CopilotChatProvider { @@ -513,43 +513,15 @@ This extension requires an active GitHub Copilot subscription. } // No credentials found - return error for background auth checks. - // The OAuth flow will be triggered by the host when the user clicks + // The device flow will be triggered by the host when the user clicks // the "Sign in with GitHub" button, which calls llm_provider_start_oauth_sign_in. Err("CredentialsNotFound".to_string()) } fn llm_provider_start_oauth_sign_in(&mut self, _provider_id: &str) -> Result<(), String> { - let state = generate_random_state(); - let code_verifier = generate_pkce_verifier(); - let code_challenge = generate_pkce_challenge(&code_verifier); - - let result = llm_oauth_start_web_auth(&LlmOauthWebAuthConfig { - auth_url: format!( - "https://github.com/login/oauth/authorize?client_id={}&redirect_uri=http://127.0.0.1:{{port}}/callback&scope=read:user&state={}&code_challenge={}&code_challenge_method=S256", - GITHUB_COPILOT_CLIENT_ID, state, code_challenge - ), - callback_path: "/callback".to_string(), - timeout_secs: Some(300), - })?; - - let callback_url = Url::parse(&result.callback_url) - .map_err(|e| format!("Failed to parse callback URL: {}", e))?; - - let params: HashMap<_, _> = callback_url.query_pairs().collect(); - - let returned_state = params - .get("state") - .ok_or("Missing state parameter in callback")?; - if returned_state != &state { - return Err("State mismatch - possible CSRF attack".to_string()); - } - - let code = params - .get("code") - .ok_or("Missing authorization code in callback")?; - - let token_response = llm_oauth_http_request(&LlmOauthHttpRequest { - url: "https://github.com/login/oauth/access_token".to_string(), + // Step 1: Request device and user verification codes + let device_code_response = llm_oauth_http_request(&LlmOauthHttpRequest { + url: GITHUB_DEVICE_CODE_URL.to_string(), method: "POST".to_string(), headers: vec![ ("Accept".to_string(), "application/json".to_string()), @@ -558,41 +530,99 @@ This extension requires an active GitHub Copilot subscription. "application/x-www-form-urlencoded".to_string(), ), ], - body: format!( - "client_id={}&code={}&redirect_uri=http://127.0.0.1:{}/callback&code_verifier={}", - GITHUB_COPILOT_CLIENT_ID, code, result.port, code_verifier - ), + body: format!("client_id={}&scope=read:user", GITHUB_COPILOT_CLIENT_ID), })?; - if token_response.status != 200 { + if device_code_response.status != 200 { return Err(format!( - "Token exchange failed: HTTP {}", - token_response.status + "Failed to get device code: HTTP {}", + device_code_response.status )); } #[derive(Deserialize)] - struct TokenResponse { - access_token: Option, - error: Option, - error_description: Option, + struct DeviceCodeResponse { + device_code: String, + user_code: String, + verification_uri: String, + expires_in: u64, + interval: u64, } - let token_json: TokenResponse = serde_json::from_str(&token_response.body) - .map_err(|e| format!("Failed to parse token response: {}", e))?; + let device_info: DeviceCodeResponse = serde_json::from_str(&device_code_response.body) + .map_err(|e| format!("Failed to parse device code response: {}", e))?; - if let Some(error) = token_json.error { - let description = token_json.error_description.unwrap_or_default(); - return Err(format!("OAuth error: {} - {}", error, description)); - } + // Step 2: Open browser to verification URL with pre-filled user code + let verification_url = format!( + "{}?user_code={}", + device_info.verification_uri, device_info.user_code + ); + llm_oauth_open_browser(&verification_url)?; + + // Step 3: Poll for access token + let poll_interval = Duration::from_secs(device_info.interval.max(5)); + let max_attempts = (device_info.expires_in / device_info.interval.max(5)) as usize; + + for _ in 0..max_attempts { + thread::sleep(poll_interval); + + let token_response = llm_oauth_http_request(&LlmOauthHttpRequest { + url: GITHUB_ACCESS_TOKEN_URL.to_string(), + method: "POST".to_string(), + headers: vec![ + ("Accept".to_string(), "application/json".to_string()), + ( + "Content-Type".to_string(), + "application/x-www-form-urlencoded".to_string(), + ), + ], + body: format!( + "client_id={}&device_code={}&grant_type=urn:ietf:params:oauth:grant-type:device_code", + GITHUB_COPILOT_CLIENT_ID, device_info.device_code + ), + })?; - let access_token = token_json - .access_token - .ok_or("No access_token in response")?; + #[derive(Deserialize)] + struct TokenResponse { + access_token: Option, + error: Option, + error_description: Option, + } - llm_store_credential("copilot-chat", &access_token)?; + let token_json: TokenResponse = serde_json::from_str(&token_response.body) + .map_err(|e| format!("Failed to parse token response: {}", e))?; - Ok(()) + if let Some(access_token) = token_json.access_token { + llm_store_credential("copilot-chat", &access_token)?; + return Ok(()); + } + + if let Some(error) = &token_json.error { + match error.as_str() { + "authorization_pending" => { + // User hasn't authorized yet, keep polling + continue; + } + "slow_down" => { + // Need to slow down polling + thread::sleep(Duration::from_secs(5)); + continue; + } + "expired_token" => { + return Err("Device code expired. Please try again.".to_string()); + } + "access_denied" => { + return Err("Authorization was denied.".to_string()); + } + _ => { + let description = token_json.error_description.unwrap_or_default(); + return Err(format!("OAuth error: {} - {}", error, description)); + } + } + } + } + + Err("Authorization timed out. Please try again.".to_string()) } fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { @@ -772,95 +802,27 @@ This extension requires an active GitHub Copilot subscription. } } -fn generate_random_state() -> String { - let mut rng = rand::thread_rng(); - let bytes: [u8; 32] = rng.gen(); - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) -} - -fn generate_pkce_verifier() -> String { - let mut rng = rand::thread_rng(); - let bytes: [u8; 32] = rng.gen(); - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) -} - -fn generate_pkce_challenge(verifier: &str) -> String { - let mut hasher = Sha256::new(); - hasher.update(verifier.as_bytes()); - let hash = hasher.finalize(); - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash) -} - #[cfg(test)] mod tests { use super::*; #[test] - fn test_pkce_challenge_generation() { - let verifier = generate_pkce_verifier(); - let challenge = generate_pkce_challenge(&verifier); - - // Verifier should be base64url encoded 32 bytes = 43 characters - assert_eq!(verifier.len(), 43); - - // Challenge should be base64url encoded SHA256 hash = 43 characters - assert_eq!(challenge.len(), 43); - - // Challenge should be deterministic for same verifier - let challenge2 = generate_pkce_challenge(&verifier); - assert_eq!(challenge, challenge2); - - // Different verifiers should produce different challenges - let verifier2 = generate_pkce_verifier(); - let challenge3 = generate_pkce_challenge(&verifier2); - assert_ne!(challenge, challenge3); + fn test_device_flow_request_body() { + let body = format!("client_id={}&scope=read:user", GITHUB_COPILOT_CLIENT_ID); + assert!(body.contains("client_id=Iv1.b507a08c87ecfe98")); + assert!(body.contains("scope=read:user")); } #[test] - fn test_auth_url_template_with_pkce() { - let state = "test_state"; - let code_challenge = "test_challenge_abc123"; - let template = format!( - "https://github.com/login/oauth/authorize?client_id={}&redirect_uri=http://127.0.0.1:{{port}}/callback&scope=read:user&state={}&code_challenge={}&code_challenge_method=S256", - GITHUB_COPILOT_CLIENT_ID, state, code_challenge - ); - - // Verify the template contains {port} placeholder - assert!( - template.contains("{port}"), - "Template should contain {{port}} placeholder, got: {}", - template - ); - - // Simulate what the host does - let port = 54321; - let final_url = template.replace("{port}", &port.to_string()); - - // Verify it's a valid URL with all expected params - let parsed = Url::parse(&final_url).expect("should be a valid URL"); - assert_eq!(parsed.scheme(), "https"); - assert_eq!(parsed.host_str(), Some("github.com")); - assert_eq!(parsed.path(), "/login/oauth/authorize"); - - let params: HashMap<_, _> = parsed.query_pairs().collect(); - assert_eq!( - params.get("client_id").map(|s| s.as_ref()), - Some("Iv1.b507a08c87ecfe98") - ); - assert_eq!( - params.get("redirect_uri").map(|s| s.as_ref()), - Some("http://127.0.0.1:54321/callback") - ); - assert_eq!(params.get("scope").map(|s| s.as_ref()), Some("read:user")); - assert_eq!(params.get("state").map(|s| s.as_ref()), Some("test_state")); - assert_eq!( - params.get("code_challenge").map(|s| s.as_ref()), - Some("test_challenge_abc123") - ); - assert_eq!( - params.get("code_challenge_method").map(|s| s.as_ref()), - Some("S256") + fn test_token_poll_request_body() { + let device_code = "test_device_code_123"; + let body = format!( + "client_id={}&device_code={}&grant_type=urn:ietf:params:oauth:grant-type:device_code", + GITHUB_COPILOT_CLIENT_ID, device_code ); + assert!(body.contains("client_id=Iv1.b507a08c87ecfe98")); + assert!(body.contains("device_code=test_device_code_123")); + assert!(body.contains("grant_type=urn:ietf:params:oauth:grant-type:device_code")); } } From 9248955a70c0ddc1f7902a3a4886958d4e5b07a0 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 23:11:39 -0500 Subject: [PATCH 43/45] Show device code in provider auth --- crates/extension_api/src/extension_api.rs | 26 +++- .../wit/since_v0.8.0/extension.wit | 25 +++- .../src/wasm_host/llm_provider.rs | 125 +++++++++++++++--- crates/extension_host/src/wasm_host/wit.rs | 28 +++- extensions/copilot-chat/src/copilot_chat.rs | 56 ++++++-- 5 files changed, 220 insertions(+), 40 deletions(-) diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 19b0cfbbcd469b..555ba6dcc260b6 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -309,10 +309,22 @@ pub trait Extension: Send + Sync { Err("`llm_provider_authenticate` not implemented".to_string()) } - /// Start an interactive OAuth sign-in flow. + /// Start an OAuth device flow sign-in. /// This is called when the user explicitly clicks "Sign in with GitHub" or similar. - fn llm_provider_start_oauth_sign_in(&mut self, _provider_id: &str) -> Result<(), String> { - Err("`llm_provider_start_oauth_sign_in` not implemented".to_string()) + /// Opens the browser to the verification URL and returns the user code that should + /// be displayed to the user. + fn llm_provider_start_device_flow_sign_in( + &mut self, + _provider_id: &str, + ) -> Result { + Err("`llm_provider_start_device_flow_sign_in` not implemented".to_string()) + } + + /// Poll for device flow sign-in completion. + /// This is called after llm_provider_start_device_flow_sign_in returns the user code. + /// The extension should poll the OAuth provider until the user authorizes or the flow times out. + fn llm_provider_poll_device_flow_sign_in(&mut self, _provider_id: &str) -> Result<(), String> { + Err("`llm_provider_poll_device_flow_sign_in` not implemented".to_string()) } /// Reset credentials for the provider. @@ -643,8 +655,12 @@ impl wit::Guest for Component { extension().llm_provider_authenticate(&provider_id) } - fn llm_provider_start_oauth_sign_in(provider_id: String) -> Result<(), String> { - extension().llm_provider_start_oauth_sign_in(&provider_id) + fn llm_provider_start_device_flow_sign_in(provider_id: String) -> Result { + extension().llm_provider_start_device_flow_sign_in(&provider_id) + } + + fn llm_provider_poll_device_flow_sign_in(provider_id: String) -> Result<(), String> { + extension().llm_provider_poll_device_flow_sign_in(&provider_id) } fn llm_provider_reset_credentials(provider_id: String) -> Result<(), String> { diff --git a/crates/extension_api/wit/since_v0.8.0/extension.wit b/crates/extension_api/wit/since_v0.8.0/extension.wit index 08d1194cf1f79f..ef9f464d29d802 100644 --- a/crates/extension_api/wit/since_v0.8.0/extension.wit +++ b/crates/extension_api/wit/since_v0.8.0/extension.wit @@ -186,14 +186,29 @@ world extension { /// Attempt to authenticate the provider. /// This is called for background credential checks - it should check for /// existing credentials and return Ok if found, or an error if not. - /// For interactive OAuth flows, use llm-provider-start-oauth-sign-in instead. + /// For interactive OAuth flows, use the device flow functions instead. export llm-provider-authenticate: func(provider-id: string) -> result<_, string>; - /// Start an interactive OAuth sign-in flow. + /// Start an OAuth device flow sign-in. /// This is called when the user explicitly clicks "Sign in with GitHub" or similar. - /// Unlike llm-provider-authenticate, this should open a browser and perform - /// the full interactive OAuth flow. - export llm-provider-start-oauth-sign-in: func(provider-id: string) -> result<_, string>; + /// + /// The device flow works as follows: + /// 1. Extension requests a device code from the OAuth provider + /// 2. Extension opens the verification URL in the browser + /// 3. Extension returns the user code to display to the user + /// 4. Host displays the user code and calls llm-provider-poll-device-flow-sign-in + /// 5. Extension polls for the access token while user authorizes in browser + /// 6. Once authorized, extension stores the credential and returns success + /// + /// Returns the user code that should be displayed to the user while they + /// complete authorization in the browser. + export llm-provider-start-device-flow-sign-in: func(provider-id: string) -> result; + + /// Poll for device flow sign-in completion. + /// This is called after llm-provider-start-device-flow-sign-in returns the user code. + /// The extension should poll the OAuth provider until the user authorizes or the flow times out. + /// Returns Ok(()) on successful authentication, or an error message on failure. + export llm-provider-poll-device-flow-sign-in: func(provider-id: string) -> result<_, string>; /// Reset credentials for the provider. export llm-provider-reset-credentials: func(provider-id: string) -> result<_, string>; diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 69b48e3bba52e9..7355e59569c92c 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -16,8 +16,8 @@ use futures::stream::BoxStream; use futures::{FutureExt, StreamExt}; use gpui::Focusable; use gpui::{ - AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, - TextStyleRefinement, UnderlineStyle, Window, px, + AnyView, App, AppContext as _, AsyncApp, ClipboardItem, Context, Entity, EventEmitter, + MouseButton, Subscription, Task, TextStyleRefinement, UnderlineStyle, Window, px, }; use language_model::tool_schema::LanguageModelToolSchemaFormat; use language_model::{ @@ -317,6 +317,7 @@ struct ExtensionProviderConfigurationView { loading_credentials: bool, oauth_in_progress: bool, oauth_error: Option, + device_user_code: Option, _subscriptions: Vec, } @@ -356,6 +357,7 @@ impl ExtensionProviderConfigurationView { loading_credentials: true, oauth_in_progress: false, oauth_error: None, + device_user_code: None, _subscriptions: vec![state_subscription], }; @@ -611,6 +613,7 @@ impl ExtensionProviderConfigurationView { self.oauth_in_progress = true; self.oauth_error = None; + self.device_user_code = None; cx.notify(); let extension = self.extension.clone(); @@ -618,12 +621,58 @@ impl ExtensionProviderConfigurationView { let state = self.state.clone(); cx.spawn(async move |this, cx| { - let result = extension + // Step 1: Start device flow - opens browser and returns user code + let start_result = extension + .call({ + let provider_id = provider_id.clone(); + |ext, store| { + async move { + ext.call_llm_provider_start_device_flow_sign_in(store, &provider_id) + .await + } + .boxed() + } + }) + .await; + + let user_code = match start_result { + Ok(Ok(Ok(code))) => code, + Ok(Ok(Err(e))) => { + log::error!("Device flow start failed: {}", e); + this.update(cx, |this, cx| { + this.oauth_in_progress = false; + this.oauth_error = Some(e); + cx.notify(); + }) + .log_err(); + return; + } + Ok(Err(e)) | Err(e) => { + log::error!("Device flow start error: {}", e); + this.update(cx, |this, cx| { + this.oauth_in_progress = false; + this.oauth_error = Some(e.to_string()); + cx.notify(); + }) + .log_err(); + return; + } + }; + + // Update UI to show the user code before polling + this.update(cx, |this, cx| { + this.device_user_code = Some(user_code); + cx.notify(); + }) + .log_err(); + + // Step 2: Poll for authentication completion + let poll_result = extension .call({ let provider_id = provider_id.clone(); |ext, store| { async move { - ext.call_llm_provider_start_oauth_sign_in(store, &provider_id) + ext.call_llm_provider_poll_device_flow_sign_in(store, &provider_id) .await } .boxed() @@ -631,7 +680,7 @@ impl ExtensionProviderConfigurationView { }) .await; - let error_message = match &result { + let error_message = match poll_result { Ok(Ok(Ok(()))) => { let _ = cx.update(|cx| { state.update(cx, |state, cx| { @@ -642,15 +691,11 @@ impl ExtensionProviderConfigurationView { None } Ok(Ok(Err(e))) => { - log::error!("OAuth authentication failed: {}", e); - Some(e.clone()) - } - Ok(Err(e)) => { - log::error!("OAuth authentication error: {}", e); - Some(e.to_string()) + log::error!("Device flow poll failed: {}", e); + Some(e) } - Err(e) => { - log::error!("OAuth authentication error: {}", e); + Ok(Err(e)) | Err(e) => { + log::error!("Device flow poll error: {}", e); Some(e.to_string()) } }; @@ -658,6 +703,7 @@ impl ExtensionProviderConfigurationView { this.update(cx, |this, cx| { this.oauth_in_progress = false; this.oauth_error = error_message; + this.device_user_code = None; cx.notify(); }) .log_err(); @@ -839,10 +885,57 @@ impl gpui::Render for ExtensionProviderConfigurationView { })), ) .when(oauth_in_progress, |this| { + let user_code = self.device_user_code.clone(); this.child( - Label::new("Waiting for authentication...") - .size(LabelSize::Small) - .color(Color::Muted), + v_flex() + .gap_1() + .when_some(user_code, |this, code| { + let copied = cx + .read_from_clipboard() + .map(|item| item.text().as_ref() == Some(&code)) + .unwrap_or(false); + let code_for_click = code.clone(); + this.child( + h_flex() + .w_full() + .p_1() + .border_1() + .border_color(cx.theme().colors().border) + .rounded_sm() + .cursor_pointer() + .justify_between() + .on_mouse_down( + MouseButton::Left, + move |_, window, cx| { + cx.write_to_clipboard( + ClipboardItem::new_string( + code_for_click.clone(), + ), + ); + window.refresh(); + }, + ) + .child( + Label::new(code) + .size(LabelSize::Small) + .color(Color::Accent), + ) + .child( + Label::new(if copied { + "Copied!" + } else { + "Click to copy" + }) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + .child( + Label::new("Waiting for authorization in browser...") + .size(LabelSize::Small) + .color(Color::Muted), + ), ) }) .when_some(oauth_error, |this, error| { diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index d3fafbe28c280a..c2b22d2ad02278 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -1241,17 +1241,39 @@ impl Extension { } } - pub async fn call_llm_provider_start_oauth_sign_in( + pub async fn call_llm_provider_start_device_flow_sign_in( + &self, + store: &mut Store, + provider_id: &str, + ) -> Result> { + match self { + Extension::V0_8_0(ext) => { + ext.call_llm_provider_start_device_flow_sign_in(store, provider_id) + .await + } + _ => { + anyhow::bail!( + "`llm_provider_start_device_flow_sign_in` not available prior to v0.8.0" + ) + } + } + } + + pub async fn call_llm_provider_poll_device_flow_sign_in( &self, store: &mut Store, provider_id: &str, ) -> Result> { match self { Extension::V0_8_0(ext) => { - ext.call_llm_provider_start_oauth_sign_in(store, provider_id) + ext.call_llm_provider_poll_device_flow_sign_in(store, provider_id) .await } - _ => anyhow::bail!("`llm_provider_start_oauth_sign_in` not available prior to v0.8.0"), + _ => { + anyhow::bail!( + "`llm_provider_poll_device_flow_sign_in` not available prior to v0.8.0" + ) + } } } diff --git a/extensions/copilot-chat/src/copilot_chat.rs b/extensions/copilot-chat/src/copilot_chat.rs index d096130b016115..15f224bb80e8f3 100644 --- a/extensions/copilot-chat/src/copilot_chat.rs +++ b/extensions/copilot-chat/src/copilot_chat.rs @@ -11,9 +11,16 @@ const GITHUB_DEVICE_CODE_URL: &str = "https://github.com/login/device/code"; const GITHUB_ACCESS_TOKEN_URL: &str = "https://github.com/login/oauth/access_token"; const GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98"; +struct DeviceFlowState { + device_code: String, + interval: u64, + expires_in: u64, +} + struct CopilotChatProvider { streams: Mutex>, next_stream_id: Mutex, + device_flow_state: Mutex>, } struct StreamState { @@ -428,6 +435,7 @@ impl zed::Extension for CopilotChatProvider { Self { streams: Mutex::new(HashMap::new()), next_stream_id: Mutex::new(0), + device_flow_state: Mutex::new(None), } } @@ -514,11 +522,14 @@ This extension requires an active GitHub Copilot subscription. // No credentials found - return error for background auth checks. // The device flow will be triggered by the host when the user clicks - // the "Sign in with GitHub" button, which calls llm_provider_start_oauth_sign_in. + // the "Sign in with GitHub" button, which calls llm_provider_start_device_flow_sign_in. Err("CredentialsNotFound".to_string()) } - fn llm_provider_start_oauth_sign_in(&mut self, _provider_id: &str) -> Result<(), String> { + fn llm_provider_start_device_flow_sign_in( + &mut self, + _provider_id: &str, + ) -> Result { // Step 1: Request device and user verification codes let device_code_response = llm_oauth_http_request(&LlmOauthHttpRequest { url: GITHUB_DEVICE_CODE_URL.to_string(), @@ -545,6 +556,8 @@ This extension requires an active GitHub Copilot subscription. device_code: String, user_code: String, verification_uri: String, + #[serde(default)] + verification_uri_complete: Option, expires_in: u64, interval: u64, } @@ -552,16 +565,37 @@ This extension requires an active GitHub Copilot subscription. let device_info: DeviceCodeResponse = serde_json::from_str(&device_code_response.body) .map_err(|e| format!("Failed to parse device code response: {}", e))?; - // Step 2: Open browser to verification URL with pre-filled user code - let verification_url = format!( - "{}?user_code={}", - device_info.verification_uri, device_info.user_code - ); + // Store device flow state for polling + *self.device_flow_state.lock().unwrap() = Some(DeviceFlowState { + device_code: device_info.device_code, + interval: device_info.interval, + expires_in: device_info.expires_in, + }); + + // Step 2: Open browser to verification URL + // Use verification_uri_complete if available (has code pre-filled), otherwise construct URL + let verification_url = device_info.verification_uri_complete.unwrap_or_else(|| { + format!( + "{}?user_code={}", + device_info.verification_uri, &device_info.user_code + ) + }); llm_oauth_open_browser(&verification_url)?; - // Step 3: Poll for access token - let poll_interval = Duration::from_secs(device_info.interval.max(5)); - let max_attempts = (device_info.expires_in / device_info.interval.max(5)) as usize; + // Return the user code for the host to display + Ok(device_info.user_code) + } + + fn llm_provider_poll_device_flow_sign_in(&mut self, _provider_id: &str) -> Result<(), String> { + let state = self + .device_flow_state + .lock() + .unwrap() + .take() + .ok_or("No device flow in progress")?; + + let poll_interval = Duration::from_secs(state.interval.max(5)); + let max_attempts = (state.expires_in / state.interval.max(5)) as usize; for _ in 0..max_attempts { thread::sleep(poll_interval); @@ -578,7 +612,7 @@ This extension requires an active GitHub Copilot subscription. ], body: format!( "client_id={}&device_code={}&grant_type=urn:ietf:params:oauth:grant-type:device_code", - GITHUB_COPILOT_CLIENT_ID, device_info.device_code + GITHUB_COPILOT_CLIENT_ID, state.device_code ), })?; From dd7ee503e9d6da10c163470f9d941de2f7fc9ff4 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 23:24:37 -0500 Subject: [PATCH 44/45] Make the "Copy Code" button nicer --- .../src/wasm_host/llm_provider.rs | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 7355e59569c92c..acec25b8258c16 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -897,37 +897,49 @@ impl gpui::Render for ExtensionProviderConfigurationView { let code_for_click = code.clone(); this.child( h_flex() - .w_full() - .p_1() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_sm() - .cursor_pointer() - .justify_between() - .on_mouse_down( - MouseButton::Left, - move |_, window, cx| { - cx.write_to_clipboard( - ClipboardItem::new_string( - code_for_click.clone(), - ), - ); - window.refresh(); - }, - ) + .gap_1() .child( - Label::new(code) + Label::new("Enter code:") .size(LabelSize::Small) - .color(Color::Accent), + .color(Color::Muted), ) .child( - Label::new(if copied { - "Copied!" - } else { - "Click to copy" - }) - .size(LabelSize::Small) - .color(Color::Muted), + h_flex() + .gap_1() + .px_1() + .border_1() + .border_color(cx.theme().colors().border) + .rounded_sm() + .cursor_pointer() + .on_mouse_down( + MouseButton::Left, + move |_, window, cx| { + cx.write_to_clipboard( + ClipboardItem::new_string( + code_for_click.clone(), + ), + ); + window.refresh(); + }, + ) + .child( + Label::new(code) + .size(LabelSize::Small) + .color(Color::Accent), + ) + .child( + ui::Icon::new(if copied { + ui::IconName::Check + } else { + ui::IconName::Copy + }) + .size(ui::IconSize::Small) + .color(if copied { + Color::Success + } else { + Color::Muted + }), + ), ), ) }) From e990ea4aa17710fb86cabbfcb6ca0bd9d530f9be Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 5 Dec 2025 23:34:11 -0500 Subject: [PATCH 45/45] Dynamically fetch Copilot model list --- extensions/copilot-chat/src/copilot_chat.rs | 373 ++++++++++++++------ 1 file changed, 257 insertions(+), 116 deletions(-) diff --git a/extensions/copilot-chat/src/copilot_chat.rs b/extensions/copilot-chat/src/copilot_chat.rs index 15f224bb80e8f3..9d5730e85055a2 100644 --- a/extensions/copilot-chat/src/copilot_chat.rs +++ b/extensions/copilot-chat/src/copilot_chat.rs @@ -9,6 +9,7 @@ use zed_extension_api::{self as zed, *}; const GITHUB_DEVICE_CODE_URL: &str = "https://github.com/login/device/code"; const GITHUB_ACCESS_TOKEN_URL: &str = "https://github.com/login/oauth/access_token"; +const GITHUB_COPILOT_TOKEN_URL: &str = "https://api.github.com/copilot_internal/v2/token"; const GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98"; struct DeviceFlowState { @@ -17,10 +18,69 @@ struct DeviceFlowState { expires_in: u64, } +#[derive(Clone)] +struct ApiToken { + api_key: String, + api_endpoint: String, +} + +#[derive(Clone, Deserialize)] +struct CopilotModel { + id: String, + name: String, + #[serde(default)] + is_chat_default: bool, + #[serde(default)] + is_chat_fallback: bool, + #[serde(default)] + model_picker_enabled: bool, + #[serde(default)] + capabilities: ModelCapabilities, + #[serde(default)] + policy: Option, +} + +#[derive(Clone, Default, Deserialize)] +struct ModelCapabilities { + #[serde(default)] + family: String, + #[serde(default)] + limits: ModelLimits, + #[serde(default)] + supports: ModelSupportedFeatures, + #[serde(rename = "type", default)] + model_type: String, +} + +#[derive(Clone, Default, Deserialize)] +struct ModelLimits { + #[serde(default)] + max_context_window_tokens: u64, + #[serde(default)] + max_output_tokens: u64, +} + +#[derive(Clone, Default, Deserialize)] +struct ModelSupportedFeatures { + #[serde(default)] + streaming: bool, + #[serde(default)] + tool_calls: bool, + #[serde(default)] + vision: bool, +} + +#[derive(Clone, Deserialize)] +struct ModelPolicy { + state: String, +} + struct CopilotChatProvider { streams: Mutex>, next_stream_id: Mutex, device_flow_state: Mutex>, + api_token: Mutex>, + cached_models: Mutex>>, } struct StreamState { @@ -38,95 +98,6 @@ struct AccumulatedToolCall { arguments: String, } -struct ModelDefinition { - id: &'static str, - display_name: &'static str, - max_tokens: u64, - max_output_tokens: Option, - supports_images: bool, - is_default: bool, - is_default_fast: bool, -} - -const MODELS: &[ModelDefinition] = &[ - ModelDefinition { - id: "gpt-4o", - display_name: "GPT-4o", - max_tokens: 128_000, - max_output_tokens: Some(16_384), - supports_images: true, - is_default: true, - is_default_fast: false, - }, - ModelDefinition { - id: "gpt-4o-mini", - display_name: "GPT-4o Mini", - max_tokens: 128_000, - max_output_tokens: Some(16_384), - supports_images: true, - is_default: false, - is_default_fast: true, - }, - ModelDefinition { - id: "gpt-4.1", - display_name: "GPT-4.1", - max_tokens: 1_000_000, - max_output_tokens: Some(32_768), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "o1", - display_name: "o1", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "o3-mini", - display_name: "o3-mini", - max_tokens: 200_000, - max_output_tokens: Some(100_000), - supports_images: false, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "claude-3.5-sonnet", - display_name: "Claude 3.5 Sonnet", - max_tokens: 200_000, - max_output_tokens: Some(8_192), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "claude-3.7-sonnet", - display_name: "Claude 3.7 Sonnet", - max_tokens: 200_000, - max_output_tokens: Some(8_192), - supports_images: true, - is_default: false, - is_default_fast: false, - }, - ModelDefinition { - id: "gemini-2.0-flash-001", - display_name: "Gemini 2.0 Flash", - max_tokens: 1_000_000, - max_output_tokens: Some(8_192), - supports_images: true, - is_default: false, - is_default_fast: false, - }, -]; - -fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> { - MODELS.iter().find(|m| m.id == model_id) -} - #[derive(Serialize)] struct OpenAiRequest { model: String, @@ -402,10 +373,7 @@ fn convert_request( LlmToolChoice::None => "none".to_string(), }); - let model_def = get_model_definition(model_id); - let max_tokens = request - .max_tokens - .or(model_def.and_then(|m| m.max_output_tokens)); + let max_tokens = request.max_tokens; Ok(OpenAiRequest { model: model_id.to_string(), @@ -436,6 +404,8 @@ impl zed::Extension for CopilotChatProvider { streams: Mutex::new(HashMap::new()), next_stream_id: Mutex::new(0), device_flow_state: Mutex::new(None), + api_token: Mutex::new(None), + cached_models: Mutex::new(None), } } @@ -448,26 +418,27 @@ impl zed::Extension for CopilotChatProvider { } fn llm_provider_models(&self, _provider_id: &str) -> Result, String> { - Ok(MODELS - .iter() - .map(|m| LlmModelInfo { - id: m.id.to_string(), - name: m.display_name.to_string(), - max_token_count: m.max_tokens, - max_output_tokens: m.max_output_tokens, - capabilities: LlmModelCapabilities { - supports_images: m.supports_images, - supports_tools: true, - supports_tool_choice_auto: true, - supports_tool_choice_any: true, - supports_tool_choice_none: true, - supports_thinking: false, - tool_input_format: LlmToolInputFormat::JsonSchema, - }, - is_default: m.is_default, - is_default_fast: m.is_default_fast, - }) - .collect()) + // Try to get models from cache first + if let Some(models) = self.cached_models.lock().unwrap().as_ref() { + return Ok(convert_models_to_llm_info(models)); + } + + // Need to fetch models - requires authentication + let oauth_token = match llm_get_credential("copilot-chat") { + Some(token) => token, + None => return Ok(Vec::new()), // Not authenticated, return empty + }; + + // Get API token + let api_token = self.get_api_token(&oauth_token)?; + + // Fetch models from API + let models = self.fetch_models(&api_token)?; + + // Cache the models + *self.cached_models.lock().unwrap() = Some(models.clone()); + + Ok(convert_models_to_llm_info(&models)) } fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool { @@ -660,6 +631,9 @@ This extension requires an active GitHub Copilot subscription. } fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> { + // Clear cached API token and models + *self.api_token.lock().unwrap() = None; + *self.cached_models.lock().unwrap() = None; llm_delete_credential("copilot-chat") } @@ -669,21 +643,29 @@ This extension requires an active GitHub Copilot subscription. model_id: &str, request: &LlmCompletionRequest, ) -> Result { - let api_key = llm_get_credential("copilot-chat").ok_or_else(|| { + let oauth_token = llm_get_credential("copilot-chat").ok_or_else(|| { "No token configured. Please add your GitHub Copilot token in settings.".to_string() })?; + // Get or refresh API token + let api_token = self.get_api_token(&oauth_token)?; + let openai_request = convert_request(model_id, request)?; let body = serde_json::to_vec(&openai_request) .map_err(|e| format!("Failed to serialize request: {}", e))?; + let completions_url = format!("{}/chat/completions", api_token.api_endpoint); + let http_request = HttpRequest { method: HttpMethod::Post, - url: "https://api.githubcopilot.com/chat/completions".to_string(), + url: completions_url, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), - ("Authorization".to_string(), format!("Bearer {}", api_key)), + ( + "Authorization".to_string(), + format!("Bearer {}", api_token.api_key), + ), ( "Copilot-Integration-Id".to_string(), "vscode-chat".to_string(), @@ -836,6 +818,165 @@ This extension requires an active GitHub Copilot subscription. } } +impl CopilotChatProvider { + fn get_api_token(&self, oauth_token: &str) -> Result { + // Check if we have a cached token + if let Some(token) = self.api_token.lock().unwrap().clone() { + return Ok(token); + } + + // Request a new API token + let http_request = HttpRequest { + method: HttpMethod::Get, + url: GITHUB_COPILOT_TOKEN_URL.to_string(), + headers: vec![ + ( + "Authorization".to_string(), + format!("token {}", oauth_token), + ), + ("Accept".to_string(), "application/json".to_string()), + ], + body: None, + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response = http_request + .fetch() + .map_err(|e| format!("Failed to request API token: {}", e))?; + + #[derive(Deserialize)] + struct ApiTokenResponse { + token: String, + endpoints: ApiEndpoints, + } + + #[derive(Deserialize)] + struct ApiEndpoints { + api: String, + } + + let token_response: ApiTokenResponse = + serde_json::from_slice(&response.body).map_err(|e| { + format!( + "Failed to parse API token response: {} - body: {}", + e, + String::from_utf8_lossy(&response.body) + ) + })?; + + let api_token = ApiToken { + api_key: token_response.token, + api_endpoint: token_response.endpoints.api, + }; + + // Cache the token + *self.api_token.lock().unwrap() = Some(api_token.clone()); + + Ok(api_token) + } + + fn fetch_models(&self, api_token: &ApiToken) -> Result, String> { + let models_url = format!("{}/models", api_token.api_endpoint); + + let http_request = HttpRequest { + method: HttpMethod::Get, + url: models_url, + headers: vec![ + ( + "Authorization".to_string(), + format!("Bearer {}", api_token.api_key), + ), + ("Content-Type".to_string(), "application/json".to_string()), + ( + "Copilot-Integration-Id".to_string(), + "vscode-chat".to_string(), + ), + ("Editor-Version".to_string(), "Zed/1.0.0".to_string()), + ("x-github-api-version".to_string(), "2025-05-01".to_string()), + ], + body: None, + redirect_policy: RedirectPolicy::FollowAll, + }; + + let response = http_request + .fetch() + .map_err(|e| format!("Failed to fetch models: {}", e))?; + + #[derive(Deserialize)] + struct ModelsResponse { + data: Vec, + } + + let models_response: ModelsResponse = + serde_json::from_slice(&response.body).map_err(|e| { + format!( + "Failed to parse models response: {} - body: {}", + e, + String::from_utf8_lossy(&response.body) + ) + })?; + + // Filter models like the built-in Copilot Chat does + let mut models: Vec = models_response + .data + .into_iter() + .filter(|model| { + model.model_picker_enabled + && model.capabilities.model_type == "chat" + && model + .policy + .as_ref() + .map(|p| p.state == "enabled") + .unwrap_or(true) + }) + .collect(); + + // Sort so default model is first + if let Some(pos) = models.iter().position(|m| m.is_chat_default) { + let default_model = models.remove(pos); + models.insert(0, default_model); + } + + Ok(models) + } +} + +fn convert_models_to_llm_info(models: &[CopilotModel]) -> Vec { + models + .iter() + .map(|m| { + let max_tokens = if m.capabilities.limits.max_context_window_tokens > 0 { + m.capabilities.limits.max_context_window_tokens + } else { + 128_000 // Default fallback + }; + let max_output = if m.capabilities.limits.max_output_tokens > 0 { + Some(m.capabilities.limits.max_output_tokens) + } else { + None + }; + + LlmModelInfo { + id: m.id.clone(), + name: m.name.clone(), + max_token_count: max_tokens, + max_output_tokens: max_output, + capabilities: LlmModelCapabilities { + supports_images: m.capabilities.supports.vision, + supports_tools: m.capabilities.supports.tool_calls, + supports_tool_choice_auto: m.capabilities.supports.tool_calls, + supports_tool_choice_any: m.capabilities.supports.tool_calls, + supports_tool_choice_none: m.capabilities.supports.tool_calls, + supports_thinking: false, + tool_input_format: LlmToolInputFormat::JsonSchema, + }, + is_default: m.is_chat_default, + is_default_fast: m.is_chat_fallback, + } + }) + .collect() +} + #[cfg(test)] mod tests { use super::*;