diff --git a/controller/model_list_test.go b/controller/model_list_test.go index 812207b8fd44..0d3c43f357d2 100644 --- a/controller/model_list_test.go +++ b/controller/model_list_test.go @@ -267,10 +267,16 @@ func TestGetUserModelsExpandsAutoGroupsInConfiguredOrder(t *testing.T) { func TestListModelsIncludesTieredBillingModel(t *testing.T) { withSelfUseModeDisabled(t) + savedModelRatios := ratio_setting.ModelRatio2JSONString() + t.Cleanup(func() { + require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(savedModelRatios)) + }) + require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{"zz-utf8-visible-model":7.5}`)) withTieredBillingConfig(t, map[string]string{ "zz-tiered-visible-model": "tiered_expr", "zz-tiered-empty-expr-model": "tiered_expr", "zz-tiered-missing-expr-model": "tiered_expr", + "zz-utf8-visible-model": "utf8_bytes", }, map[string]string{ "zz-tiered-visible-model": `tier("base", p * 1 + c * 2)`, "zz-tiered-empty-expr-model": " ", @@ -288,6 +294,7 @@ func TestListModelsIncludesTieredBillingModel(t *testing.T) { {Group: "default", Model: "zz-tiered-visible-model", ChannelId: 1, Enabled: true}, {Group: "default", Model: "zz-tiered-empty-expr-model", ChannelId: 1, Enabled: true}, {Group: "default", Model: "zz-tiered-missing-expr-model", ChannelId: 1, Enabled: true}, + {Group: "default", Model: "zz-utf8-visible-model", ChannelId: 1, Enabled: true}, {Group: "default", Model: "zz-unpriced-model", ChannelId: 1, Enabled: true}, }).Error) @@ -302,6 +309,7 @@ func TestListModelsIncludesTieredBillingModel(t *testing.T) { require.Contains(t, ids, "zz-tiered-visible-model") require.NotContains(t, ids, "zz-tiered-empty-expr-model") require.NotContains(t, ids, "zz-tiered-missing-expr-model") + require.Contains(t, ids, "zz-utf8-visible-model") require.NotContains(t, ids, "zz-unpriced-model") pricingByName := pricingByModelName(model.GetPricing()) @@ -319,6 +327,11 @@ func TestListModelsIncludesTieredBillingModel(t *testing.T) { require.True(t, ok) require.Empty(t, missingExprPricing.BillingMode) require.Empty(t, missingExprPricing.BillingExpr) + + utf8Pricing, ok := pricingByName["zz-utf8-visible-model"] + require.True(t, ok) + require.Equal(t, "utf8_bytes", utf8Pricing.BillingMode) + require.Empty(t, utf8Pricing.BillingExpr) } func TestListModelsUsesAdvancedCustomEndpointTypesFromPricingCache(t *testing.T) { diff --git a/controller/ratio_sync.go b/controller/ratio_sync.go index 0001a60be7a1..327efb11282a 100644 --- a/controller/ratio_sync.go +++ b/controller/ratio_sync.go @@ -413,7 +413,9 @@ func FetchUpstreamRatios(c *gin.Context) { if item.ModelName == "" { continue } - if item.BillingMode == billing_setting.BillingModeTieredExpr && strings.TrimSpace(item.BillingExpr) != "" { + if item.BillingMode == billing_setting.BillingModeUTF8Bytes { + billingModeMap[item.ModelName] = billing_setting.BillingModeUTF8Bytes + } else if item.BillingMode == billing_setting.BillingModeTieredExpr && strings.TrimSpace(item.BillingExpr) != "" { billingModeMap[item.ModelName] = billing_setting.BillingModeTieredExpr billingExprMap[item.ModelName] = item.BillingExpr } diff --git a/controller/relay.go b/controller/relay.go index 8dccfe76dddd..2ab444931103 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -24,6 +24,7 @@ import ( "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" + "github.com/QuantumNous/new-api/setting/billing_setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/bytedance/gopkg/util/gopool" @@ -125,9 +126,10 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { newAPIError = types.NewError(err, types.ErrorCodeGenRelayInfoFailed) return } + relayInfo.BillingMode = billing_setting.GetBillingMode(relayInfo.OriginModelName) needSensitiveCheck := setting.ShouldCheckPromptSensitive() - needCountToken := constant.CountToken + needCountToken := constant.CountToken || relayInfo.BillingMode == billing_setting.BillingModeUTF8Bytes // Avoid building huge CombineText (strings.Join) when token counting and sensitive check are both disabled. var meta *types.TokenCountMeta if needSensitiveCheck || needCountToken { diff --git a/model/pricing.go b/model/pricing.go index 6dfbfe7aa9f7..73274306bfce 100644 --- a/model/pricing.go +++ b/model/pricing.go @@ -400,7 +400,9 @@ func updatePricing() { audioCompletionRatio := ratio_setting.GetAudioCompletionRatio(model) pricing.AudioCompletionRatio = &audioCompletionRatio } - if billingMode := billing_setting.GetBillingMode(model); billingMode == "tiered_expr" { + if billingMode := billing_setting.GetBillingMode(model); billingMode == billing_setting.BillingModeUTF8Bytes { + pricing.BillingMode = billingMode + } else if billingMode == billing_setting.BillingModeTieredExpr { if expr, ok := billing_setting.GetBillingExpr(model); ok && strings.TrimSpace(expr) != "" { pricing.BillingMode = billingMode pricing.BillingExpr = expr diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 45ae30bf9fe8..9bac0e4113b1 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -98,6 +98,7 @@ type RelayInfo struct { UsePrice bool RelayMode int OriginModelName string + BillingMode string RequestURLPath string RequestHeaders map[string]string ShouldIncludeUsage bool @@ -263,6 +264,7 @@ func (info *RelayInfo) ToString() string { fmt.Fprintf(b, "IsPlayground: %t, ", info.IsPlayground) fmt.Fprintf(b, "RequestURLPath: %q, ", info.RequestURLPath) fmt.Fprintf(b, "OriginModelName: %q, ", info.OriginModelName) + fmt.Fprintf(b, "BillingMode: %q, ", info.BillingMode) fmt.Fprintf(b, "EstimatePromptTokens: %d, ", info.estimatePromptTokens) fmt.Fprintf(b, "ShouldIncludeUsage: %t, ", info.ShouldIncludeUsage) fmt.Fprintf(b, "DisablePing: %t, ", info.DisablePing) diff --git a/relay/helper/price.go b/relay/helper/price.go index b9ae819bf57f..b3cecacbc546 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -72,11 +72,16 @@ func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) hostty func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, meta *types.TokenCountMeta) (hosttypes.PriceData, error) { modelPrice, usePrice := ratio_setting.GetModelPrice(info.OriginModelName, false) + billingMode := info.BillingMode + if billingMode == "" { + billingMode = billing_setting.GetBillingMode(info.OriginModelName) + info.BillingMode = billingMode + } groupRatioInfo := HandleGroupRatio(c, info) // Check if this model uses tiered_expr billing - if billing_setting.GetBillingMode(info.OriginModelName) == billing_setting.BillingModeTieredExpr { + if billingMode == billing_setting.BillingModeTieredExpr { return modelPriceHelperTiered(c, info, promptTokens, meta, groupRatioInfo) } @@ -92,8 +97,11 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens var audioCompletionRatio float64 var freeModel bool if !usePrice { - preConsumedTokens := common.Max(promptTokens, common.PreConsumedQuota) - if meta.MaxTokens != 0 { + preConsumedTokens := promptTokens + if billingMode != billing_setting.BillingModeUTF8Bytes { + preConsumedTokens = common.Max(promptTokens, common.PreConsumedQuota) + } + if billingMode != billing_setting.BillingModeUTF8Bytes && meta.MaxTokens != 0 { preConsumedTokens += meta.MaxTokens } var success bool diff --git a/relay/helper/price_test.go b/relay/helper/price_test.go index 0f28b5a424c5..494d3cc497ee 100644 --- a/relay/helper/price_test.go +++ b/relay/helper/price_test.go @@ -272,3 +272,41 @@ func TestModelPriceHelperRequestBillingRatiosOnlyApplyToFixedPrice(t *testing.T) require.Equal(t, common.QuotaClampOverflow, clamp.Kind) require.Nil(t, info.Billing) } + +func TestModelPriceHelperUTF8BytesDoesNotReserveCompletionTokens(t *testing.T) { + gin.SetMode(gin.TestMode) + + saved := map[string]string{} + require.NoError(t, config.GlobalConfig.SaveToDB(func(key, value string) error { + saved[key] = value + return nil + })) + savedModelRatios := ratio_setting.ModelRatio2JSONString() + t.Cleanup(func() { + require.NoError(t, config.GlobalConfig.LoadFromDB(saved)) + require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(savedModelRatios)) + }) + + require.NoError(t, config.GlobalConfig.LoadFromDB(map[string]string{ + "billing_setting.billing_mode": `{"utf8-bytes-price-model":"utf8_bytes"}`, + "group_ratio_setting.group_ratio": `{"default":1}`, + })) + require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{"utf8-bytes-price-model":7.5}`)) + + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctx.Set("group", "default") + info := &relaycommon.RelayInfo{ + OriginModelName: "utf8-bytes-price-model", + BillingMode: billing_setting.BillingModeUTF8Bytes, + UserGroup: "default", + UsingGroup: "default", + } + require.NoError(t, config.GlobalConfig.LoadFromDB(map[string]string{ + "billing_setting.billing_mode": `{}`, + })) + + priceData, err := ModelPriceHelper(ctx, info, 7, &types.TokenCountMeta{MaxTokens: 1000}) + + require.NoError(t, err) + require.Equal(t, 52, priceData.QuotaToPreConsume) +} diff --git a/service/text_quota.go b/service/text_quota.go index b7578f732786..4d03dc9a3097 100644 --- a/service/text_quota.go +++ b/service/text_quota.go @@ -17,6 +17,7 @@ import ( relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/relaykit/types" + "github.com/QuantumNous/new-api/setting/billing_setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/bytedance/gopkg/util/gopool" @@ -263,8 +264,20 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf summary.CacheCreationTokens1h = usage.ClaudeCacheCreation1hTokens summary.ImageTokens = usage.PromptTokensDetails.ImageTokens summary.AudioTokens = usage.PromptTokensDetails.AudioTokens + isUTF8BytesBilling := relayInfo.BillingMode == billing_setting.BillingModeUTF8Bytes + if isUTF8BytesBilling { + summary.PromptTokens = relayInfo.GetEstimatePromptTokens() + summary.CompletionTokens = 0 + summary.TotalTokens = summary.PromptTokens + summary.CacheTokens = 0 + summary.CacheCreationTokens = 0 + summary.CacheCreationTokens5m = 0 + summary.CacheCreationTokens1h = 0 + summary.ImageTokens = 0 + summary.AudioTokens = 0 + } legacyClaudeDerived := isLegacyClaudeDerivedOpenAIUsage(relayInfo, usage) - isOpenRouterClaudeBilling := relayInfo.ChannelMeta != nil && + isOpenRouterClaudeBilling := !isUTF8BytesBilling && relayInfo.ChannelMeta != nil && relayInfo.ChannelType == constant.ChannelTypeOpenRouter && summary.IsClaudeUsageSemantic @@ -476,7 +489,10 @@ func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us } else { other = GenerateTextOtherInfo(ctx, relayInfo, summary.ModelRatio, summary.GroupRatio, summary.CompletionRatio, summary.CacheTokens, summary.CacheRatio, summary.ModelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio) } - appendUsageBillingPathForLog(other, common.GetContextKeyBool(ctx, constant.ContextKeyLocalCountTokens), originUsage) + if relayInfo.BillingMode == billing_setting.BillingModeUTF8Bytes { + other["billing_mode"] = billing_setting.BillingModeUTF8Bytes + } + appendTextUsageBillingPathForLog(ctx, other, relayInfo, originUsage) if adminRejectReason != "" { other["reject_reason"] = adminRejectReason } @@ -541,3 +557,11 @@ func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us perfmetrics.RecordRelaySample(relayInfo, true, int64(summary.CompletionTokens)) }) } + +func appendTextUsageBillingPathForLog(ctx *gin.Context, other map[string]interface{}, relayInfo *relaycommon.RelayInfo, usage *dto.Usage) { + if relayInfo.BillingMode == billing_setting.BillingModeUTF8Bytes { + appendUsageBillingPathForLog(other, true, nil) + return + } + appendUsageBillingPathForLog(other, common.GetContextKeyBool(ctx, constant.ContextKeyLocalCountTokens), usage) +} diff --git a/service/token_counter.go b/service/token_counter.go index aad320a16773..a9582a033e04 100644 --- a/service/token_counter.go +++ b/service/token_counter.go @@ -15,6 +15,7 @@ import ( constant2 "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/relaykit/types" + "github.com/QuantumNous/new-api/setting/billing_setting" "github.com/gin-gonic/gin" ) @@ -177,15 +178,24 @@ func getImageToken(c *gin.Context, fileMeta *types.FileMeta, model string, strea } func EstimateRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relaycommon.RelayInfo) (int, error) { + if meta == nil { + return 0, errors.New("token count meta is nil") + } + + if info.BillingMode == "" { + info.BillingMode = billing_setting.GetBillingMode(info.OriginModelName) + } + if info.BillingMode == billing_setting.BillingModeUTF8Bytes { + count := len(meta.CombineText) + common.SetContextKey(c, constant.ContextKeyPromptTokens, count) + return count, nil + } + // 是否统计token if !constant.CountToken { return 0, nil } - if meta == nil { - return 0, errors.New("token count meta is nil") - } - if info.RelayFormat == types.RelayFormatOpenAIRealtime { return 0, nil } diff --git a/service/utf8_bytes_billing_test.go b/service/utf8_bytes_billing_test.go new file mode 100644 index 000000000000..05bbee640e28 --- /dev/null +++ b/service/utf8_bytes_billing_test.go @@ -0,0 +1,162 @@ +package service + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" + "github.com/QuantumNous/new-api/setting/billing_setting" + "github.com/QuantumNous/new-api/setting/config" + hosttypes "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func useUTF8BytesBilling(t *testing.T, model string) { + t.Helper() + + saved := map[string]string{} + require.NoError(t, config.GlobalConfig.SaveToDB(func(key, value string) error { + if key == "billing_setting.billing_mode" { + saved[key] = value + } + return nil + })) + t.Cleanup(func() { + require.NoError(t, config.GlobalConfig.LoadFromDB(saved)) + }) + + modes, err := common.Marshal(map[string]string{ + model: billing_setting.BillingModeUTF8Bytes, + }) + require.NoError(t, err) + require.NoError(t, config.GlobalConfig.LoadFromDB(map[string]string{ + "billing_setting.billing_mode": string(modes), + })) +} + +func TestEstimateRequestTokenUsesUTF8BytesBillingUnit(t *testing.T) { + gin.SetMode(gin.TestMode) + const model = "utf8-bytes-model" + useUTF8BytesBilling(t, model) + + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + info := &relaycommon.RelayInfo{ + OriginModelName: model, + RelayFormat: types.RelayFormatOpenAI, + } + meta := &types.TokenCountMeta{ + TokenType: types.TokenTypeTokenizer, + CombineText: "你好A", + ToolsCount: 2, + MessagesCount: 3, + NameCount: 1, + } + + count, err := EstimateRequestToken(ctx, meta, info) + + require.NoError(t, err) + require.Equal(t, 7, count) + require.Equal(t, billing_setting.BillingModeUTF8Bytes, info.BillingMode) +} + +func TestCalculateTextQuotaSummaryUsesLocalUTF8ByteCount(t *testing.T) { + gin.SetMode(gin.TestMode) + const model = "utf8-bytes-settlement-model" + useUTF8BytesBilling(t, model) + + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + info := &relaycommon.RelayInfo{ + OriginModelName: model, + BillingMode: billing_setting.BillingModeUTF8Bytes, + PriceData: hosttypes.PriceData{ + ModelRatio: 7.5, + CompletionRatio: 2, + GroupRatioInfo: hosttypes.GroupRatioInfo{ + GroupRatio: 1, + }, + }, + StartTime: time.Now(), + } + info.SetEstimatePromptTokens(7) + require.NoError(t, config.GlobalConfig.LoadFromDB(map[string]string{ + "billing_setting.billing_mode": `{}`, + })) + usage := &dto.Usage{ + PromptTokens: 100, + CompletionTokens: 20, + TotalTokens: 120, + PromptTokensDetails: dto.InputTokenDetails{ + CachedTokens: 50, + }, + } + + summary := calculateTextQuotaSummary(ctx, info, usage) + + require.Equal(t, 7, summary.PromptTokens) + require.Zero(t, summary.CompletionTokens) + require.Equal(t, 7, summary.TotalTokens) + require.Zero(t, summary.CacheTokens) + require.Equal(t, 53, summary.Quota) +} + +func TestCalculateTextQuotaSummaryUTF8BytesSkipsOpenRouterClaudeTokenAdjustments(t *testing.T) { + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + info := &relaycommon.RelayInfo{ + OriginModelName: "anthropic/claude-3.7-sonnet", + BillingMode: billing_setting.BillingModeUTF8Bytes, + FinalRequestRelayFormat: types.RelayFormatClaude, + ChannelMeta: &relaycommon.ChannelMeta{ + ChannelType: constant.ChannelTypeOpenRouter, + }, + PriceData: hosttypes.PriceData{ + ModelRatio: 1, + CompletionRatio: 2, + CacheRatio: 0.1, + CacheCreationRatio: 1.25, + GroupRatioInfo: hosttypes.GroupRatioInfo{GroupRatio: 1}, + }, + StartTime: time.Now(), + } + info.SetEstimatePromptTokens(7) + usage := &dto.Usage{ + PromptTokens: 1000, + CompletionTokens: 100, + Cost: 0.01, + PromptTokensDetails: dto.InputTokenDetails{ + CachedTokens: 900, + }, + } + + summary := calculateTextQuotaSummary(ctx, info, usage) + + require.Equal(t, 7, summary.PromptTokens) + require.Zero(t, summary.CompletionTokens) + require.Zero(t, summary.CacheTokens) + require.Zero(t, summary.CacheCreationTokens) + require.Equal(t, 7, summary.TotalTokens) + require.Equal(t, 7, summary.Quota) +} + +func TestAppendTextUsageBillingPathForLogUsesLocalPathForUTF8Bytes(t *testing.T) { + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + other := map[string]interface{}{} + usage := &dto.Usage{ + BillingUsage: dto.NewClaudeMessagesBillingUsage(&dto.ClaudeUsage{InputTokens: 100}), + } + + appendTextUsageBillingPathForLog(ctx, other, &relaycommon.RelayInfo{ + BillingMode: billing_setting.BillingModeUTF8Bytes, + }, usage) + + adminInfo, ok := other["admin_info"].(map[string]interface{}) + require.True(t, ok) + require.Equal(t, usageBillingPathLocal, adminInfo["usage_billing_path"]) +} diff --git a/setting/billing_setting/tiered_billing.go b/setting/billing_setting/tiered_billing.go index 46dc70de257f..f69e67e5b67a 100644 --- a/setting/billing_setting/tiered_billing.go +++ b/setting/billing_setting/tiered_billing.go @@ -10,6 +10,7 @@ import ( const ( BillingModeRatio = "ratio" + BillingModeUTF8Bytes = "utf8_bytes" BillingModeTieredExpr = "tiered_expr" BillingModeField = "billing_mode" BillingExprField = "billing_expr" diff --git a/web/src/features/pricing/components/model-billing-mode-badge.tsx b/web/src/features/pricing/components/model-billing-mode-badge.tsx index 86f6972999e0..2691e22d1101 100644 --- a/web/src/features/pricing/components/model-billing-mode-badge.tsx +++ b/web/src/features/pricing/components/model-billing-mode-badge.tsx @@ -21,7 +21,7 @@ import { useTranslation } from 'react-i18next' import { StatusBadge, type StatusVariant } from '@/components/status-badge' import { isDynamicPricingModel } from '../lib/dynamic-price' -import { isTokenBasedModel } from '../lib/model-helpers' +import { isTokenBasedModel, isUTF8BytesModel } from '../lib/model-helpers' import type { PricingModel } from '../types' interface ModelBillingModeBadgeProps { @@ -37,6 +37,9 @@ export function ModelBillingModeBadge(props: ModelBillingModeBadgeProps) { if (isDynamicPricingModel(props.model)) { label = t('Dynamic Pricing') variant = 'warning' + } else if (isUTF8BytesModel(props.model)) { + label = t('UTF-8 bytes') + variant = 'info' } else if (isTokenBasedModel(props.model)) { label = t('Token-based') variant = 'info' diff --git a/web/src/features/pricing/components/model-card.tsx b/web/src/features/pricing/components/model-card.tsx index 3e17ad44e7cc..c146f3a1402b 100644 --- a/web/src/features/pricing/components/model-card.tsx +++ b/web/src/features/pricing/components/model-card.tsx @@ -30,7 +30,7 @@ import { getDynamicPricingSummary, } from '../lib/dynamic-price' import { parseTags } from '../lib/filters' -import { isTokenBasedModel } from '../lib/model-helpers' +import { isTokenBasedModel, isUTF8BytesModel } from '../lib/model-helpers' import { formatPrice, formatRequestPrice } from '../lib/price' import type { PricingModel, TokenUnit } from '../types' import { ModelBillingModeBadge } from './model-billing-mode-badge' @@ -55,6 +55,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { const usdExchangeRate = props.usdExchangeRate ?? 1 const showRechargePrice = props.showRechargePrice ?? false const isTokenBased = isTokenBasedModel(props.model) + const isUTF8Bytes = isUTF8BytesModel(props.model) const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M' const tags = parseTags(props.model.tags) const groups = props.model.enable_groups || [] @@ -65,7 +66,8 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { const isDynamicPricing = props.model.billing_mode === 'tiered_expr' && Boolean(props.model.billing_expr) - const hasCachedPrice = isTokenBased && props.model.cache_ratio != null + const hasCachedPrice = + isTokenBased && !isUTF8Bytes && props.model.cache_ratio != null const dynamicSummary = isDynamicPricing ? getDynamicPricingSummary(props.model, { tokenUnit, @@ -144,20 +146,22 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { )} - - {t('Output')}{' '} - - {formatPrice( - props.model, - 'output', - tokenUnit, - showRechargePrice, - priceRate, - usdExchangeRate, - props.selectedGroup - )} + {!isUTF8Bytes && ( + + {t('Output')}{' '} + + {formatPrice( + props.model, + 'output', + tokenUnit, + showRechargePrice, + priceRate, + usdExchangeRate, + props.selectedGroup + )} + - + )} {hasCachedPrice && ( {t('Cached')}{' '} @@ -264,7 +268,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { ))} - {tokenUnitLabel} + {tokenUnitLabel} {isUTF8Bytes ? t('UTF-8 bytes') : t('tokens')} {hiddenCount > 0 && ( diff --git a/web/src/features/pricing/components/model-details.tsx b/web/src/features/pricing/components/model-details.tsx index dbe105200866..17d73cdf1a02 100644 --- a/web/src/features/pricing/components/model-details.tsx +++ b/web/src/features/pricing/components/model-details.tsx @@ -67,7 +67,11 @@ import { isDynamicPricingModel, } from '../lib/dynamic-price' import { parseTags } from '../lib/filters' -import { getAvailableGroups, isTokenBasedModel } from '../lib/model-helpers' +import { + getAvailableGroups, + isTokenBasedModel, + isUTF8BytesModel, +} from '../lib/model-helpers' import { formatFixedPrice, formatGroupPrice } from '../lib/price' import type { ModelCapability, @@ -575,6 +579,7 @@ function PriceSection(props: { }) { const { t } = useTranslation() const isTokenBased = isTokenBasedModel(props.model) + const isUTF8Bytes = isUTF8BytesModel(props.model) const tokenUnitLabel = props.tokenUnit === 'K' ? '1K' : '1M' const baseGroupKey = '_base' const baseGroupRatioMap = { [baseGroupKey]: 1 } @@ -586,10 +591,12 @@ function PriceSection(props: { groupRatioMultiplier: 1, }) - const primaryPriceTypes: { label: string; type: PriceType }[] = [ - { label: t('Input'), type: 'input' }, - { label: t('Output'), type: 'output' }, - ] + const primaryPriceTypes: { label: string; type: PriceType }[] = isUTF8Bytes + ? [{ label: t('Input'), type: 'input' }] + : [ + { label: t('Input'), type: 'input' }, + { label: t('Output'), type: 'output' }, + ] const secondaryPriceTypes: { label: string type: PriceType @@ -725,7 +732,9 @@ function PriceSection(props: { ) } - const secondaryItems = secondaryPriceTypes.filter((p) => p.available) + const secondaryItems = isUTF8Bytes + ? [] + : secondaryPriceTypes.filter((p) => p.available) const renderPrice = (type: PriceType) => ( <> {formatGroupPrice( @@ -739,7 +748,7 @@ function PriceSection(props: { baseGroupRatioMap )} - / {tokenUnitLabel} + / {tokenUnitLabel} {isUTF8Bytes && t('UTF-8 bytes')} ) @@ -868,9 +877,11 @@ function GroupPricingSection(props: { ) const isTokenBased = isTokenBasedModel(props.model) + const isUTF8Bytes = isUTF8BytesModel(props.model) const tokenUnitLabel = props.tokenUnit === 'K' ? '1K' : '1M' const extraPriceTypes = useMemo(() => { + if (isUTF8Bytes) return [] const types: { label: string; type: PriceType }[] = [] if (props.model.cache_ratio != null) { types.push({ label: t('Cache'), type: 'cache' }) @@ -891,7 +902,7 @@ function GroupPricingSection(props: { types.push({ label: t('Audio Out'), type: 'audio_output' }) } return types - }, [props.model, t]) + }, [isUTF8Bytes, props.model, t]) if (availableGroups.length === 0) { return ( @@ -1076,13 +1087,18 @@ function GroupPricingSection(props: { cellClassName: 'py-2.5 text-right font-mono', cell: (group: string) => renderGroupPrice(group, 'input'), }, - { - id: 'output', - header: t('Output'), - className: `${thClass} text-right`, - cellClassName: 'py-2.5 text-right font-mono', - cell: (group: string) => renderGroupPrice(group, 'output'), - }, + ...(isUTF8Bytes + ? [] + : [ + { + id: 'output', + header: t('Output'), + className: `${thClass} text-right`, + cellClassName: 'py-2.5 text-right font-mono', + cell: (group: string) => + renderGroupPrice(group, 'output'), + }, + ]), ...extraPriceTypes.map((ep) => ({ id: ep.type, header: ep.label, @@ -1105,7 +1121,8 @@ function GroupPricingSection(props: {
{isTokenBased && (

- {t('Prices shown per')} {tokenUnitLabel} tokens + {t('Prices shown per')} {tokenUnitLabel}{' '} + {isUTF8Bytes ? t('UTF-8 bytes') : t('tokens')}

)}
diff --git a/web/src/features/pricing/components/pricing-columns.tsx b/web/src/features/pricing/components/pricing-columns.tsx index 3d19eda2130c..e154406ccbfb 100644 --- a/web/src/features/pricing/components/pricing-columns.tsx +++ b/web/src/features/pricing/components/pricing-columns.tsx @@ -34,7 +34,7 @@ import { getDynamicPricingSummary, } from '../lib/dynamic-price' import { parseTags } from '../lib/filters' -import { isTokenBasedModel } from '../lib/model-helpers' +import { isTokenBasedModel, isUTF8BytesModel } from '../lib/model-helpers' import { formatPrice, formatRequestPrice, @@ -175,6 +175,7 @@ export function usePricingColumns( } const isTokenBased = isTokenBasedModel(model) + const isUTF8Bytes = isUTF8BytesModel(model) if (isTokenBased) { const inputPrice = stripTrailingZeros( @@ -188,27 +189,34 @@ export function usePricingColumns( selectedGroup ) ) - const outputPrice = stripTrailingZeros( - formatPrice( - model, - 'output', - tokenUnit, - showRechargePrice, - priceRate, - usdExchangeRate, - selectedGroup - ) - ) + const outputPrice = isUTF8Bytes + ? '' + : stripTrailingZeros( + formatPrice( + model, + 'output', + tokenUnit, + showRechargePrice, + priceRate, + usdExchangeRate, + selectedGroup + ) + ) return (
{inputPrice} - / - {outputPrice} + {outputPrice && ( + <> + / + {outputPrice} + + )}
- / {tokenUnitLabel} tokens + / {tokenUnitLabel}{' '} + {isUTF8Bytes ? t('UTF-8 bytes') : t('tokens')}
) diff --git a/web/src/features/pricing/lib/model-helpers.ts b/web/src/features/pricing/lib/model-helpers.ts index 746da8b92c74..67c2d3c058ac 100644 --- a/web/src/features/pricing/lib/model-helpers.ts +++ b/web/src/features/pricing/lib/model-helpers.ts @@ -107,3 +107,7 @@ export function replaceModelInPath(path: string, modelName: string): string { export function isTokenBasedModel(model: PricingModel): boolean { return model.quota_type === QUOTA_TYPE_VALUES.TOKEN } + +export function isUTF8BytesModel(model: PricingModel): boolean { + return model.billing_mode === 'utf8_bytes' +} diff --git a/web/src/features/pricing/types.ts b/web/src/features/pricing/types.ts index 8a0e244d5d09..402cce29fc4f 100644 --- a/web/src/features/pricing/types.ts +++ b/web/src/features/pricing/types.ts @@ -50,7 +50,7 @@ export type PricingModel = { supported_endpoint_types?: string[] key?: string group_ratio?: Record - /** Billing mode (e.g. "tiered_expr") used to flag dynamic pricing */ + /** Billing mode (e.g. "tiered_expr" or "utf8_bytes") */ billing_mode?: string /** Raw expression describing dynamic / tiered billing */ billing_expr?: string diff --git a/web/src/features/system-settings/models/__tests__/utf8-bytes-pricing.test.ts b/web/src/features/system-settings/models/__tests__/utf8-bytes-pricing.test.ts new file mode 100644 index 000000000000..8f4f81f73230 --- /dev/null +++ b/web/src/features/system-settings/models/__tests__/utf8-bytes-pricing.test.ts @@ -0,0 +1,49 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import assert from 'node:assert/strict' +import { describe, test } from 'node:test' + +import { + buildModelSnapshots, + getModeLabel, + getPriceSummary, +} from '../model-pricing-snapshots' + +const t = (key: string) => key + +describe('UTF-8 byte pricing', () => { + test('preserves the billing mode and displays the official per-million unit', () => { + const [snapshot] = buildModelSnapshots({ + modelPrice: '{}', + modelRatio: '{"s2.1-pro":7.5}', + cacheRatio: '{}', + createCacheRatio: '{}', + completionRatio: '{}', + imageRatio: '{}', + audioRatio: '{}', + audioCompletionRatio: '{}', + billingMode: '{"s2.1-pro":"utf8_bytes"}', + billingExpr: '{}', + }) + + assert.equal(snapshot?.billingMode, 'utf8_bytes') + assert.equal(getModeLabel(snapshot?.billingMode), 'UTF-8 bytes') + assert.equal(getPriceSummary(snapshot, t), '$15 / 1M UTF-8 bytes') + }) +}) diff --git a/web/src/features/system-settings/models/model-pricing-core.ts b/web/src/features/system-settings/models/model-pricing-core.ts index d3d20a3b3ec0..7345f68504e5 100644 --- a/web/src/features/system-settings/models/model-pricing-core.ts +++ b/web/src/features/system-settings/models/model-pricing-core.ts @@ -39,7 +39,11 @@ export type ModelPricingFormValues = z.infer< ReturnType > -export type PricingMode = 'per-token' | 'per-request' | 'tiered_expr' +export type PricingMode = + | 'per-token' + | 'utf8_bytes' + | 'per-request' + | 'tiered_expr' export type LaneKey = | 'completion' @@ -240,6 +244,17 @@ export function buildPreviewRows( ] } + if (mode === 'utf8_bytes') { + return [ + { key: 'mode', label: 'BillingMode', value: 'utf8_bytes' }, + { + key: 'inputPrice', + label: t('Input price'), + value: promptPrice ? `$${promptPrice}` : t('Empty'), + }, + ] + } + return [ { key: 'inputPrice', diff --git a/web/src/features/system-settings/models/model-pricing-sheet.tsx b/web/src/features/system-settings/models/model-pricing-sheet.tsx index 4c0178f9a452..dff9464014b0 100644 --- a/web/src/features/system-settings/models/model-pricing-sheet.tsx +++ b/web/src/features/system-settings/models/model-pricing-sheet.tsx @@ -188,13 +188,15 @@ export const ModelPricingEditorPanel = forwardRef< audioRatio: editData.audioRatio || '', audioCompletionRatio: editData.audioCompletionRatio || '', }) - setPricingMode( - editData.billingMode === 'tiered_expr' - ? 'tiered_expr' - : editData.price - ? 'per-request' - : 'per-token' - ) + let nextPricingMode: PricingMode = 'per-token' + if (editData.billingMode === 'tiered_expr') { + nextPricingMode = 'tiered_expr' + } else if (editData.billingMode === 'utf8_bytes') { + nextPricingMode = 'utf8_bytes' + } else if (editData.price) { + nextPricingMode = 'per-request' + } + setPricingMode(nextPricingMode) setBillingExpr(editData.billingExpr || '') setRequestRuleExpr(editData.requestRuleExpr || '') } else { @@ -544,10 +546,13 @@ export const ModelPricingEditorPanel = forwardRef< onValueChange={handleModeChange} className='gap-4' > - + {t('Per-token')} + + {t('UTF-8 bytes')} + {t('Per-request')} @@ -598,6 +603,22 @@ export const ModelPricingEditorPanel = forwardRef< + + + + {t('Input price')} + + + {t('USD price per 1M UTF-8 input bytes.')} + + + + + { export const getModeLabel = (mode?: string) => { if (mode === 'per-request') return 'Per-request' + if (mode === 'utf8_bytes') return 'UTF-8 bytes' if (mode === 'tiered_expr') return 'Expression' return 'Per-token' } @@ -91,6 +92,7 @@ export const getModeVariant = ( ): 'warning' | 'info' | 'success' => { if (mode === 'per-request') return 'warning' if (mode === 'tiered_expr') return 'info' + if (mode === 'utf8_bytes') return 'info' return 'success' } @@ -116,6 +118,13 @@ export const getPriceSummary = ( return row.price ? `$${row.price} / ${t('request')}` : t('Unset price') } + if (row.billingMode === 'utf8_bytes') { + const inputPrice = ratioToPrice(row.ratio) + return inputPrice + ? `$${inputPrice} / 1M ${t('UTF-8 bytes')}` + : t('Unset price') + } + const inputPrice = ratioToPrice(row.ratio) if (!inputPrice) return t('Unset price') @@ -145,6 +154,9 @@ export const getPriceDetail = ( if (row.billingMode === 'per-request') { return t('Fixed request price') } + if (row.billingMode === 'utf8_bytes') { + return t('UTF-8 input byte price') + } const inputPrice = ratioToPrice(row.ratio) if (!inputPrice) return t('No base input price') @@ -229,7 +241,7 @@ export const buildModelSnapshots = ({ ...Object.keys(billingExprMap), ]) - return Array.from(modelNames).map((name) => { + return [...modelNames].map((name) => { const price = priceMap[name]?.toString() || '' const ratio = ratioMap[name]?.toString() || '' const cache = cacheMap[name]?.toString() || '' @@ -261,6 +273,15 @@ export const buildModelSnapshots = ({ } } + if (modeForModel === 'utf8_bytes') { + return { + name, + ratio, + billingMode: 'utf8_bytes', + hasConflict: false, + } + } + return { name, price, diff --git a/web/src/features/system-settings/models/model-ratio-visual-editor.tsx b/web/src/features/system-settings/models/model-ratio-visual-editor.tsx index f50093cf0e92..f92478e49b95 100644 --- a/web/src/features/system-settings/models/model-ratio-visual-editor.tsx +++ b/web/src/features/system-settings/models/model-ratio-visual-editor.tsx @@ -273,6 +273,7 @@ const ModelRatioVisualEditorComponent = forwardRef< (acc, model) => { const mode = model.billingMode === 'per-request' || + model.billingMode === 'utf8_bytes' || model.billingMode === 'tiered_expr' ? model.billingMode : 'per-token' @@ -281,9 +282,10 @@ const ModelRatioVisualEditorComponent = forwardRef< }, { 'per-token': 0, + utf8_bytes: 0, 'per-request': 0, tiered_expr: 0, - } as Record<'per-token' | 'per-request' | 'tiered_expr', number> + } as Record ), [models] ) @@ -294,6 +296,8 @@ const ModelRatioVisualEditorComponent = forwardRef< let editBillingMode: PricingMode = 'per-token' if (editableModel.billingMode === 'tiered_expr') { editBillingMode = 'tiered_expr' + } else if (editableModel.billingMode === 'utf8_bytes') { + editBillingMode = 'utf8_bytes' } else if (editableModel.price && editableModel.price !== '') { editBillingMode = 'per-request' } @@ -527,7 +531,7 @@ const ModelRatioVisualEditorComponent = forwardRef< value: string | undefined ) => { if (!value || value === '') return - const parsed = parseFloat(value) + const parsed = Number.parseFloat(value) if (Number.isFinite(parsed)) target[name] = parsed } @@ -564,6 +568,9 @@ const ModelRatioVisualEditorComponent = forwardRef< setIfPresent(imageMap, name, data.imageRatio) setIfPresent(audioMap, name, data.audioRatio) setIfPresent(audioCompletionMap, name, data.audioCompletionRatio) + } else if (data.billingMode === 'utf8_bytes') { + billingModeMap[name] = 'utf8_bytes' + setIfPresent(ratioMap, name, data.ratio) } else if (data.price && data.price !== '') { setIfPresent(priceMap, name, data.price) } else { @@ -692,6 +699,11 @@ const ModelRatioVisualEditorComponent = forwardRef< value: 'per-token', count: modeCounts['per-token'], }, + { + label: 'UTF-8 bytes', + value: 'utf8_bytes', + count: modeCounts.utf8_bytes, + }, { label: 'Per-request', value: 'per-request', diff --git a/web/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/src/features/usage-logs/components/columns/common-logs-columns.tsx index e7b29441e2f3..07e6eb8c933a 100644 --- a/web/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/web/src/features/usage-logs/components/columns/common-logs-columns.tsx @@ -213,6 +213,12 @@ function buildTypeDetailSegments( muted: true, }) } + } else if (other.billing_mode === 'utf8_bytes') { + if (other.model_ratio != null) { + segments.push({ + text: `${t('UTF-8 bytes')} · ${formatPriceCompact(other.model_ratio * 2.0)}/M`, + }) + } } else { const modelPrice = other.model_price const isPerCall = isPerCallBilling(modelPrice) @@ -644,7 +650,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { }, { accessorKey: 'prompt_tokens', - header: 'Tokens', + header: t('Usage'), cell: ({ row }) => { const log = row.original if (!isDisplayableLogType(log.type)) return null @@ -656,6 +662,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { if (promptTokens === 0 && completionTokens === 0) { return - } + if (other?.billing_mode === 'utf8_bytes') { + return ( + + {promptTokens.toLocaleString()} {t('UTF-8 bytes')} + + ) + } const cacheReadTokens = other?.cache_tokens || 0 const cacheWrite5m = other?.cache_creation_tokens_5m || 0 diff --git a/web/src/features/usage-logs/components/dialogs/details-dialog.tsx b/web/src/features/usage-logs/components/dialogs/details-dialog.tsx index 8f57ac6ba337..fff9b22b6203 100644 --- a/web/src/features/usage-logs/components/dialogs/details-dialog.tsx +++ b/web/src/features/usage-logs/components/dialogs/details-dialog.tsx @@ -225,6 +225,7 @@ function BillingBreakdown(props: { const isPerCall = isPerCallBilling(other.model_price) const isClaude = other.claude === true const isTieredExpr = other.billing_mode === 'tiered_expr' + const isUTF8Bytes = other.billing_mode === 'utf8_bytes' const tieredSummary = getTieredBillingSummary(other) const rows: Array<{ label: string; value: string }> = [] @@ -256,6 +257,14 @@ function BillingBreakdown(props: { value: t('No matching results'), }) } + } else if (isUTF8Bytes) { + rows.push({ label: t('Billing Mode'), value: t('UTF-8 bytes') }) + if (other.model_ratio != null) { + rows.push({ + label: t('Input'), + value: `${fmtPrice(baseInputUSD)}/M ${t('UTF-8 bytes')}`, + }) + } } else if (isPerCall) { rows.push({ label: t('Billing Mode'), value: t('Per-call') }) if (other.model_price != null) { @@ -418,8 +427,21 @@ function TokenBreakdown(props: { log: UsageLog; other: LogOtherData }) { if (!hasTokens) return null const rows: Array<{ label: string; value: string }> = [] + const isUTF8Bytes = other.billing_mode === 'utf8_bytes' - rows.push({ label: t('Input Tokens'), value: promptTokens.toLocaleString() }) + rows.push({ + label: isUTF8Bytes ? t('Input UTF-8 bytes') : t('Input Tokens'), + value: promptTokens.toLocaleString(), + }) + if (isUTF8Bytes) { + return ( + + {rows.map((row) => ( + + ))} + + ) + } rows.push({ label: t('Output Tokens'), value: completionTokens.toLocaleString(), diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index b48096a7fcfa..84284d8ea338 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2332,6 +2332,7 @@ "Input price is required before saving dependent prices.": "Input price is required before saving dependent prices.", "Input tokens": "Input tokens", "Input Tokens": "Input Tokens", + "Input UTF-8 bytes": "Input UTF-8 bytes", "Inset": "Inset", "Inspect requests, errors, and billing details": "Inspect requests, errors, and billing details", "Inspect user prompts": "Inspect user prompts", @@ -4923,6 +4924,7 @@ "URL to your logo image (optional)": "URL to your logo image (optional)", "Usage": "Usage", "Usage at a glance": "Usage at a glance", + "Usage Breakdown": "Usage Breakdown", "Usage guide": "Usage guide", "Usage logs": "Usage logs", "Usage Logs": "Usage Logs", @@ -4932,6 +4934,7 @@ "USD Exchange Rate": "USD Exchange Rate", "USD price per 1M input tokens.": "USD price per 1M input tokens.", "USD price per 1M tokens.": "USD price per 1M tokens.", + "USD price per 1M UTF-8 input bytes.": "USD price per 1M UTF-8 input bytes.", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.", "Use a different stable value for each instance, then restart the service.": "Use a different stable value for each instance, then restart the service.", @@ -5015,6 +5018,8 @@ "Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.": "Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.", "uses": "uses", "Using the complete global Auto order ({{count}} groups)": "Using the complete global Auto order ({{count}} groups)", + "UTF-8 bytes": "UTF-8 bytes", + "UTF-8 input byte price": "UTF-8 input byte price", "Validity": "Validity", "Validity Period": "Validity Period", "Value": "Value", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 638cf9abadd1..66bbed99f06e 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2332,6 +2332,7 @@ "Input price is required before saving dependent prices.": "Le prix d’entrée est requis avant d’enregistrer les prix dépendants.", "Input tokens": "Jetons d’entrée", "Input Tokens": "Tokens d'entrée", + "Input UTF-8 bytes": "Octets UTF-8 en entrée", "Inset": "Encastré", "Inspect requests, errors, and billing details": "Inspecter les requêtes, les erreurs et les détails de facturation", "Inspect user prompts": "Inspecter les invites utilisateur", @@ -4923,6 +4924,7 @@ "URL to your logo image (optional)": "URL de votre image de logo (facultatif)", "Usage": "Utilisation", "Usage at a glance": "Vue d'ensemble de l'utilisation", + "Usage Breakdown": "Détail de l’utilisation", "Usage guide": "Guide d'utilisation", "Usage logs": "Journaux d'utilisation", "Usage Logs": "Journaux d'utilisation", @@ -4932,6 +4934,7 @@ "USD Exchange Rate": "Taux de change USD", "USD price per 1M input tokens.": "Prix en USD par million de tokens d’entrée.", "USD price per 1M tokens.": "Prix en USD par million de tokens.", + "USD price per 1M UTF-8 input bytes.": "Prix en USD par million d’octets UTF-8 en entrée.", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Utilisez +: pour ajouter un groupe, -: pour supprimer un groupe sélectionnable par défaut, ou aucun préfixe pour annexer un groupe.", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Utilisez un navigateur ou un appareil compatible avec l'authentification biométrique ou une clé de sécurité pour enregistrer une clé d'accès (Passkey).", "Use a different stable value for each instance, then restart the service.": "Utilisez une valeur stable différente pour chaque instance, puis redémarrez le service.", @@ -5015,6 +5018,8 @@ "Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.": "Les utilisateurs ne voient que les groupes marqués comme sélectionnables. Les groupes non sélectionnables peuvent toujours être attribués par les administrateurs.", "uses": "utilisations", "Using the complete global Auto order ({{count}} groups)": "Utilisation de l’ordre Auto global complet ({{count}} groupes)", + "UTF-8 bytes": "Octets UTF-8", + "UTF-8 input byte price": "Prix des octets UTF-8 en entrée", "Validity": "Validité", "Validity Period": "Période de validité", "Value": "Valeur", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 3b9d4ce693cc..ac59075b5bc1 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -2332,6 +2332,7 @@ "Input price is required before saving dependent prices.": "依存する価格を保存する前に入力価格が必要です。", "Input tokens": "入力トークン", "Input Tokens": "入力トークン", + "Input UTF-8 bytes": "入力 UTF-8 バイト", "Inset": "インセット", "Inspect requests, errors, and billing details": "リクエスト、エラー、請求詳細を確認", "Inspect user prompts": "ユーザープロンプトの検査", @@ -4923,6 +4924,7 @@ "URL to your logo image (optional)": "ロゴ画像のURL (オプション)", "Usage": "使用量", "Usage at a glance": "使用状況の概要", + "Usage Breakdown": "使用量の内訳", "Usage guide": "使用ガイド", "Usage logs": "使用ログ", "Usage Logs": "利用履歴", @@ -4932,6 +4934,7 @@ "USD Exchange Rate": "USD 為替レート", "USD price per 1M input tokens.": "100万入力トークンあたりのUSD価格。", "USD price per 1M tokens.": "100万トークンあたりのUSD価格。", + "USD price per 1M UTF-8 input bytes.": "入力 UTF-8 バイト 100 万バイトあたりの USD 価格。", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "+: はグループ追加、-: はデフォルト選択可能グループの削除、接頭辞なしはグループ追記に使います。", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "生体認証またはセキュリティキーを備えた互換性のあるブラウザまたはデバイスを使用して、パスキーを登録してください。", "Use a different stable value for each instance, then restart the service.": "インスタンスごとに異なる安定した値を使用し、その後サービスを再起動してください。", @@ -5015,6 +5018,8 @@ "Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.": "ユーザーにはユーザー選択可のグループだけが表示されます。選択不可グループも管理者は割り当てできます。", "uses": "使用回数", "Using the complete global Auto order ({{count}} groups)": "グローバル Auto の全順序を使用中({{count}} グループ)", + "UTF-8 bytes": "UTF-8 バイト", + "UTF-8 input byte price": "入力 UTF-8 バイト価格", "Validity": "有効期間", "Validity Period": "有効期間", "Value": "値", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 853dc874d463..7d61d0954449 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -2332,6 +2332,7 @@ "Input price is required before saving dependent prices.": "Перед сохранением зависимых цен укажите входную цену.", "Input tokens": "Входные токены", "Input Tokens": "Входные токены", + "Input UTF-8 bytes": "Входные байты UTF-8", "Inset": "Встроенная", "Inspect requests, errors, and billing details": "Проверяйте запросы, ошибки и детали оплаты", "Inspect user prompts": "Просмотр запросов пользователя", @@ -4923,6 +4924,7 @@ "URL to your logo image (optional)": "URL изображения вашего логотипа (необязательно)", "Usage": "Использование", "Usage at a glance": "Краткий обзор использования", + "Usage Breakdown": "Детализация использования", "Usage guide": "Руководство", "Usage logs": "Журналы использования", "Usage Logs": "Журнал использования", @@ -4932,6 +4934,7 @@ "USD Exchange Rate": "Обменный курс USD", "USD price per 1M input tokens.": "Цена в USD за 1 млн входных токенов.", "USD price per 1M tokens.": "Цена в USD за 1 млн токенов.", + "USD price per 1M UTF-8 input bytes.": "Цена в USD за 1 млн входных байтов UTF-8.", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Используйте +: для добавления группы, -: для удаления выбираемой по умолчанию группы, без префикса — для добавления в конец.", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Используйте совместимый браузер или устройство с биометрической аутентификацией или ключ безопасности для регистрации ключа доступа.", "Use a different stable value for each instance, then restart the service.": "Используйте разные стабильные значения для каждого экземпляра, затем перезапустите сервис.", @@ -5015,6 +5018,8 @@ "Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.": "Пользователи видят только группы, отмеченные как доступные для выбора. Недоступные для выбора группы всё равно могут назначаться администраторами.", "uses": "использует", "Using the complete global Auto order ({{count}} groups)": "Используется полный глобальный порядок Auto (групп: {{count}})", + "UTF-8 bytes": "Байты UTF-8", + "UTF-8 input byte price": "Цена входных байтов UTF-8", "Validity": "Срок действия", "Validity Period": "Срок действия", "Value": "Значение", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index cdc1a12626e1..53daeb29dac6 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -2332,6 +2332,7 @@ "Input price is required before saving dependent prices.": "Cần có giá đầu vào trước khi lưu các giá phụ thuộc.", "Input tokens": "Token đầu vào", "Input Tokens": "Token đầu vào", + "Input UTF-8 bytes": "Byte UTF-8 đầu vào", "Inset": "Khung trong", "Inspect requests, errors, and billing details": "Kiểm tra yêu cầu, lỗi và chi tiết thanh toán", "Inspect user prompts": "Kiểm tra lời nhắc của người dùng", @@ -4923,6 +4924,7 @@ "URL to your logo image (optional)": "URL hình ảnh logo của bạn (tùy chọn)", "Usage": "Sử dụng", "Usage at a glance": "Tổng quan mức dùng", + "Usage Breakdown": "Chi tiết mức sử dụng", "Usage guide": "Hướng dẫn sử dụng", "Usage logs": "Nhật ký sử dụng", "Usage Logs": "Nhật ký sử dụng", @@ -4932,6 +4934,7 @@ "USD Exchange Rate": "Tỷ giá USD", "USD price per 1M input tokens.": "Giá USD cho mỗi 1 triệu token đầu vào.", "USD price per 1M tokens.": "Giá USD cho mỗi 1 triệu token.", + "USD price per 1M UTF-8 input bytes.": "Giá USD cho mỗi 1 triệu byte UTF-8 đầu vào.", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Dùng +: để thêm nhóm, -: để xóa nhóm có thể chọn mặc định, hoặc không có tiền tố để nối nhóm.", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Sử dụng trình duyệt hoặc thiết bị tương thích có xác thực sinh trắc học hoặc khóa bảo mật để đăng ký Khóa truy cập.", "Use a different stable value for each instance, then restart the service.": "Dùng một giá trị ổn định khác nhau cho mỗi phiên bản, sau đó khởi động lại dịch vụ.", @@ -5015,6 +5018,8 @@ "Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.": "Người dùng chỉ thấy các nhóm được đánh dấu là có thể chọn. Nhóm không thể chọn vẫn có thể do quản trị viên gán.", "uses": "sử dụng", "Using the complete global Auto order ({{count}} groups)": "Đang dùng thứ tự Auto toàn cục đầy đủ ({{count}} nhóm)", + "UTF-8 bytes": "Byte UTF-8", + "UTF-8 input byte price": "Giá byte UTF-8 đầu vào", "Validity": "Hiệu lực", "Validity Period": "Thời hạn hiệu lực", "Value": "Giá trị", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index f8b5fafd8d2c..e046b02dda09 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -2332,6 +2332,7 @@ "Input price is required before saving dependent prices.": "儲存依賴價格前必須先填寫輸入價格。", "Input tokens": "輸入 token", "Input Tokens": "輸入 Token", + "Input UTF-8 bytes": "輸入 UTF-8 位元組", "Inset": "內嵌", "Inspect requests, errors, and billing details": "查看請求、錯誤和收費詳情", "Inspect user prompts": "檢查用戶提示", @@ -4923,6 +4924,7 @@ "URL to your logo image (optional)": "您的徽標圖片 URL(可選)", "Usage": "用量", "Usage at a glance": "用量概覽", + "Usage Breakdown": "用量明細", "Usage guide": "使用教學", "Usage logs": "使用日誌", "Usage Logs": "使用日誌", @@ -4932,6 +4934,7 @@ "USD Exchange Rate": "美元匯率", "USD price per 1M input tokens.": "每 100 萬輸入 token 的美元價格。", "USD price per 1M tokens.": "每 100 萬 token 的美元價格。", + "USD price per 1M UTF-8 input bytes.": "每 100 萬 UTF-8 輸入位元組的美元價格。", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "使用 +: 新增分組,使用 -: 移除預設可選分組,不加前綴則追加分組。", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "請使用支援生物識別認證或安全金鑰的兼容瀏覽器或設備來註冊通行金鑰。", "Use a different stable value for each instance, then restart the service.": "每個實例使用不同且穩定的值,然後重啟服務。", @@ -5015,6 +5018,8 @@ "Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.": "用戶只能看到標記為用戶可選的分組。不可選分組仍可由管理員分配。", "uses": "使用次數", "Using the complete global Auto order ({{count}} groups)": "正在使用完整的全域 Auto 順序({{count}} 個分組)", + "UTF-8 bytes": "UTF-8 位元組", + "UTF-8 input byte price": "UTF-8 輸入位元組價格", "Validity": "有效期", "Validity Period": "有效期", "Value": "值", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 842991e714f0..69034d0ad918 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -2332,6 +2332,7 @@ "Input price is required before saving dependent prices.": "保存依赖价格前必须先填写输入价格。", "Input tokens": "输入 token", "Input Tokens": "输入 Token", + "Input UTF-8 bytes": "输入 UTF-8 字节", "Inset": "内嵌", "Inspect requests, errors, and billing details": "查看请求、错误和计费详情", "Inspect user prompts": "检查用户提示", @@ -4923,6 +4924,7 @@ "URL to your logo image (optional)": "您的徽标图片 URL(可选)", "Usage": "用量", "Usage at a glance": "用量概览", + "Usage Breakdown": "用量明细", "Usage guide": "使用教程", "Usage logs": "使用日志", "Usage Logs": "使用日志", @@ -4932,6 +4934,7 @@ "USD Exchange Rate": "美元汇率", "USD price per 1M input tokens.": "每 100 万输入 token 的美元价格。", "USD price per 1M tokens.": "每 100 万 token 的美元价格。", + "USD price per 1M UTF-8 input bytes.": "每 100 万 UTF-8 输入字节的美元价格。", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "使用 +: 添加分组,使用 -: 移除默认可选分组,不加前缀则追加分组。", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "请使用支持生物识别认证或安全密钥的兼容浏览器或设备来注册通行密钥。", "Use a different stable value for each instance, then restart the service.": "每个实例使用不同且稳定的值,然后重启服务。", @@ -5015,6 +5018,8 @@ "Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.": "用户只能看到标记为用户可选的分组。不可选分组仍可由管理员分配。", "uses": "使用次数", "Using the complete global Auto order ({{count}} groups)": "正在使用完整全局 Auto 顺序({{count}} 个分组)", + "UTF-8 bytes": "UTF-8 字节", + "UTF-8 input byte price": "UTF-8 输入字节价格", "Validity": "有效期", "Validity Period": "有效期", "Value": "值",