| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327 |
- /*
- Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
- For commercial licensing, please contact support@quantumnous.com
- */
- import i18next from 'i18next';
- import { Modal, Tag, Typography, Avatar } from '@douyinfe/semi-ui';
- import { copy, showSuccess } from './utils';
- import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile';
- import {
- BILLING_VARS,
- BILLING_VAR_KEY_TO_FIELD,
- BILLING_VAR_REGEX,
- } from '../constants';
- import { visit } from 'unist-util-visit';
- import * as LobeIcons from '@lobehub/icons';
- import {
- OpenAI,
- Claude,
- Gemini,
- Moonshot,
- Zhipu,
- Qwen,
- DeepSeek,
- Minimax,
- Wenxin,
- Spark,
- Midjourney,
- Hunyuan,
- Cohere,
- Cloudflare,
- Ai360,
- Yi,
- Jina,
- Mistral,
- XAI,
- Ollama,
- Doubao,
- Suno,
- Xinference,
- OpenRouter,
- Dify,
- Coze,
- SiliconCloud,
- FastGPT,
- Kling,
- Jimeng,
- Perplexity,
- Replicate,
- } from '@lobehub/icons';
- import {
- LayoutDashboard,
- TerminalSquare,
- MessageSquare,
- Key,
- BarChart3,
- Image as ImageIcon,
- CheckSquare,
- CreditCard,
- Layers,
- Gift,
- User,
- Settings,
- CircleUser,
- Package,
- Server,
- CalendarClock,
- } from 'lucide-react';
- import {
- SiAtlassian,
- SiAuth0,
- SiAuthentik,
- SiBitbucket,
- SiDiscord,
- SiDropbox,
- SiFacebook,
- SiGitea,
- SiGithub,
- SiGitlab,
- SiGoogle,
- SiKeycloak,
- SiLinkedin,
- SiNextcloud,
- SiNotion,
- SiOkta,
- SiOpenid,
- SiReddit,
- SiSlack,
- SiTelegram,
- SiTwitch,
- SiWechat,
- SiX,
- } from 'react-icons/si';
- // 获取侧边栏Lucide图标组件
- export function getLucideIcon(key, selected = false) {
- const size = 16;
- const strokeWidth = 2;
- const SELECTED_COLOR = 'var(--semi-color-primary)';
- const iconColor = selected ? SELECTED_COLOR : 'currentColor';
- const commonProps = {
- size,
- strokeWidth,
- className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`,
- };
- // 根据不同的key返回不同的图标
- switch (key) {
- case 'detail':
- return <LayoutDashboard {...commonProps} color={iconColor} />;
- case 'playground':
- return <TerminalSquare {...commonProps} color={iconColor} />;
- case 'chat':
- return <MessageSquare {...commonProps} color={iconColor} />;
- case 'token':
- return <Key {...commonProps} color={iconColor} />;
- case 'log':
- return <BarChart3 {...commonProps} color={iconColor} />;
- case 'midjourney':
- return <ImageIcon {...commonProps} color={iconColor} />;
- case 'task':
- return <CheckSquare {...commonProps} color={iconColor} />;
- case 'topup':
- return <CreditCard {...commonProps} color={iconColor} />;
- case 'channel':
- return <Layers {...commonProps} color={iconColor} />;
- case 'redemption':
- return <Gift {...commonProps} color={iconColor} />;
- case 'user':
- case 'personal':
- return <User {...commonProps} color={iconColor} />;
- case 'models':
- return <Package {...commonProps} color={iconColor} />;
- case 'deployment':
- return <Server {...commonProps} color={iconColor} />;
- case 'subscription':
- return <CalendarClock {...commonProps} color={iconColor} />;
- case 'setting':
- return <Settings {...commonProps} color={iconColor} />;
- default:
- return <CircleUser {...commonProps} color={iconColor} />;
- }
- }
- // 获取模型分类
- export const getModelCategories = (() => {
- let categoriesCache = null;
- let lastLocale = null;
- return (t) => {
- const currentLocale = i18next.language;
- if (categoriesCache && lastLocale === currentLocale) {
- return categoriesCache;
- }
- categoriesCache = {
- all: {
- label: t('全部模型'),
- icon: null,
- filter: () => true,
- },
- openai: {
- label: 'OpenAI',
- icon: <OpenAI />,
- filter: (model) =>
- model.model_name.toLowerCase().includes('gpt') ||
- model.model_name.toLowerCase().includes('dall-e') ||
- model.model_name.toLowerCase().includes('whisper') ||
- model.model_name.toLowerCase().includes('tts-1') ||
- model.model_name.toLowerCase().includes('text-embedding-3') ||
- model.model_name.toLowerCase().includes('text-moderation') ||
- model.model_name.toLowerCase().includes('babbage') ||
- model.model_name.toLowerCase().includes('davinci') ||
- model.model_name.toLowerCase().includes('curie') ||
- model.model_name.toLowerCase().includes('ada') ||
- model.model_name.toLowerCase().includes('o1') ||
- model.model_name.toLowerCase().includes('o3') ||
- model.model_name.toLowerCase().includes('o4'),
- },
- anthropic: {
- label: 'Anthropic',
- icon: <Claude.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('claude'),
- },
- gemini: {
- label: 'Gemini',
- icon: <Gemini.Color />,
- filter: (model) =>
- model.model_name.toLowerCase().includes('gemini') ||
- model.model_name.toLowerCase().includes('gemma') ||
- model.model_name.toLowerCase().includes('learnlm') ||
- model.model_name.toLowerCase().startsWith('embedding-') ||
- model.model_name.toLowerCase().includes('text-embedding-004') ||
- model.model_name.toLowerCase().includes('imagen-4') ||
- model.model_name.toLowerCase().includes('veo-') ||
- model.model_name.toLowerCase().includes('aqa'),
- },
- moonshot: {
- label: 'Moonshot',
- icon: <Moonshot />,
- filter: (model) =>
- model.model_name.toLowerCase().includes('moonshot') ||
- model.model_name.toLowerCase().includes('kimi'),
- },
- zhipu: {
- label: t('智谱'),
- icon: <Zhipu.Color />,
- filter: (model) =>
- model.model_name.toLowerCase().includes('chatglm') ||
- model.model_name.toLowerCase().includes('glm-') ||
- model.model_name.toLowerCase().includes('cogview') ||
- model.model_name.toLowerCase().includes('cogvideo'),
- },
- qwen: {
- label: t('通义千问'),
- icon: <Qwen.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('qwen'),
- },
- deepseek: {
- label: 'DeepSeek',
- icon: <DeepSeek.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('deepseek'),
- },
- minimax: {
- label: 'MiniMax',
- icon: <Minimax.Color />,
- filter: (model) =>
- model.model_name.toLowerCase().includes('abab') ||
- model.model_name.toLowerCase().includes('minimax'),
- },
- baidu: {
- label: t('文心一言'),
- icon: <Wenxin.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('ernie'),
- },
- xunfei: {
- label: t('讯飞星火'),
- icon: <Spark.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('spark'),
- },
- midjourney: {
- label: 'Midjourney',
- icon: <Midjourney />,
- filter: (model) => model.model_name.toLowerCase().includes('mj_'),
- },
- tencent: {
- label: t('腾讯混元'),
- icon: <Hunyuan.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('hunyuan'),
- },
- cohere: {
- label: 'Cohere',
- icon: <Cohere.Color />,
- filter: (model) =>
- model.model_name.toLowerCase().includes('command') ||
- model.model_name.toLowerCase().includes('c4ai-') ||
- model.model_name.toLowerCase().includes('embed-'),
- },
- cloudflare: {
- label: 'Cloudflare',
- icon: <Cloudflare.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('@cf/'),
- },
- ai360: {
- label: t('360智脑'),
- icon: <Ai360.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('360'),
- },
- jina: {
- label: 'Jina',
- icon: <Jina />,
- filter: (model) => model.model_name.toLowerCase().includes('jina'),
- },
- mistral: {
- label: 'Mistral AI',
- icon: <Mistral.Color />,
- filter: (model) =>
- model.model_name.toLowerCase().includes('mistral') ||
- model.model_name.toLowerCase().includes('codestral') ||
- model.model_name.toLowerCase().includes('pixtral') ||
- model.model_name.toLowerCase().includes('voxtral') ||
- model.model_name.toLowerCase().includes('magistral'),
- },
- xai: {
- label: 'xAI',
- icon: <XAI />,
- filter: (model) => model.model_name.toLowerCase().includes('grok'),
- },
- llama: {
- label: 'Llama',
- icon: <Ollama />,
- filter: (model) => model.model_name.toLowerCase().includes('llama'),
- },
- doubao: {
- label: t('豆包'),
- icon: <Doubao.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('doubao'),
- },
- yi: {
- label: t('零一万物'),
- icon: <Yi.Color />,
- filter: (model) => model.model_name.toLowerCase().includes('yi'),
- },
- };
- lastLocale = currentLocale;
- return categoriesCache;
- };
- })();
- /**
- * 根据渠道类型返回对应的厂商图标
- * @param {number} channelType - 渠道类型值
- * @returns {JSX.Element|null} - 对应的厂商图标组件
- */
- export function getChannelIcon(channelType) {
- const iconSize = 14;
- switch (channelType) {
- case 1: // OpenAI
- case 3: // Azure OpenAI
- case 57: // Codex
- return <OpenAI size={iconSize} />;
- case 2: // Midjourney Proxy
- case 5: // Midjourney Proxy Plus
- return <Midjourney size={iconSize} />;
- case 36: // Suno API
- return <Suno size={iconSize} />;
- case 4: // Ollama
- return <Ollama size={iconSize} />;
- case 14: // Anthropic Claude
- case 33: // AWS Claude
- return <Claude.Color size={iconSize} />;
- case 41: // Vertex AI
- return <Gemini.Color size={iconSize} />;
- case 34: // Cohere
- return <Cohere.Color size={iconSize} />;
- case 39: // Cloudflare
- return <Cloudflare.Color size={iconSize} />;
- case 43: // DeepSeek
- return <DeepSeek.Color size={iconSize} />;
- case 15: // 百度文心千帆
- case 46: // 百度文心千帆V2
- return <Wenxin.Color size={iconSize} />;
- case 17: // 阿里通义千问
- return <Qwen.Color size={iconSize} />;
- case 18: // 讯飞星火认知
- return <Spark.Color size={iconSize} />;
- case 16: // 智谱 ChatGLM
- case 26: // 智谱 GLM-4V
- return <Zhipu.Color size={iconSize} />;
- case 24: // Google Gemini
- case 11: // Google PaLM2
- return <Gemini.Color size={iconSize} />;
- case 47: // Xinference
- return <Xinference.Color size={iconSize} />;
- case 25: // Moonshot
- return <Moonshot size={iconSize} />;
- case 27: // Perplexity
- return <Perplexity.Color size={iconSize} />;
- case 20: // OpenRouter
- return <OpenRouter size={iconSize} />;
- case 19: // 360 智脑
- return <Ai360.Color size={iconSize} />;
- case 23: // 腾讯混元
- return <Hunyuan.Color size={iconSize} />;
- case 31: // 零一万物
- return <Yi.Color size={iconSize} />;
- case 35: // MiniMax
- return <Minimax.Color size={iconSize} />;
- case 37: // Dify
- return <Dify.Color size={iconSize} />;
- case 38: // Jina
- return <Jina size={iconSize} />;
- case 40: // SiliconCloud
- return <SiliconCloud.Color size={iconSize} />;
- case 42: // Mistral AI
- return <Mistral.Color size={iconSize} />;
- case 45: // 字节火山方舟、豆包通用
- return <Doubao.Color size={iconSize} />;
- case 48: // xAI
- return <XAI size={iconSize} />;
- case 49: // Coze
- return <Coze size={iconSize} />;
- case 50: // 可灵 Kling
- return <Kling.Color size={iconSize} />;
- case 51: // 即梦 Jimeng
- return <Jimeng.Color size={iconSize} />;
- case 54: // 豆包视频 Doubao Video
- return <Doubao.Color size={iconSize} />;
- case 56: // Replicate
- return <Replicate size={iconSize} />;
- case 8: // 自定义渠道
- case 22: // 知识库:FastGPT
- return <FastGPT.Color size={iconSize} />;
- case 21: // 知识库:AI Proxy
- case 44: // 嵌入模型:MokaAI M3E
- default:
- return null; // 未知类型或自定义渠道不显示图标
- }
- }
- /**
- * 根据图标名称动态获取 LobeHub 图标组件
- * 支持:
- * - 基础:"OpenAI"、"OpenAI.Color" 等
- * - 额外属性(点号链式):"OpenAI.Avatar.type={'platform'}"、"OpenRouter.Avatar.shape={'square'}"
- * - 继续兼容第二参数 size;若字符串里有 size=,以字符串为准
- * @param {string} iconName - 图标名称/描述
- * @param {number} size - 图标大小,默认为 14
- * @returns {JSX.Element} - 对应的图标组件或 Avatar
- */
- export function getLobeHubIcon(iconName, size = 14) {
- if (typeof iconName === 'string') iconName = iconName.trim();
- // 如果没有图标名称,返回 Avatar
- if (!iconName) {
- return <Avatar size='extra-extra-small'>?</Avatar>;
- }
- // 解析组件路径与点号链式属性
- const segments = String(iconName).split('.');
- const baseKey = segments[0];
- const BaseIcon = LobeIcons[baseKey];
- let IconComponent = undefined;
- let propStartIndex = 1;
- if (BaseIcon && segments.length > 1 && BaseIcon[segments[1]]) {
- IconComponent = BaseIcon[segments[1]];
- propStartIndex = 2;
- } else {
- IconComponent = LobeIcons[baseKey];
- propStartIndex = 1;
- }
- // 失败兜底
- if (
- !IconComponent ||
- (typeof IconComponent !== 'function' && typeof IconComponent !== 'object')
- ) {
- const firstLetter = String(iconName).charAt(0).toUpperCase();
- return <Avatar size='extra-extra-small'>{firstLetter}</Avatar>;
- }
- // 解析点号链式属性,形如:key={...}、key='...'、key="..."、key=123、key、key=true/false
- const props = {};
- const parseValue = (raw) => {
- if (raw == null) return true;
- let v = String(raw).trim();
- // 去除一层花括号包裹
- if (v.startsWith('{') && v.endsWith('}')) {
- v = v.slice(1, -1).trim();
- }
- // 去除引号
- if (
- (v.startsWith('"') && v.endsWith('"')) ||
- (v.startsWith("'") && v.endsWith("'"))
- ) {
- return v.slice(1, -1);
- }
- // 布尔
- if (v === 'true') return true;
- if (v === 'false') return false;
- // 数字
- if (/^-?\d+(?:\.\d+)?$/.test(v)) return Number(v);
- // 其他原样返回字符串
- return v;
- };
- for (let i = propStartIndex; i < segments.length; i++) {
- const seg = segments[i];
- if (!seg) continue;
- const eqIdx = seg.indexOf('=');
- if (eqIdx === -1) {
- props[seg.trim()] = true;
- continue;
- }
- const key = seg.slice(0, eqIdx).trim();
- const valRaw = seg.slice(eqIdx + 1).trim();
- props[key] = parseValue(valRaw);
- }
- // 兼容第二参数 size,若字符串中未显式指定 size,则使用函数入参
- if (props.size == null && size != null) props.size = size;
- return <IconComponent {...props} />;
- }
- const oauthProviderIconMap = {
- github: SiGithub,
- gitlab: SiGitlab,
- gitea: SiGitea,
- google: SiGoogle,
- discord: SiDiscord,
- facebook: SiFacebook,
- linkedin: SiLinkedin,
- x: SiX,
- twitter: SiX,
- slack: SiSlack,
- telegram: SiTelegram,
- wechat: SiWechat,
- keycloak: SiKeycloak,
- nextcloud: SiNextcloud,
- authentik: SiAuthentik,
- openid: SiOpenid,
- okta: SiOkta,
- auth0: SiAuth0,
- atlassian: SiAtlassian,
- bitbucket: SiBitbucket,
- notion: SiNotion,
- twitch: SiTwitch,
- reddit: SiReddit,
- dropbox: SiDropbox,
- };
- function isHttpUrl(value) {
- return /^https?:\/\//i.test(value || '');
- }
- function isSimpleEmoji(value) {
- if (!value) return false;
- const trimmed = String(value).trim();
- return trimmed.length > 0 && trimmed.length <= 4 && !isHttpUrl(trimmed);
- }
- function normalizeOAuthIconKey(raw) {
- return raw
- .trim()
- .toLowerCase()
- .replace(/^ri:/, '')
- .replace(/^react-icons:/, '')
- .replace(/^si:/, '');
- }
- /**
- * Render custom OAuth provider icon with react-icons or URL/emoji fallback.
- * Supported formats:
- * - react-icons simple key: github / gitlab / google / keycloak
- * - prefixed key: ri:github / si:github
- * - full URL image: https://example.com/logo.png
- * - emoji: 🐱
- */
- export function getOAuthProviderIcon(iconName, size = 20) {
- const raw = String(iconName || '').trim();
- const iconSize = Number(size) > 0 ? Number(size) : 20;
- if (!raw) {
- return <Layers size={iconSize} color='var(--semi-color-text-2)' />;
- }
- if (isHttpUrl(raw)) {
- return (
- <img
- src={raw}
- alt='provider icon'
- width={iconSize}
- height={iconSize}
- style={{ borderRadius: 4, objectFit: 'cover' }}
- />
- );
- }
- if (isSimpleEmoji(raw)) {
- return (
- <span
- style={{
- width: iconSize,
- height: iconSize,
- lineHeight: `${iconSize}px`,
- textAlign: 'center',
- display: 'inline-block',
- fontSize: Math.max(Math.floor(iconSize * 0.8), 14),
- }}
- >
- {raw}
- </span>
- );
- }
- const key = normalizeOAuthIconKey(raw);
- const IconComp = oauthProviderIconMap[key];
- if (IconComp) {
- return <IconComp size={iconSize} />;
- }
- return (
- <Avatar size='extra-extra-small'>{raw.charAt(0).toUpperCase()}</Avatar>
- );
- }
- // 颜色列表
- const colors = [
- 'amber',
- 'blue',
- 'cyan',
- 'green',
- 'grey',
- 'indigo',
- 'light-blue',
- 'lime',
- 'orange',
- 'pink',
- 'purple',
- 'red',
- 'teal',
- 'violet',
- 'yellow',
- ];
- // 基础10色色板 (N ≤ 10)
- const baseColors = [
- '#1664FF', // 主色
- '#1AC6FF',
- '#FF8A00',
- '#3CC780',
- '#7442D4',
- '#FFC400',
- '#304D77',
- '#B48DEB',
- '#009488',
- '#FF7DDA',
- ];
- // 扩展20色色板 (10 < N ≤ 20)
- const extendedColors = [
- '#1664FF',
- '#B2CFFF',
- '#1AC6FF',
- '#94EFFF',
- '#FF8A00',
- '#FFCE7A',
- '#3CC780',
- '#B9EDCD',
- '#7442D4',
- '#DDC5FA',
- '#FFC400',
- '#FAE878',
- '#304D77',
- '#8B959E',
- '#B48DEB',
- '#EFE3FF',
- '#009488',
- '#59BAA8',
- '#FF7DDA',
- '#FFCFEE',
- ];
- // 模型颜色映射
- export const modelColorMap = {
- 'dall-e': 'rgb(147,112,219)', // 深紫色
- // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
- 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
- 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
- // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
- 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
- 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
- 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
- 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
- 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
- 'gpt-4': 'rgb(135,206,235)', // 天蓝色
- // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
- 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
- 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
- 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
- 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
- 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
- // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
- 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
- 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
- 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
- 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
- 'text-ada-001': 'rgb(255,192,203)', // 粉红色
- 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
- 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
- // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
- 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
- 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
- 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
- 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
- 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
- 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
- 'tts-1': 'rgb(255,140,0)', // 深橙色
- 'tts-1-1106': 'rgb(255,165,0)', // 橙色
- 'tts-1-hd': 'rgb(255,215,0)', // 金色
- 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
- 'whisper-1': 'rgb(245,245,220)', // 米色
- 'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
- 'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
- 'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
- };
- export function modelToColor(modelName) {
- // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
- if (modelColorMap[modelName]) {
- return modelColorMap[modelName];
- }
- // 2. 生成一个稳定的数字作为索引
- let hash = 0;
- for (let i = 0; i < modelName.length; i++) {
- hash = (hash << 5) - hash + modelName.charCodeAt(i);
- hash = hash & hash; // Convert to 32-bit integer
- }
- hash = Math.abs(hash);
- // 3. 根据模型名称长度选择不同的色板
- const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
- // 4. 使用hash值选择颜色
- const index = hash % colorPalette.length;
- return colorPalette[index];
- }
- export function stringToColor(str) {
- let sum = 0;
- for (let i = 0; i < str.length; i++) {
- sum += str.charCodeAt(i);
- }
- let i = sum % colors.length;
- return colors[i];
- }
- // 渲染带有模型图标的标签
- export function renderModelTag(modelName, options = {}) {
- const {
- color,
- size = 'default',
- shape = 'circle',
- onClick,
- suffixIcon,
- } = options;
- const categories = getModelCategories(i18next.t);
- let icon = null;
- for (const [key, category] of Object.entries(categories)) {
- if (key !== 'all' && category.filter({ model_name: modelName })) {
- icon = category.icon;
- break;
- }
- }
- return (
- <Tag
- color={color || stringToColor(modelName)}
- prefixIcon={icon}
- suffixIcon={suffixIcon}
- size={size}
- shape={shape}
- onClick={onClick}
- >
- {modelName}
- </Tag>
- );
- }
- export function renderText(text, limit) {
- if (text.length > limit) {
- return text.slice(0, limit - 3) + '...';
- }
- return text;
- }
- /**
- * Render group tags based on the input group string
- * @param {string} group - The input group string
- * @returns {JSX.Element} - The rendered group tags
- */
- export function renderGroup(group) {
- if (group === '') {
- return (
- <Tag key='default' color='white' shape='circle'>
- {i18next.t('用户分组')}
- </Tag>
- );
- }
- const tagColors = {
- vip: 'yellow',
- pro: 'yellow',
- svip: 'red',
- premium: 'red',
- };
- const groups = group.split(',').sort();
- return (
- <span key={group}>
- {groups.map((group) => (
- <Tag
- color={tagColors[group] || stringToColor(group)}
- key={group}
- shape='circle'
- onClick={async (event) => {
- event.stopPropagation();
- if (await copy(group)) {
- showSuccess(i18next.t('已复制:') + group);
- } else {
- Modal.error({
- title: i18next.t('无法复制到剪贴板,请手动复制'),
- content: group,
- });
- }
- }}
- >
- {group}
- </Tag>
- ))}
- </span>
- );
- }
- export function renderRatio(ratio) {
- let color = 'green';
- if (ratio > 5) {
- color = 'red';
- } else if (ratio > 3) {
- color = 'orange';
- } else if (ratio > 1) {
- color = 'blue';
- }
- return (
- <Tag color={color}>
- {ratio}x {i18next.t('倍率')}
- </Tag>
- );
- }
- const measureTextWidth = (
- text,
- style = {
- fontSize: '14px',
- fontFamily:
- '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
- },
- containerWidth,
- ) => {
- const span = document.createElement('span');
- span.style.visibility = 'hidden';
- span.style.position = 'absolute';
- span.style.whiteSpace = 'nowrap';
- span.style.fontSize = style.fontSize;
- span.style.fontFamily = style.fontFamily;
- span.textContent = text;
- document.body.appendChild(span);
- const width = span.offsetWidth;
- document.body.removeChild(span);
- return width;
- };
- export function truncateText(text, maxWidth = 200) {
- const isMobileScreen = window.matchMedia(
- `(max-width: ${MOBILE_BREAKPOINT - 1}px)`,
- ).matches;
- if (!isMobileScreen) {
- return text;
- }
- if (!text) return text;
- try {
- // Handle percentage-based maxWidth
- let actualMaxWidth = maxWidth;
- if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) {
- const percentage = parseFloat(maxWidth) / 100;
- // Use window width as fallback container width
- actualMaxWidth = window.innerWidth * percentage;
- }
- const width = measureTextWidth(text);
- if (width <= actualMaxWidth) return text;
- let left = 0;
- let right = text.length;
- let result = text;
- while (left <= right) {
- const mid = Math.floor((left + right) / 2);
- const truncated = text.slice(0, mid) + '...';
- const currentWidth = measureTextWidth(truncated);
- if (currentWidth <= actualMaxWidth) {
- result = truncated;
- left = mid + 1;
- } else {
- right = mid - 1;
- }
- }
- return result;
- } catch (error) {
- console.warn(
- 'Text measurement failed, falling back to character count',
- error,
- );
- if (text.length > 20) {
- return text.slice(0, 17) + '...';
- }
- return text;
- }
- }
- export const renderGroupOption = (item) => {
- const {
- disabled,
- selected,
- label,
- value,
- focused,
- className,
- style,
- onMouseEnter,
- onClick,
- empty,
- emptyContent,
- ...rest
- } = item;
- const baseStyle = {
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- padding: '8px 16px',
- cursor: disabled ? 'not-allowed' : 'pointer',
- backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
- opacity: disabled ? 0.5 : 1,
- ...(selected && {
- backgroundColor: 'var(--semi-color-primary-light-default)',
- }),
- '&:hover': {
- backgroundColor: !disabled && 'var(--semi-color-fill-1)',
- },
- };
- const handleClick = () => {
- if (!disabled && onClick) {
- onClick();
- }
- };
- const handleMouseEnter = (e) => {
- if (!disabled && onMouseEnter) {
- onMouseEnter(e);
- }
- };
- return (
- <div
- style={baseStyle}
- onClick={handleClick}
- onMouseEnter={handleMouseEnter}
- >
- <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
- <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
- {value}
- </Typography.Text>
- <Typography.Text type='secondary' size='small'>
- {label}
- </Typography.Text>
- </div>
- {item.ratio && renderRatio(item.ratio)}
- </div>
- );
- };
- export function renderNumber(num) {
- if (num >= 1000000000) {
- return (num / 1000000000).toFixed(1) + 'B';
- } else if (num >= 1000000) {
- return (num / 1000000).toFixed(1) + 'M';
- } else if (num >= 10000) {
- return (num / 1000).toFixed(1) + 'k';
- } else {
- return num;
- }
- }
- export function renderQuotaNumberWithDigit(num, digits = 2) {
- if (typeof num !== 'number' || isNaN(num)) {
- return 0;
- }
- const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
- num = num.toFixed(digits);
- if (quotaDisplayType === 'CNY') {
- return '¥' + num;
- } else if (quotaDisplayType === 'USD') {
- return '$' + num;
- } else if (quotaDisplayType === 'CUSTOM') {
- const statusStr = localStorage.getItem('status');
- let symbol = '¤';
- try {
- if (statusStr) {
- const s = JSON.parse(statusStr);
- symbol = s?.custom_currency_symbol || symbol;
- }
- } catch (e) {}
- return symbol + num;
- } else {
- return num;
- }
- }
- export function renderNumberWithPoint(num) {
- if (num === undefined) return '';
- num = num.toFixed(2);
- if (num >= 100000) {
- // Convert number to string to manipulate it
- let numStr = num.toString();
- // Find the position of the decimal point
- let decimalPointIndex = numStr.indexOf('.');
- let wholePart = numStr;
- let decimalPart = '';
- // If there is a decimal point, split the number into whole and decimal parts
- if (decimalPointIndex !== -1) {
- wholePart = numStr.slice(0, decimalPointIndex);
- decimalPart = numStr.slice(decimalPointIndex);
- }
- // Take the first two and last two digits of the whole number part
- let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
- // Return the formatted number
- return shortenedWholePart + decimalPart;
- }
- // If the number is less than 100,000, return it unmodified
- return num;
- }
- export function getQuotaPerUnit() {
- let quotaPerUnit = localStorage.getItem('quota_per_unit');
- quotaPerUnit = parseFloat(quotaPerUnit);
- return quotaPerUnit;
- }
- export function renderUnitWithQuota(quota) {
- let quotaPerUnit = localStorage.getItem('quota_per_unit');
- quotaPerUnit = parseFloat(quotaPerUnit);
- quota = parseFloat(quota);
- return quotaPerUnit * quota;
- }
- export function getQuotaWithUnit(quota, digits = 6) {
- let quotaPerUnit = localStorage.getItem('quota_per_unit');
- quotaPerUnit = parseFloat(quotaPerUnit);
- return (quota / quotaPerUnit).toFixed(digits);
- }
- export function renderQuotaWithAmount(amount) {
- const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
- if (quotaDisplayType === 'TOKENS') {
- return renderNumber(renderUnitWithQuota(amount));
- }
- const numericAmount = Number(amount);
- const formattedAmount = Number.isFinite(numericAmount)
- ? numericAmount.toFixed(2)
- : amount;
- if (quotaDisplayType === 'CNY') {
- return '¥' + formattedAmount;
- } else if (quotaDisplayType === 'CUSTOM') {
- const statusStr = localStorage.getItem('status');
- let symbol = '¤';
- try {
- if (statusStr) {
- const s = JSON.parse(statusStr);
- symbol = s?.custom_currency_symbol || symbol;
- }
- } catch (e) {}
- return symbol + formattedAmount;
- }
- return '$' + formattedAmount;
- }
- /**
- * 获取当前货币配置信息
- * @returns {Object} - { symbol, rate, type }
- */
- export function getCurrencyConfig() {
- const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
- const statusStr = localStorage.getItem('status');
- let symbol = '$';
- let rate = 1;
- if (quotaDisplayType === 'CNY') {
- symbol = '¥';
- try {
- if (statusStr) {
- const s = JSON.parse(statusStr);
- rate = s?.usd_exchange_rate || 7;
- }
- } catch (e) {}
- } else if (quotaDisplayType === 'CUSTOM') {
- try {
- if (statusStr) {
- const s = JSON.parse(statusStr);
- symbol = s?.custom_currency_symbol || '¤';
- rate = s?.custom_currency_exchange_rate || 1;
- }
- } catch (e) {}
- }
- return { symbol, rate, type: quotaDisplayType };
- }
- /**
- * 将美元金额转换为当前选择的货币
- * @param {number} usdAmount - 美元金额
- * @param {number} digits - 小数位数
- * @returns {string} - 格式化后的货币字符串
- */
- export function convertUSDToCurrency(usdAmount, digits = 2) {
- const { symbol, rate } = getCurrencyConfig();
- const convertedAmount = usdAmount * rate;
- return symbol + convertedAmount.toFixed(digits);
- }
- export function renderQuota(quota, digits = 2) {
- let quotaPerUnit = localStorage.getItem('quota_per_unit');
- const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
- quotaPerUnit = parseFloat(quotaPerUnit);
- if (quotaDisplayType === 'TOKENS') {
- return renderNumber(quota);
- }
- const resultUSD = quota / quotaPerUnit;
- let symbol = '$';
- let value = resultUSD;
- if (quotaDisplayType === 'CNY') {
- const statusStr = localStorage.getItem('status');
- let usdRate = 1;
- try {
- if (statusStr) {
- const s = JSON.parse(statusStr);
- usdRate = s?.usd_exchange_rate || 1;
- }
- } catch (e) {}
- value = resultUSD * usdRate;
- symbol = '¥';
- } else if (quotaDisplayType === 'CUSTOM') {
- const statusStr = localStorage.getItem('status');
- let symbolCustom = '¤';
- let rate = 1;
- try {
- if (statusStr) {
- const s = JSON.parse(statusStr);
- symbolCustom = s?.custom_currency_symbol || symbolCustom;
- rate = s?.custom_currency_exchange_rate || rate;
- }
- } catch (e) {}
- value = resultUSD * rate;
- symbol = symbolCustom;
- }
- const fixedResult = value.toFixed(digits);
- if (parseFloat(fixedResult) === 0 && quota > 0 && value > 0) {
- const minValue = Math.pow(10, -digits);
- return symbol + minValue.toFixed(digits);
- }
- return symbol + fixedResult;
- }
- function isValidGroupRatio(ratio) {
- return Number.isFinite(ratio) && ratio !== -1;
- }
- /**
- * Helper function to get effective ratio and label
- * @param {number} groupRatio - The default group ratio
- * @param {number} user_group_ratio - The user-specific group ratio
- * @returns {Object} - Object containing { ratio, label, useUserGroupRatio }
- */
- function getEffectiveRatio(groupRatio, user_group_ratio) {
- const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
- const ratioLabel = useUserGroupRatio
- ? i18next.t('专属倍率')
- : i18next.t('分组倍率');
- const effectiveRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
- return {
- ratio: effectiveRatio,
- label: ratioLabel,
- useUserGroupRatio: useUserGroupRatio,
- };
- }
- function getQuotaDisplayType() {
- return localStorage.getItem('quota_display_type') || 'USD';
- }
- function resolveBillingDisplayMode(displayMode, modelPrice = -1) {
- if (modelPrice !== -1) {
- return 'price';
- }
- if (getQuotaDisplayType() === 'TOKENS') {
- return 'ratio';
- }
- return displayMode === 'ratio' ? 'ratio' : 'price';
- }
- function isPriceDisplayMode(displayMode, modelPrice = -1) {
- return resolveBillingDisplayMode(displayMode, modelPrice) === 'price';
- }
- function shouldUseRatioBillingProcess(modelPrice = -1) {
- return modelPrice === -1 && getQuotaDisplayType() === 'TOKENS';
- }
- function formatCompactDisplayPrice(usdAmount, digits = 6) {
- const { symbol, rate } = getCurrencyConfig();
- const amount = Number((usdAmount * rate).toFixed(digits));
- return `${symbol}${amount}`;
- }
- function appendPricePart(parts, condition, key, vars) {
- if (!condition) {
- return;
- }
- parts.push(i18next.t(key, vars));
- }
- function joinBillingSummary(parts) {
- return parts.filter(Boolean).join(',');
- }
- function getGroupRatioText(groupRatio, user_group_ratio) {
- const { ratio, label } = getEffectiveRatio(groupRatio, user_group_ratio);
- return i18next.t('{{ratioType}} {{ratio}}x', {
- ratioType: label,
- ratio,
- });
- }
- function formatRatioValue(value, digits = 6) {
- const num = Number(value);
- if (!Number.isFinite(num)) {
- return 0;
- }
- return Number(num.toFixed(digits));
- }
- function renderDisplayAmountFromUsd(usdAmount, digits = 6) {
- return renderQuotaWithAmount(Number(Number(usdAmount || 0).toFixed(digits)));
- }
- function formatBillingDisplayPrice(usdAmount, rate, digits = 6) {
- return (usdAmount * rate).toFixed(digits);
- }
- function buildBillingText(key, vars) {
- return i18next.t(key, vars);
- }
- function buildBillingPriceText(
- key,
- { symbol, usdAmount, rate, amountKey = 'price', digits = 6, ...vars },
- ) {
- return buildBillingText(key, {
- symbol,
- [amountKey]: formatBillingDisplayPrice(usdAmount, rate, digits),
- ...vars,
- });
- }
- function renderBillingArticle(lines, { showReferenceNote = true } = {}) {
- const articleLines = lines.filter(Boolean);
- if (showReferenceNote) {
- articleLines.push(buildBillingText('仅供参考,以实际扣费为准'));
- }
- return (
- <article>
- {articleLines.map((line, index) => (
- <p key={index}>{line}</p>
- ))}
- </article>
- );
- }
- // Shared core for simple price rendering (used by OpenAI-like and Claude-like variants)
- function renderPriceSimpleCore({
- modelRatio,
- modelPrice = -1,
- groupRatio,
- user_group_ratio,
- cacheTokens = 0,
- cacheRatio = 1.0,
- cacheCreationTokens = 0,
- cacheCreationRatio = 1.0,
- cacheCreationTokens5m = 0,
- cacheCreationRatio5m = 1.0,
- cacheCreationTokens1h = 0,
- cacheCreationRatio1h = 1.0,
- image = false,
- imageRatio = 1.0,
- isSystemPromptOverride = false,
- displayMode = 'price',
- outputMode = 'text',
- }) {
- const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
- groupRatio,
- user_group_ratio,
- );
- const finalGroupRatio = effectiveGroupRatio;
- const { symbol, rate } = getCurrencyConfig();
- const hasSplitCacheCreation =
- cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
- const shouldShowLegacyCacheCreation =
- !hasSplitCacheCreation && cacheCreationTokens !== 0;
- const shouldShowCache = cacheTokens !== 0;
- const shouldShowCacheCreation5m =
- hasSplitCacheCreation && cacheCreationTokens5m > 0;
- const shouldShowCacheCreation1h =
- hasSplitCacheCreation && cacheCreationTokens1h > 0;
- if (outputMode === 'segments') {
- const segments = [
- {
- tone: 'primary',
- text: getGroupRatioText(groupRatio, user_group_ratio),
- },
- ];
- if (modelPrice !== -1) {
- segments.push({
- tone: 'secondary',
- text: isPriceDisplayMode(displayMode, modelPrice)
- ? i18next.t('模型价格 {{price}}', {
- price: formatCompactDisplayPrice(modelPrice),
- })
- : i18next.t('按次'),
- });
- } else if (isPriceDisplayMode(displayMode, modelPrice)) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('输入 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(modelRatio * 2.0),
- }),
- });
- if (shouldShowCache) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('缓存读 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(modelRatio * 2.0 * cacheRatio),
- }),
- });
- }
- if (hasSplitCacheCreation && shouldShowCacheCreation5m) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('5m缓存创建 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(
- modelRatio * 2.0 * cacheCreationRatio5m,
- ),
- }),
- });
- }
- if (hasSplitCacheCreation && shouldShowCacheCreation1h) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('1h缓存创建 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(
- modelRatio * 2.0 * cacheCreationRatio1h,
- ),
- }),
- });
- }
- if (!hasSplitCacheCreation && shouldShowLegacyCacheCreation) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('缓存创建 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(
- modelRatio * 2.0 * cacheCreationRatio,
- ),
- }),
- });
- }
- if (image) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('图片输入 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(modelRatio * 2.0 * imageRatio),
- }),
- });
- }
- } else {
- segments.push({
- tone: 'secondary',
- text: i18next.t('模型: {{ratio}}', {
- ratio: modelRatio,
- }),
- });
- if (shouldShowCache) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('缓存: {{cacheRatio}}', {
- cacheRatio: cacheRatio,
- }),
- });
- }
- if (hasSplitCacheCreation) {
- if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
- segments.push({
- tone: 'secondary',
- text: i18next.t(
- '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
- {
- cacheCreationRatio5m: cacheCreationRatio5m,
- cacheCreationRatio1h: cacheCreationRatio1h,
- },
- ),
- });
- } else if (shouldShowCacheCreation5m) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}', {
- cacheCreationRatio5m: cacheCreationRatio5m,
- }),
- });
- } else if (shouldShowCacheCreation1h) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}', {
- cacheCreationRatio1h: cacheCreationRatio1h,
- }),
- });
- }
- } else if (shouldShowLegacyCacheCreation) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('缓存创建: {{cacheCreationRatio}}', {
- cacheCreationRatio: cacheCreationRatio,
- }),
- });
- }
- if (image) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('图片输入: {{imageRatio}}', {
- imageRatio: imageRatio,
- }),
- });
- }
- }
- if (isSystemPromptOverride) {
- segments.push({
- tone: 'primary',
- text: i18next.t('系统提示覆盖'),
- });
- }
- return segments;
- }
- if (modelPrice !== -1) {
- if (isPriceDisplayMode(displayMode, modelPrice)) {
- return joinBillingSummary([
- i18next.t('模型价格:{{symbol}}{{price}}', {
- symbol: symbol,
- price: (modelPrice * rate).toFixed(6),
- }),
- getGroupRatioText(groupRatio, user_group_ratio),
- ]);
- }
- const displayPrice = (modelPrice * rate).toFixed(6);
- return i18next.t('价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}', {
- symbol: symbol,
- price: displayPrice,
- ratioType: ratioLabel,
- ratio: finalGroupRatio,
- });
- }
- if (isPriceDisplayMode(displayMode, modelPrice)) {
- const parts = [];
- if (modelPrice !== -1) {
- parts.push(
- i18next.t('模型价格 {{price}}', {
- price: formatCompactDisplayPrice(modelPrice),
- }),
- );
- parts.push(getGroupRatioText(groupRatio, user_group_ratio));
- return joinBillingSummary(parts);
- }
- parts.push(
- i18next.t('输入 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(modelRatio * 2.0),
- }),
- );
- if (shouldShowCache) {
- parts.push(
- i18next.t('缓存读 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(modelRatio * 2.0 * cacheRatio),
- }),
- );
- }
- if (hasSplitCacheCreation && shouldShowCacheCreation5m) {
- parts.push(
- i18next.t('5m缓存创建 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(
- modelRatio * 2.0 * cacheCreationRatio5m,
- ),
- }),
- );
- }
- if (hasSplitCacheCreation && shouldShowCacheCreation1h) {
- parts.push(
- i18next.t('1h缓存创建 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(
- modelRatio * 2.0 * cacheCreationRatio1h,
- ),
- }),
- );
- }
- if (!hasSplitCacheCreation && shouldShowLegacyCacheCreation) {
- parts.push(
- i18next.t('缓存创建 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(
- modelRatio * 2.0 * cacheCreationRatio,
- ),
- }),
- );
- }
- if (image) {
- parts.push(
- i18next.t('图片输入 {{price}} / 1M tokens', {
- price: formatCompactDisplayPrice(modelRatio * 2.0 * imageRatio),
- }),
- );
- }
- parts.push(getGroupRatioText(groupRatio, user_group_ratio));
- let result = joinBillingSummary(parts);
- if (isSystemPromptOverride) {
- result += '\n\r' + i18next.t('系统提示覆盖');
- }
- return result;
- }
- const parts = [];
- // base: model ratio
- parts.push(i18next.t('模型: {{ratio}}'));
- // cache part (label differs when with image)
- if (shouldShowCache) {
- parts.push(i18next.t('缓存: {{cacheRatio}}'));
- }
- if (hasSplitCacheCreation) {
- if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
- parts.push(
- i18next.t(
- '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
- ),
- );
- } else if (shouldShowCacheCreation5m) {
- parts.push(i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}'));
- } else if (shouldShowCacheCreation1h) {
- parts.push(i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}'));
- }
- } else if (shouldShowLegacyCacheCreation) {
- parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}'));
- }
- // image part
- if (image) {
- parts.push(i18next.t('图片输入: {{imageRatio}}'));
- }
- parts.push(`{{ratioType}}: {{groupRatio}}`);
- let result = i18next.t(parts.join(' * '), {
- ratio: modelRatio,
- ratioType: ratioLabel,
- groupRatio: finalGroupRatio,
- cacheRatio: cacheRatio,
- cacheCreationRatio: cacheCreationRatio,
- cacheCreationRatio5m: cacheCreationRatio5m,
- cacheCreationRatio1h: cacheCreationRatio1h,
- imageRatio: imageRatio,
- });
- if (isSystemPromptOverride) {
- result += '\n\r' + i18next.t('系统提示覆盖');
- }
- return result;
- }
- export function renderTaskBillingProcess(other, content) {
- if (other?.task_id != null) {
- return renderBillingArticle(
- [content].filter(Boolean),
- { showReferenceNote: false },
- );
- }
- return renderBillingArticle([
- buildBillingText('任务预扣费(将在任务完成后按实际token重算)'),
- ]);
- }
- export function renderModelPrice(opts) {
- const {
- prompt_tokens: inputTokens = 0,
- completion_tokens: completionTokens = 0,
- model_ratio: modelRatio = 0,
- model_price: modelPrice = -1,
- completion_ratio: completionRatio,
- group_ratio: _groupRatio,
- user_group_ratio,
- cache_tokens: cacheTokens = 0,
- cache_ratio: cacheRatio = 1.0,
- image = false,
- image_ratio: imageRatio = 1.0,
- image_output: imageOutputTokens = 0,
- web_search: webSearch = false,
- web_search_call_count: webSearchCallCount = 0,
- web_search_price: webSearchPrice = 0,
- file_search: fileSearch = false,
- file_search_call_count: fileSearchCallCount = 0,
- file_search_price: fileSearchPrice = 0,
- audio_input_seperate_price: audioInputSeperatePrice = false,
- audio_input_token_count: audioInputTokens = 0,
- audio_input_price: audioInputPrice = 0,
- image_generation_call: imageGenerationCall = false,
- image_generation_call_price: imageGenerationCallPrice = 0,
- displayMode = 'price',
- } = opts;
- const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
- _groupRatio,
- user_group_ratio,
- );
- let groupRatio = effectiveGroupRatio;
- const { symbol, rate } = getCurrencyConfig();
- if (!shouldUseRatioBillingProcess(modelPrice)) {
- if (modelPrice !== -1) {
- return renderBillingArticle([
- buildBillingPriceText('按次:{{symbol}}{{price}}', {
- symbol,
- usdAmount: modelPrice,
- rate,
- }),
- buildBillingPriceText(
- '按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
- {
- symbol,
- usdAmount: modelPrice,
- rate,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amountKey: 'price',
- total: formatBillingDisplayPrice(modelPrice * groupRatio, rate),
- },
- ),
- ]);
- }
- if (completionRatio === undefined) {
- completionRatio = 0;
- }
- const inputRatioPrice = modelRatio * 2.0;
- const completionRatioPrice = modelRatio * 2.0 * completionRatio;
- const cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
- const imageRatioPrice = modelRatio * 2.0 * imageRatio;
- let effectiveInputTokens =
- inputTokens - cacheTokens + cacheTokens * cacheRatio;
- if (image && imageOutputTokens > 0) {
- effectiveInputTokens =
- inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
- }
- if (audioInputTokens > 0) {
- effectiveInputTokens -= audioInputTokens;
- }
- const price =
- (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
- (audioInputTokens / 1000000) * audioInputPrice * groupRatio +
- (completionTokens / 1000000) * completionRatioPrice * groupRatio +
- (webSearchCallCount / 1000) * webSearchPrice * groupRatio +
- (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
- imageGenerationCallPrice * groupRatio;
- let inputDesc = '';
- if (image && imageOutputTokens > 0) {
- inputDesc = buildBillingPriceText(
- '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}',
- {
- nonImageInput: inputTokens - imageOutputTokens,
- imageInput: imageOutputTokens,
- symbol,
- usdAmount: inputRatioPrice,
- rate,
- },
- );
- } else if (cacheTokens > 0) {
- inputDesc = buildBillingText(
- '(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}',
- {
- nonCacheInput: inputTokens - cacheTokens,
- cacheInput: cacheTokens,
- symbol,
- price: formatBillingDisplayPrice(inputRatioPrice, rate),
- cachePrice: formatBillingDisplayPrice(cacheRatioPrice, rate),
- },
- );
- } else if (audioInputSeperatePrice && audioInputTokens > 0) {
- inputDesc = buildBillingText(
- '(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}',
- {
- nonAudioInput: inputTokens - audioInputTokens,
- audioInput: audioInputTokens,
- symbol,
- price: formatBillingDisplayPrice(inputRatioPrice, rate),
- audioPrice: formatBillingDisplayPrice(audioInputPrice, rate),
- },
- );
- } else {
- inputDesc = buildBillingPriceText(
- '(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}',
- {
- input: inputTokens,
- symbol,
- usdAmount: inputRatioPrice,
- rate,
- },
- );
- }
- const outputDesc = buildBillingText(
- '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}',
- {
- completion: completionTokens,
- symbol,
- compPrice: formatBillingDisplayPrice(completionRatioPrice, rate),
- ratio: groupRatio,
- ratioType: ratioLabel,
- },
- );
- const extraServices = [
- webSearch && webSearchCallCount > 0
- ? buildBillingPriceText(
- ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
- {
- count: webSearchCallCount,
- symbol,
- usdAmount: webSearchPrice,
- rate,
- ratio: groupRatio,
- ratioType: ratioLabel,
- },
- )
- : '',
- fileSearch && fileSearchCallCount > 0
- ? buildBillingPriceText(
- ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
- {
- count: fileSearchCallCount,
- symbol,
- usdAmount: fileSearchPrice,
- rate,
- ratio: groupRatio,
- ratioType: ratioLabel,
- },
- )
- : '',
- imageGenerationCall && imageGenerationCallPrice > 0
- ? buildBillingPriceText(
- ' + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}',
- {
- symbol,
- usdAmount: imageGenerationCallPrice,
- rate,
- ratio: groupRatio,
- ratioType: ratioLabel,
- },
- )
- : '',
- ].join('');
- const billingLines = [
- buildBillingPriceText(
- '输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}',
- {
- symbol,
- usdAmount: inputRatioPrice,
- rate,
- audioPrice: audioInputSeperatePrice
- ? `,${i18next.t('音频输入价格')} ${symbol}${formatBillingDisplayPrice(audioInputPrice, rate)} / 1M tokens`
- : '',
- },
- ),
- buildBillingPriceText('输出价格:{{symbol}}{{total}} / 1M tokens', {
- symbol,
- usdAmount: completionRatioPrice,
- rate,
- amountKey: 'total',
- }),
- cacheTokens > 0
- ? buildBillingPriceText(
- '缓存读取价格:{{symbol}}{{total}} / 1M tokens',
- {
- symbol,
- usdAmount: inputRatioPrice * cacheRatio,
- rate,
- amountKey: 'total',
- },
- )
- : null,
- image && imageOutputTokens > 0
- ? buildBillingPriceText(
- '图片输入价格:{{symbol}}{{total}} / 1M tokens',
- {
- symbol,
- usdAmount: imageRatioPrice,
- rate,
- amountKey: 'total',
- },
- )
- : null,
- webSearch && webSearchCallCount > 0
- ? buildBillingPriceText('Web搜索价格:{{symbol}}{{price}} / 1K 次', {
- symbol,
- usdAmount: webSearchPrice,
- rate,
- })
- : null,
- fileSearch && fileSearchCallCount > 0
- ? buildBillingPriceText('文件搜索价格:{{symbol}}{{price}} / 1K 次', {
- symbol,
- usdAmount: fileSearchPrice,
- rate,
- })
- : null,
- imageGenerationCall && imageGenerationCallPrice > 0
- ? buildBillingPriceText('图片生成调用:{{symbol}}{{price}} / 1次', {
- symbol,
- usdAmount: imageGenerationCallPrice,
- rate,
- })
- : null,
- buildBillingText(
- '{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}',
- {
- inputDesc,
- outputDesc,
- extraServices,
- symbol,
- total: formatBillingDisplayPrice(price, rate),
- },
- ),
- ];
- return renderBillingArticle(billingLines);
- }
- if (modelPrice !== -1) {
- const displayPrice = (modelPrice * rate).toFixed(6);
- const displayTotal = (modelPrice * groupRatio * rate).toFixed(6);
- return i18next.t(
- '按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}',
- {
- symbol: symbol,
- price: displayPrice,
- ratio: groupRatio,
- total: displayTotal,
- ratioType: ratioLabel,
- },
- );
- }
- if (completionRatio === undefined) {
- completionRatio = 0;
- }
- const modelRatioValue = formatRatioValue(modelRatio);
- const completionRatioValue = formatRatioValue(completionRatio);
- const cacheRatioValue = formatRatioValue(cacheRatio);
- const imageRatioValue = formatRatioValue(imageRatio);
- const inputRatioPrice = modelRatio * 2.0;
- const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
- const audioRatioValue =
- audioInputSeperatePrice && audioInputPrice > 0
- ? formatRatioValue(audioInputPrice / inputRatioPrice)
- : null;
- const textInputTokens = Math.max(
- inputTokens - cacheTokens - audioInputTokens,
- 0,
- );
- const imageInputTokens =
- image && imageOutputTokens > 0 ? imageOutputTokens : 0;
- const cacheInputTokens = cacheTokens;
- const textInputAmount =
- (textInputTokens / 1000000) * inputRatioPrice * groupRatio;
- const cacheInputAmount =
- (cacheInputTokens / 1000000) *
- inputRatioPrice *
- cacheRatioValue *
- groupRatio;
- const imageInputAmount =
- (imageInputTokens / 1000000) *
- inputRatioPrice *
- imageRatioValue *
- groupRatio;
- const audioInputAmount =
- (audioInputTokens / 1000000) * audioInputPrice * groupRatio;
- const completionAmount =
- (completionTokens / 1000000) * completionRatioPrice * groupRatio;
- const webSearchAmount =
- (webSearchCallCount / 1000) * webSearchPrice * groupRatio;
- const fileSearchAmount =
- (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
- const imageGenerationAmount = imageGenerationCallPrice * groupRatio;
- const totalAmount =
- textInputAmount +
- cacheInputAmount +
- imageInputAmount +
- audioInputAmount +
- completionAmount +
- webSearchAmount +
- fileSearchAmount +
- imageGenerationAmount;
- return renderBillingArticle([
- [
- buildBillingText('模型倍率 {{modelRatio}}', {
- modelRatio: modelRatioValue,
- }),
- buildBillingText('补全倍率 {{completionRatio}}', {
- completionRatio: completionRatioValue,
- }),
- cacheInputTokens > 0
- ? buildBillingText('缓存倍率 {{cacheRatio}}', {
- cacheRatio: cacheRatioValue,
- })
- : null,
- imageInputTokens > 0
- ? buildBillingText('图片倍率 {{imageRatio}}', {
- imageRatio: imageRatioValue,
- })
- : null,
- audioRatioValue !== null
- ? buildBillingText('音频倍率 {{audioRatio}}', {
- audioRatio: audioRatioValue,
- })
- : null,
- buildBillingText('{{ratioType}} {{ratio}}', {
- ratioType: ratioLabel,
- ratio: groupRatio,
- }),
- ]
- .filter(Boolean)
- .join(','),
- textInputTokens > 0
- ? buildBillingText(
- '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: textInputTokens,
- modelRatio: modelRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(textInputAmount),
- },
- )
- : null,
- cacheInputTokens > 0
- ? buildBillingText(
- '缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: cacheInputTokens,
- modelRatio: modelRatioValue,
- cacheRatio: cacheRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(cacheInputAmount),
- },
- )
- : null,
- imageInputTokens > 0
- ? buildBillingText(
- '图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: imageInputTokens,
- modelRatio: modelRatioValue,
- imageRatio: imageRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(imageInputAmount),
- },
- )
- : null,
- audioInputTokens > 0 && audioRatioValue !== null
- ? buildBillingText(
- '音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: audioInputTokens,
- modelRatio: modelRatioValue,
- audioRatio: audioRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(audioInputAmount),
- },
- )
- : null,
- buildBillingText(
- '输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: completionTokens,
- modelRatio: modelRatioValue,
- completionRatio: completionRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(completionAmount),
- },
- ),
- webSearch && webSearchCallCount > 0
- ? buildBillingText(
- 'Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- count: webSearchCallCount,
- price: renderDisplayAmountFromUsd(webSearchPrice),
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(webSearchAmount),
- },
- )
- : null,
- fileSearch && fileSearchCallCount > 0
- ? buildBillingText(
- '文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- count: fileSearchCallCount,
- price: renderDisplayAmountFromUsd(fileSearchPrice),
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(fileSearchAmount),
- },
- )
- : null,
- imageGenerationCall && imageGenerationCallPrice > 0
- ? buildBillingText(
- '图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- price: renderDisplayAmountFromUsd(imageGenerationCallPrice),
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(imageGenerationAmount),
- },
- )
- : null,
- buildBillingText('合计:{{total}}', {
- total: renderDisplayAmountFromUsd(totalAmount),
- }),
- ]);
- }
- export function renderLogContent(opts) {
- const {
- model_ratio: modelRatio,
- completion_ratio: completionRatio,
- model_price: modelPrice = -1,
- group_ratio: groupRatio,
- user_group_ratio,
- cache_ratio: cacheRatio = 1.0,
- image = false,
- image_ratio: imageRatio = 1.0,
- web_search: webSearch = false,
- web_search_call_count: webSearchCallCount = 0,
- file_search: fileSearch = false,
- file_search_call_count: fileSearchCallCount = 0,
- displayMode = 'price',
- } = opts;
- const {
- ratio,
- label: ratioLabel,
- useUserGroupRatio: useUserGroupRatio,
- } = getEffectiveRatio(groupRatio, user_group_ratio);
- // 获取货币配置
- const { symbol, rate } = getCurrencyConfig();
- if (isPriceDisplayMode(displayMode, modelPrice)) {
- if (modelPrice !== -1) {
- return joinBillingSummary([
- i18next.t('模型价格 {{symbol}}{{price}} / 次', {
- symbol,
- price: (modelPrice * rate).toFixed(6),
- }),
- getGroupRatioText(groupRatio, user_group_ratio),
- ]);
- }
- const parts = [
- i18next.t('输入价格 {{symbol}}{{price}} / 1M tokens', {
- symbol,
- price: (modelRatio * 2.0 * rate).toFixed(6),
- }),
- i18next.t('输出价格 {{symbol}}{{price}} / 1M tokens', {
- symbol,
- price: (modelRatio * 2.0 * completionRatio * rate).toFixed(6),
- }),
- ];
- appendPricePart(
- parts,
- cacheRatio !== 1.0,
- '缓存读取价格 {{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- price: (modelRatio * 2.0 * cacheRatio * rate).toFixed(6),
- },
- );
- appendPricePart(
- parts,
- image,
- '图片输入价格 {{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- price: (modelRatio * 2.0 * imageRatio * rate).toFixed(6),
- },
- );
- appendPricePart(
- parts,
- webSearch,
- 'Web 搜索调用 {{webSearchCallCount}} 次',
- {
- webSearchCallCount,
- },
- );
- appendPricePart(
- parts,
- fileSearch,
- '文件搜索调用 {{fileSearchCallCount}} 次',
- {
- fileSearchCallCount,
- },
- );
- parts.push(getGroupRatioText(groupRatio, user_group_ratio));
- return joinBillingSummary(parts);
- }
- if (modelPrice !== -1) {
- return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', {
- symbol: symbol,
- price: (modelPrice * rate).toFixed(6),
- ratioType: ratioLabel,
- ratio,
- });
- } else {
- if (image) {
- return i18next.t(
- '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}',
- {
- modelRatio: modelRatio,
- cacheRatio: cacheRatio,
- completionRatio: completionRatio,
- imageRatio: imageRatio,
- ratioType: ratioLabel,
- ratio,
- },
- );
- } else if (webSearch) {
- return i18next.t(
- '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次',
- {
- modelRatio: modelRatio,
- cacheRatio: cacheRatio,
- completionRatio: completionRatio,
- ratioType: ratioLabel,
- ratio,
- webSearchCallCount,
- },
- );
- } else {
- return i18next.t(
- '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
- {
- modelRatio: modelRatio,
- cacheRatio: cacheRatio,
- completionRatio: completionRatio,
- ratioType: ratioLabel,
- ratio,
- },
- );
- }
- }
- }
- export function stripExprVersion(exprStr) {
- if (!exprStr) return { version: 1, body: '' };
- const m = exprStr.match(/^v(\d+):([\s\S]*)$/);
- if (m) return { version: Number(m[1]), body: m[2] };
- return { version: 1, body: exprStr };
- }
- function parseTierBody(bodyStr) {
- const coeffs = {};
- const re = new RegExp(BILLING_VAR_REGEX.source, 'g');
- let m;
- while ((m = re.exec(bodyStr)) !== null) {
- if (!(m[1] in coeffs)) coeffs[m[1]] = Number(m[2]);
- }
- const tier = {};
- for (const [varName, field] of Object.entries(BILLING_VAR_KEY_TO_FIELD)) {
- tier[field] = coeffs[varName] || 0;
- }
- return tier;
- }
- export function parseTiersFromExpr(exprStr) {
- if (!exprStr) return [];
- try {
- const { body } = stripExprVersion(exprStr);
- const condGroup = `((?:(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)(?:\\s*&&\\s*(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`;
- const tierRe = new RegExp(`(?:${condGroup}\\s*\\?\\s*)?tier\\("([^"]*)",\\s*([^)]+)\\)`, 'g');
- const tiers = [];
- let m;
- while ((m = tierRe.exec(body)) !== null) {
- const condStr = m[1] || '';
- const conditions = [];
- if (condStr) {
- for (const cp of condStr.split(/\s*&&\s*/)) {
- const cm = cp.trim().match(/^(p|c)\s*(<|<=|>|>=)\s*([\d.eE+]+)$/);
- if (cm) conditions.push({ var: cm[1], op: cm[2], value: Number(cm[3]) });
- }
- }
- const tier = parseTierBody(m[3]);
- tier.label = m[2];
- tier.conditions = conditions;
- tiers.push(tier);
- }
- return tiers;
- } catch {
- return [];
- }
- }
- export function renderTieredModelPrice(opts) {
- const {
- prompt_tokens: inputTokens = 0,
- completion_tokens: completionTokens = 0,
- expr_b64: exprB64,
- matched_tier: matchedTier,
- group_ratio: groupRatio,
- cache_tokens: cacheTokens = 0,
- cache_creation_tokens: cacheCreationTokens = 0,
- cache_creation_tokens_5m: cacheCreationTokens5m = 0,
- cache_creation_tokens_1h: cacheCreationTokens1h = 0,
- } = opts;
- let exprStr = '';
- try { exprStr = atob(exprB64); } catch { /* ignore */ }
- const tiers = parseTiersFromExpr(exprStr);
- if (tiers.length === 0) {
- return i18next.t('阶梯计费(表达式解析失败)');
- }
- const tier = tiers.find((t) => t.label === matchedTier) || tiers[0];
- const { symbol, rate } = getCurrencyConfig();
- const gr = groupRatio || 1;
- const priceLines = BILLING_VARS.map((v) => [v.field, v.label]);
- const lines = [
- buildBillingText('命中档位:{{tier}}', { tier: matchedTier || tier.label }),
- ...priceLines
- .filter(([field]) => tier[field] > 0)
- .map(([field, label]) =>
- buildBillingPriceText(`${label}:{{symbol}}{{price}} / 1M tokens`, { symbol, usdAmount: tier[field], rate }),
- ),
- ];
- return renderBillingArticle(lines);
- }
- export function renderTieredModelPriceSimple(opts) {
- const {
- expr_b64: exprB64,
- matched_tier: matchedTier,
- group_ratio: groupRatio,
- user_group_ratio,
- cache_tokens: cacheTokens = 0,
- cache_creation_tokens_5m: cacheCreationTokens5m = 0,
- cache_creation_tokens_1h: cacheCreationTokens1h = 0,
- cache_creation_tokens: cacheCreationTokens = 0,
- displayMode = 'price',
- outputMode = 'segments',
- } = opts;
- let exprStr = '';
- try { exprStr = atob(exprB64); } catch { /* ignore */ }
- const tiers = parseTiersFromExpr(exprStr);
- const tier = tiers.find((t) => t.label === matchedTier) || tiers[0];
- if (outputMode === 'segments') {
- const segments = [
- {
- tone: 'primary',
- text: getGroupRatioText(groupRatio, user_group_ratio),
- },
- ];
- if (tier && isPriceDisplayMode(displayMode)) {
- const priceSegments = BILLING_VARS.map((v) => [v.field, v.shortLabel]);
- for (const [field, label] of priceSegments) {
- if (tier[field] > 0) {
- segments.push({
- tone: 'secondary',
- text: i18next.t('{{label}} {{price}} / 1M tokens', {
- label: i18next.t(label),
- price: formatCompactDisplayPrice(tier[field]),
- }),
- });
- }
- }
- }
- return segments;
- }
- return [];
- }
- export function renderModelPriceSimple(opts) {
- const {
- model_ratio: modelRatio,
- model_price: modelPrice = -1,
- group_ratio: groupRatio,
- user_group_ratio,
- cache_tokens: cacheTokens = 0,
- cache_ratio: cacheRatio = 1.0,
- cache_creation_tokens: cacheCreationTokens = 0,
- cache_creation_ratio: cacheCreationRatio = 1.0,
- cache_creation_tokens_5m: cacheCreationTokens5m = 0,
- cache_creation_ratio_5m: cacheCreationRatio5m = 1.0,
- cache_creation_tokens_1h: cacheCreationTokens1h = 0,
- cache_creation_ratio_1h: cacheCreationRatio1h = 1.0,
- image = false,
- image_ratio: imageRatio = 1.0,
- is_system_prompt_overwritten: isSystemPromptOverride = false,
- provider = 'openai',
- displayMode = 'price',
- outputMode = 'text',
- } = opts;
- return renderPriceSimpleCore({
- modelRatio,
- modelPrice,
- groupRatio,
- user_group_ratio,
- cacheTokens,
- cacheRatio,
- cacheCreationTokens,
- cacheCreationRatio,
- cacheCreationTokens5m,
- cacheCreationRatio5m,
- cacheCreationTokens1h,
- cacheCreationRatio1h,
- image,
- imageRatio,
- isSystemPromptOverride,
- displayMode,
- outputMode,
- });
- }
- export function renderAudioModelPrice(opts) {
- const {
- prompt_tokens: inputTokens = 0,
- completion_tokens: completionTokens = 0,
- model_ratio: modelRatio = 0,
- model_price: modelPrice = -1,
- completion_ratio: completionRatio,
- audio_input: audioInputTokens = 0,
- audio_output: audioCompletionTokens = 0,
- audio_ratio: audioRatio,
- audio_completion_ratio: audioCompletionRatio,
- group_ratio: _groupRatio,
- user_group_ratio,
- cache_tokens: cacheTokens = 0,
- cache_ratio: cacheRatio = 1.0,
- displayMode = 'price',
- } = opts;
- const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
- _groupRatio,
- user_group_ratio,
- );
- let groupRatio = effectiveGroupRatio;
- // 获取货币配置
- const { symbol, rate } = getCurrencyConfig();
- if (!shouldUseRatioBillingProcess(modelPrice)) {
- if (modelPrice !== -1) {
- return renderBillingArticle([
- buildBillingPriceText('模型价格:{{symbol}}{{price}} / 次', {
- symbol,
- usdAmount: modelPrice,
- rate,
- }),
- buildBillingPriceText(
- '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
- {
- symbol,
- usdAmount: modelPrice,
- rate,
- ratioType: ratioLabel,
- ratio: groupRatio,
- total: formatBillingDisplayPrice(modelPrice * groupRatio, rate),
- },
- ),
- ]);
- }
- if (completionRatio === undefined) {
- completionRatio = 0;
- }
- audioRatio = parseFloat(audioRatio).toFixed(6);
- const inputRatioPrice = modelRatio * 2.0;
- const completionRatioPrice = modelRatio * 2.0 * completionRatio;
- const textPrice =
- ((inputTokens - cacheTokens + cacheTokens * cacheRatio) / 1000000) *
- inputRatioPrice *
- groupRatio +
- (completionTokens / 1000000) * completionRatioPrice * groupRatio;
- const audioPrice =
- (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
- (audioCompletionTokens / 1000000) *
- inputRatioPrice *
- audioRatio *
- audioCompletionRatio *
- groupRatio;
- const totalPrice = textPrice + audioPrice;
- return renderBillingArticle([
- buildBillingPriceText('输入价格:{{symbol}}{{price}} / 1M tokens', {
- symbol,
- usdAmount: inputRatioPrice,
- rate,
- }),
- buildBillingPriceText('输出价格:{{symbol}}{{price}} / 1M tokens', {
- symbol,
- usdAmount: completionRatioPrice,
- rate,
- }),
- cacheTokens > 0
- ? buildBillingPriceText(
- '缓存读取价格:{{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- usdAmount: inputRatioPrice * cacheRatio,
- rate,
- },
- )
- : null,
- buildBillingPriceText('音频输入价格:{{symbol}}{{price}} / 1M tokens', {
- symbol,
- usdAmount: inputRatioPrice * audioRatio,
- rate,
- }),
- buildBillingPriceText('音频补全价格:{{symbol}}{{price}} / 1M tokens', {
- symbol,
- usdAmount: inputRatioPrice * audioRatio * audioCompletionRatio,
- rate,
- }),
- buildBillingText(
- '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
- {
- input: inputTokens,
- completion: completionTokens,
- audioInput: audioInputTokens,
- audioCompletion: audioCompletionTokens,
- textInputPrice: formatBillingDisplayPrice(inputRatioPrice, rate),
- textCompPrice: formatBillingDisplayPrice(completionRatioPrice, rate),
- audioInputPrice: formatBillingDisplayPrice(
- audioRatio * inputRatioPrice,
- rate,
- ),
- audioCompPrice: formatBillingDisplayPrice(
- audioRatio * audioCompletionRatio * inputRatioPrice,
- rate,
- ),
- ratioType: ratioLabel,
- ratio: groupRatio,
- symbol,
- total: formatBillingDisplayPrice(totalPrice, rate),
- },
- ),
- ]);
- }
- // 1 ratio = $0.002 / 1K tokens
- if (modelPrice !== -1) {
- return i18next.t(
- '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}',
- {
- symbol: symbol,
- price: (modelPrice * rate).toFixed(6),
- ratio: groupRatio,
- total: (modelPrice * groupRatio * rate).toFixed(6),
- ratioType: ratioLabel,
- },
- );
- }
- if (completionRatio === undefined) {
- completionRatio = 0;
- }
- const modelRatioValue = formatRatioValue(modelRatio);
- const completionRatioValue = formatRatioValue(completionRatio);
- const cacheRatioValue = formatRatioValue(cacheRatio);
- const audioRatioValue = formatRatioValue(audioRatio);
- const audioCompletionRatioValue = formatRatioValue(audioCompletionRatio);
- const inputRatioPrice = modelRatio * 2.0;
- const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
- const effectiveInputTokens =
- inputTokens - cacheTokens + cacheTokens * cacheRatioValue;
- const textPrice =
- (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
- (completionTokens / 1000000) * completionRatioPrice * groupRatio;
- const audioPrice =
- (audioInputTokens / 1000000) *
- inputRatioPrice *
- audioRatioValue *
- groupRatio +
- (audioCompletionTokens / 1000000) *
- inputRatioPrice *
- audioRatioValue *
- audioCompletionRatioValue *
- groupRatio;
- const totalPrice = textPrice + audioPrice;
- return renderBillingArticle([
- buildBillingText(
- '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}',
- {
- modelRatio: modelRatioValue,
- completionRatio: completionRatioValue,
- audioRatio: audioRatioValue,
- audioCompletionRatio: audioCompletionRatioValue,
- cachePart:
- cacheTokens > 0
- ? `${i18next.t('缓存倍率')} ${cacheRatioValue},`
- : '',
- ratioType: ratioLabel,
- ratio: groupRatio,
- },
- ),
- buildBillingText(
- '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: Math.max(inputTokens - cacheTokens, 0),
- modelRatio: modelRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (Math.max(inputTokens - cacheTokens, 0) / 1000000) *
- inputRatioPrice *
- groupRatio,
- ),
- },
- ),
- cacheTokens > 0
- ? buildBillingText(
- '缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: cacheTokens,
- modelRatio: modelRatioValue,
- cacheRatio: cacheRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (cacheTokens / 1000000) *
- inputRatioPrice *
- cacheRatioValue *
- groupRatio,
- ),
- },
- )
- : null,
- buildBillingText(
- '文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: completionTokens,
- modelRatio: modelRatioValue,
- completionRatio: completionRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (completionTokens / 1000000) *
- inputRatioPrice *
- completionRatioValue *
- groupRatio,
- ),
- },
- ),
- buildBillingText(
- '音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: audioInputTokens,
- modelRatio: modelRatioValue,
- audioRatio: audioRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (audioInputTokens / 1000000) *
- inputRatioPrice *
- audioRatioValue *
- groupRatio,
- ),
- },
- ),
- buildBillingText(
- '音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: audioCompletionTokens,
- modelRatio: modelRatioValue,
- audioRatio: audioRatioValue,
- audioCompletionRatio: audioCompletionRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (audioCompletionTokens / 1000000) *
- inputRatioPrice *
- audioRatioValue *
- audioCompletionRatioValue *
- groupRatio,
- ),
- },
- ),
- buildBillingText(
- '合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}',
- {
- textTotal: renderDisplayAmountFromUsd(textPrice),
- audioTotal: renderDisplayAmountFromUsd(audioPrice),
- total: renderDisplayAmountFromUsd(totalPrice),
- },
- ),
- ]);
- }
- export function renderQuotaWithPrompt(quota, digits) {
- const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
- if (quotaDisplayType !== 'TOKENS') {
- return i18next.t('等价金额:') + renderQuota(quota, digits);
- }
- return '';
- }
- export function renderClaudeModelPrice(opts) {
- const {
- prompt_tokens: inputTokens = 0,
- completion_tokens: completionTokens = 0,
- model_ratio: modelRatio = 0,
- model_price: modelPrice = -1,
- completion_ratio: completionRatio,
- group_ratio: _groupRatio,
- user_group_ratio,
- cache_tokens: cacheTokens = 0,
- cache_ratio: cacheRatio = 1.0,
- cache_creation_tokens: cacheCreationTokens = 0,
- cache_creation_ratio: cacheCreationRatio = 1.0,
- cache_creation_tokens_5m: cacheCreationTokens5m = 0,
- cache_creation_ratio_5m: cacheCreationRatio5m = 1.0,
- cache_creation_tokens_1h: cacheCreationTokens1h = 0,
- cache_creation_ratio_1h: cacheCreationRatio1h = 1.0,
- displayMode = 'price',
- } = opts;
- const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
- _groupRatio,
- user_group_ratio,
- );
- let groupRatio = effectiveGroupRatio;
- // 获取货币配置
- const { symbol, rate } = getCurrencyConfig();
- if (!shouldUseRatioBillingProcess(modelPrice)) {
- if (modelPrice !== -1) {
- return renderBillingArticle([
- buildBillingPriceText('模型价格:{{symbol}}{{price}} / 次', {
- symbol,
- usdAmount: modelPrice,
- rate,
- }),
- buildBillingPriceText(
- '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
- {
- symbol,
- usdAmount: modelPrice,
- rate,
- ratioType: ratioLabel,
- ratio: groupRatio,
- total: formatBillingDisplayPrice(modelPrice * groupRatio, rate),
- },
- ),
- ]);
- }
- if (completionRatio === undefined) {
- completionRatio = 0;
- }
- const inputRatioPrice = modelRatio * 2.0;
- const completionRatioPrice = modelRatio * 2.0 * completionRatio;
- const cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
- const cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
- const cacheCreationRatioPrice5m = modelRatio * 2.0 * cacheCreationRatio5m;
- const cacheCreationRatioPrice1h = modelRatio * 2.0 * cacheCreationRatio1h;
- const hasSplitCacheCreation =
- cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
- const legacyCacheCreationTokens = hasSplitCacheCreation
- ? 0
- : cacheCreationTokens;
- const effectiveInputTokens =
- inputTokens +
- cacheTokens * cacheRatio +
- legacyCacheCreationTokens * cacheCreationRatio +
- cacheCreationTokens5m * cacheCreationRatio5m +
- cacheCreationTokens1h * cacheCreationRatio1h;
- const price =
- (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
- (completionTokens / 1000000) * completionRatioPrice * groupRatio;
- const inputUnitPrice = inputRatioPrice * rate;
- const completionUnitPrice = completionRatioPrice * rate;
- const cacheUnitPrice = cacheRatioPrice * rate;
- const cacheCreationUnitPrice = cacheCreationRatioPrice * rate;
- const cacheCreationUnitPrice5m = cacheCreationRatioPrice5m * rate;
- const cacheCreationUnitPrice1h = cacheCreationRatioPrice1h * rate;
- const cacheCreationUnitPriceTotal =
- cacheCreationUnitPrice5m + cacheCreationUnitPrice1h;
- const shouldShowCache = cacheTokens > 0;
- const shouldShowLegacyCacheCreation =
- !hasSplitCacheCreation && cacheCreationTokens > 0;
- const shouldShowCacheCreation5m =
- hasSplitCacheCreation && cacheCreationTokens5m > 0;
- const shouldShowCacheCreation1h =
- hasSplitCacheCreation && cacheCreationTokens1h > 0;
- const breakdownSegments = [
- i18next.t('提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', {
- input: inputTokens,
- symbol,
- price: inputUnitPrice.toFixed(6),
- }),
- ];
- if (shouldShowCache) {
- breakdownSegments.push(
- i18next.t('缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}', {
- tokens: cacheTokens,
- symbol,
- price: cacheUnitPrice.toFixed(6),
- }),
- );
- }
- if (shouldShowLegacyCacheCreation) {
- breakdownSegments.push(
- i18next.t(
- '缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
- {
- tokens: cacheCreationTokens,
- symbol,
- price: cacheCreationUnitPrice.toFixed(6),
- },
- ),
- );
- }
- if (shouldShowCacheCreation5m) {
- breakdownSegments.push(
- i18next.t(
- '5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
- {
- tokens: cacheCreationTokens5m,
- symbol,
- price: cacheCreationUnitPrice5m.toFixed(6),
- },
- ),
- );
- }
- if (shouldShowCacheCreation1h) {
- breakdownSegments.push(
- i18next.t(
- '1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
- {
- tokens: cacheCreationTokens1h,
- symbol,
- price: cacheCreationUnitPrice1h.toFixed(6),
- },
- ),
- );
- }
- breakdownSegments.push(
- i18next.t(
- '补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}',
- {
- completion: completionTokens,
- symbol,
- price: completionUnitPrice.toFixed(6),
- },
- ),
- );
- const breakdownText = breakdownSegments.join(' + ');
- return renderBillingArticle([
- buildBillingPriceText('输入价格:{{symbol}}{{price}} / 1M tokens', {
- symbol,
- usdAmount: inputRatioPrice,
- rate,
- }),
- buildBillingPriceText('输出价格:{{symbol}}{{price}} / 1M tokens', {
- symbol,
- usdAmount: completionRatioPrice,
- rate,
- }),
- cacheTokens > 0
- ? buildBillingPriceText(
- '缓存读取价格:{{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- usdAmount: cacheRatioPrice,
- rate,
- },
- )
- : null,
- !hasSplitCacheCreation && cacheCreationTokens > 0
- ? buildBillingPriceText(
- '缓存创建价格:{{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- usdAmount: cacheCreationRatioPrice,
- rate,
- },
- )
- : null,
- hasSplitCacheCreation && cacheCreationTokens5m > 0
- ? buildBillingPriceText(
- '5m缓存创建价格:{{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- usdAmount: cacheCreationRatioPrice5m,
- rate,
- },
- )
- : null,
- hasSplitCacheCreation && cacheCreationTokens1h > 0
- ? buildBillingPriceText(
- '1h缓存创建价格:{{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- usdAmount: cacheCreationRatioPrice1h,
- rate,
- },
- )
- : null,
- buildBillingText(
- '{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
- {
- breakdown: breakdownText,
- ratioType: ratioLabel,
- ratio: groupRatio,
- symbol,
- total: formatBillingDisplayPrice(price, rate),
- },
- ),
- ]);
- }
- if (modelPrice !== -1) {
- return i18next.t(
- '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}',
- {
- symbol: symbol,
- price: (modelPrice * rate).toFixed(6),
- ratioType: ratioLabel,
- ratio: groupRatio,
- total: (modelPrice * groupRatio * rate).toFixed(6),
- },
- );
- }
- if (completionRatio === undefined) {
- completionRatio = 0;
- }
- const modelRatioValue = formatRatioValue(modelRatio);
- const completionRatioValue = formatRatioValue(completionRatio);
- const cacheRatioValue = formatRatioValue(cacheRatio);
- const cacheCreationRatioValue = formatRatioValue(cacheCreationRatio);
- const cacheCreationRatio5mValue = formatRatioValue(cacheCreationRatio5m);
- const cacheCreationRatio1hValue = formatRatioValue(cacheCreationRatio1h);
- const inputRatioPrice = modelRatio * 2.0;
- const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
- const hasSplitCacheCreation =
- cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
- const shouldShowCache = cacheTokens > 0;
- const shouldShowLegacyCacheCreation =
- !hasSplitCacheCreation && cacheCreationTokens > 0;
- const shouldShowCacheCreation5m =
- hasSplitCacheCreation && cacheCreationTokens5m > 0;
- const shouldShowCacheCreation1h =
- hasSplitCacheCreation && cacheCreationTokens1h > 0;
- const legacyCacheCreationTokens = hasSplitCacheCreation
- ? 0
- : cacheCreationTokens;
- const effectiveInputTokens =
- inputTokens +
- cacheTokens * cacheRatioValue +
- legacyCacheCreationTokens * cacheCreationRatioValue +
- cacheCreationTokens5m * cacheCreationRatio5mValue +
- cacheCreationTokens1h * cacheCreationRatio1hValue;
- const totalAmount =
- (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
- (completionTokens / 1000000) * completionRatioPrice * groupRatio;
- return renderBillingArticle([
- buildBillingText(
- '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}',
- {
- modelRatio: modelRatioValue,
- completionRatio: completionRatioValue,
- cacheRatio: cacheRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- },
- ),
- hasSplitCacheCreation
- ? buildBillingText(
- '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
- {
- cacheCreationRatio5m: cacheCreationRatio5mValue,
- cacheCreationRatio1h: cacheCreationRatio1hValue,
- },
- )
- : buildBillingText('缓存创建倍率 {{cacheCreationRatio}}', {
- cacheCreationRatio: cacheCreationRatioValue,
- }),
- buildBillingText(
- '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: inputTokens,
- modelRatio: modelRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (inputTokens / 1000000) * inputRatioPrice * groupRatio,
- ),
- },
- ),
- shouldShowCache
- ? buildBillingText(
- '缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: cacheTokens,
- modelRatio: modelRatioValue,
- cacheRatio: cacheRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (cacheTokens / 1000000) *
- inputRatioPrice *
- cacheRatioValue *
- groupRatio,
- ),
- },
- )
- : null,
- shouldShowLegacyCacheCreation
- ? buildBillingText(
- '缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: cacheCreationTokens,
- modelRatio: modelRatioValue,
- cacheCreationRatio: cacheCreationRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (cacheCreationTokens / 1000000) *
- inputRatioPrice *
- cacheCreationRatioValue *
- groupRatio,
- ),
- },
- )
- : null,
- shouldShowCacheCreation5m
- ? buildBillingText(
- '5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: cacheCreationTokens5m,
- modelRatio: modelRatioValue,
- cacheCreationRatio5m: cacheCreationRatio5mValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (cacheCreationTokens5m / 1000000) *
- inputRatioPrice *
- cacheCreationRatio5mValue *
- groupRatio,
- ),
- },
- )
- : null,
- shouldShowCacheCreation1h
- ? buildBillingText(
- '1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: cacheCreationTokens1h,
- modelRatio: modelRatioValue,
- cacheCreationRatio1h: cacheCreationRatio1hValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (cacheCreationTokens1h / 1000000) *
- inputRatioPrice *
- cacheCreationRatio1hValue *
- groupRatio,
- ),
- },
- )
- : null,
- buildBillingText(
- '补全 {{completion}} tokens * 输出倍率 {{completionRatio}}',
- {
- completion: completionTokens,
- completionRatio: completionRatioValue,
- },
- ),
- buildBillingText(
- '输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
- {
- tokens: completionTokens,
- modelRatio: modelRatioValue,
- completionRatio: completionRatioValue,
- ratioType: ratioLabel,
- ratio: groupRatio,
- amount: renderDisplayAmountFromUsd(
- (completionTokens / 1000000) *
- inputRatioPrice *
- completionRatioValue *
- groupRatio,
- ),
- },
- ),
- buildBillingText('合计:{{total}}', {
- total: renderDisplayAmountFromUsd(totalAmount),
- }),
- ]);
- }
- export function renderClaudeLogContent(opts) {
- const {
- model_ratio: modelRatio,
- completion_ratio: completionRatio,
- model_price: modelPrice = -1,
- group_ratio: _groupRatio,
- user_group_ratio,
- cache_ratio: cacheRatio = 1.0,
- cache_creation_ratio: cacheCreationRatio = 1.0,
- cache_creation_tokens_5m: cacheCreationTokens5m = 0,
- cache_creation_ratio_5m: cacheCreationRatio5m = 1.0,
- cache_creation_tokens_1h: cacheCreationTokens1h = 0,
- cache_creation_ratio_1h: cacheCreationRatio1h = 1.0,
- displayMode = 'price',
- } = opts;
- const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
- _groupRatio,
- user_group_ratio,
- );
- let groupRatio = effectiveGroupRatio;
- // 获取货币配置
- const { symbol, rate } = getCurrencyConfig();
- if (isPriceDisplayMode(displayMode, modelPrice)) {
- if (modelPrice !== -1) {
- return joinBillingSummary([
- i18next.t('模型价格 {{symbol}}{{price}} / 次', {
- symbol,
- price: (modelPrice * rate).toFixed(6),
- }),
- getGroupRatioText(groupRatio, user_group_ratio),
- ]);
- }
- const parts = [
- i18next.t('输入价格 {{symbol}}{{price}} / 1M tokens', {
- symbol,
- price: (modelRatio * 2.0 * rate).toFixed(6),
- }),
- i18next.t('输出价格 {{symbol}}{{price}} / 1M tokens', {
- symbol,
- price: (modelRatio * 2.0 * completionRatio * rate).toFixed(6),
- }),
- i18next.t('缓存读取价格 {{symbol}}{{price}} / 1M tokens', {
- symbol,
- price: (modelRatio * 2.0 * cacheRatio * rate).toFixed(6),
- }),
- ];
- const hasSplitCacheCreation =
- cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
- appendPricePart(
- parts,
- hasSplitCacheCreation && cacheCreationTokens5m > 0,
- '5m缓存创建价格 {{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- price: (modelRatio * 2.0 * cacheCreationRatio5m * rate).toFixed(6),
- },
- );
- appendPricePart(
- parts,
- hasSplitCacheCreation && cacheCreationTokens1h > 0,
- '1h缓存创建价格 {{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- price: (modelRatio * 2.0 * cacheCreationRatio1h * rate).toFixed(6),
- },
- );
- appendPricePart(
- parts,
- !hasSplitCacheCreation,
- '缓存创建价格 {{symbol}}{{price}} / 1M tokens',
- {
- symbol,
- price: (modelRatio * 2.0 * cacheCreationRatio * rate).toFixed(6),
- },
- );
- parts.push(getGroupRatioText(groupRatio, user_group_ratio));
- return joinBillingSummary(parts);
- }
- if (modelPrice !== -1) {
- return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', {
- symbol: symbol,
- price: (modelPrice * rate).toFixed(6),
- ratioType: ratioLabel,
- ratio: groupRatio,
- });
- } else {
- const hasSplitCacheCreation =
- cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
- const shouldShowCacheCreation5m =
- hasSplitCacheCreation && cacheCreationTokens5m > 0;
- const shouldShowCacheCreation1h =
- hasSplitCacheCreation && cacheCreationTokens1h > 0;
- let cacheCreationPart = null;
- if (hasSplitCacheCreation) {
- if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
- cacheCreationPart = i18next.t(
- '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
- {
- cacheCreationRatio5m,
- cacheCreationRatio1h,
- },
- );
- } else if (shouldShowCacheCreation5m) {
- cacheCreationPart = i18next.t(
- '缓存创建倍率 5m {{cacheCreationRatio5m}}',
- {
- cacheCreationRatio5m,
- },
- );
- } else if (shouldShowCacheCreation1h) {
- cacheCreationPart = i18next.t(
- '缓存创建倍率 1h {{cacheCreationRatio1h}}',
- {
- cacheCreationRatio1h,
- },
- );
- }
- }
- if (!cacheCreationPart) {
- cacheCreationPart = i18next.t('缓存创建倍率 {{cacheCreationRatio}}', {
- cacheCreationRatio,
- });
- }
- const parts = [
- i18next.t('模型倍率 {{modelRatio}}', { modelRatio }),
- i18next.t('输出倍率 {{completionRatio}}', { completionRatio }),
- i18next.t('缓存倍率 {{cacheRatio}}', { cacheRatio }),
- cacheCreationPart,
- i18next.t('{{ratioType}} {{ratio}}', {
- ratioType: ratioLabel,
- ratio: groupRatio,
- }),
- ];
- return parts.join(',');
- }
- }
- // 已统一至 renderModelPriceSimple,若仍有遗留引用,请改为传入 provider='claude'
- /**
- * rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
- * 仅在流式渲染阶段使用,避免已渲染文字重复动画。
- */
- export function rehypeSplitWordsIntoSpans(options = {}) {
- const { previousContentLength = 0 } = options;
- return (tree) => {
- let currentCharCount = 0; // 当前已处理的字符数
- visit(tree, 'element', (node) => {
- if (
- ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(
- node.tagName,
- ) &&
- node.children
- ) {
- const newChildren = [];
- node.children.forEach((child) => {
- if (child.type === 'text') {
- try {
- // 使用 Intl.Segmenter 精准拆分中英文及标点
- const segmenter = new Intl.Segmenter('zh', {
- granularity: 'word',
- });
- const segments = segmenter.segment(child.value);
- Array.from(segments)
- .map((seg) => seg.segment)
- .filter(Boolean)
- .forEach((word) => {
- const wordStartPos = currentCharCount;
- const wordEndPos = currentCharCount + word.length;
- // 判断这个词是否是新增的(在 previousContentLength 之后)
- const isNewContent = wordStartPos >= previousContentLength;
- newChildren.push({
- type: 'element',
- tagName: 'span',
- properties: {
- className: isNewContent ? ['animate-fade-in'] : [],
- },
- children: [{ type: 'text', value: word }],
- });
- currentCharCount = wordEndPos;
- });
- } catch (_) {
- // Fallback:如果浏览器不支持 Segmenter
- const textStartPos = currentCharCount;
- const isNewContent = textStartPos >= previousContentLength;
- if (isNewContent) {
- // 新内容,添加动画
- newChildren.push({
- type: 'element',
- tagName: 'span',
- properties: {
- className: ['animate-fade-in'],
- },
- children: [{ type: 'text', value: child.value }],
- });
- } else {
- // 旧内容,不添加动画
- newChildren.push(child);
- }
- currentCharCount += child.value.length;
- }
- } else {
- newChildren.push(child);
- }
- });
- node.children = newChildren;
- }
- });
- };
- }
|