feat: scope model mappings by endpoint

This commit is contained in:
fawney19
2026-05-07 00:48:15 +08:00
parent 4e9f063385
commit 012f8bcdf7
58 changed files with 731 additions and 101 deletions

View File

@@ -143,7 +143,9 @@ async fn admin_gemini_files_upload_single_key(
if !state.supports_local_gemini_transport_with_network(&transport, "gemini:generate_content") {
return Err("Key 传输配置不支持 Gemini Files 上传".to_string());
}
if transport.endpoint.body_rules.is_some() {
if crate::provider_transport::body_rules_have_enabled_rules(
transport.endpoint.body_rules.as_ref(),
) {
return Err("Gemini Files 二进制上传暂不支持 endpoint body_rules".to_string());
}
let (auth_header, auth_value) = state

View File

@@ -28,25 +28,43 @@ fn normalize_provider_model_mappings_api_formats(
let Some(object) = item.as_object_mut() else {
continue;
};
let Some(api_formats) = object.get_mut("api_formats") else {
continue;
};
let Some(array) = api_formats.as_array() else {
continue;
};
let mut seen = BTreeSet::new();
let normalized = array
.iter()
.filter_map(serde_json::Value::as_str)
.map(crate::ai_serving::normalize_api_format_alias)
.filter(|format| seen.insert(format.clone()))
.map(serde_json::Value::String)
.collect::<Vec<_>>();
*api_formats = serde_json::Value::Array(normalized);
normalize_provider_model_mapping_string_array_field(
object,
"api_formats",
crate::ai_serving::normalize_api_format_alias,
);
normalize_provider_model_mapping_string_array_field(object, "endpoint_ids", |value| {
value.trim().to_string()
});
}
Some(value)
}
fn normalize_provider_model_mapping_string_array_field(
object: &mut serde_json::Map<String, serde_json::Value>,
field: &str,
normalize: impl Fn(&str) -> String,
) {
let Some(array) = object.get(field).and_then(serde_json::Value::as_array) else {
return;
};
let mut seen = BTreeSet::new();
let normalized = array
.iter()
.filter_map(serde_json::Value::as_str)
.map(normalize)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.filter(|value| seen.insert(value.clone()))
.map(serde_json::Value::String)
.collect::<Vec<_>>();
if normalized.is_empty() {
object.remove(field);
} else {
object.insert(field.to_string(), serde_json::Value::Array(normalized));
}
}
impl<'a> AdminAppState<'a> {
pub(crate) async fn admin_provider_model_name_exists(
&self,

View File

@@ -114,14 +114,23 @@ fn auth_snapshot_allows_model_for_models(
fn mapping_scope_matches_for_models(
mapping: &StoredProviderModelMapping,
row: &StoredMinimalCandidateSelectionRow,
api_format: &str,
) -> bool {
let Some(api_formats) = mapping.api_formats.as_ref() else {
return true;
};
api_formats
.iter()
.any(|value| value.trim().eq_ignore_ascii_case(api_format))
let api_format_matches = mapping.api_formats.as_ref().is_none_or(|api_formats| {
api_formats
.iter()
.any(|value| value.trim().eq_ignore_ascii_case(api_format))
});
if !api_format_matches {
return false;
}
mapping.endpoint_ids.as_ref().is_none_or(|endpoint_ids| {
endpoint_ids
.iter()
.any(|endpoint_id| endpoint_id == &row.endpoint_id)
})
}
fn candidate_model_names_for_models(
@@ -131,7 +140,7 @@ fn candidate_model_names_for_models(
let mut names = std::collections::BTreeSet::from([row.model_provider_model_name.clone()]);
if let Some(mappings) = row.model_provider_model_mappings.as_ref() {
for mapping in mappings {
if mapping_scope_matches_for_models(mapping, api_format) {
if mapping_scope_matches_for_models(mapping, row, api_format) {
names.insert(mapping.name.clone());
}
}