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

@@ -51,7 +51,7 @@ pub(crate) use aether_provider_transport::{
apply_local_body_rules_with_request_headers, apply_local_header_rules,
apply_local_header_rules_with_request_headers, apply_standard_provider_request_body_rules,
apply_standard_provider_request_body_rules_with_request_headers,
body_rules_are_locally_supported, body_rules_handle_path,
body_rules_are_locally_supported, body_rules_handle_path, body_rules_have_enabled_rules,
build_cross_format_openai_chat_upstream_url, build_cross_format_openai_responses_upstream_url,
build_gemini_files_headers, build_gemini_files_request_body, build_gemini_files_upstream_url,
build_kiro_cross_format_upstream_url, build_local_openai_chat_upstream_url,
@@ -65,7 +65,8 @@ pub(crate) use aether_provider_transport::{
build_video_create_upstream_url, candidate_common_transport_skip_reason,
candidate_transport_pair_skip_reason, classify_same_format_provider_request_behavior,
ensure_upstream_auth_header, gemini_files_transport_unsupported_reason,
header_rules_are_locally_supported, local_gemini_transport_unsupported_reason_with_network,
header_rules_are_locally_supported, header_rules_have_enabled_rules,
local_gemini_transport_unsupported_reason_with_network,
local_openai_chat_transport_unsupported_reason,
local_standard_transport_unsupported_reason_with_network,
openai_image_transport_unsupported_reason, request_conversion_direct_auth,

View File

@@ -242,6 +242,7 @@ mod tests {
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -341,6 +342,7 @@ mod tests {
name: "gemini-2.5-pro-alias".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]);
let state = state_with_rows(vec![row]);
let decision = decision_with_allowed_models(vec!["gpt-5".to_string()]);

View File

@@ -482,6 +482,7 @@ fn sample_minimal_candidate_selection_row(
name: "gpt-4.1-canary".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: None,
model_is_active: true,

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());
}
}

View File

