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
13 changes: 13 additions & 0 deletions messages/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,19 @@
"cancel": "Cancel",
"confirm": "Confirm Delete"
},
"failureThresholdConfirmDialog": {
"title": "Confirm Special Configuration",
"descriptionDisabledPrefix": "You are setting the circuit breaker failure threshold to ",
"descriptionDisabledValue": "0",
"descriptionDisabledMiddle": ", which means ",
"descriptionDisabledAction": "disabling the circuit breaker",
"descriptionDisabledSuffix": ". The provider will not be circuit-broken due to consecutive failures.",
"descriptionHighValuePrefix": "You are setting the circuit breaker failure threshold to ",
"descriptionHighValueSuffix": ", which is a high value and may cause the provider to be circuit-broken only after many failures.",
"confirmQuestion": "Are you sure you want to save this configuration?",
"cancel": "Cancel",
"confirm": "Confirm Save"
},
"errors": {
"invalidUrl": "Please enter a valid API address",
"invalidWebsiteUrl": "Please enter a valid provider website URL",
Expand Down
13 changes: 13 additions & 0 deletions messages/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,19 @@
"cancel": "キャンセル",
"confirm": "削除を確定"
},
"failureThresholdConfirmDialog": {
"title": "特別な設定を確認",
"descriptionDisabledPrefix": "サーキットブレーカーの失敗閾値を",
"descriptionDisabledValue": "0",
"descriptionDisabledMiddle": "に設定しています。これは",
"descriptionDisabledAction": "サーキットブレーカーを無効化",
"descriptionDisabledSuffix": "することを意味し、プロバイダーは連続した失敗によって遮断されません。",
"descriptionHighValuePrefix": "サーキットブレーカーの失敗閾値を",
"descriptionHighValueSuffix": "に設定しています。これは高い値であり、プロバイダーが多数の失敗の後にのみ遮断される可能性があります。",
"confirmQuestion": "この設定を保存してもよろしいですか?",
"cancel": "キャンセル",
"confirm": "保存を確定"
},
"errors": {
"invalidUrl": "有効な API アドレスを入力してください",
"invalidWebsiteUrl": "有効な公式サイト URL を入力してください",
Expand Down
13 changes: 13 additions & 0 deletions messages/ru/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,19 @@
"cancel": "Отмена",
"confirm": "Подтвердить удаление"
},
"failureThresholdConfirmDialog": {
"title": "Подтвердите особую конфигурацию",
"descriptionDisabledPrefix": "Вы устанавливаете порог сбоев автоматического выключателя на ",
"descriptionDisabledValue": "0",
"descriptionDisabledMiddle": ", что означает ",
"descriptionDisabledAction": "отключение автоматического выключателя",
"descriptionDisabledSuffix": ". Провайдер не будет отключаться из-за последовательных сбоев.",
"descriptionHighValuePrefix": "Вы устанавливаете порог сбоев автоматического выключателя на ",
"descriptionHighValueSuffix": ", что является высоким значением и может привести к отключению провайдера только после многочисленных сбоев.",
"confirmQuestion": "Вы уверены, что хотите сохранить эту конфигурацию?",
"cancel": "Отмена",
"confirm": "Подтвердить сохранение"
},
"errors": {
"invalidUrl": "Введите корректный адрес API",
"invalidWebsiteUrl": "Введите корректный адрес сайта провайдера",
Expand Down
13 changes: 13 additions & 0 deletions messages/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,19 @@
"cancel": "取消",
"confirm": "确认删除"
},
"failureThresholdConfirmDialog": {
"title": "确认特殊配置",
"descriptionDisabledPrefix": "您将熔断失败阈值设置为 ",
"descriptionDisabledValue": "0",
"descriptionDisabledMiddle": ",这表示",
"descriptionDisabledAction": "禁用熔断器",
"descriptionDisabledSuffix": ",供应商将不会因为连续失败而被熔断。",
"descriptionHighValuePrefix": "您将熔断失败阈值设置为 ",
"descriptionHighValueSuffix": ",这是一个较高的值,可能会导致供应商在大量失败后才被熔断。",
"confirmQuestion": "是否确认保存此配置?",
"cancel": "取消",
"confirm": "确认保存"
},
"errors": {
"invalidUrl": "请输入有效的 API 地址",
"invalidWebsiteUrl": "请输入有效的供应商官网地址",
Expand Down
13 changes: 13 additions & 0 deletions messages/zh-TW/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,19 @@
"cancel": "取消",
"confirm": "確認刪除"
},
"failureThresholdConfirmDialog": {
"title": "確認特殊設定",
"descriptionDisabledPrefix": "您將熔斷失敗閾值設定為 ",
"descriptionDisabledValue": "0",
"descriptionDisabledMiddle": ",這表示",
"descriptionDisabledAction": "停用熔斷器",
"descriptionDisabledSuffix": ",供應商將不會因為連續失敗而被熔斷。",
"descriptionHighValuePrefix": "您將熔斷失敗閾值設定為 ",
"descriptionHighValueSuffix": ",這是一個較高的值,可能會導致供應商在大量失敗後才被熔斷。",
"confirmQuestion": "是否確認儲存此設定?",
"cancel": "取消",
"confirm": "確認儲存"
},
"errors": {
"invalidUrl": "請輸入有效的 API 位址",
"invalidWebsiteUrl": "請輸入有效的供應商官網",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogTrigger,
AlertDialogHeader as AlertHeader,
AlertDialogTitle as AlertTitle,
Expand Down Expand Up @@ -200,6 +201,9 @@ export function ProviderForm({
mcpPassthrough: false,
});

// failureThreshold 确认对话框状态
const [showFailureThresholdConfirm, setShowFailureThresholdConfirm] = useState(false);

// 从 localStorage 加载折叠偏好
useEffect(() => {
const saved = localStorage.getItem("provider-form-sections");
Expand Down Expand Up @@ -276,6 +280,19 @@ export function ProviderForm({
return;
}

// 检查 failureThreshold 是否为特殊值(0 或大于 100)
const threshold = failureThreshold ?? 5;
if (threshold === 0 || threshold > 100) {
setShowFailureThresholdConfirm(true);
return;
}

// 正常提交
performSubmit();
};

// 实际提交逻辑
const performSubmit = () => {
// 处理模型重定向(空对象转为 null)
const parsedModelRedirects = Object.keys(modelRedirects).length > 0 ? modelRedirects : null;

Expand Down Expand Up @@ -1189,8 +1206,7 @@ export function ProviderForm({
}}
placeholder={t("sections.circuitBreaker.failureThreshold.placeholder")}
disabled={isPending}
min="1"
max="100"
min="0"
step="1"
/>
<p className="text-xs text-muted-foreground">
Expand Down Expand Up @@ -1676,6 +1692,51 @@ export function ProviderForm({
</CollapsibleContent>
</Collapsible>

{/* failureThreshold 特殊值确认对话框 */}
<AlertDialog
open={showFailureThresholdConfirm}
onOpenChange={setShowFailureThresholdConfirm}
>
<AlertDialogContent>
<AlertHeader>
<AlertTitle>{t("failureThresholdConfirmDialog.title")}</AlertTitle>
<AlertDialogDescription asChild>
<div className="space-y-3">
{failureThreshold === 0 ? (
<p>
{t("failureThresholdConfirmDialog.descriptionDisabledPrefix")}
<strong>{t("failureThresholdConfirmDialog.descriptionDisabledValue")}</strong>
{t("failureThresholdConfirmDialog.descriptionDisabledMiddle")}
<strong>
{t("failureThresholdConfirmDialog.descriptionDisabledAction")}
</strong>
{t("failureThresholdConfirmDialog.descriptionDisabledSuffix")}
</p>
) : (
<p>
{t("failureThresholdConfirmDialog.descriptionHighValuePrefix")}
<strong>{failureThreshold}</strong>
{t("failureThresholdConfirmDialog.descriptionHighValueSuffix")}
</p>
)}
<p>{t("failureThresholdConfirmDialog.confirmQuestion")}</p>
</div>
</AlertDialogDescription>
</AlertHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("failureThresholdConfirmDialog.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setShowFailureThresholdConfirm(false);
performSubmit();
}}
>
{t("failureThresholdConfirmDialog.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

{isEdit ? (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 pt-4">
<AlertDialog>
Expand All @@ -1693,7 +1754,7 @@ export function ProviderForm({
})}
</AlertDialogDescription>
</AlertHeader>
<div className="flex flex-col-reverse sm:flex-row gap-2 justify-end">
<AlertDialogFooter>
<AlertDialogCancel>{t("deleteDialog.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
Expand All @@ -1715,7 +1776,7 @@ export function ProviderForm({
>
{t("deleteDialog.confirm")}
</AlertDialogAction>
</div>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

Expand Down
3 changes: 2 additions & 1 deletion src/lib/circuit-breaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ export async function recordFailure(providerId: number, error: Error): Promise<v
);

// 检查是否需要打开熔断器
if (health.failureCount >= config.failureThreshold) {
// failureThreshold = 0 表示禁用熔断器
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [TEST-MISSING-CRITICAL] No tests for critical circuit breaker behavior change

Why this is a problem: This change adds a critical condition that allows failureThreshold = 0 to disable the circuit breaker entirely. This is a behavioral change to production code with no test coverage. Per CLAUDE.md, the project uses Vitest and has coverage thresholds, but there are no existing tests for circuit-breaker.ts at all.

Suggested fix: Add unit tests covering these cases:

// tests/unit/circuit-breaker.test.ts
describe('circuit breaker with threshold = 0', () => {
  it('should never open circuit when threshold is 0', async () => {
    // Set failureThreshold = 0
    // Record 100 failures
    // Verify circuit remains closed
  });
});

describe('circuit breaker with threshold > 100', () => {
  it('should function normally with high threshold values', async () => {
    // Set failureThreshold = 150
    // Record 149 failures - circuit should stay closed
    // Record 150th failure - circuit should open
  });
});

Without tests, we cannot verify this critical behavior works correctly across application restarts, Redis failures, or concurrent requests.

if (config.failureThreshold > 0 && health.failureCount >= config.failureThreshold) {
health.circuitState = "open";
health.circuitOpenUntil = Date.now() + config.openDuration;
health.halfOpenSuccessCount = 0;
Expand Down
6 changes: 2 additions & 4 deletions src/lib/validation/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,7 @@ export const CreateProviderSchema = z.object({
circuit_breaker_failure_threshold: z.coerce
.number()
.int("失败阈值必须是整数")
.min(1, "失败阈值不能少于1次")
.max(100, "失败阈值不能超过100次")
.min(0, "失败阈值不能为负数")
.optional(),
circuit_breaker_open_duration: z.coerce
.number()
Expand Down Expand Up @@ -626,8 +625,7 @@ export const UpdateProviderSchema = z
circuit_breaker_failure_threshold: z.coerce
.number()
.int("失败阈值必须是整数")
.min(1, "失败阈值不能少于1次")
.max(100, "失败阈值不能超过100次")
.min(0, "失败阈值不能为负数")
.optional(),
circuit_breaker_open_duration: z.coerce
.number()
Expand Down