feat: configure auth channel mismatch formats

This commit is contained in:
fawney19
2026-05-03 00:49:22 +08:00
parent 3a770306cc
commit e3ea2d1451
63 changed files with 585 additions and 39 deletions

View File

@@ -69,6 +69,7 @@ struct GatewayLocalCandidateMaterializationPort<'a, F, G> {
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&'a Value>,
sticky_session_token: Option<&'a str>,
request_auth_channel: Option<&'a str>,
persistence_policy: LocalCandidatePersistencePolicy<'a>,
resolution_mode: LocalCandidateResolutionMode,
build_available_extra_data: F,
@@ -123,6 +124,7 @@ where
self.auth_snapshot,
self.required_capabilities,
self.sticky_session_token,
self.request_auth_channel,
)
.await
}
@@ -135,6 +137,7 @@ where
self.auth_snapshot,
self.required_capabilities,
self.sticky_session_token,
self.request_auth_channel,
)
.await
}
@@ -314,6 +317,7 @@ pub(crate) async fn materialize_local_execution_candidates_with_serving<F, G>(
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&Value>,
sticky_session_token: Option<&str>,
request_auth_channel: Option<&str>,
persistence_policy: LocalCandidatePersistencePolicy<'_>,
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
preselection_skipped: Vec<SkippedLocalExecutionCandidate>,
@@ -333,6 +337,7 @@ where
auth_snapshot,
required_capabilities,
sticky_session_token,
request_auth_channel,
persistence_policy,
resolution_mode,
build_available_extra_data,
@@ -682,6 +687,7 @@ mod tests {
is_active: true,
api_formats: Some(vec!["openai:chat".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,

View File

@@ -182,6 +182,7 @@ mod tests {
is_active: true,
api_formats: None,
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
@@ -240,6 +241,7 @@ mod tests {
is_active: true,
api_formats: Some(vec!["claude:messages".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,

View File

@@ -136,6 +136,7 @@ mod tests {
is_active: true,
api_formats: Some(vec!["openai:chat".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,

View File

@@ -1048,6 +1048,7 @@ mod tests {
None,
None,
None,
None,
)
.await;
@@ -1125,6 +1126,7 @@ mod tests {
None,
None,
None,
None,
)
.await;
@@ -1198,6 +1200,7 @@ mod tests {
None,
None,
None,
None,
)
.await;
@@ -1262,6 +1265,7 @@ mod tests {
None,
None,
None,
None,
)
.await;
@@ -1342,6 +1346,7 @@ mod tests {
None,
None,
None,
None,
)
.await;
@@ -1413,6 +1418,7 @@ mod tests {
Some(&auth_snapshot),
None,
None,
None,
)
.await;
@@ -1488,6 +1494,7 @@ mod tests {
None,
None,
None,
None,
)
.await;
@@ -1580,6 +1587,7 @@ mod tests {
Some(&auth_snapshot),
None,
None,
None,
)
.await;
@@ -1680,6 +1688,7 @@ mod tests {
Some(&auth_snapshot),
None,
None,
None,
)
.await;
@@ -1781,6 +1790,7 @@ mod tests {
Some(&auth_snapshot),
None,
None,
None,
)
.await;
@@ -1882,6 +1892,7 @@ mod tests {
Some(&auth_snapshot),
None,
None,
None,
)
.await;

View File

@@ -50,6 +50,7 @@ struct GatewayLocalCandidateResolutionPort<'a> {
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&'a serde_json::Value>,
sticky_session_token: Option<&'a str>,
request_auth_channel: Option<&'a str>,
}
#[async_trait]
@@ -86,6 +87,11 @@ impl AiCandidateResolutionPort for GatewayLocalCandidateResolutionPort<'_> {
transport: &Self::Transport,
requested_model: Option<&str>,
) -> Option<&'static str> {
if let Some(skip_reason) =
candidate_auth_channel_skip_reason(transport, self.request_auth_channel)
{
return Some(skip_reason);
}
candidate_common_transport_skip_reason(
transport,
candidate_transport_policy_facts(candidate),
@@ -169,6 +175,7 @@ pub(crate) async fn resolve_and_rank_local_execution_candidates(
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
request_auth_channel: Option<&str>,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
@@ -182,6 +189,7 @@ pub(crate) async fn resolve_and_rank_local_execution_candidates(
auth_snapshot,
required_capabilities,
sticky_session_token,
request_auth_channel,
AiCandidateResolutionMode::Standard,
)
.await
@@ -195,6 +203,7 @@ pub(crate) async fn resolve_and_rank_local_execution_candidates_without_transpor
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
request_auth_channel: Option<&str>,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
@@ -208,6 +217,7 @@ pub(crate) async fn resolve_and_rank_local_execution_candidates_without_transpor
auth_snapshot,
required_capabilities,
sticky_session_token,
request_auth_channel,
AiCandidateResolutionMode::WithoutTransportPairGate,
)
.await
@@ -221,6 +231,7 @@ async fn resolve_and_rank_local_execution_candidates_with_mode(
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
request_auth_channel: Option<&str>,
mode: AiCandidateResolutionMode,
) -> (
Vec<EligibleLocalExecutionCandidate>,
@@ -232,6 +243,7 @@ async fn resolve_and_rank_local_execution_candidates_with_mode(
auth_snapshot,
required_capabilities,
sticky_session_token,
request_auth_channel,
};
let request = AiCandidateResolutionRequest {
@@ -257,6 +269,87 @@ fn candidate_transport_policy_facts(
}
}
fn candidate_auth_channel_skip_reason(
transport: &GatewayProviderTransportSnapshot,
request_auth_channel: Option<&str>,
) -> Option<&'static str> {
let request_auth_channel = normalize_request_auth_channel(request_auth_channel?)?;
let upstream_auth_channel = resolve_transport_request_auth_channel(transport)?;
if request_auth_channel == upstream_auth_channel
|| allow_auth_channel_mismatch_for_format(transport)
{
None
} else {
Some("auth_channel_mismatch")
}
}
fn normalize_request_auth_channel(value: &str) -> Option<&'static str> {
match value.trim().to_ascii_lowercase().as_str() {
"api_key" | "api-key" | "apikey" => Some("api_key"),
"bearer_like" | "bearer-like" | "bearer" | "oauth" => Some("bearer_like"),
_ => None,
}
}
fn resolve_transport_request_auth_channel(
transport: &GatewayProviderTransportSnapshot,
) -> Option<&'static str> {
let auth_type = resolve_transport_auth_type_for_endpoint_format(transport);
match auth_type.as_str() {
"api_key" => Some("api_key"),
"bearer" => Some("bearer_like"),
"oauth" if provider_uses_bearer_like_oauth(&transport.provider.provider_type) => {
Some("bearer_like")
}
_ => None,
}
}
fn resolve_transport_auth_type_for_endpoint_format(
transport: &GatewayProviderTransportSnapshot,
) -> String {
let default_auth_type = transport.key.auth_type.trim().to_ascii_lowercase();
let api_format = crate::ai_serving::normalize_api_format_alias(&transport.endpoint.api_format);
transport
.key
.auth_type_by_format
.as_ref()
.and_then(serde_json::Value::as_object)
.and_then(|overrides| {
overrides
.get(&api_format)
.or_else(|| overrides.get(transport.endpoint.api_format.trim()))
})
.and_then(serde_json::Value::as_str)
.map(str::trim)
.map(str::to_ascii_lowercase)
.filter(|value| matches!(value.as_str(), "api_key" | "bearer"))
.unwrap_or(default_auth_type)
}
fn provider_uses_bearer_like_oauth(provider_type: &str) -> bool {
matches!(
provider_type.trim().to_ascii_lowercase().as_str(),
"claude_code" | "gemini_cli" | "antigravity" | "kiro"
)
}
fn allow_auth_channel_mismatch_for_format(transport: &GatewayProviderTransportSnapshot) -> bool {
let api_format = crate::ai_serving::normalize_api_format_alias(&transport.endpoint.api_format);
transport
.key
.allow_auth_channel_mismatch_formats
.as_ref()
.and_then(serde_json::Value::as_array)
.is_some_and(|items| {
items
.iter()
.filter_map(serde_json::Value::as_str)
.any(|item| crate::ai_serving::normalize_api_format_alias(item) == api_format)
})
}
pub(crate) async fn read_candidate_transport_snapshot(
state: PlannerAppState<'_>,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
@@ -285,3 +378,102 @@ pub(crate) async fn read_candidate_transport_snapshot(
}
}
}
#[cfg(test)]
mod tests {
use super::candidate_auth_channel_skip_reason;
use crate::ai_serving::GatewayProviderTransportSnapshot;
use aether_provider_transport::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider,
};
use serde_json::json;
fn sample_transport(auth_type: &str) -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "provider".to_string(),
provider_type: "custom".to_string(),
website: None,
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: false,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("messages".to_string()),
is_active: true,
base_url: "https://example.test".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-1".to_string(),
provider_id: "provider-1".to_string(),
name: "key".to_string(),
auth_type: auth_type.to_string(),
is_active: true,
api_formats: Some(vec!["claude:messages".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
decrypted_api_key: "secret".to_string(),
decrypted_auth_config: None,
},
}
}
#[test]
fn auth_channel_gate_skips_mismatched_raw_secret_auth() {
let transport = sample_transport("bearer");
assert_eq!(
candidate_auth_channel_skip_reason(&transport, Some("api_key")),
Some("auth_channel_mismatch")
);
}
#[test]
fn auth_channel_gate_allows_explicit_mismatch_format() {
let mut transport = sample_transport("bearer");
transport.key.allow_auth_channel_mismatch_formats = Some(json!(["claude:messages"]));
assert_eq!(
candidate_auth_channel_skip_reason(&transport, Some("api_key")),
None
);
}
#[test]
fn auth_channel_gate_treats_cli_oauth_provider_as_bearer_like() {
let mut transport = sample_transport("oauth");
transport.provider.provider_type = "claude_code".to_string();
assert_eq!(
candidate_auth_channel_skip_reason(&transport, Some("bearer_like")),
None
);
assert_eq!(
candidate_auth_channel_skip_reason(&transport, Some("api_key")),
Some("auth_channel_mismatch")
);
}
}

View File

@@ -18,6 +18,7 @@ pub(crate) struct LocalRequestedModelDecisionInput {
pub(crate) requested_model: String,
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
pub(crate) required_capabilities: Option<serde_json::Value>,
pub(crate) request_auth_channel: Option<String>,
}
#[derive(Debug, Clone)]
@@ -93,6 +94,7 @@ pub(crate) fn build_local_requested_model_decision_input(
requested_model,
auth_snapshot: resolved_input.auth_snapshot,
required_capabilities: resolved_input.required_capabilities,
request_auth_channel: None,
}
}

View File

@@ -71,10 +71,9 @@ pub(crate) async fn resolve_local_same_format_provider_decision_input(
}
};
Some(build_local_requested_model_decision_input(
resolved_input,
requested_model,
))
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
input.request_auth_channel = decision.request_auth_channel.clone();
Some(input)
}
pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
@@ -110,6 +109,7 @@ pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
sticky_session_token.as_deref(),
input.request_auth_channel.as_deref(),
persistence_policy,
candidates,
preselection_skipped

View File

@@ -1232,6 +1232,7 @@ mod tests {
is_active: true,
api_formats: Some(vec!["openai:chat".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,

View File

@@ -92,6 +92,7 @@ pub(super) async fn materialize_local_gemini_files_candidate_attempts(
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
None,
None,
persistence_policy,
candidates,
Vec::new(),

View File

@@ -65,10 +65,9 @@ pub(super) async fn resolve_local_openai_image_decision_input(
}
};
Some(build_local_requested_model_decision_input(
resolved_input,
requested_model,
))
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
input.request_auth_channel = decision.request_auth_channel.clone();
Some(input)
}
fn resolve_local_openai_image_auth_context(
@@ -155,6 +154,7 @@ async fn materialize_local_openai_image_candidate_attempts(
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
sticky_session_token.as_deref(),
input.request_auth_channel.as_deref(),
persistence_policy,
candidates,
preselection_skipped,

View File

@@ -73,10 +73,9 @@ pub(super) async fn resolve_local_video_create_decision_input(
}
};
Some(build_local_requested_model_decision_input(
resolved_input,
requested_model,
))
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
input.request_auth_channel = decision.request_auth_channel.clone();
Some(input)
}
fn resolve_local_video_create_auth_context(
@@ -167,6 +166,7 @@ async fn materialize_local_video_create_candidate_attempts(
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
sticky_session_token.as_deref(),
input.request_auth_channel.as_deref(),
persistence_policy,
candidates,
preselection_skipped,

View File

@@ -69,10 +69,9 @@ pub(super) async fn resolve_local_standard_decision_input(
}
};
Some(build_local_requested_model_decision_input(
resolved_input,
requested_model,
))
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
input.request_auth_channel = decision.request_auth_channel.clone();
Some(input)
}
pub(super) async fn materialize_local_standard_candidate_attempts(
@@ -109,6 +108,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
sticky_session_token.as_deref(),
input.request_auth_channel.as_deref(),
persistence_policy,
preselection.candidates,
preselection.skipped_candidates,

View File

@@ -338,6 +338,7 @@ mod tests {
requested_model: "claude-sonnet-4-5".to_string(),
auth_snapshot: sample_auth_snapshot(),
required_capabilities: None,
request_auth_channel: None,
}
}
@@ -398,6 +399,7 @@ mod tests {
"openai:chat".to_string(),
]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,

View File

@@ -60,7 +60,7 @@ fn sample_transport(base_url: &str, api_format: &str) -> GatewayProviderTranspor
is_active: true,
api_formats: Some(vec![api_format.to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,

View File

@@ -135,6 +135,7 @@ pub(crate) async fn materialize_local_openai_chat_candidate_attempts(
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
sticky_session_token.as_deref(),
input.request_auth_channel.as_deref(),
persistence_policy,
candidates,
preselection_skipped,

View File

@@ -104,8 +104,7 @@ pub(crate) async fn resolve_local_openai_chat_decision_input(
}
};
Some(build_local_requested_model_decision_input(
resolved_input,
requested_model,
))
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
input.request_auth_channel = decision.request_auth_channel.clone();
Some(input)
}

View File

@@ -122,10 +122,9 @@ pub(crate) async fn resolve_local_openai_responses_decision_input(
}
};
Some(build_local_requested_model_decision_input(
resolved_input,
requested_model,
))
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
input.request_auth_channel = decision.request_auth_channel.clone();
Some(input)
}
pub(crate) async fn materialize_local_openai_responses_candidate_attempts(
@@ -163,6 +162,7 @@ pub(crate) async fn materialize_local_openai_responses_candidate_attempts(
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
sticky_session_token.as_deref(),
input.request_auth_channel.as_deref(),
persistence_policy,
preselection.candidates,
preselection.skipped_candidates,

View File

@@ -856,6 +856,10 @@ pub(super) fn build_admin_pool_key_payload(
"auth_type_by_format".to_string(),
json!(key.auth_type_by_format),
);
payload.insert(
"allow_auth_channel_mismatch_formats".to_string(),
json!(key.allow_auth_channel_mismatch_formats),
);
payload.insert(
"credential_kind".to_string(),
json!(auth_semantics.credential_kind().as_str()),

View File

@@ -14,6 +14,8 @@ pub(crate) struct AdminProviderKeyCreateRequest {
#[serde(default)]
pub(crate) auth_type_by_format: Option<serde_json::Value>,
#[serde(default)]
pub(crate) allow_auth_channel_mismatch_formats: Option<Option<Vec<String>>>,
#[serde(default)]
pub(crate) auth_config: Option<serde_json::Value>,
pub(crate) name: String,
#[serde(default)]
@@ -53,6 +55,8 @@ pub(crate) struct AdminProviderKeyUpdateRequest {
#[serde(default)]
pub(crate) auth_type_by_format: Option<serde_json::Value>,
#[serde(default)]
pub(crate) allow_auth_channel_mismatch_formats: Option<Vec<String>>,
#[serde(default)]
pub(crate) auth_config: Option<serde_json::Value>,
#[serde(default)]
pub(crate) name: Option<String>,

View File

@@ -1,7 +1,8 @@
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyCreateRequest;
use crate::handlers::admin::provider::write::normalize::{
normalize_api_format_json_object_keys, normalize_api_format_list, normalize_auth_type,
normalize_auth_type_by_format, validate_vertex_api_formats,
normalize_allow_auth_channel_mismatch_formats, normalize_api_format_json_object_keys,
normalize_api_format_list, normalize_auth_type, normalize_auth_type_by_format,
validate_vertex_api_formats,
};
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::{
@@ -192,6 +193,14 @@ pub(crate) async fn build_admin_create_provider_key_record(
key.health_by_format = Some(json!({}));
key.circuit_breaker_by_format = Some(json!({}));
key.auth_type_by_format = auth_type_by_format;
let allow_auth_channel_mismatch_formats = payload
.allow_auth_channel_mismatch_formats
.unwrap_or_else(|| Some(api_formats.clone()));
key.allow_auth_channel_mismatch_formats = normalize_allow_auth_channel_mismatch_formats(
allow_auth_channel_mismatch_formats,
"allow_auth_channel_mismatch_formats",
&api_formats,
)?;
key.created_at_unix_ms = Some(now_unix_secs);
key.updated_at_unix_secs = Some(now_unix_secs);
Ok(key)

View File

@@ -1,7 +1,8 @@
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdatePatch;
use crate::handlers::admin::provider::write::normalize::{
normalize_api_format_json_object_keys, normalize_api_format_list, normalize_auth_type,
normalize_auth_type_by_format, validate_vertex_api_formats,
normalize_allow_auth_channel_mismatch_formats, normalize_api_format_json_object_keys,
normalize_api_format_list, normalize_auth_type, normalize_auth_type_by_format,
validate_vertex_api_formats,
};
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::{
@@ -221,6 +222,32 @@ pub(crate) async fn build_admin_update_provider_key_record(
} else {
updated.auth_type_by_format = None;
}
if fields.contains("allow_auth_channel_mismatch_formats") {
updated.allow_auth_channel_mismatch_formats =
normalize_allow_auth_channel_mismatch_formats(
payload.allow_auth_channel_mismatch_formats,
"allow_auth_channel_mismatch_formats",
&effective_api_formats,
)?;
} else if fields.contains("api_formats") {
let existing = updated
.allow_auth_channel_mismatch_formats
.as_ref()
.and_then(serde_json::Value::as_array)
.map(|items| {
items
.iter()
.filter_map(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
});
updated.allow_auth_channel_mismatch_formats =
normalize_allow_auth_channel_mismatch_formats(
existing,
"allow_auth_channel_mismatch_formats",
&effective_api_formats,
)?;
}
updated.auth_type = target_auth_type;

View File

@@ -81,6 +81,36 @@ pub(crate) fn normalize_auth_type_by_format(
}
}
pub(crate) fn normalize_allow_auth_channel_mismatch_formats(
values: Option<Vec<String>>,
field_name: &str,
api_formats: &[String],
) -> Result<Option<serde_json::Value>, String> {
let Some(values) = values else {
return Ok(None);
};
let allowed = api_formats.iter().cloned().collect::<BTreeSet<_>>();
let mut seen = BTreeSet::new();
let mut normalized = Vec::new();
for value in values {
let canonical = crate::ai_serving::normalize_api_format_alias(&value);
if canonical.is_empty() {
continue;
}
if !allowed.is_empty() && !allowed.contains(&canonical) {
return Err(format!("{field_name} 包含未选择的 API 格式: {canonical}"));
}
if seen.insert(canonical.clone()) {
normalized.push(serde_json::Value::String(canonical));
}
}
if normalized.is_empty() {
Ok(None)
} else {
Ok(Some(serde_json::Value::Array(normalized)))
}
}
pub(crate) fn normalize_auth_type(value: Option<&str>) -> Result<String, String> {
let auth_type = value.unwrap_or("api_key").trim().to_ascii_lowercase();
match auth_type.as_str() {

View File

@@ -120,6 +120,17 @@ pub(crate) async fn build_admin_system_export_providers_payload(
internal_priority: Some(key.internal_priority),
global_priority_by_format: key.global_priority_by_format.clone(),
auth_type_by_format: key.auth_type_by_format.clone(),
allow_auth_channel_mismatch_formats: key
.allow_auth_channel_mismatch_formats
.as_ref()
.and_then(serde_json::Value::as_array)
.map(|items| {
items
.iter()
.filter_map(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
}),
rpm_limit: key.rpm_limit,
allowed_models: key.allowed_models.as_ref().and_then(|value| {
value.as_array().map(|items| {

View File

@@ -1312,6 +1312,10 @@ pub(crate) fn build_admin_provider_key_response(
"auth_type_by_format".to_string(),
json!(key.auth_type_by_format),
);
payload.insert(
"allow_auth_channel_mismatch_formats".to_string(),
json!(key.allow_auth_channel_mismatch_formats),
);
payload.insert(
"credential_kind".to_string(),
json!(auth_semantics.credential_kind().as_str()),

View File

@@ -760,7 +760,7 @@ mod tests {
is_active: true,
api_formats: Some(vec![api_format.to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,

View File

@@ -173,6 +173,7 @@ mod tests {
is_active: true,
api_formats: None,
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,

View File

@@ -305,6 +305,7 @@ mod tests {
is_active: true,
api_formats: None,
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,

View File

@@ -1093,7 +1093,7 @@ async fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_wi
}
fn sample_provider_catalog_key() -> StoredProviderCatalogKey {
StoredProviderCatalogKey::new(
let mut key = StoredProviderCatalogKey::new(
"key-vertex-cli-stream-local-1".to_string(),
"provider-vertex-cli-stream-local-1".to_string(),
"prod".to_string(),
@@ -1114,7 +1114,10 @@ async fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_wi
None,
None,
)
.expect("key transport should build")
.expect("key transport should build");
key.allow_auth_channel_mismatch_formats =
Some(serde_json::json!(["gemini:generate_content"]));
key
}
let seen_execution_runtime = Arc::new(Mutex::new(None::<SeenExecutionRuntimeStreamRequest>));

View File

@@ -1412,7 +1412,7 @@ async fn gateway_executes_vertex_ai_gemini_cli_sync_via_local_decision_gate_with
}
fn sample_provider_catalog_key() -> StoredProviderCatalogKey {
StoredProviderCatalogKey::new(
let mut key = StoredProviderCatalogKey::new(
"key-vertex-cli-local-1".to_string(),
"provider-vertex-cli-local-1".to_string(),
"prod".to_string(),
@@ -1433,7 +1433,10 @@ async fn gateway_executes_vertex_ai_gemini_cli_sync_via_local_decision_gate_with
None,
None,
)
.expect("key transport should build")
.expect("key transport should build");
key.allow_auth_channel_mismatch_formats =
Some(serde_json::json!(["gemini:generate_content"]));
key
}
let seen_execution_runtime = Arc::new(Mutex::new(None::<SeenExecutionRuntimeSyncRequest>));