From 7f7695a268af3eb403ff1f335e88cd12677b75ff Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 17 Jul 2026 01:00:56 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E6=95=9B=20Sub2API=20=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E8=AF=84=E5=88=86=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unidesk-sub2api/references/codex-pool.md | 3 + .../R14_Task_Report.md | 32 +++++++ docs/MDTODO/sub2api-upstream-reliability.md | 4 + .../runtime-render.ts | 85 +++++++++++-------- 4 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 docs/MDTODO/details/sub2api-upstream-reliability/R14_Task_Report.md diff --git a/.agents/skills/unidesk-sub2api/references/codex-pool.md b/.agents/skills/unidesk-sub2api/references/codex-pool.md index 3064e2cf..3ffeba15 100644 --- a/.agents/skills/unidesk-sub2api/references/codex-pool.md +++ b/.agents/skills/unidesk-sub2api/references/codex-pool.md @@ -30,6 +30,9 @@ - 成功请求分母和账号级 TTFT 来自原生 admin usage; - 同一张账号表必须聚合请求数、Token、标准 API 美元计费基数、账号名称末尾的人民币采购倍率和人民币上游成本; - 常规评分与成本评估只运行一次 `runtime errors --group ... --since ...`,不再分别调用 usage、profit 和逐账号查询拼表; + - 默认文本使用账号评分与用量聚合单表: + - 同表披露请求、Token、标准 API 美元计费基数、人民币采购倍率与上游成本、可归因失败率、TTFT、切号恢复和可用性; + - 客户错误只保留有界桶摘要,完整账号证据使用 `--account`,完整有限索引使用 `--full`; - 客户错误来自原生 request-errors; - 切号、同账号重试、临时不可调度和 `forward_failed` 来自原生 system-log 索引; - 默认权重为可靠性 60、TTFT 25、当前可用性 15,并同时输出评分组成、等级、置信度和扣分原因; diff --git a/docs/MDTODO/details/sub2api-upstream-reliability/R14_Task_Report.md b/docs/MDTODO/details/sub2api-upstream-reliability/R14_Task_Report.md new file mode 100644 index 00000000..b3526835 --- /dev/null +++ b/docs/MDTODO/details/sub2api-upstream-reliability/R14_Task_Report.md @@ -0,0 +1,32 @@ +# R14 Sub2API 账号评分渐进披露改进报告 + +## 问题 + +`runtime errors --group ... --since ...` 默认文本把账号质量、用量成本和故障控制拆成三张宽表,并附完整客户错误维度与 request 索引。12 个账号时远端结果达到约 35-50 KB,默认输出可能触发全局保护;本次调优因此退化为多次逐账号查询,违背“一次常规评分命令完成评分与成本评估”的既有合同。 + +## 实现 + +- 关联 [UniDesk #2408](https://github.com/pikasTech/unidesk/issues/2408)。 +- 默认分组视图把账号名称、ID、priority、评分、等级、置信度、尝试、可归因上游失败、失败率、TTFT P95、请求、Token、API USD、人民币采购倍率、人民币上游成本、切号恢复和可用性合并为一张表。 +- 默认客户错误只保留有界模型、状态码、同步/流式和消息桶摘要。 +- `--account` 继续展示完整账号质量、成本、排除原因和故障控制;`--full` 继续提供完整有限索引;显式 JSON 合同不变。 +- 更新 `unidesk-sub2api` 权威 reference,固定一次常规命令的最短路径和渐进披露边界。 +- 未修改 Sub2API 源码、版本、生产 runtime、账号或 YAML 配置。 + +## 验证 + +执行真实只读命令: + +`bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors --target PK01 --since 8h --group 2` + +结果: + +- 一次调用直接返回 12 个账号的聚合评分、用量、成本与恢复信息。 +- 默认输出没有 `outputTruncated`,无需 dump 或逐账号拼表。 +- 同一输出保留 group 概要、TTFT、客户错误摘要、可用性、根因和策略效果。 +- 账号与 `--full` 下钻命令继续在默认输出末尾明确展示。 +- 按仓库规则未新增测试;真实只读 smoke 同时验证了参数解析、远端采集、compact 投影和默认文本渲染。 + +## 工具调用改进 + +同类账号池调优由“全组查询触发输出保护,再对多个账号逐一补查”收敛为“一次全组查询;只有具体 request 或单账号配置需要时再下钻”。变化项继续由 `--target`、`--since` 和 `--group` 参数表达,没有硬编码本次账号、窗口或分组。 diff --git a/docs/MDTODO/sub2api-upstream-reliability.md b/docs/MDTODO/sub2api-upstream-reliability.md index 4169e952..da603de5 100644 --- a/docs/MDTODO/sub2api-upstream-reliability.md +++ b/docs/MDTODO/sub2api-upstream-reliability.md @@ -327,3 +327,7 @@ ## R13 [completed] 调优 PK01 Sub2API 账号池:综合最近 8 小时所有分组的客户可见错误、上游归因、账号评分、普通与 compact TTFT、请求、Token、API USD、人民币边际成本、failover、临时不可调度规则命中和当前全局 priority,区分非上游问题后生成保守方案;仅对证据充分账号执行精准 runtime dry-run、confirm 和回读,新窗口短回归,不修改 Sub2API 源码、版本、凭据、分组、代理、容量、Secret 或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R13_Task_Report.md)。 + +## R14 [completed] + +完成 [UniDesk #2408](https://github.com/pikasTech/unidesk/issues/2408):改进 Sub2API `runtime errors --group` 默认渐进披露,将账号评分、请求、Token、API USD、人民币采购倍率与成本、失败率、TTFT、切号恢复和可用性聚合为单表,客户错误保留有界摘要,账号与 `--full` 下钻保持完整;用真实 8 小时全组命令证明一次调用不触发输出保护并替代逐账号拼表,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R14_Task_Report.md)。 diff --git a/scripts/src/platform-infra-sub2api-codex/runtime-render.ts b/scripts/src/platform-infra-sub2api-codex/runtime-render.ts index 7306d4c1..36b1905f 100644 --- a/scripts/src/platform-infra-sub2api-codex/runtime-render.ts +++ b/scripts/src/platform-infra-sub2api-codex/runtime-render.ts @@ -109,8 +109,8 @@ export function compactRuntimeErrors(value: unknown, options: RuntimeOptions): u overview: nativeOps.overview, responseHeaderTimeout: options.account === null || !options.json ? nativeOps.responseHeaderTimeout : undefined, requestTiming: options.account === null || !options.json ? compactRequestTiming(nativeOps.requestTiming) : undefined, - customerErrors: compactCustomerErrors(nativeOps.customerErrors, options.account !== null || !options.json), - accountQuality: compactAccountQuality(nativeOps.accountQuality, options.account !== null || !options.json), + customerErrors: compactCustomerErrors(nativeOps.customerErrors, options.account !== null), + accountQuality: compactAccountQuality(nativeOps.accountQuality, options.account !== null), accountAvailability: { status: availability.status, totalAccounts: availabilityGroup.total_accounts, @@ -414,39 +414,54 @@ function renderRuntimeErrors(lines: string[], runtime: Record, ])); const qualityAccounts = runtimeRecords(accountQuality.accounts); if (qualityAccounts.length > 0) { - lines.push("", "ACCOUNT QUALITY", renderTable([ - ["ACCOUNT", "ID", "PRIORITY", "SCORE", "GRADE", "QUALITY", "CONF", "ATTEMPTS", "SUCCESS", "FAIL", "UP_CUST", "EXCLUDED", "FAIL_RATE", "TTFT_P95_MS", "TTFT_N", "AVAILABLE", "REASONS"], - ...qualityAccounts.map((item) => [ - runtimeText(item.accountName), runtimeText(item.accountId), runtimeText(item.priority), runtimeText(item.score), runtimeText(item.grade), runtimeText(item.assessment), runtimeText(item.confidence), - runtimeText(item.observedAttempts), runtimeText(item.successRequests), runtimeText(item.failureRequests), runtimeText(item.scoreableUpstreamErrorRequests), - runtimeText(item.excludedNonUpstreamErrorRequests), runtimeRate(item.failureRate), - runtimeText(item.ttftP95Ms), runtimeText(item.firstTokenSamples), runtimeText(item.currentlyAvailable), - runtimeShort(Array.isArray(item.reasons) ? item.reasons.join(",") : "", 58), - ]), - ])); - lines.push("ACCOUNT USAGE AND COST", renderTable([ - ["ACCOUNT", "ID", "REQUESTS", "TOKENS", "API_USD", "COST_RATE_CNY", "UPSTREAM_COST_CNY", "EXCLUDED_REASONS"], - ...qualityAccounts.map((item) => { - const usage = runtimeRecord(item.usage) ?? {}; - const excluded = runtimeRecords(item.excludedReasonBuckets).map((entry) => `${runtimeText(entry.reason)}:${runtimeText(entry.count)}`).join(","); - return [ - runtimeText(item.accountName), runtimeText(item.accountId), runtimeText(usage.requestCount), runtimeMillions(usage.tokenCount), - runtimeDecimal(usage.apiAmountUsd, 4), runtimeDecimal(usage.costRateCnyPerApiUsd, 3), runtimeDecimal(usage.upstreamCostCny, 4), - runtimeShort(excluded, 64), - ]; - }), - ])); - const controlRows = qualityAccounts.filter((item) => [ - item.failoverRequests, item.sameAccountRetryEvents, item.tempUnschedulableEvents, item.forwardFailedRequests, item.customerErrorRequests, - ].some((value) => typeof value === "number" && value > 0)); - if (controlRows.length > 0) lines.push("ACCOUNT FAILURE CONTROL", renderTable([ - ["ACCOUNT", "ID", "FAILOVER", "RECOVERED", "FAILED", "MISSING", "RETRY", "TEMP_UNSCHED", "FORWARD_FAIL", "CUSTOMER_ERR"], - ...controlRows.map((item) => [ - runtimeText(item.accountName), runtimeText(item.accountId), runtimeText(item.failoverRequests), runtimeText(item.failoverRecovered), - runtimeText(item.failoverFailed), runtimeText(item.failoverOutcomeMissing), runtimeText(item.sameAccountRetryEvents), - runtimeText(item.tempUnschedulableEvents), runtimeText(item.forwardFailedRequests), runtimeText(item.customerErrorRequests), - ]), - ])); + if (options.account === null) { + lines.push("", "ACCOUNT SCORE AND USAGE", renderTable([ + ["ACCOUNT", "ID", "PRI", "SCORE", "GRADE", "CONF", "ATTEMPTS", "UP_FAIL", "FAIL_RATE", "TTFT_P95", "REQUESTS", "TOKENS", "API_USD", "RATE_CNY", "COST_CNY", "FAILOVER_OK", "AVAILABLE"], + ...qualityAccounts.map((item) => { + const usage = runtimeRecord(item.usage) ?? {}; + return [ + runtimeShort(item.accountName, 36), runtimeText(item.accountId), runtimeText(item.priority), runtimeText(item.score), runtimeText(item.grade), runtimeText(item.confidence), + runtimeText(item.observedAttempts), runtimeText(item.scoreableUpstreamErrorRequests), runtimeRate(item.failureRate), runtimeText(item.ttftP95Ms), + runtimeText(usage.requestCount), runtimeMillions(usage.tokenCount), runtimeDecimal(usage.apiAmountUsd, 4), runtimeDecimal(usage.costRateCnyPerApiUsd, 3), runtimeDecimal(usage.upstreamCostCny, 4), + `${runtimeText(item.failoverRecovered)}/${runtimeText(item.failoverRequests)}`, runtimeText(item.currentlyAvailable), + ]; + }), + ])); + } else { + lines.push("", "ACCOUNT QUALITY", renderTable([ + ["ACCOUNT", "ID", "PRIORITY", "SCORE", "GRADE", "QUALITY", "CONF", "ATTEMPTS", "SUCCESS", "FAIL", "UP_CUST", "EXCLUDED", "FAIL_RATE", "TTFT_P95_MS", "TTFT_N", "AVAILABLE", "REASONS"], + ...qualityAccounts.map((item) => [ + runtimeText(item.accountName), runtimeText(item.accountId), runtimeText(item.priority), runtimeText(item.score), runtimeText(item.grade), runtimeText(item.assessment), runtimeText(item.confidence), + runtimeText(item.observedAttempts), runtimeText(item.successRequests), runtimeText(item.failureRequests), runtimeText(item.scoreableUpstreamErrorRequests), + runtimeText(item.excludedNonUpstreamErrorRequests), runtimeRate(item.failureRate), + runtimeText(item.ttftP95Ms), runtimeText(item.firstTokenSamples), runtimeText(item.currentlyAvailable), + runtimeShort(Array.isArray(item.reasons) ? item.reasons.join(",") : "", 58), + ]), + ])); + lines.push("ACCOUNT USAGE AND COST", renderTable([ + ["ACCOUNT", "ID", "REQUESTS", "TOKENS", "API_USD", "COST_RATE_CNY", "UPSTREAM_COST_CNY", "EXCLUDED_REASONS"], + ...qualityAccounts.map((item) => { + const usage = runtimeRecord(item.usage) ?? {}; + const excluded = runtimeRecords(item.excludedReasonBuckets).map((entry) => `${runtimeText(entry.reason)}:${runtimeText(entry.count)}`).join(","); + return [ + runtimeText(item.accountName), runtimeText(item.accountId), runtimeText(usage.requestCount), runtimeMillions(usage.tokenCount), + runtimeDecimal(usage.apiAmountUsd, 4), runtimeDecimal(usage.costRateCnyPerApiUsd, 3), runtimeDecimal(usage.upstreamCostCny, 4), + runtimeShort(excluded, 64), + ]; + }), + ])); + const controlRows = qualityAccounts.filter((item) => [ + item.failoverRequests, item.sameAccountRetryEvents, item.tempUnschedulableEvents, item.forwardFailedRequests, item.customerErrorRequests, + ].some((value) => typeof value === "number" && value > 0)); + if (controlRows.length > 0) lines.push("ACCOUNT FAILURE CONTROL", renderTable([ + ["ACCOUNT", "ID", "FAILOVER", "RECOVERED", "FAILED", "MISSING", "RETRY", "TEMP_UNSCHED", "FORWARD_FAIL", "CUSTOMER_ERR"], + ...controlRows.map((item) => [ + runtimeText(item.accountName), runtimeText(item.accountId), runtimeText(item.failoverRequests), runtimeText(item.failoverRecovered), + runtimeText(item.failoverFailed), runtimeText(item.failoverOutcomeMissing), runtimeText(item.sameAccountRetryEvents), + runtimeText(item.tempUnschedulableEvents), runtimeText(item.forwardFailedRequests), runtimeText(item.customerErrorRequests), + ]), + ])); + } const scoreRule = runtimeRecord(accountQuality.scoreRule) ?? {}; lines.push(`SCORE RULE reliability=${runtimeText(scoreRule.reliabilityWeight)} latency=${runtimeText(scoreRule.latencyWeight)} availability=${runtimeText(scoreRule.availabilityWeight)} confidence=separate priority=lower-is-higher highest=0`); if (typeof accountQuality.attribution === "string") lines.push(`NOTE: ${accountQuality.attribution}`);