@sreetej510/pi-usage 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +14 -0
- package/package.json +5 -3
- package/src/anthropic-auth.ts +0 -33
- package/src/anthropic-query.ts +0 -72
- package/src/args.ts +0 -110
- package/src/codex-app-server.ts +0 -165
- package/src/codex-auth.ts +0 -66
- package/src/codex-query.ts +0 -52
- package/src/codex-reset-credits.ts +0 -131
- package/src/command.ts +0 -184
- package/src/constants.ts +0 -56
- package/src/errors.ts +0 -49
- package/src/footer.ts +0 -188
- package/src/format.ts +0 -208
- package/src/http.ts +0 -21
- package/src/index.ts +0 -100
- package/src/models.ts +0 -73
- package/src/normalize-anthropic.ts +0 -160
- package/src/normalize-codex.ts +0 -187
- package/src/query.ts +0 -121
- package/src/shared-cache.ts +0 -70
- package/src/statusline.ts +0 -244
- package/src/types.ts +0 -228
- package/src/utils.ts +0 -144
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import{homedir as gr}from"node:os";import{join as xr}from"node:path";var yt="usage",Oe="openai-codex",Ie="anthropic",Ae="https://chatgpt.com/backend-api/wham/usage",At="https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",Rt="https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",wt="codex-1",De="https://api.anthropic.com/api/oauth/usage",Re=15e3,E=180*1e3,Fe=3,St=1e3,bt=900*1e3,Et=30*1e3,ie=3600*1e3,W=xr(gr(),".pi","agent","usage-cache.json"),Be=1,se="provider-usage",ze=20,Ut=29,ae=600,vt="\x1B[39m",Mt=new Set(["five_hour","seven_day","extra_usage"]),je=[{value:"--refresh",label:"--refresh",description:"Refresh usage instead of cached data"},{value:"--no-statusline",label:"--no-statusline",description:"Do not update the statusline"},{value:"--clear-statusline",label:"--clear-statusline",description:"Clear the usage statusline"},{value:"--timeout ",label:"--timeout",description:"Set query timeout in seconds"},{value:"--raw",label:"--raw",description:"Show raw usage API responses (for debugging)"},{value:"--list-banked-resets",label:"--list-banked-resets",description:"List available Codex banked resets and expiry dates"},{value:"--consume-banked-reset ",label:"--consume-banked-reset",description:"Consume a Codex banked reset by id"}];function H(e){return e?.provider===Oe}function de(e){return e?.provider===Ie}function D(e){return H(e)||de(e)}function $(e,r){return r?e.provider==="codex"&&H(r)||e.provider==="anthropic"&&de(r):!1}function We(e){return de(e)?"anthropic":"codex"}function K(e){return _t(e,Oe)}function q(e){return _t(e,Ie)}function _t(e,r){let t=[],n=new Set,o=i=>{if(!i||i.provider!==r)return;let d=`${i.provider}/${i.id}`;n.has(d)||(n.add(d),t.push(i))};o(e.model);for(let i of e.modelRegistry.getAvailable())o(i);for(let i of e.modelRegistry.getAll())o(i);return t}function Pt(e){return new Promise(r=>{setTimeout(r,e).unref?.()})}function x(e){return e instanceof Error?e.message:String(e)}function V(e,r){let t;try{t=JSON.parse(e)}catch(n){throw new Error(`${r} was not valid JSON: ${x(n)}`)}return g(t,r)}function g(e,r){if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`${r} was not an object.`);return e}function f(e){return typeof e=="string"?e:void 0}function l(e){if(typeof e=="number"&&Number.isFinite(e))return e;if(typeof e=="string"&&e.trim()){let r=Number(e);return Number.isFinite(r)?r:void 0}}function F(e){return typeof e=="boolean"?e:void 0}function C(e,r){return Object.keys(e).some(t=>t.toLowerCase()===r.toLowerCase())}function Q(e){return He(e.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi,"Bearer <redacted>").replace(/"access_token"\s*:\s*"[^"]+"/gi,'"access_token":"<redacted>"').trim(),ae)}function He(e,r){return e.length<=r?e:`${e.slice(0,r-1)}\u2026`}function Ke(e){try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e.slice(0,ae*4)}}function we(e){let r=Math.round(e/6e4);if(r<60)return`${Math.max(1,r)}m`;let t=Math.floor(r/60);return t<24?`${t}h`:`${Math.floor(t/24)}d`}function v(e){return Number.isFinite(e)?Math.min(100,Math.max(0,e)):0}function kt(e,r){return Number.isFinite(e)?new Intl.NumberFormat(void 0,{maximumFractionDigits:2}).format(e):r}var Cr={USD:"$",CAD:"$",AUD:"$",EUR:"\u20AC",GBP:"\xA3",JPY:"\xA5"};function M(e,r,t=2){if(!Number.isFinite(e))return String(e);let n=new Intl.NumberFormat(void 0,{minimumFractionDigits:t,maximumFractionDigits:t}).format(e),o=Cr[r.toUpperCase()];return o?`${o}${n}`:`${r} ${n}`}function Se(e){let r=e.replace(/[_-]+/g," ").trim();return(r.match(/\bcodex\s+(.+)$/i)?.[1]?.trim()||r).toLowerCase().replace(/\s+/g," ")}function J(e){return e?.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||void 0}function qe(e,r){let t=J(r);t&&e.add(t)}function $t(e,r){return e===r||e.startsWith(`${r}-`)||e.endsWith(`-${r}`)||e.includes(`-${r}-`)}function Ve(e){return e.replace(/[\r\n\t]/g," ").replace(/ +/g," ").trim()}async function Qe(e){let r=q(e),t=[];for(let n of r){let o=await e.modelRegistry.getApiKeyAndHeaders(n);if(!o.ok){t.push(o.error);continue}let i={...o.headers??{}};if(!C(i,"Authorization")&&o.apiKey&&(i.Authorization=`Bearer ${o.apiKey}`),C(i,"Accept")||(i.Accept="application/json"),C(i,"User-Agent")||(i["User-Agent"]="pi-usage"),C(i,"Authorization"))return{headers:i}}if(t.length>0)throw new Error(t.join("; "))}import{readFileSync as yr}from"node:fs";import{homedir as Ar}from"node:os";import{join as Rr}from"node:path";async function ue(e){let r=K(e),t=[];for(let n of r){let o=await e.modelRegistry.getApiKeyAndHeaders(n);if(!o.ok){t.push(o.error);continue}let i={...o.headers??{}};if(!C(i,"Authorization")&&o.apiKey&&(i.Authorization=`Bearer ${o.apiKey}`),C(i,"User-Agent")||(i["User-Agent"]="pi-codex-usage"),C(i,"Authorization"))return{headers:i}}if(t.length>0)throw new Error(t.join("; "))}async function Je(e){let r=await ue(e);if(!r)return;let t={...r.headers};if(C(t,"Accept")||(t.Accept="application/json"),C(t,"Content-Type")||(t["Content-Type"]="application/json"),C(t,"OpenAI-Beta")||(t["OpenAI-Beta"]=wt),C(t,"originator")||(t.originator="Codex Desktop"),!C(t,"ChatGPT-Account-ID")){let n=wr();n&&(t["ChatGPT-Account-ID"]=n)}return{headers:t}}function wr(){try{let e=JSON.parse(yr(Rr(Ar(),".codex","auth.json"),"utf8"));return f(e.tokens?.account_id)??f(e.tokens?.accountId)}catch{return}}function y(e){return e instanceof Error&&e.message.includes("This extension ctx is stale after session replacement or reload")}var be=class extends Error{retryAfterMs;constructor(r,t){super(r),this.name="UsageEndpointError",this.retryAfterMs=t}};function G(e){return/\b429\b|rate_limit_error|rate.?limited/i.test(e)}function X(e,r,t){let n=`${e} usage endpoint returned ${r.status} ${r.statusText}: ${Q(t)}`;throw new be(n,r.status===429?Sr(r):void 0)}function Sr(e){let r=e.headers.get("retry-after");if(!r)return;let t=Number(r);if(Number.isFinite(t)&&t>=0)return t*1e3;let n=Date.parse(r);if(!Number.isNaN(n))return Math.max(0,n-Date.now())}function Ee(e){for(let r of e)if(r.cause instanceof be&&r.cause.retryAfterMs!==void 0)return Math.min(Math.max(r.cause.retryAfterMs,Et),ie);return bt}async function _(e,r,t,n="usage"){let o=new AbortController,i=setTimeout(()=>o.abort(),t);try{return await fetch(e,{...r,signal:o.signal})}catch(d){throw o.signal.aborted?new Error(`Timed out after ${Math.round(t/1e3)}s while fetching ${n}.`):d}finally{clearTimeout(i)}}function Lt(e,r){let t=[" >_ OpenAI Codex Usage",""];for(let n of e.snapshots){let o=n.limitName??n.limitId;Ue(n)||t.push(` ${o} limit:`),n.primary&&t.push(Nt("5h limit:",n.primary)),n.secondary&&t.push(Nt("Weekly limit:",n.secondary)),!n.primary&&!n.secondary&&t.push(" Limits unavailable for this account")}return e.bankedResetsAvailable!==void 0&&(t.push(""),t.push(` Banked resets available: ${e.bankedResetsAvailable}`)),t.join(`
|
|
2
|
+
`)}function Ot(e,r){let t=Er(e,r);if(!t)return"usage unavailable";let n=[Mr(t)];return t.primary&&n.push(`${v(t.primary.usedPercent).toFixed(0)}% 5h`),t.secondary&&n.push(`${v(t.secondary.usedPercent).toFixed(0)}% wk`),n.length===1&&t.credits&&n.push($r(t.credits)),n.join(" ")}function It(e,r){return e.provider==="anthropic"?e.statusline:Ot(e,r)}function br(e,r){return e.provider==="anthropic"?e.summaryLines.join(`
|
|
3
|
+
`):Lt(e,r)}function ve(e,r,t){let o=_r(r,e.model).map(i=>br(i,t?Date.now()-i.capturedAt:void 0)).join(`
|
|
4
|
+
|
|
5
|
+
`);e.ui.notify(e.hasUI?Pr(o):o,"info")}function Ge(e){let r=["Unable to read usage for the current provider."];for(let t of e){let n=t.source==="pi-auth"?"Pi Codex auth":t.source==="codex-app-server"?"Codex app-server fallback":"Anthropic OAuth usage";r.push(`- ${n}: ${t.message}`)}return r.push(""),r.push("Tip: use a Pi OpenAI Codex model or Pi Anthropic model. For Codex, /login with OpenAI ChatGPT Plus/Pro. For Anthropic, /login with Anthropic so the OAuth usage endpoint can be queried."),r.join(`
|
|
6
|
+
`)}function ce(e){let r=Math.round(v(e)/100*ze);return`[${"\u2588".repeat(r)}${"\u2591".repeat(ze-r)}]`}function Er(e,r){let t=e.snapshots.find(Ue);if(!r||!H(r))return t??e.snapshots[0];let n=Ur(r),o=e.snapshots.find(d=>Tt(d).some(s=>n.has(s)));if(o)return o;let i=vr(n);for(let d of i){let s=e.snapshots.filter(u=>!Ue(u)&&Tt(u).some(a=>$t(a,d)));if(s.length===1)return s[0]}return t??e.snapshots[0]}function Ur(e){let r=new Set;qe(r,e.id),qe(r,e.name);for(let t of[...r]){let n=t.indexOf("codex");n>=0&&r.add(t.slice(n))}return r}function Tt(e){return[J(e.limitId),J(e.limitName)].filter(r=>r!==void 0)}function vr(e){let r=new Set;for(let t of e){let n=t.match(/(?:^|-)codex-(.+)$/);n?.[1]&&r.add(n[1])}return[...r]}function Mr(e){if(Ue(e))return"codex";let r=e.limitName??e.limitId;return`codex ${Se(r)}`}function _r(e,r){return[...e].sort((t,n)=>{let o=$(t,r)?0:1,i=$(n,r)?0:1;return o-i})}function Pr(e){return`${vt}${e}`}function Ue(e){return J(e.limitId)==="codex"||J(e.limitName)==="codex"}function Nt(e,r){return` ${e.padEnd(Ut)}${kr(r)}`}function kr(e){let r=v(e.usedPercent),t=e.resetsAt?` (resets ${Tr(e.resetsAt)})`:"";return`${ce(r)} ${r.toFixed(0)}% used${t}`}function $r(e){if(!e.hasCredits)return"no credits";if(e.unlimited)return"unlimited credits";let r=e.balance?.trim();return r?`${kt(Number(r),r)} credits`:"credits available"}function Tr(e){let r=new Date(e*1e3);if(Number.isNaN(r.getTime()))return"at an unknown time";let t=new Date,n=`${r.getHours().toString().padStart(2,"0")}:${r.getMinutes().toString().padStart(2,"0")}`;if(r.toDateString()===t.toDateString())return n;let o=r.getDate().toString(),i=r.toLocaleDateString(void 0,{month:"short"});return`${n} on ${o} ${i}`}function Bt(e,r){let t=[" >_ Anthropic Usage",""],n=["claude"],o=[];for(let[a,c]of Object.entries(e)){if(Mt.has(a)||!c||typeof c!="object"||Array.isArray(c))continue;let p=c,m=l(p.limit_dollars),h=l(p.used_dollars);m===void 0||h===void 0||o.push({key:a,usedPercent:v(l(p.utilization)??0),usedDollars:h,limitDollars:m,resetsAt:f(p.resets_at)})}let i=!1;if(o.length>0){i=!0,o.sort((c,p)=>c.limitDollars-p.limitDollars),t.push(" Enterprise budget windows:");for(let c of o){let p=Se(c.key).replace(/\b\w/g,h=>h.toUpperCase()),m=c.resetsAt?` (resets ${Xe(c.resetsAt)})`:"";t.push(` ${p}: ${ce(c.usedPercent)} ${c.usedPercent.toFixed(0)}% used (${M(c.usedDollars,"USD")}/${M(c.limitDollars,"USD")})${m}`)}let a=[...o].sort((c,p)=>p.usedPercent-c.usedPercent)[0];n.push(`${a.usedPercent.toFixed(0)}%`),n.push(`${M(a.usedDollars,"USD",0)}/${M(a.limitDollars,"USD",0)}`)}let d=e.extra_usage;if(d){let a=g(d,"Anthropic extra usage"),c=F(a.is_enabled),p=l(a.used_credits);if(c&&p!==void 0){i&&t.push("");let m=n.length>1;i=!0;let h=f(a.currency)??"USD",w=p/100,S=l(a.monthly_limit)!==void 0?l(a.monthly_limit)/100:void 0,z=v(l(a.utilization)??0),Ce=`${M(w,h)}${S!==void 0?`/${M(S,h)}`:""}`,b=`${M(w,h,0)}${S!==void 0?`/${M(S,h,0)}`:""}`,re=f(a.reset_at)??f(a.resets_at),ne=re?` (resets ${Xe(re)})`:"";t.push(" Monthly extra usage:"),t.push(` ${ce(z)} ${z.toFixed(0)}% used ${Ce}${ne}`),n.push(`${z.toFixed(0)}%`),n.push(b),m&&n.push("extra")}}let s=Dt(e.five_hour),u=Dt(e.seven_day);if((s||u)&&(i&&t.push(""),i=!0,t.push(" Subscription usage:"),s&&t.push(` ${Ft("5h",s,!1)}`),u&&t.push(` ${Ft("7d",u,!0)}`),n.length===1&&(s&&n.push(`${s.usedPercent.toFixed(0)}% 5h`),u&&n.push(`${u.usedPercent.toFixed(0)}% 7d`))),!i)throw new Error("Anthropic usage endpoint returned no displayable usage data.");return{provider:"anthropic",source:"anthropic-oauth",capturedAt:r,summaryLines:t,statusline:n.join(" ")}}function Dt(e){if(!e)return;let r=g(e,"Anthropic rolling usage window"),t=l(r.utilization);if(t!==void 0)return{usedPercent:v(t),resetsAt:f(r.resets_at)}}function Ft(e,r,t){let n=r.resetsAt?` (resets ${Xe(r.resetsAt,t)})`:"";return`${e}: ${ce(r.usedPercent)} ${r.usedPercent.toFixed(0)}% used${n}`}function Xe(e,r=!0){let t=new Date(e);if(Number.isNaN(t.getTime()))return"at an unknown time";let n=new Date,o=`${t.getHours().toString().padStart(2,"0")}:${t.getMinutes().toString().padStart(2,"0")}`;if(!r&&t.toDateString()===n.toDateString())return o;let i=t.getDate().toString(),d=t.toLocaleDateString(void 0,{month:"short"});return`${o} on ${i} ${d}`}async function Ye(e,r){let t=await Qe(e);if(!t)throw new Error("No Pi Anthropic subscription auth was available. Use a Pi Anthropic model and run /login for Anthropic.");let n=await _(De,{headers:t.headers},r,"Anthropic usage"),o=await n.text();n.ok||X("Anthropic",n,o);let i=V(o,"Anthropic usage endpoint response");return Bt(i,Date.now())}async function zt(e,r){let t=[];if(q(e).length>0)try{let n=await Qe(e);if(!n)throw new Error("No Anthropic auth available.");let o=await _(De,{headers:n.headers},r,"Anthropic usage"),i=await o.text();t.push(`>_ Anthropic raw (${o.status}):
|
|
7
|
+
${Ke(i)}`)}catch(n){if(y(n))throw n;t.push(`>_ Anthropic raw: error \u2014 ${x(n)}`)}if(K(e).length>0)try{let n=await ue(e);if(!n)throw new Error("No Codex auth available.");let o=await _(Ae,{headers:n.headers},r,"Codex usage"),i=await o.text();t.push(`>_ Codex raw (${o.status}):
|
|
8
|
+
${Ke(i)}`)}catch(n){if(y(n))throw n;t.push(`>_ Codex raw: error \u2014 ${x(n)}`)}return t.length===0?"No providers with usable auth found.":t.join(`
|
|
9
|
+
|
|
10
|
+
`)}function Ze(e){let r=e.trimStart();if(r==="")return[...je];let t=/\s$/.test(r),n=r.trimEnd().split(/\s+/).filter(Boolean),o=n.at(-1);if(o==="--timeout"&&t||o==="--consume-banked-reset"&&t||!t&&n.at(-2)==="--timeout"||!t&&n.at(-2)==="--consume-banked-reset")return null;let i=t?"":o??"";if(i&&!i.startsWith("-"))return null;let d=t?"":r.match(/\S+$/)?.[0]??"",s=t?r:r.slice(0,r.length-d.length),u=je.filter(a=>a.value.startsWith(i));return u.length>0?u.map(a=>({...a,value:`${s}${a.value}`})):null}function et(e){let r=e.trim().split(/\s+/).filter(Boolean),t=!1,n=!1,o,i=!1,d=!1,s=!1,u=!0,a=Re;for(let p=0;p<r.length;p++){let m=r[p];if(m==="--clear-statusline"){t=!0;continue}if(m==="--raw"){d=!0;continue}if(m==="--list-banked-resets"){i=!0;continue}if(m==="--consume-banked-reset"){n=!0;let h=r[p+1];h&&!h.startsWith("-")&&(o=h,p+=1);continue}if(m==="--no-statusline"){u=!1;continue}if(m==="--refresh"){s=!0;continue}if(m==="--timeout"){let h=r[p+1];if(!h)return{ok:!1,error:"Usage: /usage [--refresh] [--raw] [--list-banked-resets] [--consume-banked-reset <id>] [--timeout seconds]"};let w=Number(h);if(!Number.isFinite(w)||w<=0||w>120)return{ok:!1,error:"--timeout must be a number of seconds between 1 and 120."};a=Math.round(w*1e3),p+=1;continue}return{ok:!1,error:`Unknown option: ${m}. Usage: /usage [--refresh] [--raw] [--list-banked-resets] [--consume-banked-reset <id>] [--no-statusline] [--clear-statusline] [--timeout seconds]`}}return[t,d,i,n].filter(Boolean).length>1?{ok:!1,error:"Choose only one of: --clear-statusline, --raw, --list-banked-resets, --consume-banked-reset."}:{ok:!0,value:{clearStatusline:t,consumeBankedReset:n,consumeBankedResetId:o,listBankedResets:i,raw:d,refresh:s,statusline:u,timeoutMs:a}}}import{randomUUID as Nr}from"node:crypto";async function rt(e,r){let t=await Je(e);if(!t)throw new Error("No Pi OpenAI Codex auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).");let n=await _(At,{headers:t.headers},r,"Codex banked resets"),o=await n.text();n.ok||X("Codex banked resets",n,o);let i=V(o,"Codex banked reset credits response");return Lr(i)}async function jt(e,r,t){let n=await Je(e);if(!n)throw new Error("No Pi OpenAI Codex auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).");let o=Nr(),i=[{id:t,redeem_request_id:o},{credit_id:t,redeem_request_id:o},{rate_limit_reset_credit_id:t,redeem_request_id:o},{id:t,redeemRequestId:o},{credit_id:t,redeemRequestId:o},{rate_limit_reset_credit_id:t,redeemRequestId:o}],d;for(let s of i){let u=await _(Rt,{method:"POST",headers:n.headers,body:JSON.stringify(s)},r,"Codex consume banked reset"),a=await u.text();if(u.ok)return;if(d=new Error(`Codex consume banked reset returned ${u.status} ${u.statusText}: ${Q(a)}`),u.status!==400&&u.status!==422)break}throw d??new Error("Failed to consume Codex banked reset.")}function Lr(e){let t=(Array.isArray(e.credits)?e.credits:[]).map(o=>Or(o)).filter(o=>o!==void 0),n=l(e.available_count)??l(e.availableCount)??t.filter(o=>(o.status??"available")==="available").length;return{availableCount:Math.max(0,Math.trunc(n)),credits:t}}function Or(e){if(!e)return;let r=g(e,"Codex reset credit"),t=f(r.id);if(t)return{id:t,resetType:f(r.reset_type)??f(r.resetType),status:f(r.status),grantedAt:f(r.granted_at)??f(r.grantedAt),expiresAt:f(r.expires_at)??f(r.expiresAt)}}function Wt(e){let r=[" >_ Codex Banked Resets",""];if(r.push(` Banked resets available: ${e.availableCount}`),e.credits.length===0)return r.push(" No banked reset credits found."),r.join(`
|
|
11
|
+
`);for(let t of e.credits){let n=t.status?` [${t.status}]`:"",o=t.grantedAt?`granted ${tt(t.grantedAt)}`:void 0,i=t.expiresAt?`expires ${tt(t.expiresAt)}`:void 0,d=[o,i].filter(Boolean).join(" \u2022 ");r.push(` ${t.id}${n}${d?` \u2014 ${d}`:""}`)}return r.join(`
|
|
12
|
+
`)}function nt(e){let r=e.expiresAt?`expires ${tt(e.expiresAt)}`:"no expiry shown";return`${e.id} \u2014 ${r}`}function tt(e,r=!0){let t=new Date(e);if(Number.isNaN(t.getTime()))return"at an unknown time";let n=new Date,o=`${t.getHours().toString().padStart(2,"0")}:${t.getMinutes().toString().padStart(2,"0")}`;if(!r&&t.toDateString()===n.toDateString())return o;let i=t.getDate().toString(),d=t.toLocaleDateString(void 0,{month:"short"});return`${o} on ${i} ${d}`}import{spawn as zr}from"node:child_process";import{createInterface as jr}from"node:readline";function ot(e,r,t){let n=[],o=f(e.plan_type),i=Qt(e.rate_limit_reset_credits),d=Ht("codex",void 0,e.rate_limit,e.credits);d&&n.push(d);let s=Array.isArray(e.additional_rate_limits)?e.additional_rate_limits:[];for(let u of s){let a=g(u,"additional rate limit"),c=f(a.metered_feature)??f(a.limit_name);if(!c)continue;let p=Ht(c,f(a.limit_name),a.rate_limit,void 0);p&&n.push(p)}if(n.length===0)throw new Error("Codex usage endpoint returned no displayable rate-limit windows.");return{provider:"codex",source:t,capturedAt:r,planType:o,bankedResetsAvailable:i,snapshots:n}}function Ht(e,r,t,n){if(t==null){let u=qt(n);return u?{limitId:e,limitName:r,credits:u}:void 0}let o=g(t,"rate limit"),i=Kt(o.primary_window),d=Kt(o.secondary_window),s=qt(n);if(!(!i&&!d&&!s))return{limitId:e,limitName:r,primary:i,secondary:d,credits:s}}function Kt(e){if(e==null)return;let r=g(e,"rate-limit window"),t=l(r.used_percent);if(t===void 0)return;let n=l(r.limit_window_seconds),o=l(r.reset_at);return{usedPercent:t,windowMinutes:n&&n>0?Math.ceil(n/60):void 0,resetsAt:o}}function qt(e){if(e==null)return;let r=g(e,"credits"),t=F(r.has_credits),n=F(r.unlimited);if(!(t===void 0||n===void 0))return{hasCredits:t,unlimited:n,balance:f(r.balance)}}function it(e,r){let t=[],n=Qt(e.rateLimitResetCredits),o=(d,s)=>{let u=Dr(d,s);if(!u)return;let a=t.findIndex(c=>c.limitId===u.limitId);a>=0?t[a]=Br(t[a],u):t.push(u)};if(o(e.rateLimits,"codex"),e.rateLimitsByLimitId&&typeof e.rateLimitsByLimitId=="object")for(let[d,s]of Object.entries(e.rateLimitsByLimitId))o(s,d);if(t.length===0)throw new Error("codex app-server returned no displayable rate-limit windows.");let i=Ir(e.rateLimits);return{provider:"codex",source:"codex-app-server",capturedAt:r,planType:i,bankedResetsAvailable:n,snapshots:t}}function Ir(e){if(e==null)return;let r=g(e,"app-server rate-limit snapshot");return f(r.planType)}function Dr(e,r){if(e==null)return;let t=g(e,"app-server rate-limit snapshot"),n=f(t.limitId)??r,o=f(t.limitName),i=Vt(t.primary),d=Vt(t.secondary),s=Fr(t.credits);if(!(!i&&!d&&!s))return{limitId:n,limitName:o,primary:i,secondary:d,credits:s}}function Vt(e){if(e==null)return;let r=g(e,"app-server rate-limit window"),t=l(r.usedPercent);if(t!==void 0)return{usedPercent:t,windowMinutes:l(r.windowDurationMins),resetsAt:l(r.resetsAt)}}function Fr(e){if(e==null)return;let r=g(e,"app-server credits"),t=F(r.hasCredits),n=F(r.unlimited);if(!(t===void 0||n===void 0))return{hasCredits:t,unlimited:n,balance:f(r.balance)}}function Qt(e){if(e==null)return;let r=g(e,"rate limit reset credits"),t=l(r.available_count)??l(r.availableCount);return t!==void 0?Math.max(0,Math.trunc(t)):void 0}function Br(e,r){return{limitId:r.limitId||e.limitId,limitName:r.limitName??e.limitName,primary:r.primary??e.primary,secondary:r.secondary??e.secondary,credits:r.credits??e.credits}}async function Me(e){let r=new st(e);try{await r.start(),await r.request("initialize",{clientInfo:{name:"pi_codex_usage",title:"Pi Codex Usage",version:"0.1.0"},capabilities:{experimentalApi:!1,requestAttestation:!1,optOutNotificationMethods:[]}}),r.notify("initialized");let t=await r.request("account/rateLimits/read",void 0);return it(g(t,"account/rateLimits/read result"),Date.now())}finally{r.dispose()}}var st=class{child;nextId=1;stderr="";pending=new Map;startPromise;exitError;timeoutMs;constructor(r){this.timeoutMs=r}start(){return this.startPromise?this.startPromise:(this.startPromise=new Promise((r,t)=>{let n=zr("codex",["app-server","--listen","stdio://"],{stdio:["pipe","pipe","pipe"]});this.child=n;let o=setTimeout(()=>{t(new Error(`Timed out after ${Math.round(this.timeoutMs/1e3)}s starting codex app-server.`))},this.timeoutMs);n.once("spawn",()=>{clearTimeout(o),r()}),n.once("error",d=>{clearTimeout(o),t(new Error(`Failed to start codex app-server: ${d.message}`)),this.rejectAll(d)}),n.once("exit",(d,s)=>{let u=this.stderr?` stderr: ${Q(this.stderr)}`:"";this.exitError=new Error(`codex app-server exited before completing the request (code ${d??"unknown"}, signal ${s??"none"}).${u}`),this.rejectAll(this.exitError)}),n.stderr.setEncoding("utf8"),n.stderr.on("data",d=>{this.stderr=He(this.stderr+d,ae)}),jr({input:n.stdout}).on("line",d=>this.handleLine(d))}),this.startPromise)}request(r,t){let n=this.child;if(!n?.stdin.writable)throw new Error("codex app-server is not running.");if(this.exitError)throw this.exitError;let o=this.nextId++,i=t===void 0?{method:r,id:o}:{method:r,id:o,params:t},d=new Promise((s,u)=>{let a=setTimeout(()=>{this.pending.delete(o),u(new Error(`Timed out after ${Math.round(this.timeoutMs/1e3)}s waiting for ${r}.`))},this.timeoutMs);this.pending.set(o,{resolve:c=>{clearTimeout(a),s(c)},reject:c=>{clearTimeout(a),u(c)}})});return n.stdin.write(`${JSON.stringify(i)}
|
|
13
|
+
`),d}notify(r){let t=this.child;t?.stdin.writable&&t.stdin.write(`${JSON.stringify({method:r})}
|
|
14
|
+
`)}dispose(){for(let[t,n]of this.pending)n.reject(new Error(`codex app-server request ${t} cancelled.`));this.pending.clear();let r=this.child;r&&(r.stdin.end(),r.killed||r.kill(),this.child=void 0)}handleLine(r){let t;try{t=JSON.parse(r)}catch{return}if(typeof t.id!="number")return;let n=this.pending.get(t.id);if(n){if(this.pending.delete(t.id),t.error){let o=typeof t.error.message=="string"?t.error.message:"unknown error";n.reject(new Error(`codex app-server request failed: ${o}`));return}n.resolve(t.result)}}rejectAll(r){for(let t of this.pending.values())t.reject(r);this.pending.clear()}};async function at(e,r){let t=await ue(e);if(!t)throw new Error("No Pi OpenAI Codex subscription auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).");let n=await _(Ae,{headers:t.headers},r),o=await n.text();n.ok||X("Codex",n,o);let i=V(o,"Codex usage endpoint response");return ot(i,Date.now(),"pi-auth")}async function Jt(e,r){try{return{report:await at(e,r),errors:[]}}catch(t){if(y(t))throw t;let n=[{source:"pi-auth",message:x(t),cause:t}];try{return{report:await Me(r),errors:n}}catch(o){if(y(o))throw o;return n.push({source:"codex-app-server",message:x(o),cause:o}),{errors:n}}}}async function dt(e,r){let t=K(e).length>0,n=q(e).length>0,o={errors:[]},[i,d]=await Promise.allSettled([t?Jt(e,r.timeoutMs):Promise.resolve(o),n?Ye(e,r.timeoutMs).then(c=>({report:c,errors:[]})):Promise.resolve(o)]),s=[],u=[],a=(c,p)=>{if(c.status==="rejected"){if(y(c.reason))throw c.reason;u.push({source:p,message:x(c.reason),cause:c.reason})}else c.value.report&&s.push(c.value.report),u.push(...c.value.errors)};return a(i,"pi-auth"),a(d,"anthropic-oauth"),{reports:s,errors:u}}async function Gt(e,r){let t;for(let n=1;n<=Fe;n+=1){if(t=await Wr(e,r),t.ok||t.errors.some(o=>G(o.message)))return t;n<Fe&&await Pt(St)}return t??{ok:!1,errors:[{source:"pi-auth",message:"Usage query failed."}]}}async function Wr(e,r){if(de(e.model))try{return{ok:!0,report:await Ye(e,r.timeoutMs)}}catch(n){if(y(n))throw n;return{ok:!1,errors:[{source:"anthropic-oauth",message:x(n),cause:n}]}}if(!H(e.model))return{ok:!1,errors:[{source:"pi-auth",message:"Current model provider is not supported."}]};let t=[];try{return{ok:!0,report:await at(e,r.timeoutMs)}}catch(n){if(y(n))throw n;t.push({source:"pi-auth",message:x(n),cause:n})}try{return{ok:!0,report:await Me(r.timeoutMs)}}catch(n){if(y(n))throw n;t.push({source:"codex-app-server",message:x(n),cause:n})}return{ok:!1,errors:t}}import{mkdirSync as Hr,readFileSync as Kr,renameSync as qr,writeFileSync as Xt}from"node:fs";import{dirname as Vr}from"node:path";function Y(){try{let e=JSON.parse(Kr(W,"utf8"));return!e||e.version!==Be||!e.entries||typeof e.entries!="object"?void 0:e}catch{return}}function _e(e){try{let r=Y()??{version:Be,entries:{}};e(r),Hr(Vr(W),{recursive:!0});let t=`${W}.${process.pid}.tmp`;Xt(t,JSON.stringify(r));try{qr(t,W)}catch{Xt(W,JSON.stringify(r))}}catch{}}function pe(e){_e(r=>{r.entries[e.provider]={createdAt:Date.now(),report:e},r.backoffUntil&&delete r.backoffUntil[e.provider]})}function Yt(e){_e(r=>{delete r.entries[e]})}function Pe(e,r){let t=Math.min(r,Date.now()+ie);_e(n=>{n.backoffUntil={...n.backoffUntil??{},[e]:t}})}function Zt(){_e(e=>{e.backoffUntil=void 0})}function er(e){let r=Y()?.backoffUntil?.[e]??0;return Math.min(r-Date.now(),ie)}var Z,le,R,T,P=0,ee=!1,me;function ct(){return ee}function pt(e){ee=e}function tr(){return le}function ke(e){le=e}function rr(){Z=void 0}var nr=()=>{R&&clearTimeout(R),T&&clearTimeout(T),R=void 0,T=void 0};function N(e,r){return y(r)?(e===me&&(P+=1,nr(),me=void 0),!0):!1}var he=e=>r=>{if(!N(e,r))throw r},k=(e,r)=>{try{return e.ui.setStatus(se,r),!0}catch(t){if(N(e,t))return!1;throw t}};function L(e){P+=1,nr(),me=void 0,k(e,void 0)}var Qr=e=>{R&&clearTimeout(R);let r=P;R=setTimeout(()=>{R=void 0,!(!ee||r!==P)&&k(e,void 0)},E),R.unref?.()},ut=(e,r,t=E)=>{T&&clearTimeout(T);let n=P;T=setTimeout(()=>{T=void 0,!(!ee||n!==P)&&ge(e,r).catch(he(e))},t),T.unref?.()},fe=(e,r,t)=>{let n=It(r,t.model);t.staleAgeMs!==void 0&&t.staleAgeMs>E&&(n=`${n} (${we(t.staleAgeMs)} old)`),k(e,n)&&(me=e,R&&clearTimeout(R),R=void 0,t.autoRefresh?ut(e,t.model,t.refreshDelayMs):Qr(e))},Jr=e=>{let r=Z&&$(Z.report,e)?Z:void 0;if(le){let n=le.reports.find(o=>$(o,e));if(n){let o={createdAt:le.createdAt,report:n};(!r||o.createdAt>r.createdAt)&&(r=o)}}let t=Y()?.entries?.[We(e)];return t?.report&&$(t.report,e)&&(!r||t.createdAt>r.createdAt)&&(r=t),r};async function ge(e,r){if(!ee)return;me=e;let t=r??e.model;if(!D(t)){L(e);return}let n=P+1;P=n;let o=Jr(t),i=o&&Date.now()-o.createdAt<E?o:void 0;if(i){let a=E-(Date.now()-i.createdAt);fe(e,i.report,{autoRefresh:!0,model:t,refreshDelayMs:Math.max(a,1e4)});return}let d=We(t),s=er(d);if(s>0){o?fe(e,o.report,{autoRefresh:!0,model:t,staleAgeMs:Date.now()-o.report.capturedAt,refreshDelayMs:Math.max(s,1e4)}):k(e,`usage rate-limited (${we(s)})`)&&ut(e,t,Math.max(s,1e4));return}if(!o&&!k(e,"checking"))return;let u=await Gt(e,{timeoutMs:Re});if(!(!ee||n!==P)){if(!u.ok){let a=u.errors.some(m=>G(m.message)),c=a?Ee(u.errors):E;if(a&&Pe(d,Date.now()+c),o){fe(e,o.report,{autoRefresh:!0,model:t,staleAgeMs:Date.now()-o.report.capturedAt,refreshDelayMs:c});return}let p=a?`usage rate-limited (${we(c)})`:"usage error";k(e,p)&&ut(e,t,c);return}Z={createdAt:Date.now(),report:u.report},pe(u.report),fe(e,u.report,{autoRefresh:!0,model:t})}}function ft(e,r){let t=r.find(n=>$(n,e.model));return t?(Z={createdAt:Date.now(),report:t},fe(e,t,{autoRefresh:D(e.model),model:e.model}),!0):(k(e,void 0),!1)}function or(e){return k(e,"checking")}function $e(e){return k(e,void 0)}function ir(e){e.registerCommand(yt,{description:"Show usage for all configured providers (Codex and Anthropic)",getArgumentCompletions:Ze,handler:async(r,t)=>{try{let n=et(r);if(!n.ok){t.ui.notify(n.error,"warning");return}if(n.value.clearStatusline){L(t),t.ui.notify("Usage statusline cleared.","info");return}if(n.value.raw){let s=n.value.timeoutMs;zt(t,s).then(u=>{ct()&&t.ui.notify(u,"info")}).catch(u=>{N(t,u)||t.ui.notify(x(u),"error")});return}if(n.value.listBankedResets){let s=await rt(t,n.value.timeoutMs);t.ui.notify(Wt(s),"info");return}if(n.value.consumeBankedReset){let s=await rt(t,n.value.timeoutMs);if(s.availableCount<=0||s.credits.length===0){t.ui.notify("No Codex banked resets are available.","info");return}let u=n.value.consumeBankedResetId;if(!u){if(!t.hasUI){t.ui.notify("Usage: /usage --consume-banked-reset <id> (or run with a TUI to pick interactively)","warning");return}let a=await t.ui.select("Choose a Codex banked reset to consume",s.credits.map(c=>nt(c)));if(!a)return;u=s.credits.find(c=>nt(c)===a)?.id}if(!u){t.ui.notify("Could not determine which banked reset to consume.","warning");return}await jt(t,n.value.timeoutMs,u),rr(),ke(void 0),Yt("codex"),t.ui.notify(`Consumed Codex banked reset ${u}.`,"info"),dt(t,{timeoutMs:n.value.timeoutMs}).then(a=>{if(a.reports.length>0){ke({createdAt:Date.now(),reports:a.reports});for(let c of a.reports)pe(c);ve(t,a.reports,!1)}}).catch(()=>{});return}let o=tr(),i=o&&Date.now()-o.createdAt<E?o:void 0;if(!i){let s=Y();if(s){let u=Object.values(s.entries).filter(a=>!!a&&Date.now()-a.createdAt<E);u.length>0&&(i={createdAt:Math.min(...u.map(a=>a.createdAt)),reports:u.map(a=>a.report)})}}if(i&&!n.value.refresh){n.value.statusline&&ft(t,i.reports),ve(t,i.reports,!0);return}let d=n.value;d.refresh&&Zt(),d.statusline&&or(t),dt(t,d).then(s=>{if(!ct())return;if(s.reports.length===0){d.statusline&&$e(t),t.ui.notify(Ge(s.errors),"error");return}ke({createdAt:Date.now(),reports:s.reports});for(let a of s.reports)pe(a);for(let a of s.errors)G(a.message)&&Pe(a.source==="anthropic-oauth"?"anthropic":"codex",Date.now()+Ee([a]));let u=d.statusline?ft(t,s.reports):!1;d.statusline&&!u&&$e(t),ve(t,s.reports,!1),s.errors.length>0&&t.ui.notify(Ge(s.errors),"warning")}).catch(s=>{d.statusline&&$e(t),N(t,s)||t.ui.notify(x(s),"error")})}catch(n){if(N(t,n))return;throw n}}})}import{isAbsolute as Gr,relative as Xr,resolve as sr,sep as ar}from"node:path";import{truncateToWidth as xe,visibleWidth as B}from"@earendil-works/pi-tui";var dr=!1;function ur(){return dr}function cr(e){dr=e}function pr(e,r){r.ui.setFooter((t,n,o)=>{let i=t,d=n,s=o;return{dispose:s.onBranchChange(()=>i.requestRender()),invalidate(){},render(a){return Yr(e,r,d,s,a)}}})}function Yr(e,r,t,n,o){let i=0,d=0,s=0,u=0,a=0,c;for(let A of r.sessionManager.getEntries()){let oe=A.message;if(A.type!=="message"||oe?.role!=="assistant")continue;let I=oe.usage??{},gt=l(I.input)??0,mr=l(I.output)??0,Le=l(I.cacheRead)??0,xt=l(I.cacheWrite)??0,hr=I.cost;i+=gt,d+=mr,s+=Le,u+=xt,a+=l(hr?.total)??0;let Ct=gt+Le+xt;c=Ct>0?Le/Ct*100:void 0}let p=r.getContextUsage(),m=p?.contextWindow??r.model?.contextWindow??0,h=p?.percent??0,w=p&&p.percent!==null?h.toFixed(1):"?",S=en(r.sessionManager.getCwd(),process.env.HOME||process.env.USERPROFILE),z=n.getGitBranch();z&&(S=`${S} (${z})`);let Ce=r.sessionManager.getSessionName();Ce&&(S=`${S} \u2022 ${Ce}`);let b=[];i&&b.push(`\u2191${te(i)}`),d&&b.push(`\u2193${te(d)}`),s&&b.push(`R${te(s)}`),u&&b.push(`W${te(u)}`),(s>0||u>0)&&c!==void 0&&b.push(`CH${c.toFixed(1)}%`);let re=r.model?r.modelRegistry.isUsingOAuth(r.model):!1;(a||re)&&b.push(`$${a.toFixed(3)}${re?" (sub)":""}`);let ne=w==="?"?`?/${te(m)}`:`${w}%/${te(m)}`,Te=ne;h>90?Te=t.fg("error",ne):h>70&&(Te=t.fg("warning",ne)),b.push(Te);let U=b.join(" "),O=B(U);O>o&&(U=xe(U,o,"..."),O=B(U));let Ne=r.model?.id||"no-model",j=Ne;if(r.model?.reasoning){let A=e.getThinkingLevel?.()??"off";j=A==="off"?`${Ne} \u2022 thinking off`:`${Ne} \u2022 ${A}`}if(n.getAvailableProviderCount()>1&&r.model){let A=`(${r.model.provider}) ${j}`;O+2+B(A)<=o&&(j=A)}let lt=B(j),ye;if(O+2+lt<=o)ye=U+" ".repeat(o-O-lt)+j;else{let A=o-O-2;if(A>0){let oe=xe(j,A,""),I=Math.max(0,o-O-B(oe));ye=U+" ".repeat(I)+oe}else ye=U}let fr=t.fg("dim",U),lr=t.fg("dim",ye.slice(U.length)),mt=[xe(t.fg("dim",S),o,t.fg("dim","...")),fr+lr],ht=Zr(t,n,o);return ht!==void 0&&mt.push(ht),mt}function Zr(e,r,t){let n=r.getExtensionStatuses();if(n.size===0)return;let o=n.get(se),d=Array.from(n.entries()).filter(([p])=>p!==se).sort(([p],[m])=>p.localeCompare(m)).map(([,p])=>Ve(p)).join(" ");if(!o)return d?xe(d,t,e.fg("dim","...")):void 0;let s=Ve(o),u=B(d),a=B(s);if(u+1+a>t)return xe(d?`${d} ${s}`:s,t,e.fg("dim","..."));let c=" ".repeat(Math.max(1,t-u-a));return d+c+s}function te(e){return e<1e3?e.toString():e<1e4?`${(e/1e3).toFixed(1)}k`:e<1e6?`${Math.round(e/1e3)}k`:e<1e7?`${(e/1e6).toFixed(1)}M`:`${Math.round(e/1e6)}M`}function en(e,r){if(!r)return e;let t=sr(e),n=sr(r),o=Xr(n,t);return o===""||o!==".."&&!o.startsWith(`..${ar}`)&&!Gr(o)?o===""?"~":`~${ar}${o}`:e}function tn(e){ir(e);let r=t=>{if(!(ur()||!t.hasUI))try{pr(e,t),cr(!0)}catch(n){if(!N(t,n))throw n}};e.on("session_start",(t,n)=>{pt(!0),r(n),D(n.model)?ge(n,n.model).catch(he(n)):L(n)}),e.on("session_tree",(t,n)=>{D(n.model)?ge(n,n.model).catch(he(n)):L(n)}),e.on("model_select",(t,n)=>{D(t.model)?ge(n,t.model).catch(he(n)):L(n)}),e.on("session_shutdown",(t,n)=>{pt(!1),L(n)})}export{Ze as completeCodexStatusArguments,tn as default,Lt as formatCodexUsageReport,Ot as formatCodexUsageStatusline,y as isStaleExtensionContextError,it as normalizeAppServerResponse,ot as normalizeBackendPayload,et as parseArgs};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sreetej510/pi-usage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Pi extension that reports provider usage/rate-limit budgets (Codex, Anthropic OAuth, etc.) via /usage, with an optional statusline widget.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,16 +14,18 @@
|
|
|
14
14
|
"statusline"
|
|
15
15
|
],
|
|
16
16
|
"files": [
|
|
17
|
-
"
|
|
17
|
+
"dist",
|
|
18
18
|
"README.md",
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"pi": {
|
|
22
22
|
"extensions": [
|
|
23
|
-
"./
|
|
23
|
+
"./dist/index.js"
|
|
24
24
|
]
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
+
"build": "node ../../scripts/build-extension.mjs",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
27
29
|
"check": "biome check . && npm run typecheck",
|
|
28
30
|
"format": "biome check --write .",
|
|
29
31
|
"typecheck": "tsc --noEmit"
|
package/src/anthropic-auth.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { anthropicAuthCandidateModels } from "./models.js";
|
|
3
|
-
import { hasHeader } from "./utils.js";
|
|
4
|
-
|
|
5
|
-
export async function resolvePiAnthropicAuth(
|
|
6
|
-
ctx: ExtensionContext,
|
|
7
|
-
): Promise<{ headers: Record<string, string> } | undefined> {
|
|
8
|
-
const models = anthropicAuthCandidateModels(ctx);
|
|
9
|
-
const errors: string[] = [];
|
|
10
|
-
|
|
11
|
-
for (const model of models) {
|
|
12
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
13
|
-
if (!auth.ok) {
|
|
14
|
-
errors.push(auth.error);
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const headers = { ...(auth.headers ?? {}) };
|
|
19
|
-
if (!hasHeader(headers, "Authorization") && auth.apiKey) {
|
|
20
|
-
headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
21
|
-
}
|
|
22
|
-
if (!hasHeader(headers, "Accept")) headers.Accept = "application/json";
|
|
23
|
-
if (!hasHeader(headers, "User-Agent")) headers["User-Agent"] = "pi-usage";
|
|
24
|
-
if (hasHeader(headers, "Authorization")) {
|
|
25
|
-
return { headers };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (errors.length > 0) {
|
|
30
|
-
throw new Error(errors.join("; "));
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
package/src/anthropic-query.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { resolvePiAnthropicAuth } from "./anthropic-auth.js";
|
|
3
|
-
import { resolvePiCodexAuth } from "./codex-auth.js";
|
|
4
|
-
import { ANTHROPIC_OAUTH_USAGE_URL, CODEX_USAGE_URL } from "./constants.js";
|
|
5
|
-
import { isStaleExtensionContextError, throwUsageEndpointError } from "./errors.js";
|
|
6
|
-
import { fetchWithTimeout } from "./http.js";
|
|
7
|
-
import { anthropicAuthCandidateModels, codexAuthCandidateModels } from "./models.js";
|
|
8
|
-
import { normalizeAnthropicUsagePayload } from "./normalize-anthropic.js";
|
|
9
|
-
import type { AnthropicOAuthUsagePayload, AnthropicUsageReport } from "./types.js";
|
|
10
|
-
import { errorMessage, parseJsonObject, prettyJson } from "./utils.js";
|
|
11
|
-
|
|
12
|
-
export async function queryAnthropicUsage(ctx: ExtensionContext, timeoutMs: number): Promise<AnthropicUsageReport> {
|
|
13
|
-
const auth = await resolvePiAnthropicAuth(ctx);
|
|
14
|
-
if (!auth) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
"No Pi Anthropic subscription auth was available. Use a Pi Anthropic model and run /login for Anthropic.",
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const response = await fetchWithTimeout(
|
|
21
|
-
ANTHROPIC_OAUTH_USAGE_URL,
|
|
22
|
-
{ headers: auth.headers },
|
|
23
|
-
timeoutMs,
|
|
24
|
-
"Anthropic usage",
|
|
25
|
-
);
|
|
26
|
-
const text = await response.text();
|
|
27
|
-
if (!response.ok) {
|
|
28
|
-
throwUsageEndpointError("Anthropic", response, text);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const payload = parseJsonObject(text, "Anthropic usage endpoint response");
|
|
32
|
-
return normalizeAnthropicUsagePayload(payload as AnthropicOAuthUsagePayload, Date.now());
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Fetch and pretty-print the raw usage API responses for debugging (/usage --raw). */
|
|
36
|
-
export async function fetchRawUsagePayloads(ctx: ExtensionContext, timeoutMs: number): Promise<string> {
|
|
37
|
-
const sections: string[] = [];
|
|
38
|
-
|
|
39
|
-
if (anthropicAuthCandidateModels(ctx).length > 0) {
|
|
40
|
-
try {
|
|
41
|
-
const auth = await resolvePiAnthropicAuth(ctx);
|
|
42
|
-
if (!auth) throw new Error("No Anthropic auth available.");
|
|
43
|
-
const response = await fetchWithTimeout(
|
|
44
|
-
ANTHROPIC_OAUTH_USAGE_URL,
|
|
45
|
-
{ headers: auth.headers },
|
|
46
|
-
timeoutMs,
|
|
47
|
-
"Anthropic usage",
|
|
48
|
-
);
|
|
49
|
-
const text = await response.text();
|
|
50
|
-
sections.push(`>_ Anthropic raw (${response.status}):\n${prettyJson(text)}`);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (isStaleExtensionContextError(error)) throw error;
|
|
53
|
-
sections.push(`>_ Anthropic raw: error \u2014 ${errorMessage(error)}`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (codexAuthCandidateModels(ctx).length > 0) {
|
|
58
|
-
try {
|
|
59
|
-
const auth = await resolvePiCodexAuth(ctx);
|
|
60
|
-
if (!auth) throw new Error("No Codex auth available.");
|
|
61
|
-
const response = await fetchWithTimeout(CODEX_USAGE_URL, { headers: auth.headers }, timeoutMs, "Codex usage");
|
|
62
|
-
const text = await response.text();
|
|
63
|
-
sections.push(`>_ Codex raw (${response.status}):\n${prettyJson(text)}`);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
if (isStaleExtensionContextError(error)) throw error;
|
|
66
|
-
sections.push(`>_ Codex raw: error \u2014 ${errorMessage(error)}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (sections.length === 0) return "No providers with usable auth found.";
|
|
71
|
-
return sections.join("\n\n");
|
|
72
|
-
}
|
package/src/args.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { COMMAND_COMPLETIONS, DEFAULT_TIMEOUT_MS } from "./constants.js";
|
|
2
|
-
import type { CommandArgumentCompletion, QueryUsageOptions } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export function completeCodexStatusArguments(argumentPrefix: string): CommandArgumentCompletion[] | null {
|
|
5
|
-
const prefix = argumentPrefix.trimStart();
|
|
6
|
-
if (prefix === "") return [...COMMAND_COMPLETIONS];
|
|
7
|
-
|
|
8
|
-
const trailingSpace = /\s$/.test(prefix);
|
|
9
|
-
const tokens = prefix.trimEnd().split(/\s+/).filter(Boolean);
|
|
10
|
-
const previous = tokens.at(-1);
|
|
11
|
-
if (previous === "--timeout" && trailingSpace) return null;
|
|
12
|
-
if (previous === "--consume-banked-reset" && trailingSpace) return null;
|
|
13
|
-
if (!trailingSpace && tokens.at(-2) === "--timeout") return null;
|
|
14
|
-
if (!trailingSpace && tokens.at(-2) === "--consume-banked-reset") return null;
|
|
15
|
-
|
|
16
|
-
const current = trailingSpace ? "" : (previous ?? "");
|
|
17
|
-
if (current && !current.startsWith("-")) return null;
|
|
18
|
-
|
|
19
|
-
const currentRaw = trailingSpace ? "" : (prefix.match(/\S+$/)?.[0] ?? "");
|
|
20
|
-
const completionPrefix = trailingSpace ? prefix : prefix.slice(0, prefix.length - currentRaw.length);
|
|
21
|
-
const matches = COMMAND_COMPLETIONS.filter((item) => item.value.startsWith(current));
|
|
22
|
-
return matches.length > 0 ? matches.map((item) => ({ ...item, value: `${completionPrefix}${item.value}` })) : null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function parseArgs(args: string): { ok: true; value: QueryUsageOptions } | { ok: false; error: string } {
|
|
26
|
-
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
27
|
-
let clearStatusline = false;
|
|
28
|
-
let consumeBankedReset = false;
|
|
29
|
-
let consumeBankedResetId: string | undefined;
|
|
30
|
-
let listBankedResets = false;
|
|
31
|
-
let raw = false;
|
|
32
|
-
let refresh = false;
|
|
33
|
-
let statusline = true;
|
|
34
|
-
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
35
|
-
|
|
36
|
-
for (let index = 0; index < tokens.length; index++) {
|
|
37
|
-
const token = tokens[index];
|
|
38
|
-
if (token === "--clear-statusline") {
|
|
39
|
-
clearStatusline = true;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (token === "--raw") {
|
|
43
|
-
raw = true;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (token === "--list-banked-resets") {
|
|
47
|
-
listBankedResets = true;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (token === "--consume-banked-reset") {
|
|
51
|
-
consumeBankedReset = true;
|
|
52
|
-
const rawValue = tokens[index + 1];
|
|
53
|
-
if (rawValue && !rawValue.startsWith("-")) {
|
|
54
|
-
consumeBankedResetId = rawValue;
|
|
55
|
-
index += 1;
|
|
56
|
-
}
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (token === "--no-statusline") {
|
|
60
|
-
statusline = false;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (token === "--refresh") {
|
|
64
|
-
refresh = true;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (token === "--timeout") {
|
|
68
|
-
const rawValue = tokens[index + 1];
|
|
69
|
-
if (!rawValue)
|
|
70
|
-
return {
|
|
71
|
-
ok: false,
|
|
72
|
-
error:
|
|
73
|
-
"Usage: /usage [--refresh] [--raw] [--list-banked-resets] [--consume-banked-reset <id>] [--timeout seconds]",
|
|
74
|
-
};
|
|
75
|
-
const parsed = Number(rawValue);
|
|
76
|
-
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 120) {
|
|
77
|
-
return { ok: false, error: "--timeout must be a number of seconds between 1 and 120." };
|
|
78
|
-
}
|
|
79
|
-
timeoutMs = Math.round(parsed * 1000);
|
|
80
|
-
index += 1;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
ok: false,
|
|
85
|
-
error: `Unknown option: ${token}. Usage: /usage [--refresh] [--raw] [--list-banked-resets] [--consume-banked-reset <id>] [--no-statusline] [--clear-statusline] [--timeout seconds]`,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const exclusiveActionCount = [clearStatusline, raw, listBankedResets, consumeBankedReset].filter(Boolean).length;
|
|
90
|
-
if (exclusiveActionCount > 1) {
|
|
91
|
-
return {
|
|
92
|
-
ok: false,
|
|
93
|
-
error: "Choose only one of: --clear-statusline, --raw, --list-banked-resets, --consume-banked-reset.",
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
ok: true,
|
|
99
|
-
value: {
|
|
100
|
-
clearStatusline,
|
|
101
|
-
consumeBankedReset,
|
|
102
|
-
consumeBankedResetId,
|
|
103
|
-
listBankedResets,
|
|
104
|
-
raw,
|
|
105
|
-
refresh,
|
|
106
|
-
statusline,
|
|
107
|
-
timeoutMs,
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
package/src/codex-app-server.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
|
-
import { MAX_ERROR_BODY_CHARS } from "./constants.js";
|
|
4
|
-
import { normalizeAppServerResponse } from "./normalize-codex.js";
|
|
5
|
-
import type { CodexUsageReport, PendingRpc, RpcResponse } from "./types.js";
|
|
6
|
-
import { assertObject, redactErrorBody, truncateEnd } from "./utils.js";
|
|
7
|
-
|
|
8
|
-
export async function queryViaCodexAppServer(timeoutMs: number): Promise<CodexUsageReport> {
|
|
9
|
-
const client = new CodexAppServerClient(timeoutMs);
|
|
10
|
-
try {
|
|
11
|
-
await client.start();
|
|
12
|
-
await client.request("initialize", {
|
|
13
|
-
clientInfo: {
|
|
14
|
-
name: "pi_codex_usage",
|
|
15
|
-
title: "Pi Codex Usage",
|
|
16
|
-
version: "0.1.0",
|
|
17
|
-
},
|
|
18
|
-
capabilities: {
|
|
19
|
-
experimentalApi: false,
|
|
20
|
-
requestAttestation: false,
|
|
21
|
-
optOutNotificationMethods: [],
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
client.notify("initialized");
|
|
25
|
-
const result = await client.request("account/rateLimits/read", undefined);
|
|
26
|
-
return normalizeAppServerResponse(assertObject(result, "account/rateLimits/read result"), Date.now());
|
|
27
|
-
} finally {
|
|
28
|
-
client.dispose();
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
class CodexAppServerClient {
|
|
33
|
-
private child?: ChildProcessWithoutNullStreams;
|
|
34
|
-
private nextId = 1;
|
|
35
|
-
private stderr = "";
|
|
36
|
-
private readonly pending = new Map<number, PendingRpc>();
|
|
37
|
-
private startPromise?: Promise<void>;
|
|
38
|
-
private exitError?: Error;
|
|
39
|
-
private readonly timeoutMs: number;
|
|
40
|
-
|
|
41
|
-
constructor(timeoutMs: number) {
|
|
42
|
-
this.timeoutMs = timeoutMs;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
start(): Promise<void> {
|
|
46
|
-
if (this.startPromise) return this.startPromise;
|
|
47
|
-
|
|
48
|
-
this.startPromise = new Promise((resolve, reject) => {
|
|
49
|
-
const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
50
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
51
|
-
});
|
|
52
|
-
this.child = child;
|
|
53
|
-
|
|
54
|
-
const startupTimeout = setTimeout(() => {
|
|
55
|
-
reject(new Error(`Timed out after ${Math.round(this.timeoutMs / 1000)}s starting codex app-server.`));
|
|
56
|
-
}, this.timeoutMs);
|
|
57
|
-
|
|
58
|
-
child.once("spawn", () => {
|
|
59
|
-
clearTimeout(startupTimeout);
|
|
60
|
-
resolve();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
child.once("error", (error) => {
|
|
64
|
-
clearTimeout(startupTimeout);
|
|
65
|
-
reject(new Error(`Failed to start codex app-server: ${error.message}`));
|
|
66
|
-
this.rejectAll(error);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
child.once("exit", (code, signal) => {
|
|
70
|
-
const suffix = this.stderr ? ` stderr: ${redactErrorBody(this.stderr)}` : "";
|
|
71
|
-
this.exitError = new Error(
|
|
72
|
-
`codex app-server exited before completing the request (code ${code ?? "unknown"}, signal ${signal ?? "none"}).${suffix}`,
|
|
73
|
-
);
|
|
74
|
-
this.rejectAll(this.exitError);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
child.stderr.setEncoding("utf8");
|
|
78
|
-
child.stderr.on("data", (chunk: string) => {
|
|
79
|
-
this.stderr = truncateEnd(this.stderr + chunk, MAX_ERROR_BODY_CHARS);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const lines = createInterface({ input: child.stdout });
|
|
83
|
-
lines.on("line", (line) => this.handleLine(line));
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
return this.startPromise;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
request(method: string, params: unknown): Promise<unknown> {
|
|
90
|
-
const child = this.child;
|
|
91
|
-
if (!child?.stdin.writable) {
|
|
92
|
-
throw new Error("codex app-server is not running.");
|
|
93
|
-
}
|
|
94
|
-
if (this.exitError) throw this.exitError;
|
|
95
|
-
|
|
96
|
-
const id = this.nextId++;
|
|
97
|
-
const payload = params === undefined ? { method, id } : { method, id, params };
|
|
98
|
-
const response = new Promise<unknown>((resolve, reject) => {
|
|
99
|
-
const timeout = setTimeout(() => {
|
|
100
|
-
this.pending.delete(id);
|
|
101
|
-
reject(new Error(`Timed out after ${Math.round(this.timeoutMs / 1000)}s waiting for ${method}.`));
|
|
102
|
-
}, this.timeoutMs);
|
|
103
|
-
|
|
104
|
-
this.pending.set(id, {
|
|
105
|
-
resolve: (value) => {
|
|
106
|
-
clearTimeout(timeout);
|
|
107
|
-
resolve(value);
|
|
108
|
-
},
|
|
109
|
-
reject: (error) => {
|
|
110
|
-
clearTimeout(timeout);
|
|
111
|
-
reject(error);
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
117
|
-
return response;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
notify(method: string): void {
|
|
121
|
-
const child = this.child;
|
|
122
|
-
if (!child?.stdin.writable) return;
|
|
123
|
-
child.stdin.write(`${JSON.stringify({ method })}\n`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
dispose(): void {
|
|
127
|
-
for (const [id, pending] of this.pending) {
|
|
128
|
-
pending.reject(new Error(`codex app-server request ${id} cancelled.`));
|
|
129
|
-
}
|
|
130
|
-
this.pending.clear();
|
|
131
|
-
|
|
132
|
-
const child = this.child;
|
|
133
|
-
if (!child) return;
|
|
134
|
-
child.stdin.end();
|
|
135
|
-
if (!child.killed) child.kill();
|
|
136
|
-
this.child = undefined;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private handleLine(line: string): void {
|
|
140
|
-
let parsed: RpcResponse;
|
|
141
|
-
try {
|
|
142
|
-
parsed = JSON.parse(line) as RpcResponse;
|
|
143
|
-
} catch {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (typeof parsed.id !== "number") return;
|
|
148
|
-
const pending = this.pending.get(parsed.id);
|
|
149
|
-
if (!pending) return;
|
|
150
|
-
this.pending.delete(parsed.id);
|
|
151
|
-
|
|
152
|
-
if (parsed.error) {
|
|
153
|
-
const message = typeof parsed.error.message === "string" ? parsed.error.message : "unknown error";
|
|
154
|
-
pending.reject(new Error(`codex app-server request failed: ${message}`));
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
pending.resolve(parsed.result);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private rejectAll(error: Error): void {
|
|
162
|
-
for (const pending of this.pending.values()) pending.reject(error);
|
|
163
|
-
this.pending.clear();
|
|
164
|
-
}
|
|
165
|
-
}
|
package/src/codex-auth.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { CODEX_OPENAI_BETA } from "./constants.js";
|
|
6
|
-
import { codexAuthCandidateModels } from "./models.js";
|
|
7
|
-
import { asString, hasHeader } from "./utils.js";
|
|
8
|
-
|
|
9
|
-
export async function resolvePiCodexAuth(
|
|
10
|
-
ctx: ExtensionContext,
|
|
11
|
-
): Promise<{ headers: Record<string, string> } | undefined> {
|
|
12
|
-
const models = codexAuthCandidateModels(ctx);
|
|
13
|
-
const errors: string[] = [];
|
|
14
|
-
|
|
15
|
-
for (const model of models) {
|
|
16
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
17
|
-
if (!auth.ok) {
|
|
18
|
-
errors.push(auth.error);
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const headers = { ...(auth.headers ?? {}) };
|
|
23
|
-
if (!hasHeader(headers, "Authorization") && auth.apiKey) {
|
|
24
|
-
headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
25
|
-
}
|
|
26
|
-
if (!hasHeader(headers, "User-Agent")) {
|
|
27
|
-
headers["User-Agent"] = "pi-codex-usage";
|
|
28
|
-
}
|
|
29
|
-
if (hasHeader(headers, "Authorization")) {
|
|
30
|
-
return { headers };
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (errors.length > 0) {
|
|
35
|
-
throw new Error(errors.join("; "));
|
|
36
|
-
}
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function resolvePiCodexWhamAuth(
|
|
41
|
-
ctx: ExtensionContext,
|
|
42
|
-
): Promise<{ headers: Record<string, string> } | undefined> {
|
|
43
|
-
const auth = await resolvePiCodexAuth(ctx);
|
|
44
|
-
if (!auth) return undefined;
|
|
45
|
-
const headers = { ...auth.headers };
|
|
46
|
-
if (!hasHeader(headers, "Accept")) headers.Accept = "application/json";
|
|
47
|
-
if (!hasHeader(headers, "Content-Type")) headers["Content-Type"] = "application/json";
|
|
48
|
-
if (!hasHeader(headers, "OpenAI-Beta")) headers["OpenAI-Beta"] = CODEX_OPENAI_BETA;
|
|
49
|
-
if (!hasHeader(headers, "originator")) headers.originator = "Codex Desktop";
|
|
50
|
-
if (!hasHeader(headers, "ChatGPT-Account-ID")) {
|
|
51
|
-
const accountId = readCodexAccountIdFromAuthFile();
|
|
52
|
-
if (accountId) headers["ChatGPT-Account-ID"] = accountId;
|
|
53
|
-
}
|
|
54
|
-
return { headers };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function readCodexAccountIdFromAuthFile(): string | undefined {
|
|
58
|
-
try {
|
|
59
|
-
const raw = JSON.parse(readFileSync(join(homedir(), ".codex", "auth.json"), "utf8")) as {
|
|
60
|
-
tokens?: { account_id?: unknown; accountId?: unknown };
|
|
61
|
-
};
|
|
62
|
-
return asString(raw.tokens?.account_id) ?? asString(raw.tokens?.accountId);
|
|
63
|
-
} catch {
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
}
|
package/src/codex-query.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { queryViaCodexAppServer } from "./codex-app-server.js";
|
|
3
|
-
import { resolvePiCodexAuth } from "./codex-auth.js";
|
|
4
|
-
import { CODEX_USAGE_URL } from "./constants.js";
|
|
5
|
-
import { isStaleExtensionContextError, throwUsageEndpointError } from "./errors.js";
|
|
6
|
-
import { fetchWithTimeout } from "./http.js";
|
|
7
|
-
import { normalizeBackendPayload } from "./normalize-codex.js";
|
|
8
|
-
import type { CodexUsageReport, RateLimitStatusPayload, UsageQueryError, UsageReport } from "./types.js";
|
|
9
|
-
import { errorMessage, parseJsonObject } from "./utils.js";
|
|
10
|
-
|
|
11
|
-
export async function queryViaPiAuth(ctx: ExtensionContext, timeoutMs: number): Promise<CodexUsageReport> {
|
|
12
|
-
const auth = await resolvePiCodexAuth(ctx);
|
|
13
|
-
if (!auth) {
|
|
14
|
-
throw new Error(
|
|
15
|
-
"No Pi OpenAI Codex subscription auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).",
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const response = await fetchWithTimeout(CODEX_USAGE_URL, { headers: auth.headers }, timeoutMs);
|
|
20
|
-
const text = await response.text();
|
|
21
|
-
if (!response.ok) {
|
|
22
|
-
throwUsageEndpointError("Codex", response, text);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const payload = parseJsonObject(text, "Codex usage endpoint response");
|
|
26
|
-
return normalizeBackendPayload(payload as RateLimitStatusPayload, Date.now(), "pi-auth");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function queryCodexUsageWithFallback(
|
|
30
|
-
ctx: ExtensionContext,
|
|
31
|
-
timeoutMs: number,
|
|
32
|
-
): Promise<{ report?: UsageReport; errors: UsageQueryError[] }> {
|
|
33
|
-
try {
|
|
34
|
-
const report = await queryViaPiAuth(ctx, timeoutMs);
|
|
35
|
-
return { report, errors: [] };
|
|
36
|
-
} catch (cause) {
|
|
37
|
-
if (isStaleExtensionContextError(cause)) throw cause;
|
|
38
|
-
const errors: UsageQueryError[] = [{ source: "pi-auth", message: errorMessage(cause), cause }];
|
|
39
|
-
try {
|
|
40
|
-
const report = await queryViaCodexAppServer(timeoutMs);
|
|
41
|
-
return { report, errors };
|
|
42
|
-
} catch (fallbackCause) {
|
|
43
|
-
if (isStaleExtensionContextError(fallbackCause)) throw fallbackCause;
|
|
44
|
-
errors.push({
|
|
45
|
-
source: "codex-app-server",
|
|
46
|
-
message: errorMessage(fallbackCause),
|
|
47
|
-
cause: fallbackCause,
|
|
48
|
-
});
|
|
49
|
-
return { errors };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|