feat: add usage cost reporting
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
- Auto-reply: avoid splitting outbound chunks inside parentheses. (#499) — thanks @philipp-spiess
|
- Auto-reply: avoid splitting outbound chunks inside parentheses. (#499) — thanks @philipp-spiess
|
||||||
- Auto-reply: preserve spacing when stripping inline directives. (#539) — thanks @joshp123
|
- Auto-reply: preserve spacing when stripping inline directives. (#539) — thanks @joshp123
|
||||||
- Status: show provider prefix in /status model display. (#506) — thanks @mcinteerj
|
- Status: show provider prefix in /status model display. (#506) — thanks @mcinteerj
|
||||||
|
- Status: compact /status with session token usage + estimated cost, add `/cost` per-response usage lines (tokens-only for OAuth).
|
||||||
- macOS: package ClawdbotKit resources and Swift 6.2 compatibility dylib to avoid launch/tool crashes. (#473) — thanks @gupsammy
|
- macOS: package ClawdbotKit resources and Swift 6.2 compatibility dylib to avoid launch/tool crashes. (#473) — thanks @gupsammy
|
||||||
- WhatsApp: group `/model list` output by provider for scannability. (#456) - thanks @mcinteerj
|
- WhatsApp: group `/model list` output by provider for scannability. (#456) - thanks @mcinteerj
|
||||||
- Hooks: allow per-hook model overrides for webhook/Gmail runs (e.g. GPT 5 Mini).
|
- Hooks: allow per-hook model overrides for webhook/Gmail runs (e.g. GPT 5 Mini).
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -240,11 +240,12 @@ ClawdHub is a minimal skill registry. With ClawdHub enabled, the agent can searc
|
|||||||
|
|
||||||
Send these in WhatsApp/Telegram/Slack/WebChat (group commands are owner-only):
|
Send these in WhatsApp/Telegram/Slack/WebChat (group commands are owner-only):
|
||||||
|
|
||||||
- `/status` — health + session info (group shows activation mode)
|
- `/status` — compact session status (model + tokens, cost when available)
|
||||||
- `/new` or `/reset` — reset the session
|
- `/new` or `/reset` — reset the session
|
||||||
- `/compact` — compact session context (summary)
|
- `/compact` — compact session context (summary)
|
||||||
- `/think <level>` — off|minimal|low|medium|high
|
- `/think <level>` — off|minimal|low|medium|high
|
||||||
- `/verbose on|off`
|
- `/verbose on|off`
|
||||||
|
- `/cost on|off` — append per-response token/cost usage lines
|
||||||
- `/restart` — restart the gateway (owner-only in groups)
|
- `/restart` — restart the gateway (owner-only in groups)
|
||||||
- `/activation mention|always` — group activation toggle (groups only)
|
- `/activation mention|always` — group activation toggle (groups only)
|
||||||
|
|
||||||
@@ -460,10 +461,10 @@ Thanks to all clawtributors:
|
|||||||
<a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a>
|
<a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a>
|
||||||
<a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a>
|
<a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a>
|
||||||
<a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a>
|
<a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a>
|
||||||
<a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a>
|
<a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a>
|
||||||
<a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a>
|
<a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a>
|
||||||
<a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a>
|
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a>
|
||||||
<a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a>
|
<a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a>
|
||||||
<a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a>
|
<a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a>
|
||||||
<a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/search?q=Tobias%20Bischoff"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
<a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/search?q=Tobias%20Bischoff"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -664,19 +664,22 @@ public struct SessionsListParams: Codable, Sendable {
|
|||||||
public let includeglobal: Bool?
|
public let includeglobal: Bool?
|
||||||
public let includeunknown: Bool?
|
public let includeunknown: Bool?
|
||||||
public let spawnedby: String?
|
public let spawnedby: String?
|
||||||
|
public let agentid: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
limit: Int?,
|
limit: Int?,
|
||||||
activeminutes: Int?,
|
activeminutes: Int?,
|
||||||
includeglobal: Bool?,
|
includeglobal: Bool?,
|
||||||
includeunknown: Bool?,
|
includeunknown: Bool?,
|
||||||
spawnedby: String?
|
spawnedby: String?,
|
||||||
|
agentid: String?
|
||||||
) {
|
) {
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
self.activeminutes = activeminutes
|
self.activeminutes = activeminutes
|
||||||
self.includeglobal = includeglobal
|
self.includeglobal = includeglobal
|
||||||
self.includeunknown = includeunknown
|
self.includeunknown = includeunknown
|
||||||
self.spawnedby = spawnedby
|
self.spawnedby = spawnedby
|
||||||
|
self.agentid = agentid
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case limit
|
case limit
|
||||||
@@ -684,6 +687,7 @@ public struct SessionsListParams: Codable, Sendable {
|
|||||||
case includeglobal = "includeGlobal"
|
case includeglobal = "includeGlobal"
|
||||||
case includeunknown = "includeUnknown"
|
case includeunknown = "includeUnknown"
|
||||||
case spawnedby = "spawnedBy"
|
case spawnedby = "spawnedBy"
|
||||||
|
case agentid = "agentId"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,6 +696,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
public let thinkinglevel: AnyCodable?
|
public let thinkinglevel: AnyCodable?
|
||||||
public let verboselevel: AnyCodable?
|
public let verboselevel: AnyCodable?
|
||||||
public let reasoninglevel: AnyCodable?
|
public let reasoninglevel: AnyCodable?
|
||||||
|
public let responseusage: AnyCodable?
|
||||||
public let elevatedlevel: AnyCodable?
|
public let elevatedlevel: AnyCodable?
|
||||||
public let model: AnyCodable?
|
public let model: AnyCodable?
|
||||||
public let spawnedby: AnyCodable?
|
public let spawnedby: AnyCodable?
|
||||||
@@ -703,6 +708,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
thinkinglevel: AnyCodable?,
|
thinkinglevel: AnyCodable?,
|
||||||
verboselevel: AnyCodable?,
|
verboselevel: AnyCodable?,
|
||||||
reasoninglevel: AnyCodable?,
|
reasoninglevel: AnyCodable?,
|
||||||
|
responseusage: AnyCodable?,
|
||||||
elevatedlevel: AnyCodable?,
|
elevatedlevel: AnyCodable?,
|
||||||
model: AnyCodable?,
|
model: AnyCodable?,
|
||||||
spawnedby: AnyCodable?,
|
spawnedby: AnyCodable?,
|
||||||
@@ -713,6 +719,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
self.thinkinglevel = thinkinglevel
|
self.thinkinglevel = thinkinglevel
|
||||||
self.verboselevel = verboselevel
|
self.verboselevel = verboselevel
|
||||||
self.reasoninglevel = reasoninglevel
|
self.reasoninglevel = reasoninglevel
|
||||||
|
self.responseusage = responseusage
|
||||||
self.elevatedlevel = elevatedlevel
|
self.elevatedlevel = elevatedlevel
|
||||||
self.model = model
|
self.model = model
|
||||||
self.spawnedby = spawnedby
|
self.spawnedby = spawnedby
|
||||||
@@ -724,6 +731,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
case thinkinglevel = "thinkingLevel"
|
case thinkinglevel = "thinkingLevel"
|
||||||
case verboselevel = "verboseLevel"
|
case verboselevel = "verboseLevel"
|
||||||
case reasoninglevel = "reasoningLevel"
|
case reasoninglevel = "reasoningLevel"
|
||||||
|
case responseusage = "responseUsage"
|
||||||
case elevatedlevel = "elevatedLevel"
|
case elevatedlevel = "elevatedLevel"
|
||||||
case model
|
case model
|
||||||
case spawnedby = "spawnedBy"
|
case spawnedby = "spawnedBy"
|
||||||
@@ -1100,6 +1108,51 @@ public struct WebLoginWaitParams: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct AgentSummary: Codable, Sendable {
|
||||||
|
public let id: String
|
||||||
|
public let name: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
name: String?
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsListParams: Codable, Sendable {
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsListResult: Codable, Sendable {
|
||||||
|
public let defaultid: String
|
||||||
|
public let mainkey: String
|
||||||
|
public let scope: AnyCodable
|
||||||
|
public let agents: [AgentSummary]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
defaultid: String,
|
||||||
|
mainkey: String,
|
||||||
|
scope: AnyCodable,
|
||||||
|
agents: [AgentSummary]
|
||||||
|
) {
|
||||||
|
self.defaultid = defaultid
|
||||||
|
self.mainkey = mainkey
|
||||||
|
self.scope = scope
|
||||||
|
self.agents = agents
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case defaultid = "defaultId"
|
||||||
|
case mainkey = "mainKey"
|
||||||
|
case scope
|
||||||
|
case agents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct ModelChoice: Codable, Sendable {
|
public struct ModelChoice: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let name: String
|
public let name: String
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ read_when:
|
|||||||
- No estimated costs; only the provider-reported windows.
|
- No estimated costs; only the provider-reported windows.
|
||||||
|
|
||||||
## Where it shows up
|
## Where it shows up
|
||||||
- `/status` in chats: adds a short “Usage” line (only if available).
|
- `/status` in chats: compact one‑liner with session tokens + estimated cost (API key only) and provider quota windows when available.
|
||||||
|
- `/cost on|off` in chats: toggles per‑response usage lines (OAuth shows tokens only).
|
||||||
- CLI: `clawdbot status --usage` prints a full per-provider breakdown.
|
- CLI: `clawdbot status --usage` prints a full per-provider breakdown.
|
||||||
- CLI: `clawdbot providers list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
- CLI: `clawdbot providers list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
||||||
- macOS menu bar: “Usage” section under Context (only if available).
|
- macOS menu bar: “Usage” section under Context (only if available).
|
||||||
|
|||||||
@@ -556,6 +556,7 @@
|
|||||||
"concepts/agent",
|
"concepts/agent",
|
||||||
"concepts/agent-loop",
|
"concepts/agent-loop",
|
||||||
"concepts/system-prompt",
|
"concepts/system-prompt",
|
||||||
|
"token-use",
|
||||||
"concepts/oauth",
|
"concepts/oauth",
|
||||||
"concepts/agent-workspace",
|
"concepts/agent-workspace",
|
||||||
"concepts/multi-agent",
|
"concepts/multi-agent",
|
||||||
|
|||||||
72
docs/token-use.md
Normal file
72
docs/token-use.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
summary: "How Clawdbot builds prompt context and reports token usage + costs"
|
||||||
|
read_when:
|
||||||
|
- Explaining token usage, costs, or context windows
|
||||||
|
- Debugging context growth or compaction behavior
|
||||||
|
---
|
||||||
|
# Token use & costs
|
||||||
|
|
||||||
|
Clawdbot tracks **tokens**, not characters. Tokens are model-specific, but most
|
||||||
|
OpenAI-style models average ~4 characters per token for English text.
|
||||||
|
|
||||||
|
## How the system prompt is built
|
||||||
|
|
||||||
|
Clawdbot assembles its own system prompt on every run. It includes:
|
||||||
|
|
||||||
|
- Tool list + short descriptions
|
||||||
|
- Skills list (only metadata; instructions are loaded on demand with `read`)
|
||||||
|
- Self-update instructions
|
||||||
|
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new)
|
||||||
|
- Time (UTC + user timezone)
|
||||||
|
- Reply tags + heartbeat behavior
|
||||||
|
- Runtime metadata (host/OS/model/thinking)
|
||||||
|
|
||||||
|
See the full breakdown in [System Prompt](/concepts/system-prompt).
|
||||||
|
|
||||||
|
## What counts in the context window
|
||||||
|
|
||||||
|
Everything the model receives counts toward the context limit:
|
||||||
|
|
||||||
|
- System prompt (all sections listed above)
|
||||||
|
- Conversation history (user + assistant messages)
|
||||||
|
- Tool calls and tool results
|
||||||
|
- Attachments/transcripts (images, audio, files)
|
||||||
|
- Compaction summaries and pruning artifacts
|
||||||
|
- Provider wrappers or safety headers (not visible, but still counted)
|
||||||
|
|
||||||
|
## How to see current token usage
|
||||||
|
|
||||||
|
Use these in chat:
|
||||||
|
|
||||||
|
- `/status` → **compact one‑liner** with the session model, context usage,
|
||||||
|
last response input/output tokens, and **estimated cost** (API key only).
|
||||||
|
- `/cost on|off` → appends a **per-response usage line** to every reply.
|
||||||
|
- Persists per session (stored as `responseUsage`).
|
||||||
|
- OAuth auth **hides cost** (tokens only).
|
||||||
|
|
||||||
|
Other surfaces:
|
||||||
|
|
||||||
|
- **TUI/Web TUI:** `/status` + `/cost` are supported.
|
||||||
|
- **CLI:** `clawdbot status --usage` and `clawdbot providers list` show
|
||||||
|
provider quota windows (not per-response costs).
|
||||||
|
|
||||||
|
## Cost estimation (when shown)
|
||||||
|
|
||||||
|
Costs are estimated from your model pricing config:
|
||||||
|
|
||||||
|
```
|
||||||
|
models.providers.<provider>.models[].cost
|
||||||
|
```
|
||||||
|
|
||||||
|
These are **USD per 1M tokens** for `input`, `output`, `cacheRead`, and
|
||||||
|
`cacheWrite`. If pricing is missing, Clawdbot shows tokens only. OAuth tokens
|
||||||
|
never show dollar cost.
|
||||||
|
|
||||||
|
## Tips for reducing token pressure
|
||||||
|
|
||||||
|
- Use `/compact` to summarize long sessions.
|
||||||
|
- Trim large tool outputs in your workflows.
|
||||||
|
- Keep skill descriptions short (skill list is injected into the prompt).
|
||||||
|
- Prefer smaller models for verbose, exploratory work.
|
||||||
|
|
||||||
|
See [Skills](/tools/skills) for the exact skill list overhead formula.
|
||||||
@@ -163,6 +163,23 @@ This is **scoped to the agent run**, not a global shell environment.
|
|||||||
|
|
||||||
Clawdbot snapshots the eligible skills **when a session starts** and reuses that list for subsequent turns in the same session. Changes to skills or config take effect on the next new session.
|
Clawdbot snapshots the eligible skills **when a session starts** and reuses that list for subsequent turns in the same session. Changes to skills or config take effect on the next new session.
|
||||||
|
|
||||||
|
## Token impact (skills list)
|
||||||
|
|
||||||
|
When skills are eligible, Clawdbot injects a compact XML list of available skills into the system prompt (via `formatSkillsForPrompt` in `pi-coding-agent`). The cost is deterministic:
|
||||||
|
|
||||||
|
- **Base overhead (only when ≥1 skill):** 195 characters.
|
||||||
|
- **Per skill:** 97 characters + the length of the XML-escaped `<name>`, `<description>`, and `<location>` values.
|
||||||
|
|
||||||
|
Formula (characters):
|
||||||
|
|
||||||
|
```
|
||||||
|
total = 195 + Σ (97 + len(name_escaped) + len(description_escaped) + len(location_escaped))
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- XML escaping expands `& < > " '` into entities (`&`, `<`, etc.), increasing length.
|
||||||
|
- Token counts vary by model tokenizer. A rough OpenAI-style estimate is ~4 chars/token, so **97 chars ≈ 24 tokens** per skill plus your actual field lengths.
|
||||||
|
|
||||||
## Managed skills lifecycle
|
## Managed skills lifecycle
|
||||||
|
|
||||||
Clawdbot ships a baseline set of skills as **bundled skills** as part of the
|
Clawdbot ships a baseline set of skills as **bundled skills** as part of the
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ Directives (`/think`, `/verbose`, `/reasoning`, `/elevated`) are parsed even whe
|
|||||||
Text + native (when enabled):
|
Text + native (when enabled):
|
||||||
- `/help`
|
- `/help`
|
||||||
- `/status`
|
- `/status`
|
||||||
|
- `/cost on|off` (toggle per-response usage line)
|
||||||
- `/stop`
|
- `/stop`
|
||||||
- `/restart`
|
- `/restart`
|
||||||
- `/activation mention|always` (groups only)
|
- `/activation mention|always` (groups only)
|
||||||
@@ -52,6 +53,7 @@ Text-only:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
||||||
|
- `/cost` appends per-response token usage; it only shows dollar cost when the model uses an API key (OAuth hides cost).
|
||||||
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
|
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
|
||||||
- `/reasoning` (and `/verbose`) are risky in group settings: they may reveal internal reasoning or tool output you did not intend to expose. Prefer leaving them off, especially in group chats.
|
- `/reasoning` (and `/verbose`) are risky in group settings: they may reveal internal reasoning or tool output you did not intend to expose. Prefer leaving them off, especially in group chats.
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ Session controls:
|
|||||||
- `/think <off|minimal|low|medium|high>`
|
- `/think <off|minimal|low|medium|high>`
|
||||||
- `/verbose <on|off>`
|
- `/verbose <on|off>`
|
||||||
- `/reasoning <on|off|stream>`
|
- `/reasoning <on|off|stream>`
|
||||||
|
- `/cost <on|off>`
|
||||||
- `/elevated <on|off>` (alias: `/elev`)
|
- `/elevated <on|off>` (alias: `/elev`)
|
||||||
- `/activation <mention|always>`
|
- `/activation <mention|always>`
|
||||||
- `/deliver <on|off>`
|
- `/deliver <on|off>`
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
|
|||||||
- `/think <off|minimal|low|medium|high>`
|
- `/think <off|minimal|low|medium|high>`
|
||||||
- `/verbose <on|off>`
|
- `/verbose <on|off>`
|
||||||
- `/reasoning <on|off|stream>` (stream = Telegram draft only)
|
- `/reasoning <on|off|stream>` (stream = Telegram draft only)
|
||||||
|
- `/cost <on|off>`
|
||||||
- `/elevated <on|off>`
|
- `/elevated <on|off>`
|
||||||
- `/elev <on|off>`
|
- `/elev <on|off>`
|
||||||
- `/activation <mention|always>`
|
- `/activation <mention|always>`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"ensureLogins": [
|
"ensureLogins": [
|
||||||
"jdrhyne",
|
"jdrhyne",
|
||||||
|
"latitudeki5223",
|
||||||
"manmal"
|
"manmal"
|
||||||
],
|
],
|
||||||
"seedCommit": "d6863f87",
|
"seedCommit": "d6863f87",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export async function resolveApiKeyForProvider(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EnvApiKeyResult = { apiKey: string; source: string };
|
export type EnvApiKeyResult = { apiKey: string; source: string };
|
||||||
|
export type ModelAuthMode = "api-key" | "oauth" | "mixed" | "unknown";
|
||||||
|
|
||||||
export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
||||||
const applied = new Set(getShellEnvAppliedKeys());
|
const applied = new Set(getShellEnvAppliedKeys());
|
||||||
@@ -143,6 +144,37 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
return pick(envVar);
|
return pick(envVar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveModelAuthMode(
|
||||||
|
provider?: string,
|
||||||
|
cfg?: ClawdbotConfig,
|
||||||
|
store?: AuthProfileStore,
|
||||||
|
): ModelAuthMode | undefined {
|
||||||
|
const resolved = provider?.trim();
|
||||||
|
if (!resolved) return undefined;
|
||||||
|
|
||||||
|
const authStore = store ?? ensureAuthProfileStore();
|
||||||
|
const profiles = listProfilesForProvider(authStore, resolved);
|
||||||
|
if (profiles.length > 0) {
|
||||||
|
const modes = new Set(
|
||||||
|
profiles
|
||||||
|
.map((id) => authStore.profiles[id]?.type)
|
||||||
|
.filter((mode): mode is "api_key" | "oauth" => Boolean(mode)),
|
||||||
|
);
|
||||||
|
if (modes.has("oauth") && modes.has("api_key")) return "mixed";
|
||||||
|
if (modes.has("oauth")) return "oauth";
|
||||||
|
if (modes.has("api_key")) return "api-key";
|
||||||
|
}
|
||||||
|
|
||||||
|
const envKey = resolveEnvApiKey(resolved);
|
||||||
|
if (envKey?.apiKey) {
|
||||||
|
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getCustomProviderApiKey(cfg, resolved)) return "api-key";
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
export async function getApiKeyForModel(params: {
|
export async function getApiKeyForModel(params: {
|
||||||
model: Model<Api>;
|
model: Model<Api>;
|
||||||
cfg?: ClawdbotConfig;
|
cfg?: ClawdbotConfig;
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ const CHAT_COMMANDS: ChatCommandDefinition[] = [
|
|||||||
description: "Show current status.",
|
description: "Show current status.",
|
||||||
textAliases: ["/status"],
|
textAliases: ["/status"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "cost",
|
||||||
|
nativeName: "cost",
|
||||||
|
description: "Toggle per-response usage line.",
|
||||||
|
textAliases: ["/cost"],
|
||||||
|
acceptsArgs: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "stop",
|
key: "stop",
|
||||||
nativeName: "stop",
|
nativeName: "stop",
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ describe("trigger handling", () => {
|
|||||||
makeCfg(home),
|
makeCfg(home),
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("ClawdBot");
|
expect(text).toContain("status");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import crypto from "node:crypto";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { lookupContextTokens } from "../../agents/context.js";
|
import { lookupContextTokens } from "../../agents/context.js";
|
||||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||||
|
import { resolveModelAuthMode } from "../../agents/model-auth.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||||
import {
|
import {
|
||||||
queueEmbeddedPiMessage,
|
queueEmbeddedPiMessage,
|
||||||
runEmbeddedPiAgent,
|
runEmbeddedPiAgent,
|
||||||
} from "../../agents/pi-embedded.js";
|
} from "../../agents/pi-embedded.js";
|
||||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
import { hasNonzeroUsage, type NormalizedUsage } from "../../agents/usage.js";
|
||||||
import {
|
import {
|
||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
resolveSessionTranscriptPath,
|
resolveSessionTranscriptPath,
|
||||||
@@ -18,6 +19,12 @@ import type { TypingMode } from "../../config/types.js";
|
|||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import {
|
||||||
|
estimateUsageCost,
|
||||||
|
formatTokenCount,
|
||||||
|
formatUsd,
|
||||||
|
resolveModelCostConfig,
|
||||||
|
} from "../../utils/usage-format.js";
|
||||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||||
import type { OriginatingChannelType, TemplateContext } from "../templating.js";
|
import type { OriginatingChannelType, TemplateContext } from "../templating.js";
|
||||||
import { normalizeVerboseLevel, type VerboseLevel } from "../thinking.js";
|
import { normalizeVerboseLevel, type VerboseLevel } from "../thinking.js";
|
||||||
@@ -62,6 +69,65 @@ const formatBunFetchSocketError = (message: string) => {
|
|||||||
].join("\n");
|
].join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatResponseUsageLine = (params: {
|
||||||
|
usage?: NormalizedUsage;
|
||||||
|
showCost: boolean;
|
||||||
|
costConfig?: {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
};
|
||||||
|
}): string | null => {
|
||||||
|
const usage = params.usage;
|
||||||
|
if (!usage) return null;
|
||||||
|
const input = usage.input;
|
||||||
|
const output = usage.output;
|
||||||
|
if (typeof input !== "number" && typeof output !== "number") return null;
|
||||||
|
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
|
||||||
|
const outputLabel =
|
||||||
|
typeof output === "number" ? formatTokenCount(output) : "?";
|
||||||
|
const cost =
|
||||||
|
params.showCost && typeof input === "number" && typeof output === "number"
|
||||||
|
? estimateUsageCost({
|
||||||
|
usage: {
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
cacheRead: usage.cacheRead,
|
||||||
|
cacheWrite: usage.cacheWrite,
|
||||||
|
},
|
||||||
|
cost: params.costConfig,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const costLabel = params.showCost ? formatUsd(cost) : undefined;
|
||||||
|
const suffix = costLabel ? ` · est ${costLabel}` : "";
|
||||||
|
return `Usage: ${inputLabel} in / ${outputLabel} out${suffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendUsageLine = (
|
||||||
|
payloads: ReplyPayload[],
|
||||||
|
line: string,
|
||||||
|
): ReplyPayload[] => {
|
||||||
|
let index = -1;
|
||||||
|
for (let i = payloads.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (payloads[i]?.text) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index === -1) return [...payloads, { text: line }];
|
||||||
|
const existing = payloads[index];
|
||||||
|
const existingText = existing.text ?? "";
|
||||||
|
const separator = existingText.endsWith("\n") ? "" : "\n";
|
||||||
|
const next = {
|
||||||
|
...existing,
|
||||||
|
text: `${existingText}${separator}${line}`,
|
||||||
|
};
|
||||||
|
const updated = payloads.slice();
|
||||||
|
updated[index] = next;
|
||||||
|
return updated;
|
||||||
|
};
|
||||||
|
|
||||||
const withTimeout = async <T>(
|
const withTimeout = async <T>(
|
||||||
promise: Promise<T>,
|
promise: Promise<T>,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
@@ -191,6 +257,7 @@ export async function runReplyAgent(params: {
|
|||||||
replyToChannel,
|
replyToChannel,
|
||||||
);
|
);
|
||||||
const applyReplyToMode = createReplyToModeFilter(replyToMode);
|
const applyReplyToMode = createReplyToModeFilter(replyToMode);
|
||||||
|
const cfg = followupRun.run.config;
|
||||||
|
|
||||||
if (shouldSteer && isStreaming) {
|
if (shouldSteer && isStreaming) {
|
||||||
const steered = queueEmbeddedPiMessage(
|
const steered = queueEmbeddedPiMessage(
|
||||||
@@ -242,6 +309,7 @@ export async function runReplyAgent(params: {
|
|||||||
|
|
||||||
let didLogHeartbeatStrip = false;
|
let didLogHeartbeatStrip = false;
|
||||||
let autoCompactionCompleted = false;
|
let autoCompactionCompleted = false;
|
||||||
|
let responseUsageLine: string | undefined;
|
||||||
try {
|
try {
|
||||||
const runId = crypto.randomUUID();
|
const runId = crypto.randomUUID();
|
||||||
if (sessionKey) {
|
if (sessionKey) {
|
||||||
@@ -641,20 +709,20 @@ export async function runReplyAgent(params: {
|
|||||||
await typingSignals.signalRunStart();
|
await typingSignals.signalRunStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionStore && sessionKey) {
|
const usage = runResult.meta.agentMeta?.usage;
|
||||||
const usage = runResult.meta.agentMeta?.usage;
|
const modelUsed =
|
||||||
const modelUsed =
|
runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||||
runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
const providerUsed =
|
||||||
const providerUsed =
|
runResult.meta.agentMeta?.provider ??
|
||||||
runResult.meta.agentMeta?.provider ??
|
fallbackProvider ??
|
||||||
fallbackProvider ??
|
followupRun.run.provider;
|
||||||
followupRun.run.provider;
|
const contextTokensUsed =
|
||||||
const contextTokensUsed =
|
agentCfgContextTokens ??
|
||||||
agentCfgContextTokens ??
|
lookupContextTokens(modelUsed) ??
|
||||||
lookupContextTokens(modelUsed) ??
|
sessionEntry?.contextTokens ??
|
||||||
sessionEntry?.contextTokens ??
|
DEFAULT_CONTEXT_TOKENS;
|
||||||
DEFAULT_CONTEXT_TOKENS;
|
|
||||||
|
|
||||||
|
if (sessionStore && sessionKey) {
|
||||||
if (hasNonzeroUsage(usage)) {
|
if (hasNonzeroUsage(usage)) {
|
||||||
const entry = sessionEntry ?? sessionStore[sessionKey];
|
const entry = sessionEntry ?? sessionStore[sessionKey];
|
||||||
if (entry) {
|
if (entry) {
|
||||||
@@ -694,6 +762,29 @@ export async function runReplyAgent(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responseUsageEnabled =
|
||||||
|
(sessionEntry?.responseUsage ??
|
||||||
|
(sessionKey
|
||||||
|
? sessionStore?.[sessionKey]?.responseUsage
|
||||||
|
: undefined)) === "on";
|
||||||
|
if (responseUsageEnabled && hasNonzeroUsage(usage)) {
|
||||||
|
const authMode = resolveModelAuthMode(providerUsed, cfg);
|
||||||
|
const showCost = authMode === "api-key";
|
||||||
|
const costConfig = showCost
|
||||||
|
? resolveModelCostConfig({
|
||||||
|
provider: providerUsed,
|
||||||
|
model: modelUsed,
|
||||||
|
config: cfg,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const formatted = formatResponseUsageLine({
|
||||||
|
usage,
|
||||||
|
showCost,
|
||||||
|
costConfig,
|
||||||
|
});
|
||||||
|
if (formatted) responseUsageLine = formatted;
|
||||||
|
}
|
||||||
|
|
||||||
// If verbose is enabled and this is a new session, prepend a session hint.
|
// If verbose is enabled and this is a new session, prepend a session hint.
|
||||||
let finalPayloads = replyPayloads;
|
let finalPayloads = replyPayloads;
|
||||||
if (autoCompactionCompleted) {
|
if (autoCompactionCompleted) {
|
||||||
@@ -717,6 +808,9 @@ export async function runReplyAgent(params: {
|
|||||||
...finalPayloads,
|
...finalPayloads,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (responseUsageLine) {
|
||||||
|
finalPayloads = appendUsageLine(finalPayloads, responseUsageLine);
|
||||||
|
}
|
||||||
|
|
||||||
return finalizeWithFollowup(
|
return finalizeWithFollowup(
|
||||||
finalPayloads.length === 1 ? finalPayloads[0] : finalPayloads,
|
finalPayloads.length === 1 ? finalPayloads[0] : finalPayloads,
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import {
|
import crypto from "node:crypto";
|
||||||
ensureAuthProfileStore,
|
import { resolveModelAuthMode } from "../../agents/model-auth.js";
|
||||||
listProfilesForProvider,
|
|
||||||
} from "../../agents/auth-profiles.js";
|
|
||||||
import {
|
|
||||||
getCustomProviderApiKey,
|
|
||||||
resolveEnvApiKey,
|
|
||||||
} from "../../agents/model-auth.js";
|
|
||||||
import { normalizeProviderId } from "../../agents/model-selection.js";
|
import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||||
import {
|
import {
|
||||||
abortEmbeddedPiRun,
|
abortEmbeddedPiRun,
|
||||||
@@ -55,8 +49,10 @@ import type {
|
|||||||
ElevatedLevel,
|
ElevatedLevel,
|
||||||
ReasoningLevel,
|
ReasoningLevel,
|
||||||
ThinkLevel,
|
ThinkLevel,
|
||||||
|
UsageDisplayLevel,
|
||||||
VerboseLevel,
|
VerboseLevel,
|
||||||
} from "../thinking.js";
|
} from "../thinking.js";
|
||||||
|
import { normalizeUsageDisplay } from "../thinking.js";
|
||||||
import type { ReplyPayload } from "../types.js";
|
import type { ReplyPayload } from "../types.js";
|
||||||
import { isAbortTrigger, setAbortMemory } from "./abort.js";
|
import { isAbortTrigger, setAbortMemory } from "./abort.js";
|
||||||
import type { InlineDirectives } from "./directive-handling.js";
|
import type { InlineDirectives } from "./directive-handling.js";
|
||||||
@@ -109,36 +105,6 @@ export type CommandContext = {
|
|||||||
to?: string;
|
to?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveModelAuthLabel(
|
|
||||||
provider?: string,
|
|
||||||
cfg?: ClawdbotConfig,
|
|
||||||
): string | undefined {
|
|
||||||
const resolved = provider?.trim();
|
|
||||||
if (!resolved) return undefined;
|
|
||||||
|
|
||||||
const store = ensureAuthProfileStore();
|
|
||||||
const profiles = listProfilesForProvider(store, resolved);
|
|
||||||
if (profiles.length > 0) {
|
|
||||||
const modes = new Set(
|
|
||||||
profiles
|
|
||||||
.map((id) => store.profiles[id]?.type)
|
|
||||||
.filter((mode): mode is "api_key" | "oauth" => Boolean(mode)),
|
|
||||||
);
|
|
||||||
if (modes.has("oauth") && modes.has("api_key")) return "mixed";
|
|
||||||
if (modes.has("oauth")) return "oauth";
|
|
||||||
if (modes.has("api_key")) return "api-key";
|
|
||||||
}
|
|
||||||
|
|
||||||
const envKey = resolveEnvApiKey(resolved);
|
|
||||||
if (envKey?.apiKey) {
|
|
||||||
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getCustomProviderApiKey(cfg, resolved)) return "api-key";
|
|
||||||
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractCompactInstructions(params: {
|
function extractCompactInstructions(params: {
|
||||||
rawBody?: string;
|
rawBody?: string;
|
||||||
ctx: MsgContext;
|
ctx: MsgContext;
|
||||||
@@ -468,6 +434,7 @@ export async function handleCommands(params: {
|
|||||||
defaultGroupActivation())
|
defaultGroupActivation())
|
||||||
: undefined;
|
: undefined;
|
||||||
const statusText = buildStatusMessage({
|
const statusText = buildStatusMessage({
|
||||||
|
config: cfg,
|
||||||
agent: {
|
agent: {
|
||||||
...cfg.agent,
|
...cfg.agent,
|
||||||
model: {
|
model: {
|
||||||
@@ -488,7 +455,7 @@ export async function handleCommands(params: {
|
|||||||
resolvedVerbose: resolvedVerboseLevel,
|
resolvedVerbose: resolvedVerboseLevel,
|
||||||
resolvedReasoning: resolvedReasoningLevel,
|
resolvedReasoning: resolvedReasoningLevel,
|
||||||
resolvedElevated: resolvedElevatedLevel,
|
resolvedElevated: resolvedElevatedLevel,
|
||||||
modelAuth: resolveModelAuthLabel(provider, cfg),
|
modelAuth: resolveModelAuthMode(provider, cfg),
|
||||||
usageLine: usageLine ?? undefined,
|
usageLine: usageLine ?? undefined,
|
||||||
queue: {
|
queue: {
|
||||||
mode: queueSettings.mode,
|
mode: queueSettings.mode,
|
||||||
@@ -503,6 +470,51 @@ export async function handleCommands(params: {
|
|||||||
return { shouldContinue: false, reply: { text: statusText } };
|
return { shouldContinue: false, reply: { text: statusText } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const costRequested =
|
||||||
|
command.commandBodyNormalized === "/cost" ||
|
||||||
|
command.commandBodyNormalized.startsWith("/cost ");
|
||||||
|
if (allowTextCommands && costRequested) {
|
||||||
|
if (!command.isAuthorizedSender) {
|
||||||
|
logVerbose(
|
||||||
|
`Ignoring /cost from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
|
||||||
|
);
|
||||||
|
return { shouldContinue: false };
|
||||||
|
}
|
||||||
|
const rawArgs = command.commandBodyNormalized.slice("/cost".length).trim();
|
||||||
|
const normalized =
|
||||||
|
rawArgs.length > 0 ? normalizeUsageDisplay(rawArgs) : undefined;
|
||||||
|
if (rawArgs.length > 0 && !normalized) {
|
||||||
|
return {
|
||||||
|
shouldContinue: false,
|
||||||
|
reply: { text: "⚙️ Usage: /cost on|off" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const current: UsageDisplayLevel =
|
||||||
|
sessionEntry?.responseUsage === "on" ? "on" : "off";
|
||||||
|
const next = normalized ?? (current === "on" ? "off" : "on");
|
||||||
|
if (sessionStore && sessionKey) {
|
||||||
|
const entry = sessionEntry ??
|
||||||
|
sessionStore[sessionKey] ?? {
|
||||||
|
sessionId: crypto.randomUUID(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
if (next === "off") delete entry.responseUsage;
|
||||||
|
else entry.responseUsage = next;
|
||||||
|
entry.updatedAt = Date.now();
|
||||||
|
sessionStore[sessionKey] = entry;
|
||||||
|
if (storePath) {
|
||||||
|
await saveSessionStore(storePath, sessionStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
shouldContinue: false,
|
||||||
|
reply: {
|
||||||
|
text:
|
||||||
|
next === "on" ? "⚙️ Usage line enabled." : "⚙️ Usage line disabled.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const stopRequested = command.commandBodyNormalized === "/stop";
|
const stopRequested = command.commandBodyNormalized === "/stop";
|
||||||
if (allowTextCommands && stopRequested) {
|
if (allowTextCommands && stopRequested) {
|
||||||
if (!command.isAuthorizedSender) {
|
if (!command.isAuthorizedSender) {
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export async function initSessionState(params: {
|
|||||||
// Persist previously stored thinking/verbose levels when present.
|
// Persist previously stored thinking/verbose levels when present.
|
||||||
thinkingLevel: persistedThinking ?? baseEntry?.thinkingLevel,
|
thinkingLevel: persistedThinking ?? baseEntry?.thinkingLevel,
|
||||||
verboseLevel: persistedVerbose ?? baseEntry?.verboseLevel,
|
verboseLevel: persistedVerbose ?? baseEntry?.verboseLevel,
|
||||||
|
responseUsage: baseEntry?.responseUsage,
|
||||||
modelOverride: persistedModelOverride ?? baseEntry?.modelOverride,
|
modelOverride: persistedModelOverride ?? baseEntry?.modelOverride,
|
||||||
providerOverride: persistedProviderOverride ?? baseEntry?.providerOverride,
|
providerOverride: persistedProviderOverride ?? baseEntry?.providerOverride,
|
||||||
sendPolicy: baseEntry?.sendPolicy,
|
sendPolicy: baseEntry?.sendPolicy,
|
||||||
|
|||||||
@@ -63,20 +63,18 @@ describe("buildStatusMessage", () => {
|
|||||||
resolvedThink: "medium",
|
resolvedThink: "medium",
|
||||||
resolvedVerbose: "off",
|
resolvedVerbose: "off",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
now: 10 * 60_000, // 10 minutes later
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("🦞 ClawdBot");
|
expect(text).toContain("status agent:main:main");
|
||||||
expect(text).toContain("🧠 Model:");
|
expect(text).toContain("model anthropic/pi:opus (api-key)");
|
||||||
expect(text).toContain("Runtime: direct");
|
expect(text).toContain("Context 16k/32k (50%)");
|
||||||
expect(text).toContain("Context: 16k/32k (50%)");
|
expect(text).toContain("compactions 2");
|
||||||
expect(text).toContain("🧹 Compactions: 2");
|
expect(text).toContain("think medium");
|
||||||
expect(text).toContain("Session: agent:main:main");
|
expect(text).toContain("verbose off");
|
||||||
expect(text).toContain("updated 10m ago");
|
expect(text).toContain("reasoning off");
|
||||||
expect(text).toContain("Think: medium");
|
expect(text).toContain("elevated on");
|
||||||
expect(text).not.toContain("Verbose");
|
expect(text).toContain("queue collect");
|
||||||
expect(text).toContain("Elevated");
|
|
||||||
expect(text).toContain("Queue: collect");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows verbose/elevated labels only when enabled", () => {
|
it("shows verbose/elevated labels only when enabled", () => {
|
||||||
@@ -91,10 +89,8 @@ describe("buildStatusMessage", () => {
|
|||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("Verbose");
|
expect(text).toContain("verbose on");
|
||||||
expect(text).toContain("Elevated");
|
expect(text).toContain("elevated on");
|
||||||
expect(text).not.toContain("Verbose:");
|
|
||||||
expect(text).not.toContain("Elevated:");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers model overrides over last-run model", () => {
|
it("prefers model overrides over last-run model", () => {
|
||||||
@@ -115,9 +111,10 @@ describe("buildStatusMessage", () => {
|
|||||||
sessionKey: "agent:main:main",
|
sessionKey: "agent:main:main",
|
||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("🧠 Model: openai/gpt-4.1-mini");
|
expect(text).toContain("model openai/gpt-4.1-mini");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps provider prefix from configured model", () => {
|
it("keeps provider prefix from configured model", () => {
|
||||||
@@ -127,21 +124,23 @@ describe("buildStatusMessage", () => {
|
|||||||
},
|
},
|
||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("🧠 Model: google-antigravity/claude-sonnet-4-5");
|
expect(text).toContain("model google-antigravity/claude-sonnet-4-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles missing agent config gracefully", () => {
|
it("handles missing agent config gracefully", () => {
|
||||||
const text = buildStatusMessage({
|
const text = buildStatusMessage({
|
||||||
agent: {},
|
agent: {},
|
||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
webLinked: false,
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("🧠 Model:");
|
expect(text).toContain("model");
|
||||||
expect(text).toContain("Context:");
|
expect(text).toContain("Context");
|
||||||
expect(text).toContain("Queue:");
|
expect(text).toContain("queue collect");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes group activation for group sessions", () => {
|
it("includes group activation for group sessions", () => {
|
||||||
@@ -156,9 +155,10 @@ describe("buildStatusMessage", () => {
|
|||||||
sessionKey: "agent:main:whatsapp:group:123@g.us",
|
sessionKey: "agent:main:whatsapp:group:123@g.us",
|
||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("Activation: always");
|
expect(text).toContain("activation always");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows queue details when overridden", () => {
|
it("shows queue details when overridden", () => {
|
||||||
@@ -175,10 +175,11 @@ describe("buildStatusMessage", () => {
|
|||||||
dropPolicy: "old",
|
dropPolicy: "old",
|
||||||
showDetails: true,
|
showDetails: true,
|
||||||
},
|
},
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain(
|
expect(text).toContain(
|
||||||
"Queue: collect (depth 3 · debounce 2s · cap 5 · drop old)",
|
"queue collect (depth 3 · debounce 2s · cap 5 · drop old)",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,12 +191,10 @@ describe("buildStatusMessage", () => {
|
|||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
usageLine: "📊 Usage: Claude 80% left (5h)",
|
usageLine: "📊 Usage: Claude 80% left (5h)",
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
const lines = text.split("\n");
|
expect(text).toContain("📊 Usage: Claude 80% left (5h)");
|
||||||
const contextIndex = lines.findIndex((line) => line.startsWith("📚 "));
|
|
||||||
expect(contextIndex).toBeGreaterThan(-1);
|
|
||||||
expect(lines[contextIndex + 1]).toBe("📊 Usage: Claude 80% left (5h)");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers cached prompt tokens from the session log", async () => {
|
it("prefers cached prompt tokens from the session log", async () => {
|
||||||
@@ -255,9 +254,10 @@ describe("buildStatusMessage", () => {
|
|||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
includeTranscriptUsage: true,
|
includeTranscriptUsage: true,
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("Context: 1.0k/32k");
|
expect(text).toContain("Context 1.0k/32k");
|
||||||
} finally {
|
} finally {
|
||||||
restoreHomeEnv(previousHome);
|
restoreHomeEnv(previousHome);
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
DEFAULT_PROVIDER,
|
DEFAULT_PROVIDER,
|
||||||
} from "../agents/defaults.js";
|
} from "../agents/defaults.js";
|
||||||
|
import { resolveModelAuthMode } from "../agents/model-auth.js";
|
||||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||||
import {
|
import {
|
||||||
derivePromptTokens,
|
derivePromptTokens,
|
||||||
@@ -14,13 +15,16 @@ import {
|
|||||||
} from "../agents/usage.js";
|
} from "../agents/usage.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveMainSessionKey,
|
|
||||||
resolveSessionFilePath,
|
resolveSessionFilePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
type SessionScope,
|
type SessionScope,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { resolveCommitHash } from "../infra/git-commit.js";
|
import {
|
||||||
import { VERSION } from "../version.js";
|
estimateUsageCost,
|
||||||
|
formatTokenCount as formatTokenCountShared,
|
||||||
|
formatUsd,
|
||||||
|
resolveModelCostConfig,
|
||||||
|
} from "../utils/usage-format.js";
|
||||||
import type {
|
import type {
|
||||||
ElevatedLevel,
|
ElevatedLevel,
|
||||||
ReasoningLevel,
|
ReasoningLevel,
|
||||||
@@ -30,6 +34,8 @@ import type {
|
|||||||
|
|
||||||
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
|
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
|
||||||
|
|
||||||
|
export const formatTokenCount = formatTokenCountShared;
|
||||||
|
|
||||||
type QueueStatus = {
|
type QueueStatus = {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
@@ -40,6 +46,7 @@ type QueueStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StatusArgs = {
|
type StatusArgs = {
|
||||||
|
config?: ClawdbotConfig;
|
||||||
agent: AgentConfig;
|
agent: AgentConfig;
|
||||||
sessionEntry?: SessionEntry;
|
sessionEntry?: SessionEntry;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
@@ -53,37 +60,20 @@ type StatusArgs = {
|
|||||||
usageLine?: string;
|
usageLine?: string;
|
||||||
queue?: QueueStatus;
|
queue?: QueueStatus;
|
||||||
includeTranscriptUsage?: boolean;
|
includeTranscriptUsage?: boolean;
|
||||||
now?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAge = (ms?: number | null) => {
|
|
||||||
if (!ms || ms < 0) return "unknown";
|
|
||||||
const minutes = Math.round(ms / 60_000);
|
|
||||||
if (minutes < 1) return "just now";
|
|
||||||
if (minutes < 60) return `${minutes}m ago`;
|
|
||||||
const hours = Math.round(minutes / 60);
|
|
||||||
if (hours < 48) return `${hours}h ago`;
|
|
||||||
const days = Math.round(hours / 24);
|
|
||||||
return `${days}d ago`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatKTokens = (value: number) =>
|
|
||||||
`${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
|
||||||
|
|
||||||
export const formatTokenCount = (value: number) => formatKTokens(value);
|
|
||||||
|
|
||||||
const formatTokens = (
|
const formatTokens = (
|
||||||
total: number | null | undefined,
|
total: number | null | undefined,
|
||||||
contextTokens: number | null,
|
contextTokens: number | null,
|
||||||
) => {
|
) => {
|
||||||
const ctx = contextTokens ?? null;
|
const ctx = contextTokens ?? null;
|
||||||
if (total == null) {
|
if (total == null) {
|
||||||
const ctxLabel = ctx ? formatKTokens(ctx) : "?";
|
const ctxLabel = ctx ? formatTokenCount(ctx) : "?";
|
||||||
return `unknown/${ctxLabel}`;
|
return `?/${ctxLabel}`;
|
||||||
}
|
}
|
||||||
const pct = ctx ? Math.min(999, Math.round((total / ctx) * 100)) : null;
|
const pct = ctx ? Math.min(999, Math.round((total / ctx) * 100)) : null;
|
||||||
const totalLabel = formatKTokens(total);
|
const totalLabel = formatTokenCount(total);
|
||||||
const ctxLabel = ctx ? formatKTokens(ctx) : "?";
|
const ctxLabel = ctx ? formatTokenCount(ctx) : "?";
|
||||||
return `${totalLabel}/${ctxLabel}${pct !== null ? ` (${pct}%)` : ""}`;
|
return `${totalLabel}/${ctxLabel}${pct !== null ? ` (${pct}%)` : ""}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,8 +161,15 @@ const readUsageFromSessionLog = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatUsagePair = (input?: number | null, output?: number | null) => {
|
||||||
|
if (input == null && output == null) return null;
|
||||||
|
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
|
||||||
|
const outputLabel =
|
||||||
|
typeof output === "number" ? formatTokenCount(output) : "?";
|
||||||
|
return `usage ${inputLabel} in / ${outputLabel} out`;
|
||||||
|
};
|
||||||
|
|
||||||
export function buildStatusMessage(args: StatusArgs): string {
|
export function buildStatusMessage(args: StatusArgs): string {
|
||||||
const now = args.now ?? Date.now();
|
|
||||||
const entry = args.sessionEntry;
|
const entry = args.sessionEntry;
|
||||||
const resolved = resolveConfiguredModelRef({
|
const resolved = resolveConfiguredModelRef({
|
||||||
cfg: { agent: args.agent ?? {} },
|
cfg: { agent: args.agent ?? {} },
|
||||||
@@ -188,6 +185,8 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
lookupContextTokens(model) ??
|
lookupContextTokens(model) ??
|
||||||
DEFAULT_CONTEXT_TOKENS;
|
DEFAULT_CONTEXT_TOKENS;
|
||||||
|
|
||||||
|
let inputTokens = entry?.inputTokens;
|
||||||
|
let outputTokens = entry?.outputTokens;
|
||||||
let totalTokens =
|
let totalTokens =
|
||||||
entry?.totalTokens ??
|
entry?.totalTokens ??
|
||||||
(entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
|
(entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
|
||||||
@@ -205,6 +204,8 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
if (!contextTokens && logUsage.model) {
|
if (!contextTokens && logUsage.model) {
|
||||||
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
|
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
|
||||||
}
|
}
|
||||||
|
if (!inputTokens || inputTokens === 0) inputTokens = logUsage.input;
|
||||||
|
if (!outputTokens || outputTokens === 0) outputTokens = logUsage.output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,33 +219,6 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
args.agent?.elevatedDefault ??
|
args.agent?.elevatedDefault ??
|
||||||
"on";
|
"on";
|
||||||
|
|
||||||
const runtime = (() => {
|
|
||||||
const sandboxMode = args.agent?.sandbox?.mode ?? "off";
|
|
||||||
if (sandboxMode === "off") return { label: "direct" };
|
|
||||||
const sessionScope = args.sessionScope ?? "per-sender";
|
|
||||||
const mainKey = resolveMainSessionKey({
|
|
||||||
session: { scope: sessionScope },
|
|
||||||
});
|
|
||||||
const sessionKey = args.sessionKey?.trim();
|
|
||||||
const sandboxed = sessionKey
|
|
||||||
? sandboxMode === "all" || sessionKey !== mainKey.trim()
|
|
||||||
: false;
|
|
||||||
const runtime = sandboxed ? "docker" : sessionKey ? "direct" : "unknown";
|
|
||||||
return {
|
|
||||||
label: `${runtime}/${sandboxMode}`,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
const updatedAt = entry?.updatedAt;
|
|
||||||
const sessionLine = [
|
|
||||||
`Session: ${args.sessionKey ?? "unknown"}`,
|
|
||||||
typeof updatedAt === "number"
|
|
||||||
? `updated ${formatAge(now - updatedAt)}`
|
|
||||||
: "no activity",
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" • ");
|
|
||||||
|
|
||||||
const isGroupSession =
|
const isGroupSession =
|
||||||
entry?.chatType === "group" ||
|
entry?.chatType === "group" ||
|
||||||
entry?.chatType === "room" ||
|
entry?.chatType === "room" ||
|
||||||
@@ -255,52 +229,66 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const contextLine = [
|
const authMode =
|
||||||
`Context: ${formatTokens(totalTokens, contextTokens ?? null)}`,
|
args.modelAuth ?? resolveModelAuthMode(provider, args.config);
|
||||||
`🧹 Compactions: ${entry?.compactionCount ?? 0}`,
|
const showCost = authMode === "api-key";
|
||||||
]
|
const costConfig = showCost
|
||||||
.filter(Boolean)
|
? resolveModelCostConfig({
|
||||||
.join(" · ");
|
provider,
|
||||||
|
model,
|
||||||
|
config: args.config,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const hasUsage =
|
||||||
|
typeof inputTokens === "number" || typeof outputTokens === "number";
|
||||||
|
const cost =
|
||||||
|
showCost && hasUsage
|
||||||
|
? estimateUsageCost({
|
||||||
|
usage: {
|
||||||
|
input: inputTokens ?? undefined,
|
||||||
|
output: outputTokens ?? undefined,
|
||||||
|
},
|
||||||
|
cost: costConfig,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
|
||||||
|
|
||||||
|
const parts: Array<string | null> = [];
|
||||||
|
parts.push(`status ${args.sessionKey ?? "unknown"}`);
|
||||||
|
|
||||||
|
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
||||||
|
const authLabel = authMode && authMode !== "unknown" ? ` (${authMode})` : "";
|
||||||
|
parts.push(`model ${modelLabel}${authLabel}`);
|
||||||
|
|
||||||
|
const usagePair = formatUsagePair(inputTokens, outputTokens);
|
||||||
|
if (usagePair) parts.push(usagePair);
|
||||||
|
if (costLabel) parts.push(`cost ${costLabel}`);
|
||||||
|
|
||||||
|
const contextSummary = formatContextUsageShort(
|
||||||
|
totalTokens && totalTokens > 0 ? totalTokens : null,
|
||||||
|
contextTokens ?? null,
|
||||||
|
);
|
||||||
|
parts.push(contextSummary);
|
||||||
|
parts.push(`compactions ${entry?.compactionCount ?? 0}`);
|
||||||
|
parts.push(`think ${thinkLevel}`);
|
||||||
|
parts.push(`verbose ${verboseLevel}`);
|
||||||
|
parts.push(`reasoning ${reasoningLevel}`);
|
||||||
|
parts.push(`elevated ${elevatedLevel}`);
|
||||||
|
if (groupActivationValue) parts.push(`activation ${groupActivationValue}`);
|
||||||
|
|
||||||
const queueMode = args.queue?.mode ?? "unknown";
|
const queueMode = args.queue?.mode ?? "unknown";
|
||||||
const queueDetails = formatQueueDetails(args.queue);
|
const queueDetails = formatQueueDetails(args.queue);
|
||||||
const optionParts = [
|
parts.push(`queue ${queueMode}${queueDetails}`);
|
||||||
`Runtime: ${runtime.label}`,
|
|
||||||
`Think: ${thinkLevel}`,
|
|
||||||
verboseLevel === "on" ? "Verbose" : null,
|
|
||||||
reasoningLevel !== "off" ? `Reasoning: ${reasoningLevel}` : null,
|
|
||||||
elevatedLevel === "on" ? "Elevated" : null,
|
|
||||||
];
|
|
||||||
const optionsLine = optionParts.filter(Boolean).join(" · ");
|
|
||||||
const activationParts = [
|
|
||||||
groupActivationValue ? `👥 Activation: ${groupActivationValue}` : null,
|
|
||||||
`🪢 Queue: ${queueMode}${queueDetails}`,
|
|
||||||
];
|
|
||||||
const activationLine = activationParts.filter(Boolean).join(" · ");
|
|
||||||
|
|
||||||
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
if (args.usageLine) parts.push(args.usageLine);
|
||||||
const authLabel = args.modelAuth ? ` · 🔑 ${args.modelAuth}` : "";
|
|
||||||
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
|
|
||||||
const commit = resolveCommitHash();
|
|
||||||
const versionLine = `🦞 ClawdBot ${VERSION}${commit ? ` (${commit})` : ""}`;
|
|
||||||
|
|
||||||
return [
|
return parts.filter(Boolean).join(" · ");
|
||||||
versionLine,
|
|
||||||
modelLine,
|
|
||||||
`📚 ${contextLine}`,
|
|
||||||
args.usageLine,
|
|
||||||
`🧵 ${sessionLine}`,
|
|
||||||
`⚙️ ${optionsLine}`,
|
|
||||||
activationLine,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHelpMessage(): string {
|
export function buildHelpMessage(): string {
|
||||||
return [
|
return [
|
||||||
"ℹ️ Help",
|
"ℹ️ Help",
|
||||||
"Shortcuts: /new reset | /compact [instructions] | /restart relink",
|
"Shortcuts: /new reset | /compact [instructions] | /restart relink",
|
||||||
"Options: /think <level> | /verbose on|off | /reasoning on|off | /elevated on|off | /model <id>",
|
"Options: /think <level> | /verbose on|off | /reasoning on|off | /elevated on|off | /model <id> | /cost on|off",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
|
|||||||
export type VerboseLevel = "off" | "on";
|
export type VerboseLevel = "off" | "on";
|
||||||
export type ElevatedLevel = "off" | "on";
|
export type ElevatedLevel = "off" | "on";
|
||||||
export type ReasoningLevel = "off" | "on" | "stream";
|
export type ReasoningLevel = "off" | "on" | "stream";
|
||||||
|
export type UsageDisplayLevel = "off" | "on";
|
||||||
|
|
||||||
// Normalize user-provided thinking level strings to the canonical enum.
|
// Normalize user-provided thinking level strings to the canonical enum.
|
||||||
export function normalizeThinkLevel(
|
export function normalizeThinkLevel(
|
||||||
@@ -46,6 +47,19 @@ export function normalizeVerboseLevel(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize response-usage display flags used to toggle cost/token lines.
|
||||||
|
export function normalizeUsageDisplay(
|
||||||
|
raw?: string | null,
|
||||||
|
): UsageDisplayLevel | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const key = raw.toLowerCase();
|
||||||
|
if (["off", "false", "no", "0", "disable", "disabled"].includes(key))
|
||||||
|
return "off";
|
||||||
|
if (["on", "true", "yes", "1", "enable", "enabled"].includes(key))
|
||||||
|
return "on";
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize elevated flags used to toggle elevated bash permissions.
|
// Normalize elevated flags used to toggle elevated bash permissions.
|
||||||
export function normalizeElevatedLevel(
|
export function normalizeElevatedLevel(
|
||||||
raw?: string | null,
|
raw?: string | null,
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export type SessionEntry = {
|
|||||||
verboseLevel?: string;
|
verboseLevel?: string;
|
||||||
reasoningLevel?: string;
|
reasoningLevel?: string;
|
||||||
elevatedLevel?: string;
|
elevatedLevel?: string;
|
||||||
|
responseUsage?: "on" | "off";
|
||||||
providerOverride?: string;
|
providerOverride?: string;
|
||||||
modelOverride?: string;
|
modelOverride?: string;
|
||||||
authProfileOverride?: string;
|
authProfileOverride?: string;
|
||||||
|
|||||||
@@ -325,6 +325,9 @@ export const SessionsPatchParamsSchema = Type.Object(
|
|||||||
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
responseUsage: Type.Optional(
|
||||||
|
Type.Union([Type.Literal("on"), Type.Literal("off"), Type.Null()]),
|
||||||
|
),
|
||||||
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
|
|||||||
import {
|
import {
|
||||||
normalizeReasoningLevel,
|
normalizeReasoningLevel,
|
||||||
normalizeThinkLevel,
|
normalizeThinkLevel,
|
||||||
|
normalizeUsageDisplay,
|
||||||
normalizeVerboseLevel,
|
normalizeVerboseLevel,
|
||||||
} from "../../auto-reply/thinking.js";
|
} from "../../auto-reply/thinking.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
@@ -234,6 +235,28 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("responseUsage" in p) {
|
||||||
|
const raw = p.responseUsage;
|
||||||
|
if (raw === null) {
|
||||||
|
delete next.responseUsage;
|
||||||
|
} else if (raw !== undefined) {
|
||||||
|
const normalized = normalizeUsageDisplay(String(raw));
|
||||||
|
if (!normalized) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
'invalid responseUsage (use "on"|"off")',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (normalized === "off") delete next.responseUsage;
|
||||||
|
else next.responseUsage = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("model" in p) {
|
if ("model" in p) {
|
||||||
const raw = p.model;
|
const raw = p.model;
|
||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
@@ -394,6 +417,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
thinkingLevel: entry?.thinkingLevel,
|
thinkingLevel: entry?.thinkingLevel,
|
||||||
verboseLevel: entry?.verboseLevel,
|
verboseLevel: entry?.verboseLevel,
|
||||||
reasoningLevel: entry?.reasoningLevel,
|
reasoningLevel: entry?.reasoningLevel,
|
||||||
|
responseUsage: entry?.responseUsage,
|
||||||
model: entry?.model,
|
model: entry?.model,
|
||||||
contextTokens: entry?.contextTokens,
|
contextTokens: entry?.contextTokens,
|
||||||
sendPolicy: entry?.sendPolicy,
|
sendPolicy: entry?.sendPolicy,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export type GatewaySessionRow = {
|
|||||||
inputTokens?: number;
|
inputTokens?: number;
|
||||||
outputTokens?: number;
|
outputTokens?: number;
|
||||||
totalTokens?: number;
|
totalTokens?: number;
|
||||||
|
responseUsage?: "on" | "off";
|
||||||
|
modelProvider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
contextTokens?: number;
|
contextTokens?: number;
|
||||||
lastProvider?: SessionEntry["lastProvider"];
|
lastProvider?: SessionEntry["lastProvider"];
|
||||||
@@ -503,6 +505,8 @@ export function listSessionsFromStore(params: {
|
|||||||
inputTokens: entry?.inputTokens,
|
inputTokens: entry?.inputTokens,
|
||||||
outputTokens: entry?.outputTokens,
|
outputTokens: entry?.outputTokens,
|
||||||
totalTokens: total,
|
totalTokens: total,
|
||||||
|
responseUsage: entry?.responseUsage,
|
||||||
|
modelProvider: entry?.modelProvider,
|
||||||
model: entry?.model,
|
model: entry?.model,
|
||||||
contextTokens: entry?.contextTokens,
|
contextTokens: entry?.contextTokens,
|
||||||
lastProvider: entry?.lastProvider,
|
lastProvider: entry?.lastProvider,
|
||||||
|
|||||||
@@ -64,6 +64,14 @@ export function getSlashCommands(): SlashCommand[] {
|
|||||||
(value) => ({ value, label: value }),
|
(value) => ({ value, label: value }),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "cost",
|
||||||
|
description: "Toggle per-response usage line",
|
||||||
|
getArgumentCompletions: (prefix) =>
|
||||||
|
TOGGLE.filter((v) => v.startsWith(prefix.toLowerCase())).map(
|
||||||
|
(value) => ({ value, label: value }),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "elevated",
|
name: "elevated",
|
||||||
description: "Set elevated on/off",
|
description: "Set elevated on/off",
|
||||||
@@ -116,6 +124,7 @@ export function helpText(): string {
|
|||||||
"/think <off|minimal|low|medium|high>",
|
"/think <off|minimal|low|medium|high>",
|
||||||
"/verbose <on|off>",
|
"/verbose <on|off>",
|
||||||
"/reasoning <on|off>",
|
"/reasoning <on|off>",
|
||||||
|
"/cost <on|off>",
|
||||||
"/elevated <on|off>",
|
"/elevated <on|off>",
|
||||||
"/elev <on|off>",
|
"/elev <on|off>",
|
||||||
"/activation <mention|always>",
|
"/activation <mention|always>",
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ export type GatewaySessionList = {
|
|||||||
sendPolicy?: string;
|
sendPolicy?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
contextTokens?: number | null;
|
contextTokens?: number | null;
|
||||||
|
inputTokens?: number | null;
|
||||||
|
outputTokens?: number | null;
|
||||||
totalTokens?: number | null;
|
totalTokens?: number | null;
|
||||||
|
responseUsage?: "on" | "off";
|
||||||
|
modelProvider?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
room?: string;
|
room?: string;
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TUI,
|
TUI,
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
|
import { normalizeUsageDisplay } from "../auto-reply/thinking.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
buildAgentMainSessionKey,
|
buildAgentMainSessionKey,
|
||||||
normalizeAgentId,
|
normalizeAgentId,
|
||||||
parseAgentSessionKey,
|
parseAgentSessionKey,
|
||||||
} from "../routing/session-key.js";
|
} from "../routing/session-key.js";
|
||||||
|
import { formatTokenCount } from "../utils/usage-format.js";
|
||||||
import { getSlashCommands, helpText, parseCommand } from "./commands.js";
|
import { getSlashCommands, helpText, parseCommand } from "./commands.js";
|
||||||
import { ChatLog } from "./components/chat-log.js";
|
import { ChatLog } from "./components/chat-log.js";
|
||||||
import { CustomEditor } from "./components/custom-editor.js";
|
import { CustomEditor } from "./components/custom-editor.js";
|
||||||
@@ -52,8 +54,12 @@ type SessionInfo = {
|
|||||||
verboseLevel?: string;
|
verboseLevel?: string;
|
||||||
reasoningLevel?: string;
|
reasoningLevel?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
modelProvider?: string;
|
||||||
contextTokens?: number | null;
|
contextTokens?: number | null;
|
||||||
|
inputTokens?: number | null;
|
||||||
|
outputTokens?: number | null;
|
||||||
totalTokens?: number | null;
|
totalTokens?: number | null;
|
||||||
|
responseUsage?: "on" | "off";
|
||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
};
|
};
|
||||||
@@ -99,13 +105,16 @@ function extractTextFromMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatTokens(total?: number | null, context?: number | null) {
|
function formatTokens(total?: number | null, context?: number | null) {
|
||||||
if (!total && !context) return "tokens ?";
|
if (total == null && context == null) return "tokens ?";
|
||||||
if (!context) return `tokens ${total ?? 0}`;
|
const totalLabel = total == null ? "?" : formatTokenCount(total);
|
||||||
|
if (context == null) return `tokens ${totalLabel}`;
|
||||||
const pct =
|
const pct =
|
||||||
typeof total === "number" && context > 0
|
typeof total === "number" && context > 0
|
||||||
? Math.min(999, Math.round((total / context) * 100))
|
? Math.min(999, Math.round((total / context) * 100))
|
||||||
: null;
|
: null;
|
||||||
return `tokens ${total ?? 0}/${context}${pct !== null ? ` (${pct}%)` : ""}`;
|
return `tokens ${totalLabel}/${formatTokenCount(context)}${
|
||||||
|
pct !== null ? ` (${pct}%)` : ""
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asString(value: unknown, fallback = ""): string {
|
function asString(value: unknown, fallback = ""): string {
|
||||||
@@ -213,7 +222,11 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
? `${sessionKeyLabel} (${sessionInfo.displayName})`
|
? `${sessionKeyLabel} (${sessionInfo.displayName})`
|
||||||
: sessionKeyLabel;
|
: sessionKeyLabel;
|
||||||
const agentLabel = formatAgentLabel(currentAgentId);
|
const agentLabel = formatAgentLabel(currentAgentId);
|
||||||
const modelLabel = sessionInfo.model ?? "unknown";
|
const modelLabel = sessionInfo.model
|
||||||
|
? sessionInfo.modelProvider
|
||||||
|
? `${sessionInfo.modelProvider}/${sessionInfo.model}`
|
||||||
|
: sessionInfo.model
|
||||||
|
: "unknown";
|
||||||
const tokens = formatTokens(
|
const tokens = formatTokens(
|
||||||
sessionInfo.totalTokens ?? null,
|
sessionInfo.totalTokens ?? null,
|
||||||
sessionInfo.contextTokens ?? null,
|
sessionInfo.contextTokens ?? null,
|
||||||
@@ -321,8 +334,12 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
verboseLevel: entry?.verboseLevel,
|
verboseLevel: entry?.verboseLevel,
|
||||||
reasoningLevel: entry?.reasoningLevel,
|
reasoningLevel: entry?.reasoningLevel,
|
||||||
model: entry?.model ?? result.defaults?.model ?? undefined,
|
model: entry?.model ?? result.defaults?.model ?? undefined,
|
||||||
|
modelProvider: entry?.modelProvider,
|
||||||
contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens,
|
contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens,
|
||||||
|
inputTokens: entry?.inputTokens ?? null,
|
||||||
|
outputTokens: entry?.outputTokens ?? null,
|
||||||
totalTokens: entry?.totalTokens ?? null,
|
totalTokens: entry?.totalTokens ?? null,
|
||||||
|
responseUsage: entry?.responseUsage,
|
||||||
updatedAt: entry?.updatedAt ?? null,
|
updatedAt: entry?.updatedAt ?? null,
|
||||||
displayName: entry?.displayName,
|
displayName: entry?.displayName,
|
||||||
};
|
};
|
||||||
@@ -773,6 +790,28 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
chatLog.addSystem(`reasoning failed: ${String(err)}`);
|
chatLog.addSystem(`reasoning failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "cost": {
|
||||||
|
const normalized = args ? normalizeUsageDisplay(args) : undefined;
|
||||||
|
if (args && !normalized) {
|
||||||
|
chatLog.addSystem("usage: /cost <on|off>");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const current = sessionInfo.responseUsage === "on" ? "on" : "off";
|
||||||
|
const next = normalized ?? (current === "on" ? "off" : "on");
|
||||||
|
try {
|
||||||
|
await client.patchSession({
|
||||||
|
key: currentSessionKey,
|
||||||
|
responseUsage: next === "off" ? null : next,
|
||||||
|
});
|
||||||
|
chatLog.addSystem(
|
||||||
|
next === "on" ? "usage line enabled" : "usage line disabled",
|
||||||
|
);
|
||||||
|
await refreshSessionInfo();
|
||||||
|
} catch (err) {
|
||||||
|
chatLog.addSystem(`cost failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "elevated":
|
case "elevated":
|
||||||
if (!args) {
|
if (!args) {
|
||||||
chatLog.addSystem("usage: /elevated <on|off>");
|
chatLog.addSystem("usage: /elevated <on|off>");
|
||||||
|
|||||||
60
src/utils/usage-format.test.ts
Normal file
60
src/utils/usage-format.test.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
estimateUsageCost,
|
||||||
|
formatTokenCount,
|
||||||
|
formatUsd,
|
||||||
|
resolveModelCostConfig,
|
||||||
|
} from "./usage-format.js";
|
||||||
|
|
||||||
|
describe("usage-format", () => {
|
||||||
|
it("formats token counts", () => {
|
||||||
|
expect(formatTokenCount(999)).toBe("999");
|
||||||
|
expect(formatTokenCount(1234)).toBe("1.2k");
|
||||||
|
expect(formatTokenCount(12000)).toBe("12k");
|
||||||
|
expect(formatTokenCount(2_500_000)).toBe("2.5m");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats USD values", () => {
|
||||||
|
expect(formatUsd(1.234)).toBe("$1.23");
|
||||||
|
expect(formatUsd(0.5)).toBe("$0.50");
|
||||||
|
expect(formatUsd(0.0042)).toBe("$0.0042");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves model cost config and estimates usage cost", () => {
|
||||||
|
const config = {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
test: {
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
cost: { input: 1, output: 2, cacheRead: 0.5, cacheWrite: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const cost = resolveModelCostConfig({
|
||||||
|
provider: "test",
|
||||||
|
model: "m1",
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(cost).toEqual({
|
||||||
|
input: 1,
|
||||||
|
output: 2,
|
||||||
|
cacheRead: 0.5,
|
||||||
|
cacheWrite: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = estimateUsageCost({
|
||||||
|
usage: { input: 1000, output: 500, cacheRead: 2000 },
|
||||||
|
cost,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(total).toBeCloseTo(0.003);
|
||||||
|
});
|
||||||
|
});
|
||||||
69
src/utils/usage-format.ts
Normal file
69
src/utils/usage-format.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { NormalizedUsage } from "../agents/usage.js";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
|
||||||
|
export type ModelCostConfig = {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UsageTotals = {
|
||||||
|
input?: number;
|
||||||
|
output?: number;
|
||||||
|
cacheRead?: number;
|
||||||
|
cacheWrite?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatTokenCount(value?: number): string {
|
||||||
|
if (value === undefined || !Number.isFinite(value)) return "0";
|
||||||
|
const safe = Math.max(0, value);
|
||||||
|
if (safe >= 1_000_000) return `${(safe / 1_000_000).toFixed(1)}m`;
|
||||||
|
if (safe >= 1_000)
|
||||||
|
return `${(safe / 1_000).toFixed(safe >= 10_000 ? 0 : 1)}k`;
|
||||||
|
return String(Math.round(safe));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUsd(value?: number): string | undefined {
|
||||||
|
if (value === undefined || !Number.isFinite(value)) return undefined;
|
||||||
|
if (value >= 1) return `$${value.toFixed(2)}`;
|
||||||
|
if (value >= 0.01) return `$${value.toFixed(2)}`;
|
||||||
|
return `$${value.toFixed(4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveModelCostConfig(params: {
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
}): ModelCostConfig | undefined {
|
||||||
|
const provider = params.provider?.trim();
|
||||||
|
const model = params.model?.trim();
|
||||||
|
if (!provider || !model) return undefined;
|
||||||
|
const providers = params.config?.models?.providers ?? {};
|
||||||
|
const entry = providers[provider]?.models?.find((item) => item.id === model);
|
||||||
|
return entry?.cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNumber = (value: number | undefined): number =>
|
||||||
|
typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||||
|
|
||||||
|
export function estimateUsageCost(params: {
|
||||||
|
usage?: NormalizedUsage | UsageTotals | null;
|
||||||
|
cost?: ModelCostConfig;
|
||||||
|
}): number | undefined {
|
||||||
|
const usage = params.usage;
|
||||||
|
const cost = params.cost;
|
||||||
|
if (!usage || !cost) return undefined;
|
||||||
|
const input = toNumber(usage.input);
|
||||||
|
const output = toNumber(usage.output);
|
||||||
|
const cacheRead = toNumber(usage.cacheRead);
|
||||||
|
const cacheWrite = toNumber(usage.cacheWrite);
|
||||||
|
const total =
|
||||||
|
input * cost.input +
|
||||||
|
output * cost.output +
|
||||||
|
cacheRead * cost.cacheRead +
|
||||||
|
cacheWrite * cost.cacheWrite;
|
||||||
|
if (!Number.isFinite(total)) return undefined;
|
||||||
|
return total / 1_000_000;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user