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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ STORE_SESSION_RESPONSE_BODY=true # 是否在 Redis 中存储会话响应
# - 启用:适用于网络稳定环境,连续网络错误也应触发熔断保护,避免持续请求不可达的供应商
ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS=false

# 端点级别熔断器
# 功能说明:控制是否启用端点级别的熔断器
# - false (默认):禁用端点熔断器,所有启用的端点均可使用
# - true:启用端点熔断器,连续失败的端点会被临时屏蔽(默认 3 次失败后熔断 5 分钟)
ENABLE_ENDPOINT_CIRCUIT_BREAKER=false

# 供应商缓存配置
# 功能说明:控制是否启用供应商进程级缓存
# - true (默认):启用缓存,30s TTL + Redis Pub/Sub 跨实例即时失效,提升供应商查询性能
Expand Down
16 changes: 13 additions & 3 deletions messages/en/provider-chain.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"concurrentLimit": "Concurrent Limit",
"http2Fallback": "HTTP/2 Fallback",
"clientError": "Client Error",
"endpointPoolExhausted": "Endpoint Pool Exhausted"
"endpointPoolExhausted": "Endpoint Pool Exhausted",
"vendorTypeAllTimeout": "Vendor-Type All Endpoints Timeout"
},
"reasons": {
"request_success": "Success",
Expand All @@ -50,7 +51,8 @@
"http2_fallback": "HTTP/2 Fallback",
"session_reuse": "Session Reuse",
"initial_selection": "Initial Selection",
"endpoint_pool_exhausted": "Endpoint Pool Exhausted"
"endpoint_pool_exhausted": "Endpoint Pool Exhausted",
"vendor_type_all_timeout": "Vendor-Type All Endpoints Timeout"
},
"filterReasons": {
"rate_limited": "Rate Limited",
Expand All @@ -67,6 +69,12 @@
"endpoint_circuit_open": "Endpoint Circuit Open",
"endpoint_disabled": "Endpoint Disabled"
},
"filterDetails": {
"vendor_type_circuit_open": "Vendor-type temporarily circuit-broken",
"circuit_open": "Circuit breaker open",
"circuit_half_open": "Circuit breaker half-open",
"rate_limited": "Rate limited"
},
"details": {
"selectionMethod": "Selection",
"attemptNumber": "Attempt",
Expand Down Expand Up @@ -197,6 +205,8 @@
"endpointStatsCircuitOpen": "Circuit-Open Endpoints: {count}",
"endpointStatsAvailable": "Available Endpoints: {count}",
"strictBlockNoEndpoints": "Strict mode: no endpoint candidates available, provider skipped without fallback",
"strictBlockSelectorError": "Strict mode: endpoint selector encountered an error, provider skipped without fallback"
"strictBlockSelectorError": "Strict mode: endpoint selector encountered an error, provider skipped without fallback",
"vendorTypeAllTimeout": "Vendor-Type All Endpoints Timeout (524)",
"vendorTypeAllTimeoutNote": "All endpoints for this vendor-type timed out. Vendor-type circuit breaker triggered."
}
}
16 changes: 13 additions & 3 deletions messages/ja/provider-chain.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"concurrentLimit": "同時実行制限",
"http2Fallback": "HTTP/2 フォールバック",
"clientError": "クライアントエラー",
"endpointPoolExhausted": "エンドポイントプール枯渇"
"endpointPoolExhausted": "エンドポイントプール枯渇",
"vendorTypeAllTimeout": "ベンダータイプ全エンドポイントタイムアウト"
},
"reasons": {
"request_success": "成功",
Expand All @@ -50,7 +51,8 @@
"http2_fallback": "HTTP/2 フォールバック",
"session_reuse": "セッション再利用",
"initial_selection": "初期選択",
"endpoint_pool_exhausted": "エンドポイントプール枯渇"
"endpoint_pool_exhausted": "エンドポイントプール枯渇",
"vendor_type_all_timeout": "ベンダータイプ全エンドポイントタイムアウト"
},
"filterReasons": {
"rate_limited": "レート制限",
Expand All @@ -67,6 +69,12 @@
"endpoint_circuit_open": "エンドポイントサーキットオープン",
"endpoint_disabled": "エンドポイント無効"
},
"filterDetails": {
"vendor_type_circuit_open": "ベンダータイプ一時サーキットブレイク",
"circuit_open": "サーキットブレーカーオープン",
"circuit_half_open": "サーキットブレーカーハーフオープン",
"rate_limited": "レート制限"
},
"details": {
"selectionMethod": "選択方法",
"attemptNumber": "試行回数",
Expand Down Expand Up @@ -197,6 +205,8 @@
"endpointStatsCircuitOpen": "サーキットオープンのエンドポイント: {count}",
"endpointStatsAvailable": "利用可能なエンドポイント: {count}",
"strictBlockNoEndpoints": "厳格モード:利用可能なエンドポイント候補がないため、フォールバックなしでプロバイダーをスキップ",
"strictBlockSelectorError": "厳格モード:エンドポイントセレクターでエラーが発生したため、フォールバックなしでプロバイダーをスキップ"
"strictBlockSelectorError": "厳格モード:エンドポイントセレクターでエラーが発生したため、フォールバックなしでプロバイダーをスキップ",
"vendorTypeAllTimeout": "ベンダータイプ全エンドポイントタイムアウト(524)",
"vendorTypeAllTimeoutNote": "このベンダータイプの全エンドポイントがタイムアウトしました。ベンダータイプサーキットブレーカーが発動しました。"
}
}
30 changes: 20 additions & 10 deletions messages/ru/provider-chain.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"concurrentLimit": "Лимит параллельных запросов",
"http2Fallback": "Откат HTTP/2",
"clientError": "Ошибка клиента",
"endpointPoolExhausted": "Пул конечная точкаов исчерпан"
"endpointPoolExhausted": "Пул конечных точек исчерпан",
"vendorTypeAllTimeout": "Тайм-аут всех конечных точек"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

