mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
refactor: tighten runtime configuration
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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` 中配置。
|
||||
|
||||
## 开发验证
|
||||
|
||||
后端:
|
||||
|
||||
-31
@@ -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
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Utc>,
|
||||
) -> 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<Utc>,
|
||||
end: DateTime<Utc>,
|
||||
context: BillingContext<'_>,
|
||||
) -> Option<f64> {
|
||||
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<f64> {
|
||||
fn bucket_cost(record: &AdminUsageTimeBucketRecord) -> Option<f64> {
|
||||
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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String, String>,
|
||||
pub model_account_routes: BTreeMap<String, Vec<String>>,
|
||||
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,
|
||||
|
||||
+8
-93
@@ -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<String>,
|
||||
/// 默认服务层级。
|
||||
pub service_tier: Option<String>,
|
||||
/// 模型别名映射。
|
||||
pub model_aliases: BTreeMap<String, String>,
|
||||
/// 模型到账号 ID 的显式路由。
|
||||
pub model_account_routes: BTreeMap<String, Vec<String>>,
|
||||
/// 是否启用访问令牌刷新。
|
||||
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<String>,
|
||||
/// 配额刷新间隔分钟数。
|
||||
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<u64>,
|
||||
}
|
||||
|
||||
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<u8>,
|
||||
/// secondary 配额预警百分比。
|
||||
pub secondary: Vec<u8>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
/// 模型别名映射。
|
||||
pub model_aliases: Option<BTreeMap<String, String>>,
|
||||
/// 模型到账号 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<AdminSettings, Runti
|
||||
let row = sqlx::query(
|
||||
r"
|
||||
select
|
||||
default_model,
|
||||
model_aliases_json,
|
||||
refresh_margin_seconds,
|
||||
refresh_concurrency,
|
||||
@@ -559,7 +476,6 @@ where id = ?",
|
||||
fn runtime_settings_from_row(row: &SqliteRow) -> Result<AdminSettings, RuntimeSettingsError> {
|
||||
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<AdminSettings, RuntimeSe
|
||||
row.get("request_interval_ms"),
|
||||
)?,
|
||||
rotation_strategy: validate_rotation_strategy(row.get("rotation_strategy"))?,
|
||||
..AdminSettings::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -643,9 +558,9 @@ pub fn account_pool_options_from_config(config: &AppConfig) -> 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()
|
||||
}
|
||||
}
|
||||
|
||||
+70
-37
@@ -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<String, String>,
|
||||
/// 运行时模型账号路由,由 `model_account_routes` 表加载,配置文件不承载该字段。
|
||||
#[serde(skip)]
|
||||
pub model_account_routes: BTreeMap<String, Vec<String>>,
|
||||
/// 认证与刷新配置,由固定默认值和数据库运行时设置承载,配置文件不承载该字段。
|
||||
#[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<String>,
|
||||
/// 默认服务层级。
|
||||
#[serde(rename = "default_service_tier")]
|
||||
pub service_tier: Option<String>,
|
||||
/// 模型别名映射。
|
||||
pub aliases: BTreeMap<String, String>,
|
||||
/// 模型到账号 ID 的显式路由。
|
||||
pub account_routes: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// 认证、轮换与 token 续期配置。
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct AuthConfig {
|
||||
@@ -83,12 +76,26 @@ pub struct AuthConfig {
|
||||
pub tier_priority: Vec<String>,
|
||||
/// 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<u8>,
|
||||
}
|
||||
|
||||
/// 用量历史保留配置。
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct UsageStatsConfig {
|
||||
/// 历史保留天数。
|
||||
pub history_retention_days: Option<u64>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
+9
-12
@@ -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),
|
||||
|
||||
@@ -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<AppConfig> for RuntimeConfig {
|
||||
fn from(config: AppConfig) -> Self {
|
||||
Self {
|
||||
model: config.model.clone(),
|
||||
admin: config.admin.clone(),
|
||||
auth: config.auth.clone(),
|
||||
quota: config.quota,
|
||||
|
||||
+13
-60
@@ -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<String>,
|
||||
/// 默认服务层级。
|
||||
pub service_tier: Option<String>,
|
||||
/// 模型别名映射。
|
||||
pub aliases: BTreeMap<String, String>,
|
||||
pub model_aliases: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// 单个 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<CodexModelInfo>,
|
||||
aliases: BTreeMap<String, String>,
|
||||
default_model: String,
|
||||
model_aliases: BTreeMap<String, String>,
|
||||
model_plan_index: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -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<String, Vec<String>> = 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<String, String>) -> BTreeMap<String, Strin
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn default_model_info(config: &ModelConfig) -> 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")
|
||||
|
||||
@@ -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<String> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+4
-17
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<String, String> = serde_json::from_str(&row.1).unwrap();
|
||||
assert_eq!(row.0, "gpt-6");
|
||||
let aliases: BTreeMap<String, String> = 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+50
-68
@@ -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::<AppConfig>(
|
||||
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::<AppConfig>(&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();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<Vec<_>>(),
|
||||
[
|
||||
"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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+4
-17
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Cpu, Gauge, GitBranch, Plus, RefreshCw, Save, Timer, Trash2, Zap } from '@lucide/vue'
|
||||
import { Gauge, GitBranch, Plus, RefreshCw, Save, Timer, Trash2, Zap } from '@lucide/vue'
|
||||
|
||||
import { getSettings, updateSettings } from '@/api'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
@@ -17,7 +17,6 @@ interface AliasRow {
|
||||
}
|
||||
|
||||
interface SettingsForm {
|
||||
defaultModel: string
|
||||
modelAliases: Record<string, string>
|
||||
refreshMarginSeconds: number
|
||||
refreshConcurrency: number
|
||||
@@ -32,7 +31,6 @@ const saving = ref(false)
|
||||
const aliasError = ref('')
|
||||
|
||||
const form = reactive<SettingsForm>({
|
||||
defaultModel: '',
|
||||
modelAliases: {},
|
||||
refreshMarginSeconds: 300,
|
||||
refreshConcurrency: 2,
|
||||
@@ -86,42 +84,41 @@ const refreshConcurrencyValue = numericModel('refreshConcurrency')
|
||||
const maxConcurrentPerAccountValue = numericModel('maxConcurrentPerAccount')
|
||||
const requestIntervalMsValue = numericModel('requestIntervalMs')
|
||||
|
||||
function aliasesToRows(aliases: Record<string, string> = {}) {
|
||||
return Object.entries(aliases)
|
||||
function modelAliasesToRows(modelAliases: Record<string, string> = {}) {
|
||||
return Object.entries(modelAliases)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([alias, target]) => ({ alias, target }))
|
||||
}
|
||||
|
||||
function rowsToAliases(rows: AliasRow[]) {
|
||||
const aliases: Record<string, string> = {}
|
||||
const modelAliases: Record<string, string> = {}
|
||||
for (const row of rows) {
|
||||
const alias = row.alias.trim()
|
||||
const target = row.target.trim()
|
||||
if (!alias && !target) continue
|
||||
if (!alias || !target) {
|
||||
return { aliases: {}, error: '别名和目标模型需要同时填写' }
|
||||
return { modelAliases: {}, error: '别名和目标模型需要同时填写' }
|
||||
}
|
||||
if (alias === target) {
|
||||
return { aliases: {}, error: '别名不能指向自身' }
|
||||
return { modelAliases: {}, error: '别名不能指向自身' }
|
||||
}
|
||||
if (aliases[alias] !== undefined) {
|
||||
return { aliases: {}, error: `别名重复:${alias}` }
|
||||
if (modelAliases[alias] !== undefined) {
|
||||
return { modelAliases: {}, error: `别名重复:${alias}` }
|
||||
}
|
||||
aliases[alias] = target
|
||||
modelAliases[alias] = target
|
||||
}
|
||||
|
||||
return { aliases, error: '' }
|
||||
return { modelAliases, error: '' }
|
||||
}
|
||||
|
||||
function applySettings(data: any) {
|
||||
form.defaultModel = data.defaultModel || ''
|
||||
form.modelAliases = data.modelAliases || {}
|
||||
form.refreshMarginSeconds = Number(data.refreshMarginSeconds ?? 300)
|
||||
form.refreshConcurrency = Number(data.refreshConcurrency ?? 2)
|
||||
form.maxConcurrentPerAccount = Number(data.maxConcurrentPerAccount ?? 3)
|
||||
form.requestIntervalMs = Number(data.requestIntervalMs ?? 50)
|
||||
form.rotationStrategy = (data.rotationStrategy || 'least_used') as RotationStrategy
|
||||
aliasRows.value = aliasesToRows(form.modelAliases)
|
||||
aliasRows.value = modelAliasesToRows(form.modelAliases)
|
||||
aliasError.value = ''
|
||||
}
|
||||
|
||||
@@ -175,8 +172,7 @@ async function handleSave() {
|
||||
try {
|
||||
saving.value = true
|
||||
const data = await updateSettings({
|
||||
defaultModel: form.defaultModel,
|
||||
modelAliases: aliasResult.aliases,
|
||||
modelAliases: aliasResult.modelAliases,
|
||||
refreshMarginSeconds: form.refreshMarginSeconds,
|
||||
refreshConcurrency: form.refreshConcurrency,
|
||||
maxConcurrentPerAccount: form.maxConcurrentPerAccount,
|
||||
@@ -233,22 +229,11 @@ onMounted(loadSettings)
|
||||
v-loading="loading"
|
||||
:padded="false"
|
||||
title="运行参数"
|
||||
description="默认模型、请求节奏和 Token 刷新。"
|
||||
description="请求节奏、账号并发和 Token 刷新。"
|
||||
header-class="px-5 pt-4"
|
||||
body-class="px-5 py-5"
|
||||
>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<span class="text-xs leading-[1.15] font-bold text-(--cp-text-secondary)">
|
||||
默认模型
|
||||
</span>
|
||||
<BaseInput v-model="form.defaultModel">
|
||||
<template #prefix>
|
||||
<Cpu class="size-4" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<span class="text-xs leading-[1.15] font-bold text-(--cp-text-secondary)">
|
||||
单账号最大并发
|
||||
@@ -408,7 +393,7 @@ onMounted(loadSettings)
|
||||
<span class="text-[14px] leading-[1.15] font-[760]">{{ option.label }}</span>
|
||||
</span>
|
||||
<span
|
||||
class="mt-2 block text-[13px] leading-[1.5] font-[650] text-(--cp-text-secondary)"
|
||||
class="mt-2 block text-[13px] leading-normal font-[650] text-(--cp-text-secondary)"
|
||||
>
|
||||
{{ option.description }}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user