Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/architecture/cliproxy-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ CPA 调整对外约定时,路径后缀与默认路由能力的改动集中在

## 转发路径中的 CPA 分支

CPA 上游在请求生命周期里只有一处特殊处理,即单账号映射上游的模型前缀注入,发生在 `src/app/api/proxy/v1/[...path]/route.ts:1519-1532`
CPA 上游在请求生命周期里只有一处特殊处理,即单账号映射上游的模型前缀注入,发生在 `src/app/api/proxy/v1/[...path]/proxy-execution.ts` 的 `forwardWithFailover` 上游调用前

```ts
let cliproxyModelOverride: string | undefined;
Expand Down
27 changes: 8 additions & 19 deletions docs/guide/architecture/failover-circuit.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,11 @@ AutoRouter 把「上游会失败」当作常态。一次客户端请求可能触

## 单次请求内的故障转移循环

入口函数 `forwardWithFailover`,源码 `src/app/api/proxy/v1/[...path]/route.ts:1295-1760`。签名:
入口函数 `forwardWithFailover`,源码 `src/app/api/proxy/v1/[...path]/proxy-execution.ts`。签名:

```ts
// route.ts:1295-1320(节选)
async function forwardWithFailover(
request,
routeCapability,
path,
requestId,
candidateUpstreamIds: string[],
requestModel,
affinityContext,
compensationHeaders,
onQueueStateChange?,
config: FailoverConfig = DEFAULT_FAILOVER_CONFIG
);
// proxy-execution.ts
async function forwardWithFailover(input: ProxyExecutionInput): Promise<ProxyExecutionResult>;
```

默认配置在 `src/lib/services/failover-config.ts:44-48`:
Expand All @@ -94,7 +83,7 @@ export const DEFAULT_FAILOVER_CONFIG: FailoverConfig = {

主循环每一轮做三件事:

1. 调用 `selectFromUpstreamCandidates(candidateUpstreamIds, failedUpstreamIds, affinityContext)`,把已经失败的上游排除(`route.ts:1371` 维护 `failedUpstreamIds` 数组)
1. 调用 `selectFromUpstreamCandidates(candidateUpstreamIds, failedUpstreamIds, affinityContext)`,把已经失败的上游排除;
2. 调用 `forwardRequest(...)` 实际转发;
3. 根据结果决定下一步:
- 成功 → `markHealthy` + `recordSuccess` + 返回响应
Expand All @@ -105,7 +94,7 @@ export const DEFAULT_FAILOVER_CONFIG: FailoverConfig = {

代理层把两类错误判定为可故障转移:

**异常类(`isFailoverableError`,`route.ts:844-869`)**:
**异常类(`isFailoverableError`,`proxy-execution.ts`)**:

- `CircuitBreakerOpenError`
- `FirstByteTimeoutError` / `StreamIdleTimeoutError` / `UpstreamNoContentStreamError`
Expand All @@ -117,7 +106,7 @@ export const DEFAULT_FAILOVER_CONFIG: FailoverConfig = {

- 状态码非 2xx 且不在 `excludeStatusCodes` 中

默认 `excludeStatusCodes` 为空数组,意味着**所有 4xx(包括 401 / 403 / 404 / 429)都会触发故障转移**。`getErrorType()` 会区分 `http_429` 和通用 `http_4xx`(`route.ts:829-830`),但并不影响是否触发转移。如果不希望客户端的 401 把所有上游试一遍,需要在 `FailoverConfig.excludeStatusCodes` 里配置 `[401, 403]` 等。
默认 `excludeStatusCodes` 为空数组,意味着**所有 4xx(包括 401 / 403 / 404 / 429)都会触发故障转移**。`getErrorType()` 会区分 `http_429` 和通用 `http_4xx`(`proxy-execution.ts`),但并不影响是否触发转移。如果不希望客户端的 401 把所有上游试一遍,需要在 `FailoverConfig.excludeStatusCodes` 里配置 `[401, 403]` 等。

### 失败是否记入熔断器:FailureRule

Expand All @@ -130,13 +119,13 @@ export const DEFAULT_FAILOVER_CONFIG: FailoverConfig = {
| `bodyPattern` | 响应体正则 |
| `headerName` + `headerPattern` | 响应头名 + 值正则 |

源码 `src/lib/services/upstream-failure-rules.ts:12-18`。当 `matchFailureRule()` 命中一条规则时,本次失败仍然会触发故障转移,但 `circuitBreakerRecorded = false`(`route.ts:1555-1556, 1714-1715`),不写入 `circuit_breaker_states.failure_count`。
源码 `src/lib/services/upstream-failure-rules.ts:12-18`。当 `matchFailureRule()` 命中一条规则时,本次失败仍然会触发故障转移,但 `circuitBreakerRecorded = false`(`proxy-execution.ts`),不写入 `circuit_breaker_states.failure_count`。

典型用法:上游对应 OAuth 受控的 CLIProxyAPI auth-file,正常会偶发 401 触发后台 refresh,不希望把上游打到熔断;可以加一条 `statusCodes: [401], bodyPattern: "token expired"` 的规则。上游层 `upstreams.failure_rule_config.useGlobalRules`(默认 `true`)控制是否同时参与全局规则匹配(`upstream-failure-rules.ts:353`)。

### 并发已满与队列等待

当 `selectFromUpstreamCandidates` 抛出 `AllCandidatesConcurrencyFullError` 并携带 `waitableCandidate` 时,主循环不会立即返回失败,而是调用 `resumeQueuedUpstreamSelection`(`route.ts:1409-1452`),内部通过 `upstreamQueueAdmission` 等待该上游的并发槽位释放。等待时长由 `upstream.queue_policy` 控制,超时会抛 `UpstreamQueueWaitTimeoutError`,此时不再尝试其他上游,直接返回 503 / 504。
当 `selectFromUpstreamCandidates` 抛出 `AllCandidatesConcurrencyFullError` 并携带 `waitableCandidate` 时,主循环不会立即返回失败,而是调用 `resumeQueuedUpstreamSelection`(`proxy-execution.ts`),内部通过 `upstreamQueueAdmission` 等待该上游的并发槽位释放。等待时长由 `upstream.queue_policy` 控制,超时会抛 `UpstreamQueueWaitTimeoutError`,此时不再尝试其他上游,直接返回 503 / 504。

### 故障转移决策日志

Expand Down
32 changes: 16 additions & 16 deletions docs/guide/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,21 @@ AutoRouter 是一个 Next.js 全栈应用:同一个进程同时承担「管理

代码组织遵循 Next.js App Router 的常规分层,运行期逻辑集中在 `src/lib/services/`:

| 路径 | 职责 |
| ----------------------------------------- | --------------------------------------------------------------------------- |
| `src/app/api/proxy/v1/[...path]/route.ts` | 唯一的代理入口,GET/POST/PUT/DELETE/PATCH 都委托给同一个 `handleProxy` 函数 |
| `src/app/api/admin/` | 管理 API:上游、密钥、熔断、日志、统计、计费、流量录制、CLIProxy 等 |
| `src/app/api/health/route.ts` | 公开健康探针,不需要鉴权 |
| `src/app/[locale]/(dashboard)/` | 管理后台页面集合(需要登录) |
| `src/app/[locale]/(auth)/login/` | 登录页(独立布局,不挂 dashboard 框架) |
| `src/lib/services/` | 全部运行期业务逻辑模块 |
| `src/lib/db/` | Drizzle ORM schema 与数据库 client |
| `src/lib/utils/` | 通用工具:配置加载、鉴权 helper、加密、CORS 等 |
| `src/components/` | 管理后台 React 组件(shadcn/ui 基础) |
| `src/hooks/` | TanStack Query 包装的数据获取 hooks |
| `src/i18n/`、`src/messages/` | next-intl 配置与中英文翻译 |

`src/app/api/proxy/v1/[...path]/route.ts` 在文件末尾把所有 HTTP 方法都导向同一个内部函数(`POST` 位于第 4147 行、`handleProxy` 位于第 2440 行),后文「请求生命周期」会逐步展开它的内部流程。
| 路径 | 职责 |
| ----------------------------------------- | ------------------------------------------------------------------------------------- |
| `src/app/api/proxy/v1/[...path]/route.ts` | 唯一的代理入口,GET/POST/PUT/DELETE/PATCH 都委托给 `executeProxyRequest` 生命周期入口 |
| `src/app/api/admin/` | 管理 API:上游、密钥、熔断、日志、统计、计费、流量录制、CLIProxy 等 |
| `src/app/api/health/route.ts` | 公开健康探针,不需要鉴权 |
| `src/app/[locale]/(dashboard)/` | 管理后台页面集合(需要登录) |
| `src/app/[locale]/(auth)/login/` | 登录页(独立布局,不挂 dashboard 框架) |
| `src/lib/services/` | 全部运行期业务逻辑模块 |
| `src/lib/db/` | Drizzle ORM schema 与数据库 client |
| `src/lib/utils/` | 通用工具:配置加载、鉴权 helper、加密、CORS 等 |
| `src/components/` | 管理后台 React 组件(shadcn/ui 基础) |
| `src/hooks/` | TanStack Query 包装的数据获取 hooks |
| `src/i18n/`、`src/messages/` | next-intl 配置与中英文翻译 |

`src/app/api/proxy/v1/[...path]/route.ts` 在文件末尾把所有 HTTP 方法都导向同一个生命周期入口(`executeProxyRequest` 位于 `proxy-request-lifecycle.ts`),后文「请求生命周期」会逐步展开它的内部流程。

## 服务模块清单

Expand Down Expand Up @@ -171,7 +171,7 @@ AutoRouter 是一个 Next.js 全栈应用:同一个进程同时承担「管理
| `/api/health` | 无 | 健康探针 |
| `/[locale]/...` 页面 | 浏览器侧 sessionStorage Token | 管理后台 UI |

代理入口的全部 HTTP 方法都委托给 `handleProxy`;管理 API 的每个路由独立鉴权;健康探针完全公开。next-intl 中间件位于 `src/proxy.ts`(注意:是 `src/proxy.ts`,不是 Next.js 默认惯用的 `src/middleware.ts`),其 matcher 显式排除 `/_next`、`/api`、带扩展名的资源路径,因此中间件**不会**拦截任何 API 请求,所有 API 鉴权都发生在 route handler 自身内部。
代理入口的全部 HTTP 方法都委托给 `executeProxyRequest`;管理 API 的每个路由独立鉴权;健康探针完全公开。next-intl 中间件位于 `src/proxy.ts`(注意:是 `src/proxy.ts`,不是 Next.js 默认惯用的 `src/middleware.ts`),其 matcher 显式排除 `/_next`、`/api`、带扩展名的资源路径,因此中间件**不会**拦截任何 API 请求,所有 API 鉴权都发生在 route handler 自身内部。

## 国际化与路由分组

Expand Down
13 changes: 7 additions & 6 deletions docs/guide/architecture/request-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ outline: deep

# 请求生命周期

这一页跟踪一次客户端请求从进入 AutoRouter、完成鉴权与上游准入、发送到上游,再到响应、日志、计费和流量录制落地的完整流程。代理请求现在由三个边界清晰的模块协作:`src/app/api/proxy/v1/[...path]/route.ts` 只负责 HTTP 方法与参数适配,`proxy-request-lifecycle.ts` 的 `handleProxy` 负责生命周期编排,`proxy-execution.ts` 的 `forwardWithFailover` 负责候选选择、队列准入、上游调用、失败转移和资源释放
这一页跟踪一次客户端请求从进入 AutoRouter、完成鉴权与上游准入、发送到上游,再到响应、日志、计费和流量录制落地的完整流程。代理请求由多个边界清晰的模块协作:`route.ts` 只负责 HTTP 方法与参数适配,`proxy-request-lifecycle.ts` 的 `executeProxyRequest` 负责生命周期编排,`proxy-execution.ts` 的 `forwardWithFailover` 负责候选选择、队列准入、上游调用与失败转移,`proxy-non-stream-lifecycle.ts` / `proxy-stream-lifecycle.ts` 负责终态响应和日志、计费、录制收口

示例以最常见的 `POST /api/proxy/v1/chat/completions` 为基准,其他协议(Anthropic `/v1/messages`、Gemini `/v1beta/models/<model>:generateContent`、OpenAI `/v1/responses` 等)的差异在相应阶段标出。

Expand All @@ -17,11 +17,12 @@ outline: deep

```ts
export async function POST(request: NextRequest, context: RouteContext) {
return handleProxy(request, context);
const { path } = await context.params;
return executeProxyRequest(request, path.join("/"));
}
```

`route.ts` 不再直接编排鉴权、路由、上游调用、日志、计费或 recording。阅读代理行为时,以 `proxy-request-lifecycle.ts` 的 `handleProxy` 为主时序,以 `proxy-execution.ts` 的 `forwardWithFailover` 为上游执行子流程。
`route.ts` 不再直接编排鉴权、路由、上游调用、日志、计费或 recording。阅读代理行为时,以 `proxy-request-lifecycle.ts` 的 `executeProxyRequest` 为主时序,以 `proxy-execution.ts` 的 `forwardWithFailover` 为上游执行子流程,并在 `proxy-non-stream-lifecycle.ts` 与 `proxy-stream-lifecycle.ts` 查看终态副作用

## 阶段二:CORS 与 OPTIONS

Expand All @@ -35,7 +36,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
2. `x-api-key`:Anthropic SDK 的默认 header。
3. `x-goog-api-key`:Gemini SDK 的默认 header。

提取后,`handleProxy` 按 key prefix 找候选记录并用 `verifyApiKey` 做 bcrypt 比对,再检查过期与用户状态。
提取后,`executeProxyRequest` 按 key prefix 找候选记录并用 `verifyApiKey` 做 bcrypt 比对,再检查过期与用户状态。

| 场景 | HTTP 响应 | 说明 |
| ------------------------ | ---------------------------------------- | ---------------------------- |
Expand Down Expand Up @@ -68,7 +69,7 @@ export async function POST(request: NextRequest, context: RouteContext) {

## 阶段五:候选过滤与上游选路

`handleProxy` 先读取活跃上游快照,再根据 Key 的 `accessMode` 构建候选集合:
`executeProxyRequest` 先读取活跃上游快照,再根据 Key 的 `accessMode` 构建候选集合:

- `restricted`:只允许 `apiKeyUpstreams` 关联表中的上游。
- `unrestricted`:允许所有活跃上游,但仍受 capability、model rule、健康和熔断状态限制。
Expand Down Expand Up @@ -169,7 +170,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
[2] CORS / OPTIONS(当前没有自定义 preflight handler)
[3] handleProxy 鉴权
[3] executeProxyRequest 鉴权
├ 缺失 / 无效 / 过期 / disabled key → 401
└ 记录拒绝日志,不访问上游
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/architecture/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const keyValueEncrypted = encrypt(keyValue); // Fernet

### 转发时的验证

代理路由 `src/app/api/proxy/v1/[...path]/route.ts:2452-2473` 用前缀查候选行,再对候选逐条 bcrypt 比对:
代理请求生命周期 `src/app/api/proxy/v1/[...path]/proxy-request-lifecycle.ts` 的 `executeProxyRequest` 用前缀查候选行,再对候选逐条 bcrypt 比对:

```ts
const keyPrefix = getKeyPrefix(keyValue);
Expand Down
Loading
Loading