description.vendorTypeAllTimeout 缺少"типа поставщика"(供应商类型),与其他 locale 不一致。

对比同文件第 55 行 reasons.vendor_type_all_timeout("Тайм-аут всех конечных точек типа поставщика")和第 209 行 timeline.vendorTypeAllTimeout(同样包含 "типа поставщика"),以及 zh-TW 第 42 行("供應商類型全端點逾時"),此处第 42 行的 description.vendorTypeAllTimeout 仅为 "Тайм-аут всех конечных точек",缺少"供应商类型"的限定。

建议修复
-    "vendorTypeAllTimeout": "Тайм-аут всех конечных точек"
+    "vendorTypeAllTimeout": "Тайм-аут всех конечных точек типа поставщика"
🤖 Prompt for AI Agents
In `@messages/ru/provider-chain.json` at line 42, The string for
description.vendorTypeAllTimeout is missing the "типа поставщика" qualifier and
should match other entries (reasons.vendor_type_all_timeout and
timeline.vendorTypeAllTimeout); update the value of
description.vendorTypeAllTimeout to include "типа поставщика" so it reads the
same as the other locales and entries (e.g., "Тайм-аут всех конечных точек типа
поставщика").

},
"reasons": {
"request_success": "Успешно",
Expand All @@ -50,7 +51,8 @@
"http2_fallback": "Откат HTTP/2",
"session_reuse": "Повторное использование сессии",
"initial_selection": "Первоначальный выбор",
"endpoint_pool_exhausted": "Пул конечная точкаов исчерпан"
"endpoint_pool_exhausted": "Пул конечных точек исчерпан",
"vendor_type_all_timeout": "Тайм-аут всех конечных точек типа поставщика"
},
"filterReasons": {
"rate_limited": "Ограничение скорости",
Expand All @@ -64,9 +66,15 @@
"model_not_supported": "Модель не поддерживается",
"group_mismatch": "Несоответствие группы",
"health_check_failed": "Проверка состояния не пройдена",
"endpoint_circuit_open": "Автомат конечная точкаа открыт",
"endpoint_circuit_open": "Автомат конечной точки открыт",
"endpoint_disabled": "Эндпоинт отключен"
},
"filterDetails": {
"vendor_type_circuit_open": "Временное размыкание типа поставщика",
"circuit_open": "Размыкатель открыт",
"circuit_half_open": "Размыкатель полуоткрыт",
"rate_limited": "Ограничение скорости"
},
"details": {
"selectionMethod": "Метод выбора",
"attemptNumber": "Номер попытки",
Expand Down Expand Up @@ -190,13 +198,15 @@
"ruleDescription": "Описание: {description}",
"ruleHasOverride": "Переопределения: response={response}, statusCode={statusCode}",
"clientErrorNote": "Эта ошибка вызвана вводом клиента, не повторяется и не учитывается в автомате защиты.",
"endpointPoolExhausted": "Пул конечная точкаов исчерпан (все конечная точкаы недоступны)",
"endpointStats": "Статистика фильтрации конечная точкаов",
"endpointStatsTotal": "Всего конечная точкаов: {count}",
"endpointStatsEnabled": "Включено конечная точкаов: {count}",
"endpointPoolExhausted": "Пул конечных точек исчерпан (все конечные точки недоступны)",
"endpointStats": "Статистика фильтрации конечных точек",
"endpointStatsTotal": "Всего конечных точек: {count}",
"endpointStatsEnabled": "Включено конечных точек: {count}",
"endpointStatsCircuitOpen": "Эндпоинтов с открытым автоматом: {count}",
"endpointStatsAvailable": "Доступных конечная точкаов: {count}",
"strictBlockNoEndpoints": "Строгий режим: нет доступных кандидатов конечная точкаов, провайдер пропущен без отката",
"strictBlockSelectorError": "Строгий режим: ошибка селектора конечная точкаов, провайдер пропущен без отката"
"endpointStatsAvailable": "Доступных конечных точек: {count}",
"strictBlockNoEndpoints": "Строгий режим: нет доступных кандидатов конечных точек, провайдер пропущен без отката",
"strictBlockSelectorError": "Строгий режим: ошибка селектора конечных точек, провайдер пропущен без отката",
"vendorTypeAllTimeout": "Тайм-аут всех конечных точек типа поставщика (524)",
"vendorTypeAllTimeoutNote": "Все конечные точки этого типа поставщика превысили тайм-аут. Активирован размыкатель типа поставщика."
}
}
16 changes: 13 additions & 3 deletions messages/zh-CN/provider-chain.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"concurrentLimit": "并发限制",
"http2Fallback": "HTTP/2 回退",
"clientError": "客户端错误",
"endpointPoolExhausted": "端点池耗尽"
"endpointPoolExhausted": "端点池耗尽",
"vendorTypeAllTimeout": "供应商类型全端点超时"
},
"reasons": {
"request_success": "成功",
Expand All @@ -50,7 +51,8 @@
"http2_fallback": "HTTP/2 回退",
"session_reuse": "会话复用",
"initial_selection": "首次选择",
"endpoint_pool_exhausted": "端点池耗尽"
"endpoint_pool_exhausted": "端点池耗尽",
"vendor_type_all_timeout": "供应商类型全端点超时"
},
"filterReasons": {
"rate_limited": "速率限制",
Expand All @@ -67,6 +69,12 @@
"endpoint_circuit_open": "端点已熔断",
"endpoint_disabled": "端点已禁用"
},
"filterDetails": {
"vendor_type_circuit_open": "供应商类型临时熔断",
"circuit_open": "熔断器打开",
"circuit_half_open": "熔断器半开",
"rate_limited": "速率限制"
},
"details": {
"selectionMethod": "选择方式",
"attemptNumber": "尝试次数",
Expand Down Expand Up @@ -197,6 +205,8 @@
"endpointStatsCircuitOpen": "已熔断端点: {count}",
"endpointStatsAvailable": "可用端点: {count}",
"strictBlockNoEndpoints": "严格模式:无可用端点候选,跳过该供应商且不降级",
"strictBlockSelectorError": "严格模式:端点选择器发生错误,跳过该供应商且不降级"
"strictBlockSelectorError": "严格模式:端点选择器发生错误,跳过该供应商且不降级",
"vendorTypeAllTimeout": "供应商类型全端点超时(524)",
"vendorTypeAllTimeoutNote": "该供应商类型的所有端点均超时,已触发供应商类型临时熔断。"
}
}
16 changes: 13 additions & 3 deletions messages/zh-TW/provider-chain.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"concurrentLimit": "並發限制",
"http2Fallback": "HTTP/2 回退",
"clientError": "客戶端錯誤",
"endpointPoolExhausted": "端點池耗盡"
"endpointPoolExhausted": "端點池耗盡",
"vendorTypeAllTimeout": "供應商類型全端點逾時"
},
"reasons": {
"request_success": "成功",
Expand All @@ -50,7 +51,8 @@
"http2_fallback": "HTTP/2 回退",
"session_reuse": "會話複用",
"initial_selection": "首次選擇",
"endpoint_pool_exhausted": "端點池耗盡"
"endpoint_pool_exhausted": "端點池耗盡",
"vendor_type_all_timeout": "供應商類型全端點逾時"
},
"filterReasons": {
"rate_limited": "速率限制",
Expand All @@ -67,6 +69,12 @@
"endpoint_circuit_open": "端點已熔斷",
"endpoint_disabled": "端點已停用"
},
"filterDetails": {
"vendor_type_circuit_open": "供應商類型臨時熔斷",
"circuit_open": "熔斷器打開",
"circuit_half_open": "熔斷器半開",
"rate_limited": "速率限制"
},
"details": {
"selectionMethod": "選擇方式",
"attemptNumber": "嘗試次數",
Expand Down Expand Up @@ -197,6 +205,8 @@
"endpointStatsCircuitOpen": "已熔斷端點: {count}",
"endpointStatsAvailable": "可用端點: {count}",
"strictBlockNoEndpoints": "嚴格模式:無可用端點候選,跳過該供應商且不降級",
"strictBlockSelectorError": "嚴格模式:端點選擇器發生錯誤,跳過該供應商且不降級"
"strictBlockSelectorError": "嚴格模式:端點選擇器發生錯誤,跳過該供應商且不降級",
"vendorTypeAllTimeout": "供應商類型全端點逾時(524)",
"vendorTypeAllTimeoutNote": "該供應商類型的所有端點均逾時,已觸發供應商類型臨時熔斷。"
}
}
1 change: 1 addition & 0 deletions scripts/deploy.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ ENABLE_SECURE_COOKIES=$secureCookies

