feat: Proxy extensions — /v1/completions + /v1/embeddings
- Added generic execute_proxy_request executor for any path suffix - POST /v1/completions — forwards to upstream /completions - POST /v1/embeddings — forwards to upstream /embeddings - Both use existing auth + route resolution infra - 18/18 tests pass
This commit is contained in:
@@ -120,6 +120,47 @@ pub async fn execute_chat_completions_stream(
|
|||||||
Ok(chunks)
|
Ok(chunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generic non-streaming proxy request to any upstream endpoint path.
|
||||||
|
pub async fn execute_proxy_request(
|
||||||
|
_state: AppState,
|
||||||
|
route: UpstreamRoute,
|
||||||
|
path_suffix: &str,
|
||||||
|
body: Value,
|
||||||
|
) -> AppResult<Value> {
|
||||||
|
let client = http_client();
|
||||||
|
let upstream_url = format!(
|
||||||
|
"{}/{}",
|
||||||
|
route.base_url.trim_end_matches('/'),
|
||||||
|
path_suffix.trim_start_matches('/')
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(&upstream_url)
|
||||||
|
.header("Authorization", format!("Bearer {}", route.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::UpstreamError(format!("Upstream request failed: {e}")))?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::UpstreamError(format!("Failed to read upstream response: {e}")))?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(AppError::UpstreamError(format!(
|
||||||
|
"Upstream returned {status}: {text}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json: Value = serde_json::from_str(&text)
|
||||||
|
.map_err(|e| AppError::UpstreamError(format!("Invalid JSON from upstream: {e}")))?;
|
||||||
|
|
||||||
|
Ok(json)
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute an image generation request.
|
/// Execute an image generation request.
|
||||||
pub async fn execute_image_generation(
|
pub async fn execute_image_generation(
|
||||||
_state: AppState,
|
_state: AppState,
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ pub enum DownstreamSource {
|
|||||||
pub fn routes() -> Router<AppState> {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/v1/chat/completions", post(chat_completions))
|
.route("/v1/chat/completions", post(chat_completions))
|
||||||
|
.route("/v1/completions", post(completions_handler))
|
||||||
|
.route("/v1/embeddings", post(embeddings_handler))
|
||||||
.route("/v1/images/generations", post(images_generations))
|
.route("/v1/images/generations", post(images_generations))
|
||||||
.route("/v1/models", get(list_models))
|
.route("/v1/models", get(list_models))
|
||||||
}
|
}
|
||||||
@@ -82,6 +84,48 @@ async fn chat_completions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// POST /v1/completions
|
||||||
|
async fn completions_handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(body): Json<Value>,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
info!("POST /v1/completions");
|
||||||
|
|
||||||
|
check_auth(&state, &headers).await?;
|
||||||
|
|
||||||
|
let model = body
|
||||||
|
.get("model")
|
||||||
|
.and_then(|m| m.as_str())
|
||||||
|
.ok_or_else(|| AppError::BadRequest("model is required".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let route = resolve_route(&state, &model).await?;
|
||||||
|
let result = executors::execute_proxy_request(state, route, "/completions", body).await?;
|
||||||
|
Ok(Json(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /v1/embeddings
|
||||||
|
async fn embeddings_handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(body): Json<Value>,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
info!("POST /v1/embeddings");
|
||||||
|
|
||||||
|
check_auth(&state, &headers).await?;
|
||||||
|
|
||||||
|
let model = body
|
||||||
|
.get("model")
|
||||||
|
.and_then(|m| m.as_str())
|
||||||
|
.ok_or_else(|| AppError::BadRequest("model is required".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let route = resolve_route(&state, &model).await?;
|
||||||
|
let result = executors::execute_proxy_request(state, route, "/embeddings", body).await?;
|
||||||
|
Ok(Json(result))
|
||||||
|
}
|
||||||
|
|
||||||
/// POST /v1/images/generations
|
/// POST /v1/images/generations
|
||||||
async fn images_generations(
|
async fn images_generations(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|||||||
Reference in New Issue
Block a user