diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index 70aef99bd..9a1cb49f7 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -188,7 +188,7 @@ export function Composer(props: { const tokensHistory = _historyTokenCount; const tokensReponseMax = (props.chatLLM?.options as DOpenAILLMOptions /* FIXME: BIG ASSUMPTION */)?.llmResponseTokens || 0; const tokenLimit = props.chatLLM?.contextTokens || 0; - const tokenPricing = props.chatLLM?.pricing?.chat; + const tokenChatPricing = props.chatLLM?.pricing?.chat; // Effect: load initial text if queued up (e.g. by /link/share_targe) @@ -699,11 +699,11 @@ export function Composer(props: { }} /> {!showChatInReferenceTo && tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && ( - + )} {!showChatInReferenceTo && tokenLimit > 0 && ( - + )} diff --git a/src/apps/chat/components/composer/tokens/TokenBadge.tsx b/src/apps/chat/components/composer/tokens/TokenBadge.tsx index ebc8b2d1a..e3a48656a 100644 --- a/src/apps/chat/components/composer/tokens/TokenBadge.tsx +++ b/src/apps/chat/components/composer/tokens/TokenBadge.tsx @@ -13,16 +13,13 @@ import { formatTokenCost, tokenCountsMathAndMessage, TokenTooltip } from './Toke export const TokenBadgeMemo = React.memo(TokenBadge); function TokenBadge(props: { + chatPricing?: DPriceChatGenerate, + direct: number, history?: number, responseMax?: number, limit: number, - // FIXME: continue from here - tokenPricing?: DPriceChatGenerate, - tokenPriceIn?: number, - tokenPriceOut?: number, - enableHover?: boolean, showCost?: boolean showExcess?: boolean, @@ -34,7 +31,7 @@ function TokenBadge(props: { const [isHovering, setIsHovering] = React.useState(false); const { message, color, remainingTokens, costMax, costMin } = - tokenCountsMathAndMessage(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut); + tokenCountsMathAndMessage(props.limit, props.direct, props.history, props.responseMax, props.chatPricing); // handlers diff --git a/src/apps/chat/components/composer/tokens/TokenProgressbar.tsx b/src/apps/chat/components/composer/tokens/TokenProgressbar.tsx index 65fa3e7be..1d4d3c1d1 100644 --- a/src/apps/chat/components/composer/tokens/TokenProgressbar.tsx +++ b/src/apps/chat/components/composer/tokens/TokenProgressbar.tsx @@ -15,15 +15,12 @@ import { tokenCountsMathAndMessage, TokenTooltip } from './TokenTooltip'; export const TokenProgressbarMemo = React.memo(TokenProgressbar); function TokenProgressbar(props: { + chatPricing?: DPriceChatGenerate, + direct: number, history: number, responseMax: number, limit: number, - - // FIXME: continue from here - tokenPricing?: DPriceChatGenerate, - tokenPriceIn?: number, - tokenPriceOut?: number, }) { // external state @@ -53,7 +50,7 @@ function TokenProgressbar(props: { const overflowColor = theme.palette.danger.softColor; // tooltip message/color - const { message, color } = tokenCountsMathAndMessage(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut); + const { message, color } = tokenCountsMathAndMessage(props.limit, props.direct, props.history, props.responseMax, props.chatPricing); // sizes const containerHeight = 8; diff --git a/src/apps/chat/components/composer/tokens/TokenTooltip.tsx b/src/apps/chat/components/composer/tokens/TokenTooltip.tsx index dc721a9c4..a3e2452d8 100644 --- a/src/apps/chat/components/composer/tokens/TokenTooltip.tsx +++ b/src/apps/chat/components/composer/tokens/TokenTooltip.tsx @@ -3,11 +3,13 @@ import * as React from 'react'; import type { SxProps } from '@mui/joy/styles/types'; import { Box, ColorPaletteProp, Tooltip } from '@mui/joy'; +import type { DPriceChatGenerate } from '~/common/stores/llms/dllm.types'; import { adjustContentScaling, themeScalingMap } from '~/common/app.theme'; +import { getPriceForTokens } from '~/common/stores/llms/llms.pricing'; import { useUIContentScaling } from '~/common/state/store-ui'; -export function tokenCountsMathAndMessage(tokenLimit: number | 0, directTokens: number, historyTokens?: number, responseMaxTokens?: number, tokenPriceIn?: number, tokenPriceOut?: number): { +export function tokenCountsMathAndMessage(tokenLimit: number | 0, directTokens: number, historyTokens?: number, responseMaxTokens?: number, chatPricing?: DPriceChatGenerate): { color: ColorPaletteProp, message: string, remainingTokens: number, @@ -40,28 +42,43 @@ export function tokenCountsMathAndMessage(tokenLimit: number | 0, directTokens: ` - Max response: ${_alignRight(responseMaxTokens || 0)}`; // add the price, if available - if (tokenPriceIn || tokenPriceOut) { - costMin = tokenPriceIn ? usedInputTokens * tokenPriceIn / 1E6 : undefined; - const costOutMax = (tokenPriceOut && responseMaxTokens) ? responseMaxTokens * tokenPriceOut / 1E6 : undefined; - if (costMin || costOutMax) { + if (chatPricing) { + const inputPrice = getPriceForTokens(usedInputTokens, usedInputTokens, chatPricing.input); + const outputPrice = getPriceForTokens(usedInputTokens, responseMaxTokens || 0, chatPricing.output); + + costMin = inputPrice; + const costOutMax = outputPrice; + + if (costMin !== undefined || costOutMax !== undefined) { message += `\n\n\n▶ Chat Turn Cost (max, approximate)\n`; - if (costMin) message += '\n' + - ` Input tokens: ${_alignRight(usedInputTokens)}\n` + - ` Input Price $/M: ${tokenPriceIn!.toFixed(2).padStart(8)}\n` + - ` Input cost: ${('$' + costMin!.toFixed(4)).padStart(8)}\n`; + if (costMin !== undefined) { + const inputPricePerM = costMin * 1e6 / usedInputTokens; + message += '\n' + + ` Input tokens: ${_alignRight(usedInputTokens)}\n` + + ` Input Price $/M: ${inputPricePerM.toFixed(2).padStart(8)}\n` + + ` Input cost: ${('$' + costMin.toFixed(4)).padStart(8)}\n`; + } - if (costOutMax) message += '\n' + - ` Max output tokens: ${_alignRight(responseMaxTokens!)}\n` + - ` Output Price $/M: ${tokenPriceOut!.toFixed(2).padStart(8)}\n` + - ` Max output cost: ${('$' + costOutMax!.toFixed(4)).padStart(8)}\n`; + if (costOutMax !== undefined) { + const outputPricePerM = costOutMax * 1e6 / (responseMaxTokens || 1); + message += '\n' + + ` Max output tokens: ${_alignRight(responseMaxTokens!)}\n` + + ` Output Price $/M: ${outputPricePerM.toFixed(2).padStart(8)}\n` + + ` Max output cost: ${('$' + costOutMax.toFixed(4)).padStart(8)}\n`; + } - if (costMin) message += '\n' + - ` > Min message cost: ${formatTokenCost(costMin).padStart(8)}`; - costMax = (costMin && costOutMax) ? costMin + costOutMax : undefined; - if (costMax) message += '\n' + - ` < Max message cost: ${formatTokenCost(costMax).padStart(8)}\n` + - ' (depends on assistant response)'; + if (costMin !== undefined) { + message += '\n' + + ` > Min message cost: ${formatTokenCost(costMin).padStart(8)}`; + } + + costMax = (costMin !== undefined && costOutMax !== undefined) ? costMin + costOutMax : undefined; + if (costMax !== undefined) { + message += '\n' + + ` < Max message cost: ${formatTokenCost(costMax).padStart(8)}\n` + + ' (depends on assistant response)'; + } } } } diff --git a/src/common/stores/llms/llms.pricing.ts b/src/common/stores/llms/llms.pricing.ts index f23c4f1d0..e8015f6a8 100644 --- a/src/common/stores/llms/llms.pricing.ts +++ b/src/common/stores/llms/llms.pricing.ts @@ -1,6 +1,8 @@ import type { DLLM, DPriceChatGenerate, DPricePerMToken, DTieredPrice } from './dllm.types'; +/// detect Free Pricing + export function isModelPriceFree(priceChatGenerate: DPriceChatGenerate): boolean { if (!priceChatGenerate) return true; return _isPriceFree(priceChatGenerate.input) && _isPriceFree(priceChatGenerate.output); @@ -19,6 +21,28 @@ function _isPricePerMTokenFree(price: DPricePerMToken): boolean { } +/// Human readable price formatting + +export function getPriceForTokens(inputTokens: number, tokens: number, pricing: DTieredPrice | undefined): number | undefined { + if (!pricing) return undefined; + if (pricing === 'free') return 0; + if (typeof pricing === 'number') return tokens * pricing / 1e6; + + // Find the applicable tier based on input tokens + const applicableTier = pricing.find(tier => tier.upTo === null || inputTokens <= tier.upTo); + + // This should not happen if the pricing is well-formed + if (!applicableTier) { + console.log('[DEV] getPriceForTokens: No applicable tier found for input tokens', { inputTokens, pricing }); + return undefined; + } + + // Apply the price of the found tier to all tokens + if (applicableTier.price === 'free') return 0; + return tokens * applicableTier.price / 1e6; +} + + // Compatibiltiy layer for pricing V2 -> V3 interface Was_DModelPricingV2 {