mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
refactor: migrate backend to Rust 1.97 and edition 2024
Unify the local, CI, release, and container toolchains on Rust 1.97. Move update configuration, release caching, and event delivery into service-owned state, apply Edition 2024 idioms, and refresh repository ignore rules and documentation. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
warnings = "deny"
|
||||
@@ -24,7 +24,6 @@ jobs:
|
||||
CARGO_TERM_COLOR: always
|
||||
CPR_TEST_DATABASE_URL: postgres://codex_proxy:codex_proxy@127.0.0.1:5432/codex_proxy
|
||||
CPR_TEST_REDIS_URL: redis://127.0.0.1:6379
|
||||
RUSTFLAGS: -D warnings
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-bookworm@sha256:d9c83446333daec3f0588cc709adb80c26090b7f9f0f7ec8d43c243385d79818
|
||||
@@ -56,7 +55,7 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@eac0f66a48bc4b70a10b9acd4c4e930f835d95ff # 1.95
|
||||
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
|
||||
@@ -71,7 +70,7 @@ jobs:
|
||||
|
||||
- name: Rust clippy
|
||||
working-directory: backend
|
||||
run: cargo clippy --all-targets --all-features --locked -- -D warnings
|
||||
run: cargo clippy --all-targets --all-features --locked
|
||||
|
||||
- name: Rust tests
|
||||
working-directory: backend
|
||||
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
-v "$HOME/.cargo:/cargo" \
|
||||
-v "$PWD/..:/workspace" \
|
||||
-w /workspace/backend \
|
||||
rust:1.95-bookworm@sha256:6258907abe69656e41cd992e0b705cdcfabcbbe3db374f92ed2d47121282d4a1 \
|
||||
rust:1.97-bookworm@sha256:7d0723df719e7f213b69dc7c8c595985c3f4b060cfbee4f7bc0e347a86fe3b6a \
|
||||
cargo build --release --locked --target "${{ matrix.target }}" --bin codex-proxy-rs
|
||||
cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/${{ matrix.binary }}"
|
||||
working-directory: backend
|
||||
@@ -239,7 +239,7 @@ jobs:
|
||||
ref: ${{ needs.release-info.outputs.git_sha }}
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@eac0f66a48bc4b70a10b9acd4c4e930f835d95ff # 1.95
|
||||
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@eac0f66a48bc4b70a10b9acd4c4e930f835d95ff # 1.95
|
||||
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
|
||||
|
||||
- name: Cache Rust
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
|
||||
+36
-25
@@ -1,43 +1,54 @@
|
||||
# Rust build output.
|
||||
# Rust
|
||||
/backend/target/
|
||||
/target/
|
||||
/dist/
|
||||
/package/
|
||||
/checksums.txt
|
||||
/codex-proxy-rs_*.tar.gz
|
||||
/codex-proxy-rs_*.zip
|
||||
*.profraw
|
||||
*.profdata
|
||||
|
||||
# Frontend dependencies and build output.
|
||||
# Frontend
|
||||
/frontend/node_modules/
|
||||
/frontend/dist/
|
||||
.pnpm-store
|
||||
/frontend/.vite/
|
||||
/.pnpm-store/
|
||||
/frontend/.pnpm-store/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Application runtime state.
|
||||
*.runtime
|
||||
# Release artifacts
|
||||
/dist/
|
||||
|
||||
# Runtime data
|
||||
/.runtime/
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.sqlite-*
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
*.log
|
||||
|
||||
# Local configuration.
|
||||
/local.yaml
|
||||
/local.yml
|
||||
/.env
|
||||
# Local configuration and secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!**/.env.example
|
||||
/.envrc
|
||||
/config.yaml
|
||||
/backend/config.yaml
|
||||
/deploy/.env
|
||||
|
||||
# Local workspaces and tooling state.
|
||||
# Editors and operating systems
|
||||
/.idea/
|
||||
/.vscode/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Local tools and worktrees
|
||||
/.codegraph/
|
||||
/.codex-ws-audit/
|
||||
/.agents/
|
||||
/.reasonix/
|
||||
/.worktrees/
|
||||
/worktrees/
|
||||
/.codex-ws-audit/
|
||||
/.codegraph/
|
||||
/.cargo/*
|
||||
!/.cargo/config.toml
|
||||
*.tmp
|
||||
*.swp
|
||||
*~
|
||||
*.codegraph
|
||||
/.agents/
|
||||
/.cargo/
|
||||
/.reasonix/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h1>Codex Proxy RS</h1>
|
||||
<p>OpenAI Responses 兼容的 Codex 多账号网关</p>
|
||||
<p>
|
||||
<a href="https://www.rust-lang.org/"><img alt="Rust 1.95" src="https://img.shields.io/badge/Rust-1.95-000?style=flat-square&logo=rust" /></a>
|
||||
<a href="https://www.rust-lang.org/"><img alt="Rust 1.97" src="https://img.shields.io/badge/Rust-1.97-000?style=flat-square&logo=rust" /></a>
|
||||
<a href="https://github.com/tokio-rs/axum"><img alt="Axum 0.8" src="https://img.shields.io/badge/Axum-0.8-2f7d95?style=flat-square" /></a>
|
||||
<a href="https://vuejs.org/"><img alt="Vue 3.5" src="https://img.shields.io/badge/Vue-3.5-42b883?style=flat-square&logo=vuedotjs&logoColor=white" /></a>
|
||||
<a href="https://vite.dev/"><img alt="Vite 8" src="https://img.shields.io/badge/Vite-8-646cff?style=flat-square&logo=vite&logoColor=white" /></a>
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "codex-proxy-rs"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
rust-version = "1.95"
|
||||
rust-version = "1.97"
|
||||
|
||||
[dependencies]
|
||||
argon2 = "0.5.3"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.95"
|
||||
channel = "1.97"
|
||||
components = ["clippy", "rustfmt"]
|
||||
profile = "minimal"
|
||||
components = ["rustfmt", "clippy"]
|
||||
|
||||
@@ -33,12 +33,12 @@ pub async fn health_check_accounts(
|
||||
"stagger_ms must be between 500 and 30000",
|
||||
));
|
||||
}
|
||||
if let Some(concurrency) = payload.concurrency {
|
||||
if !(1..=10).contains(&concurrency) {
|
||||
return Err(AdminError::bad_request(
|
||||
"concurrency must be between 1 and 10",
|
||||
));
|
||||
}
|
||||
if let Some(concurrency) = payload.concurrency
|
||||
&& !(1..=10).contains(&concurrency)
|
||||
{
|
||||
return Err(AdminError::bad_request(
|
||||
"concurrency must be between 1 and 10",
|
||||
));
|
||||
}
|
||||
match state
|
||||
.services
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//! 账号管理 HTTP 处理器。
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
extract::{Query, State},
|
||||
http::{
|
||||
header::{CACHE_CONTROL, CONNECTION, CONTENT_TYPE},
|
||||
StatusCode,
|
||||
header::{CACHE_CONTROL, CONNECTION, CONTENT_TYPE},
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
@@ -17,22 +17,22 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
api::AppState,
|
||||
api::admin::{
|
||||
response::{
|
||||
parse_editable_update, AdminEnvelope, AdminError, AdminResponse, BatchDeleteData,
|
||||
EditableUpdateMessages, PageMeta, ADMIN_OK_CODE, ADMIN_OK_MESSAGE,
|
||||
ADMIN_OK_CODE, ADMIN_OK_MESSAGE, AdminEnvelope, AdminError, AdminResponse,
|
||||
BatchDeleteData, EditableUpdateMessages, PageMeta, parse_editable_update,
|
||||
},
|
||||
session::AdminAuth,
|
||||
},
|
||||
api::AppState,
|
||||
fleet::{
|
||||
account::AccountStatus,
|
||||
manage::{
|
||||
quota_view::{
|
||||
quota_data, AccountQuotaData, AccountQuotaUsageWindow, AccountQuotaWindowLocalUsage,
|
||||
},
|
||||
AccountHealthCheck, AccountManageError, AccountRefreshOutcome, AccountRefreshResult,
|
||||
AccountUpdate, ManagedAccount, OAuthExchangeInput,
|
||||
quota_view::{
|
||||
AccountQuotaData, AccountQuotaUsageWindow, AccountQuotaWindowLocalUsage, quota_data,
|
||||
},
|
||||
},
|
||||
refresh::token_refresh_status_eligible,
|
||||
},
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
//! 管理端认证路由。
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Extension, Json,
|
||||
extract::State,
|
||||
http::{HeaderMap, HeaderValue, StatusCode, header::SET_COOKIE},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
AppState,
|
||||
admin::{
|
||||
response::{AdminEnvelope, AdminError, AdminResponse},
|
||||
session::{admin_session_cookie, ADMIN_SESSION_COOKIE},
|
||||
session::{ADMIN_SESSION_COOKIE, admin_session_cookie},
|
||||
},
|
||||
middleware::request_id::ClientIp,
|
||||
AppState,
|
||||
},
|
||||
auth::types::SessionError,
|
||||
infra::time::china_rfc3339,
|
||||
@@ -107,15 +107,14 @@ pub(crate) async fn logout(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, AdminError> {
|
||||
if let Some(session_id) = admin_session_cookie(&headers) {
|
||||
if let Err(error) = state
|
||||
if let Some(session_id) = admin_session_cookie(&headers)
|
||||
&& let Err(error) = state
|
||||
.services
|
||||
.admin_sessions
|
||||
.delete_session(&session_id)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %error, "failed to revoke admin session during logout");
|
||||
}
|
||||
{
|
||||
tracing::warn!(error = %error, "failed to revoke admin session during logout");
|
||||
}
|
||||
|
||||
let mut response = AdminResponse::new(
|
||||
|
||||
@@ -12,15 +12,15 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
api::AppState,
|
||||
api::admin::{
|
||||
response::{AdminEnvelope, AdminError, AdminResponse},
|
||||
session::AdminAuth,
|
||||
usage_routes::{
|
||||
usage_cost_details, usage_record_models, usage_token_details,
|
||||
UsageRecordCostDetailsData, UsageRecordTokenDetailsData,
|
||||
UsageRecordCostDetailsData, UsageRecordTokenDetailsData, usage_cost_details,
|
||||
usage_record_models, usage_token_details,
|
||||
},
|
||||
},
|
||||
api::AppState,
|
||||
fleet::{
|
||||
account::{Account, AccountStatus},
|
||||
pool::AccountCapacitySummary,
|
||||
@@ -36,13 +36,13 @@ use crate::{
|
||||
telemetry::{
|
||||
account_usage::query::AccountUsageTimeBucket,
|
||||
dashboard::{
|
||||
dashboard_cards, dashboard_health_timeline_data, dashboard_trend_data,
|
||||
DashboardAccountCounts, DashboardCardsData, DashboardHealthTimelineData,
|
||||
DashboardTrendData, DashboardTrendKind,
|
||||
DashboardTrendData, DashboardTrendKind, dashboard_cards,
|
||||
dashboard_health_timeline_data, dashboard_trend_data,
|
||||
},
|
||||
usage::{
|
||||
query::{UsageQueryFilter, UsageRecordAccountUsage},
|
||||
types::{metadata_string, UsageRecord},
|
||||
types::{UsageRecord, metadata_string},
|
||||
},
|
||||
},
|
||||
upstream::openai::fingerprint::Fingerprint,
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
//! 管理端 v1 接口访问 Key 处理器(列表、创建、生命周期)。
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
api::AppState,
|
||||
api::admin::response::{
|
||||
parse_editable_update, AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse,
|
||||
BatchDeleteData, EditableUpdateMessages,
|
||||
AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse, BatchDeleteData,
|
||||
EditableUpdateMessages, parse_editable_update,
|
||||
},
|
||||
api::admin::session::AdminAuth,
|
||||
api::AppState,
|
||||
infra::{
|
||||
json::{clamp_limit, clamp_page, NumberedPage},
|
||||
json::{NumberedPage, clamp_limit, clamp_page},
|
||||
time::{china_relative_time_str, china_rfc3339_str},
|
||||
},
|
||||
keys::types::{KeyManageError, ManagedClientApiKey},
|
||||
|
||||
@@ -8,13 +8,13 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
api::AppState,
|
||||
api::admin::{
|
||||
response::{AdminError, AdminPageEnvelope, AdminResponse},
|
||||
session::AdminAuth,
|
||||
},
|
||||
api::AppState,
|
||||
infra::{
|
||||
json::{clamp_limit, clamp_page, NumberedPage},
|
||||
json::{NumberedPage, clamp_limit, clamp_page},
|
||||
time::china_datetime,
|
||||
},
|
||||
telemetry::{ops::query::OpsQueryError, ops::store::OpsErrorFilter, ops::types::OpsErrorLog},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! 管理端响应封装与 From 转换。
|
||||
|
||||
use crate::infra::json::{total_pages, NumberedPage};
|
||||
use crate::infra::json::{NumberedPage, total_pages};
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! 管理端路由。
|
||||
|
||||
use axum::{
|
||||
http::{header, HeaderValue},
|
||||
Router,
|
||||
http::{HeaderValue, header},
|
||||
middleware,
|
||||
response::Response,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
|
||||
use crate::api::AppState;
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::{request::Parts, HeaderMap},
|
||||
http::{HeaderMap, request::Parts},
|
||||
};
|
||||
|
||||
use crate::api::{admin::response::AdminError, AppState};
|
||||
use crate::api::{AppState, admin::response::AdminError};
|
||||
|
||||
pub(super) const ADMIN_SESSION_COOKIE: &str = "cpr_admin_session";
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{
|
||||
api::AppState,
|
||||
api::admin::{
|
||||
response::{AdminEnvelope, AdminError, AdminResponse},
|
||||
session::AdminAuth,
|
||||
},
|
||||
api::AppState,
|
||||
settings::{SettingsError, SettingsPatch, SettingsSnapshot, SettingsValidationError},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,24 +6,20 @@ use axum::{
|
||||
extract::{Json, Query, State},
|
||||
http::StatusCode,
|
||||
response::{
|
||||
sse::{Event, KeepAlive, Sse},
|
||||
IntoResponse,
|
||||
sse::{Event, KeepAlive, Sse},
|
||||
},
|
||||
};
|
||||
use futures::Stream;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
api::AppState,
|
||||
api::admin::{
|
||||
response::{AdminEnvelope, AdminError, AdminResponse},
|
||||
session::AdminAuth,
|
||||
},
|
||||
api::AppState,
|
||||
update::{
|
||||
service::{self, RestartAction},
|
||||
state::operation_id,
|
||||
types::UpdateError,
|
||||
},
|
||||
update::{service::RestartAction, state::operation_id, types::UpdateError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -38,20 +34,30 @@ pub(crate) struct UpdateRequest {
|
||||
target_version: String,
|
||||
}
|
||||
|
||||
pub(crate) async fn version(_auth: AdminAuth) -> Result<impl IntoResponse, AdminError> {
|
||||
pub(crate) async fn version(
|
||||
State(state): State<AppState>,
|
||||
_auth: AdminAuth,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(service::version_data().await),
|
||||
AdminEnvelope::ok(state.services.system_update.version_data().await),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_detail(
|
||||
State(state): State<AppState>,
|
||||
_auth: AdminAuth,
|
||||
Query(query): Query<UpdateDetailQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(service::update_detail(query.refresh.unwrap_or(false)).await),
|
||||
AdminEnvelope::ok(
|
||||
state
|
||||
.services
|
||||
.system_update
|
||||
.update_detail(query.refresh.unwrap_or(false))
|
||||
.await,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -59,7 +65,7 @@ pub(crate) async fn update_event_stream(
|
||||
State(state): State<AppState>,
|
||||
_auth: AdminAuth,
|
||||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, AdminError> {
|
||||
let receiver = service::subscribe_update_events();
|
||||
let receiver = state.services.system_update.subscribe_update_events();
|
||||
let shutdown = state.services.process_control.subscribe_shutdown();
|
||||
let stream = futures::stream::unfold(
|
||||
(receiver, shutdown, false),
|
||||
@@ -90,11 +96,15 @@ pub(crate) async fn update_event_stream(
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_update(
|
||||
State(state): State<AppState>,
|
||||
_auth: AdminAuth,
|
||||
payload: Option<Json<UpdateRequest>>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let target = payload.map(|Json(payload)| payload.target_version);
|
||||
let result = service::perform_update(target)
|
||||
let result = state
|
||||
.services
|
||||
.system_update
|
||||
.perform_update(target)
|
||||
.await
|
||||
.map_err(update_error)?;
|
||||
Ok(AdminResponse::new(
|
||||
@@ -103,16 +113,31 @@ pub(crate) async fn perform_update(
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_status(_auth: AdminAuth) -> Result<impl IntoResponse, AdminError> {
|
||||
let status = service::update_status().map_err(update_error)?;
|
||||
pub(crate) async fn update_status(
|
||||
State(state): State<AppState>,
|
||||
_auth: AdminAuth,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let status = state
|
||||
.services
|
||||
.system_update
|
||||
.update_status()
|
||||
.map_err(update_error)?;
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(status),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn rollback(_auth: AdminAuth) -> Result<impl IntoResponse, AdminError> {
|
||||
let operation_id = service::rollback().await.map_err(update_error)?;
|
||||
pub(crate) async fn rollback(
|
||||
State(state): State<AppState>,
|
||||
_auth: AdminAuth,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let operation_id = state
|
||||
.services
|
||||
.system_update
|
||||
.rollback()
|
||||
.await
|
||||
.map_err(update_error)?;
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(serde_json::json!({
|
||||
@@ -127,7 +152,11 @@ pub(crate) async fn restart(
|
||||
State(state): State<AppState>,
|
||||
_auth: AdminAuth,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let plan = service::restart_plan().map_err(update_error)?;
|
||||
let plan = state
|
||||
.services
|
||||
.system_update
|
||||
.restart_plan()
|
||||
.map_err(update_error)?;
|
||||
let message = plan.message;
|
||||
let process_control = state.services.process_control.clone();
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -10,26 +10,26 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
api::AppState,
|
||||
api::admin::{
|
||||
response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse},
|
||||
session::AdminAuth,
|
||||
},
|
||||
api::AppState,
|
||||
infra::{
|
||||
format::{
|
||||
format_compact_number, format_cost, format_duration_ms, format_duration_ms_f64,
|
||||
format_multiplier, format_number, format_percent, format_token_price, format_tokens,
|
||||
},
|
||||
json::{clamp_limit, clamp_page, NumberedPage},
|
||||
json::{NumberedPage, clamp_limit, clamp_page},
|
||||
time::china_datetime,
|
||||
},
|
||||
telemetry::{
|
||||
usage::query::{
|
||||
usage_record_cost_details, UsageQueryError, UsageQueryFilter, UsageRecordBreakdown,
|
||||
UsageRecordEndpointSource, UsageRecordModelSource, UsageRecordSummary,
|
||||
UsageRecordTrendPoint,
|
||||
UsageQueryError, UsageQueryFilter, UsageRecordBreakdown, UsageRecordEndpointSource,
|
||||
UsageRecordModelSource, UsageRecordSummary, UsageRecordTrendPoint,
|
||||
usage_record_cost_details,
|
||||
},
|
||||
usage::types::{metadata_string, UsageRecord},
|
||||
usage::types::{UsageRecord, metadata_string},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ use std::{
|
||||
};
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json, Router,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tower::service_fn;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! 提供 Bearer token 提取与客户端 API key 校验。
|
||||
|
||||
use axum::http::{header::AUTHORIZATION, HeaderMap};
|
||||
use axum::http::{HeaderMap, header::AUTHORIZATION};
|
||||
|
||||
use crate::api::AppState;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! OpenAI 路由共享错误响应。
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
dispatch::errors::{DispatchFailureClass, ResponseDispatchError},
|
||||
@@ -117,12 +117,10 @@ pub fn responses_dispatch_error_response(error: ResponseDispatchError) -> Respon
|
||||
|
||||
pub fn responses_dispatch_error_response_ref(error: &ResponseDispatchError) -> Response {
|
||||
if let ResponseDispatchError::Upstream(CodexClientError::Upstream { status, body, .. }) = error
|
||||
&& status.is_client_error()
|
||||
&& let Ok(body) = serde_json::from_str::<Value>(body)
|
||||
{
|
||||
if status.is_client_error() {
|
||||
if let Ok(body) = serde_json::from_str::<Value>(body) {
|
||||
return (*status, Json(body)).into_response();
|
||||
}
|
||||
}
|
||||
return (*status, Json(body)).into_response();
|
||||
}
|
||||
|
||||
match error {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//! OpenAI 模型列表以及模型处理器。
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
api::client::auth::authorize_client_api_key, api::AppState, models::catalog::ModelCatalog,
|
||||
api::AppState, api::client::auth::authorize_client_api_key, models::catalog::ModelCatalog,
|
||||
};
|
||||
|
||||
use super::errors::{missing_client_api_key_response, model_not_found_response};
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
//! 做最小 patch 后交给调度层,请求语义原样透传上游。
|
||||
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
body::{Body, Bytes},
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Extension, Json,
|
||||
};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::{
|
||||
api::middleware::request_id::{ClientIp, RequestId},
|
||||
api::AppState,
|
||||
api::middleware::request_id::{ClientIp, RequestId},
|
||||
dispatch::{errors::ResponseDispatchError, service::ResponseDispatchStream},
|
||||
upstream::openai::protocol::{
|
||||
responses::{CodexCompactRequest, CodexResponsesRequest},
|
||||
@@ -449,15 +449,11 @@ pub(crate) fn user_agent_from_headers(headers: &HeaderMap) -> Option<String> {
|
||||
}
|
||||
|
||||
fn openai_subagent_from_headers(headers: &HeaderMap) -> Option<String> {
|
||||
header_string(headers, OPENAI_SUBAGENT_HEADER).and_then(|value| {
|
||||
if matches!(
|
||||
header_string(headers, OPENAI_SUBAGENT_HEADER).filter(|value| {
|
||||
matches!(
|
||||
value.as_str(),
|
||||
"review" | "compact" | "memory_consolidation" | "collab_spawn"
|
||||
) {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! OpenAI 兼容 API 路由。
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::DefaultBodyLimit,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
|
||||
use crate::api::AppState;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{
|
||||
header::{CACHE_CONTROL, CONTENT_TYPE},
|
||||
StatusCode,
|
||||
header::{CACHE_CONTROL, CONTENT_TYPE},
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use axum::{
|
||||
extract::{connect_info::ConnectInfo, Request},
|
||||
extract::{Request, connect_info::ConnectInfo},
|
||||
http::{HeaderMap, HeaderValue},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
|
||||
@@ -10,7 +10,7 @@ use tower_http::{
|
||||
classify::{ServerErrorsAsFailures, ServerErrorsFailureClass, SharedClassifier},
|
||||
trace::TraceLayer,
|
||||
};
|
||||
use tracing::{debug, info, warn, Span};
|
||||
use tracing::{Span, debug, info, warn};
|
||||
|
||||
use crate::api::middleware::request_id::{ClientIp, RequestId};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{extract::State, http::StatusCode, middleware, routing::get, Router};
|
||||
use axum::{Router, extract::State, http::StatusCode, middleware, routing::get};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
@@ -26,6 +26,7 @@ use crate::{
|
||||
account_usage::query::AccountUsageQueryService, ops::query::OpsQueryService,
|
||||
usage::query::UsageQueryService,
|
||||
},
|
||||
update::service::SystemUpdateService,
|
||||
upstream::openai::{fingerprint::RuntimeFingerprint, token_client::OpenAiTokenClient},
|
||||
};
|
||||
|
||||
@@ -49,6 +50,7 @@ pub struct ApiServices {
|
||||
pub session_affinity: Arc<SessionAffinityService>,
|
||||
pub fingerprint: RuntimeFingerprint,
|
||||
pub process_control: Arc<dyn ProcessControl>,
|
||||
pub system_update: Arc<SystemUpdateService>,
|
||||
}
|
||||
|
||||
/// 由 bootstrap 实现的进程生命周期控制端口。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::BTreeMap, env, path::Path};
|
||||
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use futures::TryStreamExt;
|
||||
use sqlx::{types::Json, Postgres, Row, SqlitePool, Transaction};
|
||||
use sqlx::{Postgres, Row, SqlitePool, Transaction, types::Json};
|
||||
|
||||
use crate::infra::identity::hash_credential;
|
||||
|
||||
use super::{
|
||||
parse_cookie_expiration, parse_json, parse_optional_json, parse_optional_timestamp,
|
||||
parse_timestamp, ImportSqliteError, ImportSqliteReport,
|
||||
ImportSqliteError, ImportSqliteReport, parse_cookie_expiration, parse_json,
|
||||
parse_optional_json, parse_optional_timestamp, parse_timestamp,
|
||||
};
|
||||
|
||||
pub(super) async fn import_core_tables(
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::{collections::BTreeMap, fmt, path::Path};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::Value;
|
||||
use sqlx::{
|
||||
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
|
||||
PgPool, Postgres, SqlitePool, Transaction,
|
||||
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use futures::TryStreamExt;
|
||||
use serde_json::Value;
|
||||
use sqlx::{types::Json, Postgres, Row, SqlitePool, Transaction};
|
||||
use sqlx::{Postgres, Row, SqlitePool, Transaction, types::Json};
|
||||
|
||||
use super::{
|
||||
normalized_dimension, parse_json, parse_timestamp, ImportSqliteError, ImportSqliteReport,
|
||||
ImportSqliteError, ImportSqliteReport, normalized_dimension, parse_json, parse_timestamp,
|
||||
};
|
||||
|
||||
pub(super) async fn import_telemetry_tables(
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::{
|
||||
infra::{
|
||||
database::connect,
|
||||
identity::AccountPseudonymizer,
|
||||
logging::{init_tracing, LogError, LogGuard, RotationConfig, TracingConfig},
|
||||
logging::{LogError, LogGuard, RotationConfig, TracingConfig, init_tracing},
|
||||
paths::{ensure_data_dir, load_or_create_identity_secret},
|
||||
redis::RedisConnection,
|
||||
},
|
||||
@@ -36,7 +36,7 @@ use crate::{
|
||||
store::{ModelSnapshotStore, RedisModelSnapshotStore},
|
||||
types::ModelConfig,
|
||||
},
|
||||
settings::{service::SettingsService, SettingsSnapshot},
|
||||
settings::{SettingsSnapshot, service::SettingsService},
|
||||
telemetry::{
|
||||
account_usage::query::AccountUsageQueryService,
|
||||
account_usage::store::{AccountUsageStore, PgAccountUsageStore},
|
||||
@@ -45,14 +45,15 @@ use crate::{
|
||||
ops::store::PgOpsErrorLogStore,
|
||||
recorder::Recorder,
|
||||
usage::query::UsageQueryService,
|
||||
usage::store::{PgUsageRecordStore, DEFAULT_USAGE_RECORD_CAPTURE_BODY},
|
||||
usage::store::{DEFAULT_USAGE_RECORD_CAPTURE_BODY, PgUsageRecordStore},
|
||||
},
|
||||
update::service::SystemUpdateService,
|
||||
upstream::openai::{
|
||||
fingerprint::{Fingerprint, PgFingerprintStore, RuntimeFingerprint},
|
||||
token_client::{default_openai_token_client, OpenAiTokenClient, TokenClientConfig},
|
||||
token_client::{OpenAiTokenClient, TokenClientConfig, default_openai_token_client},
|
||||
transport::{
|
||||
build_reqwest_client, tls::CustomCaError, CodexBackendClient, CodexModelCatalogClient,
|
||||
CodexWebSocketPool, CodexWebSocketPoolConfig,
|
||||
CodexBackendClient, CodexModelCatalogClient, CodexWebSocketPool,
|
||||
CodexWebSocketPoolConfig, build_reqwest_client, tls::CustomCaError,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -117,6 +118,7 @@ pub struct Services {
|
||||
pub websocket_pool: Option<Arc<CodexWebSocketPool>>,
|
||||
pub fingerprint: RuntimeFingerprint,
|
||||
pub account_pseudonymizer: Arc<AccountPseudonymizer>,
|
||||
pub system_update: Arc<SystemUpdateService>,
|
||||
pub background_tasks: BackgroundTaskStores,
|
||||
pub(crate) account_pool_static: AccountPoolStaticSettings,
|
||||
}
|
||||
@@ -304,6 +306,7 @@ impl Services {
|
||||
account_identity,
|
||||
cloudflare: cloudflare_recovery,
|
||||
}));
|
||||
let system_update = Arc::new(SystemUpdateService::from_env());
|
||||
|
||||
Ok(Self {
|
||||
database,
|
||||
@@ -327,6 +330,7 @@ impl Services {
|
||||
websocket_pool,
|
||||
fingerprint,
|
||||
account_pseudonymizer,
|
||||
system_update,
|
||||
background_tasks: stores,
|
||||
account_pool_static,
|
||||
})
|
||||
@@ -362,6 +366,7 @@ impl From<&Services> for crate::api::router::ApiServices {
|
||||
session_affinity: services.session_affinity.clone(),
|
||||
fingerprint: services.fingerprint.clone(),
|
||||
process_control: Arc::new(crate::bootstrap::shutdown::RuntimeProcessControl),
|
||||
system_update: services.system_update.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use tracing::{debug, info, warn};
|
||||
|
||||
use super::{
|
||||
coordinator::SchedulerHandle,
|
||||
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner},
|
||||
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, spawn_periodic_task},
|
||||
};
|
||||
|
||||
/// 支持按时间清理过期记录的存储。
|
||||
|
||||
@@ -7,8 +7,8 @@ use tokio::task::JoinHandle;
|
||||
use super::{
|
||||
cookie_cleanup::CookieCleanupTask,
|
||||
fingerprint_update::{
|
||||
start_fingerprint_update_task, CODEX_DESKTOP_APPCAST_URL,
|
||||
DEFAULT_EXTRACTED_FINGERPRINT_PATH,
|
||||
CODEX_DESKTOP_APPCAST_URL, DEFAULT_EXTRACTED_FINGERPRINT_PATH,
|
||||
start_fingerprint_update_task,
|
||||
},
|
||||
model_refresh::ModelRefreshTask,
|
||||
quota_refresh::QuotaRefreshTask,
|
||||
|
||||
@@ -5,12 +5,12 @@ use std::path::PathBuf;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::upstream::openai::fingerprint::{
|
||||
PgFingerprintStore, RuntimeFingerprint, UpdateChecker, APPCAST_POLL_INTERVAL,
|
||||
APPCAST_POLL_INTERVAL, PgFingerprintStore, RuntimeFingerprint, UpdateChecker,
|
||||
};
|
||||
|
||||
use super::{
|
||||
coordinator::SchedulerHandle,
|
||||
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner},
|
||||
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, spawn_periodic_task},
|
||||
};
|
||||
|
||||
/// Codex Desktop 官方 appcast 地址。
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::models::service::{
|
||||
|
||||
use super::{
|
||||
coordinator::SchedulerHandle,
|
||||
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner, PeriodicTaskStartup},
|
||||
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, PeriodicTaskStartup, spawn_periodic_task},
|
||||
};
|
||||
|
||||
/// 模型刷新任务接线器。
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
coordinator::SchedulerHandle,
|
||||
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner},
|
||||
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, spawn_periodic_task},
|
||||
};
|
||||
|
||||
/// 主动配额刷新后台任务。
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
coordinator::SchedulerHandle,
|
||||
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner, TaskFuture},
|
||||
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, TaskFuture, spawn_periodic_task},
|
||||
};
|
||||
|
||||
const RETENTION_TRIM_INTERVAL_SECS: u64 = 60 * 60;
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::upstream::openai::token_client::TokenRefresher;
|
||||
|
||||
use super::{
|
||||
coordinator::SchedulerHandle,
|
||||
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner},
|
||||
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, spawn_periodic_task},
|
||||
};
|
||||
|
||||
const DEFAULT_INTERVAL_SECS: u64 = 60;
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
},
|
||||
upstream::openai::protocol::{
|
||||
events::TokenUsage,
|
||||
responses::{completed_response_metadata, CodexResponsesRequest},
|
||||
responses::{CodexResponsesRequest, completed_response_metadata},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -146,12 +146,11 @@ fn build_variant_identity(request: &CodexResponsesRequest) -> Option<String> {
|
||||
if let Some(window_id) = non_empty_str(request.codex_window_id.as_deref()) {
|
||||
parts.push(format!("window:{window_id}"));
|
||||
}
|
||||
if request.explicit_prompt_cache_key
|
||||
|| non_empty_str(request.client_conversation_id.as_deref()).is_some()
|
||||
if (request.explicit_prompt_cache_key
|
||||
|| non_empty_str(request.client_conversation_id.as_deref()).is_some())
|
||||
&& let Some(anchor) = derive_stable_conversation_key(request)
|
||||
{
|
||||
if let Some(anchor) = derive_stable_conversation_key(request) {
|
||||
parts.push(format!("anchor:{anchor}"));
|
||||
}
|
||||
parts.push(format!("anchor:{anchor}"));
|
||||
}
|
||||
|
||||
(!parts.is_empty()).then(|| parts.join("\0"))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Shared upstream error classification for dispatch routes.
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde_json::{json, Map, Value};
|
||||
use serde_json::{Map, Value, json};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::{
|
||||
dispatch::errors::{
|
||||
backend_transport_name, insert_dispatch_error_metadata,
|
||||
insert_upstream_diagnostics_metadata, upstream_error_http_status, DispatchErrorMetadata,
|
||||
DispatchErrorMetadata, backend_transport_name, insert_dispatch_error_metadata,
|
||||
insert_upstream_diagnostics_metadata, upstream_error_http_status,
|
||||
},
|
||||
infra::time::elapsed_millis_i64,
|
||||
telemetry::{
|
||||
ops::types::OpsErrorLog,
|
||||
recorder::{
|
||||
enrich_event_route_metadata, enrich_usage_record_identity,
|
||||
event_kind as response_event_kind, reasoning_effort_from_request,
|
||||
record_dispatch_error_event, DispatchErrorLogRecord, Recorder,
|
||||
DispatchErrorLogRecord, Recorder, enrich_event_route_metadata,
|
||||
enrich_usage_record_identity, event_kind as response_event_kind,
|
||||
reasoning_effort_from_request, record_dispatch_error_event,
|
||||
},
|
||||
usage::types::UsageRecordLevel,
|
||||
},
|
||||
@@ -28,10 +28,10 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
errors::{
|
||||
dispatch_error_metadata, enrich_response_dispatch_error_metadata, ResponseDispatchError,
|
||||
ResponseDispatchError, dispatch_error_metadata, enrich_response_dispatch_error_metadata,
|
||||
},
|
||||
stream::{
|
||||
live::{latest_response_id, LiveResponseStreamContext},
|
||||
live::{LiveResponseStreamContext, latest_response_id},
|
||||
sse_failure::{status_code_for_stream_failure, stream_failure_metadata},
|
||||
trace::{ResponseDispatchAttempt, ResponseDispatchTrace},
|
||||
},
|
||||
@@ -236,13 +236,13 @@ pub(super) async fn record_prefetched_response_stream_failure_event(
|
||||
event.model = Some(record.model.to_string());
|
||||
event.status_code = Some(event_status_code);
|
||||
event.latency_ms = Some(elapsed_millis_i64(record.started_at));
|
||||
if !record.rate_limit_headers.is_empty() {
|
||||
if let Some(object) = metadata.as_object_mut() {
|
||||
object.insert(
|
||||
"rateLimitHeaders".to_string(),
|
||||
serde_json::json!(record.rate_limit_headers),
|
||||
);
|
||||
}
|
||||
if !record.rate_limit_headers.is_empty()
|
||||
&& let Some(object) = metadata.as_object_mut()
|
||||
{
|
||||
object.insert(
|
||||
"rateLimitHeaders".to_string(),
|
||||
serde_json::json!(record.rate_limit_headers),
|
||||
);
|
||||
}
|
||||
event.metadata = metadata;
|
||||
if let Err(error) = record.recorder.record_error(event).await {
|
||||
|
||||
@@ -12,9 +12,9 @@ use crate::{
|
||||
recovery::{
|
||||
auth::{auth_failure_account_status, is_auth_upstream_error},
|
||||
cloudflare::{
|
||||
cloudflare_challenge_error_message, cloudflare_path_block_error_message,
|
||||
is_cloudflare_challenge_upstream_error, is_cloudflare_path_block_upstream_error,
|
||||
CloudflareRecovery,
|
||||
CloudflareRecovery, cloudflare_challenge_error_message,
|
||||
cloudflare_path_block_error_message, is_cloudflare_challenge_upstream_error,
|
||||
is_cloudflare_path_block_upstream_error,
|
||||
},
|
||||
exhaustion::AccountExhaustionTracker,
|
||||
},
|
||||
@@ -26,7 +26,7 @@ use crate::{
|
||||
fleet::{account::AccountStatus, pool::AccountPoolService},
|
||||
upstream::openai::{
|
||||
protocol::responses::ResponsesSseFailure,
|
||||
transport::{is_banned_upstream_error, CodexClientError},
|
||||
transport::{CodexClientError, is_banned_upstream_error},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::{
|
||||
fleet::account::AccountStatus,
|
||||
upstream::openai::transport::{is_banned_auth_signal, CodexClientError},
|
||||
upstream::openai::transport::{CodexClientError, is_banned_auth_signal},
|
||||
};
|
||||
|
||||
pub(crate) fn is_auth_upstream_error(error: &CodexClientError) -> bool {
|
||||
|
||||
@@ -5,9 +5,9 @@ use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
dispatch::affinity::{
|
||||
SessionAffinityEntry, SessionAffinityService,
|
||||
service::{MAX_REPLAY_DEPTH, MAX_REPLAY_SESSION_BYTES, MAX_REPLAY_SNAPSHOT_BYTES},
|
||||
types::ResponseReplaySnapshot,
|
||||
SessionAffinityEntry, SessionAffinityService,
|
||||
},
|
||||
upstream::openai::protocol::responses::{CodexResponsesRequest, StreamCommitPolicy},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{pin::Pin, sync::Arc, time::Instant};
|
||||
use bytes::Bytes;
|
||||
use chrono::Utc;
|
||||
use futures::stream::Stream;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
dispatch::{
|
||||
@@ -31,9 +31,9 @@ use crate::{
|
||||
history::HistoryRecoveryPlan,
|
||||
},
|
||||
upstream_call::{
|
||||
create_response_with_account_retrying_5xx, verify_acquired_quota_if_required,
|
||||
AccountUpstreamContext, QuotaVerificationContext, QuotaVerificationDecision,
|
||||
QUOTA_VERIFY_LIMIT_REACHED_MESSAGE,
|
||||
AccountUpstreamContext, QUOTA_VERIFY_LIMIT_REACHED_MESSAGE, QuotaVerificationContext,
|
||||
QuotaVerificationDecision, create_response_with_account_retrying_5xx,
|
||||
verify_acquired_quota_if_required,
|
||||
},
|
||||
},
|
||||
fleet::{
|
||||
@@ -42,16 +42,16 @@ use crate::{
|
||||
},
|
||||
models::service::ModelService,
|
||||
telemetry::{
|
||||
recorder::{reasoning_effort_from_request, record_response_event, Recorder},
|
||||
recorder::{Recorder, reasoning_effort_from_request, record_response_event},
|
||||
usage::types::ResponseUsageRecord,
|
||||
},
|
||||
upstream::openai::{
|
||||
protocol::{
|
||||
events::TokenUsage,
|
||||
responses::{response_from_codex_sse, CodexResponsesRequest, CollectedResponse},
|
||||
responses::{CodexResponsesRequest, CollectedResponse, response_from_codex_sse},
|
||||
},
|
||||
transport::{
|
||||
backend_transport_for_response_request, CodexBackendClient, CodexBackendResponse,
|
||||
CodexBackendClient, CodexBackendResponse, backend_transport_for_response_request,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -59,11 +59,11 @@ use crate::{
|
||||
use super::{
|
||||
errors::{ResponseDispatchError, ResponseDispatchStreamError},
|
||||
recording::{
|
||||
insert_response_status_metadata, insert_response_trace_metadata,
|
||||
insert_response_upstream_diagnostics, insert_websocket_pool_decision,
|
||||
record_response_dispatch_error_event, record_response_upstream_error_event,
|
||||
ResponseDispatchErrorDetails, ResponseDispatchErrorEventRecord,
|
||||
ResponseUpstreamErrorEventRecord,
|
||||
ResponseUpstreamErrorEventRecord, insert_response_status_metadata,
|
||||
insert_response_trace_metadata, insert_response_upstream_diagnostics,
|
||||
insert_websocket_pool_decision, record_response_dispatch_error_event,
|
||||
record_response_upstream_error_event,
|
||||
},
|
||||
stream::{
|
||||
sse_failure::is_history_recovery_sse_failure,
|
||||
|
||||
@@ -7,14 +7,14 @@ use crate::{
|
||||
affinity::prepare_variant_identity,
|
||||
attempts::AccountAttemptLedger,
|
||||
errors::{
|
||||
backend_transport_name, is_continuation_busy_error, is_history_recovery_upstream_error,
|
||||
is_retryable_account_transport_error, upstream_error_body,
|
||||
upstream_error_set_cookie_headers, ResponseDispatchError,
|
||||
ResponseDispatchError, backend_transport_name, is_continuation_busy_error,
|
||||
is_history_recovery_upstream_error, is_retryable_account_transport_error,
|
||||
upstream_error_body, upstream_error_set_cookie_headers,
|
||||
},
|
||||
recording::{
|
||||
record_prefetched_response_stream_failure_event, record_response_upstream_error_event,
|
||||
ResponseDispatchErrorDetails, ResponseStreamFailureEventRecord,
|
||||
ResponseUpstreamErrorEventRecord,
|
||||
ResponseUpstreamErrorEventRecord, record_prefetched_response_stream_failure_event,
|
||||
record_response_upstream_error_event,
|
||||
},
|
||||
recovery::{
|
||||
account_failure::{isolate_rotatable_account_failure, isolate_sse_account_failure},
|
||||
@@ -23,7 +23,7 @@ use crate::{
|
||||
},
|
||||
service::{ResponseDispatchService, ResponseDispatchStream},
|
||||
stream::{
|
||||
live::{spawn_live_response_stream, LiveResponseStreamContext},
|
||||
live::{LiveResponseStreamContext, spawn_live_response_stream},
|
||||
prefetch::prefetch_first_sse_chunk,
|
||||
sse_failure::{
|
||||
first_sse_failure, is_history_recovery_sse_failure, sse_failure_error_body,
|
||||
@@ -31,9 +31,9 @@ use crate::{
|
||||
trace::ResponseDispatchTrace,
|
||||
},
|
||||
upstream_call::{
|
||||
create_response_stream_with_account_retrying_5xx, verify_acquired_quota_if_required,
|
||||
AccountUpstreamContext, QuotaVerificationContext, QuotaVerificationDecision,
|
||||
QUOTA_VERIFY_LIMIT_REACHED_MESSAGE,
|
||||
AccountUpstreamContext, QUOTA_VERIFY_LIMIT_REACHED_MESSAGE, QuotaVerificationContext,
|
||||
QuotaVerificationDecision, create_response_stream_with_account_retrying_5xx,
|
||||
verify_acquired_quota_if_required,
|
||||
},
|
||||
},
|
||||
fleet::pool::AccountAcquireRequest,
|
||||
|
||||
@@ -6,14 +6,14 @@ use std::{
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::{stream::Stream, StreamExt};
|
||||
use futures::{StreamExt, stream::Stream};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use crate::{
|
||||
dispatch::{
|
||||
affinity::{resolve::record_response_affinity, SessionAffinityService},
|
||||
errors::{backend_transport_name, ResponseDispatchStreamError},
|
||||
affinity::{SessionAffinityService, resolve::record_response_affinity},
|
||||
errors::{ResponseDispatchStreamError, backend_transport_name},
|
||||
recording::{
|
||||
insert_first_token_ms, live_response_rate_limit_headers, live_response_turn_state,
|
||||
record_live_response_stream_event,
|
||||
@@ -28,13 +28,13 @@ use crate::{
|
||||
protocol::{
|
||||
events::extract_sse_usage,
|
||||
responses::{
|
||||
CodexResponsesRequest, CollectedResponse,
|
||||
reconvert_responses_sse_event_tuple_values, response_from_codex_sse,
|
||||
response_sse_event_is_terminal, update_first_response_output_ms,
|
||||
CodexResponsesRequest, CollectedResponse,
|
||||
},
|
||||
sse::{
|
||||
encode_sse_event, parse_sse_events, response_failed_sse_event_with_id,
|
||||
sse_body_has_done, sse_frame_end, DONE_SSE_FRAME,
|
||||
DONE_SSE_FRAME, encode_sse_event, parse_sse_events,
|
||||
response_failed_sse_event_with_id, sse_body_has_done, sse_frame_end,
|
||||
},
|
||||
},
|
||||
transport::{
|
||||
@@ -47,9 +47,8 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
sse_failure::{
|
||||
status_code_for_stream_failure, stream_failure_metadata, stream_failure_source,
|
||||
synthetic_stream_disconnected_detail, STREAM_DISCONNECTED_CODE,
|
||||
STREAM_DISCONNECTED_MESSAGE,
|
||||
STREAM_DISCONNECTED_CODE, STREAM_DISCONNECTED_MESSAGE, status_code_for_stream_failure,
|
||||
stream_failure_metadata, stream_failure_source, synthetic_stream_disconnected_detail,
|
||||
},
|
||||
trace::ResponseDispatchAttempt,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::upstream::openai::{
|
||||
protocol::sse::{sse_frame_end, SseError},
|
||||
protocol::sse::{SseError, sse_frame_end},
|
||||
transport::CodexBackendSseStream,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
upstream::openai::{
|
||||
protocol::{
|
||||
events::TokenUsage,
|
||||
responses::{response_from_codex_sse, CollectedResponse, ResponsesSseFailure},
|
||||
responses::{CollectedResponse, ResponsesSseFailure, response_from_codex_sse},
|
||||
sse::SseError,
|
||||
},
|
||||
transport::is_banned_auth_signal,
|
||||
|
||||
@@ -3,29 +3,30 @@
|
||||
use std::{future::Future, time::Instant};
|
||||
|
||||
use chrono::Utc;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
dispatch::{
|
||||
affinity::{AccountIdentityService, AccountScopedIdentity},
|
||||
attempts::AccountAttemptLedger,
|
||||
errors::{
|
||||
is_model_unsupported_upstream_error, is_quota_exhausted_upstream_error,
|
||||
is_rate_limit_upstream_error, is_retryable_account_transport_error,
|
||||
is_retryable_upstream_5xx_error, rate_limit_cooldown_until, upstream_error_body,
|
||||
upstream_error_http_status, upstream_error_set_cookie_headers, ResponseDispatchError,
|
||||
ResponseDispatchError, is_model_unsupported_upstream_error,
|
||||
is_quota_exhausted_upstream_error, is_rate_limit_upstream_error,
|
||||
is_retryable_account_transport_error, is_retryable_upstream_5xx_error,
|
||||
rate_limit_cooldown_until, upstream_error_body, upstream_error_http_status,
|
||||
upstream_error_set_cookie_headers,
|
||||
},
|
||||
recording::{
|
||||
insert_response_status_metadata, insert_response_trace_metadata,
|
||||
insert_response_upstream_diagnostics, record_response_dispatch_error_event,
|
||||
ResponseDispatchErrorEventRecord,
|
||||
ResponseDispatchErrorEventRecord, insert_response_status_metadata,
|
||||
insert_response_trace_metadata, insert_response_upstream_diagnostics,
|
||||
record_response_dispatch_error_event,
|
||||
},
|
||||
recovery::{
|
||||
auth::{auth_failure_account_status, is_auth_upstream_error},
|
||||
cloudflare::{
|
||||
cloudflare_challenge_error_message, cloudflare_path_block_error_message,
|
||||
is_cloudflare_challenge_upstream_error, is_cloudflare_path_block_upstream_error,
|
||||
CloudflareRecovery,
|
||||
CloudflareRecovery, cloudflare_challenge_error_message,
|
||||
cloudflare_path_block_error_message, is_cloudflare_challenge_upstream_error,
|
||||
is_cloudflare_path_block_upstream_error,
|
||||
},
|
||||
exhaustion::AccountExhaustionTracker,
|
||||
},
|
||||
@@ -50,9 +51,8 @@ use crate::{
|
||||
responses::{CodexCompactRequest, CodexResponsesRequest},
|
||||
},
|
||||
transport::{
|
||||
is_banned_upstream_error, CodexBackendClient, CodexBackendResponse,
|
||||
CodexBackendStreamingResponse, CodexClientError, CodexCompactResponse,
|
||||
CodexRequestContext,
|
||||
CodexBackendClient, CodexBackendResponse, CodexBackendStreamingResponse,
|
||||
CodexClientError, CodexCompactResponse, CodexRequestContext, is_banned_upstream_error,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::fleet::account::AccountStatus;
|
||||
use crate::fleet::refresh::{jwt_expiry, JwtExpiry};
|
||||
use crate::fleet::refresh::{JwtExpiry, jwt_expiry};
|
||||
|
||||
/// 账号导入条目。
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
use super::{
|
||||
types::{ExportedAccount, ExportedAccounts},
|
||||
AccountManageError, AccountManageService,
|
||||
types::{ExportedAccount, ExportedAccounts},
|
||||
};
|
||||
use crate::{fleet::store::StoredAccount, infra::time::china_rfc3339_str};
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ use chrono::Utc;
|
||||
use secrecy::SecretString;
|
||||
|
||||
use super::{
|
||||
types::{
|
||||
import_quota_plan_type, import_status_from_usage_error, import_usage_plan_type,
|
||||
import_usage_string, AccountManageError, ImportSupplementalAccountInfo,
|
||||
ImportSupplementalNeeds, ImportedAccountState, ImportedAccounts, ResolvedImportTokens,
|
||||
},
|
||||
AccountManageService,
|
||||
types::{
|
||||
AccountManageError, ImportSupplementalAccountInfo, ImportSupplementalNeeds,
|
||||
ImportedAccountState, ImportedAccounts, ResolvedImportTokens, import_quota_plan_type,
|
||||
import_status_from_usage_error, import_usage_plan_type, import_usage_string,
|
||||
},
|
||||
};
|
||||
|
||||
impl AccountManageService {
|
||||
@@ -304,15 +304,14 @@ impl AccountManageService {
|
||||
|
||||
let chatgpt_account_id = crate::fleet::import::normalize_nonempty_str(account_id);
|
||||
let chatgpt_user_id = crate::fleet::import::normalize_nonempty_str(user_id);
|
||||
if let Some(chatgpt_account_id) = chatgpt_account_id {
|
||||
if let Some(existing) = self
|
||||
if let Some(chatgpt_account_id) = chatgpt_account_id
|
||||
&& let Some(existing) = self
|
||||
.store
|
||||
.find_by_chatgpt_identity(chatgpt_account_id, chatgpt_user_id)
|
||||
.await
|
||||
.map_err(|_| AccountManageError::Inspect)?
|
||||
{
|
||||
return Ok(existing.id);
|
||||
}
|
||||
{
|
||||
return Ok(existing.id);
|
||||
}
|
||||
|
||||
Ok(provided_id.unwrap_or_else(|| crate::fleet::import::normalized_account_id(None)))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, HashSet},
|
||||
collections::{HashSet, hash_map::DefaultHasher},
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use futures::{stream, StreamExt};
|
||||
use futures::{StreamExt, stream};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use tokio::time::sleep;
|
||||
use uuid::Uuid;
|
||||
@@ -16,12 +16,12 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
types::{
|
||||
parse_account_status, stored_to_admin_metadata, AccountHealthCheck, AccountManageError,
|
||||
AccountRefreshOutcome, AccountRefreshResult, AccountUpdate, BatchDeleteAccounts,
|
||||
ManagedAccount, ManualCreateTokens,
|
||||
},
|
||||
AccountManageService,
|
||||
types::{
|
||||
AccountHealthCheck, AccountManageError, AccountRefreshOutcome, AccountRefreshResult,
|
||||
AccountUpdate, BatchDeleteAccounts, ManagedAccount, ManualCreateTokens,
|
||||
parse_account_status, stored_to_admin_metadata,
|
||||
},
|
||||
};
|
||||
|
||||
const ADMIN_REFRESH_LEASE_TTL_SECONDS: i64 = 5 * 60;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::HashMap, fmt::Write as _, sync::Arc};
|
||||
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rand::Rng;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
@@ -9,8 +9,8 @@ use sha2::{Digest, Sha256};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::{
|
||||
types::{AccountManageError, OAuthAuthorizeResult, OAuthExchangeInput},
|
||||
AccountManageService,
|
||||
types::{AccountManageError, OAuthAuthorizeResult, OAuthExchangeInput},
|
||||
};
|
||||
|
||||
const OPENAI_OAUTH_AUTHORIZE_ENDPOINT: &str = "https://auth.openai.com/oauth/authorize";
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{convert::Infallible, pin::Pin};
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
use secrecy::ExposeSecret;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
@@ -12,15 +12,15 @@ use crate::{
|
||||
upstream::openai::{
|
||||
protocol::{
|
||||
responses::{CodexResponsesRequest, ResponsesSseFailure},
|
||||
sse::{encode_sse_event, parse_sse_events, SseEvent},
|
||||
sse::{SseEvent, encode_sse_event, parse_sse_events},
|
||||
},
|
||||
transport::{
|
||||
is_banned_auth_signal, is_banned_upstream_error, CodexClientError, CodexRequestContext,
|
||||
CodexClientError, CodexRequestContext, is_banned_auth_signal, is_banned_upstream_error,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{types::AccountManageError, AccountManageService};
|
||||
use super::{AccountManageService, types::AccountManageError};
|
||||
|
||||
pub(super) type AccountTestStream = Pin<Box<dyn Stream<Item = Result<Bytes, Infallible>> + Send>>;
|
||||
|
||||
@@ -221,14 +221,14 @@ impl AccountManageService {
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(status, AccountStatus::Expired | AccountStatus::Banned) {
|
||||
if let Err(error) = self.store.set_next_refresh_at(account_id, None).await {
|
||||
tracing::warn!(
|
||||
account_id,
|
||||
error = %error,
|
||||
"failed to clear token refresh schedule after connection test"
|
||||
);
|
||||
}
|
||||
if matches!(status, AccountStatus::Expired | AccountStatus::Banned)
|
||||
&& let Err(error) = self.store.set_next_refresh_at(account_id, None).await
|
||||
{
|
||||
tracing::warn!(
|
||||
account_id,
|
||||
error = %error,
|
||||
"failed to clear token refresh schedule after connection test"
|
||||
);
|
||||
}
|
||||
self.sync_account_pool_best_effort(account_id, "connection test")
|
||||
.await;
|
||||
@@ -297,10 +297,10 @@ async fn process_upstream_test_stream(
|
||||
}
|
||||
}
|
||||
|
||||
if !buffer.trim().is_empty() {
|
||||
if let Some(outcome) = process_sse_frame(tx, &buffer).await {
|
||||
return outcome;
|
||||
}
|
||||
if !buffer.trim().is_empty()
|
||||
&& let Some(outcome) = process_sse_frame(tx, &buffer).await
|
||||
{
|
||||
return outcome;
|
||||
}
|
||||
|
||||
AccountTestOutcome::error("Stream ended before response.completed", None)
|
||||
@@ -332,10 +332,10 @@ async fn process_sse_event(
|
||||
};
|
||||
match value.get("type").and_then(Value::as_str) {
|
||||
Some("response.output_text.delta") => {
|
||||
if let Some(delta) = value.get("delta").and_then(Value::as_str) {
|
||||
if !delta.is_empty() {
|
||||
send_test_event(tx, json!({ "type": "content", "text": delta })).await;
|
||||
}
|
||||
if let Some(delta) = value.get("delta").and_then(Value::as_str)
|
||||
&& !delta.is_empty()
|
||||
{
|
||||
send_test_event(tx, json!({ "type": "content", "text": delta })).await;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
use super::{types::AccountManageError, AccountManageService};
|
||||
use super::{AccountManageService, types::AccountManageError};
|
||||
|
||||
impl AccountManageService {
|
||||
async fn usage_cookie_header(&self, account_id: &str) -> Option<String> {
|
||||
|
||||
@@ -13,10 +13,10 @@ use indexmap::IndexMap;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::fleet::quota::{quota_snapshot_limit_reached, quota_snapshot_reset_at};
|
||||
use crate::fleet::refresh::{jwt_expiry, JwtExpiry};
|
||||
use crate::fleet::refresh::{JwtExpiry, jwt_expiry};
|
||||
use crate::fleet::scheduler::{
|
||||
candidates::{self, CandidateFilter, CandidateRequest},
|
||||
AccountScheduler, ScoreWeights,
|
||||
candidates::{self, CandidateFilter, CandidateRequest},
|
||||
};
|
||||
use crate::fleet::store::AccountStore;
|
||||
use crate::fleet::{
|
||||
@@ -27,7 +27,7 @@ use crate::telemetry::account_usage::store::{
|
||||
AccountUsageDelta, AccountUsageSnapshot, AccountUsageStore, AccountUsageWindow,
|
||||
};
|
||||
use crate::upstream::openai::protocol::events::{
|
||||
parse_rate_limit_headers, rate_limit_quota, TokenUsage as CodexTokenUsage,
|
||||
TokenUsage as CodexTokenUsage, parse_rate_limit_headers, rate_limit_quota,
|
||||
};
|
||||
|
||||
pub use crate::fleet::scheduler::RotationStrategy;
|
||||
@@ -457,21 +457,20 @@ impl AccountPoolService {
|
||||
"failed to persist refreshed runtime account state"
|
||||
);
|
||||
}
|
||||
if refreshed.sync_usage_window {
|
||||
if let Err(error) = self
|
||||
if refreshed.sync_usage_window
|
||||
&& let Err(error) = self
|
||||
.usage_store
|
||||
.sync_runtime_window(
|
||||
&refreshed.account.id,
|
||||
account_usage_window(&refreshed.account),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
account_id = %refreshed.account.id,
|
||||
error = %error,
|
||||
"failed to persist refreshed runtime account usage window"
|
||||
);
|
||||
}
|
||||
{
|
||||
tracing::error!(
|
||||
account_id = %refreshed.account.id,
|
||||
error = %error,
|
||||
"failed to persist refreshed runtime account usage window"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -641,18 +640,17 @@ impl AccountPoolService {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(reset_at) = reset_at {
|
||||
if let Err(error) = self
|
||||
if let Some(reset_at) = reset_at
|
||||
&& let Err(error) = self
|
||||
.usage_store
|
||||
.sync_rate_limit_window(account_id, reset_at, limit_window_seconds)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
account_id,
|
||||
error = %error,
|
||||
"failed to persist verified quota window"
|
||||
);
|
||||
}
|
||||
{
|
||||
tracing::error!(
|
||||
account_id,
|
||||
error = %error,
|
||||
"failed to persist verified quota window"
|
||||
);
|
||||
}
|
||||
|
||||
let in_memory = {
|
||||
|
||||
@@ -451,13 +451,12 @@ impl AccountPool {
|
||||
});
|
||||
|
||||
let mut account_ids = Vec::with_capacity(ordered.len());
|
||||
if let Some(preferred_account_id) = request.preferred_account_id.as_deref() {
|
||||
if let Some(index) = ordered
|
||||
if let Some(preferred_account_id) = request.preferred_account_id.as_deref()
|
||||
&& let Some(index) = ordered
|
||||
.iter()
|
||||
.position(|account| account.id == preferred_account_id)
|
||||
{
|
||||
account_ids.push(ordered.remove(index).id);
|
||||
}
|
||||
{
|
||||
account_ids.push(ordered.remove(index).id);
|
||||
}
|
||||
account_ids.extend(ordered.into_iter().map(|account| account.id));
|
||||
AccountCandidateSnapshotWithStatusRefresh {
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use chrono::Utc;
|
||||
use thiserror::Error;
|
||||
use tokio::time::{sleep, Instant};
|
||||
use tokio::time::{Instant, sleep};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::fleet::{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const MONTH_WINDOW_MINUTES: u64 = 43_200;
|
||||
const MONTH_WINDOW_SECONDS: u64 = 2_592_000;
|
||||
|
||||
@@ -7,10 +7,10 @@ use std::{
|
||||
time::Duration as StdDuration,
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||
use serde_json::{Map, Value};
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tokio::sync::{Semaphore, watch};
|
||||
|
||||
use crate::fleet::account::{Account, AccountStatus};
|
||||
use crate::upstream::openai::token_client::{RefreshFailure, TokenPair, TokenRefresher};
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex as StdMutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -30,12 +30,12 @@ use crate::upstream::openai::token_client::TokenRefresher;
|
||||
use super::{
|
||||
lease::{RedisRefreshLeaseStore, RedisRefreshLeaseStoreError},
|
||||
policy::{
|
||||
default_refresh_retry_delays, duration_until, is_permanent_refresh_failure_status,
|
||||
jittered_refresh_at, jwt_expiration, same_refresh_time, scheduled_at_from_delay,
|
||||
stable_jittered_duration, token_refresh_status_eligible, RefreshError, RefreshPolicy,
|
||||
RefreshScheduler, RuntimeRefreshPolicy, TokenTimerSummary,
|
||||
PERMANENT_FAILURE_CONFIRMATION_THRESHOLD, RECOVERY_DELAY_JITTER, RECOVERY_DELAY_SECONDS,
|
||||
RETRY_DELAY_JITTER,
|
||||
RETRY_DELAY_JITTER, RefreshError, RefreshPolicy, RefreshScheduler, RuntimeRefreshPolicy,
|
||||
TokenTimerSummary, default_refresh_retry_delays, duration_until,
|
||||
is_permanent_refresh_failure_status, jittered_refresh_at, jwt_expiration,
|
||||
same_refresh_time, scheduled_at_from_delay, stable_jittered_duration,
|
||||
token_refresh_status_eligible,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//! 对齐 sub2api 的设计,避免选择热路径上的锁与 DB 往返。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// EWMA 平滑系数(新样本权重)。与 sub2api 一致取 0.2。
|
||||
const EWMA_ALPHA: f64 = 0.2;
|
||||
@@ -59,11 +59,7 @@ impl AccountFeedback {
|
||||
/// 读取 EWMA 样本;哨兵(NaN)返回 `None`。
|
||||
fn load_sample(bits: &AtomicU64) -> Option<f64> {
|
||||
let value = f64::from_bits(bits.load(Ordering::Relaxed));
|
||||
if value.is_nan() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
if value.is_nan() { None } else { Some(value) }
|
||||
}
|
||||
|
||||
/// 无锁 CAS 更新 EWMA:首个样本直接落值,其后按 alpha 混合。
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::cmp::Ordering;
|
||||
|
||||
use crate::fleet::account::Account;
|
||||
use crate::fleet::scheduler::{
|
||||
compare_last_used, compare_window_reset, order_by, select_by, SelectionInput,
|
||||
SelectionInput, compare_last_used, compare_window_reset, order_by, select_by,
|
||||
};
|
||||
|
||||
/// QuotaResetPriority 策略选择。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
use crate::fleet::account::Account;
|
||||
use crate::fleet::scheduler::feedback::{FeedbackSample, FeedbackStats};
|
||||
use crate::fleet::scheduler::{account_window_token_count, SelectionInput};
|
||||
use crate::fleet::scheduler::{SelectionInput, account_window_token_count};
|
||||
|
||||
/// 配额封顶账号的惩罚分。远大于各因子加权和的理论上限(各因子 ∈ [0,1],权重和通常为
|
||||
/// 个位数),保证未封顶账号总是优先于封顶账号。
|
||||
@@ -202,11 +202,11 @@ impl NormalizationContext {
|
||||
max_window_cached = max_window_cached.max(account.window_cached_tokens as f64);
|
||||
max_window_requests = max_window_requests.max(account.window_request_count as f64);
|
||||
|
||||
if let Some(ttft) = sample.ttft_ms {
|
||||
if ttft > 0.0 {
|
||||
min_ttft_ms = min_ttft_ms.min(ttft);
|
||||
max_ttft_ms = max_ttft_ms.max(ttft);
|
||||
}
|
||||
if let Some(ttft) = sample.ttft_ms
|
||||
&& ttft > 0.0
|
||||
{
|
||||
min_ttft_ms = min_ttft_ms.min(ttft);
|
||||
max_ttft_ms = max_ttft_ms.max(ttft);
|
||||
}
|
||||
|
||||
if let Some(reset_at) = account.window_reset_at {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::fleet::account::Account;
|
||||
use crate::fleet::scheduler::{compare_last_used, SelectionInput};
|
||||
use crate::fleet::scheduler::{SelectionInput, compare_last_used};
|
||||
|
||||
/// 选中最近使用过的账号(`last_used_at` 最大者)。
|
||||
///
|
||||
|
||||
@@ -8,7 +8,7 @@ use sqlx::{PgPool, Postgres, QueryBuilder, Row};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::fleet::account::{Account, AccountStatus};
|
||||
use crate::infra::json::{page_offset, NumberedPage};
|
||||
use crate::infra::json::{NumberedPage, page_offset};
|
||||
|
||||
mod queries;
|
||||
mod rows;
|
||||
@@ -249,7 +249,7 @@ pub trait AccountStore: Send + Sync + 'static {
|
||||
|
||||
/// 更新账号状态。
|
||||
async fn set_status(&self, account_id: &str, status: AccountStatus)
|
||||
-> AccountStoreResult<bool>;
|
||||
-> AccountStoreResult<bool>;
|
||||
|
||||
/// 读取账号当前配额 JSON。
|
||||
async fn get_quota_json(&self, account_id: &str) -> AccountStoreResult<Option<String>>;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use secrecy::SecretString;
|
||||
use serde_json::Value;
|
||||
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||
use sqlx::{PgPool, Postgres, QueryBuilder, Row, postgres::PgRow};
|
||||
|
||||
use crate::{
|
||||
fleet::{
|
||||
@@ -13,9 +13,9 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
queries::{GET_POOL_ACCOUNT_SQL, LIST_POOL_ACCOUNTS_SQL},
|
||||
AccountQuotaSnapshot, AccountStoreError, PgAccountStore, PgAccountStoreError,
|
||||
PgAccountStoreResult, StoredAccount, StoredAccountMetadata,
|
||||
queries::{GET_POOL_ACCOUNT_SQL, LIST_POOL_ACCOUNTS_SQL},
|
||||
};
|
||||
|
||||
pub(super) async fn list_pool_accounts(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{postgres::PgPoolOptions, PgPool, Postgres, Row, Transaction};
|
||||
use sqlx::{PgPool, Postgres, Row, Transaction, postgres::PgPoolOptions};
|
||||
|
||||
const MIGRATIONS: &[Migration] = &[Migration {
|
||||
version: 1,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! 身份认证相关原语:管理员密码哈希与 API Key 生成。
|
||||
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Argon2,
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
|
||||
};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
@@ -16,10 +16,10 @@ use chrono::{DateTime, Duration, NaiveDate, Utc};
|
||||
use thiserror::Error;
|
||||
use tracing_appender::non_blocking::{ErrorCounter, NonBlockingBuilder, WorkerGuard};
|
||||
use tracing_subscriber::{
|
||||
EnvFilter,
|
||||
fmt::{self as tracing_fmt, format::Writer, time::FormatTime},
|
||||
layer::SubscriberExt,
|
||||
util::SubscriberInitExt,
|
||||
EnvFilter,
|
||||
};
|
||||
|
||||
use crate::infra::time::{china_date, china_rfc3339_millis};
|
||||
@@ -459,14 +459,16 @@ impl DroppedLogMonitor {
|
||||
let (shutdown_tx, shutdown_rx) = mpsc::channel();
|
||||
let handle = thread::Builder::new()
|
||||
.name("tracing-drop-monitor".to_string())
|
||||
.spawn(move || loop {
|
||||
match shutdown_rx.recv_timeout(DROP_MONITOR_INTERVAL) {
|
||||
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
report_dropped_logs(&mut counters, &mut io::stderr().lock());
|
||||
break;
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
report_dropped_logs(&mut counters, &mut io::stderr().lock());
|
||||
.spawn(move || {
|
||||
loop {
|
||||
match shutdown_rx.recv_timeout(DROP_MONITOR_INTERVAL) {
|
||||
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
report_dropped_logs(&mut counters, &mut io::stderr().lock());
|
||||
break;
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
report_dropped_logs(&mut counters, &mut io::stderr().lock());
|
||||
}
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Redis 运行态连接与键前缀。
|
||||
|
||||
use redis::{aio::ConnectionManager, AsyncCommands};
|
||||
use redis::{AsyncCommands, aio::ConnectionManager};
|
||||
|
||||
/// Redis 运行态连接。
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::infra::json::NumberedPage;
|
||||
use super::{
|
||||
store::PgClientKeyStore,
|
||||
types::{
|
||||
parse_client_key_status, BatchDeleteClientApiKeys, KeyManageError, ManagedClientApiKey,
|
||||
BatchDeleteClientApiKeys, KeyManageError, ManagedClientApiKey, parse_client_key_status,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::infra::{
|
||||
identity::generate_client_api_key,
|
||||
json::{page_offset, NumberedPage},
|
||||
json::{NumberedPage, page_offset},
|
||||
};
|
||||
|
||||
const CLIENT_KEY_SELECT_SQL: &str = r"select
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use subtle::ConstantTimeEq;
|
||||
use tokio::sync::{watch, Mutex};
|
||||
use tokio::sync::{Mutex, watch};
|
||||
|
||||
use crate::infra::identity::{generate_admin_api_key, hash_credential};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::Utc;
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
use sqlx::{PgPool, Row, postgres::PgRow};
|
||||
|
||||
use super::types::{ManagementApiKeyStatus, SettingsError, SettingsSnapshot};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||
use sqlx::{PgPool, Postgres, QueryBuilder, Row, postgres::PgRow};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::infra::format::nonnegative_i64_to_u64;
|
||||
|
||||
@@ -5,7 +5,7 @@ use sqlx::{PgPool, Postgres, QueryBuilder, Row};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
infra::json::{page_offset, NumberedPage},
|
||||
infra::json::{NumberedPage, page_offset},
|
||||
telemetry::{buckets::store::PgRequestBucketStore, ops::types::OpsErrorLog},
|
||||
};
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::{
|
||||
usage::{
|
||||
store::{PgUsageRecordStore, PgUsageRecordStoreError},
|
||||
types::{
|
||||
metadata_i64, metadata_service_tier, metadata_string as metadata_string_any,
|
||||
ResponseUsageRecord, UsageRecord,
|
||||
ResponseUsageRecord, UsageRecord, metadata_i64, metadata_service_tier,
|
||||
metadata_string as metadata_string_any,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -150,13 +150,13 @@ pub(crate) async fn record_response_event(record: ResponseUsageRecord<'_>) {
|
||||
record.reasoning_effort,
|
||||
record.service_tier,
|
||||
);
|
||||
if !record.rate_limit_headers.is_empty() {
|
||||
if let Some(object) = metadata.as_object_mut() {
|
||||
object.insert(
|
||||
"rateLimitHeaders".to_string(),
|
||||
serde_json::json!(record.rate_limit_headers),
|
||||
);
|
||||
}
|
||||
if !record.rate_limit_headers.is_empty()
|
||||
&& let Some(object) = metadata.as_object_mut()
|
||||
{
|
||||
object.insert(
|
||||
"rateLimitHeaders".to_string(),
|
||||
serde_json::json!(record.rate_limit_headers),
|
||||
);
|
||||
}
|
||||
|
||||
lift_success_fact_fields(&mut event, &mut metadata);
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
telemetry::{
|
||||
billing,
|
||||
usage::store::{
|
||||
push_filter, PgUsageRecordStore, PgUsageRecordStoreResult, UsageRecordFilter,
|
||||
PgUsageRecordStore, PgUsageRecordStoreResult, UsageRecordFilter, push_filter,
|
||||
},
|
||||
usage::types::UsageRecord,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! PostgreSQL 成功使用事实存储。
|
||||
|
||||
use super::query::{
|
||||
count_usage_records, usage_account_usage, usage_breakdown, usage_record_account_email_map,
|
||||
usage_record_from_row, usage_summary, usage_trend, UsageRecordAccountUsage,
|
||||
UsageRecordBreakdown, UsageRecordEndpointSource, UsageRecordModelSource, UsageRecordSummary,
|
||||
UsageRecordTrendPoint,
|
||||
UsageRecordAccountUsage, UsageRecordBreakdown, UsageRecordEndpointSource,
|
||||
UsageRecordModelSource, UsageRecordSummary, UsageRecordTrendPoint, count_usage_records,
|
||||
usage_account_usage, usage_breakdown, usage_record_account_email_map, usage_record_from_row,
|
||||
usage_summary, usage_trend,
|
||||
};
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -14,7 +14,7 @@ use sqlx::{PgPool, Postgres, QueryBuilder};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
infra::json::{page_offset, NumberedPage},
|
||||
infra::json::{NumberedPage, page_offset},
|
||||
telemetry::{buckets::store::PgRequestBucketStore, usage::types::UsageRecord},
|
||||
};
|
||||
|
||||
@@ -221,12 +221,8 @@ insert into usage_records (
|
||||
source: UsageRecordModelSource,
|
||||
) -> PgUsageRecordStoreResult<Vec<UsageRecordBreakdown>> {
|
||||
let expression = match source {
|
||||
UsageRecordModelSource::Requested => {
|
||||
"coalesce(nullif(requested_model, ''), model)"
|
||||
}
|
||||
UsageRecordModelSource::Upstream => {
|
||||
"coalesce(nullif(upstream_model, ''), model)"
|
||||
}
|
||||
UsageRecordModelSource::Requested => "coalesce(nullif(requested_model, ''), model)",
|
||||
UsageRecordModelSource::Upstream => "coalesce(nullif(upstream_model, ''), model)",
|
||||
UsageRecordModelSource::Mapping => {
|
||||
"coalesce(nullif(requested_model, ''), model) || ' -> ' || coalesce(nullif(upstream_model, ''), model)"
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use tar::Archive;
|
||||
|
||||
use crate::update::types::UpdateError;
|
||||
|
||||
use super::service::{internal_error, internal_error_with, SystemUpdateConfig, APP_BINARY_NAME};
|
||||
use super::service::{APP_BINARY_NAME, SystemUpdateConfig, internal_error, internal_error_with};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ExtractedRelease {
|
||||
@@ -125,22 +125,22 @@ pub(super) fn replace_release_files(
|
||||
};
|
||||
|
||||
let binary_backup = backup_path_for(exe_path);
|
||||
if binary_backup.exists() {
|
||||
if let Err(error) = fs::remove_file(&binary_backup) {
|
||||
let mut rollback_errors = Vec::new();
|
||||
if web_replaced {
|
||||
collect_rollback_error(
|
||||
&mut rollback_errors,
|
||||
"restore web assets",
|
||||
restore_dir(web_dist_dir, &web_backup),
|
||||
);
|
||||
}
|
||||
return Err(update_error_with_rollback(
|
||||
"Failed to remove old binary backup",
|
||||
error,
|
||||
rollback_errors,
|
||||
));
|
||||
if binary_backup.exists()
|
||||
&& let Err(error) = fs::remove_file(&binary_backup)
|
||||
{
|
||||
let mut rollback_errors = Vec::new();
|
||||
if web_replaced {
|
||||
collect_rollback_error(
|
||||
&mut rollback_errors,
|
||||
"restore web assets",
|
||||
restore_dir(web_dist_dir, &web_backup),
|
||||
);
|
||||
}
|
||||
return Err(update_error_with_rollback(
|
||||
"Failed to remove old binary backup",
|
||||
error,
|
||||
rollback_errors,
|
||||
));
|
||||
}
|
||||
if let Err(error) = move_file(exe_path, &binary_backup) {
|
||||
let mut rollback_errors = Vec::new();
|
||||
|
||||
@@ -12,14 +12,15 @@ use sha2::{Digest, Sha256};
|
||||
use crate::update::types::UpdateError;
|
||||
|
||||
use super::service::{
|
||||
bad_gateway_with, bad_request_with, emit_update_event, format_bytes, internal_error_with,
|
||||
UpdateLogLevel,
|
||||
UpdateEventSender, UpdateLogLevel, bad_gateway_with, bad_request_with, format_bytes,
|
||||
internal_error_with,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct DownloadProgress<'a> {
|
||||
pub operation_id: &'a str,
|
||||
pub total_size: u64,
|
||||
pub events: &'a UpdateEventSender,
|
||||
}
|
||||
|
||||
pub(super) async fn download_file(
|
||||
@@ -65,7 +66,7 @@ pub(super) async fn download_file(
|
||||
if progress.total_size > 0
|
||||
&& (percent >= next_progress || downloaded >= progress.total_size)
|
||||
{
|
||||
emit_update_event(
|
||||
progress.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(progress.operation_id),
|
||||
Some("download"),
|
||||
|
||||
@@ -13,13 +13,11 @@ use crate::update::types::UpdateError;
|
||||
|
||||
use super::{
|
||||
download::validate_github_api_base,
|
||||
service::{build_type_label, deployment_mode_label, SystemUpdateConfig, APP_BINARY_NAME},
|
||||
service::{APP_BINARY_NAME, SystemUpdateConfig, build_type_label, deployment_mode_label},
|
||||
};
|
||||
|
||||
const UPDATE_CACHE_TTL: Duration = Duration::from_secs(20 * 60);
|
||||
|
||||
static RELEASE_CACHE: Mutex<Option<CachedUpdateInfo>> = Mutex::const_new(None);
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct UpdateInfoData {
|
||||
@@ -45,6 +43,18 @@ struct CachedUpdateInfo {
|
||||
cached_at: Instant,
|
||||
}
|
||||
|
||||
pub(super) struct ReleaseCache {
|
||||
entry: Mutex<Option<CachedUpdateInfo>>,
|
||||
}
|
||||
|
||||
impl Default for ReleaseCache {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entry: Mutex::const_new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(super) struct GitHubRelease {
|
||||
pub tag_name: String,
|
||||
@@ -64,6 +74,7 @@ pub(super) struct GitHubAsset {
|
||||
}
|
||||
|
||||
pub(super) async fn check_latest_release(
|
||||
cache: &ReleaseCache,
|
||||
config: &SystemUpdateConfig,
|
||||
force: bool,
|
||||
) -> UpdateInfoData {
|
||||
@@ -103,19 +114,17 @@ pub(super) async fn check_latest_release(
|
||||
}
|
||||
let cache_key = config.release_cache_key();
|
||||
|
||||
if !force {
|
||||
if let Some(info) = cached_release_info(&cache_key).await {
|
||||
return info;
|
||||
}
|
||||
if !force && let Some(info) = cached_release_info(cache, &cache_key).await {
|
||||
return info;
|
||||
}
|
||||
|
||||
match fetch_latest_release(&config.github_api_base, repository).await {
|
||||
Ok(release) => {
|
||||
let info = update_info_from_release(config, release);
|
||||
cache_release_info(cache_key, &info).await;
|
||||
cache_release_info(cache, cache_key, &info).await;
|
||||
info
|
||||
}
|
||||
Err(error) => cached_release_info(&cache_key)
|
||||
Err(error) => cached_release_info(cache, &cache_key)
|
||||
.await
|
||||
.unwrap_or_else(|| UpdateInfoData {
|
||||
current_version: config.version.clone(),
|
||||
@@ -224,24 +233,24 @@ pub(super) fn select_release_archive<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
async fn cached_release_info(cache_key: &str) -> Option<UpdateInfoData> {
|
||||
async fn cached_release_info(cache: &ReleaseCache, cache_key: &str) -> Option<UpdateInfoData> {
|
||||
let mut info = {
|
||||
let cache = RELEASE_CACHE.lock().await;
|
||||
let cached = cache.as_ref()?;
|
||||
let entry = cache.entry.lock().await;
|
||||
let cached = entry.as_ref()?;
|
||||
if cached.key != cache_key || cached.cached_at.elapsed() > UPDATE_CACHE_TTL {
|
||||
return None;
|
||||
}
|
||||
let info = cached.info.clone();
|
||||
drop(cache);
|
||||
drop(entry);
|
||||
info
|
||||
};
|
||||
info.cached = true;
|
||||
Some(info)
|
||||
}
|
||||
|
||||
async fn cache_release_info(cache_key: String, info: &UpdateInfoData) {
|
||||
let mut cache = RELEASE_CACHE.lock().await;
|
||||
*cache = Some(CachedUpdateInfo {
|
||||
async fn cache_release_info(cache: &ReleaseCache, cache_key: String, info: &UpdateInfoData) {
|
||||
let mut entry = cache.entry.lock().await;
|
||||
*entry = Some(CachedUpdateInfo {
|
||||
key: cache_key,
|
||||
info: info.clone(),
|
||||
cached_at: Instant::now(),
|
||||
|
||||
+386
-361
@@ -4,26 +4,25 @@ use std::{
|
||||
env, fmt, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
|
||||
use super::{
|
||||
archive::{extract_release_archive, replace_release_files, rollback_release_update},
|
||||
download::{
|
||||
download_file, validate_download_url, validate_github_api_base, verify_checksum,
|
||||
DownloadProgress,
|
||||
DownloadProgress, download_file, validate_download_url, validate_github_api_base,
|
||||
verify_checksum,
|
||||
},
|
||||
release::{
|
||||
check_latest_release, fetch_latest_release, normalize_version_tag, select_release_archive,
|
||||
update_info_from_release, GitHubRelease, UpdateInfoData,
|
||||
GitHubRelease, ReleaseCache, UpdateInfoData, check_latest_release, fetch_latest_release,
|
||||
normalize_version_tag, select_release_archive, update_info_from_release,
|
||||
},
|
||||
state::{
|
||||
finish_operation, operation_id, read_state, set_operation_running, OperationLock,
|
||||
SystemOperationKind, SystemUpdateStatusData,
|
||||
OperationLock, SystemOperationKind, SystemUpdateStatusData, finish_operation, operation_id,
|
||||
read_state, set_operation_running,
|
||||
},
|
||||
types::UpdateError,
|
||||
};
|
||||
@@ -35,9 +34,6 @@ const MAX_DOWNLOAD_SIZE: u64 = 500 * 1024 * 1024;
|
||||
const RESTART_DELAY_ENV: &str = "CPR_RESTART_DELAY_MS";
|
||||
const REPLACEMENT_START_DELAY_MS: &str = "1200";
|
||||
|
||||
static SYSTEM_OPERATION_LOCK: Mutex<()> = Mutex::const_new(());
|
||||
static UPDATE_EVENT_SENDER: OnceLock<broadcast::Sender<SystemUpdateEvent>> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct VersionData {
|
||||
@@ -96,145 +92,170 @@ impl SystemUpdateEvent {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct SystemUpdateConfig {
|
||||
pub(super) version: String,
|
||||
pub(super) git_sha: String,
|
||||
pub(super) build_time: String,
|
||||
pub(super) deployment_mode: String,
|
||||
pub(super) build_type: String,
|
||||
pub(super) update_channel: String,
|
||||
pub(super) update_repository: Option<String>,
|
||||
pub(super) github_api_base: String,
|
||||
pub(super) executable_path: Option<PathBuf>,
|
||||
pub(super) web_dist_dir: PathBuf,
|
||||
pub(super) update_state_file: PathBuf,
|
||||
pub(super) update_lock_file: PathBuf,
|
||||
pub(super) update_temp_dir: PathBuf,
|
||||
pub struct SystemUpdateConfig {
|
||||
pub version: String,
|
||||
pub git_sha: String,
|
||||
pub build_time: String,
|
||||
pub deployment_mode: String,
|
||||
pub build_type: String,
|
||||
pub update_channel: String,
|
||||
pub update_repository: Option<String>,
|
||||
pub github_api_base: String,
|
||||
pub executable_path: Option<PathBuf>,
|
||||
pub web_dist_dir: PathBuf,
|
||||
pub update_state_file: PathBuf,
|
||||
pub update_lock_file: PathBuf,
|
||||
pub update_temp_dir: PathBuf,
|
||||
pub self_restart_enabled: bool,
|
||||
}
|
||||
|
||||
pub(crate) async fn version_data() -> VersionData {
|
||||
let config = SystemUpdateConfig::from_env();
|
||||
let update_info = check_latest_release(&config, false).await;
|
||||
config.version_data(&update_info)
|
||||
pub struct SystemUpdateService {
|
||||
config: SystemUpdateConfig,
|
||||
operation_lock: Mutex<()>,
|
||||
release_cache: ReleaseCache,
|
||||
events: UpdateEventSender,
|
||||
}
|
||||
|
||||
pub(crate) async fn update_detail(refresh: bool) -> UpdateInfoData {
|
||||
let config = SystemUpdateConfig::from_env();
|
||||
check_latest_release(&config, refresh).await
|
||||
}
|
||||
|
||||
pub(crate) fn subscribe_update_events() -> broadcast::Receiver<SystemUpdateEvent> {
|
||||
update_event_sender().subscribe()
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_update(
|
||||
target_version: Option<String>,
|
||||
) -> Result<UpdateStartedData, UpdateError> {
|
||||
let _guard = SYSTEM_OPERATION_LOCK
|
||||
.try_lock()
|
||||
.map_err(|_| UpdateError::conflict("System update already running"))?;
|
||||
let config = SystemUpdateConfig::from_env();
|
||||
if let Some(reason) = config.update_support_error() {
|
||||
emit_terminal_update_event(
|
||||
UpdateLogLevel::Error,
|
||||
None,
|
||||
Some("preflight"),
|
||||
reason.clone(),
|
||||
);
|
||||
return Err(UpdateError::conflict(reason));
|
||||
impl SystemUpdateService {
|
||||
pub fn new(config: SystemUpdateConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
operation_lock: Mutex::const_new(()),
|
||||
release_cache: ReleaseCache::default(),
|
||||
events: UpdateEventSender::default(),
|
||||
}
|
||||
}
|
||||
let confirmed_target_version = confirmed_update_target(target_version)?;
|
||||
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
None,
|
||||
Some("release"),
|
||||
"正在获取最新 Release 信息",
|
||||
);
|
||||
let release = fetch_latest_release(
|
||||
&config.github_api_base,
|
||||
config
|
||||
.update_repository
|
||||
.as_deref()
|
||||
.ok_or_else(|| UpdateError::conflict("Update checks require CPR_UPDATE_REPOSITORY"))?,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
emit_terminal_update_event(UpdateLogLevel::Error, None, Some("release"), error.clone());
|
||||
UpdateError::bad_gateway(error)
|
||||
})?;
|
||||
let info = update_info_from_release(&config, release.clone());
|
||||
if info.latest_version != confirmed_target_version {
|
||||
let message = format!(
|
||||
"远端最新版本已变更为 v{},请重新检查并确认",
|
||||
info.latest_version
|
||||
);
|
||||
emit_terminal_update_event(
|
||||
UpdateLogLevel::Warning,
|
||||
pub(crate) fn from_env() -> Self {
|
||||
Self::new(SystemUpdateConfig::from_env())
|
||||
}
|
||||
|
||||
pub(crate) async fn version_data(&self) -> VersionData {
|
||||
let update_info = check_latest_release(&self.release_cache, &self.config, false).await;
|
||||
self.config.version_data(&update_info)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_detail(&self, refresh: bool) -> UpdateInfoData {
|
||||
check_latest_release(&self.release_cache, &self.config, refresh).await
|
||||
}
|
||||
|
||||
pub(crate) fn subscribe_update_events(&self) -> broadcast::Receiver<SystemUpdateEvent> {
|
||||
self.events.subscribe()
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_update(
|
||||
&self,
|
||||
target_version: Option<String>,
|
||||
) -> Result<UpdateStartedData, UpdateError> {
|
||||
let _guard = self
|
||||
.operation_lock
|
||||
.try_lock()
|
||||
.map_err(|_| UpdateError::conflict("System update already running"))?;
|
||||
let config = &self.config;
|
||||
if let Some(reason) = config.update_support_error() {
|
||||
self.events.emit_terminal(
|
||||
UpdateLogLevel::Error,
|
||||
None,
|
||||
Some("preflight"),
|
||||
reason.clone(),
|
||||
);
|
||||
return Err(UpdateError::conflict(reason));
|
||||
}
|
||||
let confirmed_target_version = confirmed_update_target(target_version)?;
|
||||
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
None,
|
||||
Some("release"),
|
||||
message.clone(),
|
||||
"正在获取最新 Release 信息",
|
||||
);
|
||||
return Err(UpdateError::conflict(message));
|
||||
}
|
||||
if !info.has_update {
|
||||
emit_terminal_update_event(
|
||||
UpdateLogLevel::Warning,
|
||||
None,
|
||||
Some("release"),
|
||||
"当前版本已是最新",
|
||||
);
|
||||
return Err(UpdateError::conflict("Already up to date"));
|
||||
}
|
||||
let target_version = info.latest_version.clone();
|
||||
let operation_id = operation_id("update");
|
||||
let lock = OperationLock::acquire(&config.update_lock_file)?;
|
||||
set_operation_running(
|
||||
&config.update_state_file,
|
||||
&operation_id,
|
||||
SystemOperationKind::Update,
|
||||
Some(&target_version),
|
||||
)?;
|
||||
let release = fetch_latest_release(
|
||||
&config.github_api_base,
|
||||
config.update_repository.as_deref().ok_or_else(|| {
|
||||
UpdateError::conflict("Update checks require CPR_UPDATE_REPOSITORY")
|
||||
})?,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
self.events
|
||||
.emit_terminal(UpdateLogLevel::Error, None, Some("release"), error.clone());
|
||||
UpdateError::bad_gateway(error)
|
||||
})?;
|
||||
let info = update_info_from_release(config, release.clone());
|
||||
if info.latest_version != confirmed_target_version {
|
||||
let message = format!(
|
||||
"远端最新版本已变更为 v{},请重新检查并确认",
|
||||
info.latest_version
|
||||
);
|
||||
self.events.emit_terminal(
|
||||
UpdateLogLevel::Warning,
|
||||
None,
|
||||
Some("release"),
|
||||
message.clone(),
|
||||
);
|
||||
return Err(UpdateError::conflict(message));
|
||||
}
|
||||
if !info.has_update {
|
||||
self.events.emit_terminal(
|
||||
UpdateLogLevel::Warning,
|
||||
None,
|
||||
Some("release"),
|
||||
"当前版本已是最新",
|
||||
);
|
||||
return Err(UpdateError::conflict("Already up to date"));
|
||||
}
|
||||
let target_version = info.latest_version.clone();
|
||||
let operation_id = operation_id("update");
|
||||
let lock = OperationLock::acquire(&config.update_lock_file)?;
|
||||
set_operation_running(
|
||||
&config.update_state_file,
|
||||
&operation_id,
|
||||
SystemOperationKind::Update,
|
||||
Some(&target_version),
|
||||
)?;
|
||||
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(&operation_id),
|
||||
Some("prepare"),
|
||||
format!("准备更新到 v{target_version}"),
|
||||
);
|
||||
let result = perform_release_update(&config, &release, &target_version, &operation_id).await;
|
||||
if let Err(error) = &result {
|
||||
emit_terminal_update_event(
|
||||
UpdateLogLevel::Error,
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(&operation_id),
|
||||
Some("failed"),
|
||||
error.to_string(),
|
||||
Some("prepare"),
|
||||
format!("准备更新到 v{target_version}"),
|
||||
);
|
||||
} else {
|
||||
emit_terminal_update_event(
|
||||
UpdateLogLevel::Success,
|
||||
Some(&operation_id),
|
||||
Some("done"),
|
||||
"更新文件已替换,等待服务重启生效",
|
||||
let result = self
|
||||
.perform_release_update(&release, &target_version, &operation_id)
|
||||
.await;
|
||||
if let Err(error) = &result {
|
||||
self.events.emit_terminal(
|
||||
UpdateLogLevel::Error,
|
||||
Some(&operation_id),
|
||||
Some("failed"),
|
||||
error.to_string(),
|
||||
);
|
||||
} else {
|
||||
self.events.emit_terminal(
|
||||
UpdateLogLevel::Success,
|
||||
Some(&operation_id),
|
||||
Some("done"),
|
||||
"更新文件已替换,等待服务重启生效",
|
||||
);
|
||||
}
|
||||
finish_operation(
|
||||
&config.update_state_file,
|
||||
&operation_id,
|
||||
SystemOperationKind::Update,
|
||||
result.as_ref().ok().map(|_| target_version.clone()),
|
||||
result.as_ref().err().map(ToString::to_string),
|
||||
);
|
||||
}
|
||||
finish_operation(
|
||||
&config.update_state_file,
|
||||
&operation_id,
|
||||
SystemOperationKind::Update,
|
||||
result.as_ref().ok().map(|_| target_version.clone()),
|
||||
result.as_ref().err().map(ToString::to_string),
|
||||
);
|
||||
drop(lock);
|
||||
result?;
|
||||
drop(lock);
|
||||
result?;
|
||||
|
||||
Ok(UpdateStartedData {
|
||||
operation_id,
|
||||
deployment_mode: config.deployment_mode,
|
||||
message: "更新完成,请重启服务。".to_string(),
|
||||
need_restart: true,
|
||||
target_version,
|
||||
})
|
||||
Ok(UpdateStartedData {
|
||||
operation_id,
|
||||
deployment_mode: config.deployment_mode.clone(),
|
||||
message: "更新完成,请重启服务。".to_string(),
|
||||
need_restart: true,
|
||||
target_version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn confirmed_update_target(target_version: Option<String>) -> Result<String, UpdateError> {
|
||||
@@ -248,51 +269,52 @@ fn confirmed_update_target(target_version: Option<String>) -> Result<String, Upd
|
||||
Ok(target_version)
|
||||
}
|
||||
|
||||
pub(crate) fn update_status() -> Result<SystemUpdateStatusData, UpdateError> {
|
||||
let config = SystemUpdateConfig::from_env();
|
||||
read_state(&config.update_state_file)
|
||||
}
|
||||
|
||||
pub(crate) async fn rollback() -> Result<String, UpdateError> {
|
||||
let _guard = SYSTEM_OPERATION_LOCK
|
||||
.try_lock()
|
||||
.map_err(|_| UpdateError::conflict("系统操作正在执行中"))?;
|
||||
let config = SystemUpdateConfig::from_env();
|
||||
if let Some(reason) = config.update_support_error() {
|
||||
return Err(UpdateError::conflict(reason));
|
||||
impl SystemUpdateService {
|
||||
pub(crate) fn update_status(&self) -> Result<SystemUpdateStatusData, UpdateError> {
|
||||
read_state(&self.config.update_state_file)
|
||||
}
|
||||
|
||||
let operation_id = operation_id("rollback");
|
||||
let lock = OperationLock::acquire(&config.update_lock_file)?;
|
||||
set_operation_running(
|
||||
&config.update_state_file,
|
||||
&operation_id,
|
||||
SystemOperationKind::Rollback,
|
||||
None,
|
||||
)?;
|
||||
let result = rollback_release_update(&config).await;
|
||||
finish_operation(
|
||||
&config.update_state_file,
|
||||
&operation_id,
|
||||
SystemOperationKind::Rollback,
|
||||
None,
|
||||
result.as_ref().err().map(ToString::to_string),
|
||||
);
|
||||
drop(lock);
|
||||
result?;
|
||||
pub(crate) async fn rollback(&self) -> Result<String, UpdateError> {
|
||||
let _guard = self
|
||||
.operation_lock
|
||||
.try_lock()
|
||||
.map_err(|_| UpdateError::conflict("系统操作正在执行中"))?;
|
||||
let config = &self.config;
|
||||
if let Some(reason) = config.update_support_error() {
|
||||
return Err(UpdateError::conflict(reason));
|
||||
}
|
||||
|
||||
Ok(operation_id)
|
||||
}
|
||||
let operation_id = operation_id("rollback");
|
||||
let lock = OperationLock::acquire(&config.update_lock_file)?;
|
||||
set_operation_running(
|
||||
&config.update_state_file,
|
||||
&operation_id,
|
||||
SystemOperationKind::Rollback,
|
||||
None,
|
||||
)?;
|
||||
let result = rollback_release_update(config).await;
|
||||
finish_operation(
|
||||
&config.update_state_file,
|
||||
&operation_id,
|
||||
SystemOperationKind::Rollback,
|
||||
None,
|
||||
result.as_ref().err().map(ToString::to_string),
|
||||
);
|
||||
drop(lock);
|
||||
result?;
|
||||
|
||||
pub(crate) fn restart_plan() -> Result<RestartPlan, UpdateError> {
|
||||
if env_string("CPR_ENABLE_SELF_RESTART").as_deref() != Some("true") {
|
||||
return Err(UpdateError::conflict(
|
||||
"自重启未启用,请设置 CPR_ENABLE_SELF_RESTART=true",
|
||||
));
|
||||
Ok(operation_id)
|
||||
}
|
||||
|
||||
let config = SystemUpdateConfig::from_env();
|
||||
schedule_restart(&config)
|
||||
pub(crate) fn restart_plan(&self) -> Result<RestartPlan, UpdateError> {
|
||||
if !self.config.self_restart_enabled {
|
||||
return Err(UpdateError::conflict(
|
||||
"自重启未启用,请设置 CPR_ENABLE_SELF_RESTART=true",
|
||||
));
|
||||
}
|
||||
|
||||
schedule_restart(&self.config)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RestartPlan {
|
||||
@@ -369,6 +391,7 @@ impl SystemUpdateConfig {
|
||||
update_state_file: state_file,
|
||||
update_lock_file,
|
||||
update_temp_dir,
|
||||
self_restart_enabled: env_string("CPR_ENABLE_SELF_RESTART").as_deref() == Some("true"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,127 +466,131 @@ pub(super) fn bad_gateway_with<E: fmt::Display>(
|
||||
move |error| UpdateError::bad_gateway(format!("{context}: {error}"))
|
||||
}
|
||||
|
||||
async fn perform_release_update(
|
||||
config: &SystemUpdateConfig,
|
||||
release: &GitHubRelease,
|
||||
version: &str,
|
||||
operation_id: &str,
|
||||
) -> Result<(), UpdateError> {
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("asset"),
|
||||
"正在选择匹配当前平台的更新包",
|
||||
);
|
||||
let archive = select_release_archive(release, version)?;
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("asset"),
|
||||
format!(
|
||||
"已选择更新包 {} ({})",
|
||||
archive.name,
|
||||
format_bytes(archive.size)
|
||||
),
|
||||
);
|
||||
impl SystemUpdateService {
|
||||
async fn perform_release_update(
|
||||
&self,
|
||||
release: &GitHubRelease,
|
||||
version: &str,
|
||||
operation_id: &str,
|
||||
) -> Result<(), UpdateError> {
|
||||
let config = &self.config;
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("asset"),
|
||||
"正在选择匹配当前平台的更新包",
|
||||
);
|
||||
let archive = select_release_archive(release, version)?;
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("asset"),
|
||||
format!(
|
||||
"已选择更新包 {} ({})",
|
||||
archive.name,
|
||||
format_bytes(archive.size)
|
||||
),
|
||||
);
|
||||
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("verify"),
|
||||
"正在校验下载地址",
|
||||
);
|
||||
validate_download_url(&archive.browser_download_url, &config.github_api_base)?;
|
||||
if archive.size > MAX_DOWNLOAD_SIZE {
|
||||
return Err(UpdateError::bad_request("Release archive is too large"));
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("verify"),
|
||||
"正在校验下载地址",
|
||||
);
|
||||
validate_download_url(&archive.browser_download_url, &config.github_api_base)?;
|
||||
if archive.size > MAX_DOWNLOAD_SIZE {
|
||||
return Err(UpdateError::bad_request("Release archive is too large"));
|
||||
}
|
||||
let checksum = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "checksums.txt")
|
||||
.ok_or_else(|| UpdateError::bad_gateway("Release checksums.txt is required"))?;
|
||||
validate_download_url(&checksum.browser_download_url, &config.github_api_base)?;
|
||||
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("prepare"),
|
||||
"正在创建临时更新目录",
|
||||
);
|
||||
fs::create_dir_all(&config.update_temp_dir)
|
||||
.map_err(internal_error_with("Failed to prepare update temp dir"))?;
|
||||
let temp_dir = fs::canonicalize(&config.update_temp_dir)
|
||||
.and_then(|dir| tempfile_dir_in(&dir, ".codex-proxy-rs-update-"))
|
||||
.map_err(internal_error_with("Failed to create update temp dir"))?;
|
||||
let archive_path = temp_dir.join(&archive.name);
|
||||
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("download"),
|
||||
"开始下载更新包",
|
||||
);
|
||||
download_file(
|
||||
&archive.browser_download_url,
|
||||
&archive_path,
|
||||
MAX_DOWNLOAD_SIZE,
|
||||
Some(DownloadProgress {
|
||||
operation_id,
|
||||
total_size: archive.size,
|
||||
events: &self.events,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Success,
|
||||
Some(operation_id),
|
||||
Some("download"),
|
||||
"更新包下载完成",
|
||||
);
|
||||
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("checksum"),
|
||||
"正在校验 checksum",
|
||||
);
|
||||
verify_checksum(&archive_path, &archive.name, &checksum.browser_download_url).await?;
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Success,
|
||||
Some(operation_id),
|
||||
Some("checksum"),
|
||||
"checksum 校验通过",
|
||||
);
|
||||
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("extract"),
|
||||
"正在解压更新包",
|
||||
);
|
||||
let extracted = extract_release_archive(&archive_path, &temp_dir)?;
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Success,
|
||||
Some(operation_id),
|
||||
Some("extract"),
|
||||
"更新包解压完成",
|
||||
);
|
||||
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("replace"),
|
||||
"正在替换应用文件",
|
||||
);
|
||||
let exe_path = config.executable_path()?;
|
||||
replace_release_files(&exe_path, &config.web_dist_dir, extracted)?;
|
||||
self.events.emit(
|
||||
UpdateLogLevel::Success,
|
||||
Some(operation_id),
|
||||
Some("replace"),
|
||||
"应用文件替换完成",
|
||||
);
|
||||
let _ = fs::remove_dir_all(temp_dir);
|
||||
Ok(())
|
||||
}
|
||||
let checksum = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "checksums.txt")
|
||||
.ok_or_else(|| UpdateError::bad_gateway("Release checksums.txt is required"))?;
|
||||
validate_download_url(&checksum.browser_download_url, &config.github_api_base)?;
|
||||
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("prepare"),
|
||||
"正在创建临时更新目录",
|
||||
);
|
||||
fs::create_dir_all(&config.update_temp_dir)
|
||||
.map_err(internal_error_with("Failed to prepare update temp dir"))?;
|
||||
let temp_dir = fs::canonicalize(&config.update_temp_dir)
|
||||
.and_then(|dir| tempfile_dir_in(&dir, ".codex-proxy-rs-update-"))
|
||||
.map_err(internal_error_with("Failed to create update temp dir"))?;
|
||||
let archive_path = temp_dir.join(&archive.name);
|
||||
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("download"),
|
||||
"开始下载更新包",
|
||||
);
|
||||
download_file(
|
||||
&archive.browser_download_url,
|
||||
&archive_path,
|
||||
MAX_DOWNLOAD_SIZE,
|
||||
Some(DownloadProgress {
|
||||
operation_id,
|
||||
total_size: archive.size,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Success,
|
||||
Some(operation_id),
|
||||
Some("download"),
|
||||
"更新包下载完成",
|
||||
);
|
||||
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("checksum"),
|
||||
"正在校验 checksum",
|
||||
);
|
||||
verify_checksum(&archive_path, &archive.name, &checksum.browser_download_url).await?;
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Success,
|
||||
Some(operation_id),
|
||||
Some("checksum"),
|
||||
"checksum 校验通过",
|
||||
);
|
||||
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("extract"),
|
||||
"正在解压更新包",
|
||||
);
|
||||
let extracted = extract_release_archive(&archive_path, &temp_dir)?;
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Success,
|
||||
Some(operation_id),
|
||||
Some("extract"),
|
||||
"更新包解压完成",
|
||||
);
|
||||
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Info,
|
||||
Some(operation_id),
|
||||
Some("replace"),
|
||||
"正在替换应用文件",
|
||||
);
|
||||
let exe_path = config.executable_path()?;
|
||||
replace_release_files(&exe_path, &config.web_dist_dir, extracted)?;
|
||||
emit_update_event(
|
||||
UpdateLogLevel::Success,
|
||||
Some(operation_id),
|
||||
Some("replace"),
|
||||
"应用文件替换完成",
|
||||
);
|
||||
let _ = fs::remove_dir_all(temp_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tempfile_dir_in(parent: &Path, prefix: &str) -> io::Result<PathBuf> {
|
||||
@@ -598,90 +625,88 @@ fn env_string(key: &str) -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn runtime_build_info_override(key: &str) -> Option<String> {
|
||||
if cfg!(debug_assertions) {
|
||||
env_string(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn build_version() -> String {
|
||||
runtime_build_info_override("CPR_VERSION").unwrap_or_else(|| {
|
||||
option_env!("CPR_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||
.to_string()
|
||||
})
|
||||
option_env!("CPR_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn build_git_sha() -> String {
|
||||
runtime_build_info_override("CPR_GIT_SHA")
|
||||
.unwrap_or_else(|| option_env!("CPR_GIT_SHA").unwrap_or("unknown").to_string())
|
||||
option_env!("CPR_GIT_SHA").unwrap_or("unknown").to_string()
|
||||
}
|
||||
|
||||
fn build_time() -> String {
|
||||
runtime_build_info_override("CPR_BUILD_TIME").unwrap_or_else(|| {
|
||||
option_env!("CPR_BUILD_TIME")
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
})
|
||||
option_env!("CPR_BUILD_TIME")
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn build_type() -> String {
|
||||
runtime_build_info_override("CPR_BUILD_TYPE").unwrap_or_else(|| {
|
||||
option_env!("CPR_BUILD_TYPE")
|
||||
.unwrap_or("source")
|
||||
.to_string()
|
||||
})
|
||||
option_env!("CPR_BUILD_TYPE")
|
||||
.unwrap_or("source")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn update_event_sender() -> &'static broadcast::Sender<SystemUpdateEvent> {
|
||||
UPDATE_EVENT_SENDER.get_or_init(|| {
|
||||
pub(super) struct UpdateEventSender {
|
||||
sender: broadcast::Sender<SystemUpdateEvent>,
|
||||
}
|
||||
|
||||
impl Default for UpdateEventSender {
|
||||
fn default() -> Self {
|
||||
let (sender, _receiver) = broadcast::channel(256);
|
||||
sender
|
||||
})
|
||||
Self { sender }
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn emit_update_event(
|
||||
level: UpdateLogLevel,
|
||||
operation_id: Option<&str>,
|
||||
step: Option<&str>,
|
||||
message: impl Into<String>,
|
||||
) {
|
||||
emit_update_event_with_terminal(level, operation_id, step, message, false);
|
||||
}
|
||||
impl UpdateEventSender {
|
||||
fn subscribe(&self) -> broadcast::Receiver<SystemUpdateEvent> {
|
||||
self.sender.subscribe()
|
||||
}
|
||||
|
||||
fn emit_terminal_update_event(
|
||||
level: UpdateLogLevel,
|
||||
operation_id: Option<&str>,
|
||||
step: Option<&str>,
|
||||
message: impl Into<String>,
|
||||
) {
|
||||
emit_update_event_with_terminal(level, operation_id, step, message, true);
|
||||
}
|
||||
pub(super) fn emit(
|
||||
&self,
|
||||
level: UpdateLogLevel,
|
||||
operation_id: Option<&str>,
|
||||
step: Option<&str>,
|
||||
message: impl Into<String>,
|
||||
) {
|
||||
self.emit_with_terminal(level, operation_id, step, message, false);
|
||||
}
|
||||
|
||||
fn emit_update_event_with_terminal(
|
||||
level: UpdateLogLevel,
|
||||
operation_id: Option<&str>,
|
||||
step: Option<&str>,
|
||||
message: impl Into<String>,
|
||||
terminal: bool,
|
||||
) {
|
||||
let now = Utc::now();
|
||||
let event = SystemUpdateEvent {
|
||||
id: format!(
|
||||
"update-log-{}",
|
||||
now.timestamp_nanos_opt()
|
||||
.unwrap_or_else(|| now.timestamp_millis())
|
||||
),
|
||||
operation_id: operation_id.map(ToString::to_string),
|
||||
level,
|
||||
step: step.map(ToString::to_string),
|
||||
message: message.into(),
|
||||
terminal,
|
||||
at: now.to_rfc3339(),
|
||||
};
|
||||
let _ = update_event_sender().send(event);
|
||||
fn emit_terminal(
|
||||
&self,
|
||||
level: UpdateLogLevel,
|
||||
operation_id: Option<&str>,
|
||||
step: Option<&str>,
|
||||
message: impl Into<String>,
|
||||
) {
|
||||
self.emit_with_terminal(level, operation_id, step, message, true);
|
||||
}
|
||||
|
||||
fn emit_with_terminal(
|
||||
&self,
|
||||
level: UpdateLogLevel,
|
||||
operation_id: Option<&str>,
|
||||
step: Option<&str>,
|
||||
message: impl Into<String>,
|
||||
terminal: bool,
|
||||
) {
|
||||
let now = Utc::now();
|
||||
let event = SystemUpdateEvent {
|
||||
id: format!(
|
||||
"update-log-{}",
|
||||
now.timestamp_nanos_opt()
|
||||
.unwrap_or_else(|| now.timestamp_millis())
|
||||
),
|
||||
operation_id: operation_id.map(ToString::to_string),
|
||||
level,
|
||||
step: step.map(ToString::to_string),
|
||||
message: message.into(),
|
||||
terminal,
|
||||
at: now.to_rfc3339(),
|
||||
};
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_false(value: &bool) -> bool {
|
||||
|
||||
@@ -204,10 +204,10 @@ impl OperationLock {
|
||||
|
||||
impl Drop for OperationLock {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = fs::remove_file(&self.path) {
|
||||
if error.kind() != io::ErrorKind::NotFound {
|
||||
eprintln!("failed to remove update lock: {error}");
|
||||
}
|
||||
if let Err(error) = fs::remove_file(&self.path)
|
||||
&& error.kind() != io::ErrorKind::NotFound
|
||||
{
|
||||
eprintln!("failed to remove update lock: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::upstream::openai::protocol::sse::{parse_sse_events, SseError};
|
||||
use crate::upstream::openai::protocol::sse::{SseError, parse_sse_events};
|
||||
|
||||
/// 从 Codex/OpenAI usage 结构中提取出的标准化 token 用量。
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -356,17 +356,16 @@ pub fn rate_limit_quota(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(existing_quota) = existing_quota {
|
||||
if let Some(existing_snapshots) = existing_quota.get("snapshots").and_then(Value::as_array)
|
||||
{
|
||||
for snapshot in existing_snapshots {
|
||||
if !rate_limits
|
||||
.limits
|
||||
.keys()
|
||||
.any(|limit_id| quota_snapshot_matches_limit(snapshot, limit_id))
|
||||
{
|
||||
snapshots.push(snapshot.clone());
|
||||
}
|
||||
if let Some(existing_quota) = existing_quota
|
||||
&& let Some(existing_snapshots) = existing_quota.get("snapshots").and_then(Value::as_array)
|
||||
{
|
||||
for snapshot in existing_snapshots {
|
||||
if !rate_limits
|
||||
.limits
|
||||
.keys()
|
||||
.any(|limit_id| quota_snapshot_matches_limit(snapshot, limit_id))
|
||||
{
|
||||
snapshots.push(snapshot.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
infra::time::elapsed_millis_i64,
|
||||
upstream::openai::protocol::{
|
||||
schema::reconvert_tuple_values,
|
||||
sse::{parse_sse_events, SseError, SseEvent},
|
||||
sse::{SseError, SseEvent, parse_sse_events},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::upstream::openai::protocol::responses::{
|
||||
http_sse_fallback_allowed, transport_for_request, CodexResponsesRequest, CodexTransport,
|
||||
CodexResponsesRequest, CodexTransport, http_sse_fallback_allowed, transport_for_request,
|
||||
};
|
||||
use crate::upstream::openai::protocol::sse::encode_sse_event;
|
||||
|
||||
|
||||
@@ -11,21 +11,21 @@ use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, TryStreamExt};
|
||||
use reqwest::{
|
||||
header::{
|
||||
HeaderMap, HeaderName, HeaderValue, ACCEPT, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE,
|
||||
COOKIE, RETRY_AFTER, USER_AGENT,
|
||||
},
|
||||
Client, Response as ReqwestResponse, StatusCode,
|
||||
header::{
|
||||
ACCEPT, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, COOKIE, HeaderMap, HeaderName,
|
||||
HeaderValue, RETRY_AFTER, USER_AGENT,
|
||||
},
|
||||
};
|
||||
use serde_json::{map::Map, Value};
|
||||
use serde_json::{Value, map::Map};
|
||||
use thiserror::Error;
|
||||
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
|
||||
|
||||
use crate::upstream::openai::fingerprint::{Fingerprint, RuntimeFingerprint};
|
||||
use crate::upstream::openai::protocol::events::{extract_sse_usage, retry_after_seconds_from_body};
|
||||
use crate::upstream::openai::protocol::responses::{
|
||||
http_sse_fallback_allowed, transport_for_request, CodexCompactRequest, CodexResponsesRequest,
|
||||
CodexTransport,
|
||||
CodexCompactRequest, CodexResponsesRequest, CodexTransport, http_sse_fallback_allowed,
|
||||
transport_for_request,
|
||||
};
|
||||
use crate::upstream::openai::protocol::sse::SseError;
|
||||
use crate::upstream::openai::protocol::websocket::{
|
||||
@@ -33,20 +33,20 @@ use crate::upstream::openai::protocol::websocket::{
|
||||
};
|
||||
|
||||
use super::diagnostics::CodexUpstreamDiagnostics;
|
||||
use super::endpoints::{endpoint_url, CODEX_RESPONSES_COMPACT_PATH, CODEX_RESPONSES_PATH};
|
||||
use super::endpoints::{CODEX_RESPONSES_COMPACT_PATH, CODEX_RESPONSES_PATH, endpoint_url};
|
||||
use super::headers::{
|
||||
build_ordered_codex_base_headers, insert_optional_header, insert_ordered_headers,
|
||||
websocket_header_pairs,
|
||||
};
|
||||
use super::response_meta;
|
||||
use super::tls::{build_reqwest_client_with_custom_ca, custom_ca_env_cache_key, CustomCaError};
|
||||
use super::tls::{CustomCaError, build_reqwest_client_with_custom_ca, custom_ca_env_cache_key};
|
||||
use super::websocket::{
|
||||
execute_response_create_request_stream_with_pool, execute_response_create_request_with_pool,
|
||||
write_websocket_audit_artifact_from_env, CodexWebSocketConnection, CodexWebSocketExchangeError,
|
||||
CodexWebSocketRateLimitHeaderUpdates, CodexWebSocketTurnStateUpdate,
|
||||
CodexWebSocketConnection, CodexWebSocketExchangeError, CodexWebSocketRateLimitHeaderUpdates,
|
||||
CodexWebSocketTurnStateUpdate, execute_response_create_request_stream_with_pool,
|
||||
execute_response_create_request_with_pool, write_websocket_audit_artifact_from_env,
|
||||
};
|
||||
use super::websocket_pool::{
|
||||
CodexWebSocketPool, CodexWebSocketPoolKey, WebSocketPoolDecision, DEFAULT_INITIAL_EVENT_TIMEOUT,
|
||||
CodexWebSocketPool, CodexWebSocketPoolKey, DEFAULT_INITIAL_EVENT_TIMEOUT, WebSocketPoolDecision,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -13,17 +13,17 @@ pub(crate) mod websocket_pump;
|
||||
|
||||
pub use self::{
|
||||
client::{
|
||||
backend_transport_for_response_request, build_reqwest_client, is_banned_auth_signal,
|
||||
is_banned_upstream_error, CodexBackendClient, CodexBackendResponse, CodexBackendSseStream,
|
||||
CodexBackendClient, CodexBackendResponse, CodexBackendSseStream,
|
||||
CodexBackendStreamingResponse, CodexBackendTransport, CodexClientError, CodexClientResult,
|
||||
CodexCompactResponse, CodexModelCatalogClient, CodexModelCatalogClientError,
|
||||
CodexModelCatalogRequest, CodexRateLimitHeaderUpdates, CodexRequestContext,
|
||||
CodexResponseMetadata, CodexTurnStateUpdate,
|
||||
CodexResponseMetadata, CodexTurnStateUpdate, backend_transport_for_response_request,
|
||||
build_reqwest_client, is_banned_auth_signal, is_banned_upstream_error,
|
||||
},
|
||||
diagnostics::CodexUpstreamDiagnostics,
|
||||
endpoints::{
|
||||
endpoint_request_path, endpoint_url, usage_endpoint_urls, CODEX_RESPONSES_COMPACT_PATH,
|
||||
CODEX_RESPONSES_PATH, CODEX_USAGE_API_PATH, CODEX_USAGE_PATH, WHAM_USAGE_PATH,
|
||||
CODEX_RESPONSES_COMPACT_PATH, CODEX_RESPONSES_PATH, CODEX_USAGE_API_PATH, CODEX_USAGE_PATH,
|
||||
WHAM_USAGE_PATH, endpoint_request_path, endpoint_url, usage_endpoint_urls,
|
||||
},
|
||||
headers::{
|
||||
build_codex_base_headers, build_codex_headers, build_ordered_codex_base_headers,
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::{env, fs, io, path::PathBuf, sync::Arc};
|
||||
|
||||
use rustls::{ClientConfig, RootCertStore};
|
||||
use rustls_pki_types::{
|
||||
pem::{self, PemObject, SectionKind},
|
||||
CertificateDer,
|
||||
pem::{self, PemObject, SectionKind},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ use serde_json::Value;
|
||||
use crate::upstream::openai::protocol::events::retry_after_seconds_from_body;
|
||||
|
||||
use super::{
|
||||
CodexBackendClient, CodexClientError, CodexClientResult, CodexRequestContext,
|
||||
CodexUpstreamDiagnostics,
|
||||
client::{read_capped_error_body, retry_after_seconds, truncate_for_error},
|
||||
endpoints::usage_endpoint_urls,
|
||||
response_meta, CodexBackendClient, CodexClientError, CodexClientResult, CodexRequestContext,
|
||||
CodexUpstreamDiagnostics,
|
||||
response_meta,
|
||||
};
|
||||
|
||||
impl CodexBackendClient {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user