use 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 { 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) -> Result>, ApiError> { provider::ensure_provider_defaults(&state).await?; let rows: Vec = 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, #[serde(default)] clear_key: bool, models: Vec, #[serde(default)] options: Option, enabled: Option, } async fn save(state: &AppState, id: &str, input: SaveProvider) -> Result { 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, Json(input): Json, ) -> Result<(StatusCode, Json), ApiError> { Ok(( StatusCode::CREATED, Json(save(&state, &Uuid::new_v4().to_string(), input).await?), )) } async fn update( State(state): State, Path(id): Path, Json(input): Json, ) -> Result, ApiError> { provider::provider_setting(&state, &id).await?; Ok(Json(save(&state, &id, input).await?)) } async fn delete( State(state): State, Path(id): Path, ) -> Result { 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, Path(id): Path, ) -> Result, 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}))) }