From 0c5aa638a40ff7cb94faec2b8225c3d87e7545ea Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:28:17 +1100 Subject: [PATCH] =?UTF-8?q?dashscope-cn:=20bill=20a=20=E9=99=90=E6=97=B6N?= =?UTF-8?q?=E6=8A=98=20token=20quote=20at=20the=20discounted=20rate=20(#60?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../providers/dashscope-cn-docs.test.ts | 33 +++++++++++++++++++ src/server/providers/dashscope-cn-docs.ts | 22 ++++++++++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/server/providers/dashscope-cn-docs.test.ts b/src/server/providers/dashscope-cn-docs.test.ts index 29415b5d..3800273c 100644 --- a/src/server/providers/dashscope-cn-docs.test.ts +++ b/src/server/providers/dashscope-cn-docs.test.ts @@ -214,6 +214,39 @@ describe('DashScope China native source parsing', () => { 'unreadable RMB token quote: 13元 未知后缀:1.3元', ) }) + it('bills 原价N元 (限时X折) at the fold of list price, with no invented end', () => { + const cell = '原价2元 (限时2.5折)' + const quoted = ( + input: string, + output = '原价6元 (限时2.5折)', + ): CnDoc => ({ + ...prices, + text: `## 文本生成-第三方模型\n模型 ID(Model ID)输入单价(每百万Token)输出单价(每百万Token)antdigital/Ling-3.1-flash${input}${output}`, + }) + const [row] = cnPriceModels(quoted(cell)) + const card = rateCardSchema.parse(row!.pricing) + expect(row!.rawId).toBe('antdigital/Ling-3.1-flash') + expect(cardCurrency(card)).toBe('CNY') + expect(card.source.expiresAt).toBeUndefined() + expect( + price(card, {}, { input_tokens: 1_000_000, output_tokens: 0 }), + ).toBeCloseTo(0.5) + expect( + price(card, {}, { input_tokens: 0, output_tokens: 1_000_000 }), + ).toBeCloseTo(1.5) + const eight = rateCardSchema.parse( + cnPriceModels(quoted('原价2元 (限时8折)', '8元'))[0]!.pricing, + ) + expect( + price(eight, {}, { input_tokens: 1_000_000, output_tokens: 0 }), + ).toBeCloseTo(1.6) + expect(() => cnPriceModels(quoted('原价2元 (限时优惠)'))).toThrow( + 'unreadable RMB token quote: 原价2元 (限时优惠)', + ) + expect(() => + cnPriceModels(quoted('原价2元 (限时2.5折至2026-12-31)')), + ).toThrow('unreadable RMB token quote: 原价2元 (限时2.5折至2026-12-31)') + }) it('reads a price cell that tiers itself by prompt size', () => { const tiered = (input: string, output: string): CnDoc => ({ ...prices, diff --git a/src/server/providers/dashscope-cn-docs.ts b/src/server/providers/dashscope-cn-docs.ts index 9904e94b..fab522c0 100644 --- a/src/server/providers/dashscope-cn-docs.ts +++ b/src/server/providers/dashscope-cn-docs.ts @@ -127,15 +127,27 @@ interface YuanQuote { } /** * A plain cell is `元`. It may also state the cache-hit input rate: - * `13元 缓存命中输入:1.3元`. Any other suffix throws. + * `13元 缓存命中输入:1.3元`. + * + * `原价2元 (限时2.5折)` states a list price and the share billed while + * the promo runs (N折 = N/10 of list, so 2.5折 of 2元 is 0.5元). The cell + * names no end, so the card carries no `expiresAt` (#552). Any other promo + * wording throws. */ function plainQuote(text: string): YuanQuote { const match = /^(\d+(?:\.\d+)?)元(?:\s+缓存命中输入:(\d+(?:\.\d+)?)元)?$/.exec(text) - if (!match) fail(`unreadable RMB token quote: ${text}`) - const quote: YuanQuote = { rate: yuanPerMillion(match[1]!) } - if (match[2] !== undefined) quote.cacheRead = yuanPerMillion(match[2]) - return quote + if (match) { + const quote: YuanQuote = { rate: yuanPerMillion(match[1]!) } + if (match[2] !== undefined) quote.cacheRead = yuanPerMillion(match[2]) + return quote + } + const promo = /^原价(\d+(?:\.\d+)?)元\s*(限时(\d+(?:\.\d+)?)折)$/.exec(text) + if (!promo) fail(`unreadable RMB token quote: ${text}`) + const fold = Number(promo[2]) + // 10折 is the list price. Above that, or a zero fold, is not this wording. + if (!(fold > 0) || fold > 10) fail(`unreadable RMB token quote: ${text}`) + return { rate: yuanPerMillion(String(Number(promo[1]) * (fold / 10))) } } /** * One quote per prompt-size floor. A cell may tier itself, as MiniMax-M3's