2026-05-23 19:36:20 +08:00
|
|
|
use std::fmt::Debug;
|
|
|
|
|
use std::future::Future;
|
|
|
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
2026-04-04 01:40:24 +08:00
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
use aether_data_contracts::repository::candidate_selection::StoredMinimalCandidateSelectionRow;
|
2026-04-04 01:40:24 +08:00
|
|
|
use axum::{body::Body, response::Response};
|
2026-05-23 19:36:20 +08:00
|
|
|
use tokio::time::timeout;
|
|
|
|
|
use tracing::warn;
|
2026-04-04 01:40:24 +08:00
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
use super::models_responses::{
|
|
|
|
|
build_claude_model_detail_response, build_claude_models_list_response,
|
|
|
|
|
build_empty_models_list_response, build_gemini_model_detail_response,
|
|
|
|
|
build_gemini_models_list_response, build_models_auth_error_response,
|
|
|
|
|
build_models_not_found_response, build_openai_model_detail_response,
|
|
|
|
|
build_openai_models_list_response,
|
|
|
|
|
};
|
2026-04-09 13:51:50 +08:00
|
|
|
use super::models_shared::{
|
|
|
|
|
filter_rows_for_models, models_api_format, models_detail_id, models_query_api_formats,
|
|
|
|
|
};
|
2026-04-04 01:40:24 +08:00
|
|
|
use super::{query_param_value, AppState, GatewayPublicRequestContext};
|
2026-04-03 14:59:58 +08:00
|
|
|
|
2026-05-23 19:36:20 +08:00
|
|
|
#[cfg(not(test))]
|
|
|
|
|
const MODELS_ROUTE_READ_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
const MODELS_ROUTE_READ_TIMEOUT: Duration = Duration::from_millis(50);
|
|
|
|
|
|
|
|
|
|
async fn await_models_route_read<T, E, Fut>(operation: &'static str, future: Fut) -> Option<T>
|
|
|
|
|
where
|
|
|
|
|
E: Debug,
|
|
|
|
|
Fut: Future<Output = Result<T, E>>,
|
|
|
|
|
{
|
|
|
|
|
match timeout(MODELS_ROUTE_READ_TIMEOUT, future).await {
|
|
|
|
|
Ok(Ok(value)) => Some(value),
|
|
|
|
|
Ok(Err(error)) => {
|
|
|
|
|
warn!(
|
|
|
|
|
event_name = "models_route_read_error",
|
|
|
|
|
log_type = "ops",
|
|
|
|
|
operation,
|
|
|
|
|
error = ?error,
|
|
|
|
|
"gateway local models route read failed"
|
|
|
|
|
);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
warn!(
|
|
|
|
|
event_name = "models_route_read_timeout",
|
|
|
|
|
log_type = "ops",
|
|
|
|
|
operation,
|
|
|
|
|
timeout_ms = MODELS_ROUTE_READ_TIMEOUT.as_millis() as u64,
|
|
|
|
|
"gateway local models route read timed out"
|
|
|
|
|
);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_models_read_fallback_response(
|
|
|
|
|
request_context: &GatewayPublicRequestContext,
|
|
|
|
|
api_format: &str,
|
|
|
|
|
) -> Response<Body> {
|
|
|
|
|
let route_kind = request_context
|
|
|
|
|
.control_decision
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|decision| decision.route_kind.as_deref());
|
|
|
|
|
match route_kind {
|
|
|
|
|
Some("detail") => {
|
|
|
|
|
let model_id = models_detail_id(&request_context.request_path)
|
|
|
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
|
|
|
build_models_not_found_response(&model_id, api_format)
|
|
|
|
|
}
|
|
|
|
|
_ => build_empty_models_list_response(api_format),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
fn sort_and_dedup_model_rows(
|
|
|
|
|
mut rows: Vec<StoredMinimalCandidateSelectionRow>,
|
|
|
|
|
) -> Vec<StoredMinimalCandidateSelectionRow> {
|
|
|
|
|
rows.sort_by(|left, right| {
|
|
|
|
|
left.global_model_name
|
|
|
|
|
.cmp(&right.global_model_name)
|
|
|
|
|
.then(left.provider_priority.cmp(&right.provider_priority))
|
|
|
|
|
.then(left.key_internal_priority.cmp(&right.key_internal_priority))
|
|
|
|
|
.then(left.provider_id.cmp(&right.provider_id))
|
|
|
|
|
.then(left.endpoint_id.cmp(&right.endpoint_id))
|
|
|
|
|
.then(left.key_id.cmp(&right.key_id))
|
|
|
|
|
.then(left.model_id.cmp(&right.model_id))
|
|
|
|
|
});
|
|
|
|
|
let mut deduped = Vec::with_capacity(rows.len());
|
|
|
|
|
let mut last_model_name: Option<String> = None;
|
|
|
|
|
for row in rows {
|
|
|
|
|
if last_model_name.as_deref() == Some(row.global_model_name.as_str()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
last_model_name = Some(row.global_model_name.clone());
|
|
|
|
|
deduped.push(row);
|
|
|
|
|
}
|
|
|
|
|
deduped
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_model_rows_for_client_format(
|
|
|
|
|
state: &AppState,
|
|
|
|
|
api_format: &str,
|
|
|
|
|
auth_snapshot: Option<&crate::data::auth::GatewayAuthApiKeySnapshot>,
|
|
|
|
|
) -> Option<Vec<StoredMinimalCandidateSelectionRow>> {
|
|
|
|
|
let mut collected = Vec::new();
|
|
|
|
|
for query_format in models_query_api_formats(api_format) {
|
2026-05-23 19:36:20 +08:00
|
|
|
let rows = await_models_route_read(
|
|
|
|
|
"candidate_selection_by_api_format",
|
|
|
|
|
state.list_minimal_candidate_selection_rows_for_api_format(query_format),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2026-04-09 13:51:50 +08:00
|
|
|
let mut filtered = filter_rows_for_models(rows, auth_snapshot, query_format);
|
|
|
|
|
collected.append(&mut filtered);
|
|
|
|
|
}
|
|
|
|
|
Some(sort_and_dedup_model_rows(collected))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_model_rows_for_client_format_and_global_model(
|
|
|
|
|
state: &AppState,
|
|
|
|
|
api_format: &str,
|
|
|
|
|
global_model_name: &str,
|
|
|
|
|
auth_snapshot: Option<&crate::data::auth::GatewayAuthApiKeySnapshot>,
|
|
|
|
|
) -> Option<Vec<StoredMinimalCandidateSelectionRow>> {
|
|
|
|
|
let mut collected = Vec::new();
|
|
|
|
|
for query_format in models_query_api_formats(api_format) {
|
2026-05-23 19:36:20 +08:00
|
|
|
let rows = await_models_route_read(
|
|
|
|
|
"candidate_selection_by_global_model",
|
|
|
|
|
state.list_minimal_candidate_selection_rows_for_api_format_and_global_model(
|
2026-04-09 13:51:50 +08:00
|
|
|
query_format,
|
|
|
|
|
global_model_name,
|
2026-05-23 19:36:20 +08:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2026-04-09 13:51:50 +08:00
|
|
|
let mut filtered = filter_rows_for_models(rows, auth_snapshot, query_format);
|
|
|
|
|
collected.append(&mut filtered);
|
|
|
|
|
}
|
|
|
|
|
Some(sort_and_dedup_model_rows(collected))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
pub(super) async fn maybe_build_local_models_route_response(
|
|
|
|
|
state: &AppState,
|
|
|
|
|
request_context: &GatewayPublicRequestContext,
|
|
|
|
|
) -> Option<Response<Body>> {
|
|
|
|
|
let decision = request_context.control_decision.as_ref()?;
|
|
|
|
|
if decision.route_family.as_deref() != Some("models") {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let api_format = models_api_format(request_context)?;
|
|
|
|
|
if !state.has_minimal_candidate_selection_reader() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let auth_context = decision.auth_context.as_ref()?;
|
|
|
|
|
let now_unix_secs = SystemTime::now()
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_secs();
|
2026-05-23 19:36:20 +08:00
|
|
|
let auth_snapshot = match await_models_route_read(
|
|
|
|
|
"auth_api_key_snapshot",
|
|
|
|
|
state.data.read_auth_api_key_snapshot(
|
2026-04-03 14:59:58 +08:00
|
|
|
&auth_context.user_id,
|
|
|
|
|
&auth_context.api_key_id,
|
|
|
|
|
now_unix_secs,
|
2026-05-23 19:36:20 +08:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Some(snapshot) => snapshot,
|
|
|
|
|
None => {
|
|
|
|
|
return Some(build_models_read_fallback_response(
|
|
|
|
|
request_context,
|
|
|
|
|
api_format,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-03 14:59:58 +08:00
|
|
|
let auth_snapshot = auth_snapshot.as_ref();
|
|
|
|
|
|
|
|
|
|
match decision.route_kind.as_deref() {
|
|
|
|
|
Some("list") => {
|
2026-05-23 19:36:20 +08:00
|
|
|
let rows =
|
|
|
|
|
match list_model_rows_for_client_format(state, api_format, auth_snapshot).await {
|
|
|
|
|
Some(rows) => rows,
|
|
|
|
|
None => {
|
|
|
|
|
return Some(build_models_read_fallback_response(
|
|
|
|
|
request_context,
|
|
|
|
|
api_format,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-03 14:59:58 +08:00
|
|
|
if rows.is_empty() {
|
|
|
|
|
return Some(build_empty_models_list_response(api_format));
|
|
|
|
|
}
|
|
|
|
|
let response = match api_format {
|
2026-04-29 09:25:19 +08:00
|
|
|
"claude:messages" => {
|
2026-04-03 14:59:58 +08:00
|
|
|
let before_id = query_param_value(
|
|
|
|
|
request_context.request_query_string.as_deref(),
|
|
|
|
|
"before_id",
|
|
|
|
|
);
|
|
|
|
|
let after_id = query_param_value(
|
|
|
|
|
request_context.request_query_string.as_deref(),
|
|
|
|
|
"after_id",
|
|
|
|
|
);
|
|
|
|
|
let limit =
|
|
|
|
|
query_param_value(request_context.request_query_string.as_deref(), "limit")
|
|
|
|
|
.and_then(|value| value.parse::<usize>().ok())
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(20);
|
|
|
|
|
build_claude_models_list_response(
|
|
|
|
|
&rows,
|
|
|
|
|
before_id.as_deref(),
|
|
|
|
|
after_id.as_deref(),
|
|
|
|
|
limit,
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-04-29 09:25:19 +08:00
|
|
|
"gemini:generate_content" => {
|
2026-04-03 14:59:58 +08:00
|
|
|
let page_size = query_param_value(
|
|
|
|
|
request_context.request_query_string.as_deref(),
|
|
|
|
|
"pageSize",
|
|
|
|
|
)
|
|
|
|
|
.and_then(|value| value.parse::<usize>().ok())
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(50);
|
|
|
|
|
let page_token = query_param_value(
|
|
|
|
|
request_context.request_query_string.as_deref(),
|
|
|
|
|
"pageToken",
|
|
|
|
|
);
|
|
|
|
|
build_gemini_models_list_response(&rows, page_size, page_token.as_deref())
|
|
|
|
|
}
|
|
|
|
|
_ => build_openai_models_list_response(&rows),
|
|
|
|
|
};
|
|
|
|
|
Some(response)
|
|
|
|
|
}
|
|
|
|
|
Some("detail") => {
|
|
|
|
|
let model_id = models_detail_id(&request_context.request_path)?;
|
2026-05-23 19:36:20 +08:00
|
|
|
let rows = match list_model_rows_for_client_format_and_global_model(
|
2026-04-09 13:51:50 +08:00
|
|
|
state,
|
|
|
|
|
api_format,
|
|
|
|
|
&model_id,
|
|
|
|
|
auth_snapshot,
|
|
|
|
|
)
|
2026-05-23 19:36:20 +08:00
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Some(rows) => rows,
|
|
|
|
|
None => {
|
|
|
|
|
return Some(build_models_read_fallback_response(
|
|
|
|
|
request_context,
|
|
|
|
|
api_format,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-03 14:59:58 +08:00
|
|
|
let Some(row) = rows.first() else {
|
|
|
|
|
return Some(build_models_not_found_response(&model_id, api_format));
|
|
|
|
|
};
|
|
|
|
|
let response = match api_format {
|
2026-04-29 09:25:19 +08:00
|
|
|
"claude:messages" => build_claude_model_detail_response(row),
|
|
|
|
|
"gemini:generate_content" => build_gemini_model_detail_response(row),
|
2026-04-03 14:59:58 +08:00
|
|
|
_ => build_openai_model_detail_response(row),
|
|
|
|
|
};
|
|
|
|
|
Some(response)
|
|
|
|
|
}
|
|
|
|
|
_ => Some(build_models_auth_error_response(api_format)),
|
|
|
|
|
}
|
|
|
|
|
}
|