feat: provider api_formats 可空继承、OpenAI 图片 edit/variation 与用量配额多项补强

- 鉴权: provider_api_keys.api_formats 改为可空,OAuth 托管 key 自动继承 provider endpoints 激活格式,相关 handler/测试同步更新
- 图片 planner: OpenAI 图片路由新增 edit/variation 操作并完善参数校验、响应合并与流式处理
- 用量: user me usage 返回区分 client_requested_stream/upstream_is_stream,前端 usage 列表筛选与展示增强
- 统计: stats_daily_model 新增 cache_creation_ephemeral_5m/1h tokens 字段与回填链路
- 配额/observability: quota repository 新增内存与 SQL 扩展,admin observability usage 字段扩充
- 其它: OAuth 导入/轮询收敛、provider 汇总与 pool admin 读写链路小修、新增 system_config 缓存与 provider template handler

Closes #318

Co-authored-by: Entropy.Xu <53283266+Entropy-Xu@users.noreply.github.com>
This commit is contained in:
fawney19
2026-04-23 14:42:51 +08:00
parent f55f22d2e8
commit fa328e18a1
128 changed files with 5583 additions and 690 deletions

View File

@@ -64,6 +64,27 @@ WHERE p.is_active = TRUE
AND LOWER(pe.api_format) = LOWER($1)
AND (
pak.api_formats IS NULL
OR (
LOWER(BTRIM(p.provider_type)) IN (
'claude_code',
'codex',
'gemini_cli',
'vertex_ai',
'antigravity'
)
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
)
OR (
LOWER(BTRIM(p.provider_type)) = 'kiro'
AND (
LOWER(BTRIM(pak.auth_type)) = 'oauth'
OR (
LOWER(BTRIM(pak.auth_type)) = 'bearer'
AND pak.auth_config IS NOT NULL
AND BTRIM(pak.auth_config) <> ''
)
)
)
OR EXISTS (
SELECT 1
FROM json_array_elements_text(pak.api_formats) AS fmt(value)
@@ -137,6 +158,27 @@ WHERE p.is_active = TRUE
AND gm.name = $2
AND (
pak.api_formats IS NULL
OR (
LOWER(BTRIM(p.provider_type)) IN (
'claude_code',
'codex',
'gemini_cli',
'vertex_ai',
'antigravity'
)
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
)
OR (
LOWER(BTRIM(p.provider_type)) = 'kiro'
AND (
LOWER(BTRIM(pak.auth_type)) = 'oauth'
OR (
LOWER(BTRIM(pak.auth_type)) = 'bearer'
AND pak.auth_config IS NOT NULL
AND BTRIM(pak.auth_config) <> ''
)
)
)
OR EXISTS (
SELECT 1
FROM json_array_elements_text(pak.api_formats) AS fmt(value)

View File

@@ -249,19 +249,22 @@ SELECT
provider_id,
COALESCE(NULLIF(name, ''), id) AS name,
COALESCE(NULLIF(auth_type, ''), 'summary') AS auth_type,
capabilities,
NULL::jsonb AS capabilities,
is_active,
api_formats,
'summary' AS api_key,
NULL::text AS auth_config,
CASE
WHEN auth_config IS NULL THEN NULL
ELSE '{}'::text
END AS auth_config,
NULL::text AS note,
internal_priority,
rate_multipliers,
global_priority_by_format,
NULL::integer AS internal_priority,
NULL::jsonb AS rate_multipliers,
NULL::jsonb AS global_priority_by_format,
NULL::jsonb AS allowed_models,
NULL::bigint AS expires_at_unix_secs,
cache_ttl_minutes,
max_probe_interval_minutes,
NULL::integer AS cache_ttl_minutes,
NULL::integer AS max_probe_interval_minutes,
NULL::jsonb AS proxy,
NULL::jsonb AS fingerprint,
NULL::integer AS rpm_limit,
@@ -274,27 +277,27 @@ SELECT
NULL::jsonb AS utilization_samples,
NULL::bigint AS last_probe_increase_at_unix_secs,
NULL::integer AS last_rpm_peak,
request_count,
NULL::integer AS request_count,
0::bigint AS total_tokens,
0::double precision AS total_cost_usd,
success_count,
NULL::integer AS success_count,
NULL::integer AS error_count,
total_response_time_ms,
EXTRACT(EPOCH FROM last_used_at)::bigint AS last_used_at_unix_secs,
auto_fetch_models,
NULL::integer AS total_response_time_ms,
NULL::bigint AS last_used_at_unix_secs,
FALSE AS auto_fetch_models,
NULL::bigint AS last_models_fetch_at_unix_secs,
NULL::text AS last_models_fetch_error,
NULL::jsonb AS locked_models,
NULL::jsonb AS model_include_patterns,
NULL::jsonb AS model_exclude_patterns,
NULL::jsonb AS upstream_metadata,
EXTRACT(EPOCH FROM oauth_invalid_at)::bigint AS oauth_invalid_at_unix_secs,
oauth_invalid_reason,
NULL::bigint AS oauth_invalid_at_unix_secs,
NULL::text AS oauth_invalid_reason,
NULL::jsonb AS status_snapshot,
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs,
NULL::bigint AS created_at_unix_ms,
NULL::bigint AS updated_at_unix_secs,
health_by_format,
circuit_breaker_by_format
NULL::jsonb AS circuit_breaker_by_format
FROM provider_api_keys
WHERE provider_id IN (
"#;

View File

@@ -42,6 +42,17 @@ impl ProviderQuotaReadRepository for InMemoryProviderQuotaRepository {
.get(provider_id)
.cloned())
}
async fn find_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderQuotaSnapshot>, DataLayerError> {
let quotas = self.by_provider_id.read().expect("quota repository lock");
Ok(provider_ids
.iter()
.filter_map(|provider_id| quotas.get(provider_id).cloned())
.collect())
}
}
#[async_trait]
@@ -106,4 +117,35 @@ mod tests {
.expect("quota should exist");
assert_eq!(stored.monthly_used_usd, 0.0);
}
#[tokio::test]
async fn finds_quotas_by_provider_ids() {
let repository = InMemoryProviderQuotaRepository::seed(vec![
sample_quota(),
StoredProviderQuotaSnapshot::new(
"provider-2".to_string(),
"payg".to_string(),
None,
1.5,
None,
None,
None,
true,
)
.expect("quota should build"),
]);
let stored = repository
.find_by_provider_ids(&[
"provider-2".to_string(),
"missing".to_string(),
"provider-1".to_string(),
])
.await
.expect("lookup should succeed");
assert_eq!(stored.len(), 2);
assert_eq!(stored[0].provider_id, "provider-2");
assert_eq!(stored[1].provider_id, "provider-1");
}
}

View File

@@ -21,6 +21,21 @@ WHERE id = $1
LIMIT 1
"#;
const FIND_BY_PROVIDER_IDS_SQL: &str = r#"
SELECT
id AS provider_id,
CAST(billing_type AS TEXT) AS billing_type,
CAST(monthly_quota_usd AS DOUBLE PRECISION) AS monthly_quota_usd,
CAST(COALESCE(monthly_used_usd, 0) AS DOUBLE PRECISION) AS monthly_used_usd,
quota_reset_day,
CAST(EXTRACT(EPOCH FROM quota_last_reset_at) AS BIGINT) AS quota_last_reset_at_unix_secs,
CAST(EXTRACT(EPOCH FROM quota_expires_at) AS BIGINT) AS quota_expires_at_unix_secs,
is_active
FROM providers
WHERE id = ANY($1::TEXT[])
ORDER BY id ASC
"#;
const RESET_DUE_SQL: &str = r#"
UPDATE providers
SET
@@ -60,6 +75,24 @@ impl ProviderQuotaReadRepository for SqlxProviderQuotaRepository {
.map_postgres_err()?;
row.as_ref().map(map_row).transpose()
}
async fn find_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderQuotaSnapshot>, DataLayerError> {
if provider_ids.is_empty() {
return Ok(Vec::new());
}
sqlx::query(FIND_BY_PROVIDER_IDS_SQL)
.bind(provider_ids)
.fetch_all(&self.pool)
.await
.map_postgres_err()?
.iter()
.map(map_row)
.collect()
}
}
#[async_trait]