Merge remote-tracking branch 'origin/master' into feat/pikaoa-test-nc01-runtime

This commit is contained in:
Codex
2026-07-15 03:52:28 +02:00
25 changed files with 713 additions and 60 deletions
@@ -95,6 +95,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --template <template-id>
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --template <template-id> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --kind priority --priority <0-1000> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <id-or-exact-name> --kind account-name --name <new-name> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --target PK01 --account <name-or-id> --kind temp-unschedulable
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <name-or-id> --kind model-mapping --model <client-model> --upstream-model <upstream-model>
bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --target PK01 --account <name-or-id> --kind model-mapping --model <client-model>
@@ -106,6 +107,9 @@ bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --sinc
bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --since 24h --json
bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --since 24h --accounts
bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --since 24h --missing-costs
bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --month 2026-07
bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --month 2026-07 --forecast
bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --target PK01 --active-since 24h --amount-usd 20 --reason <text>
bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --target D601 --confirm
@@ -125,6 +129,9 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60
- `profit` 只读聚合 Sub2API 原生 `/api/v1/admin/usage`
- 默认窗口、盈利分组、自用分组、排除分组、人民币售价和账号采购成本来源由 `config/platform-infra/sub2api-codex-pool.yaml#profit` 声明;
- 原生 `actual_cost` 是标准 API 美元计费基数;`saleRateCnyPerApiUsd` 和账号名称末尾成本都表示每 1 美元标准 API 计费对应的人民币单价,不做汇率换算;
- `periodCosts` 声明按 `YYYY-MM` 入账的一次性人民币固定成本;`--month YYYY-MM` 使用原生 `/api/v1/admin/usage/stats` 按分组和账号聚合该月流水并一次扣除匹配期间成本,任意 `--since` 窗口继续读取逐条 usage 以保持滚动窗口精度且不分摊固定成本;
- 账号调度经济性使用边际采购倍率,不把已经发生的一次性期间成本摊回请求成本或用于压低账号优先级;月度净利润必须单列期间成本后再计算;
- `--month YYYY-MM --forecast` 以月初至当前的实际秒数计算线性 run-rate:收入、盈利池流量成本和自用流量成本按完整月份秒数外推,一次性 `periodCosts` 只扣一次;同时保留 MTD 实际汇总和两种整月预计利润,不能把预测值表述为已实现收益;
- 默认 Kubernetes 风格输出优先给出总收入、已确认成本、扣除已知成本后的待定利润基数、未知成本对应的 API USD 基数、完整性和参数化利润公式;
- 显式 `--json` 返回同一份稳定紧凑汇总,不携带完整账号数组,避免大窗口触发全局输出保护;
- `--accounts` 固定返回一页完整账号名称、账号 ID、请求数、Token、标准 API 美元计费基数、人民币采购单价、人民币收入、人民币上游成本和人民币利润贡献;存在后续页时返回由分类、分组、账号 ID 和名称生成的 opaque `--page-token` 命令,不提供手工 `limit`
@@ -133,6 +140,13 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60
- 账号名称末尾缺少采购成本系数,或存在未分类流量时,相关口径必须显示 `partial`,禁止把缺失值当作零;
- `--json` 保留精确十进制字符串、原生来源、时间窗、分页数和完整性明细;命令不修改 runtime。
- `credits grant` 管理客户余额补偿:
- `--active-since <duration>` 从原生 usage 选取窗口内有请求的非管理员活跃用户;`--users <id-or-email,...>` 使用精确 selector;两者必须且只能选择一个;
- 默认 dry-run,逐用户披露完整邮箱、用户 ID、请求数、当前余额、增加额和目标余额;
- 活跃窗口只允许生成计划;实际写入必须把 dry-run 返回的用户 ID 固定为精确 `--users` selectors,再显式 `--confirm`,防止移动窗口在确认时扩大或缩小名单;
- 确认写入为每个用户生成由精确名单、金额和原因确定的 `Idempotency-Key`,调用原生 `POST /api/v1/admin/users/:id/balance``add` 操作,随后逐用户回读余额并对账;
- 实际批量充值属于外部财务影响操作,执行前必须取得用户对本次具体名单、金额和原因的明确授权;仅提出设计目标或要求 dry-run 不构成授权。
`config/platform-infra/sub2api-codex-pool.yaml` 控制:
- `pool.groupName`: Sub2API group 名称。
@@ -151,6 +165,9 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60
- 数值越小优先级越高,不存在分组级 priority;
- 单账号和精准批量都必须先 dry-run,确认写入后逐账号回读并显示 before、desired、actual 与 reconciled
- 只更新账号 priority,不携带 credentials、分组、容量、代理、状态、可调度或临时不可调度字段。
- 账号人类可读名称使用 `runtime apply --kind account-name --name <new-name>`
- 只接受单个精确账号 selector,先 dry-run,确认后自动回读名称;
- 只更新名称,不携带 credentials、priority、分组、容量、代理、状态或可调度字段。
- 模型映射先用 `runtime models --account ...` 获取原生配置与实时上游能力:
- 只有实时模型列表或真实模型请求证明支持时才确认写入。
- 禁止把名称相近的模型猜测为别名。
@@ -7,5 +7,7 @@
- 不给 Sub2API manifest 添加 CPU/memory limits,除非有新的 YAML 化明确决策。
- 不打印完整 API key、admin password 或 Secret 明文。
- 不把普通上游增删做成代码变更、CI/CD、feature flag 或兼容双路径。
- 除非用户明确指定,不修改 Sub2API 源码或版本;优先使用官方配置、原生调度能力和 owning YAML,官方不支持时明确报告能力边界。
- 除非用户明确授权,不启用、扩展或调整 Sub2API 外部哨兵;外部哨兵不是配置优化、换号或错误缓解的默认手段。
- 不把手动禁用账号、删除账号、移除 YAML entry、降低 membership、临时改 priority/capacity/loadFactor、provider pinning 或给某个账号特权当作通用 failover / 哨兵隔离恢复问题的修复。
- 不魔改 Sub2API:Sub2API 本身不支持的能力就不做,不通过 UniDesk 脚本、k8s 原地热补、本地 fork、YAML 伪声明或隐藏 fallback 代替上游实现。
+58
View File
@@ -10,6 +10,7 @@ metadata:
- 281
- 297
- 300
- 2078
cluster:
role: primary
@@ -374,6 +375,16 @@ postgres:
user: pikaoa
address: 152.53.229.148/32
method: scram-sha-256
- type: hostssl
database: pikaoa_test
user: pikaoa_test
address: 10.0.8.0/22
method: scram-sha-256
- type: hostssl
database: pikaoa_test
user: pikaoa_test
address: 152.53.229.148/32
method: scram-sha-256
secrets:
source: master-local
@@ -491,6 +502,20 @@ secrets:
PIKAOA_DB_NAME: pikaoa
randomHex:
PIKAOA_DB_PASSWORD: 32
- name: pikaoa-test-db-credentials
sourceRef: platform-db/pikaoa-test-db.env
type: env
requiredKeys:
- PIKAOA_TEST_DB_USER
- PIKAOA_TEST_DB_PASSWORD
- PIKAOA_TEST_DB_NAME
createIfMissing:
enabled: true
values:
PIKAOA_TEST_DB_USER: pikaoa_test
PIKAOA_TEST_DB_NAME: pikaoa_test
randomHex:
PIKAOA_TEST_DB_PASSWORD: 32
objects:
roles:
- name: sub2api
@@ -565,6 +590,15 @@ objects:
createdb: false
createrole: false
superuser: false
- name: pikaoa_test
passwordRef:
sourceRef: platform-db/pikaoa-test-db.env
key: PIKAOA_TEST_DB_PASSWORD
login: true
attributes:
createdb: false
createrole: false
superuser: false
databases:
- name: sub2api
owner: sub2api
@@ -606,6 +640,11 @@ objects:
encoding: UTF8
locale: C.UTF-8
extensions: []
- name: pikaoa_test
owner: pikaoa_test
encoding: UTF8
locale: C.UTF-8
extensions: []
exports:
connectionStrings:
@@ -764,6 +803,22 @@ exports:
- scope: pikaoa
secret: pikaoa-runtime
key: DATABASE_URL
- name: pikaoa-test-database-url
sourceSecretRef: platform-db/pikaoa-test-db.env
render:
envKey: DATABASE_URL
format: postgresql://$(PIKAOA_TEST_DB_USER):$(PIKAOA_TEST_DB_PASSWORD)@$(PGHOST):$(PGPORT)/$(PIKAOA_TEST_DB_NAME)?sslmode=require
variables:
PGHOST: 82.156.23.220
PGPORT: "5432"
writeToSecretSource:
sourceRef: platform-infra/pikaoa-test.env
key: DATABASE_URL
mode: update-or-insert
consumers:
- scope: pikaoa-test
secret: pikaoa-test-runtime
key: DATABASE_URL
backup:
phase: minimum-restoreable
@@ -812,6 +867,9 @@ observability:
- kind: psql-app-role
database: pikaoa
user: pikaoa
- kind: psql-app-role
database: pikaoa_test
user: pikaoa_test
- kind: disk-free
path: /var/lib/postgresql/16/main
minFreeGiB: 10
@@ -65,6 +65,12 @@ profit:
usageCostField: actual_cost
accountCostSource: account-name-trailing-cny-per-api-usd
saleRateCnyPerApiUsd: 0.10
periodCosts:
- id: lyon9801-acquisition
period: "2026-07"
accountName: lyon9801 0.0
amountCny: 1300
description: lyon9801 的一次性采购成本;边际流量采购倍率为 0.0。
groups:
profitable: [unidesk-codex-pool]
selfUse: [自用]
@@ -0,0 +1,36 @@
# R1.5.9.4 任务报告
## 任务结果
-`config/platform-db/postgres-pk01.yaml` 声明独立 `pikaoa_test` role、database、凭据 `sourceRef`、连接串 export、NC01 来源 HBA 和有界状态检查。
- 测试凭据来源为 `platform-db/pikaoa-test-db.env`,连接串写入 `platform-infra/pikaoa-test.env`consumer 为 `pikaoa-test-runtime/DATABASE_URL`
- 保持既有生产 `pikaoa` role、database、Secret export 和 consumer 声明不变。
- 收紧 `platform-db postgres plan` 的 role、database 和 Secret 默认摘要,确保新增对象后仍保持有界可见。
## 交付证据
- 执行记录:[pikasTech/unidesk#2078](https://github.com/pikasTech/unidesk/issues/2078)。
- 提交:[d281b560](https://github.com/pikasTech/unidesk/commit/d281b560746b0cd61b018a5d628490685b62d210)。
- PR[pikasTech/unidesk#2083](https://github.com/pikasTech/unidesk/pull/2083)。
## 验证摘要
- `bun --check scripts/src/platform-db.ts`:通过。
- `git diff --check`:通过。
- `bun scripts/cli.ts platform-db postgres plan --config config/platform-db/postgres-pk01.yaml`:显示 `pikaoa_test` role、database、Secret sourceRef 和连接串 export;输出包含 `mutation=false``valuesPrinted=false`
- `bun scripts/cli.ts platform-db postgres status --config config/platform-db/postgres-pk01.yaml`:对尚未落地的 `pikaoa_test` role、database 和 export 返回有界 actionable 摘要;未执行 mutation。
- `bun scripts/cli.ts platform-db postgres export-secrets --config config/platform-db/postgres-pk01.yaml --dry-run`:显示独立测试凭据与连接串 export 的 create 计划;输出包含 `mutation=false``valuesPrinted=false`
- PR preflight`MERGEABLE/CLEAN`,无 pending 或失败检查。
## 执行边界
- 未执行 live `platform-db postgres apply`
- 未写入任何 Secret source 文件。
- 未触碰 NC01 k3s 运行对象。
- 未扩展 PikaOA 产品源码或其他配置范围。
## 已知风险
- 当前 PK01 状态同时显示既有生产 `pikaoa` role、database 和 export 尚未落地。
- owning YAML 的 live `apply` 是 shared apply;主代理后续执行时,production `pikaoa` 与新增 `pikaoa_test` 将被同批收敛。
- 该 shared apply 风险不由本任务提前消除或绕过;执行 live apply 前必须由主代理明确确认影响范围,并继续保持 Secret 脱敏和非阻塞漂移 warning 边界。
@@ -0,0 +1,15 @@
# R1.5.9.6 任务报告
## 结果
- 产品仓库新增 API、Worker、Web 三个职责独立的多阶段 OCI 构建输入。
- API 镜像同时包含服务二进制和迁移 CLI;API、Worker 使用 nonroot 运行。
- Web 使用 nginx-unprivileged,运行时由 `PIKAOA_API_UPSTREAM` 注入同源 API 上游,保留 `/healthz` 和 SPA fallback。
- PRpikainc/pikaoa#15;合并提交:`0184d200db3f1e5fb1ce6aec1a10cf7d3843849d`
## 验证
- NC01 从干净提交构建 API、Worker、Web 三个镜像成功。
- `go test ./...` 通过。
- Web 测试与构建通过,代理、health、SPA 深链 smoke 通过。
- 构建验证发现既有事件版本一致性校验阻塞 API 启动,已登记 pikainc/pikaoa#16 并作为独立最小修复处理。
+5 -2
View File
@@ -102,15 +102,18 @@
##### R1.5.9.3 [completed]
按用户确认方案将测试架构定锚为现有 NC01 k3s 的 pikaoa-test namespace、PK01 host PostgreSQL 独立测试库与角色、YAML hostIP 端口,并更新 SPEC 与 [pikasTech/unidesk#2058](https://github.com/pikasTech/unidesk/issues/2058),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9.3_Task_Report.md)。
##### R1.5.9.4 [in_progress]
##### R1.5.9.4 [completed]
在 PK01 host PostgreSQL 声明独立 pikaoa_test database/role、Secret export 与 NC01 HBA,执行记录见 [pikasTech/unidesk#2078](https://github.com/pikasTech/unidesk/issues/2078),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9.4_Task_Report.md)。
##### R1.5.9.5 [in_progress]
在 NC01 pikaoa-test namespace 实现外部 PK01 测试 DSN、API/Worker/Web/迁移/附件 PVC、OTel/Prometheus、YAML hostIP 端口和受控单步 CLI,执行记录见 [pikasTech/unidesk#2079](https://github.com/pikasTech/unidesk/issues/2079),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9.5_Task_Report.md)。
##### R1.5.9.6 [in_progress]
##### R1.5.9.6 [completed]
为产品仓库补齐 API、Worker、Web 的 commit-pinned OCI 构建输入,使 NC01 测试运行面和后续 PaC 可构建镜像,执行记录见 [pikainc/pikaoa#14](https://github.com/pikainc/pikaoa/issues/14),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9.6_Task_Report.md)。
##### R1.5.9.7 [in_progress]
降级事件版本一致性校验并解除 MVP 启动阻塞(pikainc/pikaoa#16),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9.7_Task_Report.md)。
### R1.6
审核并合并双仓 PR,执行首次受控 PaC bootstrap,由正常 source PR merge 自动发布,依赖 R1.3-R1.5,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.6_Task_Report.md)。
+15 -14
View File
@@ -1113,27 +1113,28 @@ function compactPlan(
transport: pg.postgres.network.transport,
sslmode: pg.postgres.network.sslmode,
},
roles: pg.objects.roles.map((role) => ({
roles: compactActionableSummary(pg.objects.roles.map((role) => ({
name: role.name,
exists: observedRoles.find((item) => item.name === role.name)?.exists ?? null,
action: observedRoles.some((item) => item.name === role.name && item.exists) ? "none" : "create-or-update",
})),
databases: pg.objects.databases.map((database) => ({
})), {
passed: (item) => item.action === "none",
missing: (item) => item.exists !== true,
actionable: (item) => item.action !== "none",
project: (item) => item,
}),
databases: compactActionableSummary(pg.objects.databases.map((database) => ({
name: database.name,
owner: database.owner,
exists: observedDatabases.find((item) => item.name === database.name)?.exists ?? null,
action: observedDatabases.some((item) => item.name === database.name && item.exists) ? "none" : "create",
})),
secrets: {
ok: secrets.ok,
root: secrets.root,
entries: secrets.entries.map((entry) => ({
sourceRef: entry.sourceRef,
missingKeys: entry.missingKeys,
action: entry.action,
})),
valuesPrinted: false,
},
})), {
passed: (item) => item.action === "none",
missing: (item) => item.exists !== true,
actionable: (item) => item.action !== "none",
project: (item) => item,
}),
secrets: compactSecretInspection(secrets),
exports: pg.exports.connectionStrings.map((item) => ({
name: item.name,
sourceRef: item.sourceSecretRef,
@@ -226,11 +226,32 @@ function readProfitConfig(value: unknown): CodexPoolProfitConfig {
if (accountCostSource !== "account-name-trailing-cny-per-api-usd") {
throw new Error(`${codexPoolConfigPath}.profit.accountCostSource must be account-name-trailing-cny-per-api-usd`);
}
if (!Array.isArray(value.periodCosts)) {
throw new Error(`${codexPoolConfigPath}.profit.periodCosts must be an array`);
}
const periodCostIds = new Set<string>();
const periodCosts = value.periodCosts.map((item, index) => {
const path = `profit.periodCosts[${index}]`;
if (!isRecord(item)) throw new Error(`${codexPoolConfigPath}.${path} must be an object`);
const id = requiredStringConfigField(item, "id", path);
if (periodCostIds.has(id)) throw new Error(`${codexPoolConfigPath}.profit.periodCosts contains duplicate id ${id}`);
periodCostIds.add(id);
const period = requiredStringConfigField(item, "period", path);
if (!/^\d{4}-(?:0[1-9]|1[0-2])$/u.test(period)) {
throw new Error(`${codexPoolConfigPath}.${path}.period must use YYYY-MM`);
}
const accountName = requiredStringConfigField(item, "accountName", path);
const amountCny = numberValue(item.amountCny);
if (amountCny === null || amountCny < 0) throw new Error(`${codexPoolConfigPath}.${path}.amountCny must be >= 0`);
const description = requiredStringConfigField(item, "description", path);
return { id, period, accountName, amountCny, description };
});
return {
defaultWindow,
usageCostField,
accountCostSource,
saleRateCnyPerApiUsd,
periodCosts,
groups: { profitable, selfUse, excluded },
};
}
@@ -0,0 +1,153 @@
import type { UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { renderTable } from "./render";
import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
import { creditsScript } from "./remote-scripts";
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runtime-target";
interface CreditsOptions {
activeSince: string | null;
users: string[];
amountUsd: number;
reason: string;
confirm: boolean;
targetId: string;
json: boolean;
}
export async function codexPoolCredits(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
if (args.includes("--help")) return renderCreditsHelp();
const options = parseCreditsOptions(args);
const target = codexPoolRuntimeTarget(options.targetId);
const remote = await runRemoteCodexPoolScript(config, "credits", creditsScript(options, target), target);
const report = parseJsonOutput(remote.stdout);
const ok = remote.exitCode === 0 && boolField(report, "ok", false);
const response = {
ok,
action: "platform-infra-sub2api-codex-pool-credits",
target: { id: target.id, route: target.route, runtimeMode: target.runtimeMode, endpoint: target.serviceDns },
request: {
selection: options.activeSince === null ? "exact-users" : "active-users",
activeSince: options.activeSince,
users: options.users,
amountUsd: options.amountUsd,
reason: options.reason,
confirm: options.confirm,
},
report,
remote: compactCapture(remote, { full: !ok || report === null }),
valuesPrinted: false,
};
return options.json ? renderCreditsJson(response) : renderCredits(response);
}
function parseCreditsOptions(args: string[]): CreditsOptions {
const [action = "grant", ...rest] = args;
if (action !== "grant") throw new Error("credits usage: grant (--active-since 24h|--users <id-or-email,...>) --amount-usd <amount> --reason <text> [--confirm] [--target <id>] [--json]");
let activeSince: string | null = null;
let users: string[] = [];
let amountUsd: number | null = null;
let reason: string | null = null;
let confirm = false;
let targetId = defaultCodexPoolRuntimeTargetId();
let json = false;
const readValue = (index: number, option: string): [string, number] => {
const value = rest[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${option} requires a value`);
return [value.trim(), index + 1];
};
for (let index = 0; index < rest.length; index += 1) {
const arg = rest[index]!;
if (arg === "--confirm") confirm = true;
else if (arg === "--json") json = true;
else if (arg === "--active-since") [activeSince, index] = readValue(index, arg);
else if (arg.startsWith("--active-since=")) activeSince = arg.slice("--active-since=".length).trim();
else if (arg === "--users") {
let value: string;
[value, index] = readValue(index, arg);
users = parseSelectors(value);
} else if (arg.startsWith("--users=")) users = parseSelectors(arg.slice("--users=".length));
else if (arg === "--amount-usd") {
let value: string;
[value, index] = readValue(index, arg);
amountUsd = parseAmount(value);
} else if (arg.startsWith("--amount-usd=")) amountUsd = parseAmount(arg.slice("--amount-usd=".length));
else if (arg === "--reason") [reason, index] = readValue(index, arg);
else if (arg.startsWith("--reason=")) reason = arg.slice("--reason=".length).trim();
else if (arg === "--target") [targetId, index] = readValue(index, arg);
else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim();
else throw new Error(`unsupported credits option: ${arg}`);
}
if ((activeSince === null) === (users.length === 0)) throw new Error("use exactly one of --active-since or --users");
if (activeSince !== null && !/^[1-9][0-9]*(?:m|h|d)$/u.test(activeSince)) throw new Error("--active-since must use a duration such as 24h");
if (confirm && activeSince !== null) throw new Error("confirmed credits require the exact --users selectors returned by an active-user dry-run");
if (amountUsd === null) throw new Error("credits grant requires --amount-usd");
if (reason === null || reason.length === 0 || reason.length > 500 || /[\r\n\0]/u.test(reason)) throw new Error("credits grant requires a single-line --reason up to 500 characters");
return { activeSince, users, amountUsd, reason, confirm, targetId, json };
}
function parseSelectors(value: string): string[] {
const selectors = value.split(",").map((item) => item.trim()).filter(Boolean);
if (selectors.length === 0 || selectors.length > 200) throw new Error("--users requires 1 to 200 comma-separated exact IDs or emails");
if (selectors.some((item) => item.length > 256 || /[\r\n\0]/u.test(item))) throw new Error("--users contains an unsupported selector");
if (new Set(selectors).size !== selectors.length) throw new Error("--users contains duplicate selectors");
return selectors;
}
function parseAmount(value: string): number {
const amount = Number(value);
if (!Number.isFinite(amount) || amount <= 0) throw new Error("--amount-usd must be greater than zero");
return amount;
}
function renderCreditsHelp(): RenderedCliResult {
return {
ok: true,
command: "platform-infra sub2api codex-pool credits --help",
renderedText: [
"SUB2API USER CREDITS",
"Usage:",
" bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-since 24h --amount-usd 20 --reason <text> [--confirm]",
" bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --users <id-or-email,...> --amount-usd 20 --reason <text> [--confirm]",
"Notes:",
" Default mode is dry-run. --active-since selects distinct active non-admin users from native usage.",
" --confirm performs additive balance writes and then reads every selected user back.",
].join("\n"),
contentType: "text/plain",
};
}
function renderCredits(response: Record<string, unknown>): RenderedCliResult {
const report = record(response.report);
const summary = record(report.summary);
const rows = arrayOfRecords(report.items);
const lines = [
`SUB2API USER CREDITS ok=${response.ok === true} mode=${text(report.mode)} selection=${text(record(report.selection).kind)} selected=${text(summary.selected)} amount-usd=${text(summary.amountUsd)} total-usd=${text(summary.totalAmountUsd)}`,
`WRITE attempted=${text(summary.writeAttempted)} succeeded=${text(summary.writeSucceeded)} failed=${text(summary.writeFailed)} reconciled=${text(summary.reconciled)} mutation=${text(report.mutation)}`,
];
if (typeof report.error === "string") lines.push(`ERROR ${text(report.error)}`);
if (rows.length > 0) {
lines.push("", renderTable([
["EMAIL", "USER_ID", "ROLE", "STATUS", "REQUESTS", "BALANCE_BEFORE", "ADD_USD", "BALANCE_AFTER", "WRITE", "RECONCILED"],
...rows.map((row) => [
text(row.email), text(row.userId), text(row.role), text(row.status), text(row.requestCount), usd(row.balanceBefore), usd(row.amountUsd),
usd(row.balanceAfter), text(record(row.write).status), text(row.reconciled),
]),
]));
}
if (report.mode === "dry-run") {
const selectors = Array.isArray(report.confirmationUserIds) ? report.confirmationUserIds.join(",") : "<exact-user-ids>";
lines.push("", `CONFIRM SELECTORS --users ${selectors}`);
lines.push("NEXT: use these exact selectors with --confirm only after explicit user authorization.");
}
return { ok: response.ok === true, command: "platform-infra sub2api codex-pool credits", renderedText: lines.join("\n"), contentType: "text/plain", projection: response };
}
function renderCreditsJson(response: Record<string, unknown>): RenderedCliResult {
return { ok: response.ok === true, command: "platform-infra sub2api codex-pool credits --json", renderedText: JSON.stringify(response), contentType: "application/json", projection: response };
}
function record(value: unknown): Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {}; }
function arrayOfRecords(value: unknown): Record<string, unknown>[] { return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : []; }
function text(value: unknown): string { return value === null || value === undefined || value === "" ? "-" : String(value).replace(/[\r\n\t]+/gu, " "); }
function usd(value: unknown): string { const numeric = Number(value); return Number.isFinite(numeric) ? `$${numeric.toFixed(8)}` : text(value); }
@@ -15,4 +15,5 @@ export * from "./remote-python-sync";
export * from "./remote";
export * from "./feedback";
export * from "./profit";
export * from "./credits";
export * from "./error-passthrough";
@@ -28,6 +28,7 @@ import { renderCodexPoolPlan } from "./render";
import { codexPoolRuntime } from "./runtime";
import { codexPoolFaults } from "./faults";
import { codexPoolProfit } from "./profit";
import { codexPoolCredits } from "./credits";
import { codexPoolErrorPassthrough } from "./error-passthrough";
import { defaultCodexPoolRuntimeTargetId } from "./runtime-target";
import { codexPoolHelp } from "./types";
@@ -44,6 +45,7 @@ export async function runCodexPoolCommand(config: UniDeskConfig, args: string[])
if (action === "runtime") return await codexPoolRuntime(config, args.slice(1));
if (action === "faults") return await codexPoolFaults(config, args.slice(1));
if (action === "profit") return await codexPoolProfit(config, args.slice(1));
if (action === "credits") return await codexPoolCredits(config, args.slice(1));
if (action === "error-passthrough") return await codexPoolErrorPassthrough(config, args.slice(1));
if (action === "trace") return await codexPoolTrace(config, parseTraceOptions(args.slice(1)));
if (action === "feedback") return await codexPoolFeedback(config, parseFeedbackOptions(args.slice(1)));
@@ -8,6 +8,8 @@ import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runti
interface ProfitOptions {
since: string;
month: string | null;
forecast: boolean;
targetId: string;
json: boolean;
accounts: boolean;
@@ -22,12 +24,13 @@ export async function codexPoolProfit(config: UniDeskConfig, args: string[]): Pr
if (args.includes("--help")) return renderProfitHelp(pool.profit.defaultWindow);
const options = parseProfitOptions(args, pool.profit.defaultWindow);
const target = codexPoolRuntimeTarget(options.targetId);
const payload = { since: options.since, config: pool.profit };
const payload = { since: options.since, month: options.month, forecast: options.forecast, config: pool.profit };
const remote = await runRemoteCodexPoolScript(config, "profit", profitScript(payload, pool, target), target);
const report = parseJsonOutput(remote.stdout);
const ok = remote.exitCode === 0 && boolField(report, "ok", false);
const projectedReport = projectProfitReport(report, options);
const baseCommand = `bun scripts/cli.ts platform-infra sub2api codex-pool profit --target ${target.id} --since ${options.since}`;
const windowOption = options.month === null ? `--since ${options.since}` : `--month ${options.month}${options.forecast ? " --forecast" : ""}`;
const baseCommand = `bun scripts/cli.ts platform-infra sub2api codex-pool profit --target ${target.id} ${windowOption}`;
const accountPage = record(record(projectedReport).accountPage);
const response = {
ok,
@@ -56,6 +59,8 @@ export async function codexPoolProfit(config: UniDeskConfig, args: string[]): Pr
function parseProfitOptions(args: string[], defaultWindow: string): ProfitOptions {
let since = defaultWindow;
let month: string | null = null;
let forecast = false;
let targetId = defaultCodexPoolRuntimeTargetId();
let json = false;
let accounts = false;
@@ -69,8 +74,12 @@ function parseProfitOptions(args: string[], defaultWindow: string): ProfitOption
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]!;
if (arg === "--json") json = true;
else if (arg === "--forecast") forecast = true;
else if (arg === "--accounts") accounts = true;
else if (arg === "--missing-costs") missingCosts = true;
else if (arg === "--month") {
[month, index] = readValue(index, "--month");
} else if (arg.startsWith("--month=")) month = arg.slice("--month=".length).trim();
else if (arg === "--page-token") {
[pageToken, index] = readValue(index, "--page-token");
} else if (arg.startsWith("--page-token=")) pageToken = arg.slice("--page-token=".length).trim();
@@ -83,11 +92,14 @@ function parseProfitOptions(args: string[], defaultWindow: string): ProfitOption
else throw new Error(`unsupported profit option: ${arg}`);
}
if (!/^[1-9][0-9]*(?:s|m|h|d)$/u.test(since)) throw new Error("--since must be a duration such as 24h, 90m, or 7d");
if (month !== null && !/^\d{4}-(?:0[1-9]|1[0-2])$/u.test(month)) throw new Error("--month must use YYYY-MM");
if (month !== null && args.some((arg) => arg === "--since" || arg.startsWith("--since="))) throw new Error("use only one of --since or --month");
if (forecast && month === null) throw new Error("--forecast requires --month YYYY-MM");
if (!targetId || /[\r\n\0]/u.test(targetId)) throw new Error("--target has an unsupported format");
if (accounts && missingCosts) throw new Error("--accounts and --missing-costs are separate semantic disclosures");
if (pageToken !== null && !accounts) throw new Error("--page-token requires --accounts");
if (pageToken !== null && (!pageToken || /[\r\n\0\s]/u.test(pageToken))) throw new Error("--page-token has an unsupported format");
return { since, targetId, json, accounts, missingCosts, pageToken };
return { since, month, forecast, targetId, json, accounts, missingCosts, pageToken };
}
function renderProfitHelp(defaultWindow: string): RenderedCliResult {
@@ -97,9 +109,11 @@ function renderProfitHelp(defaultWindow: string): RenderedCliResult {
renderedText: [
"SUB2API CODEX-POOL PROFIT",
"Usage:",
" bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h] [--target PK01] [--accounts [--page-token <token>]|--missing-costs] [--json]",
" bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--accounts [--page-token <token>]|--missing-costs] [--json]",
"Options:",
` --since <duration> Native usage window; YAML default is ${defaultWindow}.`,
" --month <YYYY-MM> Calendar-month usage plus YAML-declared one-time period costs.",
" --forecast Extrapolate month-to-date variable revenue and costs to month end; period costs remain one-time.",
" --target <id>",
" --accounts Include one fixed-size account page with complete names and IDs.",
" --page-token <token> Continue from the opaque token returned by the previous account page.",
@@ -125,19 +139,40 @@ function renderProfit(response: Record<string, unknown>): RenderedCliResult {
const formulas = record(summary.profitFormula);
const disclosure = record(response.disclosure);
const lines = [
`SUB2API PROFIT status=${text(report.status)} target=${text(record(response.target).id)} window=${text(window.since)}`,
`SUB2API PROFIT status=${text(report.status)} target=${text(record(response.target).id)} window=${text(window.month ?? window.since)}`,
`WINDOW ${text(window.startAt)} -> ${text(window.endAt)} SOURCE native-admin-usage/${text(record(report.source).usageCostField)}`,
`POLICY sale-rate=${text(policy.saleRateCnyPerApiUsd)}CNY/API_USD account-cost=${text(policy.accountCostSource)}`,
"",
"SUMMARY",
];
lines.push(table(
["SCOPE", "STATUS", "REVENUE_CNY", "PROFIT_POOL_COST_CNY", "SELF_USE_COST_CNY", "PROFIT_BEFORE_UNKNOWN_COSTS_CNY", "UNKNOWN_API_USD"],
[
["不扣自用", text(record(summary.withoutSelfUse).status), cny(summary.revenueCny), cny(confirmedCosts.profitPool), "-", cny(profitBeforeUnknownCosts.withoutSelfUse), apiUsd(unknownBasis.profitPool)],
["扣除自用", text(record(summary.withSelfUse).status), cny(summary.revenueCny), cny(confirmedCosts.profitPool), cny(confirmedCosts.selfUse), cny(profitBeforeUnknownCosts.withSelfUse), apiUsd(unknownBasis.total)],
["SCOPE", "STATUS", "REVENUE_CNY", "TRAFFIC_COST_CNY", "SELF_USE_COST_CNY", "PERIOD_COST_CNY", "PROFIT_BEFORE_UNKNOWN_COSTS_CNY", "UNKNOWN_API_USD"],
[
["不扣自用", text(record(summary.withoutSelfUse).status), cny(summary.revenueCny), cny(confirmedCosts.profitPool), "-", cny(confirmedCosts.period), cny(profitBeforeUnknownCosts.withoutSelfUse), apiUsd(unknownBasis.profitPool)],
["扣除自用", text(record(summary.withSelfUse).status), cny(summary.revenueCny), cny(confirmedCosts.profitPool), cny(confirmedCosts.selfUse), cny(confirmedCosts.period), cny(profitBeforeUnknownCosts.withSelfUse), apiUsd(unknownBasis.total)],
],
));
const forecast = record(summary.forecast);
if (Object.keys(forecast).length > 0) {
const forecastCosts = record(forecast.costsCny);
const forecastProfit = record(forecast.profitCny);
lines.push("", `FULL-MONTH FORECAST method=${text(forecast.method)} scale=${text(forecast.scale)} observed=${text(forecast.observedSeconds)}s full-period=${text(forecast.fullPeriodSeconds)}s`);
lines.push(table(
["SCOPE", "STATUS", "REVENUE_CNY", "TRAFFIC_COST_CNY", "SELF_USE_COST_CNY", "PERIOD_COST_CNY", "FORECAST_PROFIT_CNY"],
[
["不扣自用", text(record(forecast.withoutSelfUse).status), cny(forecast.revenueCny), cny(forecastCosts.profitPool), "-", cny(forecastCosts.period), cny(forecastProfit.withoutSelfUse)],
["扣除自用", text(record(forecast.withSelfUse).status), cny(forecast.revenueCny), cny(forecastCosts.profitPool), cny(forecastCosts.selfUse), cny(forecastCosts.period), cny(forecastProfit.withSelfUse)],
],
));
}
const periodCosts = arrayOfRecords(report.periodCosts);
if (periodCosts.length > 0) {
lines.push("", "PERIOD COSTS");
lines.push(table(
["ID", "PERIOD", "ACCOUNT", "AMOUNT_CNY", "DESCRIPTION"],
periodCosts.map((row) => [text(row.id), text(row.period), text(row.accountName), cny(row.amountCny), text(row.description)]),
));
}
lines.push("", "FORMULA");
lines.push(`- 不扣自用: ${text(formulas.withoutSelfUse)}`);
lines.push(`- 扣除自用: ${text(formulas.withSelfUse)}`);
@@ -30,7 +30,7 @@ function pyJson(value: unknown): string {
return `json.loads(${JSON.stringify(JSON.stringify(value))})`;
}
export function remotePythonCoreScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe" | "profit", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
export function remotePythonCoreScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe" | "profit" | "credits", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
const hostDockerEnvPath = target.runtimeMode === "host-docker" ? target.hostDockerEnvPath : null;
return `
set -u
@@ -335,13 +335,14 @@ def sentinel_probe_change_reasons(current, profile):
reasons.append("sentinel-protect")
return reasons
def curl_api(method, path, bearer=None, payload=None):
def curl_api(method, path, bearer=None, payload=None, idempotency_key=None):
body = b"" if payload is None else json.dumps(payload, separators=(",", ":")).encode("utf-8")
script = r'''
set -eu
method="$1"
url="$2"
token="\${3:-}"
idempotency_key="\${4:-}"
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
cat > "$tmp"
@@ -349,7 +350,11 @@ if [ -n "$token" ]; then
if [ "$method" = "GET" ] && [ ! -s "$tmp" ]; then
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H "Authorization: Bearer $token" "$url"
else
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' -H "Authorization: Bearer $token" --data-binary @"$tmp" "$url"
if [ -n "$idempotency_key" ]; then
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' -H "Authorization: Bearer $token" -H "Idempotency-Key: $idempotency_key" --data-binary @"$tmp" "$url"
else
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' -H "Authorization: Bearer $token" --data-binary @"$tmp" "$url"
fi
fi
else
if [ "$method" = "GET" ] && [ ! -s "$tmp" ]; then
@@ -362,11 +367,11 @@ fi
if RUNTIME_MODE == "host-docker":
if not isinstance(HOST_DOCKER_APP_PORT, int):
raise RuntimeError("host-docker app port missing")
proc = run(["sh", "-c", script, "sh", method, f"http://127.0.0.1:{HOST_DOCKER_APP_PORT}{path}", bearer or ""], body)
proc = run(["sh", "-c", script, "sh", method, f"http://127.0.0.1:{HOST_DOCKER_APP_PORT}{path}", bearer or "", idempotency_key or ""], body)
else:
proc = run([
"kubectl", "-n", NAMESPACE, "exec", "-i", APP_POD,
"--", "sh", "-c", script, "sh", method, f"http://127.0.0.1:8080{path}", bearer or "",
"--", "sh", "-c", script, "sh", method, f"http://127.0.0.1:8080{path}", bearer or "", idempotency_key or "",
], body)
return parse_curl_output(proc)
@@ -0,0 +1,143 @@
export const remotePythonCreditsScript = String.raw`
def credits_parse_time(value):
if not isinstance(value, str) or not value:
return None
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc)
except ValueError:
return None
def credits_window_start(value, end_at):
matched = re.fullmatch(r"([1-9][0-9]*)([mhd])", str(value or ""))
if matched is None:
raise RuntimeError("credits active window must use a duration such as 24h")
seconds = int(matched.group(1)) * {"m": 60, "h": 3600, "d": 86400}[matched.group(2)]
return end_at - timedelta(seconds=seconds)
def credits_all_users(token):
users = []
page = 1
while True:
data = ensure_success(curl_api("GET", f"/api/v1/admin/users?page={page}&page_size=100", bearer=token), "list users")
batch = extract_items(data)
users.extend(item for item in batch if isinstance(item, dict))
total = int(data.get("total", len(users))) if isinstance(data, dict) else len(users)
if not batch or len(batch) < 100 or page * 100 >= total:
return users
page += 1
def credits_active_usage(token, since):
end_at = datetime.now(timezone.utc)
start_at = credits_window_start(since, end_at)
user_requests = {}
page = 1
while True:
path = f"/api/v1/admin/usage?page={page}&page_size=100&start_date={start_at.date().isoformat()}&end_date={end_at.date().isoformat()}&timezone=UTC&sort_by=created_at&sort_order=desc"
data = ensure_success(curl_api("GET", path, bearer=token), "list active user usage")
batch = extract_items(data)
reached_start = False
for row in batch:
if not isinstance(row, dict):
continue
created_at = credits_parse_time(row.get("created_at"))
if created_at is not None and created_at < start_at:
reached_start = True
continue
user_id = row.get("user_id") if row.get("user_id") is not None else row.get("userId")
if user_id is not None and (created_at is None or created_at <= end_at):
user_requests[str(user_id)] = user_requests.get(str(user_id), 0) + 1
total = int(data.get("total", 0)) if isinstance(data, dict) else 0
if not batch or len(batch) < 100 or reached_start or page * 100 >= total:
return user_requests, start_at, end_at
page += 1
def credits_selected_users(token, payload):
users = credits_all_users(token)
users_by_id = {str(item.get("id")): item for item in users if item.get("id") is not None}
users_by_email = {str(item.get("email") or "").strip().lower(): item for item in users if item.get("email")}
active_since = payload.get("activeSince")
if active_since:
requests, start_at, end_at = credits_active_usage(token, active_since)
selected = []
excluded = []
for user_id, request_count in requests.items():
user = users_by_id.get(user_id)
if not isinstance(user, dict):
excluded.append({"userId": user_id, "reason": "user-not-found"})
elif user.get("role") == "admin":
excluded.append({"userId": user.get("id"), "email": user.get("email"), "reason": "admin-excluded"})
elif user.get("status") != "active" or user.get("deleted_at") is not None:
excluded.append({"userId": user.get("id"), "email": user.get("email"), "reason": "inactive-or-deleted"})
else:
selected.append((user, request_count))
return selected, excluded, {"kind": "active-users", "since": active_since, "startAt": start_at.isoformat(), "endAt": end_at.isoformat()}
selected = []
seen = set()
for selector in payload.get("users") or []:
user = users_by_id.get(str(selector)) or users_by_email.get(str(selector).strip().lower())
if not isinstance(user, dict):
raise RuntimeError("exact user selector not found: " + str(selector))
user_id = str(user.get("id"))
if user_id in seen:
raise RuntimeError("duplicate user selector: " + str(selector))
seen.add(user_id)
selected.append((user, None))
return selected, [], {"kind": "exact-users", "selectors": payload.get("users") or []}
def run_credits_grant():
payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8")) if PAYLOAD_B64 else {}
_, token, _ = login()
selected, excluded, selection = credits_selected_users(token, payload)
amount = profit_decimal(payload.get("amountUsd"))
confirm = payload.get("confirm") is True
batch_identity = json.dumps({
"users": sorted(str(value) for value in (payload.get("users") or [])),
"amountUsd": profit_decimal_text(amount),
"reason": payload.get("reason"),
}, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
batch_key = hashlib.sha256(batch_identity.encode("utf-8")).hexdigest()[:24]
items = []
write_succeeded = 0
write_failed = 0
reconciled = 0
for user, request_count in sorted(selected, key=lambda item: (str(item[0].get("email") or ""), int(item[0].get("id") or 0))):
before = profit_decimal(user.get("balance"))
after = before + amount
item = {
"userId": user.get("id"), "email": user.get("email"), "role": user.get("role"), "status": user.get("status"),
"requestCount": request_count, "balanceBefore": profit_decimal_text(before), "amountUsd": profit_decimal_text(amount),
"balanceAfter": profit_decimal_text(after), "write": {"attempted": False, "status": "dry-run", "error": None}, "reconciled": None,
}
if confirm:
try:
ensure_success(curl_api("POST", f"/api/v1/admin/users/{user.get('id')}/balance", bearer=token, payload={
"balance": float(amount), "operation": "add", "notes": payload.get("reason"),
}, idempotency_key=f"unidesk-credit-{batch_key}-{user.get('id')}"), "add user compensation balance")
item["write"] = {"attempted": True, "status": "succeeded", "error": None}
write_succeeded += 1
actual = ensure_success(curl_api("GET", f"/api/v1/admin/users/{user.get('id')}", bearer=token), "read user balance after grant")
actual_balance = profit_decimal(actual.get("balance") if isinstance(actual, dict) else None)
item["actualBalance"] = profit_decimal_text(actual_balance)
item["reconciled"] = actual_balance == after
reconciled += 1 if item["reconciled"] else 0
except Exception as exc:
item["write"] = {"attempted": True, "status": "failed", "error": str(exc)[:500]}
item["reconciled"] = False
write_failed += 1
items.append(item)
selected_count = len(items)
return {
"ok": not confirm or (write_failed == 0 and reconciled == selected_count),
"mode": "confirmed" if confirm else "dry-run", "mutation": confirm and write_succeeded > 0,
"selection": selection, "reason": payload.get("reason"), "items": items, "excluded": excluded,
"confirmationUserIds": [item.get("userId") for item in items],
"summary": {
"selected": selected_count, "excluded": len(excluded), "amountUsd": profit_decimal_text(amount),
"totalAmountUsd": profit_decimal_text(amount * selected_count), "writeAttempted": selected_count if confirm else 0,
"writeSucceeded": write_succeeded, "writeFailed": write_failed, "reconciled": reconciled,
},
"valuesPrinted": False,
}
`;
@@ -33,6 +33,20 @@ def profit_window_start(window, end_at):
seconds = amount * {"s": 1, "m": 60, "h": 3600, "d": 86400}[unit]
return end_at - timedelta(seconds=seconds)
def profit_period_bounds(window, month, end_at):
if month is None:
return profit_window_start(window, end_at), end_at, None, end_at
matched = re.fullmatch(r"(\d{4})-(0[1-9]|1[0-2])", str(month))
if matched is None:
raise RuntimeError("profit month must use YYYY-MM")
year = int(matched.group(1))
month_number = int(matched.group(2))
start_at = datetime(year, month_number, 1, tzinfo=timezone.utc)
next_at = datetime(year + 1, 1, 1, tzinfo=timezone.utc) if month_number == 12 else datetime(year, month_number + 1, 1, tzinfo=timezone.utc)
if start_at > end_at:
raise RuntimeError("profit month must not be in the future")
return start_at, min(next_at, end_at), str(month), next_at
def profit_group_class(name, config):
groups = config.get("groups") if isinstance(config.get("groups"), dict) else {}
if name in (groups.get("profitable") or []):
@@ -74,6 +88,19 @@ def profit_usage_rows(token, group_id, start_at, end_at):
return rows, page
page += 1
def profit_usage_stats(token, group_id, account_id, start_at, end_at):
query = (
f"/api/v1/admin/usage/stats?group_id={group_id}&account_id={account_id}"
f"&start_date={start_at.date().isoformat()}&end_date={end_at.date().isoformat()}&timezone=UTC&nocache=1"
)
data = ensure_success(curl_api("GET", query, bearer=token), f"get usage stats for group {group_id} account {account_id}")
return {
"request_count": int(data.get("total_requests") or 0) if isinstance(data, dict) else 0,
"input_tokens": int(data.get("total_input_tokens") or 0) if isinstance(data, dict) else 0,
"output_tokens": int(data.get("total_output_tokens") or 0) if isinstance(data, dict) else 0,
"actual_cost": profit_decimal(data.get("total_actual_cost") if isinstance(data, dict) else None),
}
def profit_row_account_id(row):
for key in ("account_id", "accountId"):
if row.get(key) is not None:
@@ -94,6 +121,12 @@ def profit_add_usage(target, row, cost_field):
target["outputTokens"] += int(row.get("output_tokens") or 0)
target["apiAmount"] += profit_decimal(row.get(cost_field))
def profit_add_stats(target, stats, cost_field):
target["requestCount"] += int(stats.get("request_count") or 0)
target["inputTokens"] += int(stats.get("input_tokens") or 0)
target["outputTokens"] += int(stats.get("output_tokens") or 0)
target["apiAmount"] += profit_decimal(stats.get(cost_field))
def profit_empty_account(group, group_class, account_id, account_name):
return {
"groupId": group.get("id"),
@@ -111,10 +144,13 @@ def run_profit_report():
payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8")) if PAYLOAD_B64 else {}
config = payload.get("config") if isinstance(payload.get("config"), dict) else {}
window = payload.get("since")
requested_month = payload.get("month")
cost_field = config.get("usageCostField")
sale_rate = profit_decimal(config.get("saleRateCnyPerApiUsd"))
end_at = datetime.now(timezone.utc)
start_at = profit_window_start(window, end_at)
start_at, end_at, month, period_end_at = profit_period_bounds(window, requested_month, end_at)
period_costs = [item for item in (config.get("periodCosts") or []) if isinstance(item, dict) and item.get("period") == month]
period_cost = sum((profit_decimal(item.get("amountCny")) for item in period_costs), profit_decimal(0))
_, token, _ = login()
groups = [item for item in list_groups(token) if isinstance(item, dict)]
account_rows = []
@@ -132,17 +168,30 @@ def run_profit_report():
group_class = profit_group_class(group_name, config)
members = list_accounts_for_group(token, group_id)
accounts_by_id = {str(item.get("id")): item for item in members if isinstance(item, dict) and item.get("id") is not None}
usage, pages = profit_usage_rows(token, group_id, start_at, end_at)
total_pages += pages
aggregates = {}
for row in usage:
account_id = profit_row_account_id(row)
account = accounts_by_id.get(str(account_id))
account_name = profit_row_account_name(row) or (account.get("name") if isinstance(account, dict) else None) or f"account-{account_id}"
key = str(account_id) if account_id is not None else "name:" + str(account_name)
if key not in aggregates:
aggregates[key] = profit_empty_account(group, group_class, account_id, account_name)
profit_add_usage(aggregates[key], row, cost_field)
if month is not None:
for account in members:
if not isinstance(account, dict) or account.get("id") is None:
continue
account_id = account.get("id")
account_name = account.get("name") or f"account-{account_id}"
stats = profit_usage_stats(token, group_id, account_id, start_at, end_at)
total_pages += 1
if int(stats.get("request_count") or 0) <= 0:
continue
aggregates[str(account_id)] = profit_empty_account(group, group_class, account_id, account_name)
profit_add_stats(aggregates[str(account_id)], stats, cost_field)
else:
usage, pages = profit_usage_rows(token, group_id, start_at, end_at)
total_pages += pages
for row in usage:
account_id = profit_row_account_id(row)
account = accounts_by_id.get(str(account_id))
account_name = profit_row_account_name(row) or (account.get("name") if isinstance(account, dict) else None) or f"account-{account_id}"
key = str(account_id) if account_id is not None else "name:" + str(account_name)
if key not in aggregates:
aggregates[key] = profit_empty_account(group, group_class, account_id, account_name)
profit_add_usage(aggregates[key], row, cost_field)
group_amount = profit_decimal(0)
group_tokens = 0
group_requests = 0
@@ -180,26 +229,66 @@ def run_profit_report():
self_cost = sum((profit_decimal(row["upstreamCostCny"]) for row in self_accounts if row["upstreamCostCny"] is not None), profit_decimal(0))
unknown_profit_api_usd = sum((profit_decimal(row["apiAmountUsd"]) for row in missing_profit), profit_decimal(0))
unknown_self_api_usd = sum((profit_decimal(row["apiAmountUsd"]) for row in missing_self), profit_decimal(0))
profit_before_unknown_without_self = revenue - profit_cost
profit_before_unknown_with_self = profit_before_unknown_without_self - self_cost
operating_profit_without_self = revenue - profit_cost
operating_profit_with_self = operating_profit_without_self - self_cost
profit_before_unknown_without_self = operating_profit_without_self - period_cost
profit_before_unknown_with_self = operating_profit_with_self - period_cost
without_complete = not missing_profit and not unclassified
with_complete = without_complete and not missing_self
forecast = None
if payload.get("forecast") is True:
if month is None:
raise RuntimeError("profit forecast requires a calendar month")
observed_seconds = profit_decimal((end_at - start_at).total_seconds())
full_period_seconds = profit_decimal((period_end_at - start_at).total_seconds())
if observed_seconds <= 0:
raise RuntimeError("profit forecast has no observed period")
scale = full_period_seconds / observed_seconds
forecast_revenue = revenue * scale
forecast_profit_cost = profit_cost * scale
forecast_self_cost = self_cost * scale
forecast_without_self = forecast_revenue - forecast_profit_cost - period_cost
forecast_with_self = forecast_without_self - forecast_self_cost
forecast = {
"method": "month-to-date-linear-run-rate",
"scale": profit_decimal_text(scale),
"observedSeconds": profit_decimal_text(observed_seconds),
"fullPeriodSeconds": profit_decimal_text(full_period_seconds),
"revenueCny": profit_decimal_text(forecast_revenue),
"costsCny": {
"profitPool": profit_decimal_text(forecast_profit_cost),
"selfUse": profit_decimal_text(forecast_self_cost),
"period": profit_decimal_text(period_cost),
},
"profitCny": {
"withoutSelfUse": profit_decimal_text(forecast_without_self) if without_complete else None,
"withSelfUse": profit_decimal_text(forecast_with_self) if with_complete else None,
},
"withoutSelfUse": {"status": "complete" if without_complete else "partial"},
"withSelfUse": {"status": "complete" if with_complete else "partial"},
}
account_rows.sort(key=lambda row: (row["class"], row["groupName"], str(row["accountId"]), str(row["accountName"])))
return {
"ok": True,
"mode": "profit",
"status": "complete" if with_complete else "partial",
"window": {"since": window, "startAt": start_at.isoformat(), "endAt": end_at.isoformat(), "timezone": "UTC"},
"source": {"kind": "sub2api-native-admin-usage", "endpoint": "/api/v1/admin/usage", "usageCostField": cost_field, "pagesRead": total_pages},
"window": {"since": window if month is None else None, "month": month, "startAt": start_at.isoformat(), "endAt": end_at.isoformat(), "timezone": "UTC"},
"source": {"kind": "sub2api-native-admin-usage", "endpoint": "/api/v1/admin/usage/stats" if month is not None else "/api/v1/admin/usage", "usageCostField": cost_field, "queriesRead": total_pages},
"policy": config,
"groups": group_rows,
"accounts": account_rows,
"periodCosts": [{"id": item.get("id"), "period": item.get("period"), "accountName": item.get("accountName"), "amountCny": profit_decimal_text(profit_decimal(item.get("amountCny"))), "description": item.get("description")} for item in period_costs],
"summary": {
"revenueCny": profit_decimal_text(revenue),
"confirmedCostsCny": {
"profitPool": profit_decimal_text(profit_cost),
"selfUse": profit_decimal_text(self_cost),
"total": profit_decimal_text(profit_cost + self_cost),
"period": profit_decimal_text(period_cost),
"total": profit_decimal_text(profit_cost + self_cost + period_cost),
},
"operatingProfitBeforeUnknownCostsCny": {
"withoutSelfUse": profit_decimal_text(operating_profit_without_self),
"withSelfUse": profit_decimal_text(operating_profit_with_self),
},
"profitBeforeUnknownCostsCny": {
"withoutSelfUse": profit_decimal_text(profit_before_unknown_without_self),
@@ -211,11 +300,12 @@ def run_profit_report():
"total": profit_decimal_text(unknown_profit_api_usd + unknown_self_api_usd),
},
"profitFormula": {
"withoutSelfUse": "profitBeforeUnknownCostsCny.withoutSelfUse - sum(missing profit account apiAmountUsd * unknown costRateCnyPerApiUsd)",
"withSelfUse": "profitBeforeUnknownCostsCny.withSelfUse - sum(all missing account apiAmountUsd * unknown costRateCnyPerApiUsd)",
"withoutSelfUse": "revenueCny - confirmedCostsCny.profitPool - confirmedCostsCny.period - sum(missing profit account apiAmountUsd * unknown costRateCnyPerApiUsd)",
"withSelfUse": "revenueCny - confirmedCostsCny.profitPool - confirmedCostsCny.selfUse - confirmedCostsCny.period - sum(all missing account apiAmountUsd * unknown costRateCnyPerApiUsd)",
},
"withoutSelfUse": {"status": "complete" if without_complete else "partial", "profitCny": profit_decimal_text(profit_before_unknown_without_self) if without_complete else None},
"withSelfUse": {"status": "complete" if with_complete else "partial", "profitCny": profit_decimal_text(profit_before_unknown_with_self) if with_complete else None},
"forecast": forecast,
},
"completeness": {
"missingCostAccounts": [{"class": row["class"], "groupName": row["groupName"], "accountId": row["accountId"], "accountName": row["accountName"], "apiAmountUsd": row["apiAmountUsd"]} for row in missing_profit + missing_self],
@@ -172,6 +172,8 @@ try:
result = run_sentinel_probe()
elif MODE == "profit":
result = run_profit_report()
elif MODE == "credits":
result = run_credits_grant()
else:
result = run_validate()
except Exception as exc:
@@ -8,8 +8,9 @@ import { remotePythonSyncValidateScript } from "./remote-python-sync-validate";
import { remotePythonTraceScript } from "./remote-python-trace";
import { remotePythonSentinelProbeScript } from "./remote-python-sentinel-probe";
import { remotePythonProfitScript } from "./remote-python-profit";
import { remotePythonCreditsScript } from "./remote-python-credits";
export function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe" | "profit", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
export function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe" | "profit" | "credits", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
return [
remotePythonCoreScript(mode, encodedPayload, pool, target),
remotePythonSentinelScript,
@@ -17,6 +18,7 @@ export function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanu
remotePythonSyncValidateScript,
remotePythonTraceScript,
remotePythonProfitScript,
remotePythonCreditsScript,
remotePythonSentinelProbeScript,
].join("");
}
@@ -22,6 +22,7 @@ import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "..
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import type { CodexPoolConfig, CodexPoolRuntimeTarget, TraceOptions } from "./types";
import { readCodexPoolConfig } from "./config";
import { shQuote } from "./remote";
import { remotePythonScript } from "./remote-python-sync";
import { sentinelImageDockerfilePath } from "./types";
@@ -56,6 +57,12 @@ export function profitScript(payload: unknown, pool: CodexPoolConfig, target: Co
return remotePythonScript("profit", encoded, pool, target);
}
export function creditsScript(payload: unknown, target: CodexPoolRuntimeTarget): string {
const pool = readCodexPoolConfig();
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
return remotePythonScript("credits", encoded, pool, target);
}
export function sentinelReportScript(pool: CodexPoolConfig, events: number, target: CodexPoolRuntimeTarget): string {
const stateName = pool.sentinel.stateConfigMapName;
const cronJobName = pool.sentinel.cronJobName;
@@ -28,7 +28,7 @@ export async function capture(config: UniDeskConfig, target: string, args: strin
return await runSshCommandCapture(config, target, args, input);
}
export type RemoteCodexPoolMode = "sync" | "validate" | "runtime" | "faults" | "profit" | "error-passthrough" | "sentinel-probe" | "sentinel-image-status" | "sentinel-image-build";
export type RemoteCodexPoolMode = "sync" | "validate" | "runtime" | "faults" | "profit" | "credits" | "error-passthrough" | "sentinel-probe" | "sentinel-image-status" | "sentinel-image-build";
export async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string, target = codexPoolRuntimeTarget()): Promise<SshCaptureResult> {
const jobName = `codex-pool-${mode}-${Date.now().toString(36)}`.slice(0, 63);
@@ -85,6 +85,7 @@ export function codexPoolModeCommand(mode: RemoteCodexPoolMode): string {
if (mode === "sync") return "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm";
if (mode === "runtime") return "bun scripts/cli.ts platform-infra sub2api codex-pool runtime list";
if (mode === "profit") return "bun scripts/cli.ts platform-infra sub2api codex-pool profit --since 24h";
if (mode === "credits") return "bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-since 24h --amount-usd 20 --reason <text>";
if (mode === "error-passthrough") return "bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough list";
if (mode === "sentinel-probe") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account <accountName> --confirm";
if (mode === "sentinel-image-status") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status";
@@ -29,6 +29,15 @@ export function renderRuntimeMutationResult(ok: boolean, options: RuntimeOptions
stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-",
]),
]));
else if (options.kind === "account-name") lines.push(renderTable([
["ACCOUNT", "ID", "CHANGE", "WRITE", "RECONCILED", "BEFORE_NAME", "DESIRED_NAME", "ACTUAL_NAME", "FAILURE"],
...fields.map(({ item, write, before, desired, actual }) => [
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.change) ?? "-",
stringValue(write.status) ?? "-", item.reconciled === true ? "true" : item.reconciled === false ? "false" : "-",
stringValue(before.name) ?? "-", stringValue(desired.name) ?? "-", stringValue(actual.name) ?? "-",
stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-",
]),
]));
else lines.push(renderTable([
["ACCOUNT", "ID", "CHANGE", "WRITE", "RECONCILED", "BEFORE", "DESIRED", "ACTUAL", "BEFORE_CODES", "DESIRED_CODES", "ACTUAL_CODES", "FAILURE"],
...fields.map(({ item, write, before, desired, actual }) => {
@@ -11,8 +11,9 @@ export interface RuntimeOptions {
platform: string | null;
pageToken: string | null;
template: string | null;
kind: "temp-unschedulable" | "priority";
kind: "temp-unschedulable" | "priority" | "account-name";
priority: number | null;
name: string | null;
confirm: boolean;
full: boolean;
raw: boolean;
@@ -25,7 +26,7 @@ export interface RuntimeOptions {
export function parseRuntimeOptions(args: string[]): RuntimeOptions {
const [actionRaw = "list", ...rest] = args;
if (!(["list", "get", "errors", "infrastructure", "apply", "delete"] as string[]).includes(actionRaw)) {
throw new Error("runtime usage: list|get|errors|infrastructure|apply|delete [--account <name-or-id>|--accounts <selectors>] [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <token>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable|priority] [--priority <0-1000>] [--confirm] [--target <id>] [--json|--full|--raw]");
throw new Error("runtime usage: list|get|errors|infrastructure|apply|delete [--account <name-or-id>|--accounts <selectors>] [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <token>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable|priority|account-name] [--priority <0-1000>|--name <account-name>] [--confirm] [--target <id>] [--json|--full|--raw]");
}
let account: string | null = null;
let accounts: string[] = [];
@@ -34,8 +35,9 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
let platform: string | null = null;
let pageToken: string | null = null;
let template: string | null = null;
let kind: "temp-unschedulable" | "priority" = "temp-unschedulable";
let kind: "temp-unschedulable" | "priority" | "account-name" = "temp-unschedulable";
let priority: number | null = null;
let name: string | null = null;
let confirm = false;
let full = false;
let raw = false;
@@ -73,6 +75,8 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
else if (arg.startsWith("--kind=")) kind = parseRuntimeKind(arg.slice("--kind=".length));
else if (arg === "--priority") priority = parsePriority(readValue("--priority"));
else if (arg.startsWith("--priority=")) priority = parsePriority(arg.slice("--priority=".length));
else if (arg === "--name") name = readValue("--name");
else if (arg.startsWith("--name=")) name = arg.slice("--name=".length).trim();
else if (arg === "--target") targetId = readValue("--target");
else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim();
else if (arg === "--since") since = readValue("--since");
@@ -100,12 +104,19 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
if (kind === "priority" && priority === null) throw new Error("runtime apply --kind priority requires --priority <0-1000>");
if (kind === "priority" && template !== null) throw new Error("runtime apply --kind priority does not accept --template");
if (kind !== "priority" && priority !== null) throw new Error("--priority requires --kind priority");
if (kind === "account-name" && action !== "apply") throw new Error("runtime --kind account-name only supports apply");
if (kind === "account-name" && (name === null || name.length === 0 || name.length > 256 || /[\r\n]/u.test(name))) {
throw new Error("runtime apply --kind account-name requires --name with a supported account name");
}
if (kind === "account-name" && accounts.length > 0) throw new Error("runtime account-name updates exactly one --account at a time");
if (kind === "account-name" && template !== null) throw new Error("runtime apply --kind account-name does not accept --template");
if (kind !== "account-name" && name !== null) throw new Error("--name requires --kind account-name");
if (action !== "apply" && template !== null) throw new Error(`runtime ${action} does not accept --template`);
if (action !== "errors" && action !== "infrastructure" && (since !== "24h" || tail !== 50_000)) throw new Error(`runtime ${action} does not accept --since or --tail`);
if (!/^\d+[mhd]$/u.test(since)) throw new Error("--since must use <number>m, <number>h, or <number>d");
if (["list", "get", "errors", "infrastructure"].includes(action) && confirm) throw new Error(`runtime ${action} does not accept --confirm`);
if ([full, raw, json].filter(Boolean).length > 1) throw new Error("use only one of --json, --full, or --raw");
return { action, account, accounts, group, allGroups, platform, pageToken, template, kind, priority, confirm, full, raw, json, since, tail, targetId };
return { action, account, accounts, group, allGroups, platform, pageToken, template, kind, priority, name, confirm, full, raw, json, since, tail, targetId };
}
function parseAccountSelectors(value: string): string[] {
@@ -116,8 +127,8 @@ function parseAccountSelectors(value: string): string[] {
return selectors;
}
function parseRuntimeKind(value: string): "temp-unschedulable" | "priority" {
if (value !== "temp-unschedulable" && value !== "priority") throw new Error("--kind must be temp-unschedulable or priority");
function parseRuntimeKind(value: string): "temp-unschedulable" | "priority" | "account-name" {
if (value !== "temp-unschedulable" && value !== "priority" && value !== "account-name") throw new Error("--kind must be temp-unschedulable, priority, or account-name");
return value;
}
@@ -1068,6 +1068,19 @@ def selected_account_details(token, accounts):
return details
def mutation_plan(detail, action, include_rules):
if PAYLOAD.get("kind") == "account-name":
before_name = str(detail.get("name") or "")
desired_name = str(PAYLOAD.get("name") or "")
return {
"accountName": before_name,
"accountId": detail.get("id"),
"change": "noop" if before_name == desired_name else "update",
"before": {"name": before_name},
"desired": {"name": desired_name},
"template": None,
"_kind": "account-name",
"_desiredName": desired_name,
}
if PAYLOAD.get("kind") == "priority":
before_priority = int(detail.get("priority") or 0)
desired_priority = int(PAYLOAD.get("priority"))
@@ -1114,7 +1127,12 @@ def write_mutation_plan(token, plan):
if plan["change"] == "noop":
return {"attempted": False, "status": "noop", "error": None}
try:
payload = {"priority": plan["_desiredPriority"]} if plan["_kind"] == "priority" else {"credentials": plan["_desiredCredentials"]}
if plan["_kind"] == "priority":
payload = {"priority": plan["_desiredPriority"]}
elif plan["_kind"] == "account-name":
payload = {"name": plan["_desiredName"]}
else:
payload = {"credentials": plan["_desiredCredentials"]}
data_of(curl_api("PUT", f"/api/v1/admin/accounts/{plan['accountId']}", bearer=token, payload=payload), "update account runtime config")
return {"attempted": True, "status": "succeeded", "error": None}
except Exception as exc:
@@ -1129,6 +1147,10 @@ def reconcile_mutation_plan(token, plan, write, include_rules):
actual_priority = int(actual_detail.get("priority") or 0)
item["actual"] = {"priority": actual_priority}
item["reconciled"] = actual_priority == plan["_desiredPriority"]
elif plan["_kind"] == "account-name":
actual_name = str(actual_detail.get("name") or "")
item["actual"] = {"name": actual_name}
item["reconciled"] = actual_name == plan["_desiredName"]
else:
actual_policy = normalize_policy(actual_detail.get("credentials"))
item["actual"] = policy_summary(actual_policy, include_rules)
@@ -14,7 +14,7 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
const options = parseRuntimeOptions(args);
const pool = readCodexPoolConfig();
const target = codexPoolRuntimeTarget(options.targetId);
const selectedTemplate = options.kind === "priority" || options.template === null ? null : pool.runtime.templatesById[options.template];
const selectedTemplate = options.kind !== "temp-unschedulable" || options.template === null ? null : pool.runtime.templatesById[options.template];
if (options.template !== null && selectedTemplate === undefined) {
throw new Error(`unknown runtime template ${options.template}; available: ${pool.runtime.templates.map((item) => item.id).join(", ")}`);
}
@@ -28,6 +28,7 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
pageToken: options.pageToken,
kind: options.kind,
priority: options.priority,
name: options.name,
confirm: options.confirm,
full: options.full,
since: options.since,
@@ -84,6 +85,7 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
template: options.template,
kind: options.kind,
priority: options.priority,
name: options.name,
confirm: options.confirm,
since: options.action === "errors" || options.action === "infrastructure" ? options.since : undefined,
tail: options.action === "errors" || options.action === "infrastructure" ? options.tail : undefined,
@@ -205,6 +205,13 @@ export interface CodexPoolProfitConfig {
usageCostField: "actual_cost";
accountCostSource: "account-name-trailing-cny-per-api-usd";
saleRateCnyPerApiUsd: number;
periodCosts: Array<{
id: string;
period: string;
accountName: string;
amountCny: number;
description: string;
}>;
groups: {
profitable: string[];
selfUse: string[];
@@ -382,7 +389,7 @@ export function codexPoolHelp(): unknown {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget();
return {
command: "platform-infra sub2api codex-pool plan|sync|validate|runtime|faults|profit|error-passthrough|trace|feedback|sentinel-image|sentinel-probe|sentinel-report|cleanup-probes|expose|configure-local",
command: "platform-infra sub2api codex-pool plan|sync|validate|runtime|faults|profit|credits|error-passthrough|trace|feedback|sentinel-image|sentinel-probe|sentinel-report|cleanup-probes|expose|configure-local",
output: "json, except trace, feedback, and sentinel-report default to low-noise text tables",
usage: [
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
@@ -393,11 +400,13 @@ export function codexPoolHelp(): unknown {
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --account <name-or-id> [--target PK01] [--json|--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <group-id>] [--account <name-or-id>] [--since 24h] [--tail 50000] [--target PK01] [--json|--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool faults [--level P0|P1|P2] [--group <name-or-id>] [--account <name-or-id>] [--model <model>] [--stream sync|stream] [--endpoint <path>] [--request-id <id>] [--page-token <token>] [--target PK01] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h] [--target PK01] [--accounts [--page-token <token>]|--missing-costs] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--accounts [--page-token <token>]|--missing-costs] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool credits grant (--active-since 24h|--users <id-or-email,...>) --amount-usd <amount> --reason <text> [--target PK01] [--confirm] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough list|apply|delete [--template <id>] [--target PK01] [--confirm] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --template <id> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --template <id> [--target PK01] [--confirm] [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --kind priority --priority <0-1000> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --kind account-name --name <new-name> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --account <name-or-id> --kind temp-unschedulable [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --accounts <name-or-id,...> --kind temp-unschedulable [--target PK01] [--confirm] [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool trace [--target D601] --request-id <requestId> [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",