Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
5.5 kB · 165 lines
Rust
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166use crate::{ completion::{endpoint_url, protocol}, domains::provider::{self, ProviderSettingRow, UpdateProviderRequest}, error::ApiError, state::AppState,};use axum::{ Json, Router, extract::{Path, State}, http::StatusCode, routing::{get, post, put},};use serde::Deserialize;use serde_json::{Value, json};use uuid::Uuid;
pub(crate) fn routes() -> Router<AppState> { Router::new() .route("/api/reader/providers", get(list).post(create)) .route("/api/reader/providers/{id}", put(update).delete(delete)) .route("/api/reader/providers/{id}/test", post(test))}
pub(crate) fn provider_json(row: &ProviderSettingRow) -> Value { let base = row .base_url .as_deref() .unwrap_or(if row.provider == "anthropic" { "https://api.anthropic.com/v1" } else { "https://api.openai.com/v1" }); json!({"id":row.provider,"name":row.name,"baseUrl":base,"hasKey":row.api_key_ciphertext.is_some(),"models":row.models,"options":row.options,"enabled":row.enabled})}
async fn list(State(state): State<AppState>) -> Result<Json<Vec<Value>>, ApiError> { provider::ensure_provider_defaults(&state).await?; let rows: Vec<ProviderSettingRow> = sqlx::query_as::<_, ProviderSettingRow>( "select * from provider_settings order by provider", ) .fetch_all(&state.platform_db) .await?; Ok(Json(rows.iter().map(provider_json).collect()))}
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]struct SaveProvider { name: String, base_url: String, api_key: Option<String>, #[serde(default)] clear_key: bool, models: Vec<String>, #[serde(default)] options: Option<Value>, enabled: Option<bool>,}
async fn save(state: &AppState, id: &str, input: SaveProvider) -> Result<Value, ApiError> { let options = input.options.unwrap_or_else( || json!({"protocol":"chat","contextTokens":6000,"maxTokens":4096,"vision":false}), ); provider::validate_provider_options(&options)?; let row = provider::save_provider( state, id, UpdateProviderRequest { model: input.models.first().cloned().unwrap_or_default(), name: Some(input.name), models: Some(json!(input.models)), options: Some(options), base_url: Some(input.base_url), api_key: input.api_key, clear_key: input.clear_key, enabled: input.enabled.unwrap_or(!input.clear_key), }, ) .await?; Ok(provider_json(&row))}
async fn create( State(state): State<AppState>, Json(input): Json<SaveProvider>,) -> Result<(StatusCode, Json<Value>), ApiError> { Ok(( StatusCode::CREATED, Json(save(&state, &Uuid::new_v4().to_string(), input).await?), ))}async fn update( State(state): State<AppState>, Path(id): Path<String>, Json(input): Json<SaveProvider>,) -> Result<Json<Value>, ApiError> { provider::provider_setting(&state, &id).await?; Ok(Json(save(&state, &id, input).await?))}async fn delete( State(state): State<AppState>, Path(id): Path<String>,) -> Result<StatusCode, ApiError> { let busy:bool=sqlx::query_scalar("select exists(select 1 from agent_conversations c join agent_messages m on m.conversation_id=c.id where c.provider=$1 and m.metadata->>'status'='streaming')").bind(&id).fetch_one(&state.platform_db).await?; if busy { return Err(ApiError::Conflict( "Stop provider responses before deleting its settings.".into(), )); } sqlx::query("delete from provider_settings where provider=$1") .bind(id) .execute(&state.platform_db) .await?; Ok(StatusCode::NO_CONTENT)}async fn test( State(state): State<AppState>, Path(id): Path<String>,) -> Result<Json<Value>, ApiError> { let row = provider::provider_setting(&state, &id).await?; let ciphertext = row .api_key_ciphertext .as_deref() .ok_or_else(|| ApiError::BadRequest("Add and save an API key first.".into()))?; let key = String::from_utf8(provider::decrypt_secret(&state, ciphertext)?) .map_err(anyhow::Error::from)?; let base = row .base_url .as_deref() .unwrap_or(if protocol(&row) == "anthropic" { "https://api.anthropic.com" } else { "https://api.openai.com" }); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(15)) .build() .map_err(anyhow::Error::from)?; let req = client.get(endpoint_url(base, "models")); let req = if protocol(&row) == "anthropic" { req.header("x-api-key", key) .header("anthropic-version", "2023-06-01") } else { req.bearer_auth(key) }; let response = req .send() .await .map_err(|_| ApiError::BadRequest("Provider connection failed or timed out.".into()))?; if !response.status().is_success() { return Err(ApiError::BadRequest(format!( "The provider returned HTTP {}. Check the key and base URL.", response.status() ))); } let payload: Value = response.json().await.map_err(anyhow::Error::from)?; let models: Vec<&str> = payload["data"] .as_array() .into_iter() .flatten() .filter_map(|v| v["id"].as_str()) .take(5000) .collect(); Ok(Json(json!({"ok":true,"models":models})))}