Files
dsh-balance/lib/index.js
T

298 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// deepseek-balance — host face
//
// 职责(全部进程内、可逆):
// 1. 监听 `llm/stream` 瀑布事件,按 session 累计 DeepSeek 模型的 token 用量。
// 2. 从 models.dev 拉取 DeepSeek 单价(内存缓存 24h + 硬编码兜底),
// 展示时用最新单价把累计 token 换算成人民币估算花费。
// 3. 通过 credentials 服务读取 DEEPSEEK_API_KEYcurl 官方 /user/balance 查余额。
// 4. 注册 /deepseek-balance/status HTTP route 供 client 轮询。
const inject = ["webServer", "credentials", "subprocess"];
const CNY_PER_USD = 7.2;
const PRICING_TTL_MS = 24 * 60 * 60 * 1000;
const MODELS_DEV_URL = "https://models.dev/api.json";
// models.dev 拉取失败时的兜底单价,USD / 百万 token。
const FALLBACK_PRICING = [
{ prefix: "deepseek-v4-pro", input: 0.435, output: 0.87, cacheRead: 0.003625 },
{ prefix: "deepseek-v4-flash", input: 0.14, output: 0.28, cacheRead: 0.0028 },
{ prefix: "deepseek-chat", input: 0.14, output: 0.28, cacheRead: 0.0028 },
{ prefix: "deepseek-reasoner", input: 0.14, output: 0.28, cacheRead: 0.0028 },
];
function isDeepSeekModel(model) {
return typeof model === "string" && model.indexOf("deepseek-") === 0;
}
function round2(n) {
return Math.round(n * 100) / 100;
}
function writeJson(res, status, body) {
const payload = JSON.stringify(body);
res.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
});
res.end(payload);
}
function isLoopbackHostname(hostname) {
if (hostname === "localhost" || hostname === "::1" || hostname === "[::1]") return true;
const parts = hostname.split(".");
if (parts.length !== 4 || parts[0] !== "127") return false;
return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
}
// 同源/本机请求防护:拒绝跨站读取余额,只放行 loopback 同源请求。
function isTrusted(req) {
const host = req.headers.host;
if (!host) return false;
const hostname = host.split(":")[0];
if (!isLoopbackHostname(hostname)) return false;
if (req.headers["sec-fetch-site"] === "cross-site") return false;
const origin = req.headers.origin;
if (origin === undefined) return true;
try {
return new URL(origin).host === host;
} catch {
return false;
}
}
function apply(ctx) {
const credentials = ctx.credentials;
const subprocess = ctx.subprocess;
const agentDefaultModel = ctx.get("agentDefaultModel");
const pricing = { entries: null, fetchedAt: 0, source: "fallback" };
let pricingPromise = null;
const state = { lastModel: null };
const tokensBySession = new Map();
function recordUsage(options, usage) {
if (!usage || !isDeepSeekModel(options && options.model)) return;
const key = options.sessionId ? String(options.sessionId) : "__global__";
const cur = tokensBySession.get(key) || {
inputTokens: 0,
cacheReadTokens: 0,
outputTokens: 0,
requests: 0,
};
cur.inputTokens += usage.inputTokens || 0;
cur.cacheReadTokens += usage.cacheReadTokens || 0;
cur.outputTokens += usage.outputTokens || 0;
cur.requests += 1;
tokensBySession.set(key, cur);
}
// 透传每个 chunk,流结束后把 usage 记入本会话累计;不改变下游语义。
ctx.on("llm/stream", function (options, next) {
if (options && typeof options.model === "string") state.lastModel = options.model;
const upstream = next();
return (async function* () {
let usage = null;
try {
for await (const chunk of upstream) {
if (chunk && chunk.type === "usage" && chunk.usage) usage = chunk.usage;
yield chunk;
}
} finally {
if (usage) {
try {
recordUsage(options, usage);
} catch (error) {
console.error("deepseek-balance: record usage failed", error);
}
}
}
})();
});
function defaultModel() {
if (!agentDefaultModel) return null;
try {
const sel = agentDefaultModel.currentSelection();
return sel && typeof sel.model === "string" ? sel.model : null;
} catch {
return null;
}
}
function httpGet(url) {
const handle = subprocess.spawn({
argv: ["curl", "-sS", "--max-time", "20", url],
cwd: "/",
stdio: { stdin: "ignore", stdout: { maxBytes: 8388608 }, stderr: { maxBytes: 16384 } },
graceMs: 5000,
});
return handle.done.then(function (outcome) {
const out = handle.collected.stdout ? handle.collected.stdout.readFrom(0).text : "";
const err = handle.collected.stderr ? handle.collected.stderr.readFrom(0).text : "";
if (outcome.exitCode !== 0) {
throw new Error((err && err.trim()) ? err.trim() : "curl exit " + outcome.exitCode);
}
return out;
});
}
function parseDeepSeekPricing(body) {
const data = JSON.parse(body);
const models = data && data.deepseek && data.deepseek.models;
if (!models || typeof models !== "object") return null;
const entries = [];
for (const id in models) {
const cost = models[id] && models[id].cost;
if (!cost || typeof cost !== "object") continue;
const input = typeof cost.input === "number" ? cost.input : 0;
const output = typeof cost.output === "number" ? cost.output : 0;
const cacheRead = typeof cost.cache_read === "number" ? cost.cache_read : 0;
if (!(input > 0) && !(output > 0)) continue;
entries.push({ prefix: id, input, output, cacheRead });
}
if (!entries.length) return null;
entries.sort(function (a, b) {
return b.prefix.length - a.prefix.length;
});
return entries;
}
function ensurePricing() {
const now = Date.now();
if (pricing.entries && now - pricing.fetchedAt < PRICING_TTL_MS) return Promise.resolve();
if (pricingPromise) return pricingPromise;
pricingPromise = (async function () {
try {
const body = await httpGet(MODELS_DEV_URL);
const entries = parseDeepSeekPricing(body);
if (entries) {
pricing.entries = entries;
pricing.fetchedAt = Date.now();
pricing.source = "live";
}
} catch (error) {
console.error("deepseek-balance: fetch pricing failed, using fallback", error);
} finally {
pricingPromise = null;
}
})();
return pricingPromise;
}
function matchPricing(model) {
const entries = pricing.entries || FALLBACK_PRICING;
for (let i = 0; i < entries.length; i++) {
if (model.indexOf(entries[i].prefix) === 0) return entries[i];
}
return null;
}
async function queryBalance() {
let resolved;
try {
resolved = await credentials.resolve("DEEPSEEK_API_KEY");
} catch {
return { ok: false, error: "读取凭据失败" };
}
if (!resolved || !resolved.value) return { ok: false, error: "未配置 DEEPSEEK_API_KEY" };
// 密钥经 curl 的 stdin config 注入,避免出现在 argv(ps 可见)。
const config =
'header = "Authorization: Bearer ' + resolved.value + '"\n' +
'header = "Accept: application/json"\n';
let handle;
try {
handle = subprocess.spawn({
argv: ["curl", "-sS", "--max-time", "15", "--config", "-", "https://api.deepseek.com/user/balance"],
cwd: "/",
stdio: { stdin: { data: config }, stdout: { maxBytes: 65536 }, stderr: { maxBytes: 16384 } },
graceMs: 5000,
});
} catch {
return { ok: false, error: "启动 curl 失败" };
}
let outcome;
try {
outcome = await handle.done;
} catch {
return { ok: false, error: "curl 执行异常" };
}
const out = handle.collected.stdout ? handle.collected.stdout.readFrom(0).text : "";
const err = handle.collected.stderr ? handle.collected.stderr.readFrom(0).text : "";
if (outcome.exitCode !== 0) {
return { ok: false, error: (err && err.trim()) ? err.trim() : "curl exit " + outcome.exitCode };
}
let body;
try {
body = JSON.parse(out);
} catch {
return { ok: false, error: "响应不是合法 JSON" };
}
return { ok: true, body };
}
async function getStatus(sessionId) {
const key = sessionId ? String(sessionId) : "__global__";
const model = state.lastModel || defaultModel();
const tokens = tokensBySession.get(key) || {
inputTokens: 0,
cacheReadTokens: 0,
outputTokens: 0,
requests: 0,
};
if (!isDeepSeekModel(model)) {
return { ok: true, isDeepSeek: false, model: model || null, requests: tokens.requests };
}
await ensurePricing();
const p = matchPricing(model);
const usd =
(p ? p.input : 0) * (tokens.inputTokens / 1000000) +
(p ? p.cacheRead : 0) * (tokens.cacheReadTokens / 1000000) +
(p ? p.output : 0) * (tokens.outputTokens / 1000000);
const spendCny = round2(usd * CNY_PER_USD);
const balance = await queryBalance();
return {
ok: true,
isDeepSeek: true,
model: model,
spendCny: spendCny,
requests: tokens.requests,
pricingSource: pricing.source,
balance: balance.ok ? balance.body : null,
balanceError: balance.ok ? null : balance.error,
};
}
ctx.effect(
() =>
ctx.webServer.register({
kind: "exact",
path: "/deepseek-balance/status",
handler: async (req, res) => {
if (!isTrusted(req)) {
writeJson(res, 403, { ok: false, error: "forbidden" });
return;
}
try {
const url = new URL(req.url ?? "/", "http://dsh.internal");
const sessionId = url.searchParams.get("sessionId");
writeJson(res, 200, await getStatus(sessionId));
} catch (error) {
writeJson(res, 500, { ok: false, error: error && error.message ? error.message : String(error) });
}
},
}),
"deepseek-balance: /deepseek-balance/status route",
);
}
export { apply, inject };