feat: add admin diagnostics summary

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-06-13 11:24:28 +08:00
co-authored by Codex
parent f66c69c7fa
commit 02f76b0477
8 changed files with 145 additions and 29 deletions
+27
View File
@@ -166,6 +166,33 @@ Admin JSON uses lower camelCase field names. Every admin response includes an `X
Use real HTTP status codes and body-level frontend codes together. Do not return HTTP `200` for failed requests. The body `code` exists for frontend branching; the HTTP status remains the transport truth.
### `GET /admin/diagnostics`
Returns the same non-secret runtime diagnostics summary as `/debug/diagnostics`, wrapped in the admin envelope and gated by the admin session cookie.
Success response:
```json
{
"code": 200,
"message": "OK",
"data": {
"status": "ok",
"runtime": {
"packageName": "codex-proxy-rs",
"packageVersion": "0.1.0"
},
"transport": {
"backendBaseUrl": "https://chatgpt.com/backend-api"
},
"accounts": {
"authenticatedState": true
}
},
"requestId": "req_01"
}
```
### `POST /admin/login`
Authenticates only with the configured admin password stored in `admin_users`. Client API keys (`Bearer cpr_...`) are ignored by the admin login flow and cannot create admin sessions.
File diff suppressed because one or more lines are too long
@@ -2976,7 +2976,7 @@ The Rust project has no legacy compatibility requirement, but the in-scope behav
- [x] **Login routes:** implement OAuth PKCE login-start/code-relay/callback, device-login/device-poll, CLI auth import, manual access-token import, and logout as account-login flows. CLI auth import, validated manual token import, device login/poll, OAuth PKCE login-start/code-relay/callback, admin-session-gated auth status, and logout are complete. Keep this separate from admin password login and client API key creation.
- [x] **Client API key management:** implement admin CRUD for local client API keys: list/create/delete/batch-delete/label/status/export/import. Create/list/status/delete/batch-delete/label/export/import and `/v1/*` SQLite verification are complete. Remove provider/model binding semantics from the TypeScript version; Rust client API keys authorize only local `/v1/*` access.
- [x] **Settings:** implement admin settings for server/runtime fields that remain in scope: default model, reasoning effort, service tier, model aliases, refresh enabled/margin/concurrency, max concurrent per account, request interval, rotation strategy, tier priority, quota refresh/thresholds/skip exhausted, logs state/capacity/body capture, and usage history retention. GET and PATCH `/admin/settings` are admin-session gated, PATCH validates retained fields, writes a root `local.yaml` overlay, and rejects proxy URL, Ollama, third-party provider, OpenAI official key, proxy API key, and Electron/self-update settings. Existing constructed runtime services still use construction-time config until restart or a later runtime reconfiguration task.
- [ ] **Diagnostics:** expand health/admin diagnostics to include authenticated state, pool summary, capacity summary, transport/fingerprint status, paths, runtime metadata, and test-connection checks. Local-only `/debug/diagnostics` now returns runtime metadata, paths, account repository/pool state, capacity summary, transport config, and static fingerprint status without secrets. Local-only `/debug/fingerprint` returns the runtime Desktop fingerprint summary. Local-only `/debug/upstream` probes the Codex models endpoint with runtime TLS/fingerprint headers and returns only status metadata. Remaining: richer admin diagnostics surfaces. Keep production-local gating for sensitive debug endpoints.
- [x] **Diagnostics:** expand health/admin diagnostics to include authenticated state, pool summary, capacity summary, transport/fingerprint status, paths, runtime metadata, and test-connection checks. `/admin/diagnostics` returns the non-secret runtime summary behind admin-session auth. Local-only `/debug/diagnostics` returns runtime metadata, paths, account repository/pool state, capacity summary, transport config, and static fingerprint status without secrets. Local-only `/debug/fingerprint` returns the runtime Desktop fingerprint summary. Local-only `/debug/upstream` probes the Codex models endpoint with runtime TLS/fingerprint headers and returns only status metadata. Keep production-local gating for sensitive debug endpoints.
- [ ] **Logs and usage stats:** add `/admin/logs/state`, clear/detail, error-log equivalents if retained, and richer bounded history queries if needed. `/admin/logs/state`, `/admin/logs/{id}`, `DELETE /admin/logs`, log file rotation, SQLite event pagination, `/admin/usage-stats` pagination, and `/admin/usage-stats/summary` are complete. Remaining: decide whether to retain old error-log grouping/raw/count/seen/report APIs and richer bounded body-history queries.
- [x] **Model catalog:** replace the hard-coded `/v1/models` list with a model store that supports static defaults, configured aliases, `-low/-medium/-high/-xhigh/-fast/-flex` suffix parsing, backend model fetching per plan, cache persistence, `/v1/models/catalog`, `/v1/models/{id}`, `/v1/models/{id}/info`, `/admin/refresh-models`, and `/debug/models`. Static defaults, aliases, suffix parsing, cached backend snapshots, plan allowlists, admin refresh, read-only model routes, and debug summary are complete.
- [ ] **Account scheduling parity:** extend account acquisition beyond `active + least last_used`. Completed in the in-memory pool: max concurrent slots per account, stale slot cleanup, least-used/round-robin/sticky strategies, quota-cache skip, tier priority, model-plan filtering, exclude IDs, preferred account from session affinity, Cloudflare cooldown skip, request-staggering metadata, capacity summary, settings-backed construction, import-to-pool sync, startup pool snapshots from SQLite, HTTP fallback acquisition across alternate accounts, WebSocket fallback for non-history requests, previous-response account affinity, and runtime pool clearing on logout. Remaining: durable release-time slot state and durable quota-cache/cooldown restore.
@@ -3001,7 +3001,7 @@ This pass re-read the TypeScript routes and core modules under `/home/zyy/Codes/
- [ ] **Account scheduling and retry:** Rust has in-memory slots, stale cleanup, strategies, tier priority, preferred/exclude IDs, SQLite startup restore, request-staggering metadata, 429/402/403 fallback across alternate accounts, Cloudflare cooldown skip, successful WebSocket rate-limit header application, non-history WebSocket fallback, previous-response history affinity, and status/token/logout mutation sync for the runtime pool. Missing strict request-interval sleep enforcement, durable quota/cache restore, Cloudflare path-block tracker, cooldown clearing, and richer capacity/health endpoints.
- [ ] **Refresh scheduler parity:** Rust has typed refresh policy and 401-triggered refresh retry. Missing long-running refresh timers at `exp - margin`, crash recovery for `refreshing`/`expired` accounts, exponential backoff, recovery scheduling, permanent-failure thresholding, per-account in-flight suppression, cross-process refresh locks, and disk refresh-token sync before consuming one-time RTs.
- [ ] **Codex Desktop fingerprint and transport parity:** Rust uses pinned `reqwest`/`rustls` and core headers, but exact parity is not proven. Missing header-order/default-header parity (`sec-ch-ua`, accept language/encoding/fetch headers), `x-codex-installation-id`, runtime selection of the newest stored fingerprint, Sparkle appcast XML polling, update-state/version-state persistence, optional local Codex.app Chromium extraction, and real Desktop TLS/header capture comparison. `x-openai-internal-codex-residency` currently needs real-client verification because the builder inserts `global` and the reqwest client overwrites it with `us`.
- [ ] **Health and diagnostics:** Rust `/health` returns `status: ok`, `/admin/auth/status` returns admin-session-gated authenticated state plus account pool summary, local-only `/debug/diagnostics` returns capacity summary, transport/fingerprint status, paths, runtime metadata, and retained settings summary without secrets, and local-only `/debug/upstream` probes the Codex models endpoint without returning tokens or upstream bodies. Missing richer admin diagnostics surfaces.
- [x] **Health and diagnostics:** Rust `/health` returns `status: ok`, `/admin/auth/status` returns admin-session-gated authenticated state plus account pool summary, `/admin/diagnostics` returns the non-secret runtime diagnostics summary, local-only `/debug/diagnostics` returns capacity summary, transport/fingerprint status, paths, runtime metadata, and retained settings summary without secrets, and local-only `/debug/upstream` probes the Codex models endpoint without returning tokens or upstream bodies.
- [ ] **Background service lifecycle:** Rust startup restores accounts and initializes tracing, but does not start/stop scheduler tasks for account refresh, model refresh, quota refresh, active quota refresh, session cleanup, fingerprint polling, or graceful shutdown drains. Ollama, proxy health timers, self-update, Electron, and provider router startup remain intentionally deleted.
### Keep Removed
+27
View File
@@ -0,0 +1,27 @@
use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension,
};
use crate::{
app::state::AppState,
http::{diagnostics::diagnostics_data, middleware::RequestId},
};
use super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
pub async fn diagnostics(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(diagnostics_data(&state).await, request_id),
))
}
+1
View File
@@ -1,6 +1,7 @@
pub mod accounts;
pub mod api_keys;
pub mod auth;
pub mod diagnostics;
pub mod logs;
pub mod models;
pub mod response;
+2
View File
@@ -21,6 +21,7 @@ use super::{
auth_callback, auth_code_relay, auth_device_login, auth_device_poll, auth_login_start,
auth_logout, auth_status, login,
},
diagnostics::diagnostics,
logs::{clear_logs, log_detail, logs, logs_state},
models::refresh_models,
settings::{settings, update_settings},
@@ -42,6 +43,7 @@ pub fn router() -> Router<AppState> {
"/admin/auth/device-poll/{device_code}",
get(auth_device_poll),
)
.route("/admin/diagnostics", get(diagnostics))
.route("/admin/logs", get(logs).delete(clear_logs))
.route("/admin/logs/state", get(logs_state))
.route("/admin/logs/{log_id}", get(log_detail))
+25 -25
View File
@@ -17,7 +17,7 @@ use crate::{
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct DiagnosticsData {
pub(crate) struct DiagnosticsData {
status: &'static str,
runtime: RuntimeDiagnostics,
paths: PathDiagnostics,
@@ -120,35 +120,35 @@ pub async fn diagnostics(State(state): State<AppState>, headers: HeaderMap) -> i
.into_response();
}
(StatusCode::OK, Json(diagnostics_data(&state).await)).into_response()
}
pub(crate) async fn diagnostics_data(state: &AppState) -> DiagnosticsData {
let config = state.services.settings.current().await;
let auth_status = state.services.admin_auth.status().await.ok();
let capacity = state.services.accounts.runtime_capacity_summary().await;
let fingerprint = Fingerprint::default_codex_desktop();
(
StatusCode::OK,
Json(DiagnosticsData {
status: "ok",
runtime: RuntimeDiagnostics {
package_name: env!("CARGO_PKG_NAME"),
package_version: env!("CARGO_PKG_VERSION"),
},
paths: paths_diagnostics(&state, &config),
transport: transport_diagnostics(&config, fingerprint),
accounts: AccountDiagnostics {
repository_available: state.services.accounts.has_repository(),
authenticated_state: auth_status
.as_ref()
.is_some_and(|status| status.authenticated),
pool: auth_status
.map(|status| AccountPoolDiagnostics::from(status.pool))
.unwrap_or_default(),
capacity: CapacityDiagnostics::from(capacity),
},
settings: SettingsDiagnostics::from(&config),
}),
)
.into_response()
DiagnosticsData {
status: "ok",
runtime: RuntimeDiagnostics {
package_name: env!("CARGO_PKG_NAME"),
package_version: env!("CARGO_PKG_VERSION"),
},
paths: paths_diagnostics(state, &config),
transport: transport_diagnostics(&config, fingerprint),
accounts: AccountDiagnostics {
repository_available: state.services.accounts.has_repository(),
authenticated_state: auth_status
.as_ref()
.is_some_and(|status| status.authenticated),
pool: auth_status
.map(|status| AccountPoolDiagnostics::from(status.pool))
.unwrap_or_default(),
capacity: CapacityDiagnostics::from(capacity),
},
settings: SettingsDiagnostics::from(&config),
}
}
pub async fn debug_fingerprint(headers: HeaderMap) -> impl IntoResponse {
+59
View File
@@ -201,3 +201,62 @@ async fn debug_upstream_should_reject_forwarded_remote_requests_without_probe()
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn admin_diagnostics_should_require_admin_session_cookie() {
let imported = build_imported_app("https://chatgpt.test/backend-api".to_string()).await;
let response = imported
.app
.oneshot(
Request::builder()
.method("GET")
.uri("/admin/diagnostics")
.header("x-request-id", "req_admin_diagnostics")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
let body = response_json(response).await;
assert_eq!(body["code"], 40101);
assert_eq!(body["requestId"], "req_admin_diagnostics");
}
#[tokio::test]
async fn admin_diagnostics_should_return_admin_enveloped_runtime_summary_without_secrets() {
let imported = build_imported_app("https://chatgpt.test/backend-api".to_string()).await;
let response = imported
.app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri("/admin/diagnostics")
.header("cookie", "cpr_admin_session=session_1")
.header("x-request-id", "req_admin_diagnostics")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response_json(response).await;
assert_eq!(body["code"], 200);
assert_eq!(body["requestId"], "req_admin_diagnostics");
assert_eq!(body["data"]["status"], "ok");
assert_eq!(
body["data"]["transport"]["backendBaseUrl"],
"https://chatgpt.test/backend-api"
);
assert_eq!(body["data"]["accounts"]["pool"]["active"], 1);
assert_eq!(body["data"]["accounts"]["capacity"]["totalSlots"], 3);
let serialized = serde_json::to_string(&body).unwrap();
assert!(!serialized.contains("access-secret"));
assert!(!serialized.contains("refresh-secret"));
}