# Circuit Breaker Configuration
ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS=false
ENABLE_ENDPOINT_CIRCUIT_BREAKER=false

# Environment
NODE_ENV=production
Expand Down
1 change: 1 addition & 0 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ ENABLE_SECURE_COOKIES=${secure_cookies}

# Circuit Breaker Configuration
ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS=false
ENABLE_ENDPOINT_CIRCUIT_BREAKER=false

# Environment
NODE_ENV=production
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,13 @@ export function LogicTraceTab({
{tChain(`filterReasons.${p.reason}`)}
</span>
{p.details && (
<span className="text-muted-foreground break-all">({p.details})</span>
<span className="text-muted-foreground break-all">
(
{tChain.has(`filterDetails.${p.details}`)
? tChain(`filterDetails.${p.details}`)
: p.details}
)
</span>
)}
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ interface ProviderChainPopoverProps {
function isActualRequest(item: ProviderChainItem): boolean {
if (item.reason === "concurrent_limit_failed") return true;
if (item.reason === "retry_failed" || item.reason === "system_error") return true;
if (item.reason === "endpoint_pool_exhausted") return true;
if (item.reason === "vendor_type_all_timeout") return true;
if (item.reason === "client_error_non_retryable") return true;
if ((item.reason === "request_success" || item.reason === "retry_success") && item.statusCode) {
return true;
}
Expand Down Expand Up @@ -89,6 +92,13 @@ function getItemStatus(item: ProviderChainItem): {
bgColor: "bg-orange-50 dark:bg-orange-950/30",
};
}
if (item.reason === "endpoint_pool_exhausted" || item.reason === "vendor_type_all_timeout") {
return {
icon: XCircle,
color: "text-rose-600",
bgColor: "bg-rose-50 dark:bg-rose-950/30",
};
}
return {
icon: RefreshCw,
color: "text-slate-500",
Expand Down
20 changes: 20 additions & 0 deletions src/app/v1/_lib/proxy/forwarder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,26 @@ export class ProxyForwarder {
allEndpointAttemptsTimedOut &&
currentProvider.providerVendorId
) {
// Record to decision chain BEFORE triggering vendor-type circuit breaker
session.addProviderToChain(currentProvider, {
...endpointAudit,
reason: "vendor_type_all_timeout",
attemptNumber: attemptCount,
statusCode: 524,
errorMessage: errorMessage,
errorDetails: {
provider: {
id: currentProvider.id,
name: currentProvider.name,
statusCode: 524,
statusText: proxyError.message,
upstreamBody: proxyError.upstreamError?.body,
upstreamParsed: proxyError.upstreamError?.parsed,
},
request: buildRequestDetails(session),
},
});

await recordVendorTypeAllEndpointsTimeout(
currentProvider.providerVendorId,
currentProvider.providerType
Expand Down
6 changes: 3 additions & 3 deletions src/app/v1/_lib/proxy/provider-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,7 @@ export class ProxyProviderResolver {
id: p.id,
name: p.name,
reason: "circuit_open",
details: "供应商类型临时熔断",
details: "vendor_type_circuit_open",
});
continue;
}
Expand All @@ -896,14 +896,14 @@ export class ProxyProviderResolver {
id: p.id,
name: p.name,
reason: "circuit_open",
details: `熔断器${state === "open" ? "打开" : "半开"}`,
details: state === "open" ? "circuit_open" : "circuit_half_open",
});
} else {
context.filteredProviders?.push({
id: p.id,
name: p.name,
reason: "rate_limited",
details: "费用限制",
details: "rate_limited",
});
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/app/v1/_lib/proxy/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,8 @@ export class ProxySession {
| "retry_with_cached_instructions" // Codex instructions 智能重试(缓存)
| "client_error_non_retryable" // 不可重试的客户端错误(Prompt 超限、内容过滤、PDF 限制、Thinking 格式)
| "http2_fallback" // HTTP/2 协议错误,回退到 HTTP/1.1(不切换供应商、不计入熔断器)
| "endpoint_pool_exhausted"; // 端点池耗尽(strict endpoint policy 阻止了 fallback)
| "endpoint_pool_exhausted" // 端点池耗尽(strict endpoint policy 阻止了 fallback)
| "vendor_type_all_timeout"; // 供应商类型全端点超时(524),触发 vendor-type 临时熔断
selectionMethod?:
| "session_reuse"
| "weighted_random"
Expand Down
20 changes: 20 additions & 0 deletions src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ export async function register() {
});
}

// 初始化端点熔断器(禁用时清理残留状态)
try {
const { initEndpointCircuitBreaker } = await import("@/lib/endpoint-circuit-breaker");
await initEndpointCircuitBreaker();
} catch (error) {
logger.warn("[Instrumentation] Failed to initialize endpoint circuit breaker", {
error: error instanceof Error ? error.message : String(error),
});
}

try {
const { startEndpointProbeLogCleanup } = await import(
"@/lib/provider-endpoints/probe-log-cleanup"
Expand Down Expand Up @@ -456,6 +466,16 @@ export async function register() {
});
}

// 初始化端点熔断器(禁用时清理残留状态)
try {
const { initEndpointCircuitBreaker } = await import("@/lib/endpoint-circuit-breaker");
await initEndpointCircuitBreaker();
} catch (error) {
logger.warn("[Instrumentation] Failed to initialize endpoint circuit breaker", {
error: error instanceof Error ? error.message : String(error),
});
}

try {
const { startEndpointProbeLogCleanup } = await import(
"@/lib/provider-endpoints/probe-log-cleanup"
Expand Down
Loading
Loading