feat: Proxy extensions — audio, video, completions, embeddings

- Generic execute_proxy_request executor for arbitrary paths
- POST /v1/completions, /v1/embeddings
- POST /v1/audio/speech, /v1/audio/transcriptions, /v1/audio/translations
- POST /v1/videos
- All reuse existing auth + route resolution infra
- 18/18 tests pass
This commit is contained in:
boos
2026-05-17 17:01:15 +08:00
parent 84b4b16ebd
commit e94c6cf853
+87
View File
@@ -47,7 +47,11 @@ pub fn routes() -> Router<AppState> {
.route("/v1/chat/completions", post(chat_completions))
.route("/v1/completions", post(completions_handler))
.route("/v1/embeddings", post(embeddings_handler))
.route("/v1/audio/transcriptions", post(audio_transcriptions))
.route("/v1/audio/translations", post(audio_translations))
.route("/v1/audio/speech", post(audio_speech))
.route("/v1/images/generations", post(images_generations))
.route("/v1/videos", post(videos_handler))
.route("/v1/models", get(list_models))
}
@@ -126,6 +130,89 @@ async fn embeddings_handler(
Ok(Json(result))
}
/// POST /v1/audio/speech
async fn audio_speech(
State(state): State<AppState>,
headers: HeaderMap,
Json(body): Json<Value>,
) -> AppResult<Json<Value>> {
info!("POST /v1/audio/speech");
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, "/audio/speech", body).await?;
Ok(Json(result))
}
/// POST /v1/audio/transcriptions — proxy to upstream
async fn audio_transcriptions(
State(state): State<AppState>,
headers: HeaderMap,
Json(body): Json<Value>,
) -> AppResult<Json<Value>> {
info!("POST /v1/audio/transcriptions");
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, "/audio/transcriptions", body).await?;
Ok(Json(result))
}
/// POST /v1/audio/translations — proxy to upstream
async fn audio_translations(
State(state): State<AppState>,
headers: HeaderMap,
Json(body): Json<Value>,
) -> AppResult<Json<Value>> {
info!("POST /v1/audio/translations");
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, "/audio/translations", body).await?;
Ok(Json(result))
}
/// POST /v1/videos — list/create video generation tasks
async fn videos_handler(
State(state): State<AppState>,
headers: HeaderMap,
Json(body): Json<Value>,
) -> AppResult<Json<Value>> {
info!("POST /v1/videos");
check_auth(&state, &headers).await?;
let model = body
.get("model")
.and_then(|m| m.as_str())
.unwrap_or("default");
let route = resolve_route(&state, model).await?;
let result = executors::execute_proxy_request(state, route, "/videos", body).await?;
Ok(Json(result))
}
/// POST /v1/images/generations
async fn images_generations(
State(state): State<AppState>,