mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
feat: add admin diagnostics summary
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
+27
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user