From d4ba2eed42f70f3e15511c3b1fb77d412f230fcd Mon Sep 17 00:00:00 2001 From: Marvin-Cypher Date: Wed, 27 May 2026 10:32:42 -0700 Subject: [PATCH] feat: add tabby template --- templates/config.json | 22 ++ templates/icons/tabby.png | Bin 0 -> 46728 bytes templates/prebuilt/tabby/README.md | 149 ++++++++++ templates/prebuilt/tabby/docker-compose.yml | 287 ++++++++++++++++++++ 4 files changed, 458 insertions(+) create mode 100644 templates/icons/tabby.png create mode 100644 templates/prebuilt/tabby/README.md create mode 100644 templates/prebuilt/tabby/docker-compose.yml diff --git a/templates/config.json b/templates/config.json index fb4b8076..fcaf2fa0 100644 --- a/templates/config.json +++ b/templates/config.json @@ -3429,5 +3429,27 @@ "diskSize": 10 }, "tags": ["AI Agents", "Developer Tools"] + }, +{ + "id": "tabby", + "name": "TabbyML/tabby", + "description": "CPU-safe Tabby source and release verifier with health, demo, and model-list endpoints, without starting full inference or downloading model weights.", + "repo": "https://github.com/Phala-Network/phala-cloud/tree/main/templates/prebuilt/tabby", + "author": "TabbyML", + "icon": "tabby.png", + "envs": [ + { + "key": "TABBY_REF", + "required": false, + "description": "Tabby Git release tag, commit, or simple branch ref used for CPU-safe source checks.", + "default": "v0.32.0" + } + ], + "defaultResource": { + "vCPU": 1, + "memory": 1024, + "diskSize": 10 + }, + "tags": ["LLM Inference & Model Serving", "Developer Tools", "TEE & Privacy"] } ] diff --git a/templates/icons/tabby.png b/templates/icons/tabby.png new file mode 100644 index 0000000000000000000000000000000000000000..450b6618082ad11583e9b0fdf57f5c5da44eeeba GIT binary patch literal 46728 zcmV)ZK&!urP)(;upZmnDE*1ENBty}BXy0vbtTkF=kwY~>SqMLA; zk3X)^0k@iNd96iDO->|STBFfu-gn=9I^9~|b)7o(WpAO7@5$$j9aXLBv{+Qb;k2cP z`}#Kvh10qF?)%m6yK8sT8Sg8f|Maos=JK>AX+cjU;_!vLa^dcoON!)JpiCy)fAnaL zZmn0>|M;zs-~Zf+r$@`xQn%aV3HyRUuc}zpVyR?g3x#UAQq6|L9UF)D-G2Je9S0Zh zyYI0weV?qG#(00@H-5wAl&iN7`Q0DN#y5^8bMcTVt5((FO9TRg7u-Jog^o`5`qWVW z({+!U@9Vp>AxV-!KY;7xD+%UB9YOUgEnr2N=t>tD*s-$y; zR;gMxNwIghoL+mmSUK%;dB5uQTTlM`|MX|;xOOJJ$(Kqu$#PTKWl!$I%kN?l#=~77g^9PRK)s>8Ij<)o=noPuL zX=#~GpL&IsSJ$Y8|0ZW9>CkNl^Ig5Ye`U2=|Jv*GU&_|9sfQo_CmKJ`_k&0I+t$ZF z{xi=0K;&?(TKZ6Ieg3`gf5$!Pfv|Thv9Uqvc#1N)0+s8yN~)bo4O|PGlOnxC)X_IU zn%=B_<;!2TB(~$3NU-CHhu`|pzkA1re?Crc;&sz_-cq?NlVQ3m^?Ef|Dj2ct4N_pN zQxgMpaQ`@6n_r^OJpLp-|Kb@scX`=AvwPQ1-f_pFN5@C)e?8ng@;?%nzIL&5@7ov; zf8QFf-x0nqo=E?K%VO^P$dCW%Ry7yf{mj?DL1w){&4xy`Mw1HVI>oXj+Dzmqp0AMI z8=}F{aeDZj?{vQReIKB&eCexQmo8j>|4SFnYw*7R^3A;26}m~*&wco3EdHQpxPPFh z;PZPtxopb4v9YRNzIvXn&0VCEihdwvEk={nc{)NAdmp zy>>JH&wlwMTmR`l{r>k=WDO&eeQNz< z7p^YQ<%JDe-pW!LSD@S^%2jpRPNcE<7s=)J(rvfjL3XFhbK-^P`_kE1$Jd^I?i=(b zT{jJExN_xjYc1)zZ9BgDpYOPBe}jx>@1=`px-MNhYd`zSOH`@WXlCjFJ^JX6;YO^{ zpZ>)Yv>8ic`$@PdPIGvm-#EB;I=Xk)$Sb>dP5)nV#In)(-%r-0Uw!lA;ottXU;g<* zHuhtaqk}U;y&Xzyb4f1cQ?^VpN*fznlrGe%VaVdE(ln`9HK^1y@NbnGrp(4q7R0te zubbZg{&!RV;2?eWFaMI3=NAkA<9GhsgT8+0!ua^x8}ueyH;w20o#T&Ey5E`J+SvZb zz1`ibhS>_{^I5-1rp52~Q?Xd2wUrgxSlgh^j&9n$cRxuMD<#u8Dwir!F<+3QTU)NB z`MLgVB5^m>rTzKoKF2Tpvk&Fo|HG5DPk;K^H>DAO_K*Mj@t2-^`sYUnd-sj?cUnv7 zs5KQ`vy}5`3iy0vbGV7DE~*(8%2YMV78{fY?kzWEYDzXTtZq`RHUTU2R+CIwB7ZPI z<6~pAaOJYSoXvKu%&nh1_Lg@Po_+S&Hx-1qX^i(*pLoLj-QWF8^T$8($1n-`Z>c51;s_#fRVjVHokJ-!w-2)aM@`e&X>z`qkSG>^(F++UM5GIVH8d zCRg(rsuc2+h{eR|vz^RREQ4UJ)+7VhL$Nxjg+C3&N*Z8=-ZDtV8Ea8hoM08QIUTfj z_Y4J`E;{|f^IbE$CzmD%yO#gr$*1b{CR#U5z;=DP0-irz+t@tfa!bWiTC6RTp&E4Y>J@6qHoEKRy>#1w17w%1bpGY@R47%bj^lD^GfVMwDHM-q?o7p^ z{J+2c@n0H<1n(W{3wuc` zQ7*YDRr4uoR0_blCN{I35akM^^ma5hHhT%LdluK^O%pHNbjJI@10THnsZajfL?TrP^mlc( zBB36g`ZbGIsnWJHMiB&VrPUN&ym*C7U8Q^Pf&m}gPZr$F(`V079CQ(}H!f7D(O9Q= zJR0uo4BRuE%^vN}7m}{6)vqUFv#+>Z&&*zZ<&Up6EK9s9LO%R@caAnY;^tbrn)9 zGK{=MdK0h#e>C?P6syGP!ooZ)EX63cl_%LyX#eiL^x#|Wr{3O9`qEQh zr7u78EI3WZLkc{Sm2^WV9bCN2p-^vknEJXyG(9uY@&-IlJ8g~=7S%Ez@Q0!ie<ecQnSO0Qm_s7o7*uDlRAUP;qF)3bXQmR;|M6pUGz=np-=BWZUD5N7; zGw`RW7-SkPJU71N7Cdr``nx*lz3+HCb$5j5e|++HXlQuoTreE^H;?}KFMav&;loXO z6Rexg^YWUU9R2fWpF91wYE2spC^qVdbdk^FpoOn5P^+m^S4SrmvJK$eCN0j-k<%j4 zyB>KvJ^1ij>D>GkT3Fm5uhU1GrjZE)Ce?xnyiD5aD&@0r+SpoEx;nxS4G#}L)Zf?F zaM^tqn}+&jU9~^k<@c@*4*pmQZ{xe?P|VI+NL6hS=YX^6DErDWGg7r0{jH{D2*y65 zP_tjx%^^*1M2xy_Xb6&8dJ6$lL#b4XQofvR7Rx!Up*QquwV`M_&U)D*%kU$uhAx*X zC2PJ|^!xli>D>?fV_H1A1Mz|_Qbv)gz>D`aSO${~(8edcBQ$3OL9>h0^pHQC(TGRI97IaNVqr6&npImNhDaV~r(K zluTtPTPQ#zD3D&`qr8qko5I*?IK~Oy&JFJ$@L9?4lgWmoJQD6Et2aUw(+MNAP%4|J z&5bo|JRAM;$39B^9*Gv7{|dEANm4)wnlR29471z<2(bI8p*ms2Wq4aVu&hSuY*9Q% zp;8l`*N_+y<4Z=shHKDl;QM-=Y>3{RR+S`Er%(Lr|AW?7mg!Ia?9ZuZVDQX=!-xOh zU;VZJ`Z&D_)=lYod5um^{^{DnwQ*T*cI+D)H+zRirAzZ?Nvmnp*A;;)F=+nk3Z>I| zU|XH$7uM;kPoJQN-~SGJ?6F7bvw!}V!sX})Sn7=yB5)hYvVy2vA`85@p*W}pOK+46 zV8t|TM7QY5C4+jr4#n@b1tJ~2cYA!^qq3wlH5fdoCZ*YI$(3r+Ual9Z1Pq=numR&| zx`ANG#9M-wH8cwQJk)@~<#YL{x2Kn!p!%F17dDCw@CD44j8mgtCcDK3wjfKJ`6wmw zRVu^ix;w-4fpEp+pT0KmzQ5ZUJ1V>y%HGDUZ)pY{-<&Xq3q} z@cU5O;BjR3`E+NQec@V?t z;>Nvr>KyrdL-dxn-AAjlS7~WsnFogzwyVvP*~-2{(sG%N6enh1O(vca2M#F5-=RDn)R3i|BS<+B0W zOwvH#5NR+L77X(B8tAMHl}b4{VHkZOLv{pSg^C6vBnS!;g#rP(ecvv+>) z3=Y2Vy!h;OvOw;w0V<>@27Ft_<7+L#=Ct7&sH9;dDqtNLEVP(qc=f9d+xhR;m-rh# zH{d;8x^kKBx%)2a>g=Sgc*0h%H;+H{)HfCpIR5_4o;zHYPn1tUQ0y~LR2g<4umIZN|c8;Y{9GDee4K%0VOi2B&CvRYPC3U zv5*EMW**d#`6z|?!5Co!?IRY(q2X~Qc(qEyAYI4b5ycluRTy*?-)jo5W(7yu0ngt% z&=0ucqFgRdPy=CjUKnc!4fgfW$iM)(!Lc@Kb&AEJv=!T=IMlEl_*n#Qq{u2cTyFA* zJMo+{@OYiPZZ}wyJ@nSQ4wI_k_+P$4S}}vyFOg((lj-o2X7P}2^^y$8qq_WLg+O7k zd#H+yYv6eZpM$?ld_EI!!Gytbh{*9>FTQ&qZ&*UjsBu|`M7PR`C=9;;N#hx+>aRJ+4NZl?#{&4=Ko0SxINA0*FG zRi5||^X(tt!{SwQk?6G9vO35lw z6TzK=APAsU_-MD=PN8swLLFgp`(5zfprwjg&{1`~zMXvTFg#ZW-WwPL9Pdd4QpK$- z)hi|7-2$GWP7a5KI{Y5;g5vUcLZslctO8mT;XZBfthepoLw6n8OA)t9sglJRov|U_$%> z7(TdV9Os!-o|4ftxG$4BgB}>S3!BW2K+Q&eznh$3L1c@Qw!j!HuB=fK%GmAuchUXF zZl|%H5XE7*`Baotz#O~VOR5*1Ib9aaLK#X~3xQu= zYnW}Xy8{|tyQpT{aDnB;d*(+hEH2YM_a3Ltt}b#oor_?hpcX8^4VRT!{d~SZ{g@|!C>Hh;b|+?Dmm>f;9r%>x=tm~W3!j9 zP`R8F1~WX+P0=j`WC(CngN+w30emM;R&l&a=8iElp9F(NXiwc?7d)VgCWi)Tw7-|a z!613yF_Wn*%|YFpot>lMi4h8PhTy5K@cbH#IYyaej#~95*%f$M{A^;dgB)^!Txy-H zYWp?yhQ`%QbbfA~GQ|q@^mWoh58gw2Ci|(B*rZ${N*X?s)#{=|wn-LuH-&(uTkfXF zS;zw;8wgNU3t|Asz`Cx9q$M53ZixTn*9@3;jMtO}Ss8o*PMw=7~SwZF}ARiMgEiM8Nv%we?@Y=`?{7al0VYeVyGO3QEQo@m} z2t$VPBJg3k)ors-53t|8NA}VPJgv{|6dT0paMI>>lAikd35v&()Zf=ndv_n8SUyY3 z8(YA$U_2`D+IY_aK!@EEL!jNd@Vx~2?KYrkpa5Xz3UGP4)TDC@pw_@rSn+xfJ$N@A z+C56?=n_R27s#kpfN)%-k&CXatWd65pc?SH#pwlHQOOCTQ$e}a;dx6CBx-sC!JbaS zbQ$ZKJ0^_B5#(UTz9dZgI*e#rut2=HSWOUvGAOu}^#o1r8WYAlKQ|AMWf`7ZUAc>H z8Snr9)$JWzy>RB`FXgkDACd1-E&foDu3mZx#=Ah_jvzc&2aex3oxQXL1NPBICXFKz zD6?9jW+^XjS_ssY14g9d#@4{idV*dm*MLVW%pAz%1#XUn1GH~)fPUni_mKpwd-|1A znGN!N#b!vzzX{{~1dVhAdpJ zh}ym#^RkG6w~cFMgVC$By17jUZ$ALaY!K417wCHmUeo}wTBiJvBqKSE!6 z`WgDhv(Hl+zZ2>nB>&J5{ngi=p+c$v!}NeRh2WropAFk+Y9L5`5j**TQ#UgRAjt=e z+@jUBZFsf~yaxns2>w*3K?nDZ(@*{Q2Vj(oREk4{03`5&IcVZ%b8B0)wXi`$6Z@TxGnmZ1Di9Kj|IQzR0hwUu?})oZikmoA;|rJFS0;iflUn4CE_GM-8& zwol<=zW0#_@2BN!mna!qMeyUK+i$yrQXuIs&qu|9s8!0;$u}SnrWU+!9#mEy9+*Xf zN*$~O@TA*`1D?H@q|rkU-Ani0eTXa;jh_7MU(xF7GX4C=ehIXhjXv}E7wHo4ZMFoF z!{Gy_HfVNfg=FArH#SKSEJHCBBj_45(&42pcwTl=EYBvgWmbSQ}&gjt#EWs1!B+r?9J=l`xhf{nKhES?6nI6Bf#yydXn1cLrHaK~vn z`P_?;Rx|Y02Oc4Jpo@O*5B`MCoWCT*4<`Lh1VlRi<1pyRp#v1MxhQ^Z2^LVH=^j9Z zt{|{CL=f3dSLWmN%KSQ&YT)=GfXI+(2S7tjPmj`Vx9_Dr`^FGFHfa9Bc~anwoiNOe zZ3Jt@5_nrTc|u)u`@!33_R2iPYMX#SHL>Y*fb%9@=caLS;PtK85G;Y$fr|yh07bC{ z56qs|Wbe9DAk5fTuo5P{7QC5l!^L4n7O0Q$Sc3X`J1LCK&xt%OQ@75}UVZHG#~=Ug z#~yp^)%fpbSvS4$9y)Z$SXw$=0tL2)Bf1caZ-Z!UDoprCx3|Sj>FMnUrcAFsPKp;RA4t0%rP?mSuBRYW>4KNb@-64ADZO3V5*C=JN0bYLjG=vP3?z-nK;Bp7( zw?6ep0_UbdiwSxH|8qF4G&I~#yLU|^pi=16*Po#gP<5l@-7s9pzc_v~xiW2lEx5S2 z4h77>?|7&)(m@?yD`qCg=m12B{{9Ydt0juAuaREOQyHQQv=OqpT-1aZ!aTJnSEpdG zL4Do5)P;@2j7q6orXnzU8A@IQURnhdvO2ISAylNwZ3d!_pwMXT@Ym+wWoK;#?)u|6 z_jeoDbI+bB0UPWHa_jYm-82pBAwey zON*0z-2oVYE*N<$j#LZeSLS$G${iaYqD;*sl+TxF-~I#C+doV%oxM!oc@ay`9KXO5@|Uai+T!$;tXI* z5)Zkl6gQdPz{UBn{PppVe_X!)_iz3BZok8k?(yTtHJ756hDU~dt&eI=1@p)iME24HQFsMWkzlDzM8KSq}bBOj1_0ZK|*9}FlmIXVq-BtrFCgRWg$p!1h!X?A{@w&EGel{o@XsHJdU27_Y2 z)hnCem$7j)1f)V}0hACB>H78C`On-T4A_9h`7cS|AsCDH1C+;e@pE(o_p&$U1fKcX zYxlqO($g-w$<__^yf|ug){CU3cC?A9(){!Aq9tkN@a1;DcxBfrsBpd-v_3 zOLJH0@xS~6RVxq~n41MA^#Ur~1yw0VSJ&QkJG#CPkH8n&QzdlDt-Od%YBcOq|Va zk=uj!*)>eNcI^gLwFLDo4{-Nz?t6svfYkV8g{+-Wv41?6Nd3=hsV@{mtKs z5)%zOiwzm8v9zSm+bxc2E|a&1f+1;abRTW5ouX194vum^J^Yq?s8!gaOl*y+z=jJO zYv4$iMTCF<1NYNAA9)m!zd^tA$v>nEuUvvLPl2);rDaI6Uw!K9l+A({uB+t3F3u)?c6#8*BpKB-U7kHh>vOAMFlscje~RR0j=I1wWQs*f#8No!3)Bs%co#6EE8s=Y z6#}KVpS-}kSw!)<_%;>Ta3O3IO2E=EJ|;Z0S_|w*Kg};~&}M8In;=VFy#q8pGetuP zytdb(_#lMxCsYTQcHQw&=gPf6{+JOWT z!2^rhEqc@t6$!4Cv*2(A>)FakI+Z)LwH*Cer5+3R_4PzoF8}49`<pq*!t(-b@o>o@3=*aE2(L3MuRv5?@=%*Ek37d50!X?_mjp*&`rMExy zAl>`c2PlEy?5j^bPp4jbk?sfX{gHq4c3N1yN>6|D1VzEMx)3yZ;LUtCh5q%w{y%7U zpO*^j7bv;4K-E+l^jeulhr_gY*N{l)E+c?iSze}GHb=d^-83}TPqHe*%XicMg9n7y zOo2bG0k)I@6H;uz2m&1lhO&h$^+4{9r>mgyYBUIZ>n+y=L|8k2fwZ&dVL*d`Kp_Yb zp#J&?$O7-*0-h~^%gvNZl*nYn>0$`^SEcLN_PZ^9Uu?9EMjdu7<2|@{IAQ)hQO7|J zeD(?qw~9^D-`5ShmPHOU?w@7fuIYQ8c;bID?!9TMEZ&fSZO3y;pboHW4~cKl%na|~ zO6BZXt?F~6&0y=YquTh3uF-H;q0oF+Y;OCfWJ~-}tJC%D%F5+GS<7E&Ooa@)Ha&gr z^I!a{vF+`ob!C17(f=NL+wnW8R!kv4OVNcZb3#T<=gWW+o%HZS572{ey^pF0+Ag1a zg}(XKFHnbHfkEE`);utT0SN1qu_#IOm!wLj~N~u7Jc$8ur5GpDq zcSjtB^{iH)}s&jM03NEYdhBsi{?_|FsxI-Ct1(uc2=F4nYk{ow|4$CdJcU7fBytOv`oACkG1&8Z=OzB z9rmxwvhk2sFCV>f`I)gjGy9)w%I<4{pzq0r#ih5mG~ILC{v-6(<43_Fv}kd0Q6!5m zB6!P}D)3Tny7%~TI&#}#Ar)V`c$rSU_yXmDJ^$s$ei1juM_>E=mk{toadYfsLBv1M z+ebh2zV}hcuhOZfze0AcNMXByKuQ+mza7TY5pa=SD^e5MYPAa9*U+g0vUDWirXoD2 z7n@+LuaDNDoGBnsb-t4aJ6Eys&{Lx z)>{{^UhdmJG4;nmci^1c?yhzYbhYle>rS~VziAYEmrA+AN*0e z^T=%oW^}qZdyUS55?fwbrTxI6cS4x>A~5cS%U3|7X=GKbG&4L(_uYLbjY4cV{oGTy zVR1;gI7Wd5VV?-%EY#(5l242 z6N4py@f!HI!-^wV=akEm;DskfhiNNQqRe&lq#z3|)EraqI5K9=j>hByX7UCa+v^r{`bnt<>v1 z7SnLl3bM73E83+dVsx{vG!0$dyn0!NzAifzGQgGAqi0hazUhHQSGrc{OI+6b?mu*X zb19iAW(!6>m3!aT+RA&nd;6I%0(8kt==uvRx+C1EkN^g7nJ17STaq{dd6iWgU0Q2>CcG3|r4!cJOXz|i%T3fh4 zBfUX#I8>^_i{mZgxV2Ib0x=6bog06z;uvl840T(2At2!6k6LbUOjy9^=5)@q- z##=8J;dKqfSE|6CoJh~fFm`x+Hw-%BcMGXn!*iFoE)A3wGYnQ6jML3|#@E9vY$SMp z4fKGuhdN4JfFWJ73d(=BFLZ^)CTq71@q zK&zBA!_*X6YMCmc3&S+6pkb`CDcf4I;%T-dcdC#tFW0gQx+F(?6wji6ITP;>cV{w& z@)f5f2h;K72evlWZTt5fP^ZQxjAlLwkGn*fY!VTCnbzP*{D|&9@V@tw39q-dx(QFb z2`Xy?MxoQ# zBB_iT+BwPJWwH6zz>=)3tkL|nYa)wo_wH#ra9|(33lh0SG3ce2PEk`lAK(xIIZk#D z2HTQ%6`!?Uuk&%Ue?7W(g1#S(mndVC>u$fSs!m-d7lMg=O9DVtn`KpzQm$T44L7{S zeb6PTCMk7OF=UIgUaWT|vxx(RVt!Af);jKXdu{1bd7-$XU&_bJ>w|saHL|(CSa((K zU0z*#&*ZK{n>LF@jmD#DBDO8mEylU|Vuc>K>u#DFAE0Ny{5dKY3s9?KU>P7Ahr8+c zTOXusxlUhv_9c<`-~d+Sg2#K09HbWB=EYZDg25DMc<(eh2J$i#J4M8 zCsgpmykR+jEEThK4e(L4P;X71# z*ibSaeQb4UU%yUS>rs{xPwiApY;tX0Z}4WX}1G6UJzhT5zZaIuRNkH@G89vY>Q(Fyv-3ojx_%E9Xq-2u;Y|M6qw#L@lY$yX?a zjW9Jeh32|)^yy5Cc zz^SI-pl$fxOVlS@rM|v90Ea7v+^m(SLc@PB)jRaZON*;NT`U&fyLff!|LN!moM_f{ zx7%T%*z#qcp~JIP(o_I+$b;*&+PpL{G72`pPEWt|72wziZLGooK!fl_3voF&{c%dq!hQu%7_0`bh5d`*iZiuX8L+^B2{e=h?;8jQ#pA15Q&co_GPns( z>cU2`;^&f4Bgx(Zx2sYUl-eppi&z?mmt7T*q0(rGier;gA#t250?R@jliO6fNVG9u zYkwx7u&8HiV_)WYEl}lTaM|4RZ1(ac7%U|6$#F0;v$$q!U{V_5ga}6>;yEgy30(+M z)7h+F&EgH;B=>qo*XMx8@=U>83lZkxfI=CEQ$vh@^wR&c5)6laATupBbP*G znF?>sWs{sY%$QbGLEz44@Q#j<7eunvaT2Al@i)dU=rR2*RzRmK5kQ4k+V zUb{?@dZT`LabbDJYO!wl3Z7?sf{_a@n`5p-`KJLnK6-HfjHh1HBpF_+hQPvtyUd** zyFxCil`>!-HgV+k;>OnDp&B$bGC?CFBUCKrX=8m&7|HRYhiQDMi{`Feq$^ji(iWs) zpOfrA=lr6!A%>}MBw7$Br1$u4NRHXf>e6BNFE{uf&!K)N& zU0YgSF!t`5vO1vgSIc=S6>8+-gl~^c9JfVKTinCU=LynUauc^*qHuQ?=rbRLiaE;W z(&Wdn`0)Mr5Ch-X_BN9D1wc5YAvDra4 z!Ft_|H<759aHiCI>>WDooe_OrPnUaG1E zJ|!h*eRR*BZ#L?s(`ir$#X|l7u-EO`Y-(?*TFh;4Z{h~E5b6793yK_9{{{mdan#it zMR*J-UXdXj!wQ0oIwWF<00AEYt_E#vuZrA+L$@7*7%@zX%S*JfxCSjBh6B$7Ury@? z?rOOx0+=-_H&wC#^Ksrn5J$Hi60rhFn6V(YRN~f8!gytPCI}J?9T-+JwX~B>AdYi3 znKGaa_k^&(@KhES*tf$toWjdOpbmIEPNH~VX(<+f501!gaX<4_U7SKBKcn2NE~zB#8&zywAG?@;=)pVxmbg?004l5U$`HO|pXd#_?x5wc@ z9HYm{?Rd`bau=mYb2gKR9ox5eh7!>jh5cdSIihhKsW4uDX9pqphC$&d#I~_>S&UYt zpf5yDpOrGD6vZ;z~0TzZo$8{==J%1G^7tgvA4Z7Le%M6iJ z7GI#Z$VWC4$*u6f)Fyr@L*JuyLyUK8Ys>D{(tT31I-xh~5l~`fpDT1`XrT9t{By$( zlBVS`I>GBk!SRB3l$AnR4)zdLEvl=OOWz*fT>q!5t1CZ^Q`iL$T-(2QcXVxKaRB5n zf)qsj;3^xcjgqAX`7egh5}w!t?9F&H znuyY_UAsgoXDksHrRm)26w0FLKshqhC#r=vvlW_;L4q!VwNP~6=O#R+DLi<4wduOH z(K7QPC=wx4GgZg3@yhZF)HA4pjW$fEW8+-Bx%^;M;( zr~54a`6c=uts7#zeSLimI-6LdmF<$S3ZcO~EuwR- zKa+P7Voa!NE{9dX2^nxicri95{JVn3amrz_2+9jwtpleBANwpuc)eaRGc65nIg`nV znmwk^S`a_nE?{1q1lQ*0XggH~s{+}&$$7dCye>D7W-P7jG|Iu>IHy8{uSPpylXf_2 zet&q*Xgoo`|35zs&t9fvDj|A76bsmh#T3~b&YeCGE#UxUoGxx`R=d;L`{K#xevH1~ zj29o)5ZUL45C3fgNvKdL+d-^*CCTZi7i=xnR;syWXQ+j^!S3_b^+KLDV_P_YO+Zp9 z`Ql42c8u@ZV_)8W8L>}^(je*^@LcR=qyla}jFcO9=Bf;>r|oEh&Rx7psbqro z9=ekrIChw{?A0vm8!4BXwl!5!GSh|2WzSg>?1Ox$~FaRck4djtd)0q(u>BDAEa>>7*K> z@;t-@m!t_UlzTR?G%LeU%H^_P9>kFgc#s2jj3ziKO2&;3(&3|b(L-;47{Q&7zHsUk z#bYsH;O8%1qHKzLhb0L_1KoA>PEncM;kMAJmCK?mo=0Z6Tuy5D|4@Xdtkx=2!B1z~koGoDXSoz65{q z^^@8?&RA4=waOUZ=c%LNWLiCb|J{9m@<)I2t9X(hqVK_aL;E(cT)eVaM(Cjj-b&L` z6LjJ7EX`fJCcK{k?3&5us0fZ#)@pR*;9lClZ$^0Y?afso2=KAa$GHVCfrC4q&dJ`k zS#DCPQU=-Y7Fk@vHrgMaxdy>BNGviiHw(z20t;~N02`$uw+|-X5bSmHxtz%C;x3iS z4n*OBFzgoGb|+P!D4LL}Ia!E_FB_iJZEZiUVsdb4`L+^wdiCqu7leoQrr04xWI+K+ z?P_Gx+<`!Hn_k2F&6lbyRZEK1>Vv3s@3|MhG`O@~*?Oayhc~+Ei0QT2%ZX|wsy2)* zo5fz}>J1jERsgvef;C^V^?-ekTu!aJf9vf3eAqoY;I!CW-m;mSU>av`Zk0;FbD3OG zbQ2#N8ldU%5pc6{n!C`TmaX0Lh-oav;3OIxfg)akSAuuTfoi%qdlkl2qtS^;y62v| zX>wu=#sp(c#wdxvh|wWQTve-xy0ridc4T-EFrtg9Q0Y?1D0OyqPz8o9!8jROFtN{S zD^p9%Mgb2B8{y;K#x0<@IS6m2ow6Ga+*k;9SrtJ|@#9o@QbleTNwVm`<@z27FMgk2 z1cFQp@`=FMnFj>27-E4zv#0J1`pM1tz<>)CAcIE1Rj>Y`X&6;f|g{IhX0-#&&hwoQ-H)49eRDqZ{e( zp~2n=t%QA`hT6#{TuEy%>CTy4@XmZ3vX@oiskQ+(Sn}m^@w<=RP5bukqX0yVEhue? z*cNH<$T$Op$86!|*{p5D9USO~k+gGpopvkvA)zJ{TkSCccvU+*CI3zxKj-mS{y+e% zL!;dYgbj74X99nG_Oz-+73Sb^*o3z>Eo~L64GUO(W8M!OFMclTO>v4a1X8)9BX>s! zqrrHBjXi?vA^|!z@I0JxR;@!W26SrK+?0d@DZ_(X#c|JEI4~`s73VE*Vm!l-APk?S z;XHWp)((x$*d-qV7Aq7csJOtB4bj?ZbZnIR`ylDZ0f>A1MS(c3+^hq8p0e1*tf}e{eWf-@O12OS9 zsSP8zAs(ke!YJ4SwuI+v3GBD5*ysxVxJT_lu6Y*ako5-2vW*C%B&>D|L#_Yk#Pp}t|z;ifZQFci&C{^qZ~Wng?T z(9_$A`=onvxh#(7nmEdBJX8h2%M=6z?jhDF7s2+8 z@O(Dzxh9TsSxmCDI3aDrK<&1gFy1-`U@&YqxM)@txnD1DoD@zCehx;ZHYjl}9RFbm zFjyXI& zpQBOEdW*#pmH3OJYr9VGdH8<1>(CzR@H;87#g!6Gk)2hj*F^VoZkWsfaX^m0g@zm= zS#k7RA)Qvs{2T?k|K59O@62w>g6?61;Z~tV?p07JQ56B6&F&Wgi_4?ZyB>KL?cTFn z1T-!$a3#F^g>$Eg!=q4lw=kY`DouS8y9D=H0+Yf07(5+4)Kcw&f#8-;PEK~o=f!J6 zUKJt)e}#J7!jMy$G_8PpE*2}mz!s6;8wzz&M+ay$M?j=XTD%T$%XQIES`u`d0@fuX zyn4Xz6$w6!-P!YV8l)YD>Fnv#^soNeFIax>Q=dGJ;C0~21`!=ZpO1IC-1CxT%O?dgSw2X=%99O)eqJrFo4#$xf1$I6)42i)CG^yVtu zI!+8vPnZ;6m7<%Q)C3#h^ukE(B!&{IK0uF-u$U&a9m~=BR!$5Ex4O2;?uQpe@YNp) z5S%GlU_cJe9hNx=wUf^IxT1F%KCdYBYzjuAgcGSy%Tf`lV9N?v6NF#`RkDg8j=KYG zLuSu{|F*cjROEynY)rkSi0WZ6WX8DOe(!HCA*iTsvYA!7cHt6O!zS&ToRn0X?csG} za|I7spf_OMbjHg<##`QUmlK%E$>%??uEp)~iD0G(_;Bp5+o`W3B&rT{tv%P5|8#M6 zueFUGIU}oC=aH(q=&S3r0Vm^zjt>k_e^;02uUiL`z?o!hz%JatKp==4whWxv043(8 zL!y^%ub7`!tQ5qI;jaE+F^HvDD#4=;0Ta7LfMtPkvXOIQP7{ygq12p}?g)308{UkG zfA)}mUzj=uMuq3ppyH*|t0Yyjq?KgyS*@a#sRc*3(;1>IN1nFeSyQ<(r2#)Surb~6 zz&xNO0#ED1Yx_KY(Qk~q=P(V&g9KDV6*&{C>cD#k_RZHJ=oEqVvs7rb`!aCal!}wV z!4UVPJu-8r`wn|%7PnQsZ^?~P%2x)UcWpzI)MOfoYvMhz%Fzny7!4H z+cFH7MF^&#m<@2+ETSaGihzkT1~}+|bH+tR$bq9Y1dQr+It3NQ$F_|(1q`?Y%3E*$ z0J%YFOUPn-hVJqKM>kk!;9$uY9~KI(a1< zRRD7o1WT;G*_=k3ZQzUsLGZ#X;C6AK6KmrPUw9lrO@-pd1`6yOgpn$0iPQj2YoPj=41;sosv9Opb0DsgSO%H;efIQ z!>i)@mFi7_Wo6MCm67g_;hL|tNfn7i)XtLQGAC(=|D~PBodR^&w{MRXCs1!^C^&Wc z#Gmxg8!*S>ri2Xx0|U=3E-gg@9%t9y$x$iX*-Zz*4y3`wBDzIj;T97lvxz7bU?ir& z)w$qT873H}kcBO>EP@VE(gGr%1FS~9Dsmf`%wegOZy0-#NJl5_-LsE2Ha10q`uOOW zU==v^QczT~oA{R>9HYwLNwZ{%q$da2g?VsQegnC)buC z1rH#!`?^8f*#-P)sA;m|y(l~&+X`yT3w${kB-yUgMy^UpmTh?+Jz!7@7+x*|ADboM za2TGfiQEM?T;8yp^UTZz8y(|Va2ld)$iV{)RUUoN5<(4kgRk&RH{K{aNjuj~wq0d> z{fDUV8at{|hg^ za9?zN)wg$Igdzx#9(?HSFba4ow})Cd;6-P_y}fJ?$<*;4^i^ z7v1IHCV1KQ&B5dCFkYU=#!2Oz6}D^Fw7B6{XRpx6;4nD>9eCzjJGg0yh`uRVg~!gs zfF;X$!H7DX4k1Kv{w>#m$=F3Y@F;Zuc87AVX_ML-6|Ac*4O^kewXh*%up3seB*56L z0xE1R2>2vyGOKD*U%)M%)1qVmdko4pB&r#0+K=-QB<65kRubtHh9Uwg^Ex}grTF{< zQ7z1AmM~iGKV7d2rh+{)2b|1jD?H4F3pI^4%Ozb;+kCZz=(@(%YNxPsj9|7;9G1B= z*#fPCVY%zh+m%{lWltiJA2-cso{^**J9zu1G~S>3sekg~zxj8+eAidM@P$36UUw4C>N1YpL^>^6iDn?zW`SoJs${?r z@Xhu6yh1AWdGG~3n*Z(r+;9Q|b3GVWNwmA^OFQ1z6l?|y4pxZ`*iI(Aw`o;G0BS`b z$ijwIE^`wy0WoYnAqpVCTuYT`J6{vc{bbHpz~@tevvq?#Y+Fb$cI04j3}BPUTw#Gz zzFFf*_065?<#wYI=6l)Ww(v7Q#2U_1=D8N;&U0OtlI0z*%SrO`j)dLfIz72Pkeo;7 zZXp0Hmx^BC`IggV@B8}KHyAo>&<$KSrSX3J;~!TqUwnDAp=o}mhAv#XLKn|npqKaW zp$~odBP6>5z@r`nOdgSa)r4nMEQat_Dqm!Hr2ykq7-p`Ch`$7ZL3NA99~@LMZZ&mP zs70y}KTJW`JAhGboox64aTDvHgDzaWOlR=gbMp(dwy`0ggAg6~KXD)1N*rgExxhR3 z_4d)^!;d@ZyYldE05QC@WsyZUxie1QUTE%itDMZVP3$R4T!E zbkJM*OooCs2YLHDDBuP470bYgvs5>6@&K<^t9hOt2s5*a&1{HK-|Yk*rnq2djUIh`RXs6n4*=lgF2%LI0@VyL+YFyKY z(_tOTWg;GQjH!=ks};Jzy4io?0@Vpfgt%Xyf1i zCjF~l`h|8*0ylsIbjU-h3}oTRnC zs!bXj*hO8PT{OSa5Cdp<3E5h{UW(inEJS(!tKE3659Y?^RIRj*jh#rwX?%RZo-S1; z7v`5{@SxAp4O};+@irPsr$v$EcfbANjiZP5+0)ybgXb??q%VKtIZg(l-}&87!GQlc z4UbQV9tmaeqYXAJKnV}Lr^js<1EJYG`8emw0PZ@|*%73r#d(@MuuoX6Dh7t&7=|Ru zVg{#D=E2K8{q#5Jv!DAsIM;-zHDg6j6iISIOiLFzx(zTK7Sj|P#VWUZG_*U_?953+ zbicH;LJ7#U-#BqX7$y(<9vmEk+#Cio0C52ui|hRZUIpF{_?f%zaEFstO`IrBHxxZ! zTB*r}lA4fl>qZ%{$w7DTo1lF&({yEd8?Up44OSx{bkKj`c}d=B{{-4_rzeC+P8Yc! zoYSE56j(qh7mW`N(lk`W`MG6L_xEi%XXZ|3nOHD-?sRFlihN=2s?K>JEK{3$JMpN< zrAn1@mt9qg-Ju{YEMjTh0m9Kc2Ks%p!oRSj!BOA#xMbpw*9O`)yr zBt8E5uh3(UdEi|YQA0+CEc)zvT_)7D0y!*-;D9-zwjikR0|Rq+#9ECLpLPb1z<6!& zX58A7dmrQr70^B})BpUVKLvg(kSNyS+AZcjnH$FQ@eDMR2XNyq+wp%IZa7bd-#a-X z+L$ga&rv*`qgV;%g2$k$VrpF?{QHt7?n=YgbhG}JlwgHCjW2%XpVDmjoQfn9ONcMEw{W8hb0wUtm1y0FB08 zlW(e+;bt**>dtggNy|CFi|~{@*}c|i(`hys)vl>=>K_~s!&a_dTcl)x6U3Xs=#44| zPoSK%5`_csV#B@E)8VA$%_x20#VdfK?XglUUo)%_30DGNi{fh1xswuid&FDGa&l0y zy@5JC7V7h>)a9~}4eyOh`9*NV)EA6}@p!Y}|s=?pgR*zk}a(*AHdQ@o){+f8Y_R-10o8xKO6R=$lN|y5Q zs#Z!RlGy1Rg!o1@yu&ZSdpSi_L5)2Q=q52RPgaEr#i@*(pPSC)apRjJwBe}RY_*$k zWN_hMIeURFUtJWP9Tllc<3oM)w&O?1kK_J40=G463^ms-hj7C9xeSOKj#MfT5}@<< z_IFYbm;o8@!+B|y3iXzdpV>?C`w+0=)dAMnF!)^s@0;NW>~h6LN?yCw5S&w z!fOMZKu{%QYCt0HCBe_d8Uhzea1L0zMvVhjsMnm3#M~~=A$Hh>*JLq5-AVe9MPId+ z$Y?8ai4^{|9gR~G_*xeY|J3%FENl=qc6JW0AzpK*0-5O5r1qV(NzN}$rt@OJc%x-@ zIL+3u-mKf{2CkbkB{CWd+8qiEx6qR*mK3YoM;0hdUVnhht&}KHvEe2rqFbcQt{ZX?b~t&cJv%g16xJ zxC`X&$zeJ;Gb*yiniWooWSLnb2VP%=m+J55!6+8WWfGLhr$ytso}NCkuScQEc_>xO ziIQ^0g;q9RL#Ed{n9CyTjMFSzqEgZ#j7G!jra;Ih0b@4PRpOS64hR*zF+JQw1=YsZ?zNf7d;VT5?FLQewIP zM&QUe|qW<>}Pz=ZjzV_*oz z857DGcdikQ;vj{p;DuQN&2ZC>Qc=uXU_pS7T?;UDmZu0^xD33yisRKO(gGnhh#Z0- zg1<&FLw2PlV2cFYZH2*euiXJ`mPQpKK`o1~3h6LZ7sL^xxf9>B$5q<-xejaykJ~BI z1GwCr`zvUoOHLbC@|*}RC1S&<;(2t_E~Xll-~pi;lZs$cr3UleDFT|1VM!{N z7l(v^H=+(srq^m`+p1dI7q`Wb*3L%IxrcGpz$sQ{)}}TzFjm@n=_U5QH>9)9&FOi! z)^_y`52~9hE2i7&knXQzY!9LaIP6++Qy|o(BP#q5}?;8Uizl&B2BV%snxJsD6B0 zG(1_nc8%hR7**Kw+Y~W{N9TI9I#=zsoAJqj353rK_t);SBWNl^3_B}(9w)i* zklqkHJM4oSTE|OeQR=VN>Y^?GFxU+jHchdVqa2KqVJs_(ocFLa4>klxeMz@bM{hSg zuSYaeX#vM8P~~*PXhRgn+|>5K?TJHXJD;A~)gdj>2SUY$lL6&YG%}E(p3V+X@s4oO zFu4--2gi65iNw^@_=u{CMz^xMz7?lK2lmsMmtUsAfj+u2drg$kI9TF^7ms>1tvxQG#Il|KkE1JELkfP_nR#Ko7|ea5&XCk4f90fxJ|y(Q|qYK;=P*@L+} zcwSWmBaAsa177M2duSHkuf~*|%Lmm9Oa-7s1TBn9MRF^3JDz3FSMQR&r$91$<@IMk@Qet! zc$Tom;h|)XyN25-+|wg+20Rb}oK6=xA-lpE3fZ%%@@;R4#u=h)+-wiyW8q>0xS0*N zST4e|Sw+KUHkCZM;}=g~pmA)L>ES-npEtF=N_XrYqf^&b$PJGez)f!8%iGyOU7-M_ z;rX_rIXbtxCb0HM{~*2dp|{W@58hA12;^GDC{URE&Fl4r$l(csqb^aq z_rq&aj`?a4vMI{{g}g3bPvo(Ihc8ktU$3Z&&04KhC;h$MH>{@ZrZnDSv23pvt8(9g zJ|1ySm(I`8lb?H%e(3#gr?IgS&^u+(K$^qrLWMKC2zc0sNSP=T+r)+UA+V~FFXV@T zyWrKTA}L+rISyhZX}g`i)8(hl=xJKtjtYsG2cHNX3V%n$Oq;PZt;f?)#|9A$j?naE zR$#xS^-WUhMSNAr2k&gbG0a@>W-LaRw$rqkii;MaKm7jp(nI&&0f=Lx)HY6mvJ0RV@e?NWWn_tJptv6>evlL{2qis16WWnGzvy>~~_=N#!bq;9CBBHj!z~k`h zFTQeFG+jP0IZo4i??7M$9@J)`g)8StZWN)6;Us`On#mPtVL3|A&#j13q@VuDp90Nx zHy{Q0&y9Iny>^~rt4maa{OYv1X|#8Qs+lFQ1rSPL=%U{*Y{3c1*yDDP*I^-_y&sz^ zg$yLZrD;8pQWl*MO)T#wIF0|hszDT z>=N~IHT>RM0uTp0I_E_5b?pp>;Dv`685#stWD#_e#qK53XFw*lh#Uejt3j&4C{0i}6<}Zlbq+5gdpDx$3WScR z=mWt*hM1AYs+-j+BJ@I~MrXi6*tzN#H*%t{n|k{OK!btJXjQ>zsG@8pU#`QowR>%Ti`5O4SOE=YS|Z5{v*|!_Ma~J5z4d z$8~5fl>&ny@__5L+wv66HK?F*XLdXX_k3UhxLFa^(!8XdOfX?yoQla(FlW#)a>k}* z7KU?s8{l&_(ciZYN&yCkO~9QsJHW1t^!7o3vBMK55vZ!70U(#k8#_~n+U1#T!xfN0 zw033jq#=@zxZP`^TyPXB6=Tfd2$rk4JLm?DzPc%m*Je>GU=j3Ev0&lW_anmt^!9f= zOq#6I3(ucGg zTzW35tkifGg(f6i?nPjQHxf;vd8RKHa^gm1^Lcvtm9tdKWoZWRAslqUtATO@?&Mh! z=@g=PctylMG&VX#yY}y)qj%p6NRgwtOJ^yzbb(ss6gfDjShW6^c9P;jcljew*_y(0 zvfVLr(yTGf#Qkx>lz0MSY^>A)4DYZ~&{wB*V9FHuR7J6i;HfNVuboCI-B>D#H+$Ky5`=;xvRnD%8;r?;8<$vAEM>2sT#@uyA3*qtvpTI9?K+zUvJHa6T8* zq%>JWVMN9;1U+?9x!n;<#+(q&O+>&p1b{n3VZX?+B^X-{N9&pMIbzgNS6f^%Ax2QO znhM^-5bYiwq67POgC^?1F`NSUq*F;pBC#!bJzxQV zJI6+b=_fw?)Aaj)_-UT!M*X9^sBd6GU_nF{oF)c(2kKbMCNP@8(}Hk3m-1OLGKvEm zo-@HY5vJQimoA^9_4Va;g#ZU@%wmLsw159Tn%Xl(;f^qQp*Xd;Z4{oPkV?|}#yV9b z9nvlg5D_`265es(0QJDw2`XH6V~ws|K1*wxOYr<`AUauDelpt9u$c4%Yha966MY>l zqGePCr-EIzk;~&3IT)UfAY|L9$fICt&Sa4U^7LqzpSrbF6(@)Z8#xx;A_q=;bw=WVy&z<^^~p~!>QCP3l(L0o!8B2yq#U^ z@z_SGTCWGv>4a)gH5!mcDPXnJ@X!E#=)*rrpZvYwr}3%1P=y9X(`QcfeyvJ?&JSkdU2l5}z2hD4q)>N6%tGUY z8g{<$>4^8+&)?({8=1#RaXKL6WB}iCJW8wcv$QpLiA>Nl zeWXtf(ABj#Mau?lWvZfCb`AV5Gb&s+$Hv7*sKFSyfr_XL7A;E^^5C_?@VIv$nxUYr z1qnO@IEJH%+QYp;bBTM=h6UeR1TVchMXal2=6-McTL|ui%n=2;B8-X$6i)HHA+B)D#2Psak7>`v1!N0nmrPb{in2>ea zH915I0!%LFciZuPOjYzj z(Zp!1AOhBY@V?y;D}Ym|nAjw@2Z13N4g;_ymxBUXEQ`b)?z{Fk>eo%_TgYD8fhaKk zLY`)3Se}Q8x?vw{QhrvG* zZCp7;^Xp0K4cO`SebY41*AEZl5q+~n0V9mo$`uL*0;pJuGKlg^>1|P2>%`6RKvG^? zjf&uodlWRqNP(#;Gds(yHSz^OXLWVK`;-JN#?v+x7-hED5dC~R`$wUk>69uINe86yKndf1ygH=e zI5^#_E8EyOh~5$0b@U8lgHBM;$$qXWyc&CY4U%>V8-d9!rHah}x-NqAy(0jy5QG?_ z``e{Q@V@*RtN1-D{_O%q#=NZtXi+L`iXki(wMf@# z<8N8t(k_(+pH_S9m6+by6~<@bK4>jxCS7z)jtpmW`RWG%U%vQy_j0&7jn{8qclbO@ z4PBdvClbMA%taxmjeCevE}o!c?|45=jE#%tdo^t*QIBmab4bx(3Z3 z-VztJg~z!Ntl1IpsLh%n`WY8mfd4o-o2O_R@I-ElDGXQ~``m09*tS-yi;|pNGD&(S zMQScd%~BHVMH(BYEM~JYoy2{_JUEJr1st6)0;zWF8KSy}WW$NW{C8|LIoHU3X-+lf}!f|$Qxcr2Mtlk7HD&Mg+jP} z9tRg^vVg3M#?RaZYbqHJ=jXzA3@btui@zS|lgqz@thqP?y6dx=iv7 zHF4Z?HEs!;s=*ZsFfI%5avtw{ZY@EvWL9Lnaei+TMyD}<3=E&C=wj4>3trU)4wW11 zRY1I^QVmi{IS4Ck%;0CS`K(I26Qv|F*(@MK?Be~`xT*r4R8<=I3>7iS(#siSZZE_V z2ZS9S)NK_FqlmsF#5ecdoR3l_DmE<@7VHc zN}&NKNP3<2-+2rQS}$&py*;xG_*20VFN3^SxdRQ{!@}HU7+elVK9{xVqA3ZJ^j!U* z)CeBOLK*}LW&^mCn=59sNks2!YvgYf1n+5rMyexv=IU5Zp5X?0JSatLz+kxN7Yu@h z0IvSEaj6hj>*5<+mhQl7Wg87qhGat!SZvjCRJVj-wNzCk!?3WikSx9V;X^c^L!gD=OEpC9s%WQ=D_=!W7hPYWUbUADCS^HshdUuEZ8?EET z;sG-}NfYnY#?=&pnp4EkmJk?=PT+eR-~cD1a{$@G&w2I*2a63DWf9Cop~BsHNaQLw zK!I`p;|zomuRDm}(ZxJClCB%C(Vh%1i5big5j9Zh?iTQM4M7}eYoplEaF$e>b_zc# z(d)QwTH}4oTOLV0`Q#VRM%9(k#LD_x=AudIz3;yZtU!S*a%KxOlLA6)XSnhDfc<6z+0y?5I4Z>T5a56 zT^AL`%)4^n!$iE_=@Aorz^)3cX9z>$4(F;!-eziw(+ItReh4Zi6gqD8?-D~@(oo`( zS)Ou~0OWypHt$W!i)naJ9UOafDc=^MyhCWTEAZUz9U?SF09Z z6EQMWq_by#FpL+=eCInq@YUb?FaI%AHI=(9-k|OFWA}&z8m?k6TiVWq1YJ;Fg=8E& z>m20~RB>Zz?h7V~7W8UcUBZ#gUStF}^NvFY1b4Z(xG4BvZhOQk6jN$71Y!UQ%3HUVdhak4ox1ERqlH^|$GjJTJBq-!FlfE8dxZp*zbFdkgDX9e%A<3zAq z6>%DIUo{24&x2ovyF10$gHE3xCqNFbT5liu@TMBBTb1PseD1|-OVruhM;oa;EdhTQ z8(=A%eo=hc1WUoaCnFGGqVc#8R$lD@WxmD%x94cKbENCFs@rC_Cp}&dA4UD9Y`A4b zfh*9QxS&5U#!C-A_~7Q--u9NI;mN7o-aUJ|0v+ALNW2J!O1YxQXkuwM524}GE9XT0 z8n^u{gX?6B$<6j;)4|?=;5O*cp~EyhwHrZH4I)8-Mh1s)vB7JWNb1=BY?QLMkx)|cZ zUXpWib!rE0qCuV2BBFZ_GZNSwWpJ~c;^^~opNWe2{0<9G_Q{Jfr&hBm5`kDy;p1F} zB=hGILwB}>4soFg0sFT1dOl@1*_ zN{IyUe!3`nvB^-~*zthDxx2R423g^>$JAM zx*=B^WtZDM+G-lzs-!r8P<%<3OQzRx-L%Ggol;5Xa!NQ7ksf;c!`ao1mEei5eU%~} zHw|=$$%#N=X>OKguUy3OpBF&|7bkLyeGe!xC&Y#tI8?>rpVPeBmB^>LPV? z_6uX-ZslT-HP0dgy%qHP=p!GSq32FKL#Ix^NZXr9>V#2rk9N{F4D!YEa}?b!z*}=Q zu3ZcOx~(M7;;aJwp( zTBXk6aQkz>qxJ-*1s><5A(w-@RjOj(Am<)%EnLTeQ3^X`AP1h~R>Q^tqtPf+K9MKI z&eJvX6zS-|sT08FGHHH!i&h|jaKY+k-quVSE!Q9gTF=e4OXG7^YnIIl!hxDMD5Ig z;H`e!ZMV?_`|c!LWPqZOQM)?3MU??(o7ucUq2aR(><8z{2Ve=S#*XCD5;L|;}hx*2?qGxU{UlwqnQmBZ3vruAKBtagvBy%S3cD@T-`hbw9bs|YyI?3GzXyg{5oP^mtw0T1SuiCQ)fBw5VP}s_!cfETlntC5 z?D_e_GFElky}-d%s^fEo`#Nx{RB3a56PqqYWf;8&#w@!%bY*Fk7FKxTCs${H7ZxL5 z6`@lK?81%p@WpKdc<>AhI${PfwUf_9IhA3GVc~kKAN&6Wzz+u`wgRd1;zL*QhgVR&RHHj=c<406qJRR)G$^%AD!?1^u)v+LQ1HUsRfiLF+x_>N zq9GqwM{q(F>;Ig6VMuMiX2FK*4dO%?0WJ;s5d75<@svR0IZ46$)ba^(c18p<5%Rf2 zt5qIF#VmnrDk6tBjNfC*&gOOsjLINW=Po{+2#26-JDH(aYEekrZUyR^%`3#19Cz2r zY*T^xZM>|PWnhy}2emziZf6=0XP$A-UpF9w%gUW7TRR=kb~>c<02hZ{G&!sSE3@&o z8>~oPr^j9Q2mPEV6tQp_w_$W!ZTg$T^ER=Z7FVWeRp8iKE~BOSoDN?DAJ3&4nrwm; zH93n;hSsGh4tTW?sCB2M*J3DEN4-?3$;Es!Sgq9){lkM#Cbu@G5irbr^PAu3+_z^h zaBUHWRuP>W*I-mFTpta=k<27N0u3Goz-BWzI6`A%lY+E2Wv=JcMIty?5^$n62TGiH z&c{9XBV+F=S=ve1DoA`6jKbsN^vEs>N4g<^BF@y|w75lQ^EwQ*!l4kneieaR0Uns=ckslaGO&C_U~paVv^pS&j;mk93B?&Q zCGI&4W0kFbd{&F-%fQ8GvLs!v#X!u^pfHTq1~$iqAegydPSF(gfM&aOE1z0khZk`H z-*s-nVk+Q$!#=-9$5piGtyY(!$~YjKUWtndU&nRBjQ76#?!#v_Q%g%rIlsTEmP*^s z{^0O(%V^rQW>bPv2)a~m>9vMxnlcOWlei-T!BALk=#5&VQFQ|h+8XszeRObOQ8o3l z$L)G*ZFTJfz^M+8%Pl1k9L#|+h~iI~6(O5bcv7w><18+(&r1`dqck=$`l>3#lW#=B zX{9}Vl5sEJFpl!A);8R@o=Rw#?e5>~QMj>=1<}3N%@caSeLC3dm=p#L<-(caawwvi z9}5mOcs=%TrrJ&&l#O;mt75jhwR68)Zb}tEaHci#U^Ln)9anwuxGW9c%Y`QulPh7Y zTqw#$$&IunKq9LX-j(a@IAhFU4p8}P`Y13S}l&WN~%Qk!v*Rf`(1 z*j3Hv_XAbv0d72^H5#hT{`#8R4KrST!;k|_;8*?8M;~o=cJ6INqhF{{+2h2i)urWg zp;EQ#QDqsifTaft*kN498C9{Uz)jg)KIgaFt=+&=SHhB^Ife&nzwoi2{p270;b-nW z@ys*52M-=rQ@N~|+$bhWBI@PN=iD)d8+dS#!F>`A?LQ!9Oz<(tN$K41nJF(Gio!w% z8$I{v;+qf*aQjhJ^f%yMyJ9Yy+~#{*qUxAL543zHLHYDH3??fKs0>e8traN`$vBxT zf#k=HhmkX_#u*gclh?W<7^pkJ8YAH4XbM=d-Yzv4akwG8wdjM8=H3yKNR(*;2XI$P zjmNKw8O9JVcotfVOW|AX%3bNz_+M;FiMye*5yLBU2YDSE#0^Q|M#%@8@|z9$7!3Dltx^pc zdJBP^#RY@1Tg2I3t#LmENSd7paS} zT&f^IyDSpY*%NbGArDL8Mjbp7Ho{F!s(rMw4B2=28s)$*Z^hE^{u%JN6``atr6o#_ zK!G*6ExE`@ZJ=UC>Osnep!<88x0(K^*2QQ6EQ1ULTU*H?Dx(~O&eyGTZ#B6+<&zCqWc8XPMtAWjS&_WXc@>J{1Qc_4+zuV5pcBl9 zi4(727}ht3@g6^Z9MsaBD;TtQXw`IAQz{;br&7nPN<|KMLeCHM4*pKi>x);-y4}!> z4qTH?s8d}Qi?v*D)T8GvUHEC-=e-u2LsHyMyU(FIaU}ZR``$->|JmnG{JUH(6XJ?j zrj+VCLP9tgpdfhD4}9PQbobrIME?esa4n2saUAo{?RY#74Lo?@pfC~^FuJ?DcDlew zVlzxmO|}hir@M|AfMRg3Ur!q+<9T-}$>HmdtvD$RH!Vr#X&0GxxRZ|~p zYNg}3Z0_MyA`yo1wBSMD@Ctp1`Aiqc&Pu5gvMTbh!|t@!b{gXF_;$HvEUE$6NB(I4iS70E=K_Tg#>tuDNiLr^^};bqy| z4Gj)bHk%U$5s5^^5os4)7J1}qhL)~fp-f^6cr`1^Y1$3M#hFVTR)cIFg)Yx7QnA?{ zXc7p)YdeEb!rU+(tB{jfK&Vx-5Fct_J(}=>hM22dD1gGNmZ(4IqC7+lD~@FDDPgpg zSkY&UH-fq|$&$S-Lmq?)I|(y86qekED=cVm$9PuZ9C+OZp3|t6gy0Z>VeXoqq&{rk z(`V07t5Fh0Yk~=}ss{CThA7~1k`JCa2dE*j9L_DQaECfSnGzy#@wH!Zp=!+9iU`>gzg}_;okl#l>?Go6@)wdq3g{*s^v@;8S(TM^=xV zGpv#(x!e|6YxNb<)kku<)kDoz%@gtm5ae*qz+MfEz(0zAr|0xbt{r!3%HGF5&xM|8pg-v`MhzgLh?1u zs^|gi1Wji{Fc=I*0A;+wh}mNn>lI3uau644FiyQaN12$NX!mGSc!ZZ^Z5KBv(oO@) zw&5~{W$es`ZnU^xaUOUT94|a%Fyf}co?b99?avI!S)5s^rA3DWwe;C~ra4{RCvW5yN6Yp44`u4DA$SbU>9#)wnSKFSx8`xU8v%ga zbUy$7==#cx%7a?)`{ecdMHfd#1T1i{_w4EH5e94F z)~KQtzA5JaiVQR^IpPv|+#oOLvnd$($k@23{>_6fOC_=ti=}8O%3X61kX6%^2W!w^ zuf@3-oFl<{6Cgo@Gtclh7ixKC^{!FYbOBfo3|sEoRj* z(_nD_1sHgkd$V;%;EC()A=+%bBKg7)NlLs^xM0&EdLoIX&c1~6Tess>+@zXWhXP;9D@`qH@38fTA{}$t_1#*%a@gij z)>~F%79TC+4Toa7(Ja(!jgC|?bG(?%-iD*5%k6Sr=sFop+m>?PkiFz(uY2DyrY=NPF8CA zu+8EKXqvQYcei2X=M@R|U~6hdufB<~H>Y^ot?dVtf?`INX&I612ioeBid4 zMMLDoR`LkWpum-~d6BFx@)^h?`jyFoYSToI2WAd9^_5%eD|Yb1I=A$1Pk_g!#KutZ ze08o&<38uWsK&%2;IDRyY=(CgZ};dL?Iox-}} z^?xrep7Q?q4@+HUxjv-pQV)~-2s{_MM?=p>A}Jhr2h`0gi9MA@^&WVPX<0H5oGJy- zV4GUG5+Sv<3-oss{A^XuzacBgQz2)08>Gj&5`mnls9I9!6eg(@WYUhb$< z=b(>!a=~arP9Kh83)lxR9iJ zrjXlCVOX6Pbbv)?Ccz?%DqYCnRN=s?L6TO2QP%`qkr2EUfX!DEY1)9W(qP;SEbGAa z>hd~hyeCAxeh;Y#$T-`MV+s?&UJ-#L({t=3m@|{Hk(n3gY7^RFYudt_R0D=Dv7~9W zJC<-`u66YutkUbeUiW|vN36QMd}#p3#2!OxNp^>K))^UEDVOK9NF?0YddX%@DRUD} z#rukRd?$l~D8psi4;9a>nLpNb|{TwX^g4GpQJ z8v3P1HUAc~SvPFh{Y7}Ejg567Ja`aLvDanj&>r2y?@AJ>gxph?r?T(ge^4ZnbFk3? zxFIBB9-tiw3jE0QT3_D)FsDN(T8hmrzTYrqQQ~75z_7Jidq5!f@`VV}hnrpM4pDR~ zK=X^MbarkPyl6W|m%X4~*Ts8kVPjQuJUk^M`UkTR!tcX|Bi`%r^AJ9~M;z;QW)<*w zyCF*6Ila?tw%b9qnw*{p#sc0py#-tiqj&d3$l=201b625yF}hbt(v3GNC#zcGI*?9 z7l)u#L2wEvGQX9il{iieyq4YV6v3JYhB(sKiLV|6g%TAq1#BK35f6q0kfT`52>x2k z*TB`~VdQ{#LZ&A%@knWpC@{^k&0$X?XmbN%EJ(8DGTngnx*G2@&pe~f&!6wWS<%^Q znVU_?zEabz>6zXs92ZkCqb+;Mc7C5;&>l9OYQ+;^hQV+__B@C{Y?s^WDs)Xu{E=cv z-KAn-UuQ?BOSw8%E){b_R@dD?s1 z?X)Wp5W2od+7|fl`ZRNHOkyXO&qEntJjQICY_79_u|xk@Sm@w%RfZ!j2hY?JwPq4a z&%O@fZ5T>(?*YR#Jj$^eD5v$Mc^ncN)iX&l>m}lnM-73S*V3Zf4jq6fV1@T9QIs1z zr*c$+%0-+W$wN-q8^TM9FNXyf_H z9GFUWMQi{a9WpjIxZoqn7jkm8a*rlrS7910Eg z_w@YC7e4p7e{}C75C8G*-H&Nz>|AN1uzk{Hv)&O7`1>~FNzowsr8B3gv$vaEpbnTO zlXf!8WLCCtqZ-@^9QytiaGuWx^4;R0Xnc!YPCG?HA&~Sra=^+ufT9KBA+)Dicr7L zFUAS;nAAinLrtYUj*7%Mt(Jg4Vn8=%$`$jX%xPoeiZI@h&Mul57ywN0Qmdw5lfWSB z28`HBF1*KPp#7P-B!|00iWaE&+%Bfzz>1Xc*{^NJ>HK1p)-!d|WQWMi<$_G37^i`7 zH_eO=f!*K|_7Y`tIlT82g(rt0cLUdh=7a5Wk$fB7a`gv%f(P%7M}ILF&G?=RSM%BI zw#VVPY>M)1N{^4fy+Jo%y{^XVaJZeCR=rj#=~*`1>)$^fiO8`lr^Yw4(SPm@27cG3 zSXRrW{0Q8f-RrP^$?I`0wCXKyu9Q8#6^;J$&YrHv0r;E6V)=uEW5c_W>6B|}Wo_c6 zuf6awY=-|ZJNrbf%OAcxH8pvuUT=-ACevXaVE6lf^oO*%wLw4i;g5jB927UO-FaRX z5LNF4XN~qV!X0kA#i3E4Wu8JOKoPrpF=i*kS_4pE{ z(^2rU0YS_8AQ^Lc(mLM5rHhwo>Fffqf>$KG+W}Rg@eGV`6VJIR9?My3lfwf7J2D>6 z!RUEXW{K;{#2korUM(wcC1B-frb5>;i}b?kIt^jNjCXfX2UNZQ_aufm5sT-=sbB|B z&BdDBYmDpiD%hx}PMoC4-TUeJS1!|XEJsBHo|bzI147gQv!q6zW(LA^WExVrlQ%_0 z7-IpH98Z_`cl5&Z%Cx$krlLloJt`Y7CmsbnRto$5IGrlk*qonXaPPK8UteF*ZnbZ6 zqMFiZ<>&^j*Ufm@!)|SDZ4M9T^x@%lRLS3?tu1?8DRpKN+4tD&^5asowT5HIr&y$7 zOSMG19nO4NGgYW$U8}3B_w)@7e4Ps&aT9mfb(+Tt{cl2(v;a=r3y~La%v0mcND+l zra(9(#yq7`SyG&C8pY<=PUgj+L!SGfb54Mwh*4MRTt*z@8Un=tAdbQHaxnY>cvTt3 z$UPRgi)Fr45Hu9`O0WWZs}LqM9Mzl24492Pg(2Mxg#*;fRT6mL++U1O22P=;hn%3ug^MCp0LL8Ig=XRImQn0YH9nw)e& zcIB>=1>E?0wJuT$85@?Nyjfr*$xK%8qgMRP4r8$c$9ljwkHIUq%qFEE9%LIlzSTf5 zWku85_36Gnle5-`u(odaT z9(ddwaC?sYE`^Tn8>7)qkLZ2I17-LoaxMmoI4*G9JleGm5hq=&i-sgzCd8?o@T}B< z%~P+I#q><>(#eyR{2o`ezo%Q1tx^WkaBBBsk7;y+){RVwRJSfX*9|Y%E7{em-DjR0 z8JR6DEgiA+C9DP=+GCzQdsYhQ+K^#LhvEKo@Ty;ENv6M4)sEsuFW6k}uX;A*2= zTeOMeeAljN8XOuzB<_K_WDyh+H|Wn<2Ip zrEAv~aWiE~q}zta$?dE(uB#0+Gw*GWzX<5h*i{k@|2(dTT>3wGKqqDn*KhA1$2&OU?wDjHn3 ztej+{2+znkTX=Fjce2!=4BnsHt56UkM~B-=PPdN+A}*Th@snA}igo}T)D>_dFdpan zxNfL%jEi}uaaC(qXK*q+pB8+EbwgqXPfw)^pcZ3i=9eAnR(E$et<{?~N5qWN4O}$3+s2#w;Plp^MYk5nw zt69pgAey@Bv`ViGPKIJchm8B}-L;XCbN~In|L(@b)_U~qrY;{#ZfD>OQ{hy;5V2T1 zEr(St_jPrw*bMyuw+&^oo*Pkb0dHP~F&_W}Fg`wpo6-sGJ}3kURdk9p1wO7fDu}9K zyfCCpmYa}lL3qfDmPj?Q4k>uws|!&oY3+1G4N$^^04ES|3hyX-gBdL$B9tKE7RnGn zU?ib%Ck)3+ro}4MFb>!_UqK{GC~eu9!%8;lVlEHXAuUo6d3wDk;uaObA+RJq9Md+Q zbV)W~P6Tc|M?!8_f#k7q8hDRY#RIPh+76p)DVn6&tu#fU_VG}09YLt7Ho+(as3+i~ zt^l5ouyM3vJ0}>YK?1Bu6wl@6sq=t9oTa*jh?J%5mJsiFvx2?@T24Nj;0lK`W%|aqRt08CpWKG1GAuEw#O+d4I?%M z;~fQIdd1)0ee#l$m2Ihp2U74Ai`_hR>Qp{4y8E!u!@00wX6EsQQtdsz@$di5@BMNj zRXV(~wmH%=>JYAMJi)OvIW~Fj(MKNstJ$+B|7#%}lA_z&QaYQXYNbk7E?=SbjV8x#(K+VY2lFp^KFL)fQZ;+n(Xq?cz*K#CN=( z7+8mf>1`(fah`eNB;1>JN93;)C<}3 zr%r}6cq`5c$lyqg!JB2_Mb|es>B{V7(c_@Ivy-|ZFo50@6qUvh90tWHiX6ZdaU_RP`RNqKXWT#5PrF9?AhhTrjWp01K**60 z4G1eR`i0FH#WOjaN-UOyz(MQSd>K*O$E~qMLOo7|5H^aRM=Vr1x1uJBTAkcwJra(C z>s=0Xg|;G*6R*3X_MPj77%w7SXQ4scGc$(@f6MNZ;`z0MR*O-REb?>v_WgLpH2c*` zuHuvBu2)W%7NadNBwSL(ZPhbwiAIe0+TCfA#aVxvvh$7K>j}&DFgJKX{es!!VNj zbij#q82jHVZSMa2JJ(I~m;S@A{hNQ=Dx{+yu}KYkCKc@q+r5huQ^TLGWn;I+wxfG; zrLvFb^l}ipah*5}*rLdGy;~-hN>3 zkk>2bjE(#m$*Qk?tFiul4Y&Q+`}9wK@{_B*-BTAVl7GEi z$OSK+J$JM>aHpnN^!e_t?!PXV%bu1b@8fRZ9^kY+@DR(ZD+pA)i2Tca0GJOe81N&lsOK-958GLR5&gLvLZ0+#nD}c*XkS`qBQXD7CdW%Z?M%X%I}$m zsMb@Y1E$nw=O_X%+uJ`tUI-McE6Xq(uo2Ara?h}Qj*^$E0)y`vpCoKJ+J?}=q*Yzx z9%~-3392aAPAAPsi7sLS5&l39Yd zQh}uHa5=#@L#VKFtJ5%D+ep&l8su$>b3a-_!Go#^?_S3V0xGV^%|uiIjTF50_+TFk z6Pau#*8tA|r#|4hWaXbkDV}5QFK1i7tac#ZMNF24n&Vgu6Ab08yiIz9ePxOJzKS$DE{PRok;{^T|PGj zofZm0NKinjZEh^%GZt{f1D^w9gVzGX!0o9lbaiP9fn!nR4-brlA$o*F3n$*xEVQsN z!AVSRrz#rm?cOzRsETZvy*AgDd$`#NHv68L>Dhsyp2DM#(i`G+dA;GryCzlo6z&<=Pv+&B<^Jh)j@vetv@HRI+ z5wqenrmd>*&@MW3$6>mBZ616x6fCWsQ6TDd0V6m`UKCNXs)fLdTl#kfTr}Jb3M}Fy zA7`rZkVIhnjkR@Z;xSE#8Xg~%I!MSh1anC+AX_Vocn@W2Ve|HP1ZaA=AMAyjQlR=g-93CQ7&aC= z`f8q3J~ybf%zeJD!Jws5ewR&=`%HhstC%f?XHhWDv>_s|!*H9LPhj0tHU$celW3A) z3%Il$URSdrB5%=l6vhn0ujt$ciRbg0Lg=u`(oV^NB(l}G0F;9@o@>DoJ1cGdef>1p z-w(P9vMX>X!w2rk0682U93q8QA;o2*(XkP5)Nx7!mQ))64hG|2MI^SX!0_C;oJ-LM zdLlFg3akU-hKdt{6|yWt6Yiy!$N{U@sVnRSl@&pN=A~#+qs3^BmR4ghT%9_7Z1@oh zF&7Q9Z0B9`-kab27axzX3%`LfGlU0hyXo{2?cRz75S21b8>PqwQwFt=xF zxD?j?^hT{W)OcBSyL9PUC2#n_h>gE4nMTZJ{N``|=El`a`ZM{|_8p5$OJkecvAr$b zylS_#wmgopTwhDh6e6Km(5)`Rc1E0b*J!s0lB~nPJ9GIk>1YklWQb?Q$9Zce@OojkgHJ zZe?YS_D)Yiz8(NRtx*CHMKg5K!ilj)vD~CtaLjkzaXW+n?)_UQEyuH%A*hJ9ssU?ADk)A_|rCgo3T#*S3Md)ri}s8Ajo z8JO$ojZ_N_Nxza!-EfxS`g%i+cVXcycP*~8TItsn(Es8u{^I&S`N<#oS|sFo_xAe6 zZHo(w$NWBLB3n?e*=>un2>Ul@^kj3R=*erQeM`2q7Nu5gmt<5M&1TVBD`uo*bW`M| zvhlof{vuU@HF->G5;w6<2+-hROEm}#U=wssH*9lS?ZJg2J501R-${Po=>=)3B1Q4~ z?1olbZV?@N&Ye3)cO96fJ<}tg(;5O_atfm=dj-6Z8ZEkr_i^VP`$eiG2Y~G9xh0eX zQfz;hheo;_)Zvryd_~H`;7d7(67Z-62rK-u4#?JFyGe%*9wMvP2N6P}%Zo|6vYHYt zO;vdQP{2k*-9ZFl-2co%>(LCY#;a79pc<-nF=5GRQ}mTej^^}MG7 zU7rgmc@pM}Cr?7stx~X~2c9X8XkMcVJgknRlY7@Tn2FFWf(w?-_B}Fo`q)b5caFJf zm!eDN&d+!LpK)+&=ijfKJxfP!-$&DvrOM=lZy0GlU!j*zpP}1sKLUop zPPCo?B?U1c=qIbq5KXU3Joz$}5Z&3iZ*LU_3aAe3>!oqfUgNzXvANbWHM+DChhY{4 z_2sZ~RS41TP#ZmJRa8V=T8`5);&MB6Q7x@ibr=%WqZbkKHqG~&b8?F*k&x| zZ(=hC!kzle^h9iWY;;A}wN1T&bL|aWuWQ%7ziZ9TzI4QD&0V-r$;rR|D?b~k>ec`A z3!ne&zZmN6YfVi~$m0`3QpoSB`)s!8WZ&QwL)R03{p1&adSrY8d~Z~IB>@WxqQ^o_ zsBv(5f|!@=PBEp26UezK9hVmwt#(kR>CxOe_qOg@{cV9Qn40m&RDipjJT?L$o z0#=l`10|RVd`>gu@@W71|NfPK=MRVf;t&7$_kQi_wZ#Xcn=vmZ;q-S!>i_)be)i<# z;NUk@UHa6-yM81iy+Je?uQ$~5HX7CVu3hCDcC7r$um89Fr#|(6{pQrJse=o%^LOO( zIcF*zr;d={<}}TrXk@9|u3GeLGD9otC#VD-HGyC#&6!bj~dUlm^!09|*K+>xMA5Zl|B^&4z70L6PSvqrVo#Np78h{G?9&Pw^px$cl zc<}z`2KxFRfA-9&qvx+I&QxkOw*tQ0=W{f7PY>sY`#Os*Ti|@u@2|X(hRf>>GhR-{ zNhB_TVYvT>0{Wl%nSYzeMsvSjEEfOoTDiD;b!{V%ils!Knq_&@5rV=M1;<+gZmj@Y zW+6~$B8PxgI7J8-4yRQdnQXLN+TxVk%z@J4gmsrl7c|>Rz}G(<{cX3+xDFk5CXtwQ zeF(K8`b25m`wV>R=6W3BKphfmmq=P~q)NcCWkmWec;XsK$r_A~D9jD1Cx>Yj;zCmb z{;oF3@3ujZ@W8NrpyIgUFM&}g)78~&z<@GksuE5OC~H!UA`lez^+srNfX5q1w3N=# zsrhY6A`orbAU|_XG#HOApGTV<8`<7FGxI5b&{cf$$)`S0DK!SPrVu?0zt>&A>-K{i zR>cwz%8oOGci#DiLIz&1>wt^@)#6?G-~anx*KXwQvFDG);W)1A(p1ATPfPc@tlV|ny+yMfE5M?L*=L5PQEtHcEjk`zWx_)%50CQGIwU?d^=Jz+B4V40>PvKxw?e+p#etpA(A(Eunq&|+kIN@Y zHAfM^iUZ?4w0B~Z95y=`lsvt7d7ifNO=@yx7(Bg%&*=3y^>EO?e8=I#e=t2U^78-u zqrdpEbTW59*GxzihS?tp=zAx|^}+7`hR5#uis$(8%p2`>dA+V}*8i^c?6c3lAs_J5 zpMFC7-QW9{8{4b#sH~`I)nbRBB+Kb~!;#ZV(wndC1%c7elcam(6Uv-HY7D0#(BmEuJ)9Dk1n2g(( zHWPGh6%uotn^w0(KMIG@prG2M-NRk9Z)$?vJ|C^eQ*?C=o*8(!DS`Uqd2`etn@uvi z!hwy+(a|rw`#tae+~Vd|;K?sO_3>P`)FOxG*p>Sn~S=2yoIp>2$WYl1zq+^}2<}V<`yY*pn$c{UH9{>su(a z>lj$t8OGO=+qagDg8yg8acKdE%U5jWQ zScF&I1f{o?EYK!XWjI+lx!uGYby*EjzP<~=+5V|+Ct!~p}@c_m(p%hme7cRVD zyY$Lj2mxJRxmvn&bu)VRRy@A9+^D;?X1n=w(CZfknPPpLL4Nys^$?X_d(d~n3rQ5x zNup7Q(e5lQ5`5;DH^}D>(B4CbDE7)FsAjTIvUnPi(U7QAs?);!G9BJK3>7dba(%h; zO|}XSS>vo*8KPK8@x0;5_W;3QIca?`nr_Z=N!7n(gbb-Ey>-*k# zudf|U##WbsA!onxm9JP|dHEGm?+L~u!xd^?`E{pcl^zy>lWUgr}CQhvXsmtw>SY^X37f^HCH+F#P(dJN*Cc7 zADTWuy*<6Oz8QrG*kDr=qh5>UJgu(9sHZDP3oBa`%Y)_ahR{O@;*VRpX9 zn}J6F@Pr<2wx_7=Zk24nip@b;NW7=doS}h{QOd;Ql;f&kg$JZ?-JK7{*nr1PQLty2 z;*BV2#ky$u&%F~kD~yXnj~zQk?|JxsvN@de_>)i5%NJ%TTYvAQWF4vdO6+B$D06Gi=f~l*Ds#~p6 z-S2i)+c^qs30A~)Dz_!jbcV-<>B(iXp}%36=JnugXSIj1efwjsPf8cFBHE*_co2z| z=MGc5?*V^Lv{QwGx3+YRZr?XU*?^mx*)+94?-`JExjwGO!&Gt_P4@;VSKbh{Z(i=B zz%w_%-9G%bx6(roKTH=d&(c#*{WV=%+@?y6E2Zt=PwJrKs(=ZqsNMVF$A02JP3_wE z`v(pjSo`|dKX)vXss6&+X6h|A1culZU@_d~;}0A=+c(tnsh&{an`3v~lcJkoecv1J z+}sPEa=Fm~Syveyo4gqA?w*f?!*UtQl%{E|YNf2z>ovx`Ez_tQoa?LDy(Xe=*=_UL zU2cB}NBa2Y*4CZND@(^R`MgK|mW)56Et}D+GqA2N^VR?Rn@fy6RmkaZ6?^-8l1_(1 zD;Dy-nQYFN%V$Li5*u{`?7_zBJPmXQsh%&;_I8pgtnsK8k?CcT9kjF&rJ=EL$|jTK zbJ?oPTsI4BLBPV zrZ?W5O5^L<14-NJl3C%v(8{=jq3ocKU|J9?*~wQRCg zF&#FGHnVqfd3bp6GpgNk_ANaJveMz7{+{C8Z(VN+&-=s^PteH7h@vQ)uE!t$q&JyQ zco3soYQ=^{LgZn{!v7got3!7R5l@Y-c#I zl}W}rPR_r$Z+UHfkemEjo&L5OD2sl@rpxPy27K-pB3;2J|LH&d*>Bu+*MrG_`)@yZ z@a&maeqwz+^A4>>JvP;Dy4?2i#KiEzz5{!on3I$Ht)ee2ZVZuqYTCxq^MQ&+zwNfm*@`a+UR;k;FyL25-slc)%MS{oWh7cm);%PrdQ>JVVT!P|ITyBq4ZIn2- zy$MgA4@Ekzc7}Z~-nM_=g5NH=&tEuy@ao*+u|zsOR?`fn1u=!&PH{30;Jb#+G~d(R z^Yy8Tk)|<39m&@g zcW=ET(o2m-%SfiUHMFwgblYG4nV;kSRm(|vOUXWwnTsg3vbT06M<`nBMG!s74TQ}{Z@-g%5zpZLTl zlp{xuD66ZdEsKlmYN@1vu1MJp!{UWOhdO(@`;v*&cruxuSXx}%n@VMRHBEOA4;)fe ziOcL^RCPtR74Xazuh&-%hr`uYOLyn;xvpBZ*4YH_%NQ6S!qgg?wr~G!QK!S3)M|~n z```MuFCIO9_t(GpSAP&{=#BSXc=7oUK;)Rz4aJcuH>?HF3U%()tE!sM>51;yz5A(| zJ-a@0{P@G??!5EP`Y-?TFT0b;r5V7h4}I;aZ+z6}j!>;!)k6Va=Fr}mXWn}Mu|M5E zj#qO{mvy_p9cMRJm&GoA6I&MdYlhDaF?f0NDj-gZOCqS^i-?a zfmEtAu(h>4n8{?jtyZg(pRdtqz{AV%@Umf1E!+>kX}39yKp>b3g~A)Vckg;(aAfc* zjHvb0SD$)teQo{jLOvhjF;sr9n^KiBXFnJ|f3OgaL{8p+?|pxIbpPR#Yxzy}%nQ%n zbNSlhKRb8+<@>nhKTZo1r-7I^XO^p6b52Rf5^%qM8f3wN6LhKT}qQQa& zj1)pGjKR-D%R?QdcwiC~ObMK|gmww{f zwX4UVra8I7xsXqZCMKa^7hcz0hLn8eu^)Qme?0V#d!Fd;Kl*Ch+2!S@eZTV0|J_e+ zZg2lWu~Zu7dCJ__dSq<$!ku^C^=t2W&%3{Q;J|%(dXua-4QvooNa!lhE)r-K0glJZFz^-a~-kFBP$&mFGN5T=Q#)OkF^N7DP6Kou~pBEH$)-Vzb#>tXbIY_L{?C z&+~Xv&`;aJV8^!2Vu`xFzKv?3oX(b#d0lsur>3T~v9Vr9A|0EVpIf-=+SRLva{2sp zHk0pgySliiW7XLPf9Zg{vSH9mewxp3js182^hdiTom>g}3V z4>#&fXA@DNYHcAPD*<{q@BVJXOp#p51e3Z22P}`B&xZsfv$3{)dM?^GAR9 z;kC83hpUy^pv9t?4Nb4V|IzpTzSHUWOtDy8`OV+_z5I=Cj`w=5Hy!5XquUK#*WoBz zW@nQ&NlIBZHVSs5T2(Z?s%En}%T_X~1RY^Zq$lKtcMRGs7B5WIt!suIAK3xA$-$kQ z5X-1~OGnUVDDb2(W&@&HJMss&)RGig$DV0g6h%{2E651F2EMnd=~}tb&~oW?rd%%9 zst`09p-`vR+dEk6?(S@aZ2rc;uFiUwwOc9}#=x1gFOR-*_VnIZDlrW~q)*rNZfLU} zJhu%SK=1DEnfLhvSM65o> zVbQd*TCbNZi1Z-|I~9049PKT9)g*cq;Xxd*%U}Q;phraknh|mbT9RbrPBFS+wP@Yl z5p8DX&RSnzUlSa(i9fBno_8-TEk*MATqK!{cU-=FsW+W2^npR>gop30RvHmN6_3Sg zYkIxjY@{Q&s>qhbSaN$6DqeKo%tUl>a3%{6UD@2+?0xQ;uOFJdeDw~y(=nCJrN>*X zMi_#yWUJF4Q}@K9$NdkpNjw@JSy<}?Ch*%;mVbX|L`CF z-ygQxt@mZKscEg*u!TdRs~w$PU$dyvi68s$KR*A+Bagf;{WHE7>lWMaw=BlSU>#%_ zrjke`ti@u~uIn{Np-^$8(+NkVQunT|E(K$;M4$u=4Eibz#VibI*Mr{%cMNX1T8G+f zV=lPZY_zhqnwA7wPxkc$#iB9i1xas%%<9&TK^Tdhep0j7q4X>@N%@{3nrd+Ac zq%)Z*p0+K?M$7AUrR@&OlEorj>*(mbHaR&lfBg7c=Z+tL*SA$MT)A?^`q|I^dGD2r z7mmiGiMQl(#RoyX`8;mVs?}ya-QCmk?48Gsy!4B|_$$kAHc$JV>j%97i_0 z0TDy9rPEnkwOX(?8Wqr9WeXsIVwDstf;Owq=eD>#K8GTiPKXHjlUxlzU3gPZv6%Pg zas_)bnX*?ZRojl9QUPo%I2?{7Sclxez(`?aWGpqT)4=oH258u=j z*xz^kKpOAj;-W3Ju@O#|GVV&H=qnV!EL5ss8LAL9ijGFDX2pephfO24gSKX~$jt^v zZ#r;<(!wze46Ky<`+Li7w?FT2*ororQVs-MrEqw#QmZMI`|i8%+j>(lR!4AWTUt6D zSYFl` zcNtE?@W^Jf1)^(fJ=t9RSgw$H7Z`-Sx^7wwYAo;G)AyD8?tA!&W5?X9wd0X5sIHLmBoI3h|DWmBo9$`lT=b^VoK(n_F7;_I54onx6d9J05!Y zv-iLKeH(9fO#64OTgLnUb8$d-`t)gcHoF!9$K1QVzA?12vNF8BzBX8?RQezZ2gBir z4ns|W|IIiY?j&&bw#VaHuGNgKOP5ZkIy;9N6B82*1sDU@|1OU8nP)!hf9|E1_g%Vt z{v8W*3wKnD)sCt0@vZTR@fRRWyi_ez77VK=^^uQ!q;dH0;hSC|`-iSu#{0imd?Ys- zX_rx}`eKPpFqcnDzYAa- z3Z0$3Wv@5f7#ZoP1cL(&&|$T|uW}{7WU%j(ls*ZI9!!>^}uu^Mi z=gOLXVS0KjbL7b0)VtpGGxax<$nXzcw{jc)7pu`|G$$r{sw*q;mcAaX%ceC))N+6e zwh>uCoi(@`1Hwk7>G!*8c6(=iU?5nHME1du9gS3qv_~I(wE4e#e47aFPIw~y&wTB( z2Y%@zAKW)MJ{Wd|{Bn0sU$T_WUNr0G`dIJi>Vf-@#@_Ol_hw&v{*TfR(z@k&Z-T|# z?DqD8e{*iRV}5z5FOyA%db)@GF1OQu=7r}KNu`p%vvX@?W^8@9cQE=RKk_q~?{_@= z_gS}$_j|AyFMs`sCp@i2&Y4POT^6U+W%q$+tCRza%S*wPm9?PL?x^iQbYy@h?7J93F{ty}BXy0vbtTkF=kwQj9j>(;upZmnDE z*1ENBty}BXy0vbtTkF=kwQj9j>(;up{^9HY1IEL#9Oc7)B>(^b07*qoM6N<$f=lF; AdH?_b literal 0 HcmV?d00001 diff --git a/templates/prebuilt/tabby/README.md b/templates/prebuilt/tabby/README.md new file mode 100644 index 00000000..4a35d63a --- /dev/null +++ b/templates/prebuilt/tabby/README.md @@ -0,0 +1,149 @@ +# TabbyML/tabby on Phala Cloud + +Deploy a CPU-safe Tabby source and release verifier on Phala Cloud. + +## Metadata + +- Template id: `tabby` +- Category: LLM Inference & Model Serving +- Template repository: https://github.com/Phala-Network/phala-cloud/tree/main/templates/prebuilt/tabby +- Upstream repository: https://github.com/TabbyML/tabby +- Upstream documentation: https://tabby.tabbyml.com/docs/welcome/ +- Default source ref: `v0.32.0` +- Icon source: `tabby.png` is copied from the upstream repository asset `ee/tabby-ui/assets/tabby.png` at https://github.com/TabbyML/tabby/blob/v0.32.0/ee/tabby-ui/assets/tabby.png + +## What This Template Runs + +Tabby is a self-hosted AI coding assistant and open-source, on-premises alternative to GitHub Copilot. Upstream Tabby serves code completion and chat APIs, integrates with IDE extensions, and supports local model serving through the Tabby runtime. + +The upstream Docker and Docker Compose quick-start paths are designed for full Tabby serving with selected completion and chat models. They use model identifiers such as `StarCoder-1B` and `Qwen2-1.5B-Instruct`, request GPU devices in the documented CUDA path, and mount `/data` for Tabby state and model data. + +This Phala prebuilt template intentionally keeps the default deployment safe for a CPU-only `tdx.small` smoke test. It starts a small Python HTTP service on the public `python:3.11-slim-bookworm` image, fetches a pinned Tabby source ref from GitHub, verifies Tabby CLI, HTTP route, model-download, Docker, and release markers, then exposes JSON endpoints for health and inspection. + +The demo does not start the full Tabby server, run `tabby serve`, download model weights, load a model, require hosted provider credentials, request GPU devices, use host bind mounts, or require privileged container features. + +## Services + +- `app`: Python HTTP source and release verifier exposed on container port `8080`. + +## Ports + +- `8080`: Public HTTP endpoint for health, demo metadata, and an OpenAI-compatible model-list stub. + +## Environment Variables + +No credentials are required for the default verifier. + +| Variable | Required | Default | Purpose | +| --- | --- | --- | --- | +| `TABBY_REF` | No | `v0.32.0` | Tabby Git release tag, commit, or simple branch ref used for source checks. Release asset checks run when this is a `v*` release tag. | + +If you adapt this template for real Tabby inference, add only the variables required by your deployment, models, and access policy. For private repositories, hosted model providers, or gated model downloads, use Phala Cloud secrets or required environment variables. Do not hardcode tokens in `docker-compose.yml` or this README. + +## Deploy + +1. Deploy the `tabby` prebuilt template on Phala Cloud. +2. Keep the default CPU-only resources for the source and release verifier. +3. Optionally set `TABBY_REF` to another public Tabby release tag, commit, or simple branch ref. +4. Open `https:///healthz` after startup completes. + +The first startup fetches a small set of public Tabby source files and release metadata from GitHub. No private models, paid credentials, GPU devices, host mounts, Docker socket access, host networking, or privileged container features are required. + +## Usage Endpoints + +- `GET /healthz`: Returns `200` when the pinned Tabby source files and expected source/release markers were verified. +- `GET /demo`: Returns upstream source metadata and confirms that no Tabby inference server, model download, or model load is running. +- `GET /v1/models`: Returns an OpenAI-compatible model-list shape with an empty `data` array because no model server is running. +- `GET /`: Same readiness payload as `/healthz`. + +Example: + +```bash +curl -fsS https:///healthz +curl -fsS https:///demo +curl -fsS https:///v1/models +``` + +Expected `/demo` fields include: + +```json +{ + "ok": true, + "check": "Fetch a pinned Tabby release/source ref and verify CLI, HTTP route, model-download, Docker, and release markers.", + "cpu_only": true, + "model_downloaded": false, + "model_loaded": false, + "inference_started": false, + "tabby_server_started": false +} +``` + +## Smoke Verification + +Run locally from the monorepo worktree: + +```bash +docker compose -f templates/prebuilt/tabby/docker-compose.yml config >/dev/null +docker compose -f templates/prebuilt/tabby/docker-compose.yml up -d +curl -fsS http://localhost:8080/healthz +curl -fsS http://localhost:8080/demo +curl -fsS http://localhost:8080/v1/models +docker compose -f templates/prebuilt/tabby/docker-compose.yml down +``` + +Template validation commands from the monorepo worktree: + +```bash +python3 templates/validate.py +git diff --check origin/main...HEAD +docker compose -f templates/prebuilt/tabby/docker-compose.yml config >/dev/null +``` + +## Extending To Real Tabby Serving + +For production Tabby, replace the verifier service with an upstream Tabby serving image or binary and size the deployment around the chosen models: + +```bash +tabby serve \ + --host 0.0.0.0 \ + --port 8080 \ + --model StarCoder-1B \ + --chat-model Qwen2-1.5B-Instruct \ + --device cuda +``` + +The upstream Docker documentation uses `registry.tabbyml.com/tabbyml/tabby` and a persistent `/data` mount for full serving. On Phala Cloud, replace host bind mounts with named volumes, pin the image or release version, and add GPU resources only when the selected Phala deployment target supports them. + +Before enabling real inference, review: + +- Completion model and chat model licenses. +- Model download size and disk requirements. +- CPU latency or GPU memory requirements. +- Whether private or gated model access requires credentials. +- Whether the deployment needs authentication before exposing IDE, chat, or API access. + +After a real Tabby server is running, useful upstream endpoints include: + +```text +https:///v1/health +https:///v1beta/models +``` + +Tabby also exposes completion and chat endpoints when the corresponding models are configured and loaded. + +## Security Notes + +- The default verifier exposes unauthenticated health and metadata endpoints. Add authentication before exposing real model inference, private repositories, or user data. +- Do not put secrets in `docker-compose.yml`. Use Phala Cloud environment variables or secret handling for credentials. +- The container does not request GPU access, privileged mode, host networking, host IPC, host bind mounts, external build contexts, or Docker socket access. +- Pin `TABBY_REF` to a release tag or commit for reproducible deployments. + +## Cleanup + +For local Docker Compose testing: + +```bash +docker compose -f templates/prebuilt/tabby/docker-compose.yml down +``` + +The default verifier does not create named volumes. In Phala Cloud, delete the deployment when you no longer need the CVM. diff --git a/templates/prebuilt/tabby/docker-compose.yml b/templates/prebuilt/tabby/docker-compose.yml new file mode 100644 index 00000000..ebd62c68 --- /dev/null +++ b/templates/prebuilt/tabby/docker-compose.yml @@ -0,0 +1,287 @@ +services: + app: + image: python:3.11-slim-bookworm + ports: + - "8080:8080" + environment: + - TABBY_REF=${TABBY_REF:-v0.32.0} + - PYTHONUNBUFFERED=1 + command: + - python + - /server.py + configs: + - source: server_py + target: /server.py + healthcheck: + test: + - CMD + - python + - -c + - import urllib.request; urllib.request.urlopen("http://127.0.0.1:8080/healthz", timeout=5).read() + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s + restart: unless-stopped + +configs: + server_py: + content: | + import hashlib + import json + import os + import platform + import re + import sys + import time + import urllib.error + import urllib.parse + import urllib.request + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + + STARTED_AT = time.time() + UPSTREAM = "https://github.com/TabbyML/tabby" + RAW_BASE = "https://raw.githubusercontent.com/TabbyML/tabby" + GITHUB_API = "https://api.github.com/repos/TabbyML/tabby" + DEFAULT_REF = "v0.32.0" + MAX_SOURCE_BYTES = 2 * 1024 * 1024 + + REQUIRED_SOURCES = [ + ("README.md", "project README and positioning"), + ("Cargo.toml", "workspace version metadata"), + ("crates/tabby/Cargo.toml", "Tabby CLI package metadata"), + ("crates/tabby/src/serve.rs", "server CLI, health routes, and model loading logic"), + ("crates/tabby/src/routes/health.rs", "health endpoint implementation"), + ("crates/tabby/src/routes/models.rs", "model-list endpoint implementation"), + ("website/docs/quick-start/installation/docker.mdx", "upstream Docker deployment guide"), + ("website/docs/quick-start/installation/docker-compose.mdx", "upstream Docker Compose guide"), + ] + + + def read_env(name, default): + value = os.environ.get(name, "").strip() + return value or default + + + def source_url(ref, path): + encoded_ref = urllib.parse.quote(ref, safe="") + encoded_path = urllib.parse.quote(path, safe="/") + return f"{RAW_BASE}/{encoded_ref}/{encoded_path}" + + + def fetch_url(url): + request = urllib.request.Request( + url, + headers={ + "Accept": "application/vnd.github+json", + "User-Agent": "phala-cloud-tabby-template", + }, + ) + with urllib.request.urlopen(request, timeout=30) as response: + return response.read(MAX_SOURCE_BYTES + 1) + + + def fetch_source(ref, path): + url = source_url(ref, path) + data = fetch_url(url) + if len(data) > MAX_SOURCE_BYTES: + raise RuntimeError(f"{path} is larger than {MAX_SOURCE_BYTES} bytes") + return { + "path": path, + "url": url, + "bytes": len(data), + "sha256": hashlib.sha256(data).hexdigest(), + "text": data.decode("utf-8", errors="replace"), + } + + + def fetch_release(ref): + tag = ref if ref.startswith("v") else "" + if not tag: + return {"checked": False, "reason": "TABBY_REF is not a release tag"} + url = f"{GITHUB_API}/releases/tags/{urllib.parse.quote(tag, safe='')}" + try: + data = fetch_url(url) + if len(data) > MAX_SOURCE_BYTES: + raise RuntimeError("release response is too large") + release = json.loads(data.decode("utf-8")) + asset_names = [asset.get("name", "") for asset in release.get("assets", [])] + return { + "checked": True, + "tag_name": release.get("tag_name"), + "name": release.get("name"), + "published_at": release.get("published_at"), + "html_url": release.get("html_url"), + "asset_names": asset_names, + "has_cpu_linux_asset": any(name == "tabby_x86_64-manylinux_2_28.tar.gz" for name in asset_names), + "has_cuda_linux_asset": any("cuda" in name and name.endswith(".tar.gz") for name in asset_names), + "has_vulkan_linux_asset": any("vulkan" in name and name.endswith(".tar.gz") for name in asset_names), + } + except urllib.error.HTTPError as exc: + return {"checked": False, "reason": f"release tag lookup failed: HTTP {exc.code}"} + except Exception as exc: + return {"checked": False, "reason": f"{type(exc).__name__}: {exc}"} + + + def build_check(): + ref = read_env("TABBY_REF", DEFAULT_REF) + files = [] + errors = [] + sources = {} + + for path, purpose in REQUIRED_SOURCES: + try: + source = fetch_source(ref, path) + files.append({ + "path": path, + "purpose": purpose, + "bytes": source["bytes"], + "sha256": source["sha256"], + "url": source["url"], + "present": True, + "contains_expected_content": True, + }) + sources[path] = source + except Exception as exc: + errors.append({"path": path, "purpose": purpose, "error": f"{type(exc).__name__}: {exc}"}) + + readme = sources.get("README.md", {}).get("text", "") + workspace_cargo = sources.get("Cargo.toml", {}).get("text", "") + tabby_cargo = sources.get("crates/tabby/Cargo.toml", {}).get("text", "") + serve = sources.get("crates/tabby/src/serve.rs", {}).get("text", "") + health_route = sources.get("crates/tabby/src/routes/health.rs", {}).get("text", "") + models_route = sources.get("crates/tabby/src/routes/models.rs", {}).get("text", "") + docker_doc = sources.get("website/docs/quick-start/installation/docker.mdx", {}).get("text", "") + compose_doc = sources.get("website/docs/quick-start/installation/docker-compose.mdx", {}).get("text", "") + + version_match = re.search( + r"\[workspace\.package\][\s\S]*?version\s*=\s*[\"']([^\"']+)[\"']", + workspace_cargo, + ) + version = version_match.group(1) if version_match else None + + content_checks = { + "readme_describes_self_hosted_ai_coding_assistant": "self-hosted AI coding assistant" in readme, + "readme_mentions_consumer_grade_gpus": "Supports consumer-grade GPUs" in readme, + "workspace_version_matches_ref": bool(version) and (ref.lstrip("v") == version or not ref.startswith("v")), + "tabby_package_default_run_present": 'default-run = "tabby"' in tabby_cargo, + "tabby_package_uses_llama_cpp_runtime": "llama-cpp-server" in tabby_cargo, + "serve_args_define_model_chat_and_device": "struct ServeArgs" in serve and "model:" in serve and "chat_model:" in serve and "device:" in serve, + "serve_loads_models_by_downloading_when_needed": "download_model_if_needed" in serve and "ModelKind::Completion" in serve and "ModelKind::Chat" in serve, + "serve_defines_health_and_model_routes": '"/v1/health"' in serve and '"/v1beta/models"' in serve, + "health_route_returns_health_state": "pub async fn health" in health_route and "HealthState" in health_route, + "models_route_returns_configured_models": "pub async fn models" in models_route and "ModelInfo" in models_route, + "docker_docs_require_gpu_and_models": "--gpus all" in docker_doc and "--model StarCoder-1B" in docker_doc and "--chat-model Qwen2-1.5B-Instruct" in docker_doc, + "compose_docs_require_gpu_and_host_volume": "capabilities: [gpu]" in compose_doc and "$HOME/.tabby:/data" in compose_doc, + } + + for name, ok in content_checks.items(): + if not ok: + errors.append({"check": name, "error": "Expected upstream source marker was not found"}) + + release = fetch_release(ref) + if release.get("checked"): + if release.get("tag_name") != ref: + errors.append({"check": "release_tag_matches_ref", "error": "GitHub release tag did not match TABBY_REF"}) + if not release.get("has_cpu_linux_asset"): + errors.append({"check": "release_has_cpu_linux_asset", "error": "Expected Linux CPU release artifact was not found"}) + + return { + "ok": not errors, + "ref": ref, + "version": version, + "upstream": UPSTREAM, + "raw_base": RAW_BASE, + "files": files, + "content_checks": content_checks, + "release": release, + "errors": errors, + } + + + SOURCE_CHECK = build_check() + + + def base_payload(): + return { + "service": "tabby-demo", + "upstream": UPSTREAM, + "python": sys.version.split()[0], + "platform": platform.platform(), + "uptime_seconds": round(time.time() - STARTED_AT, 3), + "source_check": SOURCE_CHECK, + } + + + class Handler(BaseHTTPRequestHandler): + server_version = "tabby-demo/1.0" + + def log_message(self, fmt, *args): + print("%s - %s" % (self.address_string(), fmt % args), flush=True) + + def respond_json(self, status, payload): + body = json.dumps(payload, sort_keys=True).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + path = urllib.parse.urlparse(self.path).path + status = 200 if SOURCE_CHECK["ok"] else 500 + + if path in ("/", "/healthz"): + payload = base_payload() + payload.update({ + "ok": SOURCE_CHECK["ok"], + "status": "ready" if SOURCE_CHECK["ok"] else "source_check_failed", + "cpu_only": True, + "model_downloaded": False, + "model_loaded": False, + "inference_started": False, + "tabby_server_started": False, + }) + self.respond_json(status, payload) + return + + if path == "/demo": + payload = base_payload() + payload.update({ + "ok": SOURCE_CHECK["ok"], + "check": "Fetch a pinned Tabby release/source ref and verify CLI, HTTP route, model-download, Docker, and release markers.", + "cpu_only": True, + "model_downloaded": False, + "model_loaded": False, + "inference_started": False, + "tabby_server_started": False, + "message": ( + "This Phala template verifies Tabby upstream source facts and release metadata. " + "It does not run the full Tabby server, download model weights, request GPU devices, " + "or mount host paths." + ), + }) + self.respond_json(status, payload) + return + + if path == "/v1/models": + payload = { + "object": "list", + "data": [], + "demo": "No Tabby inference server or model worker is running in this CPU-safe verifier template.", + "source_ok": SOURCE_CHECK["ok"], + "source_ref": SOURCE_CHECK["ref"], + "tabby_version": SOURCE_CHECK["version"], + } + if SOURCE_CHECK["errors"]: + payload["errors"] = SOURCE_CHECK["errors"] + self.respond_json(status, payload) + return + + self.respond_json(404, {"ok": False, "error": "not found"}) + + + port = int(os.environ.get("PORT", "8080")) + server = ThreadingHTTPServer(("0.0.0.0", port), Handler) + print(f"tabby demo server listening on 0.0.0.0:{port}", flush=True) + server.serve_forever()