feat: support cached input token billing
This commit is contained in:
@@ -30,9 +30,10 @@ const modeDefinitions: ModeDefinition[] = [
|
||||
{
|
||||
key: 'text',
|
||||
label: '文本',
|
||||
formula: '一次对话扣费 = 输入 Token / 1000 × 输入扣费单价 + 输出 Token / 1000 × 输出扣费单价。',
|
||||
match: (rule) => rule.resourceType.startsWith('text_'),
|
||||
templates: (currency) => [createTextRule(currency, 0.01, 0.03)],
|
||||
formula:
|
||||
'一次对话扣费 = 未命中缓存输入 Token / 1000 × 输入扣费单价 + 命中缓存输入 Token / 1000 × 缓存输入扣费单价 + 输出 Token / 1000 × 输出扣费单价。',
|
||||
match: isTextPricingRule,
|
||||
templates: (currency) => [createTextRule(currency, 0.01, 0.01, 0.03)],
|
||||
parameterGroups: [],
|
||||
},
|
||||
{
|
||||
@@ -160,7 +161,7 @@ function summarizePricingMode(mode: ModeDefinition, rules: PricingRuleInput[]) {
|
||||
const rule = rules[0];
|
||||
if (mode.key === 'text') {
|
||||
const prices = textPrices(rule);
|
||||
return `文本按输入/输出 Token 分别计费,输入 ${prices.inputTokenPrice}/${unitLabel(rule.unit)},输出 ${prices.outputTokenPrice}/${unitLabel(rule.unit)}。`;
|
||||
return `文本按输入/缓存输入/输出 Token 分别计费,输入 ${prices.inputTokenPrice}/${unitLabel(rule.unit)},缓存输入 ${prices.cachedInputTokenPrice}/${unitLabel(rule.unit)},输出 ${prices.outputTokenPrice}/${unitLabel(rule.unit)}。`;
|
||||
}
|
||||
const groupSummaries = mode.parameterGroups
|
||||
.map((group) => summarizeWeightGroup(group, readGroup(rule.dynamicWeight, group)))
|
||||
@@ -260,7 +261,11 @@ function TextRuleEditor(props: {
|
||||
}) {
|
||||
const prices = textPrices(props.rule);
|
||||
|
||||
function updatePrices(inputTokenPrice: number, outputTokenPrice: number) {
|
||||
function updatePrices(
|
||||
inputTokenPrice: number,
|
||||
cachedInputTokenPrice: number,
|
||||
outputTokenPrice: number,
|
||||
) {
|
||||
props.onChange({
|
||||
...props.rule,
|
||||
basePrice: inputTokenPrice,
|
||||
@@ -272,11 +277,17 @@ function TextRuleEditor(props: {
|
||||
...(props.rule.formulaConfig ?? {}),
|
||||
formula: textFormula,
|
||||
inputTokenPrice,
|
||||
cachedInputTokenPrice,
|
||||
outputTokenPrice,
|
||||
},
|
||||
dimensionSchema: {
|
||||
...(props.rule.dimensionSchema ?? {}),
|
||||
metrics: ['input_tokens', 'output_tokens'],
|
||||
metrics: [
|
||||
'input_tokens',
|
||||
'uncached_input_tokens',
|
||||
'cached_input_tokens',
|
||||
'output_tokens',
|
||||
],
|
||||
unitScale: 1000,
|
||||
},
|
||||
});
|
||||
@@ -301,10 +312,52 @@ function TextRuleEditor(props: {
|
||||
<Select size="sm" value={props.rule.status ?? 'active'} onChange={(event) => props.onChange({ ...props.rule, status: event.target.value })}>{statuses.map((item) => <option key={item} value={item}>{item}</option>)}</Select>
|
||||
</PricingFormRow>
|
||||
<PricingFormRow label="输入单价/1K tokens">
|
||||
<Input size="sm" min="0" step="0.0001" type="number" value={prices.inputTokenPrice} onChange={(event) => updatePrices(Number(event.target.value), prices.outputTokenPrice)} />
|
||||
<Input
|
||||
size="sm"
|
||||
min="0"
|
||||
step="0.0001"
|
||||
type="number"
|
||||
value={prices.inputTokenPrice}
|
||||
onChange={(event) =>
|
||||
updatePrices(
|
||||
Number(event.target.value),
|
||||
prices.cachedInputTokenPrice,
|
||||
prices.outputTokenPrice,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</PricingFormRow>
|
||||
<PricingFormRow label="缓存输入单价/1K tokens">
|
||||
<Input
|
||||
size="sm"
|
||||
min="0"
|
||||
step="0.0001"
|
||||
type="number"
|
||||
value={prices.cachedInputTokenPrice}
|
||||
onChange={(event) =>
|
||||
updatePrices(
|
||||
prices.inputTokenPrice,
|
||||
Number(event.target.value),
|
||||
prices.outputTokenPrice,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</PricingFormRow>
|
||||
<PricingFormRow label="输出单价/1K tokens">
|
||||
<Input size="sm" min="0" step="0.0001" type="number" value={prices.outputTokenPrice} onChange={(event) => updatePrices(prices.inputTokenPrice, Number(event.target.value))} />
|
||||
<Input
|
||||
size="sm"
|
||||
min="0"
|
||||
step="0.0001"
|
||||
type="number"
|
||||
value={prices.outputTokenPrice}
|
||||
onChange={(event) =>
|
||||
updatePrices(
|
||||
prices.inputTokenPrice,
|
||||
prices.cachedInputTokenPrice,
|
||||
Number(event.target.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</PricingFormRow>
|
||||
<PricingReadonlyRow label="计价单位" value={unitLabel(props.rule.unit)} />
|
||||
<PricingReadonlyRow label="计算方式" value={calculatorLabel(props.rule.calculatorType)} />
|
||||
@@ -462,14 +515,38 @@ function mergeTemplateRules(templates: PricingRuleInput[], rules: PricingRuleInp
|
||||
}
|
||||
|
||||
function mergeTextRules(rules: PricingRuleInput[], currency: string): PricingRuleInput {
|
||||
const totalRule = rules.find((rule) => rule.resourceType === 'text_total');
|
||||
const totalRule = rules.find((rule) => rule.resourceType === 'text_total' || rule.resourceType === 'text' || rule.ruleKey === 'text');
|
||||
const inputRule = rules.find((rule) => rule.resourceType === 'text_input');
|
||||
const cachedInputRule = rules.find(
|
||||
(rule) => rule.resourceType === 'text_cached_input',
|
||||
);
|
||||
const outputRule = rules.find((rule) => rule.resourceType === 'text_output');
|
||||
const source = totalRule ?? inputRule ?? outputRule ?? createTextRule(currency, 0.01, 0.03);
|
||||
const source =
|
||||
totalRule ??
|
||||
inputRule ??
|
||||
cachedInputRule ??
|
||||
outputRule ??
|
||||
createTextRule(currency, 0.01, 0.01, 0.03);
|
||||
return createTextRule(
|
||||
source.currency ?? currency,
|
||||
Number(source.formulaConfig?.inputTokenPrice ?? inputRule?.basePrice ?? source.basePrice ?? 0.01),
|
||||
Number(source.formulaConfig?.outputTokenPrice ?? outputRule?.basePrice ?? 0.03),
|
||||
Number(
|
||||
source.formulaConfig?.inputTokenPrice ??
|
||||
inputRule?.basePrice ??
|
||||
source.basePrice ??
|
||||
0.01,
|
||||
),
|
||||
Number(
|
||||
source.formulaConfig?.cachedInputTokenPrice ??
|
||||
source.formulaConfig?.inputCacheHitTokenPrice ??
|
||||
cachedInputRule?.basePrice ??
|
||||
source.formulaConfig?.inputTokenPrice ??
|
||||
inputRule?.basePrice ??
|
||||
source.basePrice ??
|
||||
0.01,
|
||||
),
|
||||
Number(
|
||||
source.formulaConfig?.outputTokenPrice ?? outputRule?.basePrice ?? 0.03,
|
||||
),
|
||||
source,
|
||||
);
|
||||
}
|
||||
@@ -478,9 +555,16 @@ function readGroup(value: RecordValue | undefined, group: ModeDefinition['parame
|
||||
return isPlainObject(value?.[group.key]) ? value?.[group.key] as RecordValue : group.defaults;
|
||||
}
|
||||
|
||||
const textFormula = 'input_tokens / 1000 * input_token_price + output_tokens / 1000 * output_token_price';
|
||||
const textFormula =
|
||||
'uncached_input_tokens / 1000 * input_token_price + cached_input_tokens / 1000 * cached_input_token_price + output_tokens / 1000 * output_token_price';
|
||||
|
||||
function createTextRule(currency: string, inputTokenPrice: number, outputTokenPrice: number, source?: PricingRuleInput): PricingRuleInput {
|
||||
function createTextRule(
|
||||
currency: string,
|
||||
inputTokenPrice: number,
|
||||
cachedInputTokenPrice: number,
|
||||
outputTokenPrice: number,
|
||||
source?: PricingRuleInput,
|
||||
): PricingRuleInput {
|
||||
return {
|
||||
...(source ?? {}),
|
||||
ruleKey: 'text',
|
||||
@@ -493,7 +577,12 @@ function createTextRule(currency: string, inputTokenPrice: number, outputTokenPr
|
||||
baseWeight: {},
|
||||
dynamicWeight: {},
|
||||
dimensionSchema: {
|
||||
metrics: ['input_tokens', 'output_tokens'],
|
||||
metrics: [
|
||||
'input_tokens',
|
||||
'uncached_input_tokens',
|
||||
'cached_input_tokens',
|
||||
'output_tokens',
|
||||
],
|
||||
unitScale: 1000,
|
||||
...(source?.dimensionSchema ?? {}),
|
||||
},
|
||||
@@ -501,6 +590,7 @@ function createTextRule(currency: string, inputTokenPrice: number, outputTokenPr
|
||||
...(source?.formulaConfig ?? {}),
|
||||
formula: textFormula,
|
||||
inputTokenPrice,
|
||||
cachedInputTokenPrice,
|
||||
outputTokenPrice,
|
||||
},
|
||||
priority: source?.priority ?? 100,
|
||||
@@ -510,12 +600,36 @@ function createTextRule(currency: string, inputTokenPrice: number, outputTokenPr
|
||||
}
|
||||
|
||||
function textPrices(rule: PricingRuleInput) {
|
||||
const compatRule = rule as PricingRuleInput & {
|
||||
cachedInputTokenPrice?: number;
|
||||
inputCacheHitTokenPrice?: number;
|
||||
inputTokenPrice?: number;
|
||||
outputTokenPrice?: number;
|
||||
};
|
||||
const inputTokenPrice = Number(
|
||||
rule.formulaConfig?.inputTokenPrice ?? compatRule.inputTokenPrice ?? rule.basePrice ?? 0.01,
|
||||
);
|
||||
return {
|
||||
inputTokenPrice: Number(rule.formulaConfig?.inputTokenPrice ?? rule.basePrice ?? 0.01),
|
||||
outputTokenPrice: Number(rule.formulaConfig?.outputTokenPrice ?? 0.03),
|
||||
inputTokenPrice,
|
||||
cachedInputTokenPrice: Number(
|
||||
rule.formulaConfig?.cachedInputTokenPrice ??
|
||||
rule.formulaConfig?.inputCacheHitTokenPrice ??
|
||||
compatRule.cachedInputTokenPrice ??
|
||||
compatRule.inputCacheHitTokenPrice ??
|
||||
inputTokenPrice,
|
||||
),
|
||||
outputTokenPrice: Number(rule.formulaConfig?.outputTokenPrice ?? compatRule.outputTokenPrice ?? 0.03),
|
||||
};
|
||||
}
|
||||
|
||||
function isTextPricingRule(rule: PricingRuleInput) {
|
||||
const resourceType = String(rule.resourceType ?? '');
|
||||
const ruleKey = String(rule.ruleKey ?? '');
|
||||
return textPricingKeys.has(resourceType) || textPricingKeys.has(ruleKey);
|
||||
}
|
||||
|
||||
const textPricingKeys = new Set(['text', 'text_total', 'text_input', 'text_cached_input', 'text_output']);
|
||||
|
||||
function createRule(ruleKey: string, displayName: string, resourceType: string, unit: string, basePrice: number, currency: string, calculatorType: string, formula: string, dynamicWeight: RecordValue, dimensionSchema: RecordValue): PricingRuleInput {
|
||||
return {
|
||||
ruleKey,
|
||||
|
||||
@@ -266,14 +266,27 @@ function pricingRuleSummaries(ruleSet: PricingRuleSet): PricingRuleSummary[] {
|
||||
const rules = ruleSet.rules ?? [];
|
||||
const textTotal = rules.find((rule) => rule.resourceType === 'text_total' || rule.ruleKey === 'text');
|
||||
const textInput = rules.find((rule) => rule.resourceType === 'text_input');
|
||||
const textCachedInput = rules.find(
|
||||
(rule) => rule.resourceType === 'text_cached_input',
|
||||
);
|
||||
const textOutput = rules.find((rule) => rule.resourceType === 'text_output');
|
||||
const image = rules.find((rule) => rule.resourceType === 'image');
|
||||
const video = rules.find((rule) => rule.resourceType === 'video');
|
||||
const items: PricingRuleSummary[] = [];
|
||||
if (textTotal || textInput || textOutput) {
|
||||
if (textTotal || textInput || textCachedInput || textOutput) {
|
||||
const inputPrice = Number(textTotal?.formulaConfig?.inputTokenPrice ?? textInput?.basePrice ?? textTotal?.basePrice ?? 0);
|
||||
const cachedInputPrice = Number(
|
||||
textTotal?.formulaConfig?.cachedInputTokenPrice ??
|
||||
textTotal?.formulaConfig?.inputCacheHitTokenPrice ??
|
||||
textCachedInput?.basePrice ??
|
||||
inputPrice,
|
||||
);
|
||||
const outputPrice = Number(textTotal?.formulaConfig?.outputTokenPrice ?? textOutput?.basePrice ?? 0);
|
||||
items.push({ kind: 'text', label: '文本', value: `输入 ${formatPrice(inputPrice)} / 输出 ${formatPrice(outputPrice)} / 1K Token` });
|
||||
items.push({
|
||||
kind: 'text',
|
||||
label: '文本',
|
||||
value: `输入 ${formatPrice(inputPrice)} / 缓存输入 ${formatPrice(cachedInputPrice)} / 输出 ${formatPrice(outputPrice)} / 1K Token`,
|
||||
});
|
||||
}
|
||||
if (image) items.push({ kind: 'image', label: '图像', value: `${formatPrice(image.basePrice)} / ${unitLabel(image.unit)}` });
|
||||
if (video) items.push({ kind: 'video', label: '视频', value: `${formatPrice(video.basePrice)} / ${unitLabel(video.unit)}` });
|
||||
|
||||
Reference in New Issue
Block a user