mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
♻️ refactor: fix model runtime cost calculate with CNY (#9834)
* fix model runtime cost calculate * add tests
This commit is contained in:
@@ -256,15 +256,11 @@ export class AgentRuntime {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new agent state with flexible initialization
|
||||
* @param partialState - Partial state to override defaults
|
||||
* @returns Complete AgentState with defaults filled in
|
||||
* Create default usage statistics structure
|
||||
* @returns Default Usage object with all counters set to 0
|
||||
*/
|
||||
static createInitialState(partialState: Partial<AgentState> & { sessionId: string }): AgentState {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Default usage statistics
|
||||
const defaultUsage: Usage = {
|
||||
static createDefaultUsage(): Usage {
|
||||
return {
|
||||
humanInteraction: {
|
||||
approvalRequests: 0,
|
||||
promptRequests: 0,
|
||||
@@ -282,9 +278,15 @@ export class AgentRuntime {
|
||||
totalTimeMs: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Default cost structure
|
||||
const defaultCost: Cost = {
|
||||
/**
|
||||
* Create default cost structure
|
||||
* @returns Default Cost object with all costs set to 0
|
||||
*/
|
||||
static createDefaultCost(): Cost {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
calculatedAt: now,
|
||||
currency: 'USD',
|
||||
llm: {
|
||||
@@ -299,16 +301,25 @@ export class AgentRuntime {
|
||||
},
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new agent state with flexible initialization
|
||||
* @param partialState - Partial state to override defaults
|
||||
* @returns Complete AgentState with defaults filled in
|
||||
*/
|
||||
static createInitialState(partialState: Partial<AgentState> & { sessionId: string }): AgentState {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return {
|
||||
cost: defaultCost,
|
||||
cost: AgentRuntime.createDefaultCost(),
|
||||
// Default values
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
status: 'idle',
|
||||
stepCount: 0,
|
||||
usage: defaultUsage,
|
||||
usage: AgentRuntime.createDefaultUsage(),
|
||||
// User provided values override defaults
|
||||
...partialState,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ModelUsage } from '@lobechat/types';
|
||||
|
||||
import type { FinishReason } from './event';
|
||||
import { AgentState, ToolRegistry, ToolsCalling } from './state';
|
||||
import type { Cost, CostCalculationContext, Usage } from './usage';
|
||||
@@ -26,6 +28,8 @@ export interface AgentRuntimeContext {
|
||||
status: AgentState['status'];
|
||||
stepCount: number;
|
||||
};
|
||||
/** Usage statistics from the current step (if applicable) */
|
||||
stepUsage?: ModelUsage;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// in 2025.01.26
|
||||
export const USD_TO_CNY = 7.24;
|
||||
// in 2025.10.22
|
||||
export const USD_TO_CNY = 7.12;
|
||||
|
||||
export const CREDITS_PER_DOLLAR = 1_000_000;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Pricing } from 'model-bank';
|
||||
import anthropicChatModels from 'model-bank/anthropic';
|
||||
import googleChatModels from 'model-bank/google';
|
||||
import lobehubChatModels from 'model-bank/lobehub';
|
||||
@@ -157,13 +158,13 @@ describe('computeChatPricing', () => {
|
||||
// Verify cached tokens (over 200k threshold, use higher tier rate)
|
||||
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
||||
expect(cached?.quantity).toBe(253_891);
|
||||
expect(cached?.credits).toBeCloseTo(158_681.875, 6);
|
||||
expect(cached?.credits).toBe(158_682); // ceil(158681.875) = 158682
|
||||
expect(cached?.segments).toEqual([{ quantity: 253_891, rate: 0.625, credits: 158_681.875 }]);
|
||||
|
||||
// Verify input cache miss tokens (calculated as totalInputTokens - inputCachedTokens = 4275)
|
||||
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
||||
expect(input?.quantity).toBe(4_275); // 258_166 - 253_891 = 4_275 (cache miss)
|
||||
expect(input?.credits).toBeCloseTo(5_343.75, 6);
|
||||
expect(input?.credits).toBe(5_344); // ceil(5343.75) = 5344
|
||||
expect(input?.segments).toEqual([{ quantity: 4_275, rate: 1.25, credits: 5_343.75 }]);
|
||||
|
||||
// Verify output tokens include reasoning tokens (under 200k threshold, use lower tier rate)
|
||||
@@ -173,7 +174,7 @@ describe('computeChatPricing', () => {
|
||||
expect(output?.segments).toEqual([{ quantity: 3_063, rate: 10, credits: 30_630 }]);
|
||||
|
||||
// Verify corrected totals (no double counting of cached tokens)
|
||||
expect(totalCredits).toBe(194_656); // ceil(158681.875 + 5343.75 + 30630) = 194656
|
||||
expect(totalCredits).toBe(194_656); // 158682 + 5344 + 30630 = 194656
|
||||
expect(totalCost).toBeCloseTo(0.194656, 6); // 194656 credits = $0.194656
|
||||
});
|
||||
|
||||
@@ -274,13 +275,13 @@ describe('computeChatPricing', () => {
|
||||
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
||||
expect(cached?.quantity).toBe(257_955);
|
||||
|
||||
expect(cached?.credits).toBeCloseTo(161_221.875, 6);
|
||||
expect(cached?.credits).toBe(161_222); // ceil(161221.875) = 161222
|
||||
expect(cached?.segments).toEqual([{ quantity: 257_955, rate: 0.625, credits: 161_221.875 }]);
|
||||
|
||||
// Verify input cache miss tokens (under 200k tier, use lower rate)
|
||||
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
||||
expect(input?.quantity).toBe(5_005);
|
||||
expect(input?.credits).toBeCloseTo(6_256.25, 6);
|
||||
expect(input?.credits).toBe(6_257); // ceil(6256.25) = 6257
|
||||
expect(input?.segments).toEqual([{ quantity: 5_005, rate: 1.25, credits: 6_256.25 }]);
|
||||
|
||||
// Verify output tokens (under 200k threshold, use lower tier rate)
|
||||
@@ -290,7 +291,7 @@ describe('computeChatPricing', () => {
|
||||
expect(output?.segments).toEqual([{ quantity: 1_744, rate: 10, credits: 17_440 }]);
|
||||
|
||||
// Verify totals match actual billing log
|
||||
expect(totalCredits).toBe(184_919); // ceil(161221.875 + 6256.25 + 17440) = 184919
|
||||
expect(totalCredits).toBe(184_919); // 161222 + 6257 + 17440 = 184919
|
||||
expect(totalCost).toBeCloseTo(0.184919, 6); // 184919 credits = $0.184919
|
||||
});
|
||||
});
|
||||
@@ -468,16 +469,16 @@ describe('computeChatPricing', () => {
|
||||
// Verify cached tokens (discounted rate)
|
||||
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
||||
expect(cached?.quantity).toBe(1183);
|
||||
expect(cached?.credits).toBeCloseTo(354.9, 6);
|
||||
expect(cached?.credits).toBe(355); // 354.9 rounded = 355
|
||||
|
||||
// Verify cache write tokens
|
||||
const cacheWrite = breakdown.find((item) => item.unit.name === 'textInput_cacheWrite');
|
||||
expect(cacheWrite?.quantity).toBe(458);
|
||||
expect(cacheWrite?.lookupKey).toBe('5m');
|
||||
expect(cacheWrite?.credits).toBeCloseTo(1_717.5, 6);
|
||||
expect(cacheWrite?.credits).toBe(1_718); // 1717.5 rounded = 1718
|
||||
|
||||
// Verify totals match the actual billing log
|
||||
expect(totalCredits).toBe(9_915); // ceil(12 + 7830 + 354.9 + 1717.5) = 9915
|
||||
expect(totalCredits).toBe(9_915); // 12 + 7830 + 355 + 1718 = 9915
|
||||
expect(totalCost).toBeCloseTo(0.009915, 6); // 9915 credits = $0.009915
|
||||
});
|
||||
|
||||
@@ -516,15 +517,15 @@ describe('computeChatPricing', () => {
|
||||
// Verify cached tokens (discounted rate)
|
||||
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
||||
expect(cached?.quantity).toBe(3021);
|
||||
expect(cached?.credits).toBeCloseTo(906.3, 6);
|
||||
expect(cached?.credits).toBe(907); // ceil(906.3) = 907
|
||||
|
||||
// Verify cache write tokens (fixed strategy in lobehub model)
|
||||
const cacheWrite = breakdown.find((item) => item.unit.name === 'textInput_cacheWrite');
|
||||
expect(cacheWrite?.quantity).toBe(1697);
|
||||
expect(cacheWrite?.credits).toBeCloseTo(6_363.75, 6);
|
||||
expect(cacheWrite?.credits).toBe(6_364); // ceil(6363.75) = 6364
|
||||
|
||||
// Verify totals match the actual billing log
|
||||
expect(totalCredits).toBe(49_916); // ceil(30 + 42615 + 906.3 + 6363.75) = 49916
|
||||
expect(totalCredits).toBe(49_916); // 30 + 42615 + 907 + 6364 = 49916
|
||||
expect(totalCost).toBeCloseTo(0.049916, 6); // 49916 credits = $0.049916
|
||||
});
|
||||
});
|
||||
@@ -682,4 +683,196 @@ describe('computeChatPricing', () => {
|
||||
expect(result?.totalCost).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Currency Conversion', () => {
|
||||
describe('DeepSeek (CNY pricing)', () => {
|
||||
it('converts CNY to USD for deepseek-chat without cache', () => {
|
||||
// DeepSeek pricing in CNY
|
||||
const pricing = {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
};
|
||||
|
||||
const usage: ModelTokensUsage = {
|
||||
inputCacheMissTokens: 1000,
|
||||
inputTextTokens: 1000,
|
||||
outputTextTokens: 500,
|
||||
totalInputTokens: 1000,
|
||||
totalOutputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
};
|
||||
|
||||
// Use fixed exchange rate for testing
|
||||
const result = computeChatCost(pricing as any, usage, { usdToCnyRate: 5 });
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.issues).toHaveLength(0);
|
||||
|
||||
const { breakdown, totalCost, totalCredits } = result!;
|
||||
expect(breakdown).toHaveLength(2); // Input and output
|
||||
|
||||
// Verify input tokens
|
||||
// 1000 tokens * 2 CNY/M = 2000 raw CNY-credits
|
||||
// 2000 / 5 = 400 USD-credits
|
||||
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
||||
expect(input?.quantity).toBe(1000);
|
||||
expect(input?.credits).toBe(400); // USD credits
|
||||
|
||||
// Verify output tokens
|
||||
// 500 tokens * 3 CNY/M = 1500 raw CNY-credits
|
||||
// 1500 / 5 = 300 USD-credits
|
||||
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
||||
expect(output?.quantity).toBe(500);
|
||||
expect(output?.credits).toBe(300); // USD credits
|
||||
|
||||
// Verify totals with CNY to USD conversion
|
||||
// Total USD credits = 400 + 300 = 700
|
||||
// totalCredits = ceil(700) = 700
|
||||
expect(totalCredits).toBe(700);
|
||||
|
||||
// totalCost = 700 / 1_000_000 = 0.0007 USD
|
||||
expect(totalCost).toBeCloseTo(0.0007, 6);
|
||||
});
|
||||
|
||||
it('converts CNY to USD for deepseek-chat with cache tokens', () => {
|
||||
const pricing = {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput_cacheRead', rate: 0.2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
} satisfies Pricing;
|
||||
|
||||
const usage: ModelTokensUsage = {
|
||||
inputCacheMissTokens: 785,
|
||||
inputCachedTokens: 2752,
|
||||
inputTextTokens: 3537,
|
||||
outputTextTokens: 77,
|
||||
totalInputTokens: 3537,
|
||||
totalOutputTokens: 77,
|
||||
totalTokens: 3614,
|
||||
};
|
||||
|
||||
const result = computeChatCost(pricing, usage, { usdToCnyRate: 5 });
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.issues).toHaveLength(0);
|
||||
|
||||
const { breakdown, totalCost, totalCredits } = result!;
|
||||
expect(breakdown).toHaveLength(3); // Cache read, input, and output
|
||||
|
||||
// Verify cache miss tokens
|
||||
// 785 tokens * 2 CNY/M = 1570 raw CNY-credits
|
||||
// 1570 / 5 = 314 USD-credits
|
||||
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
||||
expect(input?.quantity).toBe(785);
|
||||
expect(input?.credits).toBe(314); // USD credits
|
||||
|
||||
// Verify cached tokens
|
||||
// 2752 tokens * 0.2 CNY/M = 550.4 raw CNY-credits
|
||||
// 550.4 / 5 = 110.08 -> ceil(110.08) = 111 USD-credits
|
||||
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
||||
expect(cached?.quantity).toBe(2752);
|
||||
expect(cached?.credits).toBe(111); // USD credits
|
||||
|
||||
// Verify output tokens
|
||||
// 77 tokens * 3 CNY/M = 231 raw CNY-credits
|
||||
// 231 / 5 = 46.2 -> ceil(46.2) = 47 USD-credits
|
||||
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
||||
expect(output?.quantity).toBe(77);
|
||||
expect(output?.credits).toBe(47); // USD credits
|
||||
|
||||
// Verify totals with CNY to USD conversion
|
||||
// Total USD credits = 314 + 111 + 47 = 472
|
||||
expect(totalCredits).toBe(472);
|
||||
|
||||
// totalCost = 472 / 1_000_000 = 0.000472 USD
|
||||
expect(totalCost).toBe(0.000472);
|
||||
});
|
||||
|
||||
it('converts CNY to USD for large token usage', () => {
|
||||
const pricing = {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
};
|
||||
|
||||
const usage: ModelTokensUsage = {
|
||||
inputTextTokens: 1_000_000, // 1M input tokens
|
||||
outputTextTokens: 500_000, // 500K output tokens
|
||||
};
|
||||
|
||||
const result = computeChatCost(pricing as any, usage, { usdToCnyRate: 5 });
|
||||
expect(result).toBeDefined();
|
||||
|
||||
const { totalCost, totalCredits } = result!;
|
||||
|
||||
// Input: 1M * 2 CNY = 2M CNY-credits = 2M / 5 = 400000 USD-credits
|
||||
// Output: 500K * 3 CNY = 1.5M CNY-credits = 1.5M / 5 = 300000 USD-credits
|
||||
// Total: 700000 USD-credits
|
||||
expect(totalCredits).toBe(700_000);
|
||||
|
||||
// totalCost = 700000 / 1_000_000 = 0.7 USD
|
||||
expect(totalCost).toBe(0.7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('USD pricing (no conversion)', () => {
|
||||
it('does not convert USD pricing', () => {
|
||||
const pricing = {
|
||||
currency: 'USD',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 8, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
};
|
||||
|
||||
const usage: ModelTokensUsage = {
|
||||
inputTextTokens: 1000,
|
||||
outputTextTokens: 500,
|
||||
};
|
||||
|
||||
const result = computeChatCost(pricing as any, usage);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
const { totalCost, totalCredits } = result!;
|
||||
|
||||
// Input: 1000 * 2 = 2000 USD-credits
|
||||
// Output: 500 * 8 = 4000 USD-credits
|
||||
// Total: 6000 USD-credits
|
||||
expect(totalCredits).toBe(6000);
|
||||
|
||||
// totalCost = 6000 / 1_000_000 = 0.006 USD
|
||||
expect(totalCost).toBeCloseTo(0.006, 6);
|
||||
});
|
||||
|
||||
it('defaults to USD when currency is not specified', () => {
|
||||
const pricing = {
|
||||
// No currency field
|
||||
units: [
|
||||
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 8, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
};
|
||||
|
||||
const usage: ModelTokensUsage = {
|
||||
inputTextTokens: 1000,
|
||||
outputTextTokens: 500,
|
||||
};
|
||||
|
||||
const result = computeChatCost(pricing as any, usage);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
const { totalCost, totalCredits } = result!;
|
||||
|
||||
// Should be treated as USD (no conversion)
|
||||
expect(totalCredits).toBe(6000);
|
||||
expect(totalCost).toBeCloseTo(0.006, 6);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
import { CREDITS_PER_DOLLAR } from '@lobechat/const/currency';
|
||||
import { CREDITS_PER_DOLLAR, USD_TO_CNY } from '@lobechat/const/currency';
|
||||
import debug from 'debug';
|
||||
import {
|
||||
FixedPricingUnit,
|
||||
@@ -17,6 +17,7 @@ const log = debug('lobe-cost:computeChatPricing');
|
||||
export interface PricingUnitBreakdown {
|
||||
cost: number;
|
||||
credits: number;
|
||||
currency: string | 'USD' | 'CNY';
|
||||
/**
|
||||
* For lookup strategies we expose the resolved key.
|
||||
*/
|
||||
@@ -39,6 +40,11 @@ export interface ComputeChatCostOptions {
|
||||
* Input parameters used by lookup strategies (e.g. ttl, thinkingMode).
|
||||
*/
|
||||
lookupParams?: Record<string, string | number | boolean>;
|
||||
/**
|
||||
* Exchange rate for CNY to USD conversion. Defaults to USD_TO_CNY constant.
|
||||
* Useful for testing with fixed exchange rates.
|
||||
*/
|
||||
usdToCnyRate?: number;
|
||||
}
|
||||
|
||||
export interface PricingComputationResult {
|
||||
@@ -98,6 +104,27 @@ const UNIT_QUANTITY_RESOLVERS: Partial<Record<PricingUnitName, UnitQuantityResol
|
||||
audioOutput: (usage) => usage.outputAudioTokens,
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert currency-specific credits to USD credits and ceil to integer
|
||||
* @param credits - Credits in the original currency
|
||||
* @param currency - The currency of the credits ('USD' or 'CNY')
|
||||
* @param usdToCnyRate - Exchange rate for CNY to USD conversion (defaults to USD_TO_CNY constant)
|
||||
* @returns USD-equivalent credits (ceiled to integer)
|
||||
*/
|
||||
const toUSDCredits = (
|
||||
credits: number,
|
||||
currency: string = 'USD',
|
||||
usdToCnyRate = USD_TO_CNY,
|
||||
): number => {
|
||||
const usdCredits = currency === 'CNY' ? credits / usdToCnyRate : credits;
|
||||
return Math.ceil(usdCredits);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert credits to USD dollar amount
|
||||
* @param credits - USD credits
|
||||
* @returns USD dollar amount
|
||||
*/
|
||||
const creditsToUSD = (credits: number) => credits / CREDITS_PER_DOLLAR;
|
||||
|
||||
/**
|
||||
@@ -221,6 +248,8 @@ export const computeChatCost = (
|
||||
|
||||
const breakdown: PricingUnitBreakdown[] = [];
|
||||
const issues: PricingComputationIssue[] = [];
|
||||
const currency = pricing.currency || 'USD';
|
||||
const usdToCnyRate = options?.usdToCnyRate ?? USD_TO_CNY;
|
||||
|
||||
for (const unit of pricing.units) {
|
||||
const quantity = resolveQuantity(unit, usage);
|
||||
@@ -231,11 +260,13 @@ export const computeChatCost = (
|
||||
throw new Error(`Unsupported chat pricing unit: ${unit.unit}`);
|
||||
|
||||
const fixedUnit = unit as FixedPricingUnit;
|
||||
const credits = computeFixedCredits(fixedUnit, quantity);
|
||||
const rawCredits = computeFixedCredits(fixedUnit, quantity);
|
||||
const usdCredits = toUSDCredits(rawCredits, currency, usdToCnyRate);
|
||||
breakdown.push({
|
||||
cost: creditsToUSD(credits),
|
||||
credits,
|
||||
cost: creditsToUSD(usdCredits),
|
||||
credits: usdCredits,
|
||||
quantity,
|
||||
currency,
|
||||
unit,
|
||||
});
|
||||
continue;
|
||||
@@ -243,11 +274,13 @@ export const computeChatCost = (
|
||||
|
||||
if (unit.strategy === 'tiered') {
|
||||
const tieredUnit = unit as TieredPricingUnit;
|
||||
const { credits, segments } = computeTieredCredits(tieredUnit, quantity);
|
||||
const { credits: rawCredits, segments } = computeTieredCredits(tieredUnit, quantity);
|
||||
const usdCredits = toUSDCredits(rawCredits, currency, usdToCnyRate);
|
||||
breakdown.push({
|
||||
cost: creditsToUSD(credits),
|
||||
credits,
|
||||
cost: creditsToUSD(usdCredits),
|
||||
credits: usdCredits,
|
||||
quantity,
|
||||
currency,
|
||||
segments,
|
||||
unit,
|
||||
});
|
||||
@@ -257,18 +290,20 @@ export const computeChatCost = (
|
||||
if (unit.strategy === 'lookup') {
|
||||
const lookupUnit = unit as LookupPricingUnit;
|
||||
const {
|
||||
credits,
|
||||
credits: rawCredits,
|
||||
key,
|
||||
issues: lookupIssue,
|
||||
} = computeLookupCredits(lookupUnit, quantity, options);
|
||||
|
||||
if (lookupIssue) issues.push(lookupIssue);
|
||||
|
||||
const usdCredits = toUSDCredits(rawCredits, currency, usdToCnyRate);
|
||||
breakdown.push({
|
||||
cost: creditsToUSD(credits),
|
||||
credits,
|
||||
cost: creditsToUSD(usdCredits),
|
||||
credits: usdCredits,
|
||||
lookupKey: key,
|
||||
quantity,
|
||||
currency,
|
||||
unit,
|
||||
});
|
||||
continue;
|
||||
@@ -277,9 +312,10 @@ export const computeChatCost = (
|
||||
issues.push({ reason: 'Unsupported pricing strategy', unit });
|
||||
}
|
||||
|
||||
// Sum up USD credits from all breakdown items
|
||||
const rawTotalCredits = breakdown.reduce((sum, item) => sum + item.credits, 0);
|
||||
const totalCredits = Math.ceil(rawTotalCredits);
|
||||
// !: totalCredits has been uniformly rounded up to integer credits, divided by CREDITS_PER_DOLLAR naturally retains only 6 decimal places, no additional processing needed
|
||||
// !: totalCredits has been uniformly rounded up to integer USD credits, divided by CREDITS_PER_DOLLAR naturally retains only 6 decimal places, no additional processing needed
|
||||
const totalCost = creditsToUSD(totalCredits);
|
||||
|
||||
log(`computeChatPricing breakdown: ${JSON.stringify(breakdown, null, 2)}`);
|
||||
|
||||
18
src/app/__tests__/desktop.routes.test.ts
Normal file
18
src/app/__tests__/desktop.routes.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import fs from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('Desktop Routes', () => {
|
||||
const appRootDir = resolve(__dirname, '..');
|
||||
|
||||
const desktopRoutes = [
|
||||
'(backend)/trpc/desktop/[trpc]/route.ts',
|
||||
'desktop/devtools/page.tsx',
|
||||
'desktop/layout.tsx',
|
||||
];
|
||||
|
||||
it.each(desktopRoutes)('should have file: %s', (route) => {
|
||||
const filePath = resolve(appRootDir, route);
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user