@@ -105,6 +105,7 @@ async fn same_priority_candidates_are_distributed_by_affinity_key() {
name: "gpt-4.1-a".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let mut second = sample_row();
@@ -121,6 +122,7 @@ async fn same_priority_candidates_are_distributed_by_affinity_key() {
name: "gpt-4.1-b".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![

View File

@@ -36,6 +36,32 @@ fn candidate_model_names_keep_base_and_scoped_mappings() {
assert!(!names.contains("gpt-4.1-responses"));
}
#[test]
fn provider_model_mapping_respects_endpoint_scope() {
let mut row = sample_row();
row.endpoint_id = "endpoint-selected".to_string();
row.model_provider_model_mappings = Some(vec![StoredProviderModelMapping {
name: "gpt-4.1-endpoint".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec!["endpoint-selected".to_string()]),
}]);
assert_eq!(
select_provider_model_name(&row, "openai:chat"),
"gpt-4.1-endpoint"
);
assert!(candidate_model_names(&row, "openai:chat").contains("gpt-4.1-endpoint"));
row.endpoint_id = "endpoint-other".to_string();
assert_eq!(
select_provider_model_name(&row, "openai:chat"),
"gpt-4.1-upstream"
);
assert!(!candidate_model_names(&row, "openai:chat").contains("gpt-4.1-endpoint"));
}
#[test]
fn resolves_mapping_matched_model_from_key_allowed_models() {
let mut row = sample_row();
@@ -74,6 +100,7 @@ fn resolves_requested_global_model_from_provider_model_alias() {
name: "gpt-5.2".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let resolved = resolve_requested_global_model_name(&[row], "gpt-5.2", "openai:chat");
@@ -127,6 +154,7 @@ async fn enumerate_minimal_candidate_selection_resolves_provider_model_alias() {
name: "gpt-5.2".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -152,6 +180,47 @@ async fn enumerate_minimal_candidate_selection_resolves_provider_model_alias() {
assert_eq!(selection[0].selected_provider_model_name, "gpt-5.2");
}
#[tokio::test]
async fn enumerate_minimal_candidate_selection_filters_endpoint_scoped_alias_rows() {
let mut selected = sample_row();
selected.endpoint_id = "endpoint-selected".to_string();
selected.key_id = "key-selected".to_string();
selected.global_model_name = "gpt-5".to_string();
selected.model_provider_model_name = "gpt-5-upstream".to_string();
selected.model_provider_model_mappings = Some(vec![StoredProviderModelMapping {
name: "gpt-5-alias".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec!["endpoint-selected".to_string()]),
}]);
let mut other = selected.clone();
other.endpoint_id = "endpoint-other".to_string();
other.key_id = "key-other".to_string();
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
selected, other,
]));
let quotas = Arc::new(InMemoryProviderQuotaRepository::seed(vec![]));
let state = GatewayDataState::with_candidate_selection_and_quota_for_tests(candidates, quotas);
let selection = enumerate_minimal_candidate_selection_with_required_capabilities(
&state,
"openai:chat",
"gpt-5-alias",
false,
None,
None,
false,
)
.await
.expect("selection should succeed");
assert_eq!(selection.len(), 1);
assert_eq!(selection[0].endpoint_id, "endpoint-selected");
assert_eq!(selection[0].selected_provider_model_name, "gpt-5-alias");
}
#[tokio::test]
async fn enumerate_minimal_candidate_selection_keeps_only_resolved_global_model_rows() {
let mut exact = sample_row();
@@ -177,6 +246,7 @@ async fn enumerate_minimal_candidate_selection_keeps_only_resolved_global_model_
name: "claude-sonnet-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -215,6 +285,7 @@ async fn enumerate_minimal_candidate_selection_allows_resolved_global_model_in_a
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![

View File

@@ -707,6 +707,7 @@ async fn selects_next_candidate_when_first_provider_quota_is_exhausted() {
name: "gpt-4.1-primary".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
first.key_global_priority_by_format = Some(serde_json::json!({"openai:chat": 1}));
@@ -721,6 +722,7 @@ async fn selects_next_candidate_when_first_provider_quota_is_exhausted() {
name: "gpt-4.1-secondary".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
second.key_global_priority_by_format = Some(serde_json::json!({"openai:chat": 2}));
@@ -776,6 +778,7 @@ async fn cooled_down_when_recent_failures_are_recorded_for_same_key() {
name: "gpt-4.1-primary".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
first.key_global_priority_by_format = Some(serde_json::json!({"openai:chat": 1}));
@@ -790,6 +793,7 @@ async fn cooled_down_when_recent_failures_are_recorded_for_same_key() {
name: "gpt-4.1-secondary".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
second.key_global_priority_by_format = Some(serde_json::json!({"openai:chat": 2}));

View File

@@ -39,11 +39,13 @@ pub(super) fn sample_row() -> StoredMinimalCandidateSelectionRow {
name: "gpt-4.1-canary".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
},
StoredProviderModelMapping {
name: "gpt-4.1-responses".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
},
]),
model_supports_streaming: None,

View File

@@ -106,6 +106,7 @@ async fn gateway_executes_openai_chat_sync_upstream_stream_via_local_finalize_re
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -596,6 +597,7 @@ async fn gateway_executes_openai_chat_cross_format_upstream_stream_via_local_fin
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1032,6 +1034,7 @@ async fn gateway_executes_openai_chat_cross_format_tool_use_upstream_stream_via_
name: "claude-sonnet-4-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1485,6 +1488,7 @@ async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_tr
name: "claude-sonnet-4-5".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1845,6 +1849,7 @@ async fn gateway_executes_openai_chat_cross_format_claude_upstream_sync_via_loca
name: "claude-sonnet-4-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2196,6 +2201,7 @@ async fn gateway_executes_openai_chat_cross_format_gemini_upstream_sync_via_loca
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -104,6 +104,7 @@ async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses:compact".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -105,6 +105,7 @@ async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_loca
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -563,6 +564,7 @@ async fn gateway_executes_openai_responses_cross_format_function_call_upstream_s
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1033,6 +1035,7 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str
name: "claude-sonnet-4-5".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -127,6 +127,7 @@ async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finali
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -635,6 +636,7 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz
name: "claude-sonnet-4-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -130,6 +130,7 @@ async fn gateway_executes_claude_chat_sync_same_format_via_local_finalize_respon
name: "claude-sonnet-4-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -585,6 +586,7 @@ async fn gateway_executes_claude_chat_sync_upstream_stream_via_local_finalize_re
name: "claude-sonnet-4-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1045,6 +1047,7 @@ async fn gateway_executes_claude_cli_sync_upstream_stream_via_local_finalize_res
name: "claude-code-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -135,6 +135,7 @@ async fn gateway_executes_gemini_chat_sync_same_format_via_local_finalize_respon
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -638,6 +639,7 @@ async fn gateway_executes_gemini_chat_sync_upstream_stream_via_local_finalize_re
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1131,6 +1133,7 @@ async fn gateway_executes_gemini_cli_sync_upstream_stream_via_local_finalize_res
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1638,6 +1641,7 @@ async fn gateway_executes_antigravity_gemini_cli_sync_upstream_stream_via_local_
name: "claude-sonnet-4-5".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -85,6 +85,7 @@ fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelectionRow {
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -98,6 +98,7 @@ async fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_exe
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -322,6 +323,7 @@ async fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_exe
name: "gpt-5-upstream-backup".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -523,6 +525,7 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
name: "gpt-5.4".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1010,6 +1013,7 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1504,6 +1508,7 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
name: mapped_model.to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -104,6 +104,7 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
name: "gpt-image-2".to_string(),
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -510,6 +511,7 @@ async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse() {
name: "gpt-image-2".to_string(),
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -110,6 +110,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses:compact".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -113,6 +113,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
name: "gpt-5.4".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -149,6 +149,7 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand
name: "claude-sonnet-4-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -658,6 +659,7 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait
name: "claude-code-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1110,6 +1112,7 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo
name: "claude-code-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1634,6 +1637,7 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_
name: "claude-sonnet-4-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -92,6 +92,7 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -92,6 +92,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -533,6 +534,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1041,6 +1043,7 @@ async fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_wi
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1485,6 +1488,7 @@ async fn gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_
name: "claude-sonnet-4-5".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -85,6 +85,7 @@ async fn gateway_skips_unsupported_local_openai_chat_sync_candidate_before_tryin
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -289,6 +290,7 @@ async fn gateway_skips_unsupported_local_openai_chat_sync_candidate_before_tryin
name: "gpt-5-upstream-backup".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -485,6 +487,7 @@ async fn gateway_surfaces_local_execution_runtime_miss_reason_when_all_openai_ch
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -758,6 +761,7 @@ async fn gateway_retries_next_local_openai_chat_sync_candidate_after_auth_failur
name: mapped_model.to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -84,6 +84,7 @@ async fn gateway_executes_openai_chat_sync_via_local_decision_gate_without_execu
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -276,6 +277,7 @@ async fn gateway_executes_openai_chat_sync_via_local_decision_gate_without_execu
name: "gpt-5-upstream-backup".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -470,6 +472,7 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -490,6 +493,7 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
name: "gemini-2.5-flash-upstream".to_string(),
priority: 2,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]);
row
}
@@ -1032,6 +1036,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_claude_cli_syn
name: "claude-code-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1429,6 +1434,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1826,6 +1832,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_claude_sync_fa
name: "claude-sonnet-4-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2229,6 +2236,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_sync_fa
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2672,6 +2680,7 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -128,6 +128,7 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca
name: "claude-code-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -169,6 +169,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
name: "claude-sonnet-4-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -735,6 +736,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
name: "claude-sonnet-4-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -123,6 +123,7 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy
name: "claude-sonnet-4-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -692,6 +693,7 @@ async fn gateway_returns_claude_chat_error_for_local_sync_failure_impl() {
name: "claude-sonnet-4-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -123,6 +123,7 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn
name: "claude-code-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -568,6 +569,7 @@ async fn gateway_returns_claude_cli_error_for_local_sync_failure_impl() {
name: "claude-code-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -850,6 +852,7 @@ async fn gateway_marks_claude_cli_cross_format_runtime_miss_when_format_conversi
name: "gpt-5.4".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -110,6 +110,7 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -611,6 +612,7 @@ async fn gateway_waits_for_api_key_concurrency_slot_then_executes_openai_respons
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -993,6 +995,7 @@ async fn gateway_returns_concurrency_limited_after_wait_budget_expires_for_opena
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1341,6 +1344,7 @@ async fn gateway_returns_openai_responses_error_for_local_sync_failure() {
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1643,6 +1647,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2019,6 +2024,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_claude_sy
name: "claude-code-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2395,6 +2401,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_claude_ch
name: "claude-sonnet-4-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2774,6 +2781,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_ch
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -3160,6 +3168,7 @@ async fn gateway_executes_codex_cli_sync_via_local_decision_gate_after_oauth_ref
name: "gpt-5.4".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -122,6 +122,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -553,6 +554,7 @@ async fn gateway_returns_gemini_cli_error_for_local_sync_failure_impl() {
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -852,6 +854,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1362,6 +1365,7 @@ async fn gateway_executes_vertex_ai_gemini_cli_sync_via_local_decision_gate_with
name: "gemini-cli-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1807,6 +1811,7 @@ async fn gateway_executes_antigravity_gemini_cli_sync_via_local_decision_gate_af
name: "claude-sonnet-4-5".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -122,6 +122,7 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -543,6 +544,7 @@ async fn gateway_returns_gemini_chat_error_for_local_sync_failure_impl() {
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -110,6 +110,7 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
name: "gpt-image-2".to_string(),
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -604,6 +605,7 @@ async fn gateway_plans_chatgpt_web_image_sync_with_internal_web_executor_url() {
name: "gpt-image-2".to_string(),
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -90,6 +90,7 @@ fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelectionRow {
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -90,6 +90,7 @@ fn sample_files_candidate_row() -> StoredMinimalCandidateSelectionRow {
name: "gemini-2.5-pro-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:files".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -269,6 +269,7 @@ fn sample_models_candidate_row(
name: global_model_name.to_string(),
priority: 1,
api_formats: Some(vec![api_format.to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -100,6 +100,7 @@ pub(super) fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelec
name: "gpt-5-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -1459,6 +1459,7 @@ async fn gateway_records_failed_usage_when_all_local_claude_cli_candidates_are_s
name: "gpt-5.4".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1773,6 +1774,7 @@ fn gateway_keeps_failed_usage_request_capture_lightweight_for_large_local_claude
name: "gpt-5.4".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -269,6 +269,7 @@ fn sample_candidate_row(spec: ProviderSpec) -> StoredMinimalCandidateSelectionRo
name: spec.provider_model_name.to_string(),
priority: 1,
api_formats: Some(vec![spec.api_format.to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,

View File

@@ -109,6 +109,7 @@ async fn gateway_executes_gemini_video_create_via_local_decision_gate_with_local
name: "veo-3-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["gemini:video".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(false),
model_is_active: true,

View File

@@ -114,6 +114,7 @@ async fn gateway_executes_openai_video_create_via_local_decision_gate_with_local
name: "sora-2-upstream".to_string(),
priority: 1,
api_formats: Some(vec!["openai:video".to_string()]),
endpoint_ids: None,
}]),
model_supports_streaming: Some(false),
model_is_active: true,