ezllm 0.0.26 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/cli.js +24512 -65
- package/dist/server.js +41850 -202
- package/dist/web-ui/assets/DashboardView-ChJpgKis.js +1 -0
- package/dist/web-ui/assets/DashboardView-DOtZtkUN.css +1 -0
- package/dist/web-ui/assets/{LayoutView-CZWTEtiI.js → LayoutView-C8YPw0x2.js} +1 -1
- package/dist/web-ui/assets/{LoginView-Crwq9D2r.js → LoginView-C4hkT-3T.js} +1 -1
- package/dist/web-ui/assets/{LogsView-CVnmnD7w.js → LogsView-DdS8qKEs.js} +1 -1
- package/dist/web-ui/assets/ProvidersView-BD33caY8.js +14 -0
- package/dist/web-ui/assets/ProvidersView-DNqX_AWw.css +1 -0
- package/dist/web-ui/assets/{ServiceStatusView-DnJ_x40Z.js → ServiceStatusView-BtF40E0V.js} +1 -1
- package/dist/web-ui/assets/{SettingsView-DFGf2qGB.js → SettingsView-SdL8ednl.js} +1 -1
- package/dist/web-ui/assets/{SetupView-DPfeIQ8u.js → SetupView-BZFBBRSd.js} +1 -1
- package/dist/web-ui/assets/{config-BLeVzAng.js → config-Dbms19Et.js} +1 -1
- package/dist/web-ui/assets/{index-D8q_jlRU.js → index-C6wv4UB9.js} +1 -1
- package/dist/web-ui/assets/{message-CPl1jEnd.js → message-CEBSyhlN.js} +1 -1
- package/dist/web-ui/assets/{providers-CrgmFKCl.js → providers-DkujdEZ8.js} +1 -1
- package/dist/web-ui/assets/quota-help-fetch-node-S7ELzwnj.png +0 -0
- package/dist/web-ui/assets/{router-CjERSWL9.js → router-BqkHX02I.js} +2 -2
- package/dist/web-ui/assets/{useLoadError-CKk_ZYc1.js → useLoadError-Yu7YH4ZH.js} +1 -1
- package/dist/web-ui/assets/{useManagerApi-BKQpMU8c.js → useManagerApi-fVrRDZkc.js} +1 -1
- package/dist/web-ui/assets/useToast-DJwG-33L.js +1 -0
- package/dist/web-ui/index.html +3 -3
- package/docs/DESIGN.md +2 -2
- package/hook.example.js +176 -0
- package/package.json +3 -2
- package/dist/web-ui/assets/DashboardView-CAv7TPGt.css +0 -1
- package/dist/web-ui/assets/DashboardView-CjEo1VeN.js +0 -1
- package/dist/web-ui/assets/ProvidersView-BOLgh-Q7.js +0 -14
- package/dist/web-ui/assets/ProvidersView-Bi_ojHNQ.css +0 -1
- package/dist/web-ui/assets/quota-pWFONauN.js +0 -1
- package/dist/web-ui/assets/useToast-BrWn2CV2.js +0 -1
package/hook.example.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ezllm 用户请求钩子(示例)
|
|
3
|
+
* ============================================================================
|
|
4
|
+
*
|
|
5
|
+
* 用法:把本文件复制为 appDir 下的钩子文件并按需修改,然后**重启服务**生效。
|
|
6
|
+
* - 发布包安装:appDir = ~/.ezllm → 复制为 ~/.ezllm/hook.cjs
|
|
7
|
+
* - 仓库开发模式:appDir = packages/server → 复制为 packages/server/hook.cjs
|
|
8
|
+
* ⚠️ 本包 package.json 含 "type": "module",`.js` 会被当 ESM 解析,而本示例是 CJS
|
|
9
|
+
* (module.exports)——**开发模式下必须用 hook.cjs**,否则报
|
|
10
|
+
* "module is not defined in ES module scope"。
|
|
11
|
+
* 文件不存在 = 不挂任何钩子,按原样运行。加载失败只告警、不影响网关启动。
|
|
12
|
+
*
|
|
13
|
+
* 候选文件名(按序取第一个存在的):hook.cjs → hook.mjs → hook.js
|
|
14
|
+
* **优先用 .cjs**:`.js` 的解析模式(CJS 还是 ESM)由它**祖先目录里最近的 package.json**
|
|
15
|
+
* 的 type 字段决定——不一定是 appDir 自己的那份(Node 逐级向上找,家目录常有一份)。
|
|
16
|
+
* 也就是说 `.js` 的行为用户改不动、也未必知晓:`~/.ezllm` 自身没有 package.json,但若
|
|
17
|
+
* `~` 下存在一份带 "type": "module" 的(用户跑过 npm init 并设过 type),`hook.js` 里的
|
|
18
|
+
* CJS 写法就会静默失效。`.cjs` / `.mjs` 强制解析模式、不受任何 package.json 影响,
|
|
19
|
+
* 两个模式都稳。
|
|
20
|
+
*
|
|
21
|
+
* 本示例是 CJS(module.exports);也支持 ESM 的 `export default { ... }`(放 .mjs)。
|
|
22
|
+
*
|
|
23
|
+
* 唯一可用的钩子是 onModelRequest(发上游前改写请求报文)。刻意不提供响应侧钩子:
|
|
24
|
+
* 流式响应当前无帧级改写入口(onModelResponse 的 body 流式恒为 null),暴露「能改」会
|
|
25
|
+
* 与事实不符。控制型钩子(onAuthorize/onModel 等)也不对用户开放。
|
|
26
|
+
*
|
|
27
|
+
* 关于 ctx.request.body 的形状
|
|
28
|
+
* ----------------------------------------------------------------------------
|
|
29
|
+
* **形状取决于入站端点,不是上游协议**:
|
|
30
|
+
* POST /v1/chat/completions 和 /chat/completions → OpenAI Chat 形状(messages/tools)
|
|
31
|
+
* POST /v1/messages 和 /messages → Anthropic 形状(system/messages/tools)
|
|
32
|
+
* POST /v1/responses 和 /responses → **本钩子不触发**(该入口不跑 hook 管线)
|
|
33
|
+
* 所以规则里判断字段时建议两种形状都兜住(见下方 stripToolDocs 的 tools 归一)。
|
|
34
|
+
*
|
|
35
|
+
* 三条硬约束(改坏会踩项目已知的语义坑)
|
|
36
|
+
* ----------------------------------------------------------------------------
|
|
37
|
+
* 1. **凡改动必须内容稳定**。别插入时间戳、随机 id、请求计数——被改写的那一段一变,
|
|
38
|
+
* 上游的缓存前缀就断,每轮都会 miss(缓存命中率价值通常远大于你省下的那几个 token)。
|
|
39
|
+
* 裁掉固定段落是安全的。
|
|
40
|
+
* 2. **改了 Anthropic 入站体 = 请求侧字节透传失效**。双 anthropic 链路写死
|
|
41
|
+
* `anthropic_passthrough: true`,原生 tools 的 {name,input_schema} 与 thinking
|
|
42
|
+
* signature 会改走 JSON 往返,可能丢语义(架构地图坑表 #9)。OpenAI 入站
|
|
43
|
+
* (chat/completions)本来就是 JSON 解析,改它零风险。
|
|
44
|
+
* 3. **本钩子抛错会让该请求失败**(onModelRequest 是"修改型"钩子,异常不被隔离)。
|
|
45
|
+
* 所以每个函数都要能容忍形状不符:类型不对就原样返回,别抛。
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
"use strict";
|
|
49
|
+
|
|
50
|
+
/** 只在 description 大到足以塞下文档时才处理,避免误伤正常的短描述 */
|
|
51
|
+
const DOC_BLOCK_MIN_BYTES = 2000;
|
|
52
|
+
|
|
53
|
+
/** 按字节估算:本文件的规则只做裁剪,不要求精确 */
|
|
54
|
+
function byteLength(s) {
|
|
55
|
+
return Buffer.byteLength(s, "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 裁掉工具定义里内嵌的整套 API 文档代码块(``` 围栏块),只留下散文说明。
|
|
60
|
+
*
|
|
61
|
+
* 动机(实测):某客户端把同一份 19KB 的 façade API 手册**逐字重复**塞进
|
|
62
|
+
* CreateWorkflow 与 SaveWorkflow 两个工具的描述里,EvalWorkflowSnippet 又贴了其中
|
|
63
|
+
* 7.3KB。三处合计 26.5KB(约占该请求总量的 12%),而同一份 façade 在这三个工具对应的
|
|
64
|
+
* skill 文档里有完整副本、按需可读。
|
|
65
|
+
*
|
|
66
|
+
* 为什么可以安全裁掉:façade 是「怎么用」的参考,不是「要不要用」的判据;调用方需要时
|
|
67
|
+
* 会去读那份 skill 文档(documents/zcode-guide 的 dynamic-workflows 之类)。若你的
|
|
68
|
+
* 部署里没有该 skill 副本,请勿开启这条规则。
|
|
69
|
+
*
|
|
70
|
+
* @param {string} description 工具描述原文
|
|
71
|
+
* @returns {string} 裁掉围栏代码块后的描述;无大块可裁时原样返回
|
|
72
|
+
*/
|
|
73
|
+
function stripToolDocBlocks(description) {
|
|
74
|
+
if (typeof description !== "string") return description;
|
|
75
|
+
if (!description.includes("```")) return description;
|
|
76
|
+
const blocks = description.match(/```[\s\S]*?```/g) ?? [];
|
|
77
|
+
const bigBlocks = blocks.filter((b) => byteLength(b) >= DOC_BLOCK_MIN_BYTES);
|
|
78
|
+
if (bigBlocks.length === 0) return description;
|
|
79
|
+
let out = description;
|
|
80
|
+
for (const b of bigBlocks) out = out.replace(b, "");
|
|
81
|
+
return out.replace(/\n{3,}/g, "\n\n").trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 归一 reasoning 字段冲突:`reasoning_effort` 与 `reasoning.effort` 并存时删掉嵌套的
|
|
86
|
+
* `reasoning`,只留 `reasoning_effort`。
|
|
87
|
+
*
|
|
88
|
+
* 动机(实测):ZCode 3.14.1 的内置模板 endpoint-*.json 里,一条**通用 chat 规则**的
|
|
89
|
+
* `reasoningLevel.map` 同时产出 `reasoning_effort` 与 `reasoning:{effort}` 两个字段(模板
|
|
90
|
+
* 写重)。商汤(api.senseaudio.cn,Bifrost 框架)对该 schema 校验严格,两者并存即 400
|
|
91
|
+
* `invalid request format: both reasoning_effort and reasoning.effort cannot be present
|
|
92
|
+
* at the same time`——请求未达模型层,token 全 0。本机实测 15 条此错。
|
|
93
|
+
*
|
|
94
|
+
* 只在该后缀端点(/chat/completions)生效:`reasoning` 是 OpenAI **Responses** API 的
|
|
95
|
+
* 规范字段(Responses 客户端会合法携带),而本钩子在 /v1/responses 入口根本不触发——
|
|
96
|
+
* 但非 /v1/responses 的路径若带着 `reasoning`(非 OpenAI 规范的用法),一并归一是安全的。
|
|
97
|
+
*
|
|
98
|
+
* 为什么保留 `reasoning_effort` 而不是 `reasoning`(实测依据,不可凭直觉反选):
|
|
99
|
+
* 同链(st/glm-5.3-flash × /v1/chat/completions)200 条成功请求里,
|
|
100
|
+
* **192 条只带 reasoning_effort**、7 条两者都无、**仅 1 条只带 reasoning**。
|
|
101
|
+
* 两个字段单独出现上游都接受,故删任一都能修好;选 `reasoning_effort` 是因为:
|
|
102
|
+
* ① 上游接受度 192:1;② ZCode 对该模型的专属规则(providerSiteRules 的
|
|
103
|
+
* `modelMatch: glm-5\.3-flash`)本来就只产 `reasoning_effort`,保留它才与该客户端
|
|
104
|
+
* 在别的 baseUrl 下的行为一致;③ 它是 OpenAI Chat 的规范字段(`reasoning` 属 Responses)。
|
|
105
|
+
*
|
|
106
|
+
* **仅在并存时动手**——单独出现的任一种原样透传,不做「规范化改写」(无必要的改写会
|
|
107
|
+
* 增加行为面)。
|
|
108
|
+
*
|
|
109
|
+
* @param {object} body 请求体(原地改)
|
|
110
|
+
*/
|
|
111
|
+
function normalizeReasoningEffort(body) {
|
|
112
|
+
if (!("reasoning_effort" in body)) return;
|
|
113
|
+
const r = body.reasoning;
|
|
114
|
+
if (r === null || typeof r !== "object" || Array.isArray(r)) return;
|
|
115
|
+
if (!("effort" in r)) return;
|
|
116
|
+
delete body.reasoning;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 按名字裁掉整份工具定义(重型、当前会话大概率用不到的工具)。
|
|
121
|
+
*
|
|
122
|
+
* 默认关闭:**删工具会连带删掉模型"知道它存在"的能力**——模型不会再主动用被删的工具。
|
|
123
|
+
* 只有确定该部署/该客户端不需要时才打开。改名字列表即可。
|
|
124
|
+
*/
|
|
125
|
+
const DROP_TOOL_NAMES = [
|
|
126
|
+
// "CreateWorkflow",
|
|
127
|
+
// "SaveWorkflow",
|
|
128
|
+
// "EvalWorkflowSnippet",
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 工具数组归一 + 就地处理。
|
|
133
|
+
* 兼容两种形状:OpenAI({type:"function", function:{name,description}})与
|
|
134
|
+
* Anthropic({name, description, input_schema})。
|
|
135
|
+
*/
|
|
136
|
+
function processTools(tools) {
|
|
137
|
+
if (!Array.isArray(tools)) return;
|
|
138
|
+
for (let i = tools.length - 1; i >= 0; i--) {
|
|
139
|
+
const t = tools[i];
|
|
140
|
+
if (t === null || typeof t !== "object") continue;
|
|
141
|
+
const def = typeof t.function === "object" && t.function !== null ? t.function : t;
|
|
142
|
+
const name = typeof def.name === "string" ? def.name : "";
|
|
143
|
+
if (DROP_TOOL_NAMES.includes(name)) {
|
|
144
|
+
tools.splice(i, 1);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
def.description = stripToolDocBlocks(def.description);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 请求钩子:发上游前改写 ctx.request.body / ctx.request.headers。
|
|
153
|
+
*
|
|
154
|
+
* 入参 ctx(UpstreamRequestContext)关键字段:
|
|
155
|
+
* ctx.request.body 请求体(**原地改**即生效)
|
|
156
|
+
* ctx.request.headers 追加/覆盖上游请求头(黑名单头写不进去,见下)
|
|
157
|
+
* ctx.model { name, provider, upstreamModel, apiBase }
|
|
158
|
+
* ctx.requestId / ctx.requestModel / ctx.userAgent
|
|
159
|
+
*
|
|
160
|
+
* 注意:authorization / x-api-key / cookie / proxy-* / x-forwarded-* 属黑名单,
|
|
161
|
+
* 由协议层统一剥离、最终凭据仍来自部署配置。想改**上游凭据**请改配置文件,不是这里。
|
|
162
|
+
*
|
|
163
|
+
* @param {object} ctx 上游请求上下文
|
|
164
|
+
*/
|
|
165
|
+
function onModelRequest(ctx) {
|
|
166
|
+
const body = ctx && ctx.request && ctx.request.body;
|
|
167
|
+
if (body === null || typeof body !== "object") return;
|
|
168
|
+
|
|
169
|
+
// —— 规则 1:归一 reasoning 字段冲突,修上游 400(默认开启;见函数注释的实测依据)——
|
|
170
|
+
normalizeReasoningEffort(body);
|
|
171
|
+
|
|
172
|
+
// —— 规则 2:裁掉工具定义里内嵌的 API 文档代码块(默认开启,见函数注释确认前提)——
|
|
173
|
+
processTools(body.tools);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { onModelRequest };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ezllm",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
4
|
"description": "Self-hosted LLM gateway with admin web UI (single process, built-in process management)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"dist",
|
|
30
30
|
"docs",
|
|
31
31
|
"ezllm.config.example.json",
|
|
32
|
+
"hook.example.js",
|
|
32
33
|
"README.md",
|
|
33
34
|
"LICENSE"
|
|
34
35
|
],
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"@types/node": "^22.20.1",
|
|
51
52
|
"@vitest/coverage-v8": "5.0.1",
|
|
52
53
|
"tsx": "^4.23.12",
|
|
53
|
-
"typescript": "^
|
|
54
|
+
"typescript": "^7.0.2",
|
|
54
55
|
"vite": "^8.3.0",
|
|
55
56
|
"vitest": "^5.0.1"
|
|
56
57
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.copy-text[data-v-dc65c386]{color:#409eff;cursor:pointer}.copy-text[data-v-dc65c386]:hover{text-decoration:underline}.quota-cap[data-v-dc65c386]{color:#909399;font-size:12px}.quota-reset[data-v-dc65c386]{color:#c0c4cc;font-size:11px}.quota-exceeded[data-v-dc65c386]{color:#f56c6c;font-weight:600}.quota-empty[data-v-dc65c386]{color:#c0c4cc}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{$ as e,A as t,Et as n,F as r,Ft as i,Gt as a,H as o,I as s,L as c,M as l,N as u,Q as d,T as f,a as p,at as m,cn as ee,d as h,gt as te,ht as g,j as _,k as v,ot as ne,pn as y,rt as b,tt as x,yt as S}from"./router-CjERSWL9.js";import{t as C}from"./useManagerApi-BKQpMU8c.js";import{t as w}from"./providers-CrgmFKCl.js";import{t as T}from"./quota-pWFONauN.js";import{t as E}from"./useToast-BrWn2CV2.js";import{t as D}from"./useLoadError-CKk_ZYc1.js";import{t as O}from"./latestRequest-Dk21IcrH.js";import{t as re}from"./clipboard-CorU9X8e.js";import{a as ie,o as k,r as ae,t as oe}from"./format-DrbP4_cY.js";S();function se(e){let{loadError:t,run:r}=D(e),i=n(null);function a(e){let t=async()=>{let n=await r(e);return n?i.value===t&&(i.value=null):i.value=t,n};return t}function o(e){(i.value??e)()}return{loadError:t,track:a,retry:o}}o(),S(),a();var ce={class:`flex-col-gap`},le=[`onClick`],ue={key:1},de={class:`flex-between`},fe={class:`quota-cap`},pe={class:`quota-reset`},me={key:1,class:`quota-empty`},he={key:0,class:`test-fail-text`},ge={key:1},_e={class:`flex-between`},ve={class:`flex-gap-8`},ye={class:`usage-grid`},be={class:`usage-cell`},A={class:`usage-num orange`},j={class:`usage-cell`},M={class:`usage-num blue`},N={class:`usage-cell`},xe={class:`usage-num green`},Se={class:`usage-cell`},Ce={class:`usage-num purple`},we={class:`usage-cell`},Te={class:`usage-num purple`},Ee={key:0,class:`usage-cell`},De={class:`usage-num gray`},Oe={key:0},ke=[`onClick`],P=h(c({__name:`DashboardView`,setup(a){let o=w(),c=T(),{toast:h}=E(),{loadError:S,track:D,retry:P}=se(),F=n(null),I=n(!0),L=n(``),R=n(`today`),z=e=>`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,`0`)}-${String(e.getDate()).padStart(2,`0`)}`,B=z(new Date),V=n([B,B]),Ae=e=>{let t=new Date,n,r=t;if(e===`today`)n=t;else if(e===`yesterday`)n=new Date(t),n.setDate(t.getDate()-1),r=n;else if(e===`beforeYesterday`)n=new Date(t),n.setDate(t.getDate()-2),r=n;else if(e===`last3days`)n=new Date(t),n.setDate(t.getDate()-2);else if(e===`week`){n=new Date(t);let e=(t.getDay()+6)%7;n.setDate(t.getDate()-e)}else if(e===`month`)n=new Date(t.getFullYear(),t.getMonth(),1);else return;R.value=e,V.value=[z(n),z(r)],H()},je=e=>{e?Ae(e):(R.value=``,V.value=null,H())},Me=()=>{R.value=``,H()},Ne=v(()=>{let e=new Set;for(let t of o.suppliers)for(let n of t.models)e.add(n.modelName);if(F.value?.models)for(let t of F.value.models)e.add(t.model);return[...e].sort()}),Pe=O(),H=D(async()=>{let e=Pe();I.value=!0;try{let t=new URLSearchParams;L.value&&t.set(`model`,L.value),V.value?.length===2&&(t.set(`startDate`,V.value[0]),t.set(`endDate`,V.value[1]));let n=t.toString(),r=await p.get(`/api/usage${n?`?${n}`:``}`);if(!e())return;F.value=r}catch(t){if(!e())return;throw t}finally{e()&&(I.value=!1)}}),Fe=O(),U=D(async()=>{let e=Fe();try{await Promise.all([H(),o.loadProviders()])}catch(t){if(!e())return;throw t}finally{e()&&(I.value=!1)}}),Ie=()=>P(U),Le=window.location.hostname,Re=Number(window.location.port)||null,ze=v(()=>oe({protocol:window.location.protocol,hostname:Le,port:Re})),W=n(null),G=n(null),K=n(!1),q=n(Date.now()),J=async()=>{try{let[e,t]=await Promise.all([C.processes(),p.get(`/api/config`).catch(()=>null)]);W.value=e[0]??null,K.value=t===null,G.value=t?.masterKey??``,q.value=Date.now()}catch{}},Be=e=>{if(!e.startedAt)return`-`;let t=Math.max(0,Math.floor((q.value-e.startedAt)/1e3)),n=Math.floor(t/86400);t-=n*86400;let r=Math.floor(t/3600);t-=r*3600;let i=Math.floor(t/60);return t-=i*60,[n?`${n}天`:``,r?`${r}时`:``,i?`${i}分`:``,`${t}秒`].filter(Boolean).join(` `)},Ve=v(()=>{let e=[{key:`API 地址`,value:ze.value},{key:`API 密钥`,value:K.value?`获取失败,请刷新重试`:G.value||`未设置(/v1 免鉴权)`},{key:`供应商数量`,value:String(o.providerCount)},{key:`模型总数`,value:String(o.totalModelCount)}],t=W.value;return t&&e.push({key:`运行状态`,value:`运行中`},{key:`PID`,value:String(t.pid??`-`)},{key:`运行时长`,value:Be(t)},{key:`内存占用`,value:ie(t.memory)}),e}),He=[{key:`fiveHour`,label:`5 小时额度`},{key:`weekly`,label:`周额度`},{key:`monthly`,label:`月额度`}],Y=v(()=>{let e=[];for(let t of o.suppliers){let n=c.entryOf(t.name);if(!n||!n.snapshot)continue;let r=n.snapshot;e.push({name:t.name,fiveHour:r.fiveHour,weekly:r.weekly,monthly:r.monthly,totalTokens:r.totalTokens,totalRuns:r.totalRuns,error:n.error??``,fetchedAtText:n.fetchedAt?new Date(n.fetchedAt).toLocaleString():`-`})}return e}),X=e=>Number.isInteger(e)?String(e):e.toFixed(2),Ue=e=>{if(e===null||e<=0)return`按账期结算`;let t=e-q.value;if(t<=0)return`即将重置`;let n=t/36e5;return n<48?`${n.toFixed(1)} 小时后重置`:`${(n/24).toFixed(1)} 天后重置`},Z=D(async()=>{await c.load()}),Q,$=async e=>{await re(e)?h(`已复制到剪贴板`,`success`):h(`复制失败`,`error`)};return d(()=>{U(),J(),Z(),Q=window.setInterval(J,1e4)}),e(()=>window.clearInterval(Q)),(e,n)=>{let a=m(`el-button`),o=m(`el-alert`),d=m(`el-table-column`),p=m(`el-table`),h=m(`el-card`),v=m(`el-option`),C=m(`el-select`),w=m(`el-date-picker`),T=m(`el-tag`),E=m(`el-empty`),D=ne(`loading`);return x(),u(`div`,ce,[i(S)?(x(),_(o,{key:0,title:i(S),type:`error`,"show-icon":``,closable:!1},{default:g(()=>[s(a,{size:`small`,onClick:Ie},{default:g(()=>[...n[3]||=[r(`重试`,-1)]]),_:1})]),_:1},8,[`title`])):l(``,!0),s(h,{shadow:`never`},{header:g(()=>[...n[4]||=[t(`span`,{class:`fw-600`},`服务信息`,-1)]]),default:g(()=>[te((x(),_(p,{border:``,data:Ve.value,"show-header":!1,class:`full-width`},{default:g(()=>[s(d,{prop:`key`,width:`160`}),s(d,null,{default:g(({row:e})=>[e.key===`API 地址`||e.key===`API 密钥`?(x(),u(`span`,{key:0,class:`copy-text`,onClick:t=>$(e.value)},y(e.value),9,le)):(x(),u(`span`,ue,y(e.value),1))]),_:1})]),_:1},8,[`data`])),[[D,I.value]])]),_:1}),Y.value.length?(x(),_(h,{key:1,shadow:`never`},{header:g(()=>[t(`div`,de,[n[6]||=t(`span`,{class:`fw-600`},`供应商额度`,-1),s(a,{loading:i(c).loading,onClick:i(Z)},{default:g(()=>[...n[5]||=[r(`刷新`,-1)]]),_:1},8,[`loading`,`onClick`])])]),default:g(()=>[s(p,{data:Y.value,border:``,stripe:``,class:`full-width`},{default:g(()=>[s(d,{prop:`name`,label:`供应商`,"min-width":`150`,"show-overflow-tooltip":``}),(x(),u(f,null,b(He,e=>s(d,{key:e.key,label:e.label,width:`190`,align:`center`},{default:g(({row:n})=>[n[e.key]?(x(),u(`span`,{key:0,class:ee({"quota-exceeded":n[e.key].exceeded})},[r(y(X(n[e.key].used))+` `,1),t(`span`,fe,`/ `+y(n[e.key].cap??`-`),1),t(`div`,pe,y(Ue(n[e.key].resetAt)),1)],2)):(x(),u(`span`,me,`-`))]),_:2},1032,[`label`])),64)),s(d,{label:`累计 Token`,width:`130`,align:`center`},{default:g(({row:e})=>[r(y(i(k)(e.totalTokens)),1)]),_:1}),s(d,{label:`累计次数`,width:`110`,align:`center`},{default:g(({row:e})=>[r(y(e.totalRuns??`-`),1)]),_:1}),s(d,{label:`更新时间`,width:`180`,align:`center`},{default:g(({row:e})=>[e.error?(x(),u(`span`,he,y(e.error),1)):(x(),u(`span`,ge,y(e.fetchedAtText),1))]),_:1})]),_:1},8,[`data`]),n[7]||=t(`div`,{class:`muted-sm`},`服务端每 10 分钟自动查询一次;数值口径来自各供应商自己的接口`,-1)]),_:1})):l(``,!0),F.value?(x(),_(h,{key:2,shadow:`never`},{header:g(()=>[t(`div`,_e,[n[9]||=t(`span`,{class:`fw-600`},`用量统计`,-1),t(`div`,ve,[s(C,{modelValue:L.value,"onUpdate:modelValue":n[0]||=e=>L.value=e,placeholder:`全部模型`,clearable:``,filterable:``,style:{width:`220px`},onChange:i(H)},{default:g(()=>[(x(!0),u(f,null,b(Ne.value,e=>(x(),_(v,{key:e,label:e,value:e},null,8,[`label`,`value`]))),128))]),_:1},8,[`modelValue`,`onChange`]),s(C,{modelValue:R.value,"onUpdate:modelValue":n[1]||=e=>R.value=e,placeholder:`时间范围`,clearable:``,style:{width:`130px`},onChange:je},{default:g(()=>[s(v,{label:`今天`,value:`today`}),s(v,{label:`昨天`,value:`yesterday`}),s(v,{label:`前天`,value:`beforeYesterday`}),s(v,{label:`近三天`,value:`last3days`}),s(v,{label:`本周`,value:`week`}),s(v,{label:`本月`,value:`month`})]),_:1},8,[`modelValue`]),s(w,{modelValue:V.value,"onUpdate:modelValue":n[2]||=e=>V.value=e,type:`daterange`,"range-separator":`至`,"start-placeholder":`开始日期`,"end-placeholder":`结束日期`,"value-format":`YYYY-MM-DD`,style:{width:`260px`},onChange:Me},null,8,[`modelValue`]),s(a,{loading:I.value,onClick:i(H)},{default:g(()=>[...n[8]||=[r(`刷新`,-1)]]),_:1},8,[`loading`,`onClick`])]),s(T,{type:`info`},{default:g(()=>[r(y(F.value.total.calls)+` 次调用`,1)]),_:1})])]),default:g(()=>[t(`div`,ye,[t(`div`,be,[t(`div`,A,y(i(k)(F.value.total.total_tokens)),1),n[10]||=t(`div`,{class:`usage-label`},`总 Token`,-1)]),t(`div`,j,[t(`div`,M,y(i(k)(F.value.total.input_tokens)),1),n[11]||=t(`div`,{class:`usage-label`},`输入 Token`,-1)]),t(`div`,N,[t(`div`,xe,y(i(k)(F.value.total.output_tokens)),1),n[12]||=t(`div`,{class:`usage-label`},`输出 Token`,-1)]),t(`div`,Se,[t(`div`,Ce,y(i(k)(F.value.total.cache_read_tokens)),1),n[13]||=t(`div`,{class:`usage-label`},`缓存读取`,-1)]),t(`div`,we,[t(`div`,Te,y(F.value.total.cache_rate==null?`-`:F.value.total.cache_rate+`%`),1),n[14]||=t(`div`,{class:`usage-label`},`缓存命中率`,-1)]),F.value.logStorageMB==null?l(``,!0):(x(),u(`div`,Ee,[t(`div`,De,y(F.value.logStorageMB),1),n[15]||=t(`div`,{class:`usage-label`},`数据目录占用 (MB)`,-1)]))]),F.value.models?.length?(x(),u(`div`,Oe,[n[16]||=t(`div`,{class:`section-title`},`模型用量`,-1),s(p,{data:F.value.models,border:``,stripe:``,class:`full-width`},{default:g(()=>[s(d,{prop:`model`,label:`模型`,"min-width":`200`},{default:g(({row:e})=>[t(`span`,{class:`copy-text`,onClick:t=>$(e.model)},y(e.model),9,ke)]),_:1}),s(d,{label:`调用次数`,width:`90`,align:`center`},{default:g(({row:e})=>[r(y(e.calls),1)]),_:1}),s(d,{label:`平均耗时`,width:`110`,align:`center`},{default:g(({row:e})=>[r(y(i(ae)(e.avg_duration)),1)]),_:1}),s(d,{label:`总 Token`,width:`140`,align:`center`},{default:g(({row:e})=>[r(y(i(k)(e.total_tokens)),1)]),_:1}),s(d,{label:`输入 Token`,width:`140`,align:`center`},{default:g(({row:e})=>[r(y(i(k)(e.input_tokens)),1)]),_:1}),s(d,{label:`输出 Token`,width:`140`,align:`center`},{default:g(({row:e})=>[r(y(i(k)(e.output_tokens)),1)]),_:1}),s(d,{label:`缓存读取`,width:`130`,align:`center`},{default:g(({row:e})=>[r(y(i(k)(e.cache_read_tokens)),1)]),_:1}),s(d,{label:`缓存命中率`,width:`110`,align:`center`},{default:g(({row:e})=>[r(y(e.cache_rate==null?`-`:e.cache_rate+`%`),1)]),_:1})]),_:1},8,[`data`])])):(x(),_(E,{key:1,description:`暂无调用记录`,"image-size":60}))]),_:1})):l(``,!0)])}}}),[[`__scopeId`,`data-v-dc65c386`]]);export{P as default};
|