feat: update billing metrics and CPA baseline
This commit is contained in:
+1
-1
Submodule .externals/CLIProxyAPI updated: ee2c494788...85d2faddd1
@@ -34,8 +34,8 @@
|
||||
|
||||
- 本地 CPA 在 WSL 中运行,项目运行目录统一使用 `cpa-plugin/.runtime`;Windows 客户端经 `127.0.0.1` 访问 WSL 时,localhost 转发层可能不会立即向 CPA 传递客户端断开,取消链路测试优先在 WSL 内直连执行。
|
||||
- 从 PowerShell 跨 WSL 或 SSH 执行包含 Bash 变量、命令替换或多层引号的逻辑时,优先在 `.runtime/tmp/` 创建临时脚本后传入目标环境执行,不拼接复杂的单行 shell;临时脚本不提交。
|
||||
- CLIProxyAPI 宿主补丁保存在 `patch/`,当前已验证基线为 `v7.2.135` / `ee2c4947`,必须按 `usage-context`、`usage-identity`、`request-lifecycle-cancel` 的顺序应用;升级 CPA 后按 `docs/operations.md` 核对补丁和已知上游测试例外。
|
||||
- 构建本项目 patched CPA 时必须按上游 release workflow 使用 `-ldflags` 注入 `main.Version`、`main.Commit` 和 `main.BuildDate`。`Version` 固定使用“当前上游最新 tag + `-dev`”格式(例如 `v7.2.135-dev`),用于明确区分官方 Release 与本项目补丁构建;不得发布只显示 `dev`、`none` 或 `unknown` 的产物。上传前先在本地执行二进制并核对完整版本输出。
|
||||
- CLIProxyAPI 宿主补丁保存在 `patch/`,当前已验证基线为 `v7.2.137` / `85d2fadd`,必须按 `usage-context`、`usage-identity`、`request-lifecycle-cancel` 的顺序应用;升级 CPA 后按 `docs/operations.md` 核对补丁和已知上游测试例外。
|
||||
- 构建本项目 patched CPA 时必须按上游 release workflow 使用 `-ldflags` 注入 `main.Version`、`main.Commit` 和 `main.BuildDate`。`Version` 固定使用“当前上游最新 tag + `-dev`”格式(例如 `v7.2.137-dev`),用于明确区分官方 Release 与本项目补丁构建;不得发布只显示 `dev`、`none` 或 `unknown` 的产物。上传前先在本地执行二进制并核对完整版本输出。
|
||||
- CPA Linux 可执行文件解压后约 60 MiB,`gzip -9` 后约 20 MiB。上传远端时使用 gzip/tar.gz 或 `scp -C`,不要直接传输未压缩二进制;部署后的运行文件仍需解压为原始可执行文件。
|
||||
- 实际服务器为 `root@akko.pchuan.top`,CPA 根目录为 `/root/cpa`。只维护本项目的 `billing` 和已要求启用的 `keeper`,不启用或部署其他参考插件。
|
||||
- 远端替换 CPA 或 billing 前,先停止 `/root/cpa/cpa.sh` 管理的进程,并把旧二进制、插件和 `data/cpa-ext.db*` 备份到 `/root/cpa/backups/<timestamp>/`;新版本启动失败时恢复旧二进制和插件。
|
||||
|
||||
@@ -66,7 +66,7 @@ CLIProxyAPI 负责 HTTP 接入、协议转换、上游凭证和实际请求执
|
||||
|
||||
## 当前兼容目标
|
||||
|
||||
- CLIProxyAPI 基线:`v7.2.135` / `ee2c494788f8a089c58f7ae9aa6ebd1b422211cb`,由 `.externals/CLIProxyAPI` submodule 锁定,需依次应用 `patch/` 中三项宿主补丁
|
||||
- CLIProxyAPI 基线:`v7.2.137` / `85d2faddd17e6f4f8675a84ee28b131f702e8eaa`,由 `.externals/CLIProxyAPI` submodule 锁定,需依次应用 `patch/` 中三项宿主补丁
|
||||
- 插件版本:`0.1.0`
|
||||
- Native ABI:`1`
|
||||
- RPC schema:最高 `4`,注册时按宿主版本向下协商;schema v4 提供 Usage 请求身份
|
||||
|
||||
+10
-8
@@ -4,12 +4,12 @@
|
||||
|
||||
| 项目 | 当前值 |
|
||||
|---|---|
|
||||
| CLIProxyAPI Tag | `v7.2.135` |
|
||||
| submodule 提交 | `ee2c494788f8a089c58f7ae9aa6ebd1b422211cb` |
|
||||
| patched 构建版本 | `v7.2.135-dev` |
|
||||
| CLIProxyAPI Tag | `v7.2.137` |
|
||||
| submodule 提交 | `85d2faddd17e6f4f8675a84ee28b131f702e8eaa` |
|
||||
| patched 构建版本 | `v7.2.137-dev` |
|
||||
| Native ABI | `1` |
|
||||
| RPC schema | `4` |
|
||||
| Go | `1.26.6` |
|
||||
| Go | `1.26.x` |
|
||||
| 本地运行目录 | `.runtime` |
|
||||
| 本地管理页面 | `http://127.0.0.1:8317/management.html` |
|
||||
| 远端目录 | `root@akko.pchuan.top:/root/cpa` |
|
||||
@@ -29,19 +29,21 @@ DeepSeek 必须配置在 CPA 的 `codex-api-key` 通道,`base-url` 使用 `htt
|
||||
|
||||
## 已知上游测试失败
|
||||
|
||||
CLIProxyAPI `ee2c4947` 在 Debian WSL 中运行 `go test ./... -count=1` 时有以下失败:
|
||||
CLIProxyAPI `85d2fadd` 在 Debian WSL 中运行 `go test ./... -count=1` 时有以下失败:
|
||||
|
||||
| 测试 | 实际值 | 期望值 |
|
||||
|---|---|---|
|
||||
| `TestApplyClaudeHeaders_DisableDeviceProfileStabilization` | `X-Stainless-Os=MacOS` | `Linux` |
|
||||
| `TestApplyClaudeHeaders_LegacyModePreservesConfiguredUserAgentOverrideForClaudeClients` | `X-Stainless-Os=MacOS` | `Linux` |
|
||||
| `TestClaudeExecutor_NonClaudeRequestUsesClaudeCode220CLIFingerprint` | `X-Stainless-Os=MacOS` | `Linux` |
|
||||
| `TestInPlaceByteWritesAreReviewed` | `internal/home/client.go` 清单项已过期 | 文件已无原地字节写入 |
|
||||
| `TestInPlaceByteWritesAreReviewed` | `internal/pluginstore/auth.go` 清单项已过期 | 文件已无原地字节写入 |
|
||||
|
||||
2026-08-18 已在未应用任何 billing 补丁的纯上游 `ee2c4947` 临时 worktree 中复现相同结果。三项补丁没有修改 `internal/runtime/executor`,因此同一提交、同一 WSL 环境、同一失败内容再次出现时,直接记为已知上游失败,不再创建干净 worktree复测。
|
||||
2026-08-20 已在未应用任何 billing 补丁的纯上游 `85d2fadd` 临时 worktree 中复现以上 5 项。三项补丁不修改 `internal/runtime/executor`、`internal/util`、`internal/home` 或 `internal/pluginstore`,因此同一提交、同一 WSL 环境、同一失败内容再次出现时,直接记为已知上游失败,不修复、不再创建干净 worktree复测。
|
||||
|
||||
出现以下任一情况时重新验证:
|
||||
|
||||
- submodule 提交不再是 `ee2c4947`;
|
||||
- submodule 提交不再是 `85d2fadd`;
|
||||
- 上游修改了 `internal/runtime/executor/claude_executor_test.go`、`claude_executor_request.go` 或 `helps/claude_device_profile.go`;
|
||||
- 失败测试、行号、实际值或期望值发生变化;
|
||||
- 三项补丁开始修改 `internal/runtime/executor`。
|
||||
@@ -108,7 +110,7 @@ CPA 版本必须使用当前最新 Tag 加 `-dev`,提交取 submodule HEAD:
|
||||
|
||||
```bash
|
||||
cd .externals/CLIProxyAPI
|
||||
version=v7.2.135-dev
|
||||
version=v7.2.137-dev
|
||||
commit="$(git rev-parse --short=8 HEAD)"
|
||||
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
CGO_ENABLED=1 go build -buildvcs=false \
|
||||
|
||||
@@ -75,6 +75,7 @@ func (a *App) interceptRequest(raw []byte, afterAuth bool) ([]byte, error) {
|
||||
if err := json.Unmarshal(raw, &req); err != nil {
|
||||
return nil, fmt.Errorf("解析请求拦截事件: %w", err)
|
||||
}
|
||||
a.markCompactRequest(req)
|
||||
keyID := metadataString(req.Metadata, callerScopeMetadata)
|
||||
if keyID == "" && req.RequestID != "" {
|
||||
if pending, ok := a.pending.Load(req.RequestID); ok {
|
||||
|
||||
+45
-1
@@ -29,6 +29,7 @@ type App struct {
|
||||
closed bool
|
||||
seen atomic.Uint64
|
||||
pending sync.Map
|
||||
compactRequests sync.Map
|
||||
scopes sync.Map
|
||||
observedUpstreams sync.Map
|
||||
}
|
||||
@@ -177,6 +178,7 @@ func (a *App) handleUsage(raw []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("用量数据库尚未初始化")
|
||||
}
|
||||
// CPA wire 类型只存在于适配层,采集模块接收与协议无关的观察值。
|
||||
compact := a.isCompactRequest(record.RequestID)
|
||||
observed := collection.Record{
|
||||
RequestID: record.RequestID,
|
||||
TraceID: record.TraceID,
|
||||
@@ -200,11 +202,20 @@ func (a *App) handleUsage(raw []byte) ([]byte, error) {
|
||||
TTFT: record.TTFT,
|
||||
Latency: record.Latency,
|
||||
}
|
||||
if compact {
|
||||
observed.Endpoint = compactEndpoint
|
||||
}
|
||||
if key, keyErr := a.resolveUsageManagedKey(a.store, record.APIKey); keyErr == nil {
|
||||
observed.ManagedKeyID = key.ID
|
||||
observed.KeyAlias = key.Name
|
||||
}
|
||||
if policy, found := a.prices[normalizeModelName(record.Model)]; found {
|
||||
if compact {
|
||||
zero := int64(0)
|
||||
observed.CostMicros = &zero
|
||||
observed.PriceTier = "compact"
|
||||
observed.PriceMultiplierNumerator = 1
|
||||
observed.PriceMultiplierDenominator = 1
|
||||
} else if policy, found := a.prices[normalizeModelName(record.Model)]; found {
|
||||
cost, calculateErr := pricing.Calculate(policy, pricing.Usage{
|
||||
InputTokens: record.Detail.InputTokens, CacheReadTokens: record.Detail.CacheReadTokens,
|
||||
CacheWriteTokens: record.Detail.CacheCreationTokens, OutputTokens: record.Detail.OutputTokens,
|
||||
@@ -262,6 +273,9 @@ func (a *App) handleRequestComplete(raw []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("释放并发占用: %w", err)
|
||||
}
|
||||
endpoint, _ := completion.Metadata["request_path"].(string)
|
||||
if a.isCompactRequest(completion.RequestID) {
|
||||
endpoint = compactEndpoint
|
||||
}
|
||||
managedKeyID := metadataString(completion.Metadata, callerScopeMetadata)
|
||||
if key, keyErr := a.store.ManagedKeyByReference(context.Background(), managedKeyID); keyErr == nil {
|
||||
managedKeyID = key.ID
|
||||
@@ -293,6 +307,36 @@ func isCompactEndpoint(endpoint string) bool {
|
||||
return strings.HasSuffix(strings.TrimRight(path, "/"), "/responses/compact")
|
||||
}
|
||||
|
||||
const compactEndpoint = "/v1/responses/compact"
|
||||
|
||||
func (a *App) markCompactRequest(req RequestInterceptRequest) {
|
||||
requestID := strings.TrimSpace(req.RequestID)
|
||||
if requestID == "" {
|
||||
return
|
||||
}
|
||||
endpoint := metadataString(req.Metadata, "request_path")
|
||||
if isCompactEndpoint(endpoint) || hasCompactionTrigger(req.Body) {
|
||||
a.compactRequests.Store(requestID, struct{}{})
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) isCompactRequest(requestID string) bool {
|
||||
_, ok := a.compactRequests.Load(strings.TrimSpace(requestID))
|
||||
return ok
|
||||
}
|
||||
|
||||
func hasCompactionTrigger(body []byte) bool {
|
||||
var payload struct {
|
||||
Input []struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"input"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err != nil || len(payload.Input) == 0 {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(payload.Input[len(payload.Input)-1].Type), "compaction_trigger")
|
||||
}
|
||||
|
||||
func (a *App) Seen() uint64 {
|
||||
return a.seen.Load()
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ func TestCanceledRequestWithoutUsageRemainsVisible(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactUsageIsPricedAndUsesJSONMetrics(t *testing.T) {
|
||||
func TestCompactUsageIsZeroPricedAndUsesJSONMetrics(t *testing.T) {
|
||||
app := NewApp()
|
||||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -384,8 +384,15 @@ func TestCompactUsageIsPricedAndUsesJSONMetrics(t *testing.T) {
|
||||
t.Fatalf("put price status = %d, body = %s", response.StatusCode, response.Body)
|
||||
}
|
||||
startedAt := time.Now()
|
||||
interceptRaw, _ := json.Marshal(RequestInterceptRequest{
|
||||
RequestID: "compact-success", Body: []byte(`{"model":"deepseek-v4-flash"}`),
|
||||
Metadata: map[string]any{"request_path": "/v1/responses/compact"},
|
||||
})
|
||||
if _, err := app.HandleMethod(MethodRequestBefore, interceptRaw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
record := UsageRecord{
|
||||
Provider: "openai", Model: "deepseek-v4-flash", RequestedAt: startedAt,
|
||||
RequestID: "compact-success", Provider: "openai", Model: "deepseek-v4-flash", RequestedAt: startedAt,
|
||||
Latency: time.Second, TTFT: 200 * time.Millisecond,
|
||||
Detail: UsageDetail{InputTokens: 1_000, OutputTokens: 100, TotalTokens: 1_100},
|
||||
}
|
||||
@@ -413,11 +420,62 @@ func TestCompactUsageIsPricedAndUsesJSONMetrics(t *testing.T) {
|
||||
if got.RequestType != "JSON" || got.Endpoint != "/v1/responses/compact" || got.TTFTMilliseconds != 0 || got.SpeedTPS != nil {
|
||||
t.Fatalf("unexpected compact metadata: %+v", got)
|
||||
}
|
||||
if !got.CostAvailable || got.CostUSD == nil || *got.CostUSD != 0.004 {
|
||||
if !got.CostAvailable || got.CostUSD == nil || *got.CostUSD != 0 {
|
||||
t.Fatalf("unexpected compact cost: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteV2CompactTriggerDisplaysCompactAndCostsZero(t *testing.T) {
|
||||
app := NewApp()
|
||||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
priceBody := []byte(`{"model":"gpt-5.6-sol","base":{"input_per_1m":"2.5","cache_read_per_1m":"0.25","cache_write_per_1m":"3.125","output_per_1m":"15"}}`)
|
||||
if response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, priceBody); response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("put price status = %d, body = %s", response.StatusCode, response.Body)
|
||||
}
|
||||
startedAt := time.Now()
|
||||
interceptRaw, _ := json.Marshal(RequestInterceptRequest{
|
||||
RequestID: "compact-v2", Model: "gpt-5.6-sol", Stream: true,
|
||||
Body: []byte(`{"model":"gpt-5.6-sol","input":[{"type":"message","role":"user","content":"history"},{"type":"compaction_trigger"}]}`),
|
||||
Metadata: map[string]any{"request_path": "/v1/responses"},
|
||||
})
|
||||
if _, err := app.HandleMethod(MethodRequestBefore, interceptRaw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
usageRaw, _ := json.Marshal(UsageRecord{
|
||||
RequestID: "compact-v2", Provider: "codex", Model: "gpt-5.6-sol", RequestedAt: startedAt,
|
||||
Latency: time.Second, TTFT: 200 * time.Millisecond,
|
||||
Detail: UsageDetail{InputTokens: 17_235, OutputTokens: 144, TotalTokens: 17_379},
|
||||
})
|
||||
if _, err := app.HandleMethod(MethodUsageHandle, usageRaw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
completionRaw, _ := json.Marshal(RequestCompletion{
|
||||
RequestID: "compact-v2", Model: "gpt-5.6-sol", Stream: true, Outcome: RequestCompletionSucceeded,
|
||||
StartedAt: startedAt, CompletedAt: startedAt.Add(time.Second),
|
||||
Metadata: map[string]any{"request_path": "/v1/responses"},
|
||||
})
|
||||
if _, err := app.HandleMethod(MethodRequestComplete, completionRaw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||||
var payload usageListResponse
|
||||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(payload.Records) != 1 {
|
||||
t.Fatalf("records = %d, want 1", len(payload.Records))
|
||||
}
|
||||
got := payload.Records[0]
|
||||
if got.Endpoint != compactEndpoint || got.TTFTMilliseconds != 0 || got.SpeedTPS != nil {
|
||||
t.Fatalf("unexpected compact v2 metadata: %+v", got)
|
||||
}
|
||||
if !got.CostAvailable || got.CostUSD == nil || *got.CostUSD != 0 {
|
||||
t.Fatalf("unexpected compact v2 cost: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactFailureAndCancellationRemainVisible(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
@@ -636,7 +694,7 @@ func TestUsageResourceServesFeatureModules(t *testing.T) {
|
||||
}
|
||||
javascript.Write(response.Body)
|
||||
}
|
||||
for _, feature := range []string{"initializeRuntime", "dataFetch", "isManagementAuthorized", "adminPageSize = 100", "userPageSize = 50", `row.setAttribute("role", "button")`, "syncLongSectionVisibility", `return "compact"`, "isCompactEndpoint(record.endpoint)", `identity.title = "点击复制 Key"`, `return "自由选择"`, "剩余额度", `document.body.classList.toggle("read-only"`, `删除 ${model} 的真实价格配置`, `发现 ${changed.length} 个已关联价格发生变化`} {
|
||||
for _, feature := range []string{"initializeRuntime", "dataFetch", "isManagementAuthorized", "adminPageSize = 100", "userPageSize = 50", `row.setAttribute("role", "button")`, "syncLongSectionVisibility", `return "compact"`, "isCompactEndpoint(record.endpoint)", `identity.title = "点击复制 Key"`, `return "自由选择"`, "剩余额度", "今日使用 / 剩余额度", "quotaUsage(item.cost, item.balance)", "resetTime(item)", `document.body.classList.toggle("read-only"`, "compactTokens(price.long_context.threshold_input_tokens)", "appendPriceRow(table, \"长上下文\", price.long_context, true)", `删除 ${model} 的真实价格配置`, `发现 ${changed.length} 个已关联价格发生变化`} {
|
||||
if !strings.Contains(javascript.String(), feature) {
|
||||
t.Fatalf("UI modules do not contain feature %q", feature)
|
||||
}
|
||||
|
||||
@@ -192,11 +192,13 @@ function renderUserCharts() {
|
||||
const byUser = new Map(managedKeys.map(key => [key.name, {
|
||||
name: key.name, requests: 0, tokens: 0, cost: 0,
|
||||
balance: key.billing ? Number(key.billing.balance_usd || 0) : null,
|
||||
resetPeriod: key.billing?.reset_period || null,
|
||||
nextResetAt: key.billing?.next_reset_at ? new Date(key.billing.next_reset_at) : null,
|
||||
days: [], last: null
|
||||
}]));
|
||||
(usageDashboard.users || []).forEach(user => {
|
||||
const name = user.key_alias || "未识别";
|
||||
if (!byUser.has(name)) byUser.set(name, { name, requests: 0, tokens: 0, cost: 0, balance: null, days: [], last: null });
|
||||
if (!byUser.has(name)) byUser.set(name, { name, requests: 0, tokens: 0, cost: 0, balance: null, resetPeriod: null, nextResetAt: null, days: [], last: null });
|
||||
const value = byUser.get(name);
|
||||
const today = user.today || {};
|
||||
value.requests = today.requests || 0;
|
||||
@@ -208,7 +210,7 @@ function renderUserCharts() {
|
||||
const userRows = [...byUser.values()].sort((left, right) => right.tokens - left.tokens || left.name.localeCompare(right.name));
|
||||
const header = document.createElement("div");
|
||||
header.className = "user-usage-row header";
|
||||
["用户", "今日请求", "今日 Token", "今日成本", "剩余额度", "最近使用"].forEach(label => {
|
||||
["用户", "今日请求", "今日 Token", "今日使用 / 剩余额度", "最近使用", "重置时间"].forEach(label => {
|
||||
const cell = document.createElement("span");
|
||||
cell.textContent = label;
|
||||
header.append(cell);
|
||||
@@ -216,7 +218,7 @@ function renderUserCharts() {
|
||||
document.querySelector("#user-usage").replaceChildren(header, ...userRows.map(item => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "user-usage-row";
|
||||
[item.name, number(item.requests), number(item.tokens), money(item.cost), item.balance === null ? "—" : money(item.balance), item.last ? item.last.toLocaleString() : "从未"].forEach(value => {
|
||||
[item.name, number(item.requests), number(item.tokens), quotaUsage(item.cost, item.balance), item.last ? item.last.toLocaleString() : "从未", resetTime(item)].forEach(value => {
|
||||
const cell = document.createElement("span");
|
||||
cell.textContent = value;
|
||||
row.append(cell);
|
||||
@@ -227,6 +229,24 @@ function renderUserCharts() {
|
||||
renderQuotaMatrix(days, [...byUser.values()]);
|
||||
}
|
||||
|
||||
function quotaUsage(cost, balance) {
|
||||
const used = Number(cost || 0);
|
||||
const remaining = Number(balance);
|
||||
return `${(Number.isFinite(used) ? used : 0).toFixed(2)} / ${balance === null || !Number.isFinite(remaining) ? "—" : remaining.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function resetTime(item) {
|
||||
if (item.resetPeriod === "none") return "永不";
|
||||
if (!item.nextResetAt || !Number.isFinite(item.nextResetAt.getTime())) return "—";
|
||||
const value = item.nextResetAt;
|
||||
const parts = [
|
||||
value.getFullYear(),
|
||||
String(value.getMonth() + 1).padStart(2, "0"),
|
||||
String(value.getDate()).padStart(2, "0")
|
||||
];
|
||||
return `${parts.join("-")} ${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function renderQuotaMatrix(days, users) {
|
||||
const dates = days.map(day => dateKey(day.date));
|
||||
const rows = users.map(user => {
|
||||
|
||||
@@ -144,7 +144,10 @@ function renderPriceCard(price) {
|
||||
name.textContent = price.model;
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "model-price-meta";
|
||||
if (price.long_context) meta.append(priceBadge("长上下文"));
|
||||
if (price.long_context) {
|
||||
const comparison = price.long_context.comparison === "gte" ? "≥" : ">";
|
||||
meta.append(priceBadge(`${comparison} ${compactTokens(price.long_context.threshold_input_tokens)}`));
|
||||
}
|
||||
if (price.fast_pricing_enabled) meta.append(priceBadge(`Fast ×${price.fast_multiplier || "2.5"}`));
|
||||
header.append(name, meta);
|
||||
|
||||
@@ -153,8 +156,7 @@ function renderPriceCard(price) {
|
||||
["", "输入", "缓存读取", "缓存写入", "输出"].forEach(label => table.append(priceCell(label, "model-price-column")));
|
||||
appendPriceRow(table, "标准", price.base);
|
||||
if (price.long_context) {
|
||||
const comparison = price.long_context.comparison === "gte" ? "≥" : ">";
|
||||
appendPriceRow(table, `长上下文\n${comparison} ${compactTokens(price.long_context.threshold_input_tokens)}`, price.long_context, true);
|
||||
appendPriceRow(table, "长上下文", price.long_context, true);
|
||||
} else {
|
||||
table.append(
|
||||
priceCell("长上下文", "model-price-row-label model-price-last-row"),
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
.chart-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 9px; margin-bottom: 12px; }
|
||||
.chart-card { min-height: 180px; padding: 13px 14px; border: 1px solid var(--border-soft); border-radius: 9px; background: var(--panel); }
|
||||
.quota-matrix-wrap { overflow-x: auto; }
|
||||
.quota-matrix { display: grid; min-width: 720px; border-top: 1px solid var(--border-soft); border-left: 1px solid var(--border-soft); font-size: 9px; }
|
||||
.quota-matrix { display: grid; min-width: 720px; border-top: 1px solid var(--border-soft); border-left: 1px solid var(--border-soft); font-size: 12px; }
|
||||
.quota-cell { display: grid; min-width: 0; min-height: 32px; padding: 7px 6px; place-items: center; overflow: hidden; border-right: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft); color: var(--text-secondary); text-align: center; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.quota-cell.header { min-height: 27px; background: var(--bg-primary); color: var(--muted); }
|
||||
.quota-cell.user { text-align: center; }
|
||||
.quota-cell.total { background: var(--bg-tertiary); color: var(--text); font-weight: 600; }
|
||||
.quota-cell.usage { background: color-mix(in srgb, var(--primary-active) var(--quota-intensity), var(--bg-primary)); color: var(--text); }
|
||||
.user-usage { display: grid; overflow-x: auto; }
|
||||
.user-usage-row { display: grid; grid-template-columns: minmax(120px, 1.25fr) repeat(4, minmax(100px, 1fr)) minmax(150px, 1.4fr); align-items: center; gap: 10px; min-width: 790px; min-height: 31px; border-bottom: 1px solid var(--border-soft); font-size: 10px; }
|
||||
.user-usage-row { display: grid; grid-template-columns: 120px repeat(5, minmax(150px, 1fr)); align-items: center; gap: 10px; min-width: 900px; min-height: 34px; border-bottom: 1px solid var(--border-soft); font-size: 12px; }
|
||||
.user-usage-row:last-child { border-bottom: 0; }
|
||||
.user-usage-row.header { min-height: 26px; color: var(--muted); font-size: 9px; }
|
||||
.user-usage-row.header { min-height: 30px; color: var(--muted); font-size: 12px; }
|
||||
.user-usage-row span:not(:first-child) { text-align: right; }
|
||||
.user-management { display: grid; gap: 14px; }
|
||||
.user-management-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
|
||||
@@ -40,14 +40,14 @@
|
||||
.read-only #view-pricing .price-editor { display: none; }
|
||||
.model-price-card { min-width: 0; overflow: hidden; border: 1px solid var(--border); border-radius: 9px; background: var(--bg-primary); }
|
||||
.model-price-card-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 49px; padding: 12px 14px; border-bottom: 1px solid var(--border-soft); }
|
||||
.model-price-card-header h2 { min-width: 0; margin: 0; overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.model-price-card-header h2 { min-width: 0; margin: 0; overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.model-price-meta { display: flex; flex: 0 0 auto; gap: 5px; }
|
||||
.model-price-meta span { padding: 3px 6px; border: 1px solid var(--border); border-radius: 5px; color: var(--text-secondary); font-size: 8px; white-space: nowrap; }
|
||||
.model-price-table { display: grid; grid-template-columns: minmax(72px, .9fr) repeat(4, minmax(65px, 1fr)); }
|
||||
.model-price-table > div { display: flex; align-items: center; min-width: 0; min-height: 35px; padding: 8px 10px; border-right: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft); }
|
||||
.model-price-table > div:nth-child(5n) { border-right: 0; }
|
||||
.model-price-column { color: var(--muted); font-size: 8px; }
|
||||
.model-price-row-label { color: var(--muted); font-size: 8px; line-height: 1.35; white-space: pre-line; }
|
||||
.model-price-value { justify-content: flex-end; font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.model-price-column { justify-content: center; color: var(--muted); font-size: 12px; }
|
||||
.model-price-row-label { color: var(--muted); font-size: 12px; line-height: 1.35; white-space: pre-line; }
|
||||
.model-price-value { justify-content: center; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.model-price-last-row { min-height: 39px !important; border-bottom: 0 !important; }
|
||||
.model-price-unavailable { grid-column: span 4; justify-content: center; border-right: 0 !important; color: var(--muted); font-size: 9px; }
|
||||
.model-price-unavailable { grid-column: span 4; justify-content: center; border-right: 0 !important; color: var(--muted); font-size: 12px; }
|
||||
|
||||
@@ -7,7 +7,7 @@ summary::-webkit-details-marker { display: none; }
|
||||
.table-wrap { overflow: auto; }
|
||||
table { width: 100%; border-collapse: collapse; white-space: nowrap; font-size: 13px; }
|
||||
th, td { padding: 8px 10px; border-bottom: 1px solid var(--border-soft); text-align: right; }
|
||||
th { position: sticky; top: 0; z-index: 1; background: var(--bg-quinary); color: var(--muted); font-size: 10px; font-weight: 650; letter-spacing: .03em; text-transform: uppercase; }
|
||||
th { position: sticky; top: 0; z-index: 1; background: var(--bg-quinary); color: var(--muted); font-size: 12px; font-weight: 650; letter-spacing: .03em; text-transform: uppercase; }
|
||||
th.left, td.left { text-align: left; }
|
||||
tr:last-child td { border-bottom: 0; }
|
||||
tbody tr:hover { background: color-mix(in srgb, var(--text-primary) 3%, transparent); }
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
# CLIProxyAPI 宿主补丁
|
||||
|
||||
这里保存 billing 最初为 CLIProxyAPI `v7.2.133` 制作、已在 `v7.2.135` 重新验证的三项宿主修复。三项补丁都不改变计费规则或数据库结构,必须按下列顺序应用:
|
||||
这里保存 billing 最初为 CLIProxyAPI `v7.2.133` 制作、已在 `v7.2.137` 重新验证的三项宿主修复。三项补丁都不改变计费规则或数据库结构,必须按下列顺序应用:
|
||||
|
||||
1. [`cli-proxy-api-usage-context.patch`](cli-proxy-api-usage-context.patch)
|
||||
2. [`cli-proxy-api-usage-identity.patch`](cli-proxy-api-usage-identity.patch)
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
适用基线:
|
||||
|
||||
- CLIProxyAPI:`v7.2.135`
|
||||
- 提交:`ee2c494788f8a089c58f7ae9aa6ebd1b422211cb`
|
||||
- CLIProxyAPI:`v7.2.137`
|
||||
- 提交:`85d2faddd17e6f4f8675a84ee28b131f702e8eaa`
|
||||
- Native ABI:`1`
|
||||
- 第二项补丁把 RPC schema 从 `3` 提升到 `4`
|
||||
|
||||
@@ -84,8 +84,8 @@ git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-request-lifecycle-
|
||||
cd .externals/CLIProxyAPI
|
||||
gofmt -w internal/pluginhost sdk/api/handlers sdk/cliproxy/usage sdk/pluginabi sdk/pluginapi
|
||||
go test -race ./internal/pluginhost ./sdk/api/handlers ./sdk/cliproxy/usage -count=1
|
||||
version=v7.2.135-dev
|
||||
commit=ee2c4947
|
||||
version=v7.2.137-dev
|
||||
commit=85d2fadd
|
||||
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
CGO_ENABLED=1 go build -buildvcs=false \
|
||||
-ldflags="-s -w -X main.Version=$version -X main.Commit=$commit -X main.BuildDate=$build_date" \
|
||||
@@ -129,7 +129,7 @@ billing 必须使用 RPC schema v4 重新构建;旧 billing 动态库会向下
|
||||
- billing 全部测试、billing race 测试及 CLIProxyAPI 相关包 race 测试通过;
|
||||
- schema v3 对照插件仍能正常注册,新增字段不会发送给旧 schema。
|
||||
|
||||
升级到 CLIProxyAPI `v7.2.135` / `ee2c4947` 后,三项补丁可从干净基线按顺序重放,补丁涉及的三个包及其 race 测试、billing 全仓 race 测试和两个 Linux amd64 产物构建均通过。宿主全仓 `go test ./...` 有 3 个与补丁无关的 Claude 指纹测试失败;相同失败已在未应用补丁的纯上游 `ee2c4947` 上复现,详见 [`../docs/operations.md`](../docs/operations.md)。
|
||||
升级到 CLIProxyAPI `v7.2.137` / `85d2fadd` 后,三项补丁可从干净基线按顺序重放,补丁涉及的三个包及其 race 测试通过。宿主全仓测试中的 3 个 Claude 指纹失败和 2 个 `reviewedInPlaceByteWrites` 清单过期失败均已在未应用补丁的纯上游 `85d2fadd` 上原样复现,属于上游例外,详见 [`../docs/operations.md`](../docs/operations.md)。
|
||||
|
||||
## 性能影响
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ printf '%s\n' \
|
||||
"target=linux/amd64" \
|
||||
"native_abi=1" \
|
||||
"rpc_schema=4" \
|
||||
"cliproxyapi_revision=ee2c494788f8a089c58f7ae9aa6ebd1b422211cb+usage-context+usage-identity+request-lifecycle-cancel" \
|
||||
"cliproxyapi_revision=85d2faddd17e6f4f8675a84ee28b131f702e8eaa+usage-context+usage-identity+request-lifecycle-cancel" \
|
||||
>"$package_dir/VERSION.txt"
|
||||
|
||||
archive="$root/dist/$name.tar.gz"
|
||||
|
||||
Reference in New Issue
Block a user