|
|
@@ -96,11 +96,14 @@ function buildConditionStr(conditions) {
|
|
|
.join(' && ');
|
|
|
}
|
|
|
|
|
|
-// CACHE_VAR_MAP maps tier data fields to Expr variable names
|
|
|
+// CACHE_VAR_MAP maps tier data fields to Expr variable names (cache + image + audio)
|
|
|
const CACHE_VAR_MAP = [
|
|
|
{ field: 'cache_read_unit_cost', exprVar: 'cr' },
|
|
|
{ field: 'cache_create_unit_cost', exprVar: 'cc' },
|
|
|
{ field: 'cache_create_1h_unit_cost', exprVar: 'cc1h' },
|
|
|
+ { field: 'image_unit_cost', exprVar: 'img' },
|
|
|
+ { field: 'audio_input_unit_cost', exprVar: 'ai' },
|
|
|
+ { field: 'audio_output_unit_cost', exprVar: 'ao' },
|
|
|
];
|
|
|
|
|
|
function getTierCacheMode(tier) {
|
|
|
@@ -130,7 +133,7 @@ function createDefaultVisualConfig() {
|
|
|
conditions: [],
|
|
|
input_unit_cost: 0,
|
|
|
output_unit_cost: 0,
|
|
|
- label: '默认',
|
|
|
+ label: 'base',
|
|
|
cache_mode: CACHE_MODE_GENERIC,
|
|
|
}),
|
|
|
],
|
|
|
@@ -270,64 +273,58 @@ function tryParseVisualConfig(exprStr) {
|
|
|
function ConditionRow({ cond, onChange, onRemove, t }) {
|
|
|
const hint = formatTokenHint(cond.value);
|
|
|
return (
|
|
|
- <div style={{ marginBottom: 6 }}>
|
|
|
- <div
|
|
|
- style={{
|
|
|
- display: 'flex',
|
|
|
- alignItems: 'center',
|
|
|
- gap: 6,
|
|
|
- }}
|
|
|
+ <div style={{
|
|
|
+ marginBottom: 6,
|
|
|
+ display: 'grid',
|
|
|
+ gridTemplateColumns: '1fr auto 1fr auto',
|
|
|
+ gap: '4px 6px',
|
|
|
+ alignItems: 'center',
|
|
|
+ }}>
|
|
|
+ <Select
|
|
|
+ size='small'
|
|
|
+ value={cond.var || 'p'}
|
|
|
+ onChange={(val) => onChange({ ...cond, var: val })}
|
|
|
>
|
|
|
- <Select
|
|
|
- size='small'
|
|
|
- value={cond.var || 'p'}
|
|
|
- onChange={(val) => onChange({ ...cond, var: val })}
|
|
|
- style={{ width: 110 }}
|
|
|
- >
|
|
|
- {VAR_OPTIONS.map((v) => (
|
|
|
- <Select.Option key={v.value} value={v.value}>
|
|
|
- {v.label}
|
|
|
- </Select.Option>
|
|
|
- ))}
|
|
|
- </Select>
|
|
|
- <Select
|
|
|
- size='small'
|
|
|
- value={cond.op || '<'}
|
|
|
- onChange={(val) => onChange({ ...cond, op: val })}
|
|
|
- style={{ width: 70 }}
|
|
|
- >
|
|
|
- {OPS.map((op) => (
|
|
|
- <Select.Option key={op} value={op}>
|
|
|
- {op}
|
|
|
- </Select.Option>
|
|
|
- ))}
|
|
|
- </Select>
|
|
|
- <InputNumber
|
|
|
- size='small'
|
|
|
- min={0}
|
|
|
- value={cond.value ?? ''}
|
|
|
- onChange={(val) => onChange({ ...cond, value: val })}
|
|
|
- style={{ flex: 1, minWidth: 100 }}
|
|
|
- />
|
|
|
- <Button
|
|
|
- icon={<IconDelete />}
|
|
|
- type='danger'
|
|
|
- theme='borderless'
|
|
|
- size='small'
|
|
|
- onClick={onRemove}
|
|
|
- />
|
|
|
- </div>
|
|
|
+ {VAR_OPTIONS.map((v) => (
|
|
|
+ <Select.Option key={v.value} value={v.value}>
|
|
|
+ {v.label}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <Select
|
|
|
+ size='small'
|
|
|
+ value={cond.op || '<'}
|
|
|
+ onChange={(val) => onChange({ ...cond, op: val })}
|
|
|
+ style={{ width: 70 }}
|
|
|
+ >
|
|
|
+ {OPS.map((op) => (
|
|
|
+ <Select.Option key={op} value={op}>
|
|
|
+ {op}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <InputNumber
|
|
|
+ size='small'
|
|
|
+ min={0}
|
|
|
+ value={cond.value ?? ''}
|
|
|
+ onChange={(val) => onChange({ ...cond, value: val })}
|
|
|
+ />
|
|
|
+ <Button
|
|
|
+ icon={<IconDelete />}
|
|
|
+ type='danger'
|
|
|
+ theme='borderless'
|
|
|
+ size='small'
|
|
|
+ onClick={onRemove}
|
|
|
+ />
|
|
|
{hint ? (
|
|
|
<Text
|
|
|
size='small'
|
|
|
style={{
|
|
|
color: 'var(--semi-color-text-3)',
|
|
|
- marginLeft: 186,
|
|
|
- display: 'block',
|
|
|
- marginTop: 1,
|
|
|
+ gridColumn: '3 / 4',
|
|
|
}}
|
|
|
>
|
|
|
- {hint}
|
|
|
+ = {hint}
|
|
|
</Text>
|
|
|
) : null}
|
|
|
</div>
|
|
|
@@ -388,8 +385,8 @@ const CACHE_FIELDS_GENERIC = [
|
|
|
];
|
|
|
|
|
|
function ExtendedPriceBlock({ tier, index, onUpdate, t }) {
|
|
|
- const hasAny = [...CACHE_FIELDS_TIMED].some(
|
|
|
- (f) => Number(tier[f.field]) > 0,
|
|
|
+ const hasAny = [...CACHE_FIELDS_TIMED, 'image_unit_cost', 'audio_input_unit_cost', 'audio_output_unit_cost'].some(
|
|
|
+ (f) => Number(tier[typeof f === 'string' ? f : f.field]) > 0,
|
|
|
);
|
|
|
const [expanded, setExpanded] = useState(hasAny);
|
|
|
const cacheMode = getTierCacheMode(tier);
|
|
|
@@ -461,6 +458,37 @@ function ExtendedPriceBlock({ tier, index, onUpdate, t }) {
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
+ <div className='text-xs text-gray-500 mb-2 mt-3'>
|
|
|
+ {t('图片/音频价格(可选)')}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: 'grid',
|
|
|
+ gridTemplateColumns: '1fr 1fr 1fr',
|
|
|
+ gap: 8,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {[
|
|
|
+ { field: 'image_unit_cost', labelKey: '图片输入价格' },
|
|
|
+ { field: 'audio_input_unit_cost', labelKey: '音频输入价格' },
|
|
|
+ { field: 'audio_output_unit_cost', labelKey: '音频补全价格' },
|
|
|
+ ].map((cf) => (
|
|
|
+ <div key={cf.field}>
|
|
|
+ <Text
|
|
|
+ size='small'
|
|
|
+ style={{ color: 'var(--semi-color-text-2)' }}
|
|
|
+ >
|
|
|
+ {t(cf.labelKey)}
|
|
|
+ </Text>
|
|
|
+ <PriceInput
|
|
|
+ unitCost={tier[cf.field]}
|
|
|
+ field={cf.field}
|
|
|
+ index={index}
|
|
|
+ onUpdate={onUpdate}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</Collapsible>
|
|
|
</div>
|
|
|
@@ -730,73 +758,114 @@ function VisualEditor({ visualConfig, onChange, t }) {
|
|
|
// Raw Expr editor with preset templates
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
-const PRESETS = [
|
|
|
+const PRESET_GROUPS = [
|
|
|
{
|
|
|
- key: 'claude-opus',
|
|
|
- label: 'Claude Opus 4.6',
|
|
|
- expr: 'tier("default", p * 2.5 + c * 12.5 + cr * 0.25 + cc * 3.125 + cc1h * 5)',
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'claude-opus-fast',
|
|
|
- label: 'Claude Opus 4.6 Fast',
|
|
|
- expr: 'tier("default", p * 2.5 + c * 12.5 + cr * 0.25 + cc * 3.125 + cc1h * 5)',
|
|
|
- requestRules: [
|
|
|
- { conditions: [{ source: SOURCE_HEADER, path: 'anthropic-beta', mode: MATCH_CONTAINS, value: 'fast-mode-2026-02-01' }], multiplier: '6' },
|
|
|
+ group: '固定价格',
|
|
|
+ presets: [
|
|
|
+ { key: 'flat', label: 'Flat', expr: 'tier("base", p * 1 + c * 2)' },
|
|
|
+ { key: 'claude-opus', label: 'Claude Opus 4.6', expr: 'tier("base", p * 2.5 + c * 12.5 + cr * 0.25 + cc * 3.125 + cc1h * 5)' },
|
|
|
+ { key: 'gpt-5.4', label: 'GPT-5.4', expr: 'tier("base", p * 1.25 + c * 5 + cr * 0.125)' },
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
- key: 'claude-sonnet',
|
|
|
- label: 'Claude Sonnet 4.5',
|
|
|
- expr: 'p <= 200000 ? tier("standard", p * 1.5 + c * 7.5 + cr * 0.15 + cc * 1.875 + cc1h * 3) : tier("long_context", p * 3 + c * 11.25 + cr * 0.3 + cc * 3.75 + cc1h * 6)',
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'glm-4.5-air',
|
|
|
- label: 'GLM-4.5-Air',
|
|
|
- expr: 'p < 32000 && c < 200 ? tier("short_output", p * 0.4 + c * 1 + cr * 0.08) : p < 32000 && c >= 200 ? tier("long_output", p * 0.4 + c * 3 + cr * 0.08) : tier("mid_context", p * 0.6 + c * 4 + cr * 0.12)',
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'gpt-5.4-fast',
|
|
|
- label: 'GPT-5.4 Fast',
|
|
|
- expr: 'tier("default", p * 1.25 + c * 5 + cr * 0.125)',
|
|
|
- requestRules: [
|
|
|
- { conditions: [{ source: SOURCE_PARAM, path: 'service_tier', mode: MATCH_EQ, value: 'fast' }], multiplier: '2' },
|
|
|
+ group: '阶梯计费',
|
|
|
+ presets: [
|
|
|
+ { key: 'claude-sonnet', label: 'Claude Sonnet 4.5', expr: 'p <= 200000 ? tier("standard", p * 1.5 + c * 7.5 + cr * 0.15 + cc * 1.875 + cc1h * 3) : tier("long_context", p * 3 + c * 11.25 + cr * 0.3 + cc * 3.75 + cc1h * 6)' },
|
|
|
+ { key: 'qwen3-max', label: 'Qwen3-Max', expr: 'p <= 32000 ? tier("short", p * 0.6 + c * 3 + cr * 0.12 + cc * 0.75) : p <= 128000 ? tier("mid", p * 1.2 + c * 6 + cr * 0.24 + cc * 1.5) : tier("long", p * 1.5 + c * 7.5 + cr * 0.3 + cc * 1.875)' },
|
|
|
+ { key: 'glm-4.5-air', label: 'GLM-4.5-Air', expr: 'p < 32000 && c < 200 ? tier("short_output", p * 0.4 + c * 1 + cr * 0.08) : p < 32000 && c >= 200 ? tier("long_output", p * 0.4 + c * 3 + cr * 0.08) : tier("mid_context", p * 0.6 + c * 4 + cr * 0.12)' },
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
- key: 'flat',
|
|
|
- label: 'Flat',
|
|
|
- expr: 'tier("default", p * 1 + c * 2)',
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'night-discount',
|
|
|
- label: '夜间半价',
|
|
|
- expr: 'tier("default", p * 1.5 + c * 7.5)',
|
|
|
- requestRules: [
|
|
|
- { conditions: [{ source: SOURCE_TIME, timeFunc: 'hour', timezone: 'Asia/Shanghai', mode: MATCH_RANGE, rangeStart: '21', rangeEnd: '6' }], multiplier: '0.5' },
|
|
|
+ group: '多模态',
|
|
|
+ presets: [
|
|
|
+ { key: 'qwen3-omni-flash', label: 'Qwen3-Omni-Flash', expr: 'tier("base", p * 0.215 + c * 1.53 + img * 0.39 + ai * 1.905 + ao * 7.555)' },
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
- key: 'weekend-discount',
|
|
|
- label: '周末8折',
|
|
|
- expr: 'tier("default", p * 1.5 + c * 7.5)',
|
|
|
- requestRules: [
|
|
|
- { conditions: [{ source: SOURCE_TIME, timeFunc: 'weekday', timezone: 'Asia/Shanghai', mode: MATCH_EQ, value: '0' }], multiplier: '0.8' },
|
|
|
- { conditions: [{ source: SOURCE_TIME, timeFunc: 'weekday', timezone: 'Asia/Shanghai', mode: MATCH_EQ, value: '6' }], multiplier: '0.8' },
|
|
|
+ group: '请求条件',
|
|
|
+ presets: [
|
|
|
+ {
|
|
|
+ key: 'claude-opus-fast', label: 'Claude Opus 4.6 Fast',
|
|
|
+ expr: 'tier("base", p * 2.5 + c * 12.5 + cr * 0.25 + cc * 3.125 + cc1h * 5)',
|
|
|
+ requestRules: [{ conditions: [{ source: SOURCE_HEADER, path: 'anthropic-beta', mode: MATCH_CONTAINS, value: 'fast-mode-2026-02-01' }], multiplier: '6' }],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'gpt-5.4-fast', label: 'GPT-5.4 Fast',
|
|
|
+ expr: 'tier("base", p * 1.25 + c * 5 + cr * 0.125)',
|
|
|
+ requestRules: [{ conditions: [{ source: SOURCE_PARAM, path: 'service_tier', mode: MATCH_EQ, value: 'fast' }], multiplier: '2' }],
|
|
|
+ },
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
- key: 'new-year-promo',
|
|
|
- label: '新年促销',
|
|
|
- expr: 'tier("default", p * 1.5 + c * 7.5)',
|
|
|
- requestRules: [
|
|
|
- { conditions: [
|
|
|
- { source: SOURCE_TIME, timeFunc: 'month', timezone: 'Asia/Shanghai', mode: MATCH_EQ, value: '1' },
|
|
|
- { source: SOURCE_TIME, timeFunc: 'day', timezone: 'Asia/Shanghai', mode: MATCH_EQ, value: '1' },
|
|
|
- ], multiplier: '0.5' },
|
|
|
+ group: '时间促销',
|
|
|
+ presets: [
|
|
|
+ {
|
|
|
+ key: 'night-discount', label: '夜间半价',
|
|
|
+ expr: 'tier("base", p * 1.5 + c * 7.5)',
|
|
|
+ requestRules: [{ conditions: [{ source: SOURCE_TIME, timeFunc: 'hour', timezone: 'Asia/Shanghai', mode: MATCH_RANGE, rangeStart: '21', rangeEnd: '6' }], multiplier: '0.5' }],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'weekend-discount', label: '周末8折',
|
|
|
+ expr: 'tier("base", p * 1.5 + c * 7.5)',
|
|
|
+ requestRules: [
|
|
|
+ { conditions: [{ source: SOURCE_TIME, timeFunc: 'weekday', timezone: 'Asia/Shanghai', mode: MATCH_EQ, value: '0' }], multiplier: '0.8' },
|
|
|
+ { conditions: [{ source: SOURCE_TIME, timeFunc: 'weekday', timezone: 'Asia/Shanghai', mode: MATCH_EQ, value: '6' }], multiplier: '0.8' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'new-year-promo', label: '新年促销',
|
|
|
+ expr: 'tier("base", p * 1.5 + c * 7.5)',
|
|
|
+ requestRules: [{ conditions: [
|
|
|
+ { source: SOURCE_TIME, timeFunc: 'month', timezone: 'Asia/Shanghai', mode: MATCH_EQ, value: '1' },
|
|
|
+ { source: SOURCE_TIME, timeFunc: 'day', timezone: 'Asia/Shanghai', mode: MATCH_EQ, value: '1' },
|
|
|
+ ], multiplier: '0.5' }],
|
|
|
+ },
|
|
|
],
|
|
|
},
|
|
|
];
|
|
|
|
|
|
+const PRESET_DEFAULT_VISIBLE = 2;
|
|
|
+
|
|
|
+function PresetSection({ applyPreset, t }) {
|
|
|
+ const [expanded, setExpanded] = useState(false);
|
|
|
+ const visibleGroups = expanded ? PRESET_GROUPS : PRESET_GROUPS.slice(0, PRESET_DEFAULT_VISIBLE);
|
|
|
+ const hasMore = PRESET_GROUPS.length > PRESET_DEFAULT_VISIBLE;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div style={{ marginBottom: 12 }}>
|
|
|
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
|
|
+ <Text size='small' style={{ color: 'var(--semi-color-text-2)' }}>
|
|
|
+ {t('预设模板')}
|
|
|
+ </Text>
|
|
|
+ {hasMore && (
|
|
|
+ <Button
|
|
|
+ theme='borderless'
|
|
|
+ size='small'
|
|
|
+ onClick={() => setExpanded(!expanded)}
|
|
|
+ style={{ padding: '0 4px', fontSize: 12, color: 'var(--semi-color-primary)' }}
|
|
|
+ >
|
|
|
+ {expanded ? t('收起') : t('更多模板...')}
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
|
+ {visibleGroups.map((g) => (
|
|
|
+ <div key={g.group} style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
|
|
+ <Tag size='small' color='grey' style={{ minWidth: 60, textAlign: 'center' }}>
|
|
|
+ {t(g.group)}
|
|
|
+ </Tag>
|
|
|
+ {g.presets.map((p) => (
|
|
|
+ <Button key={p.key} size='small' theme='light' onClick={() => applyPreset(p)}>
|
|
|
+ {p.label}
|
|
|
+ </Button>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
function RawExprEditor({ exprString, onChange, t }) {
|
|
|
return (
|
|
|
<div>
|
|
|
@@ -843,10 +912,13 @@ function RawExprEditor({ exprString, onChange, t }) {
|
|
|
// Cache token inputs for estimator — auto-shown when expression uses cache vars
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
-const CACHE_ESTIMATOR_FIELDS = [
|
|
|
+const EXTRA_ESTIMATOR_FIELDS = [
|
|
|
{ var: 'cr', stateKey: 'cacheReadTokens', labelKey: '缓存读取 Token (cr)' },
|
|
|
{ var: 'cc', stateKey: 'cacheCreateTokens', labelKey: '缓存创建 Token (cc)' },
|
|
|
{ var: 'cc1h', stateKey: 'cacheCreate1hTokens', labelKey: '缓存创建-1小时 (cc1h)' },
|
|
|
+ { var: 'img', stateKey: 'imageTokens', labelKey: '图片输入 Token (img)' },
|
|
|
+ { var: 'ai', stateKey: 'audioInputTokens', labelKey: '音频输入 Token (ai)' },
|
|
|
+ { var: 'ao', stateKey: 'audioOutputTokens', labelKey: '音频补全 Token (ao)' },
|
|
|
];
|
|
|
|
|
|
function CacheTokenEstimatorInputs({
|
|
|
@@ -854,25 +926,34 @@ function CacheTokenEstimatorInputs({
|
|
|
cacheReadTokens, setCacheReadTokens,
|
|
|
cacheCreateTokens, setCacheCreateTokens,
|
|
|
cacheCreate1hTokens, setCacheCreate1hTokens,
|
|
|
+ imageTokens, setImageTokens,
|
|
|
+ audioInputTokens, setAudioInputTokens,
|
|
|
+ audioOutputTokens, setAudioOutputTokens,
|
|
|
t,
|
|
|
}) {
|
|
|
const setters = {
|
|
|
cacheReadTokens: setCacheReadTokens,
|
|
|
cacheCreateTokens: setCacheCreateTokens,
|
|
|
cacheCreate1hTokens: setCacheCreate1hTokens,
|
|
|
+ imageTokens: setImageTokens,
|
|
|
+ audioInputTokens: setAudioInputTokens,
|
|
|
+ audioOutputTokens: setAudioOutputTokens,
|
|
|
};
|
|
|
const values = {
|
|
|
cacheReadTokens,
|
|
|
cacheCreateTokens,
|
|
|
cacheCreate1hTokens,
|
|
|
+ imageTokens,
|
|
|
+ audioInputTokens,
|
|
|
+ audioOutputTokens,
|
|
|
};
|
|
|
|
|
|
- const usesCache = useMemo(() => {
|
|
|
+ const usesExtra = useMemo(() => {
|
|
|
if (!effectiveExpr) return false;
|
|
|
- return /\b(cr|cc1h|cc)\b/.test(effectiveExpr);
|
|
|
+ return /\b(cr|cc1h|cc|img|ai|ao)\b/.test(effectiveExpr);
|
|
|
}, [effectiveExpr]);
|
|
|
|
|
|
- if (!usesCache) return null;
|
|
|
+ if (!usesExtra) return null;
|
|
|
|
|
|
return (
|
|
|
<div
|
|
|
@@ -883,7 +964,7 @@ function CacheTokenEstimatorInputs({
|
|
|
marginBottom: 12,
|
|
|
}}
|
|
|
>
|
|
|
- {CACHE_ESTIMATOR_FIELDS.map((cf) => (
|
|
|
+ {EXTRA_ESTIMATOR_FIELDS.map((cf) => (
|
|
|
<div key={cf.var}>
|
|
|
<Text size='small' className='mb-1' style={{ display: 'block' }}>
|
|
|
{t(cf.labelKey)}
|
|
|
@@ -904,7 +985,7 @@ function CacheTokenEstimatorInputs({
|
|
|
// Cost estimator (works with any Expr string)
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
-function evalExprLocally(exprStr, p, c, cr, cc, cc1h) {
|
|
|
+function evalExprLocally(exprStr, p, c, cr, cc, cc1h, img, ai, ao) {
|
|
|
try {
|
|
|
let matchedTier = '';
|
|
|
const tierFn = (name, value) => {
|
|
|
@@ -917,6 +998,17 @@ function evalExprLocally(exprStr, p, c, cr, cc, cc1h) {
|
|
|
cr: cr || 0,
|
|
|
cc: cc || 0,
|
|
|
cc1h: cc1h || 0,
|
|
|
+ img: img || 0,
|
|
|
+ ai: ai || 0,
|
|
|
+ ao: ao || 0,
|
|
|
+ prompt_tokens: p,
|
|
|
+ completion_tokens: c,
|
|
|
+ cache_read_tokens: cr || 0,
|
|
|
+ cache_create_tokens: cc || 0,
|
|
|
+ cache_create_1h_tokens: cc1h || 0,
|
|
|
+ image_tokens: img || 0,
|
|
|
+ audio_input_tokens: ai || 0,
|
|
|
+ audio_output_tokens: ao || 0,
|
|
|
tier: tierFn,
|
|
|
max: Math.max,
|
|
|
min: Math.min,
|
|
|
@@ -995,37 +1087,47 @@ function RuleConditionRow({ cond, onChange, onRemove, t }) {
|
|
|
const ph = TIME_FUNC_PLACEHOLDERS[normalized.timeFunc] || '';
|
|
|
const hint = TIME_FUNC_HINTS[normalized.timeFunc] || '';
|
|
|
return (
|
|
|
- <div style={{ marginBottom: 6 }}>
|
|
|
- <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
|
+ <div style={{
|
|
|
+ marginBottom: 8,
|
|
|
+ padding: '8px 10px',
|
|
|
+ borderRadius: 6,
|
|
|
+ background: 'var(--semi-color-fill-0)',
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ gap: 6,
|
|
|
+ }}>
|
|
|
+ <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
|
|
{sourceSelect}
|
|
|
<Select
|
|
|
size='small'
|
|
|
value={normalized.timeFunc}
|
|
|
onChange={(value) => onChange({ ...normalized, timeFunc: value })}
|
|
|
- style={{ width: 80 }}
|
|
|
+ style={{ flex: 1 }}
|
|
|
>
|
|
|
{TIME_FUNCS.map((fn) => (
|
|
|
<Select.Option key={fn} value={fn}>{t(TIME_FUNC_LABELS[fn] || fn)}</Select.Option>
|
|
|
))}
|
|
|
</Select>
|
|
|
- <Select
|
|
|
- size='small'
|
|
|
- value={normalized.timezone}
|
|
|
- onChange={(value) => onChange({ ...normalized, timezone: value })}
|
|
|
- filter
|
|
|
- allowCreate
|
|
|
- placeholder={t('时区')}
|
|
|
- style={{ width: 180 }}
|
|
|
- >
|
|
|
- {COMMON_TIMEZONES.map((tz) => (
|
|
|
- <Select.Option key={tz.value} value={tz.value}>{tz.label}</Select.Option>
|
|
|
- ))}
|
|
|
- </Select>
|
|
|
+ {removeBtn}
|
|
|
+ </div>
|
|
|
+ <Select
|
|
|
+ size='small'
|
|
|
+ value={normalized.timezone}
|
|
|
+ onChange={(value) => onChange({ ...normalized, timezone: value })}
|
|
|
+ filter
|
|
|
+ allowCreate
|
|
|
+ placeholder={t('时区')}
|
|
|
+ >
|
|
|
+ {COMMON_TIMEZONES.map((tz) => (
|
|
|
+ <Select.Option key={tz.value} value={tz.value}>{tz.label}</Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
|
|
<Select
|
|
|
size='small'
|
|
|
value={normalized.mode}
|
|
|
onChange={(value) => onChange(normalizeCondition({ ...normalized, mode: value }))}
|
|
|
- style={{ width: 100 }}
|
|
|
+ style={{ flex: 1 }}
|
|
|
>
|
|
|
{matchOptions.map((item) => (
|
|
|
<Select.Option key={item.value} value={item.value}>{item.label}</Select.Option>
|
|
|
@@ -1038,12 +1140,11 @@ function RuleConditionRow({ cond, onChange, onRemove, t }) {
|
|
|
<Input size='small' value={normalized.rangeEnd} placeholder={ph} style={{ flex: 1 }} onChange={(value) => onChange({ ...normalized, rangeEnd: value })} />
|
|
|
</div>
|
|
|
) : (
|
|
|
- <Input size='small' value={normalized.value} placeholder={ph} style={{ flex: 1, minWidth: 60 }} onChange={(value) => onChange({ ...normalized, value })} />
|
|
|
+ <Input size='small' value={normalized.value} placeholder={ph} style={{ flex: 1 }} onChange={(value) => onChange({ ...normalized, value })} />
|
|
|
)}
|
|
|
- {removeBtn}
|
|
|
</div>
|
|
|
{hint && (
|
|
|
- <Text size='small' style={{ color: 'var(--semi-color-text-3)', marginLeft: 116, marginTop: 2, display: 'block' }}>
|
|
|
+ <Text size='small' style={{ color: 'var(--semi-color-text-3)' }}>
|
|
|
{t(hint)}
|
|
|
</Text>
|
|
|
)}
|
|
|
@@ -1053,20 +1154,27 @@ function RuleConditionRow({ cond, onChange, onRemove, t }) {
|
|
|
|
|
|
const showValue = normalized.mode !== MATCH_EXISTS;
|
|
|
return (
|
|
|
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
|
|
|
+ <div style={{
|
|
|
+ marginBottom: 8,
|
|
|
+ padding: '8px 10px',
|
|
|
+ borderRadius: 6,
|
|
|
+ background: 'var(--semi-color-fill-0)',
|
|
|
+ display: 'grid',
|
|
|
+ gridTemplateColumns: '1fr 1fr auto',
|
|
|
+ gap: '6px 8px',
|
|
|
+ }}>
|
|
|
{sourceSelect}
|
|
|
<Input
|
|
|
size='small'
|
|
|
value={normalized.path}
|
|
|
placeholder={normalized.source === SOURCE_HEADER ? t('例如 anthropic-beta') : t('例如 service_tier')}
|
|
|
onChange={(value) => onChange({ ...normalized, path: value })}
|
|
|
- style={{ flex: 1, minWidth: 120 }}
|
|
|
/>
|
|
|
+ {removeBtn}
|
|
|
<Select
|
|
|
size='small'
|
|
|
value={normalized.mode}
|
|
|
onChange={(value) => onChange(normalizeCondition({ ...normalized, mode: value, value: value === MATCH_EXISTS ? '' : normalized.value }))}
|
|
|
- style={{ width: 100 }}
|
|
|
>
|
|
|
{matchOptions.map((item) => (
|
|
|
<Select.Option key={item.value} value={item.value}>{item.label}</Select.Option>
|
|
|
@@ -1078,9 +1186,8 @@ function RuleConditionRow({ cond, onChange, onRemove, t }) {
|
|
|
placeholder={normalized.mode === MATCH_CONTAINS ? t('匹配内容') : normalized.mode === MATCH_EXISTS ? '' : t('匹配值')}
|
|
|
disabled={!showValue}
|
|
|
onChange={(value) => onChange({ ...normalized, value })}
|
|
|
- style={{ flex: 1, minWidth: 80 }}
|
|
|
/>
|
|
|
- {removeBtn}
|
|
|
+ <div />
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
@@ -1172,6 +1279,9 @@ export default function TieredPricingEditor({ model, onExprChange, requestRuleEx
|
|
|
const [cacheReadTokens, setCacheReadTokens] = useState(0);
|
|
|
const [cacheCreateTokens, setCacheCreateTokens] = useState(0);
|
|
|
const [cacheCreate1hTokens, setCacheCreate1hTokens] = useState(0);
|
|
|
+ const [imageTokens, setImageTokens] = useState(0);
|
|
|
+ const [audioInputTokens, setAudioInputTokens] = useState(0);
|
|
|
+ const [audioOutputTokens, setAudioOutputTokens] = useState(0);
|
|
|
|
|
|
const currentRequestRuleExpr = requestRuleExpr || '';
|
|
|
const parsedRequestRuleGroups = useMemo(
|
|
|
@@ -1282,9 +1392,11 @@ export default function TieredPricingEditor({ model, onExprChange, requestRuleEx
|
|
|
() => evalExprLocally(
|
|
|
effectiveExpr, promptTokens, completionTokens,
|
|
|
cacheReadTokens, cacheCreateTokens, cacheCreate1hTokens,
|
|
|
+ imageTokens, audioInputTokens, audioOutputTokens,
|
|
|
),
|
|
|
[effectiveExpr, promptTokens, completionTokens,
|
|
|
- cacheReadTokens, cacheCreateTokens, cacheCreate1hTokens],
|
|
|
+ cacheReadTokens, cacheCreateTokens, cacheCreate1hTokens,
|
|
|
+ imageTokens, audioInputTokens, audioOutputTokens],
|
|
|
);
|
|
|
|
|
|
return (
|
|
|
@@ -1301,24 +1413,7 @@ export default function TieredPricingEditor({ model, onExprChange, requestRuleEx
|
|
|
</RadioGroup>
|
|
|
</div>
|
|
|
|
|
|
- <div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 6 }}>
|
|
|
- <Text
|
|
|
- size='small'
|
|
|
- style={{ color: 'var(--semi-color-text-2)' }}
|
|
|
- >
|
|
|
- {t('预设模板')}:
|
|
|
- </Text>
|
|
|
- {PRESETS.map((p) => (
|
|
|
- <Button
|
|
|
- key={p.key}
|
|
|
- size='small'
|
|
|
- theme='light'
|
|
|
- onClick={() => applyPreset(p)}
|
|
|
- >
|
|
|
- {p.label}
|
|
|
- </Button>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
+ <PresetSection applyPreset={applyPreset} t={t} />
|
|
|
|
|
|
<Card
|
|
|
bodyStyle={{ padding: 16 }}
|
|
|
@@ -1440,6 +1535,12 @@ export default function TieredPricingEditor({ model, onExprChange, requestRuleEx
|
|
|
setCacheCreateTokens={setCacheCreateTokens}
|
|
|
cacheCreate1hTokens={cacheCreate1hTokens}
|
|
|
setCacheCreate1hTokens={setCacheCreate1hTokens}
|
|
|
+ imageTokens={imageTokens}
|
|
|
+ setImageTokens={setImageTokens}
|
|
|
+ audioInputTokens={audioInputTokens}
|
|
|
+ setAudioInputTokens={setAudioInputTokens}
|
|
|
+ audioOutputTokens={audioOutputTokens}
|
|
|
+ setAudioOutputTokens={setAudioOutputTokens}
|
|
|
t={t}
|
|
|
/>
|
|
|
<div
|