mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-10 03:07:25 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 850920155a | |||
| eaac0ef6b3 |
13
deploy.sh
13
deploy.sh
@ -36,8 +36,15 @@ readonly REPO_DIR="/home/program/llmsproxy"
|
|||||||
readonly HEALTH_URL="http://127.0.0.1:8081/v1/models"
|
readonly HEALTH_URL="http://127.0.0.1:8081/v1/models"
|
||||||
readonly BIN_NAME="llmsproxy"
|
readonly BIN_NAME="llmsproxy"
|
||||||
|
|
||||||
# 构建目录在 /tmp(tmpfs 更快)
|
# 构建目录。默认落在仓库下的构建工作区,而不是 /tmp:本机 /tmp 是 8.8G
|
||||||
readonly TMP_DIR="/tmp/llmsproxy-deploy"
|
# tmpfs,实测被 Go 构建缓存占满后会让 go test/build 在链接阶段直接失败。
|
||||||
|
# 可用 LLMSPROXY_BUILD_DIR 覆盖;同一目录也被测试用作 GOTMPDIR。
|
||||||
|
readonly BUILD_SUBDIR=".build-work"
|
||||||
|
if [[ -n "${LLMSPROXY_BUILD_DIR:-}" ]]; then
|
||||||
|
readonly TMP_DIR="${LLMSPROXY_BUILD_DIR}"
|
||||||
|
else
|
||||||
|
readonly TMP_DIR="${REPO_DIR}/${BUILD_SUBDIR}"
|
||||||
|
fi
|
||||||
# 同文件系统暂存文件(用于原子 rename)
|
# 同文件系统暂存文件(用于原子 rename)
|
||||||
readonly STAGING_BIN="${TARGET_BIN}.staging"
|
readonly STAGING_BIN="${TARGET_BIN}.staging"
|
||||||
readonly STAGING_CONFIG="${TARGET_CONFIG}.staging"
|
readonly STAGING_CONFIG="${TARGET_CONFIG}.staging"
|
||||||
@ -142,7 +149,7 @@ build() {
|
|||||||
mkdir -p "$TMP_DIR/bin"
|
mkdir -p "$TMP_DIR/bin"
|
||||||
|
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
CGO_ENABLED=1 go build -tags luajit \
|
CGO_ENABLED=1 GOTMPDIR="$TMP_DIR" go build -tags luajit \
|
||||||
-trimpath \
|
-trimpath \
|
||||||
-ldflags="-s -w" \
|
-ldflags="-s -w" \
|
||||||
-o "$TMP_DIR/bin/$BIN_NAME" \
|
-o "$TMP_DIR/bin/$BIN_NAME" \
|
||||||
|
|||||||
@ -228,6 +228,26 @@ func incStatus(a *Stat, name string, r Req) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addStat merges an already aggregated row into a total. It deliberately
|
||||||
|
// includes failed requests without a model: routing can fail before a model is
|
||||||
|
// resolved, but that request still belongs in the gateway total.
|
||||||
|
func addStat(dst, src *Stat) {
|
||||||
|
if dst == nil || src == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dst.Reqs += src.Reqs
|
||||||
|
dst.OK += src.OK
|
||||||
|
dst.Err += src.Err
|
||||||
|
dst.Tokens += src.Tokens
|
||||||
|
dst.Prompt += src.Prompt
|
||||||
|
dst.Compl += src.Compl
|
||||||
|
dst.LatSum += src.LatSum
|
||||||
|
dst.FirstByteSum += src.FirstByteSum
|
||||||
|
if src.LatMax > dst.LatMax {
|
||||||
|
dst.LatMax = src.LatMax
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LoadAudit primes the in-memory state from the audit file.
|
// LoadAudit primes the in-memory state from the audit file.
|
||||||
//
|
//
|
||||||
// The aggregates (totals, per-key/model/source rows, status counts and the
|
// The aggregates (totals, per-key/model/source rows, status counts and the
|
||||||
@ -1219,17 +1239,13 @@ func (s *Stats) Snapshot(limit int, key string) map[string]interface{} {
|
|||||||
byModel = s.byKeyModel[key]
|
byModel = s.byKeyModel[key]
|
||||||
bySrc = s.byKeySrc[key]
|
bySrc = s.byKeySrc[key]
|
||||||
}
|
}
|
||||||
for _, a := range byModel {
|
// The total is the sum of every recorded request, not the model rows.
|
||||||
total.Reqs += a.Reqs
|
// Routing failures are valid requests but may have no resolved model; deriving
|
||||||
total.OK += a.OK
|
// total from byModel silently dropped those 502/503 records. Use byKey, which
|
||||||
total.Err += a.Err
|
// is populated for every request (including an empty key), and keep the same
|
||||||
total.Tokens += a.Tokens
|
// scope as the selected key filter.
|
||||||
total.Prompt += a.Prompt
|
for _, a := range byKey {
|
||||||
total.Compl += a.Compl
|
addStat(&total, a)
|
||||||
total.LatSum += a.LatSum
|
|
||||||
if a.LatMax > total.LatMax {
|
|
||||||
total.LatMax = a.LatMax
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
bs := make([]agrRow, 0, len(s.byStatus))
|
bs := make([]agrRow, 0, len(s.byStatus))
|
||||||
for code, st := range s.byStatus {
|
for code, st := range s.byStatus {
|
||||||
|
|||||||
@ -560,3 +560,17 @@ func TestAuditRotationThresholds(t *testing.T) {
|
|||||||
t.Fatalf("total audit budget = %d, want 256 MiB", total)
|
t.Fatalf("total audit budget = %d, want 256 MiB", total)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSnapshotTotalIncludesRequestsWithoutModel(t *testing.T) {
|
||||||
|
s := NewStats(100)
|
||||||
|
s.Record(Req{Key: "k", Status: 200, OK: true})
|
||||||
|
s.Record(Req{Key: "k", Model: "m", Status: 200, OK: true, Prompt: 3, Compl: 2})
|
||||||
|
snap := s.Snapshot(0, "")
|
||||||
|
total, ok := snap["total"].(Stat)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("total missing: %#v", snap["total"])
|
||||||
|
}
|
||||||
|
if total.Reqs != 2 || total.OK != 2 || total.Tokens != 5 {
|
||||||
|
t.Fatalf("total must include model-less requests: %#v", total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7110,27 +7110,24 @@ function plugin.usage_token_ledger(req)
|
|||||||
if key ~= "" then
|
if key ~= "" then
|
||||||
url = url .. "&key=" .. (key:len() <= 6 and key or ("***" .. key:sub(-6)))
|
url = url .. "&key=" .. (key:len() <= 6 and key or ("***" .. key:sub(-6)))
|
||||||
end
|
end
|
||||||
local resp, err = __llmsproxy_http({ url = url, method = "GET", timeout_ms = 4000 })
|
local headers = {}
|
||||||
|
if key ~= "" then
|
||||||
|
headers["Authorization"] = "Bearer " .. key
|
||||||
|
end
|
||||||
|
local resp, err = __llmsproxy_http({ url = url, method = "GET", timeout_ms = 4000, headers = headers })
|
||||||
if err or not resp or resp.status ~= 200 then
|
if err or not resp or resp.status ~= 200 then
|
||||||
return { status = 200, json = { ok = true, snapshot = { available = false,
|
return { status = 503, json = { ok = false, error = "usage snapshot unavailable: " .. tostring(err or (resp and resp.status or "no response")) } }
|
||||||
error = err or (resp and tostring(resp.status) or "no response") } } }
|
|
||||||
end
|
end
|
||||||
local ok, rec = pcall(json.decode, resp.body or "")
|
local ok, rec = pcall(json.decode, resp.body or "")
|
||||||
if not ok or type(rec) ~= "table" then
|
if not ok or type(rec) ~= "table" then
|
||||||
return { status = 200, json = { ok = true, snapshot = { available = false, error = "stats 无法解析" } } }
|
return { status = 502, json = { ok = false, error = "usage snapshot 无法解析" } }
|
||||||
end
|
|
||||||
-- 汇总 token 总量(兼容 stats 的不同形状)
|
|
||||||
local prompt, completion, requests = 0, 0, 0
|
|
||||||
local rows = rec.requests or rec.rows or rec.data or {}
|
|
||||||
if type(rows) == "table" then
|
|
||||||
for _, r in ipairs(rows) do
|
|
||||||
if type(r) == "table" then
|
|
||||||
prompt = prompt + (tonumber(r.prompt_tokens) or 0)
|
|
||||||
completion = completion + (tonumber(r.completion_tokens) or 0)
|
|
||||||
requests = requests + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
-- 汇总 token 总量。/api/stats 的聚合在 `total` 下,不在顶层数组中。
|
||||||
|
-- 之前错误地遍历 rec.requests/rows/data,导致该端点永远返回 0。
|
||||||
|
local total = type(rec.total) == "table" and rec.total or {}
|
||||||
|
local prompt = tonumber(total.prompt_tokens) or 0
|
||||||
|
local completion = tonumber(total.completion_tokens) or 0
|
||||||
|
local requests = tonumber(total.reqs) or 0
|
||||||
return { status = 200, json = { ok = true, snapshot = { available = true,
|
return { status = 200, json = { ok = true, snapshot = { available = true,
|
||||||
tokens = { prompt = prompt, completion = completion, total = prompt + completion },
|
tokens = { prompt = prompt, completion = completion, total = prompt + completion },
|
||||||
requests = requests, raw = rec } } }
|
requests = requests, raw = rec } } }
|
||||||
|
|||||||
Reference in New Issue
Block a user