feat: configure auth channel mismatch formats

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

View File

@@ -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>>,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,
]
);
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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})),

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,