Files
windmill/backend/windmill-api/src/openai.rs

173 lines
4.7 KiB
Rust

use crate::{db::DB, users::Authed, variables::build_crypt, HTTP_CLIENT};
use axum::{
body::{Bytes, StreamBody},
extract::{Extension, Path},
http::HeaderMap,
response::IntoResponse,
routing::post,
Router,
};
use magic_crypt::MagicCryptTrait;
use serde_json::json;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::error::{to_anyhow, Error};
use serde::Deserialize;
pub fn workspaced_service() -> Router {
let router = Router::new().route("/proxy/*openai_path", post(proxy));
router
}
#[derive(Deserialize)]
struct OpenaiResource {
api_key: String,
organization_id: Option<String>,
}
fn create_openai_json_error(msg: String) -> Error {
Error::OpenAIError(
serde_json::to_string(&json!({
"error": {
"message": msg
}
}))
.unwrap(),
)
}
struct Variable {
value: String,
is_secret: bool,
}
async fn get_variable(path: String, db: &DB, w_id: &String) -> Result<String, Error> {
let mut tx = db.begin().await?;
let mut variable = sqlx::query_as!(
Variable,
"SELECT value, is_secret
FROM variable
WHERE path = $1 AND workspace_id = $2",
&path,
&w_id
)
.fetch_one(&mut *tx)
.await?;
if variable.is_secret {
let mc = build_crypt(&mut tx, &w_id).await?;
variable.value = mc
.decrypt_base64_to_string(variable.value)
.map_err(|e| Error::InternalErr(e.to_string()))?;
}
tx.commit().await?;
Ok(variable.value)
}
lazy_static::lazy_static! {
pub static ref OPENAI_AZURE_BASE_PATH: Option<String> = std::env::var("OPENAI_AZURE_BASE_PATH").ok();
}
async fn proxy(
authed: Authed,
Extension(db): Extension<DB>,
Path((w_id, openai_path)): Path<(String, String)>,
body: Bytes,
) -> impl IntoResponse {
let mut tx = db.begin().await?;
let openai_resource_path = sqlx::query_scalar!(
"SELECT openai_resource_path FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
if openai_resource_path.is_none() {
return Err(create_openai_json_error(
"OpenAI resource not configured".to_string(),
));
}
let openai_resource_path = openai_resource_path.unwrap();
tx = db.begin().await?;
let resource = sqlx::query_scalar!(
"SELECT value
FROM resource
WHERE path = $1 AND workspace_id = $2",
&openai_resource_path,
&w_id
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
if resource.is_none() {
return Err(create_openai_json_error(
"OpenAI resource missing value".to_string(),
));
}
let mut resource: OpenaiResource = serde_json::from_value(resource.unwrap())
.map_err(|e| Error::InternalErr(format!("validating openai resource {e}")))?;
if resource.api_key.starts_with("$var:") {
let openai_api_key_path = resource.api_key.strip_prefix("$var:").unwrap().to_string();
resource.api_key = get_variable(openai_api_key_path, &db, &w_id).await?;
}
let base_url = if let Some(base_url) = &*OPENAI_AZURE_BASE_PATH {
base_url
} else {
"https://api.openai.com/v1"
};
let mut request = HTTP_CLIENT
.post(base_url.to_string() + "/" + &openai_path)
.header("content-type", "application/json")
.body(body);
if base_url != "https://api.openai.com/v1" {
request = request
.header("api-key", resource.api_key)
.query(&[("api-version", "2023-05-15")])
} else {
request = request.header("authorization", format!("Bearer {}", resource.api_key))
}
if let Some(mut org_id) = resource.organization_id {
tracing::info!("org_id: {:?}", org_id);
if org_id.starts_with("$var:") {
let openai_organisation_path = org_id.strip_prefix("$var:").unwrap().to_string();
org_id = get_variable(openai_organisation_path, &db, &w_id).await?;
}
request = request.header("OpenAI-Organization", org_id);
}
let resp = request.send().await.map_err(to_anyhow)?;
tx = db.begin().await?;
audit_log(
&mut *tx,
&authed.username,
"openai.request",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("openai_path", &format!("{:?}", openai_path)[..])].into()),
)
.await?;
tx.commit().await?;
let mut headers = HeaderMap::new();
for (k, v) in resp.headers().iter() {
headers.insert(k, v.clone());
}
let status_code = resp.status();
let stream = resp.bytes_stream();
Ok((status_code, headers, StreamBody::new(stream)))
}