-
+
{t('Original Model')}
{t('Replacement Model')}
@@ -272,9 +275,10 @@ export function ModelMappingEditor(props: ModelMappingEditorProps) {
{rows.map((row) => (
handleRowChange(row.id, 'from', e.target.value)
@@ -282,6 +286,7 @@ export function ModelMappingEditor(props: ModelMappingEditorProps) {
placeholder='gpt-3.5-turbo'
disabled={props.disabled}
list={sourceListId}
+ aria-label={t('Original Model')}
/>
= {
- native: '原生支持',
- compatible: '兼容支持',
- configurable: '可配置',
- limited: '有限支持',
- unsupported: '不支持',
+ native: 'Native support',
+ compatible: 'Compatible support',
+ configurable: 'Configurable',
+ limited: 'Limited support',
+ unsupported: 'Not supported',
}
export const CHANNEL_CAPABILITY_STATUS_VARIANTS: Record<
diff --git a/web/default/src/features/channels/lib/channel-config-validation.ts b/web/default/src/features/channels/lib/channel-config-validation.ts
index 8a552e7b..e83ab8d0 100644
--- a/web/default/src/features/channels/lib/channel-config-validation.ts
+++ b/web/default/src/features/channels/lib/channel-config-validation.ts
@@ -17,14 +17,14 @@ along with this program. If not, see .
For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
*/
import { MODEL_FETCHABLE_TYPES } from '../constants'
-import type { ChannelFormValues } from './channel-form'
+import { isVideoTaskChannelType } from './channel-capabilities'
import {
BASE_URL_REQUIRED_TYPES,
OTHER_REQUIRED_TYPES,
hasVertexDefaultRegion,
hasVideoTaskQueryPlaceholder,
} from './channel-config-rules'
-import { isVideoTaskChannelType } from './channel-capabilities'
+import type { ChannelFormValues } from './channel-form'
export type ChannelConfigValidationSeverity = 'error' | 'warning' | 'info'
@@ -87,7 +87,7 @@ function validateJsonFields(
id: `${field}_invalid_json`,
severity: 'error',
field,
- message: '此 JSON 配置必须是 JSON 对象。',
+ message: 'This JSON configuration must be a JSON object.',
})
}
}
@@ -107,12 +107,13 @@ function validateVertexConfig(
try {
regionConfig = parseJson(otherValue)
} catch {
- addIssue(issues, {
- id: 'vertex_region_invalid_json',
- severity: 'error',
- field: 'other',
- message: 'Vertex AI 区域配置必须是包含 default 字段的 JSON 对象。',
- })
+ addIssue(issues, {
+ id: 'vertex_region_invalid_json',
+ severity: 'error',
+ field: 'other',
+ message:
+ 'Vertex AI region configuration must be a JSON object containing a default field.',
+ })
return
}
@@ -121,7 +122,7 @@ function validateVertexConfig(
id: 'vertex_region_missing_default',
severity: 'error',
field: 'other',
- message: 'Vertex AI 区域配置必须包含 default 字段。',
+ message: 'Vertex AI region configuration must contain a default field.',
})
}
@@ -131,7 +132,7 @@ function validateVertexConfig(
id: 'vertex_api_key_batch_mode',
severity: 'error',
field: 'multi_key_mode',
- message: 'Vertex AI 的 API Key 模式不支持批量创建。',
+ message: 'Vertex AI API Key mode does not support batch creation.',
})
}
return
@@ -148,7 +149,7 @@ function validateVertexConfig(
id: 'vertex_key_invalid_shape',
severity: 'error',
field: 'key',
- message: 'Vertex AI 服务账号密钥必须是有效的 JSON。',
+ message: 'Vertex AI service account credentials must be valid JSON.',
})
}
} catch {
@@ -156,7 +157,7 @@ function validateVertexConfig(
id: 'vertex_key_invalid_json',
severity: 'error',
field: 'key',
- message: 'Vertex AI 服务账号密钥必须是有效的 JSON。',
+ message: 'Vertex AI service account credentials must be valid JSON.',
})
}
}
@@ -173,7 +174,7 @@ function validateCodexConfig(
id: 'codex_batch_mode',
severity: 'error',
field: 'multi_key_mode',
- message: 'Codex 渠道不支持批量创建。',
+ message: 'Codex channels do not support batch creation.',
})
}
@@ -192,7 +193,7 @@ function validateCodexConfig(
id: 'codex_key_missing_fields',
severity: 'error',
field: 'key',
- message: 'Codex 凭证必须包含 access_token 和 account_id 字段。',
+ message: 'Codex credentials must contain access_token and account_id.',
})
}
} catch {
@@ -200,7 +201,7 @@ function validateCodexConfig(
id: 'codex_key_invalid_json',
severity: 'error',
field: 'key',
- message: 'Codex 凭证必须是有效的 JSON 对象。',
+ message: 'Codex credentials must be a valid JSON object.',
})
}
}
@@ -218,7 +219,8 @@ function validateVideoTaskConfig(
id: 'video_task_unsupported_type',
severity: 'warning',
field: 'video_task_protocol_enabled',
- message: '视频任务协议设置仅适用于支持视频任务的渠道类型。',
+ message:
+ 'Video task protocol settings are only available for channel types that support video tasks.',
})
return
}
@@ -235,7 +237,7 @@ function validateVideoTaskConfig(
id: 'video_task_missing_submit_path',
severity: 'error',
field: 'video_task_submit_path',
- message: '视频任务提交路径不能为空。',
+ message: 'Video task submission path is required.',
})
}
@@ -245,7 +247,7 @@ function validateVideoTaskConfig(
id: 'video_task_missing_query_path',
severity: 'error',
field: 'video_task_query_path',
- message: '视频任务查询路径不能为空。',
+ message: 'Video task query path is required.',
})
} else if (!hasVideoTaskQueryPlaceholder(queryPath)) {
addIssue(issues, {
@@ -253,7 +255,7 @@ function validateVideoTaskConfig(
severity: 'error',
field: 'video_task_query_path',
message:
- '视频任务查询路径必须包含 {task_id}、{operation_name} 或 {upstream_task_id}。',
+ 'Video task query path must contain {task_id}, {operation_name}, or {upstream_task_id}.',
})
}
@@ -263,7 +265,8 @@ function validateVideoTaskConfig(
id: 'video_task_missing_task_id_path',
severity: 'error',
field: 'video_task_task_id_path',
- message: '启用完整任务协议时,必须填写任务 ID 响应路径。',
+ message:
+ 'Task ID response path is required when the full task protocol is enabled.',
})
}
if (!trim(values.video_task_status_path)) {
@@ -271,7 +274,8 @@ function validateVideoTaskConfig(
id: 'video_task_missing_status_path',
severity: 'error',
field: 'video_task_status_path',
- message: '启用完整任务协议时,必须填写状态响应路径。',
+ message:
+ 'Status response path is required when the full task protocol is enabled.',
})
}
if (!trim(values.video_task_result_url_paths)) {
@@ -279,7 +283,8 @@ function validateVideoTaskConfig(
id: 'video_task_missing_result_paths',
severity: 'warning',
field: 'video_task_result_url_paths',
- message: '未配置结果 URL 路径,视频任务完成后可能无法返回内容地址。',
+ message:
+ 'Without a result URL path, completed video tasks may not return a content URL.',
})
}
}
@@ -297,7 +302,7 @@ export function getChannelConfigValidationIssues(
id: 'key_required_create',
severity: 'error',
field: 'key',
- message: '新建渠道时 API Key 不能为空。',
+ message: 'API Key is required when creating a channel.',
})
}
@@ -306,7 +311,7 @@ export function getChannelConfigValidationIssues(
id: 'models_required',
severity: 'error',
field: 'models',
- message: '该渠道至少需要发布一个模型。',
+ message: 'At least one model must be published for this channel.',
})
}
@@ -315,7 +320,7 @@ export function getChannelConfigValidationIssues(
id: 'base_url_required',
severity: 'error',
field: 'base_url',
- message: '当前渠道类型需要填写 Base URL。',
+ message: 'Base URL is required for this channel type.',
})
}
@@ -325,7 +330,7 @@ export function getChannelConfigValidationIssues(
severity: 'warning',
field: 'base_url',
message:
- 'Base URL 通常不应以 /v1 结尾,MAX API 会自动拼接上游路径。',
+ 'Base URL should not usually end with /v1 because MAX API appends the upstream path automatically.',
})
}
@@ -334,7 +339,8 @@ export function getChannelConfigValidationIssues(
id: 'other_required',
severity: 'error',
field: 'other',
- message: '当前渠道类型需要填写额外的上游配置。',
+ message:
+ 'Additional upstream configuration is required for this channel type.',
})
}
@@ -347,7 +353,8 @@ export function getChannelConfigValidationIssues(
id: 'model_discovery_not_supported',
severity: 'warning',
field: 'upstream_model_update_check_enabled',
- message: '当前渠道类型不支持上游模型发现,该功能不会运行。',
+ message:
+ 'Upstream model discovery is not supported for this channel type and will not run.',
})
}
diff --git a/web/default/src/features/channels/lib/channel-form.ts b/web/default/src/features/channels/lib/channel-form.ts
index 14e91ee1..71a9030c 100644
--- a/web/default/src/features/channels/lib/channel-form.ts
+++ b/web/default/src/features/channels/lib/channel-form.ts
@@ -19,6 +19,7 @@ For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API
import { z } from 'zod'
import {
CHANNEL_STATUS,
+ CHANNEL_TYPE_DOUBAO_VIDEO,
CHANNEL_TYPE_VOLCENGINE,
CHANNEL_TYPE_XUNFEI,
ERROR_MESSAGES,
@@ -246,6 +247,7 @@ export const channelFormSchema = z
// Video task protocol settings (stored in settings JSON)
video_task_path_override_enabled: z.boolean().optional(),
video_task_protocol_enabled: z.boolean().optional(),
+ video_task_delta_settlement_enabled: z.boolean().optional(),
video_task_submit_path: z.string().optional(),
video_task_query_path: z.string().optional(),
video_task_task_id_path: z.string().optional(),
@@ -376,7 +378,6 @@ export const channelFormSchema = z
)
}
}
-
})
export type ChannelFormValues = z.infer
@@ -437,6 +438,7 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = {
upstream_model_update_ignored_models: '',
video_task_path_override_enabled: false,
video_task_protocol_enabled: false,
+ video_task_delta_settlement_enabled: true,
video_task_submit_path: DEFAULT_VIDEO_TASK_SUBMIT_PATH,
video_task_query_path: DEFAULT_VIDEO_TASK_QUERY_PATH,
video_task_task_id_path: DEFAULT_VIDEO_TASK_TASK_ID_PATH,
@@ -587,6 +589,7 @@ export function transformChannelToFormDefaults(
let upstreamModelUpdateIgnoredModels = ''
let videoTaskPathOverrideEnabled = false
let videoTaskProtocolEnabled = false
+ let videoTaskDeltaSettlementEnabled = true
let videoTaskSubmitPath = DEFAULT_VIDEO_TASK_SUBMIT_PATH
let videoTaskQueryPath = DEFAULT_VIDEO_TASK_QUERY_PATH
let videoTaskIDPath = DEFAULT_VIDEO_TASK_TASK_ID_PATH
@@ -624,6 +627,8 @@ export function transformChannelToFormDefaults(
)
? parsed.upstream_model_update_ignored_models.join(',')
: ''
+ videoTaskDeltaSettlementEnabled =
+ parsed.disable_task_delta_settlement !== true
if (parsed.advanced_custom) {
advancedCustom = stringifyAdvancedCustomConfig(parsed.advanced_custom)
}
@@ -743,6 +748,7 @@ export function transformChannelToFormDefaults(
upstream_model_update_ignored_models: upstreamModelUpdateIgnoredModels,
video_task_path_override_enabled: videoTaskPathOverrideEnabled,
video_task_protocol_enabled: videoTaskProtocolEnabled,
+ video_task_delta_settlement_enabled: videoTaskDeltaSettlementEnabled,
video_task_submit_path: videoTaskSubmitPath,
video_task_query_path: videoTaskQueryPath,
video_task_task_id_path: videoTaskIDPath,
@@ -880,6 +886,16 @@ function buildSettingsJSON(formData: ChannelFormValues): string {
}
const isVideoTaskChannel = VIDEO_TASK_CHANNEL_TYPES.has(formData.type)
+ const isDoubaoVideoChannel = formData.type === CHANNEL_TYPE_DOUBAO_VIDEO
+ if (
+ isDoubaoVideoChannel &&
+ formData.video_task_delta_settlement_enabled === false
+ ) {
+ settingsObj.disable_task_delta_settlement = true
+ } else if ('disable_task_delta_settlement' in settingsObj) {
+ delete settingsObj.disable_task_delta_settlement
+ }
+
const enableVideoTaskProtocol =
isVideoTaskChannel && formData.video_task_protocol_enabled === true
const enableVideoTaskPathOverride =
diff --git a/web/default/src/features/channels/types.ts b/web/default/src/features/channels/types.ts
index 1c724a81..62ece3ee 100644
--- a/web/default/src/features/channels/types.ts
+++ b/web/default/src/features/channels/types.ts
@@ -108,6 +108,7 @@ export interface ChannelOtherSettings {
upstream_model_update_last_removed_models?: string[]
task_protocol?: string
task_protocol_config?: TaskProtocolConfig
+ disable_task_delta_settlement?: boolean
advanced_custom?: AdvancedCustomConfig
}
diff --git a/web/default/src/features/models/components/models-columns.tsx b/web/default/src/features/models/components/models-columns.tsx
index 399c519d..712fe970 100644
--- a/web/default/src/features/models/components/models-columns.tsx
+++ b/web/default/src/features/models/components/models-columns.tsx
@@ -137,7 +137,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] {
meta: {
label: t('Model Name'),
mobileTitle: true,
- className: 'max-w-[28rem] whitespace-normal',
+ cellClassName: 'max-w-[28rem] whitespace-normal',
+ mobileClassName: '[overflow-wrap:anywhere]',
},
header: ({ column }) => (
@@ -150,6 +151,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] {
variant='neutral'
copyText={name}
size='sm'
+ wrap
className='max-w-[min(100%,24rem)] font-mono'
/>
)
diff --git a/web/default/src/features/playground/components/playground-input.tsx b/web/default/src/features/playground/components/playground-input.tsx
index a7f69d2a..21914f6b 100644
--- a/web/default/src/features/playground/components/playground-input.tsx
+++ b/web/default/src/features/playground/components/playground-input.tsx
@@ -57,7 +57,13 @@ import {
} from '@/components/ai-elements/prompt-input'
import { Suggestion, Suggestions } from '@/components/ai-elements/suggestion'
import { ModelGroupSelector } from '@/components/model-group-selector'
-import type { ModelOption, GroupOption } from '../types'
+import type {
+ GroupOption,
+ ModelOption,
+ ParameterEnabled,
+ PlaygroundConfig,
+} from '../types'
+import { PlaygroundParameterPanel } from './playground-parameter-panel'
interface PlaygroundInputProps {
onSubmit: (text: string) => void
@@ -74,6 +80,16 @@ interface PlaygroundInputProps {
onGroupChange: (value: string) => void
hasMessages?: boolean
onClearHistory?: () => void
+ config: PlaygroundConfig
+ parameterEnabled: ParameterEnabled
+ onConfigChange: (
+ key: K,
+ value: PlaygroundConfig[K]
+ ) => void
+ onParameterEnabledChange: (
+ key: keyof ParameterEnabled,
+ value: boolean
+ ) => void
}
const suggestions = [
@@ -100,6 +116,7 @@ export function PlaygroundInput({
onGroupChange,
hasMessages = false,
onClearHistory,
+ ...parameterProps
}: PlaygroundInputProps) {
const { t } = useTranslation()
const [text, setText] = useState('')
@@ -194,6 +211,8 @@ export function PlaygroundInput({
{t('Search')}
+
+
}>
diff --git a/web/default/src/features/playground/components/playground-parameter-panel.tsx b/web/default/src/features/playground/components/playground-parameter-panel.tsx
new file mode 100644
index 00000000..abbf92da
--- /dev/null
+++ b/web/default/src/features/playground/components/playground-parameter-panel.tsx
@@ -0,0 +1,176 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { SlidersHorizontalIcon } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { cn } from '@/lib/utils'
+import { useIsMobile } from '@/hooks/use-mobile'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover'
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@/components/ui/sheet'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip'
+import { PromptInputButton } from '@/components/ai-elements/prompt-input'
+import {
+ PLAYGROUND_PARAMETER_CONTROLS,
+ PLAYGROUND_PARAMETER_PANEL_SCROLL_CLASS,
+ type PlaygroundParameterKey,
+} from '../lib/playground-parameters'
+import type { ParameterEnabled, PlaygroundConfig } from '../types'
+import { PlaygroundParameterRow } from './playground-parameter-row'
+
+type PlaygroundParameterPanelProps = {
+ config: PlaygroundConfig
+ disabled?: boolean
+ onConfigChange: (
+ key: K,
+ value: PlaygroundConfig[K]
+ ) => void
+ onParameterEnabledChange: (
+ key: PlaygroundParameterKey,
+ value: boolean
+ ) => void
+ parameterEnabled: ParameterEnabled
+}
+
+type PlaygroundParameterContentProps = PlaygroundParameterPanelProps & {
+ compact?: boolean
+}
+
+function PlaygroundParameterContent(props: PlaygroundParameterContentProps) {
+ const updateParameterConfig = (
+ key: PlaygroundParameterKey,
+ value: number | null
+ ) => {
+ if (key === 'seed') {
+ props.onConfigChange('seed', value)
+ return
+ }
+
+ const fallback = PLAYGROUND_PARAMETER_CONTROLS.find(
+ (control) => control.key === key
+ )?.min
+ props.onConfigChange(key, value ?? fallback ?? 0)
+ }
+
+ return (
+
+ {PLAYGROUND_PARAMETER_CONTROLS.map((control) => (
+
+ props.onParameterEnabledChange(control.key, enabled)
+ }
+ onValueChange={(value) => updateParameterConfig(control.key, value)}
+ value={props.config[control.key]}
+ />
+ ))}
+
+ )
+}
+
+export function PlaygroundParameterPanel(props: PlaygroundParameterPanelProps) {
+ const { t } = useTranslation()
+ const isMobile = useIsMobile()
+ const activeCount = PLAYGROUND_PARAMETER_CONTROLS.filter(
+ (control) => props.parameterEnabled[control.key]
+ ).length
+
+ const trigger = (
+
+
+
+ {activeCount}
+
+
+ )
+
+ if (isMobile) {
+ return (
+
+
+ } />
+
+ {t('Parameters')}
+
+
+
+
+ {t('Parameter settings')}
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ } />
+
+ {t('Parameters')}
+
+
+
+
+
{t('Parameter settings')}
+
+ {t('Only enabled parameters are sent with the request.')}
+
+
+
+
+
+ )
+}
diff --git a/web/default/src/features/playground/components/playground-parameter-row.tsx b/web/default/src/features/playground/components/playground-parameter-row.tsx
new file mode 100644
index 00000000..92ef7b75
--- /dev/null
+++ b/web/default/src/features/playground/components/playground-parameter-row.tsx
@@ -0,0 +1,132 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { cn } from '@/lib/utils'
+import { Badge } from '@/components/ui/badge'
+import { Input } from '@/components/ui/input'
+import { Slider } from '@/components/ui/slider'
+import { Switch } from '@/components/ui/switch'
+import {
+ getParameterControlValueText,
+ normalizeParameterNumberValue,
+ type PlaygroundParameterControl,
+} from '../lib/playground-parameters'
+
+type PlaygroundParameterRowProps = {
+ control: PlaygroundParameterControl
+ disabled?: boolean
+ enabled: boolean
+ onEnabledChange: (value: boolean) => void
+ onValueChange: (value: number | null) => void
+ value: number | null
+}
+
+export function PlaygroundParameterRow(props: PlaygroundParameterRowProps) {
+ const { t } = useTranslation()
+ const [draft, setDraft] = useState()
+ const controlId = `playground-${props.control.key}`
+
+ const updateValue = (value: string | number) => {
+ props.onValueChange(normalizeParameterNumberValue(props.control.key, value))
+ }
+
+ const commitDraft = (rawValue: string) => {
+ updateValue(rawValue)
+ setDraft(undefined)
+ }
+
+ return (
+
+
+
+
+
+ {t(props.control.labelKey)}
+
+
+ {t(getParameterControlValueText(props.control.key, props.value))}
+
+
+
+ {t(props.control.descriptionKey)}
+
+
+
+
+
+
+ {props.control.valueType === 'slider' ? (
+
{
+ const firstValue = Array.isArray(nextValue)
+ ? nextValue[0]
+ : nextValue
+ updateValue(firstValue)
+ }}
+ step={props.control.step}
+ value={[Number(props.value)]}
+ />
+ ) : (
+ commitDraft(event.currentTarget.value)}
+ onChange={(event) => setDraft(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ event.currentTarget.blur()
+ }
+ }}
+ step={props.control.step}
+ type='number'
+ value={draft ?? props.value ?? ''}
+ />
+ )}
+
+ )
+}
diff --git a/web/default/src/features/playground/index.tsx b/web/default/src/features/playground/index.tsx
index 1cd2ba54..a67a4ad2 100644
--- a/web/default/src/features/playground/index.tsx
+++ b/web/default/src/features/playground/index.tsx
@@ -42,6 +42,7 @@ export function Playground() {
setModels,
setGroups,
updateConfig,
+ updateParameterEnabled,
clearMessages,
} = usePlaygroundState()
@@ -200,6 +201,10 @@ export function Playground() {
onGroupChange={(value) => updateConfig('group', value)}
onClearHistory={handleClearHistory}
onModelChange={(value) => updateConfig('model', value)}
+ config={config}
+ onConfigChange={updateConfig}
+ onParameterEnabledChange={updateParameterEnabled}
+ parameterEnabled={parameterEnabled}
onStop={stopGeneration}
onSubmit={handleSendMessage}
/>
diff --git a/web/default/src/features/playground/lib/index.ts b/web/default/src/features/playground/lib/index.ts
index 085ba088..7321f720 100644
--- a/web/default/src/features/playground/lib/index.ts
+++ b/web/default/src/features/playground/lib/index.ts
@@ -21,3 +21,4 @@ export * from './payload-builder'
export * from './storage'
export * from './message-styles'
export * from './playground-option-utils'
+export * from './playground-parameters'
diff --git a/web/default/src/features/playground/lib/payload-builder.test.ts b/web/default/src/features/playground/lib/payload-builder.test.ts
new file mode 100644
index 00000000..f1da005d
--- /dev/null
+++ b/web/default/src/features/playground/lib/payload-builder.test.ts
@@ -0,0 +1,58 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import { DEFAULT_CONFIG, DEFAULT_PARAMETER_ENABLED } from '../constants'
+import { buildChatCompletionPayload } from './payload-builder'
+
+describe('buildChatCompletionPayload', () => {
+ test('sends enabled zero values and omits disabled or unset parameters', () => {
+ const payload = buildChatCompletionPayload(
+ [
+ {
+ key: 'user-1',
+ from: 'user',
+ versions: [{ id: 'version-1', content: 'Hello' }],
+ },
+ ],
+ {
+ ...DEFAULT_CONFIG,
+ temperature: 0,
+ frequency_penalty: 0,
+ presence_penalty: 0,
+ seed: null,
+ },
+ {
+ ...DEFAULT_PARAMETER_ENABLED,
+ temperature: true,
+ frequency_penalty: true,
+ presence_penalty: true,
+ max_tokens: true,
+ seed: true,
+ }
+ )
+
+ assert.equal(payload.temperature, 0)
+ assert.equal(payload.frequency_penalty, 0)
+ assert.equal(payload.presence_penalty, 0)
+ assert.equal(payload.max_tokens, DEFAULT_CONFIG.max_tokens)
+ assert.equal('seed' in payload, false)
+ assert.equal('top_p' in payload, true)
+ })
+})
diff --git a/web/default/src/features/playground/lib/playground-parameters.test.ts b/web/default/src/features/playground/lib/playground-parameters.test.ts
new file mode 100644
index 00000000..6c08f660
--- /dev/null
+++ b/web/default/src/features/playground/lib/playground-parameters.test.ts
@@ -0,0 +1,50 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import {
+ getParameterControlValueText,
+ normalizeParameterNumberValue,
+} from './playground-parameters'
+
+describe('playground parameter controls', () => {
+ test('clamps decimal and integer values to supported ranges', () => {
+ assert.equal(normalizeParameterNumberValue('temperature', 2.8), 2)
+ assert.equal(normalizeParameterNumberValue('top_p', -1), 0)
+ assert.equal(normalizeParameterNumberValue('frequency_penalty', 0.26), 0.3)
+ assert.equal(normalizeParameterNumberValue('max_tokens', 200000.9), 200000)
+ assert.equal(normalizeParameterNumberValue('seed', 2147483648), 2147483647)
+ })
+
+ test('preserves the optional seed empty state', () => {
+ assert.equal(normalizeParameterNumberValue('seed', ''), null)
+ assert.equal(getParameterControlValueText('seed', null), 'Not Set')
+ assert.equal(normalizeParameterNumberValue('temperature', ''), 0)
+ })
+
+ test('falls back to the control minimum for empty or invalid values', () => {
+ assert.equal(normalizeParameterNumberValue('max_tokens', ''), 1)
+ assert.equal(normalizeParameterNumberValue('max_tokens', 'invalid'), 1)
+ assert.equal(
+ normalizeParameterNumberValue('frequency_penalty', 'invalid'),
+ -2
+ )
+ assert.equal(normalizeParameterNumberValue('seed', 'invalid'), null)
+ })
+})
diff --git a/web/default/src/features/playground/lib/playground-parameters.ts b/web/default/src/features/playground/lib/playground-parameters.ts
new file mode 100644
index 00000000..f5425e83
--- /dev/null
+++ b/web/default/src/features/playground/lib/playground-parameters.ts
@@ -0,0 +1,128 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import type { ParameterEnabled } from '../types'
+
+export type PlaygroundParameterKey = keyof ParameterEnabled
+
+export type PlaygroundParameterControl = {
+ key: PlaygroundParameterKey
+ labelKey: string
+ descriptionKey: string
+ valueType: 'slider' | 'number'
+ min: number
+ max: number
+ step: number
+}
+
+export const PLAYGROUND_PARAMETER_CONTROLS = [
+ {
+ key: 'temperature',
+ labelKey: 'Temperature',
+ descriptionKey: 'Controls randomness and creativity',
+ valueType: 'slider',
+ min: 0,
+ max: 2,
+ step: 0.1,
+ },
+ {
+ key: 'top_p',
+ labelKey: 'Top P',
+ descriptionKey: 'Limits token selection to a probability mass',
+ valueType: 'slider',
+ min: 0,
+ max: 1,
+ step: 0.05,
+ },
+ {
+ key: 'frequency_penalty',
+ labelKey: 'Frequency Penalty',
+ descriptionKey: 'Reduces repeated wording',
+ valueType: 'slider',
+ min: -2,
+ max: 2,
+ step: 0.1,
+ },
+ {
+ key: 'presence_penalty',
+ labelKey: 'Presence Penalty',
+ descriptionKey: 'Encourages new topics',
+ valueType: 'slider',
+ min: -2,
+ max: 2,
+ step: 0.1,
+ },
+ {
+ key: 'max_tokens',
+ labelKey: 'Max Tokens',
+ descriptionKey: 'Caps the response length',
+ valueType: 'number',
+ min: 1,
+ max: 200000,
+ step: 1,
+ },
+ {
+ key: 'seed',
+ labelKey: 'Seed',
+ descriptionKey: 'Keeps compatible responses more repeatable',
+ valueType: 'number',
+ min: 0,
+ max: 2147483647,
+ step: 1,
+ },
+] as const satisfies readonly PlaygroundParameterControl[]
+
+export const PLAYGROUND_PARAMETER_PANEL_SCROLL_CLASS =
+ 'max-h-[min(28rem,calc(100vh-10rem))] overflow-y-auto pr-1'
+
+export function normalizeParameterNumberValue(
+ key: PlaygroundParameterKey,
+ value: string | number
+): number | null {
+ const control = PLAYGROUND_PARAMETER_CONTROLS.find((item) => item.key === key)
+
+ if (value === '') {
+ return key === 'seed' ? null : (control?.min ?? 0)
+ }
+
+ const parsed = typeof value === 'number' ? value : Number.parseFloat(value)
+
+ if (!control || Number.isNaN(parsed)) {
+ return key === 'seed' ? null : (control?.min ?? 0)
+ }
+
+ const clamped = Math.min(control.max, Math.max(control.min, parsed))
+
+ if (control.step >= 1) {
+ return Math.trunc(clamped)
+ }
+
+ const precision = Math.max(0, String(control.step).split('.')[1]?.length ?? 0)
+ return Number(clamped.toFixed(precision))
+}
+
+export function getParameterControlValueText(
+ key: PlaygroundParameterKey,
+ value: number | null
+): string {
+ if (key === 'seed' && value === null) {
+ return 'Not Set'
+ }
+
+ return String(value)
+}
diff --git a/web/default/src/features/system-settings/models/model-pricing-sheet.tsx b/web/default/src/features/system-settings/models/model-pricing-sheet.tsx
index 0e1bd2d9..f15ced41 100644
--- a/web/default/src/features/system-settings/models/model-pricing-sheet.tsx
+++ b/web/default/src/features/system-settings/models/model-pricing-sheet.tsx
@@ -273,6 +273,34 @@ function createInitialLaneState(data?: ModelRatioData | null) {
}
}
+function getInitialPricingMode(data?: ModelRatioData | null): PricingMode {
+ if (isPricingMode(data?.billingMode)) return data.billingMode
+ if (data?.price) return 'per-request'
+ return 'per-token'
+}
+
+function isPricingMode(value: unknown): value is PricingMode {
+ return (
+ value === 'per-token' || value === 'per-request' || value === 'tiered_expr'
+ )
+}
+
+function createInitialFormValues(
+ data?: ModelRatioData | null
+): ModelPricingFormValues {
+ return {
+ name: data?.name || '',
+ price: data?.price || '',
+ ratio: data?.ratio || '',
+ cacheRatio: data?.cacheRatio || '',
+ createCacheRatio: data?.createCacheRatio || '',
+ completionRatio: data?.completionRatio || '',
+ imageRatio: data?.imageRatio || '',
+ audioRatio: data?.audioRatio || '',
+ audioCompletionRatio: data?.audioCompletionRatio || '',
+ }
+}
+
function getModeLabel(mode: PricingMode) {
if (mode === 'per-request') return 'Per-request'
if (mode === 'tiered_expr') return 'Expression'
@@ -414,7 +442,12 @@ export function ModelPricingSheet({
)
}
-export function ModelPricingEditorPanel({
+export function ModelPricingEditorPanel(props: ModelPricingEditorPanelProps) {
+ const identity = props.editData?.name ?? '__new-model-pricing__'
+ return
+}
+
+function ModelPricingEditorPanelContent({
onSave,
editData,
selectedTargetCount = 0,
@@ -422,32 +455,34 @@ export function ModelPricingEditorPanel({
className,
}: ModelPricingEditorPanelProps) {
const { t } = useTranslation()
- const [pricingMode, setPricingMode] = useState('per-token')
- const [promptPrice, setPromptPrice] = useState('')
- const [lanePrices, setLanePrices] = useState>({
- ...EMPTY_LANE_PRICES,
- })
- const [laneEnabled, setLaneEnabled] = useState>({
- ...EMPTY_LANE_ENABLED,
- })
- const [billingExpr, setBillingExpr] = useState('')
- const [requestRuleExpr, setRequestRuleExpr] = useState('')
+ const [initialState] = useState(() => ({
+ laneState: createInitialLaneState(editData),
+ pricingMode: getInitialPricingMode(editData),
+ billingExpr: editData?.billingExpr || '',
+ requestRuleExpr: editData?.requestRuleExpr || '',
+ }))
+ const [pricingMode, setPricingMode] = useState(
+ initialState.pricingMode
+ )
+ const [promptPrice, setPromptPrice] = useState(
+ initialState.laneState.promptPrice
+ )
+ const [lanePrices, setLanePrices] = useState>(
+ initialState.laneState.prices
+ )
+ const [laneEnabled, setLaneEnabled] = useState>(
+ initialState.laneState.enabled
+ )
+ const [billingExpr, setBillingExpr] = useState(initialState.billingExpr)
+ const [requestRuleExpr, setRequestRuleExpr] = useState(
+ initialState.requestRuleExpr
+ )
const [previewOpen, setPreviewOpen] = useState(true)
const isEditMode = !!editData
const form = useForm({
resolver: zodResolver(createModelPricingSchema(t)),
- defaultValues: {
- name: '',
- price: '',
- ratio: '',
- cacheRatio: '',
- createCacheRatio: '',
- completionRatio: '',
- imageRatio: '',
- audioRatio: '',
- audioCompletionRatio: '',
- },
+ defaultValues: createInitialFormValues(editData),
})
useEffect(() => {
@@ -465,13 +500,7 @@ export function ModelPricingEditorPanel({
audioRatio: editData.audioRatio || '',
audioCompletionRatio: editData.audioCompletionRatio || '',
})
- setPricingMode(
- editData.billingMode === 'tiered_expr'
- ? 'tiered_expr'
- : editData.price
- ? 'per-request'
- : 'per-token'
- )
+ setPricingMode(getInitialPricingMode(editData))
setBillingExpr(editData.billingExpr || '')
setRequestRuleExpr(editData.requestRuleExpr || '')
} else {
@@ -908,6 +937,7 @@ export function ModelPricingEditorPanel({
>
.
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { createInstance } from 'i18next'
+import { JSDOM } from 'jsdom'
+import assert from 'node:assert/strict'
+import { after, before, describe, test } from 'node:test'
+import { I18nextProvider } from 'react-i18next'
+import {
+ ModelPricingEditorPanel,
+ type ModelRatioData,
+} from './model-pricing-sheet'
+import { TieredPricingEditor } from './tiered-pricing-editor'
+
+const dom = new JSDOM('', {
+ url: 'http://localhost/',
+})
+const i18n = createInstance()
+const globalKeys = [
+ 'window',
+ 'document',
+ 'navigator',
+ 'HTMLElement',
+ 'Element',
+ 'Node',
+ 'Event',
+ 'MouseEvent',
+ 'KeyboardEvent',
+ 'MutationObserver',
+ 'getComputedStyle',
+ 'requestAnimationFrame',
+ 'cancelAnimationFrame',
+ 'ResizeObserver',
+ 'IS_REACT_ACT_ENVIRONMENT',
+] as const
+const previousDescriptors = new Map<
+ (typeof globalKeys)[number],
+ PropertyDescriptor | undefined
+>()
+
+class ResizeObserverStub {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+
+function setGlobal(key: (typeof globalKeys)[number], value: unknown) {
+ previousDescriptors.set(key, Object.getOwnPropertyDescriptor(globalThis, key))
+ Object.defineProperty(globalThis, key, {
+ configurable: true,
+ writable: true,
+ value,
+ })
+}
+
+function restoreGlobals() {
+ for (const key of [...globalKeys].reverse()) {
+ const descriptor = previousDescriptors.get(key)
+ if (descriptor) {
+ Object.defineProperty(globalThis, key, descriptor)
+ } else {
+ Reflect.deleteProperty(globalThis, key)
+ }
+ }
+}
+
+function createContainer() {
+ const container = dom.window.document.createElement('div')
+ dom.window.document.body.append(container)
+ return container
+}
+
+async function unmount(root: Root, container: HTMLElement) {
+ await act(async () => root.unmount())
+ container.remove()
+}
+
+const modelA: ModelRatioData = {
+ name: 'model-a',
+ billingMode: 'tiered_expr',
+ billingExpr: 'tier("a", p * 1 + c * 2)',
+ requestRuleExpr: '',
+}
+const modelB: ModelRatioData = {
+ name: 'model-b',
+ billingMode: 'tiered_expr',
+ billingExpr: 'tier("b", p * 3 + c * 4)',
+ requestRuleExpr: '',
+}
+
+before(async () => {
+ await i18n.init({
+ lng: 'en',
+ fallbackLng: 'en',
+ resources: { en: { translation: {} } },
+ interpolation: { escapeValue: false },
+ })
+
+ const window = dom.window
+ Object.defineProperty(window, 'matchMedia', {
+ configurable: true,
+ value: () => ({
+ matches: false,
+ media: '',
+ onchange: null,
+ addEventListener() {},
+ removeEventListener() {},
+ addListener() {},
+ removeListener() {},
+ dispatchEvent: () => false,
+ }),
+ })
+
+ setGlobal('window', window)
+ setGlobal('document', window.document)
+ setGlobal('navigator', window.navigator)
+ setGlobal('HTMLElement', window.HTMLElement)
+ setGlobal('Element', window.Element)
+ setGlobal('Node', window.Node)
+ setGlobal('Event', window.Event)
+ setGlobal('MouseEvent', window.MouseEvent)
+ setGlobal('KeyboardEvent', window.KeyboardEvent)
+ setGlobal('MutationObserver', window.MutationObserver)
+ setGlobal('getComputedStyle', window.getComputedStyle.bind(window))
+ setGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
+ setTimeout(() => callback(Date.now()), 0)
+ )
+ setGlobal('cancelAnimationFrame', (handle: number) => clearTimeout(handle))
+ setGlobal('ResizeObserver', ResizeObserverStub)
+ setGlobal('IS_REACT_ACT_ENVIRONMENT', true)
+})
+
+after(() => {
+ restoreGlobals()
+ dom.window.close()
+})
+
+describe('TieredPricingEditor runtime behavior', () => {
+ test('mounts the visual editor without a runtime hook error', async () => {
+ const container = createContainer()
+ const root = createRoot(container)
+
+ try {
+ await act(async () => {
+ root.render(
+
+ undefined}
+ onRequestRuleExprChange={() => undefined}
+ />
+
+ )
+ })
+
+ assert.match(container.textContent || '', /Tier 1 \/ 1/)
+ } finally {
+ await unmount(root, container)
+ }
+ })
+
+ test('never emits model A billing through model B callbacks', async () => {
+ const container = createContainer()
+ const root = createRoot(container)
+ const changes: Array<{ modelName: string; next: string }> = []
+ const renderEditor = (model: ModelRatioData) => (
+
+
+ changes.push({ modelName: model.name, next })
+ }
+ onRequestRuleExprChange={() => undefined}
+ />
+
+ )
+
+ try {
+ await act(async () => root.render(renderEditor(modelA)))
+ await act(async () => root.render(renderEditor(modelB)))
+
+ assert.equal(
+ changes.some(
+ ({ modelName, next }) =>
+ modelName === modelB.name && next === modelA.billingExpr
+ ),
+ false
+ )
+ } finally {
+ await unmount(root, container)
+ }
+ })
+
+ test('does not carry model A billing state into model B', async () => {
+ const container = createContainer()
+ const root = createRoot(container)
+ const renderPanel = (editData: ModelRatioData) => (
+
+ undefined}
+ onCancel={() => undefined}
+ />
+
+ )
+
+ try {
+ await act(async () => root.render(renderPanel(modelA)))
+ assert.match(container.textContent || '', /tier\("a", p \* 1 \+ c \* 2\)/)
+
+ await act(async () => root.render(renderPanel(modelB)))
+
+ const text = container.textContent || ''
+ assert.match(text, /tier\("b", p \* 3 \+ c \* 4\)/)
+ assert.doesNotMatch(text, /tier\("a", p \* 1 \+ c \* 2\)/)
+ assert.doesNotMatch(text, /tier\("base", p \* 0 \+ c \* 0\)/)
+ } finally {
+ await unmount(root, container)
+ }
+ })
+})
diff --git a/web/default/src/features/system-settings/models/tiered-pricing-editor.tsx b/web/default/src/features/system-settings/models/tiered-pricing-editor.tsx
index e227b627..f55adc89 100644
--- a/web/default/src/features/system-settings/models/tiered-pricing-editor.tsx
+++ b/web/default/src/features/system-settings/models/tiered-pricing-editor.tsx
@@ -16,14 +16,8 @@ along with this program. If not, see .
For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
*/
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import { ChevronDown, Plus, Trash2 } from 'lucide-react'
+import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { cn } from '@/lib/utils'
-import { Alert, AlertDescription } from '@/components/ui/alert'
-import { Badge } from '@/components/ui/badge'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
@@ -33,976 +27,93 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
-import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
-import { Textarea } from '@/components/ui/textarea'
import {
- BILLING_EXTRA_VARS,
- COMMON_TIMEZONES,
- MATCH_CONTAINS,
- MATCH_EQ,
- MATCH_EXISTS,
- MATCH_GT,
- MATCH_GTE,
- MATCH_LT,
- MATCH_LTE,
- MATCH_RANGE,
- SOURCE_HEADER,
- SOURCE_PARAM,
- SOURCE_TIME,
- TIME_FUNCS,
buildRequestRuleExpr,
combineBillingExpr,
- createEmptyCondition,
- createEmptyRuleGroup,
- createEmptyTimeCondition,
- getRequestRuleMatchOptions,
splitBillingExprAndRequestRules,
tryParseRequestRuleExpr,
- type ParamHeaderCondition,
- type RequestCondition,
type RequestRuleGroup,
- type TimeCondition,
- type TimeFunc,
} from '@/features/pricing/lib/billing-expr'
import {
- CACHE_MODE_GENERIC,
- CACHE_MODE_TIMED,
- type CacheMode,
- type TierConditionInput,
type VisualConfig,
- type VisualTier,
createDefaultVisualConfig,
generateExprFromVisualConfig,
- getTierCacheMode,
- normalizeVisualConfig,
- normalizeVisualTier,
tryParseVisualConfig,
} from '@/features/pricing/lib/tier-expr'
-import { DraftNumberInput } from './tiered-pricing-fields'
+import { RequestRuleEditor } from './tiered-pricing-request-rules'
import {
CostEstimator,
LlmPromptHelper,
PresetSection,
type Preset,
} from './tiered-pricing-support'
-import {
- formatTokenHint,
- priceToUnitCost,
- unitCostToPrice,
-} from './tiered-pricing-utils'
-
-const PRICE_SUFFIX = '$/1M tokens'
-const CACHE_PRICE_VARS = BILLING_EXTRA_VARS.filter(
- (variable) => variable.group === 'cache'
-)
-const MEDIA_PRICE_VARS = BILLING_EXTRA_VARS.filter(
- (variable) => variable.group === 'media'
-)
-
-const CONDITION_INPUT_OPTIONS: {
- value: TierConditionInput['var']
- labelKey: string
-}[] = [
- { value: 'len', labelKey: 'Full input length' },
- { value: 'p', labelKey: 'Billable input tokens' },
- { value: 'c', labelKey: 'Billable output tokens' },
-]
-const OPS: TierConditionInput['op'][] = ['<', '<=', '>', '>=']
-
-// ---------------------------------------------------------------------------
-// Tier condition row
-// ---------------------------------------------------------------------------
-
-type ConditionRowProps = {
- condition: TierConditionInput
- onChange: (next: TierConditionInput) => void
- onRemove: () => void
-}
-
-function ConditionRow({ condition, onChange, onRemove }: ConditionRowProps) {
- const { t } = useTranslation()
- const currentInputOption = CONDITION_INPUT_OPTIONS.find(
- (option) => option.value === condition.var
- )
-
- return (
-
- ({
- value: option.value,
- label: t(option.labelKey),
- })),
- ]}
- value={condition.var}
- onValueChange={(value) =>
- onChange({ ...condition, var: value as TierConditionInput['var'] })
- }
- >
-
-
- {currentInputOption
- ? t(currentInputOption.labelKey)
- : condition.var}
-
-
-
-
- {CONDITION_INPUT_OPTIONS.map((option) => (
-
- {t(option.labelKey)}
-
- ))}
-
-
-
- ({ value: op, label: op }))]}
- value={condition.op}
- onValueChange={(value) =>
- onChange({ ...condition, op: value as TierConditionInput['op'] })
- }
- >
-
-
-
-
-
- {OPS.map((op) => (
-
- {op}
-
- ))}
-
-
-
- onChange({ ...condition, value })}
- placeholder='tokens'
- className='w-32'
- />
-
- {formatTokenHint(condition.value, t)}
-
-
-
-
-
- )
-}
-
-// ---------------------------------------------------------------------------
-// Price input field
-// ---------------------------------------------------------------------------
-
-type PriceFieldProps = {
- label: string
- hint?: string
- value: number
- onChange: (next: number) => void
-}
-
-function PriceField({ label, hint, value, onChange }: PriceFieldProps) {
- return (
-
-
{label}
-
- {hint &&
{hint}
}
-
- )
-}
+import { RawExprEditor, VisualEditor } from './tiered-pricing-visual-editor'
// ---------------------------------------------------------------------------
-// Single tier card (visual editor)
+// Main editor
// ---------------------------------------------------------------------------
-type VisualTierCardProps = {
- tier: VisualTier
- index: number
- total: number
- onChange: (next: VisualTier) => void
- onRemove: () => void
- onAddCondition: () => void
+export type TieredPricingEditorProps = {
+ modelName?: string
+ modelIdentity?: string
+ billingExpr: string
+ requestRuleExpr: string
+ onBillingExprChange: (next: string) => void
+ onRequestRuleExprChange: (next: string) => void
}
-function VisualTierCard({
- tier,
- index,
- total,
- onChange,
- onRemove,
- onAddCondition,
-}: VisualTierCardProps) {
- const { t } = useTranslation()
- const cacheMode = getTierCacheMode(tier)
-
- const handleConditionChange = (
- conditionIndex: number,
- next: TierConditionInput
- ) => {
- const conditions = [...tier.conditions]
- conditions[conditionIndex] = next
- onChange({ ...tier, conditions })
- }
-
- const handleConditionRemove = (conditionIndex: number) => {
- onChange({
- ...tier,
- conditions: tier.conditions.filter((_, i) => i !== conditionIndex),
- })
- }
-
- const handlePriceChange = (field: keyof VisualTier, value: number) => {
- onChange({ ...tier, [field]: value })
- }
-
- const handleCacheModeChange = (mode: CacheMode) => {
- onChange({
- ...tier,
- cache_mode: mode,
- cache_create_1h_unit_cost:
- mode === CACHE_MODE_TIMED ? (tier.cache_create_1h_unit_cost ?? 0) : 0,
- })
- }
-
- const inputUnitPrice = unitCostToPrice(tier.input_unit_cost)
- const outputUnitPrice = unitCostToPrice(tier.output_unit_cost)
- const hasMediaPricing = MEDIA_PRICE_VARS.some((variable) => {
- const fieldKey = variable.tierField as keyof VisualTier
- return unitCostToPrice((tier[fieldKey] as number | undefined) ?? 0) > 0
- })
- const [mediaOpen, setMediaOpen] = useState(hasMediaPricing)
-
- useEffect(() => {
- // Reveal newly configured media prices when controlled data changes.
- // eslint-disable-next-line react-hooks/set-state-in-effect
- if (hasMediaPricing) setMediaOpen(true)
- }, [hasMediaPricing])
-
- const renderPriceVariable = (
- variable: (typeof BILLING_EXTRA_VARS)[number]
- ) => {
- const fieldKey = variable.tierField as keyof VisualTier
- const value = unitCostToPrice((tier[fieldKey] as number | undefined) ?? 0)
-
- return (
- handlePriceChange(fieldKey, priceToUnitCost(next))}
- />
- )
- }
-
- return (
-
-
-
-
- {t('Tier')} {index + 1} / {total}
-
- {tier.conditions.length === 0 && (
- {t('Fallback tier')}
- )}
-
- onChange({ ...tier, label: event.target.value })
- }
- placeholder={t('Tier name')}
- className='h-7 w-36'
- />
-
-
-
-
-
-
- {/* Conditions */}
-
-
-
{t('Tier conditions')}
-
= 2}
- className='h-7 px-2 text-xs'
- >
-
- {t('Add condition')}
-
-
- {tier.conditions.length === 0 ? (
-
- {t('Always matches (default tier).')}
-
- ) : (
- tier.conditions.map((condition, conditionIndex) => (
-
handleConditionChange(conditionIndex, next)}
- onRemove={() => handleConditionRemove(conditionIndex)}
- />
- ))
- )}
-
-
-
-
- {t('Token prices')}
-
- {PRICE_SUFFIX}
-
-
-
-
-
-
- handlePriceChange('input_unit_cost', priceToUnitCost(value))
- }
- />
-
- handlePriceChange('output_unit_cost', priceToUnitCost(value))
- }
- />
-
-
-
-
-
- value !== null && handleCacheModeChange(value as CacheMode)
- }
- >
-
-
- {t('Generic cache')}
-
-
- {t('Time-sliced cache (Claude)')}
-
-
-
-
-
- {CACHE_PRICE_VARS.map((variable) => {
- if (variable.key === 'cc1h' && cacheMode !== CACHE_MODE_TIMED) {
- return null
- }
- return renderPriceVariable(variable)
- })}
-
-
-
-
+type EditorMode = 'visual' | 'raw'
- {/* Media prices */}
-
-
setMediaOpen((prev) => !prev)}
- >
-
- {t('Media pricing')}
-
- {mediaOpen && (
-
- {MEDIA_PRICE_VARS.map(renderPriceVariable)}
-
- )}
-
-
- )
+type RawExprFallback = {
+ rawExpr: string
+ billingExpr: string
+ requestRuleExpr: string
}
-// ---------------------------------------------------------------------------
-// Visual editor (list of tiers)
-// ---------------------------------------------------------------------------
-
-type VisualEditorProps = {
+type InitialEditorState = {
+ editorMode: EditorMode
visualConfig: VisualConfig | null
- onChange: (next: VisualConfig) => void
+ rawExpr: string
+ requestRuleGroups: RequestRuleGroup[]
+ requestRulesCompatible: boolean
+ rawExprFallback: RawExprFallback | null
}
-function VisualEditor({ visualConfig, onChange }: VisualEditorProps) {
- const { t } = useTranslation()
- const config = useMemo(
- () => normalizeVisualConfig(visualConfig),
- [visualConfig]
- )
-
- const handleTierChange = (index: number, next: VisualTier) => {
- const tiers = [...config.tiers]
- tiers[index] = normalizeVisualTier(next)
- onChange({ ...config, tiers })
- }
-
- const handleAddTier = () => {
- const tiers = [...config.tiers]
- const lastIndex = tiers.length - 1
- // When adding a new fallback, give the previous catch-all tier a default
- // upper-bound condition so the expression compiles into a sane two-tier
- // shape. Mirrors the legacy editor UX for adding tiers.
- if (lastIndex >= 0 && tiers[lastIndex].conditions.length === 0) {
- tiers[lastIndex] = normalizeVisualTier({
- ...tiers[lastIndex],
- conditions: [{ var: 'len', op: '<', value: 200000 }],
- })
- }
- tiers.push(
- normalizeVisualTier({
- label: `tier_${tiers.length + 1}`,
- conditions: [],
- input_unit_cost: 0,
- output_unit_cost: 0,
- })
- )
- onChange({ ...config, tiers })
- }
-
- const handleRemoveTier = (index: number) => {
- const tiers = config.tiers.filter((_, i) => i !== index)
- onChange({ ...config, tiers: tiers.length > 0 ? tiers : config.tiers })
- }
-
- const handleAddCondition = (index: number) => {
- const tier = config.tiers[index]
- if (tier.conditions.length >= 2) return
- // Prefer `len` (input length) over `p`/`c` for tier conditions because
- // `p` is subject to auto-exclusion when sub-categories like `cr` are
- // priced separately, which can misroute long-input requests into shorter
- // tiers when cache-hits reduce the effective `p`.
- const usedVars = new Set(tier.conditions.map((c) => c.var))
- const nextVar: TierConditionInput['var'] = usedVars.has('len') ? 'c' : 'len'
- onChange({
- ...config,
- tiers: config.tiers.map((current, i) =>
- i === index
- ? {
- ...current,
- conditions: [
- ...tier.conditions,
- { var: nextVar, op: '<', value: 200000 },
- ],
- }
- : current
- ),
- })
+function createInitialEditorState(
+ billingExpr: string,
+ requestRuleExpr: string
+): InitialEditorState {
+ const parsedConfig = tryParseVisualConfig(billingExpr)
+ const parsedRequestRuleGroups = tryParseRequestRuleExpr(requestRuleExpr)
+ const rawExpr = combineBillingExpr(billingExpr || '', requestRuleExpr || '')
+ return {
+ editorMode: parsedConfig || !billingExpr ? 'visual' : 'raw',
+ visualConfig:
+ parsedConfig || (!billingExpr ? createDefaultVisualConfig() : null),
+ rawExpr,
+ requestRuleGroups: parsedRequestRuleGroups ?? [],
+ requestRulesCompatible: parsedRequestRuleGroups !== null,
+ rawExprFallback:
+ parsedRequestRuleGroups === null && billingExpr && requestRuleExpr
+ ? { rawExpr, billingExpr, requestRuleExpr }
+ : null,
}
-
- return (
-
-
- {t(
- 'Each tier supports up to 2 conditions. The last tier without conditions is the fallback.'
- )}
-
- {config.tiers.map((tier, index) => (
-
handleTierChange(index, next)}
- onRemove={() => handleRemoveTier(index)}
- onAddCondition={() => handleAddCondition(index)}
- />
- ))}
-
-
- {t('Add tier')}
-
-
- )
-}
-
-// ---------------------------------------------------------------------------
-// Raw expression editor
-// ---------------------------------------------------------------------------
-
-type RawExprEditorProps = {
- exprString: string
- onChange: (value: string) => void
-}
-
-function RawExprEditor({ exprString, onChange }: RawExprEditorProps) {
- const { t } = useTranslation()
- return (
-
-
-
-
- {t('Variables')}: len, p, c,{' '}
- cr, cc, cc1h,{' '}
- img, img_o, ai,{' '}
- ao
-
-
- {t('Functions')}: tier(name, value), max,{' '}
- min, ceil, floor,{' '}
- abs, header(name),{' '}
- param(path), has(source, text)
-
-
-
-
- )
-}
-
-// ---------------------------------------------------------------------------
-// Request rule condition row
-// ---------------------------------------------------------------------------
-
-type RuleConditionRowProps = {
- condition: RequestCondition
- onChange: (next: RequestCondition) => void
- onRemove: () => void
}
-function RuleConditionRow({
- condition,
- onChange,
- onRemove,
-}: RuleConditionRowProps) {
- const { t } = useTranslation()
- const matchOptions = getRequestRuleMatchOptions(condition.source)
- const getMatchLabel = (mode: string) => {
- switch (mode) {
- case MATCH_EQ:
- return t('Equals')
- case MATCH_CONTAINS:
- return t('Contains')
- case MATCH_EXISTS:
- return t('Exists')
- case MATCH_GT:
- return t('Greater than')
- case MATCH_GTE:
- return t('Greater than or equal')
- case MATCH_LT:
- return t('Less than')
- case MATCH_LTE:
- return t('Less than or equal')
- case MATCH_RANGE:
- return t('Overnight range')
- default:
- return mode
- }
- }
- const getTimeFuncLabel = (timeFunc: TimeFunc) => {
- switch (timeFunc) {
- case 'hour':
- return t('Hour of day')
- case 'minute':
- return t('Minute')
- case 'weekday':
- return t('Weekday')
- case 'month':
- return t('Month number')
- case 'day':
- return t('Day of month')
- default:
- return timeFunc
- }
- }
- const sourceLabel =
- condition.source === SOURCE_PARAM
- ? t('Body param')
- : condition.source === SOURCE_HEADER
- ? t('Header')
- : t('Time')
-
- const handleSourceChange = (source: string) => {
- if (source === SOURCE_TIME) {
- onChange(createEmptyTimeCondition())
- } else if (source === SOURCE_HEADER || source === SOURCE_PARAM) {
- onChange({
- ...createEmptyCondition(),
- source: source as 'param' | 'header',
- })
+function splitRawExprWithFallback(
+ rawExpr: string,
+ fallback: RawExprFallback | null
+) {
+ if (fallback?.rawExpr === rawExpr) {
+ return {
+ billingExpr: fallback.billingExpr,
+ requestRuleExpr: fallback.requestRuleExpr,
}
}
-
- const handleModeChange = (mode: string) => {
- onChange({ ...condition, mode } as RequestCondition)
- }
-
- const renderTimeCondition = (timeCond: TimeCondition) => (
- <>
- ({
- value: fn,
- label: getTimeFuncLabel(fn),
- })),
- ]}
- value={timeCond.timeFunc}
- onValueChange={(value) =>
- onChange({ ...timeCond, timeFunc: value as TimeFunc })
- }
- >
-
- {getTimeFuncLabel(timeCond.timeFunc)}
-
-
-
- {TIME_FUNCS.map((fn) => (
-
- {getTimeFuncLabel(fn)}
-
- ))}
-
-
-
- ({
- value: tz.value,
- label: tz.label,
- })),
- ]}
- value={timeCond.timezone}
- onValueChange={(value) =>
- value !== null && onChange({ ...timeCond, timezone: value })
- }
- >
-
-
- {COMMON_TIMEZONES.find((tz) => tz.value === timeCond.timezone)
- ?.label ?? timeCond.timezone}
-
-
-
-
- {COMMON_TIMEZONES.map((tz) => (
-
- {tz.label}
-
- ))}
-
-
-
- ({
- value: option.value,
- label: getMatchLabel(option.value),
- })),
- ]}
- value={timeCond.mode}
- onValueChange={(v) => v !== null && handleModeChange(v)}
- >
-
- {getMatchLabel(timeCond.mode)}
-
-
-
- {matchOptions.map((option) => (
-
- {getMatchLabel(option.value)}
-
- ))}
-
-
-
- {timeCond.mode === MATCH_RANGE ? (
- <>
-
- onChange({ ...timeCond, rangeStart: String(value) })
- }
- placeholder={t('Start')}
- className='w-20'
- />
- ~
-
- onChange({ ...timeCond, rangeEnd: String(value) })
- }
- placeholder={t('End')}
- className='w-20'
- />
- >
- ) : (
-
- onChange({ ...timeCond, value: String(value) })
- }
- placeholder={t('Value')}
- className='w-24'
- />
- )}
- >
- )
-
- const renderParamHeaderCondition = (phCond: ParamHeaderCondition) => (
- <>
- onChange({ ...phCond, path: event.target.value })}
- placeholder={
- phCond.source === SOURCE_HEADER ? 'X-Header-Name' : 'service_tier'
- }
- className='w-44'
- />
- ({
- value: option.value,
- label: getMatchLabel(option.value),
- })),
- ]}
- value={phCond.mode}
- onValueChange={(v) => v !== null && handleModeChange(v)}
- >
-
- {getMatchLabel(phCond.mode)}
-
-
-
- {matchOptions.map((option) => (
-
- {getMatchLabel(option.value)}
-
- ))}
-
-
-
- {phCond.mode !== MATCH_EXISTS && (
-
- onChange({ ...phCond, value: event.target.value })
- }
- placeholder={t('Value')}
- className='w-44'
- />
- )}
- >
- )
-
- return (
-
- v !== null && handleSourceChange(v)}
- >
-
- {sourceLabel}
-
-
-
- {t('Body param')}
- {t('Header')}
- {t('Time')}
-
-
-
- {condition.source === SOURCE_TIME
- ? renderTimeCondition(condition as TimeCondition)
- : renderParamHeaderCondition(condition as ParamHeaderCondition)}
-
-
-
-
- )
-}
-
-// ---------------------------------------------------------------------------
-// Request rule group card
-// ---------------------------------------------------------------------------
-
-type RuleGroupCardProps = {
- group: RequestRuleGroup
- index: number
- onChange: (next: RequestRuleGroup) => void
- onRemove: () => void
-}
-
-function RuleGroupCard({
- group,
- index,
- onChange,
- onRemove,
-}: RuleGroupCardProps) {
- const { t } = useTranslation()
-
- const handleConditionChange = (
- conditionIndex: number,
- next: RequestCondition
- ) => {
- const conditions = [...group.conditions]
- conditions[conditionIndex] = next
- onChange({ ...group, conditions })
- }
-
- const handleAddCondition = (timeMode: boolean) => {
- onChange({
- ...group,
- conditions: [
- ...group.conditions,
- timeMode ? createEmptyTimeCondition() : createEmptyCondition(),
- ],
- })
- }
-
- return (
-
-
-
- {t('Rule group')} #{index + 1}
-
-
-
-
-
-
-
- {group.conditions.map((condition, conditionIndex) => (
-
handleConditionChange(conditionIndex, next)}
- onRemove={() =>
- onChange({
- ...group,
- conditions: group.conditions.filter(
- (_, i) => i !== conditionIndex
- ),
- })
- }
- />
- ))}
-
-
handleAddCondition(false)}
- >
-
- {t('Add param/header')}
-
-
handleAddCondition(true)}
- >
-
- {t('Add time condition')}
-
-
-
-
-
- {t('Multiplier')}
-
- onChange({ ...group, multiplier: String(value) })
- }
- className='w-32'
- placeholder='1.0'
- />
-
- {t('Final cost = base × multiplier when conditions match')}
-
-
-
- )
+ return splitBillingExprAndRequestRules(rawExpr)
}
-// ---------------------------------------------------------------------------
-// Main editor
-// ---------------------------------------------------------------------------
-
-export type TieredPricingEditorProps = {
- modelName?: string
- billingExpr: string
- requestRuleExpr: string
- onBillingExprChange: (next: string) => void
- onRequestRuleExprChange: (next: string) => void
-}
-
-type EditorMode = 'visual' | 'raw'
-
-export const TieredPricingEditor = memo(function TieredPricingEditor({
+const TieredPricingEditorContent = memo(function TieredPricingEditorContent({
modelName,
billingExpr: currentExpr,
requestRuleExpr: currentRequestRuleExpr,
@@ -1010,54 +121,32 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
onRequestRuleExprChange,
}: TieredPricingEditorProps) {
const { t } = useTranslation()
- const [editorMode, setEditorMode] = useState('visual')
- const [visualConfig, setVisualConfig] = useState(() =>
- tryParseVisualConfig(currentExpr)
+ const [initialState] = useState(() =>
+ createInitialEditorState(currentExpr, currentRequestRuleExpr)
+ )
+ const [editorMode, setEditorMode] = useState(
+ initialState.editorMode
)
- const [rawExpr, setRawExpr] = useState(() =>
- combineBillingExpr(currentExpr || '', currentRequestRuleExpr || '')
+ const [visualConfig, setVisualConfig] = useState(
+ initialState.visualConfig
)
+ const [rawExpr, setRawExpr] = useState(initialState.rawExpr)
const [requestRuleGroups, setRequestRuleGroups] = useState<
RequestRuleGroup[]
- >(() => tryParseRequestRuleExpr(currentRequestRuleExpr) || [])
- const initRef = useRef(false)
-
- useEffect(() => {
- if (initRef.current) return
- initRef.current = true
- const parsedConfig = tryParseVisualConfig(currentExpr)
- if (parsedConfig) {
- // eslint-disable-next-line react-hooks/set-state-in-effect
- setVisualConfig(parsedConfig)
- setEditorMode('visual')
- } else if (currentExpr) {
- setVisualConfig(null)
- setEditorMode('raw')
- } else {
- setVisualConfig(createDefaultVisualConfig())
- }
- setRawExpr(
- combineBillingExpr(currentExpr || '', currentRequestRuleExpr || '')
- )
- setRequestRuleGroups(tryParseRequestRuleExpr(currentRequestRuleExpr) || [])
- }, [currentExpr, currentRequestRuleExpr])
-
- useEffect(() => {
- initRef.current = false
- }, [modelName])
-
- const canUseVisualRules = useMemo(() => {
- if (!currentRequestRuleExpr) return true
- return tryParseRequestRuleExpr(currentRequestRuleExpr) !== null
- }, [currentRequestRuleExpr])
+ >(initialState.requestRuleGroups)
+ const [requestRulesCompatible, setRequestRulesCompatible] = useState(
+ initialState.requestRulesCompatible
+ )
+ const [rawExprFallback, setRawExprFallback] =
+ useState(initialState.rawExprFallback)
const effectiveExpr = useMemo(() => {
if (editorMode === 'visual') {
return generateExprFromVisualConfig(visualConfig)
}
- const { billingExpr } = splitBillingExprAndRequestRules(rawExpr)
+ const { billingExpr } = splitRawExprWithFallback(rawExpr, rawExprFallback)
return billingExpr
- }, [editorMode, visualConfig, rawExpr])
+ }, [editorMode, visualConfig, rawExpr, rawExprFallback])
useEffect(() => {
if (effectiveExpr !== currentExpr) {
@@ -1066,7 +155,7 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
}, [effectiveExpr, currentExpr, onBillingExprChange])
useEffect(() => {
- if (editorMode !== 'visual') return
+ if (editorMode !== 'visual' || !requestRulesCompatible) return
const ruleExpr = buildRequestRuleExpr(requestRuleGroups)
if (ruleExpr !== currentRequestRuleExpr) {
onRequestRuleExprChange(ruleExpr)
@@ -1074,6 +163,7 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
}, [
editorMode,
requestRuleGroups,
+ requestRulesCompatible,
currentRequestRuleExpr,
onRequestRuleExprChange,
])
@@ -1084,9 +174,11 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
const handleRawChange = useCallback(
(value: string) => {
+ setRawExprFallback(null)
setRawExpr(value)
const { requestRuleExpr: ruleStr } =
splitBillingExprAndRequestRules(value)
+ setRequestRulesCompatible(tryParseRequestRuleExpr(ruleStr) !== null)
onRequestRuleExprChange(ruleStr)
},
[onRequestRuleExprChange]
@@ -1096,7 +188,8 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
(next: EditorMode) => {
if (next === 'visual') {
const { billingExpr, requestRuleExpr: ruleStr } =
- splitBillingExprAndRequestRules(rawExpr)
+ splitRawExprWithFallback(rawExpr, rawExprFallback)
+ setRawExprFallback(null)
const parsed = tryParseVisualConfig(billingExpr)
if (parsed) {
setVisualConfig(parsed)
@@ -1104,16 +197,41 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
setVisualConfig(createDefaultVisualConfig())
}
const parsedGroups = tryParseRequestRuleExpr(ruleStr)
- setRequestRuleGroups(parsedGroups || [])
+ if (parsedGroups !== null) {
+ setRequestRuleGroups(parsedGroups)
+ setRequestRulesCompatible(true)
+ } else {
+ setRequestRulesCompatible(false)
+ }
onRequestRuleExprChange(ruleStr)
} else {
const expr = generateExprFromVisualConfig(visualConfig)
- const ruleExpr = buildRequestRuleExpr(requestRuleGroups)
- setRawExpr(combineBillingExpr(expr, ruleExpr) || expr)
+ const ruleExpr = requestRulesCompatible
+ ? buildRequestRuleExpr(requestRuleGroups)
+ : currentRequestRuleExpr
+ const combined = combineBillingExpr(expr, ruleExpr) || expr
+ setRawExpr(combined)
+ setRawExprFallback(
+ !requestRulesCompatible && ruleExpr
+ ? {
+ rawExpr: combined,
+ billingExpr: expr,
+ requestRuleExpr: ruleExpr,
+ }
+ : null
+ )
}
setEditorMode(next)
},
- [rawExpr, visualConfig, requestRuleGroups, onRequestRuleExprChange]
+ [
+ rawExpr,
+ rawExprFallback,
+ visualConfig,
+ requestRuleGroups,
+ requestRulesCompatible,
+ currentRequestRuleExpr,
+ onRequestRuleExprChange,
+ ]
)
const applyPreset = useCallback(
@@ -1131,6 +249,8 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
setVisualConfig(null)
}
setRequestRuleGroups(presetGroups)
+ setRequestRulesCompatible(true)
+ setRawExprFallback(null)
onRequestRuleExprChange(ruleExpr)
},
[onRequestRuleExprChange]
@@ -1138,6 +258,7 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
const handleRuleGroupsChange = useCallback((next: RequestRuleGroup[]) => {
setRequestRuleGroups(next)
+ setRequestRulesCompatible(true)
}, [])
return (
@@ -1182,62 +303,11 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
)}
{editorMode === 'visual' && (
-
-
-
- {t('Request rule pricing')}
-
-
- {t(
- 'When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.'
- )}
-
-
-
- {currentRequestRuleExpr && !canUseVisualRules ? (
-
-
- {t(
- 'This expression is too complex for the visual editor. Please switch to expression mode to edit.'
- )}
-
-
- ) : (
- <>
- {requestRuleGroups.map((group, groupIndex) => (
-
{
- const updated = [...requestRuleGroups]
- updated[groupIndex] = next
- handleRuleGroupsChange(updated)
- }}
- onRemove={() =>
- handleRuleGroupsChange(
- requestRuleGroups.filter((_, i) => i !== groupIndex)
- )
- }
- />
- ))}
-
- handleRuleGroupsChange([
- ...requestRuleGroups,
- createEmptyRuleGroup(),
- ])
- }
- >
-
- {t('Add rule group')}
-
- >
- )}
-
+
)}
@@ -1245,3 +315,12 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
)
})
+
+export const TieredPricingEditor = memo(function TieredPricingEditor(
+ props: TieredPricingEditorProps
+) {
+ const identity =
+ props.modelIdentity ?? props.modelName ?? '__new-tiered-pricing-editor__'
+
+ return
+})
diff --git a/web/default/src/features/system-settings/models/tiered-pricing-request-rules.tsx b/web/default/src/features/system-settings/models/tiered-pricing-request-rules.tsx
new file mode 100644
index 00000000..29408fc2
--- /dev/null
+++ b/web/default/src/features/system-settings/models/tiered-pricing-request-rules.tsx
@@ -0,0 +1,524 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see
.
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { useId, useMemo } from 'react'
+import { Plus, Trash2 } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import {
+ COMMON_TIMEZONES,
+ MATCH_CONTAINS,
+ MATCH_EQ,
+ MATCH_EXISTS,
+ MATCH_GT,
+ MATCH_GTE,
+ MATCH_LT,
+ MATCH_LTE,
+ MATCH_RANGE,
+ SOURCE_HEADER,
+ SOURCE_PARAM,
+ SOURCE_TIME,
+ TIME_FUNCS,
+ createEmptyCondition,
+ createEmptyRuleGroup,
+ createEmptyTimeCondition,
+ getRequestRuleMatchOptions,
+ tryParseRequestRuleExpr,
+ type ParamHeaderCondition,
+ type RequestCondition,
+ type RequestRuleGroup,
+ type TimeCondition,
+ type TimeFunc,
+} from '@/features/pricing/lib/billing-expr'
+import { DraftNumberInput } from './tiered-pricing-fields'
+
+// ---------------------------------------------------------------------------
+// Request rule condition row
+// ---------------------------------------------------------------------------
+
+type RuleConditionRowProps = {
+ condition: RequestCondition
+ onChange: (next: RequestCondition) => void
+ onRemove: () => void
+}
+
+function RuleConditionRow({
+ condition,
+ onChange,
+ onRemove,
+}: RuleConditionRowProps) {
+ const { t } = useTranslation()
+ const matchOptions = getRequestRuleMatchOptions(condition.source)
+ const getMatchLabel = (mode: string) => {
+ switch (mode) {
+ case MATCH_EQ:
+ return t('Equals')
+ case MATCH_CONTAINS:
+ return t('Contains')
+ case MATCH_EXISTS:
+ return t('Exists')
+ case MATCH_GT:
+ return t('Greater than')
+ case MATCH_GTE:
+ return t('Greater than or equal')
+ case MATCH_LT:
+ return t('Less than')
+ case MATCH_LTE:
+ return t('Less than or equal')
+ case MATCH_RANGE:
+ return t('Overnight range')
+ default:
+ return mode
+ }
+ }
+ const getTimeFuncLabel = (timeFunc: TimeFunc) => {
+ switch (timeFunc) {
+ case 'hour':
+ return t('Hour of day')
+ case 'minute':
+ return t('Minute')
+ case 'weekday':
+ return t('Weekday')
+ case 'month':
+ return t('Month number')
+ case 'day':
+ return t('Day of month')
+ default:
+ return timeFunc
+ }
+ }
+ const sourceLabel =
+ condition.source === SOURCE_PARAM
+ ? t('Body param')
+ : condition.source === SOURCE_HEADER
+ ? t('Header')
+ : t('Time')
+
+ const handleSourceChange = (source: string) => {
+ if (source === SOURCE_TIME) {
+ onChange(createEmptyTimeCondition())
+ } else if (source === SOURCE_HEADER || source === SOURCE_PARAM) {
+ onChange({
+ ...createEmptyCondition(),
+ source: source as 'param' | 'header',
+ })
+ }
+ }
+
+ const handleModeChange = (mode: string) => {
+ onChange({ ...condition, mode } as RequestCondition)
+ }
+
+ const renderTimeCondition = (timeCond: TimeCondition) => (
+ <>
+
({
+ value: fn,
+ label: getTimeFuncLabel(fn),
+ })),
+ ]}
+ value={timeCond.timeFunc}
+ onValueChange={(value) =>
+ onChange({ ...timeCond, timeFunc: value as TimeFunc })
+ }
+ >
+
+ {getTimeFuncLabel(timeCond.timeFunc)}
+
+
+
+ {TIME_FUNCS.map((fn) => (
+
+ {getTimeFuncLabel(fn)}
+
+ ))}
+
+
+
+
({
+ value: tz.value,
+ label: tz.label,
+ })),
+ ]}
+ value={timeCond.timezone}
+ onValueChange={(value) =>
+ value !== null && onChange({ ...timeCond, timezone: value })
+ }
+ >
+
+
+ {COMMON_TIMEZONES.find((tz) => tz.value === timeCond.timezone)
+ ?.label ?? timeCond.timezone}
+
+
+
+
+ {COMMON_TIMEZONES.map((tz) => (
+
+ {tz.label}
+
+ ))}
+
+
+
+
({
+ value: option.value,
+ label: getMatchLabel(option.value),
+ })),
+ ]}
+ value={timeCond.mode}
+ onValueChange={(v) => v !== null && handleModeChange(v)}
+ >
+
+ {getMatchLabel(timeCond.mode)}
+
+
+
+ {matchOptions.map((option) => (
+
+ {getMatchLabel(option.value)}
+
+ ))}
+
+
+
+ {timeCond.mode === MATCH_RANGE ? (
+ <>
+
+ onChange({ ...timeCond, rangeStart: String(value) })
+ }
+ aria-label={t('Start')}
+ placeholder={t('Start')}
+ className='w-20'
+ />
+ ~
+
+ onChange({ ...timeCond, rangeEnd: String(value) })
+ }
+ aria-label={t('End')}
+ placeholder={t('End')}
+ className='w-20'
+ />
+ >
+ ) : (
+
+ onChange({ ...timeCond, value: String(value) })
+ }
+ aria-label={t('Condition Value')}
+ placeholder={t('Value')}
+ className='w-24'
+ />
+ )}
+ >
+ )
+
+ const renderParamHeaderCondition = (phCond: ParamHeaderCondition) => (
+ <>
+ onChange({ ...phCond, path: event.target.value })}
+ aria-label={t('Condition Path')}
+ placeholder={
+ phCond.source === SOURCE_HEADER ? 'X-Header-Name' : 'service_tier'
+ }
+ className='w-44'
+ />
+ ({
+ value: option.value,
+ label: getMatchLabel(option.value),
+ })),
+ ]}
+ value={phCond.mode}
+ onValueChange={(v) => v !== null && handleModeChange(v)}
+ >
+
+ {getMatchLabel(phCond.mode)}
+
+
+
+ {matchOptions.map((option) => (
+
+ {getMatchLabel(option.value)}
+
+ ))}
+
+
+
+ {phCond.mode !== MATCH_EXISTS && (
+
+ onChange({ ...phCond, value: event.target.value })
+ }
+ aria-label={t('Condition Value')}
+ placeholder={t('Value')}
+ className='w-44'
+ />
+ )}
+ >
+ )
+
+ return (
+
+ v !== null && handleSourceChange(v)}
+ >
+
+ {sourceLabel}
+
+
+
+ {t('Body param')}
+ {t('Header')}
+ {t('Time')}
+
+
+
+ {condition.source === SOURCE_TIME
+ ? renderTimeCondition(condition as TimeCondition)
+ : renderParamHeaderCondition(condition as ParamHeaderCondition)}
+
+
+
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Request rule group card
+// ---------------------------------------------------------------------------
+
+type RuleGroupCardProps = {
+ group: RequestRuleGroup
+ index: number
+ onChange: (next: RequestRuleGroup) => void
+ onRemove: () => void
+}
+
+function RuleGroupCard({
+ group,
+ index,
+ onChange,
+ onRemove,
+}: RuleGroupCardProps) {
+ const { t } = useTranslation()
+ const multiplierInputId = useId()
+
+ const handleConditionChange = (
+ conditionIndex: number,
+ next: RequestCondition
+ ) => {
+ const conditions = [...group.conditions]
+ conditions[conditionIndex] = next
+ onChange({ ...group, conditions })
+ }
+
+ const handleAddCondition = (timeMode: boolean) => {
+ onChange({
+ ...group,
+ conditions: [
+ ...group.conditions,
+ timeMode ? createEmptyTimeCondition() : createEmptyCondition(),
+ ],
+ })
+ }
+
+ return (
+
+
+
+ {t('Rule group')} #{index + 1}
+
+
+
+
+
+
+
+ {group.conditions.map((condition, conditionIndex) => (
+
handleConditionChange(conditionIndex, next)}
+ onRemove={() =>
+ onChange({
+ ...group,
+ conditions: group.conditions.filter(
+ (_, i) => i !== conditionIndex
+ ),
+ })
+ }
+ />
+ ))}
+
+
handleAddCondition(false)}
+ >
+
+ {t('Add param/header')}
+
+
handleAddCondition(true)}
+ >
+
+ {t('Add time condition')}
+
+
+
+
+
+
+ {t('Multiplier')}
+
+
+ onChange({ ...group, multiplier: String(value) })
+ }
+ className='w-32'
+ placeholder='1.0'
+ />
+
+ {t('Final cost = base × multiplier when conditions match')}
+
+
+
+ )
+}
+
+type RequestRuleEditorProps = {
+ requestRuleExpr: string
+ groups: RequestRuleGroup[]
+ onChange: (next: RequestRuleGroup[]) => void
+}
+
+export function RequestRuleEditor({
+ requestRuleExpr,
+ groups,
+ onChange,
+}: RequestRuleEditorProps) {
+ const { t } = useTranslation()
+ const canUseVisualRules = useMemo(() => {
+ if (!requestRuleExpr) return true
+ return tryParseRequestRuleExpr(requestRuleExpr) !== null
+ }, [requestRuleExpr])
+
+ return (
+
+
+
{t('Request rule pricing')}
+
+ {t(
+ 'When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.'
+ )}
+
+
+
+ {requestRuleExpr && !canUseVisualRules ? (
+
+
+ {t(
+ 'This expression is too complex for the visual editor. Please switch to expression mode to edit.'
+ )}
+
+
+ ) : (
+ <>
+ {groups.map((group, groupIndex) => (
+
{
+ const updated = [...groups]
+ updated[groupIndex] = next
+ onChange(updated)
+ }}
+ onRemove={() =>
+ onChange(groups.filter((_, index) => index !== groupIndex))
+ }
+ />
+ ))}
+ onChange([...groups, createEmptyRuleGroup()])}
+ >
+
+ {t('Add rule group')}
+
+ >
+ )}
+
+ )
+}
diff --git a/web/default/src/features/system-settings/models/tiered-pricing-visual-editor.tsx b/web/default/src/features/system-settings/models/tiered-pricing-visual-editor.tsx
new file mode 100644
index 00000000..635706a6
--- /dev/null
+++ b/web/default/src/features/system-settings/models/tiered-pricing-visual-editor.tsx
@@ -0,0 +1,597 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { useEffect, useId, useMemo, useState } from 'react'
+import { ChevronDown, Plus, Trash2 } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { cn } from '@/lib/utils'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { Textarea } from '@/components/ui/textarea'
+import { BILLING_EXTRA_VARS } from '@/features/pricing/lib/billing-expr'
+import {
+ CACHE_MODE_GENERIC,
+ CACHE_MODE_TIMED,
+ type CacheMode,
+ type TierConditionInput,
+ type VisualConfig,
+ type VisualTier,
+ getTierCacheMode,
+ normalizeVisualConfig,
+ normalizeVisualTier,
+} from '@/features/pricing/lib/tier-expr'
+import { DraftNumberInput } from './tiered-pricing-fields'
+import {
+ formatTokenHint,
+ priceToUnitCost,
+ unitCostToPrice,
+} from './tiered-pricing-utils'
+
+const PRICE_SUFFIX_KEY = '$/1M tokens'
+const CACHE_PRICE_VARS = BILLING_EXTRA_VARS.filter(
+ (variable) => variable.group === 'cache'
+)
+const MEDIA_PRICE_VARS = BILLING_EXTRA_VARS.filter(
+ (variable) => variable.group === 'media'
+)
+
+const CONDITION_INPUT_OPTIONS: {
+ value: TierConditionInput['var']
+ labelKey: string
+}[] = [
+ { value: 'len', labelKey: 'Full input length' },
+ { value: 'p', labelKey: 'Billable input tokens' },
+ { value: 'c', labelKey: 'Billable output tokens' },
+]
+const OPS: TierConditionInput['op'][] = ['<', '<=', '>', '>=']
+
+// ---------------------------------------------------------------------------
+// Tier condition row
+// ---------------------------------------------------------------------------
+
+type ConditionRowProps = {
+ condition: TierConditionInput
+ onChange: (next: TierConditionInput) => void
+ onRemove: () => void
+ removeDisabled?: boolean
+}
+
+function ConditionRow({
+ condition,
+ onChange,
+ onRemove,
+ removeDisabled,
+}: ConditionRowProps) {
+ const { t } = useTranslation()
+ const currentInputOption = CONDITION_INPUT_OPTIONS.find(
+ (option) => option.value === condition.var
+ )
+
+ return (
+
+ ({
+ value: option.value,
+ label: t(option.labelKey),
+ })),
+ ]}
+ value={condition.var}
+ onValueChange={(value) =>
+ onChange({ ...condition, var: value as TierConditionInput['var'] })
+ }
+ >
+
+
+ {currentInputOption
+ ? t(currentInputOption.labelKey)
+ : condition.var}
+
+
+
+
+ {CONDITION_INPUT_OPTIONS.map((option) => (
+
+ {t(option.labelKey)}
+
+ ))}
+
+
+
+ ({ value: op, label: op }))]}
+ value={condition.op}
+ onValueChange={(value) =>
+ onChange({ ...condition, op: value as TierConditionInput['op'] })
+ }
+ >
+
+
+
+
+
+ {OPS.map((op) => (
+
+ {op}
+
+ ))}
+
+
+
+ onChange({ ...condition, value })}
+ aria-label={t('Condition Value')}
+ placeholder={t('tokens')}
+ className='w-32'
+ />
+
+ {formatTokenHint(condition.value, t)}
+
+
+
+
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Price input field
+// ---------------------------------------------------------------------------
+
+type PriceFieldProps = {
+ label: string
+ hint?: string
+ value: number
+ onChange: (next: number) => void
+}
+
+function PriceField({ label, hint, value, onChange }: PriceFieldProps) {
+ const inputId = useId()
+ return (
+
+
+ {label}
+
+
+ {hint &&
{hint}
}
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Single tier card (visual editor)
+// ---------------------------------------------------------------------------
+
+type VisualTierCardProps = {
+ tier: VisualTier
+ index: number
+ total: number
+ onChange: (next: VisualTier) => void
+ onRemove: () => void
+ onAddCondition: () => void
+}
+
+function VisualTierCard({
+ tier,
+ index,
+ total,
+ onChange,
+ onRemove,
+ onAddCondition,
+}: VisualTierCardProps) {
+ const { t } = useTranslation()
+ const cacheMode = getTierCacheMode(tier)
+ const isFallbackTier = index === total - 1
+
+ const handleConditionChange = (
+ conditionIndex: number,
+ next: TierConditionInput
+ ) => {
+ const conditions = [...tier.conditions]
+ conditions[conditionIndex] = next
+ onChange({ ...tier, conditions })
+ }
+
+ const handleConditionRemove = (conditionIndex: number) => {
+ if (!isFallbackTier && tier.conditions.length <= 1) return
+ onChange({
+ ...tier,
+ conditions: tier.conditions.filter((_, i) => i !== conditionIndex),
+ })
+ }
+
+ const handlePriceChange = (field: keyof VisualTier, value: number) => {
+ onChange({ ...tier, [field]: value })
+ }
+
+ const handleCacheModeChange = (mode: CacheMode) => {
+ onChange({
+ ...tier,
+ cache_mode: mode,
+ cache_create_1h_unit_cost:
+ mode === CACHE_MODE_TIMED ? (tier.cache_create_1h_unit_cost ?? 0) : 0,
+ })
+ }
+
+ const inputUnitPrice = unitCostToPrice(tier.input_unit_cost)
+ const outputUnitPrice = unitCostToPrice(tier.output_unit_cost)
+ const hasMediaPricing = MEDIA_PRICE_VARS.some((variable) => {
+ const fieldKey = variable.tierField as keyof VisualTier
+ return unitCostToPrice((tier[fieldKey] as number | undefined) ?? 0) > 0
+ })
+ const [mediaOpen, setMediaOpen] = useState(hasMediaPricing)
+
+ useEffect(() => {
+ // Reveal newly configured media prices when controlled data changes.
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ if (hasMediaPricing) setMediaOpen(true)
+ }, [hasMediaPricing])
+
+ const renderPriceVariable = (
+ variable: (typeof BILLING_EXTRA_VARS)[number]
+ ) => {
+ const fieldKey = variable.tierField as keyof VisualTier
+ const value = unitCostToPrice((tier[fieldKey] as number | undefined) ?? 0)
+
+ return (
+ handlePriceChange(fieldKey, priceToUnitCost(next))}
+ />
+ )
+ }
+
+ return (
+
+
+
+
+ {t('Tier')} {index + 1} / {total}
+
+ {tier.conditions.length === 0 && (
+ {t('Fallback tier')}
+ )}
+
+ onChange({ ...tier, label: event.target.value })
+ }
+ aria-label={t('Tier name')}
+ placeholder={t('Tier name')}
+ className='h-7 w-36'
+ />
+
+
+
+
+
+
+ {/* Conditions */}
+
+
+
{t('Tier conditions')}
+
= 2}
+ className='h-7 px-2 text-xs'
+ >
+
+ {t('Add condition')}
+
+
+ {tier.conditions.length === 0 ? (
+
+ {t('Always matches (default tier).')}
+
+ ) : (
+ tier.conditions.map((condition, conditionIndex) => (
+
handleConditionChange(conditionIndex, next)}
+ onRemove={() => handleConditionRemove(conditionIndex)}
+ removeDisabled={!isFallbackTier && tier.conditions.length <= 1}
+ />
+ ))
+ )}
+
+
+
+
+ {t('Token prices')}
+
+ {t(PRICE_SUFFIX_KEY)}
+
+
+
+
+
+
+ handlePriceChange('input_unit_cost', priceToUnitCost(value))
+ }
+ />
+
+ handlePriceChange('output_unit_cost', priceToUnitCost(value))
+ }
+ />
+
+
+
+
+
+ value !== null && handleCacheModeChange(value as CacheMode)
+ }
+ >
+
+
+ {t('Generic cache')}
+
+
+ {t('Time-sliced cache (Claude)')}
+
+
+
+
+
+ {CACHE_PRICE_VARS.map((variable) => {
+ if (variable.key === 'cc1h' && cacheMode !== CACHE_MODE_TIMED) {
+ return null
+ }
+ return renderPriceVariable(variable)
+ })}
+
+
+
+
+
+ {/* Media prices */}
+
+
setMediaOpen((prev) => !prev)}
+ >
+
+ {t('Media pricing')}
+
+ {mediaOpen && (
+
+ {MEDIA_PRICE_VARS.map(renderPriceVariable)}
+
+ )}
+
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Visual editor (list of tiers)
+// ---------------------------------------------------------------------------
+
+type VisualEditorProps = {
+ visualConfig: VisualConfig | null
+ onChange: (next: VisualConfig) => void
+}
+
+export function VisualEditor({ visualConfig, onChange }: VisualEditorProps) {
+ const { t } = useTranslation()
+ const config = useMemo(
+ () => normalizeVisualConfig(visualConfig),
+ [visualConfig]
+ )
+
+ const handleTierChange = (index: number, next: VisualTier) => {
+ const tiers = [...config.tiers]
+ const lastIndex = tiers.length - 1
+ const normalized = normalizeVisualTier(
+ index === lastIndex ? { ...next, conditions: [] } : next
+ )
+ if (index < lastIndex && normalized.conditions.length === 0) return
+ tiers[index] = normalized
+ onChange({ ...config, tiers })
+ }
+
+ const handleAddTier = () => {
+ const tiers = [...config.tiers]
+ const lastIndex = tiers.length - 1
+ // When adding a new fallback, give the previous catch-all tier a default
+ // upper-bound condition so the expression compiles into a sane two-tier
+ // shape. Mirrors the legacy editor UX for adding tiers.
+ if (lastIndex >= 0 && tiers[lastIndex].conditions.length === 0) {
+ tiers[lastIndex] = normalizeVisualTier({
+ ...tiers[lastIndex],
+ conditions: [{ var: 'len', op: '<', value: 200000 }],
+ })
+ }
+ tiers.push(
+ normalizeVisualTier({
+ label: `tier_${tiers.length + 1}`,
+ conditions: [],
+ input_unit_cost: 0,
+ output_unit_cost: 0,
+ })
+ )
+ onChange({ ...config, tiers })
+ }
+
+ const handleRemoveTier = (index: number) => {
+ if (index === config.tiers.length - 1) return
+ const tiers = config.tiers.filter((_, i) => i !== index)
+ onChange({ ...config, tiers: tiers.length > 0 ? tiers : config.tiers })
+ }
+
+ const handleAddCondition = (index: number) => {
+ const tier = config.tiers[index]
+ if (index === config.tiers.length - 1) return
+ if (tier.conditions.length >= 2) return
+ // Prefer `len` (input length) over `p`/`c` for tier conditions because
+ // `p` is subject to auto-exclusion when sub-categories like `cr` are
+ // priced separately, which can misroute long-input requests into shorter
+ // tiers when cache-hits reduce the effective `p`.
+ const usedVars = new Set(tier.conditions.map((c) => c.var))
+ const nextVar: TierConditionInput['var'] = usedVars.has('len') ? 'c' : 'len'
+ onChange({
+ ...config,
+ tiers: config.tiers.map((current, i) =>
+ i === index
+ ? {
+ ...current,
+ conditions: [
+ ...tier.conditions,
+ { var: nextVar, op: '<', value: 200000 },
+ ],
+ }
+ : current
+ ),
+ })
+ }
+
+ return (
+
+
+ {t(
+ 'Each tier supports up to 2 conditions. The last tier without conditions is the fallback.'
+ )}
+
+ {config.tiers.map((tier, index) => (
+
handleTierChange(index, next)}
+ onRemove={() => handleRemoveTier(index)}
+ onAddCondition={() => handleAddCondition(index)}
+ />
+ ))}
+
+
+ {t('Add tier')}
+
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Raw expression editor
+// ---------------------------------------------------------------------------
+
+type RawExprEditorProps = {
+ exprString: string
+ onChange: (value: string) => void
+}
+
+export function RawExprEditor({ exprString, onChange }: RawExprEditorProps) {
+ const { t } = useTranslation()
+ return (
+
+
+
+
+ {t('Variables')}: len, p, c,{' '}
+ cr, cc, cc1h,{' '}
+ img, img_o, ai,{' '}
+ ao
+
+
+ {t('Functions')}: tier(name, value), max,{' '}
+ min, ceil, floor,{' '}
+ abs, header(name),{' '}
+ param(path), has(source, text)
+
+
+
+
+ )
+}
diff --git a/web/default/src/features/usage-logs/components/dialogs/fail-reason-dialog.tsx b/web/default/src/features/usage-logs/components/dialogs/fail-reason-dialog.tsx
index 974e212b..bb6603ac 100644
--- a/web/default/src/features/usage-logs/components/dialogs/fail-reason-dialog.tsx
+++ b/web/default/src/features/usage-logs/components/dialogs/fail-reason-dialog.tsx
@@ -74,7 +74,7 @@ export function FailReasonDialog({
)}
-
+
{failReason || '-'}
diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json
index 8f3c2cef..4af82cca 100644
--- a/web/default/src/i18n/locales/en.json
+++ b/web/default/src/i18n/locales/en.json
@@ -24,6 +24,7 @@
"{{category}} Models": "{{category}} Models",
"{{completed}}/{{total}} completed": "{{completed}}/{{total}} completed",
"{{count}} {{entity}} selected. Bulk actions toolbar is available.": "{{count}} {{entity}} selected. Bulk actions toolbar is available.",
+ "{{count}} blocking issue(s)": "{{count}} blocking issue(s)",
"{{count}} channel(s) deleted": "{{count}} channel(s) deleted",
"{{count}} channel(s) disabled": "{{count}} channel(s) disabled",
"{{count}} channel(s) enabled": "{{count}} channel(s) enabled",
@@ -49,6 +50,7 @@
"{{count}} tiered billing models enabled": "{{count}} tiered billing models enabled",
"{{count}} tiers": "{{count}} tiers",
"{{count}} vendors": "{{count}} vendors",
+ "{{count}} warning(s)": "{{count}} warning(s)",
"{{count}} weeks ago": "{{count}} weeks ago",
"{{field}} updated to {{value}}": "{{field}} updated to {{value}}",
"{{field}} updated to {{value}} for tag: {{tag}}": "{{field}} updated to {{value}} for tag: {{tag}}",
@@ -64,6 +66,7 @@
"{{routeContext}} groups must not be empty": "{{routeContext}} groups must not be empty",
"{{routeContext}} groups must not exceed 64 entries": "{{routeContext}} groups must not exceed 64 entries",
"{{success}} succeeded, {{failed}} failed": "{{success}} succeeded, {{failed}} failed",
+ "{{supported}}/{{total}} available": "{{supported}}/{{total}} available",
"{{target}} test failed": "{{target}} test failed",
"{{target}} test succeeded": "{{target}} test succeeded",
"{{title}} usage: {{percent}}%": "{{title}} usage: {{percent}}%",
@@ -81,10 +84,12 @@
"`, and `-nothinking` suffixes while routing to the correct Gemini variant.": "`, and `-nothinking` suffixes while routing to the correct Gemini variant.",
"© 2025 Your Company. All rights reserved.": "© 2025 Your Company. All rights reserved.",
"+{{count}} more": "+{{count}} more",
+ "+{{count}} more mapping(s)": "+{{count}} more mapping(s)",
"= {{count}} tokens": "= {{count}} tokens",
"= {{count}}K tokens": "= {{count}}K tokens",
"= {{count}}M tokens": "= {{count}}M tokens",
"| Based on": "| Based on",
+ "$/1M tokens": "$/1M tokens",
"0 means data is kept permanently": "0 means data is kept permanently",
"0 means unlimited": "0 means unlimited",
"0 means unlimited. The value is converted to quota units when saved.": "0 means unlimited. The value is converted to quota units when saved.",
@@ -242,6 +247,7 @@
"Additional Limit": "Additional Limit",
"Additional Limits": "Additional Limits",
"Additional metered capability": "Additional metered capability",
+ "Additional upstream configuration is required for this channel type.": "Additional upstream configuration is required for this channel type.",
"Adjust filters, then search to refresh the logs.": "Adjust filters, then search to refresh the logs.",
"Adjust Quota": "Adjust Quota",
"Adjust response formatting, prompt behavior, proxy, and upstream automation.": "Adjust response formatting, prompt behavior, proxy, and upstream automation.",
@@ -438,6 +444,7 @@
"API key from the provider": "API key from the provider",
"API key is loading, please try again in a moment": "API key is loading, please try again in a moment",
"API key is required": "API key is required",
+ "API Key is required when creating a channel.": "API Key is required when creating a channel.",
"API Key mode (does not support batch creation)": "API Key mode (does not support batch creation)",
"API Key mode: use APIKey|Region": "API Key mode: use APIKey|Region",
"API Key updated successfully": "API Key updated successfully",
@@ -505,9 +512,11 @@
"async task protocols": "async task protocols",
"Async task refund": "Async task refund",
"async task surfaces": "async task surfaces",
+ "At least one model must be published for this channel.": "At least one model must be published for this channel.",
"At least one model regex pattern is required": "At least one model regex pattern is required",
"At least one valid key source is required": "At least one valid key source is required",
"Attach": "Attach",
+ "audio": "Audio",
"Audio": "Audio",
"Audio comp.": "Audio comp.",
"Audio completion ratio": "Audio completion ratio",
@@ -640,8 +649,10 @@
"Base rate limit windows for this account.": "Base rate limit windows for this account.",
"Base URL": "Base URL",
"Base URL is required for this channel type": "Base URL is required for this channel type",
+ "Base URL is required for this channel type.": "Base URL is required for this channel type.",
"Base URL is required when a route upstream path is relative": "Base URL is required when a route upstream path is relative",
"Base URL of your Uptime Kuma instance": "Base URL of your Uptime Kuma instance",
+ "Base URL should not usually end with /v1 because MAX API appends the upstream path automatically.": "Base URL should not usually end with /v1 because MAX API appends the upstream path automatically.",
"Basic Authentication": "Basic Authentication",
"Basic Configuration": "Basic Configuration",
"Basic Info": "Basic Info",
@@ -779,6 +790,7 @@
"Cancelled at": "Cancelled at",
"Capabilities": "Capabilities",
"Capability checks": "Capability checks",
+ "Caps the response length": "Caps the response length",
"Capture a reusable bundle of models, tags, or endpoints.": "Capture a reusable bundle of models, tags, or endpoints.",
"Card view": "Card view",
"cards": "cards",
@@ -840,6 +852,7 @@
"Chat preset not found": "Chat preset not found",
"Chat Presets": "Chat Presets",
"Chat session management": "Chat session management",
+ "chat/completions": "Chat / completions",
"ChatCompletions -> Responses Compatibility": "ChatCompletions -> Responses Compatibility",
"Check for updates": "Check for updates",
"Check in daily to receive random quota rewards": "Check in daily to receive random quota rewards",
@@ -937,9 +950,12 @@
"Codex Account & Usage": "Codex Account & Usage",
"Codex Authorization": "Codex Authorization",
"Codex channels do not support batch creation": "Codex channels do not support batch creation",
+ "Codex channels do not support batch creation.": "Codex channels do not support batch creation.",
"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",
"Codex credential must be a JSON object with access_token and account_id": "Codex credential must be a JSON object with access_token and account_id",
+ "Codex credentials must be a valid JSON object.": "Codex credentials must be a valid JSON object.",
+ "Codex credentials must contain access_token and account_id.": "Codex credentials must contain access_token and account_id.",
"Cohere": "Cohere",
"Collapse": "Collapse",
"Collapse All": "Collapse All",
@@ -977,11 +993,13 @@
"Compatibility field for the default auto route. It is derived from the route config when saving.": "Compatibility field for the default auto route. It is derived from the route config when saving.",
"compatible API routes": "compatible API routes",
"Compatible API routes for common AI application workflows": "Compatible API routes for common AI application workflows",
+ "Compatible support": "Compatible support",
"Complete API documentation with multi-language SDK support": "Complete API documentation with multi-language SDK support",
"Complete Order": "Complete Order",
"Complete these steps to finish the initial installation.": "Complete these steps to finish the initial installation.",
"Completed": "Completed",
"Completion": "Completion",
+ "Completion delta settlement": "Completion delta settlement",
"Completion price": "Completion price",
"Completion price ($/1M tokens)": "Completion price ($/1M tokens)",
"Completion ratio": "Completion ratio",
@@ -998,6 +1016,7 @@
"Conditions (AND)": "Conditions (AND)",
"Confidence": "Confidence",
"Config validation": "Config validation",
+ "Configurable": "Configurable",
"Configuration": "Configuration",
"Configuration File": "Configuration File",
"Configuration for Creem payment integration": "Configuration for Creem payment integration",
@@ -1005,6 +1024,8 @@
"Configuration for Stripe payment integration": "Configuration for Stripe payment integration",
"Configuration key": "Configuration key",
"Configuration required": "Configuration required",
+ "Configuration valid": "Configuration valid",
+ "Configuration validation": "Configuration validation",
"Configure": "Configure",
"Configure a Creem product for user recharge options.": "Configure a Creem product for user recharge options.",
"Configure a custom ratio for when users use a specific token group.": "Configure a custom ratio for when users use a specific token group.",
@@ -1110,6 +1131,7 @@
"Control plane": "Control plane",
"Control which models are exposed and which groups may use them.": "Control which models are exposed and which groups may use them.",
"Controls how much the model thinks before answering": "Controls how much the model thinks before answering",
+ "Controls randomness and creativity": "Controls randomness and creativity",
"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
tag in content": "Convert reasoning_content to tag in content",
@@ -1593,11 +1615,13 @@
"Email Field": "Email Field",
"Email Verification": "Email Verification",
"Email, summarisation, knowledge work": "Email, summarisation, knowledge work",
+ "embeddings": "Embeddings",
"Embeddings": "Embeddings",
"Empty": "Empty",
"Empty Completion Retry": "Empty Completion Retry",
"Empty value will be saved as {}.": "Empty value will be saved as {}.",
"Enable": "Enable",
+ "Enable {{parameter}}": "Enable {{parameter}}",
"Enable 2FA": "Enable 2FA",
"Enable All": "Enable All",
"Enable and price audio input before configuring audio output.": "Enable and price audio input before configuring audio output.",
@@ -1647,6 +1671,7 @@
"Enabled Status": "Enabled Status",
"Enabling...": "Enabling...",
"Encourages introducing new topics": "Encourages introducing new topics",
+ "Encourages new topics": "Encourages new topics",
"End": "End",
"End Error": "End Error",
"End Reason": "End Reason",
@@ -1964,6 +1989,7 @@
"Fetch Models": "Fetch Models",
"Fetched {{count}} model(s) from upstream": "Fetched {{count}} model(s) from upstream",
"Fetched {{count}} models": "Fetched {{count}} models",
+ "Fetches model lists directly from the upstream provider.": "Fetches model lists directly from the upstream provider.",
"Fetching prefill groups...": "Fetching prefill groups...",
"Fetching upstream prices...": "Fetching upstream prices...",
"Fetching upstream ratios...": "Fetching upstream ratios...",
@@ -2070,6 +2096,7 @@
"Forward requests directly to upstream providers without any post-processing.": "Forward requests directly to upstream providers without any post-processing.",
"Frames per second": "Frames per second",
"Free: {{free}} / Total: {{total}}": "Free: {{free}} / Total: {{total}}",
+ "Frequency Penalty": "Frequency Penalty",
"Friendly name to identify this channel": "Friendly name to identify this channel",
"From Address": "From Address",
"From AI model access to Agent lifecycle governance": "From AI model access to Agent lifecycle governance",
@@ -2096,6 +2123,7 @@
"Gemini Generate Content": "Gemini Generate Content",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content to OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K",
+ "Gemini native": "Gemini native API",
"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",
@@ -2314,6 +2342,7 @@
"Image to Video": "Image to Video",
"Image Tokens": "Image Tokens",
"Image, video, music, polling, result proxying, status mapping, and refund handling.": "Image, video, music, polling, result proxying, status mapping, and refund handling.",
+ "images": "Images",
"Import to CC Switch": "Import to CC Switch",
"In Progress": "In Progress",
"In:": "In:",
@@ -2451,6 +2480,7 @@
"Keep the platform ready": "Keep the platform ready",
"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",
+ "Keeps compatible responses more repeatable": "Keeps compatible responses more repeatable",
"Key": "Key",
"Key custody": "Key custody",
"Key Fingerprint": "Key Fingerprint",
@@ -2522,6 +2552,8 @@
"Limit Reached": "Limit Reached",
"Limit which models can be used with this key": "Limit which models can be used with this key",
"Limited": "Limited",
+ "Limited support": "Limited support",
+ "Limits token selection to a probability mass": "Limits token selection to a probability mass",
"LingYiWanWu": "LingYiWanWu",
"Link to your documentation site": "Link to your documentation site",
"LinuxDO": "LinuxDO",
@@ -2641,6 +2673,7 @@
"Max Success": "Max Success",
"Max successful requests": "Max successful requests",
"Max Successful Requests": "Max Successful Requests",
+ "Max Tokens": "Max Tokens",
"MAX-API-Next": "MAX-API-Next",
"Maximum 1000 characters. Supports Markdown and HTML.": "Maximum 1000 characters. Supports Markdown and HTML.",
"Maximum 200 characters": "Maximum 200 characters",
@@ -2717,6 +2750,7 @@
"Model Description": "Model Description",
"Model details": "Model details",
"Model disabled successfully": "Model disabled successfully",
+ "model discovery": "Model discovery",
"Model enabled successfully": "Model enabled successfully",
"Model fixed pricing": "Model fixed pricing",
"Model governance": "Model governance",
@@ -2861,6 +2895,7 @@
"name@example.com": "name@example.com",
"Native format": "Native format",
"Native forwarding": "Native forwarding",
+ "Native support": "Native support",
"Navigation link": "Navigation link",
"Navigation name": "Navigation name",
"Need a redemption code?": "Need a redemption code?",
@@ -2912,6 +2947,7 @@
"No backup": "No backup",
"No base input price": "No base input price",
"No billing records found": "No billing records found",
+ "No blocking configuration issues were detected for this channel type.": "No blocking configuration issues were detected for this channel type.",
"No capabilities reported for this model.": "No capabilities reported for this model.",
"No Change": "No Change",
"No changes": "No changes",
@@ -3066,6 +3102,7 @@
"Not set yet": "Not set yet",
"Not Started": "Not Started",
"Not Submitted": "Not Submitted",
+ "Not supported": "Not supported",
"Not tested": "Not tested",
"Not used for upstream training by default": "Not used for upstream training by default",
"Not used yet": "Not used yet",
@@ -3135,6 +3172,7 @@
"Only allow specific email domains": "Only allow specific email domains",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "Only configured combinations are overridden. All other calls keep the token group base ratio.",
+ "Only enabled parameters are sent with the request.": "Only enabled parameters are sent with the request.",
"Only override upstream submit/query paths and keep the official response parser": "Only override upstream submit/query paths and keep the official response parser",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.",
"Only successful requests": "Only successful requests",
@@ -3266,6 +3304,7 @@
"Parameter override template must be a JSON object": "Parameter override template must be a JSON object",
"Parameter pricing": "Parameter pricing",
"Parameter pricing rules": "Parameter pricing rules",
+ "Parameter settings": "Parameter settings",
"parameter.": "parameter.",
"Parameters": "Parameters",
"Parsed {{count}} service account file(s)": "Parsed {{count}} service account file(s)",
@@ -3483,6 +3522,7 @@
"Prepend": "Prepend",
"Prepend to Start": "Prepend to Start",
"Prepend value to array / string / object start": "Prepend value to array / string / object start",
+ "Presence Penalty": "Presence Penalty",
"Preserve the original field when applying this rule": "Preserve the original field when applying this rule",
"Preset groups": "Preset groups",
"Preset recharge amounts (JSON array)": "Preset recharge amounts (JSON array)",
@@ -3683,6 +3723,7 @@
"Reason:": "Reason:",
"Reasoning": "Reasoning",
"Reasoning Effort": "Reasoning Effort",
+ "Recalculate successful DoubaoVideo tasks from upstream usage tokens when polling completes; turn this off only for emergency containment because successful tasks will keep the pre-consumed quota.": "Recalculate successful DoubaoVideo tasks from upstream usage tokens when polling completes; turn this off only for emergency containment because successful tasks will keep the pre-consumed quota.",
"Receive Upstream Model Update Notifications": "Receive Upstream Model Update Notifications",
"Received": "Received",
"Received amount": "Received amount",
@@ -3724,6 +3765,7 @@
"Redirecting to GitHub...": "Redirecting to GitHub...",
"Redirecting to payment page...": "Redirecting to payment page...",
"Redirecting to sign in in {{seconds}} seconds.": "Redirecting to sign in in {{seconds}} seconds.",
+ "Reduces repeated wording": "Reduces repeated wording",
"Reference Video": "Reference Video",
"Referral link:": "Referral link:",
"Referral Program": "Referral Program",
@@ -3848,6 +3890,7 @@
"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.",
"Requires Record quota usage to be enabled in Operations > Log Maintenance.": "Requires Record quota usage to be enabled in Operations > Log Maintenance.",
+ "rerank": "Rerank",
"Rerank": "Rerank",
"Reroll": "Reroll",
"Research assistant": "Research assistant",
@@ -3893,6 +3936,7 @@
"Response path for upstream task ID": "Response path for upstream task ID",
"Response Time": "Response Time",
"Response time: {{duration}}": "Response time: {{duration}}",
+ "responses": "Responses API",
"Responses API Version": "Responses API Version",
"Responses APIs": "Responses APIs",
"Restore defaults": "Restore defaults",
@@ -3947,6 +3991,15 @@
"Route, auth, and balance check in one place": "Route, auth, and balance check in one place",
"route: weighted channel": "route: weighted channel",
"Routes": "Routes",
+ "Routes Anthropic Messages-compatible requests.": "Routes Anthropic Messages-compatible requests.",
+ "Routes asynchronous video generation tasks.": "Routes asynchronous video generation tasks.",
+ "Routes embedding vector requests.": "Routes embedding vector requests.",
+ "Routes Gemini native /v1beta model requests.": "Routes Gemini native /v1beta model requests.",
+ "Routes image generation and editing requests.": "Routes image generation and editing requests.",
+ "Routes OpenAI Responses API requests.": "Routes OpenAI Responses API requests.",
+ "Routes OpenAI-compatible chat and completion requests.": "Routes OpenAI-compatible chat and completion requests.",
+ "Routes search reranking requests.": "Routes search reranking requests.",
+ "Routes speech, transcription, translation, or music requests.": "Routes speech, transcription, translation, or music requests.",
"Routing": "Routing",
"Routing & Overrides": "Routing & Overrides",
"Routing and reliability governance": "Routing and reliability governance",
@@ -4083,6 +4136,7 @@
"Security boundary": "Security boundary",
"Security Check": "Security Check",
"Security verification": "Security verification",
+ "Seed": "Seed",
"Seedance": "Seedance",
"Select": "Select",
"Select {{item}}": "Select {{item}}",
@@ -4241,6 +4295,7 @@
"Showcase core capabilities with demo credentials and limited access.": "Showcase core capabilities with demo credentials and limited access.",
"Showing": "Showing",
"showing •": "showing •",
+ "Shows the request APIs, task capabilities, and operational features supported by this channel type.": "Shows the request APIs, task capabilities, and operational features supported by this channel type.",
"Sidebar": "Sidebar",
"Sidebar collapsed by default for new users": "Sidebar collapsed by default for new users",
"Sidebar modules": "Sidebar modules",
@@ -4335,6 +4390,7 @@
"Status Page Slug": "Status Page Slug",
"Status Path": "Status Path",
"Status polling": "Status polling",
+ "Status response path is required when the full task protocol is enabled.": "Status response path is required when the full task protocol is enabled.",
"Status short": "Status",
"Status:": "Status:",
"Stay": "Stay",
@@ -4482,6 +4538,7 @@
"Task History": "Task History",
"Task ID": "Task ID",
"Task ID Path": "Task ID Path",
+ "Task ID response path is required when the full task protocol is enabled.": "Task ID response path is required when the full task protocol is enabled.",
"Task ID:": "Task ID:",
"Task logs": "Task logs",
"Task Logs": "Task Logs",
@@ -4501,6 +4558,7 @@
"Telegram": "Telegram",
"Telegram login requires widget integration; coming soon": "Telegram login requires widget integration; coming soon",
"Telegram Login Widget": "Telegram Login Widget",
+ "Temperature": "Temperature",
"Template": "Template",
"Template variables:": "Template variables:",
"Templates": "Templates",
@@ -4587,6 +4645,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "This expression is too complex for the visual editor. Please switch to expression mode to edit.",
"This feature is experimental. Configuration format and behavior may change.": "This feature is experimental. Configuration format and behavior may change.",
"This feature requires server-side WeChat configuration": "This feature requires server-side WeChat configuration",
+ "This JSON configuration must be a JSON object.": "This JSON configuration must be a JSON object.",
"This may cause cache failures.": "This may cause cache failures.",
"This may take a few moments while we validate the request and update your session.": "This may take a few moments while we validate the request and update your session.",
"This model has both fixed price and ratio billing conflicts": "This model has both fixed price and ratio billing conflicts",
@@ -4723,6 +4782,7 @@
"Top models": "Top models",
"Top Models": "Top Models",
"Top models by traffic": "Top models by traffic",
+ "Top P": "Top P",
"Top up balance and view billing history.": "Top up balance and view billing history.",
"Top Users": "Top Users",
"Top vendors": "Top vendors",
@@ -4896,6 +4956,7 @@
"upstream AI ecosystems": "upstream AI ecosystems",
"Upstream model and multimodal platforms": "Upstream model and multimodal platforms",
"Upstream Model Detection Settings": "Upstream Model Detection Settings",
+ "Upstream model discovery is not supported for this channel type and will not run.": "Upstream model discovery is not supported for this channel type and will not run.",
"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",
@@ -5048,15 +5109,25 @@
"Version Overrides": "Version Overrides",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Vertex AI API Key mode does not support batch creation",
+ "Vertex AI API Key mode does not support batch creation.": "Vertex AI API Key mode does not support batch creation.",
"Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.": "Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.",
"Vertex AI Key Format": "Vertex AI Key Format",
+ "Vertex AI region configuration must be a JSON object containing a default field.": "Vertex AI region configuration must be a JSON object containing a default field.",
+ "Vertex AI region configuration must contain a default field.": "Vertex AI region configuration must contain a default field.",
+ "Vertex AI service account credentials must be valid JSON.": "Vertex AI service account credentials must be valid JSON.",
"Vertex AI service account key must be valid JSON": "Vertex AI service account key must be valid JSON",
"Video": "Video",
"Video length in seconds": "Video length in seconds",
"Video Remix": "Video Remix",
"video task adapters, status mappings, and protocol paths": "video task adapters, status mappings, and protocol paths",
+ "Video Task Billing": "Video Task Billing",
"video task channels and protocol templates": "video task channels and protocol templates",
"Video Task Protocol": "Video Task Protocol",
+ "Video task protocol settings are only available for channel types that support video tasks.": "Video task protocol settings are only available for channel types that support video tasks.",
+ "Video task query path is required.": "Video task query path is required.",
+ "Video task query path must contain {task_id}, {operation_name}, or {upstream_task_id}.": "Video task query path must contain {task_id}, {operation_name}, or {upstream_task_id}.",
+ "Video task submission path is required.": "Video task submission path is required.",
+ "video tasks": "Video tasks",
"Vidu": "Vidu",
"View": "View",
"View all currently available models": "View all currently available models",
@@ -5188,6 +5259,7 @@
"Wire encoding for the embedding vectors": "Wire encoding for the embedding vectors",
"with conflicts": "with conflicts",
"with the API key from your token settings.": "with the API key from your token settings.",
+ "Without a result URL path, completed video tasks may not return a content URL.": "Without a result URL path, completed video tasks may not return a content URL.",
"Without additional conditions, only the type above is used for pruning.": "Without additional conditions, only the type above is used for pruning.",
"Worker": "Worker",
"Worker Access Key": "Worker Access Key",
diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json
index 11cd94e2..232c1852 100644
--- a/web/default/src/i18n/locales/fr.json
+++ b/web/default/src/i18n/locales/fr.json
@@ -24,6 +24,7 @@
"{{category}} Models": "Modèles {{category}}",
"{{completed}}/{{total}} completed": "{{completed}}/{{total}} terminé(s)",
"{{count}} {{entity}} selected. Bulk actions toolbar is available.": "{{count}} {{entity}} sélectionné(s). La barre d’actions groupées est disponible.",
+ "{{count}} blocking issue(s)": "{{count}} problème(s) bloquant(s)",
"{{count}} channel(s) deleted": "{{count}} canal(canaux) supprimé(s)",
"{{count}} channel(s) disabled": "{{count}} canal(canaux) désactivé(s)",
"{{count}} channel(s) enabled": "{{count}} canal(canaux) activé(s)",
@@ -49,6 +50,7 @@
"{{count}} tiered billing models enabled": "Facturation par paliers activée pour {{count}} modèles",
"{{count}} tiers": "{{count}} paliers",
"{{count}} vendors": "{{count}} fournisseurs",
+ "{{count}} warning(s)": "{{count}} avertissement(s)",
"{{count}} weeks ago": "il y a {{count}} semaines",
"{{field}} updated to {{value}}": "{{field}} mis à jour en {{value}}",
"{{field}} updated to {{value}} for tag: {{tag}}": "{{field}} mis à jour en {{value}} pour le tag : {{tag}}",
@@ -64,6 +66,7 @@
"{{routeContext}} groups must not be empty": "Les groupes {{routeContext}} ne doivent pas être vides",
"{{routeContext}} groups must not exceed 64 entries": "Les groupes {{routeContext}} ne doivent pas dépasser 64 entrées",
"{{success}} succeeded, {{failed}} failed": "{{success}} réussis, {{failed}} échoués",
+ "{{supported}}/{{total}} available": "{{supported}}/{{total}} disponibles",
"{{target}} test failed": "Échec du test de {{target}}",
"{{target}} test succeeded": "Test de {{target}} réussi",
"{{title}} usage: {{percent}}%": "Utilisation de {{title}} : {{percent}} %",
@@ -81,10 +84,12 @@
"`, and `-nothinking` suffixes while routing to the correct Gemini variant.": "`, et les suffixes `-nothinking` lors du routage vers la variante Gemini correcte.",
"© 2025 Your Company. All rights reserved.": "© 2025 Votre entreprise. Tous droits réservés.",
"+{{count}} more": "+{{count}} de plus",
+ "+{{count}} more mapping(s)": "+{{count}} mappage(s) en plus",
"= {{count}} tokens": "= {{count}} jetons",
"= {{count}}K tokens": "= {{count}} milliers de jetons",
"= {{count}}M tokens": "= {{count}} millions de jetons",
"| Based on": "| Basé sur",
+ "$/1M tokens": "$/1M jetons",
"0 means data is kept permanently": "0 signifie que les données sont conservées indéfiniment",
"0 means unlimited": "0 signifie illimité",
"0 means unlimited. The value is converted to quota units when saved.": "0 signifie illimité. La valeur est convertie en unités de quota à l’enregistrement.",
@@ -242,6 +247,7 @@
"Additional Limit": "Limite supplémentaire",
"Additional Limits": "Limites supplémentaires",
"Additional metered capability": "Fonctionnalité supplémentaire facturée à l’usage",
+ "Additional upstream configuration is required for this channel type.": "Une configuration en amont supplémentaire est obligatoire pour ce type de canal.",
"Adjust filters, then search to refresh the logs.": "Ajustez les filtres, puis lancez la recherche pour actualiser les journaux.",
"Adjust Quota": "Ajuster le quota",
"Adjust response formatting, prompt behavior, proxy, and upstream automation.": "Ajustez le formatage des réponses, le comportement des prompts, le proxy et l’automatisation amont.",
@@ -438,6 +444,7 @@
"API key from the provider": "Clé API du fournisseur",
"API key is loading, please try again in a moment": "La clé API est en cours de chargement, veuillez réessayer dans un instant",
"API key is required": "La clé API est requise",
+ "API Key is required when creating a channel.": "La clé API est obligatoire lors de la création d'un canal.",
"API Key mode (does not support batch creation)": "Mode clé API (ne prend pas en charge la création par lots)",
"API Key mode: use APIKey|Region": "Mode clé API : utiliser APIKey|Region",
"API Key updated successfully": "Clé API mise à jour avec succès",
@@ -505,9 +512,11 @@
"async task protocols": "protocoles de tâches asynchrones",
"Async task refund": "Remboursement de tâche asynchrone",
"async task surfaces": "surfaces de tâches asynchrones",
+ "At least one model must be published for this channel.": "Au moins un modèle doit être publié pour ce canal.",
"At least one model regex pattern is required": "Au moins un modèle de regex est requis",
"At least one valid key source is required": "Au moins une source de clé valide est requise",
"Attach": "Joindre",
+ "audio": "Audio",
"Audio": "Audio",
"Audio comp.": "Comp. audio",
"Audio completion ratio": "Ratio de complétion audio",
@@ -640,8 +649,10 @@
"Base rate limit windows for this account.": "Fenêtres de limitation de débit de base pour ce compte.",
"Base URL": "URL de base",
"Base URL is required for this channel type": "L'URL de base est requise pour ce type de canal",
+ "Base URL is required for this channel type.": "L'URL de base est obligatoire pour ce type de canal.",
"Base URL is required when a route upstream path is relative": "La Base URL est requise lorsqu’un chemin upstream de route est relatif",
"Base URL of your Uptime Kuma instance": "URL de base de votre instance Uptime Kuma",
+ "Base URL should not usually end with /v1 because MAX API appends the upstream path automatically.": "L'URL de base ne doit généralement pas se terminer par /v1, car MAX API ajoute automatiquement le chemin en amont.",
"Basic Authentication": "Authentification de base",
"Basic Configuration": "Configuration de base",
"Basic Info": "Informations de base",
@@ -779,6 +790,7 @@
"Cancelled at": "Annulé le",
"Capabilities": "Capacités",
"Capability checks": "Contrôles de capacité",
+ "Caps the response length": "Limite la longueur de la réponse",
"Capture a reusable bundle of models, tags, or endpoints.": "Capturez un ensemble réutilisable de modèles, d'étiquettes ou de points de terminaison.",
"Card view": "Vue cartes",
"cards": "cartes",
@@ -840,6 +852,7 @@
"Chat preset not found": "Préréglage de chat introuvable",
"Chat Presets": "Préréglages de chat",
"Chat session management": "Gestion des sessions de chat",
+ "chat/completions": "Chat / complétions",
"ChatCompletions -> Responses Compatibility": "Compatibilité ChatCompletions -> Réponses",
"Check for updates": "Vérifier les mises à jour",
"Check in daily to receive random quota rewards": "Connectez-vous quotidiennement pour recevoir des récompenses de quota aléatoires",
@@ -937,9 +950,12 @@
"Codex Account & Usage": "Compte et utilisation Codex",
"Codex Authorization": "Autorisation Codex",
"Codex channels do not support batch creation": "Les canaux Codex ne prennent pas en charge la création par lot",
+ "Codex channels do not support batch creation.": "Les canaux Codex ne prennent pas en charge la création par lots.",
"Codex channels use an OAuth JSON credential as the key.": "Les canaux Codex utilisent un identifiant OAuth JSON comme clé.",
"Codex CLI Header Passthrough": "Passthrough en-tête Codex CLI",
"Codex credential must be a JSON object with access_token and account_id": "L'identifiant Codex doit être un objet JSON avec access_token et account_id",
+ "Codex credentials must be a valid JSON object.": "Les identifiants Codex doivent être un objet JSON valide.",
+ "Codex credentials must contain access_token and account_id.": "Les identifiants Codex doivent contenir access_token et account_id.",
"Cohere": "Cohere",
"Collapse": "Réduire",
"Collapse All": "Tout réduire",
@@ -977,11 +993,13 @@
"Compatibility field for the default auto route. It is derived from the route config when saving.": "Champ de compatibilité pour la route auto par défaut. Il est dérivé de la configuration des routes lors de l'enregistrement.",
"compatible API routes": "routes API compatibles",
"Compatible API routes for common AI application workflows": "Routes API compatibles pour les workflows courants des applications d'IA",
+ "Compatible support": "Prise en charge compatible",
"Complete API documentation with multi-language SDK support": "Documentation API complète avec support SDK multilingue",
"Complete Order": "Compléter la commande",
"Complete these steps to finish the initial installation.": "Suivez ces étapes pour terminer l'installation initiale.",
"Completed": "Terminé",
"Completion": "Achèvement",
+ "Completion delta settlement": "Règlement du delta à la fin",
"Completion price": "Prix de complétion",
"Completion price ($/1M tokens)": "Prix de la complétion (USD/1M jetons)",
"Completion ratio": "Ratio de complétion",
@@ -998,6 +1016,7 @@
"Conditions (AND)": "Conditions (ET)",
"Confidence": "Confiance",
"Config validation": "Validation de configuration",
+ "Configurable": "Configurable",
"Configuration": "Configuration",
"Configuration File": "Fichier de configuration",
"Configuration for Creem payment integration": "Configuration pour l'intégration de paiement Creem",
@@ -1005,6 +1024,8 @@
"Configuration for Stripe payment integration": "Configuration pour l'intégration de paiement Stripe",
"Configuration key": "Clé de configuration",
"Configuration required": "Configuration requise",
+ "Configuration valid": "Configuration valide",
+ "Configuration validation": "Validation de la configuration",
"Configure": "Configurer",
"Configure a Creem product for user recharge options.": "Configurez un produit Creem pour les options de recharge utilisateur.",
"Configure a custom ratio for when users use a specific token group.": "Configurer un ratio personnalisé lorsque les utilisateurs utilisent un groupe de jetons spécifique.",
@@ -1110,6 +1131,7 @@
"Control plane": "Plan de contrôle",
"Control which models are exposed and which groups may use them.": "Contrôlez les modèles exposés et les groupes autorisés à les utiliser.",
"Controls how much the model thinks before answering": "Contrôle la quantité de raisonnement avant la réponse",
+ "Controls randomness and creativity": "Contrôle l'aléatoire et la créativité",
"Controls whether user verification (biometrics/PIN) is required during Passkey flows.": "Contrôle si la vérification de l'utilisateur (biométrie/PIN) est requise lors des flux de Passkey.",
"Conversion rate from USD to your custom currency": "Taux de conversion de l'USD vers votre devise personnalisée",
"Convert reasoning_content to tag in content": "Convertir reasoning_content en balise dans content",
@@ -1593,11 +1615,13 @@
"Email Field": "Champ d'e-mail",
"Email Verification": "Vérification d'e-mail",
"Email, summarisation, knowledge work": "Email, résumé, travail intellectuel",
+ "embeddings": "Vectorisation",
"Embeddings": "Embeddings",
"Empty": "Vide",
"Empty Completion Retry": "Relance pour complétion vide",
"Empty value will be saved as {}.": "Une valeur vide sera enregistrée comme {}.",
"Enable": "Activer",
+ "Enable {{parameter}}": "Activer {{parameter}}",
"Enable 2FA": "Activer 2FA",
"Enable All": "Tout activer",
"Enable and price audio input before configuring audio output.": "Activez et tarifez l’entrée audio avant de configurer la sortie audio.",
@@ -1647,6 +1671,7 @@
"Enabled Status": "Statut activé",
"Enabling...": "Activation en cours...",
"Encourages introducing new topics": "Encourage l'introduction de nouveaux sujets",
+ "Encourages new topics": "Encourage de nouveaux sujets",
"End": "End",
"End Error": "Erreur finale",
"End Reason": "Raison de fin",
@@ -1964,6 +1989,7 @@
"Fetch Models": "Récupérer les modèles",
"Fetched {{count}} model(s) from upstream": "{{count}} modèle(s) récupéré(s) depuis l'amont",
"Fetched {{count}} models": "{{count}} modèles récupérés",
+ "Fetches model lists directly from the upstream provider.": "Récupère les listes de modèles directement auprès du fournisseur en amont.",
"Fetching prefill groups...": "Récupération des groupes de préremplissage...",
"Fetching upstream prices...": "Récupération des prix amont...",
"Fetching upstream ratios...": "Récupération des ratios amont...",
@@ -2070,6 +2096,7 @@
"Forward requests directly to upstream providers without any post-processing.": "Transférer les requêtes directement aux fournisseurs amont sans aucun post-traitement.",
"Frames per second": "Images par seconde",
"Free: {{free}} / Total: {{total}}": "Disponible : {{free}} / Total : {{total}}",
+ "Frequency Penalty": "Pénalité de fréquence",
"Friendly name to identify this channel": "Nom convivial pour identifier ce canal",
"From Address": "De l'adresse",
"From AI model access to Agent lifecycle governance": "De l’accès aux modèles d’IA à la gouvernance du cycle de vie des Agents",
@@ -2096,6 +2123,7 @@
"Gemini Generate Content": "Gemini Generate Content",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content vers OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K",
+ "Gemini native": "API native Gemini",
"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 continuera à détecter automatiquement le mode de pensée même avec l'adaptateur désactivé. Activez ceci uniquement lorsque vous avez besoin d'un contrôle plus fin sur la tarification et le budget.",
"General": "Général",
"General Settings": "Paramètres généraux",
@@ -2314,6 +2342,7 @@
"Image to Video": "Image vers vidéo",
"Image Tokens": "Tokens image",
"Image, video, music, polling, result proxying, status mapping, and refund handling.": "Image, vidéo, musique, polling, proxy des résultats, mappage d'état et gestion des remboursements.",
+ "images": "Images",
"Import to CC Switch": "Importer vers CC Switch",
"In Progress": "En cours",
"In:": "Entrée :",
@@ -2451,6 +2480,7 @@
"Keep the platform ready": "Gardez la plateforme prête",
"Keep this above 1 minute to avoid heavy database load": "Gardez cette valeur au-dessus de 1 minute pour éviter une charge excessive de la base de données",
"Keep-alive Ping": "Ping de maintien de connexion",
+ "Keeps compatible responses more repeatable": "Rend les réponses compatibles plus répétables",
"Key": "Clé",
"Key custody": "Garde des clés",
"Key Fingerprint": "Empreinte de clé",
@@ -2522,6 +2552,8 @@
"Limit Reached": "Limite atteinte",
"Limit which models can be used with this key": "Limiter les modèles pouvant être utilisés avec cette clé",
"Limited": "Limité",
+ "Limited support": "Prise en charge limitée",
+ "Limits token selection to a probability mass": "Limite la sélection des tokens à une masse de probabilité",
"LingYiWanWu": "LingYiWanWu",
"Link to your documentation site": "Lien vers votre site de documentation",
"LinuxDO": "LinuxDO",
@@ -2641,6 +2673,7 @@
"Max Success": "Max Succès",
"Max successful requests": "Nombre max de requêtes réussies",
"Max Successful Requests": "Max Requêtes réussies",
+ "Max Tokens": "Nombre maximal de tokens",
"MAX-API-Next": "MAX-API-Next",
"Maximum 1000 characters. Supports Markdown and HTML.": "Maximum 1000 caractères. Prend en charge Markdown et HTML.",
"Maximum 200 characters": "Maximum 200 caractères",
@@ -2717,6 +2750,7 @@
"Model Description": "Description du modèle",
"Model details": "Détails du modèle",
"Model disabled successfully": "Modèle désactivé avec succès",
+ "model discovery": "Découverte des modèles",
"Model enabled successfully": "Modèle activé avec succès",
"Model fixed pricing": "Tarification fixe du modèle",
"Model governance": "Model governance",
@@ -2861,6 +2895,7 @@
"name@example.com": "name@example.com",
"Native format": "Format natif",
"Native forwarding": "Transfert natif",
+ "Native support": "Prise en charge native",
"Navigation link": "Lien de navigation",
"Navigation name": "Nom de navigation",
"Need a redemption code?": "Besoin d'un code d'échange ?",
@@ -2912,6 +2947,7 @@
"No backup": "Pas de sauvegarde",
"No base input price": "Aucun prix d’entrée de base",
"No billing records found": "Aucun enregistrement de facturation trouvé",
+ "No blocking configuration issues were detected for this channel type.": "Aucun problème de configuration bloquant n'a été détecté pour ce type de canal.",
"No capabilities reported for this model.": "Aucune capacité n'a été signalée pour ce modèle.",
"No Change": "Aucun changement",
"No changes": "Aucune modification",
@@ -3066,6 +3102,7 @@
"Not set yet": "Non défini",
"Not Started": "Non démarré",
"Not Submitted": "Non soumis",
+ "Not supported": "Non pris en charge",
"Not tested": "Non testé",
"Not used for upstream training by default": "Non utilisé pour l'entraînement amont par défaut",
"Not used yet": "Pas encore utilisé",
@@ -3135,6 +3172,7 @@
"Only allow specific email domains": "Autoriser uniquement des domaines d'e-mail spécifiques",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Uniquement disponible pour les administrateurs. Lorsque cette option est activée, vous recevrez une notification récapitulative via votre méthode sélectionnée lorsque la vérification planifiée des modèles détecte des changements de modèles en amont ou des échecs de vérification.",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "Seules les combinaisons configurées sont remplacées. Les autres appels conservent le ratio de base du groupe du jeton.",
+ "Only enabled parameters are sent with the request.": "Seuls les paramètres activés sont envoyés avec la requête.",
"Only override upstream submit/query paths and keep the official response parser": "Remplacer uniquement les chemins d'envoi/requête amont et conserver l'analyseur de réponse officiel",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Seuls les champs sélectionnés seront écrasés. Vous pouvez relancer l'assistant de synchronisation si de nouveaux conflits apparaissent.",
"Only successful requests": "Uniquement les requêtes réussies",
@@ -3266,6 +3304,7 @@
"Parameter override template must be a JSON object": "Le modèle de remplacement de paramètres doit être un objet JSON",
"Parameter pricing": "Tarification des paramètres",
"Parameter pricing rules": "Règles de tarification des paramètres",
+ "Parameter settings": "Paramètres",
"parameter.": "paramètre.",
"Parameters": "Paramètres",
"Parsed {{count}} service account file(s)": "{{count}} fichier(s) de compte de service analysé(s)",
@@ -3483,6 +3522,7 @@
"Prepend": "Ajouter au début",
"Prepend to Start": "Ajouter au début",
"Prepend value to array / string / object start": "Ajouter la valeur au début du tableau / chaîne / objet",
+ "Presence Penalty": "Pénalité de présence",
"Preserve the original field when applying this rule": "Conserver le champ original lors de l’application de cette règle",
"Preset groups": "Groupes prédéfinis",
"Preset recharge amounts (JSON array)": "Montants de recharge prédéfinis (tableau JSON)",
@@ -3683,6 +3723,7 @@
"Reason:": "Raison :",
"Reasoning": "Raisonnement",
"Reasoning Effort": "Effort de raisonnement",
+ "Recalculate successful DoubaoVideo tasks from upstream usage tokens when polling completes; turn this off only for emergency containment because successful tasks will keep the pre-consumed quota.": "Recalcule les tâches DoubaoVideo réussies à partir des tokens usage en amont lorsque le polling se termine ; désactivez cette option uniquement en urgence, car les tâches réussies conserveront le quota préconsommé.",
"Receive Upstream Model Update Notifications": "Recevoir les notifications de mise à jour des modèles en amont",
"Received": "Reçu",
"Received amount": "Montant reçu",
@@ -3724,6 +3765,7 @@
"Redirecting to GitHub...": "Redirection vers GitHub...",
"Redirecting to payment page...": "Redirection vers la page de paiement...",
"Redirecting to sign in in {{seconds}} seconds.": "Redirection vers la connexion dans {{seconds}} secondes.",
+ "Reduces repeated wording": "Réduit les formulations répétées",
"Reference Video": "Vidéo de référence",
"Referral link:": "Lien de parrainage :",
"Referral Program": "Programme de parrainage",
@@ -3848,6 +3890,7 @@
"Required provider, authentication, model, and group settings": "Paramètres requis de fournisseur, authentification, modèles et groupes",
"Required to expose Midjourney-style image generation to end users.": "Requis pour exposer la génération d'images style Midjourney aux utilisateurs finaux.",
"Requires Record quota usage to be enabled in Operations > Log Maintenance.": "Nécessite l'activation de « Enregistrer l'utilisation du quota » dans Opérations > Maintenance des journaux.",
+ "rerank": "Réordonnancement",
"Rerank": "Reclasser",
"Reroll": "Relancer",
"Research assistant": "Research assistant",
@@ -3893,6 +3936,7 @@
"Response path for upstream task ID": "Chemin de réponse pour l’ID de tâche amont",
"Response Time": "Temps de réponse",
"Response time: {{duration}}": "Temps de réponse : {{duration}}",
+ "responses": "API Responses",
"Responses API Version": "Version de l'API des réponses",
"Responses APIs": "API Responses",
"Restore defaults": "Restaurer les paramètres par défaut",
@@ -3947,6 +3991,15 @@
"Route, auth, and balance check in one place": "Routage, authentification et solde au même endroit",
"route: weighted channel": "routage : canal pondéré",
"Routes": "Routes",
+ "Routes Anthropic Messages-compatible requests.": "Achemine les requêtes compatibles avec Anthropic Messages.",
+ "Routes asynchronous video generation tasks.": "Achemine les tâches asynchrones de génération vidéo.",
+ "Routes embedding vector requests.": "Achemine les requêtes de vecteurs d'embedding.",
+ "Routes Gemini native /v1beta model requests.": "Achemine les requêtes de modèles Gemini natifs /v1beta.",
+ "Routes image generation and editing requests.": "Achemine les requêtes de génération et de modification d'images.",
+ "Routes OpenAI Responses API requests.": "Achemine les requêtes de l'API OpenAI Responses.",
+ "Routes OpenAI-compatible chat and completion requests.": "Achemine les requêtes de chat et de complétion compatibles OpenAI.",
+ "Routes search reranking requests.": "Achemine les requêtes de reclassement de recherche.",
+ "Routes speech, transcription, translation, or music requests.": "Achemine les requêtes de parole, transcription, traduction ou musique.",
"Routing": "Routage",
"Routing & Overrides": "Routage et surcharges",
"Routing and reliability governance": "Gouvernance du routage et de la fiabilité",
@@ -4083,6 +4136,7 @@
"Security boundary": "Frontière de sécurité",
"Security Check": "Vérification de sécurité",
"Security verification": "Vérification de sécurité",
+ "Seed": "Graine",
"Seedance": "Seedance",
"Select": "Sélectionner",
"Select {{item}}": "Sélectionner {{item}}",
@@ -4241,6 +4295,7 @@
"Showcase core capabilities with demo credentials and limited access.": "Présenter les fonctionnalités principales avec des identifiants de démonstration et un accès limité.",
"Showing": "Affichage de",
"showing •": "affichage •",
+ "Shows the request APIs, task capabilities, and operational features supported by this channel type.": "Affiche les API de requête, les fonctionnalités de tâches et les fonctions opérationnelles prises en charge par ce type de canal.",
"Sidebar": "Barre latérale",
"Sidebar collapsed by default for new users": "Barre latérale masquée par défaut pour les nouveaux utilisateurs",
"Sidebar modules": "Modules de la barre latérale",
@@ -4335,6 +4390,7 @@
"Status Page Slug": "Slug de la page d'état",
"Status Path": "Chemin du statut",
"Status polling": "Polling de statut",
+ "Status response path is required when the full task protocol is enabled.": "Le chemin de réponse du statut est obligatoire lorsque le protocole complet est activé.",
"Status short": "État",
"Status:": "Statut :",
"Stay": "Rester",
@@ -4482,6 +4538,7 @@
"Task History": "Historique des tâches",
"Task ID": "ID de la tâche",
"Task ID Path": "Chemin de l’ID de tâche",
+ "Task ID response path is required when the full task protocol is enabled.": "Le chemin de réponse de l'ID de tâche est obligatoire lorsque le protocole complet est activé.",
"Task ID:": "ID de tâche :",
"Task logs": "Journaux des tâches",
"Task Logs": "Journaux de tâches",
@@ -4501,6 +4558,7 @@
"Telegram": "Telegram",
"Telegram login requires widget integration; coming soon": "La connexion Telegram nécessite l'intégration d'un widget ; disponible bientôt",
"Telegram Login Widget": "Widget de connexion Telegram",
+ "Temperature": "Température",
"Template": "Modèle",
"Template variables:": "Variables de modèle :",
"Templates": "Modèles",
@@ -4587,6 +4645,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Cette expression est trop complexe pour l'éditeur visuel. Passez en mode expression pour la modifier.",
"This feature is experimental. Configuration format and behavior may change.": "Cette fonctionnalité est expérimentale. Le format de configuration et le comportement peuvent changer.",
"This feature requires server-side WeChat configuration": "Cette fonctionnalité nécessite une configuration WeChat côté serveur",
+ "This JSON configuration must be a JSON object.": "Cette configuration JSON doit être un objet JSON.",
"This may cause cache failures.": "Cela peut provoquer des échecs de cache.",
"This may take a few moments while we validate the request and update your session.": "Cela peut prendre quelques instants pendant que nous validons la requête et mettons à jour votre session.",
"This model has both fixed price and ratio billing conflicts": "Ce modèle présente des conflits de facturation à la fois en prix fixe et au ratio",
@@ -4723,6 +4782,7 @@
"Top models": "Top modèles",
"Top Models": "Top Modèles",
"Top models by traffic": "Modèles les plus utilisés",
+ "Top P": "Top P",
"Top up balance and view billing history.": "Recharger le solde et consulter l'historique de facturation.",
"Top Users": "Top utilisateurs",
"Top vendors": "Top fournisseurs",
@@ -4896,6 +4956,7 @@
"upstream AI ecosystems": "écosystèmes IA amont",
"Upstream model and multimodal platforms": "Plateformes de modèles amont et multimodales",
"Upstream Model Detection Settings": "Paramètres de détection des modèles en amont",
+ "Upstream model discovery is not supported for this channel type and will not run.": "La découverte des modèles en amont n'est pas prise en charge pour ce type de canal et ne sera pas exécutée.",
"Upstream Model Update Check": "Vérification des mises à jour des modèles en amont",
"Upstream Model Updates": "Mises à jour des modèles en amont",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Mises à jour des modèles en amont appliquées : {{added}} ajoutés, {{removed}} supprimés, {{ignored}} ignorés cette fois, {{totalIgnored}} modèles ignorés au total",
@@ -5048,15 +5109,25 @@
"Version Overrides": "Remplacements de version",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Le mode clé API Vertex AI ne prend pas en charge la création par lot",
+ "Vertex AI API Key mode does not support batch creation.": "Le mode API Key de Vertex AI ne prend pas en charge la création par lots.",
"Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.": "Vertex AI ne prend pas en charge functionResponse.id. Activez ceci pour supprimer automatiquement ce champ.",
"Vertex AI Key Format": "Format de clé Vertex AI",
+ "Vertex AI region configuration must be a JSON object containing a default field.": "La configuration de région Vertex AI doit être un objet JSON contenant un champ default.",
+ "Vertex AI region configuration must contain a default field.": "La configuration de région Vertex AI doit contenir un champ default.",
+ "Vertex AI service account credentials must be valid JSON.": "Les identifiants de compte de service Vertex AI doivent être un JSON valide.",
"Vertex AI service account key must be valid JSON": "La clé de compte de service Vertex AI doit être un JSON valide",
"Video": "Vidéo",
"Video length in seconds": "Durée de la vidéo en secondes",
"Video Remix": "Remix vidéo",
"video task adapters, status mappings, and protocol paths": "adaptateurs de tâches vidéo, correspondances d’état et chemins de protocole",
+ "Video Task Billing": "Facturation des tâches vidéo",
"video task channels and protocol templates": "canaux de tâches vidéo et modèles de protocoles",
"Video Task Protocol": "Protocole de tâche vidéo",
+ "Video task protocol settings are only available for channel types that support video tasks.": "Les paramètres du protocole de tâche vidéo sont réservés aux types de canaux prenant en charge les tâches vidéo.",
+ "Video task query path is required.": "Le chemin de requête des tâches vidéo est obligatoire.",
+ "Video task query path must contain {task_id}, {operation_name}, or {upstream_task_id}.": "Le chemin de requête des tâches vidéo doit contenir {task_id}, {operation_name} ou {upstream_task_id}.",
+ "Video task submission path is required.": "Le chemin de soumission des tâches vidéo est obligatoire.",
+ "video tasks": "Tâches vidéo",
"Vidu": "Vidu",
"View": "Afficher",
"View all currently available models": "Voir tous les modèles actuellement disponibles",
@@ -5188,6 +5259,7 @@
"Wire encoding for the embedding vectors": "Encodage filaire pour les vecteurs",
"with conflicts": "avec des conflits",
"with the API key from your token settings.": "par la clé API de votre page de jetons.",
+ "Without a result URL path, completed video tasks may not return a content URL.": "Sans chemin d'URL de résultat, les tâches vidéo terminées peuvent ne pas renvoyer d'URL de contenu.",
"Without additional conditions, only the type above is used for pruning.": "Sans conditions supplémentaires, seul le type ci-dessus est utilisé pour le nettoyage.",
"Worker": "Worker",
"Worker Access Key": "Clé d'accès du Worker",
diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json
index 2c27026f..9a9ea505 100644
--- a/web/default/src/i18n/locales/ja.json
+++ b/web/default/src/i18n/locales/ja.json
@@ -24,6 +24,7 @@
"{{category}} Models": "{{category}} モデル",
"{{completed}}/{{total}} completed": "{{completed}}/{{total}} 完了",
"{{count}} {{entity}} selected. Bulk actions toolbar is available.": "{{entity}}を{{count}}件選択しました。一括操作ツールバーを利用できます。",
+ "{{count}} blocking issue(s)": "ブロック中の問題 {{count}} 件",
"{{count}} channel(s) deleted": "{{count}} 個のチャネルを削除しました",
"{{count}} channel(s) disabled": "{{count}} 個のチャネルを無効にしました",
"{{count}} channel(s) enabled": "{{count}} 個のチャネルを有効にしました",
@@ -49,6 +50,7 @@
"{{count}} tiered billing models enabled": "段階制課金が有効なモデル:{{count}}件",
"{{count}} tiers": "{{count}} 段階",
"{{count}} vendors": "{{count}} ベンダー",
+ "{{count}} warning(s)": "警告 {{count}} 件",
"{{count}} weeks ago": "{{count}} 週間前",
"{{field}} updated to {{value}}": "{{field}} を {{value}} に更新しました",
"{{field}} updated to {{value}} for tag: {{tag}}": "タグ「{{tag}}」の {{field}} を {{value}} に更新しました",
@@ -64,6 +66,7 @@
"{{routeContext}} groups must not be empty": "{{routeContext}} のグループは空にできません",
"{{routeContext}} groups must not exceed 64 entries": "{{routeContext}} のグループは64件以下にしてください",
"{{success}} succeeded, {{failed}} failed": "{{success}} 件成功、{{failed}} 件失敗",
+ "{{supported}}/{{total}} available": "{{supported}}/{{total}} 件利用可能",
"{{target}} test failed": "{{target}} のテストに失敗しました",
"{{target}} test succeeded": "{{target}} のテストに成功しました",
"{{title}} usage: {{percent}}%": "{{title}} の使用率: {{percent}}%",
@@ -81,10 +84,12 @@
"`, and `-nothinking` suffixes while routing to the correct Gemini variant.": "`, および `-nothinking` サフィックスを、正しい Gemini バリアントにルーティングする際に使用します。",
"© 2025 Your Company. All rights reserved.": "© 2025 Your Company. 全著作権所有。",
"+{{count}} more": "他 {{count}} 件",
+ "+{{count}} more mapping(s)": "他 {{count}} 件のマッピング",
"= {{count}} tokens": "= {{count}} トークン",
"= {{count}}K tokens": "= {{count}}K トークン",
"= {{count}}M tokens": "= {{count}}M トークン",
"| Based on": "| に基づく",
+ "$/1M tokens": "$/100万トークン",
"0 means data is kept permanently": "0 はデータを永続的に保持することを意味します",
"0 means unlimited": "0は無制限を意味します",
"0 means unlimited. The value is converted to quota units when saved.": "0 は無制限を意味します。保存時にクォータ単位へ変換されます。",
@@ -242,6 +247,7 @@
"Additional Limit": "追加上限",
"Additional Limits": "追加上限",
"Additional metered capability": "追加の従量制機能",
+ "Additional upstream configuration is required for this channel type.": "このチャネル種別では追加のアップストリーム設定が必要です。",
"Adjust filters, then search to refresh the logs.": "フィルターを調整してから検索し、ログを更新します。",
"Adjust Quota": "クォータを調整",
"Adjust response formatting, prompt behavior, proxy, and upstream automation.": "レスポンス形式、プロンプト動作、プロキシ、上流自動化を調整します。",
@@ -438,6 +444,7 @@
"API key from the provider": "プロバイダからのAPIキー",
"API key is loading, please try again in a moment": "API キーを読み込み中です。しばらくしてから再試行してください",
"API key is required": "APIキーが必要です",
+ "API Key is required when creating a channel.": "チャネルの作成時には API Key が必要です。",
"API Key mode (does not support batch creation)": "APIキー モード(一括作成には対応していません)",
"API Key mode: use APIKey|Region": "APIキーモード: use APIKey | Region",
"API Key updated successfully": "APIキーが正常に更新されました",
@@ -505,9 +512,11 @@
"async task protocols": "非同期タスクプロトコル",
"Async task refund": "非同期タスク返金",
"async task surfaces": "非同期タスク面",
+ "At least one model must be published for this channel.": "このチャネルでは少なくとも 1 つのモデルを公開する必要があります。",
"At least one model regex pattern is required": "少なくとも1つのモデル正規表現パターンが必要です",
"At least one valid key source is required": "少なくとも1つの有効なキーソースが必要です",
"Attach": "添付",
+ "audio": "音声",
"Audio": "音声",
"Audio comp.": "音声補完",
"Audio completion ratio": "音声補完倍率",
@@ -640,8 +649,10 @@
"Base rate limit windows for this account.": "このアカウント向けの基本レート制限ウィンドウ。",
"Base URL": "ベースURL",
"Base URL is required for this channel type": "このチャンネルタイプには Base URL が必要です",
+ "Base URL is required for this channel type.": "このチャネル種別では Base URL が必要です。",
"Base URL is required when a route upstream path is relative": "ルートの upstream パスが相対パスの場合、Base URL は必須です",
"Base URL of your Uptime Kuma instance": "Uptime KumaインスタンスのベースURL",
+ "Base URL should not usually end with /v1 because MAX API appends the upstream path automatically.": "MAX API がアップストリームパスを自動追加するため、Base URL は通常 /v1 で終わらせないでください。",
"Basic Authentication": "基本認証",
"Basic Configuration": "基本設定",
"Basic Info": "基本情報",
@@ -779,6 +790,7 @@
"Cancelled at": "キャンセル日時",
"Capabilities": "機能",
"Capability checks": "機能チェック",
+ "Caps the response length": "応答の長さを制限",
"Capture a reusable bundle of models, tags, or endpoints.": "モデル、タグ、またはエンドポイントの再利用可能なバンドルを保存。",
"Card view": "カード表示",
"cards": "カード",
@@ -840,6 +852,7 @@
"Chat preset not found": "チャットプリセットが見つかりません",
"Chat Presets": "チャットプリセット",
"Chat session management": "チャットセッション管理",
+ "chat/completions": "チャット / 補完 API",
"ChatCompletions -> Responses Compatibility": "ChatCompletions → レスポンス互換",
"Check for updates": "更新を確認",
"Check in daily to receive random quota rewards": "毎日チェックインして、ランダムなノルマ報酬を受け取りましょう",
@@ -937,9 +950,12 @@
"Codex Account & Usage": "Codex アカウントと使用量",
"Codex Authorization": "Codex認証",
"Codex channels do not support batch creation": "Codex チャンネルは一括作成をサポートしていません",
+ "Codex channels do not support batch creation.": "Codex チャネルは一括作成に対応していません。",
"Codex channels use an OAuth JSON credential as the key.": "CodexチャンネルはOAuth JSON認証情報をキーとして使用します。",
"Codex CLI Header Passthrough": "Codex CLI ヘッダーパススルー",
"Codex credential must be a JSON object with access_token and account_id": "Codex 認証情報は access_token と account_id を含む JSON オブジェクトである必要があります",
+ "Codex credentials must be a valid JSON object.": "Codex の認証情報は有効な JSON オブジェクトである必要があります。",
+ "Codex credentials must contain access_token and account_id.": "Codex の認証情報には access_token と account_id が必要です。",
"Cohere": "Cohere",
"Collapse": "折りたたむ",
"Collapse All": "すべて折りたたむ",
@@ -977,11 +993,13 @@
"Compatibility field for the default auto route. It is derived from the route config when saving.": "デフォルト自動ルート用の互換フィールドです。保存時にルート設定から生成されます。",
"compatible API routes": "互換APIルート",
"Compatible API routes for common AI application workflows": "一般的なAIアプリケーションワークフロー向けの互換APIルート",
+ "Compatible support": "互換対応",
"Complete API documentation with multi-language SDK support": "多言語SDKをサポートする完全なAPIドキュメント",
"Complete Order": "手動チャージ",
"Complete these steps to finish the initial installation.": "初期インストールを完了するには、これらの手順を完了してください。",
"Completed": "完了",
"Completion": "補完",
+ "Completion delta settlement": "完了時の差額精算",
"Completion price": "補完価格",
"Completion price ($/1M tokens)": "完了価格 (トークン100万あたり$)",
"Completion ratio": "補完倍率",
@@ -998,6 +1016,7 @@
"Conditions (AND)": "条件(AND)",
"Confidence": "信頼度",
"Config validation": "設定検証",
+ "Configurable": "設定可能",
"Configuration": "設定",
"Configuration File": "設定ファイル",
"Configuration for Creem payment integration": "Creem決済統合の設定",
@@ -1005,6 +1024,8 @@
"Configuration for Stripe payment integration": "Stripe決済連携のための設定",
"Configuration key": "設定キー",
"Configuration required": "設定が必要です",
+ "Configuration valid": "設定は有効です",
+ "Configuration validation": "設定の検証",
"Configure": "設定",
"Configure a Creem product for user recharge options.": "ユーザー チャージオプション用の Creem 製品を設定。",
"Configure a custom ratio for when users use a specific token group.": "ユーザーが特定のトークングループを使用する際のカスタム倍率を設定します。",
@@ -1110,6 +1131,7 @@
"Control plane": "コントロールプレーン",
"Control which models are exposed and which groups may use them.": "公開するモデルと、それらを利用できるグループを制御します。",
"Controls how much the model thinks before answering": "モデルが回答前に考える深さを制御します",
+ "Controls randomness and creativity": "ランダム性と創造性を制御",
"Controls whether user verification (biometrics/PIN) is required during Passkey flows.": "Passkeyフロー中にユーザー認証(生体認証/PIN)が必要かどうかを制御します。",
"Conversion rate from USD to your custom currency": "USDからカスタム通貨への換算レート",
"Convert reasoning_content to tag in content": "content内のreasoning_contentをタグに変換",
@@ -1593,11 +1615,13 @@
"Email Field": "メールフィールド",
"Email Verification": "メール認証",
"Email, summarisation, knowledge work": "メール・要約・ナレッジワーク",
+ "embeddings": "埋め込み",
"Embeddings": "埋め込み",
"Empty": "空",
"Empty Completion Retry": "空補完リトライ",
"Empty value will be saved as {}.": "空の値は {} として保存されます。",
"Enable": "有効にする",
+ "Enable {{parameter}}": "{{parameter}}を有効化",
"Enable 2FA": "2FA を有効にする",
"Enable All": "すべて有効にする",
"Enable and price audio input before configuring audio output.": "音声出力を設定する前に、音声入力を有効にして価格を設定してください。",
@@ -1647,6 +1671,7 @@
"Enabled Status": "有効ステータス",
"Enabling...": "有効化中...",
"Encourages introducing new topics": "新しい話題への展開を促進します",
+ "Encourages new topics": "新しいトピックを促す",
"End": "終了",
"End Error": "終了エラー",
"End Reason": "終了理由",
@@ -1964,6 +1989,7 @@
"Fetch Models": "モデルを取得",
"Fetched {{count}} model(s) from upstream": "上流から {{count}} 個のモデルを取得しました",
"Fetched {{count}} models": "{{count}} 個のモデルを取得しました",
+ "Fetches model lists directly from the upstream provider.": "アップストリームプロバイダーからモデル一覧を直接取得します。",
"Fetching prefill groups...": "プリフィルグループをフェッチ中...",
"Fetching upstream prices...": "上流価格を取得中...",
"Fetching upstream ratios...": "アップストリーム比率をフェッチ中...",
@@ -2070,6 +2096,7 @@
"Forward requests directly to upstream providers without any post-processing.": "ポストプロセスなしで、リクエストをアップストリームプロバイダーに直接転送します。",
"Frames per second": "フレームレート",
"Free: {{free}} / Total: {{total}}": "空き容量: {{free}} / 合計: {{total}}",
+ "Frequency Penalty": "頻度ペナルティ",
"Friendly name to identify this channel": "このチャネルを識別するための表示名",
"From Address": "差出人アドレス",
"From AI model access to Agent lifecycle governance": "AIモデルアクセスからAgentライフサイクルガバナンスへ",
@@ -2096,6 +2123,7 @@
"Gemini Generate Content": "Gemini コンテンツ生成",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content から OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K",
+ "Gemini native": "Gemini ネイティブ API",
"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は思考モードを自動検出します。価格設定と予算編成をより細かく制御する必要がある場合にのみ、これを有効にしてください。",
"General": "一般",
"General Settings": "一般設定",
@@ -2314,6 +2342,7 @@
"Image to Video": "画像から動画",
"Image Tokens": "画像トークン",
"Image, video, music, polling, result proxying, status mapping, and refund handling.": "画像、動画、音楽、ポーリング、結果プロキシ、ステータスマッピング、返金処理。",
+ "images": "画像",
"Import to CC Switch": "CC Switch にインポート",
"In Progress": "処理中",
"In:": "入力:",
@@ -2451,6 +2480,7 @@
"Keep the platform ready": "プラットフォームを準備状態に保つ",
"Keep this above 1 minute to avoid heavy database load": "データベースへの負荷を避けるため、これを1分以上に保ってください",
"Keep-alive Ping": "キープアライブPing",
+ "Keeps compatible responses more repeatable": "応答の再現性を高める",
"Key": "キー",
"Key custody": "キー管理",
"Key Fingerprint": "キーフィンガープリント",
@@ -2522,6 +2552,8 @@
"Limit Reached": "上限に達しました",
"Limit which models can be used with this key": "このキーで使用できるモデルを制限する",
"Limited": "制限",
+ "Limited support": "限定対応",
+ "Limits token selection to a probability mass": "トークン選択を確率質量に制限",
"LingYiWanWu": "LingYiWanWu",
"Link to your documentation site": "ドキュメントサイトへのリンク",
"LinuxDO": "LinuxDO",
@@ -2641,6 +2673,7 @@
"Max Success": "最大成功数",
"Max successful requests": "最大成功リクエスト数",
"Max Successful Requests": "最大成功リクエスト数",
+ "Max Tokens": "最大トークン数",
"MAX-API-Next": "MAX-API-Next",
"Maximum 1000 characters. Supports Markdown and HTML.": "最大1000文字。MarkdownとHTMLをサポートしています。",
"Maximum 200 characters": "最大200文字",
@@ -2717,6 +2750,7 @@
"Model Description": "モデルの説明",
"Model details": "モデル詳細",
"Model disabled successfully": "モデルが正常に無効化されました",
+ "model discovery": "モデル検出",
"Model enabled successfully": "モデルが正常に有効化されました",
"Model fixed pricing": "モデルの固定価格設定",
"Model governance": "モデルガバナンス",
@@ -2861,6 +2895,7 @@
"name@example.com": "name@example.com",
"Native format": "ネイティブ形式",
"Native forwarding": "ネイティブ転送",
+ "Native support": "ネイティブ対応",
"Navigation link": "ナビゲーションリンク",
"Navigation name": "ナビゲーション名",
"Need a redemption code?": "引き換えコードが必要ですか?",
@@ -2912,6 +2947,7 @@
"No backup": "バックアップなし",
"No base input price": "基本入力価格なし",
"No billing records found": "請求記録が見つかりません",
+ "No blocking configuration issues were detected for this channel type.": "このチャネル種別では、設定を妨げる問題は検出されませんでした。",
"No capabilities reported for this model.": "このモデルには報告されている機能がありません。",
"No Change": "変更なし",
"No changes": "変更なし",
@@ -3066,6 +3102,7 @@
"Not set yet": "未設定",
"Not Started": "未開始",
"Not Submitted": "未送信",
+ "Not supported": "非対応",
"Not tested": "未テスト",
"Not used for upstream training by default": "デフォルトで上流の学習には使用されません",
"Not used yet": "未使用",
@@ -3135,6 +3172,7 @@
"Only allow specific email domains": "特定のEメール ドメインのみを許可する",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "管理者のみ利用可能です。有効にすると、スケジュールされたモデルチェックでアップストリームモデルの変更やチェック失敗が検出された際に、選択した方法で概要通知を受け取ります。",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "設定済みの組み合わせだけが上書きされます。他の呼び出しはトークングループの基本倍率を維持します。",
+ "Only enabled parameters are sent with the request.": "有効なパラメータのみリクエストに送信されます。",
"Only override upstream submit/query paths and keep the official response parser": "上流の送信/照会パスのみを上書きし、公式レスポンスパーサーを維持します",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "選択されたフィールドのみが上書きされます。新しい競合が発生した場合は、同期ウィザードを再実行できます。",
"Only successful requests": "成功したリクエストのみ",
@@ -3266,6 +3304,7 @@
"Parameter override template must be a JSON object": "パラメータオーバーライドテンプレートはJSONオブジェクトである必要があります",
"Parameter pricing": "パラメータ価格",
"Parameter pricing rules": "パラメータ価格ルール",
+ "Parameter settings": "パラメータ設定",
"parameter.": "パラメーター。",
"Parameters": "パラメータ",
"Parsed {{count}} service account file(s)": "__ PH_0 __サービスアカウントファイルを解析しました",
@@ -3483,6 +3522,7 @@
"Prepend": "先頭に追加",
"Prepend to Start": "先頭に追加",
"Prepend value to array / string / object start": "配列/文字列/オブジェクトの先頭に値を追加",
+ "Presence Penalty": "プレゼンスペナルティ",
"Preserve the original field when applying this rule": "このルール適用時に元のフィールドを保持します",
"Preset groups": "プリセットグループ",
"Preset recharge amounts (JSON array)": "プリセットチャージ金額 (JSON配列)",
@@ -3683,6 +3723,7 @@
"Reason:": "理由:",
"Reasoning": "理由",
"Reasoning Effort": "推論強度",
+ "Recalculate successful DoubaoVideo tasks from upstream usage tokens when polling completes; turn this off only for emergency containment because successful tasks will keep the pre-consumed quota.": "ポーリング完了時に、成功した DoubaoVideo タスクを上流の usage token で再計算します。成功タスクは事前消費クォータのままになるため、緊急時のみオフにしてください。",
"Receive Upstream Model Update Notifications": "アップストリームモデル更新通知を受け取る",
"Received": "受信済み",
"Received amount": "受領額",
@@ -3724,6 +3765,7 @@
"Redirecting to GitHub...": "GitHub にリダイレクトしています...",
"Redirecting to payment page...": "支払いページにリダイレクト中...",
"Redirecting to sign in in {{seconds}} seconds.": "{{seconds}} 秒後にログインページへ移動します。",
+ "Reduces repeated wording": "繰り返し表現を減らす",
"Reference Video": "参照動画",
"Referral link:": "紹介リンク:",
"Referral Program": "紹介プログラム",
@@ -3848,6 +3890,7 @@
"Required provider, authentication, model, and group settings": "必須のプロバイダー、認証、モデル、グループ設定",
"Required to expose Midjourney-style image generation to end users.": "エンドユーザーに Midjourney スタイルの画像生成を公開するために必要です。",
"Requires Record quota usage to be enabled in Operations > Log Maintenance.": "「運用 > ログのメンテナンス」で「クォータ使用量を記録」を有効にする必要があります。",
+ "rerank": "再ランキング",
"Rerank": "再ランク付け",
"Reroll": "やり直し",
"Research assistant": "リサーチアシスタント",
@@ -3893,6 +3936,7 @@
"Response path for upstream task ID": "上流タスク ID のレスポンスパス",
"Response Time": "応答時間",
"Response time: {{duration}}": "応答時間: {{duration}}",
+ "responses": "レスポンス API",
"Responses API Version": "応答APIバージョン",
"Responses APIs": "Responses API",
"Restore defaults": "既定に戻す",
@@ -3947,6 +3991,15 @@
"Route, auth, and balance check in one place": "ルート、認証、残高確認を一か所に集約",
"route: weighted channel": "ルート: 重み付きチャネル",
"Routes": "ルート",
+ "Routes Anthropic Messages-compatible requests.": "Anthropic Messages 互換リクエストをルーティングします。",
+ "Routes asynchronous video generation tasks.": "非同期動画生成タスクをルーティングします。",
+ "Routes embedding vector requests.": "埋め込みベクトルリクエストをルーティングします。",
+ "Routes Gemini native /v1beta model requests.": "Gemini ネイティブ /v1beta モデルリクエストをルーティングします。",
+ "Routes image generation and editing requests.": "画像生成および編集リクエストをルーティングします。",
+ "Routes OpenAI Responses API requests.": "OpenAI Responses API リクエストをルーティングします。",
+ "Routes OpenAI-compatible chat and completion requests.": "OpenAI 互換のチャットおよび補完リクエストをルーティングします。",
+ "Routes search reranking requests.": "検索リランキングリクエストをルーティングします。",
+ "Routes speech, transcription, translation, or music requests.": "音声、文字起こし、翻訳、音楽リクエストをルーティングします。",
"Routing": "ルーティング",
"Routing & Overrides": "ルーティングと上書き",
"Routing and reliability governance": "ルーティングと信頼性のガバナンス",
@@ -4083,6 +4136,7 @@
"Security boundary": "セキュリティ境界",
"Security Check": "セキュリティチェック",
"Security verification": "セキュリティ確認",
+ "Seed": "シード",
"Seedance": "Seedance(動画生成)",
"Select": "選択",
"Select {{item}}": "{{item}}を選択",
@@ -4241,6 +4295,7 @@
"Showcase core capabilities with demo credentials and limited access.": "デモ用の認証情報と制限付きアクセスでコア機能を紹介します。",
"Showing": "表示",
"showing •": "表示中 •",
+ "Shows the request APIs, task capabilities, and operational features supported by this channel type.": "このチャネル種別が対応するリクエスト API、タスク機能、運用機能を表示します。",
"Sidebar": "サイドバー",
"Sidebar collapsed by default for new users": "新規ユーザー向けにサイドバーをデフォルトで折りたたむ",
"Sidebar modules": "サイドバーモジュール",
@@ -4335,6 +4390,7 @@
"Status Page Slug": "ステータスページスラッグ",
"Status Path": "ステータスパス",
"Status polling": "ステータスポーリング",
+ "Status response path is required when the full task protocol is enabled.": "完全なタスクプロトコルを有効にする場合、ステータスのレスポンスパスは必須です。",
"Status short": "状態",
"Status:": "ステータス:",
"Stay": "残る",
@@ -4482,6 +4538,7 @@
"Task History": "タスク履歴",
"Task ID": "タスクID",
"Task ID Path": "タスク ID パス",
+ "Task ID response path is required when the full task protocol is enabled.": "完全なタスクプロトコルを有効にする場合、タスク ID のレスポンスパスは必須です。",
"Task ID:": "タスクID:",
"Task logs": "タスクログ",
"Task Logs": "タスク履歴",
@@ -4501,6 +4558,7 @@
"Telegram": "Telegram",
"Telegram login requires widget integration; coming soon": "Telegramログインにはウィジェット統合が必要です;近日公開",
"Telegram Login Widget": "Telegramログインウィジェット",
+ "Temperature": "温度",
"Template": "テンプレート",
"Template variables:": "テンプレート変数:",
"Templates": "テンプレート",
@@ -4587,6 +4645,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "この式はビジュアルエディタでは扱いにくいです。式モードに切り替えて編集してください。",
"This feature is experimental. Configuration format and behavior may change.": "この機能は実験的です。設定フォーマットや動作は変更される可能性があります。",
"This feature requires server-side WeChat configuration": "この機能にはサーバー側のWeChat設定が必要です",
+ "This JSON configuration must be a JSON object.": "この JSON 設定は JSON オブジェクトである必要があります。",
"This may cause cache failures.": "これによりキャッシュ障害が発生する可能性があります。",
"This may take a few moments while we validate the request and update your session.": "リクエストを検証し、セッションを更新するのに数分かかる場合があります。",
"This model has both fixed price and ratio billing conflicts": "このモデルには固定価格と比率請求の両方の競合があります",
@@ -4723,6 +4782,7 @@
"Top models": "人気モデル",
"Top Models": "トップモデル",
"Top models by traffic": "トラフィック上位モデル",
+ "Top P": "Top P",
"Top up balance and view billing history.": "残高をチャージし、請求履歴を確認。",
"Top Users": "上位ユーザー",
"Top vendors": "人気ベンダー",
@@ -4896,6 +4956,7 @@
"upstream AI ecosystems": "上流 AI エコシステム",
"Upstream model and multimodal platforms": "上流モデルとマルチモーダルプラットフォーム",
"Upstream Model Detection Settings": "アップストリームモデル検出設定",
+ "Upstream model discovery is not supported for this channel type and will not run.": "このチャネル種別はアップストリームモデル検出に対応していないため、実行されません。",
"Upstream Model Update Check": "アップストリームモデル更新チェック",
"Upstream Model Updates": "上流モデルの更新",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "上流モデル更新を処理しました:{{added}} 個追加、{{removed}} 個削除、今回 {{ignored}} 個無視、合計 {{totalIgnored}} 個の無視モデル",
@@ -5048,15 +5109,25 @@
"Version Overrides": "バージョンオーバーライド",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Vertex AI API Key モードは一括作成をサポートしていません",
+ "Vertex AI API Key mode does not support batch creation.": "Vertex AI の API Key モードは一括作成に対応していません。",
"Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.": "Vertex AI は functionResponse.id フィールドをサポートしません。有効にすると自動的に削除します。",
"Vertex AI Key Format": "Vertex AIキー形式",
+ "Vertex AI region configuration must be a JSON object containing a default field.": "Vertex AI のリージョン設定は default フィールドを含む JSON オブジェクトである必要があります。",
+ "Vertex AI region configuration must contain a default field.": "Vertex AI のリージョン設定には default フィールドが必要です。",
+ "Vertex AI service account credentials must be valid JSON.": "Vertex AI のサービスアカウント認証情報は有効な JSON である必要があります。",
"Vertex AI service account key must be valid JSON": "Vertex AI サービスアカウントキーは有効な JSON である必要があります",
"Video": "動画",
"Video length in seconds": "動画の長さ(秒)",
"Video Remix": "動画 Remix",
"video task adapters, status mappings, and protocol paths": "動画タスクアダプター、ステータスマッピング、プロトコルパス",
+ "Video Task Billing": "動画タスク課金",
"video task channels and protocol templates": "動画タスクチャネルとプロトコルテンプレート",
"Video Task Protocol": "動画タスクプロトコル",
+ "Video task protocol settings are only available for channel types that support video tasks.": "動画タスクプロトコル設定は、動画タスクに対応するチャネル種別でのみ使用できます。",
+ "Video task query path is required.": "動画タスクの照会パスは必須です。",
+ "Video task query path must contain {task_id}, {operation_name}, or {upstream_task_id}.": "動画タスクの照会パスには {task_id}、{operation_name}、{upstream_task_id} のいずれかが必要です。",
+ "Video task submission path is required.": "動画タスクの送信パスは必須です。",
+ "video tasks": "動画タスク",
"Vidu": "Vidu",
"View": "表示",
"View all currently available models": "現在利用可能なすべてのモデルを表示",
@@ -5188,6 +5259,7 @@
"Wire encoding for the embedding vectors": "ベクトルの転送エンコーディング",
"with conflicts": "競合あり",
"with the API key from your token settings.": "をトークン設定の API キーに置き換えてください。",
+ "Without a result URL path, completed video tasks may not return a content URL.": "結果 URL のパスがない場合、完了した動画タスクからコンテンツ URL が返されない可能性があります。",
"Without additional conditions, only the type above is used for pruning.": "追加条件がない場合、上記のtypeのみが削除に使用されます。",
"Worker": "ワーカー",
"Worker Access Key": "Workerアクセスキー",
diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json
index f2db8956..36c5d72f 100644
--- a/web/default/src/i18n/locales/ru.json
+++ b/web/default/src/i18n/locales/ru.json
@@ -24,6 +24,7 @@
"{{category}} Models": "Модели {{category}}",
"{{completed}}/{{total}} completed": "{{completed}}/{{total}} выполнено",
"{{count}} {{entity}} selected. Bulk actions toolbar is available.": "Выбрано: {{count}} {{entity}}. Панель массовых действий доступна.",
+ "{{count}} blocking issue(s)": "Блокирующих проблем: {{count}}",
"{{count}} channel(s) deleted": "Удалено {{count}} каналов",
"{{count}} channel(s) disabled": "Отключено {{count}} каналов",
"{{count}} channel(s) enabled": "Включено {{count}} каналов",
@@ -49,6 +50,7 @@
"{{count}} tiered billing models enabled": "Моделей с включённой многоуровневой тарификацией: {{count}}",
"{{count}} tiers": "{{count}} уровней",
"{{count}} vendors": "поставщиков: {{count}}",
+ "{{count}} warning(s)": "Предупреждений: {{count}}",
"{{count}} weeks ago": "{{count}} недель назад",
"{{field}} updated to {{value}}": "{{field}} обновлено на {{value}}",
"{{field}} updated to {{value}} for tag: {{tag}}": "{{field}} обновлено на {{value}} для тега: {{tag}}",
@@ -64,6 +66,7 @@
"{{routeContext}} groups must not be empty": "Группы {{routeContext}} не должны быть пустыми",
"{{routeContext}} groups must not exceed 64 entries": "Группы {{routeContext}} не должны содержать более 64 элементов",
"{{success}} succeeded, {{failed}} failed": "{{success}} успешно, {{failed}} с ошибкой",
+ "{{supported}}/{{total}} available": "Доступно: {{supported}}/{{total}}",
"{{target}} test failed": "Тест {{target}} не пройден",
"{{target}} test succeeded": "Тест {{target}} успешно выполнен",
"{{title}} usage: {{percent}}%": "Использование {{title}}: {{percent}}%",
@@ -81,10 +84,12 @@
"`, and `-nothinking` suffixes while routing to the correct Gemini variant.": "суффиксы `, и `-nothinking` при маршрутизации к правильному варианту Gemini.",
"© 2025 Your Company. All rights reserved.": "© 2025 Ваша Компания. Все права защищены.",
"+{{count}} more": "ещё {{count}}",
+ "+{{count}} more mapping(s)": "Ещё сопоставления: {{count}}",
"= {{count}} tokens": "= {{count}} токенов",
"= {{count}}K tokens": "= {{count}} тыс. токенов",
"= {{count}}M tokens": "= {{count}} млн токенов",
"| Based on": "| На основе",
+ "$/1M tokens": "$/1 млн токенов",
"0 means data is kept permanently": "0 означает, что данные хранятся постоянно",
"0 means unlimited": "0 означает без ограничений",
"0 means unlimited. The value is converted to quota units when saved.": "0 означает без ограничений. При сохранении значение преобразуется в единицы квоты.",
@@ -242,6 +247,7 @@
"Additional Limit": "Доп. лимит",
"Additional Limits": "Дополнительные лимиты",
"Additional metered capability": "Дополнительная зарезервированная ёмкость (metered)",
+ "Additional upstream configuration is required for this channel type.": "Для этого типа канала требуется дополнительная конфигурация вышестоящего сервиса.",
"Adjust filters, then search to refresh the logs.": "Настройте фильтры, затем выполните поиск, чтобы обновить журналы.",
"Adjust Quota": "Изменить квоту",
"Adjust response formatting, prompt behavior, proxy, and upstream automation.": "Настройте форматирование ответов, поведение промпта, прокси и автоматизацию upstream.",
@@ -438,6 +444,7 @@
"API key from the provider": "Ключ API от провайдера",
"API key is loading, please try again in a moment": "API-ключ загружается, повторите попытку через несколько секунд",
"API key is required": "Требуется ключ API",
+ "API Key is required when creating a channel.": "При создании канала требуется API Key.",
"API Key mode (does not support batch creation)": "Режим API-ключа (не поддерживает пакетное создание)",
"API Key mode: use APIKey|Region": "Режим API Key: use APIKey|Region",
"API Key updated successfully": "API ключ успешно обновлен",
@@ -505,9 +512,11 @@
"async task protocols": "протоколы асинхронных задач",
"Async task refund": "Возврат асинхронной задачи",
"async task surfaces": "поверхности асинхронных задач",
+ "At least one model must be published for this channel.": "Для этого канала необходимо опубликовать хотя бы одну модель.",
"At least one model regex pattern is required": "Требуется хотя бы один шаблон регулярного выражения модели",
"At least one valid key source is required": "Требуется хотя бы один действительный источник ключа",
"Attach": "Прикрепить",
+ "audio": "Аудио",
"Audio": "Аудио",
"Audio comp.": "Аудио-вывод",
"Audio completion ratio": "Коэффициент завершения аудио",
@@ -640,8 +649,10 @@
"Base rate limit windows for this account.": "Окна базовых лимитов для этого аккаунта.",
"Base URL": "Адрес API",
"Base URL is required for this channel type": "Для этого типа канала требуется Base URL",
+ "Base URL is required for this channel type.": "Для этого типа канала требуется Base URL.",
"Base URL is required when a route upstream path is relative": "Base URL обязателен, если upstream-путь маршрута является относительным",
"Base URL of your Uptime Kuma instance": "Базовый URL вашего экземпляра Uptime Kuma",
+ "Base URL should not usually end with /v1 because MAX API appends the upstream path automatically.": "Base URL обычно не должен заканчиваться на /v1, поскольку MAX API автоматически добавляет вышестоящий путь.",
"Basic Authentication": "Базовая аутентификация",
"Basic Configuration": "Базовая конфигурация",
"Basic Info": "Основная информация",
@@ -779,6 +790,7 @@
"Cancelled at": "Отменено",
"Capabilities": "Возможности",
"Capability checks": "Проверки возможностей",
+ "Caps the response length": "Ограничивает длину ответа",
"Capture a reusable bundle of models, tags, or endpoints.": "Создайте повторно используемый набор моделей, тегов или конечных точек.",
"Card view": "Карточки",
"cards": "карточки",
@@ -840,6 +852,7 @@
"Chat preset not found": "Предустановка чата не найдена",
"Chat Presets": "Предустановки чата",
"Chat session management": "Управление сессиями чата",
+ "chat/completions": "API чата / завершений",
"ChatCompletions -> Responses Compatibility": "Совместимость ChatCompletions → Ответы",
"Check for updates": "Проверить обновления",
"Check in daily to receive random quota rewards": "Регистрируйтесь ежедневно, чтобы получать случайные вознаграждения по квоте",
@@ -937,9 +950,12 @@
"Codex Account & Usage": "Аккаунт и использование Codex",
"Codex Authorization": "Авторизация Codex",
"Codex channels do not support batch creation": "Каналы Codex не поддерживают пакетное создание",
+ "Codex channels do not support batch creation.": "Каналы Codex не поддерживают пакетное создание.",
"Codex channels use an OAuth JSON credential as the key.": "Каналы Codex используют учётные данные OAuth в формате JSON в качестве ключа.",
"Codex CLI Header Passthrough": "Проброс заголовков Codex CLI",
"Codex credential must be a JSON object with access_token and account_id": "Учетные данные Codex должны быть JSON-объектом с access_token и account_id",
+ "Codex credentials must be a valid JSON object.": "Учетные данные Codex должны быть корректным объектом JSON.",
+ "Codex credentials must contain access_token and account_id.": "Учетные данные Codex должны содержать access_token и account_id.",
"Cohere": "Cohere",
"Collapse": "Свернуть",
"Collapse All": "Свернуть все",
@@ -977,11 +993,13 @@
"Compatibility field for the default auto route. It is derived from the route config when saving.": "Поле совместимости для автомаршрута по умолчанию. При сохранении оно формируется из конфигурации маршрутов.",
"compatible API routes": "совместимых API-маршрутов",
"Compatible API routes for common AI application workflows": "Совместимые API-маршруты для типовых сценариев ИИ-приложений",
+ "Compatible support": "Совместимая поддержка",
"Complete API documentation with multi-language SDK support": "Полная документация API с поддержкой SDK на нескольких языках",
"Complete Order": "Вывод заказа",
"Complete these steps to finish the initial installation.": "Выполните эти шаги, чтобы завершить начальную установку.",
"Completed": "Завершено",
"Completion": "Вывод",
+ "Completion delta settlement": "Расчет разницы при завершении",
"Completion price": "Цена завершения",
"Completion price ($/1M tokens)": "Цена завершения ($/1 млн токенов)",
"Completion ratio": "Коэффициент завершения",
@@ -998,6 +1016,7 @@
"Conditions (AND)": "Условия (AND)",
"Confidence": "Уверенность",
"Config validation": "Проверка конфигурации",
+ "Configurable": "Настраивается",
"Configuration": "Конфигурация",
"Configuration File": "Файл конфигурации",
"Configuration for Creem payment integration": "Конфигурация для интеграции платежей Creem",
@@ -1005,6 +1024,8 @@
"Configuration for Stripe payment integration": "Конфигурация для интеграции платежей Stripe",
"Configuration key": "Ключ настройки",
"Configuration required": "Требуется настройка",
+ "Configuration valid": "Конфигурация корректна",
+ "Configuration validation": "Проверка конфигурации",
"Configure": "Настройка",
"Configure a Creem product for user recharge options.": "Настройте продукт Creem для опций пополнения пользователя.",
"Configure a custom ratio for when users use a specific token group.": "Настроить пользовательский коэффициент при использовании определённой группы токенов.",
@@ -1110,6 +1131,7 @@
"Control plane": "Плоскость управления",
"Control which models are exposed and which groups may use them.": "Управляйте тем, какие модели доступны и какие группы могут их использовать.",
"Controls how much the model thinks before answering": "Регулирует глубину размышлений модели перед ответом",
+ "Controls randomness and creativity": "Управляет случайностью и творческой вариативностью",
"Controls whether user verification (biometrics/PIN) is required during Passkey flows.": "Определяет, требуется ли проверка пользователя (биометрия/PIN) во время процессов Passkey.",
"Conversion rate from USD to your custom currency": "Курс конвертации из USD в вашу пользовательскую валюту",
"Convert reasoning_content to tag in content": "Преобразовать reasoning_content в тег в content",
@@ -1593,11 +1615,13 @@
"Email Field": "Поле email",
"Email Verification": "Верификация Email",
"Email, summarisation, knowledge work": "Электронная почта, резюме, knowledge work",
+ "embeddings": "Векторные представления",
"Embeddings": "Встраивания",
"Empty": "Пусто",
"Empty Completion Retry": "Повтор при пустом завершении",
"Empty value will be saved as {}.": "Пустое значение будет сохранено как {}.",
"Enable": "Включить",
+ "Enable {{parameter}}": "Включить {{parameter}}",
"Enable 2FA": "Включить 2FA",
"Enable All": "Включить все",
"Enable and price audio input before configuring audio output.": "Перед настройкой аудиовыхода включите и укажите цену аудиовхода.",
@@ -1647,6 +1671,7 @@
"Enabled Status": "Статус включения",
"Enabling...": "Включается...",
"Encourages introducing new topics": "Поощряет введение новых тем",
+ "Encourages new topics": "Поощряет новые темы",
"End": "End",
"End Error": "Ошибка завершения",
"End Reason": "Причина завершения",
@@ -1964,6 +1989,7 @@
"Fetch Models": "Получить модели",
"Fetched {{count}} model(s) from upstream": "Получено {{count}} моделей из upstream",
"Fetched {{count}} models": "Получено {{count}} моделей",
+ "Fetches model lists directly from the upstream provider.": "Получает список моделей непосредственно от вышестоящего провайдера.",
"Fetching prefill groups...": "Загрузка групп предварительного заполнения...",
"Fetching upstream prices...": "Получение цен провайдера...",
"Fetching upstream ratios...": "Загрузка коэффициентов upstream...",
@@ -2070,6 +2096,7 @@
"Forward requests directly to upstream providers without any post-processing.": "Перенаправлять запросы напрямую upstream-провайдерам без какой-либо постобработки.",
"Frames per second": "Кадров в секунду",
"Free: {{free}} / Total: {{total}}": "Свободно: {{free}} / Всего: {{total}}",
+ "Frequency Penalty": "Штраф за частотность",
"Friendly name to identify this channel": "Дружественное имя для идентификации этого канала",
"From Address": "Отправитель",
"From AI model access to Agent lifecycle governance": "От доступа к моделям ИИ к управлению жизненным циклом Agent",
@@ -2096,6 +2123,7 @@
"Gemini Generate Content": "Генерация контента Gemini",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content в OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K",
+ "Gemini native": "Нативный API Gemini",
"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 продолжит автоматически определять режим мышления, даже если адаптер отключен. Включайте это только тогда, когда вам нужен более тонкий контроль над ценообразованием и бюджетированием.",
"General": "Общие",
"General Settings": "Общие настройки",
@@ -2314,6 +2342,7 @@
"Image to Video": "Изображение в видео",
"Image Tokens": "Токены изображений",
"Image, video, music, polling, result proxying, status mapping, and refund handling.": "Изображения, видео, музыка, polling, проксирование результатов, сопоставление статусов и обработка возвратов.",
+ "images": "Изображения",
"Import to CC Switch": "Импорт в CC Switch",
"In Progress": "Выполняется",
"In:": "Вх:",
@@ -2451,6 +2480,7 @@
"Keep the platform ready": "Поддерживайте платформу в готовности",
"Keep this above 1 minute to avoid heavy database load": "Держите это значение выше 1 минуты, чтобы избежать высокой нагрузки на базу данных",
"Keep-alive Ping": "Пинг Keep-alive",
+ "Keeps compatible responses more repeatable": "Делает ответы более воспроизводимыми на совместимых моделях",
"Key": "Ключ",
"Key custody": "Хранение ключей",
"Key Fingerprint": "Отпечаток ключа",
@@ -2522,6 +2552,8 @@
"Limit Reached": "Достигнут лимит",
"Limit which models can be used with this key": "Ограничить модели, которые могут быть использованы с этим ключом",
"Limited": "Ограничено",
+ "Limited support": "Ограниченная поддержка",
+ "Limits token selection to a probability mass": "Ограничивает выбор токенов заданной вероятностной массой",
"LingYiWanWu": "LingYiWanWu",
"Link to your documentation site": "Ссылка на ваш сайт документации",
"LinuxDO": "LinuxDO",
@@ -2641,6 +2673,7 @@
"Max Success": "Макс. успешных",
"Max successful requests": "Макс. успешных запросов",
"Max Successful Requests": "Макс. успешных запросов",
+ "Max Tokens": "Максимум токенов",
"MAX-API-Next": "MAX-API-Next",
"Maximum 1000 characters. Supports Markdown and HTML.": "Максимум 1000 символов. Поддерживает Markdown и HTML.",
"Maximum 200 characters": "Максимум 200 символов",
@@ -2717,6 +2750,7 @@
"Model Description": "Описание модели",
"Model details": "Сведения о модели",
"Model disabled successfully": "Модель успешно отключена",
+ "model discovery": "Обнаружение моделей",
"Model enabled successfully": "Модель успешно включена",
"Model fixed pricing": "Фиксированная цена модели",
"Model governance": "Управление моделями",
@@ -2861,6 +2895,7 @@
"name@example.com": "name@example.com",
"Native format": "Собственный формат",
"Native forwarding": "Нативная пересылка",
+ "Native support": "Нативная поддержка",
"Navigation link": "Ссылка навигации",
"Navigation name": "Название навигации",
"Need a redemption code?": "Нужен код активации?",
@@ -2912,6 +2947,7 @@
"No backup": "Нет резервной копии",
"No base input price": "Нет базовой цены входа",
"No billing records found": "Записи о выставлении счетов не найдены",
+ "No blocking configuration issues were detected for this channel type.": "Для этого типа канала не обнаружено блокирующих проблем конфигурации.",
"No capabilities reported for this model.": "Для этой модели не указаны возможности.",
"No Change": "Без изменений",
"No changes": "Нет изменений",
@@ -3066,6 +3102,7 @@
"Not set yet": "Ещё не задано",
"Not Started": "Не начато",
"Not Submitted": "Не отправлено",
+ "Not supported": "Не поддерживается",
"Not tested": "Не протестировано",
"Not used for upstream training by default": "По умолчанию не используется для обучения у поставщика",
"Not used yet": "Ещё не использовано",
@@ -3135,6 +3172,7 @@
"Only allow specific email domains": "Разрешить только определенные домены электронной почты",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Доступно только для администраторов. При включении вы будете получать сводное уведомление выбранным способом, когда запланированная проверка моделей обнаружит изменения в вышестоящих моделях или сбои проверки.",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "Переопределяются только настроенные комбинации. Остальные вызовы используют базовый коэффициент группы токена.",
+ "Only enabled parameters are sent with the request.": "В запрос отправляются только включённые параметры.",
"Only override upstream submit/query paths and keep the official response parser": "Переопределять только пути отправки/запроса upstream и сохранять официальный парсер ответа",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Будут перезаписаны только выбранные поля. Вы можете повторно запустить мастер синхронизации, если появятся новые конфликты.",
"Only successful requests": "Только успешные запросы",
@@ -3266,6 +3304,7 @@
"Parameter override template must be a JSON object": "Шаблон переопределения параметров должен быть объектом JSON",
"Parameter pricing": "Цены параметров",
"Parameter pricing rules": "Правила цен параметров",
+ "Parameter settings": "Настройки параметров",
"parameter.": "параметр.",
"Parameters": "Параметры",
"Parsed {{count}} service account file(s)": "Проанализировано файлов сервисного аккаунта {{count}}",
@@ -3483,6 +3522,7 @@
"Prepend": "Добавить в начало",
"Prepend to Start": "Добавить в начало",
"Prepend value to array / string / object start": "Добавить значение в начало массива / строки / объекта",
+ "Presence Penalty": "Штраф за присутствие",
"Preserve the original field when applying this rule": "Сохранять исходное поле при применении этого правила",
"Preset groups": "Предустановленные группы",
"Preset recharge amounts (JSON array)": "Предустановленные суммы пополнения (массив JSON)",
@@ -3683,6 +3723,7 @@
"Reason:": "Причина:",
"Reasoning": "Обоснование",
"Reasoning Effort": "Интенсивность рассуждения",
+ "Recalculate successful DoubaoVideo tasks from upstream usage tokens when polling completes; turn this off only for emergency containment because successful tasks will keep the pre-consumed quota.": "Пересчитывать успешные задачи DoubaoVideo по upstream usage tokens после завершения опроса; отключайте только для экстренного сдерживания, так как успешные задачи сохранят предварительно списанную квоту.",
"Receive Upstream Model Update Notifications": "Получать уведомления об обновлениях вышестоящих моделей",
"Received": "Получено",
"Received amount": "Полученная сумма",
@@ -3724,6 +3765,7 @@
"Redirecting to GitHub...": "Перенаправление на GitHub...",
"Redirecting to payment page...": "Перенаправление на страницу оплаты...",
"Redirecting to sign in in {{seconds}} seconds.": "Переход на страницу входа через {{seconds}} сек.",
+ "Reduces repeated wording": "Уменьшает повторение формулировок",
"Reference Video": "Эталонное видео",
"Referral link:": "Реферальная ссылка:",
"Referral Program": "Реферальная программа",
@@ -3848,6 +3890,7 @@
"Required provider, authentication, model, and group settings": "Обязательные настройки провайдера, аутентификации, моделей и групп",
"Required to expose Midjourney-style image generation to end users.": "Необходимо для предоставления генерации изображений в стиле Midjourney конечным пользователям.",
"Requires Record quota usage to be enabled in Operations > Log Maintenance.": "Требуется включить «Записывать использование квоты» в разделе Операции > Журнал обслуживания.",
+ "rerank": "Реранжирование",
"Rerank": "Переранжировать",
"Reroll": "Повторить",
"Research assistant": "Исследовательский ассистент",
@@ -3893,6 +3936,7 @@
"Response path for upstream task ID": "Путь ответа для ID задачи апстрима",
"Response Time": "Время ответа",
"Response time: {{duration}}": "Время ответа: {{duration}}",
+ "responses": "API ответов",
"Responses API Version": "Версия API ответов",
"Responses APIs": "Responses API",
"Restore defaults": "Сбросить к значениям по умолчанию",
@@ -3947,6 +3991,15 @@
"Route, auth, and balance check in one place": "Маршрут, аутентификация и баланс в одном месте",
"route: weighted channel": "маршрут: взвешенный канал",
"Routes": "Маршруты",
+ "Routes Anthropic Messages-compatible requests.": "Маршрутизирует запросы, совместимые с Anthropic Messages.",
+ "Routes asynchronous video generation tasks.": "Маршрутизирует асинхронные задачи генерации видео.",
+ "Routes embedding vector requests.": "Маршрутизирует запросы векторных представлений.",
+ "Routes Gemini native /v1beta model requests.": "Маршрутизирует запросы к моделям через нативный Gemini /v1beta.",
+ "Routes image generation and editing requests.": "Маршрутизирует запросы генерации и редактирования изображений.",
+ "Routes OpenAI Responses API requests.": "Маршрутизирует запросы OpenAI Responses API.",
+ "Routes OpenAI-compatible chat and completion requests.": "Маршрутизирует OpenAI-совместимые запросы чата и завершений.",
+ "Routes search reranking requests.": "Маршрутизирует запросы переранжирования поиска.",
+ "Routes speech, transcription, translation, or music requests.": "Маршрутизирует запросы речи, транскрибации, перевода или музыки.",
"Routing": "Маршрутизация",
"Routing & Overrides": "Маршрутизация и переопределения",
"Routing and reliability governance": "Управление маршрутизацией и надежностью",
@@ -4083,6 +4136,7 @@
"Security boundary": "Граница безопасности",
"Security Check": "Проверка безопасности",
"Security verification": "Подтверждение безопасности",
+ "Seed": "Seed",
"Seedance": "Seedance (генерация видео)",
"Select": "Выбрать",
"Select {{item}}": "Выбрать {{item}}",
@@ -4241,6 +4295,7 @@
"Showcase core capabilities with demo credentials and limited access.": "Демонстрация основных возможностей с демо-учётными данными и ограниченным доступом.",
"Showing": "Отображать",
"showing •": "отображается •",
+ "Shows the request APIs, task capabilities, and operational features supported by this channel type.": "Показывает API запросов, возможности задач и рабочие функции, поддерживаемые этим типом канала.",
"Sidebar": "Боковая панель",
"Sidebar collapsed by default for new users": "Боковая панель свернута по умолчанию для новых пользователей",
"Sidebar modules": "Модули боковой панели",
@@ -4335,6 +4390,7 @@
"Status Page Slug": "Slug страницы статуса",
"Status Path": "Путь статуса",
"Status polling": "Опрос статуса",
+ "Status response path is required when the full task protocol is enabled.": "При включенном полном протоколе задач требуется путь к статусу в ответе.",
"Status short": "Стат.",
"Status:": "Статус:",
"Stay": "Остаться",
@@ -4482,6 +4538,7 @@
"Task History": "История задач",
"Task ID": "ID задачи",
"Task ID Path": "Путь ID задачи",
+ "Task ID response path is required when the full task protocol is enabled.": "При включенном полном протоколе задач требуется путь к ID задачи в ответе.",
"Task ID:": "ID задачи:",
"Task logs": "Журналы задач",
"Task Logs": "Журнал задач",
@@ -4501,6 +4558,7 @@
"Telegram": "Telegram",
"Telegram login requires widget integration; coming soon": "Вход через Telegram требует интеграции виджета; скоро",
"Telegram Login Widget": "Виджет входа Telegram",
+ "Temperature": "Температура",
"Template": "Шаблон",
"Template variables:": "Переменные шаблона:",
"Templates": "Шаблоны",
@@ -4587,6 +4645,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Для визуального редактора это выражение слишком сложно. Переключитесь в режим выражения для правки.",
"This feature is experimental. Configuration format and behavior may change.": "Эта функция является экспериментальной. Формат конфигурации и поведение могут измениться.",
"This feature requires server-side WeChat configuration": "Эта функция требует серверной конфигурации WeChat",
+ "This JSON configuration must be a JSON object.": "Эта конфигурация JSON должна быть объектом JSON.",
"This may cause cache failures.": "Это может привести к сбоям кэша.",
"This may take a few moments while we validate the request and update your session.": "Это может занять несколько мгновений, пока мы проверяем запрос и обновляем вашу сессию.",
"This model has both fixed price and ratio billing conflicts": "Эта модель имеет конфликты как фиксированной цены, так и пропорциональной тарификации",
@@ -4723,6 +4782,7 @@
"Top models": "Топ моделей",
"Top Models": "Лучшие модели",
"Top models by traffic": "Популярные модели по трафику",
+ "Top P": "Top P",
"Top up balance and view billing history.": "Пополнить баланс и просмотреть историю платежей.",
"Top Users": "Лучшие пользователи",
"Top vendors": "Топ поставщиков",
@@ -4896,6 +4956,7 @@
"upstream AI ecosystems": "вышестоящие AI-экосистемы",
"Upstream model and multimodal platforms": "Вышестоящие модельные и мультимодальные платформы",
"Upstream Model Detection Settings": "Настройки обнаружения моделей провайдера",
+ "Upstream model discovery is not supported for this channel type and will not run.": "Обнаружение моделей вышестоящего сервиса не поддерживается для этого типа канала и выполняться не будет.",
"Upstream Model Update Check": "Проверка обновлений моделей провайдера",
"Upstream Model Updates": "Обновления моделей источника",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Обновления моделей применены: {{added}} добавлено, {{removed}} удалено, {{ignored}} проигнорировано, всего {{totalIgnored}} проигнорированных моделей",
@@ -5048,15 +5109,25 @@
"Version Overrides": "Переопределения версий",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Режим API Key Vertex AI не поддерживает пакетное создание",
+ "Vertex AI API Key mode does not support batch creation.": "Режим API Key в Vertex AI не поддерживает пакетное создание.",
"Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.": "Vertex AI не поддерживает functionResponse.id. Включите, чтобы автоматически удалить это поле.",
"Vertex AI Key Format": "Формат ключа Vertex AI",
+ "Vertex AI region configuration must be a JSON object containing a default field.": "Конфигурация региона Vertex AI должна быть объектом JSON с полем default.",
+ "Vertex AI region configuration must contain a default field.": "Конфигурация региона Vertex AI должна содержать поле default.",
+ "Vertex AI service account credentials must be valid JSON.": "Учетные данные сервисного аккаунта Vertex AI должны быть корректным JSON.",
"Vertex AI service account key must be valid JSON": "Ключ сервисного аккаунта Vertex AI должен быть допустимым JSON",
"Video": "Видео",
"Video length in seconds": "Длительность видео в секундах",
"Video Remix": "Ремикс видео",
"video task adapters, status mappings, and protocol paths": "адаптеры видеозадач, сопоставления статусов и пути протоколов",
+ "Video Task Billing": "Биллинг видеозадач",
"video task channels and protocol templates": "каналы видеозадач и шаблоны протоколов",
"Video Task Protocol": "Протокол видеозадач",
+ "Video task protocol settings are only available for channel types that support video tasks.": "Настройки протокола видеозадач доступны только для типов каналов, поддерживающих видеозадачи.",
+ "Video task query path is required.": "Путь запроса видеозадачи обязателен.",
+ "Video task query path must contain {task_id}, {operation_name}, or {upstream_task_id}.": "Путь запроса видеозадачи должен содержать {task_id}, {operation_name} или {upstream_task_id}.",
+ "Video task submission path is required.": "Путь отправки видеозадачи обязателен.",
+ "video tasks": "Видеозадачи",
"Vidu": "Vidu",
"View": "Просмотр",
"View all currently available models": "Просмотреть все доступные модели",
@@ -5188,6 +5259,7 @@
"Wire encoding for the embedding vectors": "Кодирование векторов в передаче",
"with conflicts": "с конфликтами",
"with the API key from your token settings.": "на API-ключ из настроек токенов.",
+ "Without a result URL path, completed video tasks may not return a content URL.": "Без пути к URL результата завершенные видеозадачи могут не вернуть URL содержимого.",
"Without additional conditions, only the type above is used for pruning.": "Без дополнительных условий для очистки используется только тип выше.",
"Worker": "Воркер",
"Worker Access Key": "Ключ доступа воркера",
diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json
index 9e70edd7..19e53b3d 100644
--- a/web/default/src/i18n/locales/vi.json
+++ b/web/default/src/i18n/locales/vi.json
@@ -24,6 +24,7 @@
"{{category}} Models": "Mô hình {{category}}",
"{{completed}}/{{total}} completed": "Đã hoàn tất {{completed}}/{{total}}",
"{{count}} {{entity}} selected. Bulk actions toolbar is available.": "Đã chọn {{count}} {{entity}}. Thanh thao tác hàng loạt hiện khả dụng.",
+ "{{count}} blocking issue(s)": "{{count}} vấn đề chặn",
"{{count}} channel(s) deleted": "Đã xóa {{count}} kênh",
"{{count}} channel(s) disabled": "Đã tắt {{count}} kênh",
"{{count}} channel(s) enabled": "Đã bật {{count}} kênh",
@@ -49,6 +50,7 @@
"{{count}} tiered billing models enabled": "Đã bật tính phí theo bậc cho {{count}} mô hình",
"{{count}} tiers": "{{count}} bậc",
"{{count}} vendors": "{{count}} nhà cung cấp",
+ "{{count}} warning(s)": "{{count}} cảnh báo",
"{{count}} weeks ago": "{{count}} tuần trước",
"{{field}} updated to {{value}}": "{{field}} đã cập nhật thành {{value}}",
"{{field}} updated to {{value}} for tag: {{tag}}": "{{field}} đã cập nhật thành {{value}} cho nhãn: {{tag}}",
@@ -64,6 +66,7 @@
"{{routeContext}} groups must not be empty": "Các nhóm {{routeContext}} không được để trống",
"{{routeContext}} groups must not exceed 64 entries": "Các nhóm {{routeContext}} không được vượt quá 64 mục",
"{{success}} succeeded, {{failed}} failed": "{{success}} thành công, {{failed}} thất bại",
+ "{{supported}}/{{total}} available": "Khả dụng {{supported}}/{{total}}",
"{{target}} test failed": "Kiểm tra {{target}} thất bại",
"{{target}} test succeeded": "Kiểm tra {{target}} thành công",
"{{title}} usage: {{percent}}%": "Mức sử dụng {{title}}: {{percent}}%",
@@ -81,10 +84,12 @@
"`, and `-nothinking` suffixes while routing to the correct Gemini variant.": ", và",
"© 2025 Your Company. All rights reserved.": "© 2025 Công ty của bạn. Mọi quyền được bảo lưu.",
"+{{count}} more": "thêm {{count}} mục",
+ "+{{count}} more mapping(s)": "thêm {{count}} ánh xạ",
"= {{count}} tokens": "= {{count}} token",
"= {{count}}K tokens": "= {{count}} nghìn token",
"= {{count}}M tokens": "= {{count}} triệu token",
"| Based on": "| Dựa trên",
+ "$/1M tokens": "$/1 triệu token",
"0 means data is kept permanently": "0 nghĩa là dữ liệu được giữ vĩnh viễn",
"0 means unlimited": "0 có nghĩa là không giới hạn",
"0 means unlimited. The value is converted to quota units when saved.": "0 nghĩa là không giới hạn. Giá trị sẽ được chuyển thành đơn vị quota khi lưu.",
@@ -242,6 +247,7 @@
"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",
+ "Additional upstream configuration is required for this channel type.": "Loại kênh này yêu cầu cấu hình thượng nguồn bổ sung.",
"Adjust filters, then search to refresh the logs.": "Điều chỉnh bộ lọc, sau đó tìm kiếm để làm mới nhật ký.",
"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.",
@@ -438,6 +444,7 @@
"API key from the provider": "khóa API từ nhà cung cấp",
"API key is loading, please try again in a moment": "API key đang tải, vui lòng thử lại sau giây lát",
"API key is required": "Khóa API là bắt buộc",
+ "API Key is required when creating a channel.": "API Key là bắt buộc khi tạo kênh.",
"API Key mode (does not support batch creation)": "Chế độ Khóa API (không hỗ trợ tạo hàng loạt)",
"API Key mode: use APIKey|Region": "Chế độ khóa API: sử dụng APIKey|Region",
"API Key updated successfully": "API Key đã được cập nhật thành công",
@@ -505,9 +512,11 @@
"async task protocols": "giao thức tác vụ bất đồng bộ",
"Async task refund": "Hoàn tiền tác vụ bất đồng bộ",
"async task surfaces": "bề mặt tác vụ bất đồng bộ",
+ "At least one model must be published for this channel.": "Phải phát hành ít nhất một mô hình cho kênh này.",
"At least one model regex pattern is required": "Cần ít nhất một mẫu regex mô hình",
"At least one valid key source is required": "Cần ít nhất một nguồn khóa hợp lệ",
"Attach": "Đính kèm",
+ "audio": "Âm thanh",
"Audio": "Âm thanh",
"Audio comp.": "Nén âm thanh",
"Audio completion ratio": "Tỷ lệ hoàn thành âm thanh",
@@ -640,8 +649,10 @@
"Base rate limit windows for this account.": "Cửa sổ giới hạn tốc độ cơ bản cho tài khoản này.",
"Base URL": "URL cơ sở",
"Base URL is required for this channel type": "Loại kênh này yêu cầu Base URL",
+ "Base URL is required for this channel type.": "Base URL là bắt buộc đối với loại kênh này.",
"Base URL is required when a route upstream path is relative": "Bắt buộc có Base URL khi đường dẫn upstream của route là đường dẫn tương đối",
"Base URL of your Uptime Kuma instance": "URL cơ sở của phiên bản Uptime Kuma của bạn",
+ "Base URL should not usually end with /v1 because MAX API appends the upstream path automatically.": "Base URL thường không nên kết thúc bằng /v1 vì MAX API tự động nối đường dẫn thượng nguồn.",
"Basic Authentication": "Xác thực cơ bản",
"Basic Configuration": "Cấu hình cơ bản",
"Basic Info": "Thông tin cơ bản",
@@ -779,6 +790,7 @@
"Cancelled at": "Đã hủy lúc",
"Capabilities": "Khả năng",
"Capability checks": "Kiểm tra năng lực",
+ "Caps the response length": "Giới hạn độ dài phản hồi",
"Capture a reusable bundle of models, tags, or endpoints.": "Đóng gói một bộ có thể tái sử dụng gồm các mô hình, thẻ hoặc điểm cuối.",
"Card view": "Dạng thẻ",
"cards": "thẻ",
@@ -840,6 +852,7 @@
"Chat preset not found": "Thiết lập sẵn trò chuyện không tìm thấy",
"Chat Presets": "Cài đặt sẵn trò chuyện",
"Chat session management": "Quản lý phiên trò chuyện",
+ "chat/completions": "API chat/completions",
"ChatCompletions -> Responses Compatibility": "Tương thích ChatCompletions -> Phản hồi",
"Check for updates": "Kiểm tra cập nhật",
"Check in daily to receive random quota rewards": "Nhận phòng hàng ngày để nhận phần thưởng theo hạn ngạch ngẫu nhiên",
@@ -937,9 +950,12 @@
"Codex Account & Usage": "Tài khoản và sử dụng Codex",
"Codex Authorization": "Ủy quyền Codex",
"Codex channels do not support batch creation": "Kênh Codex không hỗ trợ tạo hàng loạt",
+ "Codex channels do not support batch creation.": "Kênh Codex không hỗ trợ tạo hàng loạt.",
"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",
"Codex credential must be a JSON object with access_token and account_id": "Thông tin xác thực Codex phải là đối tượng JSON có access_token và account_id",
+ "Codex credentials must be a valid JSON object.": "Thông tin xác thực Codex phải là một đối tượng JSON hợp lệ.",
+ "Codex credentials must contain access_token and account_id.": "Thông tin xác thực Codex phải chứa access_token và account_id.",
"Cohere": "Cohere",
"Collapse": "Thu gọn",
"Collapse All": "Thu gọn tất cả",
@@ -977,11 +993,13 @@
"Compatibility field for the default auto route. It is derived from the route config when saving.": "Trường tương thích cho tuyến tự động mặc định. Trường này được suy ra từ cấu hình tuyến khi lưu.",
"compatible API routes": "tuyến API tương thích",
"Compatible API routes for common AI application workflows": "Các tuyến API tương thích cho quy trình ứng dụng AI phổ biến",
+ "Compatible support": "Hỗ trợ tương thích",
"Complete API documentation with multi-language SDK support": "Tài liệu API đầy đủ với hỗ trợ SDK đa ngôn ngữ",
"Complete Order": "Hoàn thành đơn hàng",
"Complete these steps to finish the initial installation.": "Hoàn thành các bước này để hoàn tất quá trình cài đặt ban đầu.",
"Completed": "Hoàn thành",
"Completion": "Hoàn thành",
+ "Completion delta settlement": "Quyết toán chênh lệch khi hoàn tất",
"Completion price": "Giá hoàn thành",
"Completion price ($/1M tokens)": "Giá hoàn thành ($/1M tokens)",
"Completion ratio": "Tỷ lệ hoàn thành",
@@ -998,6 +1016,7 @@
"Conditions (AND)": "Điều kiện (AND)",
"Confidence": "Tự tin",
"Config validation": "Xác thực cấu hình",
+ "Configurable": "Có thể cấu hình",
"Configuration": "Cấu hình",
"Configuration File": "Tệp Cấu hình",
"Configuration for Creem payment integration": "Cấu hình tích hợp thanh toán Creem",
@@ -1005,6 +1024,8 @@
"Configuration for Stripe payment integration": "Cấu hình cho tích hợp thanh toán Stripe",
"Configuration key": "Khóa cấu hình",
"Configuration required": "Cần cấu hình",
+ "Configuration valid": "Cấu hình hợp lệ",
+ "Configuration validation": "Xác thực cấu hình",
"Configure": "Cấu hình",
"Configure a Creem product for user recharge options.": "Cấu hình một sản phẩm Creem cho các tùy chọn nạp tiền người dùng.",
"Configure a custom ratio for when users use a specific token group.": "Cấu hình tỷ lệ tùy chỉnh khi người dùng sử dụng nhóm token cụ thể.",
@@ -1110,6 +1131,7 @@
"Control plane": "Mặt phẳng điều khiển",
"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.",
"Controls how much the model thinks before answering": "Điều chỉnh mức suy luận trước khi trả lời",
+ "Controls randomness and creativity": "Kiểm soát tính ngẫu nhiên và khả năng sáng tạo",
"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 tag in content": "Chuyển đổi reasoning_content thành thẻ trong nội dung",
@@ -1593,11 +1615,13 @@
"Email Field": "Trường Email",
"Email Verification": "Xác minh Email",
"Email, summarisation, knowledge work": "Email, tóm tắt, làm việc tri thức",
+ "embeddings": "Nhúng",
"Embeddings": "Embeddings",
"Empty": "Trống",
"Empty Completion Retry": "Thử lại khi hoàn tất rỗng",
"Empty value will be saved as {}.": "Giá trị trống sẽ được lưu thành {}.",
"Enable": "Bật",
+ "Enable {{parameter}}": "Bật {{parameter}}",
"Enable 2FA": "Bật 2FA",
"Enable All": "Bật tất cả",
"Enable and price audio input before configuring audio output.": "Bật và đặt giá đầu vào âm thanh trước khi cấu hình đầu ra âm thanh.",
@@ -1647,6 +1671,7 @@
"Enabled Status": "Trạng thái kích hoạt",
"Enabling...": "Đang bật...",
"Encourages introducing new topics": "Khuyến khích chủ đề mới",
+ "Encourages new topics": "Khuyến khích chủ đề mới",
"End": "End",
"End Error": "Lỗi kết thúc",
"End Reason": "Lý do kết thúc",
@@ -1964,6 +1989,7 @@
"Fetch Models": "Tìm nạp Mô hình",
"Fetched {{count}} model(s) from upstream": "Đã lấy {{count}} mô hình từ upstream",
"Fetched {{count}} models": "Đã lấy {{count}} mô hình",
+ "Fetches model lists directly from the upstream provider.": "Lấy danh sách mô hình trực tiếp từ nhà cung cấp thượng nguồn.",
"Fetching prefill groups...": "Đang tải nhóm điền sẵn...",
"Fetching upstream prices...": "Đang lấy giá upstream...",
"Fetching upstream ratios...": "Đang lấy tỷ lệ thượng nguồn...",
@@ -2070,6 +2096,7 @@
"Forward requests directly to upstream providers without any post-processing.": "Chuyển tiếp các yêu cầu trực tiếp đến các nhà cung cấp ngược dòng mà không cần xử lý hậu kỳ nào.",
"Frames per second": "Khung hình / giây",
"Free: {{free}} / Total: {{total}}": "Còn trống: {{free}} / Tổng: {{total}}",
+ "Frequency Penalty": "Hình phạt tần suất",
"Friendly name to identify this channel": "Tên thân thiện để nhận dạng kênh này",
"From Address": "Địa chỉ Người gửi",
"From AI model access to Agent lifecycle governance": "Từ truy cập mô hình AI đến quản trị vòng đời Agent",
@@ -2096,6 +2123,7 @@
"Gemini Generate Content": "Gemini Generate Content",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content sang OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K",
+ "Gemini native": "API Gemini gốc",
"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",
@@ -2314,6 +2342,7 @@
"Image to Video": "Ảnh sang video",
"Image Tokens": "Token hình ảnh",
"Image, video, music, polling, result proxying, status mapping, and refund handling.": "Hình ảnh, video, nhạc, polling, proxy kết quả, ánh xạ trạng thái và xử lý hoàn tiền.",
+ "images": "Hình ảnh",
"Import to CC Switch": "Nhập vào CC Switch",
"In Progress": "Đang xử lý",
"In:": "Vào:",
@@ -2451,6 +2480,7 @@
"Keep the platform ready": "Giữ nền tảng luôn sẵn sàng",
"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ì",
+ "Keeps compatible responses more repeatable": "Giúp các phản hồi tương thích dễ lặp lại hơn",
"Key": "Khóa",
"Key custody": "Quản lý khóa",
"Key Fingerprint": "Vân tay khóa",
@@ -2522,6 +2552,8 @@
"Limit Reached": "Đã đạt giới hạn",
"Limit which models can be used with this key": "Giới hạn các mô hình có thể được sử dụng với khóa này",
"Limited": "Giới hạn",
+ "Limited support": "Hỗ trợ hạn chế",
+ "Limits token selection to a probability mass": "Giới hạn việc chọn token trong một khối lượng xác suất",
"LingYiWanWu": "LingYiWanWu",
"Link to your documentation site": "Liên kết đến trang web tài liệu của bạn",
"LinuxDO": "LinuxDO",
@@ -2641,6 +2673,7 @@
"Max Success": "Thành công tối đa",
"Max successful requests": "Số yêu cầu thành công tối đa",
"Max Successful Requests": "Yêu cầu thành công tối đa",
+ "Max Tokens": "Số token tối đa",
"MAX-API-Next": "MAX-API-Next",
"Maximum 1000 characters. Supports Markdown and HTML.": "Tối đa 1000 ký tự. Hỗ trợ Markdown và HTML.",
"Maximum 200 characters": "Tối đa 200 ký tự",
@@ -2717,6 +2750,7 @@
"Model Description": "Mô tả Mô hình",
"Model details": "Chi tiết mô hình",
"Model disabled successfully": "Model đã được vô hiệu hóa thành công",
+ "model discovery": "Khám phá mô hình",
"Model enabled successfully": "Model đã được kích hoạt thành công",
"Model fixed pricing": "Fixed-price model",
"Model governance": "Model governance",
@@ -2861,6 +2895,7 @@
"name@example.com": "name@example.com",
"Native format": "Định dạng gốc",
"Native forwarding": "Chuyển tiếp nguyên bản",
+ "Native support": "Hỗ trợ gốc",
"Navigation link": "Liên kết điều hướng",
"Navigation name": "Tên điều hướng",
"Need a redemption code?": "Cần mã đổi thưởng?",
@@ -2912,6 +2947,7 @@
"No backup": "Chưa sao lưu",
"No base input price": "Chưa có giá đầu vào cơ bản",
"No billing records found": "Không tìm thấy hồ sơ thanh toán",
+ "No blocking configuration issues were detected for this channel type.": "Không phát hiện vấn đề cấu hình chặn nào cho loại kênh này.",
"No capabilities reported for this model.": "Chưa có khả năng nào được báo cáo cho mô hình này.",
"No Change": "Không thay đổi",
"No changes": "Không có thay đổi",
@@ -3066,6 +3102,7 @@
"Not set yet": "Chưa thiết lập",
"Not Started": "Chưa bắt đầu",
"Not Submitted": "Chưa gửi",
+ "Not supported": "Không hỗ trợ",
"Not tested": "Chưa kiểm tra",
"Not used for upstream training by default": "Mặc định không dùng để huấn luyện ở phía nhà cung cấp",
"Not used yet": "Chưa sử dụng",
@@ -3135,6 +3172,7 @@
"Only allow specific email domains": "Chỉ cho phép các tên miền email cụ thể",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Chỉ khả dụng cho quản trị viên. Khi bật, bạn sẽ nhận được thông báo tổng hợp qua phương thức đã chọn khi kiểm tra mô hình định kỳ phát hiện thay đổi mô hình nguồn hoặc lỗi kiểm tra.",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "Chỉ các tổ hợp đã cấu hình mới bị ghi đè. Các lệnh gọi khác giữ tỷ lệ cơ bản của nhóm token.",
+ "Only enabled parameters are sent with the request.": "Chỉ gửi các tham số đã bật trong yêu cầu.",
"Only override upstream submit/query paths and keep the official response parser": "Chỉ ghi đè đường dẫn gửi/truy vấn upstream và giữ bộ phân tích phản hồi chính thức",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Chỉ các trường được chọn sẽ bị ghi đè. Bạn có thể chạy lại trình hướng dẫn đồng bộ hóa nếu có xung đột mới xuất hiện.",
"Only successful requests": "Chỉ các yêu cầu thành công",
@@ -3266,6 +3304,7 @@
"Parameter override template must be a JSON object": "Mẫu ghi đè tham số phải là đối tượng JSON",
"Parameter pricing": "Định giá tham số",
"Parameter pricing rules": "Quy tắc định giá tham số",
+ "Parameter settings": "Cài đặt tham số",
"parameter.": "tham số",
"Parameters": "Tham số",
"Parsed {{count}} service account file(s)": "Đã phân tích {{count}} tệp tài khoản dịch vụ",
@@ -3483,6 +3522,7 @@
"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",
+ "Presence Penalty": "Hình phạt hiện diện",
"Preserve the original field when applying this rule": "Giữ trường gốc khi áp dụng quy tắc này",
"Preset groups": "Nhóm đặt sẵn",
"Preset recharge amounts (JSON array)": "Số tiền nạp đặt trước (mảng JSON)",
@@ -3683,6 +3723,7 @@
"Reason:": "Lý do:",
"Reasoning": "Lý luận",
"Reasoning Effort": "Cường độ suy luận",
+ "Recalculate successful DoubaoVideo tasks from upstream usage tokens when polling completes; turn this off only for emergency containment because successful tasks will keep the pre-consumed quota.": "Tính lại các tác vụ DoubaoVideo thành công theo usage token từ upstream khi polling hoàn tất; chỉ tắt trong trường hợp khẩn cấp vì tác vụ thành công sẽ giữ nguyên quota đã trừ trước.",
"Receive Upstream Model Update Notifications": "Nhận thông báo cập nhật mô hình nguồn",
"Received": "Đã nhận",
"Received amount": "Số tiền đã nhận",
@@ -3724,6 +3765,7 @@
"Redirecting to GitHub...": "Đang chuyển hướng đến GitHub...",
"Redirecting to payment page...": "Đang chuyển hướng đến trang thanh toán...",
"Redirecting to sign in in {{seconds}} seconds.": "Đang chuyển đến trang đăng nhập sau {{seconds}} giây.",
+ "Reduces repeated wording": "Giảm lặp lại từ ngữ",
"Reference Video": "Video tham chiếu",
"Referral link:": "Liên kết giới thiệu:",
"Referral Program": "Chương trình Giới thiệu",
@@ -3848,6 +3890,7 @@
"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.",
"Requires Record quota usage to be enabled in Operations > Log Maintenance.": "Cần bật “Ghi lại mức sử dụng hạn mức” trong Vận hành > Bảo trì nhật ký.",
+ "rerank": "Xếp hạng lại",
"Rerank": "Re-rank",
"Reroll": "Quay lại",
"Research assistant": "Research assistant",
@@ -3893,6 +3936,7 @@
"Response path for upstream task ID": "Đường dẫn phản hồi cho ID tác vụ upstream",
"Response Time": "Thời gian phản hồi",
"Response time: {{duration}}": "Thời gian phản hồi: {{duration}}",
+ "responses": "API phản hồi",
"Responses API Version": "Phiên bản API Phản hồi",
"Responses APIs": "Responses API",
"Restore defaults": "Khôi phục mặc định",
@@ -3947,6 +3991,15 @@
"Route, auth, and balance check in one place": "Kiểm tra tuyến, xác thực và số dư ở cùng một nơi",
"route: weighted channel": "định tuyến: kênh có trọng số",
"Routes": "Route",
+ "Routes Anthropic Messages-compatible requests.": "Định tuyến yêu cầu tương thích Anthropic Messages.",
+ "Routes asynchronous video generation tasks.": "Định tuyến tác vụ tạo video bất đồng bộ.",
+ "Routes embedding vector requests.": "Định tuyến yêu cầu vector embedding.",
+ "Routes Gemini native /v1beta model requests.": "Định tuyến yêu cầu mô hình Gemini gốc /v1beta.",
+ "Routes image generation and editing requests.": "Định tuyến yêu cầu tạo và chỉnh sửa ảnh.",
+ "Routes OpenAI Responses API requests.": "Định tuyến yêu cầu OpenAI Responses API.",
+ "Routes OpenAI-compatible chat and completion requests.": "Định tuyến các yêu cầu chat và completion tương thích OpenAI.",
+ "Routes search reranking requests.": "Định tuyến yêu cầu xếp hạng lại kết quả tìm kiếm.",
+ "Routes speech, transcription, translation, or music requests.": "Định tuyến yêu cầu giọng nói, phiên âm, dịch thuật hoặc âm nhạc.",
"Routing": "Định tuyến",
"Routing & Overrides": "Định tuyến & ghi đè",
"Routing and reliability governance": "Quản trị định tuyến và độ tin cậy",
@@ -4083,6 +4136,7 @@
"Security boundary": "Ranh giới bảo mật",
"Security Check": "Kiểm tra bảo mật",
"Security verification": "Xác minh bảo mật",
+ "Seed": "Seed",
"Seedance": "Seedance",
"Select": "Chọn",
"Select {{item}}": "Chọn {{item}}",
@@ -4241,6 +4295,7 @@
"Showcase core capabilities with demo credentials and limited access.": "Trình diễn các tính năng cốt lõi với thông tin đăng nhập demo và quyền truy cập hạn chế.",
"Showing": "Đang hiển thị",
"showing •": "hiển thị •",
+ "Shows the request APIs, task capabilities, and operational features supported by this channel type.": "Hiển thị API yêu cầu, khả năng tác vụ và tính năng vận hành được loại kênh này hỗ trợ.",
"Sidebar": "Thanh bên",
"Sidebar collapsed by default for new users": "Thanh bên được thu gọn theo mặc định đối với người dùng mới",
"Sidebar modules": "Mô-đun thanh bên",
@@ -4335,6 +4390,7 @@
"Status Page Slug": "Đường dẫn phụ trang trạng thái",
"Status Path": "Đường dẫn trạng thái",
"Status polling": "Polling trạng thái",
+ "Status response path is required when the full task protocol is enabled.": "Đường dẫn trạng thái trong phản hồi là bắt buộc khi bật giao thức tác vụ đầy đủ.",
"Status short": "TT",
"Status:": "Trạng thái:",
"Stay": "Ở lại",
@@ -4482,6 +4538,7 @@
"Task History": "Lịch sử tác vụ",
"Task ID": "Mã nhiệm vụ",
"Task ID Path": "Đường dẫn ID tác vụ",
+ "Task ID response path is required when the full task protocol is enabled.": "Đường dẫn ID tác vụ trong phản hồi là bắt buộc khi bật giao thức tác vụ đầy đủ.",
"Task ID:": "ID nhiệm vụ:",
"Task logs": "Nhật ký tác vụ",
"Task Logs": "Nhật ký tác vụ",
@@ -4501,6 +4558,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",
+ "Temperature": "Nhiệt độ",
"Template": "Mẫu",
"Template variables:": "Biến mẫu:",
"Templates": "Mẫu",
@@ -4587,6 +4645,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Biểu thức này quá phức tạp cho trình sửa trực quan. Hãy chuyển sang chế độ biểu thức để chỉnh sửa.",
"This feature is experimental. Configuration format and behavior may change.": "Tính năng này đang ở giai đoạn thử nghiệm. Định dạng cấu hình và hành vi có thể thay đổi.",
"This feature requires server-side WeChat configuration": "Tính năng này yêu cầu cấu hình WeChat phía máy chủ",
+ "This JSON configuration must be a JSON object.": "Cấu hình JSON này phải là một đối tượng JSON.",
"This may cause cache failures.": "Điều này có thể gây ra lỗi bộ nhớ đệm.",
"This may take a few moments while we validate the request and update your session.": "Việc này có thể mất vài phút trong khi chúng tôi xác thực yêu cầu và cập nhật phiên của bạn.",
"This model has both fixed price and ratio billing conflicts": "Mô hình này có cả mâu thuẫn về thanh toán theo giá cố định và theo tỷ lệ.",
@@ -4723,6 +4782,7 @@
"Top models": "Mô hình hàng đầu",
"Top Models": "Người mẫu hàng đầu",
"Top models by traffic": "Mô hình có lượng truy cập cao nhất",
+ "Top P": "Top P",
"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 vendors": "Nhà cung cấp hàng đầu",
@@ -4896,6 +4956,7 @@
"upstream AI ecosystems": "hệ sinh thái AI thượng nguồn",
"Upstream model and multimodal platforms": "Nền tảng mô hình thượng nguồn và đa phương thức",
"Upstream Model Detection Settings": "Cài đặt phát hiện mô hình nguồn",
+ "Upstream model discovery is not supported for this channel type and will not run.": "Loại kênh này không hỗ trợ khám phá mô hình thượng nguồn nên tính năng sẽ không chạy.",
"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",
@@ -5048,15 +5109,25 @@
"Version Overrides": "Ghi đè phiên bản",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Chế độ API Key của Vertex AI không hỗ trợ tạo hàng loạt",
+ "Vertex AI API Key mode does not support batch creation.": "Chế độ API Key của Vertex AI không hỗ trợ tạo hàng loạt.",
"Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.": "Vertex AI không hỗ trợ functionResponse.id. Bật để tự động loại bỏ trường này.",
"Vertex AI Key Format": "Định dạng khóa Vertex AI",
+ "Vertex AI region configuration must be a JSON object containing a default field.": "Cấu hình vùng Vertex AI phải là đối tượng JSON chứa trường default.",
+ "Vertex AI region configuration must contain a default field.": "Cấu hình vùng Vertex AI phải chứa trường default.",
+ "Vertex AI service account credentials must be valid JSON.": "Thông tin xác thực tài khoản dịch vụ Vertex AI phải là JSON hợp lệ.",
"Vertex AI service account key must be valid JSON": "Khóa tài khoản dịch vụ Vertex AI phải là JSON hợp lệ",
"Video": "Video",
"Video length in seconds": "Độ dài video (giây)",
"Video Remix": "Remix video",
"video task adapters, status mappings, and protocol paths": "bộ điều hợp tác vụ video, ánh xạ trạng thái và đường dẫn giao thức",
+ "Video Task Billing": "Tính phí tác vụ video",
"video task channels and protocol templates": "kênh tác vụ video và mẫu giao thức",
"Video Task Protocol": "Giao thức tác vụ video",
+ "Video task protocol settings are only available for channel types that support video tasks.": "Cài đặt giao thức tác vụ video chỉ khả dụng cho loại kênh hỗ trợ tác vụ video.",
+ "Video task query path is required.": "Đường dẫn truy vấn tác vụ video là bắt buộc.",
+ "Video task query path must contain {task_id}, {operation_name}, or {upstream_task_id}.": "Đường dẫn truy vấn tác vụ video phải chứa {task_id}, {operation_name} hoặc {upstream_task_id}.",
+ "Video task submission path is required.": "Đường dẫn gửi tác vụ video là bắt buộc.",
+ "video tasks": "Tác vụ video",
"Vidu": "Vidu",
"View": "Xem",
"View all currently available models": "Xem tất cả mô hình hiện có",
@@ -5188,6 +5259,7 @@
"Wire encoding for the embedding vectors": "Định dạng truyền cho vector embedding",
"with conflicts": "với các xung đột",
"with the API key from your token settings.": "bằng API key từ trang Tokens của bạn.",
+ "Without a result URL path, completed video tasks may not return a content URL.": "Nếu không có đường dẫn URL kết quả, tác vụ video đã hoàn thành có thể không trả về URL nội dung.",
"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": "Nút worker",
"Worker Access Key": "Khóa truy cập nhân viên",
diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json
index 3e227824..80db13e0 100644
--- a/web/default/src/i18n/locales/zh.json
+++ b/web/default/src/i18n/locales/zh.json
@@ -24,6 +24,7 @@
"{{category}} Models": "{{category}} 模型",
"{{completed}}/{{total}} completed": "已完成 {{completed}}/{{total}}",
"{{count}} {{entity}} selected. Bulk actions toolbar is available.": "已选择 {{count}} 个{{entity}}。批量操作工具栏现已可用。",
+ "{{count}} blocking issue(s)": "{{count}} 个阻断问题",
"{{count}} channel(s) deleted": "已删除 {{count}} 个渠道",
"{{count}} channel(s) disabled": "已禁用 {{count}} 个渠道",
"{{count}} channel(s) enabled": "已启用 {{count}} 个渠道",
@@ -49,6 +50,7 @@
"{{count}} tiered billing models enabled": "已启用 {{count}} 个分阶段计费模型",
"{{count}} tiers": "{{count}} 档",
"{{count}} vendors": "{{count}} 家厂商",
+ "{{count}} warning(s)": "{{count}} 个警告",
"{{count}} weeks ago": "{{count}} 周前",
"{{field}} updated to {{value}}": "{{field}} 已更新为 {{value}}",
"{{field}} updated to {{value}} for tag: {{tag}}": "标签「{{tag}}」的 {{field}} 已更新为 {{value}}",
@@ -64,6 +66,7 @@
"{{routeContext}} groups must not be empty": "{{routeContext}} 分组不能为空",
"{{routeContext}} groups must not exceed 64 entries": "{{routeContext}} 分组不能超过 64 项",
"{{success}} succeeded, {{failed}} failed": "{{success}} 个成功,{{failed}} 个失败",
+ "{{supported}}/{{total}} available": "{{supported}}/{{total}} 项可用",
"{{target}} test failed": "{{target}} 测试失败",
"{{target}} test succeeded": "{{target}} 测试成功",
"{{title}} usage: {{percent}}%": "{{title}} 使用率:{{percent}}%",
@@ -81,10 +84,12 @@
"`, and `-nothinking` suffixes while routing to the correct Gemini variant.": "`, 和 `-nothinking` 后缀,同时路由到正确的 Gemini 变体。",
"© 2025 Your Company. All rights reserved.": "© 2025 您的公司。保留所有权利。",
"+{{count}} more": "还有 {{count}} 项",
+ "+{{count}} more mapping(s)": "+{{count}} 个映射",
"= {{count}} tokens": "= {{count}} 个 token",
"= {{count}}K tokens": "= {{count}}K 个 token",
"= {{count}}M tokens": "= {{count}}M 个 token",
"| Based on": "| 基于",
+ "$/1M tokens": "$/100 万 token",
"0 means data is kept permanently": "0 表示永久保留数据",
"0 means unlimited": "0 表示不限",
"0 means unlimited. The value is converted to quota units when saved.": "0 表示无限制。保存时会转换为额度单位。",
@@ -242,6 +247,7 @@
"Additional Limit": "附加额度",
"Additional Limits": "附加额度",
"Additional metered capability": "附加计费能力",
+ "Additional upstream configuration is required for this channel type.": "当前渠道类型需要填写额外的上游配置。",
"Adjust filters, then search to refresh the logs.": "调整筛选条件,然后搜索以刷新日志。",
"Adjust Quota": "调整额度",
"Adjust response formatting, prompt behavior, proxy, and upstream automation.": "调整响应格式、提示词行为、代理和上游自动化。",
@@ -438,6 +444,7 @@
"API key from the provider": "来自提供商的 API 密钥",
"API key is loading, please try again in a moment": "API Key 正在加载,请稍后再试",
"API key is required": "需要 API 密钥",
+ "API Key is required when creating a channel.": "新建渠道时 API Key 不能为空。",
"API Key mode (does not support batch creation)": "API Key 模式(不支持批量创建)",
"API Key mode: use APIKey|Region": "API Key 模式:使用 APIKey|Region",
"API Key updated successfully": "API 密钥更新成功",
@@ -505,9 +512,11 @@
"async task protocols": "异步任务协议",
"Async task refund": "异步任务退款",
"async task surfaces": "异步任务面",
+ "At least one model must be published for this channel.": "该渠道至少需要发布一个模型。",
"At least one model regex pattern is required": "至少需要一个模型正则匹配模式",
"At least one valid key source is required": "至少需要一个有效的密钥来源",
"Attach": "附加",
+ "audio": "音频",
"Audio": "音频",
"Audio comp.": "音频补全",
"Audio completion ratio": "音频补全倍率",
@@ -640,8 +649,10 @@
"Base rate limit windows for this account.": "当前账号的基础额度窗口。",
"Base URL": "API 地址",
"Base URL is required for this channel type": "此渠道类型需要填写 Base URL",
+ "Base URL is required for this channel type.": "当前渠道类型需要填写 Base URL。",
"Base URL is required when a route upstream path is relative": "路由上游路径为相对路径时必须填写 Base URL",
"Base URL of your Uptime Kuma instance": "您的 Uptime Kuma 实例的基础 URL",
+ "Base URL should not usually end with /v1 because MAX API appends the upstream path automatically.": "Base URL 通常不应以 /v1 结尾,MAX API 会自动拼接上游路径。",
"Basic Authentication": "基本身份验证",
"Basic Configuration": "基本配置",
"Basic Info": "基本信息",
@@ -779,6 +790,7 @@
"Cancelled at": "作废于",
"Capabilities": "能力",
"Capability checks": "能力检查",
+ "Caps the response length": "限制回复长度",
"Capture a reusable bundle of models, tags, or endpoints.": "捕获可重用的模型、标签或端点捆绑包。",
"Card view": "卡片视图",
"cards": "个规则卡",
@@ -840,6 +852,7 @@
"Chat preset not found": "未找到聊天预设",
"Chat Presets": "聊天预设",
"Chat session management": "聊天会话管理",
+ "chat/completions": "聊天 / 补全 API",
"ChatCompletions -> Responses Compatibility": "ChatCompletions → 响应兼容",
"Check for updates": "检查更新",
"Check in daily to receive random quota rewards": "每日签到可获得随机额度奖励",
@@ -937,9 +950,12 @@
"Codex Account & Usage": "Codex 账户和用量",
"Codex Authorization": "Codex 授权",
"Codex channels do not support batch creation": "Codex 渠道不支持批量创建",
+ "Codex channels do not support batch creation.": "Codex 渠道不支持批量创建。",
"Codex channels use an OAuth JSON credential as the key.": "Codex 频道使用 OAuth JSON 凭据作为密钥。",
"Codex CLI Header Passthrough": "Codex CLI 请求头透传",
"Codex credential must be a JSON object with access_token and account_id": "Codex 凭据必须是包含 access_token 和 account_id 的 JSON 对象",
+ "Codex credentials must be a valid JSON object.": "Codex 凭证必须是有效的 JSON 对象。",
+ "Codex credentials must contain access_token and account_id.": "Codex 凭证必须包含 access_token 和 account_id 字段。",
"Cohere": "Cohere",
"Collapse": "收起",
"Collapse All": "全部收起",
@@ -977,11 +993,13 @@
"Compatibility field for the default auto route. It is derived from the route config when saving.": "默认自动链路的兼容字段。保存时会从链路配置中派生。",
"compatible API routes": "兼容 API 路由",
"Compatible API routes for common AI application workflows": "兼容常见 AI 应用工作流的 API 路由",
+ "Compatible support": "兼容支持",
"Complete API documentation with multi-language SDK support": "完整的 API 文档,支持多语言 SDK",
"Complete Order": "补单",
"Complete these steps to finish the initial installation.": "完成这些步骤以完成初始安装。",
"Completed": "已完成",
"Completion": "补全",
+ "Completion delta settlement": "完成态差额结算",
"Completion price": "补全价格",
"Completion price ($/1M tokens)": "完成价格(美元/百万令牌)",
"Completion ratio": "补全倍率",
@@ -998,6 +1016,7 @@
"Conditions (AND)": "条件(AND)",
"Confidence": "置信度",
"Config validation": "配置校验",
+ "Configurable": "可配置",
"Configuration": "配置",
"Configuration File": "配置文件",
"Configuration for Creem payment integration": "Creem 支付集成的配置",
@@ -1005,6 +1024,8 @@
"Configuration for Stripe payment integration": "Stripe 支付集成的配置",
"Configuration key": "配置项",
"Configuration required": "需要配置",
+ "Configuration valid": "配置有效",
+ "Configuration validation": "配置校验",
"Configure": "配置",
"Configure a Creem product for user recharge options.": "为用户充值选项配置 Creem 产品。",
"Configure a custom ratio for when users use a specific token group.": "配置用户使用特定令牌分组时的自定义倍率。",
@@ -1110,6 +1131,7 @@
"Control plane": "控制面",
"Control which models are exposed and which groups may use them.": "控制对外暴露的模型,以及哪些分组可以使用它们。",
"Controls how much the model thinks before answering": "控制模型回答前的推理深度",
+ "Controls randomness and creativity": "控制随机性和创造性",
"Controls whether user verification (biometrics/PIN) is required during Passkey flows.": "控制在通行密钥流程中是否需要用户验证(生物识别/PIN)。",
"Conversion rate from USD to your custom currency": "从美元到您的自定义货币的转换率",
"Convert reasoning_content to tag in content": "将 reasoning_content 转换为 content 中的 标签",
@@ -1593,11 +1615,13 @@
"Email Field": "邮箱字段",
"Email Verification": "电子邮件验证",
"Email, summarisation, knowledge work": "邮件、摘要与知识工作",
+ "embeddings": "向量嵌入",
"Embeddings": "嵌入",
"Empty": "空",
"Empty Completion Retry": "空补全重试",
"Empty value will be saved as {}.": "空值将保存为 {}。",
"Enable": "启用",
+ "Enable {{parameter}}": "启用{{parameter}}",
"Enable 2FA": "启用 2FA",
"Enable All": "启用全部",
"Enable and price audio input before configuring audio output.": "请先启用音频输入并设置价格,然后再配置音频输出。",
@@ -1647,6 +1671,7 @@
"Enabled Status": "启用状态",
"Enabling...": "正在启用...",
"Encourages introducing new topics": "鼓励引入新话题",
+ "Encourages new topics": "鼓励引入新主题",
"End": "结束",
"End Error": "结束错误",
"End Reason": "结束原因",
@@ -1964,6 +1989,7 @@
"Fetch Models": "获取模型",
"Fetched {{count}} model(s) from upstream": "从上游获取了 {{count}} 个模型",
"Fetched {{count}} models": "已获取 {{count}} 个模型",
+ "Fetches model lists directly from the upstream provider.": "用于直接从上游拉取模型列表。",
"Fetching prefill groups...": "正在获取预填充分组...",
"Fetching upstream prices...": "正在获取上游价格...",
"Fetching upstream ratios...": "正在获取上游比例...",
@@ -2070,6 +2096,7 @@
"Forward requests directly to upstream providers without any post-processing.": "将请求直接转发给上游提供商,不进行任何后处理。",
"Frames per second": "帧率",
"Free: {{free}} / Total: {{total}}": "可用空间: {{free}} / 总空间: {{total}}",
+ "Frequency Penalty": "频率惩罚",
"Friendly name to identify this channel": "用于识别此渠道的友好名称",
"From Address": "发件地址",
"From AI model access to Agent lifecycle governance": "从 AI 模型接入到 Agent 全生命周期治理",
@@ -2096,6 +2123,7 @@
"Gemini Generate Content": "Gemini 内容生成",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content 到 OpenAI Chat",
"Gemini Image 4K": "Gemini 图片 4K",
+ "Gemini native": "Gemini 原生 API",
"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 也会继续自动检测思维模式。仅当您需要对定价和预算进行更精细的控制时才启用此选项。",
"General": "常规",
"General Settings": "通用设置",
@@ -2314,6 +2342,7 @@
"Image to Video": "图生视频",
"Image Tokens": "图像 Token",
"Image, video, music, polling, result proxying, status mapping, and refund handling.": "图片、视频、音乐、轮询、结果代理、状态映射和退款处理。",
+ "images": "图像",
"Import to CC Switch": "填入 CC Switch",
"In Progress": "进行中",
"In:": "入:",
@@ -2451,6 +2480,7 @@
"Keep the platform ready": "保持平台就绪",
"Keep this above 1 minute to avoid heavy database load": "保持此值大于 1 分钟以避免数据库负载过重",
"Keep-alive Ping": "保持连接心跳",
+ "Keeps compatible responses more repeatable": "让兼容的响应更具可重复性",
"Key": "密钥",
"Key custody": "密钥托管",
"Key Fingerprint": "Key 指纹",
@@ -2522,6 +2552,8 @@
"Limit Reached": "已达上限",
"Limit which models can be used with this key": "限制此密钥可使用的模型",
"Limited": "受限",
+ "Limited support": "有限支持",
+ "Limits token selection to a probability mass": "将 Token 选择限制在指定概率质量内",
"LingYiWanWu": "LingYiWanWu",
"Link to your documentation site": "您的文档站点链接",
"LinuxDO": "LinuxDO",
@@ -2641,6 +2673,7 @@
"Max Success": "最大成功数",
"Max successful requests": "最大成功请求数",
"Max Successful Requests": "最大成功请求数",
+ "Max Tokens": "最大 Token 数",
"MAX-API-Next": "MAX-API-Next",
"Maximum 1000 characters. Supports Markdown and HTML.": "最多 1000 个字符。支持 Markdown 和 HTML。",
"Maximum 200 characters": "最多 200 个字符",
@@ -2717,6 +2750,7 @@
"Model Description": "模型描述",
"Model details": "模型详情",
"Model disabled successfully": "模型禁用成功",
+ "model discovery": "模型发现",
"Model enabled successfully": "模型启用成功",
"Model fixed pricing": "模型固定定价",
"Model governance": "模型治理",
@@ -2861,6 +2895,7 @@
"name@example.com": "name@example.com",
"Native format": "原生格式",
"Native forwarding": "原生转发",
+ "Native support": "原生支持",
"Navigation link": "导航链接",
"Navigation name": "导航名称",
"Need a redemption code?": "需要兑换码?",
@@ -2912,6 +2947,7 @@
"No backup": "无备份",
"No base input price": "未设置基础输入价格",
"No billing records found": "未找到账单记录",
+ "No blocking configuration issues were detected for this channel type.": "当前渠道类型未检测到阻断性配置问题。",
"No capabilities reported for this model.": "该模型暂未报告任何能力。",
"No Change": "无变化",
"No changes": "没有变更",
@@ -3066,6 +3102,7 @@
"Not set yet": "尚未设置",
"Not Started": "未开始",
"Not Submitted": "未提交",
+ "Not supported": "不支持",
"Not tested": "未测试",
"Not used for upstream training by default": "默认不会用于上游训练",
"Not used yet": "暂未使用",
@@ -3135,6 +3172,7 @@
"Only allow specific email domains": "仅允许特定的电子邮件域名",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "仅管理员可用。启用后,当定时模型检查检测到上游模型变更或检查失败时,您将通过所选方式收到汇总通知。",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "只有已配置的组合会被覆盖,其他调用仍使用令牌分组的基础倍率。",
+ "Only enabled parameters are sent with the request.": "仅发送已启用的参数。",
"Only override upstream submit/query paths and keep the official response parser": "仅覆盖上游提交/查询路径,并保留官方响应解析器",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "仅选定的字段将被覆盖。如果出现新的冲突,您可以重新运行同步向导。",
"Only successful requests": "仅成功的请求",
@@ -3266,6 +3304,7 @@
"Parameter override template must be a JSON object": "参数覆盖模板必须是 JSON 对象",
"Parameter pricing": "参数定价",
"Parameter pricing rules": "参数计费规则",
+ "Parameter settings": "参数设置",
"parameter.": "参数。",
"Parameters": "参数",
"Parsed {{count}} service account file(s)": "已解析 {{count}} 个服务账号文件",
@@ -3483,6 +3522,7 @@
"Prepend": "前置追加",
"Prepend to Start": "追加到开头",
"Prepend value to array / string / object start": "把值追加到数组/字符串/对象开头",
+ "Presence Penalty": "存在惩罚",
"Preserve the original field when applying this rule": "应用此规则时保留原始字段",
"Preset groups": "预设分组",
"Preset recharge amounts (JSON array)": "预设充值金额(JSON 数组)",
@@ -3683,6 +3723,7 @@
"Reason:": "原因:",
"Reasoning": "推理",
"Reasoning Effort": "推理强度",
+ "Recalculate successful DoubaoVideo tasks from upstream usage tokens when polling completes; turn this off only for emergency containment because successful tasks will keep the pre-consumed quota.": "轮询完成时按上游 usage token 重新计算成功的 DoubaoVideo 任务;仅在应急止损时关闭,否则成功任务会保留预扣额度。",
"Receive Upstream Model Update Notifications": "接收上游模型更新通知",
"Received": "获得",
"Received amount": "实收金额",
@@ -3724,6 +3765,7 @@
"Redirecting to GitHub...": "正在跳转 GitHub...",
"Redirecting to payment page...": "正在重定向到支付页面...",
"Redirecting to sign in in {{seconds}} seconds.": "将在 {{seconds}} 秒后跳转到登录页。",
+ "Reduces repeated wording": "减少重复措辞",
"Reference Video": "参照生视频",
"Referral link:": "推荐链接:",
"Referral Program": "推荐计划",
@@ -3848,6 +3890,7 @@
"Required provider, authentication, model, and group settings": "必填的供应商、鉴权、模型和分组设置",
"Required to expose Midjourney-style image generation to end users.": "需要向终端用户开放 Midjourney 风格的图像生成。",
"Requires Record quota usage to be enabled in Operations > Log Maintenance.": "需要先在“运维 > 日志维护”中启用“记录配额使用量”。",
+ "rerank": "重排序",
"Rerank": "重新排序",
"Reroll": "重绘",
"Research assistant": "研究助手",
@@ -3893,6 +3936,7 @@
"Response path for upstream task ID": "上游任务 ID 的响应路径",
"Response Time": "响应时间",
"Response time: {{duration}}": "响应时间:{{duration}}",
+ "responses": "响应 API",
"Responses API Version": "响应 API 版本",
"Responses APIs": "Responses API",
"Restore defaults": "恢复默认",
@@ -3947,6 +3991,15 @@
"Route, auth, and balance check in one place": "路由、认证和余额检查集中展示",
"route: weighted channel": "路由:加权渠道",
"Routes": "路由",
+ "Routes Anthropic Messages-compatible requests.": "用于 Anthropic Messages 兼容接口请求。",
+ "Routes asynchronous video generation tasks.": "用于异步视频生成任务接口。",
+ "Routes embedding vector requests.": "用于 embedding 向量接口请求。",
+ "Routes Gemini native /v1beta model requests.": "用于 Gemini 原生 /v1beta 模型路由。",
+ "Routes image generation and editing requests.": "用于图像生成或图像编辑接口请求。",
+ "Routes OpenAI Responses API requests.": "用于 OpenAI Responses 风格接口请求。",
+ "Routes OpenAI-compatible chat and completion requests.": "用于 OpenAI 兼容的聊天与补全接口请求。",
+ "Routes search reranking requests.": "用于搜索重排序接口请求。",
+ "Routes speech, transcription, translation, or music requests.": "用于语音、转写、翻译或音乐任务接口。",
"Routing": "路由",
"Routing & Overrides": "路由与覆盖",
"Routing and reliability governance": "路由与可靠性治理",
@@ -4083,6 +4136,7 @@
"Security boundary": "安全边界",
"Security Check": "安全验证",
"Security verification": "安全验证",
+ "Seed": "随机种子",
"Seedance": "Seedance / 豆包视频",
"Select": "选择",
"Select {{item}}": "选择 {{item}}",
@@ -4241,6 +4295,7 @@
"Showcase core capabilities with demo credentials and limited access.": "使用演示凭据和有限访问权限展示核心功能。",
"Showing": "显示第",
"showing •": "显示 •",
+ "Shows the request APIs, task capabilities, and operational features supported by this channel type.": "展示当前渠道类型支持的请求接口、任务能力和运营功能。",
"Sidebar": "侧边栏",
"Sidebar collapsed by default for new users": "默认情况下为新用户折叠侧边栏",
"Sidebar modules": "侧边栏模块",
@@ -4335,6 +4390,7 @@
"Status Page Slug": "状态页面 Slug",
"Status Path": "状态路径",
"Status polling": "状态轮询",
+ "Status response path is required when the full task protocol is enabled.": "启用完整任务协议时,必须填写状态响应路径。",
"Status short": "状态",
"Status:": "状态:",
"Stay": "留下来",
@@ -4482,6 +4538,7 @@
"Task History": "任务历史",
"Task ID": "任务 ID",
"Task ID Path": "任务 ID 路径",
+ "Task ID response path is required when the full task protocol is enabled.": "启用完整任务协议时,必须填写任务 ID 响应路径。",
"Task ID:": "任务 ID:",
"Task logs": "任务日志",
"Task Logs": "任务日志",
@@ -4501,6 +4558,7 @@
"Telegram": "Telegram",
"Telegram login requires widget integration; coming soon": "Telegram 登录需要小部件集成;即将推出",
"Telegram Login Widget": "Telegram 登录小部件",
+ "Temperature": "温度",
"Template": "模板",
"Template variables:": "模板变量:",
"Templates": "模板",
@@ -4587,6 +4645,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "此表达式对可视化编辑器过于复杂,请切换到表达式模式进行编辑。",
"This feature is experimental. Configuration format and behavior may change.": "此功能为实验性功能。配置格式和行为可能会发生变化。",
"This feature requires server-side WeChat configuration": "此功能需要服务器端微信配置",
+ "This JSON configuration must be a JSON object.": "此 JSON 配置必须是 JSON 对象。",
"This may cause cache failures.": "这可能导致缓存故障。",
"This may take a few moments while we validate the request and update your session.": "这可能需要一些时间,因为我们正在验证请求并更新您的会话。",
"This model has both fixed price and ratio billing conflicts": "此模型同时存在固定价格和比例计费冲突",
@@ -4723,6 +4782,7 @@
"Top models": "热门模型",
"Top Models": "热门模型",
"Top models by traffic": "流量最高的模型",
+ "Top P": "Top P",
"Top up balance and view billing history.": "充值余额并查看账单历史。",
"Top Users": "热门用户",
"Top vendors": "热门厂商",
@@ -4896,6 +4956,7 @@
"upstream AI ecosystems": "上游 AI 生态",
"Upstream model and multimodal platforms": "上游模型与多模态平台",
"Upstream Model Detection Settings": "检测上游模型设置",
+ "Upstream model discovery is not supported for this channel type and will not run.": "当前渠道类型不支持上游模型发现,该功能不会运行。",
"Upstream Model Update Check": "上游模型更新检查",
"Upstream Model Updates": "上游模型更新",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "已处理上游模型更新:加入 {{added}} 个,删除 {{removed}} 个,本次忽略 {{ignored}} 个,当前已忽略模型 {{totalIgnored}} 个",
@@ -5048,15 +5109,25 @@
"Version Overrides": "版本覆盖",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Vertex AI API Key 模式不支持批量创建",
+ "Vertex AI API Key mode does not support batch creation.": "Vertex AI 的 API Key 模式不支持批量创建。",
"Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.": "Vertex AI 不支持 functionResponse.id 字段,开启后将自动移除该字段",
"Vertex AI Key Format": "Vertex AI 密钥格式",
+ "Vertex AI region configuration must be a JSON object containing a default field.": "Vertex AI 区域配置必须是包含 default 字段的 JSON 对象。",
+ "Vertex AI region configuration must contain a default field.": "Vertex AI 区域配置必须包含 default 字段。",
+ "Vertex AI service account credentials must be valid JSON.": "Vertex AI 服务账号密钥必须是有效的 JSON。",
"Vertex AI service account key must be valid JSON": "Vertex AI 服务账号密钥必须是有效 JSON",
"Video": "视频",
"Video length in seconds": "视频时长(秒)",
"Video Remix": "视频 Remix",
"video task adapters, status mappings, and protocol paths": "视频任务适配器、状态映射与协议路径",
+ "Video Task Billing": "视频任务计费",
"video task channels and protocol templates": "视频任务渠道与协议模板",
"Video Task Protocol": "视频任务协议",
+ "Video task protocol settings are only available for channel types that support video tasks.": "视频任务协议设置仅适用于支持视频任务的渠道类型。",
+ "Video task query path is required.": "视频任务查询路径不能为空。",
+ "Video task query path must contain {task_id}, {operation_name}, or {upstream_task_id}.": "视频任务查询路径必须包含 {task_id}、{operation_name} 或 {upstream_task_id}。",
+ "Video task submission path is required.": "视频任务提交路径不能为空。",
+ "video tasks": "视频任务",
"Vidu": "Vidu",
"View": "查看",
"View all currently available models": "查看当前可用的所有模型",
@@ -5188,6 +5259,7 @@
"Wire encoding for the embedding vectors": "向量传输的编码格式",
"with conflicts": "有冲突",
"with the API key from your token settings.": "替换为令牌设置中的 API Key。",
+ "Without a result URL path, completed video tasks may not return a content URL.": "未配置结果 URL 路径,视频任务完成后可能无法返回内容地址。",
"Without additional conditions, only the type above is used for pruning.": "未添加附加条件时,仅使用上方 type 进行清理。",
"Worker": "工作节点",
"Worker Access Key": "Worker 访问密钥",
diff --git a/web/default/src/i18n/static-keys.ts b/web/default/src/i18n/static-keys.ts
index b150714b..3ee1e998 100644
--- a/web/default/src/i18n/static-keys.ts
+++ b/web/default/src/i18n/static-keys.ts
@@ -329,6 +329,23 @@ export const STATIC_I18N_KEYS = [
'Param Override',
'Override request parameters',
+ // Playground parameter controls
+ 'Caps the response length',
+ 'Controls randomness and creativity',
+ 'Encourages new topics',
+ 'Frequency Penalty',
+ 'Keeps compatible responses more repeatable',
+ 'Limits token selection to a probability mass',
+ 'Max Tokens',
+ 'Not Set',
+ 'Only enabled parameters are sent with the request.',
+ 'Parameter settings',
+ 'Presence Penalty',
+ 'Reduces repeated wording',
+ 'Seed',
+ 'Temperature',
+ 'Top P',
+
// Profile / 2FA
'Backed up',
'Not backed up',
diff --git a/web/default/src/styles/index.css b/web/default/src/styles/index.css
index 3b9cfabb..366d3d43 100644
--- a/web/default/src/styles/index.css
+++ b/web/default/src/styles/index.css
@@ -17,6 +17,7 @@ along with this program. If not, see .
For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
*/
@import 'tailwindcss';
+@source '../features/home';
@import 'tw-animate-css';
@import 'shadcn/tailwind.css';
@import '@fontsource-variable/public-sans';
diff --git a/web/default/src/tanstack-table.d.ts b/web/default/src/tanstack-table.d.ts
index 073768e4..31404b94 100644
--- a/web/default/src/tanstack-table.d.ts
+++ b/web/default/src/tanstack-table.d.ts
@@ -27,11 +27,17 @@ declare module '@tanstack/react-table' {
description?: string
// Whether this column can be sorted (overrides default behavior)
sortable?: boolean
- // Custom CSS classes to apply to the column cells
+ // Shared CSS classes applied to both the header and body cells
className?: string
+ // Header-only CSS classes for layout that should not affect row content
+ headerClassName?: string
+ // Body-cell-only CSS classes for content wrapping, sizing, or alignment
+ cellClassName?: string
// Mobile list placement hints
mobileTitle?: boolean
mobileBadge?: boolean
mobileHidden?: boolean
+ // Mobile-only CSS classes applied to the rendered value container
+ mobileClassName?: string
}
}