render.jsx 100 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import i18next from 'i18next';
  16. import { Modal, Tag, Typography, Avatar } from '@douyinfe/semi-ui';
  17. import { copy, showSuccess } from './utils';
  18. import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile';
  19. import {
  20. BILLING_VARS,
  21. BILLING_VAR_KEY_TO_FIELD,
  22. BILLING_VAR_REGEX,
  23. } from '../constants';
  24. import { visit } from 'unist-util-visit';
  25. import * as LobeIcons from '@lobehub/icons';
  26. import {
  27. OpenAI,
  28. Claude,
  29. Gemini,
  30. Moonshot,
  31. Zhipu,
  32. Qwen,
  33. DeepSeek,
  34. Minimax,
  35. Wenxin,
  36. Spark,
  37. Midjourney,
  38. Hunyuan,
  39. Cohere,
  40. Cloudflare,
  41. Ai360,
  42. Yi,
  43. Jina,
  44. Mistral,
  45. XAI,
  46. Ollama,
  47. Doubao,
  48. Suno,
  49. Xinference,
  50. OpenRouter,
  51. Dify,
  52. Coze,
  53. SiliconCloud,
  54. FastGPT,
  55. Kling,
  56. Jimeng,
  57. Perplexity,
  58. Replicate,
  59. } from '@lobehub/icons';
  60. import {
  61. LayoutDashboard,
  62. TerminalSquare,
  63. MessageSquare,
  64. Key,
  65. BarChart3,
  66. Image as ImageIcon,
  67. CheckSquare,
  68. CreditCard,
  69. Layers,
  70. Gift,
  71. User,
  72. Settings,
  73. CircleUser,
  74. Package,
  75. Server,
  76. CalendarClock,
  77. } from 'lucide-react';
  78. import {
  79. SiAtlassian,
  80. SiAuth0,
  81. SiAuthentik,
  82. SiBitbucket,
  83. SiDiscord,
  84. SiDropbox,
  85. SiFacebook,
  86. SiGitea,
  87. SiGithub,
  88. SiGitlab,
  89. SiGoogle,
  90. SiKeycloak,
  91. SiLinkedin,
  92. SiNextcloud,
  93. SiNotion,
  94. SiOkta,
  95. SiOpenid,
  96. SiReddit,
  97. SiSlack,
  98. SiTelegram,
  99. SiTwitch,
  100. SiWechat,
  101. SiX,
  102. } from 'react-icons/si';
  103. // 获取侧边栏Lucide图标组件
  104. export function getLucideIcon(key, selected = false) {
  105. const size = 16;
  106. const strokeWidth = 2;
  107. const SELECTED_COLOR = 'var(--semi-color-primary)';
  108. const iconColor = selected ? SELECTED_COLOR : 'currentColor';
  109. const commonProps = {
  110. size,
  111. strokeWidth,
  112. className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`,
  113. };
  114. // 根据不同的key返回不同的图标
  115. switch (key) {
  116. case 'detail':
  117. return <LayoutDashboard {...commonProps} color={iconColor} />;
  118. case 'playground':
  119. return <TerminalSquare {...commonProps} color={iconColor} />;
  120. case 'chat':
  121. return <MessageSquare {...commonProps} color={iconColor} />;
  122. case 'token':
  123. return <Key {...commonProps} color={iconColor} />;
  124. case 'log':
  125. return <BarChart3 {...commonProps} color={iconColor} />;
  126. case 'midjourney':
  127. return <ImageIcon {...commonProps} color={iconColor} />;
  128. case 'task':
  129. return <CheckSquare {...commonProps} color={iconColor} />;
  130. case 'topup':
  131. return <CreditCard {...commonProps} color={iconColor} />;
  132. case 'channel':
  133. return <Layers {...commonProps} color={iconColor} />;
  134. case 'redemption':
  135. return <Gift {...commonProps} color={iconColor} />;
  136. case 'user':
  137. case 'personal':
  138. return <User {...commonProps} color={iconColor} />;
  139. case 'models':
  140. return <Package {...commonProps} color={iconColor} />;
  141. case 'deployment':
  142. return <Server {...commonProps} color={iconColor} />;
  143. case 'subscription':
  144. return <CalendarClock {...commonProps} color={iconColor} />;
  145. case 'setting':
  146. return <Settings {...commonProps} color={iconColor} />;
  147. default:
  148. return <CircleUser {...commonProps} color={iconColor} />;
  149. }
  150. }
  151. // 获取模型分类
  152. export const getModelCategories = (() => {
  153. let categoriesCache = null;
  154. let lastLocale = null;
  155. return (t) => {
  156. const currentLocale = i18next.language;
  157. if (categoriesCache && lastLocale === currentLocale) {
  158. return categoriesCache;
  159. }
  160. categoriesCache = {
  161. all: {
  162. label: t('全部模型'),
  163. icon: null,
  164. filter: () => true,
  165. },
  166. openai: {
  167. label: 'OpenAI',
  168. icon: <OpenAI />,
  169. filter: (model) =>
  170. model.model_name.toLowerCase().includes('gpt') ||
  171. model.model_name.toLowerCase().includes('dall-e') ||
  172. model.model_name.toLowerCase().includes('whisper') ||
  173. model.model_name.toLowerCase().includes('tts-1') ||
  174. model.model_name.toLowerCase().includes('text-embedding-3') ||
  175. model.model_name.toLowerCase().includes('text-moderation') ||
  176. model.model_name.toLowerCase().includes('babbage') ||
  177. model.model_name.toLowerCase().includes('davinci') ||
  178. model.model_name.toLowerCase().includes('curie') ||
  179. model.model_name.toLowerCase().includes('ada') ||
  180. model.model_name.toLowerCase().includes('o1') ||
  181. model.model_name.toLowerCase().includes('o3') ||
  182. model.model_name.toLowerCase().includes('o4'),
  183. },
  184. anthropic: {
  185. label: 'Anthropic',
  186. icon: <Claude.Color />,
  187. filter: (model) => model.model_name.toLowerCase().includes('claude'),
  188. },
  189. gemini: {
  190. label: 'Gemini',
  191. icon: <Gemini.Color />,
  192. filter: (model) =>
  193. model.model_name.toLowerCase().includes('gemini') ||
  194. model.model_name.toLowerCase().includes('gemma') ||
  195. model.model_name.toLowerCase().includes('learnlm') ||
  196. model.model_name.toLowerCase().startsWith('embedding-') ||
  197. model.model_name.toLowerCase().includes('text-embedding-004') ||
  198. model.model_name.toLowerCase().includes('imagen-4') ||
  199. model.model_name.toLowerCase().includes('veo-') ||
  200. model.model_name.toLowerCase().includes('aqa'),
  201. },
  202. moonshot: {
  203. label: 'Moonshot',
  204. icon: <Moonshot />,
  205. filter: (model) =>
  206. model.model_name.toLowerCase().includes('moonshot') ||
  207. model.model_name.toLowerCase().includes('kimi'),
  208. },
  209. zhipu: {
  210. label: t('智谱'),
  211. icon: <Zhipu.Color />,
  212. filter: (model) =>
  213. model.model_name.toLowerCase().includes('chatglm') ||
  214. model.model_name.toLowerCase().includes('glm-') ||
  215. model.model_name.toLowerCase().includes('cogview') ||
  216. model.model_name.toLowerCase().includes('cogvideo'),
  217. },
  218. qwen: {
  219. label: t('通义千问'),
  220. icon: <Qwen.Color />,
  221. filter: (model) => model.model_name.toLowerCase().includes('qwen'),
  222. },
  223. deepseek: {
  224. label: 'DeepSeek',
  225. icon: <DeepSeek.Color />,
  226. filter: (model) => model.model_name.toLowerCase().includes('deepseek'),
  227. },
  228. minimax: {
  229. label: 'MiniMax',
  230. icon: <Minimax.Color />,
  231. filter: (model) =>
  232. model.model_name.toLowerCase().includes('abab') ||
  233. model.model_name.toLowerCase().includes('minimax'),
  234. },
  235. baidu: {
  236. label: t('文心一言'),
  237. icon: <Wenxin.Color />,
  238. filter: (model) => model.model_name.toLowerCase().includes('ernie'),
  239. },
  240. xunfei: {
  241. label: t('讯飞星火'),
  242. icon: <Spark.Color />,
  243. filter: (model) => model.model_name.toLowerCase().includes('spark'),
  244. },
  245. midjourney: {
  246. label: 'Midjourney',
  247. icon: <Midjourney />,
  248. filter: (model) => model.model_name.toLowerCase().includes('mj_'),
  249. },
  250. tencent: {
  251. label: t('腾讯混元'),
  252. icon: <Hunyuan.Color />,
  253. filter: (model) => model.model_name.toLowerCase().includes('hunyuan'),
  254. },
  255. cohere: {
  256. label: 'Cohere',
  257. icon: <Cohere.Color />,
  258. filter: (model) =>
  259. model.model_name.toLowerCase().includes('command') ||
  260. model.model_name.toLowerCase().includes('c4ai-') ||
  261. model.model_name.toLowerCase().includes('embed-'),
  262. },
  263. cloudflare: {
  264. label: 'Cloudflare',
  265. icon: <Cloudflare.Color />,
  266. filter: (model) => model.model_name.toLowerCase().includes('@cf/'),
  267. },
  268. ai360: {
  269. label: t('360智脑'),
  270. icon: <Ai360.Color />,
  271. filter: (model) => model.model_name.toLowerCase().includes('360'),
  272. },
  273. jina: {
  274. label: 'Jina',
  275. icon: <Jina />,
  276. filter: (model) => model.model_name.toLowerCase().includes('jina'),
  277. },
  278. mistral: {
  279. label: 'Mistral AI',
  280. icon: <Mistral.Color />,
  281. filter: (model) =>
  282. model.model_name.toLowerCase().includes('mistral') ||
  283. model.model_name.toLowerCase().includes('codestral') ||
  284. model.model_name.toLowerCase().includes('pixtral') ||
  285. model.model_name.toLowerCase().includes('voxtral') ||
  286. model.model_name.toLowerCase().includes('magistral'),
  287. },
  288. xai: {
  289. label: 'xAI',
  290. icon: <XAI />,
  291. filter: (model) => model.model_name.toLowerCase().includes('grok'),
  292. },
  293. llama: {
  294. label: 'Llama',
  295. icon: <Ollama />,
  296. filter: (model) => model.model_name.toLowerCase().includes('llama'),
  297. },
  298. doubao: {
  299. label: t('豆包'),
  300. icon: <Doubao.Color />,
  301. filter: (model) => model.model_name.toLowerCase().includes('doubao'),
  302. },
  303. yi: {
  304. label: t('零一万物'),
  305. icon: <Yi.Color />,
  306. filter: (model) => model.model_name.toLowerCase().includes('yi'),
  307. },
  308. };
  309. lastLocale = currentLocale;
  310. return categoriesCache;
  311. };
  312. })();
  313. /**
  314. * 根据渠道类型返回对应的厂商图标
  315. * @param {number} channelType - 渠道类型值
  316. * @returns {JSX.Element|null} - 对应的厂商图标组件
  317. */
  318. export function getChannelIcon(channelType) {
  319. const iconSize = 14;
  320. switch (channelType) {
  321. case 1: // OpenAI
  322. case 3: // Azure OpenAI
  323. case 57: // Codex
  324. return <OpenAI size={iconSize} />;
  325. case 2: // Midjourney Proxy
  326. case 5: // Midjourney Proxy Plus
  327. return <Midjourney size={iconSize} />;
  328. case 36: // Suno API
  329. return <Suno size={iconSize} />;
  330. case 4: // Ollama
  331. return <Ollama size={iconSize} />;
  332. case 14: // Anthropic Claude
  333. case 33: // AWS Claude
  334. return <Claude.Color size={iconSize} />;
  335. case 41: // Vertex AI
  336. return <Gemini.Color size={iconSize} />;
  337. case 34: // Cohere
  338. return <Cohere.Color size={iconSize} />;
  339. case 39: // Cloudflare
  340. return <Cloudflare.Color size={iconSize} />;
  341. case 43: // DeepSeek
  342. return <DeepSeek.Color size={iconSize} />;
  343. case 15: // 百度文心千帆
  344. case 46: // 百度文心千帆V2
  345. return <Wenxin.Color size={iconSize} />;
  346. case 17: // 阿里通义千问
  347. return <Qwen.Color size={iconSize} />;
  348. case 18: // 讯飞星火认知
  349. return <Spark.Color size={iconSize} />;
  350. case 16: // 智谱 ChatGLM
  351. case 26: // 智谱 GLM-4V
  352. return <Zhipu.Color size={iconSize} />;
  353. case 24: // Google Gemini
  354. case 11: // Google PaLM2
  355. return <Gemini.Color size={iconSize} />;
  356. case 47: // Xinference
  357. return <Xinference.Color size={iconSize} />;
  358. case 25: // Moonshot
  359. return <Moonshot size={iconSize} />;
  360. case 27: // Perplexity
  361. return <Perplexity.Color size={iconSize} />;
  362. case 20: // OpenRouter
  363. return <OpenRouter size={iconSize} />;
  364. case 19: // 360 智脑
  365. return <Ai360.Color size={iconSize} />;
  366. case 23: // 腾讯混元
  367. return <Hunyuan.Color size={iconSize} />;
  368. case 31: // 零一万物
  369. return <Yi.Color size={iconSize} />;
  370. case 35: // MiniMax
  371. return <Minimax.Color size={iconSize} />;
  372. case 37: // Dify
  373. return <Dify.Color size={iconSize} />;
  374. case 38: // Jina
  375. return <Jina size={iconSize} />;
  376. case 40: // SiliconCloud
  377. return <SiliconCloud.Color size={iconSize} />;
  378. case 42: // Mistral AI
  379. return <Mistral.Color size={iconSize} />;
  380. case 45: // 字节火山方舟、豆包通用
  381. return <Doubao.Color size={iconSize} />;
  382. case 48: // xAI
  383. return <XAI size={iconSize} />;
  384. case 49: // Coze
  385. return <Coze size={iconSize} />;
  386. case 50: // 可灵 Kling
  387. return <Kling.Color size={iconSize} />;
  388. case 51: // 即梦 Jimeng
  389. return <Jimeng.Color size={iconSize} />;
  390. case 54: // 豆包视频 Doubao Video
  391. return <Doubao.Color size={iconSize} />;
  392. case 56: // Replicate
  393. return <Replicate size={iconSize} />;
  394. case 8: // 自定义渠道
  395. case 22: // 知识库:FastGPT
  396. return <FastGPT.Color size={iconSize} />;
  397. case 21: // 知识库:AI Proxy
  398. case 44: // 嵌入模型:MokaAI M3E
  399. default:
  400. return null; // 未知类型或自定义渠道不显示图标
  401. }
  402. }
  403. /**
  404. * 根据图标名称动态获取 LobeHub 图标组件
  405. * 支持:
  406. * - 基础:"OpenAI"、"OpenAI.Color" 等
  407. * - 额外属性(点号链式):"OpenAI.Avatar.type={'platform'}"、"OpenRouter.Avatar.shape={'square'}"
  408. * - 继续兼容第二参数 size;若字符串里有 size=,以字符串为准
  409. * @param {string} iconName - 图标名称/描述
  410. * @param {number} size - 图标大小,默认为 14
  411. * @returns {JSX.Element} - 对应的图标组件或 Avatar
  412. */
  413. export function getLobeHubIcon(iconName, size = 14) {
  414. if (typeof iconName === 'string') iconName = iconName.trim();
  415. // 如果没有图标名称,返回 Avatar
  416. if (!iconName) {
  417. return <Avatar size='extra-extra-small'>?</Avatar>;
  418. }
  419. // 解析组件路径与点号链式属性
  420. const segments = String(iconName).split('.');
  421. const baseKey = segments[0];
  422. const BaseIcon = LobeIcons[baseKey];
  423. let IconComponent = undefined;
  424. let propStartIndex = 1;
  425. if (BaseIcon && segments.length > 1 && BaseIcon[segments[1]]) {
  426. IconComponent = BaseIcon[segments[1]];
  427. propStartIndex = 2;
  428. } else {
  429. IconComponent = LobeIcons[baseKey];
  430. propStartIndex = 1;
  431. }
  432. // 失败兜底
  433. if (
  434. !IconComponent ||
  435. (typeof IconComponent !== 'function' && typeof IconComponent !== 'object')
  436. ) {
  437. const firstLetter = String(iconName).charAt(0).toUpperCase();
  438. return <Avatar size='extra-extra-small'>{firstLetter}</Avatar>;
  439. }
  440. // 解析点号链式属性,形如:key={...}、key='...'、key="..."、key=123、key、key=true/false
  441. const props = {};
  442. const parseValue = (raw) => {
  443. if (raw == null) return true;
  444. let v = String(raw).trim();
  445. // 去除一层花括号包裹
  446. if (v.startsWith('{') && v.endsWith('}')) {
  447. v = v.slice(1, -1).trim();
  448. }
  449. // 去除引号
  450. if (
  451. (v.startsWith('"') && v.endsWith('"')) ||
  452. (v.startsWith("'") && v.endsWith("'"))
  453. ) {
  454. return v.slice(1, -1);
  455. }
  456. // 布尔
  457. if (v === 'true') return true;
  458. if (v === 'false') return false;
  459. // 数字
  460. if (/^-?\d+(?:\.\d+)?$/.test(v)) return Number(v);
  461. // 其他原样返回字符串
  462. return v;
  463. };
  464. for (let i = propStartIndex; i < segments.length; i++) {
  465. const seg = segments[i];
  466. if (!seg) continue;
  467. const eqIdx = seg.indexOf('=');
  468. if (eqIdx === -1) {
  469. props[seg.trim()] = true;
  470. continue;
  471. }
  472. const key = seg.slice(0, eqIdx).trim();
  473. const valRaw = seg.slice(eqIdx + 1).trim();
  474. props[key] = parseValue(valRaw);
  475. }
  476. // 兼容第二参数 size,若字符串中未显式指定 size,则使用函数入参
  477. if (props.size == null && size != null) props.size = size;
  478. return <IconComponent {...props} />;
  479. }
  480. const oauthProviderIconMap = {
  481. github: SiGithub,
  482. gitlab: SiGitlab,
  483. gitea: SiGitea,
  484. google: SiGoogle,
  485. discord: SiDiscord,
  486. facebook: SiFacebook,
  487. linkedin: SiLinkedin,
  488. x: SiX,
  489. twitter: SiX,
  490. slack: SiSlack,
  491. telegram: SiTelegram,
  492. wechat: SiWechat,
  493. keycloak: SiKeycloak,
  494. nextcloud: SiNextcloud,
  495. authentik: SiAuthentik,
  496. openid: SiOpenid,
  497. okta: SiOkta,
  498. auth0: SiAuth0,
  499. atlassian: SiAtlassian,
  500. bitbucket: SiBitbucket,
  501. notion: SiNotion,
  502. twitch: SiTwitch,
  503. reddit: SiReddit,
  504. dropbox: SiDropbox,
  505. };
  506. function isHttpUrl(value) {
  507. return /^https?:\/\//i.test(value || '');
  508. }
  509. function isSimpleEmoji(value) {
  510. if (!value) return false;
  511. const trimmed = String(value).trim();
  512. return trimmed.length > 0 && trimmed.length <= 4 && !isHttpUrl(trimmed);
  513. }
  514. function normalizeOAuthIconKey(raw) {
  515. return raw
  516. .trim()
  517. .toLowerCase()
  518. .replace(/^ri:/, '')
  519. .replace(/^react-icons:/, '')
  520. .replace(/^si:/, '');
  521. }
  522. /**
  523. * Render custom OAuth provider icon with react-icons or URL/emoji fallback.
  524. * Supported formats:
  525. * - react-icons simple key: github / gitlab / google / keycloak
  526. * - prefixed key: ri:github / si:github
  527. * - full URL image: https://example.com/logo.png
  528. * - emoji: 🐱
  529. */
  530. export function getOAuthProviderIcon(iconName, size = 20) {
  531. const raw = String(iconName || '').trim();
  532. const iconSize = Number(size) > 0 ? Number(size) : 20;
  533. if (!raw) {
  534. return <Layers size={iconSize} color='var(--semi-color-text-2)' />;
  535. }
  536. if (isHttpUrl(raw)) {
  537. return (
  538. <img
  539. src={raw}
  540. alt='provider icon'
  541. width={iconSize}
  542. height={iconSize}
  543. style={{ borderRadius: 4, objectFit: 'cover' }}
  544. />
  545. );
  546. }
  547. if (isSimpleEmoji(raw)) {
  548. return (
  549. <span
  550. style={{
  551. width: iconSize,
  552. height: iconSize,
  553. lineHeight: `${iconSize}px`,
  554. textAlign: 'center',
  555. display: 'inline-block',
  556. fontSize: Math.max(Math.floor(iconSize * 0.8), 14),
  557. }}
  558. >
  559. {raw}
  560. </span>
  561. );
  562. }
  563. const key = normalizeOAuthIconKey(raw);
  564. const IconComp = oauthProviderIconMap[key];
  565. if (IconComp) {
  566. return <IconComp size={iconSize} />;
  567. }
  568. return (
  569. <Avatar size='extra-extra-small'>{raw.charAt(0).toUpperCase()}</Avatar>
  570. );
  571. }
  572. // 颜色列表
  573. const colors = [
  574. 'amber',
  575. 'blue',
  576. 'cyan',
  577. 'green',
  578. 'grey',
  579. 'indigo',
  580. 'light-blue',
  581. 'lime',
  582. 'orange',
  583. 'pink',
  584. 'purple',
  585. 'red',
  586. 'teal',
  587. 'violet',
  588. 'yellow',
  589. ];
  590. // 基础10色色板 (N ≤ 10)
  591. const baseColors = [
  592. '#1664FF', // 主色
  593. '#1AC6FF',
  594. '#FF8A00',
  595. '#3CC780',
  596. '#7442D4',
  597. '#FFC400',
  598. '#304D77',
  599. '#B48DEB',
  600. '#009488',
  601. '#FF7DDA',
  602. ];
  603. // 扩展20色色板 (10 < N ≤ 20)
  604. const extendedColors = [
  605. '#1664FF',
  606. '#B2CFFF',
  607. '#1AC6FF',
  608. '#94EFFF',
  609. '#FF8A00',
  610. '#FFCE7A',
  611. '#3CC780',
  612. '#B9EDCD',
  613. '#7442D4',
  614. '#DDC5FA',
  615. '#FFC400',
  616. '#FAE878',
  617. '#304D77',
  618. '#8B959E',
  619. '#B48DEB',
  620. '#EFE3FF',
  621. '#009488',
  622. '#59BAA8',
  623. '#FF7DDA',
  624. '#FFCFEE',
  625. ];
  626. // 模型颜色映射
  627. export const modelColorMap = {
  628. 'dall-e': 'rgb(147,112,219)', // 深紫色
  629. // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
  630. 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
  631. 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
  632. // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
  633. 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
  634. 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
  635. 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
  636. 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
  637. 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
  638. 'gpt-4': 'rgb(135,206,235)', // 天蓝色
  639. // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
  640. 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
  641. 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
  642. 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
  643. 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
  644. 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
  645. // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
  646. 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
  647. 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
  648. 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
  649. 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
  650. 'text-ada-001': 'rgb(255,192,203)', // 粉红色
  651. 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
  652. 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
  653. // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
  654. 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
  655. 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
  656. 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
  657. 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
  658. 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
  659. 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
  660. 'tts-1': 'rgb(255,140,0)', // 深橙色
  661. 'tts-1-1106': 'rgb(255,165,0)', // 橙色
  662. 'tts-1-hd': 'rgb(255,215,0)', // 金色
  663. 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
  664. 'whisper-1': 'rgb(245,245,220)', // 米色
  665. 'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
  666. 'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
  667. 'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
  668. };
  669. export function modelToColor(modelName) {
  670. // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
  671. if (modelColorMap[modelName]) {
  672. return modelColorMap[modelName];
  673. }
  674. // 2. 生成一个稳定的数字作为索引
  675. let hash = 0;
  676. for (let i = 0; i < modelName.length; i++) {
  677. hash = (hash << 5) - hash + modelName.charCodeAt(i);
  678. hash = hash & hash; // Convert to 32-bit integer
  679. }
  680. hash = Math.abs(hash);
  681. // 3. 根据模型名称长度选择不同的色板
  682. const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
  683. // 4. 使用hash值选择颜色
  684. const index = hash % colorPalette.length;
  685. return colorPalette[index];
  686. }
  687. export function stringToColor(str) {
  688. let sum = 0;
  689. for (let i = 0; i < str.length; i++) {
  690. sum += str.charCodeAt(i);
  691. }
  692. let i = sum % colors.length;
  693. return colors[i];
  694. }
  695. // 渲染带有模型图标的标签
  696. export function renderModelTag(modelName, options = {}) {
  697. const {
  698. color,
  699. size = 'default',
  700. shape = 'circle',
  701. onClick,
  702. suffixIcon,
  703. } = options;
  704. const categories = getModelCategories(i18next.t);
  705. let icon = null;
  706. for (const [key, category] of Object.entries(categories)) {
  707. if (key !== 'all' && category.filter({ model_name: modelName })) {
  708. icon = category.icon;
  709. break;
  710. }
  711. }
  712. return (
  713. <Tag
  714. color={color || stringToColor(modelName)}
  715. prefixIcon={icon}
  716. suffixIcon={suffixIcon}
  717. size={size}
  718. shape={shape}
  719. onClick={onClick}
  720. >
  721. {modelName}
  722. </Tag>
  723. );
  724. }
  725. export function renderText(text, limit) {
  726. if (text.length > limit) {
  727. return text.slice(0, limit - 3) + '...';
  728. }
  729. return text;
  730. }
  731. /**
  732. * Render group tags based on the input group string
  733. * @param {string} group - The input group string
  734. * @returns {JSX.Element} - The rendered group tags
  735. */
  736. export function renderGroup(group) {
  737. if (group === '') {
  738. return (
  739. <Tag key='default' color='white' shape='circle'>
  740. {i18next.t('用户分组')}
  741. </Tag>
  742. );
  743. }
  744. const tagColors = {
  745. vip: 'yellow',
  746. pro: 'yellow',
  747. svip: 'red',
  748. premium: 'red',
  749. };
  750. const groups = group.split(',').sort();
  751. return (
  752. <span key={group}>
  753. {groups.map((group) => (
  754. <Tag
  755. color={tagColors[group] || stringToColor(group)}
  756. key={group}
  757. shape='circle'
  758. onClick={async (event) => {
  759. event.stopPropagation();
  760. if (await copy(group)) {
  761. showSuccess(i18next.t('已复制:') + group);
  762. } else {
  763. Modal.error({
  764. title: i18next.t('无法复制到剪贴板,请手动复制'),
  765. content: group,
  766. });
  767. }
  768. }}
  769. >
  770. {group}
  771. </Tag>
  772. ))}
  773. </span>
  774. );
  775. }
  776. export function renderRatio(ratio) {
  777. let color = 'green';
  778. if (ratio > 5) {
  779. color = 'red';
  780. } else if (ratio > 3) {
  781. color = 'orange';
  782. } else if (ratio > 1) {
  783. color = 'blue';
  784. }
  785. return (
  786. <Tag color={color}>
  787. {ratio}x {i18next.t('倍率')}
  788. </Tag>
  789. );
  790. }
  791. const measureTextWidth = (
  792. text,
  793. style = {
  794. fontSize: '14px',
  795. fontFamily:
  796. '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
  797. },
  798. containerWidth,
  799. ) => {
  800. const span = document.createElement('span');
  801. span.style.visibility = 'hidden';
  802. span.style.position = 'absolute';
  803. span.style.whiteSpace = 'nowrap';
  804. span.style.fontSize = style.fontSize;
  805. span.style.fontFamily = style.fontFamily;
  806. span.textContent = text;
  807. document.body.appendChild(span);
  808. const width = span.offsetWidth;
  809. document.body.removeChild(span);
  810. return width;
  811. };
  812. export function truncateText(text, maxWidth = 200) {
  813. const isMobileScreen = window.matchMedia(
  814. `(max-width: ${MOBILE_BREAKPOINT - 1}px)`,
  815. ).matches;
  816. if (!isMobileScreen) {
  817. return text;
  818. }
  819. if (!text) return text;
  820. try {
  821. // Handle percentage-based maxWidth
  822. let actualMaxWidth = maxWidth;
  823. if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) {
  824. const percentage = parseFloat(maxWidth) / 100;
  825. // Use window width as fallback container width
  826. actualMaxWidth = window.innerWidth * percentage;
  827. }
  828. const width = measureTextWidth(text);
  829. if (width <= actualMaxWidth) return text;
  830. let left = 0;
  831. let right = text.length;
  832. let result = text;
  833. while (left <= right) {
  834. const mid = Math.floor((left + right) / 2);
  835. const truncated = text.slice(0, mid) + '...';
  836. const currentWidth = measureTextWidth(truncated);
  837. if (currentWidth <= actualMaxWidth) {
  838. result = truncated;
  839. left = mid + 1;
  840. } else {
  841. right = mid - 1;
  842. }
  843. }
  844. return result;
  845. } catch (error) {
  846. console.warn(
  847. 'Text measurement failed, falling back to character count',
  848. error,
  849. );
  850. if (text.length > 20) {
  851. return text.slice(0, 17) + '...';
  852. }
  853. return text;
  854. }
  855. }
  856. export const renderGroupOption = (item) => {
  857. const {
  858. disabled,
  859. selected,
  860. label,
  861. value,
  862. focused,
  863. className,
  864. style,
  865. onMouseEnter,
  866. onClick,
  867. empty,
  868. emptyContent,
  869. ...rest
  870. } = item;
  871. const baseStyle = {
  872. display: 'flex',
  873. justifyContent: 'space-between',
  874. alignItems: 'center',
  875. padding: '8px 16px',
  876. cursor: disabled ? 'not-allowed' : 'pointer',
  877. backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
  878. opacity: disabled ? 0.5 : 1,
  879. ...(selected && {
  880. backgroundColor: 'var(--semi-color-primary-light-default)',
  881. }),
  882. '&:hover': {
  883. backgroundColor: !disabled && 'var(--semi-color-fill-1)',
  884. },
  885. };
  886. const handleClick = () => {
  887. if (!disabled && onClick) {
  888. onClick();
  889. }
  890. };
  891. const handleMouseEnter = (e) => {
  892. if (!disabled && onMouseEnter) {
  893. onMouseEnter(e);
  894. }
  895. };
  896. return (
  897. <div
  898. style={baseStyle}
  899. onClick={handleClick}
  900. onMouseEnter={handleMouseEnter}
  901. >
  902. <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
  903. <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
  904. {value}
  905. </Typography.Text>
  906. <Typography.Text type='secondary' size='small'>
  907. {label}
  908. </Typography.Text>
  909. </div>
  910. {item.ratio && renderRatio(item.ratio)}
  911. </div>
  912. );
  913. };
  914. export function renderNumber(num) {
  915. if (num >= 1000000000) {
  916. return (num / 1000000000).toFixed(1) + 'B';
  917. } else if (num >= 1000000) {
  918. return (num / 1000000).toFixed(1) + 'M';
  919. } else if (num >= 10000) {
  920. return (num / 1000).toFixed(1) + 'k';
  921. } else {
  922. return num;
  923. }
  924. }
  925. export function renderQuotaNumberWithDigit(num, digits = 2) {
  926. if (typeof num !== 'number' || isNaN(num)) {
  927. return 0;
  928. }
  929. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  930. num = num.toFixed(digits);
  931. if (quotaDisplayType === 'CNY') {
  932. return '¥' + num;
  933. } else if (quotaDisplayType === 'USD') {
  934. return '$' + num;
  935. } else if (quotaDisplayType === 'CUSTOM') {
  936. const statusStr = localStorage.getItem('status');
  937. let symbol = '¤';
  938. try {
  939. if (statusStr) {
  940. const s = JSON.parse(statusStr);
  941. symbol = s?.custom_currency_symbol || symbol;
  942. }
  943. } catch (e) {}
  944. return symbol + num;
  945. } else {
  946. return num;
  947. }
  948. }
  949. export function renderNumberWithPoint(num) {
  950. if (num === undefined) return '';
  951. num = num.toFixed(2);
  952. if (num >= 100000) {
  953. // Convert number to string to manipulate it
  954. let numStr = num.toString();
  955. // Find the position of the decimal point
  956. let decimalPointIndex = numStr.indexOf('.');
  957. let wholePart = numStr;
  958. let decimalPart = '';
  959. // If there is a decimal point, split the number into whole and decimal parts
  960. if (decimalPointIndex !== -1) {
  961. wholePart = numStr.slice(0, decimalPointIndex);
  962. decimalPart = numStr.slice(decimalPointIndex);
  963. }
  964. // Take the first two and last two digits of the whole number part
  965. let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
  966. // Return the formatted number
  967. return shortenedWholePart + decimalPart;
  968. }
  969. // If the number is less than 100,000, return it unmodified
  970. return num;
  971. }
  972. export function getQuotaPerUnit() {
  973. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  974. quotaPerUnit = parseFloat(quotaPerUnit);
  975. return quotaPerUnit;
  976. }
  977. export function renderUnitWithQuota(quota) {
  978. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  979. quotaPerUnit = parseFloat(quotaPerUnit);
  980. quota = parseFloat(quota);
  981. return quotaPerUnit * quota;
  982. }
  983. export function getQuotaWithUnit(quota, digits = 6) {
  984. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  985. quotaPerUnit = parseFloat(quotaPerUnit);
  986. return (quota / quotaPerUnit).toFixed(digits);
  987. }
  988. export function renderQuotaWithAmount(amount) {
  989. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  990. if (quotaDisplayType === 'TOKENS') {
  991. return renderNumber(renderUnitWithQuota(amount));
  992. }
  993. const numericAmount = Number(amount);
  994. const formattedAmount = Number.isFinite(numericAmount)
  995. ? numericAmount.toFixed(2)
  996. : amount;
  997. if (quotaDisplayType === 'CNY') {
  998. return '¥' + formattedAmount;
  999. } else if (quotaDisplayType === 'CUSTOM') {
  1000. const statusStr = localStorage.getItem('status');
  1001. let symbol = '¤';
  1002. try {
  1003. if (statusStr) {
  1004. const s = JSON.parse(statusStr);
  1005. symbol = s?.custom_currency_symbol || symbol;
  1006. }
  1007. } catch (e) {}
  1008. return symbol + formattedAmount;
  1009. }
  1010. return '$' + formattedAmount;
  1011. }
  1012. /**
  1013. * 获取当前货币配置信息
  1014. * @returns {Object} - { symbol, rate, type }
  1015. */
  1016. export function getCurrencyConfig() {
  1017. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  1018. const statusStr = localStorage.getItem('status');
  1019. let symbol = '$';
  1020. let rate = 1;
  1021. if (quotaDisplayType === 'CNY') {
  1022. symbol = '¥';
  1023. try {
  1024. if (statusStr) {
  1025. const s = JSON.parse(statusStr);
  1026. rate = s?.usd_exchange_rate || 7;
  1027. }
  1028. } catch (e) {}
  1029. } else if (quotaDisplayType === 'CUSTOM') {
  1030. try {
  1031. if (statusStr) {
  1032. const s = JSON.parse(statusStr);
  1033. symbol = s?.custom_currency_symbol || '¤';
  1034. rate = s?.custom_currency_exchange_rate || 1;
  1035. }
  1036. } catch (e) {}
  1037. }
  1038. return { symbol, rate, type: quotaDisplayType };
  1039. }
  1040. /**
  1041. * 将美元金额转换为当前选择的货币
  1042. * @param {number} usdAmount - 美元金额
  1043. * @param {number} digits - 小数位数
  1044. * @returns {string} - 格式化后的货币字符串
  1045. */
  1046. export function convertUSDToCurrency(usdAmount, digits = 2) {
  1047. const { symbol, rate } = getCurrencyConfig();
  1048. const convertedAmount = usdAmount * rate;
  1049. return symbol + convertedAmount.toFixed(digits);
  1050. }
  1051. export function renderQuota(quota, digits = 2) {
  1052. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  1053. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  1054. quotaPerUnit = parseFloat(quotaPerUnit);
  1055. if (quotaDisplayType === 'TOKENS') {
  1056. return renderNumber(quota);
  1057. }
  1058. const resultUSD = quota / quotaPerUnit;
  1059. let symbol = '$';
  1060. let value = resultUSD;
  1061. if (quotaDisplayType === 'CNY') {
  1062. const statusStr = localStorage.getItem('status');
  1063. let usdRate = 1;
  1064. try {
  1065. if (statusStr) {
  1066. const s = JSON.parse(statusStr);
  1067. usdRate = s?.usd_exchange_rate || 1;
  1068. }
  1069. } catch (e) {}
  1070. value = resultUSD * usdRate;
  1071. symbol = '¥';
  1072. } else if (quotaDisplayType === 'CUSTOM') {
  1073. const statusStr = localStorage.getItem('status');
  1074. let symbolCustom = '¤';
  1075. let rate = 1;
  1076. try {
  1077. if (statusStr) {
  1078. const s = JSON.parse(statusStr);
  1079. symbolCustom = s?.custom_currency_symbol || symbolCustom;
  1080. rate = s?.custom_currency_exchange_rate || rate;
  1081. }
  1082. } catch (e) {}
  1083. value = resultUSD * rate;
  1084. symbol = symbolCustom;
  1085. }
  1086. const fixedResult = value.toFixed(digits);
  1087. if (parseFloat(fixedResult) === 0 && quota > 0 && value > 0) {
  1088. const minValue = Math.pow(10, -digits);
  1089. return symbol + minValue.toFixed(digits);
  1090. }
  1091. return symbol + fixedResult;
  1092. }
  1093. function isValidGroupRatio(ratio) {
  1094. return Number.isFinite(ratio) && ratio !== -1;
  1095. }
  1096. /**
  1097. * Helper function to get effective ratio and label
  1098. * @param {number} groupRatio - The default group ratio
  1099. * @param {number} user_group_ratio - The user-specific group ratio
  1100. * @returns {Object} - Object containing { ratio, label, useUserGroupRatio }
  1101. */
  1102. function getEffectiveRatio(groupRatio, user_group_ratio) {
  1103. const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
  1104. const ratioLabel = useUserGroupRatio
  1105. ? i18next.t('专属倍率')
  1106. : i18next.t('分组倍率');
  1107. const effectiveRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
  1108. return {
  1109. ratio: effectiveRatio,
  1110. label: ratioLabel,
  1111. useUserGroupRatio: useUserGroupRatio,
  1112. };
  1113. }
  1114. function getQuotaDisplayType() {
  1115. return localStorage.getItem('quota_display_type') || 'USD';
  1116. }
  1117. function resolveBillingDisplayMode(displayMode, modelPrice = -1) {
  1118. if (modelPrice !== -1) {
  1119. return 'price';
  1120. }
  1121. if (getQuotaDisplayType() === 'TOKENS') {
  1122. return 'ratio';
  1123. }
  1124. return displayMode === 'ratio' ? 'ratio' : 'price';
  1125. }
  1126. function isPriceDisplayMode(displayMode, modelPrice = -1) {
  1127. return resolveBillingDisplayMode(displayMode, modelPrice) === 'price';
  1128. }
  1129. function shouldUseRatioBillingProcess(modelPrice = -1) {
  1130. return modelPrice === -1 && getQuotaDisplayType() === 'TOKENS';
  1131. }
  1132. function formatCompactDisplayPrice(usdAmount, digits = 6) {
  1133. const { symbol, rate } = getCurrencyConfig();
  1134. const amount = Number((usdAmount * rate).toFixed(digits));
  1135. return `${symbol}${amount}`;
  1136. }
  1137. function appendPricePart(parts, condition, key, vars) {
  1138. if (!condition) {
  1139. return;
  1140. }
  1141. parts.push(i18next.t(key, vars));
  1142. }
  1143. function joinBillingSummary(parts) {
  1144. return parts.filter(Boolean).join(',');
  1145. }
  1146. function getGroupRatioText(groupRatio, user_group_ratio) {
  1147. const { ratio, label } = getEffectiveRatio(groupRatio, user_group_ratio);
  1148. return i18next.t('{{ratioType}} {{ratio}}x', {
  1149. ratioType: label,
  1150. ratio,
  1151. });
  1152. }
  1153. function formatRatioValue(value, digits = 6) {
  1154. const num = Number(value);
  1155. if (!Number.isFinite(num)) {
  1156. return 0;
  1157. }
  1158. return Number(num.toFixed(digits));
  1159. }
  1160. function renderDisplayAmountFromUsd(usdAmount, digits = 6) {
  1161. return renderQuotaWithAmount(Number(Number(usdAmount || 0).toFixed(digits)));
  1162. }
  1163. function formatBillingDisplayPrice(usdAmount, rate, digits = 6) {
  1164. return (usdAmount * rate).toFixed(digits);
  1165. }
  1166. function buildBillingText(key, vars) {
  1167. return i18next.t(key, vars);
  1168. }
  1169. function buildBillingPriceText(
  1170. key,
  1171. { symbol, usdAmount, rate, amountKey = 'price', digits = 6, ...vars },
  1172. ) {
  1173. return buildBillingText(key, {
  1174. symbol,
  1175. [amountKey]: formatBillingDisplayPrice(usdAmount, rate, digits),
  1176. ...vars,
  1177. });
  1178. }
  1179. function renderBillingArticle(lines, { showReferenceNote = true } = {}) {
  1180. const articleLines = lines.filter(Boolean);
  1181. if (showReferenceNote) {
  1182. articleLines.push(buildBillingText('仅供参考,以实际扣费为准'));
  1183. }
  1184. return (
  1185. <article>
  1186. {articleLines.map((line, index) => (
  1187. <p key={index}>{line}</p>
  1188. ))}
  1189. </article>
  1190. );
  1191. }
  1192. // Shared core for simple price rendering (used by OpenAI-like and Claude-like variants)
  1193. function renderPriceSimpleCore({
  1194. modelRatio,
  1195. modelPrice = -1,
  1196. groupRatio,
  1197. user_group_ratio,
  1198. cacheTokens = 0,
  1199. cacheRatio = 1.0,
  1200. cacheCreationTokens = 0,
  1201. cacheCreationRatio = 1.0,
  1202. cacheCreationTokens5m = 0,
  1203. cacheCreationRatio5m = 1.0,
  1204. cacheCreationTokens1h = 0,
  1205. cacheCreationRatio1h = 1.0,
  1206. image = false,
  1207. imageRatio = 1.0,
  1208. isSystemPromptOverride = false,
  1209. displayMode = 'price',
  1210. outputMode = 'text',
  1211. }) {
  1212. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  1213. groupRatio,
  1214. user_group_ratio,
  1215. );
  1216. const finalGroupRatio = effectiveGroupRatio;
  1217. const { symbol, rate } = getCurrencyConfig();
  1218. const hasSplitCacheCreation =
  1219. cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  1220. const shouldShowLegacyCacheCreation =
  1221. !hasSplitCacheCreation && cacheCreationTokens !== 0;
  1222. const shouldShowCache = cacheTokens !== 0;
  1223. const shouldShowCacheCreation5m =
  1224. hasSplitCacheCreation && cacheCreationTokens5m > 0;
  1225. const shouldShowCacheCreation1h =
  1226. hasSplitCacheCreation && cacheCreationTokens1h > 0;
  1227. if (outputMode === 'segments') {
  1228. const segments = [
  1229. {
  1230. tone: 'primary',
  1231. text: getGroupRatioText(groupRatio, user_group_ratio),
  1232. },
  1233. ];
  1234. if (modelPrice !== -1) {
  1235. segments.push({
  1236. tone: 'secondary',
  1237. text: isPriceDisplayMode(displayMode, modelPrice)
  1238. ? i18next.t('模型价格 {{price}}', {
  1239. price: formatCompactDisplayPrice(modelPrice),
  1240. })
  1241. : i18next.t('按次'),
  1242. });
  1243. } else if (isPriceDisplayMode(displayMode, modelPrice)) {
  1244. segments.push({
  1245. tone: 'secondary',
  1246. text: i18next.t('输入 {{price}} / 1M tokens', {
  1247. price: formatCompactDisplayPrice(modelRatio * 2.0),
  1248. }),
  1249. });
  1250. if (shouldShowCache) {
  1251. segments.push({
  1252. tone: 'secondary',
  1253. text: i18next.t('缓存读 {{price}} / 1M tokens', {
  1254. price: formatCompactDisplayPrice(modelRatio * 2.0 * cacheRatio),
  1255. }),
  1256. });
  1257. }
  1258. if (hasSplitCacheCreation && shouldShowCacheCreation5m) {
  1259. segments.push({
  1260. tone: 'secondary',
  1261. text: i18next.t('5m缓存创建 {{price}} / 1M tokens', {
  1262. price: formatCompactDisplayPrice(
  1263. modelRatio * 2.0 * cacheCreationRatio5m,
  1264. ),
  1265. }),
  1266. });
  1267. }
  1268. if (hasSplitCacheCreation && shouldShowCacheCreation1h) {
  1269. segments.push({
  1270. tone: 'secondary',
  1271. text: i18next.t('1h缓存创建 {{price}} / 1M tokens', {
  1272. price: formatCompactDisplayPrice(
  1273. modelRatio * 2.0 * cacheCreationRatio1h,
  1274. ),
  1275. }),
  1276. });
  1277. }
  1278. if (!hasSplitCacheCreation && shouldShowLegacyCacheCreation) {
  1279. segments.push({
  1280. tone: 'secondary',
  1281. text: i18next.t('缓存创建 {{price}} / 1M tokens', {
  1282. price: formatCompactDisplayPrice(
  1283. modelRatio * 2.0 * cacheCreationRatio,
  1284. ),
  1285. }),
  1286. });
  1287. }
  1288. if (image) {
  1289. segments.push({
  1290. tone: 'secondary',
  1291. text: i18next.t('图片输入 {{price}} / 1M tokens', {
  1292. price: formatCompactDisplayPrice(modelRatio * 2.0 * imageRatio),
  1293. }),
  1294. });
  1295. }
  1296. } else {
  1297. segments.push({
  1298. tone: 'secondary',
  1299. text: i18next.t('模型: {{ratio}}', {
  1300. ratio: modelRatio,
  1301. }),
  1302. });
  1303. if (shouldShowCache) {
  1304. segments.push({
  1305. tone: 'secondary',
  1306. text: i18next.t('缓存: {{cacheRatio}}', {
  1307. cacheRatio: cacheRatio,
  1308. }),
  1309. });
  1310. }
  1311. if (hasSplitCacheCreation) {
  1312. if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
  1313. segments.push({
  1314. tone: 'secondary',
  1315. text: i18next.t(
  1316. '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
  1317. {
  1318. cacheCreationRatio5m: cacheCreationRatio5m,
  1319. cacheCreationRatio1h: cacheCreationRatio1h,
  1320. },
  1321. ),
  1322. });
  1323. } else if (shouldShowCacheCreation5m) {
  1324. segments.push({
  1325. tone: 'secondary',
  1326. text: i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}', {
  1327. cacheCreationRatio5m: cacheCreationRatio5m,
  1328. }),
  1329. });
  1330. } else if (shouldShowCacheCreation1h) {
  1331. segments.push({
  1332. tone: 'secondary',
  1333. text: i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}', {
  1334. cacheCreationRatio1h: cacheCreationRatio1h,
  1335. }),
  1336. });
  1337. }
  1338. } else if (shouldShowLegacyCacheCreation) {
  1339. segments.push({
  1340. tone: 'secondary',
  1341. text: i18next.t('缓存创建: {{cacheCreationRatio}}', {
  1342. cacheCreationRatio: cacheCreationRatio,
  1343. }),
  1344. });
  1345. }
  1346. if (image) {
  1347. segments.push({
  1348. tone: 'secondary',
  1349. text: i18next.t('图片输入: {{imageRatio}}', {
  1350. imageRatio: imageRatio,
  1351. }),
  1352. });
  1353. }
  1354. }
  1355. if (isSystemPromptOverride) {
  1356. segments.push({
  1357. tone: 'primary',
  1358. text: i18next.t('系统提示覆盖'),
  1359. });
  1360. }
  1361. return segments;
  1362. }
  1363. if (modelPrice !== -1) {
  1364. if (isPriceDisplayMode(displayMode, modelPrice)) {
  1365. return joinBillingSummary([
  1366. i18next.t('模型价格:{{symbol}}{{price}}', {
  1367. symbol: symbol,
  1368. price: (modelPrice * rate).toFixed(6),
  1369. }),
  1370. getGroupRatioText(groupRatio, user_group_ratio),
  1371. ]);
  1372. }
  1373. const displayPrice = (modelPrice * rate).toFixed(6);
  1374. return i18next.t('价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}', {
  1375. symbol: symbol,
  1376. price: displayPrice,
  1377. ratioType: ratioLabel,
  1378. ratio: finalGroupRatio,
  1379. });
  1380. }
  1381. if (isPriceDisplayMode(displayMode, modelPrice)) {
  1382. const parts = [];
  1383. if (modelPrice !== -1) {
  1384. parts.push(
  1385. i18next.t('模型价格 {{price}}', {
  1386. price: formatCompactDisplayPrice(modelPrice),
  1387. }),
  1388. );
  1389. parts.push(getGroupRatioText(groupRatio, user_group_ratio));
  1390. return joinBillingSummary(parts);
  1391. }
  1392. parts.push(
  1393. i18next.t('输入 {{price}} / 1M tokens', {
  1394. price: formatCompactDisplayPrice(modelRatio * 2.0),
  1395. }),
  1396. );
  1397. if (shouldShowCache) {
  1398. parts.push(
  1399. i18next.t('缓存读 {{price}} / 1M tokens', {
  1400. price: formatCompactDisplayPrice(modelRatio * 2.0 * cacheRatio),
  1401. }),
  1402. );
  1403. }
  1404. if (hasSplitCacheCreation && shouldShowCacheCreation5m) {
  1405. parts.push(
  1406. i18next.t('5m缓存创建 {{price}} / 1M tokens', {
  1407. price: formatCompactDisplayPrice(
  1408. modelRatio * 2.0 * cacheCreationRatio5m,
  1409. ),
  1410. }),
  1411. );
  1412. }
  1413. if (hasSplitCacheCreation && shouldShowCacheCreation1h) {
  1414. parts.push(
  1415. i18next.t('1h缓存创建 {{price}} / 1M tokens', {
  1416. price: formatCompactDisplayPrice(
  1417. modelRatio * 2.0 * cacheCreationRatio1h,
  1418. ),
  1419. }),
  1420. );
  1421. }
  1422. if (!hasSplitCacheCreation && shouldShowLegacyCacheCreation) {
  1423. parts.push(
  1424. i18next.t('缓存创建 {{price}} / 1M tokens', {
  1425. price: formatCompactDisplayPrice(
  1426. modelRatio * 2.0 * cacheCreationRatio,
  1427. ),
  1428. }),
  1429. );
  1430. }
  1431. if (image) {
  1432. parts.push(
  1433. i18next.t('图片输入 {{price}} / 1M tokens', {
  1434. price: formatCompactDisplayPrice(modelRatio * 2.0 * imageRatio),
  1435. }),
  1436. );
  1437. }
  1438. parts.push(getGroupRatioText(groupRatio, user_group_ratio));
  1439. let result = joinBillingSummary(parts);
  1440. if (isSystemPromptOverride) {
  1441. result += '\n\r' + i18next.t('系统提示覆盖');
  1442. }
  1443. return result;
  1444. }
  1445. const parts = [];
  1446. // base: model ratio
  1447. parts.push(i18next.t('模型: {{ratio}}'));
  1448. // cache part (label differs when with image)
  1449. if (shouldShowCache) {
  1450. parts.push(i18next.t('缓存: {{cacheRatio}}'));
  1451. }
  1452. if (hasSplitCacheCreation) {
  1453. if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
  1454. parts.push(
  1455. i18next.t(
  1456. '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
  1457. ),
  1458. );
  1459. } else if (shouldShowCacheCreation5m) {
  1460. parts.push(i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}'));
  1461. } else if (shouldShowCacheCreation1h) {
  1462. parts.push(i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}'));
  1463. }
  1464. } else if (shouldShowLegacyCacheCreation) {
  1465. parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}'));
  1466. }
  1467. // image part
  1468. if (image) {
  1469. parts.push(i18next.t('图片输入: {{imageRatio}}'));
  1470. }
  1471. parts.push(`{{ratioType}}: {{groupRatio}}`);
  1472. let result = i18next.t(parts.join(' * '), {
  1473. ratio: modelRatio,
  1474. ratioType: ratioLabel,
  1475. groupRatio: finalGroupRatio,
  1476. cacheRatio: cacheRatio,
  1477. cacheCreationRatio: cacheCreationRatio,
  1478. cacheCreationRatio5m: cacheCreationRatio5m,
  1479. cacheCreationRatio1h: cacheCreationRatio1h,
  1480. imageRatio: imageRatio,
  1481. });
  1482. if (isSystemPromptOverride) {
  1483. result += '\n\r' + i18next.t('系统提示覆盖');
  1484. }
  1485. return result;
  1486. }
  1487. export function renderTaskBillingProcess(other, content) {
  1488. if (other?.task_id != null) {
  1489. return renderBillingArticle(
  1490. [content].filter(Boolean),
  1491. { showReferenceNote: false },
  1492. );
  1493. }
  1494. return renderBillingArticle([
  1495. buildBillingText('任务预扣费(将在任务完成后按实际token重算)'),
  1496. ]);
  1497. }
  1498. export function renderModelPrice(opts) {
  1499. const {
  1500. prompt_tokens: inputTokens = 0,
  1501. completion_tokens: completionTokens = 0,
  1502. model_ratio: modelRatio = 0,
  1503. model_price: modelPrice = -1,
  1504. completion_ratio: completionRatio,
  1505. group_ratio: _groupRatio,
  1506. user_group_ratio,
  1507. cache_tokens: cacheTokens = 0,
  1508. cache_ratio: cacheRatio = 1.0,
  1509. image = false,
  1510. image_ratio: imageRatio = 1.0,
  1511. image_output: imageOutputTokens = 0,
  1512. web_search: webSearch = false,
  1513. web_search_call_count: webSearchCallCount = 0,
  1514. web_search_price: webSearchPrice = 0,
  1515. file_search: fileSearch = false,
  1516. file_search_call_count: fileSearchCallCount = 0,
  1517. file_search_price: fileSearchPrice = 0,
  1518. audio_input_seperate_price: audioInputSeperatePrice = false,
  1519. audio_input_token_count: audioInputTokens = 0,
  1520. audio_input_price: audioInputPrice = 0,
  1521. image_generation_call: imageGenerationCall = false,
  1522. image_generation_call_price: imageGenerationCallPrice = 0,
  1523. displayMode = 'price',
  1524. } = opts;
  1525. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  1526. _groupRatio,
  1527. user_group_ratio,
  1528. );
  1529. let groupRatio = effectiveGroupRatio;
  1530. const { symbol, rate } = getCurrencyConfig();
  1531. if (!shouldUseRatioBillingProcess(modelPrice)) {
  1532. if (modelPrice !== -1) {
  1533. return renderBillingArticle([
  1534. buildBillingPriceText('按次:{{symbol}}{{price}}', {
  1535. symbol,
  1536. usdAmount: modelPrice,
  1537. rate,
  1538. }),
  1539. buildBillingPriceText(
  1540. '按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
  1541. {
  1542. symbol,
  1543. usdAmount: modelPrice,
  1544. rate,
  1545. ratioType: ratioLabel,
  1546. ratio: groupRatio,
  1547. amountKey: 'price',
  1548. total: formatBillingDisplayPrice(modelPrice * groupRatio, rate),
  1549. },
  1550. ),
  1551. ]);
  1552. }
  1553. if (completionRatio === undefined) {
  1554. completionRatio = 0;
  1555. }
  1556. const inputRatioPrice = modelRatio * 2.0;
  1557. const completionRatioPrice = modelRatio * 2.0 * completionRatio;
  1558. const cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  1559. const imageRatioPrice = modelRatio * 2.0 * imageRatio;
  1560. let effectiveInputTokens =
  1561. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  1562. if (image && imageOutputTokens > 0) {
  1563. effectiveInputTokens =
  1564. inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
  1565. }
  1566. if (audioInputTokens > 0) {
  1567. effectiveInputTokens -= audioInputTokens;
  1568. }
  1569. const price =
  1570. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  1571. (audioInputTokens / 1000000) * audioInputPrice * groupRatio +
  1572. (completionTokens / 1000000) * completionRatioPrice * groupRatio +
  1573. (webSearchCallCount / 1000) * webSearchPrice * groupRatio +
  1574. (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
  1575. imageGenerationCallPrice * groupRatio;
  1576. let inputDesc = '';
  1577. if (image && imageOutputTokens > 0) {
  1578. inputDesc = buildBillingPriceText(
  1579. '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}',
  1580. {
  1581. nonImageInput: inputTokens - imageOutputTokens,
  1582. imageInput: imageOutputTokens,
  1583. symbol,
  1584. usdAmount: inputRatioPrice,
  1585. rate,
  1586. },
  1587. );
  1588. } else if (cacheTokens > 0) {
  1589. inputDesc = buildBillingText(
  1590. '(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}',
  1591. {
  1592. nonCacheInput: inputTokens - cacheTokens,
  1593. cacheInput: cacheTokens,
  1594. symbol,
  1595. price: formatBillingDisplayPrice(inputRatioPrice, rate),
  1596. cachePrice: formatBillingDisplayPrice(cacheRatioPrice, rate),
  1597. },
  1598. );
  1599. } else if (audioInputSeperatePrice && audioInputTokens > 0) {
  1600. inputDesc = buildBillingText(
  1601. '(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}',
  1602. {
  1603. nonAudioInput: inputTokens - audioInputTokens,
  1604. audioInput: audioInputTokens,
  1605. symbol,
  1606. price: formatBillingDisplayPrice(inputRatioPrice, rate),
  1607. audioPrice: formatBillingDisplayPrice(audioInputPrice, rate),
  1608. },
  1609. );
  1610. } else {
  1611. inputDesc = buildBillingPriceText(
  1612. '(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}',
  1613. {
  1614. input: inputTokens,
  1615. symbol,
  1616. usdAmount: inputRatioPrice,
  1617. rate,
  1618. },
  1619. );
  1620. }
  1621. const outputDesc = buildBillingText(
  1622. '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}',
  1623. {
  1624. completion: completionTokens,
  1625. symbol,
  1626. compPrice: formatBillingDisplayPrice(completionRatioPrice, rate),
  1627. ratio: groupRatio,
  1628. ratioType: ratioLabel,
  1629. },
  1630. );
  1631. const extraServices = [
  1632. webSearch && webSearchCallCount > 0
  1633. ? buildBillingPriceText(
  1634. ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
  1635. {
  1636. count: webSearchCallCount,
  1637. symbol,
  1638. usdAmount: webSearchPrice,
  1639. rate,
  1640. ratio: groupRatio,
  1641. ratioType: ratioLabel,
  1642. },
  1643. )
  1644. : '',
  1645. fileSearch && fileSearchCallCount > 0
  1646. ? buildBillingPriceText(
  1647. ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
  1648. {
  1649. count: fileSearchCallCount,
  1650. symbol,
  1651. usdAmount: fileSearchPrice,
  1652. rate,
  1653. ratio: groupRatio,
  1654. ratioType: ratioLabel,
  1655. },
  1656. )
  1657. : '',
  1658. imageGenerationCall && imageGenerationCallPrice > 0
  1659. ? buildBillingPriceText(
  1660. ' + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}',
  1661. {
  1662. symbol,
  1663. usdAmount: imageGenerationCallPrice,
  1664. rate,
  1665. ratio: groupRatio,
  1666. ratioType: ratioLabel,
  1667. },
  1668. )
  1669. : '',
  1670. ].join('');
  1671. const billingLines = [
  1672. buildBillingPriceText(
  1673. '输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}',
  1674. {
  1675. symbol,
  1676. usdAmount: inputRatioPrice,
  1677. rate,
  1678. audioPrice: audioInputSeperatePrice
  1679. ? `,${i18next.t('音频输入价格')} ${symbol}${formatBillingDisplayPrice(audioInputPrice, rate)} / 1M tokens`
  1680. : '',
  1681. },
  1682. ),
  1683. buildBillingPriceText('输出价格:{{symbol}}{{total}} / 1M tokens', {
  1684. symbol,
  1685. usdAmount: completionRatioPrice,
  1686. rate,
  1687. amountKey: 'total',
  1688. }),
  1689. cacheTokens > 0
  1690. ? buildBillingPriceText(
  1691. '缓存读取价格:{{symbol}}{{total}} / 1M tokens',
  1692. {
  1693. symbol,
  1694. usdAmount: inputRatioPrice * cacheRatio,
  1695. rate,
  1696. amountKey: 'total',
  1697. },
  1698. )
  1699. : null,
  1700. image && imageOutputTokens > 0
  1701. ? buildBillingPriceText(
  1702. '图片输入价格:{{symbol}}{{total}} / 1M tokens',
  1703. {
  1704. symbol,
  1705. usdAmount: imageRatioPrice,
  1706. rate,
  1707. amountKey: 'total',
  1708. },
  1709. )
  1710. : null,
  1711. webSearch && webSearchCallCount > 0
  1712. ? buildBillingPriceText('Web搜索价格:{{symbol}}{{price}} / 1K 次', {
  1713. symbol,
  1714. usdAmount: webSearchPrice,
  1715. rate,
  1716. })
  1717. : null,
  1718. fileSearch && fileSearchCallCount > 0
  1719. ? buildBillingPriceText('文件搜索价格:{{symbol}}{{price}} / 1K 次', {
  1720. symbol,
  1721. usdAmount: fileSearchPrice,
  1722. rate,
  1723. })
  1724. : null,
  1725. imageGenerationCall && imageGenerationCallPrice > 0
  1726. ? buildBillingPriceText('图片生成调用:{{symbol}}{{price}} / 1次', {
  1727. symbol,
  1728. usdAmount: imageGenerationCallPrice,
  1729. rate,
  1730. })
  1731. : null,
  1732. buildBillingText(
  1733. '{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}',
  1734. {
  1735. inputDesc,
  1736. outputDesc,
  1737. extraServices,
  1738. symbol,
  1739. total: formatBillingDisplayPrice(price, rate),
  1740. },
  1741. ),
  1742. ];
  1743. return renderBillingArticle(billingLines);
  1744. }
  1745. if (modelPrice !== -1) {
  1746. const displayPrice = (modelPrice * rate).toFixed(6);
  1747. const displayTotal = (modelPrice * groupRatio * rate).toFixed(6);
  1748. return i18next.t(
  1749. '按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}',
  1750. {
  1751. symbol: symbol,
  1752. price: displayPrice,
  1753. ratio: groupRatio,
  1754. total: displayTotal,
  1755. ratioType: ratioLabel,
  1756. },
  1757. );
  1758. }
  1759. if (completionRatio === undefined) {
  1760. completionRatio = 0;
  1761. }
  1762. const modelRatioValue = formatRatioValue(modelRatio);
  1763. const completionRatioValue = formatRatioValue(completionRatio);
  1764. const cacheRatioValue = formatRatioValue(cacheRatio);
  1765. const imageRatioValue = formatRatioValue(imageRatio);
  1766. const inputRatioPrice = modelRatio * 2.0;
  1767. const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
  1768. const audioRatioValue =
  1769. audioInputSeperatePrice && audioInputPrice > 0
  1770. ? formatRatioValue(audioInputPrice / inputRatioPrice)
  1771. : null;
  1772. const textInputTokens = Math.max(
  1773. inputTokens - cacheTokens - audioInputTokens,
  1774. 0,
  1775. );
  1776. const imageInputTokens =
  1777. image && imageOutputTokens > 0 ? imageOutputTokens : 0;
  1778. const cacheInputTokens = cacheTokens;
  1779. const textInputAmount =
  1780. (textInputTokens / 1000000) * inputRatioPrice * groupRatio;
  1781. const cacheInputAmount =
  1782. (cacheInputTokens / 1000000) *
  1783. inputRatioPrice *
  1784. cacheRatioValue *
  1785. groupRatio;
  1786. const imageInputAmount =
  1787. (imageInputTokens / 1000000) *
  1788. inputRatioPrice *
  1789. imageRatioValue *
  1790. groupRatio;
  1791. const audioInputAmount =
  1792. (audioInputTokens / 1000000) * audioInputPrice * groupRatio;
  1793. const completionAmount =
  1794. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  1795. const webSearchAmount =
  1796. (webSearchCallCount / 1000) * webSearchPrice * groupRatio;
  1797. const fileSearchAmount =
  1798. (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
  1799. const imageGenerationAmount = imageGenerationCallPrice * groupRatio;
  1800. const totalAmount =
  1801. textInputAmount +
  1802. cacheInputAmount +
  1803. imageInputAmount +
  1804. audioInputAmount +
  1805. completionAmount +
  1806. webSearchAmount +
  1807. fileSearchAmount +
  1808. imageGenerationAmount;
  1809. return renderBillingArticle([
  1810. [
  1811. buildBillingText('模型倍率 {{modelRatio}}', {
  1812. modelRatio: modelRatioValue,
  1813. }),
  1814. buildBillingText('补全倍率 {{completionRatio}}', {
  1815. completionRatio: completionRatioValue,
  1816. }),
  1817. cacheInputTokens > 0
  1818. ? buildBillingText('缓存倍率 {{cacheRatio}}', {
  1819. cacheRatio: cacheRatioValue,
  1820. })
  1821. : null,
  1822. imageInputTokens > 0
  1823. ? buildBillingText('图片倍率 {{imageRatio}}', {
  1824. imageRatio: imageRatioValue,
  1825. })
  1826. : null,
  1827. audioRatioValue !== null
  1828. ? buildBillingText('音频倍率 {{audioRatio}}', {
  1829. audioRatio: audioRatioValue,
  1830. })
  1831. : null,
  1832. buildBillingText('{{ratioType}} {{ratio}}', {
  1833. ratioType: ratioLabel,
  1834. ratio: groupRatio,
  1835. }),
  1836. ]
  1837. .filter(Boolean)
  1838. .join(','),
  1839. textInputTokens > 0
  1840. ? buildBillingText(
  1841. '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  1842. {
  1843. tokens: textInputTokens,
  1844. modelRatio: modelRatioValue,
  1845. ratioType: ratioLabel,
  1846. ratio: groupRatio,
  1847. amount: renderDisplayAmountFromUsd(textInputAmount),
  1848. },
  1849. )
  1850. : null,
  1851. cacheInputTokens > 0
  1852. ? buildBillingText(
  1853. '缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  1854. {
  1855. tokens: cacheInputTokens,
  1856. modelRatio: modelRatioValue,
  1857. cacheRatio: cacheRatioValue,
  1858. ratioType: ratioLabel,
  1859. ratio: groupRatio,
  1860. amount: renderDisplayAmountFromUsd(cacheInputAmount),
  1861. },
  1862. )
  1863. : null,
  1864. imageInputTokens > 0
  1865. ? buildBillingText(
  1866. '图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  1867. {
  1868. tokens: imageInputTokens,
  1869. modelRatio: modelRatioValue,
  1870. imageRatio: imageRatioValue,
  1871. ratioType: ratioLabel,
  1872. ratio: groupRatio,
  1873. amount: renderDisplayAmountFromUsd(imageInputAmount),
  1874. },
  1875. )
  1876. : null,
  1877. audioInputTokens > 0 && audioRatioValue !== null
  1878. ? buildBillingText(
  1879. '音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  1880. {
  1881. tokens: audioInputTokens,
  1882. modelRatio: modelRatioValue,
  1883. audioRatio: audioRatioValue,
  1884. ratioType: ratioLabel,
  1885. ratio: groupRatio,
  1886. amount: renderDisplayAmountFromUsd(audioInputAmount),
  1887. },
  1888. )
  1889. : null,
  1890. buildBillingText(
  1891. '输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  1892. {
  1893. tokens: completionTokens,
  1894. modelRatio: modelRatioValue,
  1895. completionRatio: completionRatioValue,
  1896. ratioType: ratioLabel,
  1897. ratio: groupRatio,
  1898. amount: renderDisplayAmountFromUsd(completionAmount),
  1899. },
  1900. ),
  1901. webSearch && webSearchCallCount > 0
  1902. ? buildBillingText(
  1903. 'Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
  1904. {
  1905. count: webSearchCallCount,
  1906. price: renderDisplayAmountFromUsd(webSearchPrice),
  1907. ratioType: ratioLabel,
  1908. ratio: groupRatio,
  1909. amount: renderDisplayAmountFromUsd(webSearchAmount),
  1910. },
  1911. )
  1912. : null,
  1913. fileSearch && fileSearchCallCount > 0
  1914. ? buildBillingText(
  1915. '文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
  1916. {
  1917. count: fileSearchCallCount,
  1918. price: renderDisplayAmountFromUsd(fileSearchPrice),
  1919. ratioType: ratioLabel,
  1920. ratio: groupRatio,
  1921. amount: renderDisplayAmountFromUsd(fileSearchAmount),
  1922. },
  1923. )
  1924. : null,
  1925. imageGenerationCall && imageGenerationCallPrice > 0
  1926. ? buildBillingText(
  1927. '图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
  1928. {
  1929. price: renderDisplayAmountFromUsd(imageGenerationCallPrice),
  1930. ratioType: ratioLabel,
  1931. ratio: groupRatio,
  1932. amount: renderDisplayAmountFromUsd(imageGenerationAmount),
  1933. },
  1934. )
  1935. : null,
  1936. buildBillingText('合计:{{total}}', {
  1937. total: renderDisplayAmountFromUsd(totalAmount),
  1938. }),
  1939. ]);
  1940. }
  1941. export function renderLogContent(opts) {
  1942. const {
  1943. model_ratio: modelRatio,
  1944. completion_ratio: completionRatio,
  1945. model_price: modelPrice = -1,
  1946. group_ratio: groupRatio,
  1947. user_group_ratio,
  1948. cache_ratio: cacheRatio = 1.0,
  1949. image = false,
  1950. image_ratio: imageRatio = 1.0,
  1951. web_search: webSearch = false,
  1952. web_search_call_count: webSearchCallCount = 0,
  1953. file_search: fileSearch = false,
  1954. file_search_call_count: fileSearchCallCount = 0,
  1955. displayMode = 'price',
  1956. } = opts;
  1957. const {
  1958. ratio,
  1959. label: ratioLabel,
  1960. useUserGroupRatio: useUserGroupRatio,
  1961. } = getEffectiveRatio(groupRatio, user_group_ratio);
  1962. // 获取货币配置
  1963. const { symbol, rate } = getCurrencyConfig();
  1964. if (isPriceDisplayMode(displayMode, modelPrice)) {
  1965. if (modelPrice !== -1) {
  1966. return joinBillingSummary([
  1967. i18next.t('模型价格 {{symbol}}{{price}} / 次', {
  1968. symbol,
  1969. price: (modelPrice * rate).toFixed(6),
  1970. }),
  1971. getGroupRatioText(groupRatio, user_group_ratio),
  1972. ]);
  1973. }
  1974. const parts = [
  1975. i18next.t('输入价格 {{symbol}}{{price}} / 1M tokens', {
  1976. symbol,
  1977. price: (modelRatio * 2.0 * rate).toFixed(6),
  1978. }),
  1979. i18next.t('输出价格 {{symbol}}{{price}} / 1M tokens', {
  1980. symbol,
  1981. price: (modelRatio * 2.0 * completionRatio * rate).toFixed(6),
  1982. }),
  1983. ];
  1984. appendPricePart(
  1985. parts,
  1986. cacheRatio !== 1.0,
  1987. '缓存读取价格 {{symbol}}{{price}} / 1M tokens',
  1988. {
  1989. symbol,
  1990. price: (modelRatio * 2.0 * cacheRatio * rate).toFixed(6),
  1991. },
  1992. );
  1993. appendPricePart(
  1994. parts,
  1995. image,
  1996. '图片输入价格 {{symbol}}{{price}} / 1M tokens',
  1997. {
  1998. symbol,
  1999. price: (modelRatio * 2.0 * imageRatio * rate).toFixed(6),
  2000. },
  2001. );
  2002. appendPricePart(
  2003. parts,
  2004. webSearch,
  2005. 'Web 搜索调用 {{webSearchCallCount}} 次',
  2006. {
  2007. webSearchCallCount,
  2008. },
  2009. );
  2010. appendPricePart(
  2011. parts,
  2012. fileSearch,
  2013. '文件搜索调用 {{fileSearchCallCount}} 次',
  2014. {
  2015. fileSearchCallCount,
  2016. },
  2017. );
  2018. parts.push(getGroupRatioText(groupRatio, user_group_ratio));
  2019. return joinBillingSummary(parts);
  2020. }
  2021. if (modelPrice !== -1) {
  2022. return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', {
  2023. symbol: symbol,
  2024. price: (modelPrice * rate).toFixed(6),
  2025. ratioType: ratioLabel,
  2026. ratio,
  2027. });
  2028. } else {
  2029. if (image) {
  2030. return i18next.t(
  2031. '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}',
  2032. {
  2033. modelRatio: modelRatio,
  2034. cacheRatio: cacheRatio,
  2035. completionRatio: completionRatio,
  2036. imageRatio: imageRatio,
  2037. ratioType: ratioLabel,
  2038. ratio,
  2039. },
  2040. );
  2041. } else if (webSearch) {
  2042. return i18next.t(
  2043. '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次',
  2044. {
  2045. modelRatio: modelRatio,
  2046. cacheRatio: cacheRatio,
  2047. completionRatio: completionRatio,
  2048. ratioType: ratioLabel,
  2049. ratio,
  2050. webSearchCallCount,
  2051. },
  2052. );
  2053. } else {
  2054. return i18next.t(
  2055. '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
  2056. {
  2057. modelRatio: modelRatio,
  2058. cacheRatio: cacheRatio,
  2059. completionRatio: completionRatio,
  2060. ratioType: ratioLabel,
  2061. ratio,
  2062. },
  2063. );
  2064. }
  2065. }
  2066. }
  2067. export function stripExprVersion(exprStr) {
  2068. if (!exprStr) return { version: 1, body: '' };
  2069. const m = exprStr.match(/^v(\d+):([\s\S]*)$/);
  2070. if (m) return { version: Number(m[1]), body: m[2] };
  2071. return { version: 1, body: exprStr };
  2072. }
  2073. function parseTierBody(bodyStr) {
  2074. const coeffs = {};
  2075. const re = new RegExp(BILLING_VAR_REGEX.source, 'g');
  2076. let m;
  2077. while ((m = re.exec(bodyStr)) !== null) {
  2078. if (!(m[1] in coeffs)) coeffs[m[1]] = Number(m[2]);
  2079. }
  2080. const tier = {};
  2081. for (const [varName, field] of Object.entries(BILLING_VAR_KEY_TO_FIELD)) {
  2082. tier[field] = coeffs[varName] || 0;
  2083. }
  2084. return tier;
  2085. }
  2086. export function parseTiersFromExpr(exprStr) {
  2087. if (!exprStr) return [];
  2088. try {
  2089. const { body } = stripExprVersion(exprStr);
  2090. const condGroup = `((?:(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)(?:\\s*&&\\s*(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`;
  2091. const tierRe = new RegExp(`(?:${condGroup}\\s*\\?\\s*)?tier\\("([^"]*)",\\s*([^)]+)\\)`, 'g');
  2092. const tiers = [];
  2093. let m;
  2094. while ((m = tierRe.exec(body)) !== null) {
  2095. const condStr = m[1] || '';
  2096. const conditions = [];
  2097. if (condStr) {
  2098. for (const cp of condStr.split(/\s*&&\s*/)) {
  2099. const cm = cp.trim().match(/^(p|c)\s*(<|<=|>|>=)\s*([\d.eE+]+)$/);
  2100. if (cm) conditions.push({ var: cm[1], op: cm[2], value: Number(cm[3]) });
  2101. }
  2102. }
  2103. const tier = parseTierBody(m[3]);
  2104. tier.label = m[2];
  2105. tier.conditions = conditions;
  2106. tiers.push(tier);
  2107. }
  2108. return tiers;
  2109. } catch {
  2110. return [];
  2111. }
  2112. }
  2113. export function renderTieredModelPrice(opts) {
  2114. const {
  2115. prompt_tokens: inputTokens = 0,
  2116. completion_tokens: completionTokens = 0,
  2117. expr_b64: exprB64,
  2118. matched_tier: matchedTier,
  2119. group_ratio: groupRatio,
  2120. cache_tokens: cacheTokens = 0,
  2121. cache_creation_tokens: cacheCreationTokens = 0,
  2122. cache_creation_tokens_5m: cacheCreationTokens5m = 0,
  2123. cache_creation_tokens_1h: cacheCreationTokens1h = 0,
  2124. } = opts;
  2125. let exprStr = '';
  2126. try { exprStr = atob(exprB64); } catch { /* ignore */ }
  2127. const tiers = parseTiersFromExpr(exprStr);
  2128. if (tiers.length === 0) {
  2129. return i18next.t('阶梯计费(表达式解析失败)');
  2130. }
  2131. const tier = tiers.find((t) => t.label === matchedTier) || tiers[0];
  2132. const { symbol, rate } = getCurrencyConfig();
  2133. const gr = groupRatio || 1;
  2134. const priceLines = BILLING_VARS.map((v) => [v.field, v.label]);
  2135. const lines = [
  2136. buildBillingText('命中档位:{{tier}}', { tier: matchedTier || tier.label }),
  2137. ...priceLines
  2138. .filter(([field]) => tier[field] > 0)
  2139. .map(([field, label]) =>
  2140. buildBillingPriceText(`${label}:{{symbol}}{{price}} / 1M tokens`, { symbol, usdAmount: tier[field], rate }),
  2141. ),
  2142. ];
  2143. return renderBillingArticle(lines);
  2144. }
  2145. export function renderTieredModelPriceSimple(opts) {
  2146. const {
  2147. expr_b64: exprB64,
  2148. matched_tier: matchedTier,
  2149. group_ratio: groupRatio,
  2150. user_group_ratio,
  2151. cache_tokens: cacheTokens = 0,
  2152. cache_creation_tokens_5m: cacheCreationTokens5m = 0,
  2153. cache_creation_tokens_1h: cacheCreationTokens1h = 0,
  2154. cache_creation_tokens: cacheCreationTokens = 0,
  2155. displayMode = 'price',
  2156. outputMode = 'segments',
  2157. } = opts;
  2158. let exprStr = '';
  2159. try { exprStr = atob(exprB64); } catch { /* ignore */ }
  2160. const tiers = parseTiersFromExpr(exprStr);
  2161. const tier = tiers.find((t) => t.label === matchedTier) || tiers[0];
  2162. if (outputMode === 'segments') {
  2163. const segments = [
  2164. {
  2165. tone: 'primary',
  2166. text: getGroupRatioText(groupRatio, user_group_ratio),
  2167. },
  2168. ];
  2169. if (tier && isPriceDisplayMode(displayMode)) {
  2170. const priceSegments = BILLING_VARS.map((v) => [v.field, v.shortLabel]);
  2171. for (const [field, label] of priceSegments) {
  2172. if (tier[field] > 0) {
  2173. segments.push({
  2174. tone: 'secondary',
  2175. text: i18next.t('{{label}} {{price}} / 1M tokens', {
  2176. label: i18next.t(label),
  2177. price: formatCompactDisplayPrice(tier[field]),
  2178. }),
  2179. });
  2180. }
  2181. }
  2182. }
  2183. return segments;
  2184. }
  2185. return [];
  2186. }
  2187. export function renderModelPriceSimple(opts) {
  2188. const {
  2189. model_ratio: modelRatio,
  2190. model_price: modelPrice = -1,
  2191. group_ratio: groupRatio,
  2192. user_group_ratio,
  2193. cache_tokens: cacheTokens = 0,
  2194. cache_ratio: cacheRatio = 1.0,
  2195. cache_creation_tokens: cacheCreationTokens = 0,
  2196. cache_creation_ratio: cacheCreationRatio = 1.0,
  2197. cache_creation_tokens_5m: cacheCreationTokens5m = 0,
  2198. cache_creation_ratio_5m: cacheCreationRatio5m = 1.0,
  2199. cache_creation_tokens_1h: cacheCreationTokens1h = 0,
  2200. cache_creation_ratio_1h: cacheCreationRatio1h = 1.0,
  2201. image = false,
  2202. image_ratio: imageRatio = 1.0,
  2203. is_system_prompt_overwritten: isSystemPromptOverride = false,
  2204. provider = 'openai',
  2205. displayMode = 'price',
  2206. outputMode = 'text',
  2207. } = opts;
  2208. return renderPriceSimpleCore({
  2209. modelRatio,
  2210. modelPrice,
  2211. groupRatio,
  2212. user_group_ratio,
  2213. cacheTokens,
  2214. cacheRatio,
  2215. cacheCreationTokens,
  2216. cacheCreationRatio,
  2217. cacheCreationTokens5m,
  2218. cacheCreationRatio5m,
  2219. cacheCreationTokens1h,
  2220. cacheCreationRatio1h,
  2221. image,
  2222. imageRatio,
  2223. isSystemPromptOverride,
  2224. displayMode,
  2225. outputMode,
  2226. });
  2227. }
  2228. export function renderAudioModelPrice(opts) {
  2229. const {
  2230. prompt_tokens: inputTokens = 0,
  2231. completion_tokens: completionTokens = 0,
  2232. model_ratio: modelRatio = 0,
  2233. model_price: modelPrice = -1,
  2234. completion_ratio: completionRatio,
  2235. audio_input: audioInputTokens = 0,
  2236. audio_output: audioCompletionTokens = 0,
  2237. audio_ratio: audioRatio,
  2238. audio_completion_ratio: audioCompletionRatio,
  2239. group_ratio: _groupRatio,
  2240. user_group_ratio,
  2241. cache_tokens: cacheTokens = 0,
  2242. cache_ratio: cacheRatio = 1.0,
  2243. displayMode = 'price',
  2244. } = opts;
  2245. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  2246. _groupRatio,
  2247. user_group_ratio,
  2248. );
  2249. let groupRatio = effectiveGroupRatio;
  2250. // 获取货币配置
  2251. const { symbol, rate } = getCurrencyConfig();
  2252. if (!shouldUseRatioBillingProcess(modelPrice)) {
  2253. if (modelPrice !== -1) {
  2254. return renderBillingArticle([
  2255. buildBillingPriceText('模型价格:{{symbol}}{{price}} / 次', {
  2256. symbol,
  2257. usdAmount: modelPrice,
  2258. rate,
  2259. }),
  2260. buildBillingPriceText(
  2261. '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
  2262. {
  2263. symbol,
  2264. usdAmount: modelPrice,
  2265. rate,
  2266. ratioType: ratioLabel,
  2267. ratio: groupRatio,
  2268. total: formatBillingDisplayPrice(modelPrice * groupRatio, rate),
  2269. },
  2270. ),
  2271. ]);
  2272. }
  2273. if (completionRatio === undefined) {
  2274. completionRatio = 0;
  2275. }
  2276. audioRatio = parseFloat(audioRatio).toFixed(6);
  2277. const inputRatioPrice = modelRatio * 2.0;
  2278. const completionRatioPrice = modelRatio * 2.0 * completionRatio;
  2279. const textPrice =
  2280. ((inputTokens - cacheTokens + cacheTokens * cacheRatio) / 1000000) *
  2281. inputRatioPrice *
  2282. groupRatio +
  2283. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  2284. const audioPrice =
  2285. (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
  2286. (audioCompletionTokens / 1000000) *
  2287. inputRatioPrice *
  2288. audioRatio *
  2289. audioCompletionRatio *
  2290. groupRatio;
  2291. const totalPrice = textPrice + audioPrice;
  2292. return renderBillingArticle([
  2293. buildBillingPriceText('输入价格:{{symbol}}{{price}} / 1M tokens', {
  2294. symbol,
  2295. usdAmount: inputRatioPrice,
  2296. rate,
  2297. }),
  2298. buildBillingPriceText('输出价格:{{symbol}}{{price}} / 1M tokens', {
  2299. symbol,
  2300. usdAmount: completionRatioPrice,
  2301. rate,
  2302. }),
  2303. cacheTokens > 0
  2304. ? buildBillingPriceText(
  2305. '缓存读取价格:{{symbol}}{{price}} / 1M tokens',
  2306. {
  2307. symbol,
  2308. usdAmount: inputRatioPrice * cacheRatio,
  2309. rate,
  2310. },
  2311. )
  2312. : null,
  2313. buildBillingPriceText('音频输入价格:{{symbol}}{{price}} / 1M tokens', {
  2314. symbol,
  2315. usdAmount: inputRatioPrice * audioRatio,
  2316. rate,
  2317. }),
  2318. buildBillingPriceText('音频补全价格:{{symbol}}{{price}} / 1M tokens', {
  2319. symbol,
  2320. usdAmount: inputRatioPrice * audioRatio * audioCompletionRatio,
  2321. rate,
  2322. }),
  2323. buildBillingText(
  2324. '文字提示 {{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}}',
  2325. {
  2326. input: inputTokens,
  2327. completion: completionTokens,
  2328. audioInput: audioInputTokens,
  2329. audioCompletion: audioCompletionTokens,
  2330. textInputPrice: formatBillingDisplayPrice(inputRatioPrice, rate),
  2331. textCompPrice: formatBillingDisplayPrice(completionRatioPrice, rate),
  2332. audioInputPrice: formatBillingDisplayPrice(
  2333. audioRatio * inputRatioPrice,
  2334. rate,
  2335. ),
  2336. audioCompPrice: formatBillingDisplayPrice(
  2337. audioRatio * audioCompletionRatio * inputRatioPrice,
  2338. rate,
  2339. ),
  2340. ratioType: ratioLabel,
  2341. ratio: groupRatio,
  2342. symbol,
  2343. total: formatBillingDisplayPrice(totalPrice, rate),
  2344. },
  2345. ),
  2346. ]);
  2347. }
  2348. // 1 ratio = $0.002 / 1K tokens
  2349. if (modelPrice !== -1) {
  2350. return i18next.t(
  2351. '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}',
  2352. {
  2353. symbol: symbol,
  2354. price: (modelPrice * rate).toFixed(6),
  2355. ratio: groupRatio,
  2356. total: (modelPrice * groupRatio * rate).toFixed(6),
  2357. ratioType: ratioLabel,
  2358. },
  2359. );
  2360. }
  2361. if (completionRatio === undefined) {
  2362. completionRatio = 0;
  2363. }
  2364. const modelRatioValue = formatRatioValue(modelRatio);
  2365. const completionRatioValue = formatRatioValue(completionRatio);
  2366. const cacheRatioValue = formatRatioValue(cacheRatio);
  2367. const audioRatioValue = formatRatioValue(audioRatio);
  2368. const audioCompletionRatioValue = formatRatioValue(audioCompletionRatio);
  2369. const inputRatioPrice = modelRatio * 2.0;
  2370. const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
  2371. const effectiveInputTokens =
  2372. inputTokens - cacheTokens + cacheTokens * cacheRatioValue;
  2373. const textPrice =
  2374. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  2375. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  2376. const audioPrice =
  2377. (audioInputTokens / 1000000) *
  2378. inputRatioPrice *
  2379. audioRatioValue *
  2380. groupRatio +
  2381. (audioCompletionTokens / 1000000) *
  2382. inputRatioPrice *
  2383. audioRatioValue *
  2384. audioCompletionRatioValue *
  2385. groupRatio;
  2386. const totalPrice = textPrice + audioPrice;
  2387. return renderBillingArticle([
  2388. buildBillingText(
  2389. '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}',
  2390. {
  2391. modelRatio: modelRatioValue,
  2392. completionRatio: completionRatioValue,
  2393. audioRatio: audioRatioValue,
  2394. audioCompletionRatio: audioCompletionRatioValue,
  2395. cachePart:
  2396. cacheTokens > 0
  2397. ? `${i18next.t('缓存倍率')} ${cacheRatioValue},`
  2398. : '',
  2399. ratioType: ratioLabel,
  2400. ratio: groupRatio,
  2401. },
  2402. ),
  2403. buildBillingText(
  2404. '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2405. {
  2406. tokens: Math.max(inputTokens - cacheTokens, 0),
  2407. modelRatio: modelRatioValue,
  2408. ratioType: ratioLabel,
  2409. ratio: groupRatio,
  2410. amount: renderDisplayAmountFromUsd(
  2411. (Math.max(inputTokens - cacheTokens, 0) / 1000000) *
  2412. inputRatioPrice *
  2413. groupRatio,
  2414. ),
  2415. },
  2416. ),
  2417. cacheTokens > 0
  2418. ? buildBillingText(
  2419. '缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2420. {
  2421. tokens: cacheTokens,
  2422. modelRatio: modelRatioValue,
  2423. cacheRatio: cacheRatioValue,
  2424. ratioType: ratioLabel,
  2425. ratio: groupRatio,
  2426. amount: renderDisplayAmountFromUsd(
  2427. (cacheTokens / 1000000) *
  2428. inputRatioPrice *
  2429. cacheRatioValue *
  2430. groupRatio,
  2431. ),
  2432. },
  2433. )
  2434. : null,
  2435. buildBillingText(
  2436. '文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2437. {
  2438. tokens: completionTokens,
  2439. modelRatio: modelRatioValue,
  2440. completionRatio: completionRatioValue,
  2441. ratioType: ratioLabel,
  2442. ratio: groupRatio,
  2443. amount: renderDisplayAmountFromUsd(
  2444. (completionTokens / 1000000) *
  2445. inputRatioPrice *
  2446. completionRatioValue *
  2447. groupRatio,
  2448. ),
  2449. },
  2450. ),
  2451. buildBillingText(
  2452. '音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2453. {
  2454. tokens: audioInputTokens,
  2455. modelRatio: modelRatioValue,
  2456. audioRatio: audioRatioValue,
  2457. ratioType: ratioLabel,
  2458. ratio: groupRatio,
  2459. amount: renderDisplayAmountFromUsd(
  2460. (audioInputTokens / 1000000) *
  2461. inputRatioPrice *
  2462. audioRatioValue *
  2463. groupRatio,
  2464. ),
  2465. },
  2466. ),
  2467. buildBillingText(
  2468. '音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2469. {
  2470. tokens: audioCompletionTokens,
  2471. modelRatio: modelRatioValue,
  2472. audioRatio: audioRatioValue,
  2473. audioCompletionRatio: audioCompletionRatioValue,
  2474. ratioType: ratioLabel,
  2475. ratio: groupRatio,
  2476. amount: renderDisplayAmountFromUsd(
  2477. (audioCompletionTokens / 1000000) *
  2478. inputRatioPrice *
  2479. audioRatioValue *
  2480. audioCompletionRatioValue *
  2481. groupRatio,
  2482. ),
  2483. },
  2484. ),
  2485. buildBillingText(
  2486. '合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}',
  2487. {
  2488. textTotal: renderDisplayAmountFromUsd(textPrice),
  2489. audioTotal: renderDisplayAmountFromUsd(audioPrice),
  2490. total: renderDisplayAmountFromUsd(totalPrice),
  2491. },
  2492. ),
  2493. ]);
  2494. }
  2495. export function renderQuotaWithPrompt(quota, digits) {
  2496. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  2497. if (quotaDisplayType !== 'TOKENS') {
  2498. return i18next.t('等价金额:') + renderQuota(quota, digits);
  2499. }
  2500. return '';
  2501. }
  2502. export function renderClaudeModelPrice(opts) {
  2503. const {
  2504. prompt_tokens: inputTokens = 0,
  2505. completion_tokens: completionTokens = 0,
  2506. model_ratio: modelRatio = 0,
  2507. model_price: modelPrice = -1,
  2508. completion_ratio: completionRatio,
  2509. group_ratio: _groupRatio,
  2510. user_group_ratio,
  2511. cache_tokens: cacheTokens = 0,
  2512. cache_ratio: cacheRatio = 1.0,
  2513. cache_creation_tokens: cacheCreationTokens = 0,
  2514. cache_creation_ratio: cacheCreationRatio = 1.0,
  2515. cache_creation_tokens_5m: cacheCreationTokens5m = 0,
  2516. cache_creation_ratio_5m: cacheCreationRatio5m = 1.0,
  2517. cache_creation_tokens_1h: cacheCreationTokens1h = 0,
  2518. cache_creation_ratio_1h: cacheCreationRatio1h = 1.0,
  2519. displayMode = 'price',
  2520. } = opts;
  2521. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  2522. _groupRatio,
  2523. user_group_ratio,
  2524. );
  2525. let groupRatio = effectiveGroupRatio;
  2526. // 获取货币配置
  2527. const { symbol, rate } = getCurrencyConfig();
  2528. if (!shouldUseRatioBillingProcess(modelPrice)) {
  2529. if (modelPrice !== -1) {
  2530. return renderBillingArticle([
  2531. buildBillingPriceText('模型价格:{{symbol}}{{price}} / 次', {
  2532. symbol,
  2533. usdAmount: modelPrice,
  2534. rate,
  2535. }),
  2536. buildBillingPriceText(
  2537. '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
  2538. {
  2539. symbol,
  2540. usdAmount: modelPrice,
  2541. rate,
  2542. ratioType: ratioLabel,
  2543. ratio: groupRatio,
  2544. total: formatBillingDisplayPrice(modelPrice * groupRatio, rate),
  2545. },
  2546. ),
  2547. ]);
  2548. }
  2549. if (completionRatio === undefined) {
  2550. completionRatio = 0;
  2551. }
  2552. const inputRatioPrice = modelRatio * 2.0;
  2553. const completionRatioPrice = modelRatio * 2.0 * completionRatio;
  2554. const cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  2555. const cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
  2556. const cacheCreationRatioPrice5m = modelRatio * 2.0 * cacheCreationRatio5m;
  2557. const cacheCreationRatioPrice1h = modelRatio * 2.0 * cacheCreationRatio1h;
  2558. const hasSplitCacheCreation =
  2559. cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  2560. const legacyCacheCreationTokens = hasSplitCacheCreation
  2561. ? 0
  2562. : cacheCreationTokens;
  2563. const effectiveInputTokens =
  2564. inputTokens +
  2565. cacheTokens * cacheRatio +
  2566. legacyCacheCreationTokens * cacheCreationRatio +
  2567. cacheCreationTokens5m * cacheCreationRatio5m +
  2568. cacheCreationTokens1h * cacheCreationRatio1h;
  2569. const price =
  2570. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  2571. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  2572. const inputUnitPrice = inputRatioPrice * rate;
  2573. const completionUnitPrice = completionRatioPrice * rate;
  2574. const cacheUnitPrice = cacheRatioPrice * rate;
  2575. const cacheCreationUnitPrice = cacheCreationRatioPrice * rate;
  2576. const cacheCreationUnitPrice5m = cacheCreationRatioPrice5m * rate;
  2577. const cacheCreationUnitPrice1h = cacheCreationRatioPrice1h * rate;
  2578. const cacheCreationUnitPriceTotal =
  2579. cacheCreationUnitPrice5m + cacheCreationUnitPrice1h;
  2580. const shouldShowCache = cacheTokens > 0;
  2581. const shouldShowLegacyCacheCreation =
  2582. !hasSplitCacheCreation && cacheCreationTokens > 0;
  2583. const shouldShowCacheCreation5m =
  2584. hasSplitCacheCreation && cacheCreationTokens5m > 0;
  2585. const shouldShowCacheCreation1h =
  2586. hasSplitCacheCreation && cacheCreationTokens1h > 0;
  2587. const breakdownSegments = [
  2588. i18next.t('提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', {
  2589. input: inputTokens,
  2590. symbol,
  2591. price: inputUnitPrice.toFixed(6),
  2592. }),
  2593. ];
  2594. if (shouldShowCache) {
  2595. breakdownSegments.push(
  2596. i18next.t('缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}', {
  2597. tokens: cacheTokens,
  2598. symbol,
  2599. price: cacheUnitPrice.toFixed(6),
  2600. }),
  2601. );
  2602. }
  2603. if (shouldShowLegacyCacheCreation) {
  2604. breakdownSegments.push(
  2605. i18next.t(
  2606. '缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
  2607. {
  2608. tokens: cacheCreationTokens,
  2609. symbol,
  2610. price: cacheCreationUnitPrice.toFixed(6),
  2611. },
  2612. ),
  2613. );
  2614. }
  2615. if (shouldShowCacheCreation5m) {
  2616. breakdownSegments.push(
  2617. i18next.t(
  2618. '5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
  2619. {
  2620. tokens: cacheCreationTokens5m,
  2621. symbol,
  2622. price: cacheCreationUnitPrice5m.toFixed(6),
  2623. },
  2624. ),
  2625. );
  2626. }
  2627. if (shouldShowCacheCreation1h) {
  2628. breakdownSegments.push(
  2629. i18next.t(
  2630. '1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
  2631. {
  2632. tokens: cacheCreationTokens1h,
  2633. symbol,
  2634. price: cacheCreationUnitPrice1h.toFixed(6),
  2635. },
  2636. ),
  2637. );
  2638. }
  2639. breakdownSegments.push(
  2640. i18next.t(
  2641. '补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}',
  2642. {
  2643. completion: completionTokens,
  2644. symbol,
  2645. price: completionUnitPrice.toFixed(6),
  2646. },
  2647. ),
  2648. );
  2649. const breakdownText = breakdownSegments.join(' + ');
  2650. return renderBillingArticle([
  2651. buildBillingPriceText('输入价格:{{symbol}}{{price}} / 1M tokens', {
  2652. symbol,
  2653. usdAmount: inputRatioPrice,
  2654. rate,
  2655. }),
  2656. buildBillingPriceText('输出价格:{{symbol}}{{price}} / 1M tokens', {
  2657. symbol,
  2658. usdAmount: completionRatioPrice,
  2659. rate,
  2660. }),
  2661. cacheTokens > 0
  2662. ? buildBillingPriceText(
  2663. '缓存读取价格:{{symbol}}{{price}} / 1M tokens',
  2664. {
  2665. symbol,
  2666. usdAmount: cacheRatioPrice,
  2667. rate,
  2668. },
  2669. )
  2670. : null,
  2671. !hasSplitCacheCreation && cacheCreationTokens > 0
  2672. ? buildBillingPriceText(
  2673. '缓存创建价格:{{symbol}}{{price}} / 1M tokens',
  2674. {
  2675. symbol,
  2676. usdAmount: cacheCreationRatioPrice,
  2677. rate,
  2678. },
  2679. )
  2680. : null,
  2681. hasSplitCacheCreation && cacheCreationTokens5m > 0
  2682. ? buildBillingPriceText(
  2683. '5m缓存创建价格:{{symbol}}{{price}} / 1M tokens',
  2684. {
  2685. symbol,
  2686. usdAmount: cacheCreationRatioPrice5m,
  2687. rate,
  2688. },
  2689. )
  2690. : null,
  2691. hasSplitCacheCreation && cacheCreationTokens1h > 0
  2692. ? buildBillingPriceText(
  2693. '1h缓存创建价格:{{symbol}}{{price}} / 1M tokens',
  2694. {
  2695. symbol,
  2696. usdAmount: cacheCreationRatioPrice1h,
  2697. rate,
  2698. },
  2699. )
  2700. : null,
  2701. buildBillingText(
  2702. '{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
  2703. {
  2704. breakdown: breakdownText,
  2705. ratioType: ratioLabel,
  2706. ratio: groupRatio,
  2707. symbol,
  2708. total: formatBillingDisplayPrice(price, rate),
  2709. },
  2710. ),
  2711. ]);
  2712. }
  2713. if (modelPrice !== -1) {
  2714. return i18next.t(
  2715. '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}',
  2716. {
  2717. symbol: symbol,
  2718. price: (modelPrice * rate).toFixed(6),
  2719. ratioType: ratioLabel,
  2720. ratio: groupRatio,
  2721. total: (modelPrice * groupRatio * rate).toFixed(6),
  2722. },
  2723. );
  2724. }
  2725. if (completionRatio === undefined) {
  2726. completionRatio = 0;
  2727. }
  2728. const modelRatioValue = formatRatioValue(modelRatio);
  2729. const completionRatioValue = formatRatioValue(completionRatio);
  2730. const cacheRatioValue = formatRatioValue(cacheRatio);
  2731. const cacheCreationRatioValue = formatRatioValue(cacheCreationRatio);
  2732. const cacheCreationRatio5mValue = formatRatioValue(cacheCreationRatio5m);
  2733. const cacheCreationRatio1hValue = formatRatioValue(cacheCreationRatio1h);
  2734. const inputRatioPrice = modelRatio * 2.0;
  2735. const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
  2736. const hasSplitCacheCreation =
  2737. cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  2738. const shouldShowCache = cacheTokens > 0;
  2739. const shouldShowLegacyCacheCreation =
  2740. !hasSplitCacheCreation && cacheCreationTokens > 0;
  2741. const shouldShowCacheCreation5m =
  2742. hasSplitCacheCreation && cacheCreationTokens5m > 0;
  2743. const shouldShowCacheCreation1h =
  2744. hasSplitCacheCreation && cacheCreationTokens1h > 0;
  2745. const legacyCacheCreationTokens = hasSplitCacheCreation
  2746. ? 0
  2747. : cacheCreationTokens;
  2748. const effectiveInputTokens =
  2749. inputTokens +
  2750. cacheTokens * cacheRatioValue +
  2751. legacyCacheCreationTokens * cacheCreationRatioValue +
  2752. cacheCreationTokens5m * cacheCreationRatio5mValue +
  2753. cacheCreationTokens1h * cacheCreationRatio1hValue;
  2754. const totalAmount =
  2755. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  2756. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  2757. return renderBillingArticle([
  2758. buildBillingText(
  2759. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}',
  2760. {
  2761. modelRatio: modelRatioValue,
  2762. completionRatio: completionRatioValue,
  2763. cacheRatio: cacheRatioValue,
  2764. ratioType: ratioLabel,
  2765. ratio: groupRatio,
  2766. },
  2767. ),
  2768. hasSplitCacheCreation
  2769. ? buildBillingText(
  2770. '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
  2771. {
  2772. cacheCreationRatio5m: cacheCreationRatio5mValue,
  2773. cacheCreationRatio1h: cacheCreationRatio1hValue,
  2774. },
  2775. )
  2776. : buildBillingText('缓存创建倍率 {{cacheCreationRatio}}', {
  2777. cacheCreationRatio: cacheCreationRatioValue,
  2778. }),
  2779. buildBillingText(
  2780. '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2781. {
  2782. tokens: inputTokens,
  2783. modelRatio: modelRatioValue,
  2784. ratioType: ratioLabel,
  2785. ratio: groupRatio,
  2786. amount: renderDisplayAmountFromUsd(
  2787. (inputTokens / 1000000) * inputRatioPrice * groupRatio,
  2788. ),
  2789. },
  2790. ),
  2791. shouldShowCache
  2792. ? buildBillingText(
  2793. '缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2794. {
  2795. tokens: cacheTokens,
  2796. modelRatio: modelRatioValue,
  2797. cacheRatio: cacheRatioValue,
  2798. ratioType: ratioLabel,
  2799. ratio: groupRatio,
  2800. amount: renderDisplayAmountFromUsd(
  2801. (cacheTokens / 1000000) *
  2802. inputRatioPrice *
  2803. cacheRatioValue *
  2804. groupRatio,
  2805. ),
  2806. },
  2807. )
  2808. : null,
  2809. shouldShowLegacyCacheCreation
  2810. ? buildBillingText(
  2811. '缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2812. {
  2813. tokens: cacheCreationTokens,
  2814. modelRatio: modelRatioValue,
  2815. cacheCreationRatio: cacheCreationRatioValue,
  2816. ratioType: ratioLabel,
  2817. ratio: groupRatio,
  2818. amount: renderDisplayAmountFromUsd(
  2819. (cacheCreationTokens / 1000000) *
  2820. inputRatioPrice *
  2821. cacheCreationRatioValue *
  2822. groupRatio,
  2823. ),
  2824. },
  2825. )
  2826. : null,
  2827. shouldShowCacheCreation5m
  2828. ? buildBillingText(
  2829. '5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}',
  2830. {
  2831. tokens: cacheCreationTokens5m,
  2832. modelRatio: modelRatioValue,
  2833. cacheCreationRatio5m: cacheCreationRatio5mValue,
  2834. ratioType: ratioLabel,
  2835. ratio: groupRatio,
  2836. amount: renderDisplayAmountFromUsd(
  2837. (cacheCreationTokens5m / 1000000) *
  2838. inputRatioPrice *
  2839. cacheCreationRatio5mValue *
  2840. groupRatio,
  2841. ),
  2842. },
  2843. )
  2844. : null,
  2845. shouldShowCacheCreation1h
  2846. ? buildBillingText(
  2847. '1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}',
  2848. {
  2849. tokens: cacheCreationTokens1h,
  2850. modelRatio: modelRatioValue,
  2851. cacheCreationRatio1h: cacheCreationRatio1hValue,
  2852. ratioType: ratioLabel,
  2853. ratio: groupRatio,
  2854. amount: renderDisplayAmountFromUsd(
  2855. (cacheCreationTokens1h / 1000000) *
  2856. inputRatioPrice *
  2857. cacheCreationRatio1hValue *
  2858. groupRatio,
  2859. ),
  2860. },
  2861. )
  2862. : null,
  2863. buildBillingText(
  2864. '补全 {{completion}} tokens * 输出倍率 {{completionRatio}}',
  2865. {
  2866. completion: completionTokens,
  2867. completionRatio: completionRatioValue,
  2868. },
  2869. ),
  2870. buildBillingText(
  2871. '输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
  2872. {
  2873. tokens: completionTokens,
  2874. modelRatio: modelRatioValue,
  2875. completionRatio: completionRatioValue,
  2876. ratioType: ratioLabel,
  2877. ratio: groupRatio,
  2878. amount: renderDisplayAmountFromUsd(
  2879. (completionTokens / 1000000) *
  2880. inputRatioPrice *
  2881. completionRatioValue *
  2882. groupRatio,
  2883. ),
  2884. },
  2885. ),
  2886. buildBillingText('合计:{{total}}', {
  2887. total: renderDisplayAmountFromUsd(totalAmount),
  2888. }),
  2889. ]);
  2890. }
  2891. export function renderClaudeLogContent(opts) {
  2892. const {
  2893. model_ratio: modelRatio,
  2894. completion_ratio: completionRatio,
  2895. model_price: modelPrice = -1,
  2896. group_ratio: _groupRatio,
  2897. user_group_ratio,
  2898. cache_ratio: cacheRatio = 1.0,
  2899. cache_creation_ratio: cacheCreationRatio = 1.0,
  2900. cache_creation_tokens_5m: cacheCreationTokens5m = 0,
  2901. cache_creation_ratio_5m: cacheCreationRatio5m = 1.0,
  2902. cache_creation_tokens_1h: cacheCreationTokens1h = 0,
  2903. cache_creation_ratio_1h: cacheCreationRatio1h = 1.0,
  2904. displayMode = 'price',
  2905. } = opts;
  2906. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  2907. _groupRatio,
  2908. user_group_ratio,
  2909. );
  2910. let groupRatio = effectiveGroupRatio;
  2911. // 获取货币配置
  2912. const { symbol, rate } = getCurrencyConfig();
  2913. if (isPriceDisplayMode(displayMode, modelPrice)) {
  2914. if (modelPrice !== -1) {
  2915. return joinBillingSummary([
  2916. i18next.t('模型价格 {{symbol}}{{price}} / 次', {
  2917. symbol,
  2918. price: (modelPrice * rate).toFixed(6),
  2919. }),
  2920. getGroupRatioText(groupRatio, user_group_ratio),
  2921. ]);
  2922. }
  2923. const parts = [
  2924. i18next.t('输入价格 {{symbol}}{{price}} / 1M tokens', {
  2925. symbol,
  2926. price: (modelRatio * 2.0 * rate).toFixed(6),
  2927. }),
  2928. i18next.t('输出价格 {{symbol}}{{price}} / 1M tokens', {
  2929. symbol,
  2930. price: (modelRatio * 2.0 * completionRatio * rate).toFixed(6),
  2931. }),
  2932. i18next.t('缓存读取价格 {{symbol}}{{price}} / 1M tokens', {
  2933. symbol,
  2934. price: (modelRatio * 2.0 * cacheRatio * rate).toFixed(6),
  2935. }),
  2936. ];
  2937. const hasSplitCacheCreation =
  2938. cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  2939. appendPricePart(
  2940. parts,
  2941. hasSplitCacheCreation && cacheCreationTokens5m > 0,
  2942. '5m缓存创建价格 {{symbol}}{{price}} / 1M tokens',
  2943. {
  2944. symbol,
  2945. price: (modelRatio * 2.0 * cacheCreationRatio5m * rate).toFixed(6),
  2946. },
  2947. );
  2948. appendPricePart(
  2949. parts,
  2950. hasSplitCacheCreation && cacheCreationTokens1h > 0,
  2951. '1h缓存创建价格 {{symbol}}{{price}} / 1M tokens',
  2952. {
  2953. symbol,
  2954. price: (modelRatio * 2.0 * cacheCreationRatio1h * rate).toFixed(6),
  2955. },
  2956. );
  2957. appendPricePart(
  2958. parts,
  2959. !hasSplitCacheCreation,
  2960. '缓存创建价格 {{symbol}}{{price}} / 1M tokens',
  2961. {
  2962. symbol,
  2963. price: (modelRatio * 2.0 * cacheCreationRatio * rate).toFixed(6),
  2964. },
  2965. );
  2966. parts.push(getGroupRatioText(groupRatio, user_group_ratio));
  2967. return joinBillingSummary(parts);
  2968. }
  2969. if (modelPrice !== -1) {
  2970. return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', {
  2971. symbol: symbol,
  2972. price: (modelPrice * rate).toFixed(6),
  2973. ratioType: ratioLabel,
  2974. ratio: groupRatio,
  2975. });
  2976. } else {
  2977. const hasSplitCacheCreation =
  2978. cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  2979. const shouldShowCacheCreation5m =
  2980. hasSplitCacheCreation && cacheCreationTokens5m > 0;
  2981. const shouldShowCacheCreation1h =
  2982. hasSplitCacheCreation && cacheCreationTokens1h > 0;
  2983. let cacheCreationPart = null;
  2984. if (hasSplitCacheCreation) {
  2985. if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
  2986. cacheCreationPart = i18next.t(
  2987. '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
  2988. {
  2989. cacheCreationRatio5m,
  2990. cacheCreationRatio1h,
  2991. },
  2992. );
  2993. } else if (shouldShowCacheCreation5m) {
  2994. cacheCreationPart = i18next.t(
  2995. '缓存创建倍率 5m {{cacheCreationRatio5m}}',
  2996. {
  2997. cacheCreationRatio5m,
  2998. },
  2999. );
  3000. } else if (shouldShowCacheCreation1h) {
  3001. cacheCreationPart = i18next.t(
  3002. '缓存创建倍率 1h {{cacheCreationRatio1h}}',
  3003. {
  3004. cacheCreationRatio1h,
  3005. },
  3006. );
  3007. }
  3008. }
  3009. if (!cacheCreationPart) {
  3010. cacheCreationPart = i18next.t('缓存创建倍率 {{cacheCreationRatio}}', {
  3011. cacheCreationRatio,
  3012. });
  3013. }
  3014. const parts = [
  3015. i18next.t('模型倍率 {{modelRatio}}', { modelRatio }),
  3016. i18next.t('输出倍率 {{completionRatio}}', { completionRatio }),
  3017. i18next.t('缓存倍率 {{cacheRatio}}', { cacheRatio }),
  3018. cacheCreationPart,
  3019. i18next.t('{{ratioType}} {{ratio}}', {
  3020. ratioType: ratioLabel,
  3021. ratio: groupRatio,
  3022. }),
  3023. ];
  3024. return parts.join(',');
  3025. }
  3026. }
  3027. // 已统一至 renderModelPriceSimple,若仍有遗留引用,请改为传入 provider='claude'
  3028. /**
  3029. * rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
  3030. * 仅在流式渲染阶段使用,避免已渲染文字重复动画。
  3031. */
  3032. export function rehypeSplitWordsIntoSpans(options = {}) {
  3033. const { previousContentLength = 0 } = options;
  3034. return (tree) => {
  3035. let currentCharCount = 0; // 当前已处理的字符数
  3036. visit(tree, 'element', (node) => {
  3037. if (
  3038. ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(
  3039. node.tagName,
  3040. ) &&
  3041. node.children
  3042. ) {
  3043. const newChildren = [];
  3044. node.children.forEach((child) => {
  3045. if (child.type === 'text') {
  3046. try {
  3047. // 使用 Intl.Segmenter 精准拆分中英文及标点
  3048. const segmenter = new Intl.Segmenter('zh', {
  3049. granularity: 'word',
  3050. });
  3051. const segments = segmenter.segment(child.value);
  3052. Array.from(segments)
  3053. .map((seg) => seg.segment)
  3054. .filter(Boolean)
  3055. .forEach((word) => {
  3056. const wordStartPos = currentCharCount;
  3057. const wordEndPos = currentCharCount + word.length;
  3058. // 判断这个词是否是新增的(在 previousContentLength 之后)
  3059. const isNewContent = wordStartPos >= previousContentLength;
  3060. newChildren.push({
  3061. type: 'element',
  3062. tagName: 'span',
  3063. properties: {
  3064. className: isNewContent ? ['animate-fade-in'] : [],
  3065. },
  3066. children: [{ type: 'text', value: word }],
  3067. });
  3068. currentCharCount = wordEndPos;
  3069. });
  3070. } catch (_) {
  3071. // Fallback:如果浏览器不支持 Segmenter
  3072. const textStartPos = currentCharCount;
  3073. const isNewContent = textStartPos >= previousContentLength;
  3074. if (isNewContent) {
  3075. // 新内容,添加动画
  3076. newChildren.push({
  3077. type: 'element',
  3078. tagName: 'span',
  3079. properties: {
  3080. className: ['animate-fade-in'],
  3081. },
  3082. children: [{ type: 'text', value: child.value }],
  3083. });
  3084. } else {
  3085. // 旧内容,不添加动画
  3086. newChildren.push(child);
  3087. }
  3088. currentCharCount += child.value.length;
  3089. }
  3090. } else {
  3091. newChildren.push(child);
  3092. }
  3093. });
  3094. node.children = newChildren;
  3095. }
  3096. });
  3097. };
  3098. }