mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-12 20:19:51 +08:00
feat: configure auth channel mismatch formats
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>));
|
||||
|
||||
@@ -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>));
|
||||
|
||||
@@ -214,6 +214,8 @@ pub struct AdminSystemConfigProviderKey {
|
||||
#[serde(default)]
|
||||
pub auth_type_by_format: Option<Value>,
|
||||
#[serde(default)]
|
||||
pub allow_auth_channel_mismatch_formats: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub rpm_limit: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub allowed_models: Option<Vec<String>>,
|
||||
|
||||
@@ -249,6 +249,7 @@ pub struct StoredProviderCatalogKey {
|
||||
pub is_active: bool,
|
||||
pub api_formats: Option<serde_json::Value>,
|
||||
pub auth_type_by_format: Option<serde_json::Value>,
|
||||
pub allow_auth_channel_mismatch_formats: Option<serde_json::Value>,
|
||||
pub encrypted_api_key: Option<String>,
|
||||
pub encrypted_auth_config: Option<String>,
|
||||
pub note: Option<String>,
|
||||
@@ -323,6 +324,7 @@ impl StoredProviderCatalogKey {
|
||||
is_active,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
encrypted_api_key: None,
|
||||
encrypted_auth_config: None,
|
||||
note: None,
|
||||
|
||||
@@ -470,6 +470,7 @@ CREATE TABLE IF NOT EXISTS public.provider_api_keys (
|
||||
note character varying(500),
|
||||
internal_priority integer DEFAULT 50,
|
||||
rpm_limit integer,
|
||||
concurrent_limit integer,
|
||||
allowed_models json,
|
||||
capabilities json,
|
||||
learned_rpm_limit integer,
|
||||
@@ -497,6 +498,7 @@ CREATE TABLE IF NOT EXISTS public.provider_api_keys (
|
||||
provider_id character varying(36) NOT NULL,
|
||||
api_formats json,
|
||||
auth_type_by_format json,
|
||||
allow_auth_channel_mismatch_formats json,
|
||||
rate_multipliers json,
|
||||
health_by_format jsonb,
|
||||
circuit_breaker_by_format jsonb,
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
ALTER TABLE public.provider_api_keys
|
||||
ADD COLUMN IF NOT EXISTS allow_auth_channel_mismatch_formats json;
|
||||
|
||||
ALTER TABLE public.provider_api_keys
|
||||
ADD COLUMN IF NOT EXISTS concurrent_limit integer;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.aether_default_auth_mismatch_api_format(value text)
|
||||
RETURNS text
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
AS $$
|
||||
SELECT CASE LOWER(BTRIM(COALESCE(value, '')))
|
||||
WHEN 'openai:cli' THEN 'openai:responses'
|
||||
WHEN 'openai:compact' THEN 'openai:responses:compact'
|
||||
WHEN 'claude:chat' THEN 'claude:messages'
|
||||
WHEN 'claude:cli' THEN 'claude:messages'
|
||||
WHEN 'gemini:chat' THEN 'gemini:generate_content'
|
||||
WHEN 'gemini:cli' THEN 'gemini:generate_content'
|
||||
ELSE LOWER(BTRIM(COALESCE(value, '')))
|
||||
END
|
||||
$$;
|
||||
|
||||
WITH supported_formats AS (
|
||||
SELECT
|
||||
pak.id,
|
||||
public.aether_default_auth_mismatch_api_format(format.value) AS api_format,
|
||||
0 AS source_priority,
|
||||
MIN(format.ordinality) AS first_ordinality
|
||||
FROM public.provider_api_keys AS pak
|
||||
CROSS JOIN LATERAL json_array_elements_text(
|
||||
CASE
|
||||
WHEN pak.api_formats IS NOT NULL
|
||||
AND json_typeof(pak.api_formats) = 'array'
|
||||
THEN pak.api_formats
|
||||
ELSE '[]'::json
|
||||
END
|
||||
) WITH ORDINALITY AS format(value, ordinality)
|
||||
WHERE pak.api_formats IS NOT NULL
|
||||
AND json_typeof(pak.api_formats) = 'array'
|
||||
GROUP BY pak.id, api_format
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
pak.id,
|
||||
public.aether_default_auth_mismatch_api_format(endpoint.api_format) AS api_format,
|
||||
1 AS source_priority,
|
||||
0 AS first_ordinality
|
||||
FROM public.provider_api_keys AS pak
|
||||
INNER JOIN public.provider_endpoints AS endpoint
|
||||
ON endpoint.provider_id = pak.provider_id
|
||||
WHERE pak.api_formats IS NULL
|
||||
OR json_typeof(pak.api_formats) <> 'array'
|
||||
),
|
||||
deduplicated_formats AS (
|
||||
SELECT
|
||||
id,
|
||||
api_format,
|
||||
MIN(source_priority) AS source_priority,
|
||||
MIN(first_ordinality) AS first_ordinality
|
||||
FROM supported_formats
|
||||
WHERE api_format <> ''
|
||||
GROUP BY id, api_format
|
||||
),
|
||||
rebuilt AS (
|
||||
SELECT
|
||||
id,
|
||||
json_agg(api_format ORDER BY source_priority, first_ordinality, api_format) AS api_formats
|
||||
FROM deduplicated_formats
|
||||
GROUP BY id
|
||||
)
|
||||
UPDATE public.provider_api_keys AS pak
|
||||
SET
|
||||
allow_auth_channel_mismatch_formats = rebuilt.api_formats,
|
||||
updated_at = NOW()
|
||||
FROM rebuilt
|
||||
WHERE pak.id = rebuilt.id
|
||||
AND pak.allow_auth_channel_mismatch_formats IS NULL;
|
||||
|
||||
DROP FUNCTION public.aether_default_auth_mismatch_api_format(text);
|
||||
@@ -8,7 +8,7 @@ use tracing::{error, info, warn};
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||
static BASELINE_V2_SQL: &str = include_str!("../bootstrap/20260413020000_baseline_v2.sql");
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260428000000;
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260502000000;
|
||||
const MIGRATIONS_TABLE_EXISTS_SQL: &str =
|
||||
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
|
||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||
@@ -665,6 +665,7 @@ SELECT EXISTS (
|
||||
20260423000000,
|
||||
20260424000000,
|
||||
20260428000000,
|
||||
20260502000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -732,7 +733,20 @@ SELECT EXISTS (
|
||||
.sql
|
||||
.contains("api_formats json DEFAULT '[]'::json NOT NULL"));
|
||||
assert!(BASELINE_V2_SQL.contains("api_formats json,"));
|
||||
assert!(BASELINE_V2_SQL.contains("concurrent_limit integer,"));
|
||||
assert!(BASELINE_V2_SQL.contains("allow_auth_channel_mismatch_formats json,"));
|
||||
assert!(!BASELINE_V2_SQL.contains("api_formats json DEFAULT '[]'::json NOT NULL"));
|
||||
|
||||
let auth_mismatch_migration = MIGRATOR
|
||||
.iter()
|
||||
.find(|migration| migration.version == 20260502000000)
|
||||
.expect("auth mismatch migration should be embedded");
|
||||
assert!(auth_mismatch_migration
|
||||
.sql
|
||||
.contains("allow_auth_channel_mismatch_formats = rebuilt.api_formats"));
|
||||
assert!(auth_mismatch_migration
|
||||
.sql
|
||||
.contains("pak.allow_auth_channel_mismatch_formats IS NULL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1256,6 +1270,7 @@ ORDER BY id
|
||||
20260423000000,
|
||||
20260424000000,
|
||||
20260428000000,
|
||||
20260502000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ SELECT
|
||||
is_active,
|
||||
api_formats,
|
||||
auth_type_by_format,
|
||||
allow_auth_channel_mismatch_formats,
|
||||
api_key,
|
||||
auth_config,
|
||||
note,
|
||||
@@ -198,6 +199,7 @@ SELECT
|
||||
is_active,
|
||||
api_formats,
|
||||
auth_type_by_format,
|
||||
allow_auth_channel_mismatch_formats,
|
||||
api_key,
|
||||
auth_config,
|
||||
note,
|
||||
@@ -1234,7 +1236,8 @@ INSERT INTO provider_api_keys (
|
||||
circuit_breaker_by_format,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
updated_at,
|
||||
allow_auth_channel_mismatch_formats
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
@@ -1310,7 +1313,8 @@ INSERT INTO provider_api_keys (
|
||||
CASE
|
||||
WHEN $51::double precision IS NULL THEN NOW()
|
||||
ELSE TO_TIMESTAMP($51::double precision)
|
||||
END
|
||||
END,
|
||||
$52
|
||||
)
|
||||
"#,
|
||||
)
|
||||
@@ -1373,7 +1377,7 @@ INSERT INTO provider_api_keys (
|
||||
.bind(key.is_active)
|
||||
.bind(key.created_at_unix_ms.map(|value| value as f64))
|
||||
.bind(key.updated_at_unix_secs.map(|value| value as f64))
|
||||
.bind(key.expires_at_unix_secs.map(|value| value as f64))
|
||||
.bind(&key.allow_auth_channel_mismatch_formats)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
@@ -1723,6 +1727,7 @@ SET
|
||||
provider_id = $2,
|
||||
api_formats = $3,
|
||||
auth_type_by_format = $39,
|
||||
allow_auth_channel_mismatch_formats = $40,
|
||||
auth_type = $4,
|
||||
api_key = $5,
|
||||
auth_config = $6,
|
||||
@@ -1818,6 +1823,7 @@ WHERE id = $1
|
||||
.bind(key.updated_at_unix_secs.map(|value| value as f64))
|
||||
.bind(key.expires_at_unix_secs.map(|value| value as f64))
|
||||
.bind(&key.auth_type_by_format)
|
||||
.bind(&key.allow_auth_channel_mismatch_formats)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?
|
||||
@@ -2479,6 +2485,8 @@ fn map_key_row(row: &PgRow) -> Result<StoredProviderCatalogKey, DataLayerError>
|
||||
);
|
||||
key.note = row.try_get("note").ok();
|
||||
key.auth_type_by_format = row.try_get("auth_type_by_format").ok();
|
||||
key.allow_auth_channel_mismatch_formats =
|
||||
row.try_get("allow_auth_channel_mismatch_formats").ok();
|
||||
key.internal_priority = row.try_get("internal_priority").unwrap_or(50);
|
||||
key.cache_ttl_minutes = row.try_get("cache_ttl_minutes").unwrap_or(5);
|
||||
key.max_probe_interval_minutes = row.try_get("max_probe_interval_minutes").unwrap_or(32);
|
||||
|
||||
@@ -1173,6 +1173,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -588,6 +588,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,
|
||||
|
||||
@@ -361,6 +361,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,
|
||||
|
||||
@@ -81,6 +81,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,
|
||||
|
||||
@@ -120,6 +120,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,
|
||||
|
||||
@@ -397,6 +397,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,
|
||||
|
||||
@@ -299,6 +299,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,
|
||||
rate_multipliers: None,
|
||||
@@ -356,6 +357,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,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -177,6 +177,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,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -199,6 +199,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,
|
||||
|
||||
@@ -137,6 +137,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,
|
||||
|
||||
@@ -233,6 +233,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,
|
||||
|
||||
@@ -299,6 +299,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,
|
||||
|
||||
@@ -672,6 +672,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,
|
||||
|
||||
@@ -116,6 +116,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,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -407,6 +407,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,
|
||||
|
||||
@@ -403,6 +403,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,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -62,6 +62,7 @@ pub struct GatewayProviderTransportKey {
|
||||
pub is_active: bool,
|
||||
pub api_formats: Option<Vec<String>>,
|
||||
pub auth_type_by_format: Option<serde_json::Value>,
|
||||
pub allow_auth_channel_mismatch_formats: Option<serde_json::Value>,
|
||||
pub allowed_models: Option<Vec<String>>,
|
||||
pub capabilities: Option<serde_json::Value>,
|
||||
pub rate_multipliers: Option<serde_json::Value>,
|
||||
@@ -388,6 +389,7 @@ mod tests {
|
||||
"openai:responses".to_string(),
|
||||
]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: Some(vec!["gpt-4.1".to_string(), "gpt-4.1-mini".to_string(),]),
|
||||
capabilities: Some(serde_json::json!({"cache_1h": true})),
|
||||
|
||||
@@ -95,6 +95,9 @@ pub(super) fn map_key(
|
||||
"provider_api_keys.api_formats",
|
||||
)?,
|
||||
auth_type_by_format: normalize_optional_json(key.auth_type_by_format),
|
||||
allow_auth_channel_mismatch_formats: normalize_optional_json(
|
||||
key.allow_auth_channel_mismatch_formats,
|
||||
),
|
||||
allowed_models: normalize_string_list(
|
||||
normalize_optional_json(key.allowed_models),
|
||||
"provider_api_keys.allowed_models",
|
||||
|
||||
@@ -283,6 +283,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,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -89,6 +89,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -112,6 +112,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -204,6 +204,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -297,6 +297,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,
|
||||
|
||||
@@ -174,6 +174,7 @@ export interface ProviderKeyExport {
|
||||
internal_priority?: number
|
||||
global_priority_by_format?: Record<string, number> | null
|
||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
||||
allow_auth_channel_mismatch_formats?: string[] | null
|
||||
rpm_limit?: number | null
|
||||
allowed_models?: string[] | null
|
||||
capabilities?: Record<string, boolean>
|
||||
|
||||
@@ -140,6 +140,7 @@ export async function addProviderKey(
|
||||
api_key: string
|
||||
auth_type?: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
||||
allow_auth_channel_mismatch_formats?: string[] | null
|
||||
auth_config?: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||
name: string
|
||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
||||
@@ -169,6 +170,7 @@ export async function updateProviderKey(
|
||||
api_key: string
|
||||
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
||||
auth_type_by_format: Record<string, 'api_key' | 'bearer'> | null
|
||||
allow_auth_channel_mismatch_formats: string[] | null
|
||||
auth_config: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||
name: string
|
||||
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface PoolKeyDetail {
|
||||
is_active: boolean
|
||||
auth_type: string
|
||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
||||
allow_auth_channel_mismatch_formats?: string[] | null
|
||||
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
||||
oauth_managed?: boolean
|
||||
@@ -212,6 +213,7 @@ export interface PoolKeySelectionItem {
|
||||
key_name: string
|
||||
auth_type: string
|
||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
||||
allow_auth_channel_mismatch_formats?: string[] | null
|
||||
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
||||
oauth_managed?: boolean
|
||||
|
||||
@@ -231,6 +231,7 @@ export interface EndpointAPIKey {
|
||||
api_key_plain?: string | null
|
||||
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型(必返回)
|
||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
||||
allow_auth_channel_mismatch_formats?: string[] | null
|
||||
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
||||
oauth_managed?: boolean
|
||||
@@ -388,6 +389,7 @@ export interface EndpointAPIKeyUpdate {
|
||||
api_key?: string // 仅在需要更新时提供
|
||||
auth_type?: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
||||
allow_auth_channel_mismatch_formats?: string[] | null
|
||||
auth_config?: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
||||
internal_priority?: number
|
||||
|
||||
@@ -163,6 +163,18 @@
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="canToggleAuthChannelMismatch(format)"
|
||||
class="flex items-center gap-1"
|
||||
title="允许客户端认证方式不一致时使用"
|
||||
@click.stop
|
||||
>
|
||||
<Switch
|
||||
:model-value="isAuthChannelMismatchAllowed(format)"
|
||||
class="scale-75"
|
||||
@update:model-value="(value) => setAuthChannelMismatchAllowed(format, value)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center text-xs text-muted-foreground gap-1"
|
||||
@click.stop
|
||||
@@ -478,6 +490,25 @@ function sanitizeAuthTypeByFormat(
|
||||
return sanitized
|
||||
}
|
||||
|
||||
function sanitizeAllowAuthChannelMismatchFormats(
|
||||
formats: string[] | null | undefined,
|
||||
selectedFormats = form.value.api_formats
|
||||
): string[] {
|
||||
if (!formats) return []
|
||||
const selected = new Set(selectedFormats.map(normalizeApiFormat))
|
||||
const seen = new Set<string>()
|
||||
const sanitized: string[] = []
|
||||
for (const format of formats) {
|
||||
const normalizedFormat = normalizeApiFormat(format)
|
||||
if (!normalizedFormat || !selected.has(normalizedFormat) || seen.has(normalizedFormat)) {
|
||||
continue
|
||||
}
|
||||
seen.add(normalizedFormat)
|
||||
sanitized.push(normalizedFormat)
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
function getDefaultApiFormats(): string[] {
|
||||
const endpointFormat = props.endpoint?.api_format
|
||||
if (endpointFormat) {
|
||||
@@ -549,12 +580,35 @@ function setFormatAuthType(format: string, authType: RawSecretAuthType) {
|
||||
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(next)
|
||||
}
|
||||
|
||||
function canToggleAuthChannelMismatch(format: string): boolean {
|
||||
return canOverrideFormatAuth(format)
|
||||
}
|
||||
|
||||
function isAuthChannelMismatchAllowed(format: string): boolean {
|
||||
return form.value.allow_auth_channel_mismatch_formats.includes(normalizeApiFormat(format))
|
||||
}
|
||||
|
||||
function setAuthChannelMismatchAllowed(format: string, allowed: boolean) {
|
||||
const normalizedFormat = normalizeApiFormat(format)
|
||||
const next = new Set(form.value.allow_auth_channel_mismatch_formats.map(normalizeApiFormat))
|
||||
if (allowed) {
|
||||
next.add(normalizedFormat)
|
||||
} else {
|
||||
next.delete(normalizedFormat)
|
||||
}
|
||||
form.value.allow_auth_channel_mismatch_formats = sanitizeAllowAuthChannelMismatchFormats([...next])
|
||||
}
|
||||
|
||||
function buildAuthTypeByFormatPayload(): Record<string, RawSecretAuthType> | null {
|
||||
const sanitized = sanitizeAuthTypeByFormat(form.value.auth_type_by_format)
|
||||
return Object.keys(sanitized).length > 0 ? sanitized : null
|
||||
}
|
||||
|
||||
function buildAllowAuthChannelMismatchFormatsPayload(): string[] {
|
||||
const sanitized = sanitizeAllowAuthChannelMismatchFormats(form.value.allow_auth_channel_mismatch_formats)
|
||||
return sanitized
|
||||
}
|
||||
|
||||
const serviceAccountDescription = computed(() => (
|
||||
props.editingKey
|
||||
? '留空表示不修改;JSON 格式,包含 project_id、private_key 等字段'
|
||||
@@ -566,6 +620,10 @@ function getDefaultAuthType(): ProviderKeyFormAuthType {
|
||||
return authTypeOptions.value[0]?.value || 'api_key'
|
||||
}
|
||||
|
||||
function getDefaultAllowAuthChannelMismatchFormats(formats = getDefaultApiFormats()): string[] {
|
||||
return sanitizeAllowAuthChannelMismatchFormats(formats, formats)
|
||||
}
|
||||
|
||||
// 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models
|
||||
const showAutoFetchWarning = computed(() => {
|
||||
if (!props.editingKey) return false
|
||||
@@ -632,6 +690,7 @@ const form = ref({
|
||||
api_key: '', // 标准 API Key
|
||||
auth_type: 'api_key' as ProviderKeyFormAuthType, // 认证类型
|
||||
auth_type_by_format: {} as Record<string, RawSecretAuthType>,
|
||||
allow_auth_channel_mismatch_formats: [] as string[],
|
||||
auth_config_text: '', // Service Account JSON 文本(用于表单输入)
|
||||
api_formats: [] as string[], // 支持的 API 格式列表
|
||||
rate_multipliers: {} as Record<string, number>, // 按 API 格式的成本倍率
|
||||
@@ -661,6 +720,9 @@ watch(
|
||||
form.value.api_formats = [...filtered]
|
||||
}
|
||||
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(form.value.auth_type_by_format)
|
||||
form.value.allow_auth_channel_mismatch_formats = sanitizeAllowAuthChannelMismatchFormats(
|
||||
form.value.allow_auth_channel_mismatch_formats
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -676,6 +738,10 @@ watch(
|
||||
if (filtered.length !== form.value.api_formats.length) {
|
||||
form.value.api_formats = [...filtered]
|
||||
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(form.value.auth_type_by_format, filtered)
|
||||
form.value.allow_auth_channel_mismatch_formats = sanitizeAllowAuthChannelMismatchFormats(
|
||||
form.value.allow_auth_channel_mismatch_formats,
|
||||
filtered
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -683,6 +749,8 @@ watch(
|
||||
const defaults = getDefaultApiFormats()
|
||||
if (defaults.length > 0) {
|
||||
form.value.api_formats = defaults
|
||||
form.value.allow_auth_channel_mismatch_formats =
|
||||
getDefaultAllowAuthChannelMismatchFormats(defaults)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -708,10 +776,14 @@ function toggleApiFormat(format: string) {
|
||||
if (index === -1) {
|
||||
// 添加格式
|
||||
form.value.api_formats.push(format)
|
||||
setAuthChannelMismatchAllowed(format, true)
|
||||
} else {
|
||||
// 移除格式,但保留倍率配置(用户可能只是临时取消)
|
||||
form.value.api_formats.splice(index, 1)
|
||||
}
|
||||
form.value.allow_auth_channel_mismatch_formats = sanitizeAllowAuthChannelMismatchFormats(
|
||||
form.value.allow_auth_channel_mismatch_formats
|
||||
)
|
||||
}
|
||||
|
||||
// 更新指定格式的成本倍率
|
||||
@@ -738,13 +810,16 @@ function updateRateMultiplier(format: string, value: string | number) {
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
formNonce.value = createFieldNonce()
|
||||
const defaultApiFormats = getDefaultApiFormats()
|
||||
form.value = {
|
||||
name: '',
|
||||
api_key: '',
|
||||
auth_type: getDefaultAuthType(),
|
||||
auth_type_by_format: {},
|
||||
allow_auth_channel_mismatch_formats:
|
||||
getDefaultAllowAuthChannelMismatchFormats(defaultApiFormats),
|
||||
auth_config_text: '',
|
||||
api_formats: getDefaultApiFormats(),
|
||||
api_formats: defaultApiFormats,
|
||||
rate_multipliers: {},
|
||||
internal_priority: 10,
|
||||
rpm_limit: undefined,
|
||||
@@ -766,6 +841,9 @@ function clearForNextAdd() {
|
||||
form.value.api_key = ''
|
||||
form.value.auth_config_text = ''
|
||||
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(form.value.auth_type_by_format)
|
||||
form.value.allow_auth_channel_mismatch_formats = sanitizeAllowAuthChannelMismatchFormats(
|
||||
form.value.allow_auth_channel_mismatch_formats
|
||||
)
|
||||
}
|
||||
|
||||
// 加载密钥数据(编辑模式)
|
||||
@@ -781,6 +859,10 @@ function loadKeyData() {
|
||||
props.editingKey.api_formats || [],
|
||||
normalizeFormAuthType(props.editingKey.auth_type)
|
||||
),
|
||||
allow_auth_channel_mismatch_formats: sanitizeAllowAuthChannelMismatchFormats(
|
||||
props.editingKey.allow_auth_channel_mismatch_formats || [],
|
||||
props.editingKey.api_formats || []
|
||||
),
|
||||
auth_config_text: '', // auth_config 不返回给前端,编辑时需要重新输入
|
||||
api_formats: props.editingKey.api_formats?.length > 0
|
||||
? sanitizeApiFormats(
|
||||
@@ -873,6 +955,9 @@ async function handleSave() {
|
||||
}
|
||||
|
||||
form.value.api_formats = sanitizeApiFormats(form.value.api_formats)
|
||||
form.value.allow_auth_channel_mismatch_formats = sanitizeAllowAuthChannelMismatchFormats(
|
||||
form.value.allow_auth_channel_mismatch_formats
|
||||
)
|
||||
|
||||
// 验证至少选择一个 API 格式
|
||||
if (form.value.api_formats.length === 0) {
|
||||
@@ -905,6 +990,7 @@ async function handleSave() {
|
||||
// 准备认证相关数据
|
||||
const authConfig = parseAuthConfig()
|
||||
const authTypeByFormat = buildAuthTypeByFormatPayload()
|
||||
const allowAuthChannelMismatchFormats = buildAllowAuthChannelMismatchFormatsPayload()
|
||||
|
||||
if (props.editingKey) {
|
||||
const shouldClearAllowedModels = !!props.editingKey.auto_fetch_models && !form.value.auto_fetch_models
|
||||
@@ -916,6 +1002,7 @@ async function handleSave() {
|
||||
name: form.value.name,
|
||||
auth_type: form.value.auth_type,
|
||||
auth_type_by_format: authTypeByFormat,
|
||||
allow_auth_channel_mismatch_formats: allowAuthChannelMismatchFormats,
|
||||
rate_multipliers: rateMultipliersData,
|
||||
internal_priority: form.value.internal_priority,
|
||||
rpm_limit: form.value.rpm_limit,
|
||||
@@ -947,6 +1034,7 @@ async function handleSave() {
|
||||
api_key: form.value.api_key,
|
||||
auth_type: form.value.auth_type,
|
||||
auth_type_by_format: authTypeByFormat,
|
||||
allow_auth_channel_mismatch_formats: allowAuthChannelMismatchFormats,
|
||||
auth_config: authConfig || undefined,
|
||||
name: form.value.name,
|
||||
rate_multipliers: rateMultipliersData,
|
||||
|
||||
Reference in New Issue
Block a user