feat(cliproxy): expose request retry config

The generated CLIProxy config pinned request-retry and
max-retry-interval to 0 with no way to change them. Add an opt-in
config.cliproxy.retry block (schema validation, generator wiring,
dashboard inputs beside the routing controls, i18n x5). Unset config
keeps the generated output byte-identical; max-retry-interval unit
confirmed as seconds against upstream CLIProxyAPI.
This commit is contained in:
milesnguyen2405 committed 2026-07-22 16:42:36 +07:00
1 parent 51e8716630
commit d5ea4eeea3
7 files changed
+433 -2

No files matched your search

@@ -0,0 +1,156 @@
/**
* CLIProxy request-retry config — generator tests.
*
* Covers:
* 1. Unset retry config produces today's exact request-retry/max-retry-interval bytes (0/0).
* 2. Configured retry values are emitted verbatim into the generated CLIProxy config.yaml.
* 3. Invalid persisted values (negative/non-integer) fall back to 0, matching the
* unset default (defensive: a corrupt config.yaml must not crash the generator).
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
function createTestHome(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-retry-test-'));
const ccsDir = path.join(dir, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(path.join(ccsDir, 'config.yaml'), 'version: 1\n', 'utf8');
return dir;
}
describe('CLIProxy retry config — generator', () => {
let tempHome: string;
let originalCcsHome: string | undefined;
beforeEach(() => {
tempHome = createTestHome();
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
fs.rmSync(tempHome, { recursive: true, force: true });
});
function regenerate(tag: string): string {
return path.join(tempHome, '.ccs', 'cliproxy', `config-${tag}.yaml`);
}
it("unset retry config emits request-retry: 0 and max-retry-interval: 0 (today's default)", async () => {
const { regenerateConfig } = await import(`../config/generator?crgen1=${Date.now()}`);
const configPath = regenerate('unset');
const authDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.mkdirSync(authDir, { recursive: true });
regenerateConfig(8317, { configPath, authDir });
const content = fs.readFileSync(configPath, 'utf-8');
expect(content).toContain('request-retry: 0');
expect(content).toContain('max-retry-interval: 0');
});
it('unset retry config produces byte-identical output to an explicit 0/0 config', async () => {
const { mutateConfig, invalidateConfigCache } = await import(
`../../config/config-loader-facade?crgen2a=${Date.now()}`
);
const { regenerateConfig } = await import(`../config/generator?crgen2a=${Date.now()}`);
const unsetPath = regenerate('unset-baseline');
const authDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(path.dirname(unsetPath), { recursive: true });
fs.mkdirSync(authDir, { recursive: true });
regenerateConfig(8317, { configPath: unsetPath, authDir });
const unsetContent = fs.readFileSync(unsetPath, 'utf-8');
mutateConfig((cfg: { cliproxy?: { retry?: Record<string, unknown> } }) => {
cfg.cliproxy = cfg.cliproxy ?? {};
cfg.cliproxy.retry = { request_retry: 0, max_retry_interval: 0 };
});
invalidateConfigCache();
const explicitPath = regenerate('explicit-zero');
regenerateConfig(8317, { configPath: explicitPath, authDir });
const explicitContent = fs.readFileSync(explicitPath, 'utf-8');
const stripTimestamp = (s: string) => s.replace(/# Generated: .+/g, '# Generated: TIMESTAMP');
expect(stripTimestamp(explicitContent)).toBe(stripTimestamp(unsetContent));
});
it('configured retry values are emitted into the generated config', async () => {
const { mutateConfig, invalidateConfigCache } = await import(
`../../config/config-loader-facade?crgen3=${Date.now()}`
);
mutateConfig((cfg: { cliproxy?: { retry?: Record<string, unknown> } }) => {
cfg.cliproxy = cfg.cliproxy ?? {};
cfg.cliproxy.retry = { request_retry: 3, max_retry_interval: 30 };
});
invalidateConfigCache();
const { regenerateConfig } = await import(`../config/generator?crgen3=${Date.now()}`);
const configPath = regenerate('configured');
const authDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.mkdirSync(authDir, { recursive: true });
regenerateConfig(8317, { configPath, authDir });
const content = fs.readFileSync(configPath, 'utf-8');
expect(content).toContain('request-retry: 3');
expect(content).toContain('max-retry-interval: 30');
});
it('only request_retry set falls back to 0 for max_retry_interval', async () => {
const { mutateConfig, invalidateConfigCache } = await import(
`../../config/config-loader-facade?crgen4=${Date.now()}`
);
mutateConfig((cfg: { cliproxy?: { retry?: Record<string, unknown> } }) => {
cfg.cliproxy = cfg.cliproxy ?? {};
cfg.cliproxy.retry = { request_retry: 5 };
});
invalidateConfigCache();
const { regenerateConfig } = await import(`../config/generator?crgen4=${Date.now()}`);
const configPath = regenerate('partial');
const authDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.mkdirSync(authDir, { recursive: true });
regenerateConfig(8317, { configPath, authDir });
const content = fs.readFileSync(configPath, 'utf-8');
expect(content).toContain('request-retry: 5');
expect(content).toContain('max-retry-interval: 0');
});
it('a negative persisted request_retry falls back to 0 (defensive, does not throw)', async () => {
const { mutateConfig, invalidateConfigCache } = await import(
`../../config/config-loader-facade?crgen5=${Date.now()}`
);
mutateConfig((cfg: { cliproxy?: { retry?: Record<string, unknown> } }) => {
cfg.cliproxy = cfg.cliproxy ?? {};
cfg.cliproxy.retry = { request_retry: -1, max_retry_interval: 2.5 };
});
invalidateConfigCache();
const { regenerateConfig } = await import(`../config/generator?crgen5=${Date.now()}`);
const configPath = regenerate('invalid');
const authDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.mkdirSync(authDir, { recursive: true });
expect(() => regenerateConfig(8317, { configPath, authDir })).not.toThrow();
const content = fs.readFileSync(configPath, 'utf-8');
expect(content).toContain('request-retry: 0');
expect(content).toContain('max-retry-interval: 0');
});
});
+24 -2
View File
@@ -16,6 +16,7 @@ import { getAuthDir, getProviderAuthDir, getConfigPathForPort } from './path-res
import { CLIPROXY_DEFAULT_PORT } from './port-manager';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
import { getActiveDockerLegacyApiKeys } from '../../docker/docker-key-rotation';
import { isValidCliproxyRetryValue } from '../../config/schemas/cliproxy';
/** Internal API key for CCS-managed requests */
export const CCS_INTERNAL_API_KEY = 'ccs-internal-managed';
@@ -192,6 +193,25 @@ function getSessionAffinityTtl(): string {
return ttl && GO_DURATION_PATTERN.test(ttl) && hasPositiveDuration(ttl) ? ttl : '1h';
}
/**
* Number of times CLIProxy retries a request on a transient error
* (403, 408, 500, 502, 503, 504). Opt-in via config.cliproxy.retry.request_retry;
* defaults to 0 (disabled) to avoid burning quota on multi-account pools.
*/
function getRequestRetry(): number {
const value = loadOrCreateUnifiedConfig().cliproxy?.retry?.request_retry;
return isValidCliproxyRetryValue(value) ? value : 0;
}
/**
* Maximum wait time in seconds for a cooled-down credential before CLIProxy
* retries. Opt-in via config.cliproxy.retry.max_retry_interval; defaults to 0.
*/
function getMaxRetryInterval(): number {
const value = loadOrCreateUnifiedConfig().cliproxy?.retry?.max_retry_interval;
return isValidCliproxyRetryValue(value) ? value : 0;
}
function normalizeManagementPanelRepository(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
@@ -659,6 +679,8 @@ function generateUnifiedConfigContent(
const routingStrategy = getRoutingStrategy();
const sessionAffinityEnabled = getSessionAffinityEnabled();
const sessionAffinityTtl = getSessionAffinityTtl();
const requestRetry = getRequestRetry();
const maxRetryInterval = getMaxRetryInterval();
const managementPanelRepository = getManagementPanelRepository();
// Get effective auth tokens (respects user customization)
@@ -751,8 +773,8 @@ ${coolingComment}
disable-cooling: ${disableCoolingValue}
# Auto-retry on transient errors (403, 408, 500, 502, 503, 504)
request-retry: 0
max-retry-interval: 0
request-retry: ${requestRetry}
max-retry-interval: ${maxRetryInterval}
${poolRoutingBlock}
# Auto-switch accounts on quota exceeded (429)
# This enables seamless multi-account rotation when rate limited
@@ -0,0 +1,48 @@
/**
* Tests: CLIProxy retry config schema bounds (request_retry, max_retry_interval).
*
* Verifies the opt-in contract: both fields accept only non-negative integers.
*/
import { describe, it, expect } from 'bun:test';
import {
isValidCliproxyRetryValue,
CLIPROXY_RETRY_MIN_VALUE,
CLIPROXY_RETRY_RANGE_MESSAGE,
} from '../cliproxy';
describe('isValidCliproxyRetryValue', () => {
it('accepts 0 (the disabled default)', () => {
expect(isValidCliproxyRetryValue(0)).toBe(true);
});
it('accepts positive integers', () => {
expect(isValidCliproxyRetryValue(3)).toBe(true);
expect(isValidCliproxyRetryValue(30)).toBe(true);
});
it('rejects negative numbers', () => {
expect(isValidCliproxyRetryValue(-1)).toBe(false);
});
it('rejects non-integer numbers', () => {
expect(isValidCliproxyRetryValue(1.5)).toBe(false);
});
it('rejects non-number types', () => {
expect(isValidCliproxyRetryValue('3')).toBe(false);
expect(isValidCliproxyRetryValue(undefined)).toBe(false);
expect(isValidCliproxyRetryValue(null)).toBe(false);
expect(isValidCliproxyRetryValue(true)).toBe(false);
});
it('rejects NaN and Infinity', () => {
expect(isValidCliproxyRetryValue(Number.NaN)).toBe(false);
expect(isValidCliproxyRetryValue(Number.POSITIVE_INFINITY)).toBe(false);
});
it('exposes the accepted-range constants used in recoverable error messages', () => {
expect(CLIPROXY_RETRY_MIN_VALUE).toBe(0);
expect(CLIPROXY_RETRY_RANGE_MESSAGE).toContain('0');
});
});
+29
View File
@@ -122,6 +122,33 @@ export interface CLIProxyRoutingConfig {
session_affinity_ttl?: string;
}
/**
* CLIProxy request-retry configuration.
* Controls CLIProxy's own retry-on-transient-error behavior
* (403, 408, 500, 502, 503, 504). Defaults to disabled (0/0): retrying can
* burn quota on multi-account pools, so this is opt-in.
*/
export interface CLIProxyRetryConfig {
/** Number of times to retry a request on a transient error (integer >= 0, default: 0 = disabled) */
request_retry?: number;
/** Maximum wait time in seconds for a cooled-down credential before retrying (integer >= 0, default: 0) */
max_retry_interval?: number;
}
/** Lower bound (inclusive) accepted for CLIProxy retry config fields. */
export const CLIPROXY_RETRY_MIN_VALUE = 0;
/** Human-readable accepted-range description for CLIProxy retry config fields. */
export const CLIPROXY_RETRY_RANGE_MESSAGE = `must be a whole number ${CLIPROXY_RETRY_MIN_VALUE} or greater`;
/**
* Validate a single CLIProxy retry config field value (request_retry or
* max_retry_interval). Both fields share the same bounds: non-negative integer.
*/
export function isValidCliproxyRetryValue(value: unknown): value is number {
return typeof value === 'number' && Number.isInteger(value) && value >= CLIPROXY_RETRY_MIN_VALUE;
}
/**
* Pool routing configuration for multi-account CLIProxy rotation.
*
@@ -188,4 +215,6 @@ export interface CLIProxyConfig {
routing?: CLIProxyRoutingConfig;
/** Pool routing opt-in state and configuration */
pool_routing?: CLIProxyPoolRoutingConfig;
/** Request-retry behavior for transient upstream errors (opt-in, default: disabled) */
retry?: CLIProxyRetryConfig;
}
@@ -7,9 +7,15 @@ import type {
RoutingStrategy,
CliproxySessionAffinityState,
} from '@/lib/api-client';
import { useCliproxyRetryConfig, useUpdateCliproxyRetryConfig } from '@/hooks/use-cliproxy';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
/** Retry fields accept only non-negative integers (matches the CLIProxy schema bounds). */
function isValidRetryFieldValue(value: number): boolean {
return Number.isInteger(value) && value >= 0;
}
interface RoutingGuidanceCardProps {
className?: string;
compact?: boolean;
@@ -73,6 +79,17 @@ export function RoutingGuidanceCard({
const pendingAffinityRef = useRef<{ enabled: boolean; ttl: string } | null>(null);
const suppressNextAffinityBlurRef = useRef(false);
const retryConfigQuery = useCliproxyRetryConfig();
const updateRetryConfig = useUpdateCliproxyRetryConfig();
const currentRequestRetry = retryConfigQuery.data?.request_retry ?? 0;
const currentMaxRetryInterval = retryConfigQuery.data?.max_retry_interval ?? 0;
const [requestRetryInput, setRequestRetryInput] = useState(String(currentRequestRetry));
const [maxRetryIntervalInput, setMaxRetryIntervalInput] = useState(
String(currentMaxRetryInterval)
);
const [retryFieldError, setRetryFieldError] = useState<string | null>(null);
const retryControlDisabled = retryConfigQuery.isLoading || updateRetryConfig.isPending;
useEffect(() => {
setSelected(currentStrategy);
}, [currentStrategy]);
@@ -82,6 +99,11 @@ export function RoutingGuidanceCard({
setSelectedAffinityTtl(currentAffinityTtl);
}, [currentAffinityEnabled, currentAffinityTtl]);
useEffect(() => {
setRequestRetryInput(String(currentRequestRetry));
setMaxRetryIntervalInput(String(currentMaxRetryInterval));
}, [currentRequestRetry, currentMaxRetryInterval]);
useEffect(() => {
if (isSaving || !pendingAffinityRef.current) {
return;
@@ -122,6 +144,34 @@ export function RoutingGuidanceCard({
onApplyAffinity({ enabled: selectedAffinityEnabled, ttl: nextTtl });
};
const handleRetryBlur = () => {
const nextRequestRetry = Number(requestRetryInput.trim());
const nextMaxRetryInterval = Number(maxRetryIntervalInput.trim());
if (
!isValidRetryFieldValue(nextRequestRetry) ||
!isValidRetryFieldValue(nextMaxRetryInterval)
) {
setRetryFieldError(t('routingGuidance.retryRangeError'));
setRequestRetryInput(String(currentRequestRetry));
setMaxRetryIntervalInput(String(currentMaxRetryInterval));
return;
}
setRetryFieldError(null);
if (
nextRequestRetry === currentRequestRetry &&
nextMaxRetryInterval === currentMaxRetryInterval
) {
return;
}
updateRetryConfig.mutate({
request_retry: nextRequestRetry,
max_retry_interval: nextMaxRetryInterval,
});
};
if (compact) {
const handleApply = (s: RoutingStrategy) => {
setSelected(s);
@@ -274,6 +324,44 @@ export function RoutingGuidanceCard({
{sessionAffinityState.message}
</div>
) : null}
<div className="flex items-center justify-between gap-2 rounded-lg border border-border/60 bg-muted/20 px-2 py-1.5">
<div className="min-w-0">
<div className="text-[10px] font-medium text-foreground">
{t('routingGuidance.retryTitle')}
</div>
<div className="text-[10px] text-muted-foreground">
{t('routingGuidance.retryHint')}
</div>
</div>
<div className="flex items-center gap-1">
<input
aria-label={t('routingGuidance.requestRetryLabel')}
inputMode="numeric"
className="h-6 w-10 rounded border border-border/70 bg-background px-1.5 text-[10px] text-foreground"
value={requestRetryInput}
onChange={(event) => setRequestRetryInput(event.target.value.replace(/\D/g, ''))}
onBlur={handleRetryBlur}
disabled={retryControlDisabled}
/>
<span className="text-[10px] text-muted-foreground">/</span>
<input
aria-label={t('routingGuidance.maxRetryIntervalLabel')}
inputMode="numeric"
className="h-6 w-10 rounded border border-border/70 bg-background px-1.5 text-[10px] text-foreground"
value={maxRetryIntervalInput}
onChange={(event) => setMaxRetryIntervalInput(event.target.value.replace(/\D/g, ''))}
onBlur={handleRetryBlur}
disabled={retryControlDisabled}
/>
</div>
</div>
{retryFieldError ? (
<div className="rounded-lg border border-destructive/25 bg-destructive/5 px-2 py-1.5 text-[10px] text-destructive">
{retryFieldError}
</div>
) : null}
</div>
);
}
+58
View File
@@ -106,6 +106,64 @@ export function useUpdateCliproxySessionAffinity() {
});
}
/** CLIProxy request-retry config (config.cliproxy.retry). Defaults to disabled (0/0). */
export interface CliproxyRetryConfig {
request_retry: number;
max_retry_interval: number;
}
const DEFAULT_CLIPROXY_RETRY_CONFIG: CliproxyRetryConfig = {
request_retry: 0,
max_retry_interval: 0,
};
export function useCliproxyRetryConfig() {
return useQuery({
queryKey: ['cliproxy-retry-config'],
queryFn: async (): Promise<CliproxyRetryConfig> => {
const config = await api.config.get();
const cliproxy = config.cliproxy as { retry?: Partial<CliproxyRetryConfig> } | undefined;
return {
request_retry:
cliproxy?.retry?.request_retry ?? DEFAULT_CLIPROXY_RETRY_CONFIG.request_retry,
max_retry_interval:
cliproxy?.retry?.max_retry_interval ?? DEFAULT_CLIPROXY_RETRY_CONFIG.max_retry_interval,
};
},
});
}
/**
* Save CLIProxy retry config. Reuses the generic unified-config save path
* (PUT /config) — retry is a static opt-in default, not a live-proxy setting
* like routing.strategy/session_affinity, so it does not need a dedicated
* management-API round trip.
*/
export function useUpdateCliproxyRetryConfig() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: async (retry: CliproxyRetryConfig): Promise<CliproxyRetryConfig> => {
const config = await api.config.get();
const existingCliproxy = (config.cliproxy ?? {}) as Record<string, unknown>;
await api.config.update({
...config,
cliproxy: { ...existingCliproxy, retry },
});
return retry;
},
onSuccess: (retry) => {
queryClient.setQueryData(['cliproxy-retry-config'], retry);
queryClient.invalidateQueries({ queryKey: ['cliproxy-retry-config'] });
toast.success(t('toasts.cliproxyRetryUpdated'));
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
export function useCreateVariant() {
const queryClient = useQueryClient();
const { t } = useTranslation();
+30
View File
@@ -1954,6 +1954,11 @@ const resources = {
sessionRecognitionTitle: 'Session recognition',
sessionRecognitionDescription:
'CCS does not promise one universal precedence order here. In practice, upstream backends prefer explicit session or thread IDs first, then fall back to metadata fields and finally a hash based on the opening prompt history.',
retryTitle: 'Auto-retry',
retryHint: 'Retries / max wait (s)',
requestRetryLabel: 'Request retry count',
maxRetryIntervalLabel: 'Max retry interval (seconds)',
retryRangeError: 'Must be a whole number, 0 or greater.',
},
extendedContext: {
extendedContext: 'Extended Context',
@@ -2347,6 +2352,7 @@ const resources = {
noProfilesToSync: 'No profiles to sync',
syncFailed: 'Sync failed: {{error}}',
sessionAffinityUpdated: 'Session affinity {{state}}.',
cliproxyRetryUpdated: 'Retry settings updated.',
providerAuthSuccess: '{{provider}} authentication successful',
providerDeviceCodeInCallback: 'Provider returned Device Code flow in callback mode',
providerAuthTimeout: 'Authentication timed out. Please try again.',
@@ -4587,6 +4593,11 @@ const resources = {
sessionRecognitionTitle: '会话识别',
sessionRecognitionDescription:
'CCS 不承诺所有后端都使用同一优先级顺序。通常上游会优先使用显式会话或线程 ID,然后回退到元数据字段,最后再回退到基于开场提示历史的哈希。',
retryTitle: '自动重试',
retryHint: '重试次数 / 最大等待(秒)',
requestRetryLabel: '重试次数',
maxRetryIntervalLabel: '最大重试间隔(秒)',
retryRangeError: '必须是 0 或更大的整数。',
},
extendedContext: {
extendedContext: '扩展上下文',
@@ -4963,6 +4974,7 @@ const resources = {
noProfilesToSync: '没有可同步的配置',
syncFailed: '同步失败:{{error}}',
sessionAffinityUpdated: '会话粘性已{{state}}。',
cliproxyRetryUpdated: '重试设置已更新。',
providerAuthSuccess: '{{provider}} 认证成功',
providerDeviceCodeInCallback: '提供商在回调模式中返回了设备码流程',
providerAuthTimeout: '认证超时,请重试。',
@@ -7295,6 +7307,11 @@ const resources = {
sessionRecognitionTitle: 'Nhận diện phiên',
sessionRecognitionDescription:
'CCS không cam kết một thứ tự ưu tiên chung cho mọi backend. Trên thực tế, backend upstream thường ưu tiên session hoặc thread id tường minh, rồi mới fallback sang metadata và cuối cùng là hàm băm của lịch sử prompt mở đầu.',
retryTitle: 'Tự động thử lại',
retryHint: 'Số lần thử lại / chờ tối đa (giây)',
requestRetryLabel: 'Số lần thử lại yêu cầu',
maxRetryIntervalLabel: 'Khoảng chờ thử lại tối đa (giây)',
retryRangeError: 'Phải là số nguyên từ 0 trở lên.',
},
extendedContext: {
extendedContext: 'Ngữ cảnh mở rộng',
@@ -7676,6 +7693,7 @@ const resources = {
noProfilesToSync: 'Không có hồ sơ để đồng bộ',
syncFailed: 'Đồng bộ thất bại: {{error}}',
sessionAffinityUpdated: 'Ghim phiên đã {{state}}.',
cliproxyRetryUpdated: 'Đã cập nhật cài đặt thử lại.',
providerAuthSuccess: 'Xác thực {{provider}} thành công',
providerDeviceCodeInCallback: 'Nhà cung cấp trả về Device Code flow trong chế độ callback',
providerAuthTimeout: 'Đã hết thời gian xác thực. Vui lòng thử lại.',
@@ -10475,6 +10493,11 @@ const resources = {
sessionRecognitionTitle: 'セッション認識',
sessionRecognitionDescription:
'CCS はすべてのバックエンドで同一の優先順位を保証しません。一般に上流バックエンドは明示的なセッション / スレッド ID を優先し、その後にメタデータ、最後に冒頭プロンプト履歴のハッシュへフォールバックします。',
retryTitle: '自動リトライ',
retryHint: 'リトライ回数 / 最大待機(秒)',
requestRetryLabel: 'リトライ回数',
maxRetryIntervalLabel: '最大リトライ間隔(秒)',
retryRangeError: '0 以上の整数を入力してください。',
},
settingsDialog: {
editProfile: 'プロファイルを編集: {{name}}',
@@ -10706,6 +10729,7 @@ const resources = {
noProfilesToSync: '同期するプロファイルがありません',
syncFailed: '同期に失敗しました: {{error}}',
sessionAffinityUpdated: 'セッション固定を{{state}}にしました。',
cliproxyRetryUpdated: '再試行設定を更新しました。',
providerAuthSuccess: '{{provider}} の認証に成功しました',
providerDeviceCodeInCallback:
'コールバックモードでプロバイダーがデバイスコードフローを返しました',
@@ -12762,6 +12786,11 @@ const resources = {
sessionRecognitionTitle: '세션 인식',
sessionRecognitionDescription:
'CCS는 여기서 보편적인 우선순위를 약속하지 않습니다. 실제로 업스트림 백엔드는 명시적 세션 또는 스레드 ID를 먼저 선호한 다음, 메타데이터 필드로 폴백하고 최종적으로 시작 프롬프트 기록 기반 해시로 폴백합니다.',
retryTitle: '자동 재시도',
retryHint: '재시도 횟수 / 최대 대기(초)',
requestRetryLabel: '재시도 횟수',
maxRetryIntervalLabel: '최대 재시도 간격(초)',
retryRangeError: '0 이상의 정수여야 합니다.',
},
extendedContext: {
extendedContext: '확장 컨텍스트',
@@ -13158,6 +13187,7 @@ const resources = {
noProfilesToSync: '동기화할 프로필이 없습니다',
syncFailed: '동기화 실패: {{error}}',
sessionAffinityUpdated: '세션 어피니티 {{state}}.',
cliproxyRetryUpdated: '재시도 설정이 업데이트되었습니다.',
providerAuthSuccess: '{{provider}} 인증 성공',
providerDeviceCodeInCallback:
'프로바이더가 콜백 모드에서 디바이스 코드 흐름을 반환했습니다',