feat(saas): Phase 3 — 模型请求中转服务

- OpenAI 兼容 API 代理 (/api/v1/relay/chat/completions)
- 中转任务管理 (创建/查询/状态跟踪)
- 可用模型列表端点 (仅 enabled providers+models)
- 任务生命周期 (queued → processing → completed/failed)
- 用量自动记录 (token 统计 + 错误追踪)
- 3 个新集成测试覆盖中转端点
This commit is contained in:
iven
2026-03-27 12:50:05 +08:00
parent fec64af565
commit a99a3df9dd
6 changed files with 500 additions and 1 deletions

View File

@@ -22,6 +22,7 @@ async fn build_test_app() -> axum::Router {
let protected_routes = zclaw_saas::auth::protected_routes()
.merge(zclaw_saas::account::routes())
.merge(zclaw_saas::model_config::routes())
.merge(zclaw_saas::relay::routes())
.layer(axum::middleware::from_fn_with_state(
state.clone(),
zclaw_saas::auth::auth_middleware,
@@ -288,3 +289,63 @@ async fn test_api_keys_lifecycle() {
// provider 不存在 → 404
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
// ============ Phase 3: 中转服务测试 ============
#[tokio::test]
async fn test_relay_models_list() {
let app = build_test_app().await;
let token = register_and_login(&app, "relayuser", "relayuser@example.com").await;
// 列出可用中转模型 (空列表,因为没有 provider/model 种子数据)
let req = Request::builder()
.method("GET")
.uri("/api/v1/relay/models")
.header("Authorization", auth_header(&token))
.body(Body::empty())
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body_bytes = axum::body::to_bytes(resp.into_body(), MAX_BODY_SIZE).await.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(body.is_array());
}
#[tokio::test]
async fn test_relay_chat_no_model() {
let app = build_test_app().await;
let token = register_and_login(&app, "relayfail", "relayfail@example.com").await;
// 尝试中转到不存在的模型
let req = Request::builder()
.method("POST")
.uri("/api/v1/relay/chat/completions")
.header("Content-Type", "application/json")
.header("Authorization", auth_header(&token))
.body(Body::from(serde_json::to_string(&json!({
"model": "nonexistent-model",
"messages": [{"role": "user", "content": "hello"}]
})).unwrap()))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
// 模型不存在 → 404
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_relay_tasks_list() {
let app = build_test_app().await;
let token = register_and_login(&app, "relaytasks", "relaytasks@example.com").await;
let req = Request::builder()
.method("GET")
.uri("/api/v1/relay/tasks")
.header("Authorization", auth_header(&token))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}