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:
@@ -214,6 +214,8 @@ pub struct AdminSystemConfigProviderKey {
|
||||
#[serde(default)]
|
||||
pub auth_type_by_format: Option<Value>,
|
||||
#[serde(default)]
|
||||
pub allow_auth_channel_mismatch_formats: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub rpm_limit: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub allowed_models: Option<Vec<String>>,
|
||||
|
||||
@@ -249,6 +249,7 @@ pub struct StoredProviderCatalogKey {
|
||||
pub is_active: bool,
|
||||
pub api_formats: Option<serde_json::Value>,
|
||||
pub auth_type_by_format: Option<serde_json::Value>,
|
||||
pub allow_auth_channel_mismatch_formats: Option<serde_json::Value>,
|
||||
pub encrypted_api_key: Option<String>,
|
||||
pub encrypted_auth_config: Option<String>,
|
||||
pub note: Option<String>,
|
||||
@@ -323,6 +324,7 @@ impl StoredProviderCatalogKey {
|
||||
is_active,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
encrypted_api_key: None,
|
||||
encrypted_auth_config: None,
|
||||
note: None,
|
||||
|
||||
@@ -470,6 +470,7 @@ CREATE TABLE IF NOT EXISTS public.provider_api_keys (
|
||||
note character varying(500),
|
||||
internal_priority integer DEFAULT 50,
|
||||
rpm_limit integer,
|
||||
concurrent_limit integer,
|
||||
allowed_models json,
|
||||
capabilities json,
|
||||
learned_rpm_limit integer,
|
||||
@@ -497,6 +498,7 @@ CREATE TABLE IF NOT EXISTS public.provider_api_keys (
|
||||
provider_id character varying(36) NOT NULL,
|
||||
api_formats json,
|
||||
auth_type_by_format json,
|
||||
allow_auth_channel_mismatch_formats json,
|
||||
rate_multipliers json,
|
||||
health_by_format jsonb,
|
||||
circuit_breaker_by_format jsonb,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
ALTER TABLE public.provider_api_keys
|
||||
ADD COLUMN IF NOT EXISTS allow_auth_channel_mismatch_formats json;
|
||||
|
||||
ALTER TABLE public.provider_api_keys
|
||||
ADD COLUMN IF NOT EXISTS concurrent_limit integer;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.aether_default_auth_mismatch_api_format(value text)
|
||||
RETURNS text
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
AS $$
|
||||
SELECT CASE LOWER(BTRIM(COALESCE(value, '')))
|
||||
WHEN 'openai:cli' THEN 'openai:responses'
|
||||
WHEN 'openai:compact' THEN 'openai:responses:compact'
|
||||
WHEN 'claude:chat' THEN 'claude:messages'
|
||||
WHEN 'claude:cli' THEN 'claude:messages'
|
||||
WHEN 'gemini:chat' THEN 'gemini:generate_content'
|
||||
WHEN 'gemini:cli' THEN 'gemini:generate_content'
|
||||
ELSE LOWER(BTRIM(COALESCE(value, '')))
|
||||
END
|
||||
$$;
|
||||
|
||||
WITH supported_formats AS (
|
||||
SELECT
|
||||
pak.id,
|
||||
public.aether_default_auth_mismatch_api_format(format.value) AS api_format,
|
||||
0 AS source_priority,
|
||||
MIN(format.ordinality) AS first_ordinality
|
||||
FROM public.provider_api_keys AS pak
|
||||
CROSS JOIN LATERAL json_array_elements_text(
|
||||
CASE
|
||||
WHEN pak.api_formats IS NOT NULL
|
||||
AND json_typeof(pak.api_formats) = 'array'
|
||||
THEN pak.api_formats
|
||||
ELSE '[]'::json
|
||||
END
|
||||
) WITH ORDINALITY AS format(value, ordinality)
|
||||
WHERE pak.api_formats IS NOT NULL
|
||||
AND json_typeof(pak.api_formats) = 'array'
|
||||
GROUP BY pak.id, api_format
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
pak.id,
|
||||
public.aether_default_auth_mismatch_api_format(endpoint.api_format) AS api_format,
|
||||
1 AS source_priority,
|
||||
0 AS first_ordinality
|
||||
FROM public.provider_api_keys AS pak
|
||||
INNER JOIN public.provider_endpoints AS endpoint
|
||||
ON endpoint.provider_id = pak.provider_id
|
||||
WHERE pak.api_formats IS NULL
|
||||
OR json_typeof(pak.api_formats) <> 'array'
|
||||
),
|
||||
deduplicated_formats AS (
|
||||
SELECT
|
||||
id,
|
||||
api_format,
|
||||
MIN(source_priority) AS source_priority,
|
||||
MIN(first_ordinality) AS first_ordinality
|
||||
FROM supported_formats
|
||||
WHERE api_format <> ''
|
||||
GROUP BY id, api_format
|
||||
),
|
||||
rebuilt AS (
|
||||
SELECT
|
||||
id,
|
||||
json_agg(api_format ORDER BY source_priority, first_ordinality, api_format) AS api_formats
|
||||
FROM deduplicated_formats
|
||||
GROUP BY id
|
||||
)
|
||||
UPDATE public.provider_api_keys AS pak
|
||||
SET
|
||||
allow_auth_channel_mismatch_formats = rebuilt.api_formats,
|
||||
updated_at = NOW()
|
||||
FROM rebuilt
|
||||
WHERE pak.id = rebuilt.id
|
||||
AND pak.allow_auth_channel_mismatch_formats IS NULL;
|
||||
|
||||
DROP FUNCTION public.aether_default_auth_mismatch_api_format(text);
|
||||
@@ -8,7 +8,7 @@ use tracing::{error, info, warn};
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||
static BASELINE_V2_SQL: &str = include_str!("../bootstrap/20260413020000_baseline_v2.sql");
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260428000000;
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260502000000;
|
||||
const MIGRATIONS_TABLE_EXISTS_SQL: &str =
|
||||
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
|
||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||
@@ -665,6 +665,7 @@ SELECT EXISTS (
|
||||
20260423000000,
|
||||
20260424000000,
|
||||
20260428000000,
|
||||
20260502000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -732,7 +733,20 @@ SELECT EXISTS (
|
||||
.sql
|
||||
.contains("api_formats json DEFAULT '[]'::json NOT NULL"));
|
||||
assert!(BASELINE_V2_SQL.contains("api_formats json,"));
|
||||
assert!(BASELINE_V2_SQL.contains("concurrent_limit integer,"));
|
||||
assert!(BASELINE_V2_SQL.contains("allow_auth_channel_mismatch_formats json,"));
|
||||
assert!(!BASELINE_V2_SQL.contains("api_formats json DEFAULT '[]'::json NOT NULL"));
|
||||
|
||||
let auth_mismatch_migration = MIGRATOR
|
||||
.iter()
|
||||
.find(|migration| migration.version == 20260502000000)
|
||||
.expect("auth mismatch migration should be embedded");
|
||||
assert!(auth_mismatch_migration
|
||||
.sql
|
||||
.contains("allow_auth_channel_mismatch_formats = rebuilt.api_formats"));
|
||||
assert!(auth_mismatch_migration
|
||||
.sql
|
||||
.contains("pak.allow_auth_channel_mismatch_formats IS NULL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1256,6 +1270,7 @@ ORDER BY id
|
||||
20260423000000,
|
||||
20260424000000,
|
||||
20260428000000,
|
||||
20260502000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ SELECT
|
||||
is_active,
|
||||
api_formats,
|
||||
auth_type_by_format,
|
||||
allow_auth_channel_mismatch_formats,
|
||||
api_key,
|
||||
auth_config,
|
||||
note,
|
||||
@@ -198,6 +199,7 @@ SELECT
|
||||
is_active,
|
||||
api_formats,
|
||||
auth_type_by_format,
|
||||
allow_auth_channel_mismatch_formats,
|
||||
api_key,
|
||||
auth_config,
|
||||
note,
|
||||
@@ -1234,7 +1236,8 @@ INSERT INTO provider_api_keys (
|
||||
circuit_breaker_by_format,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
updated_at,
|
||||
allow_auth_channel_mismatch_formats
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
@@ -1310,7 +1313,8 @@ INSERT INTO provider_api_keys (
|
||||
CASE
|
||||
WHEN $51::double precision IS NULL THEN NOW()
|
||||
ELSE TO_TIMESTAMP($51::double precision)
|
||||
END
|
||||
END,
|
||||
$52
|
||||
)
|
||||
"#,
|
||||
)
|
||||
@@ -1373,7 +1377,7 @@ INSERT INTO provider_api_keys (
|
||||
.bind(key.is_active)
|
||||
.bind(key.created_at_unix_ms.map(|value| value as f64))
|
||||
.bind(key.updated_at_unix_secs.map(|value| value as f64))
|
||||
.bind(key.expires_at_unix_secs.map(|value| value as f64))
|
||||
.bind(&key.allow_auth_channel_mismatch_formats)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
@@ -1723,6 +1727,7 @@ SET
|
||||
provider_id = $2,
|
||||
api_formats = $3,
|
||||
auth_type_by_format = $39,
|
||||
allow_auth_channel_mismatch_formats = $40,
|
||||
auth_type = $4,
|
||||
api_key = $5,
|
||||
auth_config = $6,
|
||||
@@ -1818,6 +1823,7 @@ WHERE id = $1
|
||||
.bind(key.updated_at_unix_secs.map(|value| value as f64))
|
||||
.bind(key.expires_at_unix_secs.map(|value| value as f64))
|
||||
.bind(&key.auth_type_by_format)
|
||||
.bind(&key.allow_auth_channel_mismatch_formats)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?
|
||||
@@ -2479,6 +2485,8 @@ fn map_key_row(row: &PgRow) -> Result<StoredProviderCatalogKey, DataLayerError>
|
||||
);
|
||||
key.note = row.try_get("note").ok();
|
||||
key.auth_type_by_format = row.try_get("auth_type_by_format").ok();
|
||||
key.allow_auth_channel_mismatch_formats =
|
||||
row.try_get("allow_auth_channel_mismatch_formats").ok();
|
||||
key.internal_priority = row.try_get("internal_priority").unwrap_or(50);
|
||||
key.cache_ttl_minutes = row.try_get("cache_ttl_minutes").unwrap_or(5);
|
||||
key.max_probe_interval_minutes = row.try_get("max_probe_interval_minutes").unwrap_or(32);
|
||||
|
||||
@@ -1173,6 +1173,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -588,6 +588,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec![api_format.to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -361,6 +361,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -81,6 +81,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -120,6 +120,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -397,6 +397,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec![api_format.to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -299,6 +299,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
@@ -356,6 +357,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -177,6 +177,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -199,6 +199,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -137,6 +137,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -233,6 +233,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -299,6 +299,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -672,6 +672,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -116,6 +116,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -407,6 +407,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -403,6 +403,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -62,6 +62,7 @@ pub struct GatewayProviderTransportKey {
|
||||
pub is_active: bool,
|
||||
pub api_formats: Option<Vec<String>>,
|
||||
pub auth_type_by_format: Option<serde_json::Value>,
|
||||
pub allow_auth_channel_mismatch_formats: Option<serde_json::Value>,
|
||||
pub allowed_models: Option<Vec<String>>,
|
||||
pub capabilities: Option<serde_json::Value>,
|
||||
pub rate_multipliers: Option<serde_json::Value>,
|
||||
@@ -388,6 +389,7 @@ mod tests {
|
||||
"openai:responses".to_string(),
|
||||
]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: Some(vec!["gpt-4.1".to_string(), "gpt-4.1-mini".to_string(),]),
|
||||
capabilities: Some(serde_json::json!({"cache_1h": true})),
|
||||
|
||||
@@ -95,6 +95,9 @@ pub(super) fn map_key(
|
||||
"provider_api_keys.api_formats",
|
||||
)?,
|
||||
auth_type_by_format: normalize_optional_json(key.auth_type_by_format),
|
||||
allow_auth_channel_mismatch_formats: normalize_optional_json(
|
||||
key.allow_auth_channel_mismatch_formats,
|
||||
),
|
||||
allowed_models: normalize_string_list(
|
||||
normalize_optional_json(key.allowed_models),
|
||||
"provider_api_keys.allowed_models",
|
||||
|
||||
@@ -283,6 +283,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
|
||||
@@ -89,6 +89,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -112,6 +112,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -204,6 +204,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
@@ -297,6 +297,7 @@ mod tests {
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
|
||||
Reference in New Issue
Block a user