Browse Source

feat(ui): overhaul default channel editor with full param override visual editor

- Port classic ParamOverrideEditorModal to default as standalone dialog (~3200 lines)
  with two-panel layout, drag-to-reorder, 23 operation modes, template library,
  visual/JSON dual mode, conditions management, and legacy format support
- Redesign channel drawer layout with clear visual hierarchy (CardHeading vs SubHeading)
  and bordered sub-modules for Field Passthrough and Upstream Model Detection
- Replace header override JsonEditor with plain textarea matching classic behavior
- Add searchable channel type combobox with scroll fix
- Add 100+ i18n keys across all 6 locales (en, zh, fr, ja, ru, vi)
CaIon 1 month ago
parent
commit
b44faec66b

+ 6 - 1
web/default/src/components/ui/combobox.tsx

@@ -103,7 +103,12 @@ export function Combobox({
           <ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
         </Button>
       </PopoverTrigger>
-      <PopoverContent className='w-[var(--radix-popover-trigger-width)] p-0'>
+      <PopoverContent
+        className='w-[var(--radix-popover-trigger-width)] p-0'
+        onWheel={(e) => e.stopPropagation()}
+        onTouchMove={(e) => e.stopPropagation()}
+        onPointerDown={(e) => e.stopPropagation()}
+      >
         <Command shouldFilter={false}>
           <CommandInput
             placeholder={searchPlaceholder}

+ 3248 - 0
web/default/src/features/channels/components/dialogs/param-override-editor-dialog.tsx

@@ -0,0 +1,3248 @@
+import {
+  type DragEvent,
+  type KeyboardEvent,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState,
+} from 'react'
+import {
+  ChevronDown,
+  ChevronUp,
+  Copy,
+  GripVertical,
+  Plus,
+  Search,
+  Trash2,
+} from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { toast } from 'sonner'
+import { cn } from '@/lib/utils'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from '@/components/ui/collapsible'
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+import { ScrollArea } from '@/components/ui/scroll-area'
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/components/ui/select'
+import { Switch } from '@/components/ui/switch'
+import { Textarea } from '@/components/ui/textarea'
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+type ParamOverrideCondition = {
+  id: string
+  path: string
+  mode: string
+  value_text: string
+  invert: boolean
+  pass_missing_key: boolean
+}
+
+type ParamOverrideOperation = {
+  id: string
+  description: string
+  path: string
+  mode: string
+  from: string
+  to: string
+  value_text: string
+  keep_origin: boolean
+  logic: string
+  conditions: ParamOverrideCondition[]
+}
+
+export type ParamOverrideEditorDialogProps = {
+  open: boolean
+  value: string
+  onOpenChange: (open: boolean) => void
+  onSave: (value: string) => void
+}
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+
+const OPERATION_MODE_OPTIONS = [
+  { label: 'Set Field', value: 'set' },
+  { label: 'Delete Field', value: 'delete' },
+  { label: 'Append to End', value: 'append' },
+  { label: 'Prepend to Start', value: 'prepend' },
+  { label: 'Copy Field', value: 'copy' },
+  { label: 'Move Field', value: 'move' },
+  { label: 'String Replace', value: 'replace' },
+  { label: 'Regex Replace', value: 'regex_replace' },
+  { label: 'Trim Prefix', value: 'trim_prefix' },
+  { label: 'Trim Suffix', value: 'trim_suffix' },
+  { label: 'Ensure Prefix', value: 'ensure_prefix' },
+  { label: 'Ensure Suffix', value: 'ensure_suffix' },
+  { label: 'Trim Space', value: 'trim_space' },
+  { label: 'To Lowercase', value: 'to_lower' },
+  { label: 'To Uppercase', value: 'to_upper' },
+  { label: 'Return Custom Error', value: 'return_error' },
+  { label: 'Prune Object Items', value: 'prune_objects' },
+  { label: 'Pass Through Headers', value: 'pass_headers' },
+  { label: 'Sync Fields', value: 'sync_fields' },
+  { label: 'Set Request Header', value: 'set_header' },
+  { label: 'Delete Request Header', value: 'delete_header' },
+  { label: 'Copy Request Header', value: 'copy_header' },
+  { label: 'Move Request Header', value: 'move_header' },
+]
+
+const OPERATION_MODE_VALUES = new Set(
+  OPERATION_MODE_OPTIONS.map((o) => o.value)
+)
+
+const OPERATION_MODE_LABEL_MAP = OPERATION_MODE_OPTIONS.reduce<
+  Record<string, string>
+>((acc, item) => {
+  acc[item.value] = item.label
+  return acc
+}, {})
+
+const CONDITION_MODE_OPTIONS = [
+  { label: 'Exact Match', value: 'full' },
+  { label: 'Prefix', value: 'prefix' },
+  { label: 'Suffix', value: 'suffix' },
+  { label: 'Contains', value: 'contains' },
+  { label: 'Greater Than', value: 'gt' },
+  { label: 'Greater Than or Equal', value: 'gte' },
+  { label: 'Less Than', value: 'lt' },
+  { label: 'Less Than or Equal', value: 'lte' },
+]
+
+const CONDITION_MODE_VALUES = new Set(
+  CONDITION_MODE_OPTIONS.map((o) => o.value)
+)
+
+const MODE_META: Record<
+  string,
+  {
+    path?: boolean
+    pathOptional?: boolean
+    value?: boolean
+    from?: boolean
+    to?: boolean
+    keepOrigin?: boolean
+    pathAlias?: boolean
+  }
+> = {
+  delete: { path: true },
+  set: { path: true, value: true, keepOrigin: true },
+  append: { path: true, value: true, keepOrigin: true },
+  prepend: { path: true, value: true, keepOrigin: true },
+  copy: { from: true, to: true },
+  move: { from: true, to: true },
+  replace: { path: true, from: true, to: false },
+  regex_replace: { path: true, from: true, to: false },
+  trim_prefix: { path: true, value: true },
+  trim_suffix: { path: true, value: true },
+  ensure_prefix: { path: true, value: true },
+  ensure_suffix: { path: true, value: true },
+  trim_space: { path: true },
+  to_lower: { path: true },
+  to_upper: { path: true },
+  return_error: { value: true },
+  prune_objects: { pathOptional: true, value: true },
+  pass_headers: { value: true, keepOrigin: true },
+  sync_fields: { from: true, to: true },
+  set_header: { path: true, value: true, keepOrigin: true },
+  delete_header: { path: true },
+  copy_header: { from: true, to: true, keepOrigin: true, pathAlias: true },
+  move_header: { from: true, to: true, keepOrigin: true, pathAlias: true },
+}
+
+const VALUE_REQUIRED_MODES = new Set([
+  'trim_prefix',
+  'trim_suffix',
+  'ensure_prefix',
+  'ensure_suffix',
+  'set_header',
+  'return_error',
+  'prune_objects',
+  'pass_headers',
+])
+
+const FROM_REQUIRED_MODES = new Set([
+  'copy',
+  'move',
+  'replace',
+  'regex_replace',
+  'copy_header',
+  'move_header',
+  'sync_fields',
+])
+
+const TO_REQUIRED_MODES = new Set([
+  'copy',
+  'move',
+  'copy_header',
+  'move_header',
+  'sync_fields',
+])
+
+const MODE_DESCRIPTIONS: Record<string, string> = {
+  set: 'Write value to the target field',
+  delete: 'Remove the target field',
+  append: 'Append value to array / string / object end',
+  prepend: 'Prepend value to array / string / object start',
+  copy: 'Copy source field to target field',
+  move: 'Move source field to target field',
+  replace: 'Do string replacement in the target field',
+  regex_replace: 'Do regex replacement in the target field',
+  trim_prefix: 'Remove string prefix',
+  trim_suffix: 'Remove string suffix',
+  ensure_prefix: 'Ensure the string has a specified prefix',
+  ensure_suffix: 'Ensure the string has a specified suffix',
+  trim_space: 'Trim leading/trailing whitespace',
+  to_lower: 'Convert string to lowercase',
+  to_upper: 'Convert string to uppercase',
+  return_error: 'Return a custom error immediately',
+  prune_objects: 'Prune object items by conditions',
+  pass_headers: 'Pass specified request headers to upstream',
+  sync_fields: 'Auto-fill when one field exists and another is missing',
+  set_header:
+    'Set runtime request header: override entire value, or manipulate comma-separated tokens',
+  delete_header: 'Delete a runtime request header',
+  copy_header: 'Copy a request header',
+  move_header: 'Move a request header',
+}
+
+const SYNC_TARGET_TYPE_OPTIONS = [
+  { label: 'Request Body Field', value: 'json' },
+  { label: 'Request Header Field', value: 'header' },
+]
+
+// Templates
+
+const LEGACY_TEMPLATE = { temperature: 0, max_tokens: 1000 }
+
+const OPERATION_TEMPLATE = {
+  operations: [
+    {
+      description: 'Set default temperature for openai/* models.',
+      path: 'temperature',
+      mode: 'set',
+      value: 0.7,
+      conditions: [{ path: 'model', mode: 'prefix', value: 'openai/' }],
+      logic: 'AND',
+    },
+  ],
+}
+
+const HEADER_PASSTHROUGH_TEMPLATE = {
+  operations: [
+    {
+      description: 'Pass through X-Request-Id header to upstream.',
+      mode: 'pass_headers',
+      value: ['X-Request-Id'],
+      keep_origin: true,
+    },
+  ],
+}
+
+const GEMINI_IMAGE_4K_TEMPLATE = {
+  operations: [
+    {
+      description:
+        'Set imageSize to 4K when model contains gemini/image and ends with 4k.',
+      mode: 'set',
+      path: 'generationConfig.imageConfig.imageSize',
+      value: '4K',
+      conditions: [
+        { path: 'original_model', mode: 'contains', value: 'gemini' },
+        { path: 'original_model', mode: 'contains', value: 'image' },
+        { path: 'original_model', mode: 'suffix', value: '4k' },
+      ],
+      logic: 'AND',
+    },
+  ],
+}
+
+const CODEX_CLI_HEADER_PASSTHROUGH_HEADERS = [
+  'Originator',
+  'Session_id',
+  'User-Agent',
+  'X-Codex-Beta-Features',
+  'X-Codex-Turn-Metadata',
+]
+
+const CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS = [
+  'X-Stainless-Arch',
+  'X-Stainless-Lang',
+  'X-Stainless-Os',
+  'X-Stainless-Package-Version',
+  'X-Stainless-Retry-Count',
+  'X-Stainless-Runtime',
+  'X-Stainless-Runtime-Version',
+  'X-Stainless-Timeout',
+  'User-Agent',
+  'X-App',
+  'Anthropic-Beta',
+  'Anthropic-Dangerous-Direct-Browser-Access',
+  'Anthropic-Version',
+]
+
+const buildPassHeadersTemplate = (headers: string[]) => ({
+  operations: [
+    { mode: 'pass_headers', value: [...headers], keep_origin: true },
+  ],
+})
+
+const CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE = buildPassHeadersTemplate(
+  CODEX_CLI_HEADER_PASSTHROUGH_HEADERS
+)
+const CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE = buildPassHeadersTemplate(
+  CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS
+)
+
+const AWS_BEDROCK_ANTHROPIC_COMPAT_TEMPLATE = {
+  operations: [
+    {
+      description:
+        'Normalize anthropic-beta header tokens for Bedrock compatibility.',
+      mode: 'set_header',
+      path: 'anthropic-beta',
+      value: {
+        'advanced-tool-use-2025-11-20': 'tool-search-tool-2025-10-19',
+        bash_20241022: null,
+        bash_20250124: null,
+        'code-execution-2025-08-25': null,
+        'compact-2026-01-12': 'compact-2026-01-12',
+        'computer-use-2025-01-24': 'computer-use-2025-01-24',
+        'computer-use-2025-11-24': 'computer-use-2025-11-24',
+        'context-1m-2025-08-07': 'context-1m-2025-08-07',
+        'context-management-2025-06-27': 'context-management-2025-06-27',
+        'effort-2025-11-24': null,
+        'fast-mode-2026-02-01': null,
+        'files-api-2025-04-14': null,
+        'fine-grained-tool-streaming-2025-05-14': null,
+        'interleaved-thinking-2025-05-14': 'interleaved-thinking-2025-05-14',
+        'mcp-client-2025-11-20': null,
+        'mcp-client-2025-04-04': null,
+        'mcp-servers-2025-12-04': null,
+        'output-128k-2025-02-19': null,
+        'structured-output-2024-03-01': null,
+        'prompt-caching-scope-2026-01-05': null,
+        'skills-2025-10-02': null,
+        'structured-outputs-2025-11-13': null,
+        text_editor_20241022: null,
+        text_editor_20250124: null,
+        'token-efficient-tools-2025-02-19': null,
+        'tool-search-tool-2025-10-19': 'tool-search-tool-2025-10-19',
+        'web-fetch-2025-09-10': null,
+        'web-search-2025-03-05': null,
+        'oauth-2025-04-20': null,
+      },
+    },
+    {
+      description:
+        'Remove all tools[*].custom.input_examples before upstream relay.',
+      mode: 'delete',
+      path: 'tools.*.custom.input_examples',
+    },
+  ],
+}
+
+type TemplatePresetConfig = {
+  label: string
+  kind: 'operations' | 'legacy'
+  payload: Record<string, unknown>
+}
+
+const TEMPLATE_PRESET_CONFIG: Record<string, TemplatePresetConfig> = {
+  operations_default: {
+    label: 'New Format Template',
+    kind: 'operations',
+    payload: OPERATION_TEMPLATE,
+  },
+  legacy_default: {
+    label: 'Legacy Format Template',
+    kind: 'legacy',
+    payload: LEGACY_TEMPLATE,
+  },
+  pass_headers_auth: {
+    label: 'Header Passthrough (X-Request-Id)',
+    kind: 'operations',
+    payload: HEADER_PASSTHROUGH_TEMPLATE,
+  },
+  gemini_image_4k: {
+    label: 'Gemini Image 4K',
+    kind: 'operations',
+    payload: GEMINI_IMAGE_4K_TEMPLATE,
+  },
+  claude_cli_headers_passthrough: {
+    label: 'Claude CLI Header Passthrough',
+    kind: 'operations',
+    payload: CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE,
+  },
+  codex_cli_headers_passthrough: {
+    label: 'Codex CLI Header Passthrough',
+    kind: 'operations',
+    payload: CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE,
+  },
+  aws_bedrock_anthropic_beta_override: {
+    label: 'AWS Bedrock Claude Compat',
+    kind: 'operations',
+    payload: AWS_BEDROCK_ANTHROPIC_COMPAT_TEMPLATE,
+  },
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+let localIdSeed = 0
+const nextLocalId = () => `po_${Date.now()}_${localIdSeed++}`
+
+const toValueText = (value: unknown): string => {
+  if (value === undefined) return ''
+  if (typeof value === 'string') return value
+  try {
+    return JSON.stringify(value)
+  } catch {
+    return String(value)
+  }
+}
+
+const parseLooseValue = (valueText: string): unknown => {
+  const raw = String(valueText ?? '').trim()
+  if (raw === '') return ''
+  try {
+    return JSON.parse(raw)
+  } catch {
+    return raw
+  }
+}
+
+const verifyJSON = (text: string): boolean => {
+  try {
+    JSON.parse(text)
+    return true
+  } catch {
+    return false
+  }
+}
+
+const normalizeCondition = (
+  condition: Record<string, unknown> = {}
+): ParamOverrideCondition => ({
+  id: nextLocalId(),
+  path: typeof condition.path === 'string' ? condition.path : '',
+  mode: CONDITION_MODE_VALUES.has(condition.mode as string)
+    ? (condition.mode as string)
+    : 'full',
+  value_text: toValueText(condition.value),
+  invert: condition.invert === true,
+  pass_missing_key: condition.pass_missing_key === true,
+})
+
+const createDefaultCondition = (): ParamOverrideCondition =>
+  normalizeCondition({})
+
+const normalizeOperation = (
+  operation: Record<string, unknown> = {}
+): ParamOverrideOperation => ({
+  id: nextLocalId(),
+  description:
+    typeof operation.description === 'string' ? operation.description : '',
+  path: typeof operation.path === 'string' ? operation.path : '',
+  mode: OPERATION_MODE_VALUES.has(operation.mode as string)
+    ? (operation.mode as string)
+    : 'set',
+  value_text: toValueText(operation.value),
+  keep_origin: operation.keep_origin === true,
+  from: typeof operation.from === 'string' ? operation.from : '',
+  to: typeof operation.to === 'string' ? operation.to : '',
+  logic: String(operation.logic || 'OR').toUpperCase() === 'AND' ? 'AND' : 'OR',
+  conditions: Array.isArray(operation.conditions)
+    ? (operation.conditions as Record<string, unknown>[]).map(
+        normalizeCondition
+      )
+    : [],
+})
+
+const createDefaultOperation = (): ParamOverrideOperation =>
+  normalizeOperation({ mode: 'set' })
+
+const reorderOperations = (
+  ops: ParamOverrideOperation[],
+  sourceId: string,
+  targetId: string,
+  position: 'before' | 'after' = 'before'
+): ParamOverrideOperation[] => {
+  if (!sourceId || !targetId || sourceId === targetId) return ops
+  const srcIdx = ops.findIndex((o) => o.id === sourceId)
+  if (srcIdx < 0) return ops
+  const next = [...ops]
+  const [moved] = next.splice(srcIdx, 1)
+  let insertIdx = next.findIndex((o) => o.id === targetId)
+  if (insertIdx < 0) return ops
+  if (position === 'after') insertIdx += 1
+  next.splice(insertIdx, 0, moved)
+  return next
+}
+
+const isOperationBlank = (operation: ParamOverrideOperation): boolean => {
+  const hasCondition = operation.conditions.some(
+    (c) =>
+      c.path.trim() ||
+      c.value_text.trim() ||
+      c.mode !== 'full' ||
+      c.invert ||
+      c.pass_missing_key
+  )
+  return (
+    operation.mode === 'set' &&
+    !operation.path.trim() &&
+    !operation.from.trim() &&
+    !operation.to.trim() &&
+    operation.value_text.trim() === '' &&
+    !operation.keep_origin &&
+    !hasCondition
+  )
+}
+
+const getOperationSummary = (
+  operation: ParamOverrideOperation,
+  index: number
+): string => {
+  const mode = operation.mode || 'set'
+  const modeLabel = OPERATION_MODE_LABEL_MAP[mode] || mode
+  if (mode === 'sync_fields') {
+    const from = operation.from.trim()
+    const to = operation.to.trim()
+    return `${index + 1}. ${modeLabel} · ${from || to || '-'}`
+  }
+  const path = operation.path.trim()
+  const from = operation.from.trim()
+  const to = operation.to.trim()
+  return `${index + 1}. ${modeLabel} · ${path || from || to || '-'}`
+}
+
+const getModeTagTailwind = (mode: string): string => {
+  if (mode.includes('header'))
+    return 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-300 border-cyan-500/20'
+  if (mode.includes('replace') || mode.includes('trim'))
+    return 'bg-violet-500/15 text-violet-700 dark:text-violet-300 border-violet-500/20'
+  if (mode.includes('copy') || mode.includes('move'))
+    return 'bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/20'
+  if (mode.includes('error') || mode.includes('prune'))
+    return 'bg-red-500/15 text-red-700 dark:text-red-300 border-red-500/20'
+  if (mode.includes('sync'))
+    return 'bg-green-500/15 text-green-700 dark:text-green-300 border-green-500/20'
+  return 'bg-muted text-muted-foreground'
+}
+
+const getModePathLabel = (mode: string): string => {
+  if (mode === 'set_header' || mode === 'delete_header') return 'Header Name'
+  if (mode === 'prune_objects') return 'Target Path (optional)'
+  return 'Target Field Path'
+}
+
+const getModePathPlaceholder = (mode: string): string => {
+  if (mode === 'set_header') return 'Authorization'
+  if (mode === 'delete_header') return 'X-Debug-Mode'
+  if (mode === 'prune_objects') return 'messages'
+  return 'temperature'
+}
+
+const getModeFromLabel = (mode: string): string => {
+  if (mode === 'replace') return 'Match Text'
+  if (mode === 'regex_replace') return 'Regex Pattern'
+  if (mode === 'copy_header' || mode === 'move_header') return 'Source Header'
+  return 'Source Field'
+}
+
+const getModeFromPlaceholder = (mode: string): string => {
+  if (mode === 'replace') return 'openai/'
+  if (mode === 'regex_replace') return '^gpt-'
+  if (mode === 'copy_header' || mode === 'move_header') return 'Authorization'
+  return 'model'
+}
+
+const getModeToLabel = (mode: string): string => {
+  if (mode === 'replace' || mode === 'regex_replace') return 'Replace With'
+  if (mode === 'copy_header' || mode === 'move_header') return 'Target Header'
+  return 'Target Field'
+}
+
+const getModeToPlaceholder = (mode: string): string => {
+  if (mode === 'replace') return '(leave empty to delete)'
+  if (mode === 'regex_replace') return 'openai/gpt-'
+  if (mode === 'copy_header' || mode === 'move_header') return 'X-Upstream-Auth'
+  return 'original_model'
+}
+
+const getModeValueLabel = (mode: string): string => {
+  if (mode === 'set_header')
+    return 'Header Value (supports string or JSON mapping)'
+  if (mode === 'pass_headers')
+    return 'Pass-through Headers (comma-separated or JSON array)'
+  if (
+    mode === 'trim_prefix' ||
+    mode === 'trim_suffix' ||
+    mode === 'ensure_prefix' ||
+    mode === 'ensure_suffix'
+  )
+    return 'Prefix/Suffix Text'
+  if (mode === 'prune_objects') return 'Prune Rule (string or JSON object)'
+  return 'Value (supports JSON or plain text)'
+}
+
+const getModeValuePlaceholder = (mode: string): string => {
+  if (mode === 'set_header') return 'Bearer sk-xxx'
+  if (mode === 'pass_headers') return 'Authorization, X-Request-Id'
+  if (
+    mode === 'trim_prefix' ||
+    mode === 'trim_suffix' ||
+    mode === 'ensure_prefix' ||
+    mode === 'ensure_suffix'
+  )
+    return 'openai/'
+  if (mode === 'prune_objects') return '{"type":"redacted_thinking"}'
+  return '0.7'
+}
+
+const parseSyncTargetSpec = (spec: string): { type: string; key: string } => {
+  const raw = String(spec ?? '').trim()
+  if (!raw) return { type: 'json', key: '' }
+  const idx = raw.indexOf(':')
+  if (idx < 0) return { type: 'json', key: raw }
+  const prefix = raw.slice(0, idx).trim().toLowerCase()
+  const key = raw.slice(idx + 1).trim()
+  return prefix === 'header' ? { type: 'header', key } : { type: 'json', key }
+}
+
+const buildSyncTargetSpec = (type: string, key: string): string => {
+  const normalizedType = type === 'header' ? 'header' : 'json'
+  const normalizedKey = String(key ?? '').trim()
+  if (!normalizedKey) return ''
+  return `${normalizedType}:${normalizedKey}`
+}
+
+// return_error helpers
+
+type ReturnErrorDraft = {
+  message: string
+  statusCode: number
+  code: string
+  type: string
+  skipRetry: boolean
+  simpleMode: boolean
+}
+
+const parseReturnErrorDraft = (valueText: string): ReturnErrorDraft => {
+  const defaults: ReturnErrorDraft = {
+    message: '',
+    statusCode: 400,
+    code: '',
+    type: '',
+    skipRetry: true,
+    simpleMode: true,
+  }
+  const raw = String(valueText ?? '').trim()
+  if (!raw) return defaults
+  try {
+    const parsed = JSON.parse(raw) as Record<string, unknown>
+    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+      const statusRaw =
+        parsed.status_code !== undefined ? parsed.status_code : parsed.status
+      const statusValue = Number(statusRaw)
+      return {
+        ...defaults,
+        message: String(
+          (parsed.message as string) || (parsed.msg as string) || ''
+        ).trim(),
+        statusCode:
+          Number.isInteger(statusValue) &&
+          statusValue >= 100 &&
+          statusValue <= 599
+            ? statusValue
+            : 400,
+        code: String((parsed.code as string) || '').trim(),
+        type: String((parsed.type as string) || '').trim(),
+        skipRetry: parsed.skip_retry !== false,
+        simpleMode: false,
+      }
+    }
+  } catch {
+    /* treat as plain text */
+  }
+  return { ...defaults, message: raw, simpleMode: true }
+}
+
+const buildReturnErrorValueText = (
+  draft: Partial<ReturnErrorDraft>
+): string => {
+  const message = String(draft.message || '').trim()
+  if (draft.simpleMode) return message
+  const statusCode = Number(draft.statusCode)
+  const payload: Record<string, unknown> = {
+    message,
+    status_code:
+      Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599
+        ? statusCode
+        : 400,
+  }
+  const code = String(draft.code || '').trim()
+  const type = String(draft.type || '').trim()
+  if (code) payload.code = code
+  if (type) payload.type = type
+  if (draft.skipRetry === false) payload.skip_retry = false
+  return JSON.stringify(payload)
+}
+
+// prune_objects helpers
+
+type PruneRule = {
+  id: string
+  path: string
+  mode: string
+  value_text: string
+  invert: boolean
+  pass_missing_key: boolean
+}
+
+type PruneObjectsDraft = {
+  simpleMode: boolean
+  typeText: string
+  logic: string
+  recursive: boolean
+  rules: PruneRule[]
+}
+
+const normalizePruneRule = (rule: Record<string, unknown> = {}): PruneRule => ({
+  id: nextLocalId(),
+  path: typeof rule.path === 'string' ? rule.path : '',
+  mode: CONDITION_MODE_VALUES.has(rule.mode as string)
+    ? (rule.mode as string)
+    : 'full',
+  value_text: toValueText(rule.value),
+  invert: rule.invert === true,
+  pass_missing_key: rule.pass_missing_key === true,
+})
+
+const parsePruneObjectsDraft = (valueText: string): PruneObjectsDraft => {
+  const defaults: PruneObjectsDraft = {
+    simpleMode: true,
+    typeText: '',
+    logic: 'AND',
+    recursive: true,
+    rules: [],
+  }
+  const raw = String(valueText ?? '').trim()
+  if (!raw) return defaults
+  try {
+    const parsed = JSON.parse(raw)
+    if (typeof parsed === 'string')
+      return { ...defaults, typeText: parsed.trim() }
+    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+      const rules: PruneRule[] = []
+      if (
+        parsed.where &&
+        typeof parsed.where === 'object' &&
+        !Array.isArray(parsed.where)
+      ) {
+        for (const [path, value] of Object.entries(
+          parsed.where as Record<string, unknown>
+        )) {
+          rules.push(normalizePruneRule({ path, mode: 'full', value }))
+        }
+      }
+      if (Array.isArray(parsed.conditions)) {
+        for (const item of parsed.conditions) {
+          if (item && typeof item === 'object')
+            rules.push(normalizePruneRule(item))
+        }
+      } else if (
+        parsed.conditions &&
+        typeof parsed.conditions === 'object' &&
+        !Array.isArray(parsed.conditions)
+      ) {
+        for (const [path, value] of Object.entries(
+          parsed.conditions as Record<string, unknown>
+        )) {
+          rules.push(normalizePruneRule({ path, mode: 'full', value }))
+        }
+      }
+      const typeText =
+        parsed.type === undefined ? '' : String(parsed.type).trim()
+      const logic =
+        String(parsed.logic || 'AND').toUpperCase() === 'OR' ? 'OR' : 'AND'
+      const recursive = parsed.recursive !== false
+      const hasAdvancedFields =
+        parsed.logic !== undefined ||
+        parsed.recursive !== undefined ||
+        parsed.where !== undefined ||
+        parsed.conditions !== undefined
+      return {
+        ...defaults,
+        simpleMode: !hasAdvancedFields,
+        typeText,
+        logic,
+        recursive,
+        rules,
+      }
+    }
+    return { ...defaults, typeText: String(parsed ?? '').trim() }
+  } catch {
+    return { ...defaults, typeText: raw }
+  }
+}
+
+const buildPruneObjectsValueText = (draft: PruneObjectsDraft): string => {
+  const typeText = String(draft.typeText || '').trim()
+  if (draft.simpleMode) return typeText
+  const payload: Record<string, unknown> = {}
+  if (typeText) payload.type = typeText
+  if (String(draft.logic || 'AND').toUpperCase() === 'OR') payload.logic = 'OR'
+  if (draft.recursive === false) payload.recursive = false
+  const conditions = (draft.rules || [])
+    .filter((rule) => String(rule.path || '').trim())
+    .map((rule) => {
+      const conditionPayload: Record<string, unknown> = {
+        path: String(rule.path || '').trim(),
+        mode: CONDITION_MODE_VALUES.has(rule.mode) ? rule.mode : 'full',
+      }
+      const valueRaw = String(rule.value_text || '').trim()
+      if (valueRaw !== '') conditionPayload.value = parseLooseValue(valueRaw)
+      if (rule.invert) conditionPayload.invert = true
+      if (rule.pass_missing_key) conditionPayload.pass_missing_key = true
+      return conditionPayload
+    })
+  if (conditions.length > 0) payload.conditions = conditions
+  if (!payload.type && !payload.conditions)
+    return JSON.stringify({ logic: 'AND' })
+  return JSON.stringify(payload)
+}
+
+// pass_headers helpers
+
+const parsePassHeaderNames = (rawValue: unknown): string[] => {
+  if (Array.isArray(rawValue))
+    return rawValue.map((i) => String(i ?? '').trim()).filter(Boolean)
+  if (rawValue && typeof rawValue === 'object') {
+    const obj = rawValue as Record<string, unknown>
+    if (Array.isArray(obj.headers))
+      return obj.headers.map((i) => String(i ?? '').trim()).filter(Boolean)
+    if (obj.header !== undefined) {
+      const single = String(obj.header ?? '').trim()
+      return single ? [single] : []
+    }
+    return []
+  }
+  if (typeof rawValue === 'string')
+    return rawValue
+      .split(',')
+      .map((i) => i.trim())
+      .filter(Boolean)
+  return []
+}
+
+// Condition payload builder
+const buildConditionPayload = (
+  condition: ParamOverrideCondition
+): Record<string, unknown> | null => {
+  const path = condition.path.trim()
+  if (!path) return null
+  const payload: Record<string, unknown> = {
+    path,
+    mode: condition.mode || 'full',
+    value: parseLooseValue(condition.value_text),
+  }
+  if (condition.invert) payload.invert = true
+  if (condition.pass_missing_key) payload.pass_missing_key = true
+  return payload
+}
+
+// Validation
+
+const validateOperations = (
+  operations: ParamOverrideOperation[],
+  t: (key: string, options?: Record<string, unknown>) => string
+): string => {
+  for (let i = 0; i < operations.length; i++) {
+    const op = operations[i]
+    const mode = op.mode || 'set'
+    const meta = MODE_META[mode] || MODE_META.set
+    const line = i + 1
+    const pathValue = op.path.trim()
+    const fromValue = op.from.trim()
+    const toValue = op.to.trim()
+
+    if (meta.path && !pathValue)
+      return t('Rule {{line}} is missing target path', { line })
+    if (FROM_REQUIRED_MODES.has(mode) && !fromValue) {
+      if (!(meta.pathAlias && pathValue))
+        return t('Rule {{line}} is missing source field', { line })
+    }
+    if (TO_REQUIRED_MODES.has(mode) && !toValue) {
+      if (!(meta.pathAlias && pathValue))
+        return t('Rule {{line}} is missing target field', { line })
+    }
+    if (VALUE_REQUIRED_MODES.has(mode) && op.value_text.trim() === '')
+      return t('Rule {{line}} is missing value', { line })
+
+    if (mode === 'return_error') {
+      const raw = op.value_text.trim()
+      if (!raw) return t('Rule {{line}} is missing value', { line })
+      try {
+        const parsed = JSON.parse(raw)
+        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+          if (!String((parsed as Record<string, unknown>).message || '').trim())
+            return t('Rule {{line}} return_error requires a message field', {
+              line,
+            })
+        }
+      } catch {
+        /* plain string is allowed */
+      }
+    }
+
+    if (mode === 'prune_objects') {
+      const raw = op.value_text.trim()
+      if (!raw)
+        return t('Rule {{line}} prune_objects is missing conditions', { line })
+    }
+
+    if (mode === 'pass_headers') {
+      const raw = op.value_text.trim()
+      if (!raw)
+        return t('Rule {{line}} pass_headers is missing header names', { line })
+      const parsed = parseLooseValue(raw)
+      const headers = parsePassHeaderNames(parsed)
+      if (headers.length === 0)
+        return t('Rule {{line}} pass_headers format is invalid', { line })
+    }
+  }
+  return ''
+}
+
+// Parse initial state
+
+type EditorState = {
+  editMode: 'visual' | 'json'
+  visualMode: 'operations' | 'legacy'
+  legacyValue: string
+  operations: ParamOverrideOperation[]
+  jsonText: string
+  jsonError: string
+}
+
+const parseInitialState = (rawValue: string): EditorState => {
+  const text = typeof rawValue === 'string' ? rawValue : ''
+  const trimmed = text.trim()
+  if (!trimmed) {
+    return {
+      editMode: 'visual',
+      visualMode: 'operations',
+      legacyValue: '',
+      operations: [createDefaultOperation()],
+      jsonText: '',
+      jsonError: '',
+    }
+  }
+
+  if (!verifyJSON(trimmed)) {
+    return {
+      editMode: 'json',
+      visualMode: 'operations',
+      legacyValue: '',
+      operations: [createDefaultOperation()],
+      jsonText: text,
+      jsonError: 'Invalid JSON format',
+    }
+  }
+
+  const parsed = JSON.parse(trimmed) as Record<string, unknown>
+  const pretty = JSON.stringify(parsed, null, 2)
+
+  if (
+    parsed &&
+    typeof parsed === 'object' &&
+    !Array.isArray(parsed) &&
+    Array.isArray(parsed.operations)
+  ) {
+    return {
+      editMode: 'visual',
+      visualMode: 'operations',
+      legacyValue: '',
+      operations:
+        (parsed.operations as Record<string, unknown>[]).length > 0
+          ? (parsed.operations as Record<string, unknown>[]).map(
+              normalizeOperation
+            )
+          : [createDefaultOperation()],
+      jsonText: pretty,
+      jsonError: '',
+    }
+  }
+
+  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+    return {
+      editMode: 'visual',
+      visualMode: 'legacy',
+      legacyValue: pretty,
+      operations: [createDefaultOperation()],
+      jsonText: pretty,
+      jsonError: '',
+    }
+  }
+
+  return {
+    editMode: 'json',
+    visualMode: 'operations',
+    legacyValue: '',
+    operations: [createDefaultOperation()],
+    jsonText: pretty,
+    jsonError: '',
+  }
+}
+
+// Build operations JSON
+
+const buildOperationsJson = (
+  sourceOperations: ParamOverrideOperation[],
+  options: { validate: boolean },
+  t: (key: string, options?: Record<string, unknown>) => string
+): string => {
+  const filteredOps = sourceOperations.filter((o) => !isOperationBlank(o))
+  if (filteredOps.length === 0) return ''
+
+  if (options.validate) {
+    const message = validateOperations(filteredOps, t)
+    if (message) throw new Error(message)
+  }
+
+  const payloadOps = filteredOps.map((operation) => {
+    const mode = operation.mode || 'set'
+    const meta = MODE_META[mode] || MODE_META.set
+    const descriptionValue = String(operation.description || '').trim()
+    const pathValue = operation.path.trim()
+    const fromValue = operation.from.trim()
+    const toValue = operation.to.trim()
+    const payload: Record<string, unknown> = { mode }
+    if (descriptionValue) payload.description = descriptionValue
+    if (meta.path) payload.path = pathValue
+    if (meta.pathOptional && pathValue) payload.path = pathValue
+    if (meta.value) payload.value = parseLooseValue(operation.value_text)
+    if (meta.keepOrigin && operation.keep_origin) payload.keep_origin = true
+    if (meta.from) payload.from = fromValue
+    if (!meta.to && operation.to.trim()) payload.to = toValue
+    if (meta.to) payload.to = toValue
+    if (meta.pathAlias) {
+      if (!payload.from && pathValue) payload.from = pathValue
+      if (!payload.to && pathValue) payload.to = pathValue
+    }
+    const conditions = operation.conditions
+      .map(buildConditionPayload)
+      .filter(Boolean)
+    if (conditions.length > 0) {
+      payload.conditions = conditions
+      payload.logic = operation.logic === 'AND' ? 'AND' : 'OR'
+    }
+    return payload
+  })
+
+  return JSON.stringify({ operations: payloadOps }, null, 2)
+}
+
+// ---------------------------------------------------------------------------
+// Component
+// ---------------------------------------------------------------------------
+
+export function ParamOverrideEditorDialog(
+  props: ParamOverrideEditorDialogProps
+) {
+  const { t } = useTranslation()
+
+  const [editMode, setEditMode] = useState<'visual' | 'json'>('visual')
+  const [visualMode, setVisualMode] = useState<'operations' | 'legacy'>(
+    'operations'
+  )
+  const [legacyValue, setLegacyValue] = useState('')
+  const [operations, setOperations] = useState<ParamOverrideOperation[]>([
+    createDefaultOperation(),
+  ])
+  const [jsonText, setJsonText] = useState('')
+  const [jsonError, setJsonError] = useState('')
+  const [operationSearch, setOperationSearch] = useState('')
+  const [selectedOperationId, setSelectedOperationId] = useState('')
+  const [expandedConditions, setExpandedConditions] = useState<
+    Record<string, boolean>
+  >({})
+  const [draggedOperationId, setDraggedOperationId] = useState('')
+  const [dragOverOperationId, setDragOverOperationId] = useState('')
+  const [dragOverPosition, setDragOverPosition] = useState<'before' | 'after'>(
+    'before'
+  )
+  const [templatePresetKey, setTemplatePresetKey] =
+    useState('operations_default')
+
+  // Initialize state when dialog opens
+  useEffect(() => {
+    if (!props.open) return
+    const state = parseInitialState(props.value)
+    setEditMode(state.editMode)
+    setVisualMode(state.visualMode)
+    setLegacyValue(state.legacyValue)
+    setOperations(state.operations)
+    setJsonText(state.jsonText)
+    setJsonError(state.jsonError)
+    setOperationSearch('')
+    setSelectedOperationId(state.operations[0]?.id || '')
+    setExpandedConditions({})
+    setDraggedOperationId('')
+    setDragOverOperationId('')
+    setDragOverPosition('before')
+    if (state.visualMode === 'legacy') {
+      setTemplatePresetKey('legacy_default')
+    } else {
+      setTemplatePresetKey('operations_default')
+    }
+  }, [props.open, props.value])
+
+  // Keep selectedOperationId valid
+  useEffect(() => {
+    if (operations.length === 0) {
+      setSelectedOperationId('')
+      return
+    }
+    if (!operations.some((o) => o.id === selectedOperationId)) {
+      setSelectedOperationId(operations[0].id)
+    }
+  }, [operations, selectedOperationId])
+
+  // Template preset options filtered by group
+  const templatePresetOptions = useMemo(
+    () =>
+      Object.entries(TEMPLATE_PRESET_CONFIG).map(([value, config]) => ({
+        value,
+        label: config.label,
+      })),
+    []
+  )
+
+  const operationCount = useMemo(
+    () => operations.filter((o) => !isOperationBlank(o)).length,
+    [operations]
+  )
+
+  const filteredOperations = useMemo(() => {
+    const keyword = operationSearch.trim().toLowerCase()
+    if (!keyword) return operations
+    return operations.filter((op) => {
+      const searchableText = [
+        op.description,
+        op.mode,
+        op.path,
+        op.from,
+        op.to,
+        op.value_text,
+      ]
+        .filter(Boolean)
+        .join(' ')
+        .toLowerCase()
+      return searchableText.includes(keyword)
+    })
+  }, [operationSearch, operations])
+
+  const selectedOperation = useMemo(
+    () => operations.find((o) => o.id === selectedOperationId),
+    [operations, selectedOperationId]
+  )
+
+  const selectedOperationIndex = useMemo(
+    () => operations.findIndex((o) => o.id === selectedOperationId),
+    [operations, selectedOperationId]
+  )
+
+  const returnErrorDraft = useMemo(() => {
+    if (!selectedOperation || selectedOperation.mode !== 'return_error')
+      return null
+    return parseReturnErrorDraft(selectedOperation.value_text)
+  }, [selectedOperation])
+
+  const pruneObjectsDraft = useMemo(() => {
+    if (!selectedOperation || selectedOperation.mode !== 'prune_objects')
+      return null
+    return parsePruneObjectsDraft(selectedOperation.value_text)
+  }, [selectedOperation])
+
+  const topOperationModes = useMemo(() => {
+    const counts: Record<string, number> = {}
+    for (const op of operations) {
+      const mode = op.mode || 'set'
+      counts[mode] = (counts[mode] || 0) + 1
+    }
+    return Object.entries(counts)
+      .sort((a, b) => b[1] - a[1])
+      .slice(0, 4)
+  }, [operations])
+
+  // ---------------------------------------------------------------------------
+  // Operations
+  // ---------------------------------------------------------------------------
+
+  const updateOperation = useCallback(
+    (operationId: string, patch: Partial<ParamOverrideOperation>) => {
+      setOperations((prev) =>
+        prev.map((o) => (o.id === operationId ? { ...o, ...patch } : o))
+      )
+    },
+    []
+  )
+
+  const addOperation = useCallback(() => {
+    const created = createDefaultOperation()
+    setOperations((prev) => [...prev, created])
+    setSelectedOperationId(created.id)
+  }, [])
+
+  const duplicateOperation = useCallback((operationId: string) => {
+    let insertedId = ''
+    setOperations((prev) => {
+      const idx = prev.findIndex((o) => o.id === operationId)
+      if (idx < 0) return prev
+      const source = prev[idx]
+      const cloned = normalizeOperation({
+        description: source.description,
+        path: source.path,
+        mode: source.mode,
+        value: parseLooseValue(source.value_text),
+        keep_origin: source.keep_origin,
+        from: source.from,
+        to: source.to,
+        logic: source.logic,
+        conditions: source.conditions.map((c) => ({
+          path: c.path,
+          mode: c.mode,
+          value: parseLooseValue(c.value_text),
+          invert: c.invert,
+          pass_missing_key: c.pass_missing_key,
+        })),
+      })
+      insertedId = cloned.id
+      const next = [...prev]
+      next.splice(idx + 1, 0, cloned)
+      return next
+    })
+    if (insertedId) setSelectedOperationId(insertedId)
+  }, [])
+
+  const removeOperation = useCallback((operationId: string) => {
+    setOperations((prev) => {
+      if (prev.length <= 1) return [createDefaultOperation()]
+      return prev.filter((o) => o.id !== operationId)
+    })
+  }, [])
+
+  // Conditions
+  const addCondition = useCallback((operationId: string) => {
+    const created = createDefaultCondition()
+    setOperations((prev) =>
+      prev.map((op) =>
+        op.id === operationId
+          ? { ...op, conditions: [...op.conditions, created] }
+          : op
+      )
+    )
+    setExpandedConditions((prev) => ({ ...prev, [created.id]: true }))
+  }, [])
+
+  const updateCondition = useCallback(
+    (
+      operationId: string,
+      conditionId: string,
+      patch: Partial<ParamOverrideCondition>
+    ) => {
+      setOperations((prev) =>
+        prev.map((op) =>
+          op.id === operationId
+            ? {
+                ...op,
+                conditions: op.conditions.map((c) =>
+                  c.id === conditionId ? { ...c, ...patch } : c
+                ),
+              }
+            : op
+        )
+      )
+    },
+    []
+  )
+
+  const removeCondition = useCallback(
+    (operationId: string, conditionId: string) => {
+      setOperations((prev) =>
+        prev.map((op) =>
+          op.id === operationId
+            ? {
+                ...op,
+                conditions: op.conditions.filter((c) => c.id !== conditionId),
+              }
+            : op
+        )
+      )
+    },
+    []
+  )
+
+  // return_error draft
+  const updateReturnErrorDraft = useCallback(
+    (operationId: string, draftPatch: Partial<ReturnErrorDraft>) => {
+      setOperations((prev) =>
+        prev.map((op) => {
+          if (op.id !== operationId) return op
+          const draft = parseReturnErrorDraft(op.value_text)
+          const nextDraft = { ...draft, ...draftPatch }
+          return {
+            ...op,
+            value_text: buildReturnErrorValueText(nextDraft),
+          }
+        })
+      )
+    },
+    []
+  )
+
+  // prune_objects draft
+  const updatePruneObjectsDraft = useCallback(
+    (
+      operationId: string,
+      updater:
+        | Partial<PruneObjectsDraft>
+        | ((draft: PruneObjectsDraft) => PruneObjectsDraft)
+    ) => {
+      setOperations((prev) =>
+        prev.map((op) => {
+          if (op.id !== operationId) return op
+          const draft = parsePruneObjectsDraft(op.value_text)
+          const nextDraft =
+            typeof updater === 'function'
+              ? updater(draft)
+              : { ...draft, ...updater }
+          return {
+            ...op,
+            value_text: buildPruneObjectsValueText(nextDraft),
+          }
+        })
+      )
+    },
+    []
+  )
+
+  const addPruneRule = useCallback(
+    (operationId: string) => {
+      updatePruneObjectsDraft(operationId, (draft) => ({
+        ...draft,
+        simpleMode: false,
+        rules: [...draft.rules, normalizePruneRule({})],
+      }))
+    },
+    [updatePruneObjectsDraft]
+  )
+
+  const updatePruneRule = useCallback(
+    (operationId: string, ruleId: string, patch: Partial<PruneRule>) => {
+      updatePruneObjectsDraft(operationId, (draft) => ({
+        ...draft,
+        rules: draft.rules.map((r) =>
+          r.id === ruleId ? { ...r, ...patch } : r
+        ),
+      }))
+    },
+    [updatePruneObjectsDraft]
+  )
+
+  const removePruneRule = useCallback(
+    (operationId: string, ruleId: string) => {
+      updatePruneObjectsDraft(operationId, (draft) => ({
+        ...draft,
+        rules: draft.rules.filter((r) => r.id !== ruleId),
+      }))
+    },
+    [updatePruneObjectsDraft]
+  )
+
+  // Drag and drop
+  const resetDragState = useCallback(() => {
+    setDraggedOperationId('')
+    setDragOverOperationId('')
+    setDragOverPosition('before')
+  }, [])
+
+  const handleDragStart = useCallback(
+    (event: DragEvent, operationId: string) => {
+      setDraggedOperationId(operationId)
+      setSelectedOperationId(operationId)
+      event.dataTransfer.effectAllowed = 'move'
+      event.dataTransfer.setData('text/plain', operationId)
+    },
+    []
+  )
+
+  const handleDragOver = useCallback(
+    (event: DragEvent, operationId: string) => {
+      event.preventDefault()
+      if (!draggedOperationId || draggedOperationId === operationId) return
+      const rect = event.currentTarget.getBoundingClientRect()
+      const position: 'before' | 'after' =
+        event.clientY - rect.top > rect.height / 2 ? 'after' : 'before'
+      setDragOverOperationId(operationId)
+      setDragOverPosition(position)
+      event.dataTransfer.dropEffect = 'move'
+    },
+    [draggedOperationId]
+  )
+
+  const handleDrop = useCallback(
+    (event: DragEvent, operationId: string) => {
+      event.preventDefault()
+      const sourceId =
+        draggedOperationId || event.dataTransfer.getData('text/plain')
+      const position =
+        dragOverOperationId === operationId ? dragOverPosition : 'before'
+      if (sourceId && operationId && sourceId !== operationId) {
+        setOperations((prev) =>
+          reorderOperations(prev, sourceId, operationId, position)
+        )
+        setSelectedOperationId(sourceId)
+      }
+      resetDragState()
+    },
+    [draggedOperationId, dragOverOperationId, dragOverPosition, resetDragState]
+  )
+
+  // ---------------------------------------------------------------------------
+  // Mode switching & templates
+  // ---------------------------------------------------------------------------
+
+  const buildVisualJson = useCallback((): string => {
+    if (visualMode === 'legacy') {
+      const trimmed = legacyValue.trim()
+      if (!trimmed) return ''
+      if (!verifyJSON(trimmed))
+        throw new Error(t('Parameter override must be valid JSON format'))
+      const parsed = JSON.parse(trimmed) as unknown
+      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
+        throw new Error(t('Legacy format must be a JSON object'))
+      return JSON.stringify(parsed, null, 2)
+    }
+    return buildOperationsJson(operations, { validate: true }, t)
+  }, [legacyValue, operations, t, visualMode])
+
+  const switchToJsonMode = useCallback(() => {
+    if (editMode === 'json') return
+    try {
+      setJsonText(buildVisualJson())
+      setJsonError('')
+    } catch (error) {
+      toast.error((error as Error).message)
+      if (visualMode === 'legacy') {
+        setJsonText(legacyValue)
+      } else {
+        setJsonText(buildOperationsJson(operations, { validate: false }, t))
+      }
+      setJsonError(
+        (error as Error).message || t('Parameter configuration error')
+      )
+    }
+    setEditMode('json')
+  }, [buildVisualJson, editMode, legacyValue, operations, t, visualMode])
+
+  const switchToVisualMode = useCallback(() => {
+    if (editMode === 'visual') return
+    const trimmed = jsonText.trim()
+    if (!trimmed) {
+      const fallback = createDefaultOperation()
+      setVisualMode('operations')
+      setOperations([fallback])
+      setSelectedOperationId(fallback.id)
+      setLegacyValue('')
+      setJsonError('')
+      setEditMode('visual')
+      return
+    }
+    if (!verifyJSON(trimmed)) {
+      toast.error(t('Parameter override must be valid JSON format'))
+      return
+    }
+    const parsed = JSON.parse(trimmed) as Record<string, unknown>
+    if (
+      parsed &&
+      typeof parsed === 'object' &&
+      !Array.isArray(parsed) &&
+      Array.isArray(parsed.operations)
+    ) {
+      const nextOps =
+        (parsed.operations as Record<string, unknown>[]).length > 0
+          ? (parsed.operations as Record<string, unknown>[]).map(
+              normalizeOperation
+            )
+          : [createDefaultOperation()]
+      setVisualMode('operations')
+      setOperations(nextOps)
+      setSelectedOperationId(nextOps[0]?.id || '')
+      setLegacyValue('')
+      setJsonError('')
+      setEditMode('visual')
+      setTemplatePresetKey('operations_default')
+      return
+    }
+    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+      const fallback = createDefaultOperation()
+      setVisualMode('legacy')
+      setLegacyValue(JSON.stringify(parsed, null, 2))
+      setOperations([fallback])
+      setSelectedOperationId(fallback.id)
+      setJsonError('')
+      setEditMode('visual')
+      setTemplatePresetKey('legacy_default')
+      return
+    }
+    toast.error(t('Parameter override must be a valid JSON object'))
+  }, [editMode, jsonText, t])
+
+  const fillTemplate = useCallback(
+    (mode: 'fill' | 'append') => {
+      const preset =
+        TEMPLATE_PRESET_CONFIG[templatePresetKey] ||
+        TEMPLATE_PRESET_CONFIG.operations_default
+      const payload = preset.payload as Record<string, unknown>
+
+      if (preset.kind === 'legacy') {
+        if (mode === 'append' && visualMode === 'legacy') {
+          const trimmed = legacyValue.trim()
+          let parsedCurrent: Record<string, unknown> = {}
+          if (trimmed) {
+            if (!verifyJSON(trimmed)) {
+              toast.error(t('Current legacy JSON is invalid, cannot append'))
+              return
+            }
+            parsedCurrent = JSON.parse(trimmed) as Record<string, unknown>
+          }
+          const merged = { ...(payload || {}), ...parsedCurrent }
+          const text = JSON.stringify(merged, null, 2)
+          setVisualMode('legacy')
+          setLegacyValue(text)
+          setOperations([createDefaultOperation()])
+          setJsonText(text)
+          setJsonError('')
+          setEditMode('visual')
+        } else {
+          const text = JSON.stringify(payload, null, 2)
+          setVisualMode('legacy')
+          setLegacyValue(text)
+          setOperations([createDefaultOperation()])
+          setJsonText(text)
+          setJsonError('')
+          setEditMode('visual')
+        }
+        return
+      }
+
+      const operationsPayload = ((payload as Record<string, unknown>)
+        .operations || []) as Record<string, unknown>[]
+
+      if (mode === 'append') {
+        const appended = operationsPayload.map(normalizeOperation)
+        const existing =
+          visualMode === 'operations'
+            ? operations.filter((o) => !isOperationBlank(o))
+            : []
+        const nextOps = [...existing, ...appended]
+        setVisualMode('operations')
+        setOperations(nextOps.length > 0 ? nextOps : appended)
+        setSelectedOperationId(nextOps[0]?.id || appended[0]?.id || '')
+        setLegacyValue('')
+        setJsonError('')
+        setEditMode('visual')
+        setJsonText('')
+      } else {
+        const nextOps = operationsPayload.map(normalizeOperation)
+        const finalOps =
+          nextOps.length > 0 ? nextOps : [createDefaultOperation()]
+        setVisualMode('operations')
+        setOperations(finalOps)
+        setSelectedOperationId(finalOps[0]?.id || '')
+        setJsonText(JSON.stringify({ operations: operationsPayload }, null, 2))
+        setJsonError('')
+        setEditMode('visual')
+      }
+    },
+    [legacyValue, operations, templatePresetKey, visualMode, t]
+  )
+
+  const resetEditorState = useCallback(() => {
+    const fallback = createDefaultOperation()
+    setVisualMode('operations')
+    setLegacyValue('')
+    setOperations([fallback])
+    setSelectedOperationId(fallback.id)
+    setJsonText('')
+    setJsonError('')
+    setTemplatePresetKey('operations_default')
+    setEditMode('visual')
+  }, [])
+
+  // JSON mode
+  const handleJsonChange = useCallback(
+    (nextValue: string) => {
+      setJsonText(nextValue)
+      const trimmed = nextValue.trim()
+      if (!trimmed) {
+        setJsonError('')
+        return
+      }
+      setJsonError(verifyJSON(trimmed) ? '' : t('JSON format error'))
+    },
+    [t]
+  )
+
+  const formatJson = useCallback(() => {
+    const trimmed = jsonText.trim()
+    if (!trimmed) return
+    if (!verifyJSON(trimmed)) {
+      toast.error(t('Parameter override must be valid JSON format'))
+      return
+    }
+    setJsonText(JSON.stringify(JSON.parse(trimmed), null, 2))
+    setJsonError('')
+  }, [jsonText, t])
+
+  const visualValidationError = useMemo(() => {
+    if (editMode !== 'visual') return ''
+    try {
+      buildVisualJson()
+      return ''
+    } catch (error) {
+      return (error as Error)?.message || t('Parameter configuration error')
+    }
+  }, [buildVisualJson, editMode, t])
+
+  // Save
+  const handleSave = useCallback(() => {
+    try {
+      let result = ''
+      if (editMode === 'json') {
+        const trimmed = jsonText.trim()
+        if (trimmed) {
+          if (!verifyJSON(trimmed))
+            throw new Error(t('Parameter override must be valid JSON format'))
+          result = JSON.stringify(JSON.parse(trimmed), null, 2)
+        }
+      } else {
+        result = buildVisualJson()
+      }
+      props.onSave(result)
+      props.onOpenChange(false)
+    } catch (error) {
+      toast.error((error as Error).message)
+    }
+  }, [buildVisualJson, editMode, jsonText, props, t])
+
+  // Expand/collapse all conditions
+  const expandAllConditions = useCallback(() => {
+    if (!selectedOperation) return
+    const map: Record<string, boolean> = {}
+    for (const c of selectedOperation.conditions) map[c.id] = true
+    setExpandedConditions((prev) => ({ ...prev, ...map }))
+  }, [selectedOperation])
+
+  const collapseAllConditions = useCallback(() => {
+    if (!selectedOperation) return
+    const map: Record<string, boolean> = {}
+    for (const c of selectedOperation.conditions) map[c.id] = false
+    setExpandedConditions((prev) => ({ ...prev, ...map }))
+  }, [selectedOperation])
+
+  // ---------------------------------------------------------------------------
+  // Render
+  // ---------------------------------------------------------------------------
+
+  return (
+    <Dialog open={props.open} onOpenChange={props.onOpenChange}>
+      <DialogContent className='flex max-h-[90vh] flex-col gap-0 p-0 sm:max-w-5xl'>
+        <DialogHeader className='border-b px-6 py-4'>
+          <DialogTitle>{t('Parameter Override')}</DialogTitle>
+          <DialogDescription>
+            {t(
+              'Create request parameter override rules with a visual editor or raw JSON.'
+            )}
+          </DialogDescription>
+        </DialogHeader>
+
+        {/* Toolbar */}
+        <div className='bg-muted/30 border-b px-4 py-3'>
+          <div className='flex flex-wrap items-center gap-2'>
+            <span className='text-muted-foreground text-xs font-medium'>
+              {t('Mode')}
+            </span>
+            <Button
+              type='button'
+              variant={editMode === 'visual' ? 'default' : 'outline'}
+              size='sm'
+              onClick={switchToVisualMode}
+            >
+              {t('Visual')}
+            </Button>
+            <Button
+              type='button'
+              variant={editMode === 'json' ? 'default' : 'outline'}
+              size='sm'
+              onClick={switchToJsonMode}
+            >
+              {t('JSON Text')}
+            </Button>
+
+            <div className='bg-border mx-1 h-5 w-px' />
+
+            <span className='text-muted-foreground text-xs font-medium'>
+              {t('Template')}
+            </span>
+            <Select
+              value={templatePresetKey}
+              onValueChange={(v) =>
+                setTemplatePresetKey(v || 'operations_default')
+              }
+            >
+              <SelectTrigger className='h-8 w-[220px]'>
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                {templatePresetOptions.map((o) => (
+                  <SelectItem key={o.value} value={o.value}>
+                    {t(o.label)}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            <Button
+              type='button'
+              variant='outline'
+              size='sm'
+              onClick={() => fillTemplate('fill')}
+            >
+              {t('Fill Template')}
+            </Button>
+            <Button
+              type='button'
+              variant='ghost'
+              size='sm'
+              onClick={() => fillTemplate('append')}
+            >
+              {t('Append Template')}
+            </Button>
+            <Button
+              type='button'
+              variant='ghost'
+              size='sm'
+              onClick={resetEditorState}
+            >
+              {t('Reset')}
+            </Button>
+          </div>
+        </div>
+
+        {/* Content */}
+        <div className='min-h-0 flex-1 overflow-hidden'>
+          {editMode === 'visual' ? (
+            visualMode === 'legacy' ? (
+              <div className='p-4'>
+                <p className='text-muted-foreground mb-2 text-sm'>
+                  {t('Legacy Format (JSON Object)')}
+                </p>
+                <Textarea
+                  value={legacyValue}
+                  onChange={(e) => setLegacyValue(e.target.value)}
+                  placeholder={JSON.stringify(LEGACY_TEMPLATE, null, 2)}
+                  rows={14}
+                  className='font-mono text-xs'
+                />
+                <p className='text-muted-foreground mt-2 text-xs'>
+                  {t(
+                    'Edit JSON object directly. Suitable for simple parameter overrides.'
+                  )}
+                </p>
+              </div>
+            ) : (
+              <div className='flex h-full'>
+                {/* Left sidebar */}
+                <div className='flex w-[280px] flex-shrink-0 flex-col border-r'>
+                  <div className='flex items-center justify-between border-b px-3 py-2'>
+                    <div className='flex items-center gap-2'>
+                      <span className='text-sm font-medium'>{t('Rules')}</span>
+                      <Badge variant='secondary'>
+                        {operationCount}/{operations.length}
+                      </Badge>
+                    </div>
+                    <Button
+                      type='button'
+                      variant='ghost'
+                      size='sm'
+                      onClick={addOperation}
+                    >
+                      <Plus className='h-4 w-4' />
+                    </Button>
+                  </div>
+
+                  {topOperationModes.length > 0 && (
+                    <div className='flex flex-wrap gap-1 border-b px-3 py-2'>
+                      {topOperationModes.map(([mode, count]) => (
+                        <span
+                          key={`mode_stat_${mode}`}
+                          className={cn(
+                            'inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
+                            getModeTagTailwind(mode)
+                          )}
+                        >
+                          {t(OPERATION_MODE_LABEL_MAP[mode] || mode)} · {count}
+                        </span>
+                      ))}
+                    </div>
+                  )}
+
+                  <div className='px-3 py-2'>
+                    <div className='relative'>
+                      <Search className='text-muted-foreground absolute top-2.5 left-2.5 h-3.5 w-3.5' />
+                      <Input
+                        value={operationSearch}
+                        onChange={(e) => setOperationSearch(e.target.value)}
+                        placeholder={t('Search rules...')}
+                        className='h-8 pl-8 text-xs'
+                      />
+                    </div>
+                  </div>
+
+                  <ScrollArea className='flex-1'>
+                    <div className='flex flex-col gap-1 px-3 pb-3'>
+                      {filteredOperations.length === 0 ? (
+                        <p className='text-muted-foreground py-4 text-center text-xs'>
+                          {t('No matching rules')}
+                        </p>
+                      ) : (
+                        filteredOperations.map((operation) => {
+                          const index = operations.findIndex(
+                            (o) => o.id === operation.id
+                          )
+                          const isActive = operation.id === selectedOperationId
+                          const isDragging = operation.id === draggedOperationId
+                          const isDropTarget =
+                            operation.id === dragOverOperationId &&
+                            draggedOperationId !== '' &&
+                            draggedOperationId !== operation.id
+                          return (
+                            <div
+                              key={operation.id}
+                              role='button'
+                              tabIndex={0}
+                              draggable={operations.length > 1}
+                              onClick={() =>
+                                setSelectedOperationId(operation.id)
+                              }
+                              onDragStart={(e) =>
+                                handleDragStart(e, operation.id)
+                              }
+                              onDragOver={(e) =>
+                                handleDragOver(e, operation.id)
+                              }
+                              onDrop={(e) => handleDrop(e, operation.id)}
+                              onDragEnd={resetDragState}
+                              onKeyDown={(e: KeyboardEvent) => {
+                                if (e.key === 'Enter' || e.key === ' ') {
+                                  e.preventDefault()
+                                  setSelectedOperationId(operation.id)
+                                }
+                              }}
+                              className={cn(
+                                'cursor-pointer rounded-lg border p-2.5 transition-colors',
+                                isActive
+                                  ? 'border-primary bg-primary/5'
+                                  : 'hover:bg-muted/50',
+                                isDragging && 'opacity-50',
+                                isDropTarget &&
+                                  dragOverPosition === 'before' &&
+                                  'border-t-primary border-t-2',
+                                isDropTarget &&
+                                  dragOverPosition === 'after' &&
+                                  'border-b-primary border-b-2'
+                              )}
+                            >
+                              <div className='flex items-start gap-2'>
+                                <GripVertical
+                                  className={cn(
+                                    'text-muted-foreground mt-0.5 h-3.5 w-3.5 flex-shrink-0',
+                                    operations.length > 1
+                                      ? 'cursor-grab'
+                                      : 'cursor-default'
+                                  )}
+                                />
+                                <div className='min-w-0 flex-1'>
+                                  <div className='flex items-center justify-between gap-1'>
+                                    <span className='text-xs font-semibold'>
+                                      #{index + 1}
+                                    </span>
+                                    <Badge
+                                      variant='outline'
+                                      className='text-[10px]'
+                                    >
+                                      {operation.conditions.length}
+                                    </Badge>
+                                  </div>
+                                  <p className='text-muted-foreground mt-0.5 line-clamp-1 text-[11px]'>
+                                    {getOperationSummary(operation, index)}
+                                  </p>
+                                  {operation.description.trim() && (
+                                    <p className='text-muted-foreground mt-0.5 line-clamp-2 text-[10px]'>
+                                      {operation.description}
+                                    </p>
+                                  )}
+                                  <span
+                                    className={cn(
+                                      'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
+                                      getModeTagTailwind(
+                                        operation.mode || 'set'
+                                      )
+                                    )}
+                                  >
+                                    {t(
+                                      OPERATION_MODE_LABEL_MAP[
+                                        operation.mode || 'set'
+                                      ] ||
+                                        operation.mode ||
+                                        'set'
+                                    )}
+                                  </span>
+                                </div>
+                              </div>
+                            </div>
+                          )
+                        })
+                      )}
+                    </div>
+                  </ScrollArea>
+                </div>
+
+                {/* Right panel - Rule editor */}
+                <div className='flex min-w-0 flex-1 flex-col overflow-y-auto'>
+                  {selectedOperation ? (
+                    <RuleEditor
+                      operation={selectedOperation}
+                      operationIndex={selectedOperationIndex}
+                      operations={operations}
+                      returnErrorDraft={returnErrorDraft}
+                      pruneObjectsDraft={pruneObjectsDraft}
+                      expandedConditions={expandedConditions}
+                      setExpandedConditions={setExpandedConditions}
+                      updateOperation={updateOperation}
+                      duplicateOperation={duplicateOperation}
+                      removeOperation={removeOperation}
+                      addCondition={addCondition}
+                      updateCondition={updateCondition}
+                      removeCondition={removeCondition}
+                      updateReturnErrorDraft={updateReturnErrorDraft}
+                      updatePruneObjectsDraft={updatePruneObjectsDraft}
+                      addPruneRule={addPruneRule}
+                      updatePruneRule={updatePruneRule}
+                      removePruneRule={removePruneRule}
+                      expandAllConditions={expandAllConditions}
+                      collapseAllConditions={collapseAllConditions}
+                    />
+                  ) : (
+                    <div className='flex flex-1 items-center justify-center'>
+                      <p className='text-muted-foreground text-sm'>
+                        {t('Select a rule to edit.')}
+                      </p>
+                    </div>
+                  )}
+
+                  {visualValidationError && (
+                    <div className='border-t px-4 py-2'>
+                      <p className='text-destructive text-xs'>
+                        {visualValidationError}
+                      </p>
+                    </div>
+                  )}
+                </div>
+              </div>
+            )
+          ) : (
+            /* JSON mode */
+            <div className='p-4'>
+              <div className='mb-2 flex items-center gap-2'>
+                <Button
+                  type='button'
+                  variant='outline'
+                  size='sm'
+                  onClick={formatJson}
+                >
+                  {t('Format')}
+                </Button>
+                <span className='text-muted-foreground text-xs'>
+                  {t('Advanced text editing')}
+                </span>
+              </div>
+              <Textarea
+                value={jsonText}
+                onChange={(e) => handleJsonChange(e.target.value)}
+                placeholder={JSON.stringify(OPERATION_TEMPLATE, null, 2)}
+                rows={20}
+                className='font-mono text-xs'
+              />
+              <p className='text-muted-foreground mt-2 text-xs'>
+                {t(
+                  'Edit JSON text directly. Format will be validated on save.'
+                )}
+              </p>
+              {jsonError && (
+                <p className='text-destructive mt-1 text-xs'>{jsonError}</p>
+              )}
+            </div>
+          )}
+        </div>
+
+        {/* Footer */}
+        <DialogFooter className='border-t px-6 py-4'>
+          <Button
+            type='button'
+            variant='outline'
+            onClick={() => props.onOpenChange(false)}
+          >
+            {t('Cancel')}
+          </Button>
+          <Button type='button' onClick={handleSave}>
+            {t('Save')}
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  )
+}
+
+// ---------------------------------------------------------------------------
+// RuleEditor sub-component
+// ---------------------------------------------------------------------------
+
+type RuleEditorProps = {
+  operation: ParamOverrideOperation
+  operationIndex: number
+  operations: ParamOverrideOperation[]
+  returnErrorDraft: ReturnErrorDraft | null
+  pruneObjectsDraft: PruneObjectsDraft | null
+  expandedConditions: Record<string, boolean>
+  setExpandedConditions: React.Dispatch<
+    React.SetStateAction<Record<string, boolean>>
+  >
+  updateOperation: (
+    operationId: string,
+    patch: Partial<ParamOverrideOperation>
+  ) => void
+  duplicateOperation: (operationId: string) => void
+  removeOperation: (operationId: string) => void
+  addCondition: (operationId: string) => void
+  updateCondition: (
+    operationId: string,
+    conditionId: string,
+    patch: Partial<ParamOverrideCondition>
+  ) => void
+  removeCondition: (operationId: string, conditionId: string) => void
+  updateReturnErrorDraft: (
+    operationId: string,
+    draftPatch: Partial<ReturnErrorDraft>
+  ) => void
+  updatePruneObjectsDraft: (
+    operationId: string,
+    updater:
+      | Partial<PruneObjectsDraft>
+      | ((draft: PruneObjectsDraft) => PruneObjectsDraft)
+  ) => void
+  addPruneRule: (operationId: string) => void
+  updatePruneRule: (
+    operationId: string,
+    ruleId: string,
+    patch: Partial<PruneRule>
+  ) => void
+  removePruneRule: (operationId: string, ruleId: string) => void
+  expandAllConditions: () => void
+  collapseAllConditions: () => void
+}
+
+function RuleEditor(ruleEditorProps: RuleEditorProps) {
+  const { t } = useTranslation()
+  const operation = ruleEditorProps.operation
+  const mode = operation.mode || 'set'
+  const meta = MODE_META[mode] || MODE_META.set
+  const conditions = operation.conditions
+  const syncFromTarget =
+    mode === 'sync_fields' ? parseSyncTargetSpec(operation.from) : null
+  const syncToTarget =
+    mode === 'sync_fields' ? parseSyncTargetSpec(operation.to) : null
+
+  return (
+    <ScrollArea className='flex-1'>
+      <div className='space-y-4 p-4'>
+        {/* Header */}
+        <div className='flex items-center justify-between'>
+          <div className='flex items-center gap-2'>
+            <Badge variant='outline'>
+              #{ruleEditorProps.operationIndex + 1}
+            </Badge>
+            <span className='text-muted-foreground line-clamp-1 text-xs'>
+              {getOperationSummary(operation, ruleEditorProps.operationIndex)}
+            </span>
+          </div>
+          <div className='flex items-center gap-1'>
+            <Button
+              type='button'
+              variant='ghost'
+              size='sm'
+              onClick={() => ruleEditorProps.duplicateOperation(operation.id)}
+            >
+              <Copy className='mr-1 h-3.5 w-3.5' />
+              {t('Duplicate')}
+            </Button>
+            <Button
+              type='button'
+              variant='ghost'
+              size='sm'
+              className='text-destructive hover:text-destructive'
+              onClick={() => ruleEditorProps.removeOperation(operation.id)}
+            >
+              <Trash2 className='mr-1 h-3.5 w-3.5' />
+              {t('Delete')}
+            </Button>
+          </div>
+        </div>
+
+        {/* Operation type + path */}
+        <div className='grid gap-3 sm:grid-cols-2'>
+          <div className='space-y-1.5'>
+            <label className='text-xs font-medium'>{t('Operation Type')}</label>
+            <Select
+              value={mode}
+              onValueChange={(nextMode) =>
+                ruleEditorProps.updateOperation(operation.id, {
+                  mode: nextMode,
+                })
+              }
+            >
+              <SelectTrigger className='h-9'>
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                {OPERATION_MODE_OPTIONS.map((o) => (
+                  <SelectItem key={o.value} value={o.value}>
+                    {t(o.label)}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          </div>
+          {(meta.path || meta.pathOptional) && (
+            <div className='space-y-1.5'>
+              <label className='text-xs font-medium'>
+                {t(getModePathLabel(mode))}
+              </label>
+              <Input
+                value={operation.path}
+                onChange={(e) =>
+                  ruleEditorProps.updateOperation(operation.id, {
+                    path: e.target.value,
+                  })
+                }
+                placeholder={getModePathPlaceholder(mode)}
+                className='h-9'
+              />
+            </div>
+          )}
+        </div>
+
+        {/* Mode description */}
+        {MODE_DESCRIPTIONS[mode] && (
+          <p className='text-muted-foreground text-xs'>
+            {t(MODE_DESCRIPTIONS[mode])}
+          </p>
+        )}
+
+        {/* Description */}
+        <div className='space-y-1.5'>
+          <div className='flex items-center justify-between'>
+            <label className='text-xs font-medium'>
+              {t('Rule Description (optional)')}
+            </label>
+            <span className='text-muted-foreground text-[10px]'>
+              {operation.description.length}/180
+            </span>
+          </div>
+          <Input
+            value={operation.description}
+            onChange={(e) =>
+              ruleEditorProps.updateOperation(operation.id, {
+                description: e.target.value,
+              })
+            }
+            placeholder={t(
+              'e.g. Clean tool parameters to avoid upstream validation errors'
+            )}
+            maxLength={180}
+            className='h-9'
+          />
+        </div>
+
+        {/* Value section */}
+        {meta.value &&
+          (mode === 'return_error' && ruleEditorProps.returnErrorDraft ? (
+            <ReturnErrorEditor
+              operationId={operation.id}
+              draft={ruleEditorProps.returnErrorDraft}
+              updateDraft={ruleEditorProps.updateReturnErrorDraft}
+            />
+          ) : mode === 'prune_objects' && ruleEditorProps.pruneObjectsDraft ? (
+            <PruneObjectsEditor
+              operationId={operation.id}
+              draft={ruleEditorProps.pruneObjectsDraft}
+              updateDraft={ruleEditorProps.updatePruneObjectsDraft}
+              addRule={ruleEditorProps.addPruneRule}
+              updateRule={ruleEditorProps.updatePruneRule}
+              removeRule={ruleEditorProps.removePruneRule}
+            />
+          ) : (
+            <div className='space-y-1.5'>
+              <div className='flex items-center justify-between'>
+                <label className='text-xs font-medium'>
+                  {t(getModeValueLabel(mode))}
+                </label>
+                {operation.value_text.trim().startsWith('{') && (
+                  <Button
+                    type='button'
+                    variant='ghost'
+                    size='sm'
+                    className='text-muted-foreground h-auto px-1.5 py-0.5 text-xs'
+                    onClick={() => {
+                      try {
+                        const parsed = JSON.parse(operation.value_text)
+                        ruleEditorProps.updateOperation(operation.id, {
+                          value_text: JSON.stringify(parsed, null, 2),
+                        })
+                      } catch (_e) {
+                        /* not valid JSON */
+                      }
+                    }}
+                  >
+                    {t('Format')}
+                  </Button>
+                )}
+              </div>
+              <Textarea
+                value={operation.value_text}
+                onChange={(e) =>
+                  ruleEditorProps.updateOperation(operation.id, {
+                    value_text: e.target.value,
+                  })
+                }
+                placeholder={getModeValuePlaceholder(mode)}
+                rows={3}
+                className='max-h-[200px] resize-y overflow-y-auto font-mono text-xs'
+              />
+            </div>
+          ))}
+
+        {/* keep_origin */}
+        {meta.keepOrigin && (
+          <div className='flex items-center justify-between rounded-lg border px-3 py-2'>
+            <p className='text-sm font-medium'>
+              {t('Keep original value (skip if target exists)')}
+            </p>
+            <Switch
+              checked={operation.keep_origin}
+              onCheckedChange={(checked) =>
+                ruleEditorProps.updateOperation(operation.id, {
+                  keep_origin: checked,
+                })
+              }
+            />
+          </div>
+        )}
+
+        {/* sync_fields */}
+        {mode === 'sync_fields' && syncFromTarget && syncToTarget ? (
+          <SyncFieldsEditor
+            operationId={operation.id}
+            syncFromTarget={syncFromTarget}
+            syncToTarget={syncToTarget}
+            updateOperation={ruleEditorProps.updateOperation}
+          />
+        ) : (meta.from || meta.to !== undefined) && mode !== 'sync_fields' ? (
+          <div className='grid gap-3 sm:grid-cols-2'>
+            {(meta.from || meta.to === false) && (
+              <div className='space-y-1.5'>
+                <label className='text-xs font-medium'>
+                  {t(getModeFromLabel(mode))}
+                </label>
+                <Input
+                  value={operation.from}
+                  onChange={(e) =>
+                    ruleEditorProps.updateOperation(operation.id, {
+                      from: e.target.value,
+                    })
+                  }
+                  placeholder={getModeFromPlaceholder(mode)}
+                  className='h-9'
+                />
+              </div>
+            )}
+            {(meta.to || meta.to === false) && (
+              <div className='space-y-1.5'>
+                <label className='text-xs font-medium'>
+                  {t(getModeToLabel(mode))}
+                </label>
+                <Input
+                  value={operation.to}
+                  onChange={(e) =>
+                    ruleEditorProps.updateOperation(operation.id, {
+                      to: e.target.value,
+                    })
+                  }
+                  placeholder={getModeToPlaceholder(mode)}
+                  className='h-9'
+                />
+              </div>
+            )}
+          </div>
+        ) : null}
+
+        {/* Conditions */}
+        <div className='rounded-lg border p-3'>
+          <div className='mb-2 flex items-center justify-between'>
+            <div className='flex items-center gap-2'>
+              <span className='text-sm font-medium'>{t('Conditions')}</span>
+              <Select
+                value={operation.logic || 'OR'}
+                onValueChange={(v) =>
+                  ruleEditorProps.updateOperation(operation.id, {
+                    logic: v,
+                  })
+                }
+              >
+                <SelectTrigger className='h-7 w-[120px] text-xs'>
+                  <SelectValue />
+                </SelectTrigger>
+                <SelectContent>
+                  <SelectItem value='OR'>{t('Match Any (OR)')}</SelectItem>
+                  <SelectItem value='AND'>{t('Match All (AND)')}</SelectItem>
+                </SelectContent>
+              </Select>
+            </div>
+            <div className='flex items-center gap-1'>
+              {conditions.length > 0 && (
+                <>
+                  <Button
+                    type='button'
+                    variant='ghost'
+                    size='sm'
+                    className='h-7 text-xs'
+                    onClick={ruleEditorProps.expandAllConditions}
+                  >
+                    <ChevronDown className='mr-1 h-3 w-3' />
+                    {t('Expand All')}
+                  </Button>
+                  <Button
+                    type='button'
+                    variant='ghost'
+                    size='sm'
+                    className='h-7 text-xs'
+                    onClick={ruleEditorProps.collapseAllConditions}
+                  >
+                    <ChevronUp className='mr-1 h-3 w-3' />
+                    {t('Collapse All')}
+                  </Button>
+                </>
+              )}
+              <Button
+                type='button'
+                variant='outline'
+                size='sm'
+                className='h-7 text-xs'
+                onClick={() => ruleEditorProps.addCondition(operation.id)}
+              >
+                <Plus className='mr-1 h-3 w-3' />
+                {t('Add Condition')}
+              </Button>
+            </div>
+          </div>
+
+          {conditions.length === 0 ? (
+            <p className='text-muted-foreground text-xs'>
+              {t('When no conditions are set, the operation always executes.')}
+            </p>
+          ) : (
+            <div className='space-y-2'>
+              {conditions.map((condition, conditionIndex) => (
+                <ConditionEditor
+                  key={condition.id}
+                  condition={condition}
+                  conditionIndex={conditionIndex}
+                  operationId={operation.id}
+                  expanded={
+                    ruleEditorProps.expandedConditions[condition.id] ?? false
+                  }
+                  onExpandedChange={(expanded) =>
+                    ruleEditorProps.setExpandedConditions((prev) => ({
+                      ...prev,
+                      [condition.id]: expanded,
+                    }))
+                  }
+                  updateCondition={ruleEditorProps.updateCondition}
+                  removeCondition={ruleEditorProps.removeCondition}
+                />
+              ))}
+            </div>
+          )}
+        </div>
+      </div>
+    </ScrollArea>
+  )
+}
+
+// ---------------------------------------------------------------------------
+// ConditionEditor
+// ---------------------------------------------------------------------------
+
+type ConditionEditorProps = {
+  condition: ParamOverrideCondition
+  conditionIndex: number
+  operationId: string
+  expanded: boolean
+  onExpandedChange: (expanded: boolean) => void
+  updateCondition: (
+    operationId: string,
+    conditionId: string,
+    patch: Partial<ParamOverrideCondition>
+  ) => void
+  removeCondition: (operationId: string, conditionId: string) => void
+}
+
+function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
+  const { t } = useTranslation()
+  const condition = conditionEditorProps.condition
+
+  return (
+    <Collapsible
+      open={conditionEditorProps.expanded}
+      onOpenChange={conditionEditorProps.onExpandedChange}
+    >
+      <div className='rounded-md border'>
+        <CollapsibleTrigger className='hover:bg-muted/50 flex w-full items-center justify-between px-3 py-2'>
+          <div className='flex items-center gap-2'>
+            <Badge variant='outline' className='text-[10px]'>
+              C{conditionEditorProps.conditionIndex + 1}
+            </Badge>
+            <span className='text-muted-foreground text-xs'>
+              {condition.path || t('Path not set')}
+            </span>
+          </div>
+          {conditionEditorProps.expanded ? (
+            <ChevronUp className='text-muted-foreground h-3.5 w-3.5' />
+          ) : (
+            <ChevronDown className='text-muted-foreground h-3.5 w-3.5' />
+          )}
+        </CollapsibleTrigger>
+        <CollapsibleContent>
+          <div className='space-y-3 border-t px-3 py-3'>
+            <div className='flex items-center justify-between'>
+              <span className='text-muted-foreground text-xs'>
+                {t('Condition Settings')}
+              </span>
+              <Button
+                type='button'
+                variant='ghost'
+                size='sm'
+                className='text-destructive hover:text-destructive h-7 text-xs'
+                onClick={() =>
+                  conditionEditorProps.removeCondition(
+                    conditionEditorProps.operationId,
+                    condition.id
+                  )
+                }
+              >
+                <Trash2 className='mr-1 h-3 w-3' />
+                {t('Delete Condition')}
+              </Button>
+            </div>
+            <div className='grid gap-2 sm:grid-cols-3'>
+              <div className='space-y-1'>
+                <label className='text-[10px] font-medium'>
+                  {t('Field Path')}
+                </label>
+                <Input
+                  value={condition.path}
+                  onChange={(e) =>
+                    conditionEditorProps.updateCondition(
+                      conditionEditorProps.operationId,
+                      condition.id,
+                      { path: e.target.value }
+                    )
+                  }
+                  placeholder='model'
+                  className='h-8 text-xs'
+                />
+              </div>
+              <div className='space-y-1'>
+                <label className='text-[10px] font-medium'>
+                  {t('Match Mode')}
+                </label>
+                <Select
+                  value={condition.mode}
+                  onValueChange={(v) =>
+                    conditionEditorProps.updateCondition(
+                      conditionEditorProps.operationId,
+                      condition.id,
+                      { mode: v }
+                    )
+                  }
+                >
+                  <SelectTrigger className='h-8 text-xs'>
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {CONDITION_MODE_OPTIONS.map((o) => (
+                      <SelectItem key={o.value} value={o.value}>
+                        {t(o.label)}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+              <div className='space-y-1'>
+                <label className='text-[10px] font-medium'>
+                  {t('Match Value')}
+                </label>
+                <Input
+                  value={condition.value_text}
+                  onChange={(e) =>
+                    conditionEditorProps.updateCondition(
+                      conditionEditorProps.operationId,
+                      condition.id,
+                      { value_text: e.target.value }
+                    )
+                  }
+                  placeholder='gpt'
+                  className='h-8 text-xs'
+                />
+              </div>
+            </div>
+            <div className='flex flex-wrap gap-4'>
+              <label className='flex items-center gap-2 text-xs'>
+                <Switch
+                  checked={condition.invert}
+                  onCheckedChange={(checked) =>
+                    conditionEditorProps.updateCondition(
+                      conditionEditorProps.operationId,
+                      condition.id,
+                      { invert: checked }
+                    )
+                  }
+                />
+                {t('Invert match')}
+              </label>
+              <label className='flex items-center gap-2 text-xs'>
+                <Switch
+                  checked={condition.pass_missing_key}
+                  onCheckedChange={(checked) =>
+                    conditionEditorProps.updateCondition(
+                      conditionEditorProps.operationId,
+                      condition.id,
+                      { pass_missing_key: checked }
+                    )
+                  }
+                />
+                {t('Pass when key is missing')}
+              </label>
+            </div>
+          </div>
+        </CollapsibleContent>
+      </div>
+    </Collapsible>
+  )
+}
+
+// ---------------------------------------------------------------------------
+// ReturnErrorEditor
+// ---------------------------------------------------------------------------
+
+type ReturnErrorEditorProps = {
+  operationId: string
+  draft: ReturnErrorDraft
+  updateDraft: (
+    operationId: string,
+    draftPatch: Partial<ReturnErrorDraft>
+  ) => void
+}
+
+function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
+  const { t } = useTranslation()
+  const draft = returnErrorEditorProps.draft
+
+  return (
+    <div className='rounded-lg border p-3'>
+      <div className='mb-2 flex items-center justify-between'>
+        <span className='text-sm font-medium'>
+          {t('Custom Error Response')}
+        </span>
+        <div className='flex items-center gap-1'>
+          <span className='text-muted-foreground text-xs'>{t('Mode')}</span>
+          <Button
+            type='button'
+            variant={draft.simpleMode ? 'default' : 'outline'}
+            size='sm'
+            className='h-7 text-xs'
+            onClick={() =>
+              returnErrorEditorProps.updateDraft(
+                returnErrorEditorProps.operationId,
+                { simpleMode: true }
+              )
+            }
+          >
+            {t('Simple')}
+          </Button>
+          <Button
+            type='button'
+            variant={draft.simpleMode ? 'outline' : 'default'}
+            size='sm'
+            className='h-7 text-xs'
+            onClick={() =>
+              returnErrorEditorProps.updateDraft(
+                returnErrorEditorProps.operationId,
+                { simpleMode: false }
+              )
+            }
+          >
+            {t('Advanced')}
+          </Button>
+        </div>
+      </div>
+
+      <div className='space-y-1.5'>
+        <label className='text-xs font-medium'>
+          {t('Error Message (required)')}
+        </label>
+        <Textarea
+          value={draft.message}
+          onChange={(e) =>
+            returnErrorEditorProps.updateDraft(
+              returnErrorEditorProps.operationId,
+              { message: e.target.value }
+            )
+          }
+          placeholder={t('e.g. This request does not meet access policy')}
+          rows={2}
+          className='text-xs'
+        />
+      </div>
+
+      {draft.simpleMode ? (
+        <p className='text-muted-foreground mt-2 text-xs'>
+          {t(
+            'Simple mode only returns message; status code and error type use system defaults.'
+          )}
+        </p>
+      ) : (
+        <>
+          <div className='mt-3 grid gap-3 sm:grid-cols-3'>
+            <div className='space-y-1'>
+              <label className='text-xs font-medium'>{t('Status Code')}</label>
+              <Input
+                value={String(draft.statusCode ?? '')}
+                onChange={(e) =>
+                  returnErrorEditorProps.updateDraft(
+                    returnErrorEditorProps.operationId,
+                    { statusCode: parseInt(e.target.value, 10) || 400 }
+                  )
+                }
+                placeholder='400'
+                className='h-8 text-xs'
+              />
+            </div>
+            <div className='space-y-1'>
+              <label className='text-xs font-medium'>
+                {t('Error Code (optional)')}
+              </label>
+              <Input
+                value={draft.code}
+                onChange={(e) =>
+                  returnErrorEditorProps.updateDraft(
+                    returnErrorEditorProps.operationId,
+                    { code: e.target.value }
+                  )
+                }
+                placeholder='forced_bad_request'
+                className='h-8 text-xs'
+              />
+            </div>
+            <div className='space-y-1'>
+              <label className='text-xs font-medium'>
+                {t('Error Type (optional)')}
+              </label>
+              <Input
+                value={draft.type}
+                onChange={(e) =>
+                  returnErrorEditorProps.updateDraft(
+                    returnErrorEditorProps.operationId,
+                    { type: e.target.value }
+                  )
+                }
+                placeholder='invalid_request_error'
+                className='h-8 text-xs'
+              />
+            </div>
+          </div>
+          <div className='mt-2 flex items-center gap-2'>
+            <span className='text-muted-foreground text-xs'>
+              {t('Retry Suggestion')}
+            </span>
+            <Button
+              type='button'
+              variant={draft.skipRetry ? 'default' : 'outline'}
+              size='sm'
+              className='h-7 text-xs'
+              onClick={() =>
+                returnErrorEditorProps.updateDraft(
+                  returnErrorEditorProps.operationId,
+                  { skipRetry: true }
+                )
+              }
+            >
+              {t('Stop Retry')}
+            </Button>
+            <Button
+              type='button'
+              variant={draft.skipRetry ? 'outline' : 'default'}
+              size='sm'
+              className='h-7 text-xs'
+              onClick={() =>
+                returnErrorEditorProps.updateDraft(
+                  returnErrorEditorProps.operationId,
+                  { skipRetry: false }
+                )
+              }
+            >
+              {t('Allow Retry')}
+            </Button>
+          </div>
+          <div className='mt-2 flex flex-wrap gap-1'>
+            {[
+              {
+                label: 'Bad Request',
+                statusCode: 400,
+                code: 'invalid_request',
+                type: 'invalid_request_error',
+              },
+              {
+                label: 'Unauthorized',
+                statusCode: 401,
+                code: 'unauthorized',
+                type: 'authentication_error',
+              },
+              {
+                label: 'Rate Limited',
+                statusCode: 429,
+                code: 'rate_limited',
+                type: 'rate_limit_error',
+              },
+            ].map((preset) => (
+              <Button
+                key={preset.code}
+                type='button'
+                variant='outline'
+                size='sm'
+                className='h-6 text-[10px]'
+                onClick={() =>
+                  returnErrorEditorProps.updateDraft(
+                    returnErrorEditorProps.operationId,
+                    {
+                      statusCode: preset.statusCode,
+                      code: preset.code,
+                      type: preset.type,
+                    }
+                  )
+                }
+              >
+                {t(preset.label)}
+              </Button>
+            ))}
+          </div>
+        </>
+      )}
+    </div>
+  )
+}
+
+// ---------------------------------------------------------------------------
+// PruneObjectsEditor
+// ---------------------------------------------------------------------------
+
+type PruneObjectsEditorProps = {
+  operationId: string
+  draft: PruneObjectsDraft
+  updateDraft: (
+    operationId: string,
+    updater:
+      | Partial<PruneObjectsDraft>
+      | ((draft: PruneObjectsDraft) => PruneObjectsDraft)
+  ) => void
+  addRule: (operationId: string) => void
+  updateRule: (
+    operationId: string,
+    ruleId: string,
+    patch: Partial<PruneRule>
+  ) => void
+  removeRule: (operationId: string, ruleId: string) => void
+}
+
+function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
+  const { t } = useTranslation()
+  const draft = pruneObjectsEditorProps.draft
+
+  return (
+    <div className='rounded-lg border p-3'>
+      <div className='mb-2 flex items-center justify-between'>
+        <span className='text-sm font-medium'>{t('Object Prune Rules')}</span>
+        <div className='flex items-center gap-1'>
+          <span className='text-muted-foreground text-xs'>{t('Mode')}</span>
+          <Button
+            type='button'
+            variant={draft.simpleMode ? 'default' : 'outline'}
+            size='sm'
+            className='h-7 text-xs'
+            onClick={() =>
+              pruneObjectsEditorProps.updateDraft(
+                pruneObjectsEditorProps.operationId,
+                { simpleMode: true }
+              )
+            }
+          >
+            {t('Simple')}
+          </Button>
+          <Button
+            type='button'
+            variant={draft.simpleMode ? 'outline' : 'default'}
+            size='sm'
+            className='h-7 text-xs'
+            onClick={() =>
+              pruneObjectsEditorProps.updateDraft(
+                pruneObjectsEditorProps.operationId,
+                { simpleMode: false }
+              )
+            }
+          >
+            {t('Advanced')}
+          </Button>
+        </div>
+      </div>
+
+      <div className='space-y-1.5'>
+        <label className='text-xs font-medium'>{t('Type (common)')}</label>
+        <Input
+          value={draft.typeText}
+          onChange={(e) =>
+            pruneObjectsEditorProps.updateDraft(
+              pruneObjectsEditorProps.operationId,
+              { typeText: e.target.value }
+            )
+          }
+          placeholder='redacted_thinking'
+          className='h-8 text-xs'
+        />
+      </div>
+
+      {draft.simpleMode ? (
+        <p className='text-muted-foreground mt-2 text-xs'>
+          {t('Simple mode: prune objects by type, e.g. redacted_thinking.')}
+        </p>
+      ) : (
+        <>
+          <div className='mt-3 grid gap-3 sm:grid-cols-2'>
+            <div className='space-y-1'>
+              <label className='text-xs font-medium'>{t('Logic')}</label>
+              <Select
+                value={draft.logic}
+                onValueChange={(v) =>
+                  pruneObjectsEditorProps.updateDraft(
+                    pruneObjectsEditorProps.operationId,
+                    { logic: v || 'AND' }
+                  )
+                }
+              >
+                <SelectTrigger className='h-8 text-xs'>
+                  <SelectValue />
+                </SelectTrigger>
+                <SelectContent>
+                  <SelectItem value='AND'>
+                    {t('All Must Match (AND)')}
+                  </SelectItem>
+                  <SelectItem value='OR'>{t('Any Match (OR)')}</SelectItem>
+                </SelectContent>
+              </Select>
+            </div>
+            <div className='space-y-1'>
+              <label className='text-xs font-medium'>
+                {t('Recursion Strategy')}
+              </label>
+              <div className='flex gap-1'>
+                <Button
+                  type='button'
+                  variant={draft.recursive ? 'default' : 'outline'}
+                  size='sm'
+                  className='h-8 text-xs'
+                  onClick={() =>
+                    pruneObjectsEditorProps.updateDraft(
+                      pruneObjectsEditorProps.operationId,
+                      { recursive: true }
+                    )
+                  }
+                >
+                  {t('Recursive')}
+                </Button>
+                <Button
+                  type='button'
+                  variant={draft.recursive ? 'outline' : 'default'}
+                  size='sm'
+                  className='h-8 text-xs'
+                  onClick={() =>
+                    pruneObjectsEditorProps.updateDraft(
+                      pruneObjectsEditorProps.operationId,
+                      { recursive: false }
+                    )
+                  }
+                >
+                  {t('Current Level Only')}
+                </Button>
+              </div>
+            </div>
+          </div>
+
+          <div className='bg-muted/30 mt-3 rounded-md border p-2'>
+            <div className='mb-2 flex items-center justify-between'>
+              <span className='text-xs font-medium'>
+                {t('Additional Conditions')}
+              </span>
+              <Button
+                type='button'
+                variant='outline'
+                size='sm'
+                className='h-7 text-xs'
+                onClick={() =>
+                  pruneObjectsEditorProps.addRule(
+                    pruneObjectsEditorProps.operationId
+                  )
+                }
+              >
+                <Plus className='mr-1 h-3 w-3' />
+                {t('Add Condition')}
+              </Button>
+            </div>
+            {draft.rules.length === 0 ? (
+              <p className='text-muted-foreground text-xs'>
+                {t(
+                  'Without additional conditions, only the type above is used for pruning.'
+                )}
+              </p>
+            ) : (
+              <div className='space-y-2'>
+                {draft.rules.map((rule, ruleIndex) => (
+                  <div
+                    key={rule.id}
+                    className='bg-background rounded-md border p-2'
+                  >
+                    <div className='mb-1 flex items-center justify-between'>
+                      <Badge variant='outline' className='text-[10px]'>
+                        R{ruleIndex + 1}
+                      </Badge>
+                      <Button
+                        type='button'
+                        variant='ghost'
+                        size='sm'
+                        className='text-destructive hover:text-destructive h-6 text-[10px]'
+                        onClick={() =>
+                          pruneObjectsEditorProps.removeRule(
+                            pruneObjectsEditorProps.operationId,
+                            rule.id
+                          )
+                        }
+                      >
+                        <Trash2 className='mr-1 h-3 w-3' />
+                        {t('Delete')}
+                      </Button>
+                    </div>
+                    <div className='grid gap-2 sm:grid-cols-3'>
+                      <div className='space-y-0.5'>
+                        <label className='text-[10px] font-medium'>
+                          {t('Field Path')}
+                        </label>
+                        <Input
+                          value={rule.path}
+                          onChange={(e) =>
+                            pruneObjectsEditorProps.updateRule(
+                              pruneObjectsEditorProps.operationId,
+                              rule.id,
+                              { path: e.target.value }
+                            )
+                          }
+                          placeholder='type'
+                          className='h-7 text-xs'
+                        />
+                      </div>
+                      <div className='space-y-0.5'>
+                        <label className='text-[10px] font-medium'>
+                          {t('Match Mode')}
+                        </label>
+                        <Select
+                          value={rule.mode}
+                          onValueChange={(v) =>
+                            pruneObjectsEditorProps.updateRule(
+                              pruneObjectsEditorProps.operationId,
+                              rule.id,
+                              { mode: v }
+                            )
+                          }
+                        >
+                          <SelectTrigger className='h-7 text-xs'>
+                            <SelectValue />
+                          </SelectTrigger>
+                          <SelectContent>
+                            {CONDITION_MODE_OPTIONS.map((o) => (
+                              <SelectItem key={o.value} value={o.value}>
+                                {t(o.label)}
+                              </SelectItem>
+                            ))}
+                          </SelectContent>
+                        </Select>
+                      </div>
+                      <div className='space-y-0.5'>
+                        <label className='text-[10px] font-medium'>
+                          {t('Match Value (optional)')}
+                        </label>
+                        <Input
+                          value={rule.value_text}
+                          onChange={(e) =>
+                            pruneObjectsEditorProps.updateRule(
+                              pruneObjectsEditorProps.operationId,
+                              rule.id,
+                              { value_text: e.target.value }
+                            )
+                          }
+                          placeholder='redacted_thinking'
+                          className='h-7 text-xs'
+                        />
+                      </div>
+                    </div>
+                    <div className='mt-1.5 flex flex-wrap gap-3'>
+                      <label className='flex items-center gap-1.5 text-[10px]'>
+                        <Switch
+                          checked={rule.invert}
+                          onCheckedChange={(checked) =>
+                            pruneObjectsEditorProps.updateRule(
+                              pruneObjectsEditorProps.operationId,
+                              rule.id,
+                              { invert: checked }
+                            )
+                          }
+                        />
+                        {t('Invert match')}
+                      </label>
+                      <label className='flex items-center gap-1.5 text-[10px]'>
+                        <Switch
+                          checked={rule.pass_missing_key}
+                          onCheckedChange={(checked) =>
+                            pruneObjectsEditorProps.updateRule(
+                              pruneObjectsEditorProps.operationId,
+                              rule.id,
+                              { pass_missing_key: checked }
+                            )
+                          }
+                        />
+                        {t('Pass when key is missing')}
+                      </label>
+                    </div>
+                  </div>
+                ))}
+              </div>
+            )}
+          </div>
+        </>
+      )}
+    </div>
+  )
+}
+
+// ---------------------------------------------------------------------------
+// SyncFieldsEditor
+// ---------------------------------------------------------------------------
+
+type SyncFieldsEditorProps = {
+  operationId: string
+  syncFromTarget: { type: string; key: string }
+  syncToTarget: { type: string; key: string }
+  updateOperation: (
+    operationId: string,
+    patch: Partial<ParamOverrideOperation>
+  ) => void
+}
+
+function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
+  const { t } = useTranslation()
+  return (
+    <div className='space-y-3'>
+      <label className='text-xs font-medium'>{t('Sync Endpoints')}</label>
+      <div className='grid gap-3 sm:grid-cols-2'>
+        <div className='space-y-1.5'>
+          <label className='text-[10px] font-medium'>
+            {t('Source Endpoint')}
+          </label>
+          <div className='flex gap-2'>
+            <Select
+              value={syncFieldsEditorProps.syncFromTarget.type || 'json'}
+              onValueChange={(v) =>
+                syncFieldsEditorProps.updateOperation(
+                  syncFieldsEditorProps.operationId,
+                  {
+                    from: buildSyncTargetSpec(
+                      v,
+                      syncFieldsEditorProps.syncFromTarget.key
+                    ),
+                  }
+                )
+              }
+            >
+              <SelectTrigger className='h-8 w-[110px] text-xs'>
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                {SYNC_TARGET_TYPE_OPTIONS.map((o) => (
+                  <SelectItem key={o.value} value={o.value}>
+                    {t(o.label)}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            <Input
+              value={syncFieldsEditorProps.syncFromTarget.key}
+              onChange={(e) =>
+                syncFieldsEditorProps.updateOperation(
+                  syncFieldsEditorProps.operationId,
+                  {
+                    from: buildSyncTargetSpec(
+                      syncFieldsEditorProps.syncFromTarget.type,
+                      e.target.value
+                    ),
+                  }
+                )
+              }
+              placeholder='session_id'
+              className='h-8 text-xs'
+            />
+          </div>
+        </div>
+        <div className='space-y-1.5'>
+          <label className='text-[10px] font-medium'>
+            {t('Target Endpoint')}
+          </label>
+          <div className='flex gap-2'>
+            <Select
+              value={syncFieldsEditorProps.syncToTarget.type || 'json'}
+              onValueChange={(v) =>
+                syncFieldsEditorProps.updateOperation(
+                  syncFieldsEditorProps.operationId,
+                  {
+                    to: buildSyncTargetSpec(
+                      v,
+                      syncFieldsEditorProps.syncToTarget.key
+                    ),
+                  }
+                )
+              }
+            >
+              <SelectTrigger className='h-8 w-[110px] text-xs'>
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                {SYNC_TARGET_TYPE_OPTIONS.map((o) => (
+                  <SelectItem key={o.value} value={o.value}>
+                    {t(o.label)}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            <Input
+              value={syncFieldsEditorProps.syncToTarget.key}
+              onChange={(e) =>
+                syncFieldsEditorProps.updateOperation(
+                  syncFieldsEditorProps.operationId,
+                  {
+                    to: buildSyncTargetSpec(
+                      syncFieldsEditorProps.syncToTarget.type,
+                      e.target.value
+                    ),
+                  }
+                )
+              }
+              placeholder='prompt_cache_key'
+              className='h-8 text-xs'
+            />
+          </div>
+        </div>
+      </div>
+      <div className='flex flex-wrap gap-1'>
+        {[
+          {
+            label: 'header:session_id -> json:prompt_cache_key',
+            from: 'header:session_id',
+            to: 'json:prompt_cache_key',
+          },
+          {
+            label: 'json:prompt_cache_key -> header:session_id',
+            from: 'json:prompt_cache_key',
+            to: 'header:session_id',
+          },
+        ].map((preset) => (
+          <Button
+            key={preset.label}
+            type='button'
+            variant='outline'
+            size='sm'
+            className='h-6 text-[10px]'
+            onClick={() =>
+              syncFieldsEditorProps.updateOperation(
+                syncFieldsEditorProps.operationId,
+                { from: preset.from, to: preset.to }
+              )
+            }
+          >
+            {preset.label}
+          </Button>
+        ))}
+      </div>
+    </div>
+  )
+}

File diff suppressed because it is too large
+ 1063 - 852
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx


+ 25 - 10
web/default/src/features/channels/constants.ts

@@ -60,15 +60,30 @@ export const CHANNEL_TYPES = {
   57: 'Codex',
 } as const
 
-export const CHANNEL_TYPE_OPTIONS = Object.entries(CHANNEL_TYPES)
-  .filter(([value]) => {
-    const num = Number(value)
-    return num !== 0 // Exclude Unknown
-  })
-  .map(([value, label]) => ({
-    value: Number(value),
-    label,
-  }))
+const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [
+  1, 14, 33, 24, 43, 3, 41, 48, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, 46, 23,
+  18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 22, 21, 44, 2, 5, 36, 50,
+  51, 52, 53, 54, 55, 56,
+]
+
+export const CHANNEL_TYPE_OPTIONS: { value: number; label: string }[] = (() => {
+  const ordered: { value: number; label: string }[] = []
+  const seen = new Set<number>()
+  for (const id of CHANNEL_TYPE_DISPLAY_ORDER) {
+    const label = CHANNEL_TYPES[id as keyof typeof CHANNEL_TYPES]
+    if (label) {
+      ordered.push({ value: id, label })
+      seen.add(id)
+    }
+  }
+  for (const [key, label] of Object.entries(CHANNEL_TYPES)) {
+    const id = Number(key)
+    if (id !== 0 && !seen.has(id)) {
+      ordered.push({ value: id, label })
+    }
+  }
+  return ordered
+})()
 
 // ============================================================================
 // Channel Status (label values are i18n keys; use t(config.label) in components)
@@ -347,7 +362,7 @@ export const FIELD_DESCRIPTIONS = {
 // ============================================================================
 
 export const MODEL_FETCHABLE_TYPES = new Set([
-  1, 4, 14, 17, 20, 23, 24, 25, 26, 31, 34, 35, 40, 42, 43, 47, 48,
+  1, 4, 14, 17, 20, 23, 24, 25, 26, 27, 31, 34, 35, 40, 42, 43, 47, 48,
 ])
 
 export const TYPE_TO_KEY_PROMPT: Record<number, string> = {

+ 29 - 0
web/default/src/features/channels/lib/channel-form.ts

@@ -59,6 +59,7 @@ export const channelFormSchema = z.object({
   // Upstream model update settings (stored in settings JSON)
   upstream_model_update_check_enabled: z.boolean().optional(),
   upstream_model_update_auto_sync_enabled: z.boolean().optional(),
+  upstream_model_update_ignored_models: z.string().optional(),
 })
 
 export type ChannelFormValues = z.infer<typeof channelFormSchema>
@@ -113,6 +114,9 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = {
   allow_inference_geo: false,
   allow_speed: false,
   claude_beta_query: false,
+  upstream_model_update_check_enabled: false,
+  upstream_model_update_auto_sync_enabled: false,
+  upstream_model_update_ignored_models: '',
 }
 
 // ============================================================================
@@ -166,6 +170,7 @@ export function transformChannelToFormDefaults(
   let claudeBetaQuery = false
   let upstreamModelUpdateCheckEnabled = false
   let upstreamModelUpdateAutoSyncEnabled = false
+  let upstreamModelUpdateIgnoredModels = ''
 
   if (channel.settings) {
     try {
@@ -185,6 +190,11 @@ export function transformChannelToFormDefaults(
         parsed.upstream_model_update_check_enabled === true
       upstreamModelUpdateAutoSyncEnabled =
         parsed.upstream_model_update_auto_sync_enabled === true
+      upstreamModelUpdateIgnoredModels = Array.isArray(
+        parsed.upstream_model_update_ignored_models
+      )
+        ? parsed.upstream_model_update_ignored_models.join(',')
+        : ''
     } catch (error) {
       // eslint-disable-next-line no-console
       console.error('Failed to parse channel settings:', error)
@@ -233,6 +243,7 @@ export function transformChannelToFormDefaults(
     allow_safety_identifier: allowSafetyIdentifier,
     upstream_model_update_check_enabled: upstreamModelUpdateCheckEnabled,
     upstream_model_update_auto_sync_enabled: upstreamModelUpdateAutoSyncEnabled,
+    upstream_model_update_ignored_models: upstreamModelUpdateIgnoredModels,
   }
 }
 
@@ -336,7 +347,25 @@ function buildSettingsJSON(formData: ChannelFormValues): string {
     settingsObj.upstream_model_update_check_enabled =
       formData.upstream_model_update_check_enabled === true
     settingsObj.upstream_model_update_auto_sync_enabled =
+      settingsObj.upstream_model_update_check_enabled === true &&
       formData.upstream_model_update_auto_sync_enabled === true
+    settingsObj.upstream_model_update_ignored_models = Array.from(
+      new Set(
+        String(formData.upstream_model_update_ignored_models || '')
+          .split(',')
+          .map((model) => model.trim())
+          .filter(Boolean)
+      )
+    )
+    if (
+      !Array.isArray(settingsObj.upstream_model_update_last_detected_models) ||
+      settingsObj.upstream_model_update_check_enabled !== true
+    ) {
+      settingsObj.upstream_model_update_last_detected_models = []
+    }
+    if (typeof settingsObj.upstream_model_update_last_check_time !== 'number') {
+      settingsObj.upstream_model_update_last_check_time = 0
+    }
   }
 
   return JSON.stringify(settingsObj)

+ 9 - 0
web/default/src/features/channels/types.ts

@@ -78,6 +78,15 @@ export interface ChannelOtherSettings {
   allow_service_tier?: boolean
   disable_store?: boolean
   allow_safety_identifier?: boolean
+  allow_include_obfuscation?: boolean
+  allow_inference_geo?: boolean
+  allow_speed?: boolean
+  claude_beta_query?: boolean
+  upstream_model_update_check_enabled?: boolean
+  upstream_model_update_auto_sync_enabled?: boolean
+  upstream_model_update_ignored_models?: string[]
+  upstream_model_update_last_check_time?: number
+  upstream_model_update_last_detected_models?: string[]
 }
 
 // ============================================================================

+ 2 - 2
web/default/src/i18n/locales/_reports/_sync-report.json

@@ -17,13 +17,13 @@
       "file": "ja.json",
       "missingCount": 0,
       "extrasCount": 0,
-      "untranslatedCount": 84
+      "untranslatedCount": 85
     },
     "ru": {
       "file": "ru.json",
       "missingCount": 0,
       "extrasCount": 0,
-      "untranslatedCount": 88
+      "untranslatedCount": 89
     },
     "vi": {
       "file": "vi.json",

+ 1 - 0
web/default/src/i18n/locales/_reports/ja.untranslated.json

@@ -21,6 +21,7 @@
   "footer.columns.related.links.midjourney": "Midjourney-Proxy",
   "footer.columns.related.links.neko": "neko-api-key-tool",
   "Gemini": "Gemini",
+  "Gemini Image 4K": "Gemini Image 4K",
   "GitHub": "GitHub",
   "gpt-3.5-turbo": "gpt-3.5-turbo",
   "gpt-3.5-turbo-0125": "gpt-3.5-turbo-0125",

+ 1 - 0
web/default/src/i18n/locales/_reports/ru.untranslated.json

@@ -25,6 +25,7 @@
   "footer.columns.related.links.midjourney": "Midjourney-Proxy",
   "footer.columns.related.links.neko": "neko-api-key-tool",
   "Gemini": "Gemini",
+  "Gemini Image 4K": "Gemini Image 4K",
   "GitHub": "GitHub",
   "gpt-3.5-turbo": "gpt-3.5-turbo",
   "gpt-3.5-turbo-0125": "gpt-3.5-turbo-0125",

+ 191 - 5
web/default/src/i18n/locales/en.json

@@ -10,6 +10,7 @@
     ". This action cannot be undone.": ". This action cannot be undone.",
     "...": "...",
     "\"default\": \"us-central1\", \"claude-3-5-sonnet-20240620\": \"europe-west1\"": "\"default\": \"us-central1\", \"claude-3-5-sonnet-20240620\": \"europe-west1\"",
+    "({{total}} total, {{omit}} omitted)": "({{total}} total, {{omit}} omitted)",
     "(Leave empty to dissolve tag)": "(Leave empty to dissolve tag)",
     "(Optional: redirect model names)": "(Optional: redirect model names)",
     "(Override all channels' groups)": "(Override all channels' groups)",
@@ -122,6 +123,7 @@
     "Add auto group": "Add auto group",
     "Add chat preset": "Add chat preset",
     "Add condition": "Add condition",
+    "Add Condition": "Add Condition",
     "Add custom model(s), comma-separated": "Add custom model(s), comma-separated",
     "Add discount tier": "Add discount tier",
     "Add each model or tag you want to include.": "Add each model or tag you want to include.",
@@ -164,12 +166,14 @@
     "Added {{count}} custom model(s)": "Added {{count}} custom model(s)",
     "Added {{count}} model(s)": "Added {{count}} model(s)",
     "Added successfully": "Added successfully",
+    "Additional Conditions": "Additional Conditions",
     "Additional information": "Additional information",
     "Additional Information": "Additional Information",
     "Additional Limit": "Additional Limit",
     "Additional Limits": "Additional Limits",
     "Additional metered capability": "Additional metered capability",
     "Adjust Quota": "Adjust Quota",
+    "Adjust response formatting, prompt behavior, proxy, and upstream automation.": "Adjust response formatting, prompt behavior, proxy, and upstream automation.",
     "Adjust the appearance and layout to suit your preferences.": "Adjust the appearance and layout to suit your preferences.",
     "Admin": "Admin",
     "Admin access required": "Admin access required",
@@ -185,6 +189,7 @@
     "Advanced Options": "Advanced Options",
     "Advanced platform configuration.": "Advanced platform configuration.",
     "Advanced Settings": "Advanced Settings",
+    "Advanced text editing": "Advanced text editing",
     "After clicking the button, you'll be asked to authorize the bot": "After clicking the button, you'll be asked to authorize the bot",
     "After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?": "After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?",
     "After enabling, the plan will be shown to users. Continue?": "After enabling, the plan will be shown to users. Continue?",
@@ -209,6 +214,7 @@
     "All Groups": "All Groups",
     "All Models": "All Models",
     "All models in use are properly configured.": "All models in use are properly configured.",
+    "All Must Match (AND)": "All Must Match (AND)",
     "All Status": "All Status",
     "All Sync Status": "All Sync Status",
     "All Tags": "All Tags",
@@ -229,6 +235,7 @@
     "Allow Private IPs": "Allow Private IPs",
     "Allow registration with password": "Allow registration with password",
     "Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)": "Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)",
+    "Allow Retry": "Allow Retry",
     "Allow safety_identifier passthrough": "Allow safety_identifier passthrough",
     "Allow service_tier passthrough": "Allow service_tier passthrough",
     "Allow speed passthrough": "Allow speed passthrough",
@@ -271,6 +278,8 @@
     "Announcements saved successfully": "Announcements saved successfully",
     "Answer": "Answer",
     "Anthropic": "Anthropic",
+    "Any Match (OR)": "Any Match (OR)",
+    "API Access": "API Access",
     "API Addresses": "API Addresses",
     "API Base URL (Important: Not Chat API) *": "API Base URL (Important: Not Chat API) *",
     "API Base URL *": "API Base URL *",
@@ -305,8 +314,11 @@
     "API2GPT": "API2GPT",
     "Append": "Append",
     "Append mode: New keys will be added to the end of the existing key list": "Append mode: New keys will be added to the end of the existing key list",
+    "Append Template": "Append Template",
     "Append to channel": "Append to channel",
+    "Append to End": "Append to End",
     "Append to existing keys": "Append to existing keys",
+    "Append value to array / string / object end": "Append value to array / string / object end",
     "appended": "appended",
     "Application": "Application",
     "Applies to custom completion endpoints. JSON map of model → ratio.": "Applies to custom completion endpoints. JSON map of model → ratio.",
@@ -329,9 +341,9 @@
     "Are you sure you want to unbind {{provider}} for this user? The user will no longer be able to log in via this method.": "Are you sure you want to unbind {{provider}} for this user? The user will no longer be able to log in via this method.",
     "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.",
     "Are you sure?": "Are you sure?",
+    "Area Chart": "Area Chart",
     "Args (space separated)": "Args (space separated)",
     "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.",
-    "Area Chart": "Area Chart",
     "Asc": "Asc",
     "Ask anything": "Ask anything",
     "Async task refund": "Async task refund",
@@ -370,6 +382,7 @@
     "Auto-disable status codes": "Auto-disable status codes",
     "Auto-discover": "Auto-discover",
     "Auto-discovers endpoints from the provider": "Auto-discovers endpoints from the provider",
+    "Auto-fill when one field exists and another is missing": "Auto-fill when one field exists and another is missing",
     "Auto-retry status codes": "Auto-retry status codes",
     "Automatically disable channel on repeated failures": "Automatically disable channel on repeated failures",
     "Automatically disable channels exceeding this response time": "Automatically disable channels exceeding this response time",
@@ -386,6 +399,7 @@
     "Average RPM": "Average RPM",
     "Average TPM": "Average TPM",
     "AWS": "AWS",
+    "AWS Bedrock Claude Compat": "AWS Bedrock Claude Compat",
     "AWS Key Format": "AWS Key Format",
     "Azure": "Azure",
     "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *",
@@ -399,6 +413,7 @@
     "Backup code must be in format XXXX-XXXX": "Backup code must be in format XXXX-XXXX",
     "Backup codes regenerated successfully": "Backup codes regenerated successfully",
     "Backup codes remaining: {{count}}": "Backup codes remaining: {{count}}",
+    "Bad Request": "Bad Request",
     "Badge Color": "Badge Color",
     "Baidu": "Baidu",
     "Baidu V2": "Baidu V2",
@@ -420,6 +435,7 @@
     "Basic Configuration": "Basic Configuration",
     "Basic Info": "Basic Info",
     "Basic Information": "Basic Information",
+    "Basic Templates": "Basic Templates",
     "Batch Add (one key per line)": "Batch Add (one key per line)",
     "Batch delete failed": "Batch delete failed",
     "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed",
@@ -533,6 +549,7 @@
     "Channel enabled successfully": "Channel enabled successfully",
     "Channel Extra Settings": "Channel Extra Settings",
     "Channel ID": "Channel ID",
+    "Channel key": "Channel key",
     "Channel key unlocked": "Channel key unlocked",
     "Channel models": "Channel models",
     "Channel name is required": "Channel name is required",
@@ -585,6 +602,7 @@
     "Choose where to fetch upstream metadata.": "Choose where to fetch upstream metadata.",
     "Classic (Legacy Frontend)": "Classic (Legacy Frontend)",
     "Claude": "Claude",
+    "Claude CLI Header Passthrough": "Claude CLI Header Passthrough",
     "Clean history logs": "Clean history logs",
     "Clean logs": "Clean logs",
     "Clean up inactive cache": "Clean up inactive cache",
@@ -621,6 +639,7 @@
     "Click to view full details": "Click to view full details",
     "Click to view full error message": "Click to view full error message",
     "Click to view full prompt": "Click to view full prompt",
+    "Client header value": "Client header value",
     "Client ID": "Client ID",
     "Client Secret": "Client Secret",
     "Close": "Close",
@@ -636,12 +655,15 @@
     "Codex Account & Usage": "Codex Account & Usage",
     "Codex Authorization": "Codex Authorization",
     "Codex channels use an OAuth JSON credential as the key.": "Codex channels use an OAuth JSON credential as the key.",
+    "Codex CLI Header Passthrough": "Codex CLI Header Passthrough",
     "Cohere": "Cohere",
     "Collapse": "Collapse",
+    "Collapse All": "Collapse All",
     "Color": "Color",
     "Color is required": "Color is required",
     "Color:": "Color:",
     "Coming Soon!": "Coming Soon!",
+    "Comma-separated exact model names. Prefix with regex: to ignore by regular expression.": "Comma-separated exact model names. Prefix with regex: to ignore by regular expression.",
     "Comma-separated list of allowed ports (empty = all ports)": "Comma-separated list of allowed ports (empty = all ports)",
     "Comma-separated model names (leave empty to keep current)": "Comma-separated model names (leave empty to keep current)",
     "Comma-separated model names, e.g., gpt-4,gpt-3.5-turbo": "Comma-separated model names, e.g., gpt-4,gpt-3.5-turbo",
@@ -659,7 +681,11 @@
     "Completion price ($/1M tokens)": "Completion price ($/1M tokens)",
     "Completion ratio": "Completion ratio",
     "Concatenate channel system prompt with user&apos;s prompt": "Concatenate channel system prompt with user&apos;s prompt",
+    "Condition Path": "Condition Path",
+    "Condition Settings": "Condition Settings",
+    "Condition Value": "Condition Value",
     "Conditional multipliers": "Conditional multipliers",
+    "Conditions": "Conditions",
     "Conditions (AND)": "Conditions (AND)",
     "Confidence": "Confidence",
     "Configuration": "Configuration",
@@ -768,16 +794,20 @@
     "Control log retention and clean historical data.": "Control log retention and clean historical data.",
     "Control passthrough behavior and connection keep-alive settings": "Control passthrough behavior and connection keep-alive settings",
     "Control request frequency to prevent abuse and manage system load.": "Control request frequency to prevent abuse and manage system load.",
+    "Control which models are exposed and which groups may use them.": "Control which models are exposed and which groups may use them.",
     "Control which sidebar areas and modules are available to all users.": "Control which sidebar areas and modules are available to all users.",
     "Controls whether user verification (biometrics/PIN) is required during Passkey flows.": "Controls whether user verification (biometrics/PIN) is required during Passkey flows.",
     "Conversion rate from USD to your custom currency": "Conversion rate from USD to your custom currency",
     "Convert reasoning_content to <think> tag in content": "Convert reasoning_content to <think> tag in content",
+    "Convert string to lowercase": "Convert string to lowercase",
+    "Convert string to uppercase": "Convert string to uppercase",
     "Copied": "Copied",
     "Copied {{count}} key(s)": "Copied {{count}} key(s)",
     "Copied to clipboard": "Copied to clipboard",
     "Copied: {{model}}": "Copied: {{model}}",
     "Copied!": "Copied!",
     "Copy": "Copy",
+    "Copy a request header": "Copy a request header",
     "Copy All": "Copy All",
     "Copy all backup codes": "Copy all backup codes",
     "Copy All Codes": "Copy All Codes",
@@ -787,6 +817,7 @@
     "Copy code": "Copy code",
     "Copy Connection Info": "Copy Connection Info",
     "Copy failed": "Copy failed",
+    "Copy Field": "Copy Field",
     "Copy Header": "Copy Header",
     "Copy Key": "Copy Key",
     "Copy Link": "Copy Link",
@@ -795,14 +826,17 @@
     "Copy prompt": "Copy prompt",
     "Copy redemption code": "Copy redemption code",
     "Copy referral link": "Copy referral link",
+    "Copy Request Header": "Copy Request Header",
     "Copy secret key": "Copy secret key",
     "Copy selected codes": "Copy selected codes",
     "Copy selected keys": "Copy selected keys",
+    "Copy source field to target field": "Copy source field to target field",
     "Copy the key and paste it here": "Copy the key and paste it here",
     "Copy this prompt and send it to an LLM (e.g. ChatGPT / Claude) to help design your billing expression.": "Copy this prompt and send it to an LLM (e.g. ChatGPT / Claude) to help design your billing expression.",
     "Copy to clipboard": "Copy to clipboard",
     "Copy token": "Copy token",
     "Copy URL": "Copy URL",
+    "Core Configuration": "Core Configuration",
     "Core Features": "Core Features",
     "Cost": "Cost",
     "Cost in USD per request, regardless of tokens used.": "Cost in USD per request, regardless of tokens used.",
@@ -834,6 +868,8 @@
     "Create Prefill Group": "Create Prefill Group",
     "Create Provider": "Create Provider",
     "Create Redemption Code": "Create Redemption Code",
+    "Create request parameter override rules with a visual editor or raw JSON.": "Create request parameter override rules with a visual editor or raw JSON.",
+    "Create request parameter override rules without editing raw JSON.": "Create request parameter override rules without editing raw JSON.",
     "Create reusable bundles of models, tags, endpoints, and user groups to speed up configuration elsewhere in the console.": "Create reusable bundles of models, tags, endpoints, and user groups to speed up configuration elsewhere in the console.",
     "Create succeeded": "Create succeeded",
     "Create Vendor": "Create Vendor",
@@ -858,6 +894,8 @@
     "Current Cache Size": "Current Cache Size",
     "Current email: {{email}}. Enter a new email to change.": "Current email: {{email}}. Enter a new email to change.",
     "Current key": "Current key",
+    "Current legacy JSON is invalid, cannot append": "Current legacy JSON is invalid, cannot append",
+    "Current Level Only": "Current Level Only",
     "Current models for the longest channel in this tag. May not include all models from all channels.": "Current models for the longest channel in this tag. May not include all models from all channels.",
     "Current Password": "Current Password",
     "Current Price": "Current Price",
@@ -874,6 +912,7 @@
     "Custom Currency Symbol": "Custom Currency Symbol",
     "Custom currency symbol is required": "Custom currency symbol is required",
     "Custom database driver detected.": "Custom database driver detected.",
+    "Custom Error Response": "Custom Error Response",
     "Custom Home Page": "Custom Home Page",
     "Custom message shown when access is denied": "Custom message shown when access is denied",
     "Custom model (comma-separated)": "Custom model (comma-separated)",
@@ -923,13 +962,17 @@
     "Delete": "Delete",
     "Delete (": "Delete (",
     "Delete {{count}} API key(s)?": "Delete {{count}} API key(s)?",
+    "Delete a runtime request header": "Delete a runtime request header",
     "Delete Account": "Delete Account",
     "Delete All Disabled": "Delete All Disabled",
     "Delete All Disabled Channels?": "Delete All Disabled Channels?",
     "Delete Auto-Disabled": "Delete Auto-Disabled",
     "Delete Channel": "Delete Channel",
     "Delete Channels?": "Delete Channels?",
+    "Delete condition": "Delete condition",
+    "Delete Condition": "Delete Condition",
     "Delete failed": "Delete failed",
+    "Delete Field": "Delete Field",
     "Delete group": "Delete group",
     "Delete Header": "Delete Header",
     "Delete Invalid": "Delete Invalid",
@@ -941,6 +984,7 @@
     "Delete Model": "Delete Model",
     "Delete Models?": "Delete Models?",
     "Delete Provider": "Delete Provider",
+    "Delete Request Header": "Delete Request Header",
     "Delete selected API keys": "Delete selected API keys",
     "Delete selected channels": "Delete selected channels",
     "Delete selected models": "Delete selected models",
@@ -1033,6 +1077,8 @@
     "Displays the mobile sidebar.": "Displays the mobile sidebar.",
     "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.": "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.",
     "Do not repeat check-in; only once per day": "Do not repeat check-in; only once per day",
+    "Do regex replacement in the target field": "Do regex replacement in the target field",
+    "Do string replacement in the target field": "Do string replacement in the target field",
     "Docs": "Docs",
     "Documentation Link": "Documentation Link",
     "Documentation or external knowledge base.": "Documentation or external knowledge base.",
@@ -1062,6 +1108,7 @@
     "e.g. 401, 403, 429, 500-599": "e.g. 401, 403, 429, 500-599",
     "e.g. 8 means 1 USD = 8 units": "e.g. 8 means 1 USD = 8 units",
     "e.g. Basic Plan": "e.g. Basic Plan",
+    "e.g. Clean tool parameters to avoid upstream validation errors": "e.g. Clean tool parameters to avoid upstream validation errors",
     "e.g. example.com": "e.g. example.com",
     "e.g. llama3.1:8b": "e.g. llama3.1:8b",
     "e.g. My GitLab": "e.g. My GitLab",
@@ -1069,6 +1116,7 @@
     "e.g. New API Console": "e.g. New API Console",
     "e.g. openid profile email": "e.g. openid profile email",
     "e.g. Suitable for light usage": "e.g. Suitable for light usage",
+    "e.g. This request does not meet access policy": "e.g. This request does not meet access policy",
     "e.g., 0.95": "e.g., 0.95",
     "e.g., 100": "e.g., 100",
     "e.g., 123456": "e.g., 123456",
@@ -1084,6 +1132,7 @@
     "e.g., d6b5da8hk1awo8nap34ube6gh": "e.g., d6b5da8hk1awo8nap34ube6gh",
     "e.g., default, vip, premium": "e.g., default, vip, premium",
     "e.g., gpt-4, claude-3": "e.g., gpt-4, claude-3",
+    "e.g., gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "e.g., gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$",
     "e.g., https://api.example.com (path before /suno)": "e.g., https://api.example.com (path before /suno)",
     "e.g., https://api.openai.com/v1/chat/completions": "e.g., https://api.openai.com/v1/chat/completions",
     "e.g., https://ark.cn-beijing.volces.com": "e.g., https://ark.cn-beijing.volces.com",
@@ -1111,6 +1160,8 @@
     "Edit discount tier": "Edit discount tier",
     "Edit FAQ": "Edit FAQ",
     "Edit group rate limit": "Edit group rate limit",
+    "Edit JSON object directly. Suitable for simple parameter overrides.": "Edit JSON object directly. Suitable for simple parameter overrides.",
+    "Edit JSON text directly. Format will be validated on save.": "Edit JSON text directly. Format will be validated on save.",
     "Edit model": "Edit model",
     "Edit Model": "Edit Model",
     "Edit OAuth Provider": "Edit OAuth Provider",
@@ -1195,6 +1246,8 @@
     "English": "English",
     "Ensure Prefix": "Ensure Prefix",
     "Ensure Suffix": "Ensure Suffix",
+    "Ensure the string has a specified prefix": "Ensure the string has a specified prefix",
+    "Ensure the string has a specified suffix": "Ensure the string has a specified suffix",
     "Enter 6-digit code": "Enter 6-digit code",
     "Enter a name": "Enter a name",
     "Enter a new name": "Enter a new name",
@@ -1220,6 +1273,7 @@
     "Enter display name": "Enter display name",
     "Enter HTML code (e.g., <p>About us...</p>) or a URL (e.g., https://example.com) to embed as iframe": "Enter HTML code (e.g., <p>About us...</p>) or a URL (e.g., https://example.com) to embed as iframe",
     "Enter Input price to calculate ratio": "Enter Input price to calculate ratio",
+    "Enter JSON to override request headers": "Enter JSON to override request headers",
     "Enter key, format: AccessKey|SecretAccessKey|Region": "Enter key, format: AccessKey|SecretAccessKey|Region",
     "Enter key, one per line, format: AccessKey|SecretAccessKey|Region": "Enter key, one per line, format: AccessKey|SecretAccessKey|Region",
     "Enter model name": "Enter model name",
@@ -1271,8 +1325,12 @@
     "Epay Gateway": "Epay Gateway",
     "Epay merchant ID": "Epay merchant ID",
     "Epay secret key": "Epay secret key",
+    "Equals": "Equals",
     "Error": "Error",
+    "Error Code (optional)": "Error Code (optional)",
     "Error Message": "Error Message",
+    "Error Message (required)": "Error Message (required)",
+    "Error Type (optional)": "Error Type (optional)",
     "Estimated cost": "Estimated cost",
     "Estimated quota cost": "Estimated quota cost",
     "Exact": "Exact",
@@ -1290,6 +1348,7 @@
     "Existing account will be reused": "Existing account will be reused",
     "Existing Models ({{count}})": "Existing Models ({{count}})",
     "Exists": "Exists",
+    "Expand All": "Expand All",
     "Expected a JSON array.": "Expected a JSON array.",
     "Experiment with prompts and models in real time.": "Experiment with prompts and models in real time.",
     "Expiration Time": "Expiration Time",
@@ -1456,6 +1515,7 @@
     "field": "field",
     "Field Mapping": "Field Mapping",
     "Field passthrough controls": "Field passthrough controls",
+    "Field Path": "Field Path",
     "File Search": "File Search",
     "Files to Retain": "Files to Retain",
     "Fill All Models": "Fill All Models",
@@ -1551,6 +1611,7 @@
     "GC executed": "GC executed",
     "GC execution failed": "GC execution failed",
     "Gemini": "Gemini",
+    "Gemini Image 4K": "Gemini Image 4K",
     "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.",
     "General": "General",
     "General Settings": "General Settings",
@@ -1592,6 +1653,8 @@
     "gpt-4": "gpt-4",
     "gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, etc.",
     "GPU count": "GPU count",
+    "Greater Than": "Greater Than",
+    "Greater Than or Equal": "Greater Than or Equal",
     "Grok": "Grok",
     "Grok Settings": "Grok Settings",
     "Group": "Group",
@@ -1629,8 +1692,11 @@
     "Has been invalidated": "Invalidated successfully",
     "Have a Code?": "Have a Code?",
     "Header": "Header",
+    "Header Name": "Header Name",
     "Header navigation": "Header navigation",
     "Header Override": "Header Override",
+    "Header Passthrough (X-Request-Id)": "Header Passthrough (X-Request-Id)",
+    "Header Value (supports string or JSON mapping)": "Header Value (supports string or JSON mapping)",
     "Hidden — verify to reveal": "Hidden — verify to reveal",
     "Hide": "Hide",
     "Hide API key": "Hide API key",
@@ -1697,6 +1763,7 @@
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.",
+    "Ignored upstream models": "Ignored upstream models",
     "Image": "Image",
     "Image Generation": "Image Generation",
     "Image In": "Image In",
@@ -1730,6 +1797,7 @@
     "Integrations": "Integrations",
     "Inter-group overrides": "Inter-group overrides",
     "Inter-group ratio overrides": "Inter-group ratio overrides",
+    "Internal Notes": "Internal Notes",
     "Internal notes (not shown to users)": "Internal notes (not shown to users)",
     "Internal Server Error!": "Internal Server Error!",
     "Invalid chat link. Please contact the administrator.": "Invalid chat link. Please contact the administrator.",
@@ -1750,6 +1818,7 @@
     "Invalid status code mapping entries: {{entries}}": "Invalid status code mapping entries: {{entries}}",
     "Invalidate": "Invalidate",
     "Invalidated": "Invalidated",
+    "Invert match": "Invert match",
     "Invitation Code": "Invitation Code",
     "Invitation Quota": "Invitation Quota",
     "Invite Info": "Invite Info",
@@ -1777,6 +1846,7 @@
     "JSON": "JSON",
     "JSON array of group identifiers. When enabled below, new tokens rotate through this list.": "JSON array of group identifiers. When enabled below, new tokens rotate through this list.",
     "JSON Editor": "JSON Editor",
+    "JSON format error": "JSON format error",
     "JSON format supports service account JSON files": "JSON format supports service account JSON files",
     "JSON map of group → description exposed when users create API keys.": "JSON map of group → description exposed when users create API keys.",
     "JSON map of group → ratio applied when the user selects the group explicitly.": "JSON map of group → ratio applied when the user selects the group explicitly.",
@@ -1785,11 +1855,14 @@
     "JSON Mode": "JSON Mode",
     "JSON must be an object": "JSON must be an object",
     "JSON object:": "JSON object:",
+    "JSON Text": "JSON Text",
     "JSON-based access control rules. Leave empty to allow all users.": "JSON-based access control rules. Leave empty to allow all users.",
     "Just now": "Just now",
     "JustSong": "JustSong",
     "K": "K",
     "Keep enabled if you need to proxy requests for different upstream accounts.": "Keep enabled if you need to proxy requests for different upstream accounts.",
+    "Keep original value": "Keep original value",
+    "Keep original value (skip if target exists)": "Keep original value (skip if target exists)",
     "Keep this above 1 minute to avoid heavy database load": "Keep this above 1 minute to avoid heavy database load",
     "Keep-alive Ping": "Keep-alive Ping",
     "Key": "Key",
@@ -1797,9 +1870,12 @@
     "Key Sources": "Key Sources",
     "Key Summary": "Key Summary",
     "Key Update Mode": "Key Update Mode",
+    "Keys, OAuth credentials, and multi-key update behavior.": "Keys, OAuth credentials, and multi-key update behavior.",
     "Kling": "Kling",
     "Knowledge Base ID *": "Knowledge Base ID *",
     "Landing page with system overview.": "Landing page with system overview.",
+    "Last check time": "Last check time",
+    "Last detected addable models": "Last detected addable models",
     "Last Login": "Last Login",
     "Last Seen": "Last Seen",
     "Last Tested": "Last Tested",
@@ -1823,7 +1899,11 @@
     "Leave empty to use default": "Leave empty to use default",
     "Leave empty to use system temp directory": "Leave empty to use system temp directory",
     "Leave empty to use username": "Leave empty to use username",
+    "Legacy Format (JSON Object)": "Legacy Format (JSON Object)",
+    "Legacy format must be a JSON object": "Legacy format must be a JSON object",
     "Less": "Less",
+    "Less Than": "Less Than",
+    "Less Than or Equal": "Less Than or Equal",
     "Light": "Light",
     "Lightning Fast": "Lightning Fast",
     "Limit period": "Limit period",
@@ -1863,6 +1943,7 @@
     "Log IP address for usage and error logs": "Log IP address for usage and error logs",
     "Log Maintenance": "Log Maintenance",
     "Log Type": "Log Type",
+    "Logic": "Logic",
     "Login failed": "Login failed",
     "Logo": "Logo",
     "Logo URL": "Logo URL",
@@ -1900,11 +1981,17 @@
     "Map request model names to actual provider model names (JSON format)": "Map request model names to actual provider model names (JSON format)",
     "Map response status codes (JSON format)": "Map response status codes (JSON format)",
     "Map upstream status codes to different codes": "Map upstream status codes to different codes",
+    "Match All (AND)": "Match All (AND)",
+    "Match Any (OR)": "Match Any (OR)",
+    "Match Mode": "Match Mode",
     "Match model name exactly": "Match model name exactly",
     "Match models containing this name": "Match models containing this name",
     "Match models ending with this name": "Match models ending with this name",
     "Match models starting with this name": "Match models starting with this name",
+    "Match Text": "Match Text",
     "Match Type": "Match Type",
+    "Match Value": "Match Value",
+    "Match Value (optional)": "Match Value (optional)",
     "Matched": "Matched",
     "Matched Tier": "Matched Tier",
     "Matching Rules": "Matching Rules",
@@ -2018,8 +2105,12 @@
     "More templates...": "More templates...",
     "More...": "More...",
     "Move": "Move",
+    "Move a request header": "Move a request header",
     "Move affiliate rewards to your main balance": "Move affiliate rewards to your main balance",
+    "Move Field": "Move Field",
     "Move Header": "Move Header",
+    "Move Request Header": "Move Request Header",
+    "Move source field to target field": "Move source field to target field",
     "ms": "ms",
     "Multi-key channel: Keys will be": "Multi-key channel: Keys will be",
     "Multi-Key Management": "Multi-Key Management",
@@ -2054,6 +2145,8 @@
     "Name must be between {{min}} and {{max}} characters": "Name must be between {{min}} and {{max}} characters",
     "Name Rule": "Name Rule",
     "Name Suffix": "Name Suffix",
+    "Name the channel and choose the upstream provider.": "Name the channel and choose the upstream provider.",
+    "Name the channel, choose the provider, configure API access, and set credentials.": "Name the channel, choose the provider, configure API access, and set credentials.",
     "name@example.com": "name@example.com",
     "Native format": "Native format",
     "Need a code?": "Need a code?",
@@ -2098,6 +2191,7 @@
     "No changes made": "No changes made",
     "No changes to save": "No changes to save",
     "No channel selected": "No channel selected",
+    "No channel type found.": "No channel type found.",
     "No channels available. Create your first channel to get started.": "No channels available. Create your first channel to get started.",
     "No channels found": "No channels found",
     "No Channels Found": "No Channels Found",
@@ -2133,6 +2227,7 @@
     "No mappings configured. Click \"Add Row\" to get started.": "No mappings configured. Click \"Add Row\" to get started.",
     "No matches found": "No matches found",
     "No matching results": "No matching results",
+    "No matching rules": "No matching rules",
     "No messages yet": "No messages yet",
     "No missing models found.": "No missing models found.",
     "No model found.": "No model found.",
@@ -2205,6 +2300,7 @@
     "Not available": "Not available",
     "Not backed up": "Not backed up",
     "Not bound": "Not bound",
+    "Not Equals": "Not Equals",
     "Not Set": "Not Set",
     "Not set yet": "Not set yet",
     "Not Started": "Not Started",
@@ -2225,6 +2321,7 @@
     "OAuth failed": "OAuth failed",
     "OAuth Integrations": "OAuth Integrations",
     "OAuth start failed": "OAuth start failed",
+    "Object Prune Rules": "Object Prune Rules",
     "Observability": "Observability",
     "Obtain the API key, merchant ID, and RSA key pair from the Waffo dashboard, and configure the callback URL.": "Obtain the API key, merchant ID, and RSA key pair from the Waffo dashboard, and configure the callback URL.",
     "Obtain the merchant, store, product and signing keys from your Waffo dashboard. Webhook URL: <ServerAddress>/api/waffo-pancake/webhook": "Obtain the merchant, store, product and signing keys from your Waffo dashboard. Webhook URL: <ServerAddress>/api/waffo-pancake/webhook",
@@ -2282,7 +2379,9 @@
     "Opened authorization page": "Opened authorization page",
     "OpenRouter": "OpenRouter",
     "opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.",
+    "Operation": "Operation",
     "Operation failed": "Operation failed",
+    "Operation Type": "Operation Type",
     "Operator Admin": "Operator Admin",
     "Optimize system for self-hosted single-user usage": "Optimize system for self-hosted single-user usage",
     "Optimized network architecture ensures millisecond response times": "Optimized network architecture ensures millisecond response times",
@@ -2294,6 +2393,7 @@
     "Optional notes about this channel": "Optional notes about this channel",
     "Optional notes about when to use this group": "Optional notes about when to use this group",
     "Optional ratio used when upstream cache hits occur.": "Optional ratio used when upstream cache hits occur.",
+    "Optional rule description": "Optional rule description",
     "Optional settings for advanced container configuration.": "Optional settings for advanced container configuration.",
     "Optional supplementary information (max 100 characters)": "Optional supplementary information (max 100 characters)",
     "Optional tag for grouping channels": "Optional tag for grouping channels",
@@ -2318,6 +2418,8 @@
     "Override request headers (JSON format)": "Override request headers (JSON format)",
     "Override request parameters (JSON format)": "Override request parameters (JSON format)",
     "Override request parameters. Cannot override": "Override request parameters. Cannot override",
+    "Override request parameters. Cannot override stream parameter.": "Override request parameters. Cannot override stream parameter.",
+    "Override Rules": "Override Rules",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Override the endpoint used for testing. Leave empty to auto detect.",
     "overrides for matching model prefix.": "overrides for matching model prefix.",
     "Overview": "Overview",
@@ -2327,7 +2429,10 @@
     "PaLM": "PaLM",
     "Pan": "Pan",
     "Param Override": "Param Override",
+    "Parameter configuration error": "Parameter configuration error",
     "Parameter Override": "Parameter Override",
+    "Parameter override must be a valid JSON object": "Parameter override must be a valid JSON object",
+    "Parameter override must be valid JSON format": "Parameter override must be valid JSON format",
     "Parameter Override Template (JSON)": "Parameter Override Template (JSON)",
     "Parameter override template must be a JSON object": "Parameter override template must be a JSON object",
     "parameter.": "parameter.",
@@ -2336,7 +2441,9 @@
     "Partial Submission": "Partial Submission",
     "Pass Headers": "Pass Headers",
     "Pass request body directly to upstream": "Pass request body directly to upstream",
+    "Pass specified request headers to upstream": "Pass specified request headers to upstream",
     "Pass Through Body": "Pass Through Body",
+    "Pass Through Headers": "Pass Through Headers",
     "Pass through the anthropic-beta header for beta features": "Pass through the anthropic-beta header for beta features",
     "Pass through the include field for usage obfuscation": "Pass through the include field for usage obfuscation",
     "Pass through the inference_geo field for Claude data residency region control": "Pass through the inference_geo field for Claude data residency region control",
@@ -2344,7 +2451,9 @@
     "Pass through the safety_identifier field": "Pass through the safety_identifier field",
     "Pass through the service_tier field": "Pass through the service_tier field",
     "Pass through the speed field for Claude inference speed mode control": "Pass through the speed field for Claude inference speed mode control",
+    "Pass when key is missing": "Pass when key is missing",
     "Pass-Through": "Pass-Through",
+    "Pass-through Headers (comma-separated or JSON array)": "Pass-through Headers (comma-separated or JSON array)",
     "Passkey": "Passkey",
     "Passkey Authentication": "Passkey Authentication",
     "Passkey is not available in this browser": "Passkey is not available in this browser",
@@ -2360,6 +2469,7 @@
     "Passkey registration was cancelled": "Passkey registration was cancelled",
     "Passkey removed successfully": "Passkey removed successfully",
     "Passkey reset successfully": "Passkey reset successfully",
+    "Passthrough Template": "Passthrough Template",
     "Password": "Password",
     "Password / Access Token": "Password / Access Token",
     "Password changed successfully": "Password changed successfully",
@@ -2374,6 +2484,7 @@
     "Passwords do not match": "Passwords do not match",
     "Paste the full callback URL (includes code & state)": "Paste the full callback URL (includes code & state)",
     "Path": "Path",
+    "Path not set": "Path not set",
     "Path Regex (one per line)": "Path Regex (one per line)",
     "Path:": "Path:",
     "Pay": "Pay",
@@ -2496,11 +2607,15 @@
     "Prefix": "Prefix",
     "Prefix Match": "Prefix Match",
     "Prefix used when displaying prices": "Prefix used when displaying prices",
+    "Prefix/Suffix Text": "Prefix/Suffix Text",
     "Premium chat models": "Premium chat models",
     "Preparing chat keys…": "Preparing chat keys…",
     "Preparing your chat link, please try again in a moment.": "Preparing your chat link, please try again in a moment.",
     "Preparing your chat link…": "Preparing your chat link…",
     "Prepend": "Prepend",
+    "Prepend to Start": "Prepend to Start",
+    "Prepend value to array / string / object start": "Prepend value to array / string / object start",
+    "Preserve the original field when applying this rule": "Preserve the original field when applying this rule",
     "Preset recharge amounts (JSON array)": "Preset recharge amounts (JSON array)",
     "Preset recharge amounts displayed to users": "Preset recharge amounts displayed to users",
     "Preset Template": "Preset Template",
@@ -2577,7 +2692,11 @@
     "Provider Name": "Provider Name",
     "Provider type (OpenAI, Anthropic, etc.)": "Provider type (OpenAI, Anthropic, etc.)",
     "Provider updated successfully": "Provider updated successfully",
+    "Provider-specific endpoint, account, and compatibility settings.": "Provider-specific endpoint, account, and compatibility settings.",
     "Proxy Address": "Proxy Address",
+    "Prune Object Items": "Prune Object Items",
+    "Prune object items by conditions": "Prune object items by conditions",
+    "Prune Rule (string or JSON object)": "Prune Rule (string or JSON object)",
     "Publish Date": "Publish Date",
     "Published": "Published",
     "Published:": "Published:",
@@ -2619,6 +2738,7 @@
     "Random": "Random",
     "Randomly select a key from the pool for each request": "Randomly select a key from the pool for each request",
     "Rate Limit Windows": "Rate Limit Windows",
+    "Rate Limited": "Rate Limited",
     "Rate Limiting": "Rate Limiting",
     "Ratio": "Ratio",
     "Ratio applied to audio completions for streaming models.": "Ratio applied to audio completions for streaming models.",
@@ -2648,6 +2768,8 @@
     "Recommended to keep this high to avoid upstream throttling.": "Recommended to keep this high to avoid upstream throttling.",
     "Record IP Address": "Record IP Address",
     "Record quota usage": "Record quota usage",
+    "Recursion Strategy": "Recursion Strategy",
+    "Recursive": "Recursive",
     "Redeem": "Redeem",
     "Redeem codes": "Redeem codes",
     "Redeemed By": "Redeemed By",
@@ -2681,6 +2803,8 @@
     "Refund Details": "Refund Details",
     "Regenerate": "Regenerate",
     "Regenerate Backup Codes": "Regenerate Backup Codes",
+    "Regex": "Regex",
+    "Regex Pattern": "Regex Pattern",
     "Regex Replace": "Regex Replace",
     "Register Passkey": "Register Passkey",
     "Registration Enabled": "Registration Enabled",
@@ -2709,6 +2833,9 @@
     "Remove Passkey": "Remove Passkey",
     "Remove Passkey?": "Remove Passkey?",
     "Remove rule group": "Remove rule group",
+    "Remove string prefix": "Remove string prefix",
+    "Remove string suffix": "Remove string suffix",
+    "Remove the target field": "Remove the target field",
     "Remove tier": "Remove tier",
     "Removed": "Removed",
     "Removed {{removed}} duplicate key(s). Before: {{before}}, After: {{after}}": "Removed {{removed}} duplicate key(s). Before: {{before}}, After: {{after}}",
@@ -2724,6 +2851,7 @@
     "Replace all existing keys": "Replace all existing keys",
     "Replace channel models": "Replace channel models",
     "Replace mode: Will completely replace all existing keys": "Replace mode: Will completely replace all existing keys",
+    "Replace With": "Replace With",
     "replaced": "replaced",
     "Replacement Model": "Replacement Model",
     "Replica count": "Replica count",
@@ -2731,6 +2859,7 @@
     "request": "request",
     "Request": "Request",
     "Request Body Disk Cache": "Request Body Disk Cache",
+    "Request Body Field": "Request Body Field",
     "Request Body Memory Cache": "Request Body Memory Cache",
     "Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.": "Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.",
     "Request conversion": "Request conversion",
@@ -2738,11 +2867,14 @@
     "Request Count": "Request Count",
     "Request failed": "Request failed",
     "Request flow": "Request flow",
+    "Request Header Field": "Request Header Field",
+    "Request Header Override": "Request Header Override",
     "Request Header Overrides": "Request Header Overrides",
     "Request ID": "Request ID",
     "Request Limits": "Request Limits",
     "Request Model": "Request Model",
     "Request Model:": "Request Model:",
+    "Request overrides, routing behavior, and upstream model automation": "Request overrides, routing behavior, and upstream model automation",
     "Request rule pricing": "Request rule pricing",
     "Request timed out, please refresh and restart GitHub login": "Request timed out, please refresh and restart GitHub login",
     "Request-based": "Request-based",
@@ -2755,6 +2887,7 @@
     "Require login to view models": "Require login to view models",
     "Required": "Required",
     "Required events:": "Required events:",
+    "Required provider, authentication, model, and group settings": "Required provider, authentication, model, and group settings",
     "Required to expose Midjourney-style image generation to end users.": "Required to expose Midjourney-style image generation to end users.",
     "Rerank": "Rerank",
     "Reroll": "Reroll",
@@ -2789,7 +2922,10 @@
     "Retain last N files": "Retain last N files",
     "Retry": "Retry",
     "Retry Chain": "Retry Chain",
+    "Retry Suggestion": "Retry Suggestion",
     "Retry Times": "Retry Times",
+    "Return a custom error immediately": "Return a custom error immediately",
+    "Return Custom Error": "Return Custom Error",
     "Return Error": "Return Error",
     "Return to dashboard": "Return to dashboard",
     "Reveal API key": "Reveal API key",
@@ -2806,13 +2942,25 @@
     "Route": "Route",
     "Route Description": "Route Description",
     "Route is required": "Route is required",
+    "Routing & Overrides": "Routing & Overrides",
+    "Routing Strategy": "Routing Strategy",
     "Rows per page": "Rows per page",
     "RPM": "RPM",
     "RSA Private Key (Production)": "RSA Private Key (Production)",
     "RSA Private Key (Sandbox)": "RSA Private Key (Sandbox)",
     "Rule": "Rule",
+    "Rule {{line}} is missing source field": "Rule {{line}} is missing source field",
+    "Rule {{line}} is missing target field": "Rule {{line}} is missing target field",
+    "Rule {{line}} is missing target path": "Rule {{line}} is missing target path",
+    "Rule {{line}} is missing value": "Rule {{line}} is missing value",
+    "Rule {{line}} pass_headers format is invalid": "Rule {{line}} pass_headers format is invalid",
+    "Rule {{line}} pass_headers is missing header names": "Rule {{line}} pass_headers is missing header names",
+    "Rule {{line}} prune_objects is missing conditions": "Rule {{line}} prune_objects is missing conditions",
+    "Rule {{line}} return_error requires a message field": "Rule {{line}} return_error requires a message field",
+    "Rule Description (optional)": "Rule Description (optional)",
     "Rule group": "Rule group",
     "rules": "rules",
+    "Rules": "Rules",
     "Rules JSON": "Rules JSON",
     "Rules JSON must be an array": "Rules JSON must be an array",
     "Run GC": "Run GC",
@@ -2861,12 +3009,14 @@
     "Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.": "Scan the QR code to follow the official account and send the message “验证码” to receive your verification code.",
     "Scan the QR code with WeChat to bind your account": "Scan the QR code with WeChat to bind your account",
     "Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)": "Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)",
+    "Scenario Templates": "Scenario Templates",
     "Scheduled channel tests": "Scheduled channel tests",
     "Scope": "Scope",
     "Scopes": "Scopes",
     "Search": "Search",
     "Search by name or URL...": "Search by name or URL...",
     "Search by order number...": "Search by order number...",
+    "Search channel type...": "Search channel type...",
     "Search chat presets...": "Search chat presets...",
     "Search colors...": "Search colors...",
     "Search conflicting models or fields": "Search conflicting models or fields",
@@ -2882,6 +3032,7 @@
     "Search payment methods...": "Search payment methods...",
     "Search payment types...": "Search payment types...",
     "Search products...": "Search products...",
+    "Search rules...": "Search rules...",
     "Search tags...": "Search tags...",
     "Search vendors...": "Search vendors...",
     "Search...": "Search...",
@@ -2899,6 +3050,7 @@
     "Select a group type": "Select a group type",
     "Select a preset...": "Select a preset...",
     "Select a role": "Select a role",
+    "Select a rule to edit.": "Select a rule to edit.",
     "Select a timestamp before clearing logs.": "Select a timestamp before clearing logs.",
     "Select a usage mode to continue": "Select a usage mode to continue",
     "Select a verification method first": "Select a verification method first",
@@ -2973,13 +3125,16 @@
     "Set a discount rate for a specific recharge amount threshold.": "Set a discount rate for a specific recharge amount threshold.",
     "Set a secure password (min. 8 characters)": "Set a secure password (min. 8 characters)",
     "Set a tag for": "Set a tag for",
-    "Set filters to customize your dashboard statistics and charts.": "Set filters to customize your dashboard statistics and charts.",
-    "Set filters to narrow down your log search results.": "Set filters to narrow down your log search results.",
     "Set API key access restrictions": "Set API key access restrictions",
     "Set API key basic information": "Set API key basic information",
+    "Set Field": "Set Field",
+    "Set filters to customize your dashboard statistics and charts.": "Set filters to customize your dashboard statistics and charts.",
+    "Set filters to narrow down your log search results.": "Set filters to narrow down your log search results.",
     "Set Header": "Set Header",
     "Set Project to io.cloud when creating/selecting key": "Set Project to io.cloud when creating/selecting key",
     "Set quota amount and limits": "Set quota amount and limits",
+    "Set Request Header": "Set Request Header",
+    "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Set runtime request header: override entire value, or manipulate comma-separated tokens",
     "Set Tag": "Set Tag",
     "Set tag for selected channels": "Set tag for selected channels",
     "Set the user's role (cannot be Root)": "Set the user's role (cannot be Root)",
@@ -3019,6 +3174,9 @@
     "Signed out": "Signed out",
     "Signing you in with {{provider}}": "Signing you in with {{provider}}",
     "SiliconFlow": "SiliconFlow",
+    "Simple": "Simple",
+    "Simple mode only returns message; status code and error type use system defaults.": "Simple mode only returns message; status code and error type use system defaults.",
+    "Simple mode: prune objects by type, e.g. redacted_thinking.": "Simple mode: prune objects by type, e.g. redacted_thinking.",
     "Single Key": "Single Key",
     "Site Key": "Site Key",
     "Size:": "Size:",
@@ -3043,6 +3201,9 @@
     "Sort by ID": "Sort by ID",
     "Sort Order": "Sort Order",
     "Source": "Source",
+    "Source Endpoint": "Source Endpoint",
+    "Source Field": "Source Field",
+    "Source Header": "Source Header",
     "sources": "sources",
     "Space-separated OAuth scopes": "Space-separated OAuth scopes",
     "Spark model version, e.g., v2.1 (version number in API URL)": "Spark model version, e.g., v2.1 (version number in API URL)",
@@ -3061,6 +3222,7 @@
     "Statistics reset": "Statistics reset",
     "Status": "Status",
     "Status & Sync": "Status & Sync",
+    "Status Code": "Status Code",
     "Status Code Mapping": "Status Code Mapping",
     "Status Page Slug": "Status Page Slug",
     "Status:": "Status:",
@@ -3068,6 +3230,7 @@
     "Stay tuned though!": "Stay tuned though!",
     "Step": "Step",
     "Stop": "Stop",
+    "Stop Retry": "Stop Retry",
     "Store ID": "Store ID",
     "Store ID is required": "Store ID is required",
     "Stored value is not echoed back for security": "Stored value is not echoed back for security",
@@ -3075,6 +3238,7 @@
     "Stream": "Stream",
     "Stream Mode": "Stream Mode",
     "Stream Status": "Stream Status",
+    "String Replace": "String Replace",
     "Stripe": "Stripe",
     "Stripe API key (leave blank unless updating)": "Stripe API key (leave blank unless updating)",
     "Stripe Dashboard": "Stripe Dashboard",
@@ -3111,6 +3275,7 @@
     "Super Admin": "Super Admin",
     "Support for high concurrency with automatic load balancing": "Support for high concurrency with automatic load balancing",
     "Supported Imagine Models": "Supported Imagine Models",
+    "Supported variables": "Supported variables",
     "Supports `-thinking`, `-thinking-": "Supports `-thinking`, `-thinking-",
     "Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.": "Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.",
     "Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.": "Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.",
@@ -3120,6 +3285,7 @@
     "Switch to JSON": "Switch to JSON",
     "Switch to Visual": "Switch to Visual",
     "Sync Endpoint": "Sync Endpoint",
+    "Sync Endpoints": "Sync Endpoints",
     "Sync Fields": "Sync Fields",
     "Sync from the public upstream metadata repository.": "Sync from the public upstream metadata repository.",
     "Sync this model with official upstream": "Sync this model with official upstream",
@@ -3161,7 +3327,12 @@
     "Tags": "Tags",
     "Take photo": "Take photo",
     "Take screenshot": "Take screenshot",
+    "Target Endpoint": "Target Endpoint",
+    "Target Field": "Target Field",
+    "Target Field Path": "Target Field Path",
     "Target group": "Target group",
+    "Target Header": "Target Header",
+    "Target Path (optional)": "Target Path (optional)",
     "Task": "Task",
     "Task ID": "Task ID",
     "Task ID:": "Task ID:",
@@ -3172,6 +3343,7 @@
     "Telegram": "Telegram",
     "Telegram login requires widget integration; coming soon": "Telegram login requires widget integration; coming soon",
     "Telegram Login Widget": "Telegram Login Widget",
+    "Template": "Template",
     "Template variables:": "Template variables:",
     "Templates": "Templates",
     "Templates appended": "Templates appended",
@@ -3276,9 +3448,11 @@
     "to access this resource.": "to access this resource.",
     "to confirm": "to confirm",
     "To Lower": "To Lower",
+    "To Lowercase": "To Lowercase",
     "to override billing when a user in one group uses a token of another group.": "to override billing when a user in one group uses a token of another group.",
     "to the Models list so users can use them before the mapping sends traffic upstream.": "to the Models list so users can use them before the mapping sends traffic upstream.",
     "To Upper": "To Upper",
+    "To Uppercase": "To Uppercase",
     "to view this resource.": "to view this resource.",
     "Today": "Today",
     "Toggle columns": "Toggle columns",
@@ -3309,8 +3483,8 @@
     "Tool prices": "Tool prices",
     "Top {{count}}": "Top {{count}}",
     "Top Models": "Top Models",
-    "Top Users": "Top Users",
     "Top up balance and view billing history.": "Top up balance and view billing history.",
+    "Top Users": "Top Users",
     "Top-up": "Top-up",
     "Top-up amount options": "Top-up amount options",
     "Top-up Audit Info": "Top-up Audit Info",
@@ -3349,6 +3523,7 @@
     "Transfer to Balance": "Transfer to Balance",
     "Translate `-thinking` suffixes into Anthropic native thinking models while keeping pricing predictable.": "Translate `-thinking` suffixes into Anthropic native thinking models while keeping pricing predictable.",
     "Transparent Billing": "Transparent Billing",
+    "Trim leading/trailing whitespace": "Trim leading/trailing whitespace",
     "Trim Prefix": "Trim Prefix",
     "Trim Space": "Trim Space",
     "Trim Suffix": "Trim Suffix",
@@ -3357,6 +3532,7 @@
     "TTL": "TTL",
     "TTL (seconds, 0 = default)": "TTL (seconds, 0 = default)",
     "TTL (seconds)": "TTL (seconds)",
+    "Tune selection priority, testing, status handling, and request overrides.": "Tune selection priority, testing, status handling, and request overrides.",
     "Turnstile is enabled but site key is empty.": "Turnstile is enabled but site key is empty.",
     "Two-factor Authentication": "Two-factor Authentication",
     "Two-Factor Authentication": "Two-Factor Authentication",
@@ -3365,6 +3541,7 @@
     "Two-factor authentication reset": "Two-factor authentication reset",
     "Two-Step Verification": "Two-Step Verification",
     "Type": "Type",
+    "Type (common)": "Type (common)",
     "Type *": "Type *",
     "Type a command or search...": "Type a command or search...",
     "Type-Specific Settings": "Type-Specific Settings",
@@ -3375,6 +3552,7 @@
     "Unable to load groups": "Unable to load groups",
     "Unable to open chat": "Unable to open chat",
     "Unable to prepare chat link. Please ensure you have an enabled API key.": "Unable to prepare chat link. Please ensure you have an enabled API key.",
+    "Unauthorized": "Unauthorized",
     "Unauthorized Access": "Unauthorized Access",
     "Unbind": "Unbind",
     "Unbind failed": "Unbind failed",
@@ -3431,6 +3609,7 @@
     "Upload photo": "Upload photo",
     "Upscale": "Upscale",
     "Upstream": "Upstream",
+    "Upstream Model Detection Settings": "Upstream Model Detection Settings",
     "Upstream Model Update Check": "Upstream Model Update Check",
     "Upstream Model Updates": "Upstream Model Updates",
     "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models",
@@ -3511,6 +3690,7 @@
     "Validity": "Validity",
     "Validity Period": "Validity Period",
     "Value": "Value",
+    "Value (supports JSON or plain text)": "Value (supports JSON or plain text)",
     "Value must be at least 0": "Value must be at least 0",
     "Value Regex": "Value Regex",
     "variable": "variable",
@@ -3573,10 +3753,12 @@
     "Visit Settings → General and adjust quota options...": "Visit Settings → General and adjust quota options...",
     "Visitors must authenticate before accessing the pricing directory.": "Visitors must authenticate before accessing the pricing directory.",
     "Visual": "Visual",
+    "Visual edit": "Visual edit",
     "Visual editor": "Visual editor",
     "Visual Editor": "Visual Editor",
     "Visual indicator color for the API card": "Visual indicator color for the API card",
     "Visual Mode": "Visual Mode",
+    "Visual Parameter Override": "Visual Parameter Override",
     "VolcEngine": "VolcEngine",
     "Waffo Pancake Payment Gateway": "Waffo Pancake payment gateway",
     "Waffo Payment": "Waffo Payment",
@@ -3633,6 +3815,7 @@
     "When enabled, the store field will be blocked": "When enabled, the store field will be blocked",
     "When enabled, violation requests will incur additional charges.": "When enabled, violation requests will incur additional charges.",
     "When enabled, zero-cost models also pre-consume quota before final settlement.": "When enabled, zero-cost models also pre-consume quota before final settlement.",
+    "When no conditions are set, the operation always executes.": "When no conditions are set, the operation always executes.",
     "When performance monitoring is enabled and system resource usage exceeds the set threshold, new Relay requests will be rejected.": "When performance monitoring is enabled and system resource usage exceeds the set threshold, new Relay requests will be rejected.",
     "When running in containers or ephemeral environments, ensure the SQLite file is mapped to persistent storage to avoid data loss on restart.": "When running in containers or ephemeral environments, ensure the SQLite file is mapped to persistent storage to avoid data loss on restart.",
     "Whitelist": "Whitelist",
@@ -3641,10 +3824,12 @@
     "whsec_xxx": "whsec_xxx",
     "Window:": "Window:",
     "with conflicts": "with conflicts",
+    "Without additional conditions, only the type above is used for pruning.": "Without additional conditions, only the type above is used for pruning.",
     "Worker Access Key": "Worker Access Key",
     "Worker Proxy": "Worker Proxy",
     "Worker URL": "Worker URL",
     "Workspaces": "Workspaces",
+    "Write value to the target field": "Write value to the target field",
     "x": "x",
     "xAI": "xAI",
     "Xinference": "Xinference",
@@ -3678,6 +3863,7 @@
     "Your Turnstile site key": "Your Turnstile site key",
     "Zhipu": "Zhipu",
     "Zhipu V4": "Zhipu V4",
-    "Zoom": "Zoom"
+    "Zoom": "Zoom",
+    "Legacy Format Template": "Legacy Format Template"
   }
 }

File diff suppressed because it is too large
+ 192 - 7
web/default/src/i18n/locales/fr.json


File diff suppressed because it is too large
+ 196 - 13
web/default/src/i18n/locales/ja.json


File diff suppressed because it is too large
+ 196 - 11
web/default/src/i18n/locales/ru.json


+ 195 - 9
web/default/src/i18n/locales/vi.json

@@ -10,6 +10,7 @@
     ". This action cannot be undone.": ". Hành động này không thể hoàn tác.",
     "...": "...",
     "\"default\": \"us-central1\", \"claude-3-5-sonnet-20240620\": \"europe-west1\"": "\"default\": \"us-central1\", \"claude-3-5-sonnet-20240620\": \"europe-west1\"",
+    "({{total}} total, {{omit}} omitted)": "({{total}} tổng cộng, đã lược bỏ {{omit}})",
     "(Leave empty to dissolve tag)": "Để trống để xóa thẻ.",
     "(Optional: redirect model names)": "(Tùy chọn: chuyển hướng tên mô hình)",
     "(Override all channels' groups)": "(Ghi đè các nhóm của tất cả các kênh)",
@@ -122,6 +123,7 @@
     "Add auto group": "Thêm nhóm tự động",
     "Add chat preset": "Thêm mẫu trò chuyện",
     "Add condition": "Thêm điều kiện",
+    "Add Condition": "Thêm điều kiện",
     "Add custom model(s), comma-separated": "Thêm mô hình tùy chỉnh, phân tách bằng dấu phẩy",
     "Add discount tier": "Thêm bậc giảm giá",
     "Add each model or tag you want to include.": "Thêm mỗi mô hình hoặc thẻ bạn muốn đưa vào.",
@@ -164,12 +166,14 @@
     "Added {{count}} custom model(s)": "Đã thêm {{count}} mô hình tùy chỉnh",
     "Added {{count}} model(s)": "Đã thêm {{count}} mô hình",
     "Added successfully": "Thêm thành công",
+    "Additional Conditions": "Điều kiện bổ sung",
     "Additional information": "Thông tin bổ sung",
     "Additional Information": "Thông tin bổ sung",
     "Additional Limit": "Hạn mức bổ sung",
     "Additional Limits": "Các hạn mức bổ sung",
     "Additional metered capability": "Tính năng tính phí theo mức dùng bổ sung",
     "Adjust Quota": "Điều chỉnh hạn mức",
+    "Adjust response formatting, prompt behavior, proxy, and upstream automation.": "Điều chỉnh định dạng phản hồi, hành vi prompt, proxy và tự động hóa upstream.",
     "Adjust the appearance and layout to suit your preferences.": "Điều chỉnh giao diện và bố cục để phù hợp với sở thích của bạn.",
     "Admin": "Quản trị viên",
     "Admin access required": "Yêu cầu quyền truy cập Admin",
@@ -185,6 +189,7 @@
     "Advanced Options": "Tùy chọn nâng cao",
     "Advanced platform configuration.": "Cấu hình nền tảng nâng cao.",
     "Advanced Settings": "Cài đặt nâng cao",
+    "Advanced text editing": "Chỉnh sửa văn bản nâng cao",
     "After clicking the button, you'll be asked to authorize the bot": "Sau khi nhấp vào nút, bạn sẽ được yêu cầu ủy quyền cho bot",
     "After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?": "Sau khi vô hiệu hóa, sẽ không hiển thị cho người dùng nữa, nhưng đơn hàng lịch sử không bị ảnh hưởng. Tiếp tục?",
     "After enabling, the plan will be shown to users. Continue?": "Sau khi kích hoạt, gói sẽ được hiển thị cho người dùng. Tiếp tục?",
@@ -209,6 +214,7 @@
     "All Groups": "Tất cả các nhóm",
     "All Models": "Tất cả các mẫu",
     "All models in use are properly configured.": "Tất cả các mô hình đang được sử dụng đều được cấu hình đúng cách.",
+    "All Must Match (AND)": "Tất cả phải khớp (AND)",
     "All Status": "Tất cả trạng thái",
     "All Sync Status": "Tất cả Trạng thái Đồng bộ",
     "All Tags": "Tất cả Thẻ",
@@ -229,6 +235,7 @@
     "Allow Private IPs": "Cho phép IP riêng",
     "Allow registration with password": "Cho phép đăng ký bằng mật khẩu",
     "Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)": "Cho phép các yêu cầu đến các dải IP riêng (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)",
+    "Allow Retry": "Cho phép thử lại",
     "Allow safety_identifier passthrough": "Cho phép chuyển tiếp safety_identifier",
     "Allow service_tier passthrough": "Cho phép chuyển tiếp service_tier",
     "Allow speed passthrough": "Cho phép truyền speed",
@@ -271,6 +278,8 @@
     "Announcements saved successfully": "Đã lưu thông báo thành công",
     "Answer": "Trả lời",
     "Anthropic": "Anthropic",
+    "Any Match (OR)": "Bất kỳ khớp (OR)",
+    "API Access": "Truy cập API",
     "API Addresses": "Địa chỉ API",
     "API Base URL (Important: Not Chat API) *": "URL cơ sở API (Quan trọng: Không phải API Chat) *",
     "API Base URL *": "URL cơ sở API *",
@@ -305,8 +314,11 @@
     "API2GPT": "API2GPT",
     "Append": "Thêm vào cuối",
     "Append mode: New keys will be added to the end of the existing key list": "Chế độ nối: Các khóa mới sẽ được thêm vào cuối danh sách khóa hiện có",
+    "Append Template": "Thêm mẫu",
     "Append to channel": "Nối vào kênh",
+    "Append to End": "Thêm vào cuối",
     "Append to existing keys": "Add to existing keys",
+    "Append value to array / string / object end": "Thêm giá trị vào cuối mảng / chuỗi / đối tượng",
     "appended": "đã thêm vào cuối, được phụ lục",
     "Application": "Ứng dụng",
     "Applies to custom completion endpoints. JSON map of model → ratio.": "Áp dụng cho các điểm cuối hoàn thành tùy chỉnh. Bản đồ JSON của mô hình → tỷ lệ.",
@@ -329,9 +341,9 @@
     "Are you sure you want to unbind {{provider}} for this user? The user will no longer be able to log in via this method.": "Bạn có chắc chắn muốn hủy liên kết {{provider}} cho người dùng này? Người dùng sẽ không thể đăng nhập bằng phương thức này nữa.",
     "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "Bạn có chắc chắn muốn hủy liên kết {{provider}}? Bạn sẽ không thể đăng nhập bằng phương thức này nữa.",
     "Are you sure?": "Bạn có chắc không?",
+    "Area Chart": "Biểu đồ vùng",
     "Args (space separated)": "Đối số (cách nhau bằng khoảng trắng)",
     "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "Mảng các thiết lập sẵn của ứng dụng trò chuyện. Mỗi mục là một đối tượng với",
-    "Area Chart": "Biểu đồ vùng",
     "Asc": "Asc",
     "Ask anything": "Hỏi gì cũng được",
     "Async task refund": "Hoàn tiền tác vụ bất đồng bộ",
@@ -370,6 +382,7 @@
     "Auto-disable status codes": "Mã trạng thái tự tắt",
     "Auto-discover": "Tự động khám phá",
     "Auto-discovers endpoints from the provider": "Tự động khám phá các điểm cuối từ nhà cung cấp",
+    "Auto-fill when one field exists and another is missing": "Tự động điền khi một trường có giá trị và trường khác thiếu",
     "Auto-retry status codes": "Mã trạng thái tự thử lại",
     "Automatically disable channel on repeated failures": "Tự động vô hiệu hóa kênh khi xảy ra lỗi lặp lại",
     "Automatically disable channels exceeding this response time": "Tự động vô hiệu hóa các kênh vượt quá thời gian phản hồi này",
@@ -386,6 +399,7 @@
     "Average RPM": "RPM trung bình",
     "Average TPM": "TPM trung bình",
     "AWS": "AWS",
+    "AWS Bedrock Claude Compat": "AWS Bedrock Claude tương thích",
     "AWS Key Format": "Định dạng khóa AWS",
     "Azure": "Azure",
     "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *",
@@ -399,6 +413,7 @@
     "Backup code must be in format XXXX-XXXX": "Mã dự phòng phải có định dạng XXXX-XXXX",
     "Backup codes regenerated successfully": "Mã dự phòng đã được tạo lại thành công",
     "Backup codes remaining: {{count}}": "Mã dự phòng còn lại: {{count}}",
+    "Bad Request": "Yêu cầu không hợp lệ",
     "Badge Color": "Màu huy hiệu",
     "Baidu": "Baidu",
     "Baidu V2": "Baidu V2",
@@ -420,6 +435,7 @@
     "Basic Configuration": "Cấu hình cơ bản",
     "Basic Info": "Thông tin cơ bản",
     "Basic Information": "Thông tin cơ bản",
+    "Basic Templates": "Mẫu cơ bản",
     "Batch Add (one key per line)": "Thêm hàng loạt (mỗi khóa một dòng)",
     "Batch delete failed": "Xóa hàng loạt thất bại",
     "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Phát hiện hàng loạt hoàn tất: {{channels}} kênh, {{add}} để thêm, {{remove}} để xóa, {{fails}} thất bại",
@@ -533,6 +549,7 @@
     "Channel enabled successfully": "Kênh đã được bật thành công",
     "Channel Extra Settings": "Cài đặt thêm kênh",
     "Channel ID": "Mã kênh",
+    "Channel key": "Khóa kênh",
     "Channel key unlocked": "Khóa kênh đã được mở khóa",
     "Channel models": "Channel model",
     "Channel name is required": "Tên kênh là bắt buộc",
@@ -585,6 +602,7 @@
     "Choose where to fetch upstream metadata.": "Chọn nơi để tìm nạp siêu dữ liệu thượng nguồn.",
     "Classic (Legacy Frontend)": "Cổ điển (Frontend cũ)",
     "Claude": "Claude",
+    "Claude CLI Header Passthrough": "Chuyển tiếp header Claude CLI",
     "Clean history logs": "Xóa nhật ký lịch sử",
     "Clean logs": "Dọn dẹp nhật ký",
     "Clean up inactive cache": "Dọn dẹp bộ nhớ đệm không hoạt động",
@@ -621,6 +639,7 @@
     "Click to view full details": "Nhấn để xem chi tiết đầy đủ",
     "Click to view full error message": "Nhấp để xem toàn bộ thông báo lỗi",
     "Click to view full prompt": "Nhấp để xem toàn bộ lời nhắc",
+    "Client header value": "Giá trị header client",
     "Client ID": "Mã khách hàng",
     "Client Secret": "Bí mật máy khách",
     "Close": "Đóng",
@@ -636,12 +655,15 @@
     "Codex Account & Usage": "Tài khoản và sử dụng Codex",
     "Codex Authorization": "Ủy quyền Codex",
     "Codex channels use an OAuth JSON credential as the key.": "Kênh Codex dùng thông tin xác thực OAuth JSON làm khóa.",
+    "Codex CLI Header Passthrough": "Chuyển tiếp header Codex CLI",
     "Cohere": "Cohere",
     "Collapse": "Thu gọn",
+    "Collapse All": "Thu gọn tất cả",
     "Color": "Màu",
     "Color is required": "Màu sắc là bắt buộc",
     "Color:": "Màu sắc:",
     "Coming Soon!": "Sắp ra mắt!",
+    "Comma-separated exact model names. Prefix with regex: to ignore by regular expression.": "Tên mô hình chính xác, phân tách bằng dấu phẩy. Thêm tiền tố regex: để bỏ qua bằng biểu thức chính quy.",
     "Comma-separated list of allowed ports (empty = all ports)": "Danh sách các cổng được phép, phân cách bằng dấu phẩy (để trống = tất cả các cổng)",
     "Comma-separated model names (leave empty to keep current)": "Tên mô hình phân tách bằng dấu phẩy (để trống để giữ nguyên hiện tại)",
     "Comma-separated model names, e.g., gpt-4,gpt-3.5-turbo": "Tên mô hình được phân tách bằng dấu phẩy, ví dụ: gpt-4,gpt-3.5-turbo",
@@ -659,7 +681,11 @@
     "Completion price ($/1M tokens)": "Giá hoàn thành ($/1M tokens)",
     "Completion ratio": "Tỷ lệ hoàn thành",
     "Concatenate channel system prompt with user&apos;s prompt": "Nối lời nhắc hệ thống kênh với lời nhắc của người dùng",
+    "Condition Path": "Đường dẫn điều kiện",
+    "Condition Settings": "Cài đặt điều kiện",
+    "Condition Value": "Giá trị điều kiện",
     "Conditional multipliers": "Hệ số nhân có điều kiện",
+    "Conditions": "Điều kiện",
     "Conditions (AND)": "Điều kiện (AND)",
     "Confidence": "Tự tin",
     "Configuration": "Cấu hình",
@@ -768,16 +794,20 @@
     "Control log retention and clean historical data.": "Kiểm soát việc lưu giữ nhật ký và làm sạch dữ liệu lịch sử.",
     "Control passthrough behavior and connection keep-alive settings": "Kiểm soát hành vi truyền qua và cài đặt duy trì kết nối",
     "Control request frequency to prevent abuse and manage system load.": "Kiểm soát tần suất yêu cầu để ngăn chặn lạm dụng và quản lý tải hệ thống.",
+    "Control which models are exposed and which groups may use them.": "Kiểm soát mô hình được hiển thị và nhóm nào có thể sử dụng chúng.",
     "Control which sidebar areas and modules are available to all users.": "Kiểm soát những khu vực thanh bên và mô-đun nào khả dụng cho tất cả người dùng.",
     "Controls whether user verification (biometrics/PIN) is required during Passkey flows.": "Kiểm soát xem liệu có yêu cầu xác minh người dùng (sinh trắc học/mã PIN) trong các luồng Passkey hay không.",
     "Conversion rate from USD to your custom currency": "Tỷ giá chuyển đổi từ USD sang đơn vị tiền tệ tùy chỉnh của bạn",
     "Convert reasoning_content to <think> tag in content": "Chuyển đổi reasoning_content thành thẻ <think> trong nội dung",
+    "Convert string to lowercase": "Chuyển chuỗi sang chữ thường",
+    "Convert string to uppercase": "Chuyển chuỗi sang chữ hoa",
     "Copied": "Đã sao chép",
     "Copied {{count}} key(s)": "Đã sao chép {{count}} khóa",
     "Copied to clipboard": "Đã sao chép vào bộ nhớ tạm",
     "Copied: {{model}}": "Đã sao chép: {{model}}",
     "Copied!": "Đã sao chép!",
     "Copy": "Sao chép",
+    "Copy a request header": "Sao chép header yêu cầu",
     "Copy All": "Sao chép tất cả",
     "Copy all backup codes": "Sao chép tất cả mã dự phòng",
     "Copy All Codes": "Sao chép Tất cả Mã",
@@ -787,6 +817,7 @@
     "Copy code": "Sao chép mã",
     "Copy Connection Info": "Sao chép thông tin kết nối",
     "Copy failed": "Sao chép thất bại",
+    "Copy Field": "Sao chép trường",
     "Copy Header": "Sao chép tiêu đề",
     "Copy Key": "Sao chép khóa",
     "Copy Link": "Sao chép liên kết",
@@ -795,14 +826,17 @@
     "Copy prompt": "Sao chép prompt",
     "Copy redemption code": "Sao chép mã đổi thưởng",
     "Copy referral link": "Sao chép liên kết giới thiệu",
+    "Copy Request Header": "Sao chép header yêu cầu",
     "Copy secret key": "Sao chép khóa bí mật",
     "Copy selected codes": "Sao chép các mã đã chọn",
     "Copy selected keys": "Sao chép các khóa đã chọn",
+    "Copy source field to target field": "Sao chép trường nguồn sang trường đích",
     "Copy the key and paste it here": "Sao chép khóa và dán vào đây",
     "Copy this prompt and send it to an LLM (e.g. ChatGPT / Claude) to help design your billing expression.": "Sao chép prompt này và gửi cho LLM (ví dụ: ChatGPT / Claude) để được hỗ trợ thiết kế biểu thức tính phí.",
     "Copy to clipboard": "Sao chép vào bảng tạm",
     "Copy token": "Sao chép mã thông báo",
     "Copy URL": "Sao chép URL",
+    "Core Configuration": "Cấu hình chính",
     "Core Features": "Tính năng cốt lõi",
     "Cost": "Chi phí",
     "Cost in USD per request, regardless of tokens used.": "Chi phí bằng USD cho mỗi yêu cầu, bất kể số lượng token được sử dụng.",
@@ -834,6 +868,8 @@
     "Create Prefill Group": "Tạo Nhóm Điền Sẵn",
     "Create Provider": "Tạo nhà cung cấp",
     "Create Redemption Code": "Tạo mã đổi thưởng",
+    "Create request parameter override rules with a visual editor or raw JSON.": "Tạo quy tắc ghi đè tham số yêu cầu bằng trình soạn trực quan hoặc JSON thô.",
+    "Create request parameter override rules without editing raw JSON.": "Tạo quy tắc ghi đè tham số yêu cầu mà không cần sửa JSON thô.",
     "Create reusable bundles of models, tags, endpoints, and user groups to speed up configuration elsewhere in the console.": "Tạo các gói có thể tái sử dụng gồm các mô hình, thẻ, điểm cuối và nhóm người dùng để tăng tốc cấu hình ở những nơi khác trong bảng điều khiển.",
     "Create succeeded": "Tạo thành công",
     "Create Vendor": "Tạo Nhà cung cấp",
@@ -858,6 +894,8 @@
     "Current Cache Size": "Kích thước bộ nhớ đệm hiện tại",
     "Current email: {{email}}. Enter a new email to change.": "Email hiện tại: {{email}}. Nhập email mới để thay đổi.",
     "Current key": "Khóa hiện tại",
+    "Current legacy JSON is invalid, cannot append": "JSON định dạng cũ hiện tại không hợp lệ, không thể thêm",
+    "Current Level Only": "Chỉ cấp hiện tại",
     "Current models for the longest channel in this tag. May not include all models from all channels.": "Các mô hình hiện tại cho kênh dài nhất trong thẻ này. Có thể không bao gồm tất cả các mô hình từ tất cả các kênh.",
     "Current Password": "Mật khẩu hiện tại",
     "Current Price": "Giá hiện tại",
@@ -874,6 +912,7 @@
     "Custom Currency Symbol": "Ký hiệu tiền tệ tùy chỉnh",
     "Custom currency symbol is required": "Ký hiệu tiền tệ tùy chỉnh là bắt buộc",
     "Custom database driver detected.": "Đã phát hiện trình điều khiển cơ sở dữ liệu tùy chỉnh.",
+    "Custom Error Response": "Phản hồi lỗi tùy chỉnh",
     "Custom Home Page": "Trang chủ tùy chỉnh",
     "Custom message shown when access is denied": "Thông báo tùy chỉnh hiển thị khi truy cập bị từ chối",
     "Custom model (comma-separated)": "Mô hình tùy chỉnh (phân tách bằng dấu phẩy)",
@@ -923,13 +962,17 @@
     "Delete": "Xóa",
     "Delete (": "Xóa (",
     "Delete {{count}} API key(s)?": "Xóa {{count}} khóa API?",
+    "Delete a runtime request header": "Xóa header yêu cầu runtime",
     "Delete Account": "Xóa tài khoản",
     "Delete All Disabled": "Xóa Tất Cả Đã Tắt",
     "Delete All Disabled Channels?": "Xóa tất cả kênh đã vô hiệu hóa?",
     "Delete Auto-Disabled": "Xóa Tự động vô hiệu hóa",
     "Delete Channel": "Xóa Kênh",
     "Delete Channels?": "Xóa các kênh?",
+    "Delete condition": "Xóa điều kiện",
+    "Delete Condition": "Xóa điều kiện",
     "Delete failed": "Xóa thất bại",
+    "Delete Field": "Xóa trường",
     "Delete group": "Xóa nhóm",
     "Delete Header": "Xóa tiêu đề",
     "Delete Invalid": "Xóa không hợp lệ",
@@ -941,6 +984,7 @@
     "Delete Model": "Xóa Mô hình",
     "Delete Models?": "Xóa mô hình?",
     "Delete Provider": "Xóa nhà cung cấp",
+    "Delete Request Header": "Xóa header yêu cầu",
     "Delete selected API keys": "Xóa các khóa API đã chọn",
     "Delete selected channels": "Xóa các kênh đã chọn",
     "Delete selected models": "Xóa các mô hình đã chọn",
@@ -1033,6 +1077,8 @@
     "Displays the mobile sidebar.": "Hiển thị thanh bên di động.",
     "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.": "Đừng tin tưởng quá mức vào tính năng này. IP có thể bị giả mạo. Hãy sử dụng cùng với nginx, CDN và các gateway khác.",
     "Do not repeat check-in; only once per day": "Không lặp lại check-in; chỉ một lần mỗi ngày",
+    "Do regex replacement in the target field": "Thực hiện thay thế regex trong trường đích",
+    "Do string replacement in the target field": "Thực hiện thay thế chuỗi trong trường đích",
     "Docs": "Tài liệu",
     "Documentation Link": "Liên kết tài liệu",
     "Documentation or external knowledge base.": "Tài liệu hoặc cơ sở kiến thức bên ngoài.",
@@ -1062,6 +1108,7 @@
     "e.g. 401, 403, 429, 500-599": "vd. 401, 403, 429, 500-599",
     "e.g. 8 means 1 USD = 8 units": "Ví dụ: 8 có nghĩa là 1 USD = 8 đơn vị",
     "e.g. Basic Plan": "ví dụ: Gói cơ bản",
+    "e.g. Clean tool parameters to avoid upstream validation errors": "ví dụ: Dọn dẹp tham số công cụ để tránh lỗi xác thực upstream",
     "e.g. example.com": "ví dụ example.com",
     "e.g. llama3.1:8b": "ví dụ: llama3.1:8b",
     "e.g. My GitLab": "ví dụ: GitLab của tôi",
@@ -1069,6 +1116,7 @@
     "e.g. New API Console": "Ví dụ: Bảng điều khiển API mới",
     "e.g. openid profile email": "ví dụ: openid profile email",
     "e.g. Suitable for light usage": "ví dụ: Phù hợp cho sử dụng nhẹ",
+    "e.g. This request does not meet access policy": "ví dụ: Yêu cầu này không đáp ứng chính sách truy cập",
     "e.g., 0.95": "e.g., 0.95",
     "e.g., 100": "e.g., 100",
     "e.g., 123456": "e.g., 123456",
@@ -1084,6 +1132,7 @@
     "e.g., d6b5da8hk1awo8nap34ube6gh": "ví dụ: d6b5da8hk1awo8nap34ube6gh",
     "e.g., default, vip, premium": "ví dụ: mặc định, VIP, cao cấp",
     "e.g., gpt-4, claude-3": "vd: gpt-4, claude-3",
+    "e.g., gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "ví dụ: gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$",
     "e.g., https://api.example.com (path before /suno)": "ví dụ: https://api.example.com (đường dẫn trước /suno)",
     "e.g., https://api.openai.com/v1/chat/completions": "ví dụ: https://api.openai.com/v1/chat/completions",
     "e.g., https://ark.cn-beijing.volces.com": "ví dụ, https://ark.cn-beijing.volces.com",
@@ -1111,6 +1160,8 @@
     "Edit discount tier": "Chỉnh sửa bậc giảm giá",
     "Edit FAQ": "Chỉnh sửa câu hỏi thường gặp",
     "Edit group rate limit": "Chỉnh sửa giới hạn tốc độ nhóm",
+    "Edit JSON object directly. Suitable for simple parameter overrides.": "Chỉnh sửa đối tượng JSON trực tiếp. Phù hợp cho ghi đè tham số đơn giản.",
+    "Edit JSON text directly. Format will be validated on save.": "Chỉnh sửa văn bản JSON trực tiếp. Định dạng sẽ được kiểm tra khi lưu.",
     "Edit model": "Chỉnh sửa mô hình",
     "Edit Model": "Chỉnh sửa Mô hình",
     "Edit OAuth Provider": "Chỉnh Sửa Nhà Cung Cấp OAuth",
@@ -1195,6 +1246,8 @@
     "English": "Tiếng Anh",
     "Ensure Prefix": "Đảm bảo tiền tố",
     "Ensure Suffix": "Đảm bảo hậu tố",
+    "Ensure the string has a specified prefix": "Đảm bảo chuỗi có tiền tố chỉ định",
+    "Ensure the string has a specified suffix": "Đảm bảo chuỗi có hậu tố chỉ định",
     "Enter 6-digit code": "Nhập mã 6 chữ số",
     "Enter a name": "Nhập tên",
     "Enter a new name": "Nhập tên mới",
@@ -1220,6 +1273,7 @@
     "Enter display name": "Nhập tên hiển thị",
     "Enter HTML code (e.g., <p>About us...</p>) or a URL (e.g., https://example.com) to embed as iframe": "Nhập mã HTML (ví dụ, <p>Về chúng tôi...</p>) hoặc một URL (ví dụ, https://example.com) để nhúng dưới dạng iframe",
     "Enter Input price to calculate ratio": "Nhập giá đầu vào để tính tỷ lệ",
+    "Enter JSON to override request headers": "Nhập JSON để ghi đè header yêu cầu",
     "Enter key, format: AccessKey|SecretAccessKey|Region": "Nhập khóa, định dạng: AccessKey|SecretAccessKey|Region",
     "Enter key, one per line, format: AccessKey|SecretAccessKey|Region": "Nhập khóa, mỗi dòng một khóa, định dạng: AccessKey|SecretAccessKey|Region",
     "Enter model name": "Nhập tên mô hình",
@@ -1271,8 +1325,12 @@
     "Epay Gateway": "Cổng thanh toán Epay",
     "Epay merchant ID": "ID người bán Epay",
     "Epay secret key": "Khóa bí mật Epay",
+    "Equals": "Bằng",
     "Error": "Lỗi",
+    "Error Code (optional)": "Mã lỗi (tùy chọn)",
     "Error Message": "Thông báo lỗi",
+    "Error Message (required)": "Thông báo lỗi (bắt buộc)",
+    "Error Type (optional)": "Loại lỗi (tùy chọn)",
     "Estimated cost": "Chi phí ước tính",
     "Estimated quota cost": "Ước tính chi phí hạn mức",
     "Exact": "Chính xác",
@@ -1290,6 +1348,7 @@
     "Existing account will be reused": "Tài khoản hiện có sẽ được sử dụng lại",
     "Existing Models ({{count}})": "Các mô hình hiện có ({{count}})",
     "Exists": "Tồn tại",
+    "Expand All": "Mở rộng tất cả",
     "Expected a JSON array.": "Cần là một mảng JSON.",
     "Experiment with prompts and models in real time.": "Thử nghiệm với prompt và mô hình theo thời gian thực.",
     "Expiration Time": "Thời gian hết hạn",
@@ -1456,6 +1515,7 @@
     "field": "trường",
     "Field Mapping": "Ánh Xạ Trường",
     "Field passthrough controls": "Điều khiển chuyển tiếp trường",
+    "Field Path": "Đường dẫn trường",
     "File Search": "Tìm kiếm tệp",
     "Files to Retain": "Số tệp giữ lại",
     "Fill All Models": "Điền Tất Cả Mô Hình",
@@ -1551,6 +1611,7 @@
     "GC executed": "GC đã thực thi",
     "GC execution failed": "Thực thi GC thất bại",
     "Gemini": "Song Tử",
+    "Gemini Image 4K": "Gemini Image 4K",
     "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini sẽ tiếp tục tự động phát hiện chế độ suy nghĩ ngay cả khi bộ điều hợp bị tắt. Chỉ bật tính năng này khi bạn cần kiểm soát chi tiết hơn về giá cả và lập ngân sách.",
     "General": "Chung",
     "General Settings": "General settings",
@@ -1592,6 +1653,8 @@
     "gpt-4": "gpt-4",
     "gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, v.v.",
     "GPU count": "Số lượng GPU",
+    "Greater Than": "Lớn hơn",
+    "Greater Than or Equal": "Lớn hơn hoặc bằng",
     "Grok": "Grok",
     "Grok Settings": "Cài đặt Grok",
     "Group": "Nhóm",
@@ -1629,8 +1692,11 @@
     "Has been invalidated": "Đã vô hiệu hóa thành công",
     "Have a Code?": "Có mã không?",
     "Header": "Header",
+    "Header Name": "Tên header",
     "Header navigation": "Điều hướng đầu trang",
     "Header Override": "Ghi đè tiêu đề",
+    "Header Passthrough (X-Request-Id)": "Chuyển tiếp header (X-Request-Id)",
+    "Header Value (supports string or JSON mapping)": "Giá trị header (hỗ trợ chuỗi hoặc ánh xạ JSON)",
     "Hidden — verify to reveal": "Ẩn — xác minh để hiển thị",
     "Hide": "Ẩn",
     "Hide API key": "Ẩn khóa API",
@@ -1697,6 +1763,7 @@
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "Nếu ủy quyền thành công, JSON tạo ra sẽ được chèn vào trường khóa. Bạn vẫn cần lưu kênh để áp dụng.",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Nếu kết nối với dự án relay One API hoặc New API upstream, hãy sử dụng loại OpenAI thay thế trừ khi bạn biết mình đang làm gì",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Nếu kênh ưu tiên thất bại và thử lại thành công trên kênh khác, cập nhật ưu tiên sang kênh thành công.",
+    "Ignored upstream models": "Mô hình upstream bị bỏ qua",
     "Image": "Hình ảnh",
     "Image Generation": "Tạo hình ảnh",
     "Image In": "Ảnh vào",
@@ -1730,6 +1797,7 @@
     "Integrations": "Tích hợp",
     "Inter-group overrides": "Ghi đè liên nhóm",
     "Inter-group ratio overrides": "Tỷ lệ liên nhóm ghi đè",
+    "Internal Notes": "Ghi chú nội bộ",
     "Internal notes (not shown to users)": "Ghi chú nội bộ (không hiển thị cho người dùng)",
     "Internal Server Error!": "Lỗi máy chủ nội bộ!",
     "Invalid chat link. Please contact the administrator.": "Liên kết trò chuyện không hợp lệ. Vui lòng liên hệ quản trị viên.",
@@ -1750,6 +1818,7 @@
     "Invalid status code mapping entries: {{entries}}": "Mục ánh xạ mã trạng thái không hợp lệ: {{entries}}",
     "Invalidate": "Vô hiệu hóa",
     "Invalidated": "Đã vô hiệu",
+    "Invert match": "Đảo điều kiện khớp",
     "Invitation Code": "Mã mời",
     "Invitation Quota": "Hạn mức lời mời",
     "Invite Info": "Thông tin mời",
@@ -1777,6 +1846,7 @@
     "JSON": "JSON",
     "JSON array of group identifiers. When enabled below, new tokens rotate through this list.": "Mảng JSON của các định danh nhóm. Khi được bật bên dưới, các token mới sẽ luân phiên qua danh sách này.",
     "JSON Editor": "Trình chỉnh sửa JSON",
+    "JSON format error": "Lỗi định dạng JSON",
     "JSON format supports service account JSON files": "Định dạng JSON hỗ trợ các tệp JSON tài khoản dịch vụ",
     "JSON map of group → description exposed when users create API keys.": "Ánh xạ JSON của nhóm → mô tả được hiển thị khi người dùng tạo khóa API.",
     "JSON map of group → ratio applied when the user selects the group explicitly.": "Bản đồ JSON của nhóm → tỷ lệ được áp dụng khi người dùng chọn nhóm đó một cách rõ ràng.",
@@ -1785,11 +1855,14 @@
     "JSON Mode": "Chế độ JSON",
     "JSON must be an object": "JSON phải là object",
     "JSON object:": "Đối tượng JSON:",
+    "JSON Text": "Văn bản JSON",
     "JSON-based access control rules. Leave empty to allow all users.": "Quy tắc kiểm soát truy cập dựa trên JSON. Để trống để cho phép tất cả người dùng.",
     "Just now": "Vừa nãy",
     "JustSong": "JustSong",
     "K": "K",
     "Keep enabled if you need to proxy requests for different upstream accounts.": "Giữ bật nếu bạn cần proxy yêu cầu cho các tài khoản upstream khác nhau.",
+    "Keep original value": "Giữ giá trị gốc",
+    "Keep original value (skip if target exists)": "Giữ giá trị gốc (bỏ qua nếu đích đã tồn tại)",
     "Keep this above 1 minute to avoid heavy database load": "Giữ cái này trên 1 phút để tránh tải nặng cơ sở dữ liệu",
     "Keep-alive Ping": "Ping duy trì",
     "Key": "Khóa",
@@ -1797,9 +1870,12 @@
     "Key Sources": "Nguồn khóa",
     "Key Summary": "Tóm tắt khóa",
     "Key Update Mode": "Chế độ cập nhật khóa",
+    "Keys, OAuth credentials, and multi-key update behavior.": "Khóa, thông tin xác thực OAuth và hành vi cập nhật nhiều khóa.",
     "Kling": "Kling",
     "Knowledge Base ID *": "Mã số Cơ sở kiến thức *",
     "Landing page with system overview.": "Trang chủ với tổng quan hệ thống.",
+    "Last check time": "Thời gian kiểm tra gần nhất",
+    "Last detected addable models": "Mô hình có thể thêm được phát hiện gần nhất",
     "Last Login": "Lần đăng nhập cuối",
     "Last Seen": "Lần cuối",
     "Last Tested": "Được kiểm tra lần cuối",
@@ -1823,7 +1899,11 @@
     "Leave empty to use default": "Để trống để sử dụng mặc định",
     "Leave empty to use system temp directory": "Để trống để sử dụng thư mục tạm của hệ thống",
     "Leave empty to use username": "Để trống để sử dụng tên người dùng",
+    "Legacy Format (JSON Object)": "Định dạng cũ (đối tượng JSON)",
+    "Legacy format must be a JSON object": "Định dạng cũ phải là đối tượng JSON",
     "Less": "Ít hơn",
+    "Less Than": "Nhỏ hơn",
+    "Less Than or Equal": "Nhỏ hơn hoặc bằng",
     "Light": "Ánh sáng",
     "Lightning Fast": "Nhanh như chớp",
     "Limit period": "Thời hiệu",
@@ -1863,6 +1943,7 @@
     "Log IP address for usage and error logs": "Ghi lại địa chỉ IP cho nhật ký sử dụng và lỗi",
     "Log Maintenance": "Bảo trì Nhật ký",
     "Log Type": "Loại nhật ký",
+    "Logic": "Logic",
     "Login failed": "Đăng nhập thất bại",
     "Logo": "Logo",
     "Logo URL": "URL Logo",
@@ -1900,11 +1981,17 @@
     "Map request model names to actual provider model names (JSON format)": "Ánh xạ tên mô hình yêu cầu đến tên mô hình thực tế của nhà cung cấp (định dạng JSON)",
     "Map response status codes (JSON format)": "Ánh xạ mã trạng thái phản hồi (định dạng JSON)",
     "Map upstream status codes to different codes": "Ánh xạ mã trạng thái upstream sang các mã khác",
+    "Match All (AND)": "Tất cả khớp (AND)",
+    "Match Any (OR)": "Bất kỳ khớp (OR)",
+    "Match Mode": "Chế độ khớp",
     "Match model name exactly": "Khớp chính xác tên mô hình",
     "Match models containing this name": "Khớp các mô hình chứa tên này",
     "Match models ending with this name": "Khớp các mô hình kết thúc bằng tên này",
     "Match models starting with this name": "Khớp các mô hình bắt đầu bằng tên này",
+    "Match Text": "Văn bản khớp",
     "Match Type": "Loại đối sánh",
+    "Match Value": "Giá trị khớp",
+    "Match Value (optional)": "Giá trị khớp (tùy chọn)",
     "Matched": "Đã khớp",
     "Matched Tier": "Bậc khớp",
     "Matching Rules": "Quy tắc khớp",
@@ -2018,8 +2105,12 @@
     "More templates...": "Thêm mẫu...",
     "More...": "Thêm...",
     "Move": "Di chuyển",
+    "Move a request header": "Di chuyển header yêu cầu",
     "Move affiliate rewards to your main balance": "Chuyển phần thưởng liên kết vào số dư chính của bạn",
+    "Move Field": "Di chuyển trường",
     "Move Header": "Di chuyển tiêu đề",
+    "Move Request Header": "Di chuyển header yêu cầu",
+    "Move source field to target field": "Di chuyển trường nguồn sang trường đích",
     "ms": "ms",
     "Multi-key channel: Keys will be": "Kênh đa khóa: Các khóa sẽ là",
     "Multi-Key Management": "Quản lý đa khóa",
@@ -2054,6 +2145,8 @@
     "Name must be between {{min}} and {{max}} characters": "Tên phải có giữa {{min}} và {{max}} ký tự",
     "Name Rule": "Quy tắc đặt tên",
     "Name Suffix": "Hậu tố tên",
+    "Name the channel and choose the upstream provider.": "Đặt tên kênh và chọn nhà cung cấp upstream.",
+    "Name the channel, choose the provider, configure API access, and set credentials.": "Đặt tên kênh, chọn nhà cung cấp, cấu hình truy cập API và thiết lập thông tin xác thực.",
     "name@example.com": "name@example.com",
     "Native format": "Định dạng gốc",
     "Need a code?": "Cần mã không?",
@@ -2098,6 +2191,7 @@
     "No changes made": "Không có thay đổi nào được thực hiện",
     "No changes to save": "Không có thay đổi nào để lưu",
     "No channel selected": "Chưa chọn kênh nào",
+    "No channel type found.": "Không tìm thấy loại kênh.",
     "No channels available. Create your first channel to get started.": "Không có kênh nào khả dụng. Hãy tạo kênh đầu tiên của bạn để bắt đầu.",
     "No channels found": "Không tìm thấy kênh nào",
     "No Channels Found": "Không tìm thấy kênh nào",
@@ -2133,6 +2227,7 @@
     "No mappings configured. Click \"Add Row\" to get started.": "Chưa có ánh xạ nào được cấu hình. Nhấp vào \"Thêm hàng\" để bắt đầu.",
     "No matches found": "Không tìm thấy kết quả nào",
     "No matching results": "Không có kết quả phù hợp",
+    "No matching rules": "Không có quy tắc phù hợp",
     "No messages yet": "Chưa có tin nhắn",
     "No missing models found.": "Không tìm thấy mô hình nào bị thiếu.",
     "No model found.": "Không tìm thấy mô hình.",
@@ -2205,6 +2300,7 @@
     "Not available": "Không khả dụng",
     "Not backed up": "Chưa sao lưu",
     "Not bound": "Không bị ràng buộc",
+    "Not Equals": "Không bằng",
     "Not Set": "Chưa đặt",
     "Not set yet": "Chưa thiết lập",
     "Not Started": "Chưa bắt đầu",
@@ -2225,6 +2321,7 @@
     "OAuth failed": "OAuth thất bại",
     "OAuth Integrations": "Tích hợp OAuth",
     "OAuth start failed": "Bắt đầu OAuth thất bại",
+    "Object Prune Rules": "Quy tắc dọn dẹp đối tượng",
     "Observability": "Khả năng quan sát",
     "Obtain the API key, merchant ID, and RSA key pair from the Waffo dashboard, and configure the callback URL.": "Lấy API key, mã thương gia và cặp khóa RSA từ bảng điều khiển Waffo, đồng thời cấu hình URL callback.",
     "Obtain the merchant, store, product and signing keys from your Waffo dashboard. Webhook URL: <ServerAddress>/api/waffo-pancake/webhook": "Lấy merchant, store, product và khóa ký từ bảng điều khiển Waffo. Webhook: <ServerAddress>/api/waffo-pancake/webhook",
@@ -2282,7 +2379,9 @@
     "Opened authorization page": "Đã mở trang ủy quyền",
     "OpenRouter": "OpenRouter",
     "opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "mở trong một ứng dụng bên ngoài. Kích hoạt nó từ thanh bên hoặc các hành động khóa API để khởi chạy ứng dụng đã cấu hình.",
+    "Operation": "Thao tác",
     "Operation failed": "Thao tác thất bại",
+    "Operation Type": "Loại thao tác",
     "Operator Admin": "Quản trị viên vận hành",
     "Optimize system for self-hosted single-user usage": "Tối ưu hóa hệ thống cho việc sử dụng đơn người dùng tự lưu trữ",
     "Optimized network architecture ensures millisecond response times": "Kiến trúc mạng tối ưu đảm bảo thời gian phản hồi mili giây",
@@ -2294,6 +2393,7 @@
     "Optional notes about this channel": "Ghi chú tùy chọn về kênh này",
     "Optional notes about when to use this group": "Các ghi chú tùy chọn về thời điểm sử dụng nhóm này",
     "Optional ratio used when upstream cache hits occur.": "Tỷ lệ tùy chọn được sử dụng khi xảy ra các lượt truy cập bộ nhớ đệm ngược dòng.",
+    "Optional rule description": "Mô tả quy tắc tùy chọn",
     "Optional settings for advanced container configuration.": "Cài đặt tùy chọn cho cấu hình container nâng cao.",
     "Optional supplementary information (max 100 characters)": "Thông tin bổ sung tùy chọn (tối đa 100 ký tự)",
     "Optional tag for grouping channels": "Thẻ tùy chọn để nhóm kênh",
@@ -2318,6 +2418,8 @@
     "Override request headers (JSON format)": "Ghi đè tiêu đề yêu cầu (định dạng JSON)",
     "Override request parameters (JSON format)": "Ghi đè tham số yêu cầu (định dạng JSON)",
     "Override request parameters. Cannot override": "Ghi đè tham số yêu cầu. Không thể ghi đè",
+    "Override request parameters. Cannot override stream parameter.": "Ghi đè tham số yêu cầu. Không thể ghi đè tham số stream.",
+    "Override Rules": "Quy tắc ghi đè",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Ghi đè điểm cuối dùng để kiểm thử. Để trống để tự động phát hiện.",
     "overrides for matching model prefix.": "ghi đè theo tiền tố model tương ứng.",
     "Overview": "Tổng quan",
@@ -2327,7 +2429,10 @@
     "PaLM": "PaLM",
     "Pan": "Pan",
     "Param Override": "Ghi đè tham số",
+    "Parameter configuration error": "Lỗi cấu hình tham số",
     "Parameter Override": "Ghi đè Tham số",
+    "Parameter override must be a valid JSON object": "Ghi đè tham số phải là đối tượng JSON hợp lệ",
+    "Parameter override must be valid JSON format": "Ghi đè tham số phải ở định dạng JSON hợp lệ",
     "Parameter Override Template (JSON)": "Mẫu ghi đè tham số (JSON)",
     "Parameter override template must be a JSON object": "Mẫu ghi đè tham số phải là đối tượng JSON",
     "parameter.": "tham số",
@@ -2336,7 +2441,9 @@
     "Partial Submission": "Gửi một phần",
     "Pass Headers": "Chuyển tiếp tiêu đề",
     "Pass request body directly to upstream": "Truyền phần thân yêu cầu trực tiếp lên upstream",
+    "Pass specified request headers to upstream": "Chuyển tiếp header yêu cầu chỉ định lên upstream",
     "Pass Through Body": "Xuyên qua cơ thể",
+    "Pass Through Headers": "Truyền header qua",
     "Pass through the anthropic-beta header for beta features": "Chuyển tiếp header anthropic-beta cho tính năng beta",
     "Pass through the include field for usage obfuscation": "Chuyển tiếp trường include cho che giấu sử dụng",
     "Pass through the inference_geo field for Claude data residency region control": "Chuyển tiếp trường inference_geo để kiểm soát vùng lưu trữ dữ liệu Claude",
@@ -2344,7 +2451,9 @@
     "Pass through the safety_identifier field": "Chuyển tiếp trường safety_identifier",
     "Pass through the service_tier field": "Chuyển tiếp trường service_tier",
     "Pass through the speed field for Claude inference speed mode control": "Chuyển tiếp trường speed để kiểm soát chế độ tốc độ suy luận Claude",
+    "Pass when key is missing": "Cho qua khi thiếu khóa",
     "Pass-Through": "Chuyển tiếp",
+    "Pass-through Headers (comma-separated or JSON array)": "Header chuyển tiếp (phân cách bằng dấu phẩy hoặc mảng JSON)",
     "Passkey": "Khóa truy cập",
     "Passkey Authentication": "Xác thực khóa truy cập",
     "Passkey is not available in this browser": "Passkey không khả dụng trong trình duyệt này",
@@ -2360,6 +2469,7 @@
     "Passkey registration was cancelled": "Đăng ký Passkey đã bị hủy",
     "Passkey removed successfully": "Đã xóa Passkey thành công",
     "Passkey reset successfully": "Đặt lại Passkey thành công",
+    "Passthrough Template": "Mẫu truyền qua",
     "Password": "Mật khẩu",
     "Password / Access Token": "Mật khẩu / Access Token",
     "Password changed successfully": "Đổi mật khẩu thành công",
@@ -2374,6 +2484,7 @@
     "Passwords do not match": "Mật khẩu không khớp",
     "Paste the full callback URL (includes code & state)": "Dán toàn bộ URL callback (gồm code và state)",
     "Path": "Đường dẫn",
+    "Path not set": "Chưa đặt đường dẫn",
     "Path Regex (one per line)": "Regex đường dẫn (mỗi dòng một mục)",
     "Path:": "Đường dẫn:",
     "Pay": "Pay",
@@ -2496,11 +2607,15 @@
     "Prefix": "Tiền tố",
     "Prefix Match": "Khớp tiền tố",
     "Prefix used when displaying prices": "Tiền tố được sử dụng khi hiển thị giá",
+    "Prefix/Suffix Text": "Văn bản tiền tố/hậu tố",
     "Premium chat models": "Mô hình chat cao cấp",
     "Preparing chat keys…": "Đang chuẩn bị khóa trò chuyện…",
     "Preparing your chat link, please try again in a moment.": "Đang chuẩn bị liên kết trò chuyện của bạn, vui lòng thử lại trong giây lát.",
     "Preparing your chat link…": "Đang chuẩn bị liên kết trò chuyện của bạn…",
     "Prepend": "Thêm vào đầu",
+    "Prepend to Start": "Thêm vào đầu",
+    "Prepend value to array / string / object start": "Thêm giá trị vào đầu mảng / chuỗi / đối tượng",
+    "Preserve the original field when applying this rule": "Giữ trường gốc khi áp dụng quy tắc này",
     "Preset recharge amounts (JSON array)": "Số tiền nạp đặt trước (mảng JSON)",
     "Preset recharge amounts displayed to users": "Các mức nạp tiền đặt trước hiển thị cho người dùng",
     "Preset Template": "Mẫu cài sẵn",
@@ -2577,7 +2692,11 @@
     "Provider Name": "Tên Nhà cung cấp",
     "Provider type (OpenAI, Anthropic, etc.)": "Loại nhà cung cấp (OpenAI, Anthropic, v.v.)",
     "Provider updated successfully": "Nhà cung cấp đã được cập nhật thành công",
+    "Provider-specific endpoint, account, and compatibility settings.": "Thiết lập endpoint, tài khoản và tương thích riêng cho nhà cung cấp.",
     "Proxy Address": "Địa chỉ Proxy",
+    "Prune Object Items": "Dọn mục đối tượng",
+    "Prune object items by conditions": "Dọn dẹp các mục đối tượng theo điều kiện",
+    "Prune Rule (string or JSON object)": "Quy tắc dọn dẹp (chuỗi hoặc đối tượng JSON)",
     "Publish Date": "Ngày xuất bản",
     "Published": "Đã xuất bản",
     "Published:": "Đã xuất bản:",
@@ -2619,6 +2738,7 @@
     "Random": "Ngẫu nhiên",
     "Randomly select a key from the pool for each request": "Chọn ngẫu nhiên một khóa từ kho cho mỗi yêu cầu",
     "Rate Limit Windows": "Cửa sổ giới hạn tốc độ",
+    "Rate Limited": "Giới hạn tốc độ",
     "Rate Limiting": "Rate limit",
     "Ratio": "Tỷ lệ",
     "Ratio applied to audio completions for streaming models.": "Tỷ lệ áp dụng cho các phần hoàn tất âm thanh của các mô hình phát trực tuyến.",
@@ -2648,6 +2768,8 @@
     "Recommended to keep this high to avoid upstream throttling.": "Khuyến nghị giữ mức này cao để tránh điều tiết từ phía thượng nguồn.",
     "Record IP Address": "Ghi lại địa chỉ IP",
     "Record quota usage": "Ghi lại mức sử dụng hạn mức",
+    "Recursion Strategy": "Chiến lược đệ quy",
+    "Recursive": "Đệ quy",
     "Redeem": "Đổi",
     "Redeem codes": "Đổi mã",
     "Redeemed By": "Được chuộc bởi",
@@ -2681,7 +2803,9 @@
     "Refund Details": "Chi tiết hoàn tiền",
     "Regenerate": "Tạo lại",
     "Regenerate Backup Codes": "Tạo lại Mã dự phòng",
-    "Regex Replace": "Thay thế bằng regex",
+    "Regex": "Biểu thức chính quy",
+    "Regex Pattern": "Mẫu biểu thức chính quy",
+    "Regex Replace": "Thay thế regex",
     "Register Passkey": "Đăng ký Passkey",
     "Registration Enabled": "Đăng ký đã bật",
     "Registry (optional)": "Registry (tùy chọn)",
@@ -2709,6 +2833,9 @@
     "Remove Passkey": "Xóa Khóa truy cập",
     "Remove Passkey?": "Xóa khóa truy cập?",
     "Remove rule group": "Gỡ nhóm quy tắc",
+    "Remove string prefix": "Xóa tiền tố chuỗi",
+    "Remove string suffix": "Xóa hậu tố chuỗi",
+    "Remove the target field": "Xóa trường đích",
     "Remove tier": "Gỡ bậc",
     "Removed": "Đã xóa",
     "Removed {{removed}} duplicate key(s). Before: {{before}}, After: {{after}}": "Đã xóa {{removed}} khóa trùng lặp. Trước: {{before}}, Sau: {{after}}",
@@ -2724,6 +2851,7 @@
     "Replace all existing keys": "Thay thế tất cả các khóa hiện có",
     "Replace channel models": "Thay thế mô hình kênh",
     "Replace mode: Will completely replace all existing keys": "Chế độ Thay thế: Sẽ thay thế hoàn toàn tất cả các khóa hiện có",
+    "Replace With": "Thay bằng",
     "replaced": "thay thế",
     "Replacement Model": "Replacement model",
     "Replica count": "Số bản sao",
@@ -2731,6 +2859,7 @@
     "request": "yêu cầu",
     "Request": "Yêu cầu",
     "Request Body Disk Cache": "Bộ nhớ đệm đĩa nội dung yêu cầu",
+    "Request Body Field": "Trường thân yêu cầu",
     "Request Body Memory Cache": "Bộ nhớ đệm RAM nội dung yêu cầu",
     "Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.": "Chuyển tiếp body yêu cầu đã được bật. Body yêu cầu sẽ được gửi trực tiếp mà không chuyển đổi.",
     "Request conversion": "Chuyển đổi yêu cầu",
@@ -2738,11 +2867,14 @@
     "Request Count": "Number of requests",
     "Request failed": "Yêu cầu thất bại",
     "Request flow": "Luồng yêu cầu",
+    "Request Header Field": "Trường header yêu cầu",
+    "Request Header Override": "Ghi đè header yêu cầu",
     "Request Header Overrides": "Ghi đè Tiêu đề Yêu cầu",
     "Request ID": "ID yêu cầu",
     "Request Limits": "Hạn mức yêu cầu",
     "Request Model": "Mô hình yêu cầu",
     "Request Model:": "Mô hình yêu cầu:",
+    "Request overrides, routing behavior, and upstream model automation": "Ghi đè yêu cầu, hành vi định tuyến và tự động hóa mô hình upstream",
     "Request rule pricing": "Quy tắc tính giá theo request",
     "Request timed out, please refresh and restart GitHub login": "Yêu cầu đã hết thời gian chờ, vui lòng làm mới và đăng nhập lại GitHub",
     "Request-based": "Theo yêu cầu",
@@ -2755,6 +2887,7 @@
     "Require login to view models": "Yêu cầu đăng nhập để xem các mô hình",
     "Required": "Bắt buộc",
     "Required events:": "Sự kiện bắt buộc:",
+    "Required provider, authentication, model, and group settings": "Thiết lập bắt buộc về nhà cung cấp, xác thực, mô hình và nhóm",
     "Required to expose Midjourney-style image generation to end users.": "Cần thiết để cung cấp tính năng tạo hình ảnh kiểu Midjourney cho người dùng cuối.",
     "Rerank": "Re-rank",
     "Reroll": "Quay lại",
@@ -2789,7 +2922,10 @@
     "Retain last N files": "Giữ lại N tệp gần nhất",
     "Retry": "Thử lại",
     "Retry Chain": "Chuỗi thử lại",
+    "Retry Suggestion": "Gợi ý thử lại",
     "Retry Times": "Số lần thử lại",
+    "Return a custom error immediately": "Trả về lỗi tùy chỉnh ngay lập tức",
+    "Return Custom Error": "Trả lỗi tùy chỉnh",
     "Return Error": "Trả về lỗi",
     "Return to dashboard": "Quay lại bảng điều khiển",
     "Reveal API key": "Hiển thị khóa API",
@@ -2806,13 +2942,25 @@
     "Route": "Tuyến đường",
     "Route Description": "Mô tả lộ trình",
     "Route is required": "Đường dẫn là bắt buộc",
+    "Routing & Overrides": "Định tuyến & ghi đè",
+    "Routing Strategy": "Chiến lược định tuyến",
     "Rows per page": "Số hàng trên trang",
     "RPM": "RPM",
     "RSA Private Key (Production)": "RSA Private Key (Sản xuất)",
     "RSA Private Key (Sandbox)": "Khóa riêng RSA (Sandbox)",
     "Rule": "Quy tắc",
+    "Rule {{line}} is missing source field": "Quy tắc {{line}} thiếu trường nguồn",
+    "Rule {{line}} is missing target field": "Quy tắc {{line}} thiếu trường đích",
+    "Rule {{line}} is missing target path": "Quy tắc {{line}} thiếu đường dẫn đích",
+    "Rule {{line}} is missing value": "Quy tắc {{line}} thiếu giá trị",
+    "Rule {{line}} pass_headers format is invalid": "Định dạng pass_headers của quy tắc {{line}} không hợp lệ",
+    "Rule {{line}} pass_headers is missing header names": "pass_headers của quy tắc {{line}} thiếu tên header",
+    "Rule {{line}} prune_objects is missing conditions": "prune_objects của quy tắc {{line}} thiếu điều kiện",
+    "Rule {{line}} return_error requires a message field": "return_error của quy tắc {{line}} cần trường message",
+    "Rule Description (optional)": "Mô tả quy tắc (tùy chọn)",
     "Rule group": "Nhóm quy tắc",
     "rules": "quy tắc",
+    "Rules": "Quy tắc",
     "Rules JSON": "JSON quy tắc",
     "Rules JSON must be an array": "JSON quy tắc phải là một mảng",
     "Run GC": "Chạy GC",
@@ -2861,12 +3009,14 @@
     "Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.": "Quét mã QR để theo dõi tài khoản chính thức, trả lời « 验证码 » để nhận mã xác minh.",
     "Scan the QR code with WeChat to bind your account": "Quét mã QR bằng WeChat để liên kết tài khoản của bạn",
     "Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)": "Quét mã QR này bằng ứng dụng xác thực của bạn (Google Authenticator, Microsoft Authenticator, v.v.)",
+    "Scenario Templates": "Mẫu kịch bản",
     "Scheduled channel tests": "Kiểm tra kênh theo lịch trình",
     "Scope": "Phạm vi",
     "Scopes": "Phạm vi",
     "Search": "Tìm kiếm",
     "Search by name or URL...": "Tìm kiếm theo tên hoặc URL...",
     "Search by order number...": "Tìm kiếm theo số đơn hàng...",
+    "Search channel type...": "Tìm loại kênh...",
     "Search chat presets...": "Tìm kiếm cài đặt sẵn trò chuyện...",
     "Search colors...": "Tìm kiếm màu sắc...",
     "Search conflicting models or fields": "Tìm kiếm mô hình hoặc trường xung đột",
@@ -2882,6 +3032,7 @@
     "Search payment methods...": "Tìm kiếm phương thức thanh toán...",
     "Search payment types...": "Tìm kiếm loại thanh toán...",
     "Search products...": "Tìm kiếm sản phẩm...",
+    "Search rules...": "Tìm kiếm quy tắc…",
     "Search tags...": "Tìm thẻ...",
     "Search vendors...": "Tìm nhà cung cấp...",
     "Search...": "Tìm kiếm...",
@@ -2899,6 +3050,7 @@
     "Select a group type": "Chọn loại nhóm",
     "Select a preset...": "Chọn cấu hình sẵn...",
     "Select a role": "Chọn vai trò",
+    "Select a rule to edit.": "Chọn một quy tắc để chỉnh sửa.",
     "Select a timestamp before clearing logs.": "Chọn một dấu thời gian trước khi xóa nhật ký.",
     "Select a usage mode to continue": "Chọn chế độ sử dụng để tiếp tục",
     "Select a verification method first": "Vui lòng chọn phương thức xác thực trước",
@@ -2973,13 +3125,16 @@
     "Set a discount rate for a specific recharge amount threshold.": "Đặt tỷ lệ chiết khấu cho một ngưỡng số tiền nạp cụ thể.",
     "Set a secure password (min. 8 characters)": "Đặt mật khẩu an toàn (tối thiểu 8 ký tự)",
     "Set a tag for": "Gắn thẻ cho",
-    "Set filters to customize your dashboard statistics and charts.": "Đặt bộ lọc để tùy chỉnh số liệu thống kê và biểu đồ trên bảng điều khiển của bạn.",
-    "Set filters to narrow down your log search results.": "Đặt bộ lọc để thu hẹp kết quả tìm kiếm nhật ký của bạn.",
     "Set API key access restrictions": "Thiết lập hạn chế truy cập cho khóa API",
     "Set API key basic information": "Thiết lập thông tin cơ bản cho khóa API",
+    "Set Field": "Đặt trường",
+    "Set filters to customize your dashboard statistics and charts.": "Đặt bộ lọc để tùy chỉnh số liệu thống kê và biểu đồ trên bảng điều khiển của bạn.",
+    "Set filters to narrow down your log search results.": "Đặt bộ lọc để thu hẹp kết quả tìm kiếm nhật ký của bạn.",
     "Set Header": "Đặt tiêu đề",
     "Set Project to io.cloud when creating/selecting key": "Đặt Dự án thành io.cloud khi tạo/chọn khóa",
     "Set quota amount and limits": "Thiết lập hạn mức và giới hạn",
+    "Set Request Header": "Đặt header yêu cầu",
+    "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Đặt header yêu cầu runtime: ghi đè toàn bộ giá trị hoặc thao tác token phân cách bằng dấu phẩy",
     "Set Tag": "Gán Thẻ",
     "Set tag for selected channels": "Đặt thẻ cho các kênh đã chọn",
     "Set the user's role (cannot be Root)": "Đặt vai trò của người dùng (không được là Root)",
@@ -3019,6 +3174,9 @@
     "Signed out": "Đã đăng xuất",
     "Signing you in with {{provider}}": "Đang đăng nhập bằng {{provider}}",
     "SiliconFlow": "SiliconFlow",
+    "Simple": "Đơn giản",
+    "Simple mode only returns message; status code and error type use system defaults.": "Chế độ đơn giản chỉ trả về message; mã trạng thái và loại lỗi sử dụng giá trị mặc định.",
+    "Simple mode: prune objects by type, e.g. redacted_thinking.": "Chế độ đơn giản: dọn dẹp đối tượng theo type, ví dụ redacted_thinking.",
     "Single Key": "Khóa đơn",
     "Site Key": "Khóa trang web",
     "Size:": "Kích thước:",
@@ -3043,6 +3201,9 @@
     "Sort by ID": "Sắp xếp theo ID",
     "Sort Order": "Thứ tự sắp xếp",
     "Source": "Nguồn",
+    "Source Endpoint": "Điểm nguồn",
+    "Source Field": "Trường nguồn",
+    "Source Header": "Header nguồn",
     "sources": "nguồn",
     "Space-separated OAuth scopes": "Phạm vi OAuth phân cách bằng dấu cách",
     "Spark model version, e.g., v2.1 (version number in API URL)": "Phiên bản mô hình Spark, ví dụ: v2.1 (số phiên bản trong URL API)",
@@ -3061,6 +3222,7 @@
     "Statistics reset": "Đã đặt lại thống kê",
     "Status": "Trạng thái",
     "Status & Sync": "Trạng thái & Đồng bộ",
+    "Status Code": "Mã trạng thái",
     "Status Code Mapping": "Ánh xạ mã trạng thái",
     "Status Page Slug": "Đường dẫn phụ trang trạng thái",
     "Status:": "Trạng thái:",
@@ -3068,6 +3230,7 @@
     "Stay tuned though!": "Nhưng",
     "Step": "Bước",
     "Stop": "Dừng lại",
+    "Stop Retry": "Dừng thử lại",
     "Store ID": "Mã cửa hàng",
     "Store ID is required": "Bắt buộc nhập Store ID",
     "Stored value is not echoed back for security": "Vì bảo mật, giá trị đã lưu không được hiển thị lại",
@@ -3075,6 +3238,7 @@
     "Stream": "Luồng",
     "Stream Mode": "Chế độ streaming",
     "Stream Status": "Trạng thái luồng",
+    "String Replace": "Thay thế chuỗi",
     "Stripe": "Stripe",
     "Stripe API key (leave blank unless updating)": "Khóa API Stripe (để trống trừ khi cập nhật)",
     "Stripe Dashboard": "Bảng điều khiển Stripe",
@@ -3111,6 +3275,7 @@
     "Super Admin": "Siêu Quản trị viên",
     "Support for high concurrency with automatic load balancing": "Hỗ trợ đồng thời cao với cân bằng tải tự động",
     "Supported Imagine Models": "Mô hình Imagine được hỗ trợ",
+    "Supported variables": "Biến được hỗ trợ",
     "Supports `-thinking`, `-thinking-": "Hỗ trợ `-thinking`, `-thinking-",
     "Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.": "Hỗ trợ đánh dấu HTML hoặc nhúng iframe. Nhập mã HTML trực tiếp, hoặc cung cấp một URL đầy đủ để tự động nhúng nó dưới dạng một iframe.",
     "Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.": "Hỗ trợ PNG, JPG, SVG hoặc WebP. Kích thước khuyến nghị: 128×128 hoặc nhỏ hơn.",
@@ -3120,6 +3285,7 @@
     "Switch to JSON": "Chuyển sang JSON",
     "Switch to Visual": "Chuyển sang Trực quan",
     "Sync Endpoint": "Điểm cuối đồng bộ",
+    "Sync Endpoints": "Điểm đồng bộ",
     "Sync Fields": "Đồng bộ trường",
     "Sync from the public upstream metadata repository.": "Đồng bộ từ kho lưu trữ siêu dữ liệu upstream công khai.",
     "Sync this model with official upstream": "Synchronize this model with the official source.",
@@ -3161,7 +3327,12 @@
     "Tags": "Thẻ",
     "Take photo": "Chụp ảnh",
     "Take screenshot": "Chụp màn hình",
+    "Target Endpoint": "Điểm đích",
+    "Target Field": "Trường đích",
+    "Target Field Path": "Đường dẫn trường đích",
     "Target group": "Target audience",
+    "Target Header": "Header đích",
+    "Target Path (optional)": "Đường dẫn đích (tùy chọn)",
     "Task": "Nhiệm vụ",
     "Task ID": "Mã nhiệm vụ",
     "Task ID:": "ID nhiệm vụ:",
@@ -3172,6 +3343,7 @@
     "Telegram": "Telegram",
     "Telegram login requires widget integration; coming soon": "Đăng nhập Telegram yêu cầu tích hợp widget; sắp ra mắt",
     "Telegram Login Widget": "Tiện ích đăng nhập Telegram",
+    "Template": "Mẫu",
     "Template variables:": "Biến mẫu:",
     "Templates": "Mẫu",
     "Templates appended": "Đã thêm mẫu",
@@ -3276,9 +3448,11 @@
     "to access this resource.": "để truy cập tài nguyên này.",
     "to confirm": "Chờ xác nhận",
     "To Lower": "Chữ thường",
+    "To Lowercase": "Chuyển chữ thường",
     "to override billing when a user in one group uses a token of another group.": "để ghi đè việc thanh toán khi một người dùng trong một nhóm sử dụng token của một nhóm khác.",
     "to the Models list so users can use them before the mapping sends traffic upstream.": "vào danh sách Mô hình để người dùng có thể sử dụng chúng trước khi ánh xạ gửi lưu lượng truy cập lên phía trên.",
     "To Upper": "Chữ hoa",
+    "To Uppercase": "Chuyển chữ hoa",
     "to view this resource.": "để xem tài nguyên này.",
     "Today": "Hôm nay",
     "Toggle columns": "Chuyển đổi cột",
@@ -3309,8 +3483,8 @@
     "Tool prices": "Giá công cụ",
     "Top {{count}}": "Top {{count}}",
     "Top Models": "Người mẫu hàng đầu",
-    "Top Users": "Người dùng hàng đầu",
     "Top up balance and view billing history.": "Nạp tiền vào tài khoản và xem lịch sử thanh toán.",
+    "Top Users": "Người dùng hàng đầu",
     "Top-up": "Nạp tiền",
     "Top-up amount options": "Tùy chọn số tiền nạp",
     "Top-up Audit Info": "Thông tin audit nạp tiền",
@@ -3349,14 +3523,16 @@
     "Transfer to Balance": "Chuyển vào số dư",
     "Translate `-thinking` suffixes into Anthropic native thinking models while keeping pricing predictable.": "Dịch các hậu tố `-thinking` sang các mô hình tư duy gốc của Anthropic đồng thời giữ giá cả có thể dự đoán được.",
     "Transparent Billing": "Thanh toán minh bạch",
-    "Trim Prefix": "Xóa tiền tố",
-    "Trim Space": "Xóa khoảng trắng",
-    "Trim Suffix": "Xóa hậu tố",
+    "Trim leading/trailing whitespace": "Xóa khoảng trắng đầu/cuối",
+    "Trim Prefix": "Cắt tiền tố",
+    "Trim Space": "Cắt khoảng trắng",
+    "Trim Suffix": "Cắt hậu tố",
     "Trusted": "Đáng tin cậy",
     "Try adjusting your search to locate a missing model.": "Hãy thử điều chỉnh tìm kiếm của bạn để định vị một mô hình bị thiếu.",
     "TTL": "TTL",
     "TTL (seconds, 0 = default)": "TTL (giây, 0 = mặc định)",
     "TTL (seconds)": "TTL (giây)",
+    "Tune selection priority, testing, status handling, and request overrides.": "Tinh chỉnh ưu tiên chọn, kiểm thử, xử lý trạng thái và ghi đè yêu cầu.",
     "Turnstile is enabled but site key is empty.": "Turnstile đã được bật nhưng khóa trang web trống.",
     "Two-factor Authentication": "Xác thực hai yếu tố",
     "Two-Factor Authentication": "Xác thực hai yếu tố",
@@ -3365,6 +3541,7 @@
     "Two-factor authentication reset": "Xác thực hai yếu tố đã được đặt lại",
     "Two-Step Verification": "Xác minh hai bước",
     "Type": "Loại",
+    "Type (common)": "Loại (phổ biến)",
     "Type *": "Nhập *",
     "Type a command or search...": "Nhập lệnh hoặc tìm kiếm...",
     "Type-Specific Settings": "Cài đặt theo loại",
@@ -3375,6 +3552,7 @@
     "Unable to load groups": "Không thể tải nhóm",
     "Unable to open chat": "Không thể mở trò chuyện",
     "Unable to prepare chat link. Please ensure you have an enabled API key.": "Không thể chuẩn bị liên kết chat. Vui lòng đảm bảo bạn có khóa API được kích hoạt.",
+    "Unauthorized": "Chưa xác thực",
     "Unauthorized Access": "Truy cập trái phép",
     "Unbind": "Hủy liên kết",
     "Unbind failed": "Hủy liên kết thất bại",
@@ -3431,6 +3609,7 @@
     "Upload photo": "Tải ảnh lên",
     "Upscale": "Phóng to",
     "Upstream": "Thượng nguồn",
+    "Upstream Model Detection Settings": "Cài đặt phát hiện mô hình nguồn",
     "Upstream Model Update Check": "Kiểm tra cập nhật mô hình nguồn",
     "Upstream Model Updates": "Cập nhật mô hình upstream",
     "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Đã áp dụng cập nhật mô hình upstream: {{added}} đã thêm, {{removed}} đã xóa, {{ignored}} bỏ qua lần này, {{totalIgnored}} tổng mô hình đã bỏ qua",
@@ -3511,6 +3690,7 @@
     "Validity": "Hiệu lực",
     "Validity Period": "Thời hạn hiệu lực",
     "Value": "Giá trị",
+    "Value (supports JSON or plain text)": "Giá trị (hỗ trợ JSON hoặc văn bản thuần)",
     "Value must be at least 0": "Giá trị phải ít nhất là 0",
     "Value Regex": "Regex giá trị",
     "variable": "biến",
@@ -3573,10 +3753,12 @@
     "Visit Settings → General and adjust quota options...": "Truy cập Cài đặt → Chung và điều chỉnh tùy chọn hạn mức...",
     "Visitors must authenticate before accessing the pricing directory.": "Khách truy cập phải xác thực trước khi truy cập thư mục giá.",
     "Visual": "Trực quan",
+    "Visual edit": "Chỉnh sửa trực quan",
     "Visual editor": "Trình sửa trực quan",
     "Visual Editor": "Trình soạn thảo trực quan",
     "Visual indicator color for the API card": "Màu sắc chỉ báo trực quan cho thẻ API",
     "Visual Mode": "Chế độ Trực quan",
+    "Visual Parameter Override": "Ghi đè tham số trực quan",
     "VolcEngine": "VolcEngine",
     "Waffo Pancake Payment Gateway": "Cổng thanh toán Waffo Pancake",
     "Waffo Payment": "Thanh toán Waffo",
@@ -3633,6 +3815,7 @@
     "When enabled, the store field will be blocked": "Khi được bật, trường store sẽ bị chặn",
     "When enabled, violation requests will incur additional charges.": "Khi bật, các yêu cầu vi phạm sẽ phải chịu phí bổ sung.",
     "When enabled, zero-cost models also pre-consume quota before final settlement.": "Khi được bật, các mô hình không tốn phí cũng trừ trước hạn mức trước khi quyết toán cuối cùng.",
+    "When no conditions are set, the operation always executes.": "Khi không có điều kiện, thao tác luôn được thực thi.",
     "When performance monitoring is enabled and system resource usage exceeds the set threshold, new Relay requests will be rejected.": "Khi giám sát hiệu suất được bật và mức sử dụng tài nguyên vượt quá ngưỡng, các yêu cầu Relay mới sẽ bị từ chối.",
     "When running in containers or ephemeral environments, ensure the SQLite file is mapped to persistent storage to avoid data loss on restart.": "Khi chạy trong container hoặc môi trường tạm thời, hãy đảm bảo tệp SQLite được ánh xạ vào bộ nhớ lưu trữ bền vững để tránh mất dữ liệu khi khởi động lại.",
     "Whitelist": "Danh sách trắng",
@@ -3641,10 +3824,12 @@
     "whsec_xxx": "whsec_xxx",
     "Window:": "Cửa sổ:",
     "with conflicts": "với các xung đột",
+    "Without additional conditions, only the type above is used for pruning.": "Không có điều kiện bổ sung, chỉ type ở trên được sử dụng để dọn dẹp.",
     "Worker Access Key": "Khóa truy cập nhân viên",
     "Worker Proxy": "Proxy Nhân viên",
     "Worker URL": "URL của Worker",
     "Workspaces": "Không gian làm việc",
+    "Write value to the target field": "Ghi giá trị vào trường đích",
     "x": "x",
     "xAI": "xAI",
     "Xinference": "Xinference",
@@ -3678,6 +3863,7 @@
     "Your Turnstile site key": "Khóa site Turnstile của bạn",
     "Zhipu": "Zhipu",
     "Zhipu V4": "Zhipu V4",
-    "Zoom": "Zoom"
+    "Zoom": "Zoom",
+    "Legacy Format Template": "Mẫu định dạng cũ"
   }
 }

File diff suppressed because it is too large
+ 196 - 13
web/default/src/i18n/locales/zh.json


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