@jameslovespancakes/pi-plus 1.0.0
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/LICENSE +21 -0
- package/README.md +190 -0
- package/config/pi-plus.example.json +60 -0
- package/config/skills/model-routing/SKILL.md +86 -0
- package/images/board_demo.png +0 -0
- package/images/pi-plus.svg +10 -0
- package/images/pi-plus_demo.png +0 -0
- package/images/provider_demo.png +0 -0
- package/images/remote_demo.png +0 -0
- package/images/usage_demo.png +0 -0
- package/package.json +67 -0
- package/server/board-server.mjs +641 -0
- package/server/package.json +17 -0
- package/src/core/accounts/registry.ts +93 -0
- package/src/core/anthropic/client-identity.ts +241 -0
- package/src/core/anthropic/models.ts +69 -0
- package/src/core/anthropic/oauth.ts +208 -0
- package/src/core/anthropic/quota.ts +253 -0
- package/src/core/anthropic/routing.ts +168 -0
- package/src/core/anthropic/store.ts +225 -0
- package/src/core/anthropic/vendor/README.md +36 -0
- package/src/core/anthropic/vendor/xxhash-wasm.LICENSE.md +25 -0
- package/src/core/anthropic/vendor/xxhash-wasm.js +2 -0
- package/src/core/anthropic/xxhash64.ts +33 -0
- package/src/core/catalog/quality.ts +314 -0
- package/src/core/codex/oauth.ts +129 -0
- package/src/core/codex/quota.ts +88 -0
- package/src/core/codex/store.ts +97 -0
- package/src/core/config.ts +169 -0
- package/src/core/env.ts +58 -0
- package/src/core/exec/process.ts +146 -0
- package/src/core/exec/ssh-config.ts +157 -0
- package/src/core/oauth/pkce.ts +88 -0
- package/src/core/policy/policy.ts +183 -0
- package/src/core/quota/pool.ts +64 -0
- package/src/core/quota/usage-source.ts +289 -0
- package/src/core/store.ts +43 -0
- package/src/domains/agents/board-setup.ts +409 -0
- package/src/domains/agents/index.ts +462 -0
- package/src/domains/models/catalog-tool.ts +361 -0
- package/src/domains/models/index.ts +14 -0
- package/src/domains/models/policy-gate.ts +169 -0
- package/src/domains/models/provider-picker.ts +208 -0
- package/src/domains/remote/config-path.ts +41 -0
- package/src/domains/remote/index.ts +866 -0
- package/src/domains/remote/setup.ts +425 -0
- package/src/domains/setup/index.ts +220 -0
- package/src/domains/subscriptions/accounts-picker.ts +178 -0
- package/src/domains/subscriptions/accounts.ts +242 -0
- package/src/domains/subscriptions/footer.ts +182 -0
- package/src/domains/subscriptions/index.ts +42 -0
- package/src/domains/subscriptions/provider.ts +219 -0
- package/src/domains/subscriptions/providers/anthropic.ts +149 -0
- package/src/domains/subscriptions/providers/codex.ts +148 -0
- package/src/domains/subscriptions/routing.ts +72 -0
- package/src/services/usage-service.ts +186 -0
- package/src/ui/format.ts +73 -0
- package/src/ui/usage-bars.ts +154 -0
- package/src/vendor/anthropic.ts +109 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
=====================
|
|
3
|
+
|
|
4
|
+
Copyright © 2017 Michael Jungo
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person
|
|
7
|
+
obtaining a copy of this software and associated documentation
|
|
8
|
+
files (the “Software”), to deal in the Software without
|
|
9
|
+
restriction, including without limitation the rights to use,
|
|
10
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the
|
|
12
|
+
Software is furnished to do so, subject to the following
|
|
13
|
+
conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be
|
|
16
|
+
included in all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
20
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
22
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
23
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
24
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
25
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const t=new Uint8Array([0,97,115,109,1,0,0,0,1,48,8,96,3,127,127,127,1,127,96,3,127,127,127,0,96,2,127,127,0,96,1,127,1,127,96,3,127,127,126,1,126,96,3,126,127,127,1,126,96,2,127,126,0,96,1,127,1,126,3,11,10,0,0,2,1,3,4,5,6,1,7,5,3,1,0,1,7,85,9,3,109,101,109,2,0,5,120,120,104,51,50,0,0,6,105,110,105,116,51,50,0,2,8,117,112,100,97,116,101,51,50,0,3,8,100,105,103,101,115,116,51,50,0,4,5,120,120,104,54,52,0,5,6,105,110,105,116,54,52,0,7,8,117,112,100,97,116,101,54,52,0,8,8,100,105,103,101,115,116,54,52,0,9,10,251,22,10,242,1,1,4,127,32,0,32,1,106,33,3,32,1,65,16,79,4,127,32,3,65,16,107,33,6,32,2,65,168,136,141,161,2,106,33,3,32,2,65,137,235,208,208,7,107,33,4,32,2,65,207,140,162,142,6,106,33,5,3,64,32,3,32,0,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,33,3,32,4,32,0,65,4,106,34,0,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,33,4,32,2,32,0,65,4,106,34,0,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,33,2,32,5,32,0,65,4,106,34,0,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,33,5,32,6,32,0,65,4,106,34,0,79,13,0,11,32,2,65,12,119,32,5,65,18,119,106,32,4,65,7,119,106,32,3,65,1,119,106,5,32,2,65,177,207,217,178,1,106,11,32,1,106,32,0,32,1,65,15,113,16,1,11,146,1,0,32,1,32,2,106,33,2,3,64,32,1,65,4,106,32,2,75,69,4,64,32,0,32,1,40,2,0,65,189,220,202,149,124,108,106,65,17,119,65,175,214,211,190,2,108,33,0,32,1,65,4,106,33,1,12,1,11,11,3,64,32,1,32,2,79,69,4,64,32,0,32,1,45,0,0,65,177,207,217,178,1,108,106,65,11,119,65,177,243,221,241,121,108,33,0,32,1,65,1,106,33,1,12,1,11,11,32,0,32,0,65,15,118,115,65,247,148,175,175,120,108,34,0,65,13,118,32,0,115,65,189,220,202,149,124,108,34,0,65,16,118,32,0,115,11,63,0,32,0,65,8,106,32,1,65,168,136,141,161,2,106,54,2,0,32,0,65,12,106,32,1,65,137,235,208,208,7,107,54,2,0,32,0,65,16,106,32,1,54,2,0,32,0,65,20,106,32,1,65,207,140,162,142,6,106,54,2,0,11,195,4,1,6,127,32,1,32,2,106,33,6,32,0,65,24,106,33,4,32,0,65,40,106,40,2,0,33,3,32,0,32,0,40,2,0,32,2,106,54,2,0,32,0,65,4,106,34,5,32,5,40,2,0,32,2,65,16,79,32,0,40,2,0,65,16,79,114,114,54,2,0,32,2,32,3,106,65,16,73,4,64,32,3,32,4,106,32,1,32,2,252,10,0,0,32,0,65,40,106,32,2,32,3,106,54,2,0,15,11,32,3,4,64,32,3,32,4,106,32,1,65,16,32,3,107,34,2,252,10,0,0,32,0,65,8,106,34,3,32,3,40,2,0,32,4,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,54,2,0,32,0,65,12,106,34,3,32,3,40,2,0,32,4,65,4,106,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,54,2,0,32,0,65,16,106,34,3,32,3,40,2,0,32,4,65,8,106,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,54,2,0,32,0,65,20,106,34,3,32,3,40,2,0,32,4,65,12,106,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,54,2,0,32,0,65,40,106,65,0,54,2,0,32,1,32,2,106,33,1,11,32,1,32,6,65,16,107,77,4,64,32,6,65,16,107,33,8,32,0,65,8,106,40,2,0,33,2,32,0,65,12,106,40,2,0,33,3,32,0,65,16,106,40,2,0,33,5,32,0,65,20,106,40,2,0,33,7,3,64,32,2,32,1,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,33,2,32,3,32,1,65,4,106,34,1,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,33,3,32,5,32,1,65,4,106,34,1,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,33,5,32,7,32,1,65,4,106,34,1,40,2,0,65,247,148,175,175,120,108,106,65,13,119,65,177,243,221,241,121,108,33,7,32,8,32,1,65,4,106,34,1,79,13,0,11,32,0,65,8,106,32,2,54,2,0,32,0,65,12,106,32,3,54,2,0,32,0,65,16,106,32,5,54,2,0,32,0,65,20,106,32,7,54,2,0,11,32,1,32,6,73,4,64,32,4,32,1,32,6,32,1,107,34,1,252,10,0,0,32,0,65,40,106,32,1,54,2,0,11,11,97,1,1,127,32,0,65,16,106,40,2,0,33,1,32,0,65,4,106,40,2,0,4,127,32,1,65,12,119,32,0,65,20,106,40,2,0,65,18,119,106,32,0,65,12,106,40,2,0,65,7,119,106,32,0,65,8,106,40,2,0,65,1,119,106,5,32,1,65,177,207,217,178,1,106,11,32,0,40,2,0,106,32,0,65,24,106,32,0,65,40,106,40,2,0,16,1,11,255,3,2,3,126,1,127,32,0,32,1,106,33,6,32,1,65,32,79,4,126,32,6,65,32,107,33,6,32,2,66,214,235,130,238,234,253,137,245,224,0,124,33,3,32,2,66,177,169,172,193,173,184,212,166,61,125,33,4,32,2,66,249,234,208,208,231,201,161,228,225,0,124,33,5,3,64,32,3,32,0,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,33,3,32,4,32,0,65,8,106,34,0,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,33,4,32,2,32,0,65,8,106,34,0,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,33,2,32,5,32,0,65,8,106,34,0,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,33,5,32,6,32,0,65,8,106,34,0,79,13,0,11,32,2,66,12,137,32,5,66,18,137,124,32,4,66,7,137,124,32,3,66,1,137,124,32,3,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,133,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,32,4,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,133,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,32,2,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,133,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,32,5,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,133,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,5,32,2,66,197,207,217,178,241,229,186,234,39,124,11,32,1,173,124,32,0,32,1,65,31,113,16,6,11,134,2,0,32,1,32,2,106,33,2,3,64,32,2,32,1,65,8,106,79,4,64,32,1,41,3,0,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,32,0,133,66,27,137,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,33,0,32,1,65,8,106,33,1,12,1,11,11,32,1,65,4,106,32,2,77,4,64,32,0,32,1,53,2,0,66,135,149,175,175,152,182,222,155,158,127,126,133,66,23,137,66,207,214,211,190,210,199,171,217,66,126,66,249,243,221,241,153,246,153,171,22,124,33,0,32,1,65,4,106,33,1,11,3,64,32,1,32,2,73,4,64,32,0,32,1,49,0,0,66,197,207,217,178,241,229,186,234,39,126,133,66,11,137,66,135,149,175,175,152,182,222,155,158,127,126,33,0,32,1,65,1,106,33,1,12,1,11,11,32,0,32,0,66,33,136,133,66,207,214,211,190,210,199,171,217,66,126,34,0,32,0,66,29,136,133,66,249,243,221,241,153,246,153,171,22,126,34,0,32,0,66,32,136,133,11,77,0,32,0,65,8,106,32,1,66,214,235,130,238,234,253,137,245,224,0,124,55,3,0,32,0,65,16,106,32,1,66,177,169,172,193,173,184,212,166,61,125,55,3,0,32,0,65,24,106,32,1,55,3,0,32,0,65,32,106,32,1,66,249,234,208,208,231,201,161,228,225,0,124,55,3,0,11,244,4,2,3,127,4,126,32,1,32,2,106,33,5,32,0,65,40,106,33,4,32,0,65,200,0,106,40,2,0,33,3,32,0,32,0,41,3,0,32,2,173,124,55,3,0,32,2,32,3,106,65,32,73,4,64,32,3,32,4,106,32,1,32,2,252,10,0,0,32,0,65,200,0,106,32,2,32,3,106,54,2,0,15,11,32,3,4,64,32,3,32,4,106,32,1,65,32,32,3,107,34,2,252,10,0,0,32,0,65,8,106,34,3,32,3,41,3,0,32,4,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,55,3,0,32,0,65,16,106,34,3,32,3,41,3,0,32,4,65,8,106,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,55,3,0,32,0,65,24,106,34,3,32,3,41,3,0,32,4,65,16,106,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,55,3,0,32,0,65,32,106,34,3,32,3,41,3,0,32,4,65,24,106,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,55,3,0,32,0,65,200,0,106,65,0,54,2,0,32,1,32,2,106,33,1,11,32,1,65,32,106,32,5,77,4,64,32,5,65,32,107,33,2,32,0,65,8,106,41,3,0,33,6,32,0,65,16,106,41,3,0,33,7,32,0,65,24,106,41,3,0,33,8,32,0,65,32,106,41,3,0,33,9,3,64,32,6,32,1,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,33,6,32,7,32,1,65,8,106,34,1,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,33,7,32,8,32,1,65,8,106,34,1,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,33,8,32,9,32,1,65,8,106,34,1,41,3,0,66,207,214,211,190,210,199,171,217,66,126,124,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,33,9,32,2,32,1,65,8,106,34,1,79,13,0,11,32,0,65,8,106,32,6,55,3,0,32,0,65,16,106,32,7,55,3,0,32,0,65,24,106,32,8,55,3,0,32,0,65,32,106,32,9,55,3,0,11,32,1,32,5,73,4,64,32,4,32,1,32,5,32,1,107,34,1,252,10,0,0,32,0,65,200,0,106,32,1,54,2,0,11,11,188,2,1,5,126,32,0,65,24,106,41,3,0,33,1,32,0,41,3,0,34,2,66,32,90,4,126,32,0,65,8,106,41,3,0,34,3,66,1,137,32,0,65,16,106,41,3,0,34,4,66,7,137,124,32,1,66,12,137,32,0,65,32,106,41,3,0,34,5,66,18,137,124,124,32,3,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,133,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,32,4,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,133,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,32,1,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,133,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,32,5,66,207,214,211,190,210,199,171,217,66,126,66,31,137,66,135,149,175,175,152,182,222,155,158,127,126,133,66,135,149,175,175,152,182,222,155,158,127,126,66,157,163,181,234,131,177,141,138,250,0,125,5,32,1,66,197,207,217,178,241,229,186,234,39,124,11,32,2,124,32,0,65,40,106,32,2,66,31,131,167,16,6,11]);async function e(){return function(t){const{exports:{mem:e,xxh32:n,xxh64:r,init32:i,update32:a,digest32:o,init64:s,update64:u,digest64:c}}=t;let h=new Uint8Array(e.buffer);function g(t,n){if(e.buffer.byteLength<t+n){const r=Math.ceil((t+n-e.buffer.byteLength)/65536);e.grow(r),h=new Uint8Array(e.buffer)}}function f(t,e,n,r,i,a){g(t);const o=new Uint8Array(t);return h.set(o),n(0,e),o.set(h.subarray(0,t)),{update(e){let n;return h.set(o),"string"==typeof e?(g(3*e.length,t),n=w.encodeInto(e,h.subarray(t)).written):(g(e.byteLength,t),h.set(e,t),n=e.byteLength),r(0,t,n),o.set(h.subarray(0,t)),this},digest:()=>(h.set(o),a(i(0)))}}function y(t){return t>>>0}const b=2n**64n-1n;function d(t){return t&b}const w=new TextEncoder,l=0,p=0n;function x(t,e=l){return g(3*t.length,0),y(n(0,w.encodeInto(t,h).written,e))}function L(t,e=p){return g(3*t.length,0),d(r(0,w.encodeInto(t,h).written,e))}return{h32:x,h32ToString:(t,e=l)=>x(t,e).toString(16).padStart(8,"0"),h32Raw:(t,e=l)=>(g(t.byteLength,0),h.set(t),y(n(0,t.byteLength,e))),create32:(t=l)=>f(48,t,i,a,o,y),h64:L,h64ToString:(t,e=p)=>L(t,e).toString(16).padStart(16,"0"),h64Raw:(t,e=p)=>(g(t.byteLength,0),h.set(t),d(r(0,t.byteLength,e))),create64:(t=p)=>f(88,t,s,u,c,d)}}((await WebAssembly.instantiate(t)).instance)}export{e as default};
|
|
2
|
+
//# sourceMappingURL=xxhash-wasm.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import xxhashInit from "./vendor/xxhash-wasm.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* xxHash64, from the vendored reference implementation.
|
|
5
|
+
*
|
|
6
|
+
* A hand-written BigInt version was attempted first and abandoned: it was
|
|
7
|
+
* correct for empty and tiny inputs and wrong for every longer one, and four
|
|
8
|
+
* rounds of fixes never converged. See vendor/README.md.
|
|
9
|
+
*
|
|
10
|
+
* The WASM module is instantiated once and shared.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type RawHasher = (bytes: Uint8Array, seed?: bigint) => bigint;
|
|
14
|
+
|
|
15
|
+
let hasher: Promise<RawHasher> | undefined;
|
|
16
|
+
|
|
17
|
+
function load(): Promise<RawHasher> {
|
|
18
|
+
hasher ??= (async () => {
|
|
19
|
+
const instance = await (xxhashInit as unknown as () => Promise<{ h64Raw: RawHasher }>)();
|
|
20
|
+
return instance.h64Raw;
|
|
21
|
+
})();
|
|
22
|
+
return hasher;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 64-bit hash of `bytes` with an optional seed. */
|
|
26
|
+
export async function xxhash64(bytes: Uint8Array, seed = 0n): Promise<bigint> {
|
|
27
|
+
return (await load())(bytes, seed);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function xxhash64Hex(value: string): Promise<string> {
|
|
31
|
+
const hash = await xxhash64(new TextEncoder().encode(value));
|
|
32
|
+
return hash.toString(16).padStart(16, "0").slice(0, 16);
|
|
33
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { env } from "../env.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Complete Artificial Analysis model dataset, cached on disk.
|
|
8
|
+
*
|
|
9
|
+
* Every field the API returns is stored verbatim: all 17 benchmark
|
|
10
|
+
* evaluations, all pricing fields, and all latency/throughput measurements.
|
|
11
|
+
* Derived cost-efficiency ratios are computed on top. All network access
|
|
12
|
+
* degrades to the cache so routing never blocks on the network.
|
|
13
|
+
*
|
|
14
|
+
* Refresh semantics deliberately mirror pi's own remote model catalog
|
|
15
|
+
* (`dist/core/remote-catalog-provider.js`): restore from disk first, only go to
|
|
16
|
+
* the network when the entry is older than the refresh interval, and revalidate
|
|
17
|
+
* with `If-None-Match` so an unchanged dataset costs one 304 instead of a
|
|
18
|
+
* multi-megabyte download.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const AA_URL = "https://artificialanalysis.ai/api/v2/data/llms/models";
|
|
22
|
+
/** Matches pi's REMOTE_CATALOG_REFRESH_INTERVAL_MS. */
|
|
23
|
+
export const REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
24
|
+
const TIMEOUT_MS = 30_000;
|
|
25
|
+
|
|
26
|
+
export type Confidence = "measured" | "inferred" | "unrated";
|
|
27
|
+
|
|
28
|
+
/** Every evaluation key Artificial Analysis publishes, with display metadata. */
|
|
29
|
+
export const EVALUATIONS = [
|
|
30
|
+
{ key: "artificial_analysis_intelligence_index", label: "Intelligence Index", scale: "index", group: "composite" },
|
|
31
|
+
{ key: "artificial_analysis_coding_index", label: "Coding Index", scale: "index", group: "composite" },
|
|
32
|
+
{ key: "artificial_analysis_math_index", label: "Math Index", scale: "index", group: "composite" },
|
|
33
|
+
{ key: "terminalbench_hard", label: "Terminal-Bench Hard", scale: "fraction", group: "agentic" },
|
|
34
|
+
{ key: "terminalbench_v2_1", label: "Terminal-Bench 2.1", scale: "fraction", group: "agentic" },
|
|
35
|
+
{ key: "tau2", label: "τ²-bench (agentic tools)", scale: "fraction", group: "agentic" },
|
|
36
|
+
{ key: "tau_banking", label: "τ-bench Banking", scale: "fraction", group: "agentic" },
|
|
37
|
+
{ key: "livecodebench", label: "LiveCodeBench", scale: "fraction", group: "coding" },
|
|
38
|
+
{ key: "scicode", label: "SciCode", scale: "fraction", group: "coding" },
|
|
39
|
+
{ key: "lcr", label: "Long Context Reasoning", scale: "fraction", group: "reasoning" },
|
|
40
|
+
{ key: "gpqa", label: "GPQA Diamond", scale: "fraction", group: "reasoning" },
|
|
41
|
+
{ key: "hle", label: "Humanity's Last Exam", scale: "fraction", group: "reasoning" },
|
|
42
|
+
{ key: "mmlu_pro", label: "MMLU-Pro", scale: "fraction", group: "reasoning" },
|
|
43
|
+
{ key: "ifbench", label: "IFBench (instruction following)", scale: "fraction", group: "reasoning" },
|
|
44
|
+
{ key: "aime_25", label: "AIME 2025", scale: "fraction", group: "math" },
|
|
45
|
+
{ key: "aime", label: "AIME", scale: "fraction", group: "math" },
|
|
46
|
+
{ key: "math_500", label: "MATH-500", scale: "fraction", group: "math" },
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type EvaluationKey = (typeof EVALUATIONS)[number]["key"];
|
|
50
|
+
|
|
51
|
+
export interface QualityRecord {
|
|
52
|
+
slug: string;
|
|
53
|
+
name: string;
|
|
54
|
+
creator: string;
|
|
55
|
+
releaseDate?: string;
|
|
56
|
+
evaluations: Partial<Record<EvaluationKey, number>>;
|
|
57
|
+
pricing: {
|
|
58
|
+
inputPer1M?: number;
|
|
59
|
+
outputPer1M?: number;
|
|
60
|
+
blended3to1Per1M?: number;
|
|
61
|
+
};
|
|
62
|
+
performance: {
|
|
63
|
+
outputTokensPerSecond?: number;
|
|
64
|
+
timeToFirstTokenSeconds?: number;
|
|
65
|
+
timeToFirstAnswerTokenSeconds?: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Same shape as pi's ModelsStoreEntry, so the two caches can be reasoned about
|
|
71
|
+
* (and debugged) identically.
|
|
72
|
+
*/
|
|
73
|
+
interface QualityStore {
|
|
74
|
+
records: Record<string, QualityRecord>;
|
|
75
|
+
/** Unix timestamp of the last completed remote check. */
|
|
76
|
+
checkedAt?: number;
|
|
77
|
+
/** Unix timestamp from the remote dataset's Last-Modified header. */
|
|
78
|
+
lastModified?: number;
|
|
79
|
+
/** Opaque ETag validator, stored verbatim and echoed back as If-None-Match. */
|
|
80
|
+
etag?: string;
|
|
81
|
+
source: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let cache: QualityStore | undefined;
|
|
85
|
+
let inFlight: Promise<string[]> | undefined;
|
|
86
|
+
|
|
87
|
+
function agentDir(): string {
|
|
88
|
+
return process.env.PI_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function cachePath(): string {
|
|
92
|
+
return join(agentDir(), "model-quality.json");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readKey(): string | undefined {
|
|
96
|
+
return env("ARTIFICIAL_ANALYSIS_API_KEY");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `openrouter/z-ai/glm-5.3` -> `glm-5-3`, `openai-codex/gpt-5.6-luna` -> `gpt-5-6-luna`. */
|
|
100
|
+
export function normalizeSlug(modelId: string): string {
|
|
101
|
+
const bare = modelId.includes("/") ? modelId.slice(modelId.lastIndexOf("/") + 1) : modelId;
|
|
102
|
+
return bare.toLowerCase().replace(/[._]/g, "-").replace(/-latest$/, "").replace(/-\d{8}$/, "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function num(value: unknown): number | undefined {
|
|
106
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Fractions (0-1) are reported as percentages so every metric shares one scale. */
|
|
110
|
+
export function normalizedScore(key: EvaluationKey, value: number | undefined): number | undefined {
|
|
111
|
+
if (value === undefined) return undefined;
|
|
112
|
+
const definition = EVALUATIONS.find((entry) => entry.key === key);
|
|
113
|
+
return definition?.scale === "fraction" ? value * 100 : value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function loadCache(): void {
|
|
117
|
+
if (cache) return;
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(readFileSync(cachePath(), "utf8")) as QualityStore & { fetchedAt?: number };
|
|
120
|
+
if (!parsed?.records) return;
|
|
121
|
+
// Migrate the pre-store format, which only had `fetchedAt`.
|
|
122
|
+
cache = {
|
|
123
|
+
records: parsed.records,
|
|
124
|
+
checkedAt: parsed.checkedAt ?? parsed.fetchedAt,
|
|
125
|
+
lastModified: parsed.lastModified,
|
|
126
|
+
etag: parsed.etag,
|
|
127
|
+
source: parsed.source ?? "artificial-analysis",
|
|
128
|
+
};
|
|
129
|
+
} catch { /* first run */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function persist(next: QualityStore): void {
|
|
133
|
+
cache = next;
|
|
134
|
+
try {
|
|
135
|
+
writeFileSync(cachePath(), JSON.stringify(next), "utf8");
|
|
136
|
+
} catch { /* cache write is best effort */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function qualityAgeMs(): number | undefined {
|
|
140
|
+
loadCache();
|
|
141
|
+
return cache?.checkedAt !== undefined ? Date.now() - cache.checkedAt : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Cache metadata, for display by /model-info refresh. */
|
|
145
|
+
export function qualityStatus(): { checkedAt?: number; lastModified?: number; records: number; hasEtag: boolean } {
|
|
146
|
+
loadCache();
|
|
147
|
+
return {
|
|
148
|
+
checkedAt: cache?.checkedAt,
|
|
149
|
+
lastModified: cache?.lastModified,
|
|
150
|
+
records: Object.keys(cache?.records ?? {}).length,
|
|
151
|
+
hasEtag: !!cache?.etag,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function qualityRecordCount(): number {
|
|
156
|
+
loadCache();
|
|
157
|
+
return Object.keys(cache?.records ?? {}).length;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function refreshQuality(force = false): Promise<string[]> {
|
|
161
|
+
loadCache();
|
|
162
|
+
|
|
163
|
+
// Restored from disk and still inside the window: nothing to do.
|
|
164
|
+
const age = qualityAgeMs();
|
|
165
|
+
if (!force && age !== undefined && age < REFRESH_INTERVAL_MS) return [];
|
|
166
|
+
if (inFlight) return inFlight;
|
|
167
|
+
|
|
168
|
+
inFlight = (async () => {
|
|
169
|
+
const warnings: string[] = [];
|
|
170
|
+
const key = readKey();
|
|
171
|
+
if (!key) return ["artificial-analysis: no API key, run /model-info setup"];
|
|
172
|
+
|
|
173
|
+
const stored = cache;
|
|
174
|
+
// Only revalidate when a cached body backs the validator, so a 304 can never
|
|
175
|
+
// leave the dataset empty.
|
|
176
|
+
const validator = stored && Object.keys(stored.records).length > 0 ? stored.etag : undefined;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch(AA_URL, {
|
|
180
|
+
headers: {
|
|
181
|
+
"x-api-key": key,
|
|
182
|
+
...(validator ? { "if-none-match": validator } : {}),
|
|
183
|
+
},
|
|
184
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const checkedAt = Date.now();
|
|
188
|
+
|
|
189
|
+
if (response.status === 304 && stored) {
|
|
190
|
+
// Unchanged upstream: keep the dataset and the validator, stamp the check.
|
|
191
|
+
persist({ ...stored, checkedAt });
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
// Drop the etag so the next attempt re-downloads rather than revalidating
|
|
197
|
+
// against a validator we can no longer trust.
|
|
198
|
+
if (stored) persist({ ...stored, checkedAt, etag: undefined });
|
|
199
|
+
return [`artificial-analysis: HTTP ${response.status}`];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const body = await response.json() as { data?: any[] };
|
|
203
|
+
const records: Record<string, QualityRecord> = {};
|
|
204
|
+
|
|
205
|
+
for (const model of body.data ?? []) {
|
|
206
|
+
const slug = String(model.slug ?? "").toLowerCase();
|
|
207
|
+
if (!slug) continue;
|
|
208
|
+
const source = model.evaluations ?? {};
|
|
209
|
+
const evaluations: Partial<Record<EvaluationKey, number>> = {};
|
|
210
|
+
for (const { key: evaluationKey } of EVALUATIONS) {
|
|
211
|
+
const value = num(source[evaluationKey]);
|
|
212
|
+
if (value !== undefined) evaluations[evaluationKey] = value;
|
|
213
|
+
}
|
|
214
|
+
const pricing = model.pricing ?? {};
|
|
215
|
+
records[slug] = {
|
|
216
|
+
slug,
|
|
217
|
+
name: String(model.name ?? slug),
|
|
218
|
+
creator: String(model.model_creator?.name ?? "unknown"),
|
|
219
|
+
releaseDate: typeof model.release_date === "string" ? model.release_date : undefined,
|
|
220
|
+
evaluations,
|
|
221
|
+
pricing: {
|
|
222
|
+
inputPer1M: num(pricing.price_1m_input_tokens),
|
|
223
|
+
outputPer1M: num(pricing.price_1m_output_tokens),
|
|
224
|
+
blended3to1Per1M: num(pricing.price_1m_blended_3_to_1),
|
|
225
|
+
},
|
|
226
|
+
performance: {
|
|
227
|
+
outputTokensPerSecond: num(model.median_output_tokens_per_second),
|
|
228
|
+
timeToFirstTokenSeconds: num(model.median_time_to_first_token_seconds),
|
|
229
|
+
timeToFirstAnswerTokenSeconds: num(model.median_time_to_first_answer_token),
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (Object.keys(records).length === 0) {
|
|
235
|
+
if (stored) persist({ ...stored, checkedAt, etag: undefined });
|
|
236
|
+
return ["artificial-analysis: empty dataset"];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const lastModifiedHeader = response.headers.get("last-modified");
|
|
240
|
+
const lastModified = lastModifiedHeader ? Date.parse(lastModifiedHeader) : undefined;
|
|
241
|
+
persist({
|
|
242
|
+
records,
|
|
243
|
+
checkedAt,
|
|
244
|
+
lastModified: Number.isNaN(lastModified) ? undefined : lastModified,
|
|
245
|
+
etag: response.headers.get("etag") ?? undefined,
|
|
246
|
+
source: "artificial-analysis",
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// Network failure keeps the old dataset usable but forces a full re-fetch
|
|
250
|
+
// next time rather than trusting a stale validator.
|
|
251
|
+
if (cache) persist({ ...cache, checkedAt: Date.now(), etag: undefined });
|
|
252
|
+
warnings.push(`artificial-analysis: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
|
+
}
|
|
254
|
+
return warnings;
|
|
255
|
+
})().finally(() => { inFlight = undefined; });
|
|
256
|
+
|
|
257
|
+
return inFlight;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface CostEfficiency {
|
|
261
|
+
/** Intelligence index per dollar of blended 3:1 spend. */
|
|
262
|
+
intelligencePerDollar?: number;
|
|
263
|
+
codingPerDollar?: number;
|
|
264
|
+
agenticPerDollar?: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function costEfficiency(record: QualityRecord | undefined): CostEfficiency {
|
|
268
|
+
if (!record) return {};
|
|
269
|
+
const price = record.pricing.blended3to1Per1M;
|
|
270
|
+
if (price === undefined || price <= 0) return {};
|
|
271
|
+
const ratio = (value: number | undefined) => (value === undefined ? undefined : Number((value / price).toFixed(2)));
|
|
272
|
+
return {
|
|
273
|
+
intelligencePerDollar: ratio(record.evaluations.artificial_analysis_intelligence_index),
|
|
274
|
+
codingPerDollar: ratio(record.evaluations.artificial_analysis_coding_index),
|
|
275
|
+
agenticPerDollar: ratio(normalizedScore("terminalbench_hard", record.evaluations.terminalbench_hard)),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface QualityLookup {
|
|
280
|
+
record?: QualityRecord;
|
|
281
|
+
efficiency: CostEfficiency;
|
|
282
|
+
confidence: Confidence;
|
|
283
|
+
basis: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Resolve a pi model id, preferring the entry matching the requested thinking
|
|
288
|
+
* level, then the base model, then the closest same-family sibling.
|
|
289
|
+
*/
|
|
290
|
+
export function qualityFor(modelId: string, thinkingLevel?: string): QualityLookup {
|
|
291
|
+
loadCache();
|
|
292
|
+
const records = cache?.records ?? {};
|
|
293
|
+
const base = normalizeSlug(modelId);
|
|
294
|
+
|
|
295
|
+
const levelled = thinkingLevel && thinkingLevel !== "off" ? records[`${base}-${thinkingLevel}`] : undefined;
|
|
296
|
+
if (levelled) return { record: levelled, efficiency: costEfficiency(levelled), confidence: "measured", basis: levelled.slug };
|
|
297
|
+
|
|
298
|
+
const exact = records[base];
|
|
299
|
+
if (exact) return { record: exact, efficiency: costEfficiency(exact), confidence: "measured", basis: exact.slug };
|
|
300
|
+
|
|
301
|
+
let best: QualityRecord | undefined;
|
|
302
|
+
for (const record of Object.values(records)) {
|
|
303
|
+
if (!base.includes(record.slug) && !record.slug.includes(base)) continue;
|
|
304
|
+
if (!best || record.slug.length > best.slug.length) best = record;
|
|
305
|
+
}
|
|
306
|
+
if (best) return { record: best, efficiency: costEfficiency(best), confidence: "inferred", basis: `nearest match ${best.slug}` };
|
|
307
|
+
|
|
308
|
+
return { efficiency: {}, confidence: "unrated", basis: "no Artificial Analysis entry" };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function allRecords(): QualityRecord[] {
|
|
312
|
+
loadCache();
|
|
313
|
+
return Object.values(cache?.records ?? {});
|
|
314
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { generatePkce, generateState, parseCallback } from "../oauth/pkce.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Codex OAuth (ChatGPT account login).
|
|
5
|
+
*
|
|
6
|
+
* The client id is the public one the Codex CLI itself uses; it is not a
|
|
7
|
+
* secret and the flow is PKCE precisely so that no secret is required. The
|
|
8
|
+
* redirect is a loopback URL, which is what lets a CLI complete the flow.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
12
|
+
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
13
|
+
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
14
|
+
const REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
15
|
+
const SCOPE = "openid profile email offline_access";
|
|
16
|
+
|
|
17
|
+
export interface CodexAuthStart {
|
|
18
|
+
url: string;
|
|
19
|
+
verifier: string;
|
|
20
|
+
state: string;
|
|
21
|
+
redirectUri: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CodexTokens {
|
|
25
|
+
access: string;
|
|
26
|
+
refresh: string;
|
|
27
|
+
expires: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Builds the authorization URL and the PKCE material needed to finish. */
|
|
31
|
+
export async function authorizeCodex(): Promise<CodexAuthStart> {
|
|
32
|
+
const pkce = await generatePkce();
|
|
33
|
+
const state = generateState();
|
|
34
|
+
|
|
35
|
+
const url = new URL(AUTHORIZE_URL);
|
|
36
|
+
url.searchParams.set("response_type", "code");
|
|
37
|
+
url.searchParams.set("client_id", CLIENT_ID);
|
|
38
|
+
url.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
39
|
+
url.searchParams.set("scope", SCOPE);
|
|
40
|
+
url.searchParams.set("code_challenge", pkce.challenge);
|
|
41
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
42
|
+
url.searchParams.set("state", state);
|
|
43
|
+
// Forces the account chooser, which is the entire point when adding a
|
|
44
|
+
// SECOND account: without it the browser silently reuses the existing
|
|
45
|
+
// session and you get a duplicate of the account you already have.
|
|
46
|
+
url.searchParams.set("prompt", "login");
|
|
47
|
+
|
|
48
|
+
return { url: url.toString(), verifier: pkce.verifier, state, redirectUri: REDIRECT_URI };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Exchanges the callback URL (or a bare code) for tokens. */
|
|
52
|
+
export async function exchangeCodex(
|
|
53
|
+
callback: string,
|
|
54
|
+
verifier: string,
|
|
55
|
+
redirectUri: string,
|
|
56
|
+
expectedState?: string,
|
|
57
|
+
): Promise<CodexTokens> {
|
|
58
|
+
const parsed = parseCallback(callback);
|
|
59
|
+
const code = parsed?.code ?? callback.trim();
|
|
60
|
+
if (!code) throw new Error("No authorization code found in the callback.");
|
|
61
|
+
if (expectedState && parsed?.state && parsed.state !== expectedState) {
|
|
62
|
+
throw new Error("OAuth state mismatch; the callback does not belong to this login attempt.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const response = await fetch(TOKEN_URL, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
grant_type: "authorization_code",
|
|
70
|
+
client_id: CLIENT_ID,
|
|
71
|
+
code,
|
|
72
|
+
redirect_uri: redirectUri,
|
|
73
|
+
code_verifier: verifier,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`Codex token exchange failed (${response.status}): ${(await response.text()).slice(0, 200)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const body: any = await response.json();
|
|
82
|
+
if (!body?.access_token) throw new Error("Codex token exchange returned no access token.");
|
|
83
|
+
return {
|
|
84
|
+
access: body.access_token,
|
|
85
|
+
refresh: body.refresh_token,
|
|
86
|
+
expires: Date.now() + Number(body.expires_in ?? 3600) * 1000,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Refreshes an expiring token. */
|
|
91
|
+
export async function refreshCodexToken(refresh: string): Promise<CodexTokens> {
|
|
92
|
+
const response = await fetch(TOKEN_URL, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "content-type": "application/json" },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
grant_type: "refresh_token",
|
|
97
|
+
client_id: CLIENT_ID,
|
|
98
|
+
refresh_token: refresh,
|
|
99
|
+
scope: SCOPE,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw new Error(`Codex token refresh failed (${response.status}).`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const body: any = await response.json();
|
|
108
|
+
if (!body?.access_token) throw new Error("Codex token refresh returned no access token.");
|
|
109
|
+
return {
|
|
110
|
+
// OpenAI may omit a rotated refresh token; keeping the old one is correct.
|
|
111
|
+
access: body.access_token,
|
|
112
|
+
refresh: body.refresh_token ?? refresh,
|
|
113
|
+
expires: Date.now() + Number(body.expires_in ?? 3600) * 1000,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A token good for at least a minute, refreshing and persisting if needed. */
|
|
118
|
+
export async function ensureCodexToken(
|
|
119
|
+
account: { access?: string; refresh?: string; expires?: number },
|
|
120
|
+
persist: (tokens: CodexTokens) => void,
|
|
121
|
+
): Promise<string | undefined> {
|
|
122
|
+
if (account.access && typeof account.expires === "number" && Date.now() + 60_000 < account.expires) {
|
|
123
|
+
return account.access;
|
|
124
|
+
}
|
|
125
|
+
if (!account.refresh) return account.access;
|
|
126
|
+
const tokens = await refreshCodexToken(account.refresh);
|
|
127
|
+
persist(tokens);
|
|
128
|
+
return tokens.access;
|
|
129
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { QuotaSnapshot } from "../anthropic/store.ts";
|
|
2
|
+
import { loadCodexAccounts, saveCodexAccount } from "./store.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Codex quota, read from response headers.
|
|
6
|
+
*
|
|
7
|
+
* Codex reports usage on every reply through `x-codex-*` headers, so there is
|
|
8
|
+
* no polling path here at all and no equivalent of Anthropic's usage endpoint
|
|
9
|
+
* is needed.
|
|
10
|
+
*
|
|
11
|
+
* Two differences from Anthropic worth remembering:
|
|
12
|
+
* - percentages are ALREADY 0-100 (`x-codex-primary-used-percent: 2`),
|
|
13
|
+
* unlike Anthropic's 0-1 fractions, so there is no x100 here;
|
|
14
|
+
* - the windows are self-describing via `*-window-minutes` rather than being
|
|
15
|
+
* fixed at 5h/7d, so they are mapped by duration instead of by name.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A Codex window lands in the snapshot slot closest to its real duration. */
|
|
19
|
+
const SEVEN_DAY_MINUTES = 7 * 24 * 60;
|
|
20
|
+
|
|
21
|
+
const num = (value: unknown): number | undefined => {
|
|
22
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
23
|
+
const n = Number(value);
|
|
24
|
+
return Number.isFinite(n) ? n : undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function parseCodexQuotaHeaders(
|
|
28
|
+
headers: Record<string, unknown> | undefined,
|
|
29
|
+
now = Date.now(),
|
|
30
|
+
): QuotaSnapshot | undefined {
|
|
31
|
+
if (!headers) return undefined;
|
|
32
|
+
|
|
33
|
+
const get = (name: string): unknown => {
|
|
34
|
+
const key = `x-codex-${name}`;
|
|
35
|
+
if (headers[key] !== undefined) return headers[key];
|
|
36
|
+
const found = Object.keys(headers).find((k) => k.toLowerCase() === key);
|
|
37
|
+
return found === undefined ? undefined : headers[found];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const window = (prefix: "primary" | "secondary") => {
|
|
41
|
+
const used = num(get(`${prefix}-used-percent`));
|
|
42
|
+
if (used === undefined) return undefined;
|
|
43
|
+
// A zero-length window is Codex's way of saying "not applicable".
|
|
44
|
+
const minutes = num(get(`${prefix}-window-minutes`));
|
|
45
|
+
if (minutes !== undefined && minutes <= 0) return undefined;
|
|
46
|
+
|
|
47
|
+
const clamped = Math.min(100, Math.max(0, used));
|
|
48
|
+
const resetAt = num(get(`${prefix}-reset-at`));
|
|
49
|
+
return {
|
|
50
|
+
usedPercent: clamped,
|
|
51
|
+
remainingPercent: 100 - clamped,
|
|
52
|
+
resetsAt: resetAt !== undefined && resetAt > 0
|
|
53
|
+
? new Date(resetAt * 1000).toISOString()
|
|
54
|
+
: undefined,
|
|
55
|
+
checkedAt: now,
|
|
56
|
+
windowMinutes: minutes,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const primary = window("primary");
|
|
61
|
+
const secondary = window("secondary");
|
|
62
|
+
if (!primary && !secondary) return undefined;
|
|
63
|
+
|
|
64
|
+
// Map by duration, not by name: the long window is the weekly one.
|
|
65
|
+
const primaryIsLong = (primary?.windowMinutes ?? SEVEN_DAY_MINUTES) >= SEVEN_DAY_MINUTES;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
seven_day: primaryIsLong ? primary : secondary,
|
|
69
|
+
five_hour: primaryIsLong ? secondary : primary,
|
|
70
|
+
checkedAt: now,
|
|
71
|
+
source: "headers",
|
|
72
|
+
plan: typeof get("plan-type") === "string" ? String(get("plan-type")) : undefined,
|
|
73
|
+
} as QuotaSnapshot;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Merges a header-derived snapshot into the stored account. */
|
|
77
|
+
export function applyCodexQuotaHeaders(
|
|
78
|
+
accountId: string,
|
|
79
|
+
headers: Record<string, unknown> | undefined,
|
|
80
|
+
now = Date.now(),
|
|
81
|
+
): boolean {
|
|
82
|
+
const quota = parseCodexQuotaHeaders(headers, now);
|
|
83
|
+
if (!quota) return false;
|
|
84
|
+
const account = loadCodexAccounts().accounts.find((a) => a.id === accountId);
|
|
85
|
+
if (!account) return false;
|
|
86
|
+
saveCodexAccount({ ...account, quota });
|
|
87
|
+
return true;
|
|
88
|
+
}
|