Something went wrong. Try again.
fork of gpui
Something went wrong. Try again.
12 kB · 374 lines
Rust
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375mod llm_token;mod websocket;
use std::sync::Arc;
use anyhow::{Result, anyhow};pub use cloud_api_types::*;use futures::AsyncReadExt as _;use http_client::http::request;use http_client::{ AsyncBody, HttpClientWithUrl, HttpRequestExt, Json, Method, Request, Response, StatusCode,};use parking_lot::RwLock;use serde::de::DeserializeOwned;use thiserror::Error;
pub use llm_token::LlmApiToken;
struct Credentials { user_id: u32, access_token: String,}
#[derive(Clone, Copy)]enum Authentication { Credentials, Session,}
#[derive(Debug, Error)]pub enum ClientApiError { /// 401 — credentials are invalid or expired. #[error("Unauthorized")] Unauthorized, /// No credentials have been set on the client. #[error("not signed in")] NotSignedIn, /// Connection-level failure: DNS, TCP, TLS, timeout, etc. /// The HTTP request never received a response. #[error("connection to {host} failed")] ConnectionFailed { host: String, #[source] source: anyhow::Error, }, /// Server returned a non-success HTTP status (other than 401). #[error("{host} returned {status}")] ServerError { host: String, status: StatusCode, body: String, }, /// Failed to read or parse the response body after a successful HTTP status. #[error("invalid response")] InvalidResponse(#[source] anyhow::Error), /// Failed to build the HTTP request (URL construction, serialization, etc.). /// This typically indicates a programming error. #[error("failed to build request")] RequestBuildFailed(#[source] anyhow::Error),}
pub struct CloudApiClient { credentials: RwLock<Option<Credentials>>, http_client: Arc<HttpClientWithUrl>, authentication: Authentication,}
impl CloudApiClient { pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self { Self { credentials: RwLock::new(None), http_client, authentication: Authentication::Credentials, } }
pub fn new_with_session_authentication(http_client: Arc<HttpClientWithUrl>) -> Self { Self { credentials: RwLock::new(None), http_client, authentication: Authentication::Session, } }
pub fn has_credentials(&self) -> bool { self.credentials.read().is_some() }
pub fn set_credentials(&self, user_id: u32, access_token: String) { *self.credentials.write() = Some(Credentials { user_id, access_token, }); }
pub fn clear_credentials(&self) { *self.credentials.write() = None; }
pub fn cloud_host(&self) -> String { self.http_client .build_zed_cloud_url("/") .ok() .and_then(|url| url.host_str().map(String::from)) .unwrap_or_else(|| "cloud.zed.dev".into()) }
pub async fn get_authenticated_user( &self, system_id: Option<String>, ) -> Result<GetAuthenticatedUserResponse, ClientApiError> { let request_builder = Request::builder() .method(Method::GET) .uri( self.http_client .build_zed_cloud_url("/client/users/me") .map_err(ClientApiError::RequestBuildFailed)? .as_ref(), ) .when_some(system_id, |builder, system_id| { builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id) });
self.send_authenticated_json_request(request_builder, AsyncBody::default()) .await }
async fn create_llm_token( &self, system_id: Option<String>, organization_id: OrganizationId, ) -> Result<CreateLlmTokenResponse, ClientApiError> { let request_builder = Request::builder() .method(Method::POST) .uri( self.http_client .build_zed_cloud_url("/client/llm_tokens") .map_err(ClientApiError::RequestBuildFailed)? .as_ref(), ) .when_some(system_id, |builder, system_id| { builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id) });
self.send_authenticated_json_request( request_builder, Json(CreateLlmTokenBody { organization_id }), ) .await }
pub async fn update_system_settings( &self, system_id: String, body: UpdateSystemSettingsBody, ) -> Result<SystemSettings, ClientApiError> { let request_builder = Request::builder() .method(Method::PATCH) .uri( self.http_client .build_zed_cloud_url("/client/system_settings") .map_err(ClientApiError::RequestBuildFailed)? .as_ref(), ) .header(ZED_SYSTEM_ID_HEADER_NAME, system_id);
self.send_authenticated_json_request(request_builder, Json(body)) .await }
pub async fn send_authenticated_json_request<T: DeserializeOwned>( &self, request_builder: request::Builder, body: impl Into<AsyncBody>, ) -> Result<T, ClientApiError> { let mut response = self .send_authenticated_request(request_builder, body) .await?; Self::read_response_json(&mut response).await }
async fn send_authenticated_request( &self, request_builder: request::Builder, body: impl Into<AsyncBody>, ) -> Result<Response<AsyncBody>, ClientApiError> { let request = if matches!(self.authentication, Authentication::Session) { build_request(request_builder, body, None) .map_err(ClientApiError::RequestBuildFailed)? } else { let credentials = self.credentials.read(); let credentials = credentials.as_ref().ok_or(ClientApiError::NotSignedIn)?; build_request(request_builder, body, Some(credentials)) .map_err(ClientApiError::RequestBuildFailed)? };
let host = self.cloud_host(); let mut response = self.http_client.send(request).await.map_err(|source| { ClientApiError::ConnectionFailed { host: host.clone(), source, } })?;
let status = response.status(); if status.is_success() { return Ok(response); }
if status == StatusCode::UNAUTHORIZED { return Err(ClientApiError::Unauthorized); }
let body = match Self::read_response_body(&mut response).await { Ok(body) => body, Err(error) => format!("failed to read response body: {error}"), }; Err(ClientApiError::ServerError { host, status, body }) }
async fn read_response_json<T: DeserializeOwned>( response: &mut Response<AsyncBody>, ) -> Result<T, ClientApiError> { let body = Self::read_response_body(response).await?; serde_json::from_str(&body).map_err(|error| ClientApiError::InvalidResponse(error.into())) }
async fn read_response_body( response: &mut Response<AsyncBody>, ) -> Result<String, ClientApiError> { let mut body = String::new(); response .body_mut() .read_to_string(&mut body) .await .map_err(|error| ClientApiError::InvalidResponse(error.into()))?; Ok(body) }
pub async fn validate_credentials(&self, user_id: u32, access_token: &str) -> Result<bool> { let request = build_request( Request::builder().method(Method::GET).uri( self.http_client .build_zed_cloud_url("/client/users/me")? .as_ref(), ), AsyncBody::default(), Some(&Credentials { user_id, access_token: access_token.into(), }), )?;
let mut response = self.http_client.send(request).await?;
if response.status().is_success() { Ok(true) } else { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; if response.status() == StatusCode::UNAUTHORIZED { Ok(false) } else { Err(anyhow!( "Failed to get authenticated user.\nStatus: {:?}\nBody: {body}", response.status() )) } } }
pub async fn submit_agent_feedback(&self, body: SubmitAgentThreadFeedbackBody) -> Result<()> { let request = Request::builder().method(Method::POST).uri( self.http_client .build_zed_cloud_url("/client/feedback/agent_thread")? .as_ref(), );
self.send_authenticated_request(request, AsyncBody::from(serde_json::to_string(&body)?)) .await?; Ok(()) }
pub async fn submit_agent_feedback_comments( &self, body: SubmitAgentThreadFeedbackCommentsBody, ) -> Result<()> { let request = Request::builder().method(Method::POST).uri( self.http_client .build_zed_cloud_url("/client/feedback/agent_thread_comments")? .as_ref(), );
self.send_authenticated_request(request, AsyncBody::from(serde_json::to_string(&body)?)) .await?; Ok(()) }
pub async fn submit_edit_prediction_feedback( &self, body: SubmitEditPredictionFeedbackBody, ) -> Result<()> { let request = Request::builder().method(Method::POST).uri( self.http_client .build_zed_cloud_url("/client/feedback/edit_prediction")? .as_ref(), );
self.send_authenticated_request(request, AsyncBody::from(serde_json::to_string(&body)?)) .await?; Ok(()) }}
fn build_request( req: request::Builder, body: impl Into<AsyncBody>, credentials: Option<&Credentials>,) -> Result<Request<AsyncBody>> { Ok(req .header("Content-Type", "application/json") .when_some(credentials, |request, credentials| { request.header( "Authorization", format!("{} {}", credentials.user_id, credentials.access_token), ) }) .body(body.into())?)}
#[cfg(test)]mod tests { use super::*;
#[test] fn build_session_authenticated_request_without_authorization_header() -> Result<()> { let request = build_request( Request::builder().uri("https://cloud.zed.dev/client/users/me"), AsyncBody::default(), None, )?;
assert_eq!( request .headers() .get("Content-Type") .and_then(|value| value.to_str().ok()), Some("application/json") ); assert!(!request.headers().contains_key("Authorization")); Ok(()) }
#[test] fn build_credentials_authenticated_request_with_authorization_header() -> Result<()> { let request = build_request( Request::builder().uri("https://cloud.zed.dev/client/users/me"), AsyncBody::default(), Some(&Credentials { user_id: 123, access_token: "token".into(), }), )?;
assert_eq!( request .headers() .get("Authorization") .and_then(|value| value.to_str().ok()), Some("123 token") ); Ok(()) }}