From fa286a6eae1b0d73bae341cf80da48805d66e152 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 13 Sep 2026 00:43:33 +0900 Subject: [PATCH 1/2] Revert "feat(usage): show the cached subset beside every token total" This reverts 7b3c4e9800. The reported defect was Devin-only and had nothing to do with the display layer. The logs table has always rendered a total with its cached value; Devin rows showed a bare total because the cloud-direct adapter decoded GetChatMessageResponse field 28 (response_dimension_groups, the rows the IDE draws) instead of field 7 (ModelUsageStats), so cache read and cache write never reached the log row in the first place. #4419 fixes that at the source, which is the whole fix. Changing the Usage page, the dashboard tile and the CLI tables rewrote surfaces that were already correct for every other provider, so the revert restores them. --- .../cached-token-companion.jpg | Bin 18950 -> 0 bytes gui/src/format-tokens.ts | 21 ---------- gui/src/pages/Logs.tsx | 15 +------- gui/src/pages/Usage.tsx | 12 ++---- gui/src/pages/dashboard-overview-head.tsx | 10 +---- gui/src/pages/dashboard-shared.ts | 12 +----- scripts/test-layout/layout.json | 1 - src/cli/usage-report.ts | 36 ++---------------- tests/cli/cli-usage-report.test.ts | 25 ------------ tests/fixtures/test-layout-expected.json | 1 - tests/gui/gui-format-tokens-cache.test.ts | 29 -------------- 11 files changed, 10 insertions(+), 152 deletions(-) delete mode 100644 devlog/_plan/260912_devin_hardening/cached-token-companion.jpg delete mode 100644 tests/gui/gui-format-tokens-cache.test.ts diff --git a/devlog/_plan/260912_devin_hardening/cached-token-companion.jpg b/devlog/_plan/260912_devin_hardening/cached-token-companion.jpg deleted file mode 100644 index 2110f1f8ed0d5a4c32265e344a509836c151305f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18950 zcmeIa2S8NIvM9O$5k&z}lAz!g$w`o$)eShqW{@Fgl{iDrIT%1CDlni#aTo>|g5(?| z3J6FN8G>Xa3{i4=Yt-%D=bp3gJ@>tP&wKCx*Vxs)s;j%Js=LBKq!gf3 zq-3rC+2nj+a881&SEZZ4+_Uu zz=oK<_wjod#FW?#Si(UI>WYMv{1_Rit?z+^mXz%5xnDF==`IVykBMA&I>G~|!D3Qc zGFm_ZI6SLJb&2J4AixeZl#>wUg ze(`UvKFX79?Y!-nh~^|T?F$So9>Cnh-Jm|al7l6MYDzbS_VUCq0abde0F8S?{f8F2 zFjhuJ~6Ztt()^Cn6nBg_E0ZGB$H1$F|%m$ zB?pI;!GckBR~%uvu*|{ANs?Jlf5H-XTl>mF3tu&AF{g83lWeVS*GJbEcAYfP~rg!WmTm;T^z7+=M~$y#YoFD%>_Q5j~c-ZoR7|&LN#}qnKRBIgFhp_JZdMzL;cIB8E_zsRg@^Wo7#TW=0iRi+^y zuCl<-RN)O<3dKNhd-`JHys6`Y#FZ!q_nD{}FL8Tn$w95)Ee<6{?&g6G^P!#19pi;M z8YTdc?-zIu)kD&ouuo>f(zd+0tefX@+;l4rUXEf52d_*rw`C8nDGPTW0oLgCz56?3 ze#HgW&|)Uue&;?Mlaz%iC6aJByyTU?CoRgMOgGKJ)0ZSZUrd+XQ+uU&(ox=8ezAl@ z!T=@h8JblVbOM44(6uQqUMndrpBZ8iw%uj2oaWL=w-DXtj4`g(W8-IoC3ie}^A*>A zW(yXR)rVwsDvGg1ZS?1=Kr2~AEx5HBF_JP}W7}HS%1pK<1LFKI`3CxkM;zDVX%?|} zLJ>F^q6el9Kkk{dg#A9*{%2Ae3x&nWO;O8~FOi?R&y$|OfQ@LK_(hQ$Oiw-oP6yth zQ@s7#2>`h9;0bVP;=CYL#z{wjbOlfZriwgNUPnmKDkr8=hAYDeqDdpYJ6j=Acscpk z-E$ivxn))Ue2nxL)DT)wm;t6~)k@-N_R8L49sBU!chi`D!V%!Es)Fq^kduGqEOS>_ z%<;t&-G(j>J^z>bG6;v4LnMY&kDKARka`t*Wx+KxZ`6bMY0t{MPX`pMpREGM7KQc? z*^hub^1gn!Ayu~E<|%87!fWOn!_8CG^`0h65I(* zm3*bR0@=s&M?j>H%n?9wH(+*^ZI4aJZgSIJto!g{ex~hkTxaT&OkFvG!^e@FJnB2X zc>4L<2`CG&=w8%7RJ$HH_#7I z346G~P48--#AVhL4bQK;_XF)$e{^tbf=?NpyauzUPMTU?DYmisl;1QEg39WefQXNb zMl6`dW=w-MLR+wX)On1?y{;qrWu+b-xSnAMzZyb0#%M?@X?*DME_)KlgAi-VZ8^zm z^wXv$<)U6FOeajs{xbt%Q)-||Cl88}E_4o(nO2|n5!z=*{UDdoXx2qXduU*e^=U$I z^1NM7GbdLyu~BXq?wPLKQV$0c?-IiB`Y1SQK$gicwra=^k9hlHCK>Rx;!B;Jc|Rn2 zh6IZlBh@p`AOBxR5w$~Xi|P1Jg}H8f`>8a5BXaexa;ny#V&7e~0~z{vbMR@fb@%=(1=@q!gcx6Wz5O!M z5SOVf>AXSqo<G!1cO7J$&F*@=L%8CL@jG*>Uvl#?rbOzWOI{ zSm*|`YaSe6qk(P^BS4Pssa67`B8m0aJWvsNw*g-{KG2By`T&MV=ms(VEfE|nYv(2< zLa=bH4TuF{6S}9WAdmxu`CuFTuTB!-eA1;xO{F$gc4!R)%e6;AP zRtDp5fno`BCTNL&lS`R1!E}Ai(*&qycXc!VZEjf}H7}6@-M9*ju7j1mz7a07L3uMh zvPwVAkUVyNzIx0;QSlXUDdQL=IHIIr{1#pVCje6Ly}cg#fJOUQSvl#%6mW{IuV$)D zeL~T0)gr_>3L-ul`>U_s^E-%p2rP3#RzC-`fR$G?T%%(J=L?k z6}47IbPBXz+Y6YPZ3#!_Ct5H^!$YeKqAPs6t6)O3Ax@szO2|%GiD>6i{Fu}4T>PHO z69f_-$2PfbX`67lPvvk~uJa+{(w8IOZ7ZW77z-)GC5pKP+uyDjGZpLbrdwv^oP&y# zgzrzqvC!HYC*$CcvRX%_u^QJ`L!2usx}{{?6|V7gLVIH3ml_fi%@{c+Sx;7!^S8~9LHiaU=R3PX$Tm{mZu(?C)_A;$LTG+;P~mP5 zQ7iVa%(Ih1`sP_XO8U}eWD7vG%_M{tcpzm(ISvV>?{~TwpCY3?LptDyUj5H?1E-v? zJ!u?h6kVmy(zf|^mW$7e?Ah<4{YXi6e}lEWdb7==@hBR{>lB^Hg+^FzxR3eU_GF6o zYDOVOM7K#c#79n3@w)o8Ik^dO!1)wx{PTu)xS;(Dx`>ZBINIo1Yo(Xe{S9|kGMWf& zzlpTeD8~Z<|2UsjzgXhuFC&9hf5#;7nU8b16z)$(s?}Dr7e&ej%H-m zd#@x}Z4j=1UKts^B5$fA zadIRHeRkgwBqffY#;gd57RfV8MbeK#0?nK@Zo;hY3g^B|fry z$(MU6=#)^MW-Kb+G&Z1Lzy>!e$1GfL@PUiY%ypuYGuwEF9sFlj{F~cq+UoBi`*M_X zyz|pujr(?LWb~*9EvxYv*M?3}OnF2v#67P{F{(-_s@wa8(y_6NDgRm$zbKYlISF;% zr)0z7;xgOQK^Lb0v$q6_LOnZ_Nx0AiuT)GPyqfa)BSLH^mC_7s>0?c3#4CRrB3~@6 z8||&Tgy=;-et#AU&Cz*_My*F4D;RGDdcTjqjwJi|y1ReK-xSeyqNKxw1jTT{lMKW& zl?BBv&g5Y?kt6AW`@w5|&TA4NXVFUps)y>Qf(DDHpAPpl^*WE<`-MBZP1ue*qug*} z(lVuWZA0LBP+Wmzl^U-;ZX6r0*9yF*-H z1pZ+7-$BoRSI|rOO}4wddkR##UQsy^%S`uV1N%AIs(_KnjZ5;Ze{kGzZ`=$flKC`i zou^eyjU&!Ei>oCf)39%5lS8szGg(ldPNJ5IuPTKqDX(QRBE`?OPhR|I?Pb+LLSG+J zw?l&ms&N}Br8O)XCzH!C<~dt1u8z37lAAp>tLY)0IbBp#5$fwXM_fzV|Ex{;FsLP5 zWDFI}m$VVB&54RCZJz4&^7eNxsR&w}whD`76?;uM+>40sysBHc8%;Lh@N+WV)E%5x zCqJ!r!NkBm!sjED8ea9a(3|h(w4V=KBAFpLrte18b4|dH0dcZ~Pwh@mz5A~~@0q-_ z{X#It$?~tb<$E;r$CyF49>uW0-6yl4Bi*YU=WoES!9-vkVwNu$|1)DKzg%o6Y`ay6 z)T|qL+uJzKt}9^FxmKw*f?0#iy{CElzb=J3tgXI*rO5j3`~dbbR?1hK7ow!(Ki^p+WTzG1Ua2xzzi6#$W{2n>EQ@!$ zy6KAS$4a)gi)ubfT61(zwe$c#3qn8jav)K}a{P7Xqb;fwcsSw?Hl*=(yW~>eg+kdv zdk8lsXX9gwUrN5U_Mi+Iqd9?o(S5;z zQ{E{VhY{`y`9*K6n76UECy_2PaOf^j%gxn zH>c*1ZzuVux$Ihfb;&;hCK81X*!F0?%$gkmO_HX&3Y!;hZy5UR>#oZYr#VpouCHBx zDwZ^>9o%ef7+d)GxM&*!OS}+cVr*a}`fTFSep2PZs%huXH%xM^3wD1yk_h6xS=D|v zmp2Srt~^%?EmSlu#uDqcMwVc2WEkKKP9R8N+owVUPa6du3xMxEXxR`dr|(|9lNZXk7$`oHJNjVxMXkBeAW%7= zn_*XJTur|Bi~l?77q&`9f|pU==NxfRsAJR)#L-HEbI7i;=JLU_T62;Tg8N9&NREg7 z4dUZ5a$*Z4Bo2e8HWd{OH$mhXhCdZhhg9pewra;mhlKYfEtUzDguV?W60v;#sNpo) z*R~k{esM9onnfv?vs$XzF-PZGNRfGZuc-jF$70alB{_QGPw_DbILbSr0@)oBts3gF zH@_jJm8X-dM$qrN+OC(+R4R+zNXg(`sT&$GS0=eD)05)O zzX{t@!&$>FRHcK{6*|4{TpIoSuSVb>3=K)3jQl0}E2nds-T|dNQNxsis-gGo1#bkF zl^p};3+codP*w(z2nuT45(JFEzS8>KwX zn49UiQ_D|-eK4pc_nB?E(-B2!Z$k?$;)05_tzF0XcB-o}m*g2<8&9qh?`^s3h;l5m zwdagrC@JRd#c|hr8)_vb$FnMe7W1b-@d=Y3ds!557uo6A+8d-c6!f|;4HaE`QTN^U zY`&fBJq$F_MzT5CC|4NrWhu9(veZ$|t>n01kT&y6k^RxEw$}7s2yPU7ql-E=FJ_hL z&A9PEEW?cM9dgg8H6H=4FIx$xHAnHe+S*N>S3^R)+zdGEsNM427I?CXFT@NR<*m2H zo1DAjGjBUA@2e!OmlcJ395$lFtE1KuT}xB_a;$NDOY}`|hu?)#Ot*4yzFx{h7w39AvZ$~WVx<=ME!j`;K5v&$ zPJ1<#=OZJh-6Pv$4|k5XD|P?;9y~@PhO!hxhdYjCl6R&bf9}N@BzwQ`?t!Dk0)k4S z4pa>d<_yETqKZ6>)J;C%^#~ZrF6&8Q$;>7Lkvttu8-hE>r`7-+3&tJh%7-%KxM0AKTdi2{^5AMiP zOVAgt3OC*n7JKvQ$zgnv^t(_%AfF(d^kJZ3XST0(f?MY*OIBzlLMxt}M_{8g}k+Li4 zHM1!fi-aX-l6jGK;!CMam9+)eR9QmMk8nYlr#Yv^biHHH;%JEx$2)#`NOAg$DZ12~ z=Ce=GI`mwaTm4>l#S2H7uk@&wWUfdQitBVVE$-7p-!%0$PVhm)*lRE)lP*<`j#i@Y}juW$C-Ui>E`@&xOabD=&J(BqvnY> z&M|L1IV?#@jMuzW<)P#vZVi$WYWtbo3Yh%Y4hE& z`jVCDUUKzVEp^$2kC{?1j6qal(r{veT{Lm|##huST`fPZnZ-WL>-OvV&&2`d*S#QE zMxd@K`Wg^D9r)v)vLZN<&yedV8Xi+L49pX_e~XS0Ag=?6pV0f@ zyKl_hSFiUu8v`7kpYFLqmnq23QD_zb{xs35$_Q1_OwgL zp(dRg_<{tV{v^$i(Du_y=P|36re|$#6*m}u@l_N|3{HFkR5&v`Um~3YYXqLc{(`*> z{24t07XGr-VEbOp`-K}y6RW>3cIA*R$NlThl?30wRboF|sumOM2H~9OYMg-nz2Ex_ySmyrv*D28{@SWysA$<-fZNtx8ofco5TxwuA zZWDQtCYikVS?_az5!BqTfH5fk84_2JUhzLftJL1J-6qHyeNjD=0fA@2B_76lFqK>M z20xwe`in+yez6NrxtJo$i+vJtnf{A%LHxsz{$d+B&vc51|C#=&G=taD?!@cI>qZxt zI&y*~<8R`#l5*MHj_KZru9B?L9Ao)d#*g`zS> zd~YXCGRf>aALgVN6(W!n(?lKl`IYC77g}p%p1zq`l@Ot?!jE3LVSuX?OYSn^-X0?= zhs|{G2(aA57Ezb3RLO5>l(%_@A2l;L^fG%%{*f9> zZo6`de_5=FGSE!xi7&i!HxQT~eLdx?s8m)CkbVkM9A4YDJN$- z(A9G9!7=hW@a%3KATCsC@Ba$g(Mx`l4C<~>e)Dk-?vk{mUQ_(sD70W=Lv9&woX2z5!tkO_WrK1Wk3$a*rKP62!PTTN zG;PCniDdauDEyFdU%`jXy~NOdGC|OG&}B&62#?>a!GawBDNuA{N`$pCG%=8R$`~U= z8*dM|#n4U(c)1?Py1S=KZ%|BG5Tk8yZB!Y8oVLc*1Yo=qk_Q`$)!^a0SJV1ZnGtva zRk8k|oiyJM6n)9VO{!F~qGnJg1fR5UXuHGH{m)VoTczyNf7|Of1}F+G?D%1AO+KAe zJ=)G~jG4;x^znr~kX&X~NNXDmHekQYCv@xwuQc{Oc+VvC1yLthsGhe^mQTeQto51~ zymra+@0wWa%T)mcx34=Sy7ebzd(9IT`i!ZEB$Db zlNuo%l9f%Z7%zk6P+#?UV8hHQlAhUVxEswR-iP2sjpS}PJf8+=u=(zry`Q48HcbRQ z{Ro8_X?-~j%8|GupsPCl2%yj0zIi~?{@|Xl`&VRnf!vE~Y*^C-@}98d)4m*ynV9LO z!pLf!Lo>+-r7MJG?b;VD-<|?xeEJ<`go%jGOVpzqhC5+I#I+}6rNnZnzD((e^=R97 z06^wEQ5QFFej@eEtJ{j?$AEtW;8#IU0k;S+p{R&+as~vz8o)(*HF*yI=~0#FHD|z$ zm`FUI+D{C;=m93-Q(g*Omne+Egx!l(z4aZ*0GPb{{|j*!D6&fns2J{x*T94rT>w`Q z`;J)ho}^b6)QhOswHJ!69jw1uMRHBmk$FwF?wm)rCe~fNrVPt_e?l4qSFVOU)f zVx0mETgtY5UY?-xm1}7j4m`E8#fxx+9lQ?|cuPtC^VI`bav{{WQHnEUiY-S4iU+n33}I^WAc@>97n zXB10~xh=uc6Q^E+t|QP*bXx?3sct=iBp^ZqjJID2f?gufjdUXD#^O@xVUK-$k1F5h zCV~(q|6y>*ls2^S9*<_jq_E!Un<4!(Zf2Cvo$R1O`Z6T@?ezJZ$!9Urp4Ca=sqokK zw744@&Z^?Q`?& z5yOX(p?FWL#b!<%-qULlS$Q^?hNckZI(yp4=$hOBU0=*>GCtF3hfNnR-H(ab?4k(7 z$B6rXz?oWgQl|^t9ZqGY5lh3@(!XMBu_|13U4b`s(ZD6GKW=%!psiZPYc-p)*slRd ztH%M0^B8G5>*j3n_a6Nj6Vp$_y(*~6X95D^-XA)CH^qMvrUCG0uZY*SG)@JQALFKd z5E#_-mNersIPywm;BuZcs2t3c19WL1hQtvtG6ERE6?~f_IRkL=?!CZENj{tL{ z=S#;Iz}@Q;?t8-YSxqt+sdB!j=<%iH|>?=V=6^}%zJ~ofgXCF3?bd>hl_sOA|0<%;pLjp!>7sIBb}_bPSi>HBAZ1*|Qu{`GCt4}u3R&0HOruNAo6L?VY92=tUqh0TX zyV0$GZ`sWUi61|l8j<#XAaozqq1*Lt`P|c_u4NRO~ z+9X>rRi;9;S9VRaN#pCpL*8x;3_Km70+Fn@<1Wexcx+g_PPRVwgRg&>Y}#gv$(r!= zW=!vLa5srv=-|UID{c8k<~;u7>AkTUgO+Z?NIS*+!nDjBt&1pb@+eLL6r^cjJ!o~B zA}3MnQeM}2D6K;%B>B#d_75&>(lagIx>u;~7kkaw8JYmYnfI9L}I>&3)_sA=7)e2g?w) zw}g;SHj;Ds8Sv^EY88i1VY2ck|cDf=dd0Yc`ROJlY!owCN!V( zVG*mE+gu!+3F8dgS3b8-1+I~8xE{Pj^V_b>r_D^Bteg!KoHTG<&YGs*D_pNsKmEq% z=DEkZ#k>)G*L1u03F~C<|Ar${^^Mx{s4fuvK>ygbT>idwA+8@tahJZ8kpSQ|J7VAm z_b$)!4^)>ZteRp)oT6e3;jp>9XTGXc0qPThU#l?@b`oKYCB>#6+toGcSIe?*!fH5+ zmCK|$D!n9^*?PBAzYz|v(pwq&@P>D(1eDV3F-XRD<9MHE@~^?wHlq+bYKdEsbNjbP z6|k#{=FG(8rsrWw6Uj3hVnQ{!8$fsbdJZv>Akv*bUi#lkURe7dFuC5>uLTkWVvJ=$tBbDs;T-EzJANY!3|3ez#e=oTjyt|nTR#1V(t6K5#XWU$Gf&oz z>(^^p5CIKwG?Z0)JiglFkB^&f}7-Y|DAvztd zAvp2|&;3;Pf7Lu;P-|Uq7}N{2#4ZH5aAX`x`Mo2W!x^QX$=8=f=FS+O*5I?8kVf7YPj$krm!iT%xU!<`s_oVW@^`k) zoNi+)!7IOaeFwFbQAG^S0zR{35iBy|nQRm|$&o6OD$}vd**E^W3<7)!k2c z14mz)_AXijN5>?CsL*!TDZH09dfd94bzh-~#gKY9lWgLAq5L`8u?U!8CZV#%}a8JB!dDGC{@iV^hPKOlcQYj$ey|*XV}h zn*1&d?3Hvc_|ZJoWedaI2x2bFT1Vg9V}hccutxyC#x%bt{Ucu6x*wxyeQy?8RM__j z2gz(#GpNZ^YKbEInsDw@{}a3(lig`e?BhIXCuEZN%rH(XJ4ofXID=lCacFA+~@Zi*Cj`H9DMM* zpVqZo?#++$+(k~{x=ukxlg?||pYiSU6cl>L`j1zRWOI9B4;$>LD(}CY^k|E?oQ;rB z!f?FAZz1wh*k>-T=vBceXfG(AqBW#3QAo#}FN65S_fZSz7u|H5@5?iAv*}ARD71$s zPEGSjG?i=@{+QyFId1;P06$a2RY~9D@+euBobt?Iw6R0HGt708vk2m3)h1_n(|Xc9 z{FWA;={-!)F+#7cT3^e_DB9uv^xxPCGV#JJlgb7pEqBqJWKTArpzOEfd?YujR$V;9 zlT6P{Gt!mzjhHa|N6rs;`14Kg*ven5_e7Ru3tmoV$c~R7Ot}f5HKIdxt80GyNQ1OH zO@B+BBBJT;>+#(qm<-@;nt{NR5mv4bZfVIF9$4mZ`x9d#4Zh zb}!@ECOp;aZ~mT9SqCo^RI2P93hmDYWE}Dp1=LO)0iAKXFPoOu*R~E1**++|7Dzqh zdbEG#XC^@5PlSjmM}W(UkX46xVav^Hc+Z`t zX3t@**ZuKs=Di!TgR}iY94`+m2y+7pK5==&Tm!UA=N6t$b;)OXohO#LdLPvLImvE^ zh@@Ug>#e8>zSfmxa%;$*P*N8N2y1S%Fa3%nesO;Jd0G<(-fIB2b3C+)Kh25sDnPs$ z3NR>S`n}UT#B7mOPBbSz*)GgI8($C=B<{Sx}C7yYI74v(Jhd0?f@+OP$ z$<5!;znp0GJbl*A#b9_TGC|X9GE-RuchlpfBnMBYuqg{ty0lZImOpp%4J#G#7~Z6` zu<{1I_=6x0?mIXwG#r+8dHu!`gl}Y!^J~qMHQV=OEk2DH9Xji~a}LP9DEsdBud`!w zO0R%FOz{Sf*yYNqlUMpyw5<%NBX~s;Iiq`corCbDJ%i#O$+k^;^B%gT8faO>YCb@i zZjrV8orC5t{5Jn2+S}cqS+YutyDfLF121$1>#+vmaFpF8p8OL|^S{wsRAz9&!d-cx z2`CBAR;&$nT_y4FokYaWfX7bX=IG-@BboR&_! zge%-73HlsM?@zggFdiIq>P{x07Z16}^1l$ycb-E_z(gQosJ>uNT(Q+_kJ97W6fMLC zzHXYd8s0qh1b4xh7ykP7^vPy}nV5UoJ;lpH%08Q)3;Ii7q&1g|?GSD8t&-Aqnb2$p z1abr8C*;PrAGp6nbiDZq+|PF!@?l#_5RU?X>yyyIK^L`Ca`=U#;J1NJVjp`BUmEc0 zQ{%OrDshM-ppZ~o_(sghGlb5VSygmRTMg6M_}(R9&wTCdIO3NU5vPH%~T78p1D4hZ~t1V`K*PmJ9LuZ;ZIMF0M%4_YDFC88|? z20+kzZ)Pk*baoI8Ls zJf%XTOB0mH{n>p)?q1^XuuhfW=jh=6mVUhFd|B16h~^w!FZ)T9L}yDJY!Wu6Xe<*h zbDzX@Np$HLIkA;9jQ_J}8p}33y}PB*=+#v$?@J5vs_@15LYwHNNY13@q`G3Os!X2g zFc+EWkZ!-Qzv%l-ETmZn*13tC3n#L*h~HDr?FXHVPlC9$qY6h_czug@oC|tdSuf8G zBZg0$!a89cF9{Nx53KND_8=ww$7p7Z+=!k!zXj?uKdN^xnkb-OTXMf055IF*=Jk8C zyePW6=2-4BsK(T=`M{EAzM#mj#XB~XtPG@gH&>*?)pA$G|W*mY%0&{%2Em=mdf$A zMzuxPQ+iu#l36UPxoDCJ<8`IyAl3@&QX+$>(q?|Pg{1R{*5;g(%I<-r;hk*`X4iHK z-xB5gWnnNu<$S@a>bb&I`l}l~HnJC!Qr@i*CFXcJXZrdGm^thT2%V5q_%I&6S!h4` zrnh0DB>6Jt{9%T8RDciLIgt6OMoYUdN#+qX>qH+@2k2vp1+~{Ulvnlf(2SLcoF?`q zy$2QvFN=ZxC<9QhWdBUDRHt*svXuCD^(y|Yk$tnBzX#{PqyBvQfa1i1zi)VYS)M;1 z0TIv5%bKD1@YdEiHLPi`m|sB2kRKEKy4ANrUi{;(VR9nOt;Ee+<{C|80%mDrme;ZP z*Xeli>d7X9g*JI`pCQ`Z!pLvTFVD<`J)xoL)QOT*m%SI0Tce>nxw39o`?9DcN7k|f z{RsOoh#XpNuo`3edGn4wrg@V^>nk_gUi%KtseFc|E?i5{lg-)(lWAeqg+uh)Tx{!Sy zwT6l5EbG;jSNPGsdnM^z$ux}0p;+GN+(b>77zbR_*)&>b+{U-7EAa2t`#VO?P>947 zF`0SyYC@?AuAN-^`2z>k5Og+XbJ56oFhTcJ_;6MijfPg(g?Kfrn~w)s_T1fC^C@IPLyWiUR(a7&?5mTDfuut3XS&zpG7O|HnYkPJ`Fmlg? zMD3VJqi0k6d?0W`1+x2%cPF3J-<_{7hPqkeaVW`V0i0+yzSm^;lB}n|DrVW z5x8#QhbPB;2=-1Z6bk={nxt!FAwcjZ5vZv9<#ftZ)O?&}16;PeKes<4g6F-TZCoPn zFBg}$)O~OGo@2~KEJKP*Es>Ifw;yW&=~y9q0Z?lz6vYjQ?uz^j)aJnrcd7s$;C!u!b=rpG8x5~0;&DNoV| zM0o!H5&j>go`^Kvxk|sfCUpYGmI(aM7XE)$o&?ttQxyuvsP}VEQVppLT%G8$;5ihr zw-b#l7+bM*r%hxrQ^Yv@W&?eLej553EhVXp4sGY*Gv=zFUwDUpoMQslLTF`7GD>kP zRVwk7>~XAY6`5L##*@hV=WX6XEa%BP>rj+~Z5t~;hGV;~vj zW%Yt;N_?f*Rss8aiCl1}AjvkD=yoRXUQjDg<)!)#jIF)2!Fj=sdhucisTNu7Taq)8 zV*Y%}PGq`2Iurr=C)-SvzzF^}#d9t0IT)iaQT;B(yZ=Y{FHz5_%}f8mM5k1w*dUaK z4}M}ZyE-Ie-({nj6Pd(CNzbnAmZ9;$PMueyJzgIUw=2rqIuNksWMnMeEXa9~6NVB( zBKi-riVzhVS_3UIO{07>MSCf;JAFyem-8^qgh?Km67Q1Eo<>4==2JY=9M3(cb8fs) z-qzAjG>dL;$}XZKK9%{!J!!s!P`4-WeH^{sp>{){Oh%u}9JSmy{WLSYu@l}jmhUPz zbkB}iutcO-N+)}oxJw-z6p-|doYHnr=gn~!=U{59rhI`IFKO2wh3HYVZaZ1OqlGpmV| zn}XC~Pv$sya#plxF5Hj3d5Y`0RwgXjOIhZHamI-8;p~Lqvdc(GNiLr)0a|(Uq8PuL zQ8|Vl8PUQH3&ooVP3Siy5#S-dWWWeasO8>z7OHIvTV+dU`@p{zibq=<0?Nc zn?4_~yqBq+`WWsQ8o?)GUmXSYD%L43p4efF1oZu}TbKTV1&&>g%_E>S#K>Y(La1A) z^zb+Mq|h4{1${Zpqek0`zDcpFAhF|9i!=Sw;QzHzQlqQD2^`2@#z-5AI0kciH=k_N z3tQOWOw=yJ2^w&EZN^Fm3z0vKL273l0a=gCGzPKs90}FiHOpOffPdUc%~Si*iL*c>3~qpJQ1{=F&b>&wk+}Fl!F!LHkyz27-!hJRrt= zraFYTVt(rFfP7bxnAYTVWyro}azRybdrR}<=D+9-_`, so a - // total with no companion read as a different, smaller figure than the rows - // it summarizes. - const read = cacheSplit(entry).read; - if (read !== undefined && read > 0) cachedInputTokens += read; } return { requests: entries.length, totalTokens, - cachedInputTokens, ...summarizeEstimatedCosts(entries), }; } @@ -696,11 +687,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { {t("logs.conversation.totals", { requests: conversationTotals.requests, - tokens: formatTokensWithCache( - conversationTotals.totalTokens, - conversationTotals.cachedInputTokens, - localeTag ?? locale, - ), + tokens: formatTokens(conversationTotals.totalTokens, localeTag ?? locale), cost: formatEstimatedUsdValue( conversationTotals.estimatedCostUsd, t, diff --git a/gui/src/pages/Usage.tsx b/gui/src/pages/Usage.tsx index 44028824b2..96f0f1db0c 100644 --- a/gui/src/pages/Usage.tsx +++ b/gui/src/pages/Usage.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useI18n, type TFn, type Locale } from "../i18n/shared"; import { formatProviderDisplayName } from "../provider-icons"; -import { formatTokens, formatTokensWithCache } from "../format-tokens"; +import { formatTokens } from "../format-tokens"; import { formatEstimatedUsdValue as formatUsdEstimate } from "../intl-formatters"; import { readSessionListCache, writeSessionListCache } from "../session-list-cache"; import { EmptyState, Notice } from "../ui"; @@ -64,10 +64,6 @@ interface UsageModel { totalTokens: number; inputTokens: number; outputTokens: number; - // /api/usage has carried these all along; dropping them from the row type is - // what left the token column without its cached companion. - cachedInputTokens?: number; - cacheReadInputTokens?: number; shareRatio: number; } @@ -78,8 +74,6 @@ interface UsageProvider { reportedRequests: number; estimatedRequests: number; totalTokens: number; - cachedInputTokens?: number; - cacheReadInputTokens?: number; shareRatio: number; } @@ -567,7 +561,7 @@ function UsageModelsTable({ {formatProviderDisplayName(model.provider, t)} {model.requests} {model.measuredRequests} - {formatTokensWithCache(model.totalTokens, model.cacheReadInputTokens ?? model.cachedInputTokens, locale)} + {formatTokens(model.totalTokens, locale)}
))} @@ -627,7 +621,7 @@ function UsageProvidersTable({ {formatProviderDisplayName(provider.provider, t)} {provider.requests} {provider.measuredRequests} - {formatTokensWithCache(provider.totalTokens, provider.cacheReadInputTokens ?? provider.cachedInputTokens, locale)} + {formatTokens(provider.totalTokens, locale)}
))} diff --git a/gui/src/pages/dashboard-overview-head.tsx b/gui/src/pages/dashboard-overview-head.tsx index fea1ae94e9..44b3f8ac3c 100644 --- a/gui/src/pages/dashboard-overview-head.tsx +++ b/gui/src/pages/dashboard-overview-head.tsx @@ -1,6 +1,6 @@ import { IconAlert, IconInfo } from "../icons"; import { type TKey, useT } from "../i18n/shared"; -import { formatTokensWithCache } from "../format-tokens"; +import { formatTokens } from "../format-tokens"; import { formatUptime } from "../formatUptime"; import { navigateHash } from "../hash-routing"; import type { useDashboardData } from "./use-dashboard-data"; @@ -81,13 +81,7 @@ export function DashboardOverviewHead({
{t("dash.providers")}
{providers.length}
{t("dash.tokens30d")}
-
{usage30d && usage30d.summary.requests > 0 - ? formatTokensWithCache( - usage30d.summary.totalTokens, - usage30d.summary.cacheReadInputTokens ?? usage30d.summary.cachedInputTokens, - locale, - ) - : "—"}
+
{usage30d && usage30d.summary.requests > 0 ? formatTokens(usage30d.summary.totalTokens, locale) : "—"}
{usage30d && usage30d.summary.requests > 0 ? t("dash.coverage").replace("{pct}", `${Math.round(usage30d.summary.coverageRatio * 100)}%`) diff --git a/gui/src/pages/dashboard-shared.ts b/gui/src/pages/dashboard-shared.ts index cd0f7ac66d..029e39b2da 100644 --- a/gui/src/pages/dashboard-shared.ts +++ b/gui/src/pages/dashboard-shared.ts @@ -123,17 +123,7 @@ export interface SidecarPatch { }; } export interface ShadowCallData { enabled: boolean; model: string; sourceModels?: string[] } -export interface UsageSummary30d { - summary: { - requests: number; - totalTokens: number; - coverageRatio: number; - // Already on /api/usage; the tile showed a bare total only because this - // type dropped them. - cachedInputTokens?: number; - cacheReadInputTokens?: number; - }; -} +export interface UsageSummary30d { summary: { requests: number; totalTokens: number; coverageRatio: number } } export type UpdateChannel = "latest" | "preview"; export type Installer = "npm" | "bun" | "source"; export type UpdateJobStatus = "running" | "restarting" | "succeeded" | "failed"; diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 18fc2060c0..33b960ac70 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -718,7 +718,6 @@ "grok-sync.test.ts": "providers/xai", "grok-writer-boundary.test.ts": "providers/xai", "gui-api-error.test.ts": "gui", - "gui-format-tokens-cache.test.ts": "gui", "gui-management-session.test.ts": "gui", "gui-pair-capability.test.ts": "gui", "gui-pair-client.test.ts": "gui", diff --git a/src/cli/usage-report.ts b/src/cli/usage-report.ts index 2d2c525cde..3311a781a1 100644 --- a/src/cli/usage-report.ts +++ b/src/cli/usage-report.ts @@ -17,13 +17,6 @@ interface CostRow { model?: string; requests: number; totalTokens: number; - /** - * Cache-read subset of the row's tokens. The API sends it; the CLI dropped it, - * so a mostly-cached provider's TOKENS column read as an ordinary total while - * the summary line two rows above already said `cached N`. - */ - cachedInputTokens?: number; - cacheReadInputTokens?: number; estimatedCostUsd?: number; } @@ -60,8 +53,6 @@ interface UsageReportInput { ambiguous?: boolean; requests: number; totalTokens: number; - cachedInputTokens?: number; - cacheReadInputTokens?: number; estimatedCostUsd?: number; }[]; } @@ -85,16 +76,6 @@ function count(value: number | undefined): string { return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : "—"; } -/** - * A row's token total with its cache-read subset, matching the summary line's - * `cached N` wording rather than inventing a second vocabulary for the tables. - */ -function countWithCache(total: number | undefined, cached: number | undefined): string { - const base = count(total); - if (typeof cached !== "number" || !Number.isFinite(cached) || cached <= 0) return base; - return `${base} (cached ${count(cached)})`; -} - /** * Matches the dashboard's `~$` with four fraction digits. Estimates below a * hundredth of a cent still read as a number rather than collapsing to $0.00, @@ -160,12 +141,7 @@ export function formatUsageReport(data: UsageReportInput): string[] { lines.push(""); lines.push(...table( ["PROVIDER", "REQUESTS", "TOKENS", "EST. COST"], - providers.map(row => [ - row.provider, - count(row.requests), - countWithCache(row.totalTokens, row.cacheReadInputTokens ?? row.cachedInputTokens), - usd(row.estimatedCostUsd), - ]), + providers.map(row => [row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]), )); } @@ -187,7 +163,7 @@ export function formatUsageReport(data: UsageReportInput): string[] { // wrong conclusion. Mark it rather than presenting it as a single identity. row.ambiguous ? `${terminalText(row.accountLogLabel)} (ambiguous)` : terminalText(row.accountLogLabel), count(row.requests), - countWithCache(row.totalTokens, row.cacheReadInputTokens ?? row.cachedInputTokens), + count(row.totalTokens), usd(row.estimatedCostUsd), ]), )); @@ -199,13 +175,7 @@ export function formatUsageReport(data: UsageReportInput): string[] { const shown = models.slice(0, MAX_MODEL_ROWS); lines.push(...table( ["MODEL", "PROVIDER", "REQUESTS", "TOKENS", "EST. COST"], - shown.map(row => [ - row.model ?? "-", - row.provider, - count(row.requests), - countWithCache(row.totalTokens, row.cacheReadInputTokens ?? row.cachedInputTokens), - usd(row.estimatedCostUsd), - ]), + shown.map(row => [row.model ?? "-", row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]), )); if (models.length > shown.length) { lines.push(`... ${models.length - shown.length} more (use --json)`); diff --git a/tests/cli/cli-usage-report.test.ts b/tests/cli/cli-usage-report.test.ts index 1be03f68f2..5399137a57 100644 --- a/tests/cli/cli-usage-report.test.ts +++ b/tests/cli/cli-usage-report.test.ts @@ -404,28 +404,3 @@ describe("ocx logs --follow output contract", () => { } }); }); - -describe("cached tokens in the per-row tables", () => { - test("a provider and model row name their cache-read subset", () => { - // The summary line has always said "cached N". The tables below it printed a - // bare total, so a mostly-cached provider looked like an ordinary one. - const lines = formatUsageReport({ - range: "today", - summary: { requests: 1, totalTokens: 58_000, cachedInputTokens: 57_000 }, - providers: [{ provider: "devin-cli", requests: 1, totalTokens: 58_000, cacheReadInputTokens: 57_000 }], - models: [{ provider: "devin-cli", model: "swe-2", requests: 1, totalTokens: 58_000, cachedInputTokens: 57_000 }], - }).join("\n"); - expect(lines).toContain("58,000 (cached 57,000)"); - expect(lines.match(/cached 57,000/g)?.length).toBeGreaterThanOrEqual(2); - }); - - test("a provider that reports no cache keeps a bare total", () => { - const lines = formatUsageReport({ - range: "today", - summary: { requests: 1, totalTokens: 58_000 }, - providers: [{ provider: "xai", requests: 1, totalTokens: 58_000 }], - }).join("\n"); - expect(lines).toContain("58,000"); - expect(lines).not.toContain("cached"); - }); -}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 5e57040565..9ea5f32928 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -549,7 +549,6 @@ "grok-sync.test.ts": "providers/xai", "grok-writer-boundary.test.ts": "providers/xai", "gui-api-error.test.ts": "gui", - "gui-format-tokens-cache.test.ts": "gui", "gui-management-session.test.ts": "gui", "gui-pair-capability.test.ts": "gui", "gui-pair-client.test.ts": "gui", diff --git a/tests/gui/gui-format-tokens-cache.test.ts b/tests/gui/gui-format-tokens-cache.test.ts deleted file mode 100644 index fe38fb81da..0000000000 --- a/tests/gui/gui-format-tokens-cache.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { formatTokens, formatTokensWithCache } from "../../gui/src/format-tokens"; - -/** - * A cached request's total is mostly cache. The logs table has always shown the - * total with a stacked `c `; every other surface printed the total alone, - * which reads as a different, smaller request than the rows beside it. - */ -describe("formatTokensWithCache", () => { - test("renders the cached subset beside the total in both number scales", () => { - expect(formatTokensWithCache(58_000, 57_000, "ko")).toBe("5.8만 c5.7만"); - expect(formatTokensWithCache(58_000, 57_000, "en")).toBe("58K c57K"); - expect(formatTokensWithCache(58_000, 57_000, "zh")).toBe("5.8万 c5.7万"); - }); - - test("a provider that reports no cache is left exactly as it was", () => { - for (const cached of [undefined, 0, Number.NaN]) { - expect(formatTokensWithCache(58_000, cached, "ko")).toBe(formatTokens(58_000, "ko")); - } - // A negative count is nonsense rather than a cache miss; treat it as absent. - expect(formatTokensWithCache(58_000, -1, "en")).toBe("58K"); - }); - - test("a turn served entirely from cache still shows the marker", () => { - // This is the most cached row on the page. Hiding the companion when the - // subset equals the total would blank exactly the case worth showing. - expect(formatTokensWithCache(57_000, 57_000, "en")).toBe("57K c57K"); - }); -}); From 3c3cf423769362af29402b0328b16c5d7ad23840 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 13 Sep 2026 00:54:36 +0900 Subject: [PATCH 2/2] docs(devlog): capture the restored logs rendering with Devin cache present Evidence for the revert: the logs table layout is exactly what it was before #4421, and the Devin CLI rows now carry their cached line because #4419 reads the usage field that actually holds it. --- .../logs-devin-cached-restored.jpg | Bin 0 -> 14984 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 devlog/_plan/260912_devin_hardening/logs-devin-cached-restored.jpg diff --git a/devlog/_plan/260912_devin_hardening/logs-devin-cached-restored.jpg b/devlog/_plan/260912_devin_hardening/logs-devin-cached-restored.jpg new file mode 100644 index 0000000000000000000000000000000000000000..665349ab1cc072ae07a1df99b37432e3928fc918 GIT binary patch literal 14984 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 L`UTFmH}GEo?9SxV literal 0 HcmV?d00001