mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 18:37:46 +08:00
Merge upstream main
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
mod balance;
|
||||
mod mutations;
|
||||
mod quota;
|
||||
mod reads;
|
||||
@@ -18,6 +19,10 @@ pub(crate) async fn maybe_build_local_admin_endpoints_keys_response(
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = balance::maybe_handle(state, request_context, request_body).await? {
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = mutations::maybe_handle(state, request_context, request_body).await? {
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,68 @@ pub(super) fn admin_provider_ops_is_valid_action_type(action_type: &str) -> bool
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_provider_ops_saved_connector_credentials(
|
||||
state: &AdminAppState<'_>,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
admin_provider_ops_decrypted_credentials(
|
||||
state,
|
||||
admin_provider_ops_config_object(provider)
|
||||
.and_then(admin_provider_ops_connector_object)
|
||||
.and_then(|connector| connector.get("credentials")),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_provider_ops_query_balance_response_for_credentials(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
architecture_id: &str,
|
||||
base_url: &str,
|
||||
provider_ops_config: &serde_json::Map<String, serde_json::Value>,
|
||||
connector_config: &serde_json::Map<String, serde_json::Value>,
|
||||
credentials: &serde_json::Map<String, serde_json::Value>,
|
||||
request_config: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||
) -> serde_json::Value {
|
||||
let architecture_id = normalize_architecture_id(architecture_id);
|
||||
let Some(architecture) = get_architecture(architecture_id) else {
|
||||
return responses::admin_provider_ops_action_not_supported(
|
||||
"query_balance",
|
||||
ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
|
||||
);
|
||||
};
|
||||
let headers = match build_headers(architecture.architecture_id, connector_config, credentials) {
|
||||
Ok(headers) => headers,
|
||||
Err(message) => {
|
||||
return responses::admin_provider_ops_action_not_configured("query_balance", message);
|
||||
}
|
||||
};
|
||||
let Some(action_config) = resolve_action_config(
|
||||
architecture_id,
|
||||
provider_ops_config,
|
||||
"query_balance",
|
||||
request_config,
|
||||
) else {
|
||||
return responses::admin_provider_ops_action_not_supported(
|
||||
"query_balance",
|
||||
ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
|
||||
);
|
||||
};
|
||||
|
||||
query_balance::admin_provider_ops_run_query_balance_action(
|
||||
state,
|
||||
provider_id,
|
||||
provider,
|
||||
&architecture,
|
||||
base_url,
|
||||
&action_config,
|
||||
&headers,
|
||||
credentials,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_provider_ops_local_action_response(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
|
||||
+7
-2
@@ -111,11 +111,14 @@ pub(super) async fn admin_provider_ops_run_query_balance_action(
|
||||
|
||||
if status != http::StatusCode::OK {
|
||||
let cookie_auth = architecture.query_balance_cookie_auth_errors;
|
||||
let new_api_token_auth = architecture.architecture_id == "new_api";
|
||||
return match status {
|
||||
http::StatusCode::UNAUTHORIZED => admin_provider_ops_action_error(
|
||||
"auth_failed",
|
||||
"query_balance",
|
||||
if cookie_auth {
|
||||
if new_api_token_auth {
|
||||
"访问令牌无效,请使用 New API 个人安全设置里的访问令牌"
|
||||
} else if cookie_auth {
|
||||
"Cookie 已失效,请重新配置"
|
||||
} else {
|
||||
"认证失败"
|
||||
@@ -125,7 +128,9 @@ pub(super) async fn admin_provider_ops_run_query_balance_action(
|
||||
http::StatusCode::FORBIDDEN => admin_provider_ops_action_error(
|
||||
"auth_failed",
|
||||
"query_balance",
|
||||
if cookie_auth {
|
||||
if new_api_token_auth {
|
||||
"访问令牌无效或无权限,请使用 New API 个人安全设置里的访问令牌"
|
||||
} else if cookie_auth {
|
||||
"Cookie 已失效或无权限"
|
||||
} else {
|
||||
"无权限访问"
|
||||
|
||||
+131
-1
@@ -8,7 +8,9 @@ use super::super::responses::{
|
||||
};
|
||||
use super::super::support::admin_provider_ops_json_object_map;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use aether_admin::provider::ops::parse_sub2api_balance_payload;
|
||||
use aether_admin::provider::ops::{
|
||||
parse_sub2api_api_key_usage_payload, parse_sub2api_balance_payload,
|
||||
};
|
||||
use aether_contracts::ProxySnapshot;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider;
|
||||
use serde_json::{json, Value};
|
||||
@@ -24,6 +26,23 @@ pub(super) async fn admin_provider_ops_sub2api_balance_payload(
|
||||
proxy_snapshot: Option<&ProxySnapshot>,
|
||||
) -> serde_json::Value {
|
||||
let start = std::time::Instant::now();
|
||||
if let Some(api_key) = credentials
|
||||
.get("api_key")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return admin_provider_ops_sub2api_api_key_balance_payload(
|
||||
state,
|
||||
provider_id,
|
||||
base_url,
|
||||
action_config,
|
||||
api_key,
|
||||
proxy_snapshot,
|
||||
start,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let (access_token, updated_credentials, _frontend_updated_credentials) =
|
||||
match admin_provider_ops_sub2api_exchange_token(
|
||||
state,
|
||||
@@ -191,6 +210,117 @@ pub(super) async fn admin_provider_ops_sub2api_balance_payload(
|
||||
)
|
||||
}
|
||||
|
||||
async fn admin_provider_ops_sub2api_api_key_balance_payload(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
base_url: &str,
|
||||
action_config: &serde_json::Map<String, serde_json::Value>,
|
||||
api_key: &str,
|
||||
proxy_snapshot: Option<&ProxySnapshot>,
|
||||
start: std::time::Instant,
|
||||
) -> serde_json::Value {
|
||||
let usage_endpoint = action_config
|
||||
.get("api_key_usage_endpoint")
|
||||
.or_else(|| action_config.get("usage_endpoint"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("/v1/usage");
|
||||
let usage_url = admin_provider_ops_sub2api_request_url(base_url, usage_endpoint);
|
||||
let auth_value = match reqwest::header::HeaderValue::from_str(&format!("Bearer {api_key}")) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"parse_error",
|
||||
"query_balance",
|
||||
"API Key 格式无效",
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
};
|
||||
let auth_headers = reqwest::header::HeaderMap::from_iter([
|
||||
(reqwest::header::AUTHORIZATION, auth_value),
|
||||
(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
]);
|
||||
let request_id = format!("provider-ops-action:sub2api:usage:{provider_id}");
|
||||
let result = admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
&request_id,
|
||||
reqwest::Method::GET,
|
||||
&usage_url,
|
||||
&auth_headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await;
|
||||
let response_time_ms = Some(start.elapsed().as_millis() as u64);
|
||||
let (status, response_json) = match result {
|
||||
Ok(result) => result,
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(message))
|
||||
| Err(AdminProviderOpsExecuteJsonError::Transport(message)) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
network_error_message(&message),
|
||||
response_time_ms,
|
||||
);
|
||||
}
|
||||
};
|
||||
if matches!(
|
||||
status,
|
||||
http::StatusCode::UNAUTHORIZED | http::StatusCode::FORBIDDEN
|
||||
) {
|
||||
return admin_provider_ops_action_error(
|
||||
"auth_failed",
|
||||
"query_balance",
|
||||
"认证失败,请检查 API Key",
|
||||
response_time_ms,
|
||||
);
|
||||
}
|
||||
if status != http::StatusCode::OK {
|
||||
return admin_provider_ops_action_error(
|
||||
"unknown_error",
|
||||
"query_balance",
|
||||
format!(
|
||||
"HTTP {}: {}",
|
||||
status.as_u16(),
|
||||
status.canonical_reason().unwrap_or("Unknown")
|
||||
),
|
||||
response_time_ms,
|
||||
);
|
||||
}
|
||||
|
||||
let data = match parse_sub2api_api_key_usage_payload(action_config, &response_json) {
|
||||
Ok(payload) => payload,
|
||||
Err(message) => {
|
||||
return admin_provider_ops_action_error(
|
||||
if message.contains("无效") {
|
||||
"auth_failed"
|
||||
} else if message == "响应格式无效" {
|
||||
"parse_error"
|
||||
} else {
|
||||
"unknown_error"
|
||||
},
|
||||
"query_balance",
|
||||
message,
|
||||
response_time_ms,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
admin_provider_ops_action_response(
|
||||
"success",
|
||||
"query_balance",
|
||||
data,
|
||||
None,
|
||||
response_time_ms,
|
||||
86400,
|
||||
)
|
||||
}
|
||||
|
||||
fn network_error_message(error: &str) -> String {
|
||||
let normalized = error.trim();
|
||||
let lower = normalized.to_ascii_lowercase();
|
||||
|
||||
@@ -250,6 +250,9 @@ pub(super) fn build_admin_provider_ops_saved_config_value(
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
payload: AdminProviderOpsSaveConfigRequest,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let architecture_id =
|
||||
admin_provider_ops_pure::normalize_architecture_id(payload.architecture_id.as_str())
|
||||
.to_string();
|
||||
let auth_type = payload.connector.auth_type.trim().to_string();
|
||||
if auth_type.is_empty() || !admin_provider_ops_is_supported_auth_type(auth_type.as_str()) {
|
||||
return Err("connector.auth_type 必须是合法的认证类型".to_string());
|
||||
@@ -257,7 +260,7 @@ pub(super) fn build_admin_provider_ops_saved_config_value(
|
||||
|
||||
let merged_credentials = admin_provider_ops_merge_credentials(
|
||||
state,
|
||||
payload.architecture_id.as_str(),
|
||||
architecture_id.as_str(),
|
||||
provider,
|
||||
payload.connector.credentials,
|
||||
);
|
||||
@@ -278,7 +281,7 @@ pub(super) fn build_admin_provider_ops_saved_config_value(
|
||||
.collect::<serde_json::Map<String, serde_json::Value>>();
|
||||
|
||||
Ok(json!({
|
||||
"architecture_id": payload.architecture_id,
|
||||
"architecture_id": architecture_id,
|
||||
"base_url": payload.base_url,
|
||||
"connector": {
|
||||
"auth_type": auth_type,
|
||||
@@ -328,14 +331,16 @@ pub(super) fn build_admin_provider_ops_config_payload(
|
||||
});
|
||||
};
|
||||
let connector = admin_provider_ops_connector_object(provider_ops_config);
|
||||
let architecture_id = provider_ops_config
|
||||
.get("architecture_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(admin_provider_ops_pure::normalize_architecture_id)
|
||||
.unwrap_or("generic_api");
|
||||
|
||||
json!({
|
||||
"provider_id": provider_id,
|
||||
"is_configured": true,
|
||||
"architecture_id": provider_ops_config
|
||||
.get("architecture_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("generic_api"),
|
||||
"architecture_id": architecture_id,
|
||||
"base_url": resolve_admin_provider_ops_base_url(
|
||||
provider,
|
||||
endpoints,
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::handlers::admin::provider::ops::providers::config::persist_admin_prov
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use aether_admin::provider::ops::{
|
||||
admin_provider_ops_frontend_updated_credentials, admin_provider_ops_verify_failure,
|
||||
parse_verify_payload, ADMIN_PROVIDER_OPS_USER_AGENT,
|
||||
admin_provider_ops_verify_success, admin_provider_ops_verify_user_payload,
|
||||
parse_sub2api_api_key_usage_payload, parse_verify_payload, ADMIN_PROVIDER_OPS_USER_AGENT,
|
||||
};
|
||||
use aether_contracts::ProxySnapshot;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider;
|
||||
@@ -21,6 +22,21 @@ pub(super) async fn admin_provider_ops_local_sub2api_verify_response(
|
||||
credentials: &Map<String, Value>,
|
||||
proxy_snapshot: Option<&ProxySnapshot>,
|
||||
) -> Value {
|
||||
if let Some(api_key) = credentials
|
||||
.get("api_key")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return admin_provider_ops_local_sub2api_api_key_verify_response(
|
||||
state,
|
||||
base_url,
|
||||
api_key,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let (access_token, updated_credentials, frontend_updated_credentials) =
|
||||
match admin_provider_ops_sub2api_exchange_token(
|
||||
state,
|
||||
@@ -93,6 +109,78 @@ pub(super) async fn admin_provider_ops_local_sub2api_verify_response(
|
||||
)
|
||||
}
|
||||
|
||||
async fn admin_provider_ops_local_sub2api_api_key_verify_response(
|
||||
state: &AdminAppState<'_>,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
proxy_snapshot: Option<&ProxySnapshot>,
|
||||
) -> Value {
|
||||
let usage_url = admin_provider_ops_sub2api_request_url(base_url, "/v1/usage");
|
||||
let auth_value = match reqwest::header::HeaderValue::from_str(&format!("Bearer {api_key}")) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return admin_provider_ops_verify_failure("API Key 格式无效"),
|
||||
};
|
||||
let auth_headers = reqwest::header::HeaderMap::from_iter([
|
||||
(reqwest::header::AUTHORIZATION, auth_value),
|
||||
(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
]);
|
||||
let auth_headers =
|
||||
admin_provider_ops_headers_with_transport_controls(&auth_headers, None, true);
|
||||
let (status, response_json) = match admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
"provider-ops-verify:sub2api:api-key",
|
||||
reqwest::Method::GET,
|
||||
&usage_url,
|
||||
&auth_headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(message))
|
||||
| Err(AdminProviderOpsExecuteJsonError::Transport(message)) => {
|
||||
return admin_provider_ops_verify_failure(
|
||||
admin_provider_ops_verify_execution_error_message(&message),
|
||||
);
|
||||
}
|
||||
};
|
||||
if matches!(
|
||||
status,
|
||||
http::StatusCode::UNAUTHORIZED | http::StatusCode::FORBIDDEN
|
||||
) {
|
||||
return admin_provider_ops_verify_failure("认证失败:API Key 无效或已过期");
|
||||
}
|
||||
if status != http::StatusCode::OK {
|
||||
return admin_provider_ops_verify_failure(format!("验证失败:HTTP {}", status.as_u16()));
|
||||
}
|
||||
|
||||
let payload = match parse_sub2api_api_key_usage_payload(&Map::new(), &response_json) {
|
||||
Ok(payload) => payload,
|
||||
Err(message) => return admin_provider_ops_verify_failure(message),
|
||||
};
|
||||
let quota = payload.get("total_available").and_then(Value::as_f64);
|
||||
let extra = payload
|
||||
.get("extra")
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
admin_provider_ops_verify_success(
|
||||
admin_provider_ops_verify_user_payload(
|
||||
Some("Sub2API API Key".to_string()),
|
||||
Some("Sub2API API Key".to_string()),
|
||||
None,
|
||||
quota,
|
||||
Some(extra),
|
||||
),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
// 对齐 Python httpx.AsyncClient(base_url=...) 的行为:
|
||||
// 以 "/" 开头的端点始终相对站点根路径解析,而不是简单字符串拼接。
|
||||
pub(in super::super) fn admin_provider_ops_sub2api_request_url(
|
||||
|
||||
@@ -564,6 +564,29 @@ fn provider_query_build_test_request_body_for_api_format(
|
||||
client_api_format.as_str(),
|
||||
payload,
|
||||
);
|
||||
} else if matches!(
|
||||
client_api_format.as_str(),
|
||||
"openai:responses" | "openai:responses:compact"
|
||||
) && !value_has_non_empty_text(object.get("input"))
|
||||
{
|
||||
if let Some(prompt) = object
|
||||
.remove("prompt")
|
||||
.filter(|value| value_has_non_empty_text(Some(value)))
|
||||
{
|
||||
object.insert("input".to_string(), prompt);
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
client_api_format.as_str(),
|
||||
"openai:responses" | "openai:responses:compact"
|
||||
) && value_has_non_empty_text(object.get("input"))
|
||||
{
|
||||
object.remove("prompt");
|
||||
}
|
||||
if client_api_format == "openai:responses:compact"
|
||||
&& value_has_non_empty_text(object.get("input"))
|
||||
{
|
||||
object.remove("messages");
|
||||
}
|
||||
}
|
||||
return body;
|
||||
|
||||
@@ -30,7 +30,9 @@ pub(super) fn provider_query_standard_test_client_api_format(
|
||||
provider_api_format: &str,
|
||||
) -> &'static str {
|
||||
let normalized_api_format = crate::ai_serving::normalize_api_format_alias(provider_api_format);
|
||||
if crate::ai_serving::is_embedding_api_format(&normalized_api_format) {
|
||||
if normalized_api_format == "openai:responses:compact" {
|
||||
"openai:responses:compact"
|
||||
} else if crate::ai_serving::is_embedding_api_format(&normalized_api_format) {
|
||||
"openai:embedding"
|
||||
} else if crate::ai_serving::is_rerank_api_format(&normalized_api_format) {
|
||||
"openai:rerank"
|
||||
|
||||
@@ -324,6 +324,117 @@ fn provider_query_responses_test_request_body_defaults_to_responses_input() {
|
||||
assert!(body.get("messages").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_query_compact_test_request_body_defaults_to_responses_input() {
|
||||
let payload = json!({"message": "hello from compact"});
|
||||
|
||||
let client_api_format =
|
||||
provider_query_standard_test_client_api_format("openai:responses:compact");
|
||||
let body = provider_query_build_test_request_body_for_api_format(
|
||||
&payload,
|
||||
"gpt-5.4-mini",
|
||||
"/api/admin/provider-query/test-model",
|
||||
client_api_format,
|
||||
);
|
||||
|
||||
assert_eq!(client_api_format, "openai:responses:compact");
|
||||
assert_eq!(body["model"], json!("gpt-5.4-mini"));
|
||||
assert_eq!(body["input"], json!("hello from compact"));
|
||||
assert!(body.get("messages").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_query_compact_test_request_body_promotes_prompt_to_input() {
|
||||
let payload = json!({
|
||||
"request_body": {
|
||||
"model": "custom-model",
|
||||
"prompt": "hello from prompt"
|
||||
}
|
||||
});
|
||||
|
||||
let body = provider_query_build_test_request_body_for_api_format(
|
||||
&payload,
|
||||
"fallback-model",
|
||||
"/api/admin/provider-query/test-model",
|
||||
"openai:responses:compact",
|
||||
);
|
||||
|
||||
assert_eq!(body["model"], json!("custom-model"));
|
||||
assert_eq!(body["input"], json!("hello from prompt"));
|
||||
assert!(body.get("prompt").is_none());
|
||||
assert!(provider_query_request_body_is_openai_responses_shape(&body));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_query_compact_test_request_body_strips_stale_chat_fields() {
|
||||
let payload = json!({
|
||||
"request_body": {
|
||||
"model": "custom-model",
|
||||
"input": "hello from input",
|
||||
"messages": [{ "role": "user", "content": "stale chat body" }],
|
||||
"prompt": "stale prompt"
|
||||
}
|
||||
});
|
||||
|
||||
let body = provider_query_build_test_request_body_for_api_format(
|
||||
&payload,
|
||||
"fallback-model",
|
||||
"/api/admin/provider-query/test-model",
|
||||
"openai:responses:compact",
|
||||
);
|
||||
|
||||
assert_eq!(body["model"], json!("custom-model"));
|
||||
assert_eq!(body["input"], json!("hello from input"));
|
||||
assert!(body.get("messages").is_none());
|
||||
assert!(body.get("prompt").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_query_compact_provider_body_builds_without_chat_conversion() {
|
||||
let payload = json!({"message": "hello compact provider"});
|
||||
let client_api_format =
|
||||
provider_query_standard_test_client_api_format("openai:responses:compact");
|
||||
let mut request_body = provider_query_build_test_request_body_for_api_format(
|
||||
&payload,
|
||||
"gpt-5.4-mini",
|
||||
"/api/admin/provider-query/test-model",
|
||||
client_api_format,
|
||||
);
|
||||
if let Some(object) = request_body.as_object_mut() {
|
||||
object.insert("stream".to_string(), serde_json::Value::Bool(false));
|
||||
}
|
||||
|
||||
assert!(provider_query_request_body_is_openai_responses_shape(
|
||||
&request_body
|
||||
));
|
||||
|
||||
let mut provider_request_body = crate::ai_serving::build_local_openai_responses_request_body(
|
||||
&request_body,
|
||||
"upstream-gpt",
|
||||
false,
|
||||
)
|
||||
.expect("compact model test body should build from responses shape");
|
||||
crate::ai_serving::apply_openai_responses_compact_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
"openai:responses:compact",
|
||||
);
|
||||
crate::ai_serving::enforce_request_body_stream_field(
|
||||
&mut provider_request_body,
|
||||
"openai:responses:compact",
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
assert_eq!(provider_request_body["model"], json!("upstream-gpt"));
|
||||
assert_eq!(
|
||||
provider_request_body["input"],
|
||||
json!("hello compact provider")
|
||||
);
|
||||
assert!(provider_request_body.get("messages").is_none());
|
||||
assert!(provider_request_body.get("stream").is_none());
|
||||
assert!(provider_request_body.get("store").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_query_standard_test_rejects_gemini_success_without_visible_output() {
|
||||
let result = aether_contracts::ExecutionResult {
|
||||
|
||||
@@ -12,6 +12,13 @@ pub(crate) fn admin_provider_id_for_refresh_quota(request_path: &str) -> Option<
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_provider_id_for_key_balance(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/endpoints/providers/")?
|
||||
.strip_suffix("/key-balance")
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_reveal_key_id(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/endpoints/keys/")?
|
||||
|
||||
@@ -15,9 +15,9 @@ pub(crate) use self::crud::{
|
||||
is_admin_providers_root,
|
||||
};
|
||||
pub(crate) use self::endpoint_keys::{
|
||||
admin_clear_oauth_invalid_key_id, admin_export_key_id, admin_provider_id_for_keys,
|
||||
admin_provider_id_for_refresh_quota, admin_reset_cycle_stats_key_id, admin_reveal_key_id,
|
||||
admin_update_key_id,
|
||||
admin_clear_oauth_invalid_key_id, admin_export_key_id, admin_provider_id_for_key_balance,
|
||||
admin_provider_id_for_keys, admin_provider_id_for_refresh_quota,
|
||||
admin_reset_cycle_stats_key_id, admin_reveal_key_id, admin_update_key_id,
|
||||
};
|
||||
pub(crate) use self::oauth::{
|
||||
admin_provider_oauth_batch_import_provider_id, admin_provider_oauth_batch_import_task_path,
|
||||
|
||||
@@ -31,7 +31,7 @@ pub(crate) async fn build_admin_provider_summary_payload(
|
||||
active_global_model_ids_result,
|
||||
) = tokio::join!(
|
||||
state.list_provider_catalog_endpoints_by_provider_ids(&provider_ids),
|
||||
state.list_provider_catalog_key_summaries_by_provider_ids(&provider_ids),
|
||||
state.list_provider_catalog_keys_by_provider_ids(&provider_ids),
|
||||
state.read_provider_quota_snapshot(provider_id),
|
||||
state.list_provider_model_stats(&provider_ids),
|
||||
state.list_active_global_model_ids_by_provider_ids(&provider_ids),
|
||||
@@ -197,7 +197,7 @@ pub(crate) async fn build_admin_providers_summary_payload(
|
||||
} else {
|
||||
let (endpoints_result, keys_result, model_stats_result, active_global_model_refs_result) = tokio::join!(
|
||||
state.list_provider_catalog_endpoints_by_provider_ids(&provider_ids),
|
||||
state.list_provider_catalog_key_summaries_by_provider_ids(&provider_ids),
|
||||
state.list_provider_catalog_keys_by_provider_ids(&provider_ids),
|
||||
state.list_provider_model_stats(&provider_ids),
|
||||
state.list_active_global_model_ids_by_provider_ids(&provider_ids),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ use aether_data_contracts::repository::candidates::{
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use serde_json::json;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn json_truthy(value: &serde_json::Value) -> bool {
|
||||
@@ -27,6 +27,80 @@ fn endpoint_timestamp_or_now(value: Option<u64>, now_unix_secs: u64) -> serde_js
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}
|
||||
|
||||
fn finite_json_number(value: Option<&Value>) -> Option<f64> {
|
||||
match value {
|
||||
Some(Value::Number(number)) => number.as_f64().filter(|value| value.is_finite()),
|
||||
Some(Value::String(value)) => value
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.filter(|value| value.is_finite()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn finite_json_u64(value: Option<&Value>) -> Option<u64> {
|
||||
finite_json_number(value).and_then(|value| {
|
||||
if value >= 0.0 {
|
||||
Some(value as u64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn latest_key_balance_summary(keys: &[StoredProviderCatalogKey]) -> Value {
|
||||
let mut selected: Option<(u64, &StoredProviderCatalogKey, &Map<String, Value>)> = None;
|
||||
|
||||
for key in keys {
|
||||
let Some(balance) = key
|
||||
.upstream_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get("balance_query"))
|
||||
.and_then(Value::as_object)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(updated_at) = finite_json_u64(balance.get("updated_at")) else {
|
||||
continue;
|
||||
};
|
||||
let has_balance_value = ["total_available", "total_used", "total_granted"]
|
||||
.into_iter()
|
||||
.any(|field| finite_json_number(balance.get(field)).is_some());
|
||||
if !has_balance_value {
|
||||
continue;
|
||||
}
|
||||
|
||||
if selected
|
||||
.as_ref()
|
||||
.is_none_or(|(selected_updated_at, _, _)| updated_at > *selected_updated_at)
|
||||
{
|
||||
selected = Some((updated_at, key, balance));
|
||||
}
|
||||
}
|
||||
|
||||
let Some((updated_at, key, balance)) = selected else {
|
||||
return Value::Null;
|
||||
};
|
||||
|
||||
json!({
|
||||
"key_id": key.id.clone(),
|
||||
"key_name": key.name.clone(),
|
||||
"updated_at": updated_at,
|
||||
"architecture_id": balance.get("architecture_id").cloned().unwrap_or(Value::Null),
|
||||
"status": balance.get("status").cloned().unwrap_or_else(|| json!("success")),
|
||||
"executed_at": balance.get("executed_at").cloned().unwrap_or(Value::Null),
|
||||
"response_time_ms": balance.get("response_time_ms").cloned().unwrap_or(Value::Null),
|
||||
"total_available": balance.get("total_available").cloned().unwrap_or(Value::Null),
|
||||
"total_used": balance.get("total_used").cloned().unwrap_or(Value::Null),
|
||||
"total_granted": balance.get("total_granted").cloned().unwrap_or(Value::Null),
|
||||
"currency": balance.get("currency").cloned().unwrap_or_else(|| json!("USD")),
|
||||
"plan_name": balance.get("plan_name").cloned().unwrap_or(Value::Null),
|
||||
"query_config": balance.get("query_config").cloned().unwrap_or(Value::Null),
|
||||
"extra": balance.get("extra").cloned().unwrap_or(Value::Null),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_provider_summary_value(
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
@@ -158,6 +232,7 @@ pub(crate) fn build_admin_provider_summary_value(
|
||||
.and_then(|quota| quota.quota_expires_at_unix_secs)
|
||||
.or(provider.quota_expires_at_unix_secs)
|
||||
.and_then(unix_secs_to_rfc3339);
|
||||
let key_balance_summary = latest_key_balance_summary(keys);
|
||||
|
||||
json!({
|
||||
"id": provider.id.clone(),
|
||||
@@ -196,6 +271,7 @@ pub(crate) fn build_admin_provider_summary_value(
|
||||
"endpoint_health_details": endpoint_health_details,
|
||||
"ops_configured": ops_configured,
|
||||
"ops_architecture_id": ops_architecture_id,
|
||||
"key_balance_summary": key_balance_summary,
|
||||
"kiro_simulated_cache_enabled": kiro_simulated_cache_enabled,
|
||||
"created_at": endpoint_timestamp_or_now(provider.created_at_unix_ms, now_unix_secs),
|
||||
"updated_at": endpoint_timestamp_or_now(provider.updated_at_unix_secs, now_unix_secs),
|
||||
|
||||
Reference in New Issue
Block a user