mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat: provider api_formats 可空继承、OpenAI 图片 edit/variation 与用量配额多项补强
- 鉴权: provider_api_keys.api_formats 改为可空,OAuth 托管 key 自动继承 provider endpoints 激活格式,相关 handler/测试同步更新 - 图片 planner: OpenAI 图片路由新增 edit/variation 操作并完善参数校验、响应合并与流式处理 - 用量: user me usage 返回区分 client_requested_stream/upstream_is_stream,前端 usage 列表筛选与展示增强 - 统计: stats_daily_model 新增 cache_creation_ephemeral_5m/1h tokens 字段与回填链路 - 配额/observability: quota repository 新增内存与 SQL 扩展,admin observability usage 字段扩充 - 其它: OAuth 导入/轮询收敛、provider 汇总与 pool admin 读写链路小修、新增 system_config 缓存与 provider template handler Closes #318 Co-authored-by: Entropy.Xu <53283266+Entropy-Xu@users.noreply.github.com>
This commit is contained in:
@@ -444,6 +444,94 @@ pub fn admin_usage_provider_key_name(
|
||||
.or_else(|| item.routing_key_name().map(ToOwned::to_owned))
|
||||
}
|
||||
|
||||
fn admin_usage_request_body_stream_flag(item: &StoredRequestUsageAudit) -> Option<bool> {
|
||||
item.request_body
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|body| body.get("stream"))
|
||||
.and_then(Value::as_bool)
|
||||
}
|
||||
|
||||
fn admin_usage_api_format_defaults_to_non_stream(item: &StoredRequestUsageAudit) -> bool {
|
||||
let api_format = item
|
||||
.api_format
|
||||
.as_deref()
|
||||
.or(item.endpoint_api_format.as_deref())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
matches!(
|
||||
api_format,
|
||||
Some(value)
|
||||
if value.eq_ignore_ascii_case("openai:chat")
|
||||
|| value.eq_ignore_ascii_case("openai:cli")
|
||||
|| value.eq_ignore_ascii_case("openai:compact")
|
||||
|| value.eq_ignore_ascii_case("openai:image")
|
||||
|| value.eq_ignore_ascii_case("claude:chat")
|
||||
|| value.eq_ignore_ascii_case("claude:cli")
|
||||
)
|
||||
}
|
||||
|
||||
fn admin_usage_request_body_implies_default_non_stream(item: &StoredRequestUsageAudit) -> bool {
|
||||
let Some(body) = item.request_body.as_ref().and_then(Value::as_object) else {
|
||||
return false;
|
||||
};
|
||||
!body.contains_key("stream") && admin_usage_api_format_defaults_to_non_stream(item)
|
||||
}
|
||||
|
||||
pub fn admin_usage_client_is_stream(item: &StoredRequestUsageAudit) -> bool {
|
||||
item.request_metadata
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|metadata| metadata.get("client_requested_stream"))
|
||||
.and_then(Value::as_bool)
|
||||
.or_else(|| admin_usage_request_body_stream_flag(item))
|
||||
.or_else(|| admin_usage_request_body_implies_default_non_stream(item).then_some(false))
|
||||
.unwrap_or(item.is_stream)
|
||||
}
|
||||
|
||||
fn admin_usage_active_request_json(
|
||||
item: &StoredRequestUsageAudit,
|
||||
api_key_name: Option<String>,
|
||||
provider_key_name: Option<String>,
|
||||
) -> Value {
|
||||
let cache_creation_input_tokens = admin_usage_cache_creation_tokens(item);
|
||||
let client_is_stream = admin_usage_client_is_stream(item);
|
||||
let mut value = json!({
|
||||
"id": item.id,
|
||||
"status": item.status,
|
||||
"input_tokens": item.input_tokens,
|
||||
"effective_input_tokens": admin_usage_effective_input_tokens(item),
|
||||
"output_tokens": item.output_tokens,
|
||||
"cache_creation_input_tokens": cache_creation_input_tokens,
|
||||
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_input_tokens,
|
||||
"cache_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
|
||||
"cache_read_input_tokens": item.cache_read_input_tokens,
|
||||
"cost": round_to(item.total_cost_usd, 6),
|
||||
"actual_cost": round_to(item.actual_total_cost_usd, 6),
|
||||
"response_time_ms": item.response_time_ms,
|
||||
"first_byte_time_ms": item.first_byte_time_ms,
|
||||
"provider": item.provider_name,
|
||||
"api_key_name": api_key_name,
|
||||
"provider_key_name": provider_key_name,
|
||||
"is_stream": item.is_stream,
|
||||
"upstream_is_stream": item.is_stream,
|
||||
"client_requested_stream": client_is_stream,
|
||||
"client_is_stream": client_is_stream,
|
||||
"has_fallback": admin_usage_has_fallback(item),
|
||||
});
|
||||
if let Some(api_format) = item.api_format.as_ref() {
|
||||
value["api_format"] = json!(api_format);
|
||||
}
|
||||
if let Some(endpoint_api_format) = item.endpoint_api_format.as_ref() {
|
||||
value["endpoint_api_format"] = json!(endpoint_api_format);
|
||||
}
|
||||
value["has_format_conversion"] = json!(item.has_format_conversion);
|
||||
if let Some(target_model) = item.target_model.as_ref() {
|
||||
value["target_model"] = json!(target_model);
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
pub fn admin_usage_record_json(
|
||||
item: &StoredRequestUsageAudit,
|
||||
users_by_id: &BTreeMap<String, StoredUserSummary>,
|
||||
@@ -469,8 +557,9 @@ pub fn admin_usage_record_json(
|
||||
let user_email = user
|
||||
.and_then(|value| value.email.clone())
|
||||
.unwrap_or_else(|| "已删除用户".to_string());
|
||||
let client_is_stream = admin_usage_client_is_stream(item);
|
||||
|
||||
json!({
|
||||
let mut payload = json!({
|
||||
"id": item.id,
|
||||
"user_id": item.user_id,
|
||||
"user_email": user_email,
|
||||
@@ -497,7 +586,6 @@ pub fn admin_usage_record_json(
|
||||
"response_time_ms": item.response_time_ms,
|
||||
"first_byte_time_ms": item.first_byte_time_ms,
|
||||
"created_at": unix_secs_to_rfc3339(item.created_at_unix_ms),
|
||||
"is_stream": item.is_stream,
|
||||
"input_price_per_1m": input_price_per_1m,
|
||||
"output_price_per_1m": output_price_per_1m,
|
||||
"cache_creation_price_per_1m": cache_creation_price_per_1m,
|
||||
@@ -515,7 +603,18 @@ pub fn admin_usage_record_json(
|
||||
"api_key_name": api_key_name,
|
||||
"provider_key_name": provider_key_name,
|
||||
"model_version": Value::Null,
|
||||
})
|
||||
});
|
||||
let object = payload
|
||||
.as_object_mut()
|
||||
.expect("admin usage record payload should be an object");
|
||||
object.insert("is_stream".to_string(), json!(item.is_stream));
|
||||
object.insert("upstream_is_stream".to_string(), json!(item.is_stream));
|
||||
object.insert(
|
||||
"client_requested_stream".to_string(),
|
||||
json!(client_is_stream),
|
||||
);
|
||||
object.insert("client_is_stream".to_string(), json!(client_is_stream));
|
||||
payload
|
||||
}
|
||||
|
||||
pub fn admin_usage_total_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||
@@ -1250,12 +1349,13 @@ pub fn admin_usage_resolve_request_capture_body(
|
||||
body_override: Option<Value>,
|
||||
) -> Option<Value> {
|
||||
let resolved_model = item.model.clone();
|
||||
let client_is_stream = admin_usage_client_is_stream(item);
|
||||
let mut request_body = body_override.or_else(|| item.request_body.clone())?;
|
||||
if let Some(body) = request_body.as_object_mut() {
|
||||
body.entry("model".to_string())
|
||||
.or_insert_with(|| json!(resolved_model));
|
||||
if !body.contains_key("stream") {
|
||||
body.insert("stream".to_string(), json!(item.is_stream));
|
||||
body.insert("stream".to_string(), json!(client_is_stream));
|
||||
}
|
||||
if let Some(target_model) = item
|
||||
.target_model
|
||||
@@ -1419,37 +1519,7 @@ pub fn build_admin_usage_active_requests_response(
|
||||
let provider_key_name = admin_usage_provider_key_name(item, provider_key_names);
|
||||
let api_key_name =
|
||||
admin_usage_api_key_name(item, api_key_names, auth_api_key_reader_available);
|
||||
let cache_creation_input_tokens = admin_usage_cache_creation_tokens(item);
|
||||
let mut value = json!({
|
||||
"id": item.id,
|
||||
"status": item.status,
|
||||
"input_tokens": item.input_tokens,
|
||||
"effective_input_tokens": admin_usage_effective_input_tokens(item),
|
||||
"output_tokens": item.output_tokens,
|
||||
"cache_creation_input_tokens": cache_creation_input_tokens,
|
||||
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_input_tokens,
|
||||
"cache_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
|
||||
"cache_read_input_tokens": item.cache_read_input_tokens,
|
||||
"cost": round_to(item.total_cost_usd, 6),
|
||||
"actual_cost": round_to(item.actual_total_cost_usd, 6),
|
||||
"response_time_ms": item.response_time_ms,
|
||||
"first_byte_time_ms": item.first_byte_time_ms,
|
||||
"provider": item.provider_name,
|
||||
"api_key_name": api_key_name,
|
||||
"provider_key_name": provider_key_name,
|
||||
"has_fallback": admin_usage_has_fallback(item),
|
||||
});
|
||||
if let Some(api_format) = item.api_format.as_ref() {
|
||||
value["api_format"] = json!(api_format);
|
||||
}
|
||||
if let Some(endpoint_api_format) = item.endpoint_api_format.as_ref() {
|
||||
value["endpoint_api_format"] = json!(endpoint_api_format);
|
||||
}
|
||||
value["has_format_conversion"] = json!(item.has_format_conversion);
|
||||
if let Some(target_model) = item.target_model.as_ref() {
|
||||
value["target_model"] = json!(target_model);
|
||||
}
|
||||
value
|
||||
admin_usage_active_request_json(item, api_key_name, provider_key_name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1674,9 +1744,10 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
admin_usage_has_body_value, admin_usage_has_fallback, admin_usage_is_failed,
|
||||
admin_usage_matches_search, admin_usage_matches_status, admin_usage_matches_username,
|
||||
admin_usage_record_json, build_admin_usage_detail_payload,
|
||||
admin_usage_active_request_json, admin_usage_client_is_stream, admin_usage_has_body_value,
|
||||
admin_usage_has_fallback, admin_usage_is_failed, admin_usage_matches_search,
|
||||
admin_usage_matches_status, admin_usage_matches_username, admin_usage_record_json,
|
||||
admin_usage_resolve_request_capture_body, build_admin_usage_detail_payload,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageBodyField};
|
||||
|
||||
@@ -1738,6 +1809,100 @@ mod tests {
|
||||
assert!(admin_usage_matches_status(&item, Some("completed")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_requested_stream_prefers_request_metadata_flag() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
is_stream: true,
|
||||
request_metadata: Some(json!({
|
||||
"client_requested_stream": false
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
assert!(!admin_usage_client_is_stream(&item));
|
||||
|
||||
let record = admin_usage_record_json(
|
||||
&item,
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
assert_eq!(record["is_stream"], true);
|
||||
assert_eq!(record["upstream_is_stream"], true);
|
||||
assert_eq!(record["client_requested_stream"], false);
|
||||
assert_eq!(record["client_is_stream"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_requested_stream_falls_back_to_request_body_stream_flag() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
is_stream: true,
|
||||
request_body: Some(json!({
|
||||
"model": "gpt-5.4",
|
||||
"stream": false
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
assert!(!admin_usage_client_is_stream(&item));
|
||||
|
||||
let active = admin_usage_active_request_json(&item, None, None);
|
||||
assert_eq!(active["is_stream"], true);
|
||||
assert_eq!(active["upstream_is_stream"], true);
|
||||
assert_eq!(active["client_requested_stream"], false);
|
||||
assert_eq!(active["client_is_stream"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_requested_stream_defaults_to_non_stream_for_openai_cli_request_body_without_flag() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
is_stream: true,
|
||||
api_format: Some("openai:cli".to_string()),
|
||||
request_body: Some(json!({
|
||||
"model": "gpt-5.4",
|
||||
"input": [{"role": "user", "content": "hi"}],
|
||||
"store": false
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
assert!(!admin_usage_client_is_stream(&item));
|
||||
|
||||
let record = admin_usage_record_json(
|
||||
&item,
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
assert_eq!(record["is_stream"], true);
|
||||
assert_eq!(record["upstream_is_stream"], true);
|
||||
assert_eq!(record["client_requested_stream"], false);
|
||||
assert_eq!(record["client_is_stream"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_body_defaults_stream_to_client_requested_mode() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
is_stream: true,
|
||||
request_body: Some(json!({
|
||||
"model": "gpt-5.4",
|
||||
"input": "hello"
|
||||
})),
|
||||
request_metadata: Some(json!({
|
||||
"client_requested_stream": false
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
let body = admin_usage_resolve_request_capture_body(&item, None)
|
||||
.expect("replay body should resolve");
|
||||
assert_eq!(body["stream"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_failure_signals_still_work_when_status_is_missing() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
|
||||
Reference in New Issue
Block a user