Skip to content

Commit d1b3d0c

Browse files
Pigbibicodex
andcommitted
Add provider admin usage snapshots
Co-Authored-By: Codex <noreply@openai.com>
1 parent 7003a12 commit d1b3d0c

14 files changed

Lines changed: 742 additions & 6 deletions

.github/workflows/vps_codex_service_ops.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,9 @@ jobs:
4646
CODEX_AUDIT_SERVICE_MODEL: ${{ vars.CODEX_AUDIT_SERVICE_MODEL }}
4747
CODEX_AUDIT_SERVICE_REASONING_EFFORT: ${{ vars.CODEX_AUDIT_SERVICE_REASONING_EFFORT }}
4848
CODEX_AUDIT_SERVICE_CODEX_ACCOUNT_USAGE: "1"
49+
CODEX_AUDIT_SERVICE_OPENAI_USAGE_WINDOW_DAYS: "7"
50+
CODEX_AUDIT_SERVICE_ANTHROPIC_USAGE_WINDOW_DAYS: "7"
51+
OPENAI_ADMIN_KEY: ${{ secrets.OPENAI_ADMIN_KEY }}
52+
ANTHROPIC_ADMIN_KEY: ${{ secrets.ANTHROPIC_ADMIN_KEY }}
4953
CODEX_AUDIT_SERVICE_TOKEN: ${{ secrets.CODEX_AUDIT_SERVICE_TOKEN }}
5054
run: bash scripts/deploy_codex_audit_service.sh "${{ inputs.mode }}"

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ CODEX_AUDIT_SERVICE_AUDIENCE=quant-codex-audit \
105105
CODEX_AUDIT_SERVICE_MODEL=gpt-5.4 \
106106
CODEX_AUDIT_SERVICE_REASONING_EFFORT=auto \
107107
CODEX_AUDIT_SERVICE_CODEX_ACCOUNT_USAGE=1 \
108+
CODEX_AUDIT_SERVICE_OPENAI_USAGE_WINDOW_DAYS=7 \
109+
CODEX_AUDIT_SERVICE_ANTHROPIC_USAGE_WINDOW_DAYS=7 \
108110
python3 -m service.ai_gateway_service
109111
```
110112

@@ -117,6 +119,18 @@ When `CODEX_AUDIT_SERVICE_CODEX_ACCOUNT_USAGE=1`, the dashboard reads a
117119
sanitized Codex rate-limit snapshot through the local authenticated Codex
118120
app-server; API-key rows remain internal estimates unless a platform usage
119121
provider is configured separately.
122+
Set `OPENAI_ADMIN_KEY` to an OpenAI Admin API key to add a sanitized
123+
GPT/OpenAI organization Usage/Costs snapshot to `/v1/ai/quota`. Optional
124+
`CODEX_AUDIT_SERVICE_OPENAI_ADMIN_API_KEY_IDS` can limit that snapshot to
125+
specific API key IDs; never store raw API keys in that filter. When API key
126+
IDs are configured, both usage and costs are requested with that filter.
127+
Set `ANTHROPIC_ADMIN_KEY` to an Anthropic Admin API key to add a sanitized
128+
Claude organization Usage/Cost snapshot. Do not reuse the normal
129+
`ANTHROPIC_API_KEY`; Anthropic usage/cost reporting requires an Admin API key.
130+
Optional `CODEX_AUDIT_SERVICE_ANTHROPIC_ADMIN_API_KEY_IDS` and
131+
`CODEX_AUDIT_SERVICE_ANTHROPIC_ADMIN_WORKSPACE_IDS` filter usage by IDs; costs
132+
are omitted when those filters are set to avoid mixing filtered usage with an
133+
unfiltered cost total.
120134

121135
If no custom domain is available, `cloudflare/codex-audit-proxy/` contains a minimal Cloudflare Worker that can publish a free `workers.dev` HTTPS entry point while keeping the VPS origin URL in a Cloudflare secret. The production service path is async: submit `POST /v1/codex-audit/jobs`, then poll `GET /v1/codex-audit/jobs/{job_id}`. See `docs/async_service_deployment.md` for the deployment and open-source repository checklist.
122136

cloudflare/ai-gateway-dash/src/index.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,11 @@ function healthReasonText(r){const path=r.path||"endpoint";if(r.reason==="error_
172172
function renderHealth(h){setStatus(h.status||"unknown");setUpdated("health-updated");const target=document.getElementById("health"),endpoints=h.endpoints||[],reasons=h.degradation_reasons||[],total=endpoints.reduce((s,i)=>s+(Number(i.total)||0),0),errors=endpoints.reduce((s,i)=>s+(Number(i.errors)||0),0),p95=Math.max(0,...endpoints.map(i=>Number(i.p95_ms)||0)),details=el("div","");append(details,statRow("接口数量",endpoints.length,"info"),statRow("总请求",fmtNum(total),""),statRow("错误",fmtNum(errors),errors?"err":"ok"),statRow("最高 P95",fmtNum(p95)+" ms",p95>30000?"err":p95>10000?"warn":"ok"));if(reasons.length){reasons.slice(0,3).forEach((reason,i)=>append(details,statRow(i?"降级原因 "+(i+1):"降级原因",healthReasonText(reason),clsStatus(reason.severity||h.status))));if(reasons.length>3)append(details,statRow("更多原因","+"+fmtNum(reasons.length-3),"warn"))}else if(h.last_error&&h.last_error.message){append(details,statRow("最近错误",String(h.last_error.message).slice(0,80),"warn"))}clear(target);append(target,append(el("div","metric-line"),el("div","big ",statusText(h.status||"unknown")),el("span","delta "+clsStatus(h.status||"unknown"),fmtNum(Math.round((h.uptime_seconds||0)/3600))+" h")),details)}
173173
function quotaTotalRow(label,d){const row=el("div","stat-row"),used=Number(d&&d.total_cost_usd)||0;append(row,el("span","stat-label",label),el("span","stat-value info",fmtUSD(used)));return row}
174174
function codexAccountRow(d){const row=el("div","stat-row"),r=d&&d.rate_limits||{},p=r.primary||{},s=r.secondary||{},credits=r.credits||{},parts=[];if(p.used_percent!==null&&p.used_percent!==undefined)parts.push((p.window_duration_mins?Math.round(p.window_duration_mins/60)+"h":"主窗口")+" "+p.used_percent+"%");if(s.used_percent!==null&&s.used_percent!==undefined)parts.push((s.window_duration_mins?Math.round(s.window_duration_mins/1440)+"d":"次窗口")+" "+s.used_percent+"%");if(r.plan_type)parts.push(String(r.plan_type));if(credits.balance!==null&&credits.balance!==undefined)parts.push("credits "+credits.balance);append(row,el("span","stat-label","Codex 账户"),el("span","stat-value "+(Number(p.used_percent)>80?"err":Number(p.used_percent)>50?"warn":"ok"),parts.join(" · ")||"实时可用"));return row}
175+
function providerAccountRow(label,d,usageKey){const row=el("div","stat-row"),c=d&&d.costs||{},u=d&&d[usageKey]||{},days=Number(d&&d.window_days)||7,currency=String(c.currency||"usd").toUpperCase(),cost=Number(c.total_cost)||0,tokens=Number(u.total_tokens)||((Number(u.input_tokens)||0)+(Number(u.output_tokens)||0)),requests=Number(u.num_model_requests)||0,parts=[];if(c.total_cost!==null&&c.total_cost!==undefined)parts.push(days+"d "+(currency==="USD"?fmtUSD(cost):cost.toFixed(2)+" "+currency));if(requests)parts.push(fmtNum(requests)+" req");if(tokens)parts.push(fmtNum(tokens)+" tokens");if(d&&d.filtered_api_key_count)parts.push(fmtNum(d.filtered_api_key_count)+" key");if(d&&d.filtered_workspace_count)parts.push(fmtNum(d.filtered_workspace_count)+" workspace");append(row,el("span","stat-label",label),el("span","stat-value info",parts.join(" · ")||"实时可用"));return row}
175176
function quotaHasUsage(d){return Boolean(Number(d&&d.total_cost_usd)||Number(d&&d.calls)||Number(d&&d.tokens_input)||Number(d&&d.tokens_output)||(d&&d.calls_incomplete))}
176177
function quotaUsageRow(label,d,extra){const row=el("div","stat-row"),calls=Number(d&&d.calls)||0,used=Number(d&&d.total_cost_usd)||0,tokens=Number(d&&d.tokens_input)||0,callText=d&&d.calls_incomplete?"≥ "+fmtNum(calls):fmtNum(calls),detail=fmtUSD(used)+" · "+callText+" 次"+(tokens?" · "+fmtNum(tokens)+" tokens":"")+(extra?" · "+extra:"");append(row,el("span","stat-label",label),el("span","stat-value info",detail));return row}
177178
function appendRepoQuotaRows(list,repos){for(const name of Object.keys(repos)){const d=repos[name]||{},used=Number(d.total_cost_usd)||0,budget=Number(d.daily_budget)||0,p=budget?used/budget*100:0,c=progressClass(p),item=el("div","quota-item"),meta=el("div",""),bar=el("div","bar"),fill=el("div","bar-fill "+c);fill.style.width=Math.max(0,Math.min(p,100))+"%";append(meta,el("div","quota-name",String(name).split("/").pop()||name),el("div","quota-meta",fmtUSD(used)+" / "+fmtUSD(budget)+" repo 日预算"));append(bar,fill);append(item,meta,bar,el("div","bar-label",Math.round(p)+"%"));list.appendChild(item)}}
178-
function renderQuota(q){setUpdated("quota-updated");const target=document.getElementById("quota"),repos=q.repos||{},repoCount=Object.keys(repos).length,summary=q.summary||null,account=summary&&summary.codex_account&&summary.codex_account.status==="available"?summary.codex_account:null,hasSummaryUsage=summary&&(account||quotaHasUsage(summary.combined)||quotaHasUsage(summary.api_key)||quotaHasUsage(summary.codex));clear(target);if(summary&&(repoCount||hasSummaryUsage)){const list=el("div","quota-list"),details=el("div","");appendRepoQuotaRows(list,repos);if(account)append(details,codexAccountRow(account));append(details,quotaTotalRow("合计估算",summary.combined||{}),quotaUsageRow("API Key 估算",summary.api_key||{}),quotaUsageRow("Codex 估算",summary.codex||{}));if(repoCount)target.appendChild(list);target.appendChild(details);target.appendChild(el("div","subtle","Codex 账户行来自本机 Codex rate-limit 快照;API Key、合计和 repo 行为内部用量估算/限流,不等同账户后台实时余额。"));return}if(!repoCount){empty(target,"暂无用量","当前窗口内没有记录到配额消耗。API Key 与 Codex 均无用量记录。");return}const list=el("div","quota-list");appendRepoQuotaRows(list,repos);target.appendChild(list)}
179+
function renderQuota(q){setUpdated("quota-updated");const target=document.getElementById("quota"),repos=q.repos||{},repoCount=Object.keys(repos).length,summary=q.summary||null,account=summary&&summary.codex_account&&summary.codex_account.status==="available"?summary.codex_account:null,openai=summary&&summary.openai_account&&summary.openai_account.status==="available"?summary.openai_account:null,anthropic=summary&&summary.anthropic_account&&summary.anthropic_account.status==="available"?summary.anthropic_account:null,hasSummaryUsage=summary&&(account||openai||anthropic||quotaHasUsage(summary.combined)||quotaHasUsage(summary.api_key)||quotaHasUsage(summary.codex));clear(target);if(summary&&(repoCount||hasSummaryUsage)){const list=el("div","quota-list"),details=el("div","");appendRepoQuotaRows(list,repos);if(openai)append(details,providerAccountRow("GPT API 账户",openai,"completions"));if(anthropic)append(details,providerAccountRow("Claude API 账户",anthropic,"messages"));if(account)append(details,codexAccountRow(account));append(details,quotaTotalRow("合计估算",summary.combined||{}),quotaUsageRow("API Key 估算",summary.api_key||{}),quotaUsageRow("Codex 估算",summary.codex||{}));if(repoCount)target.appendChild(list);target.appendChild(details);target.appendChild(el("div","subtle","GPT/Claude API 账户行来自各自 Admin Usage/Cost API;Codex 账户行来自本机 Codex rate-limit 快照;合计和 repo 行为内部估算/限流。"));return}if(!repoCount){empty(target,"暂无用量","当前窗口内没有记录到配额消耗。API Key 与 Codex 均无用量记录。");return}const list=el("div","quota-list");appendRepoQuotaRows(list,repos);target.appendChild(list)}
179180
function renderEffectiveness(e){setUpdated("effectiveness-updated");const target=document.getElementById("effectiveness"),rate=Number(e.improvement_rate)||0;clear(target);append(target,append(el("div","metric-line"),el("div","big blue",fmtPct(rate)),el("span","delta "+(rate>0.7?"ok":rate>0.4?"warn":"info"),"改善率")),append(el("div",""),statRow("已评估",e.evaluated||0,"info"),statRow("改善",e.improved||0,"ok"),statRow("退化",e.degraded||0,"err"),statRow("持平",e.neutral||0,""),statRow("待评估",e.pending||0,"info")))}
180181
function renderShadow(items){setUpdated("shadow-updated");const target=document.getElementById("shadow"),total=items.reduce((sum,item)=>sum+(Number(item.disagreement_count)||0),0);clear(target);if(!items.length){empty(target,"无活跃分歧","影子审计当前没有待复核分歧。");return}append(target,append(el("div","metric-line"),el("div","big violet",fmtNum(total)),el("span","delta warn","待复核")));for(const d of items.slice(0,5)){const row=el("div","stat-row"),value=el("span","stat-value warn");append(value,badge("warn",(d.disagreement_count||0)+"x"),document.createTextNode(" "+(d.ai_verdict||"")));append(row,el("span","stat-label",d.plugin||d.repo||"shadow"),value);target.appendChild(row)}}
181182
function renderChanges(items){setUpdated("changes-updated");const target=document.getElementById("changes");clear(target);if(!items.length){empty(target,"窗口内无变更","最近 7 天没有登记的变更记录。 ");return}const table=el("table",""),thead=el("thead",""),tr=el("tr","");for(const h of ["仓库","操作","效果","置信度","时间"])tr.appendChild(el("th","",h));thead.appendChild(tr);const tbody=el("tbody","");for(const c of items){const effect=c.effect||"pending",effectClass=effect==="improved"?"ok":effect==="degraded"?"err":"info",actionClass=c.action==="auto_merge"?"ok":c.action==="auto_pr"?"info":c.action?"violet":"warn",dt=c.created_at?new Date(c.created_at*1000).toLocaleDateString():"—",row=el("tr",""),repoName=String(c.repo||"").split("/").pop()||"—",repoCell=el("td","mono");if(c.external_url){const a=el("a","",repoName);a.href=c.external_url;a.target="_blank";a.rel="noreferrer";a.style.color="#93c5fd";a.style.textDecoration="none";repoCell.appendChild(a)}else repoCell.textContent=repoName;append(row,repoCell,append(el("td",""),badge(actionClass,c.action||"change")),el("td","stat-value "+effectClass,effect),el("td","mono",fmtPct(c.confidence||0)),el("td","mono",dt));tbody.appendChild(row)}append(table,thead,tbody);target.appendChild(table)}

docs/async_service_deployment.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ CODEX_AUDIT_SERVICE_AUDIENCE=quant-codex-audit \
6464
CODEX_AUDIT_SERVICE_MODEL=gpt-5.4 \
6565
CODEX_AUDIT_SERVICE_REASONING_EFFORT=auto \
6666
CODEX_AUDIT_SERVICE_CODEX_ACCOUNT_USAGE=1 \
67+
CODEX_AUDIT_SERVICE_OPENAI_USAGE_WINDOW_DAYS=7 \
68+
CODEX_AUDIT_SERVICE_ANTHROPIC_USAGE_WINDOW_DAYS=7 \
6769
CODEX_AUDIT_SERVICE_JOB_DIR=/var/lib/codex-audit-bridge/jobs \
6870
bash scripts/deploy_codex_audit_service.sh deploy
6971
```
@@ -75,6 +77,18 @@ With `CODEX_AUDIT_SERVICE_CODEX_ACCOUNT_USAGE=1`, `/v1/ai/quota` includes a
7577
sanitized Codex account rate-limit snapshot read from the local Codex
7678
app-server. This does not expose Codex auth tokens and should stay behind the
7779
existing dashboard authentication.
80+
With `OPENAI_ADMIN_KEY` set, `/v1/ai/quota` also includes a sanitized OpenAI
81+
organization Usage/Costs snapshot. Store the Admin API key only as a secret or
82+
systemd environment value, and use `CODEX_AUDIT_SERVICE_OPENAI_ADMIN_API_KEY_IDS`
83+
only for API key IDs, not raw API keys. When API key IDs are configured, both
84+
usage and costs are requested with that filter.
85+
With `ANTHROPIC_ADMIN_KEY` set, `/v1/ai/quota` also includes a sanitized Claude
86+
organization Usage/Cost snapshot. Store it separately from the normal
87+
`ANTHROPIC_API_KEY`; the usage/cost endpoints require an Admin API key. Use
88+
`CODEX_AUDIT_SERVICE_ANTHROPIC_ADMIN_API_KEY_IDS` and
89+
`CODEX_AUDIT_SERVICE_ANTHROPIC_ADMIN_WORKSPACE_IDS` only for IDs, never raw API
90+
keys. When those filters are set, costs are omitted to avoid mixing filtered
91+
usage with an unfiltered cost total.
7892
Set `CODEX_AUDIT_SERVICE_REASONING_EFFORT` only when a hard override is needed;
7993
unset or `auto` keeps task-complexity routing.
8094

