最近蹬 Codex 干活,没蹬几下就跑掉了 5% 的周限额


我严重怀疑自己被官方限额了。


可打开用量页面,它只肯告诉我一件事,还剩百分之多少。


至于 100% 到底是多少,从头到尾没写过。


分母不给,我连自己有没有被砍都验证不了。


昨天我把它算出来了。


我这个号一周的额度上限是 41666 Credits,折合 1666 美元。


听着不少,但大家之前普遍在 2200 到 2500 这个区间。


我被砍掉了三分之一,页面上一个字都没提。


下面这段代码你可以直接拿走。


粘进浏览器控制台,十几秒就能算出自己号的天花板,顺便看看有没有被动过手脚。


先说为什么官方页面看不出来。


用量页面在,chatgptcom/codex/cloud/settings/analytics


我的 Codex 额度被偷偷砍了三分之一(附自查脚本)


Codex 用量页面显示每周限额还剩 92%


我这里显示每周使用限额还剩 92%,重置时间 8 月 31 日 9 点。


绿条拉得满满的,看着很健康。


问题出在这个百分比是相对值


上限从 2500 掉到 1666,页面还是只给你一个百分比。


分母缩了,绿条照样从满格开始走,你看不出来。


这就是它偷偷的地方,它给了你一个看不出问题的数


那就把分母算出来。


我写了个脚本干这件事,直接读你自己账号的数据,跑完出一张账本。


下面是操作,四步,跟着点就行


第一步,打开上面那个用量页面。


第二步,在页面空白处右键,点菜单最下面的检查


我的 Codex 额度被偷偷砍了三分之一(附自查脚本)


右键菜单最下面那行检查


红框那行就是,在整个菜单的最下面。


第三步,在弹出来的面板里切到控制台


我的 Codex 额度被偷偷砍了三分之一(附自查脚本)


DevTools 顶部的控制台标签


我的 Codex 额度被偷偷砍了三分之一(附自查脚本)


控制台最下面那行输入区


左边这张是控制台标签的位置,右边这张是最下面那行输入区,光标点进去。


第四步,把下面整段代码复制进去,回车。


如果这时候弹出提醒不让粘贴,就先手动敲一句 allow pasting 回车,再粘一次。


Chrome 怕你被人骗着往控制台粘恶意代码,第一次会拦一下,敲完这句就放行了。


代码在这,全选复制。


