diff --git a/README.md b/README.md index ef0b335a3..94d17057b 100644 --- a/README.md +++ b/README.md @@ -99,16 +99,15 @@ cargo run ## 配置重点 -常用配置都在 `config.yaml` 中: +`config.yaml` 只放启动级配置,模型由客户端请求决定,网关不会兜底默认模型;`auth`、`quota`、模型别名和模型账号路由都不再从 YAML 读取: ```yaml -model: - default: gpt-5.5 +server: + host: 0.0.0.0 + port: 8080 -auth: - max_concurrent_per_account: 3 - request_interval_ms: 50 - rotation_strategy: least_used +api: + base_url: https://chatgpt.com/backend-api database: url: sqlite://.runtime/data/codex-proxy-rs.sqlite @@ -117,8 +116,6 @@ logging: directory: .runtime/logs ``` -本项目不依赖 `local.yaml`。本地敏感值、数据库路径、日志路径和管理员默认密码都应直接在 `config.yaml` 中配置。 - ## 开发验证 后端: diff --git a/config.yaml b/config.yaml index 7dd73e4cc..15aeaa0d8 100644 --- a/config.yaml +++ b/config.yaml @@ -3,35 +3,6 @@ server: port: 8080 api: base_url: 'https://chatgpt.com/backend-api' -model: - default: gpt-5.5 - default_reasoning_effort: null - default_service_tier: null - aliases: {} - account_routes: {} -auth: - refresh_margin_seconds: 300 - refresh_enabled: true - refresh_concurrency: 2 - max_concurrent_per_account: 3 - request_interval_ms: 50 - rotation_strategy: least_used - tier_priority: [] - oauth_client_id: app_EMoamEEZ73f0CkXaXp7hrann - oauth_auth_endpoint: 'https://auth.openai.com/oauth/authorize' - oauth_token_endpoint: 'https://auth.openai.com/oauth/token' -quota: - refresh_interval_minutes: 5 - warning_thresholds: - primary: - - 80 - - 90 - secondary: - - 80 - - 90 - skip_exhausted: true -usage_stats: - history_retention_days: null database: url: 'sqlite://.runtime/data/codex-proxy-rs.sqlite' tls: @@ -93,5 +64,3 @@ logging: directory: .runtime/logs retention_days: 14 enabled: true - capacity: 2000 - capture_body: false diff --git a/docs/database-design-audit.md b/docs/database-design-audit.md index 2b94bcc7a..0e69444be 100644 --- a/docs/database-design-audit.md +++ b/docs/database-design-audit.md @@ -84,15 +84,12 @@ - 服务监听地址和端口。 - 数据库 URL。 -- API Key pepper 文件路径。 - 上游基础地址。 -- OAuth endpoint。 - 日志目录。 - 首次启动管理员默认值。 -后台设置页改动的是运行态业务配置,更适合入库,例如: +后台设置页改动的是需要人工决策的运行态配置,更适合入库,例如: -- 默认模型。 - 模型别名。 - 提前刷新秒数。 - 刷新并发数。 @@ -106,7 +103,6 @@ ```sql create table runtime_settings ( id integer primary key check (id = 1), - default_model text not null, model_aliases_json text not null default '{}', refresh_margin_seconds integer not null check (refresh_margin_seconds > 0), refresh_concurrency integer not null check (refresh_concurrency > 0), @@ -326,16 +322,15 @@ create table usage_time_buckets ( - server。 - database。 -- API Key pepper 文件路径。 - API base URL。 -- OAuth endpoint。 - TLS。 - 日志目录。 - 首次管理员默认值。 +认证刷新、额度刷新和调度节奏不再作为 YAML 输入项;可人工调整的部分进入运行配置表,固定策略使用代码默认值。 + 运行配置进入数据库: -- default model。 - model aliases。 - model-account routes。 - refresh margin。 diff --git a/src/admin/accounts/routes.rs b/src/admin/accounts/routes.rs index fab314c3e..90c3c8085 100644 --- a/src/admin/accounts/routes.rs +++ b/src/admin/accounts/routes.rs @@ -869,7 +869,14 @@ pub(crate) async fn test_account_connection( .map(str::trim) .filter(|value| !value.is_empty()) .map(ToString::to_string) - .unwrap_or_else(|| state.config.model.default_model.clone()); + .ok_or_else(|| { + AdminError::new( + StatusCode::BAD_REQUEST, + 40001, + "Model is required", + &request_id, + ) + })?; let stream = state .services .admin_accounts diff --git a/src/admin/monitoring/dashboard.rs b/src/admin/monitoring/dashboard.rs index 6eee91ab6..60dd94337 100644 --- a/src/admin/monitoring/dashboard.rs +++ b/src/admin/monitoring/dashboard.rs @@ -210,12 +210,6 @@ struct UsageWindow { min_latency: u64, } -#[derive(Debug, Clone, Copy)] -struct BillingContext<'a> { - default_model: &'a str, - default_service_tier: Option<&'a str>, -} - impl UsageWindow { fn tokens(self) -> u64 { self.input_tokens + self.output_tokens @@ -288,13 +282,7 @@ pub async fn dashboard_summary( StatusCode::OK, AdminEnvelope::ok( DashboardSummaryData { - cards: dashboard_cards( - &accounts, - &summary, - &time_buckets, - &settings.model.default_model, - settings.model.service_tier.as_deref(), - ), + cards: dashboard_cards(&accounts, &summary, &time_buckets), trend, health_timeline: dashboard_health_timeline_data(&time_buckets), account_usage: account_usage_data( @@ -337,38 +325,22 @@ fn dashboard_cards( accounts: &[Account], summary: &AdminUsageSummary, buckets: &[AdminUsageTimeBucketRecord], - default_model: &str, - default_service_tier: Option<&str>, ) -> DashboardCardsData { - dashboard_cards_at( - accounts, - summary, - buckets, - default_model, - default_service_tier, - Utc::now(), - ) + dashboard_cards_at(accounts, summary, buckets, Utc::now()) } fn dashboard_cards_at( accounts: &[Account], summary: &AdminUsageSummary, buckets: &[AdminUsageTimeBucketRecord], - default_model: &str, - default_service_tier: Option<&str>, now: DateTime, ) -> DashboardCardsData { let today_start = china_day_start(now); let yesterday_start = today_start - Duration::days(1); - let billing_context = BillingContext { - default_model, - default_service_tier, - }; - let today = usage_window(buckets, today_start, now); let yesterday = usage_window(buckets, yesterday_start, today_start); - let today_cost = cost_window(buckets, today_start, now, billing_context).unwrap_or(0.0); - let total_cost = total_cost(buckets, summary, billing_context); + let today_cost = cost_window(buckets, today_start, now).unwrap_or(0.0); + let total_cost = total_cost(buckets); let total_input = nonnegative_i64_to_u64(summary.input_tokens); let total_cached = nonnegative_i64_to_u64(summary.cached_tokens); @@ -693,13 +665,12 @@ fn cost_window( records: &[AdminUsageTimeBucketRecord], start: DateTime, end: DateTime, - context: BillingContext<'_>, ) -> Option { let mut total = 0.0; let mut has_usage = false; for record in records { if record.bucket_start >= start && record.bucket_start < end { - if let Some(cost) = bucket_cost(record, context) { + if let Some(cost) = bucket_cost(record) { total += cost; has_usage = true; } @@ -708,15 +679,11 @@ fn cost_window( has_usage.then_some(total) } -fn total_cost( - records: &[AdminUsageTimeBucketRecord], - summary: &AdminUsageSummary, - context: BillingContext<'_>, -) -> f64 { +fn total_cost(records: &[AdminUsageTimeBucketRecord]) -> f64 { let mut total = 0.0; let mut has_usage = false; for record in records { - if let Some(cost) = bucket_cost(record, context) { + if let Some(cost) = bucket_cost(record) { total += cost; has_usage = true; } @@ -725,19 +692,10 @@ fn total_cost( return total; } - let input_tokens = nonnegative_i64_to_u64(summary.input_tokens); - let output_tokens = nonnegative_i64_to_u64(summary.output_tokens); - let cached_tokens = nonnegative_i64_to_u64(summary.cached_tokens); - billing::calculate_cost( - input_tokens, - output_tokens, - cached_tokens, - context.default_model, - context.default_service_tier, - ) + 0.0 } -fn bucket_cost(record: &AdminUsageTimeBucketRecord, context: BillingContext<'_>) -> Option { +fn bucket_cost(record: &AdminUsageTimeBucketRecord) -> Option { let input_tokens = nonnegative_i64_to_u64(record.input_tokens); let output_tokens = nonnegative_i64_to_u64(record.output_tokens); let cached_tokens = nonnegative_i64_to_u64(record.cached_tokens); @@ -745,18 +703,11 @@ fn bucket_cost(record: &AdminUsageTimeBucketRecord, context: BillingContext<'_>) return None; } - let model = { - let value = record.model.trim(); - if value.is_empty() { - context.default_model - } else { - value - } - }; - let service_tier = record - .service_tier - .as_deref() - .or(context.default_service_tier); + let model = record.model.trim(); + if model.is_empty() { + return None; + } + let service_tier = record.service_tier.as_deref(); Some(billing::calculate_cost( input_tokens, @@ -1026,7 +977,7 @@ mod tests { usage_bucket_at("2026-06-24T15:30:00Z", 20), ]; - let cards = dashboard_cards_at(&[], &empty_usage_summary(), &buckets, "gpt-5.5", None, now); + let cards = dashboard_cards_at(&[], &empty_usage_summary(), &buckets, now); assert_eq!(cards.traffic.today_requests, 1); assert_eq!(cards.traffic.yesterday_requests, 1); diff --git a/src/admin/monitoring/event_store.rs b/src/admin/monitoring/event_store.rs index 3613533a4..393639732 100644 --- a/src/admin/monitoring/event_store.rs +++ b/src/admin/monitoring/event_store.rs @@ -12,6 +12,13 @@ use crate::admin::monitoring::events::{ use crate::infra::json::{decode_cursor, encode_cursor, page_offset, NumberedPage, Page}; use crate::infra::time::china_quarter_hour_start; +/// 默认启用事件日志,便于管理端诊断请求链路。 +pub const DEFAULT_EVENT_LOG_ENABLED: bool = true; +/// 默认保留最新事件日志数量。 +pub const DEFAULT_EVENT_LOG_CAPACITY: u32 = 2_000; +/// 默认不保存请求/响应体,避免把用户内容写入诊断日志。 +pub const DEFAULT_EVENT_LOG_CAPTURE_BODY: bool = false; + /// SQLite 事件日志错误。 #[derive(Debug, Error)] pub enum SqliteEventLogStoreError { diff --git a/src/admin/settings/routes.rs b/src/admin/settings/routes.rs index 4e35138a5..89cba339e 100644 --- a/src/admin/settings/routes.rs +++ b/src/admin/settings/routes.rs @@ -22,8 +22,7 @@ use crate::{ runtime::state::AppState, }; -const ALLOWED_SETTINGS_KEYS: [&str; 8] = [ - "defaultModel", +const ALLOWED_SETTINGS_KEYS: [&str; 7] = [ "modelAliases", "modelAccountRoutes", "refreshMarginSeconds", @@ -37,7 +36,6 @@ const ALLOWED_SETTINGS_KEYS: [&str; 8] = [ #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AdminSettingsData { - pub default_model: String, pub model_aliases: BTreeMap, pub model_account_routes: BTreeMap>, pub refresh_margin_seconds: u64, @@ -50,9 +48,8 @@ pub struct AdminSettingsData { impl AdminSettingsData { fn from_config(config: &AppConfig) -> Self { Self { - default_model: config.model.default_model.clone(), - model_aliases: config.model.aliases.clone(), - model_account_routes: config.model.account_routes.clone(), + model_aliases: config.model_aliases.clone(), + model_account_routes: config.model_account_routes.clone(), refresh_margin_seconds: config.auth.refresh_margin_seconds, refresh_concurrency: config.auth.refresh_concurrency, max_concurrent_per_account: config.auth.max_concurrent_per_account, diff --git a/src/config/settings.rs b/src/config/settings.rs index 7718f43d2..e9b7113b3 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -21,18 +21,10 @@ const RUNTIME_SETTINGS_ID: i64 = 1; /// 管理端可变设置。 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AdminSettings { - /// 默认模型 ID。 - pub default_model: String, - /// 默认 reasoning effort。 - pub default_reasoning_effort: Option, - /// 默认服务层级。 - pub service_tier: Option, /// 模型别名映射。 pub model_aliases: BTreeMap, /// 模型到账号 ID 的显式路由。 pub model_account_routes: BTreeMap>, - /// 是否启用访问令牌刷新。 - pub refresh_enabled: bool, /// 访问令牌过期前多少秒开始刷新。 pub refresh_margin_seconds: u64, /// 访问令牌刷新并发数。 @@ -43,65 +35,18 @@ pub struct AdminSettings { pub request_interval_ms: u64, /// 账号轮换策略。 pub rotation_strategy: String, - /// 计划类型优先级。 - pub tier_priority: Vec, - /// 配额刷新间隔分钟数。 - pub quota_refresh_interval_minutes: u64, - /// 配额预警阈值。 - pub quota_warning_thresholds: AdminQuotaWarningThresholds, - /// 配额耗尽账号是否跳过调度。 - pub quota_skip_exhausted: bool, - /// 是否启用事件日志。 - pub logs_enabled: bool, - /// 事件日志容量。 - pub logs_capacity: u32, - /// 事件日志是否捕获请求/响应体。 - pub logs_capture_body: bool, - /// 用量历史保留天数。 - pub usage_history_retention_days: Option, } impl Default for AdminSettings { fn default() -> Self { Self { - default_model: "gpt-4o".to_string(), - default_reasoning_effort: None, - service_tier: None, model_aliases: BTreeMap::new(), model_account_routes: BTreeMap::new(), - refresh_enabled: true, refresh_margin_seconds: 300, refresh_concurrency: 2, max_concurrent_per_account: 3, request_interval_ms: 50, rotation_strategy: "least_used".to_string(), - tier_priority: Vec::new(), - quota_refresh_interval_minutes: 5, - quota_warning_thresholds: AdminQuotaWarningThresholds::default(), - quota_skip_exhausted: true, - logs_enabled: false, - logs_capacity: 2_000, - logs_capture_body: false, - usage_history_retention_days: None, - } - } -} - -/// 配额预警阈值集合。 -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AdminQuotaWarningThresholds { - /// primary 配额预警百分比。 - pub primary: Vec, - /// secondary 配额预警百分比。 - pub secondary: Vec, -} - -impl Default for AdminQuotaWarningThresholds { - fn default() -> Self { - Self { - primary: vec![80, 90], - secondary: vec![80, 90], } } } @@ -110,8 +55,6 @@ impl Default for AdminQuotaWarningThresholds { #[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdminSettingsPatch { - /// 默认模型 ID。 - pub default_model: Option, /// 模型别名映射。 pub model_aliases: Option>, /// 模型到账号 ID 的显式路由。 @@ -138,9 +81,6 @@ impl SettingsService { current: &mut AdminSettings, patch: AdminSettingsPatch, ) -> Result<(), SettingsServiceError> { - if let Some(default_model) = patch.default_model { - current.default_model = non_empty_string("defaultModel", default_model)?; - } if let Some(model_aliases) = patch.model_aliases { current.model_aliases = validate_model_aliases(model_aliases)?; } @@ -395,37 +335,20 @@ pub enum RuntimeSettingsError { } fn admin_settings_from_config(config: &AppConfig) -> AdminSettings { - let defaults = AdminSettings::default(); AdminSettings { - default_model: config.model.default_model.clone(), - default_reasoning_effort: config.model.default_reasoning_effort.clone(), - service_tier: config.model.service_tier.clone(), - model_aliases: config.model.aliases.clone(), - model_account_routes: config.model.account_routes.clone(), - refresh_enabled: defaults.refresh_enabled, + model_aliases: config.model_aliases.clone(), + model_account_routes: config.model_account_routes.clone(), refresh_margin_seconds: config.auth.refresh_margin_seconds, refresh_concurrency: config.auth.refresh_concurrency, max_concurrent_per_account: config.auth.max_concurrent_per_account, request_interval_ms: config.auth.request_interval_ms, rotation_strategy: config.auth.rotation_strategy.clone(), - tier_priority: config.auth.tier_priority.clone(), - quota_refresh_interval_minutes: config.quota.refresh_interval_minutes, - quota_warning_thresholds: AdminQuotaWarningThresholds { - primary: config.quota.warning_thresholds.primary.clone(), - secondary: config.quota.warning_thresholds.secondary.clone(), - }, - quota_skip_exhausted: defaults.quota_skip_exhausted, - logs_enabled: defaults.logs_enabled, - logs_capacity: defaults.logs_capacity, - logs_capture_body: config.logging.capture_body, - usage_history_retention_days: config.usage_stats.history_retention_days, } } fn apply_admin_settings_to_config(config: &mut AppConfig, settings: AdminSettings) { - config.model.default_model = settings.default_model; - config.model.aliases = settings.model_aliases; - config.model.account_routes = settings.model_account_routes; + config.model_aliases = settings.model_aliases; + config.model_account_routes = settings.model_account_routes; config.auth.refresh_margin_seconds = settings.refresh_margin_seconds; config.auth.refresh_concurrency = settings.refresh_concurrency; config.auth.max_concurrent_per_account = settings.max_concurrent_per_account; @@ -443,7 +366,6 @@ async fn insert_runtime_settings_if_missing( r" insert or ignore into runtime_settings ( id, - default_model, model_aliases_json, refresh_margin_seconds, refresh_concurrency, @@ -451,10 +373,9 @@ insert or ignore into runtime_settings ( request_interval_ms, rotation_strategy, updated_at -) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", +) values (?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(RUNTIME_SETTINGS_ID) - .bind(&settings.default_model) .bind(model_aliases_json) .bind( i64::try_from(settings.refresh_margin_seconds) @@ -490,7 +411,6 @@ async fn save_runtime_settings( r" insert into runtime_settings ( id, - default_model, model_aliases_json, refresh_margin_seconds, refresh_concurrency, @@ -498,9 +418,8 @@ insert into runtime_settings ( request_interval_ms, rotation_strategy, updated_at -) values (?, ?, ?, ?, ?, ?, ?, ?, ?) +) values (?, ?, ?, ?, ?, ?, ?, ?) on conflict(id) do update set - default_model = excluded.default_model, model_aliases_json = excluded.model_aliases_json, refresh_margin_seconds = excluded.refresh_margin_seconds, refresh_concurrency = excluded.refresh_concurrency, @@ -510,7 +429,6 @@ on conflict(id) do update set updated_at = excluded.updated_at", ) .bind(RUNTIME_SETTINGS_ID) - .bind(&settings.default_model) .bind(model_aliases_json) .bind( i64::try_from(settings.refresh_margin_seconds) @@ -538,7 +456,6 @@ async fn load_runtime_settings(pool: &SqlitePool) -> Result Result { let model_aliases_json: String = row.get("model_aliases_json"); Ok(AdminSettings { - default_model: row.get("default_model"), model_aliases: serde_json::from_str(&model_aliases_json)?, model_account_routes: BTreeMap::new(), refresh_margin_seconds: positive_i64_to_u64( @@ -579,7 +495,6 @@ fn runtime_settings_from_row(row: &SqliteRow) -> Result AccountPoolOption AccountPoolOptions { max_concurrent_per_account: config.auth.max_concurrent_per_account, rotation_strategy: rotation_strategy_from_config(&config.auth.rotation_strategy), - skip_quota_limited: config.quota.skip_exhausted, + skip_quota_limited: true, tier_priority: config.auth.tier_priority.clone(), - model_account_routes: config.model.account_routes.clone(), + model_account_routes: config.model_account_routes.clone(), ..AccountPoolOptions::default() } } diff --git a/src/config/types.rs b/src/config/types.rs index e58a9d3cf..76d84a18f 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -4,30 +4,40 @@ use serde::{Deserialize, Serialize}; /// 应用运行总配置。 #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct AppConfig { /// HTTP 服务配置。 pub server: ServerConfig, /// API 地址配置。 pub api: ApiConfig, - /// 模型默认值配置。 - pub model: ModelConfig, - /// 认证与刷新配置。 + /// 运行时模型别名,由数据库设置加载,配置文件不承载该字段。 + #[serde(skip)] + pub model_aliases: BTreeMap, + /// 运行时模型账号路由,由 `model_account_routes` 表加载,配置文件不承载该字段。 + #[serde(skip)] + pub model_account_routes: BTreeMap>, + /// 认证与刷新配置,由固定默认值和数据库运行时设置承载,配置文件不承载该字段。 + #[serde(skip)] pub auth: AuthConfig, - /// 配额配置。 + /// 配额配置,由固定默认值承载,配置文件不承载该字段。 + #[serde(skip)] pub quota: QuotaConfig, - /// 用量统计配置。 - pub usage_stats: UsageStatsConfig, /// 数据库配置。 pub database: DatabaseConfig, /// TLS 偏好配置。 + #[serde(default)] pub tls: TlsConfig, /// WebSocket 连接池配置。 + #[serde(default)] pub ws_pool: WebSocketPoolConfig, /// 上游请求指纹默认配置。 + #[serde(default)] pub fingerprint: FingerprintConfig, /// 管理员初始化配置。 + #[serde(default)] pub admin: AdminConfig, /// 日志配置。 + #[serde(default)] pub logging: LoggingConfig, } @@ -47,23 +57,6 @@ pub struct ApiConfig { pub base_url: String, } -/// 模型默认值与别名配置。 -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -pub struct ModelConfig { - /// 默认模型名。 - #[serde(rename = "default")] - pub default_model: String, - /// 默认推理强度。 - pub default_reasoning_effort: Option, - /// 默认服务层级。 - #[serde(rename = "default_service_tier")] - pub service_tier: Option, - /// 模型别名映射。 - pub aliases: BTreeMap, - /// 模型到账号 ID 的显式路由。 - pub account_routes: BTreeMap>, -} - /// 认证、轮换与 token 续期配置。 #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct AuthConfig { @@ -83,12 +76,26 @@ pub struct AuthConfig { pub tier_priority: Vec, /// OAuth 客户端 ID。 pub oauth_client_id: String, - /// OAuth 授权端点。 - pub oauth_auth_endpoint: String, /// OAuth token 端点。 pub oauth_token_endpoint: String, } +impl Default for AuthConfig { + fn default() -> Self { + Self { + refresh_margin_seconds: 300, + refresh_enabled: true, + refresh_concurrency: 2, + max_concurrent_per_account: 3, + request_interval_ms: 50, + rotation_strategy: "least_used".to_string(), + tier_priority: Vec::new(), + oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), + oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), + } + } +} + /// 配额刷新与跳过配置。 #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct QuotaConfig { @@ -96,8 +103,15 @@ pub struct QuotaConfig { pub refresh_interval_minutes: u64, /// 预警阈值。 pub warning_thresholds: QuotaWarningThresholds, - /// 是否跳过耗尽账号。 - pub skip_exhausted: bool, +} + +impl Default for QuotaConfig { + fn default() -> Self { + Self { + refresh_interval_minutes: 5, + warning_thresholds: QuotaWarningThresholds::default(), + } + } } /// 配额预警阈值集合。 @@ -109,11 +123,13 @@ pub struct QuotaWarningThresholds { pub secondary: Vec, } -/// 用量历史保留配置。 -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -pub struct UsageStatsConfig { - /// 历史保留天数。 - pub history_retention_days: Option, +impl Default for QuotaWarningThresholds { + fn default() -> Self { + Self { + primary: vec![80, 90], + secondary: vec![80, 90], + } + } } /// 数据库连接配置。 @@ -124,7 +140,7 @@ pub struct DatabaseConfig { } /// TLS/HTTP 协议偏好配置。 -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] pub struct TlsConfig { /// 是否强制 HTTP/1.1。 pub force_http11: bool, @@ -256,6 +272,17 @@ pub struct AdminConfig { pub default_password: String, } +impl Default for AdminConfig { + fn default() -> Self { + Self { + session_ttl_minutes: 1440, + session_cleanup_interval_secs: default_session_cleanup_interval(), + default_username: default_admin_username(), + default_password: default_admin_password(), + } + } +} + fn default_session_cleanup_interval() -> u64 { 3600 // 默认每小时清理一次过期会话 } @@ -278,8 +305,14 @@ pub struct LoggingConfig { pub retention_days: usize, /// 是否启用日志。 pub enabled: bool, - /// 内存容量。 - pub capacity: u32, - /// 是否捕获请求体。 - pub capture_body: bool, +} + +impl Default for LoggingConfig { + fn default() -> Self { + Self { + directory: ".runtime/logs".to_string(), + retention_days: 14, + enabled: true, + } + } } diff --git a/src/infra/schema.sql b/src/infra/schema.sql index f26c6ad68..d6deca503 100644 --- a/src/infra/schema.sql +++ b/src/infra/schema.sql @@ -44,7 +44,6 @@ create index if not exists idx_client_api_keys_created_id on client_api_keys(cre -- ============================================ create table if not exists runtime_settings ( id integer primary key check (id = 1), - default_model text not null, model_aliases_json text not null default '{}', refresh_margin_seconds integer not null check (refresh_margin_seconds > 0), refresh_concurrency integer not null check (refresh_concurrency > 0), diff --git a/src/proxy/dispatch/chat.rs b/src/proxy/dispatch/chat.rs index b75669aea..3ae161770 100644 --- a/src/proxy/dispatch/chat.rs +++ b/src/proxy/dispatch/chat.rs @@ -85,7 +85,7 @@ impl ChatDispatchService { let catalog = self.models.catalog().await; let parsed_model = catalog.parse_model_name(requested_model); let display_model = ModelCatalog::build_display_model_name(&parsed_model); - apply_response_model_options(&mut request, &parsed_model, self.models.config()); + apply_response_model_options(&mut request, &parsed_model); let include_reasoning = request .reasoning .as_ref() diff --git a/src/proxy/dispatch/responses.rs b/src/proxy/dispatch/responses.rs index 3bce6753d..cc6742375 100644 --- a/src/proxy/dispatch/responses.rs +++ b/src/proxy/dispatch/responses.rs @@ -385,7 +385,7 @@ impl ResponseDispatchService { let started_at = Instant::now(); let catalog = self.models.catalog().await; let parsed_model = catalog.parse_model_name(requested_model); - apply_response_model_options(&mut request, &parsed_model, self.models.config()); + apply_response_model_options(&mut request, &parsed_model); let tuple_schema = request.tuple_schema.clone(); let image_generation_requested = request.expects_image_generation(); let now = Utc::now(); @@ -976,7 +976,7 @@ impl ResponseDispatchService { let started_at = Instant::now(); let catalog = self.models.catalog().await; let parsed_model = catalog.parse_model_name(requested_model); - apply_response_model_options(&mut request, &parsed_model, self.models.config()); + apply_response_model_options(&mut request, &parsed_model); request.stream = true; let tuple_schema = request.tuple_schema.clone(); let now = Utc::now(); diff --git a/src/proxy/openai/chat.rs b/src/proxy/openai/chat.rs index a42e5dedc..2c4a8e6ab 100644 --- a/src/proxy/openai/chat.rs +++ b/src/proxy/openai/chat.rs @@ -919,11 +919,7 @@ pub async fn chat_completions( return invalid_chat_completion_request_response().into_response(); } }; - apply_response_model_options( - &mut codex_request, - &parsed_model, - state.services.models.config(), - ); + apply_response_model_options(&mut codex_request, &parsed_model); let include_reasoning = codex_request .reasoning .as_ref() diff --git a/src/runtime/services.rs b/src/runtime/services.rs index d97431c31..620553dc1 100644 --- a/src/runtime/services.rs +++ b/src/runtime/services.rs @@ -4,7 +4,10 @@ use std::sync::Arc as StdArc; use crate::{ admin::monitoring::{ - event_store::{AdminLogService, SqliteEventLogStore}, + event_store::{ + AdminLogService, SqliteEventLogStore, DEFAULT_EVENT_LOG_CAPACITY, + DEFAULT_EVENT_LOG_CAPTURE_BODY, DEFAULT_EVENT_LOG_ENABLED, + }, service::AdminUsageService, usage_store::SqliteUsageStore, }, @@ -168,9 +171,9 @@ impl Services { )); let logs = StdArc::new(AdminLogService::new( stores.event_logs.clone(), - config.logging.enabled, - config.logging.capacity, - config.logging.capture_body, + DEFAULT_EVENT_LOG_ENABLED, + DEFAULT_EVENT_LOG_CAPACITY, + DEFAULT_EVENT_LOG_CAPTURE_BODY, )); let usage_store = SqliteUsageStore::new(stores.accounts.pool().clone()); let usage = StdArc::new(AdminUsageService::new(usage_store)); @@ -184,10 +187,7 @@ impl Services { ); let models = StdArc::new(ModelService::new( crate::upstream::models::ModelConfig { - default_model: config.model.default_model.clone(), - default_reasoning_effort: config.model.default_reasoning_effort.clone(), - service_tier: config.model.service_tier.clone(), - aliases: config.model.aliases.clone(), + model_aliases: config.model_aliases.clone(), }, Some(snapshot_store.clone()), Some(upstream_client.clone()), @@ -196,10 +196,7 @@ impl Services { let admin_models = StdArc::new(AdminModelService::new( StdArc::new(ModelService::new( crate::upstream::models::ModelConfig { - default_model: config.model.default_model.clone(), - default_reasoning_effort: config.model.default_reasoning_effort.clone(), - service_tier: config.model.service_tier.clone(), - aliases: config.model.aliases.clone(), + model_aliases: config.model_aliases.clone(), }, Some(snapshot_store), Some(upstream_client), diff --git a/src/runtime/state.rs b/src/runtime/state.rs index f4ca450d2..9cf4daca8 100644 --- a/src/runtime/state.rs +++ b/src/runtime/state.rs @@ -13,7 +13,6 @@ pub struct AppState { /// 运行时配置镜像(从 AppConfig 衍生)。 #[derive(Debug, Clone)] pub struct RuntimeConfig { - pub model: crate::config::types::ModelConfig, pub admin: crate::config::types::AdminConfig, pub auth: crate::config::types::AuthConfig, pub quota: crate::config::types::QuotaConfig, @@ -22,7 +21,6 @@ pub struct RuntimeConfig { impl From for RuntimeConfig { fn from(config: AppConfig) -> Self { Self { - model: config.model.clone(), admin: config.admin.clone(), auth: config.auth.clone(), quota: config.quota, diff --git a/src/upstream/models/mod.rs b/src/upstream/models/mod.rs index 1996ec964..cdbce99d6 100644 --- a/src/upstream/models/mod.rs +++ b/src/upstream/models/mod.rs @@ -16,17 +16,11 @@ use crate::upstream::accounts::{ // 数据类型 // --------------------------------------------------------------------------- -/// 模型目录默认值与别名配置。 +/// 模型目录别名配置。 #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct ModelConfig { - /// 默认模型名。 - pub default_model: String, - /// 默认推理强度。 - pub default_reasoning_effort: Option, - /// 默认服务层级。 - pub service_tier: Option, /// 模型别名映射。 - pub aliases: BTreeMap, + pub model_aliases: BTreeMap, } /// 单个 reasoning effort 的标准化展示信息。 @@ -349,14 +343,12 @@ use std::collections::BTreeSet; const SERVICE_TIER_SUFFIXES: [&str; 2] = ["fast", "flex"]; const REASONING_EFFORT_SUFFIXES: [&str; 6] = ["none", "minimal", "low", "medium", "high", "xhigh"]; -const STATIC_REASONING_EFFORTS: [&str; 4] = ["low", "medium", "high", "xhigh"]; /// 模型目录聚合。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ModelCatalog { models: Vec, - aliases: BTreeMap, - default_model: String, + model_aliases: BTreeMap, model_plan_index: BTreeMap>, } @@ -376,12 +368,10 @@ impl ModelPlanSnapshot { impl ModelCatalog { /// 从静态配置构造模型目录。 pub fn from_config(config: &ModelConfig) -> Self { - let default_model = config.default_model.trim().to_string(); - let aliases = normalize_aliases(&config.aliases); + let model_aliases = normalize_aliases(&config.model_aliases); Self { - models: vec![default_model_info(config)], - aliases, - default_model, + models: Vec::new(), + model_aliases, model_plan_index: BTreeMap::new(), } } @@ -395,8 +385,7 @@ impl ModelCatalog { return Self::from_config(config); } - let aliases = normalize_aliases(&config.aliases); - let default_model = config.default_model.trim().to_string(); + let model_aliases = normalize_aliases(&config.model_aliases); let mut models_by_id = BTreeMap::new(); let mut model_plan_index: BTreeMap> = BTreeMap::new(); @@ -417,8 +406,7 @@ impl ModelCatalog { Self { models, - aliases, - default_model, + model_aliases, model_plan_index, } } @@ -447,7 +435,7 @@ impl ModelCatalog { if trimmed.is_empty() { return false; } - if self.aliases.contains_key(trimmed) || self.model_info(trimmed).is_some() { + if self.model_aliases.contains_key(trimmed) || self.model_info(trimmed).is_some() { return true; } @@ -457,14 +445,14 @@ impl ModelCatalog { { return false; } - self.aliases.contains_key(&stripped.model_name) + self.model_aliases.contains_key(&stripped.model_name) || self.model_info(&stripped.model_name).is_some() } /// 解析外部传入的模型名,提取别名、推理强度和服务层级后缀。 pub fn parse_model_name(&self, input: &str) -> ParsedModelName { let trimmed = input.trim(); - if self.aliases.contains_key(trimmed) || self.model_info(trimmed).is_some() { + if self.model_aliases.contains_key(trimmed) || self.model_info(trimmed).is_some() { return ParsedModelName { model_id: self.resolve_model_id(trimmed), reasoning_effort: None, @@ -495,11 +483,7 @@ impl ModelCatalog { } fn resolve_model_id(&self, input: &str) -> String { - let resolved = self.resolve_alias_chain(input); - if self.model_info(&resolved).is_some() { - return resolved; - } - self.default_model.clone() + self.resolve_alias_chain(input) } fn resolve_alias_chain(&self, input: &str) -> String { @@ -507,7 +491,7 @@ impl ModelCatalog { let mut current = original.to_string(); let mut seen = BTreeSet::new(); for _ in 0..20 { - let Some(target) = self.aliases.get(¤t).map(|value| value.trim()) else { + let Some(target) = self.model_aliases.get(¤t).map(|value| value.trim()) else { return current; }; if !seen.insert(current.clone()) || seen.contains(target) { @@ -558,37 +542,6 @@ fn normalize_aliases(input: &BTreeMap) -> BTreeMap CodexModelInfo { - let id = config.default_model.trim().to_string(); - let default_reasoning_effort = config - .default_reasoning_effort - .clone() - .unwrap_or_else(|| "medium".to_string()); - CodexModelInfo { - id: id.clone(), - display_name: id, - description: "Codex default model".to_string(), - is_default: true, - supported_reasoning_efforts: STATIC_REASONING_EFFORTS - .into_iter() - .map(|effort| ReasoningEffortInfo { - reasoning_effort: effort.to_string(), - description: effort.to_string(), - }) - .collect(), - default_reasoning_effort, - input_modalities: vec!["text".to_string(), "image".to_string()], - output_modalities: vec!["text".to_string()], - supports_personality: false, - upgrade: None, - source: "static".to_string(), - context_window: None, - max_context_window: None, - max_output_tokens: None, - truncation_policy_limit: None, - } -} - fn normalize_backend_model(raw: BackendModelEntry) -> CodexModelInfo { let id = first_non_empty([raw.slug.as_deref(), raw.id.as_deref(), raw.name.as_deref()]) .unwrap_or("unknown") diff --git a/src/upstream/protocol/responses.rs b/src/upstream/protocol/responses.rs index 66cf54c54..ad4e549ef 100644 --- a/src/upstream/protocol/responses.rs +++ b/src/upstream/protocol/responses.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use crate::upstream::{ - models::{ModelConfig, ParsedModelName}, + models::ParsedModelName, protocol::{ schema::reconvert_tuple_values, sse::{parse_sse_events, SseError}, @@ -467,18 +467,13 @@ fn failure_code(value: &Value) -> Option { pub fn apply_response_model_options( request: &mut CodexResponsesRequest, parsed_model: &ParsedModelName, - config: &ModelConfig, ) { request.model = parsed_model.model_id.clone(); - apply_reasoning_options(request, parsed_model, config); - apply_service_tier_options(request, parsed_model, config); + apply_reasoning_options(request, parsed_model); + apply_service_tier_options(request, parsed_model); } -fn apply_reasoning_options( - request: &mut CodexResponsesRequest, - parsed_model: &ParsedModelName, - config: &ModelConfig, -) { +fn apply_reasoning_options(request: &mut CodexResponsesRequest, parsed_model: &ParsedModelName) { let existing_reasoning = request.reasoning.take(); let existing_object = match existing_reasoning { Some(Value::Object(object)) => object, @@ -488,8 +483,7 @@ fn apply_reasoning_options( .get("effort") .and_then(Value::as_str) .map(ToString::to_string) - .or_else(|| non_empty_string(parsed_model.reasoning_effort.as_deref())) - .or_else(|| non_empty_string(config.default_reasoning_effort.as_deref())); + .or_else(|| non_empty_string(parsed_model.reasoning_effort.as_deref())); if effort.is_none() && existing_object.is_empty() { request.reasoning = None; return; @@ -523,17 +517,12 @@ pub(crate) fn ensure_reasoning_include(request: &mut CodexResponsesRequest) { request.include = Some(vec!["reasoning.encrypted_content".to_string()]); } -fn apply_service_tier_options( - request: &mut CodexResponsesRequest, - parsed_model: &ParsedModelName, - config: &ModelConfig, -) { +fn apply_service_tier_options(request: &mut CodexResponsesRequest, parsed_model: &ParsedModelName) { request.service_tier = request .service_tier .take() .and_then(|value| non_empty_string(Some(&value))) .or_else(|| non_empty_string(parsed_model.service_tier.as_deref())) - .or_else(|| non_empty_string(config.service_tier.as_deref())) .map(normalize_service_tier_for_upstream); } diff --git a/tests/admin/accounts/mod.rs b/tests/admin/accounts/mod.rs index 320b1aa65..c8c519dc7 100644 --- a/tests/admin/accounts/mod.rs +++ b/tests/admin/accounts/mod.rs @@ -11,9 +11,8 @@ use codex_proxy_rs::{ admin::monitoring::event_store::SqliteEventLogStore, config::types::AppConfig, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, infra::database::connect_sqlite, proxy::dispatch::session_affinity::SqliteSessionAffinityStore, @@ -241,13 +240,8 @@ fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), - account_routes: BTreeMap::new(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, @@ -257,7 +251,6 @@ fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -266,10 +259,6 @@ fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: None, }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -287,8 +276,6 @@ fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/admin/auth/session_routes.rs b/tests/admin/auth/session_routes.rs index 356e14b41..d51c99acc 100644 --- a/tests/admin/auth/session_routes.rs +++ b/tests/admin/auth/session_routes.rs @@ -11,9 +11,8 @@ use codex_proxy_rs::{ admin::monitoring::event_store::SqliteEventLogStore, config::types::AppConfig, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, infra::{database::connect_sqlite, identity::hash_admin_password}, proxy::dispatch::session_affinity::SqliteSessionAffinityStore, @@ -168,13 +167,8 @@ fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: Some("high".to_string()), - service_tier: Some("flex".to_string()), - aliases, - account_routes: BTreeMap::new(), - }, + model_aliases: aliases, + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 240, refresh_enabled: true, @@ -184,7 +178,6 @@ fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: vec!["team".to_string(), "plus".to_string()], oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -193,10 +186,6 @@ fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![70, 95], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: Some(30), }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -214,8 +203,6 @@ fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/admin/keys/mod.rs b/tests/admin/keys/mod.rs index 864e56287..4d842bbf5 100644 --- a/tests/admin/keys/mod.rs +++ b/tests/admin/keys/mod.rs @@ -10,9 +10,8 @@ use codex_proxy_rs::{ admin::monitoring::event_store::SqliteEventLogStore, config::types::AppConfig, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, infra::database::connect_sqlite, proxy::dispatch::session_affinity::SqliteSessionAffinityStore, @@ -118,13 +117,8 @@ fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), - account_routes: BTreeMap::new(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, @@ -134,7 +128,6 @@ fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -143,10 +136,6 @@ fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: None, }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -164,8 +153,6 @@ fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/admin/models/routes.rs b/tests/admin/models/routes.rs index 990723341..d537e5625 100644 --- a/tests/admin/models/routes.rs +++ b/tests/admin/models/routes.rs @@ -11,9 +11,8 @@ use codex_proxy_rs::{ admin::monitoring::event_store::SqliteEventLogStore, config::types::AppConfig, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, infra::database::connect_sqlite, proxy::dispatch::session_affinity::SqliteSessionAffinityStore, @@ -207,13 +206,8 @@ fn test_config(database_url: String, base_url: String) -> AppConfig { port: 0, }, api: ApiConfig { base_url }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), - account_routes: BTreeMap::new(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, @@ -223,7 +217,6 @@ fn test_config(database_url: String, base_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -232,10 +225,6 @@ fn test_config(database_url: String, base_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: None, }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -253,8 +242,6 @@ fn test_config(database_url: String, base_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/admin/monitoring/dashboard.rs b/tests/admin/monitoring/dashboard.rs index d7909c078..d4fb8766e 100644 --- a/tests/admin/monitoring/dashboard.rs +++ b/tests/admin/monitoring/dashboard.rs @@ -213,7 +213,7 @@ async fn dashboard_summary_should_normalize_codex_model_names_for_cost() { } #[tokio::test] -async fn dashboard_summary_should_fallback_total_cost_to_usage_summary_when_logs_have_no_usage() { +async fn dashboard_summary_should_not_price_usage_summary_without_model_dimension() { let (app, _store, pool, _dir) = dashboard_test_app( "dashboard-cost-summary-fallback.sqlite", crate::support::fingerprint::test_fingerprint(), @@ -227,7 +227,7 @@ async fn dashboard_summary_should_fallback_total_cost_to_usage_summary_when_logs body["data"]["cards"]["tokens"]["totalCostUsd"] .as_f64() .unwrap(), - 3.5, + 0.0, ); } diff --git a/tests/admin/monitoring/logs_routes.rs b/tests/admin/monitoring/logs_routes.rs index 127efdc47..4055d93aa 100644 --- a/tests/admin/monitoring/logs_routes.rs +++ b/tests/admin/monitoring/logs_routes.rs @@ -14,9 +14,8 @@ use codex_proxy_rs::{ }, config::types::AppConfig, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, infra::database::connect_sqlite, proxy::dispatch::session_affinity::SqliteSessionAffinityStore, @@ -318,13 +317,8 @@ fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), - account_routes: BTreeMap::new(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, @@ -334,7 +328,6 @@ fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -343,10 +336,6 @@ fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: None, }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -364,8 +353,6 @@ fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/admin/settings/mod.rs b/tests/admin/settings/mod.rs index 0a7e45175..853aa6995 100644 --- a/tests/admin/settings/mod.rs +++ b/tests/admin/settings/mod.rs @@ -5,8 +5,6 @@ use codex_proxy_rs::config::settings::{AdminSettings, AdminSettingsPatch, Settin #[test] fn settings_service_should_apply_retained_settings_patch() { let mut current = AdminSettings { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: Some("high".to_string()), max_concurrent_per_account: 4, request_interval_ms: 50, rotation_strategy: "least_used".to_string(), @@ -16,7 +14,6 @@ fn settings_service_should_apply_retained_settings_patch() { SettingsService::apply_patch( &mut current, AdminSettingsPatch { - default_model: Some(" gpt-6 ".to_string()), model_aliases: Some( [ (" claude-sonnet ".to_string(), " gpt-5.5 ".to_string()), @@ -40,7 +37,6 @@ fn settings_service_should_apply_retained_settings_patch() { ) .expect("settings patch should be valid"); - assert_eq!(current.default_model, "gpt-6"); assert_eq!(current.model_aliases["claude-sonnet"], "gpt-5.5"); assert_eq!(current.model_aliases["openai-fast"], "openai:gpt-4o"); assert_eq!( @@ -52,7 +48,6 @@ fn settings_service_should_apply_retained_settings_patch() { assert_eq!(current.rotation_strategy, "round_robin"); assert_eq!(current.max_concurrent_per_account, 5); assert_eq!(current.request_interval_ms, 125); - assert_eq!(current.default_reasoning_effort.as_deref(), Some("high")); } #[test] diff --git a/tests/admin/settings/routes.rs b/tests/admin/settings/routes.rs index e9c218be2..46f1cd190 100644 --- a/tests/admin/settings/routes.rs +++ b/tests/admin/settings/routes.rs @@ -9,9 +9,8 @@ use codex_proxy_rs::{ admin::keys::service::SqliteClientKeyStore, admin::monitoring::event_store::SqliteEventLogStore, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, config::{settings::RuntimeSettingsService, types::AppConfig}, infra::database::connect_sqlite, @@ -60,7 +59,6 @@ async fn admin_settings_should_return_runtime_fields() { .await .unwrap(); let body = response_json(response).await; - assert_eq!(body["data"]["defaultModel"], "gpt-5.5"); assert_eq!(body["data"]["modelAliases"]["codex-fast"], "gpt-5.5"); assert_eq!(body["data"]["modelAccountRoutes"], json!({})); assert_eq!(body["data"]["refreshMarginSeconds"], 240); @@ -78,7 +76,7 @@ async fn admin_settings_update_should_require_admin_session_cookie() { .uri("/api/admin/settings") .header("content-type", "application/json") .header("x-request-id", "req_settings_update_auth") - .body(Body::from(r#"{"defaultModel":"gpt-5.5"}"#)) + .body(Body::from(r#"{"refreshMarginSeconds":300}"#)) .unwrap(), ) .await @@ -126,7 +124,7 @@ async fn admin_settings_update_should_persist_runtime_settings_to_database() { .header("x-request-id", "req_settings_update") .body(Body::from( json!({ - "defaultModel": "gpt-6", "rotationStrategy": "round_robin", + "rotationStrategy": "round_robin", "modelAliases": { "gpt-5.2": "gpt-5.5", "claude-sonnet": "gpt-5.5" @@ -147,7 +145,6 @@ async fn admin_settings_update_should_persist_runtime_settings_to_database() { .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_json(response).await; - assert_eq!(body["data"]["defaultModel"], "gpt-6"); assert_eq!(body["data"]["modelAliases"]["gpt-5.2"], "gpt-5.5"); assert_eq!( body["data"]["modelAccountRoutes"]["gpt-5.5"][0], @@ -168,25 +165,24 @@ async fn admin_settings_update_should_persist_runtime_settings_to_database() { .await .unwrap(); assert_eq!( - response_json(get_response).await["data"]["defaultModel"], - "gpt-6" + response_json(get_response).await["data"]["rotationStrategy"], + "round_robin" ); - let row: (String, String, i64, i64, i64, i64, String) = sqlx::query_as( - "select default_model, model_aliases_json, refresh_margin_seconds, refresh_concurrency, max_concurrent_per_account, request_interval_ms, rotation_strategy from runtime_settings where id = 1", + let row: (String, i64, i64, i64, i64, String) = sqlx::query_as( + "select model_aliases_json, refresh_margin_seconds, refresh_concurrency, max_concurrent_per_account, request_interval_ms, rotation_strategy from runtime_settings where id = 1", ) .fetch_one(&pool) .await .unwrap(); - let aliases: BTreeMap = serde_json::from_str(&row.1).unwrap(); - assert_eq!(row.0, "gpt-6"); + let aliases: BTreeMap = serde_json::from_str(&row.0).unwrap(); assert_eq!(aliases["gpt-5.2"], "gpt-5.5"); assert_eq!(aliases["claude-sonnet"], "gpt-5.5"); - assert_eq!(row.2, 900); - assert_eq!(row.3, 4); - assert_eq!(row.4, 7); - assert_eq!(row.5, 80); - assert_eq!(row.6, "round_robin"); + assert_eq!(row.1, 900); + assert_eq!(row.2, 4); + assert_eq!(row.3, 7); + assert_eq!(row.4, 80); + assert_eq!(row.5, "round_robin"); let route_rows: Vec<(String, String, i64)> = sqlx::query_as( "select model, account_id, priority from model_account_routes order by model, priority", ) @@ -205,10 +201,9 @@ async fn admin_settings_update_should_persist_runtime_settings_to_database() { let restarted_config = RuntimeSettingsService::load_or_initialize_config(config.clone(), &pool) .await .unwrap(); - assert_eq!(restarted_config.model.default_model, "gpt-6"); - assert_eq!(restarted_config.model.aliases["gpt-5.2"], "gpt-5.5"); + assert_eq!(restarted_config.model_aliases["gpt-5.2"], "gpt-5.5"); assert_eq!( - restarted_config.model.account_routes["gpt-5.5"], + restarted_config.model_account_routes["gpt-5.5"], vec!["acct_route_a".to_string(), "acct_route_b".to_string()] ); assert_eq!(restarted_config.auth.refresh_margin_seconds, 900); @@ -216,10 +211,6 @@ async fn admin_settings_update_should_persist_runtime_settings_to_database() { assert_eq!(restarted_config.auth.rotation_strategy, "round_robin"); assert_eq!(restarted_config.auth.max_concurrent_per_account, 7); assert_eq!(restarted_config.auth.request_interval_ms, 80); - assert_eq!( - restarted_config.model.default_reasoning_effort, - config.model.default_reasoning_effort - ); assert_eq!(restarted_config.database.url, config.database.url); } @@ -355,13 +346,8 @@ fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: Some("high".to_string()), - service_tier: Some("flex".to_string()), - aliases, - account_routes: BTreeMap::new(), - }, + model_aliases: aliases, + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 240, refresh_enabled: true, @@ -371,7 +357,6 @@ fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: vec!["team".to_string(), "plus".to_string()], oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -380,10 +365,6 @@ fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![70, 95], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: Some(30), }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -401,8 +382,6 @@ fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: true, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/config/mod.rs b/tests/config/mod.rs index 13b9ce210..ba4caed20 100644 --- a/tests/config/mod.rs +++ b/tests/config/mod.rs @@ -8,35 +8,6 @@ server: port: 8080 api: base_url: https://chatgpt.com/backend-api -model: - default: gpt-5.5 - default_reasoning_effort: null - default_service_tier: null - aliases: {} - account_routes: {} -auth: - refresh_margin_seconds: 300 - refresh_enabled: true - refresh_concurrency: 2 - max_concurrent_per_account: 3 - request_interval_ms: 50 - rotation_strategy: least_used - tier_priority: [] - oauth_client_id: app_EMoamEEZ73f0CkXaXp7hrann - oauth_auth_endpoint: https://auth.openai.com/oauth/authorize - oauth_token_endpoint: https://auth.openai.com/oauth/token -quota: - refresh_interval_minutes: 5 - warning_thresholds: - primary: - - 80 - - 90 - secondary: - - 80 - - 90 - skip_exhausted: true -usage_stats: - history_retention_days: null database: url: sqlite://.runtime/data/codex-proxy-rs.sqlite tls: @@ -97,8 +68,6 @@ logging: directory: .runtime/logs retention_days: 14 enabled: true - capacity: 2000 - capture_body: false "#; #[test] @@ -106,7 +75,7 @@ fn default_config_keeps_only_codex_backend() { let cfg: AppConfig = serde_yml::from_str(DEFAULT_CONFIG_YAML).unwrap(); assert_eq!(cfg.server.host, "0.0.0.0"); assert_eq!(cfg.api.base_url, "https://chatgpt.com/backend-api"); - assert_eq!(cfg.model.default_model, "gpt-5.5"); + assert!(cfg.model_aliases.is_empty()); assert_eq!(cfg.auth.refresh_margin_seconds, 300); assert_eq!(cfg.auth.rotation_strategy, "least_used"); assert_eq!(cfg.auth.oauth_client_id, "app_EMoamEEZ73f0CkXaXp7hrann"); @@ -133,6 +102,52 @@ fn default_config_keeps_runtime_artifacts_under_runtime_directory() { assert_eq!(cfg.logging.directory, ".runtime/logs"); } +#[test] +fn config_should_reject_runtime_model_section() { + let err = serde_yml::from_str::( + r#" +server: + host: 127.0.0.1 + port: 8080 +api: + base_url: https://chatgpt.com/backend-api +model: {} +database: + url: sqlite://.runtime/data/codex-proxy-rs.sqlite +"#, + ) + .unwrap_err(); + + assert!( + err.to_string().contains("model"), + "expected model section to be rejected, got {err}" + ); +} + +#[test] +fn config_should_reject_runtime_auth_and_quota_sections() { + for section in ["auth", "quota"] { + let yaml = format!( + r#" +server: + host: 127.0.0.1 + port: 8080 +api: + base_url: https://chatgpt.com/backend-api +{section}: {{}} +database: + url: sqlite://.runtime/data/codex-proxy-rs.sqlite +"# + ); + let err = serde_yml::from_str::(&yaml).unwrap_err(); + + assert!( + err.to_string().contains(section), + "expected {section} section to be rejected, got {err}" + ); + } +} + #[test] fn config_loader_should_read_only_config_yaml() { let dir = tempfile::tempdir().unwrap(); @@ -144,35 +159,6 @@ server: port: 8080 api: base_url: https://chatgpt.com/backend-api -model: - default: gpt-5.5 - default_reasoning_effort: null - default_service_tier: null - aliases: {} - account_routes: {} -auth: - refresh_margin_seconds: 300 - refresh_enabled: true - refresh_concurrency: 2 - max_concurrent_per_account: 3 - request_interval_ms: 50 - rotation_strategy: least_used - tier_priority: [] - oauth_client_id: app_test_client - oauth_auth_endpoint: https://auth.openai.com/oauth/authorize - oauth_token_endpoint: https://auth.example.test/oauth/token -quota: - refresh_interval_minutes: 5 - warning_thresholds: - primary: - - 80 - - 90 - secondary: - - 80 - - 90 - skip_exhausted: true -usage_stats: - history_retention_days: null database: url: sqlite://.runtime/data/codex-proxy-rs.sqlite tls: @@ -231,8 +217,6 @@ logging: directory: .runtime/logs retention_days: 14 enabled: false - capacity: 2000 - capture_body: false "#, ) .unwrap(); @@ -259,12 +243,12 @@ ws_pool: cfg.database.url, "sqlite://.runtime/data/codex-proxy-rs.sqlite" ); - assert_eq!(cfg.model.default_model, "gpt-5.5"); + assert!(cfg.model_aliases.is_empty()); assert_eq!(cfg.auth.max_concurrent_per_account, 3); - assert_eq!(cfg.auth.oauth_client_id, "app_test_client"); + assert_eq!(cfg.auth.oauth_client_id, "app_EMoamEEZ73f0CkXaXp7hrann"); assert_eq!( cfg.auth.oauth_token_endpoint, - "https://auth.example.test/oauth/token" + "https://auth.openai.com/oauth/token" ); assert!(cfg.ws_pool.enabled); assert_eq!(cfg.ws_pool.max_age_ms, 3_300_000); @@ -281,8 +265,6 @@ directory: .runtime/logs max_file_bytes: 10485760 retention_days: 14 enabled: false -capacity: 2000 -capture_body: false "#, ) .unwrap_err(); diff --git a/tests/http/web_assets/mod.rs b/tests/http/web_assets/mod.rs index ec7037e4c..fe1452d8d 100644 --- a/tests/http/web_assets/mod.rs +++ b/tests/http/web_assets/mod.rs @@ -1,12 +1,13 @@ +use std::collections::BTreeMap; + use axum::{ body::{to_bytes, Body}, http::{header, Request, StatusCode}, }; use codex_proxy_rs::{ config::types::{ - AdminConfig, ApiConfig, AppConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AppConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, infra::database::connect_sqlite, }; @@ -134,13 +135,8 @@ fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: Default::default(), - account_routes: Default::default(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 240, refresh_enabled: true, @@ -150,7 +146,6 @@ fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_id".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.invalid/token".to_string(), }, quota: QuotaConfig { @@ -159,10 +154,6 @@ fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: Some(30), }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -180,8 +171,6 @@ fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/infra/storage_schema/mod.rs b/tests/infra/storage_schema/mod.rs index 1da13bdcc..ccb893ce9 100644 --- a/tests/infra/storage_schema/mod.rs +++ b/tests/infra/storage_schema/mod.rs @@ -32,7 +32,7 @@ async fn sqlite_schema_should_persist_runtime_settings_columns() { let pool = connect_sqlite(&url).await.unwrap(); let rows: Vec<(String,)> = sqlx::query_as( - "select name from pragma_table_info('runtime_settings') where name in ('id', 'default_model', 'model_aliases_json', 'refresh_margin_seconds', 'refresh_concurrency', 'max_concurrent_per_account', 'request_interval_ms', 'rotation_strategy', 'updated_at') order by name", + "select name from pragma_table_info('runtime_settings') where name in ('id', 'model_aliases_json', 'refresh_margin_seconds', 'refresh_concurrency', 'max_concurrent_per_account', 'request_interval_ms', 'rotation_strategy', 'updated_at') order by name", ) .fetch_all(&pool) .await @@ -41,7 +41,6 @@ async fn sqlite_schema_should_persist_runtime_settings_columns() { assert_eq!( rows.into_iter().map(|row| row.0).collect::>(), [ - "default_model", "id", "max_concurrent_per_account", "model_aliases_json", @@ -518,17 +517,17 @@ async fn sqlite_schema_should_reject_invalid_runtime_settings() { let pool = connect_sqlite(&url).await.unwrap(); let invalid_id_result = sqlx::query( - "insert into runtime_settings (id, default_model, refresh_margin_seconds, refresh_concurrency, max_concurrent_per_account, request_interval_ms, rotation_strategy, updated_at) values (2, 'gpt-5.5', 300, 2, 3, 50, 'least_used', '2026-06-14T00:00:00Z')", + "insert into runtime_settings (id, refresh_margin_seconds, refresh_concurrency, max_concurrent_per_account, request_interval_ms, rotation_strategy, updated_at) values (2, 300, 2, 3, 50, 'least_used', '2026-06-14T00:00:00Z')", ) .execute(&pool) .await; let invalid_refresh_result = sqlx::query( - "insert into runtime_settings (id, default_model, refresh_margin_seconds, refresh_concurrency, max_concurrent_per_account, request_interval_ms, rotation_strategy, updated_at) values (1, 'gpt-5.5', 0, 2, 3, 50, 'least_used', '2026-06-14T00:00:00Z')", + "insert into runtime_settings (id, refresh_margin_seconds, refresh_concurrency, max_concurrent_per_account, request_interval_ms, rotation_strategy, updated_at) values (1, 0, 2, 3, 50, 'least_used', '2026-06-14T00:00:00Z')", ) .execute(&pool) .await; let invalid_strategy_result = sqlx::query( - "insert into runtime_settings (id, default_model, refresh_margin_seconds, refresh_concurrency, max_concurrent_per_account, request_interval_ms, rotation_strategy, updated_at) values (1, 'gpt-5.5', 300, 2, 3, 50, 'random', '2026-06-14T00:00:00Z')", + "insert into runtime_settings (id, refresh_margin_seconds, refresh_concurrency, max_concurrent_per_account, request_interval_ms, rotation_strategy, updated_at) values (1, 300, 2, 3, 50, 'random', '2026-06-14T00:00:00Z')", ) .execute(&pool) .await; diff --git a/tests/proxy/dispatch/chat_upstream/mod.rs b/tests/proxy/dispatch/chat_upstream/mod.rs index 65442efad..c759c7d55 100644 --- a/tests/proxy/dispatch/chat_upstream/mod.rs +++ b/tests/proxy/dispatch/chat_upstream/mod.rs @@ -16,14 +16,13 @@ use codex_proxy_rs::{ admin::auth::service::SqliteAdminSessionStore, admin::keys::service::SqliteClientKeyStore, admin::monitoring::{ - event_store::{EventLogFilter, SqliteEventLogStore}, + event_store::{AdminLogStateUpdate, EventLogFilter, SqliteEventLogStore}, events::EventLevel, }, config::types::AppConfig, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, http::router, infra::database::connect_sqlite, @@ -300,6 +299,7 @@ async fn test_app_without_accounts(base_url: String) -> (axum::Router, String, t let url = format!("sqlite://{}", db.display()); let pool = connect_sqlite(&url).await.unwrap(); let api_key = insert_client_api_key(&pool).await; + insert_model_snapshot(&pool).await; let state = test_app_state_with_pool_and_installation_id( test_config(url, base_url), pool, @@ -323,14 +323,70 @@ async fn test_app_with_account_pool_and_logging( .await } +async fn test_app_with_account_pool_and_disabled_logging( + base_url: String, +) -> (axum::Router, String, SqlitePool, tempfile::TempDir) { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join("openai-record-affinity.sqlite"); + let url = format!("sqlite://{}", db.display()); + let pool = connect_sqlite(&url).await.unwrap(); + let api_key = insert_client_api_key(&pool).await; + insert_account(&pool).await; + let state = test_app_state_with_pool_and_installation_id( + test_config(url, base_url), + pool.clone(), + TEST_INSTALLATION_ID.to_string(), + ); + state + .services + .account_pool + .restore_from_repository() + .await + .expect("account pool should restore"); + state + .services + .logs + .update_state(AdminLogStateUpdate { + enabled: Some(false), + ..AdminLogStateUpdate::default() + }) + .await + .expect("event log should be disabled"); + (router::router().with_state(state), api_key, pool, dir) +} + async fn test_app_with_account_pool_and_logging_capture_body( base_url: String, ) -> (axum::Router, String, SqlitePool, tempfile::TempDir) { - test_app_with_account_pool_config(base_url, |config| { - config.logging.enabled = true; - config.logging.capture_body = true; - }) - .await + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join("openai-record-affinity.sqlite"); + let url = format!("sqlite://{}", db.display()); + let pool = connect_sqlite(&url).await.unwrap(); + let api_key = insert_client_api_key(&pool).await; + insert_account(&pool).await; + let mut config = test_config(url, base_url); + config.logging.enabled = true; + let state = test_app_state_with_pool_and_installation_id( + config, + pool.clone(), + TEST_INSTALLATION_ID.to_string(), + ); + state + .services + .account_pool + .restore_from_repository() + .await + .expect("account pool should restore"); + state + .services + .logs + .update_state(AdminLogStateUpdate { + capture_body: Some(true), + ..AdminLogStateUpdate::default() + }) + .await + .expect("log capture body should be enabled"); + (router::router().with_state(state), api_key, pool, dir) } async fn test_app_with_account_pool_config( @@ -579,6 +635,7 @@ async fn update_admin_account_status(app: &axum::Router, account_id: &str, statu } async fn insert_account(pool: &SqlitePool) { + insert_model_snapshot(pool).await; sqlx::query( "insert into accounts (id, email, chatgpt_account_id, chatgpt_user_id, access_token, access_token_expires_at, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", ) @@ -596,12 +653,45 @@ async fn insert_account(pool: &SqlitePool) { .unwrap(); } +async fn insert_model_snapshot(pool: &SqlitePool) { + let models_json = json!([ + { + "id": "gpt-5.5", + "displayName": "GPT 5.5", + "description": "Test model", + "isDefault": false, + "supportedReasoningEfforts": [ + {"reasoningEffort": "low", "description": "low"}, + {"reasoningEffort": "medium", "description": "medium"}, + {"reasoningEffort": "high", "description": "high"} + ], + "defaultReasoningEffort": "medium", + "inputModalities": ["text", "image"], + "outputModalities": ["text"], + "supportsPersonality": false, + "upgrade": null, + "source": "test" + } + ]) + .to_string(); + sqlx::query( + "insert or replace into model_plan_snapshots (plan_type, models_json, fetched_at) values (?, ?, ?)", + ) + .bind("plus") + .bind(models_json) + .bind("2026-06-18T00:00:00Z") + .execute(pool) + .await + .unwrap(); +} + async fn insert_named_account( pool: &SqlitePool, id: &str, access_token: &str, chatgpt_account_id: &str, ) { + insert_model_snapshot(pool).await; sqlx::query( "insert into accounts (id, email, chatgpt_account_id, chatgpt_user_id, access_token, access_token_expires_at, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", ) @@ -1566,13 +1656,8 @@ fn test_config(database_url: String, base_url: String) -> AppConfig { port: 0, }, api: ApiConfig { base_url }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), - account_routes: BTreeMap::new(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, @@ -1582,7 +1667,6 @@ fn test_config(database_url: String, base_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -1591,10 +1675,6 @@ fn test_config(database_url: String, base_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: None, }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -1612,8 +1692,6 @@ fn test_config(database_url: String, base_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/proxy/dispatch/chat_upstream/usage_logging.rs b/tests/proxy/dispatch/chat_upstream/usage_logging.rs index 63e55a9c2..508f0f01d 100644 --- a/tests/proxy/dispatch/chat_upstream/usage_logging.rs +++ b/tests/proxy/dispatch/chat_upstream/usage_logging.rs @@ -99,7 +99,8 @@ async fn responses_should_skip_event_log_when_logging_disabled() { .mount(&server) .await; - let (app, api_key, pool, _dir) = test_app_with_account_and_pool(server.uri()).await; + let (app, api_key, pool, _dir) = + test_app_with_account_pool_and_disabled_logging(server.uri()).await; let response = app .oneshot( Request::builder() diff --git a/tests/proxy/openai/models_routes.rs b/tests/proxy/openai/models_routes.rs index 1ee631bd4..b19397a51 100644 --- a/tests/proxy/openai/models_routes.rs +++ b/tests/proxy/openai/models_routes.rs @@ -10,9 +10,8 @@ use codex_proxy_rs::{ admin::monitoring::event_store::SqliteEventLogStore, config::types::AppConfig, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, infra::database::connect_sqlite, proxy::dispatch::session_affinity::SqliteSessionAffinityStore, @@ -121,13 +120,8 @@ fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), - account_routes: BTreeMap::new(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, @@ -137,7 +131,6 @@ fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -146,10 +139,6 @@ fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: None, }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -167,8 +156,6 @@ fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/proxy/openai/responses_routes.rs b/tests/proxy/openai/responses_routes.rs index 853b6bc68..989a23a56 100644 --- a/tests/proxy/openai/responses_routes.rs +++ b/tests/proxy/openai/responses_routes.rs @@ -10,9 +10,8 @@ use codex_proxy_rs::{ admin::monitoring::event_store::SqliteEventLogStore, config::types::AppConfig, config::types::{ - AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }, infra::database::connect_sqlite, proxy::dispatch::session_affinity::SqliteSessionAffinityStore, @@ -140,6 +139,7 @@ async fn test_app_with_client_api_key() -> (axum::Router, String, tempfile::Temp let url = format!("sqlite://{}", db.display()); let pool = connect_sqlite(&url).await.unwrap(); let plaintext = insert_client_api_key(&pool).await; + insert_model_snapshot(&pool).await; let config = test_config(url); let stores = BackgroundTaskStores { accounts: SqliteAccountStore::new(pool.clone()), @@ -164,6 +164,20 @@ async fn test_app_with_client_api_key() -> (axum::Router, String, tempfile::Temp ) } +async fn insert_model_snapshot(pool: &SqlitePool) { + sqlx::query( + r#"insert or replace into model_plan_snapshots (plan_type, models_json, fetched_at) values (?, ?, ?)"#, + ) + .bind("plus") + .bind( + r#"[{"id":"gpt-5.5","displayName":"GPT 5.5","description":"Test model","isDefault":false,"supportedReasoningEfforts":[{"reasoningEffort":"medium","description":"medium"}],"defaultReasoningEffort":"medium","inputModalities":["text"],"outputModalities":["text"],"supportsPersonality":false,"upgrade":null,"source":"test"}]"#, + ) + .bind("2026-06-18T00:00:00Z") + .execute(pool) + .await + .unwrap(); +} + async fn insert_client_api_key(pool: &SqlitePool) -> String { SqliteClientKeyStore::new(pool.clone()) .create("test") @@ -186,13 +200,8 @@ fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), - account_routes: BTreeMap::new(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, @@ -202,7 +211,6 @@ fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -211,10 +219,6 @@ fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: None, }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -232,8 +236,6 @@ fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/runtime/tasks/model_refresh.rs b/tests/runtime/tasks/model_refresh.rs index 96b02267d..8c677e897 100644 --- a/tests/runtime/tasks/model_refresh.rs +++ b/tests/runtime/tasks/model_refresh.rs @@ -4,10 +4,7 @@ use super::*; async fn model_refresh_task_should_start_and_shutdown() { let model_service = Arc::new(ModelService::new( ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: Default::default(), + model_aliases: Default::default(), }, None, None, diff --git a/tests/support/config.rs b/tests/support/config.rs index fc804c0f9..6e3c70cba 100644 --- a/tests/support/config.rs +++ b/tests/support/config.rs @@ -1,9 +1,8 @@ use std::collections::BTreeMap; use codex_proxy_rs::config::types::{ - AdminConfig, ApiConfig, AppConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, - QuotaConfig, QuotaWarningThresholds, ServerConfig, TlsConfig, UsageStatsConfig, - WebSocketPoolConfig, + AdminConfig, ApiConfig, AppConfig, AuthConfig, DatabaseConfig, LoggingConfig, QuotaConfig, + QuotaWarningThresholds, ServerConfig, TlsConfig, WebSocketPoolConfig, }; pub fn test_config(database_url: String) -> AppConfig { @@ -15,13 +14,8 @@ pub fn test_config(database_url: String) -> AppConfig { api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), }, - model: ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), - account_routes: BTreeMap::new(), - }, + model_aliases: BTreeMap::new(), + model_account_routes: BTreeMap::new(), auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, @@ -31,7 +25,6 @@ pub fn test_config(database_url: String) -> AppConfig { rotation_strategy: "least_used".to_string(), tier_priority: Vec::new(), oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(), - oauth_auth_endpoint: "https://auth.openai.com/oauth/authorize".to_string(), oauth_token_endpoint: "https://auth.openai.com/oauth/token".to_string(), }, quota: QuotaConfig { @@ -40,10 +33,6 @@ pub fn test_config(database_url: String) -> AppConfig { primary: vec![80, 90], secondary: vec![80, 90], }, - skip_exhausted: true, - }, - usage_stats: UsageStatsConfig { - history_retention_days: None, }, database: DatabaseConfig { url: database_url }, tls: TlsConfig { @@ -61,8 +50,6 @@ pub fn test_config(database_url: String) -> AppConfig { directory: "logs".to_string(), retention_days: 14, enabled: false, - capacity: 2_000, - capture_body: false, }, } } diff --git a/tests/upstream/models/catalog.rs b/tests/upstream/models/catalog.rs index 816d59284..c434283fc 100644 --- a/tests/upstream/models/catalog.rs +++ b/tests/upstream/models/catalog.rs @@ -17,14 +17,9 @@ use codex_proxy_rs::upstream::transport::{ #[test] fn model_catalog_should_parse_alias_reasoning_and_service_tier_suffixes() { - let mut aliases = BTreeMap::new(); - aliases.insert("codex-fast".to_string(), "gpt-5.5".to_string()); - let catalog = ModelCatalog::from_config(&ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases, - }); + let mut model_aliases = BTreeMap::new(); + model_aliases.insert("codex-fast".to_string(), "gpt-5.5".to_string()); + let catalog = ModelCatalog::from_config(&ModelConfig { model_aliases }); let parsed = catalog.parse_model_name("codex-fast-high-flex"); @@ -39,27 +34,21 @@ fn model_catalog_should_parse_alias_reasoning_and_service_tier_suffixes() { } #[test] -fn model_catalog_should_fall_back_to_default_for_unknown_model_names() { +fn model_catalog_should_keep_unknown_model_names_without_fallback() { let catalog = ModelCatalog::from_config(&ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), + model_aliases: BTreeMap::new(), }); let parsed = catalog.parse_model_name("unknown-medium"); - assert_eq!(parsed.model_id, "gpt-5.5"); + assert_eq!(parsed.model_id, "unknown"); assert_eq!(parsed.reasoning_effort, Some("medium".to_string())); } #[test] fn model_catalog_should_parse_none_and_minimal_reasoning_suffixes() { let catalog = ModelCatalog::from_config(&ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), + model_aliases: BTreeMap::new(), }); let none = catalog.parse_model_name("gpt-5.5-none"); @@ -98,10 +87,7 @@ fn model_catalog_should_merge_backend_snapshots_and_build_plan_allowlist() { ); let catalog = ModelCatalog::from_config_and_snapshots( &ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), + model_aliases: BTreeMap::new(), }, &[snapshot], ); @@ -122,13 +108,12 @@ fn model_catalog_should_merge_backend_snapshots_and_build_plan_allowlist() { } #[tokio::test] -async fn model_service_should_fall_back_to_static_catalog_when_snapshot_store_is_missing() { +async fn model_service_should_return_empty_catalog_when_snapshot_store_is_missing() { let service = ModelService::new(test_model_config(), None, None, None); let catalog = service.catalog().await; - assert_eq!(catalog.models().len(), 1); - assert_eq!(catalog.models()[0].id, "gpt-5.5"); + assert!(catalog.models().is_empty()); } #[tokio::test] @@ -205,10 +190,7 @@ async fn model_service_should_return_no_accounts_when_no_active_plans_exist() { fn test_model_config() -> ModelConfig { ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: None, - service_tier: None, - aliases: BTreeMap::new(), + model_aliases: BTreeMap::new(), } } diff --git a/tests/upstream/protocol/mod.rs b/tests/upstream/protocol/mod.rs index d689a4271..2de011830 100644 --- a/tests/upstream/protocol/mod.rs +++ b/tests/upstream/protocol/mod.rs @@ -2,7 +2,7 @@ use codex_proxy_rs::proxy::openai::responses::{ response_failed_sse_event, translate_response_to_codex, translate_response_to_compact, OpenAiResponsesRequest, }; -use codex_proxy_rs::upstream::models::{ModelConfig, ParsedModelName}; +use codex_proxy_rs::upstream::models::ParsedModelName; use codex_proxy_rs::upstream::protocol::events::{ cooldown_with_jitter, extract_sse_usage, extract_usage, parse_rate_limit_headers, parse_rate_limits_event, rate_limit_quota, retry_after_seconds_from_body, RateLimitWindow, diff --git a/tests/upstream/protocol/openai_responses.rs b/tests/upstream/protocol/openai_responses.rs index 08db6c129..94b53d67e 100644 --- a/tests/upstream/protocol/openai_responses.rs +++ b/tests/upstream/protocol/openai_responses.rs @@ -319,14 +319,7 @@ fn codex_responses_model_options_should_apply_suffix_defaults_and_include_reason reasoning_effort: Some("high".to_string()), service_tier: Some("fast".to_string()), }; - let config = ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: Some("medium".to_string()), - service_tier: Some("flex".to_string()), - aliases: Default::default(), - }; - - apply_response_model_options(&mut request, &parsed, &config); + apply_response_model_options(&mut request, &parsed); assert_eq!(request.model, "gpt-5.5"); assert_eq!( @@ -351,14 +344,7 @@ fn codex_responses_model_options_should_preserve_client_include_and_normalize_bo reasoning_effort: Some("low".to_string()), service_tier: Some("flex".to_string()), }; - let config = ModelConfig { - default_model: "gpt-5.5".to_string(), - default_reasoning_effort: Some("medium".to_string()), - service_tier: Some("flex".to_string()), - aliases: Default::default(), - }; - - apply_response_model_options(&mut request, &parsed, &config); + apply_response_model_options(&mut request, &parsed); assert_eq!( request.reasoning, diff --git a/web/src/views/accounts/composables/useAccountConnectionTest.ts b/web/src/views/accounts/composables/useAccountConnectionTest.ts index a83cc441a..bfa6ef6ba 100644 --- a/web/src/views/accounts/composables/useAccountConnectionTest.ts +++ b/web/src/views/accounts/composables/useAccountConnectionTest.ts @@ -135,7 +135,7 @@ export function useAccountConnectionTest() { function handleConnectionTestEvent(event: any) { if (event.type === 'test_start') { connectionTestModel.value = event.model || connectionTestModel.value - appendConnectionTestLog(`开始测试 ${connectionTestModel.value || '默认模型'}`, 'info') + appendConnectionTestLog(`开始测试 ${connectionTestModel.value || '未选择模型'}`, 'info') return } if (event.type === 'request') { diff --git a/web/src/views/settings/index.vue b/web/src/views/settings/index.vue index e23e6f625..0481e0ed8 100644 --- a/web/src/views/settings/index.vue +++ b/web/src/views/settings/index.vue @@ -1,6 +1,6 @@