feat: update billing metrics and CPA baseline

This commit is contained in:
chuan
2026-08-21 00:43:34 +08:00
parent 4474069a29
commit b2a79b80dc
14 changed files with 166 additions and 39 deletions
+2 -2
View File
@@ -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>/`;新版本启动失败时恢复旧二进制和插件。
+1 -1
View File
@@ -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
View File
@@ -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 \
+1
View File
@@ -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
View File
@@ -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()
}
+62 -4
View File
@@ -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)
}
+23 -3
View File
@@ -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 => {
+5 -3
View File
@@ -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"),
+3 -3
View File
@@ -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; }
+5 -5
View File
@@ -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; }
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"