Files

1 line
46 KiB
JSON

{"repo": "agustif/rustty", "files": {"gpui-source/crates/language_models/src/provider/deepseek.rs": {"74f23b45ed0c55509118d926921be63a6436ac46": {"t": 1785745854.489968, "n": 22570, "h": "82ca7e631f6643d6", "c": "use anyhow::{Result, anyhow};\nuse collections::{BTreeMap, HashMap};\nuse deepseek::DEEPSEEK_API_URL;\n\nuse futures::Stream;\nuse futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream};\nuse gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};\nuse http_client::HttpClient;\nuse language_model::{\n AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,\n LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,\n LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,\n LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,\n RateLimiter, Role, StopReason, TokenUsage,\n};\npub use settings::DeepseekAvailableModel as AvailableModel;\nuse settings::{Settings, SettingsStore};\nuse std::pin::Pin;\nuse std::str::FromStr;\nuse std::sync::{Arc, LazyLock};\n\nuse ui::{List, prelude::*};\nuse ui_input::InputField;\nuse util::ResultExt;\nuse zed_env_vars::{EnvVar, env_var};\n\nuse crate::ui::ConfiguredApiCard;\nuse crate::{api_key::ApiKeyState, ui::InstructionListItem};\n\nconst PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new(\"deepseek\");\nconst PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new(\"DeepSeek\");\n\nconst API_KEY_ENV_VAR_NAME: &str = \"DEEPSEEK_API_KEY\";\nstatic API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);\n\n#[derive(Default)]\nstruct RawToolCall {\n id: String,\n name: String,\n arguments: String,\n}\n\n#[derive(Default, Clone, Debug, PartialEq)]\npub struct DeepSeekSettings {\n pub api_url: String,\n pub available_models: Vec<AvailableModel>,\n}\npub struct DeepSeekLanguageModelProvider {\n http_client: Arc<dyn HttpClient>,\n state: Entity<State>,\n}\n\npub struct State {\n api_key_state: ApiKeyState,\n}\n\nimpl State {\n fn is_authenticated(&self) -> bool {\n self.api_key_state.has_key()\n }\n\n fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {\n let api_url = DeepSeekLanguageModelProvider::api_url(cx);\n self.api_key_state\n .store(api_url, api_key, |this| &mut this.api_key_state, cx)\n }\n\n fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {\n let api_url = DeepSeekLanguageModelProvider::api_url(cx);\n self.api_key_state.load_if_needed(\n api_url,\n &API_KEY_ENV_VAR,\n |this| &mut this.api_key_state,\n cx,\n )\n }\n}\n\nimpl DeepSeekLanguageModelProvider {\n pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {\n let state = cx.new(|cx| {\n cx.observe_global::<SettingsStore>(|this: &mut State, cx| {\n let api_url = Self::api_url(cx);\n this.api_key_state.handle_url_change(\n api_url,\n &API_KEY_ENV_VAR,\n |this| &mut this.api_key_state,\n cx,\n );\n cx.notify();\n })\n .detach();\n State {\n api_key_state: ApiKeyState::new(Self::api_url(cx)),\n }\n });\n\n Self { http_client, state }\n }\n\n fn create_language_model(&self, model: deepseek::Model) -> Arc<dyn LanguageModel> {\n Arc::new(DeepSeekLanguageModel {\n id: LanguageModelId::from(model.id().to_string()),\n model,\n state: self.state.clone(),\n http_client: self.http_client.clone(),\n request_limiter: RateLimiter::new(4),\n })\n }\n\n fn settings(cx: &App) -> &DeepSeekSettings {\n &crate::AllLanguageModelSettings::get_global(cx).deepseek\n }\n\n fn api_url(cx: &App) -> SharedString {\n let api_url = &Self::settings(cx).api_url;\n if api_url.is_empty() {\n DEEPSEEK_API_URL.into()\n } else {\n SharedString::new(api_url.as_str())\n }\n }\n}\n\nimpl LanguageModelProviderState for DeepSeekLanguageModelProvider {\n type ObservableEntity = State;\n\n fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {\n Some(self.state.clone())\n }\n}\n\nimpl LanguageModelProvider for DeepSeekLanguageModelProvider {\n fn id(&self) -> LanguageModelProviderId {\n PROVIDER_ID\n }\n\n fn name(&self) -> LanguageModelProviderName {\n PROVIDER_NAME\n }\n\n fn icon(&self) -> IconName {\n IconName::AiDeepSeek\n }\n\n fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {\n Some(self.create_language_model(deepseek::Model::default()))\n }\n\n fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {\n Some(self.create_language_model(deepseek::Model::default_fast()))\n }\n\n fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {\n let mut models = BTreeMap::default();\n\n models.insert(\"deepseek-chat\", deepseek::Model::Chat);\n models.insert(\"deepseek-reasoner\", deepseek::Model::Reasoner);\n\n for available_model in &Self::settings(cx).available_models {\n models.insert(\n &available_model.name,\n deepseek::Model::Custom {\n name: available_model.name.clone(),\n display_name: available_model.display_name.clone(),\n max_tokens: available_model.max_tokens,\n max_output_tokens: available_model.max_output_tokens,\n },\n );\n }\n\n models\n .into_values()\n .map(|model| self.create_language_model(model))\n .collect()\n }\n\n fn is_authenticated(&self, cx: &App) -> bool {\n self.state.read(cx).is_authenticated()\n }\n\n fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {\n self.state.update(cx, |state, cx| state.authenticate(cx))\n }\n\n fn configuration_view(\n &self,\n _target_agent: language_model::ConfigurationViewTargetAgent,\n window: &mut Window,\n cx: &mut App,\n ) -> AnyView {\n cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))\n .into()\n }\n\n fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {\n self.state\n .update(cx, |state, cx| state.set_api_key(None, cx))\n }\n}\n\npub struct DeepSeekLanguageModel {\n id: LanguageModelId,\n model: deepseek::Model,\n state: Entity<State>,\n http_client: Arc<dyn HttpClient>,\n request_limiter: RateLimiter,\n}\n\nimpl DeepSeekLanguageModel {\n fn stream_completion(\n &self,\n request: deepseek::Request,\n cx: &AsyncApp,\n ) -> BoxFuture<'static, Result<BoxStream<'static, Result<deepseek::StreamResponse>>>> {\n let http_client = self.http_client.clone();\n\n let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {\n let api_url = DeepSeekLanguageModelProvider::api_url(cx);\n (state.api_key_state.key(&api_url), api_url)\n }) else {\n return future::ready(Err(anyhow!(\"App state dropped\"))).boxed();\n };\n\n let future = self.request_limiter.stream(async move {\n let Some(api_key) = api_key else {\n return Err(LanguageModelCompletionError::NoApiKey {\n provider: PROVIDER_NAME,\n });\n };\n let request =\n deepseek::stream_completion(http_client.as_ref(), &api_url, &api_key, request);\n let response = request.await?;\n Ok(response)\n });\n\n async move { Ok(future.await?.boxed()) }.boxed()\n }\n}\n\nimpl LanguageModel for DeepSeekLanguageModel {\n fn id(&self) -> LanguageModelId {\n self.id.clone()\n }\n\n fn name(&self) -> LanguageModelName {\n LanguageModelName::from(self.model.display_name().to_string())\n }\n\n fn provider_id(&self) -> LanguageModelProviderId {\n PROVIDER_ID\n }\n\n fn provider_name(&self) -> LanguageModelProviderName {\n PROVIDER_NAME\n }\n\n fn supports_tools(&self) -> bool {\n true\n }\n\n fn supports_tool_choice(&self, _choice: LanguageModelToolChoice) -> bool {\n true\n }\n\n fn supports_images(&self) -> bool {\n false\n }\n\n fn telemetry_id(&self) -> String {\n format!(\"deepseek/{}\", self.model.id())\n }\n\n fn max_token_count(&self) -> u64 {\n self.model.max_token_count()\n }\n\n fn max_output_tokens(&self) -> Option<u64> {\n self.model.max_output_tokens()\n }\n\n fn count_tokens(\n &self,\n request: LanguageModelRequest,\n cx: &App,\n ) -> BoxFuture<'static, Result<u64>> {\n cx.background_spawn(async move {\n let messages = request\n .messages\n .into_iter()\n .map(|message| tiktoken_rs::ChatCompletionRequestMessage {\n role: match message.role {\n Role::User => \"user\".into(),\n Role::Assistant => \"assistant\".into(),\n Role::System => \"system\".into(),\n },\n content: Some(message.string_contents()),\n name: None,\n function_call: None,\n })\n .collect::<Vec<_>>();\n\n tiktoken_rs::num_tokens_from_messages(\"gpt-4\", &messages).map(|tokens| tokens as u64)\n })\n .boxed()\n }\n\n fn stream_completion(\n &self,\n request: LanguageModelRequest,\n cx: &AsyncApp,\n ) -> BoxFuture<\n 'static,\n Result<\n BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,\n LanguageModelCompletionError,\n >,\n > {\n let request = into_deepseek(request, &self.model, self.max_output_tokens());\n let stream = self.stream_completion(request, cx);\n\n async move {\n let mapper = DeepSeekEventMapper::new();\n Ok(mapper.map_stream(stream.await?).boxed())\n }\n .boxed()\n }\n}\n\npub fn into_deepseek(\n request: LanguageModelRequest,\n model: &deepseek::Model,\n max_output_tokens: Option<u64>,\n) -> deepseek::Request {\n let is_reasoner = model == &deepseek::Model::Reasoner;\n\n let mut messages = Vec::new();\n let mut current_reasoning: Option<String> = None;\n\n for message in request.messages {\n for content in message.content {\n match content {\n MessageContent::Text(text) => messages.push(match message.role {\n Role::User => deepseek::RequestMessage::User { content: text },\n Role::Assistant => deepseek::RequestMessage::Assistant {\n content: Some(text),\n tool_calls: Vec::new(),\n reasoning_content: current_reasoning.take(),\n },\n Role::System => deepseek::RequestMessage::System { content: text },\n }),\n MessageContent::Thinking { text, .. } => {\n // Accumulate reasoning content for next assistant message\n current_reasoning.get_or_insert_default().push_str(&text);\n }\n MessageContent::RedactedThinking(_) => {}\n MessageContent::Image(_) => {}\n MessageContent::ToolUse(tool_use) => {\n let tool_call = deepseek::ToolCall {\n id: tool_use.id.to_string(),\n content: deepseek::ToolCallContent::Function {\n function: deepseek::FunctionContent {\n name: tool_use.name.to_string(),\n arguments: serde_json::to_string(&tool_use.input)\n .unwrap_or_default(),\n },\n },\n };\n\n if let Some(deepseek::RequestMessage::Assistant { tool_calls, .. }) =\n messages.last_mut()\n {\n tool_calls.push(tool_call);\n } else {\n messages.push(deepseek::RequestMessage::Assistant {\n content: None,\n tool_calls: vec![tool_call],\n reasoning_content: current_reasoning.take(),\n });\n }\n }\n MessageContent::ToolResult(tool_result) => {\n match &tool_result.content {\n LanguageModelToolResultContent::Text(text) => {\n messages.push(deepseek::RequestMessage::Tool {\n content: text.to_string(),\n tool_call_id: tool_result.tool_use_id.to_string(),\n });\n }\n LanguageModelToolResultContent::Image(_) => {}\n };\n }\n }\n }\n }\n\n deepseek::Request {\n model: model.id().to_string(),\n messages,\n stream: true,\n max_tokens: max_output_tokens,\n temperature: if is_reasoner {\n None\n } else {\n request.temperature\n },\n response_format: None,\n tools: request\n .tools\n .into_iter()\n .map(|tool| deepseek::ToolDefinition::Function {\n function: deepseek::FunctionDefinition {\n name: tool.name,\n description: Some(tool.description),\n parameters: Some(tool.input_schema),\n },\n })\n .collect(),\n }\n}\n\npub struct DeepSeekEventMapper {\n tool_calls_by_index: HashMap<usize, RawToolCall>,\n}\n\nimpl DeepSeekEventMapper {\n pub fn new() -> Self {\n Self {\n tool_calls_by_index: HashMap::default(),\n }\n }\n\n pub fn map_stream(\n mut self,\n events: Pin<Box<dyn Send + Stream<Item = Result<deepseek::StreamResponse>>>>,\n ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>\n {\n events.flat_map(move |event| {\n futures::stream::iter(match event {\n Ok(event) => self.map_event(event),\n Err(error) => vec![Err(LanguageModelCompletionError::from(error))],\n })\n })\n }\n\n pub fn map_event(\n &mut self,\n event: deepseek::StreamResponse,\n ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {\n let Some(choice) = event.choices.first() else {\n return vec![Err(LanguageModelCompletionError::from(anyhow!(\n \"Response contained no choices\"\n )))];\n };\n\n let mut events = Vec::new();\n if let Some(content) = choice.delta.content.clone() {\n events.push(Ok(LanguageModelCompletionEvent::Text(content)));\n }\n\n if let Some(reasoning_content) = choice.delta.reasoning_content.clone() {\n events.push(Ok(LanguageModelCompletionEvent::Thinking {\n text: reasoning_content,\n signature: None,\n }));\n }\n\n if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {\n for tool_call in tool_calls {\n let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();\n\n if let Some(tool_id) = tool_call.id.clone() {\n entry.id = tool_id;\n }\n\n if let Some(function) = tool_call.function.as_ref() {\n if let Some(name) = function.name.clone() {\n entry.name = name;\n }\n\n if let Some(arguments) = function.arguments.clone() {\n entry.arguments.push_str(&arguments);\n }\n }\n }\n }\n\n if let Some(usage) = event.usage {\n events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {\n input_tokens: usage.prompt_tokens,\n output_tokens: usage.completion_tokens,\n cache_creation_input_tokens: 0,\n cache_read_input_tokens: 0,\n })));\n }\n\n match choice.finish_reason.as_deref() {\n Some(\"stop\") => {\n events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));\n }\n Some(\"tool_calls\") => {\n events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {\n match serde_json::Value::from_str(&tool_call.arguments) {\n Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(\n LanguageModelToolUse {\n id: tool_call.id.clone().into(),\n name: tool_call.name.as_str().into(),\n is_input_complete: true,\n input,\n raw_input: tool_call.arguments.clone(),\n thought_signature: None,\n },\n )),\n Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {\n id: tool_call.id.clone().into(),\n tool_name: tool_call.name.as_str().into(),\n raw_input: tool_call.arguments.into(),\n json_parse_error: error.to_string(),\n }),\n }\n }));\n\n events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));\n }\n Some(stop_reason) => {\n log::error!(\"Unexpected DeepSeek stop_reason: {stop_reason:?}\",);\n events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));\n }\n None => {}\n }\n\n events\n }\n}\n\nstruct ConfigurationView {\n api_key_editor: Entity<InputField>,\n state: Entity<State>,\n load_credentials_task: Option<Task<()>>,\n}\n\nimpl ConfigurationView {\n fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {\n let api_key_editor =\n cx.new(|cx| InputField::new(window, cx, \"sk-00000000000000000000000000000000\"));\n\n cx.observe(&state, |_, _, cx| {\n cx.notify();\n })\n .detach();\n\n let load_credentials_task = Some(cx.spawn({\n let state = state.clone();\n async move |this, cx| {\n if let Some(task) = state\n .update(cx, |state, cx| state.authenticate(cx))\n .log_err()\n {\n let _ = task.await;\n }\n\n this.update(cx, |this, cx| {\n this.load_credentials_task = None;\n cx.notify();\n })\n .log_err();\n }\n }));\n\n Self {\n api_key_editor,\n state,\n load_credentials_task,\n }\n }\n\n fn save_api_key(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {\n let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();\n if api_key.is_empty() {\n return;\n }\n\n let state = self.state.clone();\n cx.spawn(async move |_, cx| {\n state\n .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))?\n .await\n })\n .detach_and_log_err(cx);\n }\n\n fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {\n self.api_key_editor\n .update(cx, |editor, cx| editor.set_text(\"\", window, cx));\n\n let state = self.state.clone();\n cx.spawn(async move |_, cx| {\n state\n .update(cx, |state, cx| state.set_api_key(None, cx))?\n .await\n })\n .detach_and_log_err(cx);\n }\n\n fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {\n !self.state.read(cx).is_authenticated()\n }\n}\n\nimpl Render for ConfigurationView {\n fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();\n let configured_card_label = if env_var_set {\n format!(\"API key set in {API_KEY_ENV_VAR_NAME} environment variable\")\n } else {\n let api_url = DeepSeekLanguageModelProvider::api_url(cx);\n if api_url == DEEPSEEK_API_URL {\n \"API key configured\".to_string()\n } else {\n format!(\"API key configured for {}\", api_url)\n }\n };\n\n if self.load_credentials_task.is_some() {\n div()\n .child(Label::new(\"Loading credentials...\"))\n .into_any_element()\n } else if self.should_render_editor(cx) {\n v_flex()\n .size_full()\n .on_action(cx.listener(Self::save_api_key))\n .child(Label::new(\"To use DeepSeek in Zed, you need an API key:\"))\n .child(\n List::new()\n .child(InstructionListItem::new(\n \"Get your API key from the\",\n Some(\"DeepSeek console\"),\n Some(\"https://platform.deepseek.com/api_keys\"),\n ))\n .child(InstructionListItem::text_only(\n \"Paste your API key below and hit enter to start using the assistant\",\n )),\n )\n .child(self.api_key_editor.clone())\n .child(\n Label::new(format!(\n \"Or set the {API_KEY_ENV_VAR_NAME} environment variable.\"\n ))\n .size(LabelSize::Small)\n .color(Color::Muted),\n )\n .into_any_element()\n } else {\n ConfiguredApiCard::new(configured_card_label)\n .disabled(env_var_set)\n .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))\n .into_any_element()\n }\n }\n}\n"}, "4ea84853f0ae22b4a68f3a96bc32cb184000ba0a": {"t": 1785757282.2687488, "n": 22570, "h": "82ca7e631f6643d6", "c": "use anyhow::{Result, anyhow};\nuse collections::{BTreeMap, HashMap};\nuse deepseek::DEEPSEEK_API_URL;\n\nuse futures::Stream;\nuse futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream};\nuse gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};\nuse http_client::HttpClient;\nuse language_model::{\n AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,\n LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,\n LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,\n LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,\n RateLimiter, Role, StopReason, TokenUsage,\n};\npub use settings::DeepseekAvailableModel as AvailableModel;\nuse settings::{Settings, SettingsStore};\nuse std::pin::Pin;\nuse std::str::FromStr;\nuse std::sync::{Arc, LazyLock};\n\nuse ui::{List, prelude::*};\nuse ui_input::InputField;\nuse util::ResultExt;\nuse zed_env_vars::{EnvVar, env_var};\n\nuse crate::ui::ConfiguredApiCard;\nuse crate::{api_key::ApiKeyState, ui::InstructionListItem};\n\nconst PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new(\"deepseek\");\nconst PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new(\"DeepSeek\");\n\nconst API_KEY_ENV_VAR_NAME: &str = \"DEEPSEEK_API_KEY\";\nstatic API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);\n\n#[derive(Default)]\nstruct RawToolCall {\n id: String,\n name: String,\n arguments: String,\n}\n\n#[derive(Default, Clone, Debug, PartialEq)]\npub struct DeepSeekSettings {\n pub api_url: String,\n pub available_models: Vec<AvailableModel>,\n}\npub struct DeepSeekLanguageModelProvider {\n http_client: Arc<dyn HttpClient>,\n state: Entity<State>,\n}\n\npub struct State {\n api_key_state: ApiKeyState,\n}\n\nimpl State {\n fn is_authenticated(&self) -> bool {\n self.api_key_state.has_key()\n }\n\n fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {\n let api_url = DeepSeekLanguageModelProvider::api_url(cx);\n self.api_key_state\n .store(api_url, api_key, |this| &mut this.api_key_state, cx)\n }\n\n fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {\n let api_url = DeepSeekLanguageModelProvider::api_url(cx);\n self.api_key_state.load_if_needed(\n api_url,\n &API_KEY_ENV_VAR,\n |this| &mut this.api_key_state,\n cx,\n )\n }\n}\n\nimpl DeepSeekLanguageModelProvider {\n pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {\n let state = cx.new(|cx| {\n cx.observe_global::<SettingsStore>(|this: &mut State, cx| {\n let api_url = Self::api_url(cx);\n this.api_key_state.handle_url_change(\n api_url,\n &API_KEY_ENV_VAR,\n |this| &mut this.api_key_state,\n cx,\n );\n cx.notify();\n })\n .detach();\n State {\n api_key_state: ApiKeyState::new(Self::api_url(cx)),\n }\n });\n\n Self { http_client, state }\n }\n\n fn create_language_model(&self, model: deepseek::Model) -> Arc<dyn LanguageModel> {\n Arc::new(DeepSeekLanguageModel {\n id: LanguageModelId::from(model.id().to_string()),\n model,\n state: self.state.clone(),\n http_client: self.http_client.clone(),\n request_limiter: RateLimiter::new(4),\n })\n }\n\n fn settings(cx: &App) -> &DeepSeekSettings {\n &crate::AllLanguageModelSettings::get_global(cx).deepseek\n }\n\n fn api_url(cx: &App) -> SharedString {\n let api_url = &Self::settings(cx).api_url;\n if api_url.is_empty() {\n DEEPSEEK_API_URL.into()\n } else {\n SharedString::new(api_url.as_str())\n }\n }\n}\n\nimpl LanguageModelProviderState for DeepSeekLanguageModelProvider {\n type ObservableEntity = State;\n\n fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {\n Some(self.state.clone())\n }\n}\n\nimpl LanguageModelProvider for DeepSeekLanguageModelProvider {\n fn id(&self) -> LanguageModelProviderId {\n PROVIDER_ID\n }\n\n fn name(&self) -> LanguageModelProviderName {\n PROVIDER_NAME\n }\n\n fn icon(&self) -> IconName {\n IconName::AiDeepSeek\n }\n\n fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {\n Some(self.create_language_model(deepseek::Model::default()))\n }\n\n fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {\n Some(self.create_language_model(deepseek::Model::default_fast()))\n }\n\n fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {\n let mut models = BTreeMap::default();\n\n models.insert(\"deepseek-chat\", deepseek::Model::Chat);\n models.insert(\"deepseek-reasoner\", deepseek::Model::Reasoner);\n\n for available_model in &Self::settings(cx).available_models {\n models.insert(\n &available_model.name,\n deepseek::Model::Custom {\n name: available_model.name.clone(),\n display_name: available_model.display_name.clone(),\n max_tokens: available_model.max_tokens,\n max_output_tokens: available_model.max_output_tokens,\n },\n );\n }\n\n models\n .into_values()\n .map(|model| self.create_language_model(model))\n .collect()\n }\n\n fn is_authenticated(&self, cx: &App) -> bool {\n self.state.read(cx).is_authenticated()\n }\n\n fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {\n self.state.update(cx, |state, cx| state.authenticate(cx))\n }\n\n fn configuration_view(\n &self,\n _target_agent: language_model::ConfigurationViewTargetAgent,\n window: &mut Window,\n cx: &mut App,\n ) -> AnyView {\n cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))\n .into()\n }\n\n fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {\n self.state\n .update(cx, |state, cx| state.set_api_key(None, cx))\n }\n}\n\npub struct DeepSeekLanguageModel {\n id: LanguageModelId,\n model: deepseek::Model,\n state: Entity<State>,\n http_client: Arc<dyn HttpClient>,\n request_limiter: RateLimiter,\n}\n\nimpl DeepSeekLanguageModel {\n fn stream_completion(\n &self,\n request: deepseek::Request,\n cx: &AsyncApp,\n ) -> BoxFuture<'static, Result<BoxStream<'static, Result<deepseek::StreamResponse>>>> {\n let http_client = self.http_client.clone();\n\n let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {\n let api_url = DeepSeekLanguageModelProvider::api_url(cx);\n (state.api_key_state.key(&api_url), api_url)\n }) else {\n return future::ready(Err(anyhow!(\"App state dropped\"))).boxed();\n };\n\n let future = self.request_limiter.stream(async move {\n let Some(api_key) = api_key else {\n return Err(LanguageModelCompletionError::NoApiKey {\n provider: PROVIDER_NAME,\n });\n };\n let request =\n deepseek::stream_completion(http_client.as_ref(), &api_url, &api_key, request);\n let response = request.await?;\n Ok(response)\n });\n\n async move { Ok(future.await?.boxed()) }.boxed()\n }\n}\n\nimpl LanguageModel for DeepSeekLanguageModel {\n fn id(&self) -> LanguageModelId {\n self.id.clone()\n }\n\n fn name(&self) -> LanguageModelName {\n LanguageModelName::from(self.model.display_name().to_string())\n }\n\n fn provider_id(&self) -> LanguageModelProviderId {\n PROVIDER_ID\n }\n\n fn provider_name(&self) -> LanguageModelProviderName {\n PROVIDER_NAME\n }\n\n fn supports_tools(&self) -> bool {\n true\n }\n\n fn supports_tool_choice(&self, _choice: LanguageModelToolChoice) -> bool {\n true\n }\n\n fn supports_images(&self) -> bool {\n false\n }\n\n fn telemetry_id(&self) -> String {\n format!(\"deepseek/{}\", self.model.id())\n }\n\n fn max_token_count(&self) -> u64 {\n self.model.max_token_count()\n }\n\n fn max_output_tokens(&self) -> Option<u64> {\n self.model.max_output_tokens()\n }\n\n fn count_tokens(\n &self,\n request: LanguageModelRequest,\n cx: &App,\n ) -> BoxFuture<'static, Result<u64>> {\n cx.background_spawn(async move {\n let messages = request\n .messages\n .into_iter()\n .map(|message| tiktoken_rs::ChatCompletionRequestMessage {\n role: match message.role {\n Role::User => \"user\".into(),\n Role::Assistant => \"assistant\".into(),\n Role::System => \"system\".into(),\n },\n content: Some(message.string_contents()),\n name: None,\n function_call: None,\n })\n .collect::<Vec<_>>();\n\n tiktoken_rs::num_tokens_from_messages(\"gpt-4\", &messages).map(|tokens| tokens as u64)\n })\n .boxed()\n }\n\n fn stream_completion(\n &self,\n request: LanguageModelRequest,\n cx: &AsyncApp,\n ) -> BoxFuture<\n 'static,\n Result<\n BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,\n LanguageModelCompletionError,\n >,\n > {\n let request = into_deepseek(request, &self.model, self.max_output_tokens());\n let stream = self.stream_completion(request, cx);\n\n async move {\n let mapper = DeepSeekEventMapper::new();\n Ok(mapper.map_stream(stream.await?).boxed())\n }\n .boxed()\n }\n}\n\npub fn into_deepseek(\n request: LanguageModelRequest,\n model: &deepseek::Model,\n max_output_tokens: Option<u64>,\n) -> deepseek::Request {\n let is_reasoner = model == &deepseek::Model::Reasoner;\n\n let mut messages = Vec::new();\n let mut current_reasoning: Option<String> = None;\n\n for message in request.messages {\n for content in message.content {\n match content {\n MessageContent::Text(text) => messages.push(match message.role {\n Role::User => deepseek::RequestMessage::User { content: text },\n Role::Assistant => deepseek::RequestMessage::Assistant {\n content: Some(text),\n tool_calls: Vec::new(),\n reasoning_content: current_reasoning.take(),\n },\n Role::System => deepseek::RequestMessage::System { content: text },\n }),\n MessageContent::Thinking { text, .. } => {\n // Accumulate reasoning content for next assistant message\n current_reasoning.get_or_insert_default().push_str(&text);\n }\n MessageContent::RedactedThinking(_) => {}\n MessageContent::Image(_) => {}\n MessageContent::ToolUse(tool_use) => {\n let tool_call = deepseek::ToolCall {\n id: tool_use.id.to_string(),\n content: deepseek::ToolCallContent::Function {\n function: deepseek::FunctionContent {\n name: tool_use.name.to_string(),\n arguments: serde_json::to_string(&tool_use.input)\n .unwrap_or_default(),\n },\n },\n };\n\n if let Some(deepseek::RequestMessage::Assistant { tool_calls, .. }) =\n messages.last_mut()\n {\n tool_calls.push(tool_call);\n } else {\n messages.push(deepseek::RequestMessage::Assistant {\n content: None,\n tool_calls: vec![tool_call],\n reasoning_content: current_reasoning.take(),\n });\n }\n }\n MessageContent::ToolResult(tool_result) => {\n match &tool_result.content {\n LanguageModelToolResultContent::Text(text) => {\n messages.push(deepseek::RequestMessage::Tool {\n content: text.to_string(),\n tool_call_id: tool_result.tool_use_id.to_string(),\n });\n }\n LanguageModelToolResultContent::Image(_) => {}\n };\n }\n }\n }\n }\n\n deepseek::Request {\n model: model.id().to_string(),\n messages,\n stream: true,\n max_tokens: max_output_tokens,\n temperature: if is_reasoner {\n None\n } else {\n request.temperature\n },\n response_format: None,\n tools: request\n .tools\n .into_iter()\n .map(|tool| deepseek::ToolDefinition::Function {\n function: deepseek::FunctionDefinition {\n name: tool.name,\n description: Some(tool.description),\n parameters: Some(tool.input_schema),\n },\n })\n .collect(),\n }\n}\n\npub struct DeepSeekEventMapper {\n tool_calls_by_index: HashMap<usize, RawToolCall>,\n}\n\nimpl DeepSeekEventMapper {\n pub fn new() -> Self {\n Self {\n tool_calls_by_index: HashMap::default(),\n }\n }\n\n pub fn map_stream(\n mut self,\n events: Pin<Box<dyn Send + Stream<Item = Result<deepseek::StreamResponse>>>>,\n ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>\n {\n events.flat_map(move |event| {\n futures::stream::iter(match event {\n Ok(event) => self.map_event(event),\n Err(error) => vec![Err(LanguageModelCompletionError::from(error))],\n })\n })\n }\n\n pub fn map_event(\n &mut self,\n event: deepseek::StreamResponse,\n ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {\n let Some(choice) = event.choices.first() else {\n return vec![Err(LanguageModelCompletionError::from(anyhow!(\n \"Response contained no choices\"\n )))];\n };\n\n let mut events = Vec::new();\n if let Some(content) = choice.delta.content.clone() {\n events.push(Ok(LanguageModelCompletionEvent::Text(content)));\n }\n\n if let Some(reasoning_content) = choice.delta.reasoning_content.clone() {\n events.push(Ok(LanguageModelCompletionEvent::Thinking {\n text: reasoning_content,\n signature: None,\n }));\n }\n\n if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {\n for tool_call in tool_calls {\n let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();\n\n if let Some(tool_id) = tool_call.id.clone() {\n entry.id = tool_id;\n }\n\n if let Some(function) = tool_call.function.as_ref() {\n if let Some(name) = function.name.clone() {\n entry.name = name;\n }\n\n if let Some(arguments) = function.arguments.clone() {\n entry.arguments.push_str(&arguments);\n }\n }\n }\n }\n\n if let Some(usage) = event.usage {\n events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {\n input_tokens: usage.prompt_tokens,\n output_tokens: usage.completion_tokens,\n cache_creation_input_tokens: 0,\n cache_read_input_tokens: 0,\n })));\n }\n\n match choice.finish_reason.as_deref() {\n Some(\"stop\") => {\n events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));\n }\n Some(\"tool_calls\") => {\n events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {\n match serde_json::Value::from_str(&tool_call.arguments) {\n Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(\n LanguageModelToolUse {\n id: tool_call.id.clone().into(),\n name: tool_call.name.as_str().into(),\n is_input_complete: true,\n input,\n raw_input: tool_call.arguments.clone(),\n thought_signature: None,\n },\n )),\n Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {\n id: tool_call.id.clone().into(),\n tool_name: tool_call.name.as_str().into(),\n raw_input: tool_call.arguments.into(),\n json_parse_error: error.to_string(),\n }),\n }\n }));\n\n events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));\n }\n Some(stop_reason) => {\n log::error!(\"Unexpected DeepSeek stop_reason: {stop_reason:?}\",);\n events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));\n }\n None => {}\n }\n\n events\n }\n}\n\nstruct ConfigurationView {\n api_key_editor: Entity<InputField>,\n state: Entity<State>,\n load_credentials_task: Option<Task<()>>,\n}\n\nimpl ConfigurationView {\n fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {\n let api_key_editor =\n cx.new(|cx| InputField::new(window, cx, \"sk-00000000000000000000000000000000\"));\n\n cx.observe(&state, |_, _, cx| {\n cx.notify();\n })\n .detach();\n\n let load_credentials_task = Some(cx.spawn({\n let state = state.clone();\n async move |this, cx| {\n if let Some(task) = state\n .update(cx, |state, cx| state.authenticate(cx))\n .log_err()\n {\n let _ = task.await;\n }\n\n this.update(cx, |this, cx| {\n this.load_credentials_task = None;\n cx.notify();\n })\n .log_err();\n }\n }));\n\n Self {\n api_key_editor,\n state,\n load_credentials_task,\n }\n }\n\n fn save_api_key(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {\n let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();\n if api_key.is_empty() {\n return;\n }\n\n let state = self.state.clone();\n cx.spawn(async move |_, cx| {\n state\n .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))?\n .await\n })\n .detach_and_log_err(cx);\n }\n\n fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {\n self.api_key_editor\n .update(cx, |editor, cx| editor.set_text(\"\", window, cx));\n\n let state = self.state.clone();\n cx.spawn(async move |_, cx| {\n state\n .update(cx, |state, cx| state.set_api_key(None, cx))?\n .await\n })\n .detach_and_log_err(cx);\n }\n\n fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {\n !self.state.read(cx).is_authenticated()\n }\n}\n\nimpl Render for ConfigurationView {\n fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();\n let configured_card_label = if env_var_set {\n format!(\"API key set in {API_KEY_ENV_VAR_NAME} environment variable\")\n } else {\n let api_url = DeepSeekLanguageModelProvider::api_url(cx);\n if api_url == DEEPSEEK_API_URL {\n \"API key configured\".to_string()\n } else {\n format!(\"API key configured for {}\", api_url)\n }\n };\n\n if self.load_credentials_task.is_some() {\n div()\n .child(Label::new(\"Loading credentials...\"))\n .into_any_element()\n } else if self.should_render_editor(cx) {\n v_flex()\n .size_full()\n .on_action(cx.listener(Self::save_api_key))\n .child(Label::new(\"To use DeepSeek in Zed, you need an API key:\"))\n .child(\n List::new()\n .child(InstructionListItem::new(\n \"Get your API key from the\",\n Some(\"DeepSeek console\"),\n Some(\"https://platform.deepseek.com/api_keys\"),\n ))\n .child(InstructionListItem::text_only(\n \"Paste your API key below and hit enter to start using the assistant\",\n )),\n )\n .child(self.api_key_editor.clone())\n .child(\n Label::new(format!(\n \"Or set the {API_KEY_ENV_VAR_NAME} environment variable.\"\n ))\n .size(LabelSize::Small)\n .color(Color::Muted),\n )\n .into_any_element()\n } else {\n ConfiguredApiCard::new(configured_card_label)\n .disabled(env_var_set)\n .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))\n .into_any_element()\n }\n }\n}\n"}}}}