mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +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>,
|
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
|
||||||
required_capabilities: Option<&'a Value>,
|
required_capabilities: Option<&'a Value>,
|
||||||
sticky_session_token: Option<&'a str>,
|
sticky_session_token: Option<&'a str>,
|
||||||
|
request_auth_channel: Option<&'a str>,
|
||||||
persistence_policy: LocalCandidatePersistencePolicy<'a>,
|
persistence_policy: LocalCandidatePersistencePolicy<'a>,
|
||||||
resolution_mode: LocalCandidateResolutionMode,
|
resolution_mode: LocalCandidateResolutionMode,
|
||||||
build_available_extra_data: F,
|
build_available_extra_data: F,
|
||||||
@@ -123,6 +124,7 @@ where
|
|||||||
self.auth_snapshot,
|
self.auth_snapshot,
|
||||||
self.required_capabilities,
|
self.required_capabilities,
|
||||||
self.sticky_session_token,
|
self.sticky_session_token,
|
||||||
|
self.request_auth_channel,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -135,6 +137,7 @@ where
|
|||||||
self.auth_snapshot,
|
self.auth_snapshot,
|
||||||
self.required_capabilities,
|
self.required_capabilities,
|
||||||
self.sticky_session_token,
|
self.sticky_session_token,
|
||||||
|
self.request_auth_channel,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -314,6 +317,7 @@ pub(crate) async fn materialize_local_execution_candidates_with_serving<F, G>(
|
|||||||
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
||||||
required_capabilities: Option<&Value>,
|
required_capabilities: Option<&Value>,
|
||||||
sticky_session_token: Option<&str>,
|
sticky_session_token: Option<&str>,
|
||||||
|
request_auth_channel: Option<&str>,
|
||||||
persistence_policy: LocalCandidatePersistencePolicy<'_>,
|
persistence_policy: LocalCandidatePersistencePolicy<'_>,
|
||||||
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
|
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
|
||||||
preselection_skipped: Vec<SkippedLocalExecutionCandidate>,
|
preselection_skipped: Vec<SkippedLocalExecutionCandidate>,
|
||||||
@@ -333,6 +337,7 @@ where
|
|||||||
auth_snapshot,
|
auth_snapshot,
|
||||||
required_capabilities,
|
required_capabilities,
|
||||||
sticky_session_token,
|
sticky_session_token,
|
||||||
|
request_auth_channel,
|
||||||
persistence_policy,
|
persistence_policy,
|
||||||
resolution_mode,
|
resolution_mode,
|
||||||
build_available_extra_data,
|
build_available_extra_data,
|
||||||
@@ -682,6 +687,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
@@ -240,6 +241,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -1048,6 +1048,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1125,6 +1126,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1198,6 +1200,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1262,6 +1265,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1342,6 +1346,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1413,6 +1418,7 @@ mod tests {
|
|||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1488,6 +1494,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1580,6 +1587,7 @@ mod tests {
|
|||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1680,6 +1688,7 @@ mod tests {
|
|||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1781,6 +1790,7 @@ mod tests {
|
|||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1882,6 +1892,7 @@ mod tests {
|
|||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ struct GatewayLocalCandidateResolutionPort<'a> {
|
|||||||
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
|
||||||
required_capabilities: Option<&'a serde_json::Value>,
|
required_capabilities: Option<&'a serde_json::Value>,
|
||||||
sticky_session_token: Option<&'a str>,
|
sticky_session_token: Option<&'a str>,
|
||||||
|
request_auth_channel: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -86,6 +87,11 @@ impl AiCandidateResolutionPort for GatewayLocalCandidateResolutionPort<'_> {
|
|||||||
transport: &Self::Transport,
|
transport: &Self::Transport,
|
||||||
requested_model: Option<&str>,
|
requested_model: Option<&str>,
|
||||||
) -> Option<&'static 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(
|
candidate_common_transport_skip_reason(
|
||||||
transport,
|
transport,
|
||||||
candidate_transport_policy_facts(candidate),
|
candidate_transport_policy_facts(candidate),
|
||||||
@@ -169,6 +175,7 @@ pub(crate) async fn resolve_and_rank_local_execution_candidates(
|
|||||||
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
||||||
required_capabilities: Option<&serde_json::Value>,
|
required_capabilities: Option<&serde_json::Value>,
|
||||||
sticky_session_token: Option<&str>,
|
sticky_session_token: Option<&str>,
|
||||||
|
request_auth_channel: Option<&str>,
|
||||||
) -> (
|
) -> (
|
||||||
Vec<EligibleLocalExecutionCandidate>,
|
Vec<EligibleLocalExecutionCandidate>,
|
||||||
Vec<SkippedLocalExecutionCandidate>,
|
Vec<SkippedLocalExecutionCandidate>,
|
||||||
@@ -182,6 +189,7 @@ pub(crate) async fn resolve_and_rank_local_execution_candidates(
|
|||||||
auth_snapshot,
|
auth_snapshot,
|
||||||
required_capabilities,
|
required_capabilities,
|
||||||
sticky_session_token,
|
sticky_session_token,
|
||||||
|
request_auth_channel,
|
||||||
AiCandidateResolutionMode::Standard,
|
AiCandidateResolutionMode::Standard,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -195,6 +203,7 @@ pub(crate) async fn resolve_and_rank_local_execution_candidates_without_transpor
|
|||||||
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
||||||
required_capabilities: Option<&serde_json::Value>,
|
required_capabilities: Option<&serde_json::Value>,
|
||||||
sticky_session_token: Option<&str>,
|
sticky_session_token: Option<&str>,
|
||||||
|
request_auth_channel: Option<&str>,
|
||||||
) -> (
|
) -> (
|
||||||
Vec<EligibleLocalExecutionCandidate>,
|
Vec<EligibleLocalExecutionCandidate>,
|
||||||
Vec<SkippedLocalExecutionCandidate>,
|
Vec<SkippedLocalExecutionCandidate>,
|
||||||
@@ -208,6 +217,7 @@ pub(crate) async fn resolve_and_rank_local_execution_candidates_without_transpor
|
|||||||
auth_snapshot,
|
auth_snapshot,
|
||||||
required_capabilities,
|
required_capabilities,
|
||||||
sticky_session_token,
|
sticky_session_token,
|
||||||
|
request_auth_channel,
|
||||||
AiCandidateResolutionMode::WithoutTransportPairGate,
|
AiCandidateResolutionMode::WithoutTransportPairGate,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -221,6 +231,7 @@ async fn resolve_and_rank_local_execution_candidates_with_mode(
|
|||||||
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
||||||
required_capabilities: Option<&serde_json::Value>,
|
required_capabilities: Option<&serde_json::Value>,
|
||||||
sticky_session_token: Option<&str>,
|
sticky_session_token: Option<&str>,
|
||||||
|
request_auth_channel: Option<&str>,
|
||||||
mode: AiCandidateResolutionMode,
|
mode: AiCandidateResolutionMode,
|
||||||
) -> (
|
) -> (
|
||||||
Vec<EligibleLocalExecutionCandidate>,
|
Vec<EligibleLocalExecutionCandidate>,
|
||||||
@@ -232,6 +243,7 @@ async fn resolve_and_rank_local_execution_candidates_with_mode(
|
|||||||
auth_snapshot,
|
auth_snapshot,
|
||||||
required_capabilities,
|
required_capabilities,
|
||||||
sticky_session_token,
|
sticky_session_token,
|
||||||
|
request_auth_channel,
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = AiCandidateResolutionRequest {
|
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(
|
pub(crate) async fn read_candidate_transport_snapshot(
|
||||||
state: PlannerAppState<'_>,
|
state: PlannerAppState<'_>,
|
||||||
candidate: &SchedulerMinimalCandidateSelectionCandidate,
|
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) requested_model: String,
|
||||||
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
|
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
|
||||||
pub(crate) required_capabilities: Option<serde_json::Value>,
|
pub(crate) required_capabilities: Option<serde_json::Value>,
|
||||||
|
pub(crate) request_auth_channel: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -93,6 +94,7 @@ pub(crate) fn build_local_requested_model_decision_input(
|
|||||||
requested_model,
|
requested_model,
|
||||||
auth_snapshot: resolved_input.auth_snapshot,
|
auth_snapshot: resolved_input.auth_snapshot,
|
||||||
required_capabilities: resolved_input.required_capabilities,
|
required_capabilities: resolved_input.required_capabilities,
|
||||||
|
request_auth_channel: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,10 +71,9 @@ pub(crate) async fn resolve_local_same_format_provider_decision_input(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(build_local_requested_model_decision_input(
|
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
|
||||||
resolved_input,
|
input.request_auth_channel = decision.request_auth_channel.clone();
|
||||||
requested_model,
|
Some(input)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
|
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),
|
Some(&input.auth_snapshot),
|
||||||
input.required_capabilities.as_ref(),
|
input.required_capabilities.as_ref(),
|
||||||
sticky_session_token.as_deref(),
|
sticky_session_token.as_deref(),
|
||||||
|
input.request_auth_channel.as_deref(),
|
||||||
persistence_policy,
|
persistence_policy,
|
||||||
candidates,
|
candidates,
|
||||||
preselection_skipped
|
preselection_skipped
|
||||||
|
|||||||
@@ -1232,6 +1232,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ pub(super) async fn materialize_local_gemini_files_candidate_attempts(
|
|||||||
Some(&input.auth_snapshot),
|
Some(&input.auth_snapshot),
|
||||||
input.required_capabilities.as_ref(),
|
input.required_capabilities.as_ref(),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
persistence_policy,
|
persistence_policy,
|
||||||
candidates,
|
candidates,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|
|||||||
@@ -65,10 +65,9 @@ pub(super) async fn resolve_local_openai_image_decision_input(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(build_local_requested_model_decision_input(
|
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
|
||||||
resolved_input,
|
input.request_auth_channel = decision.request_auth_channel.clone();
|
||||||
requested_model,
|
Some(input)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_local_openai_image_auth_context(
|
fn resolve_local_openai_image_auth_context(
|
||||||
@@ -155,6 +154,7 @@ async fn materialize_local_openai_image_candidate_attempts(
|
|||||||
Some(&input.auth_snapshot),
|
Some(&input.auth_snapshot),
|
||||||
input.required_capabilities.as_ref(),
|
input.required_capabilities.as_ref(),
|
||||||
sticky_session_token.as_deref(),
|
sticky_session_token.as_deref(),
|
||||||
|
input.request_auth_channel.as_deref(),
|
||||||
persistence_policy,
|
persistence_policy,
|
||||||
candidates,
|
candidates,
|
||||||
preselection_skipped,
|
preselection_skipped,
|
||||||
|
|||||||
@@ -73,10 +73,9 @@ pub(super) async fn resolve_local_video_create_decision_input(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(build_local_requested_model_decision_input(
|
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
|
||||||
resolved_input,
|
input.request_auth_channel = decision.request_auth_channel.clone();
|
||||||
requested_model,
|
Some(input)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_local_video_create_auth_context(
|
fn resolve_local_video_create_auth_context(
|
||||||
@@ -167,6 +166,7 @@ async fn materialize_local_video_create_candidate_attempts(
|
|||||||
Some(&input.auth_snapshot),
|
Some(&input.auth_snapshot),
|
||||||
input.required_capabilities.as_ref(),
|
input.required_capabilities.as_ref(),
|
||||||
sticky_session_token.as_deref(),
|
sticky_session_token.as_deref(),
|
||||||
|
input.request_auth_channel.as_deref(),
|
||||||
persistence_policy,
|
persistence_policy,
|
||||||
candidates,
|
candidates,
|
||||||
preselection_skipped,
|
preselection_skipped,
|
||||||
|
|||||||
@@ -69,10 +69,9 @@ pub(super) async fn resolve_local_standard_decision_input(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(build_local_requested_model_decision_input(
|
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
|
||||||
resolved_input,
|
input.request_auth_channel = decision.request_auth_channel.clone();
|
||||||
requested_model,
|
Some(input)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn materialize_local_standard_candidate_attempts(
|
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),
|
Some(&input.auth_snapshot),
|
||||||
input.required_capabilities.as_ref(),
|
input.required_capabilities.as_ref(),
|
||||||
sticky_session_token.as_deref(),
|
sticky_session_token.as_deref(),
|
||||||
|
input.request_auth_channel.as_deref(),
|
||||||
persistence_policy,
|
persistence_policy,
|
||||||
preselection.candidates,
|
preselection.candidates,
|
||||||
preselection.skipped_candidates,
|
preselection.skipped_candidates,
|
||||||
|
|||||||
@@ -338,6 +338,7 @@ mod tests {
|
|||||||
requested_model: "claude-sonnet-4-5".to_string(),
|
requested_model: "claude-sonnet-4-5".to_string(),
|
||||||
auth_snapshot: sample_auth_snapshot(),
|
auth_snapshot: sample_auth_snapshot(),
|
||||||
required_capabilities: None,
|
required_capabilities: None,
|
||||||
|
request_auth_channel: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,6 +399,7 @@ mod tests {
|
|||||||
"openai:chat".to_string(),
|
"openai:chat".to_string(),
|
||||||
]),
|
]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ fn sample_transport(base_url: &str, api_format: &str) -> GatewayProviderTranspor
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec![api_format.to_string()]),
|
api_formats: Some(vec![api_format.to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ pub(crate) async fn materialize_local_openai_chat_candidate_attempts(
|
|||||||
Some(&input.auth_snapshot),
|
Some(&input.auth_snapshot),
|
||||||
input.required_capabilities.as_ref(),
|
input.required_capabilities.as_ref(),
|
||||||
sticky_session_token.as_deref(),
|
sticky_session_token.as_deref(),
|
||||||
|
input.request_auth_channel.as_deref(),
|
||||||
persistence_policy,
|
persistence_policy,
|
||||||
candidates,
|
candidates,
|
||||||
preselection_skipped,
|
preselection_skipped,
|
||||||
|
|||||||
@@ -104,8 +104,7 @@ pub(crate) async fn resolve_local_openai_chat_decision_input(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(build_local_requested_model_decision_input(
|
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
|
||||||
resolved_input,
|
input.request_auth_channel = decision.request_auth_channel.clone();
|
||||||
requested_model,
|
Some(input)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,10 +122,9 @@ pub(crate) async fn resolve_local_openai_responses_decision_input(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(build_local_requested_model_decision_input(
|
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
|
||||||
resolved_input,
|
input.request_auth_channel = decision.request_auth_channel.clone();
|
||||||
requested_model,
|
Some(input)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn materialize_local_openai_responses_candidate_attempts(
|
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),
|
Some(&input.auth_snapshot),
|
||||||
input.required_capabilities.as_ref(),
|
input.required_capabilities.as_ref(),
|
||||||
sticky_session_token.as_deref(),
|
sticky_session_token.as_deref(),
|
||||||
|
input.request_auth_channel.as_deref(),
|
||||||
persistence_policy,
|
persistence_policy,
|
||||||
preselection.candidates,
|
preselection.candidates,
|
||||||
preselection.skipped_candidates,
|
preselection.skipped_candidates,
|
||||||
|
|||||||
@@ -856,6 +856,10 @@ pub(super) fn build_admin_pool_key_payload(
|
|||||||
"auth_type_by_format".to_string(),
|
"auth_type_by_format".to_string(),
|
||||||
json!(key.auth_type_by_format),
|
json!(key.auth_type_by_format),
|
||||||
);
|
);
|
||||||
|
payload.insert(
|
||||||
|
"allow_auth_channel_mismatch_formats".to_string(),
|
||||||
|
json!(key.allow_auth_channel_mismatch_formats),
|
||||||
|
);
|
||||||
payload.insert(
|
payload.insert(
|
||||||
"credential_kind".to_string(),
|
"credential_kind".to_string(),
|
||||||
json!(auth_semantics.credential_kind().as_str()),
|
json!(auth_semantics.credential_kind().as_str()),
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ pub(crate) struct AdminProviderKeyCreateRequest {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) auth_type_by_format: Option<serde_json::Value>,
|
pub(crate) auth_type_by_format: Option<serde_json::Value>,
|
||||||
#[serde(default)]
|
#[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) auth_config: Option<serde_json::Value>,
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -53,6 +55,8 @@ pub(crate) struct AdminProviderKeyUpdateRequest {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) auth_type_by_format: Option<serde_json::Value>,
|
pub(crate) auth_type_by_format: Option<serde_json::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub(crate) allow_auth_channel_mismatch_formats: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
pub(crate) auth_config: Option<serde_json::Value>,
|
pub(crate) auth_config: Option<serde_json::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) name: Option<String>,
|
pub(crate) name: Option<String>,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyCreateRequest;
|
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyCreateRequest;
|
||||||
use crate::handlers::admin::provider::write::normalize::{
|
use crate::handlers::admin::provider::write::normalize::{
|
||||||
normalize_api_format_json_object_keys, normalize_api_format_list, normalize_auth_type,
|
normalize_allow_auth_channel_mismatch_formats, normalize_api_format_json_object_keys,
|
||||||
normalize_auth_type_by_format, validate_vertex_api_formats,
|
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::request::AdminAppState;
|
||||||
use crate::handlers::admin::shared::{
|
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.health_by_format = Some(json!({}));
|
||||||
key.circuit_breaker_by_format = Some(json!({}));
|
key.circuit_breaker_by_format = Some(json!({}));
|
||||||
key.auth_type_by_format = auth_type_by_format;
|
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.created_at_unix_ms = Some(now_unix_secs);
|
||||||
key.updated_at_unix_secs = Some(now_unix_secs);
|
key.updated_at_unix_secs = Some(now_unix_secs);
|
||||||
Ok(key)
|
Ok(key)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdatePatch;
|
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdatePatch;
|
||||||
use crate::handlers::admin::provider::write::normalize::{
|
use crate::handlers::admin::provider::write::normalize::{
|
||||||
normalize_api_format_json_object_keys, normalize_api_format_list, normalize_auth_type,
|
normalize_allow_auth_channel_mismatch_formats, normalize_api_format_json_object_keys,
|
||||||
normalize_auth_type_by_format, validate_vertex_api_formats,
|
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::request::AdminAppState;
|
||||||
use crate::handlers::admin::shared::{
|
use crate::handlers::admin::shared::{
|
||||||
@@ -221,6 +222,32 @@ pub(crate) async fn build_admin_update_provider_key_record(
|
|||||||
} else {
|
} else {
|
||||||
updated.auth_type_by_format = None;
|
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;
|
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> {
|
pub(crate) fn normalize_auth_type(value: Option<&str>) -> Result<String, String> {
|
||||||
let auth_type = value.unwrap_or("api_key").trim().to_ascii_lowercase();
|
let auth_type = value.unwrap_or("api_key").trim().to_ascii_lowercase();
|
||||||
match auth_type.as_str() {
|
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),
|
internal_priority: Some(key.internal_priority),
|
||||||
global_priority_by_format: key.global_priority_by_format.clone(),
|
global_priority_by_format: key.global_priority_by_format.clone(),
|
||||||
auth_type_by_format: key.auth_type_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,
|
rpm_limit: key.rpm_limit,
|
||||||
allowed_models: key.allowed_models.as_ref().and_then(|value| {
|
allowed_models: key.allowed_models.as_ref().and_then(|value| {
|
||||||
value.as_array().map(|items| {
|
value.as_array().map(|items| {
|
||||||
|
|||||||
@@ -1312,6 +1312,10 @@ pub(crate) fn build_admin_provider_key_response(
|
|||||||
"auth_type_by_format".to_string(),
|
"auth_type_by_format".to_string(),
|
||||||
json!(key.auth_type_by_format),
|
json!(key.auth_type_by_format),
|
||||||
);
|
);
|
||||||
|
payload.insert(
|
||||||
|
"allow_auth_channel_mismatch_formats".to_string(),
|
||||||
|
json!(key.allow_auth_channel_mismatch_formats),
|
||||||
|
);
|
||||||
payload.insert(
|
payload.insert(
|
||||||
"credential_kind".to_string(),
|
"credential_kind".to_string(),
|
||||||
json!(auth_semantics.credential_kind().as_str()),
|
json!(auth_semantics.credential_kind().as_str()),
|
||||||
|
|||||||
@@ -760,7 +760,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec![api_format.to_string()]),
|
api_formats: Some(vec![api_format.to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: 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 {
|
fn sample_provider_catalog_key() -> StoredProviderCatalogKey {
|
||||||
StoredProviderCatalogKey::new(
|
let mut key = StoredProviderCatalogKey::new(
|
||||||
"key-vertex-cli-stream-local-1".to_string(),
|
"key-vertex-cli-stream-local-1".to_string(),
|
||||||
"provider-vertex-cli-stream-local-1".to_string(),
|
"provider-vertex-cli-stream-local-1".to_string(),
|
||||||
"prod".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,
|
||||||
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>));
|
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 {
|
fn sample_provider_catalog_key() -> StoredProviderCatalogKey {
|
||||||
StoredProviderCatalogKey::new(
|
let mut key = StoredProviderCatalogKey::new(
|
||||||
"key-vertex-cli-local-1".to_string(),
|
"key-vertex-cli-local-1".to_string(),
|
||||||
"provider-vertex-cli-local-1".to_string(),
|
"provider-vertex-cli-local-1".to_string(),
|
||||||
"prod".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,
|
||||||
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>));
|
let seen_execution_runtime = Arc::new(Mutex::new(None::<SeenExecutionRuntimeSyncRequest>));
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ pub struct AdminSystemConfigProviderKey {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auth_type_by_format: Option<Value>,
|
pub auth_type_by_format: Option<Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub allow_auth_channel_mismatch_formats: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
pub rpm_limit: Option<u32>,
|
pub rpm_limit: Option<u32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allowed_models: Option<Vec<String>>,
|
pub allowed_models: Option<Vec<String>>,
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ pub struct StoredProviderCatalogKey {
|
|||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub api_formats: Option<serde_json::Value>,
|
pub api_formats: Option<serde_json::Value>,
|
||||||
pub auth_type_by_format: 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_api_key: Option<String>,
|
||||||
pub encrypted_auth_config: Option<String>,
|
pub encrypted_auth_config: Option<String>,
|
||||||
pub note: Option<String>,
|
pub note: Option<String>,
|
||||||
@@ -323,6 +324,7 @@ impl StoredProviderCatalogKey {
|
|||||||
is_active,
|
is_active,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
encrypted_api_key: None,
|
encrypted_api_key: None,
|
||||||
encrypted_auth_config: None,
|
encrypted_auth_config: None,
|
||||||
note: None,
|
note: None,
|
||||||
|
|||||||
@@ -470,6 +470,7 @@ CREATE TABLE IF NOT EXISTS public.provider_api_keys (
|
|||||||
note character varying(500),
|
note character varying(500),
|
||||||
internal_priority integer DEFAULT 50,
|
internal_priority integer DEFAULT 50,
|
||||||
rpm_limit integer,
|
rpm_limit integer,
|
||||||
|
concurrent_limit integer,
|
||||||
allowed_models json,
|
allowed_models json,
|
||||||
capabilities json,
|
capabilities json,
|
||||||
learned_rpm_limit integer,
|
learned_rpm_limit integer,
|
||||||
@@ -497,6 +498,7 @@ CREATE TABLE IF NOT EXISTS public.provider_api_keys (
|
|||||||
provider_id character varying(36) NOT NULL,
|
provider_id character varying(36) NOT NULL,
|
||||||
api_formats json,
|
api_formats json,
|
||||||
auth_type_by_format json,
|
auth_type_by_format json,
|
||||||
|
allow_auth_channel_mismatch_formats json,
|
||||||
rate_multipliers json,
|
rate_multipliers json,
|
||||||
health_by_format jsonb,
|
health_by_format jsonb,
|
||||||
circuit_breaker_by_format jsonb,
|
circuit_breaker_by_format jsonb,
|
||||||
|
|||||||
@@ -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 MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||||
static BASELINE_V2_SQL: &str = include_str!("../bootstrap/20260413020000_baseline_v2.sql");
|
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 =
|
const MIGRATIONS_TABLE_EXISTS_SQL: &str =
|
||||||
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
|
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
|
||||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||||
@@ -665,6 +665,7 @@ SELECT EXISTS (
|
|||||||
20260423000000,
|
20260423000000,
|
||||||
20260424000000,
|
20260424000000,
|
||||||
20260428000000,
|
20260428000000,
|
||||||
|
20260502000000,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -732,7 +733,20 @@ SELECT EXISTS (
|
|||||||
.sql
|
.sql
|
||||||
.contains("api_formats json DEFAULT '[]'::json NOT NULL"));
|
.contains("api_formats json DEFAULT '[]'::json NOT NULL"));
|
||||||
assert!(BASELINE_V2_SQL.contains("api_formats json,"));
|
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"));
|
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]
|
#[test]
|
||||||
@@ -1256,6 +1270,7 @@ ORDER BY id
|
|||||||
20260423000000,
|
20260423000000,
|
||||||
20260424000000,
|
20260424000000,
|
||||||
20260428000000,
|
20260428000000,
|
||||||
|
20260502000000,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ SELECT
|
|||||||
is_active,
|
is_active,
|
||||||
api_formats,
|
api_formats,
|
||||||
auth_type_by_format,
|
auth_type_by_format,
|
||||||
|
allow_auth_channel_mismatch_formats,
|
||||||
api_key,
|
api_key,
|
||||||
auth_config,
|
auth_config,
|
||||||
note,
|
note,
|
||||||
@@ -198,6 +199,7 @@ SELECT
|
|||||||
is_active,
|
is_active,
|
||||||
api_formats,
|
api_formats,
|
||||||
auth_type_by_format,
|
auth_type_by_format,
|
||||||
|
allow_auth_channel_mismatch_formats,
|
||||||
api_key,
|
api_key,
|
||||||
auth_config,
|
auth_config,
|
||||||
note,
|
note,
|
||||||
@@ -1234,7 +1236,8 @@ INSERT INTO provider_api_keys (
|
|||||||
circuit_breaker_by_format,
|
circuit_breaker_by_format,
|
||||||
is_active,
|
is_active,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at,
|
||||||
|
allow_auth_channel_mismatch_formats
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
@@ -1310,7 +1313,8 @@ INSERT INTO provider_api_keys (
|
|||||||
CASE
|
CASE
|
||||||
WHEN $51::double precision IS NULL THEN NOW()
|
WHEN $51::double precision IS NULL THEN NOW()
|
||||||
ELSE TO_TIMESTAMP($51::double precision)
|
ELSE TO_TIMESTAMP($51::double precision)
|
||||||
END
|
END,
|
||||||
|
$52
|
||||||
)
|
)
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
@@ -1373,7 +1377,7 @@ INSERT INTO provider_api_keys (
|
|||||||
.bind(key.is_active)
|
.bind(key.is_active)
|
||||||
.bind(key.created_at_unix_ms.map(|value| value as f64))
|
.bind(key.created_at_unix_ms.map(|value| value as f64))
|
||||||
.bind(key.updated_at_unix_secs.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)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
@@ -1723,6 +1727,7 @@ SET
|
|||||||
provider_id = $2,
|
provider_id = $2,
|
||||||
api_formats = $3,
|
api_formats = $3,
|
||||||
auth_type_by_format = $39,
|
auth_type_by_format = $39,
|
||||||
|
allow_auth_channel_mismatch_formats = $40,
|
||||||
auth_type = $4,
|
auth_type = $4,
|
||||||
api_key = $5,
|
api_key = $5,
|
||||||
auth_config = $6,
|
auth_config = $6,
|
||||||
@@ -1818,6 +1823,7 @@ WHERE id = $1
|
|||||||
.bind(key.updated_at_unix_secs.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.expires_at_unix_secs.map(|value| value as f64))
|
||||||
.bind(&key.auth_type_by_format)
|
.bind(&key.auth_type_by_format)
|
||||||
|
.bind(&key.allow_auth_channel_mismatch_formats)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?
|
.map_postgres_err()?
|
||||||
@@ -2479,6 +2485,8 @@ fn map_key_row(row: &PgRow) -> Result<StoredProviderCatalogKey, DataLayerError>
|
|||||||
);
|
);
|
||||||
key.note = row.try_get("note").ok();
|
key.note = row.try_get("note").ok();
|
||||||
key.auth_type_by_format = row.try_get("auth_type_by_format").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.internal_priority = row.try_get("internal_priority").unwrap_or(50);
|
||||||
key.cache_ttl_minutes = row.try_get("cache_ttl_minutes").unwrap_or(5);
|
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);
|
key.max_probe_interval_minutes = row.try_get("max_probe_interval_minutes").unwrap_or(32);
|
||||||
|
|||||||
@@ -1173,6 +1173,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -588,6 +588,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec![api_format.to_string()]),
|
api_formats: Some(vec![api_format.to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -397,6 +397,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec![api_format.to_string()]),
|
api_formats: Some(vec![api_format.to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
@@ -356,6 +357,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -672,6 +672,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -407,6 +407,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ pub struct GatewayProviderTransportKey {
|
|||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub api_formats: Option<Vec<String>>,
|
pub api_formats: Option<Vec<String>>,
|
||||||
pub auth_type_by_format: Option<serde_json::Value>,
|
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 allowed_models: Option<Vec<String>>,
|
||||||
pub capabilities: Option<serde_json::Value>,
|
pub capabilities: Option<serde_json::Value>,
|
||||||
pub rate_multipliers: Option<serde_json::Value>,
|
pub rate_multipliers: Option<serde_json::Value>,
|
||||||
@@ -388,6 +389,7 @@ mod tests {
|
|||||||
"openai:responses".to_string(),
|
"openai:responses".to_string(),
|
||||||
]),
|
]),
|
||||||
auth_type_by_format: None,
|
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(),]),
|
allowed_models: Some(vec!["gpt-4.1".to_string(), "gpt-4.1-mini".to_string(),]),
|
||||||
capabilities: Some(serde_json::json!({"cache_1h": true})),
|
capabilities: Some(serde_json::json!({"cache_1h": true})),
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ pub(super) fn map_key(
|
|||||||
"provider_api_keys.api_formats",
|
"provider_api_keys.api_formats",
|
||||||
)?,
|
)?,
|
||||||
auth_type_by_format: normalize_optional_json(key.auth_type_by_format),
|
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(
|
allowed_models: normalize_string_list(
|
||||||
normalize_optional_json(key.allowed_models),
|
normalize_optional_json(key.allowed_models),
|
||||||
"provider_api_keys.allowed_models",
|
"provider_api_keys.allowed_models",
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
rate_multipliers: None,
|
rate_multipliers: None,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ mod tests {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
api_formats: None,
|
api_formats: None,
|
||||||
auth_type_by_format: None,
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
|
||||||
allowed_models: None,
|
allowed_models: None,
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export interface ProviderKeyExport {
|
|||||||
internal_priority?: number
|
internal_priority?: number
|
||||||
global_priority_by_format?: Record<string, number> | null
|
global_priority_by_format?: Record<string, number> | null
|
||||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
||||||
|
allow_auth_channel_mismatch_formats?: string[] | null
|
||||||
rpm_limit?: number | null
|
rpm_limit?: number | null
|
||||||
allowed_models?: string[] | null
|
allowed_models?: string[] | null
|
||||||
capabilities?: Record<string, boolean>
|
capabilities?: Record<string, boolean>
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export async function addProviderKey(
|
|||||||
api_key: string
|
api_key: string
|
||||||
auth_type?: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
auth_type?: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
||||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
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)
|
auth_config?: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||||
name: string
|
name: string
|
||||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
||||||
@@ -169,6 +170,7 @@ export async function updateProviderKey(
|
|||||||
api_key: string
|
api_key: string
|
||||||
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
||||||
auth_type_by_format: Record<string, 'api_key' | 'bearer'> | null
|
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)
|
auth_config: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||||
name: string
|
name: string
|
||||||
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
|
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export interface PoolKeyDetail {
|
|||||||
is_active: boolean
|
is_active: boolean
|
||||||
auth_type: string
|
auth_type: string
|
||||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
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
|
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
||||||
oauth_managed?: boolean
|
oauth_managed?: boolean
|
||||||
@@ -212,6 +213,7 @@ export interface PoolKeySelectionItem {
|
|||||||
key_name: string
|
key_name: string
|
||||||
auth_type: string
|
auth_type: string
|
||||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
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
|
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
||||||
oauth_managed?: boolean
|
oauth_managed?: boolean
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ export interface EndpointAPIKey {
|
|||||||
api_key_plain?: string | null
|
api_key_plain?: string | null
|
||||||
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型(必返回)
|
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型(必返回)
|
||||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
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
|
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'mixed' | 'unknown' | string | null
|
||||||
oauth_managed?: boolean
|
oauth_managed?: boolean
|
||||||
@@ -388,6 +389,7 @@ export interface EndpointAPIKeyUpdate {
|
|||||||
api_key?: string // 仅在需要更新时提供
|
api_key?: string // 仅在需要更新时提供
|
||||||
auth_type?: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
auth_type?: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
||||||
auth_type_by_format?: Record<string, 'api_key' | 'bearer'> | null
|
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)
|
auth_config?: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
||||||
internal_priority?: number
|
internal_priority?: number
|
||||||
|
|||||||
@@ -163,6 +163,18 @@
|
|||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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
|
<div
|
||||||
class="flex items-center text-xs text-muted-foreground gap-1"
|
class="flex items-center text-xs text-muted-foreground gap-1"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -478,6 +490,25 @@ function sanitizeAuthTypeByFormat(
|
|||||||
return sanitized
|
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[] {
|
function getDefaultApiFormats(): string[] {
|
||||||
const endpointFormat = props.endpoint?.api_format
|
const endpointFormat = props.endpoint?.api_format
|
||||||
if (endpointFormat) {
|
if (endpointFormat) {
|
||||||
@@ -549,12 +580,35 @@ function setFormatAuthType(format: string, authType: RawSecretAuthType) {
|
|||||||
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(next)
|
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 {
|
function buildAuthTypeByFormatPayload(): Record<string, RawSecretAuthType> | null {
|
||||||
const sanitized = sanitizeAuthTypeByFormat(form.value.auth_type_by_format)
|
const sanitized = sanitizeAuthTypeByFormat(form.value.auth_type_by_format)
|
||||||
return Object.keys(sanitized).length > 0 ? sanitized : null
|
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(() => (
|
const serviceAccountDescription = computed(() => (
|
||||||
props.editingKey
|
props.editingKey
|
||||||
? '留空表示不修改;JSON 格式,包含 project_id、private_key 等字段'
|
? '留空表示不修改;JSON 格式,包含 project_id、private_key 等字段'
|
||||||
@@ -566,6 +620,10 @@ function getDefaultAuthType(): ProviderKeyFormAuthType {
|
|||||||
return authTypeOptions.value[0]?.value || 'api_key'
|
return authTypeOptions.value[0]?.value || 'api_key'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultAllowAuthChannelMismatchFormats(formats = getDefaultApiFormats()): string[] {
|
||||||
|
return sanitizeAllowAuthChannelMismatchFormats(formats, formats)
|
||||||
|
}
|
||||||
|
|
||||||
// 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models
|
// 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models
|
||||||
const showAutoFetchWarning = computed(() => {
|
const showAutoFetchWarning = computed(() => {
|
||||||
if (!props.editingKey) return false
|
if (!props.editingKey) return false
|
||||||
@@ -632,6 +690,7 @@ const form = ref({
|
|||||||
api_key: '', // 标准 API Key
|
api_key: '', // 标准 API Key
|
||||||
auth_type: 'api_key' as ProviderKeyFormAuthType, // 认证类型
|
auth_type: 'api_key' as ProviderKeyFormAuthType, // 认证类型
|
||||||
auth_type_by_format: {} as Record<string, RawSecretAuthType>,
|
auth_type_by_format: {} as Record<string, RawSecretAuthType>,
|
||||||
|
allow_auth_channel_mismatch_formats: [] as string[],
|
||||||
auth_config_text: '', // Service Account JSON 文本(用于表单输入)
|
auth_config_text: '', // Service Account JSON 文本(用于表单输入)
|
||||||
api_formats: [] as string[], // 支持的 API 格式列表
|
api_formats: [] as string[], // 支持的 API 格式列表
|
||||||
rate_multipliers: {} as Record<string, number>, // 按 API 格式的成本倍率
|
rate_multipliers: {} as Record<string, number>, // 按 API 格式的成本倍率
|
||||||
@@ -661,6 +720,9 @@ watch(
|
|||||||
form.value.api_formats = [...filtered]
|
form.value.api_formats = [...filtered]
|
||||||
}
|
}
|
||||||
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(form.value.auth_type_by_format)
|
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 }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
@@ -676,6 +738,10 @@ watch(
|
|||||||
if (filtered.length !== form.value.api_formats.length) {
|
if (filtered.length !== form.value.api_formats.length) {
|
||||||
form.value.api_formats = [...filtered]
|
form.value.api_formats = [...filtered]
|
||||||
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(form.value.auth_type_by_format, 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,6 +749,8 @@ watch(
|
|||||||
const defaults = getDefaultApiFormats()
|
const defaults = getDefaultApiFormats()
|
||||||
if (defaults.length > 0) {
|
if (defaults.length > 0) {
|
||||||
form.value.api_formats = defaults
|
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) {
|
if (index === -1) {
|
||||||
// 添加格式
|
// 添加格式
|
||||||
form.value.api_formats.push(format)
|
form.value.api_formats.push(format)
|
||||||
|
setAuthChannelMismatchAllowed(format, true)
|
||||||
} else {
|
} else {
|
||||||
// 移除格式,但保留倍率配置(用户可能只是临时取消)
|
// 移除格式,但保留倍率配置(用户可能只是临时取消)
|
||||||
form.value.api_formats.splice(index, 1)
|
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() {
|
function resetForm() {
|
||||||
formNonce.value = createFieldNonce()
|
formNonce.value = createFieldNonce()
|
||||||
|
const defaultApiFormats = getDefaultApiFormats()
|
||||||
form.value = {
|
form.value = {
|
||||||
name: '',
|
name: '',
|
||||||
api_key: '',
|
api_key: '',
|
||||||
auth_type: getDefaultAuthType(),
|
auth_type: getDefaultAuthType(),
|
||||||
auth_type_by_format: {},
|
auth_type_by_format: {},
|
||||||
|
allow_auth_channel_mismatch_formats:
|
||||||
|
getDefaultAllowAuthChannelMismatchFormats(defaultApiFormats),
|
||||||
auth_config_text: '',
|
auth_config_text: '',
|
||||||
api_formats: getDefaultApiFormats(),
|
api_formats: defaultApiFormats,
|
||||||
rate_multipliers: {},
|
rate_multipliers: {},
|
||||||
internal_priority: 10,
|
internal_priority: 10,
|
||||||
rpm_limit: undefined,
|
rpm_limit: undefined,
|
||||||
@@ -766,6 +841,9 @@ function clearForNextAdd() {
|
|||||||
form.value.api_key = ''
|
form.value.api_key = ''
|
||||||
form.value.auth_config_text = ''
|
form.value.auth_config_text = ''
|
||||||
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(form.value.auth_type_by_format)
|
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 || [],
|
props.editingKey.api_formats || [],
|
||||||
normalizeFormAuthType(props.editingKey.auth_type)
|
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 不返回给前端,编辑时需要重新输入
|
auth_config_text: '', // auth_config 不返回给前端,编辑时需要重新输入
|
||||||
api_formats: props.editingKey.api_formats?.length > 0
|
api_formats: props.editingKey.api_formats?.length > 0
|
||||||
? sanitizeApiFormats(
|
? sanitizeApiFormats(
|
||||||
@@ -873,6 +955,9 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.value.api_formats = sanitizeApiFormats(form.value.api_formats)
|
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 格式
|
// 验证至少选择一个 API 格式
|
||||||
if (form.value.api_formats.length === 0) {
|
if (form.value.api_formats.length === 0) {
|
||||||
@@ -905,6 +990,7 @@ async function handleSave() {
|
|||||||
// 准备认证相关数据
|
// 准备认证相关数据
|
||||||
const authConfig = parseAuthConfig()
|
const authConfig = parseAuthConfig()
|
||||||
const authTypeByFormat = buildAuthTypeByFormatPayload()
|
const authTypeByFormat = buildAuthTypeByFormatPayload()
|
||||||
|
const allowAuthChannelMismatchFormats = buildAllowAuthChannelMismatchFormatsPayload()
|
||||||
|
|
||||||
if (props.editingKey) {
|
if (props.editingKey) {
|
||||||
const shouldClearAllowedModels = !!props.editingKey.auto_fetch_models && !form.value.auto_fetch_models
|
const shouldClearAllowedModels = !!props.editingKey.auto_fetch_models && !form.value.auto_fetch_models
|
||||||
@@ -916,6 +1002,7 @@ async function handleSave() {
|
|||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
auth_type: form.value.auth_type,
|
auth_type: form.value.auth_type,
|
||||||
auth_type_by_format: authTypeByFormat,
|
auth_type_by_format: authTypeByFormat,
|
||||||
|
allow_auth_channel_mismatch_formats: allowAuthChannelMismatchFormats,
|
||||||
rate_multipliers: rateMultipliersData,
|
rate_multipliers: rateMultipliersData,
|
||||||
internal_priority: form.value.internal_priority,
|
internal_priority: form.value.internal_priority,
|
||||||
rpm_limit: form.value.rpm_limit,
|
rpm_limit: form.value.rpm_limit,
|
||||||
@@ -947,6 +1034,7 @@ async function handleSave() {
|
|||||||
api_key: form.value.api_key,
|
api_key: form.value.api_key,
|
||||||
auth_type: form.value.auth_type,
|
auth_type: form.value.auth_type,
|
||||||
auth_type_by_format: authTypeByFormat,
|
auth_type_by_format: authTypeByFormat,
|
||||||
|
allow_auth_channel_mismatch_formats: allowAuthChannelMismatchFormats,
|
||||||
auth_config: authConfig || undefined,
|
auth_config: authConfig || undefined,
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
rate_multipliers: rateMultipliersData,
|
rate_multipliers: rateMultipliersData,
|
||||||
|
|||||||
Reference in New Issue
Block a user