mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 11:19:50 +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 <[email protected]>
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use aether_provider_transport::provider_types::provider_type_is_fixed;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ProviderKeyCredentialKind {
|
||||
@@ -146,12 +150,68 @@ pub(crate) fn provider_key_is_oauth_managed(
|
||||
provider_key_auth_semantics(key, provider_type).oauth_managed()
|
||||
}
|
||||
|
||||
pub(crate) fn provider_key_configured_api_formats(key: &StoredProviderCatalogKey) -> Vec<String> {
|
||||
key.api_formats
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn provider_active_api_formats(
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
) -> Vec<String> {
|
||||
let mut formats = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
for endpoint in endpoints.iter().filter(|endpoint| endpoint.is_active) {
|
||||
let api_format = endpoint.api_format.trim();
|
||||
if api_format.is_empty() || !seen.insert(api_format.to_string()) {
|
||||
continue;
|
||||
}
|
||||
formats.push(api_format.to_string());
|
||||
}
|
||||
formats
|
||||
}
|
||||
|
||||
pub(crate) fn provider_key_inherits_provider_api_formats(
|
||||
key: &StoredProviderCatalogKey,
|
||||
provider_type: &str,
|
||||
) -> bool {
|
||||
provider_type_is_fixed(provider_type) && provider_key_is_oauth_managed(key, provider_type)
|
||||
}
|
||||
|
||||
pub(crate) fn provider_key_effective_api_formats(
|
||||
key: &StoredProviderCatalogKey,
|
||||
provider_type: &str,
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
) -> Vec<String> {
|
||||
if provider_key_inherits_provider_api_formats(key, provider_type) {
|
||||
provider_active_api_formats(endpoints)
|
||||
} else {
|
||||
provider_key_configured_api_formats(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
provider_key_auth_semantics, ProviderKeyCredentialKind, ProviderKeyRuntimeAuthKind,
|
||||
provider_active_api_formats, provider_key_auth_semantics,
|
||||
provider_key_configured_api_formats, provider_key_effective_api_formats,
|
||||
provider_key_inherits_provider_api_formats, ProviderKeyCredentialKind,
|
||||
ProviderKeyRuntimeAuthKind,
|
||||
};
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
fn sample_key(auth_type: &str) -> StoredProviderCatalogKey {
|
||||
StoredProviderCatalogKey::new(
|
||||
@@ -165,6 +225,21 @@ mod tests {
|
||||
.expect("key should build")
|
||||
}
|
||||
|
||||
fn sample_endpoint(api_format: &str, is_active: bool) -> StoredProviderCatalogEndpoint {
|
||||
let mut endpoint = StoredProviderCatalogEndpoint::new(
|
||||
format!("endpoint-{api_format}"),
|
||||
"provider-1".to_string(),
|
||||
api_format.to_string(),
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("endpoint should build");
|
||||
endpoint.is_active = is_active;
|
||||
endpoint.base_url = "https://example.invalid".to_string();
|
||||
endpoint
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_oauth_managed_key() {
|
||||
let semantics = provider_key_auth_semantics(&sample_key("oauth"), "codex");
|
||||
@@ -227,4 +302,70 @@ mod tests {
|
||||
ProviderKeyRuntimeAuthKind::ServiceAccount
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deduplicates_active_provider_api_formats() {
|
||||
let endpoints = vec![
|
||||
sample_endpoint("openai:cli", true),
|
||||
sample_endpoint("openai:image", true),
|
||||
sample_endpoint("openai:cli", true),
|
||||
sample_endpoint("openai:compact", false),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
provider_active_api_formats(&endpoints),
|
||||
vec!["openai:cli".to_string(), "openai:image".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_oauth_key_with_null_formats_inherits_provider_formats() {
|
||||
let key = sample_key("oauth");
|
||||
let endpoints = vec![
|
||||
sample_endpoint("openai:cli", true),
|
||||
sample_endpoint("openai:image", true),
|
||||
];
|
||||
|
||||
assert!(provider_key_inherits_provider_api_formats(&key, "codex"));
|
||||
assert_eq!(
|
||||
provider_key_effective_api_formats(&key, "codex", &endpoints),
|
||||
vec!["openai:cli".to_string(), "openai:image".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_oauth_key_with_legacy_explicit_formats_still_inherits_provider_formats() {
|
||||
let mut key = sample_key("oauth");
|
||||
key.api_formats = Some(json!(["openai:compact"]));
|
||||
let endpoints = vec![
|
||||
sample_endpoint("openai:cli", true),
|
||||
sample_endpoint("openai:image", true),
|
||||
];
|
||||
|
||||
assert!(provider_key_inherits_provider_api_formats(&key, "codex"));
|
||||
assert_eq!(
|
||||
provider_key_effective_api_formats(&key, "codex", &endpoints),
|
||||
vec!["openai:cli".to_string(), "openai:image".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_formats_do_not_inherit_for_non_fixed_key() {
|
||||
let mut key = sample_key("oauth");
|
||||
key.api_formats = Some(json!(["openai:compact"]));
|
||||
let endpoints = vec![
|
||||
sample_endpoint("openai:cli", true),
|
||||
sample_endpoint("openai:image", true),
|
||||
];
|
||||
|
||||
assert!(!provider_key_inherits_provider_api_formats(&key, "openai"));
|
||||
assert_eq!(
|
||||
provider_key_configured_api_formats(&key),
|
||||
vec!["openai:compact".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
provider_key_effective_api_formats(&key, "openai", &endpoints),
|
||||
vec!["openai:compact".to_string()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user