(() => {
const ID = "puer-ledger", RATE = 40 / 1000, PAGE = "/codex/cloud/settings/analytics";
  ["-panel", "-btn", "-css"].forEach((s) => document.getElementById(ID + s)?.remove());
if (!location.pathname.startsWith(PAGE))
returnalert("请先打开这个页面再运行:\nhttps://chatgpt.com" + PAGE);

const n = (v) => (Number.isFinite(+v) ? +v : 0);
const tk_ = (t = {}) => n(t.text_total_tokens) ||
n(t.cached_text_input_tokens) + n(t.uncached_text_input_tokens) + n(t.text_output_tokens);
const fmtN = (v) => v >= 1e6 ? (v / 1e6).toFixed(2) + "M" : v >= 1e3 ? (v / 1e3).toFixed(2) + "K" : v;
const dur = (s) => s >= 86400 ? (s / 86400).toFixed(1) + " 天" : s >= 3600 ? (s / 3600).toFixed(1) + " 小时" : Math.round(s / 60) + " 分钟";
const sum = (l) => ({
    c: l.reduce((s, d) => s + (d.totals?.credits || 0), 0),
    t: l.reduce((s, d) => s + tk_(d.totals), 0),
    r: l.reduce((s, d) => s + (d.totals?.turns || 0), 0),
  });

const css = document.createElement("style");
  css.id = ID + "-css";
  css.textContent = `#${ID}-panel{position:fixed;top:5%;left:50%;transform:translateX(-50%);width:min(760px,92vw);max-height:88vh;overflow:auto;background:#fff;color:#333;border:1px solid #e5e5e5;border-radius:14px;box-shadow:0 12px 60px rgba(0,0,0,.35);z-index:2147483646;padding:24px;font:14px/1.7 -apple-system,system-ui,sans-serif}
#${ID}-panel h3{margin:0 0 15px;font-size:18px}
#${ID}-panel .g{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}
#${ID}-panel .g>div{background:#f9f9f9;border:1px solid #eee;border-radius:8px;padding:12px;font-size:12px;color:#666}
#${ID}-panel .g>div:nth-child(3){background:#eefaf5;border-color:#d1f2e1}
#${ID}-panel .g b{display:block;margin-top:4px;font-size:15px;color:#10a37f}
#${ID}-panel .w{background:#fff8e6;border:1px solid #ffe1a3;color:#8a5a00;border-radius:8px;padding:10px 12px;font-size:12px;margin:12px 0}
#${ID}-panel table{width:100%;border-collapse:collapse;font-size:12px;margin-bottom:15px}
#${ID}-panel th,#${ID}-panel td{padding:8px;border-bottom:1px solid #f0f0f0;text-align:left}
#${ID}-panel th{background:#f5f5f5;color:#666}
#${ID}-panel tfoot td{font-weight:700;background:#fafafa}
#${ID}-btn{position:fixed;bottom:28px;right:28px;z-index:2147483647;padding:16px 28px;border:0;border-radius:999px;background:#10a37f;color:#fff;font-size:17px;font-weight:700;cursor:pointer;box-shadow:0 8px 28px rgba(16,163,127,.45)}`;
document.head.appendChild(css);

const panel = document.createElement("div");
  panel.id = ID + "-panel";
  panel.style.display = "none";
document.body.appendChild(panel);

constget = async (url, token) => {
const r = awaitfetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (!r.ok) thrownewError(`接口 ${r.status}`);
return r.json();
  };

// 没有油猴环境,token 只能自己从页面里捞
const readToken = () => {
const jwt = /[\w-]{30,}\.[\w-]{30,}\.[\w-]{30,}/;
returndocument.getElementById("client-bootstrap")?.textContent?.match(jwt)?.[0] ||
      [...document.scripts].map((s) => s.textContent?.match(jwt)?.[0]).find(Boolean);
  };

const sheet = (list) => {
const s = sum(list);
const row = (d) => `<tr><td>${d.date}</td><td>${(d.totals?.credits || 0).toFixed(3)}</td><td>${fmtN(tk_(d.totals))}</td><td>$ ${((d.totals?.credits || 0) * RATE).toFixed(2)}</td><td>${d.totals?.turns || 0}</td></tr>`;
return`<table><thead><tr><th>日期</th><th>Credits</th><th>Tokens</th><th>折算金额</th><th>轮数</th></tr></thead>
<tbody>${[...list].reverse().map(row).join("")}</tbody>
<tfoot><tr><td>合计</td><td>${s.c.toFixed(3)}</td><td>${fmtN(s.t)}</td><td>$ ${(s.c * RATE).toFixed(2)}</td><td>${s.r}</td></tr></tfoot></table>`;
  };

const run = async () => {
    btn.disabled = true; btn.textContent = "⏳ 算账中…";
try {
const token = readToken();
if (!token) thrownewError("没拿到登录令牌,请刷新页面后重试");

const rl = (awaitget("/backend-api/wham/usage", token))?.rate_limit || {};
// 口径经常是坑:secondary 拿不到会退回 primary(短窗口),得让人看见用的是哪个
const key = rl.secondary_window ? "secondary_window" : "primary_window";
const w = rl[key] || {};
console.log("[额度账本] rate_limit 原始数据", rl);

const DAY = 86400000;
const qs = `start_date=${new Date(Date.now() - 30 * DAY).toISOString().slice(0, 10)}&end_date=${new Date(Date.now() + DAY).toISOString().slice(0, 10)}&group_by=day`;
const list = (awaitget(`/backend-api/wham/analytics/daily-workspace-usage-counts?${qs}`, token)).data || [];

// 优先用接口给的 reset_at 反推周期起点,拿不到就退回最后一个有数据的日子
const winSec = w.limit_window_seconds || 0;
const cs = w.reset_at ? newDate((w.reset_at - winSec) * 1000) : null;
const split = cs ? cs.toISOString().slice(0, 10)
        : list.filter((d) => tk_(d.totals) > 0).map((d) => d.date).sort().pop();
const cur = list.filter((d) => !split || d.date >= split);
const his = list.filter((d) => split && d.date < split);

const c = sum(cur), ratio = (w.used_percent || 0) / 100;
let cap = "—", money = "—", why = "";
if (ratio > 0 && c.c > 0) {
        cap = (c.c / ratio).toFixed(1) + " Credits";
        money = "$ " + (c.c / ratio * RATE).toFixed(2);
// 周期不是从 00:00 开始的,起始日那天混着上一周期的量,分子会偏大
if (cs && (cs.getHours() || cs.getMinutes()))
          why = `${cs.toLocaleDateString("zh-CN")} 的日用量里含有 ${cs.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })} 之前、属于上一周期的消耗,日粒度切不开,反推分子会略偏大。`;
      } elseif (ratio > 0) {
        cap = "待统计";
        why = `本周期刚重置不久,已用 ${w.used_percent || 0}% 是实时数据,但每日用量接口还没聚合出这段时间的量,分子为 0 所以反推不出来。等几小时后再点一次按钮即可。`;
      } else {
        why = "拿不到已用比例,反推不出上限。";
      }
if (winSec && winSec <= 86400)
        why = `注意:拿到的是 ${dur(winSec)} 短窗口(${key}),这个比例说的不是周额度,反推出来的也不是周上限。` + why;

      panel.innerHTML = `<h3>Codex 额度账本 · v3.2.0<span id="${ID}-x" style="float:right;cursor:pointer;color:#999">×</span></h3>
<div class="g"><div>周期进度<b>${w.used_percent || 0}%</b></div>
<div>本周期已用<b>${c.c.toFixed(1)} Credits</b></div>
<div>额度上限(反推)<b>${cap}</b></div>
<div>周期折算金额<b>${money}</b></div></div>
<div style="font-size:12px;color:#666">口径 <b>${key}</b> · 窗口 <b>${winSec ? dur(winSec) : "未知"}</b><br>
本周期 <b>${cs ? cs.toLocaleString("zh-CN") : "未知"}</b> ~ <b>${w.reset_at ? new Date(w.reset_at * 1000).toLocaleString("zh-CN") : "未知"}</b> · 剩余 <b>${w.reset_after_seconds ? dur(w.reset_after_seconds) : "未知"}</b></div>
${why ? `<div class="w">${why}</div>` : ""}
<p><b>当前周期明细</b>(自 ${cur[0]?.date || split || "未知"} 起)</p>${sheet(cur)}
${his.length ? `<p><b>已结算区间</b>(${his[0].date} ~ ${his[his.length - 1].date})</p>${sheet(his)}` : ""}`;
      panel.style.display = "block";
document.getEle