refactor: align backend names

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-06-14 17:48:25 +08:00
co-authored by Codex
parent 48230ae72a
commit f557cb17ba
170 changed files with 2293 additions and 1339 deletions
+6 -6
View File
@@ -125,7 +125,7 @@ async fn ensure_default_admin_exists(
### 1. 管理员登录
```bash
curl -X POST http://127.0.0.1:8080/admin/login \
curl -X POST http://127.0.0.1:8080/api/admin/login \
-H "Content-Type: application/json" \
-d '{"password":"admin"}'
```
@@ -151,7 +151,7 @@ cpr_admin_session=sess_a27875537483420ab7ba6ce4d1f91b5e; Path=/; HttpOnly; SameS
### 2. 导入测试账号
```bash
curl -X POST http://127.0.0.1:8080/admin/accounts/import \
curl -X POST http://127.0.0.1:8080/api/admin/accounts/import \
-H "Content-Type: application/json" \
-H "Cookie: cpr_admin_session=sess_a27875537483420ab7ba6ce4d1f91b5e" \
-d @accounts-export.json
@@ -177,7 +177,7 @@ curl -X POST http://127.0.0.1:8080/admin/accounts/import \
#### 3.1 创建 API 密钥
```bash
curl -X POST http://127.0.0.1:8080/admin/api-keys \
curl -X POST http://127.0.0.1:8080/api/admin/api-keys \
-H "Content-Type: application/json" \
-H "Cookie: cpr_admin_session=sess_..." \
-d '{"name":"test-key","label":"测试密钥"}'
@@ -197,7 +197,7 @@ curl -X POST http://127.0.0.1:8080/admin/api-keys \
#### 3.2 刷新模型列表
```bash
curl -X POST http://127.0.0.1:8080/admin/refresh-models \
curl -X POST http://127.0.0.1:8080/api/admin/refresh-models \
-H "Cookie: cpr_admin_session=sess_..."
```
@@ -262,7 +262,7 @@ curl -X POST http://127.0.0.1:8080/v1/chat/completions \
由于 `AdminConfig` 添加了新字段,需要修复所有测试文件中的初始化代码。
**影响的测试文件:**
- `tests/admin/*.rs` (6 个文件)
- `tests/api/admin/*.rs` (6 个文件)
- `tests/codex_serving/*.rs` (2 个文件)
- `tests/runtime/startup.rs`
- `tests/support/*.rs` (2 个文件)
@@ -325,7 +325,7 @@ test result: ok. 49 passed; 0 failed
- `tests/**/*.rs` - 修复测试中的 AdminConfig 初始化
### 不变的模块
- `src/admin/auth/service.rs` - 认证逻辑无需变更
- `src/api/admin/auth/service.rs` - 认证逻辑无需变更
- `src/platform/identity/admin_session.rs` - 密码哈希逻辑无需变更
- `src/platform/storage/schema.sql` - 数据库表结构无需变更
+1 -1
View File
@@ -203,7 +203,7 @@ pub fn capture_set_cookie(&mut self, account_id: &str, raw: &str) {
- 添加所有浏览器特征头
- 修正 `x-openai-internal-codex-residency` 为 `"us"`
4. **src/codex/gateway/transport/client.rs**
4. **src/codex/gateway/transport/http_client.rs**
- 更新导入:`build_codex_headers` → `build_ordered_codex_headers`
- 重构 `request_headers()` 使用排序后的头
+3 -3
View File
@@ -24,7 +24,7 @@
### 2. 核心传输层更新
#### `src/codex/gateway/transport/client.rs`
#### `src/codex/gateway/transport/http_client.rs`
- `CodexRequestContext` 添加两个新字段:
- `installation_id: Option<&'a str>`
- `session_id: Option<&'a str>`
@@ -209,10 +209,10 @@ Dispatch Service (src/codex/serving/dispatch/mod.rs)
### 修改文件 (9)
- `src/codex/gateway/mod.rs` (+2 行)
- `src/codex/gateway/transport/client.rs` (+4 行)
- `src/codex/gateway/transport/http_client.rs` (+4 行)
- `src/codex/serving/dispatch/mod.rs` (+15 行)
- `src/codex/accounts/service/health.rs` (+2 行)
- `src/codex/accounts/models/service.rs` (+2 行)
- `src/codex/models/service.rs` (+2 行)
- `src/codex/serving/diagnostics.rs` (+2 行)
- `Cargo.toml` (+1 行)
- `tests/codex_gateway/client.rs` (+2 行)
+1 -1
View File
@@ -17,7 +17,7 @@
Rust 状态:
- 到位:`src/codex/gateway/transport/headers.rs` 构造账号、residency、request id、UA、sec-ch、默认 header 和顺序。
- 到位:`src/codex/gateway/transport/client.rs` 将 installation/window/turn/parent/subagent 同步到 header 和 `client_metadata`。
- 到位:`src/codex/gateway/transport/http_client.rs` 将 installation/window/turn/parent/subagent 同步到 header 和 `client_metadata`。
- 到位:`src/codex/gateway/installation.rs` 兼容读取 `~/.codex/installation_id`,否则落库生成。
- 到位:`src/runtime/bootstrap.rs` 从数据库加载自动更新指纹,避免实际请求继续使用硬编码默认值。
- 边界:Rust 使用 `reqwest + rustls` 传输,未声称复制原版 native transport 的完整 Chrome TLS 指纹。IP 代理/VPN 相关能力不属于移植范围。
+3 -3
View File
@@ -28,7 +28,7 @@
- 使用 SHA256 哈希生成确定性标识符
- 格式:`cp_{hash[..32]}` (conversation) 或 `cw_{hash[..32]}` (window)
- 更新 `src/codex/gateway/transport/client.rs`
- 更新 `src/codex/gateway/transport/http_client.rs`
- 在 `CodexRequestContext` 添加 `installation_id` 和 `session_id` 字段
- 在 `request_headers()` 方法中添加这两个 header
@@ -132,10 +132,10 @@ test codex::gateway::installation::tests::test_generate_and_persist ... ok
### 修改文件
1. `src/codex/gateway/mod.rs` - 导出新模块
2. `src/codex/gateway/transport/client.rs` - 添加新 header 字段
2. `src/codex/gateway/transport/http_client.rs` - 添加新 header 字段
3. `src/codex/serving/dispatch/mod.rs` - 集成身份派生逻辑
4. `src/codex/accounts/service/health.rs` - 更新 context 构造
5. `src/codex/accounts/models/service.rs` - 更新 context 构造
5. `src/codex/models/service.rs` - 更新 context 构造
6. `src/codex/serving/diagnostics.rs` - 更新 context 构造
7. `Cargo.toml` - 添加 `dirs = "5.0.1"` 依赖
+27 -25
View File
@@ -1,10 +1,12 @@
# API Contract
`codex-proxy-rs` exposes two API families.
`codex-proxy-rs` exposes two API families. The `/admin/*` path is reserved for
the future human Web console; machine-readable management endpoints live under
`/api/admin/*`.
## `/v1/*`
These endpoints are OpenAI-compatible and are authenticated only by enabled local client API keys created through `/admin/api-keys` and sent as `Authorization: Bearer cpr_...`.
These endpoints are OpenAI-compatible and are authenticated only by enabled local client API keys created through `/api/admin/api-keys` and sent as `Authorization: Bearer cpr_...`.
Responses include an `X-Request-Id` header for tracing, but the body stays OpenAI-compatible and does not include a custom `requestId` field.
Error body:
@@ -159,14 +161,14 @@ Refresh failures are recorded as non-secret `account.refresh` event logs with th
`previous_response_id` and explicit WebSocket-only requests are rejected until the WebSocket transport is implemented and verified.
## `/admin/*`
## `/api/admin/*`
Admin endpoints are authenticated only by HttpOnly admin session cookies.
Admin JSON uses lower camelCase field names. Every admin response includes an `X-Request-Id` header, and every JSON body includes `requestId`.
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`
### `GET /api/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.
@@ -193,7 +195,7 @@ Success response:
}
```
### `POST /admin/login`
### `POST /api/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.
@@ -226,7 +228,7 @@ Set-Cookie: cpr_admin_session=...; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600
Invalid password response uses HTTP `401` and body code `40102`.
### `GET /admin/settings`
### `GET /api/admin/settings`
Returns the in-scope runtime settings visible to the admin UI. This endpoint uses admin sessions, not client API keys.
@@ -263,7 +265,7 @@ Success response:
}
```
### `PATCH /admin/settings`
### `PATCH /api/admin/settings`
Updates retained settings and writes them to root `local.yaml` as a config overlay. The route requires the admin session cookie. Client API keys and removed TypeScript-era fields are not accepted.
@@ -297,7 +299,7 @@ Request:
}
```
Success response uses the same `data` shape as `GET /admin/settings`.
Success response uses the same `data` shape as `GET /api/admin/settings`.
Validation:
@@ -306,7 +308,7 @@ Validation:
- Quota warning thresholds must be between `0` and `100`.
- Removed settings such as `proxyUrl`, `openaiApiKey`, `ollama`, provider settings, proxy API key settings, and Electron/self-update settings return HTTP `400` with body code `40001`.
### `GET /admin/logs`
### `GET /api/admin/logs`
Returns cursor-paginated event logs for admin troubleshooting. The list uses the standard admin page envelope and lower camelCase fields.
@@ -348,7 +350,7 @@ Example item:
}
```
### `GET /admin/logs/state`
### `GET /api/admin/logs/state`
Returns the current retained logging configuration plus the number of stored SQLite event logs.
@@ -368,9 +370,9 @@ Success response:
}
```
### `PATCH /admin/logs/state`
### `PATCH /api/admin/logs/state`
Updates the in-process logging state used by admin log controls. This is runtime-only; persistent configuration changes still go through `PATCH /admin/settings`.
Updates the in-process logging state used by admin log controls. This is runtime-only; persistent configuration changes still go through `PATCH /api/admin/settings`.
Request:
@@ -382,13 +384,13 @@ Request:
}
```
Success response uses the same data shape as `GET /admin/logs/state`.
Success response uses the same data shape as `GET /api/admin/logs/state`.
### `GET /admin/logs/{id}`
### `GET /api/admin/logs/{id}`
Returns a single stored event log by id using the same event shape as list items. Missing ids return HTTP `404` with body code `40401`.
### `DELETE /admin/logs`
### `DELETE /api/admin/logs`
Clears stored SQLite event logs.
@@ -405,7 +407,7 @@ Success response:
}
```
### `GET /admin/usage-stats`
### `GET /api/admin/usage-stats`
Returns cursor-paginated account usage counters recorded after `/v1/*` calls. The endpoint never returns account access tokens, refresh tokens, Cookie values, or client API keys.
@@ -425,7 +427,7 @@ Example item:
}
```
### `GET /admin/usage-stats/summary`
### `GET /api/admin/usage-stats/summary`
Returns global usage totals across accounts.
@@ -446,11 +448,11 @@ Success response:
}
```
### `GET /admin/api-keys`
### `GET /api/admin/api-keys`
Returns local client API keys with cursor pagination. The plaintext key and hash are never returned by list endpoints.
### `POST /admin/api-keys`
### `POST /api/admin/api-keys`
Creates a local client API key for `/v1/*`. The plaintext value is returned only in this response; store it client-side and use it as `Authorization: Bearer <plaintext>`.
@@ -481,14 +483,14 @@ Success response:
}
```
### `GET /admin/api-keys/export`
### `GET /api/admin/api-keys/export`
Exports Rust-local client API key metadata for backup or migration. The export never includes plaintext keys, key hashes, or the raw auth pepper. Because `cpr_` keys are stored only as peppered HMAC hashes, imports rotate keys instead of preserving the original plaintext.
Optional query:
```http
GET /admin/api-keys/export?ids=key_01,key_02
GET /api/admin/api-keys/export?ids=key_01,key_02
```
Success response:
@@ -516,7 +518,7 @@ Success response:
}
```
### `POST /admin/api-keys/import`
### `POST /api/admin/api-keys/import`
Imports Rust-local client API key metadata exported by this service. Import creates new local `cpr_` keys using the target instance's pepper and returns the new plaintext values only in this response. Clients must replace the old keys with these rotated plaintext values.
@@ -567,11 +569,11 @@ Success response:
}
```
### `GET /admin/accounts`
### `GET /api/admin/accounts`
Returns stored Codex accounts with cursor pagination. Access tokens and refresh tokens are never decrypted or returned.
### `GET /admin/accounts/quota-warnings`
### `GET /api/admin/accounts/quota-warnings`
Returns active quota warnings computed from cached account quota snapshots in SQLite. The route requires an admin session cookie and does not call the upstream Codex backend.
@@ -598,7 +600,7 @@ Success response:
}
```
### `POST /admin/accounts/import`
### `POST /api/admin/accounts/import`
Imports accounts into encrypted SQLite storage. The request body uses the Rust import format, which accepts the exported account object fields needed by this service and ignores unrelated fields.
+4 -4
View File
@@ -13,7 +13,7 @@
src/
├── codex/ # OpenAI/Codex 对接层(核心业务逻辑)
├── service/ # 业务服务层(Chat, Responses, Diagnostics)
├── http/ # HTTP 路由层(/v1/* 和 /admin/*)
├── http/ # HTTP 路由层(/v1/* 和 /api/admin/*)
├── auth/ # 认证模块(API keys, sessions)
├── scheduler/ # 后台调度器(refresh, quota, model)
├── storage/ # 数据持久化(SQLite)
@@ -30,7 +30,7 @@ src/
**三层认证边界设计:**
```
Admin Auth (密码 + Session) → /admin/*
Admin Auth (密码 + Session) → /api/admin/*
Client API Keys (cpr_ 前缀) → /v1/*
Upstream Tokens (内部加密) → ChatGPT Backend
```
@@ -151,7 +151,7 @@ src/codex/accounts/service/
├── cookies.rs # Cookie 操作
├── health.rs # 健康检查
├── import.rs # 导入逻辑
├── mutation.rs # CRUD 操作
├── lifecycle.rs # CRUD 操作
├── quota.rs # 配额管理
├── refresh.rs # 令牌刷新
└── runtime_pool.rs # 运行时池同步
@@ -224,7 +224,7 @@ pub enum AccountServiceError {
### 5. 🟢 配置热重载未完全实现
**当前状态:**
- `/admin/settings PATCH` 写入 `local.yaml`
- `/api/admin/settings PATCH` 写入 `local.yaml`
- 更新 `AppState` 中的 config
- ⚠️ 但已构造的服务(如调度器)仍使用旧配置
+15 -15
View File
@@ -75,7 +75,7 @@ src/
│ │ │ ├── cookies.rs
│ │ │ ├── health.rs
│ │ │ ├── import.rs # 手动/CLI/OAuth 导入
│ │ │ ├── mutation.rs
│ │ │ ├── lifecycle.rs
│ │ │ ├── quota.rs
│ │ │ ├── refresh.rs
│ │ │ └── runtime_pool.rs
@@ -136,18 +136,18 @@ src/
│
├── admin/ # 🔵 后台管理域(控制面)
│ ├── mod.rs
│ ├── http/ # /admin/* ← http/admin/
│ │ ├── mod.rs # ← http/admin/mod.rs
│ │ ├── router.rs # ← http/admin/router.rs
│ │ ├── response.rs # ← http/admin/response.rs(AdminError envelope)
│ │ ├── auth.rs # ← http/admin/auth.rs(login/status/logout)
│ │ ├── api_keys.rs # ← http/admin/api_keys.rs
│ │ ├── logs.rs # ← http/admin/logs.rs
│ │ ├── models.rs # ← http/admin/models.rs(refresh-models)
│ │ ├── settings.rs # ← http/admin/settings.rs
│ │ ├── usage.rs # ← http/admin/usage.rs
│ │ ├── diagnostics.rs # ← http/admin/diagnostics.rs
│ │ └── accounts/ # ⭐ 拆分 http/admin/accounts.rs(1697 行)
│ ├── http/ # /api/admin/* ← http/api/admin/
│ │ ├── mod.rs # ← http/api/admin/mod.rs
│ │ ├── router.rs # ← http/api/admin/router.rs
│ │ ├── response.rs # ← http/api/admin/response.rs(AdminError envelope)
│ │ ├── auth.rs # ← http/api/admin/auth.rs(login/status/logout)
│ │ ├── api_keys.rs # ← http/api/admin/api_keys.rs
│ │ ├── logs.rs # ← http/api/admin/logs.rs
│ │ ├── models.rs # ← http/api/admin/models.rs(refresh-models)
│ │ ├── settings.rs # ← http/api/admin/settings.rs
│ │ ├── usage.rs # ← http/api/admin/usage.rs
│ │ ├── diagnostics.rs # ← http/api/admin/diagnostics.rs
│ │ └── accounts/ # ⭐ 拆分 http/api/admin/accounts.rs(1697 行)
│ │ ├── mod.rs # 路由注册 + 共享 DTO
│ │ ├── list.rs # accounts / export_accounts
│ │ ├── create.rs # create_account / import_accounts / import_cli_auth
@@ -156,7 +156,7 @@ src/
│ │ ├── health.rs # health_check_accounts
│ │ ├── quota.rs # quota_warnings / account_quota
│ │ ├── cookies.rs # get / set / delete cookies
│ │ └── oauth.rs # ← http/admin/auth.rs 中 OAuth 流程
│ │ └── oauth.rs # ← http/api/admin/auth.rs 中 OAuth 流程
│ │ # device-login / poll / login-start / code-relay / callback
│ ├── auth/ # 管理员认证 + key 管理策略
│ │ ├── mod.rs
@@ -200,7 +200,7 @@ src/
大部分是目录搬迁,但有 3 个文件需要真正动刀:
1. **`http/admin/accounts.rs`(1697 行)→ `admin/http/accounts/` 9 文件**。按上面 16 个 handler 归类(list/create/mutate/delete/health/quota/cookies/oauth + mod 路由)。
1. **`http/api/admin/accounts.rs`(1697 行)→ `admin/http/accounts/` 9 文件**。按上面 16 个 handler 归类(list/create/mutate/delete/health/quota/cookies/oauth + mod 路由)。
2. **`service/admin_auth.rs` 拆责**。密码登录/会话校验留 `admin/auth/service.rs`;OAuth token 交换 + 账号导入的编排移到 `admin/http/accounts/oauth.rs`,由它调用 `codex::gateway::oauth`(换 token)+ `codex::accounts::service::import`(落库)。
+23 -23
View File
@@ -36,7 +36,7 @@ The cleanup started from these pressure points:
| `tests/admin_accounts_route_test.rs` | 2588 | Admin account scenarios are too large to scan or move safely. |
| `tests/v1_upstream_route_test.rs` | 2115 | Responses HTTP SSE, WebSocket, fallback, error mapping, usage, and logging side effects are mixed. |
| `tests/chat_completions_route_test.rs` | 710 | Manageable, but should eventually align with the v1 split. |
| `tests/admin_api_keys_route_test.rs` | 659 | Manageable, but can share common admin test setup. |
| `tests/admin_client_keys_route_test.rs` | 659 | Manageable, but can share common admin test setup. |
The main coupling pattern is:
@@ -51,8 +51,8 @@ touch fallback, account state, usage logs, and protocol conversion at the same t
As of this cleanup pass:
- `src/http/admin/mod.rs` is the admin module entry, backed by resource-specific files
under `src/http/admin/` and a dedicated admin router.
- `src/http/api/admin/mod.rs` is the admin module entry, backed by resource-specific files
under `src/http/api/admin/` and a dedicated admin router.
- `src/http/v1/mod.rs` is the v1 module entry, backed by thin route handlers for auth,
chat, responses, models, errors, and router mounting under `src/http/v1/`.
- `src/service/` now contains backend-facing HTTP use-case services:
@@ -78,7 +78,7 @@ As of this cleanup pass:
- admin model catalog refresh orchestration lives in `codex::models::service::ModelService`.
- `tests/common/` now contains shared integration-test helpers, including generic
response/session helpers plus admin account and v1 upstream route fixtures.
- `tests/admin_accounts_route_test.rs` has been split into list, mutation,
- `tests/admin_accounts_route_test.rs` has been split into list, lifecycle,
import/export, OAuth/auth, and cookies/quota/refresh/health scenario files.
- `tests/v1_upstream_route_test.rs` has been split into Responses HTTP/SSE,
Responses WebSocket, upstream fallback/refresh, and upstream error scenario files.
@@ -100,8 +100,8 @@ As of this cleanup pass:
- `codex/upstream/dispatch.rs`, `fallback.rs`, `refresh.rs`, `stream.rs`, and
`usage.rs` now own the obvious helper groups for dispatch response helpers, retry
classification, token refresh retry, SSE response collection, and usage recording.
- `codex/accounts/service/import.rs`, `mutation.rs`, `cookies.rs`, `quota.rs`, `health.rs`,
`refresh.rs`, and `runtime_pool.rs` now own account import, mutation, cookie, quota,
- `codex/accounts/service/import.rs`, `lifecycle.rs`, `cookies.rs`, `quota.rs`, `health.rs`,
`refresh.rs`, and `runtime_pool.rs` now own account import, lifecycle, cookie, quota,
health-check, single-account refresh, and runtime-pool workflows instead of leaving
them in `service/account/mod.rs`.
- `ChatService` and `ResponsesService` own a cloned `CodexUpstreamService` instance injected from
@@ -128,7 +128,7 @@ As of this cleanup pass:
moved out of route tests into `tests/fixtures/*.sse`.
- v1 Responses WebSocket mocked response payloads have been moved into
`tests/fixtures/*.json`.
- `http/admin/response.rs` now provides `AdminError`, and the admin route modules use
- `http/api/admin/response.rs` now provides `AdminError`, and the admin route modules use
`Result<impl IntoResponse, AdminError>` for standard admin-envelope failures.
- admin auth handlers now propagate session, login, OAuth, and PKCE failures through
`AdminError`; success paths that must set cookies or redirect still build explicit
@@ -210,7 +210,7 @@ and the Rust best-practice rules below.
### Async, Sharing, And Trait Objects
- Shared runtime state should use `Arc` and explicit synchronization primitives such as
`tokio::sync::Mutex` only where mutation is required.
`tokio::sync::Mutex` only where lifecycle is required.
- Keep lock scopes short. Do not hold account-pool or session locks while performing
network requests, database calls, or expensive serialization.
- Prefer generics/static dispatch for local helper abstractions. Use `Arc<dyn Trait>`
@@ -336,7 +336,7 @@ src/
service/
mod.rs
import.rs
mutation.rs
lifecycle.rs
cookies.rs
quota.rs
health.rs
@@ -505,7 +505,7 @@ This phase should be mostly mechanical:
Create:
```text
src/http/admin/
src/http/api/admin/
mod.rs
router.rs
response.rs
@@ -526,10 +526,10 @@ Suggested ownership:
| `auth.rs` | `login`, `auth_status`, `auth_logout`, OAuth PKCE/device routes, admin session cookie helpers. |
| `accounts.rs` | account list/create/import/export/delete/status/label, health check, refresh, reset usage, quota, cookies, account import parsing helpers. |
| `api_keys.rs` | local client key list/create/import/export/delete/status/label/batch helpers. |
| `logs.rs` | `/admin/logs` handler and log DTOs. |
| `models.rs` | `/admin/refresh-models` handler and model refresh DTOs. |
| `settings.rs` | `/admin/settings` handler and settings DTOs. |
| `usage.rs` | `/admin/usage-stats` and `/admin/usage-stats/summary`. |
| `logs.rs` | `/api/admin/logs` handler and log DTOs. |
| `models.rs` | `/api/admin/refresh-models` handler and model refresh DTOs. |
| `settings.rs` | `/api/admin/settings` handler and settings DTOs. |
| `usage.rs` | `/api/admin/usage-stats` and `/api/admin/usage-stats/summary`. |
| `router.rs` | admin route mounting only. |
Keep cross-resource helpers private when possible. If a helper is needed by multiple
@@ -540,10 +540,10 @@ The admin router should own admin paths:
```rust
pub fn router() -> Router<AppState> {
Router::new()
.route("/admin/login", post(auth::login))
.route("/admin/auth/status", get(auth::auth_status))
.route("/admin/accounts", get(accounts::accounts).post(accounts::create_account))
.route("/admin/api-keys", get(api_keys::api_keys).post(api_keys::create_api_key))
.route("/api/admin/login", post(auth::login))
.route("/api/admin/auth/status", get(auth::auth_status))
.route("/api/admin/accounts", get(accounts::accounts).post(accounts::create_account))
.route("/api/admin/api-keys", get(api_keys::api_keys).post(api_keys::create_api_key))
}
```
@@ -600,7 +600,7 @@ cargo test --test admin_accounts_mutation_test
cargo test --test admin_accounts_import_export_test
cargo test --test admin_accounts_oauth_test
cargo test --test admin_accounts_cookies_quota_test
cargo test --test admin_api_keys_route_test
cargo test --test admin_client_keys_route_test
cargo test --test v1_responses_http_sse_test
cargo test --test v1_responses_websocket_test
cargo test --test v1_upstream_fallback_test
@@ -758,7 +758,7 @@ Targets:
```text
src/codex/protocol/error.rs
src/http/response.rs
src/http/admin/response.rs
src/http/api/admin/response.rs
src/http/v1/errors.rs
```
@@ -990,7 +990,7 @@ cargo test --test v1_upstream_errors_test
### Step 4: Split Former `src/http/admin.rs`
Move admin code into `src/http/admin/*.rs`, with `src/http/admin/mod.rs` as the module
Move admin code into `src/http/api/admin/*.rs`, with `src/http/api/admin/mod.rs` as the module
entry. Preserve behavior.
Verification:
@@ -1002,7 +1002,7 @@ cargo test --test admin_accounts_mutation_test
cargo test --test admin_accounts_import_export_test
cargo test --test admin_accounts_oauth_test
cargo test --test admin_accounts_cookies_quota_test
cargo test --test admin_api_keys_route_test
cargo test --test admin_client_keys_route_test
cargo test --test admin_logs_route_test
cargo test --test admin_models_route_test
cargo test --test admin_settings_route_test
@@ -1090,7 +1090,7 @@ cargo test --test admin_accounts_mutation_test
cargo test --test admin_accounts_import_export_test
cargo test --test admin_accounts_oauth_test
cargo test --test admin_accounts_cookies_quota_test
cargo test --test admin_api_keys_route_test
cargo test --test admin_client_keys_route_test
cargo test --test v1_responses_http_sse_test
cargo test --test v1_responses_websocket_test
cargo test --test v1_upstream_fallback_test
+468
View File
@@ -0,0 +1,468 @@
# Backend Naming Audit
## Verdict
当前后端命名可以作为第一版完整架构定稿。主边界已经清楚:`runtime` 负责装配,`web` 负责未来 Web 控制台承载,`admin` 负责管理域,`codex` 负责 Codex 业务内核,`platform` 负责跨领域基础设施。
命名的艺术不在于词汇华丽,而在于每个名字都能回答三个问题:
- 这个目录属于哪个领域。
- 这个文件对外暴露什么责任。
- 这个名字是否会让后来的人误以为它还承担了别的职责。
本轮审计结论:源码目录已经达到“可长期演进”的层级。剩余问题主要是少数兼容性术语和偏大的聚合文件,不影响当前架构定稿。
## Naming Principles
- 路由表面用 `api`、`http`、`web` 表达协议和承载面。
- 业务能力用领域名表达,例如 `accounts`、`models`、`events`、`gateway`。
- 基础设施只放在 `platform`,避免业务目录承担系统能力。
- 测试目录跟随生产目录命名,测试文件表达场景而不是实现细节。
- 对外兼容词可以保留在 route 和 DTO 中,内部模块优先使用更准确的领域词。
## Complete Architecture Tree
以下目录排除了生成物和本地状态:`.git`、`target`、`data`、`logs`、`designs`。
```text
.
├── docs
│ ├── superpowers
│ │ ├── plans
│ │ │ ├── 2026-06-11-codex-proxy-rs.md
│ │ │ ├── 2026-06-12-openai-gpt-codex-parity.md
│ │ │ ├── 2026-06-13-utils-module-migration.md
│ │ │ ├── 2026-06-14-backend-naming-architecture.md
│ │ │ └── 2026-06-14-logging-refactor.md
│ │ └── specs
│ │ ├── 2026-06-12-openai-gpt-codex-parity-design.md
│ │ ├── 2026-06-13-architecture-refactor-design.md
│ │ ├── 2026-06-14-backend-naming-architecture-design.md
│ │ └── 2026-06-14-logging-refactor-design.md
│ ├── ADMIN_INITIALIZATION_FIX.md
│ ├── FINGERPRINT_AUTO_UPDATE.md
│ ├── FINGERPRINT_FIX.md
│ ├── FINGERPRINT_PROPAGATION_FIX.md
│ ├── FINGERPRINT_UPDATE_TEST_REPORT.md
│ ├── IMPLEMENTATION_COMPARISON.md
│ ├── IMPLEMENTATION_COMPLETE.md
│ ├── OPENAI_CODEX_PARITY_AUDIT.md
│ ├── OPENAI_REQUEST_ALIGNMENT.md
│ ├── RELIABILITY_ENHANCEMENTS.md
│ ├── RESPONSE_CHAIN_ALIGNMENT.md
│ ├── api.md
│ ├── architecture-audit.md
│ ├── architecture-capability-plan.md
│ ├── architecture-reorganization.md
│ ├── backend-naming-audit.md
│ ├── code-quality-audit.md
│ ├── database-storage-audit.md
│ ├── dependency-policy.md
│ ├── implementation-status.md
│ ├── optimization-roadmap.md
│ ├── rust-community-review.md
│ ├── scheduler-completion-report.md
│ ├── scheduler-implementation.md
│ ├── scheduler-usage.md
│ └── status-codes.md
├── src
│ ├── admin
│ │ ├── api
│ │ │ ├── accounts
│ │ │ │ ├── cookies.rs
│ │ │ │ ├── create.rs
│ │ │ │ ├── delete.rs
│ │ │ │ ├── export.rs
│ │ │ │ ├── health.rs
│ │ │ │ ├── import.rs
│ │ │ │ ├── lifecycle.rs
│ │ │ │ ├── list.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── oauth.rs
│ │ │ │ └── quota.rs
│ │ │ ├── client_keys
│ │ │ │ ├── create.rs
│ │ │ │ ├── export.rs
│ │ │ │ ├── import.rs
│ │ │ │ ├── lifecycle.rs
│ │ │ │ ├── list.rs
│ │ │ │ └── mod.rs
│ │ │ ├── logs
│ │ │ │ ├── detail.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── query.rs
│ │ │ │ └── state.rs
│ │ │ ├── diagnostics.rs
│ │ │ ├── mod.rs
│ │ │ ├── models.rs
│ │ │ ├── response.rs
│ │ │ ├── router.rs
│ │ │ ├── session.rs
│ │ │ ├── settings.rs
│ │ │ └── usage.rs
│ │ ├── client_keys
│ │ │ ├── mod.rs
│ │ │ └── service.rs
│ │ ├── session
│ │ │ ├── mod.rs
│ │ │ ├── repository.rs
│ │ │ └── service.rs
│ │ ├── tasks
│ │ │ ├── mod.rs
│ │ │ └── session_cleanup.rs
│ │ ├── mod.rs
│ │ └── settings.rs
│ ├── codex
│ │ ├── accounts
│ │ │ ├── cookies
│ │ │ │ ├── jar.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── repository.rs
│ │ │ ├── repository
│ │ │ │ ├── accounts.rs
│ │ │ │ ├── leases.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── quotas.rs
│ │ │ │ ├── tokens.rs
│ │ │ │ └── usage.rs
│ │ │ ├── service
│ │ │ │ ├── cookies.rs
│ │ │ │ ├── health.rs
│ │ │ │ ├── import.rs
│ │ │ │ ├── lifecycle.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── pool_sync.rs
│ │ │ │ ├── quota.rs
│ │ │ │ └── refresh.rs
│ │ │ ├── cloudflare_challenge.rs
│ │ │ ├── jwt.rs
│ │ │ ├── lifecycle.rs
│ │ │ ├── mod.rs
│ │ │ ├── model.rs
│ │ │ ├── pool.rs
│ │ │ └── usage_snapshots.rs
│ │ ├── events
│ │ │ ├── event.rs
│ │ │ ├── mod.rs
│ │ │ ├── repository.rs
│ │ │ └── service.rs
│ │ ├── gateway
│ │ │ ├── fingerprint
│ │ │ │ ├── mod.rs
│ │ │ │ ├── model.rs
│ │ │ │ ├── repository.rs
│ │ │ │ ├── update_checker.rs
│ │ │ │ └── updater.rs
│ │ │ ├── oauth
│ │ │ │ ├── client.rs
│ │ │ │ ├── codex_cli.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── refresh.rs
│ │ │ │ └── token.rs
│ │ │ ├── protocol
│ │ │ │ ├── codex_to_openai.rs
│ │ │ │ ├── error.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── openai_to_codex.rs
│ │ │ │ └── schema.rs
│ │ │ ├── transport
│ │ │ │ ├── websocket
│ │ │ │ │ ├── codec.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ └── pool.rs
│ │ │ │ ├── headers.rs
│ │ │ │ ├── http_client.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── rate_limits.rs
│ │ │ │ ├── sse.rs
│ │ │ │ ├── types.rs
│ │ │ │ └── usage_events.rs
│ │ │ ├── conversation_identity.rs
│ │ │ ├── installation_id.rs
│ │ │ └── mod.rs
│ │ ├── models
│ │ │ ├── catalog.rs
│ │ │ ├── mod.rs
│ │ │ ├── repository.rs
│ │ │ └── service.rs
│ │ ├── serving
│ │ │ ├── dispatch
│ │ │ │ ├── account_refresh.rs
│ │ │ │ ├── affinity.rs
│ │ │ │ ├── fallback.rs
│ │ │ │ ├── limits.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── routing.rs
│ │ │ │ ├── stream.rs
│ │ │ │ ├── stream_audit.rs
│ │ │ │ └── usage.rs
│ │ │ ├── http
│ │ │ │ ├── auth.rs
│ │ │ │ ├── chat.rs
│ │ │ │ ├── diagnostics.rs
│ │ │ │ ├── errors.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── models.rs
│ │ │ │ ├── responses.rs
│ │ │ │ └── router.rs
│ │ │ ├── chat.rs
│ │ │ ├── diagnostics.rs
│ │ │ ├── mod.rs
│ │ │ └── responses.rs
│ │ ├── tasks
│ │ │ ├── mod.rs
│ │ │ ├── model_refresh.rs
│ │ │ ├── quota_refresh.rs
│ │ │ └── token_refresh.rs
│ │ ├── usage
│ │ │ ├── mod.rs
│ │ │ └── service.rs
│ │ └── mod.rs
│ ├── config
│ │ ├── loader.rs
│ │ ├── mod.rs
│ │ └── types.rs
│ ├── platform
│ │ ├── crypto
│ │ │ ├── mod.rs
│ │ │ └── secret_box.rs
│ │ ├── http
│ │ │ ├── auth.rs
│ │ │ ├── health.rs
│ │ │ ├── mod.rs
│ │ │ └── request_id.rs
│ │ ├── identity
│ │ │ ├── admin_session.rs
│ │ │ ├── client_key.rs
│ │ │ ├── client_key_repository.rs
│ │ │ ├── error.rs
│ │ │ └── mod.rs
│ │ ├── logging
│ │ │ ├── mod.rs
│ │ │ └── rotation.rs
│ │ ├── storage
│ │ │ ├── db.rs
│ │ │ ├── mod.rs
│ │ │ ├── paths.rs
│ │ │ └── schema.sql
│ │ └── mod.rs
│ ├── runtime
│ │ ├── tasks
│ │ │ ├── coordinator.rs
│ │ │ ├── mod.rs
│ │ │ └── types.rs
│ │ ├── bootstrap.rs
│ │ ├── mod.rs
│ │ ├── router.rs
│ │ └── state.rs
│ ├── utils
│ │ ├── json.rs
│ │ ├── mod.rs
│ │ └── pagination.rs
│ ├── web
│ │ ├── assets.rs
│ │ ├── mod.rs
│ │ ├── router.rs
│ │ ├── security.rs
│ │ └── shell.rs
│ ├── lib.rs
│ └── main.rs
├── tests
│ ├── admin
│ │ ├── accounts
│ │ │ ├── cookies_quota.rs
│ │ │ ├── import_export.rs
│ │ │ ├── lifecycle.rs
│ │ │ ├── list.rs
│ │ │ └── oauth.rs
│ │ ├── api_contract.rs
│ │ ├── client_keys_route.rs
│ │ ├── logs_route.rs
│ │ ├── models_route.rs
│ │ ├── session.rs
│ │ ├── session_login_route.rs
│ │ ├── session_repository.rs
│ │ ├── settings_route.rs
│ │ └── usage_stats_route.rs
│ ├── architecture
│ │ ├── accounts_boundary.rs
│ │ ├── admin_accounts_split.rs
│ │ ├── admin_boundary.rs
│ │ ├── codex_activity_boundary.rs
│ │ ├── gateway_boundary.rs
│ │ ├── platform_boundary.rs
│ │ ├── runtime_tasks_boundary.rs
│ │ └── serving_boundary.rs
│ ├── codex_accounts
│ │ ├── cookie_store.rs
│ │ ├── pool_scheduling.rs
│ │ ├── refresh.rs
│ │ ├── refresh_scheduler.rs
│ │ ├── repository.rs
│ │ └── service_refresh.rs
│ ├── codex_events
│ │ └── logs_pagination.rs
│ ├── codex_gateway
│ │ ├── websocket
│ │ │ └── pool.rs
│ │ ├── cli_auth_import.rs
│ │ ├── fingerprint_update.rs
│ │ ├── headers.rs
│ │ ├── http_client.rs
│ │ ├── oauth_refresh.rs
│ │ ├── usage_events.rs
│ │ └── websocket.rs
│ ├── codex_models
│ │ └── catalog.rs
│ ├── codex_serving
│ │ ├── chat_completions.rs
│ │ ├── diagnostics_route.rs
│ │ ├── responses_http_sse.rs
│ │ ├── responses_websocket.rs
│ │ ├── routes_chat.rs
│ │ ├── routes_responses.rs
│ │ ├── upstream_errors.rs
│ │ └── upstream_fallback.rs
│ ├── fixtures
│ │ ├── chat
│ │ │ ├── non_stream_text.sse
│ │ │ ├── parallel_tools_success.sse
│ │ │ ├── retry_after_success.sse
│ │ │ ├── stream_text.sse
│ │ │ ├── tool_reasoning_complete.sse
│ │ │ └── tool_stream.sse
│ │ └── responses
│ │ ├── http_sse
│ │ │ ├── after_402.sse
│ │ │ ├── after_403.sse
│ │ │ ├── after_429.sse
│ │ │ ├── after_cloudflare_403.sse
│ │ │ ├── after_refresh.sse
│ │ │ ├── completed_fields.sse
│ │ │ ├── completed_reasoning_include.sse
│ │ │ ├── completed_usage.sse
│ │ │ ├── default_stream.sse
│ │ │ ├── done_item_completed.sse
│ │ │ ├── error_event.sse
│ │ │ ├── failed_event.sse
│ │ │ ├── stream_after_429.sse
│ │ │ ├── stream_after_refresh.sse
│ │ │ ├── stream_error_event.sse
│ │ │ ├── stream_failed_event.sse
│ │ │ ├── stream_usage.sse
│ │ │ └── text_deltas_completed.sse
│ │ └── websocket
│ │ ├── completed.json
│ │ ├── first_account_limited.json
│ │ ├── history_rate_limited.json
│ │ ├── rate_limited.json
│ │ ├── second_account_limited.json
│ │ └── token_revoked.json
│ ├── platform
│ │ ├── client_key_auth.rs
│ │ ├── crypto.rs
│ │ ├── http_auth.rs
│ │ ├── log_rotation.rs
│ │ └── storage_schema.rs
│ ├── runtime
│ │ ├── http_trace.rs
│ │ └── startup.rs
│ ├── support
│ │ ├── admin_accounts.rs
│ │ ├── mod.rs
│ │ └── upstream.rs
│ ├── admin.rs
│ ├── architecture.rs
│ ├── codex_accounts.rs
│ ├── codex_events.rs
│ ├── codex_gateway.rs
│ ├── codex_models.rs
│ ├── codex_serving.rs
│ ├── config.rs
│ ├── platform.rs
│ └── runtime.rs
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── README.md
├── config.yaml
├── docker-compose.yml
└── rust-toolchain.toml
```
## Domain Audit
| Area | Verdict | Notes |
| --- | --- | --- |
| `src/runtime` | Good | `bootstrap`、`router`、`state`、`tasks` 表达装配层职责,适合单 crate 起步阶段。 |
| `src/web` | Good | 只作为未来 Admin Web 控制台的后端挂载面,没有前端历史包袱。 |
| `src/admin/api` | Good | 管理 JSON API 从 `http` 收束为 `api`,比协议名更能表达契约层。 |
| `src/admin/session` | Good | 管理员登录态与 `client_keys` 拆开,避免 `auth` 泛化。 |
| `src/admin/client_keys` | Good | 内部语义是本地客户端凭据,不是上游 provider key。 |
| `src/codex/accounts` | Good | 账号、cookies、仓储、服务、调度池都在账号域内。 |
| `src/codex/models` | Good | model catalog 从账号域剥离,边界正确。 |
| `src/codex/events` | Good | 业务事件从进程日志中独立出来,命名比 `logs` 更准确。 |
| `src/codex/gateway` | Good | `protocol`、`transport`、`oauth`、`fingerprint` 是上游链路的自然分层。 |
| `src/codex/serving` | Good | 对外 OpenAI-compatible 服务面与上游 gateway 分离。 |
| `src/platform` | Good | `crypto`、`http`、`identity`、`logging`、`storage` 都是跨领域能力。 |
| `src/config` | Good | 配置加载与类型独立,暂不需要进一步艺术化命名。 |
| `src/utils` | Acceptable | 当前只有 `json` 和 `pagination`,还没变成杂物间;后续如果扩张,应优先迁回领域目录。 |
## File Naming Audit
### Names To Keep
- `admin/api/accounts/lifecycle.rs`:覆盖状态、标签、删除、批量变更,比 `mutate.rs` 更准确。
- `admin/api/client_keys/*`:内部使用 `client_keys` 是正确选择,路由保留 `/api-keys` 是对外兼容。
- `codex/accounts/cloudflare_challenge.rs`:比 `cf_path_block.rs` 明确,表达真实安全链路问题。
- `codex/accounts/service/pool_sync.rs`:表达账号仓储与运行时池同步,比 `runtime_pool.rs` 更有边界。
- `codex/gateway/conversation_identity.rs`:比泛化 `identity.rs` 更准确。
- `codex/gateway/installation_id.rs`:表达 ChatGPT/Codex 安装标识,不混入一般 installation 概念。
- `codex/gateway/transport/http_client.rs`:比 `client.rs` 清楚,因为 transport 下还有 WebSocket。
- `codex/gateway/transport/usage_events.rs`:表达从上游事件流中提取 usage,而不是通用 usage 领域。
- `codex/tasks/{model_refresh,quota_refresh,token_refresh}.rs`:任务以动作命名,职责清晰。
- `platform/http/request_id.rs`:比 `middleware.rs` 精确。
- `platform/crypto/secret_box.rs`:表达加密封装能力,适合稳定复用。
### Names That Are Acceptable But Can Be Refined Later
- `src/codex/accounts/model.rs`:目前承载 `Account` 和 `AccountStatus`,与 `codex/models` 有轻微视觉冲突。若继续追求极致,可改成 `account.rs`,让 `accounts/account.rs` 与 `models/catalog.rs` 完全错开。
- `src/admin/api/client_keys/*` 内部 handler 名仍使用 `api_key`,例如 `create_api_key`。这是对外路由 `/api/admin/api-keys` 的兼容词,不是架构错误。若后续精修,可把 handler 改为 `create_client_key`,DTO 保留 `ClientApiKey*`。
- `tests/codex_events/logs_pagination.rs`:目录已经叫 `codex_events`,文件还叫 `logs_pagination`。如果按领域纯度,可改为 `event_log_pagination.rs`。
- `tests/admin/*_route.rs`:测试文件显式带 `route`,表达 HTTP 路由测试,可以保留。若未来测试分层更细,可迁到 `tests/admin/api/*`。
- `src/codex/gateway/transport/types.rs`:`types` 是弱名,但当前范围在 `transport` 下仍可接受;如果继续增长,应按 request/response/error 拆出更具体文件。
### Names To Avoid Reintroducing
- `admin/http`:会把管理 API 误导为协议工具层。
- `admin/auth`:同时包含 session 和 client key 时语义过宽。
- `codex/accounts/models`:会让 model catalog 看起来像账号私有数据。
- `codex/logs`:会混淆业务事件和进程日志。
- `platform/http/middleware.rs`:middleware 是技术位置,不是责任名。
- `gateway/identity.rs`:identity 太泛,会掩盖 conversation identity 的具体含义。
- `transport/client.rs`:在有 HTTP、SSE、WebSocket 多传输时过于笼统。
## Size And Organization Audit
命名已经过关,但少数文件仍偏大,后续可在不改变逻辑的前提下继续整理:
- `src/codex/serving/dispatch/mod.rs`:约 1000 行,是下一轮最值得拆的聚合点。建议按 request context、response assembly、fallback decision、event recording 继续切分。
- `src/codex/gateway/transport/http_client.rs`:约 780 行。可拆为 request building、response parsing、model fetching、probe 四块。
- `src/codex/accounts/repository/accounts.rs`:约 700 行。可按 load/list/write/status 或 token metadata 继续切分。
- `src/codex/accounts/pool.rs`、`src/codex/serving/dispatch/affinity.rs`、`src/codex/gateway/transport/websocket/*` 当前偏大但仍有领域凝聚力,暂不强拆。
这些不是命名阻断项。继续优化时应遵守“只拆大文件,不改逻辑”,每次拆分后跑 `cargo fmt --check`、`cargo test --locked`、`cargo clippy --all-targets --all-features --locked -- -D warnings`。
## Final Recommendation
当前架构可以定为:
```text
runtime -> web/admin/codex/platform/config/utils
admin -> api/session/client_keys/tasks/settings
codex -> accounts/models/gateway/serving/events/usage/tasks
platform-> crypto/http/identity/logging/storage
```
这是一个适合项目起步阶段的完整体:没有过度抽象,没有强行 workspace 化,也没有把 Web 前端塞进后端。后续前端可以独立演进,后端只保留 `src/web` 作为运行时挂载点。
+1 -1
View File
@@ -64,7 +64,7 @@ Claude 审计中“窗口字段未持久化”的结论成立,但“配额控
- v1 response 和 401 后账户刷新失败事件通过 `LogService::record` 写入。
- 在一个入口执行 `enabled`、`capacity`、`capture_body` 和 metadata 规范。
- 管理端 `/admin/logs/state` 和请求链路共享同一个 `LogService` 实例,运行时开关立即生效。
- 管理端 `/api/admin/logs/state` 和请求链路共享同一个 `LogService` 实例,运行时开关立即生效。
### P2:收敛 schema 约束(已落地)
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -37,7 +37,7 @@
**问题:**
```
1697 行 - src/http/admin/accounts.rs ⚠️ 太大!
1697 行 - src/http/api/admin/accounts.rs ⚠️ 太大!
895 行 - src/codex/upstream/mod.rs
784 行 - src/codex/accounts/repository.rs
```
@@ -46,9 +46,9 @@
**改进方案:**
```rust
// 当前:src/http/admin/accounts.rs (1697 行)
// 当前:src/http/api/admin/accounts.rs (1697 行)
// 建议拆分为:
src/http/admin/accounts/
src/http/api/admin/accounts/
├── mod.rs // 路由注册
├── list.rs // GET /accounts
├── create.rs // POST /accounts
@@ -138,7 +138,7 @@ src/codex/accounts/service/
├── cookies.rs // Cookie 操作
├── health.rs // 健康检查
├── import.rs // 导入逻辑
├── mutation.rs // CRUD
├── lifecycle.rs // CRUD
├── quota.rs // 配额
├── refresh.rs // 刷新
└── runtime_pool.rs // 池同步
@@ -397,7 +397,7 @@ pub account_pool: Arc<RwLock<AccountPool>>,
- 风险:低
2. **🟡 拆分大文件**
- 先拆 `http/admin/accounts.rs` (1697 行)
- 先拆 `http/api/admin/accounts.rs` (1697 行)
- 按功能模块拆分(list/create/update/delete/oauth)
- 成本:中等
- 收益:提升可维护性
+2 -2
View File
@@ -12,9 +12,9 @@ HTTP status codes are not replaced by body codes. Admin/frontend APIs return bot
| HTTP status | Body code range | Meaning |
| --- | --- | --- |
| `200` | `200` | Successful read, login, or mutation with response body. |
| `200` | `200` | Successful read, login, or lifecycle with response body. |
| `201` | `201` | Resource created. |
| `204` | `204` | Successful mutation with no response body. No JSON envelope is sent. |
| `204` | `204` | Successful lifecycle with no response body. No JSON envelope is sent. |
| `400` | `40000`-`40099` | Malformed JSON, invalid parameters, or invalid cursor. |
| `401` | `40100`-`40199` | Missing/expired admin session or bad admin password. |
| `403` | `40300`-`40399` | Authenticated admin lacks permission for a local-only/bootstrap action. |
@@ -0,0 +1,214 @@
# Backend Naming Architecture Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rename backend modules so every directory and file has one clear domain meaning while reserving `/admin` for the future Web console.
**Architecture:** Move the current management JSON handlers from `admin/http` to `admin/api`, keep `/v1/*` under `codex/serving`, add a `web` backend shell for future static assets, and split ambiguous support modules into domain-specific names. The work is mechanical and must not change upstream Codex behavior.
**Tech Stack:** Rust 2021, Axum, SQLx, Tokio, tracing, existing integration tests.
---
## File Structure
Create:
- `src/web/mod.rs`
- `src/web/router.rs`
- `src/web/assets.rs`
- `src/web/shell.rs`
- `src/web/security.rs`
- `src/platform/crypto/mod.rs`
- `src/platform/crypto/secret_box.rs`
- `src/platform/logging/mod.rs`
- `src/admin/api/client_keys/mod.rs`
- `src/admin/api/logs/mod.rs`
- `src/admin/session/mod.rs`
- `src/admin/client_keys/mod.rs`
- `src/codex/events/mod.rs`
- `src/codex/models/mod.rs`
Move or rename:
- `src/admin/http` to `src/admin/api`
- `src/admin/auth/repository.rs` to `src/admin/session/repository.rs`
- `src/admin/auth/service.rs` to `src/admin/session/service.rs`
- `src/admin/auth/api_key.rs` to `src/admin/client_keys/service.rs`
- `src/admin/http/api_keys.rs` to `src/admin/api/client_keys/mod.rs`
- `src/admin/http/auth.rs` to `src/admin/api/session.rs`
- `src/admin/http/logs.rs` to `src/admin/api/logs/mod.rs`
- `src/admin/http/accounts/mutate.rs` to `src/admin/api/accounts/lifecycle.rs`
- `src/codex/accounts/cf_path_block.rs` to `src/codex/accounts/cloudflare_challenge.rs`
- `src/codex/accounts/models` to `src/codex/models`
- `src/codex/accounts/repository/lease.rs` to `src/codex/accounts/repository/leases.rs`
- `src/codex/accounts/repository/quota.rs` to `src/codex/accounts/repository/quotas.rs`
- `src/codex/accounts/repository/token.rs` to `src/codex/accounts/repository/tokens.rs`
- `src/codex/accounts/service/mutation.rs` to `src/codex/accounts/service/lifecycle.rs`
- `src/codex/accounts/service/runtime_pool.rs` to `src/codex/accounts/service/pool_sync.rs`
- `src/codex/gateway/identity.rs` to `src/codex/gateway/conversation_identity.rs`
- `src/codex/gateway/installation.rs` to `src/codex/gateway/installation_id.rs`
- `src/codex/gateway/oauth/cli_import.rs` to `src/codex/gateway/oauth/codex_cli.rs`
- `src/codex/gateway/transport/client.rs` to `src/codex/gateway/transport/http_client.rs`
- `src/codex/gateway/transport/usage.rs` to `src/codex/gateway/transport/usage_events.rs`
- `src/codex/logs` to `src/codex/events`
- `src/codex/logs/rotation.rs` to `src/platform/logging/rotation.rs`
- `src/codex/tasks/model.rs` to `src/codex/tasks/model_refresh.rs`
- `src/codex/tasks/quota.rs` to `src/codex/tasks/quota_refresh.rs`
- `src/codex/tasks/refresh.rs` to `src/codex/tasks/token_refresh.rs`
- `src/codex/serving/dispatch/audit.rs` to `src/codex/serving/dispatch/stream_audit.rs`
- `src/codex/serving/dispatch/refresh.rs` to `src/codex/serving/dispatch/account_refresh.rs`
- `src/platform/crypto.rs` to `src/platform/crypto/secret_box.rs`
- `src/platform/http/middleware.rs` to `src/platform/http/request_id.rs`
- `src/platform/identity/api_key.rs` to `src/platform/identity/client_key.rs`
- `src/platform/identity/api_key_repository.rs` to `src/platform/identity/client_key_repository.rs`
## Task 1: Freeze the naming contract
**Files:**
- Create: `docs/superpowers/specs/2026-06-14-backend-naming-architecture-design.md`
- Create: `docs/superpowers/plans/2026-06-14-backend-naming-architecture.md`
- [x] **Step 1: Write the design spec**
Record route boundaries, target source tree, naming decisions, and migration policy.
- [x] **Step 2: Write this implementation plan**
List every file move and the verification gates.
## Task 2: Move management API and backend Web shell
**Files:**
- Move: `src/admin/http/**` to `src/admin/api/**`
- Create: `src/web/{mod.rs,router.rs,assets.rs,shell.rs,security.rs}`
- Modify: `src/admin/mod.rs`
- Modify: `src/runtime/router.rs`
- Test: `tests/architecture/admin_boundary.rs`
- [x] **Step 1: Move the management API directory**
Run file moves so handlers live under `admin::api`.
- [x] **Step 2: Update admin module exports**
Expose `pub mod api;`, `pub mod session;`, and `pub mod client_keys;`.
- [x] **Step 3: Add empty Web shell modules**
Add `src/web` modules with module-level documentation only. Do not serve frontend assets yet.
- [x] **Step 4: Update route mounting**
Import `crate::admin::api as admin_api` in `runtime/router.rs` and merge `admin_api::router()`.
## Task 3: Rename session and client key domains
**Files:**
- Move: `src/admin/auth/service.rs` to `src/admin/session/service.rs`
- Move: `src/admin/auth/repository.rs` to `src/admin/session/repository.rs`
- Move: `src/admin/auth/api_key.rs` to `src/admin/client_keys/service.rs`
- Move: `src/admin/api/api_keys.rs` to `src/admin/api/client_keys/mod.rs`
- Move: `src/admin/api/auth.rs` to `src/admin/api/session.rs`
- Modify: `src/runtime/state.rs`
- Modify: `src/runtime/bootstrap.rs`
- Test: `tests/architecture/admin_boundary.rs`
- [x] **Step 1: Move files into the new domains**
Keep type names stable unless a rename is required by compiler errors.
- [x] **Step 2: Replace imports**
Replace `admin::auth::service` with `admin::session::service`, `admin::auth::repository` with `admin::session::repository`, and `admin::auth::api_key` with `admin::client_keys::service`.
- [x] **Step 3: Update API router imports**
Import the API key handlers from `admin::api::client_keys` and session handlers from `admin::api::session`.
## Task 4: Rename Codex catalog, events, gateway, transport, and task modules
**Files:**
- Move the Codex files listed in the File Structure section.
- Modify: `src/codex/mod.rs`
- Modify: `src/codex/accounts/mod.rs`
- Modify: `src/codex/accounts/repository.rs`
- Modify: `src/codex/accounts/service/mod.rs`
- Modify: `src/codex/gateway/mod.rs`
- Modify: `src/codex/gateway/oauth/mod.rs`
- Modify: `src/codex/gateway/transport/mod.rs`
- Modify: `src/codex/tasks/mod.rs`
- Modify: `src/codex/serving/dispatch/mod.rs`
- Test: `tests/architecture/{accounts_boundary.rs,gateway_boundary.rs,serving_boundary.rs}`
- [x] **Step 1: Move model catalog to `codex/models`**
Update references from `codex::accounts::models` to `codex::models`.
- [x] **Step 2: Move business logs to `codex/events`**
Update references from `codex::logs` to `codex::events`.
- [x] **Step 3: Move gateway and transport files**
Update `identity`, `installation`, `cli_import`, `client`, and `usage` references to the new names.
- [x] **Step 4: Move task and dispatch helper names**
Update `model`, `quota`, `refresh`, `audit`, and dispatch refresh references to the new names.
## Task 5: Rename platform foundations
**Files:**
- Move: `src/platform/crypto.rs` to `src/platform/crypto/secret_box.rs`
- Move: `src/platform/http/middleware.rs` to `src/platform/http/request_id.rs`
- Move: `src/platform/identity/api_key.rs` to `src/platform/identity/client_key.rs`
- Move: `src/platform/identity/api_key_repository.rs` to `src/platform/identity/client_key_repository.rs`
- Modify: `src/platform/mod.rs`
- Modify: `src/platform/http/mod.rs`
- Modify: `src/platform/identity/mod.rs`
- Test: `tests/architecture/platform_boundary.rs`
- [x] **Step 1: Move files**
Create `platform::crypto::secret_box`, re-export `SecretBox`, `CryptoError`, and `CryptoResult` from `platform::crypto`.
- [x] **Step 2: Replace imports**
Update all imports of `platform::http::middleware`, `platform::identity::api_key`, and `platform::identity::api_key_repository`.
## Task 6: Update tests, docs references, and verify
**Files:**
- Modify tests under `tests/**`
- Modify current docs under `docs/api.md`, `docs/architecture-capability-plan.md`, `docs/architecture-reorganization.md`, `docs/implementation-status.md`, and this spec/plan if paths drift.
- [x] **Step 1: Update architecture tests**
Make architecture tests assert the new public module paths.
- [x] **Step 2: Update integration test imports**
Replace old paths with new paths while leaving test behavior unchanged.
- [x] **Step 3: Run formatting**
Run: `cargo fmt --check`
Expected: no formatting diff. If it fails, run `cargo fmt` and re-run `cargo fmt --check`.
- [x] **Step 4: Run tests**
Run: `cargo test --locked`
Expected: all tests pass.
- [x] **Step 5: Run Clippy**
Run: `cargo clippy --all-targets --all-features --locked -- -D warnings`
Expected: no warnings.
- [x] **Step 6: Check whitespace**
Run: `git diff --check`
Expected: no whitespace errors.
@@ -0,0 +1,97 @@
# Backend Naming Architecture Design
## Goal
将后端目录整理成一名一义的结构,为 `/admin` Web 控制台预留承载面,并把现有管理 JSON API 收束到 `/api/admin` 的后端契约层。
## Scope
本次设计只覆盖 Rust 后端目录、模块命名、路由边界和测试命名。前端工程由独立目录和独立约定承载,后端只提供 `src/web` 作为构建产物的挂载位置。
## Route Boundaries
```text
/admin/* Web 控制台入口,由 src/web 承载
/api/admin/* 管理 API,由 src/admin/api 承载
/v1/* OpenAI-compatible API,由 src/codex/serving 承载
/auth/openai/callback OpenAI OAuth 回调
/debug/* 本地诊断
/health 存活检查
```
`admin` 是管理域,`web` 是浏览器承载面,`codex` 是 Codex 上游内核,`platform` 是跨领域系统能力。任何目录名都应表达唯一责任,避免 `http`、`auth`、`api_key` 这类多义名泄漏到领域边界。
## Target Source Tree
```text
src/
main.rs
lib.rs
runtime/{mod.rs, bootstrap.rs, router.rs, state.rs}
runtime/tasks/{mod.rs, coordinator.rs, types.rs}
web/{mod.rs, router.rs, assets.rs, shell.rs, security.rs}
admin/{mod.rs, settings.rs}
admin/api/{mod.rs, router.rs, response.rs, session.rs, diagnostics.rs, settings.rs, models.rs, usage.rs}
admin/api/accounts/{mod.rs, list.rs, create.rs, import.rs, export.rs, lifecycle.rs, quota.rs, cookies.rs, oauth.rs, health.rs}
admin/api/client_keys/{mod.rs, list.rs, create.rs, import.rs, export.rs, lifecycle.rs}
admin/api/logs/{mod.rs, query.rs, detail.rs, state.rs}
admin/session/{mod.rs, service.rs, repository.rs}
admin/client_keys/{mod.rs, service.rs}
admin/tasks/{mod.rs, session_cleanup.rs}
codex/{mod.rs}
codex/accounts/{mod.rs, model.rs, jwt.rs, pool.rs, lifecycle.rs, cloudflare_challenge.rs, usage_snapshots.rs}
codex/accounts/cookies/{mod.rs, jar.rs, repository.rs}
codex/accounts/repository/{mod.rs, accounts.rs, tokens.rs, leases.rs, quotas.rs, usage.rs}
codex/accounts/service/{mod.rs, cookies.rs, health.rs, import.rs, lifecycle.rs, quota.rs, refresh.rs, pool_sync.rs}
codex/models/{mod.rs, catalog.rs, repository.rs, service.rs}
codex/gateway/{mod.rs, conversation_identity.rs, installation_id.rs}
codex/gateway/fingerprint/{mod.rs, model.rs, repository.rs, update_checker.rs, updater.rs}
codex/gateway/oauth/{mod.rs, client.rs, codex_cli.rs, refresh.rs, token.rs}
codex/gateway/protocol/{mod.rs, schema.rs, error.rs, openai_to_codex.rs, codex_to_openai.rs}
codex/gateway/transport/{mod.rs, http_client.rs, headers.rs, rate_limits.rs, sse.rs, usage_events.rs}
codex/gateway/transport/websocket/{mod.rs, codec.rs, pool.rs}
codex/events/{mod.rs, event.rs, repository.rs, service.rs}
codex/usage/{mod.rs, service.rs}
codex/tasks/{mod.rs, model_refresh.rs, quota_refresh.rs, token_refresh.rs}
codex/serving/{mod.rs, chat.rs, responses.rs, diagnostics.rs}
codex/serving/http/{mod.rs, router.rs, auth.rs, chat.rs, responses.rs, models.rs, diagnostics.rs, errors.rs}
codex/serving/dispatch/{mod.rs, affinity.rs, routing.rs, fallback.rs, limits.rs, account_refresh.rs, stream.rs, stream_audit.rs, usage.rs}
platform/{mod.rs}
platform/crypto/{mod.rs, secret_box.rs}
platform/http/{mod.rs, auth.rs, health.rs, request_id.rs}
platform/identity/{mod.rs, admin_session.rs, client_key.rs, client_key_repository.rs, error.rs}
platform/storage/{mod.rs, db.rs, paths.rs, schema.sql}
platform/logging/{mod.rs, rotation.rs}
config/{mod.rs, loader.rs, types.rs}
utils/{mod.rs, json.rs, pagination.rs}
```
## Naming Decisions
`admin/http` becomes `admin/api` because the module exposes the management API contract, not generic HTTP utilities.
`admin/auth` splits into `admin/session` and `admin/client_keys`. Admin login is session management; local `cpr_` keys are client credentials for `/v1/*`, not upstream provider keys.
`api_keys` becomes `client_keys` where the code manages local client credentials. The route can remain `/api/admin/api-keys` for product compatibility, but Rust modules should state the stronger domain meaning.
`codex/accounts/models` moves to `codex/models`. Model catalog and plan snapshots are Codex catalog concerns, not account-owned state.
`codex/logs` becomes `codex/events`. The SQLite event stream is a business event store used by the admin console; process file logging belongs under `platform/logging`.
`platform/http/middleware.rs` becomes `platform/http/request_id.rs` because it has one stable responsibility.
## Migration Policy
Implementation should be mechanical first: move files, update module declarations, update imports, then run formatting and tests. Business logic changes are out of scope unless required to preserve existing behavior under the new route boundary.
The migration must keep `/v1/*` OpenAI-compatible behavior unchanged and keep admin session auth separate from local client API keys.
@@ -7,7 +7,7 @@ use axum::{
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{platform::http::middleware::RequestId, runtime::state::AppState};
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
use super::account_service_error;
+42
View File
@@ -0,0 +1,42 @@
use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension, Json,
};
use serde::Deserialize;
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
use super::{admin_account_data_from_stored, validated_account_import_error};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAccountRequest {
pub token: Option<String>,
pub refresh_token: Option<String>,
}
pub async fn create_account(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<CreateAccountRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let stored = state
.services
.accounts
.import_validated(payload.token, payload.refresh_token)
.await
.map_err(|error| validated_account_import_error(error, &request_id))?;
// 手动添加账号的响应只返回可展示元数据,OAuth token 永不回显。
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(admin_account_data_from_stored(stored), request_id),
))
}
@@ -6,7 +6,7 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use crate::{platform::http::middleware::RequestId, runtime::state::AppState};
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
use super::account_service_error;
@@ -10,23 +10,12 @@ use serde::Deserialize;
use serde_json::{json, Map, Value};
use crate::{
codex::accounts::repository::StoredAccount,
platform::http::middleware::RequestId,
codex::accounts::repository::StoredAccount, platform::http::request_id::RequestId,
runtime::state::AppState,
utils::pagination::{clamp_limit, Page},
};
use super::super::{
require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse,
};
use super::{account_export_ids, account_service_error, account_status_value, AdminAccountData};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountsQuery {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
use super::{account_export_ids, account_service_error, account_status_value};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -41,34 +30,6 @@ enum AccountExportFormat {
Sub2Api,
}
pub async fn accounts(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Query(query): Query<AccountsQuery>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let limit = clamp_limit(query.limit.unwrap_or(50));
let page = state
.services
.accounts
.list(query.cursor, limit)
.await
.map_err(|error| account_service_error(error, &request_id))?;
let Page { items, next_cursor } = page;
let page = Page {
items: items.into_iter().map(AdminAccountData::from).collect(),
next_cursor,
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminPageEnvelope::ok(page, limit, request_id),
))
}
pub async fn export_accounts(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::{
codex::accounts::service::{AccountProbeResult, HealthCheckError},
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
};
@@ -13,13 +13,13 @@ use crate::{
accounts::service::{AccountImportEntry, StoreImportAccountError},
gateway::oauth::{default_codex_home, read_cli_auth_from_home},
},
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
utils::json::{first_string, string_at},
};
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
use super::{admin_account_data_from_stored, validated_account_import_error};
use super::validated_account_import_error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AccountImportFormat {
@@ -52,42 +52,12 @@ pub struct AccountImportData {
pub source_format: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAccountRequest {
pub token: Option<String>,
pub refresh_token: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportCliAuthRequest {
pub codex_home: Option<String>,
}
pub async fn create_account(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<CreateAccountRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let stored = state
.services
.accounts
.import_validated(payload.token, payload.refresh_token)
.await
.map_err(|error| validated_account_import_error(error, &request_id))?;
// 手动添加账号的响应只返回可展示元数据,OAuth token 永不回显。
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(admin_account_data_from_stored(stored), request_id),
))
}
pub async fn import_accounts(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::{
codex::accounts::{model::AccountStatus, service::RefreshAccountError},
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
};
+51
View File
@@ -0,0 +1,51 @@
use axum::{
extract::{Query, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension,
};
use serde::Deserialize;
use crate::{
platform::http::request_id::RequestId,
runtime::state::AppState,
utils::pagination::{clamp_limit, Page},
};
use super::super::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse};
use super::{account_service_error, AdminAccountData};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountsQuery {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
pub async fn accounts(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Query(query): Query<AccountsQuery>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let limit = clamp_limit(query.limit.unwrap_or(50));
let page = state
.services
.accounts
.list(query.cursor, limit)
.await
.map_err(|error| account_service_error(error, &request_id))?;
let Page { items, next_cursor } = page;
let page = Page {
items: items.into_iter().map(AdminAccountData::from).collect(),
next_cursor,
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminPageEnvelope::ok(page, limit, request_id),
))
}
@@ -14,9 +14,11 @@ use super::AdminError;
pub mod cookies;
pub mod create;
pub mod delete;
pub mod export;
pub mod health;
pub mod import;
pub mod lifecycle;
pub mod list;
pub mod mutate;
pub mod oauth;
pub mod quota;
@@ -24,22 +26,21 @@ pub use cookies::{
delete_account_cookies, get_account_cookies, set_account_cookies, AccountCookiesData,
DeleteAccountCookiesData, SetAccountCookiesRequest,
};
pub use create::{
create_account, import_accounts, import_cli_auth, AccountImportData, CreateAccountRequest,
ImportCliAuthRequest,
};
pub use create::{create_account, CreateAccountRequest};
pub use delete::{
batch_delete_accounts, delete_account, BatchDeleteAccountsData, BatchDeleteAccountsRequest,
DeleteAccountData,
};
pub use export::{export_accounts, AccountExportQuery};
pub use health::{health_check_accounts, AccountProbeData, HealthCheckData, HealthCheckRequest};
pub use list::{accounts, export_accounts, AccountExportQuery, AccountsQuery};
pub use mutate::{
pub use import::{import_accounts, import_cli_auth, AccountImportData, ImportCliAuthRequest};
pub use lifecycle::{
batch_update_account_status, refresh_account, reset_account_usage, update_account_label,
update_account_status, BatchUpdateAccountStatusData, BatchUpdateAccountStatusRequest,
ResetAccountUsageData, UpdateAccountLabelData, UpdateAccountLabelRequest,
UpdateAccountStatusData, UpdateAccountStatusRequest,
};
pub use list::{accounts, AccountsQuery};
pub use oauth::{
auth_callback, auth_code_relay, auth_device_login, auth_device_poll, auth_login_start,
AdminAuthCallbackQuery, AdminAuthCodeRelayData, AdminAuthCodeRelayRequest,
@@ -11,10 +11,10 @@ use reqwest::Url;
use serde::{Deserialize, Serialize};
use crate::{
admin::auth::service::{AdminAuthOAuthError, AdminAuthPkceExchange},
admin::session::service::{AdminAuthOAuthError, AdminAuthPkceExchange},
codex::gateway::oauth::{DeviceCode, OAuthError},
config::AppConfig,
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
};
@@ -13,7 +13,7 @@ use crate::{
AccountQuotaError, AccountQuotaWarning as ServiceAccountQuotaWarning,
AccountQuotaWarnings as ServiceAccountQuotaWarnings,
},
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
};
+28
View File
@@ -0,0 +1,28 @@
use axum::{
extract::State, http::HeaderMap, http::StatusCode, response::IntoResponse, Extension, Json,
};
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::{api_key_service_error, CreateApiKeyRequest, CreatedClientApiKeyData};
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
pub async fn create_api_key(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<CreateApiKeyRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state.services.api_keys.create(&payload.name).await {
Ok(created) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(
CreatedClientApiKeyData::new(created.key, created.plaintext),
request_id,
),
)),
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
+44
View File
@@ -0,0 +1,44 @@
use axum::{
extract::{Query, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension,
};
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::{
api_key_service_error, ApiKeyExportQuery, ClientApiKeyExportData, ClientApiKeyExportEntry,
};
use crate::admin::api::{
account_export_ids, require_admin_session, AdminEnvelope, AdminError, AdminResponse,
};
pub async fn export_api_keys(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Query(query): Query<ApiKeyExportQuery>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let ids = account_export_ids(query.ids.as_deref());
let keys = match state.services.api_keys.export(ids).await {
Ok(keys) => keys,
Err(error) => return Err(api_key_service_error(error, request_id)),
};
// 安全边界:本地 cpr_ key 只导出可展示元数据,绝不导出 plaintext、key_hash 或 pepper。
let data = ClientApiKeyExportData {
source_format: "rustLocalClientApiKeys",
rotation_required: true,
api_keys: keys
.into_iter()
.map(ClientApiKeyExportEntry::from)
.collect(),
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(data, request_id),
))
}
+40
View File
@@ -0,0 +1,40 @@
use axum::{
extract::State, http::HeaderMap, http::StatusCode, response::IntoResponse, Extension, Json,
};
use serde_json::Value;
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::{api_key_service_error, ClientApiKeyImportData, ImportedClientApiKeyData};
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
pub async fn import_api_keys(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<Value>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let imported = match state.services.api_keys.import(&payload).await {
Ok(imported) => imported,
Err(error) => return Err(api_key_service_error(error, request_id)),
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(
ClientApiKeyImportData {
imported: imported.imported,
skipped: imported.skipped,
rotated: true,
keys: imported
.keys
.into_iter()
.map(ImportedClientApiKeyData::new)
.collect(),
},
request_id,
),
))
}
+116
View File
@@ -0,0 +1,116 @@
use axum::{
extract::{Path, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension, Json,
};
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::{
api_key_not_found, api_key_service_error, BatchDeleteClientApiKeysData,
BatchDeleteClientApiKeysRequest, ClientApiKeyData, DeleteClientApiKeyData,
UpdateClientApiKeyLabelRequest, UpdateClientApiKeyStatusData, UpdateClientApiKeyStatusRequest,
};
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
pub async fn batch_delete_api_keys(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<BatchDeleteClientApiKeysRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let deleted = match state.services.api_keys.batch_delete(payload.ids).await {
Ok(deleted) => deleted,
Err(error) => return Err(api_key_service_error(error, request_id)),
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(
BatchDeleteClientApiKeysData {
deleted: deleted.deleted,
not_found: deleted.not_found,
},
request_id,
),
))
}
pub async fn update_api_key_label(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Path(key_id): Path<String>,
Json(payload): Json<UpdateClientApiKeyLabelRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
// label 是后台显示备注,不能影响 cpr_ key 的 hash、prefix 或启用状态。
match state
.services
.api_keys
.update_label(&key_id, payload.label)
.await
{
Ok(Some(key)) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(ClientApiKeyData::from(key), request_id),
)),
Ok(None) => Err(api_key_not_found(request_id)),
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
pub async fn update_api_key_status(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Path(key_id): Path<String>,
Json(payload): Json<UpdateClientApiKeyStatusRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state
.services
.api_keys
.update_status(key_id, &payload.status)
.await
{
Ok(Some(updated)) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(
UpdateClientApiKeyStatusData {
id: updated.id,
enabled: updated.enabled,
},
request_id,
),
)),
Ok(None) => Err(api_key_not_found(request_id)),
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
pub async fn delete_api_key(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Path(key_id): Path<String>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state.services.api_keys.delete(&key_id).await {
Ok(true) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(DeleteClientApiKeyData { deleted: true }, request_id),
)),
Ok(false) => Err(api_key_not_found(request_id)),
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
+40
View File
@@ -0,0 +1,40 @@
use axum::{
extract::{Query, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension,
};
use crate::{
platform::http::request_id::RequestId,
runtime::state::AppState,
utils::pagination::{clamp_limit, Page},
};
use super::{api_key_service_error, ApiKeysQuery, ClientApiKeyData};
use crate::admin::api::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse};
pub async fn api_keys(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Query(query): Query<ApiKeysQuery>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let limit = clamp_limit(query.limit.unwrap_or(50));
match state.services.api_keys.list(query.cursor, limit).await {
Ok(page) => {
let Page { items, next_cursor } = page;
let page = Page {
items: items.into_iter().map(ClientApiKeyData::from).collect(),
next_cursor,
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminPageEnvelope::ok(page, limit, request_id),
))
}
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
+316
View File
@@ -0,0 +1,316 @@
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
use crate::{
admin::client_keys::service::{ApiKeyServiceError, ImportedClientApiKey},
platform::identity::client_key_repository::StoredClientApiKey,
};
use super::AdminError;
pub mod create;
pub mod export;
pub mod import;
pub mod lifecycle;
pub mod list;
pub use create::create_api_key;
pub use export::export_api_keys;
pub use import::import_api_keys;
pub use lifecycle::{
batch_delete_api_keys, delete_api_key, update_api_key_label, update_api_key_status,
};
pub use list::api_keys;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeysQuery {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeyExportQuery {
pub ids: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateApiKeyRequest {
pub name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientApiKeyData {
pub id: String,
pub name: String,
pub label: Option<String>,
pub prefix: String,
pub enabled: bool,
pub created_at: String,
pub last_used_at: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatedClientApiKeyData {
pub id: String,
pub name: String,
pub label: Option<String>,
pub prefix: String,
pub enabled: bool,
pub created_at: String,
pub last_used_at: Option<String>,
pub plaintext: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateClientApiKeyStatusRequest {
pub status: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateClientApiKeyLabelRequest {
pub label: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateClientApiKeyStatusData {
pub id: String,
pub enabled: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BatchDeleteClientApiKeysRequest {
pub ids: Vec<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BatchDeleteClientApiKeysData {
pub deleted: u32,
pub not_found: Vec<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteClientApiKeyData {
pub deleted: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientApiKeyExportData {
pub source_format: &'static str,
pub rotation_required: bool,
pub api_keys: Vec<ClientApiKeyExportEntry>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientApiKeyExportEntry {
pub id: String,
pub name: String,
pub label: Option<String>,
pub prefix: String,
pub enabled: bool,
pub created_at: String,
pub last_used_at: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientApiKeyImportData {
pub imported: u32,
pub skipped: u32,
pub rotated: bool,
pub keys: Vec<ImportedClientApiKeyData>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportedClientApiKeyData {
pub source_id: Option<String>,
pub source_prefix: Option<String>,
pub id: String,
pub name: String,
pub label: Option<String>,
pub prefix: String,
pub enabled: bool,
pub created_at: String,
pub last_used_at: Option<String>,
pub plaintext: String,
}
impl From<StoredClientApiKey> for ClientApiKeyData {
fn from(key: StoredClientApiKey) -> Self {
Self {
id: key.id,
name: key.name,
label: key.label,
prefix: key.prefix,
enabled: key.enabled,
created_at: key.created_at,
last_used_at: key.last_used_at,
}
}
}
impl From<StoredClientApiKey> for ClientApiKeyExportEntry {
fn from(key: StoredClientApiKey) -> Self {
Self {
id: key.id,
name: key.name,
label: key.label,
prefix: key.prefix,
enabled: key.enabled,
created_at: key.created_at,
last_used_at: key.last_used_at,
}
}
}
impl CreatedClientApiKeyData {
pub(super) fn new(key: StoredClientApiKey, plaintext: String) -> Self {
Self {
id: key.id,
name: key.name,
label: key.label,
prefix: key.prefix,
enabled: key.enabled,
created_at: key.created_at,
last_used_at: key.last_used_at,
plaintext,
}
}
}
impl ImportedClientApiKeyData {
pub(super) fn new(imported: ImportedClientApiKey) -> Self {
let ImportedClientApiKey {
key,
plaintext,
source_id,
source_prefix,
} = imported;
Self {
source_id,
source_prefix,
id: key.id,
name: key.name,
label: key.label,
prefix: key.prefix,
enabled: key.enabled,
created_at: key.created_at,
last_used_at: key.last_used_at,
plaintext,
}
}
}
pub(super) fn api_key_service_error(error: ApiKeyServiceError, request_id: String) -> AdminError {
match error {
ApiKeyServiceError::RepositoryUnavailable => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"API key repository is not initialized",
request_id,
),
ApiKeyServiceError::HasherUnavailable => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"API key hasher is not initialized",
request_id,
),
ApiKeyServiceError::List => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to list API keys",
request_id,
),
ApiKeyServiceError::Export => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to export API keys",
request_id,
),
ApiKeyServiceError::Import => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to import API key",
request_id,
),
ApiKeyServiceError::Create => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to create API key",
request_id,
),
ApiKeyServiceError::Delete => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to delete API key",
request_id,
),
ApiKeyServiceError::UpdateLabel => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to update API key label",
request_id,
),
ApiKeyServiceError::UpdateStatus => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to update API key status",
request_id,
),
ApiKeyServiceError::Verify => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to verify API key",
request_id,
),
ApiKeyServiceError::InvalidStatus(message) => {
AdminError::new(StatusCode::BAD_REQUEST, 40001, message, request_id)
}
ApiKeyServiceError::EmptyName => AdminError::new(
StatusCode::BAD_REQUEST,
40001,
"API key name is required",
request_id,
),
ApiKeyServiceError::EmptyIds => AdminError::new(
StatusCode::BAD_REQUEST,
40001,
"API key ids are required",
request_id,
),
ApiKeyServiceError::LabelTooLong => AdminError::new(
StatusCode::BAD_REQUEST,
40001,
"API key label must be 64 characters or fewer",
request_id,
),
ApiKeyServiceError::NoImportableKeys => AdminError::new(
StatusCode::BAD_REQUEST,
40001,
"No importable API keys found",
request_id,
),
}
}
pub(super) fn api_key_not_found(request_id: String) -> AdminError {
AdminError::new(
StatusCode::NOT_FOUND,
40401,
"API key not found",
request_id,
)
}
@@ -6,7 +6,7 @@ use axum::{
};
use crate::{
codex::serving::http::diagnostics::diagnostics_data, platform::http::middleware::RequestId,
codex::serving::http::diagnostics::diagnostics_data, platform::http::request_id::RequestId,
runtime::state::AppState,
};
+35
View File
@@ -0,0 +1,35 @@
use axum::{
extract::{Path, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension,
};
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::log_service_error;
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
pub async fn log_detail(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Path(log_id): Path<String>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state.services.logs.get(&log_id).await {
Ok(Some(log)) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(log, request_id),
)),
Ok(None) => Err(AdminError::new(
StatusCode::NOT_FOUND,
40401,
"Log event not found",
request_id,
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
+60
View File
@@ -0,0 +1,60 @@
use axum::http::StatusCode;
use crate::codex::events::service::LogServiceError;
use super::AdminError;
pub mod detail;
pub mod query;
pub mod state;
pub use detail::log_detail;
pub use query::logs;
pub use state::{clear_logs, logs_state, update_logs_state};
pub(super) fn log_service_error(error: LogServiceError, request_id: String) -> AdminError {
match error {
LogServiceError::RepositoryUnavailable => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Event log repository is not initialized",
request_id,
),
LogServiceError::List => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to list event logs",
request_id,
),
LogServiceError::Get => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to load event log",
request_id,
),
LogServiceError::Count => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to count event logs",
request_id,
),
LogServiceError::Clear => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to clear event logs",
request_id,
),
LogServiceError::Write => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to write event log",
request_id,
),
LogServiceError::InvalidCapacity => AdminError::new(
StatusCode::UNPROCESSABLE_ENTITY,
42201,
"logsCapacity must be greater than 0",
request_id,
),
}
}
+71
View File
@@ -0,0 +1,71 @@
use axum::{
extract::{Query, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension,
};
use serde::Deserialize;
use crate::{
codex::events::service::LogListFilter, platform::http::request_id::RequestId,
runtime::state::AppState, utils::pagination::clamp_limit,
};
use super::log_service_error;
use crate::admin::api::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogsQuery {
pub cursor: Option<String>,
pub limit: Option<u32>,
pub kind: Option<String>,
pub level: Option<String>,
pub request_id: Option<String>,
pub account_id: Option<String>,
pub route: Option<String>,
pub model: Option<String>,
pub status_code: Option<i64>,
pub search: Option<String>,
}
pub async fn logs(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Query(query): Query<LogsQuery>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let limit = clamp_limit(query.limit.unwrap_or(50));
let cursor = query.cursor.clone();
match state.services.logs.list(cursor, limit, query.into()).await {
Ok(page) => Ok(AdminResponse::new(
StatusCode::OK,
AdminPageEnvelope::ok(page, limit, request_id),
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
impl From<LogsQuery> for LogListFilter {
fn from(query: LogsQuery) -> Self {
Self {
kind: non_empty(query.kind),
level: non_empty(query.level),
request_id: non_empty(query.request_id),
account_id: non_empty(query.account_id),
route: non_empty(query.route),
model: non_empty(query.model),
status_code: query.status_code,
search: non_empty(query.search),
}
}
}
fn non_empty(value: Option<String>) -> Option<String> {
value
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
+120
View File
@@ -0,0 +1,120 @@
use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension, Json,
};
use serde::{Deserialize, Serialize};
use crate::{
codex::events::service::{ClearLogs, LogState, LogStateUpdate},
platform::http::request_id::RequestId,
runtime::state::AppState,
};
use super::log_service_error;
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LogStateData {
pub enabled: bool,
pub capacity: u32,
pub capture_body: bool,
pub stored_count: u64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClearLogsData {
pub cleared: u64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateLogStateRequest {
pub enabled: Option<bool>,
pub capacity: Option<u32>,
pub capture_body: Option<bool>,
}
pub async fn logs_state(
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?;
match state.services.logs.state().await {
Ok(state) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(LogStateData::from(state), request_id),
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
pub async fn update_logs_state(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<UpdateLogStateRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state.services.logs.update_state(payload.into()).await {
Ok(state) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(LogStateData::from(state), request_id),
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
pub async fn clear_logs(
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?;
match state.services.logs.clear().await {
Ok(cleared) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(ClearLogsData::from(cleared), request_id),
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
impl From<LogState> for LogStateData {
fn from(state: LogState) -> Self {
Self {
enabled: state.enabled,
capacity: state.capacity,
capture_body: state.capture_body,
stored_count: state.stored_count,
}
}
}
impl From<ClearLogs> for ClearLogsData {
fn from(cleared: ClearLogs) -> Self {
Self {
cleared: cleared.cleared,
}
}
}
impl From<UpdateLogStateRequest> for LogStateUpdate {
fn from(request: UpdateLogStateRequest) -> Self {
Self {
enabled: request.enabled,
capacity: request.capacity,
capture_body: request.capture_body,
}
}
}
@@ -1,17 +1,17 @@
pub mod accounts;
pub mod api_keys;
pub mod auth;
pub mod client_keys;
pub mod diagnostics;
pub mod logs;
pub mod models;
pub mod response;
pub mod router;
pub mod session;
pub mod settings;
pub mod usage;
use self::{
accounts::{account_export_ids, account_status_value},
auth::require_admin_session,
session::require_admin_session,
};
pub use self::{
@@ -7,8 +7,8 @@ use axum::{
use serde::Serialize;
use crate::{
codex::accounts::models::service::{ModelRefreshResult, ModelServiceError},
platform::http::middleware::RequestId,
codex::models::service::{ModelRefreshResult, ModelServiceError},
platform::http::request_id::RequestId,
runtime::state::AppState,
};
+111
View File
@@ -0,0 +1,111 @@
use axum::{
routing::{delete, get, patch, post},
Router,
};
use crate::runtime::state::AppState;
use super::{
accounts::{
account_quota, accounts, auth_callback, auth_code_relay, auth_device_login,
auth_device_poll, auth_login_start, batch_delete_accounts, batch_update_account_status,
create_account, delete_account, delete_account_cookies, export_accounts,
get_account_cookies, health_check_accounts, import_accounts, import_cli_auth,
quota_warnings, refresh_account, reset_account_usage, set_account_cookies,
update_account_label, update_account_status,
},
client_keys::{
api_keys, batch_delete_api_keys, create_api_key, delete_api_key, export_api_keys,
import_api_keys, update_api_key_label, update_api_key_status,
},
diagnostics::diagnostics,
logs::{clear_logs, log_detail, logs, logs_state, update_logs_state},
models::refresh_models,
session::{auth_logout, auth_status, login},
settings::{settings, update_settings},
usage::{usage_stats, usage_stats_summary},
};
pub fn router() -> Router<AppState> {
Router::new()
.route("/api/admin/login", post(login))
.route("/api/admin/auth/status", get(auth_status))
.route("/api/admin/auth/logout", post(auth_logout))
.route("/api/admin/auth/login-start", post(auth_login_start))
.route("/api/admin/auth/code-relay", post(auth_code_relay))
.route("/api/admin/auth/callback", get(auth_callback))
.route("/auth/openai/callback", get(auth_callback))
.route("/api/admin/auth/device-login", post(auth_device_login))
.route(
"/api/admin/auth/device-poll/{device_code}",
get(auth_device_poll),
)
.route("/api/admin/diagnostics", get(diagnostics))
.route("/api/admin/logs", get(logs).delete(clear_logs))
.route(
"/api/admin/logs/state",
get(logs_state).patch(update_logs_state),
)
.route("/api/admin/logs/{log_id}", get(log_detail))
.route("/api/admin/settings", get(settings).patch(update_settings))
.route("/api/admin/refresh-models", post(refresh_models))
.route("/api/admin/usage-stats", get(usage_stats))
.route("/api/admin/usage-stats/summary", get(usage_stats_summary))
.route("/api/admin/accounts", get(accounts).post(create_account))
.route(
"/api/admin/accounts/health-check",
post(health_check_accounts),
)
.route("/api/admin/accounts/quota-warnings", get(quota_warnings))
.route(
"/api/admin/accounts/batch-delete",
post(batch_delete_accounts),
)
.route(
"/api/admin/accounts/batch-status",
post(batch_update_account_status),
)
.route("/api/admin/accounts/export", get(export_accounts))
.route(
"/api/admin/accounts/{account_id}/refresh",
post(refresh_account),
)
.route(
"/api/admin/accounts/{account_id}/reset-usage",
post(reset_account_usage),
)
.route("/api/admin/accounts/{account_id}/quota", get(account_quota))
.route(
"/api/admin/accounts/{account_id}/cookies",
get(get_account_cookies)
.post(set_account_cookies)
.delete(delete_account_cookies),
)
.route("/api/admin/accounts/{account_id}", delete(delete_account))
.route(
"/api/admin/accounts/{account_id}/label",
patch(update_account_label),
)
.route(
"/api/admin/accounts/{account_id}/status",
patch(update_account_status),
)
.route("/api/admin/accounts/import", post(import_accounts))
.route("/api/admin/accounts/import-cli", post(import_cli_auth))
.route("/api/admin/api-keys", get(api_keys).post(create_api_key))
.route("/api/admin/api-keys/export", get(export_api_keys))
.route("/api/admin/api-keys/import", post(import_api_keys))
.route(
"/api/admin/api-keys/batch-delete",
post(batch_delete_api_keys),
)
.route("/api/admin/api-keys/{key_id}", delete(delete_api_key))
.route(
"/api/admin/api-keys/{key_id}/label",
patch(update_api_key_label),
)
.route(
"/api/admin/api-keys/{key_id}/status",
patch(update_api_key_status),
)
}
@@ -10,12 +10,12 @@ use axum::{
use serde::{Deserialize, Serialize};
use crate::{
admin::auth::service::{
admin::session::service::{
AdminAuthPoolSummary as ServiceAdminAuthPoolSummary, AdminAuthServiceError,
AdminAuthStatus as ServiceAdminAuthStatus, AdminAuthUser as ServiceAdminAuthUser,
AdminSessionValidationError,
},
platform::http::{auth::admin_session_id, middleware::RequestId},
platform::http::{auth::admin_session_id, request_id::RequestId},
runtime::state::AppState,
};
@@ -11,7 +11,7 @@ use serde_json::Value;
use crate::{
admin::settings::{SettingsServiceError, SettingsUpdate},
config::{AppConfig, QuotaWarningThresholds},
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
};
@@ -11,7 +11,7 @@ use crate::{
AccountRepositoryError, AccountUsageListRecord, AccountUsageSummary,
},
codex::usage::service::UsageServiceError,
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
utils::pagination::{clamp_limit, Page},
};
+1
View File
@@ -0,0 +1 @@
pub mod service;
@@ -2,8 +2,8 @@ use serde_json::Value;
use crate::{
platform::identity::{
api_key::ApiKeyHasher,
api_key_repository::{ClientApiKeyRepository, StoredClientApiKey},
client_key::ApiKeyHasher,
client_key_repository::{ClientApiKeyRepository, StoredClientApiKey},
},
utils::{json::first_string, pagination::Page},
};
-519
View File
@@ -1,519 +0,0 @@
use axum::{
extract::{Path, Query, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension, Json,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
admin::auth::api_key::{ApiKeyServiceError, ImportedClientApiKey},
platform::{http::middleware::RequestId, identity::api_key_repository::StoredClientApiKey},
runtime::state::AppState,
utils::pagination::{clamp_limit, Page},
};
use super::{
account_export_ids, require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope,
AdminResponse,
};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeysQuery {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeyExportQuery {
pub ids: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateApiKeyRequest {
pub name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientApiKeyData {
pub id: String,
pub name: String,
pub label: Option<String>,
pub prefix: String,
pub enabled: bool,
pub created_at: String,
pub last_used_at: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatedClientApiKeyData {
pub id: String,
pub name: String,
pub label: Option<String>,
pub prefix: String,
pub enabled: bool,
pub created_at: String,
pub last_used_at: Option<String>,
pub plaintext: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateClientApiKeyStatusRequest {
pub status: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateClientApiKeyLabelRequest {
pub label: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateClientApiKeyStatusData {
pub id: String,
pub enabled: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BatchDeleteClientApiKeysRequest {
pub ids: Vec<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BatchDeleteClientApiKeysData {
pub deleted: u32,
pub not_found: Vec<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteClientApiKeyData {
pub deleted: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientApiKeyExportData {
pub source_format: &'static str,
pub rotation_required: bool,
pub api_keys: Vec<ClientApiKeyExportEntry>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientApiKeyExportEntry {
pub id: String,
pub name: String,
pub label: Option<String>,
pub prefix: String,
pub enabled: bool,
pub created_at: String,
pub last_used_at: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientApiKeyImportData {
pub imported: u32,
pub skipped: u32,
pub rotated: bool,
pub keys: Vec<ImportedClientApiKeyData>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportedClientApiKeyData {
pub source_id: Option<String>,
pub source_prefix: Option<String>,
pub id: String,
pub name: String,
pub label: Option<String>,
pub prefix: String,
pub enabled: bool,
pub created_at: String,
pub last_used_at: Option<String>,
pub plaintext: String,
}
impl From<StoredClientApiKey> for ClientApiKeyData {
fn from(key: StoredClientApiKey) -> Self {
Self {
id: key.id,
name: key.name,
label: key.label,
prefix: key.prefix,
enabled: key.enabled,
created_at: key.created_at,
last_used_at: key.last_used_at,
}
}
}
impl From<StoredClientApiKey> for ClientApiKeyExportEntry {
fn from(key: StoredClientApiKey) -> Self {
Self {
id: key.id,
name: key.name,
label: key.label,
prefix: key.prefix,
enabled: key.enabled,
created_at: key.created_at,
last_used_at: key.last_used_at,
}
}
}
impl CreatedClientApiKeyData {
fn new(key: StoredClientApiKey, plaintext: String) -> Self {
Self {
id: key.id,
name: key.name,
label: key.label,
prefix: key.prefix,
enabled: key.enabled,
created_at: key.created_at,
last_used_at: key.last_used_at,
plaintext,
}
}
}
impl ImportedClientApiKeyData {
fn new(imported: ImportedClientApiKey) -> Self {
let ImportedClientApiKey {
key,
plaintext,
source_id,
source_prefix,
} = imported;
Self {
source_id,
source_prefix,
id: key.id,
name: key.name,
label: key.label,
prefix: key.prefix,
enabled: key.enabled,
created_at: key.created_at,
last_used_at: key.last_used_at,
plaintext,
}
}
}
fn api_key_service_error(error: ApiKeyServiceError, request_id: String) -> AdminError {
match error {
ApiKeyServiceError::RepositoryUnavailable => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"API key repository is not initialized",
request_id,
),
ApiKeyServiceError::HasherUnavailable => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"API key hasher is not initialized",
request_id,
),
ApiKeyServiceError::List => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to list API keys",
request_id,
),
ApiKeyServiceError::Export => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to export API keys",
request_id,
),
ApiKeyServiceError::Import => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to import API key",
request_id,
),
ApiKeyServiceError::Create => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to create API key",
request_id,
),
ApiKeyServiceError::Delete => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to delete API key",
request_id,
),
ApiKeyServiceError::UpdateLabel => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to update API key label",
request_id,
),
ApiKeyServiceError::UpdateStatus => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to update API key status",
request_id,
),
ApiKeyServiceError::Verify => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to verify API key",
request_id,
),
ApiKeyServiceError::InvalidStatus(message) => {
AdminError::new(StatusCode::BAD_REQUEST, 40001, message, request_id)
}
ApiKeyServiceError::EmptyName => AdminError::new(
StatusCode::BAD_REQUEST,
40001,
"API key name is required",
request_id,
),
ApiKeyServiceError::EmptyIds => AdminError::new(
StatusCode::BAD_REQUEST,
40001,
"API key ids are required",
request_id,
),
ApiKeyServiceError::LabelTooLong => AdminError::new(
StatusCode::BAD_REQUEST,
40001,
"API key label must be 64 characters or fewer",
request_id,
),
ApiKeyServiceError::NoImportableKeys => AdminError::new(
StatusCode::BAD_REQUEST,
40001,
"No importable API keys found",
request_id,
),
}
}
fn api_key_not_found(request_id: String) -> AdminError {
AdminError::new(
StatusCode::NOT_FOUND,
40401,
"API key not found",
request_id,
)
}
pub async fn api_keys(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Query(query): Query<ApiKeysQuery>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let limit = clamp_limit(query.limit.unwrap_or(50));
match state.services.api_keys.list(query.cursor, limit).await {
Ok(page) => {
let Page { items, next_cursor } = page;
let page = Page {
items: items.into_iter().map(ClientApiKeyData::from).collect(),
next_cursor,
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminPageEnvelope::ok(page, limit, request_id),
))
}
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
pub async fn export_api_keys(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Query(query): Query<ApiKeyExportQuery>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let ids = account_export_ids(query.ids.as_deref());
let keys = match state.services.api_keys.export(ids).await {
Ok(keys) => keys,
Err(error) => return Err(api_key_service_error(error, request_id)),
};
// 安全边界:本地 cpr_ key 只导出可展示元数据,绝不导出 plaintext、key_hash 或 pepper。
let data = ClientApiKeyExportData {
source_format: "rustLocalClientApiKeys",
rotation_required: true,
api_keys: keys
.into_iter()
.map(ClientApiKeyExportEntry::from)
.collect(),
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(data, request_id),
))
}
pub async fn import_api_keys(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<Value>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let imported = match state.services.api_keys.import(&payload).await {
Ok(imported) => imported,
Err(error) => return Err(api_key_service_error(error, request_id)),
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(
ClientApiKeyImportData {
imported: imported.imported,
skipped: imported.skipped,
rotated: true,
keys: imported
.keys
.into_iter()
.map(ImportedClientApiKeyData::new)
.collect(),
},
request_id,
),
))
}
pub async fn create_api_key(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<CreateApiKeyRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state.services.api_keys.create(&payload.name).await {
Ok(created) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(
CreatedClientApiKeyData::new(created.key, created.plaintext),
request_id,
),
)),
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
pub async fn batch_delete_api_keys(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<BatchDeleteClientApiKeysRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let deleted = match state.services.api_keys.batch_delete(payload.ids).await {
Ok(deleted) => deleted,
Err(error) => return Err(api_key_service_error(error, request_id)),
};
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(
BatchDeleteClientApiKeysData {
deleted: deleted.deleted,
not_found: deleted.not_found,
},
request_id,
),
))
}
pub async fn update_api_key_label(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Path(key_id): Path<String>,
Json(payload): Json<UpdateClientApiKeyLabelRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
// label 是后台显示备注,不能影响 cpr_ key 的 hash、prefix 或启用状态。
match state
.services
.api_keys
.update_label(&key_id, payload.label)
.await
{
Ok(Some(key)) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(ClientApiKeyData::from(key), request_id),
)),
Ok(None) => Err(api_key_not_found(request_id)),
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
pub async fn update_api_key_status(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Path(key_id): Path<String>,
Json(payload): Json<UpdateClientApiKeyStatusRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state
.services
.api_keys
.update_status(key_id, &payload.status)
.await
{
Ok(Some(updated)) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(
UpdateClientApiKeyStatusData {
id: updated.id,
enabled: updated.enabled,
},
request_id,
),
)),
Ok(None) => Err(api_key_not_found(request_id)),
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
pub async fn delete_api_key(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Path(key_id): Path<String>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state.services.api_keys.delete(&key_id).await {
Ok(true) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(DeleteClientApiKeyData { deleted: true }, request_id),
)),
Ok(false) => Err(api_key_not_found(request_id)),
Err(error) => Err(api_key_service_error(error, request_id)),
}
}
-247
View File
@@ -1,247 +0,0 @@
use axum::{
extract::{Path, Query, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension, Json,
};
use serde::{Deserialize, Serialize};
use crate::{
codex::logs::service::{ClearLogs, LogListFilter, LogServiceError, LogState, LogStateUpdate},
platform::http::middleware::RequestId,
runtime::state::AppState,
utils::pagination::clamp_limit,
};
use super::{require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogsQuery {
pub cursor: Option<String>,
pub limit: Option<u32>,
pub kind: Option<String>,
pub level: Option<String>,
pub request_id: Option<String>,
pub account_id: Option<String>,
pub route: Option<String>,
pub model: Option<String>,
pub status_code: Option<i64>,
pub search: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LogStateData {
pub enabled: bool,
pub capacity: u32,
pub capture_body: bool,
pub stored_count: u64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClearLogsData {
pub cleared: u64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateLogStateRequest {
pub enabled: Option<bool>,
pub capacity: Option<u32>,
pub capture_body: Option<bool>,
}
pub async fn logs(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Query(query): Query<LogsQuery>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
let limit = clamp_limit(query.limit.unwrap_or(50));
let cursor = query.cursor.clone();
match state.services.logs.list(cursor, limit, query.into()).await {
Ok(page) => Ok(AdminResponse::new(
StatusCode::OK,
AdminPageEnvelope::ok(page, limit, request_id),
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
pub async fn logs_state(
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?;
match state.services.logs.state().await {
Ok(state) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(LogStateData::from(state), request_id),
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
pub async fn update_logs_state(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Json(payload): Json<UpdateLogStateRequest>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state.services.logs.update_state(payload.into()).await {
Ok(state) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(LogStateData::from(state), request_id),
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
pub async fn log_detail(
State(state): State<AppState>,
Extension(request_id): Extension<RequestId>,
headers: HeaderMap,
Path(log_id): Path<String>,
) -> Result<impl IntoResponse, AdminError> {
let request_id = request_id.as_str().to_string();
require_admin_session(&state, &headers, &request_id).await?;
match state.services.logs.get(&log_id).await {
Ok(Some(log)) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(log, request_id),
)),
Ok(None) => Err(AdminError::new(
StatusCode::NOT_FOUND,
40401,
"Log event not found",
request_id,
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
pub async fn clear_logs(
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?;
match state.services.logs.clear().await {
Ok(cleared) => Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(ClearLogsData::from(cleared), request_id),
)),
Err(error) => Err(log_service_error(error, request_id)),
}
}
impl From<LogState> for LogStateData {
fn from(state: LogState) -> Self {
Self {
enabled: state.enabled,
capacity: state.capacity,
capture_body: state.capture_body,
stored_count: state.stored_count,
}
}
}
impl From<ClearLogs> for ClearLogsData {
fn from(cleared: ClearLogs) -> Self {
Self {
cleared: cleared.cleared,
}
}
}
impl From<UpdateLogStateRequest> for LogStateUpdate {
fn from(request: UpdateLogStateRequest) -> Self {
Self {
enabled: request.enabled,
capacity: request.capacity,
capture_body: request.capture_body,
}
}
}
impl From<LogsQuery> for LogListFilter {
fn from(query: LogsQuery) -> Self {
Self {
kind: non_empty(query.kind),
level: non_empty(query.level),
request_id: non_empty(query.request_id),
account_id: non_empty(query.account_id),
route: non_empty(query.route),
model: non_empty(query.model),
status_code: query.status_code,
search: non_empty(query.search),
}
}
}
fn non_empty(value: Option<String>) -> Option<String> {
value
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
fn log_service_error(error: LogServiceError, request_id: String) -> AdminError {
match error {
LogServiceError::RepositoryUnavailable => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Event log repository is not initialized",
request_id,
),
LogServiceError::List => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to list event logs",
request_id,
),
LogServiceError::Get => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to load event log",
request_id,
),
LogServiceError::Count => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to count event logs",
request_id,
),
LogServiceError::Clear => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to clear event logs",
request_id,
),
LogServiceError::Write => AdminError::new(
StatusCode::INTERNAL_SERVER_ERROR,
50001,
"Failed to write event log",
request_id,
),
LogServiceError::InvalidCapacity => AdminError::new(
StatusCode::UNPROCESSABLE_ENTITY,
42201,
"logsCapacity must be greater than 0",
request_id,
),
}
}
-103
View File
@@ -1,103 +0,0 @@
use axum::{
routing::{delete, get, patch, post},
Router,
};
use crate::runtime::state::AppState;
use super::{
accounts::{
account_quota, accounts, auth_callback, auth_code_relay, auth_device_login,
auth_device_poll, auth_login_start, batch_delete_accounts, batch_update_account_status,
create_account, delete_account, delete_account_cookies, export_accounts,
get_account_cookies, health_check_accounts, import_accounts, import_cli_auth,
quota_warnings, refresh_account, reset_account_usage, set_account_cookies,
update_account_label, update_account_status,
},
api_keys::{
api_keys, batch_delete_api_keys, create_api_key, delete_api_key, export_api_keys,
import_api_keys, update_api_key_label, update_api_key_status,
},
auth::{auth_logout, auth_status, login},
diagnostics::diagnostics,
logs::{clear_logs, log_detail, logs, logs_state, update_logs_state},
models::refresh_models,
settings::{settings, update_settings},
usage::{usage_stats, usage_stats_summary},
};
pub fn router() -> Router<AppState> {
Router::new()
.route("/admin/login", post(login))
.route("/admin/auth/status", get(auth_status))
.route("/admin/auth/logout", post(auth_logout))
.route("/admin/auth/login-start", post(auth_login_start))
.route("/admin/auth/code-relay", post(auth_code_relay))
.route("/admin/auth/callback", get(auth_callback))
// OpenAI OAuth 客户端注册的是 /auth/callback;handler 内仍要求管理员会话。
.route("/auth/callback", get(auth_callback))
.route("/admin/auth/device-login", post(auth_device_login))
.route(
"/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).patch(update_logs_state),
)
.route("/admin/logs/{log_id}", get(log_detail))
.route("/admin/settings", get(settings).patch(update_settings))
.route("/admin/refresh-models", post(refresh_models))
.route("/admin/usage-stats", get(usage_stats))
.route("/admin/usage-stats/summary", get(usage_stats_summary))
.route("/admin/accounts", get(accounts).post(create_account))
.route("/admin/accounts/health-check", post(health_check_accounts))
.route("/admin/accounts/quota-warnings", get(quota_warnings))
.route("/admin/accounts/batch-delete", post(batch_delete_accounts))
.route(
"/admin/accounts/batch-status",
post(batch_update_account_status),
)
.route("/admin/accounts/export", get(export_accounts))
.route(
"/admin/accounts/{account_id}/refresh",
post(refresh_account),
)
.route(
"/admin/accounts/{account_id}/reset-usage",
post(reset_account_usage),
)
.route("/admin/accounts/{account_id}/quota", get(account_quota))
.route(
"/admin/accounts/{account_id}/cookies",
get(get_account_cookies)
.post(set_account_cookies)
.delete(delete_account_cookies),
)
.route("/admin/accounts/{account_id}", delete(delete_account))
.route(
"/admin/accounts/{account_id}/label",
patch(update_account_label),
)
.route(
"/admin/accounts/{account_id}/status",
patch(update_account_status),
)
.route("/admin/accounts/import", post(import_accounts))
.route("/admin/accounts/import-cli", post(import_cli_auth))
.route("/admin/api-keys", get(api_keys).post(create_api_key))
.route("/admin/api-keys/export", get(export_api_keys))
.route("/admin/api-keys/import", post(import_api_keys))
.route("/admin/api-keys/batch-delete", post(batch_delete_api_keys))
.route("/admin/api-keys/{key_id}", delete(delete_api_key))
.route(
"/admin/api-keys/{key_id}/label",
patch(update_api_key_label),
)
.route(
"/admin/api-keys/{key_id}/status",
patch(update_api_key_status),
)
}
+3 -2
View File
@@ -1,4 +1,5 @@
pub mod auth;
pub mod http;
pub mod api;
pub mod client_keys;
pub mod session;
pub mod settings;
pub mod tasks;
@@ -1,3 +1,2 @@
pub mod api_key;
pub mod repository;
pub mod service;
@@ -5,7 +5,7 @@ use tokio::sync::Mutex;
use uuid::Uuid;
use crate::{
admin::auth::repository::AdminAuthRepository,
admin::session::repository::AdminAuthRepository,
codex::accounts::service::{AccountService, ValidatedAccountImportError},
codex::accounts::{
model::AccountStatus,
+1 -1
View File
@@ -2,7 +2,7 @@ use chrono::Utc;
use tokio::time::{interval, Duration};
use tracing::{error, info};
use crate::admin::auth::repository::AdminAuthRepository;
use crate::admin::session::repository::AdminAuthRepository;
use crate::runtime::tasks::types::SchedulerHandle;
/// 会话清理调度器 - 定期删除过期的管理员会话
+1 -2
View File
@@ -1,9 +1,8 @@
pub mod cf_path_block;
pub mod cloudflare_challenge;
pub mod cookies;
pub mod jwt;
pub mod lifecycle;
pub mod model;
pub mod models;
pub mod pool;
pub mod repository;
pub mod service;
@@ -9,13 +9,6 @@ use crate::{
utils::pagination::{decode_cursor, encode_cursor, Page},
};
mod lease;
mod quota;
mod token;
mod usage;
pub use usage::AccountUsageRepository;
#[derive(Debug, Error)]
pub enum AccountRepositoryError {
#[error("database error: {0}")]
@@ -155,8 +148,8 @@ pub struct AccountUsageSummary {
#[derive(Clone)]
pub struct AccountRepository {
pool: SqlitePool,
secret_box: SecretBox,
pub(super) pool: SqlitePool,
pub(super) secret_box: SecretBox,
}
const INSERT_ACCOUNT_SQL: &str = r"
@@ -644,7 +637,7 @@ impl AccountRepository {
}
}
fn status_to_db(status: AccountStatus) -> &'static str {
pub(crate) fn status_to_db(status: AccountStatus) -> &'static str {
match status {
AccountStatus::Active => "active",
AccountStatus::Expired => "expired",
@@ -667,11 +660,13 @@ fn status_from_db(value: &str) -> AccountRepositoryResult<AccountStatus> {
}
}
fn parse_optional_rfc3339(value: Option<String>) -> AccountRepositoryResult<Option<DateTime<Utc>>> {
pub(crate) fn parse_optional_rfc3339(
value: Option<String>,
) -> AccountRepositoryResult<Option<DateTime<Utc>>> {
value.as_deref().map(parse_rfc3339).transpose()
}
fn parse_rfc3339(value: &str) -> AccountRepositoryResult<DateTime<Utc>> {
pub(crate) fn parse_rfc3339(value: &str) -> AccountRepositoryResult<DateTime<Utc>> {
Ok(DateTime::parse_from_rfc3339(value)?.with_timezone(&Utc))
}
@@ -679,7 +674,7 @@ fn nonnegative_i64_to_u64(value: i64) -> u64 {
value.max(0) as u64
}
fn optional_positive_i64_to_u64(value: Option<i64>) -> Option<u64> {
pub(crate) fn optional_positive_i64_to_u64(value: Option<i64>) -> Option<u64> {
value
.and_then(|value| u64::try_from(value).ok())
.filter(|value| *value > 0)
+9
View File
@@ -0,0 +1,9 @@
mod accounts;
mod leases;
mod quotas;
mod tokens;
mod usage;
pub use accounts::*;
pub(crate) use accounts::{optional_positive_i64_to_u64, parse_optional_rfc3339, status_to_db};
pub use usage::AccountUsageRepository;
@@ -185,7 +185,7 @@ mod tests {
);
let access_token = SecretString::new("access-secret".to_string().into());
let refresh_token = SecretString::new("refresh-secret".to_string().into());
let expires_at = super::super::parse_rfc3339("2026-06-14T00:00:00Z").unwrap();
let expires_at = super::super::accounts::parse_rfc3339("2026-06-14T00:00:00Z").unwrap();
let write = repository
.prepare_token_write(&access_token, Some(&refresh_token), Some(expires_at))
@@ -198,6 +198,6 @@ mod tests {
write.access_token_expires_at.as_deref(),
Some("2026-06-14T00:00:00+00:00")
);
assert!(super::super::parse_rfc3339(&write.updated_at).is_ok());
assert!(super::super::accounts::parse_rfc3339(&write.updated_at).is_ok());
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ use crate::{
repository::{AccountRepository, StoredAccount},
},
codex::gateway::fingerprint::model::Fingerprint,
codex::gateway::transport::client::{
codex::gateway::transport::http_client::{
build_reqwest_client, CodexBackendClient, CodexClientError, CodexRequestContext,
},
};
+2 -2
View File
@@ -13,8 +13,8 @@ use crate::{
};
use super::{
runtime_pool::pool_account_from_stored, AccountImportCounts, AccountImportEntry,
AccountService, StoreImportAccountError, ValidatedAccountImportError,
pool_sync::pool_account_from_stored, AccountImportCounts, AccountImportEntry, AccountService,
StoreImportAccountError, ValidatedAccountImportError,
};
enum StoredImportAccount {
@@ -1,8 +1,8 @@
use crate::codex::accounts::{model::AccountStatus, repository::AccountRepository};
use super::{
runtime_pool::pool_account_from_stored, AccountService, AccountServiceError,
BatchDeleteAccounts, BatchUpdateAccountStatus, UpdateAccountStatus,
pool_sync::pool_account_from_stored, AccountService, AccountServiceError, BatchDeleteAccounts,
BatchUpdateAccountStatus, UpdateAccountStatus,
};
impl AccountService {
+2 -2
View File
@@ -1,10 +1,10 @@
pub mod cookies;
pub mod health;
pub mod import;
pub mod mutation;
pub mod lifecycle;
pub mod pool_sync;
pub mod quota;
pub mod refresh;
pub mod runtime_pool;
use std::sync::Arc;
+1 -1
View File
@@ -13,7 +13,7 @@ use crate::{
};
use super::{
health::skipped_probe_result, runtime_pool::pool_account_from_stored, AccountProbeOutcome,
health::skipped_probe_result, pool_sync::pool_account_from_stored, AccountProbeOutcome,
AccountProbeResult, AccountService, RefreshAccountError,
};
@@ -1,4 +1,3 @@
pub mod event;
pub mod repository;
pub mod rotation;
pub mod service;
@@ -1,7 +1,7 @@
use sqlx::{QueryBuilder, Row, Sqlite, SqlitePool};
use crate::{
codex::logs::event::{EventLevel, EventLog},
codex::events::event::{EventLevel, EventLog},
utils::pagination::{clamp_limit, decode_cursor, encode_cursor, Page},
};
@@ -3,7 +3,7 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use crate::{
codex::logs::{
codex::events::{
event::EventLog,
repository::{EventLogFilters, EventLogRepository},
},
+2 -2
View File
@@ -1,6 +1,6 @@
pub mod conversation_identity;
pub mod fingerprint;
pub mod identity;
pub mod installation;
pub mod installation_id;
pub mod oauth;
pub mod protocol;
pub mod transport;
+1 -1
View File
@@ -19,7 +19,7 @@ const OPENAI_OAUTH_AUTHORIZE_ENDPOINT: &str = "https://auth.openai.com/oauth/aut
const OPENAI_OAUTH_DEVICE_CODE_ENDPOINT: &str = "https://auth.openai.com/oauth/device/code";
const OPENAI_OAUTH_TOKEN_ENDPOINT: &str = "https://auth.openai.com/oauth/token";
const OAUTH_CALLBACK_PORT: u16 = 1455;
const OAUTH_CALLBACK_PATH: &str = "/auth/callback";
const OAUTH_CALLBACK_PATH: &str = "/auth/openai/callback";
#[derive(Debug, Clone)]
pub struct OAuthConfig {
+2 -2
View File
@@ -1,9 +1,9 @@
pub mod cli_import;
pub mod client;
pub mod codex_cli;
pub mod refresh;
pub mod token;
pub use cli_import::*;
pub use client::*;
pub use codex_cli::*;
pub use refresh::*;
pub use token::*;
@@ -11,13 +11,12 @@ use serde_json::{Map, Value};
use thiserror::Error;
use crate::{
codex::accounts::models::catalog::BackendModelEntry,
codex::gateway::fingerprint::model::Fingerprint,
codex::gateway::transport::{
headers::build_ordered_codex_headers,
sse::SseError,
types::CodexResponsesRequest,
usage::{extract_sse_usage, TokenUsage},
usage_events::{extract_sse_usage, TokenUsage},
websocket::{
append_rate_limit_updates, create_response_via_websocket,
create_response_via_websocket_stream, create_response_via_websocket_stream_with_pool,
@@ -26,6 +25,7 @@ use crate::{
SharedRateLimitUpdates, WebSocketSupportError,
},
},
codex::models::catalog::BackendModelEntry,
};
#[derive(Debug, Error)]
+2 -2
View File
@@ -1,7 +1,7 @@
pub mod client;
pub mod headers;
pub mod http_client;
pub mod rate_limits;
pub mod sse;
pub mod types;
pub mod usage;
pub mod usage_events;
pub mod websocket;
+2 -1
View File
@@ -1,6 +1,7 @@
pub mod accounts;
pub mod events;
pub mod gateway;
pub mod logs;
pub mod models;
pub mod serving;
pub mod tasks;
pub mod usage;
@@ -2,7 +2,7 @@ use chrono::Utc;
use sqlx::{Row, SqlitePool};
use thiserror::Error;
use crate::codex::accounts::models::catalog::{CodexModelInfo, ModelPlanSnapshot};
use crate::codex::models::catalog::{CodexModelInfo, ModelPlanSnapshot};
#[derive(Debug, Error)]
pub enum ModelSnapshotRepositoryError {
@@ -3,19 +3,19 @@ use std::sync::Arc;
use tokio::sync::Mutex;
use crate::{
codex::accounts::models::{
catalog::{ModelCatalog, ModelPlanSnapshot},
repository::ModelSnapshotRepository,
},
codex::accounts::{
model::{Account, AccountStatus},
pool::AccountPool,
repository::AccountRepository,
},
codex::gateway::fingerprint::model::Fingerprint,
codex::gateway::transport::client::{
codex::gateway::transport::http_client::{
build_reqwest_client, CodexBackendClient, CodexRequestContext,
},
codex::models::{
catalog::{ModelCatalog, ModelPlanSnapshot},
repository::ModelSnapshotRepository,
},
config::AppConfig,
};
+3 -3
View File
@@ -12,15 +12,15 @@ use axum::{
use serde_json::json;
use crate::{
codex::accounts::models::catalog::ModelCatalog,
codex::accounts::models::service::ModelService,
codex::events::event::EventLevel,
codex::gateway::protocol::{
codex_to_openai::{
chat_completion_from_codex_sse, chat_completion_stream_from_codex_sse, openai_error,
},
openai_to_codex::{translate_chat_to_codex, ChatCompletionRequest},
},
codex::logs::event::EventLevel,
codex::models::catalog::ModelCatalog,
codex::models::service::ModelService,
codex::serving::dispatch::{
classify_upstream_account_retry, no_available_accounts_response,
normalize_service_tier_for_upstream, CodexRequestLogContext, CodexUpstreamService,
+1 -1
View File
@@ -6,7 +6,7 @@ use serde::Serialize;
use crate::{
codex::gateway::{
fingerprint::model::Fingerprint,
transport::client::{
transport::http_client::{
build_reqwest_client, CodexBackendClient, CodexClientError, CodexRequestContext,
},
},
@@ -7,9 +7,9 @@ use crate::{
model::{Account, AccountStatus},
repository::TokenUpdate,
},
codex::events::event::{EventLevel, EventLog},
codex::gateway::oauth::{RefreshFailure, TokenPair},
codex::gateway::transport::types::CodexResponsesRequest,
codex::logs::event::{EventLevel, EventLog},
};
use super::CodexUpstreamDependencies;
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::codex::{
model::{Account, AccountStatus},
pool::AccountAcquireRequest,
},
gateway::transport::{client::CodexClientError, rate_limits::cooldown_with_jitter},
gateway::transport::{http_client::CodexClientError, rate_limits::cooldown_with_jitter},
};
use super::{usage::record_request_attempt, CodexUpstreamDependencies};
+15 -15
View File
@@ -1,11 +1,11 @@
mod account_refresh;
pub mod affinity;
mod audit;
pub mod fallback;
mod limits;
pub mod refresh;
// 上游调度辅助命名为 routing,避免出现 dispatch::dispatch 的模块套娃。
pub mod routing;
pub mod stream;
mod stream_audit;
pub mod usage;
use std::{sync::Arc, time::Instant};
@@ -31,26 +31,26 @@ use crate::{
pool::{AccountAcquireRequest, AccountPool},
repository::AccountRepository,
},
codex::events::{
event::{EventLevel, EventLog},
service::LogService,
},
codex::gateway::conversation_identity::{build_conversation_identity, ensure_prompt_cache_key},
codex::gateway::fingerprint::model::Fingerprint,
codex::gateway::identity::{build_conversation_identity, ensure_prompt_cache_key},
codex::gateway::installation::get_installation_id,
codex::gateway::installation_id::get_installation_id,
codex::gateway::oauth::TokenRefresher,
codex::gateway::protocol::codex_to_openai::openai_error,
codex::gateway::transport::{
client::{
http_client::{
build_reqwest_client, CodexBackendClient, CodexBackendStream,
CodexBackendWebSocketStream, CodexClientError, CodexRequestContext,
},
types::CodexResponsesRequest,
usage::TokenUsage,
usage_events::TokenUsage,
websocket::{
transport_for_request, CodexTransport, CodexWebSocketError, CodexWebSocketPool,
},
},
codex::logs::{
event::{EventLevel, EventLog},
service::LogService,
},
config::AppConfig,
};
@@ -65,13 +65,13 @@ pub(crate) use self::{
};
use self::affinity::{compute_variant_hash, SessionAffinityMap, SessionAffinityRepository};
use self::audit::{StreamAudit, WebSocketStreamAudit};
use self::fallback::{
apply_upstream_account_retry_with_deps, apply_upstream_retry_and_acquire_fallback_with_deps,
};
use self::stream_audit::{StreamAudit, WebSocketStreamAudit};
use self::{
account_refresh::refresh_account_after_unauthorized,
limits::apply_rate_limit_headers_with_deps,
refresh::refresh_account_after_unauthorized,
routing::request_domain,
stream::{completed_response_metadata, ensure_stream_metadata},
usage::{record_empty_response_with_deps, record_usage_with_deps},
@@ -200,7 +200,7 @@ impl CodexUpstreamService {
request: &CodexResponsesRequest,
account: &Account,
request_id: &str,
) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError>
) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError>
{
send_codex_request_with_refresh_retry_deps(&self.deps, request, account, request_id).await
}
@@ -466,7 +466,7 @@ async fn send_codex_request_with_refresh_retry_deps(
request: &CodexResponsesRequest,
account: &Account,
request_id: &str,
) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError> {
) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError> {
let result = match send_codex_request(deps, request, account, request_id).await {
Err(CodexClientError::Upstream {
status,
@@ -503,7 +503,7 @@ async fn send_codex_request(
request: &CodexResponsesRequest,
account: &Account,
request_id: &str,
) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError> {
) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError> {
let request_domain = request_domain(&deps.config.api.base_url);
let cookie_header = match (deps.cookie_repository.as_ref(), request_domain.as_deref()) {
(Some(repo), Some(domain)) => repo.cookie_header(&account.id, domain).await.ok().flatten(),
@@ -8,11 +8,11 @@ use crate::{
codex::gateway::transport::types::CodexResponsesRequest,
codex::{
accounts::pool::AccountPool,
events::event::EventLevel,
gateway::transport::{
usage::extract_sse_usage,
usage_events::extract_sse_usage,
websocket::{append_rate_limit_updates, CodexWebSocketError, SharedRateLimitUpdates},
},
logs::event::EventLevel,
},
};
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::{
codex::accounts::repository::UsageDelta, codex::gateway::transport::usage::TokenUsage,
codex::accounts::repository::UsageDelta, codex::gateway::transport::usage_events::TokenUsage,
};
use super::CodexUpstreamDependencies;
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::{
codex::gateway::protocol::{
codex_to_openai::openai_error, openai_to_codex::ChatCompletionRequest,
},
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
};
+2 -2
View File
@@ -10,10 +10,10 @@ use serde::Serialize;
use serde_json::json;
use crate::{
admin::auth::service::AdminAuthPoolSummary,
admin::session::service::AdminAuthPoolSummary,
codex::{accounts::pool::AccountCapacitySummary, gateway::fingerprint::model::Fingerprint},
config::AppConfig,
platform::http::middleware::RequestId,
platform::http::request_id::RequestId,
runtime::state::AppState,
};
+1 -1
View File
@@ -3,7 +3,7 @@ use serde_json::Value;
use crate::{
codex::gateway::protocol::codex_to_openai::openai_error,
codex::gateway::transport::client::CodexClientError,
codex::gateway::transport::http_client::CodexClientError,
};
pub(crate) fn codex_client_error_response(error: CodexClientError) -> (StatusCode, Json<Value>) {
+1 -1
View File
@@ -6,7 +6,7 @@ use axum::{
};
use serde_json::{json, Value};
use crate::{codex::accounts::models::catalog::ModelCatalog, runtime::state::AppState};
use crate::{codex::models::catalog::ModelCatalog, runtime::state::AppState};
use super::{
auth::authorize_client_api_key,
+1 -1
View File
@@ -8,7 +8,7 @@ use axum::{
Extension,
};
use crate::{platform::http::middleware::RequestId, runtime::state::AppState};
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
use super::{auth::authorize_client_api_key, errors::missing_client_api_key_response};
+2 -2
View File
@@ -10,11 +10,11 @@ use serde::Deserialize;
use serde_json::{json, Map, Value};
use crate::{
codex::accounts::models::service::ModelService,
codex::events::event::EventLevel,
codex::gateway::fingerprint::model::Fingerprint,
codex::gateway::protocol::codex_to_openai::openai_error,
codex::gateway::transport::types::CodexResponsesRequest,
codex::logs::event::EventLevel,
codex::models::service::ModelService,
codex::serving::dispatch::{
affinity::SessionAffinityRepositoryResult, classify_upstream_account_retry,
completed_response_json, no_available_accounts_response,

Some files were not shown because too many files have changed in this diff Show More