scripts/deploy_codex_audit_service.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ JOB_DIR="${CODEX_AUDIT_SERVICE_JOB_DIR:-/var/lib/codex-audit-bridge/jobs}"
1616
AUDIT_MODEL="${CODEX_AUDIT_SERVICE_MODEL:-}"
1717
AUDIT_REASONING_EFFORT="${CODEX_AUDIT_SERVICE_REASONING_EFFORT:-}"
1818
CODEX_ACCOUNT_USAGE="${CODEX_AUDIT_SERVICE_CODEX_ACCOUNT_USAGE:-1}"
19+
OPENAI_USAGE_WINDOW_DAYS="${CODEX_AUDIT_SERVICE_OPENAI_USAGE_WINDOW_DAYS:-7}"
20+
ANTHROPIC_USAGE_WINDOW_DAYS="${CODEX_AUDIT_SERVICE_ANTHROPIC_USAGE_WINDOW_DAYS:-7}"
1921
NGINX_CONFIG="${CODEX_AUDIT_SERVICE_NGINX_CONFIG:-}"
2022

2123
require_sudo() {
@@ -57,7 +59,7 @@ systemctl_environment_brief() {
5759
| sed 's/^Environment=//' \
5860
| tr ' ' '\n' \
5961
| sed -E "s/^[\"']//; s/[\"']$//" \
60-
| grep -E '^CODEX_AUDIT_SERVICE_(ALLOWED_|AUDIENCE=|HOST=|PORT=|JOB_DIR=|QUOTA_STORE=|CODEX_ACCOUNT_USAGE=|SANDBOX=|MODEL=|REASONING_EFFORT=)' \
62+
| grep -E '^CODEX_AUDIT_SERVICE_(ALLOWED_|AUDIENCE=|HOST=|PORT=|JOB_DIR=|QUOTA_STORE=|CODEX_ACCOUNT_USAGE=|OPENAI_USAGE_WINDOW_DAYS=|ANTHROPIC_USAGE_WINDOW_DAYS=|SANDBOX=|MODEL=|REASONING_EFFORT=)' \
6163
| mask_infra || true
6264
fi
6365
}
@@ -204,6 +206,14 @@ write_audit_service_unit() {
204206
if [ -n "${CODEX_AUDIT_SERVICE_TOKEN:-}" ]; then
205207
audit_token_line="Environment=CODEX_AUDIT_SERVICE_TOKEN=${CODEX_AUDIT_SERVICE_TOKEN}"
206208
fi
209+
openai_admin_key_line=""
210+
if [ -n "${OPENAI_ADMIN_KEY:-}" ]; then
211+
openai_admin_key_line="Environment=OPENAI_ADMIN_KEY=${OPENAI_ADMIN_KEY}"
212+
fi
213+
anthropic_admin_key_line=""
214+
if [ -n "${ANTHROPIC_ADMIN_KEY:-}" ]; then
215+
anthropic_admin_key_line="Environment=ANTHROPIC_ADMIN_KEY=${ANTHROPIC_ADMIN_KEY}"
216+
fi
207217
sudo tee "/etc/systemd/system/${AUDIT_SERVICE_NAME}.service" >/dev/null <<EOF_UNIT
208218
[Unit]
209219
Description=QuantStrategyLab Codex audit service
@@ -227,10 +237,14 @@ Environment=CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES=${ALLOWED_SOURCE_REP
227237
Environment=CODEX_AUDIT_SERVICE_JOB_DIR=${JOB_DIR}
228238
Environment=CODEX_AUDIT_SERVICE_QUOTA_STORE=${JOB_DIR}/quota.json
229239
Environment=CODEX_AUDIT_SERVICE_CODEX_ACCOUNT_USAGE=${CODEX_ACCOUNT_USAGE}
240+
Environment=CODEX_AUDIT_SERVICE_OPENAI_USAGE_WINDOW_DAYS=${OPENAI_USAGE_WINDOW_DAYS}
241+
Environment=CODEX_AUDIT_SERVICE_ANTHROPIC_USAGE_WINDOW_DAYS=${ANTHROPIC_USAGE_WINDOW_DAYS}
230242
Environment=CODEX_AUDIT_SERVICE_SANDBOX=read-only
231243
${audit_model_line}
232244
${audit_reasoning_effort_line}
233245
${audit_token_line}
246+
${openai_admin_key_line}
247+
${anthropic_admin_key_line}
234248
ExecStart=/usr/bin/env python3 -m service.ai_gateway_service
235249
Restart=on-failure
236250
RestartSec=5

service/adapters/codex_adapter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from dataclasses import dataclass
1717
from pathlib import Path
1818

19-
SECRET_ENV_MARKERS = ("TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY", "CREDENTIAL", "API_KEY")
19+
SECRET_ENV_MARKERS = ("TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY", "CREDENTIAL", "API_KEY", "ADMIN_KEY")
2020
CODEX_REASONING_EFFORTS = frozenset({"minimal", "low", "medium", "high", "xhigh"})
2121

2222

0 commit comments

Comments
 (0)