From 12ca136fba82191ef13e34635a0f1e3afcfba939 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 20 May 2026 16:40:57 +0400 Subject: [PATCH] v2.5.0: AI BGP multi-provider routing with automatic failover - New AI BGP pool manager (create/edit/delete pools) - Each pool has ordered routes from any configured endpoint - Failover: tries primary, falls back to next route on error - Pools appear in endpoint dropdown with shuffle icon - Pool editor with route add/remove/reorder - Fixed TOML breakage from multi-line paste - Added OpenAdapter preset with 0G models --- CHANGELOG.md | 14 + codex-launcher_2.4.0_all.deb | Bin 28318 -> 0 bytes codex-launcher_2.5.0_all.deb | Bin 0 -> 31568 bytes src/codex-launcher-gui | 506 ++++++++++++++++++++++++++++++++++- src/translate-proxy.py | 120 +++++++-- 5 files changed, 606 insertions(+), 34 deletions(-) delete mode 100644 codex-launcher_2.4.0_all.deb create mode 100644 codex-launcher_2.5.0_all.deb diff --git a/CHANGELOG.md b/CHANGELOG.md index 442aef4..26736a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## v2.5.0 (2026-05-20) + +- **AI BGP β€” Multi-provider routing with automatic failover** + - New "AI BGP" button in main window β†’ pool manager + - Create BGP pools with ordered routes from any configured endpoint + - Each route has its own endpoint URL, API key, model, and priority + - **Failover strategy**: tries primary route, automatically falls back to next on error/timeout + - BGP pools appear in endpoint dropdown with πŸ”€ icon + - Pool editor: add/remove/reorder routes, pick endpoint + model per route + - Up/down buttons for priority reordering + - Proxy logs `[bgp] trying route 'Name'` and `[bgp] route 'Name' FAILED` on fallback + - If all routes fail: returns 502 with detailed error per route +- Fixed TOML config breakage from multi-line paste in API key field (`_toml_safe()`) + ## v2.4.0 (2026-05-20) - **Added OpenAdapter provider preset** diff --git a/codex-launcher_2.4.0_all.deb b/codex-launcher_2.4.0_all.deb deleted file mode 100644 index e9e008ef83339aaab26d631a166079099426ec0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28318 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 diff --git a/codex-launcher_2.5.0_all.deb b/codex-launcher_2.5.0_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..bd99b79f1c7267df16d36f624cece633f8c8bbb8 GIT binary patch literal 31568 zcmafaQ;;r94CUCiZQHhO-TB7m9ozOD+qP}nwry+wx83PdVvM_X#l{z{%J` z0LH}B$P!>jZ)9l)aPlA`B4Xy`qr6B7p;8xhfe>;J$IhJ}F% z#@OEOuamtE!(V_CgQxS~|F8dl(f^-6RWswa2@s_^77zt(FtdZT^VjcxsI>aIJ-j7@ zr*>LLwv!`cZ9L?}TJER6;rsE~peQabHnkAqKdV(-1NH!N2l@-N6gniN{ad`I+IL2` zz;)Di0juVOEoKPQ?g@x^RfVRtS2)`q^=%i=?V|1|h$L^CxYWPzYL(*UB957{GLg1c zLd`cd3ag-t%)pBZ)Jq1hXPD>%VyP6YR*vk^a7)Agxz38phC+8Fflvig(N=X)o;lLf zG;u1TdN9|lUpRc8A(MEv3aEo_%14C?B%(@0Gwrx8rjSmw{^h*9voRe%Db_qs;rui< z&KU{bL1fn~e`#f}9^~UDh1~qe*wnt7++>)#b5_1RGWW!MPbYz0=z>s3K2|xDw3-uj zK+?|2W9q}G-n6HawUgN77|cWfVd^MTh@g8bhexr6^(7de{^-`(1l&|Tue+kXMB@=g z7xYsfy_HvS6iy?e!o%5H-|f`gMJ;IT<|t?#Y%V$>a>A^dPqDN&ZEU6j?^yJCP(PWE zwKm#GMBYcaC-q~8G6IQBAD{w=U-o8Bf@dYLeokW8 z-O_pe^}e&kwludXKEAM>XC)<+fO2wBxV^n7R(S7I1drN-9`RQfyYn(JqEZTl!pCj~ zesID&!xJV$f+1HRiTKSgm;@GX+rW|YOM-9(J+r{PCoq(ep!O7xH|#TPOauLkQL(JJ zJQDrQ41~sa1H`hQ4_u5T{^%L?bs&yh2mz_S_Zgm|WeH@OM@S=Th*O9(7?!$E`Qx-+ z0l@RJ0%Ci<8&)in?U+%D!0dEb#gJvu?vC%^)-!B86DE)jAPbx82H;MLigua+{sR6Vq5fmb|BsWbOzd3$ zo0o<#|C3W-7DrDYG8z1Ga!Ar z?W4BddR(HfvPB*0Fw|Zz)y!0HjqEs`__*O`5c2*Wc-dapM+|#dM_s#8PP=%<{_a)o zF4pz_n4RDrb_qrUUKhC-js27`T4Hq1Z?A5L(`%MGt8riXYXz;pJvQ6Aw_7nwFNa^* zzh4b!wsPG>IWoC)gR8Q&uTS8WcV7p!U3?zBqgrdS{C@Cqa~{0br#v^Sd!f&+d^g{> ziq=9iE@~nkq?C`vi+E_ymTq*qKWx+ZXmW~IDZyu;T?vT`sJr>?ylPo z;tqQHjxdZg)#(3Kn9cGi?xC&bx~SeZrz6qx{E+|LTliK4)~nD@-cvaxJp2}Qh!9~^ zLmuVL{8=TjFQzD|;4Hjd&Gqw2Qn~TMkReUnvass4>V587{^8(#WF4iXqQfplta##Y zvL4CB-w}Uq&5+>FvV7+I9gK~39@!bEFt_q>W%sJ-J-JSrd+|D{#utI{86~HqVvyHk z`0}m0)YsP(SXC4}+@L53gl*#jpbEwG1(k5wYSc`wgFsB` zQDMW0j@&Xy>dzQzU7b66I3I;>!!XrO_0fbsHzj6_w=-1ZBs#s$u*ZDYAy(gX<~mo5)pe>g@!OW*3& zOWuCkFB0bg!L<#E2P+CeVds2e94Rw1NzqAxl0En<_3nD&&uo2Gl(g~*&-nkBd6z2~ z0^>;oK6YN96Vz$$+ey}1x~nvz5~*)9Sbq>L*?)t>ynZ!U+iZMuvRv2F0lf0-SyYh6 zWlCBk7D;aD`MVm^#8O&Z-HWeoELFQfoxOT`*}lsc-xSy#i_JLOmsLW8OBxKHe2mql z(|LpS{8#=@2VIS=x2-cXf`9n4e!qtQInK-M{!VTp1AzkJ2aab4lyk<%B%lNBwwfs} z-OZSL<^fTiCC_G(?sX!}s)hv}=h=6>7y*50lr9?|y&kP5olZ}YGgU$2;i;e<`}AzX z;5)CgV*SiIe(U&!HlEg0SL5ey4R#h11`4{5WZ2i9$bAO!Drv(fFB8?1WR>hF?6{q7 zUJ*wyiP40{OY#%I6C=&+JbCH(nD4+d?fqALx_vi?%2$Xnoq3NuUh>QxJe`2{gwP}S z$llwpd>0k+O1UcOAVMKVtMMSDo*hiSxyyKYvv3_|oiA&FBRGlbt1gt;@9HWbAYfIr z6D2{Di}r~vEy{2llz_2@M$qkP!Gd+4!?^{|h*O6nh1=7&q#McOE{${*Ki@ZZrm~{e zz8ku@dP8J)R{PxtNG2knp`jBGRK~@m45_58i(DSrVlzuCCoSk-sH!~$BFf^++LWlI z%4kAqq`t0aqR+oG7T!8=Ue+_y+RD0@qbTQ6VM=&#=?hPPCE^0cFAxgw443EGe zZMS4&E0A`EOaZ)dPEiX%3EqBYr*P0Gp(LqD&>rna7d)iCJ8ypcz1=bd+d+r1`cot;fIn?^(i#IiB(JFlhlm?C(37!N6%ksB>i9U^n&R;o7OOtMs@CDHI zN9hrF+E_&gvwNAm_VesEL$D#Jp3$5E-?=zDcH{N&X@@m{$7spEaljwSaPJ|DFv{by z_U%Ogv>WW4w*|7rxVp8Y#CcX$6?Jp&$*8Lc)`c59ze{xUa>vC7sP@7!Ml45g z)(@yT`dY98nnfNJ%Bm(ISl#}EJ$DdKNfU>fygfjPv{A-S@xmufmari;@}I(mh6i`G z{*qti_OhoY$T@3c&#$MS`=ymj&dy0sws%2j89GMD*@-R5wSnwIX)( z{7~>X2n!&CgobaQM}9|*`@}c4#5Z%-^>oV5Z}PpNGMgY4-e`d9rqrXidHB>C7!NH; zH1J@JC55P8`E(?!2TPPBalEjCAkp%`P>5MeE!lKEd;8=}Y){+rS?}U?YV%f?nv5ok zL^)v}UXqFFY<6(LwWQjK3}q?znF0dF=~qfi$ty@uw0L>P8K|H2y7(u@!!J7^Va55$|FEWxVdriji;M%1~LuR zWb8|toXjkpjjm0F|Lk}aH=H+Gozi?ig`rRkv=J+bcWCI-OJ6-FZr}gl5po5GG8A5s zs22-Yss);XMZWc;qTL@@|1!ckvp`$A9D0=-GStCfM?{H(RLZHvcS%{=ih2!7iAN9a z^ZDu`Pg>Pt`EqE*h0=Llw%f2+xhp}?JNH>OYw?aXR@oc6LwcilyovcG&+swVc{j9I z3#_CMIcljE%aqT2Y2i zp6nz!QL}hEj2MYcZN&v38H&qDQ5Um* z(pmp}Vp{+1yXFofVwl%~SQSm3UMG_257_9}!w!Q~))}HmxwHq1*HzXyJ#sL-*CdGy z;-;ql?IugJy?y-VF=_pQa$wd=oL4wclx5)l&TtD(^JRsqEGC@zY*d|Rw{=oIoqdPd zLxqiqes+NLEK*+@>XC|VpiAs|ZW543En-_)S&1p9b4&ZWZ|~YDkg7ag7%|Wv*~nSR z{5gQRYsyQ|y4te3Exmu4hEgzDTAHDmQlvkjN{BNlM84Hk4xho4{$Ll7f?HZRMh-rK z1nKjlHem1(uwt_)oJVR0#qb9vjM&;2XvZP_Km&UfN}hdhmDu;rp$EOQt++{=>3nQn zMN?qdRXR#h*UkmIDLA{i*Dc;9m~A0I{^X;@niMO7qq8b461--P-s`cl6t@$}ow$TF`+w<~$#VC%<)<0l@m1g2q>_1~xAE6^R^SxI z9|&V&$BT!w;KA(25c~4W6qC}o zN8#=&?ayKl` z6lmxVkQJgTrkh~7>^l6p$1ohZ@$xJC7ag%d^9$pD_eVZ1HY_GXqDYhu*|#QN+<)=@ zrouo0;*^M{PYGlF8|#*kHwXxjNO?dIcsTJ=?7L2Rj@_VT($z+(#`KB^RZ2(IK`cgu zQ|Hj5l*fM;T^p>Z-yAr@CV4!3=K>BL_8j-P{8M94Q;dtHOh5Y9@eSkU6+794J>tjo zXi%vp)mdYSzJP+c;i?KAgtWB~C(5x%7}Vhs!}aamw*N)rgkF2nhH`^fG?E@vqY0$# z@K6_FC>z@t$Mf?81)3; zW>coVTP#?h#&n$(HAyd$S@cbECWT4xaEWF2Pg}?;KgHlhJc*H_Sk{1J^-GWzx7}&Q z8|){R>m7y2lO>sv7OKXCU|^xPRj#o9S)9QQlWuUiiI6b(6rdo!agfp+QJK1pDGKG4 z6R?t#QX;}Zp@UEOGk@HZo-`WZMMfKVGFUp9ADL>6CqXnnVn>i9F%e@1!izN!t>2Mg zR<)l6NVsAR-R9}9n*fW51_uWL5319{S}s%>=ToAi!deR+PAn%vOO6IhX|F4bkI&%N z$7C10YSwMdxPRJS>NNU0lI{zY1YSi_h9}3&P6UQD$NLB>?P%lffzvnAmDUzU?F9#> z=At~e*Av{HcB2U5ActLiQZ65@i8$)U<&4PSNcjn68AyUEEb$pP31W`>fMt0GE#dBHTb3Wwfb9qfYs0xHvH1u$`|@Ol8RA+Siz0Y|#RQQE6F# zYd)RxHuJ&y<09)dtLqn`7$tuIbZP(vz`7Al_pQsG8T}kq|EkqhkEKAgWl<;NRBytR z226#9MJhq{;o*cOA2>{&dluirfzv*(28X~vJMJLeF=WWZL~Sd9GRTxX6>4y^?qB<-HZ z*GEPLIi^R4!Q`2vP;&qGBsg+s%Av5Gtj+$pYaoyqsO#V#k^!^s)#rArDGYlgADKFb za)4p}+}kJF$kMz3x45#vJNO}RkT5fe#t}dr1DqLdu#RUS>+4#2p)dY2iv&4NboQK^ zCJ%uq=0}CvHD~lKY)2w8D@MX#%Ig-JMv9E2AEfv#H=7`}MVu15L2l8KAuPJv2@*C} z-P1bNx|?XFFbOweN%>bk`CzIw$i@1HFfw{$gxvl7Rp25M<6@6S~W*R+- zW1K)1jhsa3q|-WM+mmBYkB5Uz7RBZ*W`~D>#}@_)<9y<(-+qmb#~0ehRO{ zGijnsk`z?j-8d^zXu3Sy#Cob-kxYQMI3BW=N_Mvt=Mb^2gl;|UE<99ncQM=3;>qE_=Hw`&yFH2#70Ah5U(>8usTX?V+b;5 z6)u{90h#Mw_O#L}4uhY84FrTl1Qf&sgoJ`cIyJ14US`<=3RVCe5hoEoICN;JYC#8; zXcP`6suV0jBnIaXVaUW$Rt`c*!Fqlam@|^KYLV}gw-;^(>Fg;iVa_D7I*3cwcB0s# zKkxpXo419Q9LC<3A?0~+2MnfEDK;!r=;Z|&eRL0|MJ6$^h}x~q56! zEp@;tN_h4r-KJu|rC*V3kv|nGmpjkgZC=ypPU;>Z(cT(S>aH+>S!P1QhkrQ8P*oow ztuaAYtIT6QPCoNji=3`^JQM#_6^MT;8wQZy(=OGYk_#!;--GkYcL4zzATJDdgcGsj4 z0)1)m@@x6_vc1QMjcsDz;Tcbki(mS%9~%zR54^-C+JEQgZGW|SQ9bHiWC9IqRK2!% zIUu1WxM5QB0i&fYK%acCCCNl6U4XTcMsI}v;B+(F z(09BuT3ti8Git`ZLy}}?>YY|?kq$vg@0NL6{Ky7EQtF}GC5-pO#M6etxIW%vEgDLC zijjj`q8a;dx#GYzCNF(ljLUPW?Q*?!44z$mg~nesg$8lHHa(l^#2Dp|6C}Pr#?b)( zXr1agjgG>6eiBslf+H->O{oCGH6o#vF~3F&>DP~we}@oMRRtuE;&h3C7zeVYmqu9G z*c1d*9FGw>UDQRxtcWAvphn1^1IyJVU(a!~2D;QmOs_->wGKL^cA12$rEVlHg06Rw z^~eagNTJo9b*Rk2c(K(|OrJRn9nvz(oT%$T)=QZg|<^eMfIQ(rIEE0l{BAK)B z%i_uKBsVWL47j#C5F>^KViKK($l26r?C3Q%UUtNr1v@V7+{vUYCF3By@0(&WTw;J=(D+7~ZK^h8~2^B-b%_`(9UQ*l>^+|ha( z&{m-goZ=iV!MGIR*6-*Gx@pkY2MeKEcnvtC+hTTC;HvBlyB)G-7)Ru~#&@vWvNi1B zerm^oP!`eG{kf}36OC|J^L{ZzZ#ZE7L%AnNbEBjDWRu7eM(fC=v7;Sp8Pm*}GmRU! zXI=aia=ZoFE@`)sxVzbbi%?BV19O~8RLr_K>X7tJbwd&Kps+-(X+o8WH1dwcgE$m# zXI@hTPg(Dx3xVgTVCl}%=|n=gEa=d@Ta_YS5t^r|yzPRpA`|`XnfMxrNCI^{OpBhm z)?R4@=(f0KqHsS#AN?7;*t;M)oABXoqCj~g1{;A82m~>#?irEMRUvj)aG==3xMt|%&Gi+lAH2uENqEGQ_A zNJYKtKvh&VN%ZW+`oPqhl^-MUh@mLF=?N_v@q_ouBU@quOXB3R1eCq-c(ftP21zdx zi7GR;aFdhT_F(>g2d*0h)Hd4b%cHnWuh7rj@-sI5GYRDKT2D{ovcvUk^9GYjTFkC; z0*!dCjY#qu7J_@!;3LuQSboe#i@`ve0mo>U?!V@{-YHEKQx@54aajK>o=IeZKh+9sI+%Rkkg|S7Ok&$`rd?&$BRfWhp$jl! z?lCvf)6au97FWX6Lj^Q?0e%xmA-SuL3X&kx|`KXe5e6A7ovOuCA zJ^hO=Z?LS}I6n0eP%QrGl_5Oz0%+3chxlRn9}G8TKlQd5DRZ>;zJ12_hu`-=!R$2Z zGpLsaLNf@;o;kIEbySKd5$i=-UTM+8+@dZJea>c{WfEecx6N#4rp@eX`$UEi@ z3S=DUevD3Z#(hvk!{6rapnd2HZCSwrHH+Pu`jyD1NH;d&;yhHOVNNZ|H?=AqV4!Zl z;H*du1(IlfojiZb5%6<{VUEY?<`@Qm4IVUfUcW_1EPMe;7XH5^So2TpAubp|Qwx$% z&l)rk^(!V12W$B?}8h6?QN7R7w zgwMZk%F-9pH49!v&?%ZWGfF|*!~%8WHKzQ+^XO_em!dIb|yRE7LV{ zEF1cc&AghsuGl`EWMPKETz@s)X3)#Ki+j8vLoh2i>|aDNf^8ymbp4{WMZZWQZUle! zOXtOW-TtZkIn0cjROilj0zba)QxFY-10w&MaoEz=i7- zM3uRLzd@y*at9$S1E7zw2PB7-ctyaR$EO$LR~KE5%z5!*eeGbgeb>irb_cxJL>Aw$ zkcpKTNSUviPlDv2fEodD)~G<{zeR>}s@Q?Kv@(@h>sZL9c5#&5{CRj(jfa=OY0<_m zpDj|~XcIAZ*wA@EaQD~M!zN2~+>-t5SvsS2zsM-F+=i$Papd8amQ&?N+Tn#Rx~6>d zI1Rc{FVEu`tq$;+@BID))rMK%Pm{R>koS49rSg6)!Tmip987yYe+o7@Z`rXvgprH~ z2-3gxGY8)k?l|nhm;b@jT|ECt5YRtW5>Q zt{pyO-6F2WBWzA9Djj!RC2On49S^>@Vv}?3W}NUT>Xw=o88RIT46sH@ikp# z&_gF_a;;r0;~beveo9`15DAuLwcjRl?RaT0!tsH}MDXT}lN|`3M*^AbW=j^^xbf6+ zU1}Yz@X*4lz@il((X172atsHWM0u9UPknNJ!@#|MY|UaLCy4L0SaSK%5=;CEDS<}V&WFg zrD&d;t+#?NtGy2hGxt`$Oww1I5k{{9a_o8ghlBl^mb70BluOtSSr`i?xUBEOWtU6( zkXp8joi3&T<*VA@y2v*iHIl{M{kpO@TU=Y5hHt7gYxJ1oU*0)OT`mmcO@*zX(qFEz zRk-b0T@}+gcd2J+Bf7?~jQ0W)p*ko-H^&-tPyqX6rib1c# z^f)IiPReW6uhVvk%9*cYs)KSK*R9!7q4I?Tp}QI!3Wa%|++34-@21}r7y1O8E!c=> zj5=qS`3Q%PDIg7=e3C>_`tUV0F;*hi$<)nrklPh04QJWI7m02tWj~z_-?w1EmzV)O z)9JLT>*WVZp+Jw`I%OBo#;Hxwx6Ko|+I|dI7oBW`0s6fDl0LM+20Cb(dqSTePI_;-tfbSH5;`?3J@wlJm0o#FFaI=d(P#uuPwQ2EFUo^ zq&e>B*$H|+H-a)gPoi6rtX8|2gk>4bLN!SUP5js6RQ{kdU(u%U5%8@LrO|F(gPz#(#`eZ*U|1$ZK&=rm%Wme&vgJnVtV?s>yFRp)^;Ba{`)pBAw? z+K7%79q>ods|0nUi_l4+l&R?Xz(NJBe6exE5n9hX=+%FrP4a`P8CTQsd?=#gQ^uep z5+&&U)goU+1>cap7ED`pn6Qm4GPz;_X!|MOH2>W86=>IBWphdQZt^{p4$O;AkuqT+ z-O#<^L5ajhiT3A5bG%3n(Z(`@KgNxpWoAyhbD*C)NG5DM1zwzv9%gw69ydL9Ukf=k z;BAtFPSWD7C+|6zhksXYC_MT)x|AE~KH1@6nM3VKlrvc}PV&H%A{f@BqDQUGn>>%C zSQjowaGn$1KXgMG*dl9}(tj;uM{xKg0Vm=uiKVQCzY7Y_LU9=b;1OEh9vS0gr4=gF z#aa}8iM?V;yPM;69RBtL%0j099*OJLgI7AESNnVC!?c@#^ne^%3(UTcL8i}mN5+z? z8CCJcH02#7X9Yl{jmA2UMC}e5nr^CwcL28~l8R0yg$Rs;3AFSueZ14@EHuIuK~j!w zf`5_PZal$f^`{ZdbUG5D6#+2aT6=nDI6IWX|tG9*OtYFMoK;bILqmaj@H zBAggXW$Z^SAeJ8qNAqp5OiX-<0B{t+)fY0U&0jgb>@=!MBvCN{j|cABBGI|G4dp)m zHhuVR4X5fn5^>%jQiW)8WCN=1qu>3B8FH(A-fQaxJOj)hI~UtlSK z;xfr)bvg%-{Dq!0pXKtCDkec5wYDEd?XF!3g*dP|scG&Djr!&Z!);^j>9P}6n0hoQ z{;jlaqbuwmQSNLQpdvUw{rrpOLX9irkV{x<`_nWe_i;=yg?ztu;SI6T7JP#aVg_nD{U#E1%?d~Fbh4DPp^&%a$@zpHWvfY~rzR_{RuUoW z4)aZ6Vo1R)h7208tIYtpgv_6L!Jbm~iXSxi&^iYG@I;lcQ5*pWG)Q38GMS#lSjO*p zCY)F`x5LrBTO-cRETGdzcat;t)8YS(-&pY=hWX%k)zXY@f`RQh#~|jC3+iB9G;qb7 z4?K$68lQTxg>Wmx&rOrK)V03`fLE_bS1C|tKasb%SZnElA}?ctN0Jx%B>~DV2~y*` zi+T_2iTt;irNo%K$lw0sznDF;Cs3B@L1Yl3x)ZLbI#J-Dq2$NquwjRYyEakdM5`r+EB^Hx@M(_ER2iJEvR^b=)1MA**PNOcR{ zEK?JDt*(3^Rn0})8q`VzFiiZ{b}Nrx%hQ~JZv>Sz(kEJ&kRBk6CW~h2LWEa@eV&-B zACdyuZ`L>RX`!&=oLl@FGe-j-;39+`v4k~ zuzglzTUfs46=gHyiw8wZr}VAjtsfY*lz;o@&t3K92G1~y5!a2vg?RtW@B0SccMi^P zf@U?6;=XIW#ad>AmJM;Kh;j*T3p1`WYbbFUtS7=rxW*^cRDEe^U+W(fv|?8qwF3K+ z+@$um7i9&vGdNM1lCM59nQo%7w<_;A=hVzzv{}RHoj9H3Cd{z7ClgyruwzXr*$sR^@5w>v=?FfmSB)l%gSkxzECm*`Y z7#*SE%EgZ8pt*_7s|fjkRBc%ps9=I%S|?g8CEt?UjbGtRRk;&A{@W()bQ4a}9gX-Q z9~FSZqZ75$9~CC=j3H1BDz>pt?DqrlRI!*`4VJi?c(InRn5ru0`7l{+QfYYpbm|z$ z|3f)t8*C#c1;>Q%mxf*>_`dGtJgHdD<~Hd75zd?%5Y@g{au4!bQ@A~pq;s}ZV*?NP zW5&6Wa?8OIabFoTr|mmA`&Ru<1NIoLI)N4BW@WH2o(9gczPh$2rq2us5ScWO$ic5D z0#y7#K+|7#ujwv8-5E6sE^1FF=yNc`_!+kyQ~7o@a%f9}1Lc)GBk;IZvp}00l(DJ_ zcfbbC`I}a2izHAH&OmpxJHlLB5p&Us5@W(140`DP=Yc{}o!FeqDkT;Bb257Z7ovnx zZ)9OE54pSUIGWhcIA7{ZN@M1O@{MW$GtX)uM??+f3PQ0)f1W{0jxhnBH7zBS!AP2^ zXM8KrdG6| zKJw{p7c=sHM#i^(^tv1OR@GC)>YoN67W&$u?W>?#lAaTFr@7;89kN-PVz|LkH?tZ` z)_M=3)UM5*HAT<^yISI}kCwM*xC`Vgc{thQ<--_JtzJ@{i3?qxuq1?`Yg$iyOa8(K z&CYPNo7g@F?+6^wVunyoFbQRmfoNXBK^chio`?aH48u@Y4nPH-IT|Ua(ZEg7!HM3$od6-sPyTC-gkglH}pE# z;>2w(IhcIRRYl{HQqPKR(*$TEN+~2|RLV0{740THIbJw(5;{*1a8l`#vM@v=MJSJl zCPNz^(#?N*1v}3xk}e$wy1_qvM$ zP!%eSt`gQ2uqqkh{9EDi#0kqVq?$CBifIv9N5pRSC`OdpQ+HqZ^(@=ljs(rX7S7|f z%LUMqw7Fgn%Sy*m>um_+0&{}h@6w5bi1uEEwlhCISPOttT*F6E9sJVYHips}MDTbO zFOs=MHpcVV7VUi@!xlC|Z(v1MQ%vjTDxQjhF z1I=1yp;9$#={0`IUZq&O^N=K(8o6)_P9(K3@@e$rW%`Ug# z4hlePBq43Z=+7mqGVmubL6Au3{Y-mTe;<`Ua)5yv{ckn(`E06^R6Hg3#<<=S-|hD# z%3&TnuUd~|K)iK!lR>v2rF*zK9zXnXjPyh8AP55&d&63x8E)YGp23Wu-b9 ztM?F-c*09MGasqCEk-)~7y_kXMlLweRezcb_ne(@TD($)}!}1VjV{glcX5-wOo~pP9M!gr1P_t zO(Ayh*y1<02+MIivqj4MAhOC7{!K{U9P5nChu^S2d=jw$SIt^xrHKD!Qm*Cg{1_;@ z=os}2&^F$oq$YqrM zuU4r|O~3Z@gpF(59!r|aQ_QMCC`6Hh5flUaeEfwcVN=#XR1M^!5*WO!9t#S^zzT%$ zkl;~4a6nf+`9@5CT80#{c$e%FnqvqIaw<|@;V|x|Y)dH|vPc)8)R)mxyskBpr^oiR zpg{*)z}d~;@7u1>!^`g0DSA!$uy)3f&j-Lhn|IN*pzFS0=#LhZ)J!U%IBVlAX(O|Q zpWD{j%*EKbb1N#}swk(wYkN;X9ErdJXZ*L6rVPhUW!i(b7EveCa2X1~wJfuCamne3 z1s&J|1}sVf_kure6~Zu2*0yhO$mTIwSFSW{!7W%$4xBsudYJl<-WN0Ej>b?pL3V(b z$rnUsYWIa9^upyqeFJ^I`EYn;vR1f{A2TW>H3siA*&D^Zi*?2x|1(bX`Rmtl(P)Q( zbTj~F(=lgVu*Ag%H4QhIZ(5H+eje<*=@(e7G#j=UF|yKf8uUJb5}%k)jy%UuzT}|S zHc^l2cyxsi(uhPIi_YIx@tR`0r{X!TC`64E1Zk4TqL|;o zsebNOXm-L)h}7>JEVfYwD@6+RxKl|XJPdj+83nbOXf7}2aZ<8w{g@w0$z5P)Ua>#C z&Ch#hQEW@;C00%6?;XIa6WAQZ9+KgI-UibfxEt;83~ij{6Xk0jJ1q6wQ>(U2-jYA zB{@Db`MOu2Fuz2k?s-lH<&o_8ZlL?%S(fu(F)@M-($-kwu<#w}LoT98Ft{yG=E~LI z&WC~;zyA9u{vd&H^=h6GwGy72YX{UGY14qN`o17?eT12<9x5}YoE56ni8{Vl9Rp#F zB|D9KIixiX*oat47$X40UsOaK2fgpp5tq95@1~sCD7PGgni1XgpmUFG& z9T=`0codTwf7#z#WYW@wW(?mSmavSw6;+FhVInUFi$V7bf2UjYC6((Y>nC>kE~btz zLHvidub9St2bG#4eAwmF;_Emq30k_KVQk)^VIrPVk|GC43r4};xT0r%k+4c$9dAOF zl|^0Vkois^+3_w_jjJ`Nr|6&bOD}x}Z$qfDvLtQgV@>~ASWQQD zint%PGfUBP=wJEDyM{7Ed@T^$Vz$#*4d?T>@vWEpLl9r!1TCY@Nd9txlUfo=TxU}rNGes z+F9y~QJT1K5PZMcD(!%q>CHtSMH>2YP_jv{EfviXO0lO3YCH0p@1$Bd+BA2|v^@AA zk^c#2;Z7j={YiTRJS`m!&?nMj9uT7QX^)RA0G5eC&Q{6t?>o5hy5i_9D6$y&xlz3W z=J4S=i0FA9is&czUS8~&iENb*3qpvy9|Y4tHsi&6)I@6e&3eX+^x< z+^e+lh(D`sUwuOdsSRIV4A83D%BF`t(tOxI;`_F`#D&_PZAoDpi4vS%eZ8BH=`p$A z51Os@G-ATeb)_gu(ds72@^_E!zd`1sdDE`w@KKH%{GM_>&sld~% z)wz3isTSkl#RU>0a^kTL_Qjuqz{M6pWYd0SX2|f4>i1P5GdpBMpkL9pI2@kuYI>u} za9?|ZAn{5W#NR5*nIxLKDFI3xNTWOcZ30#r zL4w;2p0`nBt5C0fZ=WhODRBx=On0gUzL-uw;wj%4P0Zg2nKJY?>wlZE3+r42+j-!b zUADDmyY0{mjfF|=@N%ChxFCj&G)-}i6^03DiE+Y~90G=a zOh`_Hrod=C4&$60PaH$u8%Hl~{YR9oWL2}hRvq^b|Uyx&zlePO5vPI2VB8wUD?O}LP z+DlxSawP8{ahrxyps~fKkaR}euW)kuY+)G;17M$o^R@rLV~OOsA!zkYfAxg{JbJY#jMBnt48yMJaMhZh1x}y+U(< zXGyhunL%BPNeDVzq7jR&EuOd=1j~kGLrTj7k?#?}D#Jmj8fwOqL?oam?}2Q2WAQc8 zh#0hvjb1*&zDR~Nl~D0>nHVermNmwcC(VFos5A)bS&@ej0e9L{n|(wncroi0DIM26 z*vYlP+|^rO_&$M^+=iz+s(#Tk9^ex(Of_yglsWC1sf+>-7Wb=yVisrW@u~}Im z<@MTjaoWaT(d>64Q)0!H%Tu_+wwZ8NNt87~{wp&TN96P+^gNM7ApmBMl_LHr9>t!2 zrTkbQUx2%vEQJ%UcwE|U^bx+5MM(jB#Vi0vmc>V5_INw)bbKy`awr9T%+mlIM!ThK zpln$HfwuTyzUEsSgD!nE!*a=e9J9#ixxVq1WU6&Gy_I**6i#QKh2Em1iv(j54Y?aT z_H@eqB7qYuI$XE|2E`6S=Kyt9qBGaeaJy7i&mHK;$~X8HW={T^L4)G6UTZqtqAt;F z{a>&b3s@hd%|!_kdy&HdKz=zhjb9;5F?!B(|BuBA8N*lVO=*P<$K;TNaPltQqgEOt z!VwX++EO<4pSgysCal9v2@_^Cq7xxUwaICKg7DD!L<;7p4(#nxt0FJ@CGKCWZ>;eA zEzz3pzA!S<_wy@+(tUU;wklW^y<_~hG*zK8S*&8WjbArO>@iY41t?)h&Km+e);h%0 zO-E3gI{;$zPAFb)bE|`XXOwlaK)rNBD+xZCLU(P(SX{e6p~Zi*D6y;A46*d96b%9( zD)3?)F7_u-l@yoWkwaD*Zt#KL@Z3(EXBBj>mGNoYIKSG>@wNGc-kv-XO7(*N z_+-0=V&68yCWb3>`i}1r9XCzuJnUa}d|vuIRdB#G@gR z))rd*+FRN=pQ`1?M%3B3jml>xTuh5vz$vpos*%*Wn~?trNxTB|UX>GIhXdXdCz@){ z4w67}i4p$2^e%n+58JJ1FS@i~xpK^cL(`j{{Gcx`nJTua4B5BL+RnS>gfb!w=;}>E zI9}#)cn0Cq(=bJlhPn4(J_zuz!8XF3f#z8~y{)Q1{X zBJByfBJfk14wRZz3weBWKPF$vsVw8EonZ~U(Pouz`pp1}k01%rzKqLh+UF!y!-_Ok zA)20u8g)8W_g9?htAu^Fkc|l5%x^Jtb%z z4pa?i?iQZ!JbyCMprFaytt3umA>Euf5UUiP9L1{Gc8aiKAOrr*VG>c-5@G)%dd}ElUM{gcCsS{DT9|X*f1v%2^ z9TP}D1h&?Tx)CXrEL%1ygM(~Y6w_v@PdCfnO4a9zOypC~OLJcoB?a$fvojwal8ftR z=~NSa$e6%nw}FEb+&q+Xy8`3r+Om&XAn3aW%iZX2%J{toLKmk*v+Ka*1Uyl-8?qpY z_fRYo)aotQ9QXVEfQn`x_x zLHSD^x>R`7p<7o?t{^7K+^?pgAgxLGR$G9EC)8#!m6s!Tk{%ZJAGwV2qlNO!RPTDy z0EH8s-UHL^mT*Rx9Uh2i8$i?vTnuomMtm%TO&bE!&|-x_5)lE;uYV0-W?UY>*}{|< zX#L#D&Ur$PM`buop4N7N<-fT}t<0X?u5g?rGv!q^cz@7c#mY_N-N<^7(qRxdC>Q-bA5u+OMvjNR{{d)aZ%K+LIV1Fp@m(F zHt5bK=B(oZFJsgpz}I}1UPQspU&cI(qNqJ7Z;gupA2TV!wCegT5KdD998g8(J-!6w zK)OVH6kf2!GqSd6F3^6lEgGY>n&D|Q^ z2*Wx&#S&&y)@R8@pd`gQ)4=N6dVZcvfS$T}1ou5fC4z~WHv7ZOSgf}8HE@-4+V?|f zZT#0vn5M&(cQ`(`gxGN@2t|${%b%mZ?m8tqVZt1EZ@x)^|1!9(#sy)2iQB3QnBu-A z8AClU-^=hLqxCXs@;F43^3B}-qsfSCZ%!f_3i3~{K_Y-wOBpx$wVGAX^f3zr##ZAE z+}yq*CqlAX72k(KRJ~K!?}(tU=MoQ`LZsO(s0l>LfJH$QS+UyyvO_P;No;^Hii|vBFlv z?NB*36+8IziT1D6K@ji4qC-e@8S>R@AL?!LQ9`F*9YQvduQ*!Si{A;ygkO<~31v{9Vxk zvoK(j1?^u`ASIB-je(Ky{l0wWN^MN$?@0@>;C(aW#^%@021n&Dxyb$asUFjm9s*RJTn5)bxa9ewo z#E|7#%pytsv^T>4F{~1>rjbu~$<*)ol9}-H0G~F3ur_tj;6A)6I@{Nn%B`Qi(wXA` z;xF2~;CFp8X&o0A`>dwtPv~oEa%y>oT@z$qIfreMYkDSPTrgBpJixKJnKCWZSI0B! z+xHKc0S|`4g%!^Ut;iUGVL#F8NfC0M8?{VAqU@}u7!Q;)2N2#V3%q??T`8p6MB)7b zbhm4z*nqiJnkvu^|M&cyPE|d^V219I0BuRl>Jowf(Y=Vr*f`5X66a{|fj|-(w4-Cl zyQ~1|*{RI`2s?_wJoo*?73n;zBSpfc<++M2+L)0ut+T@X`50ZhOz1rM+ZbH>Oe+PM zpQ~r|tb^G=aT0ZKkT8Svq8|KsZ)s&AiT#h3FMZsNbXf4Q&C@VK7$&qO?3uof#*3lX z$e~CWs_gJYTv_vr1|(BxSh51`tdY^4d^xfVq9S9gWKG+|B)m*StaqphA{-VPk7Jet zYl<|11glkOA>o4vsP>6r&6vr0>$b;Y!qveTP48#-+Ybg=0H0#*y8w$Ra{|`A`D`gL z#1k%J&;rw3auP@FDvb$kiGaiDEn3l)qUR9Gvv!&JR|XoCXwZ3=>~7 zs%Vv20agS}$+3V?l~3fP&Ugm6dTeCb`LWkF4C`9^0TlJaqUYcsij*%5nLMVGfkI5W z0-mH#Hf6|51hm@Ow18yhr$FgI_41&2My@Oqf5h6aZ9#-W**G&hL=Hc5zyToCX6!%Elf`f+~+(Hcg)PVll1DN z;zcQs7n!$Tcz{HaF91j}pyowgY6d|Cc=FBxL%94mQWku+ z=Nl-3YeRi~@VJF)6YZ{eqP>4zyucN(;9~5lhnPg|2 zuQ0c_P^uhi^sP5Vlx!98mGAc(*T`tYJ;O9%&!YI+@L$3m~{ z!X;aPWGNWZb2zkUBM%y=6l2mR2%sS%-;7P42^dp4{%u-NS%l?P5B;}n!d0vh923>T z4grcV>W*5%2_U)q3#mzuinduDAsz?rrRt+&vJ~(1_vpF9EAv4LdNv9u9^m&C0N=_z z!j1!*OB@G=-5RchyW#I}3dgk)&Q_~U=LMquY|#ngfxMAg*us=zNga7+A*=cUV< z%i-9K@k4pgj4xf87&YI)cH1s4C9fn{xFn7SL1}!aw-njKXm767=jAP(|P_ZxR ztOdb78!Nfnuer<-?(W~zKtA=JF4UzS8y27tdfxUCn2d!#3< ztql^TzzOV6zVqc>r7d)<7&m{MQ;vNPw>sT+UUZm{ElP+q*4ofawS1H19u)N2Sa2b?l z{=otrim^PTG31dubGkn_q4ymYaOvTCbm6RP&$ge`gy;vb**S~-ywMZ6L=J_1kkYp- zh!bMFCQTh@kG;-M457utzQyC^s$^dSm@Ge|h7GZegITUXJk@_p+VgaQE29qqaj^Lj zs{%Y4hwbM+RCqv`L-A)K5~zd*FN(sknWajp>hfrH7yvtK)hO%!hJ}n3HAdhpPuh1p z^C<}dp5vm}G2ipAh}=6UInc}T|jHkh$A4v;+bC1p;)P<6E# zfUMuiB;{Kt222UVMC%QEEcVh;(YO*%gALP8r)cCbq+2d3W%83TU{e@bPM48z3U}t9 z0)m07UrZTnuI1EFj+dNt zxq;0hrwX$>G6l-5SiB{0Z597a191V)vD#ek$-h zf%^mgW8D8tZb25bK=Ou$(uSS~E{js^{2}5AAK|9q9(Bkqw3{QyE=zk2?I4!Ndhr7hHrvv_wQf-m2 zcO8aT)*HGvq1DnO#UjNoOsgxM<(-zc%VYu$x(;+ZlMDaTjbG>P+>HVDe(6|tat{?B zkFSC;*h;d3E3J%XjGf8NWp;$BQPei@=JQ7I5TTwm+F3oA6>}Rm1li1%DLI@L0p^Y51MeH)Rl4v;wUO zYM-`At7Is4K^1(D-r$jfYD)}K93Qe*j|d{E|%l8cXr%WMxiz71>YMxK5_qqrXjh>B6dP z;kF0{bR<%@;Dm2$wl<{CxYR`xnh3zA*ChCPk8cl*CwKWij^&!g)%py@SCuErZs@F1 zc=5v27>6+om9US-rBBis!m~7Fpc7Z^NQg2|mwE7M7*|CeU6bg)J;FuNSM;~&cMJBx zF&WmZ3h1I!Sx6Wx+hUTkXO}ia@)m=3&EbG?T+l~eL7?RyJT;)$4`;EsVyIPXh;CYjZ;&CIE;^?=q`G#`PI=Gg&ZtD(Vei` z;~?vZRUL7=9?&DhU1L>m)MYeiyAoTdXy}dTvFRg^t;L&=)xs~$L{ts?KQTX_y#|LG z7VHW$3|iVKWrQ}aQ%L*@1oo z6e!XXOK!FCPOi%M*W1)T#{sclR)PwAga6-xyf!Og3vwZyHLoZ90Lsnw62Uimu^WOc z%WY=lGmC^+;6%pu1@MD4Qd4;J2j5tuqG8H(k!Nn2vA@ob_aWQZ9G&WX67LZEPRWd^ zQ^bs7r>6&4I@eO@XC2=k1K*}={ZSWka4fNKoby!oD%?0ui;K44`|qXaXP=uV(e5y5cV$$HBX6@v3tEus8GNy-rF ziRX{Q)t^7#%rL}LsDj0l-kPbMs-!&4)?j^+O-3oBfX-x2Gp0?3^vf!801HZEjg2GA z3_Obap7>eFC-~zaUw1JeL7fd?kR%?0# zEW1ewqmX0Sf;&O7BgE{^^Ic`&=xFRC%+09C`(vP^tbyp#7UWPngQzGR1=omn1X!a` zA|W&zV&qj+{L;0Eg&R6Av{zk*Zv&i{+wjE8f%R+bX8zFzN|alKSbrNhK$CO4@4(rb z{cQ~XbYS!7+-gYm9V6-gi)0wSD z)QP~UR>rdO9X=2^@I8%dA()nOXcCxKi6xCzDsz`uU{UQF(k# zQu3{ZhucIJ>STs}yCYLFm78}0A;O76VQ~iO;KDiUmdg08PaQ1(GS!2-4W1W?uil_| z@Ayz@UgDvg(jH3X3Xv}-0Z>OAFu$Tcby{}0CYl>`5HF{w0xIqxiu3~T0VlNYB8fi( zB%}tsIX*9v5hjL;MvG8tnI#~>iSioxxhpZw=VGG9LS?SEs|AF(J};{B5-BV^Z8IjZ zi^g@TU|>4yV2yKX&|Xp$vCYw(X&tf*<;B9)sQNp3o2`T z>E1O}F~oDWAU%}w>;o{sN!Tr*qAHEcu2|R<9fc4o<;JLEP%a!&qmnRUxKclQXf+=U z0$_Krf|B#a%UAX)vD4+?2>3+6GwxY4>bt~dYwZu9z-jX@RV|Q#l~c}kxzQa!2eP-P zuhA`c)K2CZjs*n(qyYswmwflUmbKV*NMteevm-+j$UdpplzIR^U?QBP@Fy;G-%J|%hn zv8friL7W=!!=ccpbfhS*(vsT9lqAm=!H{&VDr#b1EU;c`Sbcf9Nn>D7v;#wgZkt}M zp?2G5U&w=K;%ouQ$%p_V%$bAe*?RM{{n54`Ug0>LBy7*FpEPToF+#dP28_t-xaU%v zpqTa?@yI+=hq-SKvm6>$7nUfC^HXmpJirfTp(9zl5+NOMpD{=U#nr&wu=L_|t)$hg z>1=CEymcp}+%-xy;sno0=n!KDc0a8p%sUq;H*G&!@-2b0aoyF##mZn?c@)f=~)J!)vRTjtAi+ zZMeV?JBv5&pJ5Bm7$W~C9e9Duc@3i$(+Bd7?}XO30(!5N-<`E6=!-qMy+yU=>lY3S zfCrDNI!_(s`GQ>s}5W#?Ug2;K_&p}pB^^((+8y=VOQWRr}Xun zSh~b%S(l}*9+-ATSVlPnd!zA5&bvlOd5}YKfBp_rZ+cy$!$7x=TPH?^F@a+kHA3aj zsD_h(hCRw}XIwp%DW|oJ#-hTk#tx(NVJx$JIW=<{=Ct)`SwWMT<7<$s4`6|Gi49Yf z5uQt029`;Z`SA=n>0okQc1LR~vpb*%Ef65R#x99~#2%gVl@0RDs~YZ^$2IdQ#{g^*Ic%Nw?LxR=o;yqZm%R*_Z^@Ai3wB*WQbnQ2f(h@ z?!xf+_gX<6d)JUFowmJs3s+`DdJv>+0Um01Z>t(T_y~KylGwsM4d&kYUi~e9j$c6d zJ{a`irYlht_o>7v$L=V%C;*|fgno+)S}a4TPP&>cHr$#24uTFyW{ujk!mQn!3{07> z`3I+gDb{l60J3U5LO>(^wx{WOpb!c!3>a{W%VhwLoEo# zXlquaY+`TsJ1niD-+4yLnorzw$p}E#`eXzro2)dPdLa$-Eaw=bT69&NL9%q~fg$$f zaqQ?LC8G_7Y9QJ_tB!Gcl{Rinq1fXANTxqXxGU0FK_{BnqB=S`Mqez?_i((AYl!Tr zX2l|Cn<7pvn_~#9>sUgCy1~X{9Eb0FDa$`)o__B9TP5PO}?k$jtf z!9;P0yx@>n${899p2HUN1OFx*{4lPUHJGabUFL2`()g$^aB9h1&EGI^_4O2w$dFmj3`63 z3u=&3W2g9_!!#4-n9I6NUaRsNB*JC1ddaw;Hlv1A3!n2RI0 zJnD~ql)?S(DrF2&`R`FQJT4^veIqvNn5A0Sm5PTZA6| zp@u(L=c=N8{8L??d&klNC2}i1i`WvAZtYmy!0}uqWnmC2qUVXH94Y^Ur*Us7{gI;5aBJOoR`_N(-T}P zTVC1Og=CLlYY~GkS<-I+6bfR_2v3MxmJCY;XWtmD&NR@_<=o_p01lU>4z=3u)wB5d z@sQI&0!AYfovPCVxpjzqIX>!$LYWgE7N79Kl)cnptVLHGgF4ZP&E!u~D_~E}THH33 z5g^K5dS%uWVsu;u3oqtO?+D_^o=eq>6<2GW(FV`7$iP3l1$@}dxb1hte_P~f=cB7! z^M#xOprwo8erEI&sOtko5OZac({$i|XjIEsRm+^a(UA`~o>u?2DF(m5!FD+kTXZIT z9m4h3dqks_E^0l0nC@qUmk*T9Tm{EB$f8~TN7Gf#!ti}je_d4q5Ly}1erm)eEtz_f z*aq=RW=lt;?b9zAR8^=)7WIBPG>}FV67RmB8J5LxO{Z+{0dAgsGGiCvPI=Q>Vtj4| zZu(D6%Su+WOcAgK=dd8~u;;M?;!&=ZCs&I}~N^HAP%7mRuWqQG` zRtRXAR6=FM-ZHx&L`^d2Q;{VFP;0)ac0o2P;^1VlYCkv~pSrxaQ(DLscf8lYJTI7y zwrC8;ApJ>zU14)u1^zK2n;-xh3`S~R3(|+avl>En4TOVLN~u@H!usgG{zyAevu^V^ zPwh45V98K0(J>>su~JRL68^k8Sr{#8fD~#OD#ny@&3FYXyTJKya$XVRZt133HVE7) zaH4^ghR3V?vv470`yos&%#SXr`LX_YGq=sw&hkf$D1O2W0)Rwd$|FCD= z-U@lQ$*7A6Lvjj#&4i&j<3ps!@3%q=BlN3Hezg9{R?UnX6m!65N|g#Y^1)eW!6paGL(ZHbn730C7Co)u zw97S(#4)-A7$rZG;m2zLzH@8N=)}?%ta5E5gl+bScARnJrmrkW>+%eni z4Kzdaxc(#~)1E>7a8C0Kcqo1%7;0z%TEaLx^7e=r^_pIXbA(u(uKPL`MK-rvzJ3rk z+*QXWL18{q(2F{R71kiFJPu?Kmu(-p2z_)Sw%iI3#h9O30iqmK_8RsQSE3j~*2Nos z{zxD$;JC=JN2=6+F1|K_Ac#nxrNqUM8g6%I?f>I^O><%y#8=Jh)hij zg#i9#I|&U$(mIe|@6lMosXbuWmi>GJ?L(`M3Yif;22nD3oc&J#K>u>bAHnO}0K(n@ zK;a@B#XefwlJ^ZzW58R=irD(wd8A2?Vt}Ic96SUJor<@*P^P{3Y{Ua-A{{o%??qh= zewhtd8Nl{1Fe?nukby35sYr1x4odO1*K4FCfzdN*(t9@ssP&w1!<|FcsL(%nkwF4O zY}Jjw=Wf!M1!cwn_iKWG0Db_10Gm(J-<=Z6dvE^Y&E&$3W+Z7Eg+X{Zgw2FO)43nF zsZ>}XXeRK|PpRuxiK6yvC~MhfTok<47#@vAR6oV++;~?mO|*nxreza+C5?94H`F`tZ@83QZ&&lB4Od7W5@2nPlBjMGV!{JOgS?kZP zhFNzqY&rYu?4HZC=+3&@E9d;?-E*E(Y*lP@{G+XrAV{V8p-;AnA^BK!VLv*>wHzv9gSUVD3o63d4O=V0C>q=dU z!Z3~GiMLwhFpR}QMyiMdKB$Mo;tcOtF$kb>HAMThv>tIJ@usQ*ztMD>jA&wAMjwfM zfnP2Fmv$$wmSr-_Hkynl&}ed=og;}6gsN&sXWab+{{A>M?@e(jLXfz)&~%d#gqu;8 zT{_=6nJ`A#&bDSGEeIYA?#vRcQkqC`2msM9zfk8;-m4b*LB!VxW=%tBHsPBm_>h|A z6B85|K||CSNlDb$WQmb+i8rVe+cl9Wf&f!602nM#SSk@qMKocK_Vl2|1rNo>#74ux zQGu}#Tn2;;U;q=K7?MFSfMpmC7^#p%W+MQVhJ1vz1Fju~_w1RIbu=(5S%)^*uJ0VF zQlw8l)?~RzwDjzFdP^F3I+j8b&LpTW+QN2pg+8`x76APx!hTccYIyqogm}vQ8f8ET zcR*smNg#aWHe85zQD&)D;e)Nl7QZKBIB0sQGvYcHli_0!qr;gW5z*u}V2*%XAB}?m zMi?>(xoBodxV7NuZpD#t1+%pw*<39JwF=900hpv`@QEJ$gIe-aJ0q)ai}1k3k@{`y z83xgMAt#Q*q;96$aWPCY8x38-I!gbHtnB!f4mndvA5wzGl%U6s8br_Ot~U_JK{w(L zAdQOufU?lwS5fp<1G_FxJ2_LBo*?}&u@-XE0 zVr{bs7zHp9v6n04aa@x$n+vM*mESp;{xx*|s>ygI!ST6wu6{%VmNuOXfnuh6Xr~u` z${WZLbQ1BD{4H|8r*gI`JS!DiFjqULOb2-qFi2+>A)|GR1{cVmPgMTmvt|p*|5gp+ z79pIYeiMdMLa69U;Jd8)kL25hBR7pGzW-n7*(@5}F@%+R>42*h+nYN6AI)+UfHk=k zjyNCC_O4c*rCQcimWBY`m&eJS_tQp8lFLqAj@kPIaOVYx*Ff}kuYwyUQK2ySLD-wG z)(cutvZY$cVtkCpgYa&c%TR24gS01Dy^#*HDlz<*aE&hY$F5*F6Ol5^83(aGq_ctp z-VIGVwJa-pSsKs#fJ?vBq^r}^q-V{Z10eD-^;f}Ar}R-AVcgVaS{@t7NRsoDu3Mw0ee2 zZp|B;TZ|#^wnVJWvy&jXm?p-6e}NNSnyM$nUc75wJcTWiYwAxZpqm_{tp(d4xShLgyVqhPm{IPCbS;l1US5iM&trbdbAHN$eoC42v#8(7`tiel zU8nub^>6QQV3)T_b`3fG}aVK1LvpXs)Aa)Ns5Tn zsZIVR-u98mH1xg>A+CC9Lc+G$umM`!5lT4Y@bIggsSlfDeh|u2WyD9~Xd(Tz5ji2! z8K>%j%usGv3Jh}wzMust;9br46=<5(`*5iy#}Z!dJY4t#K;EVyVcvh9Y-asc#)_I* z;2)?un0eBl+g5A*#T6*C^wr*=FCsxOC%VGdtl%`27#dZ>zhV*!xAPa@ILU)2O$&kc z;qQ?*dV7YKXTGbT_TVrmGi+X9++ymXG2LG@reBbU0ar*`;K(3%wv{0>IP&_Ophnm< zu1y8jXR%eyA{>`Y9gj=Dzb()3ruYVVL^#7(m60%3Dowzv(wJ+(LuF&7NA4++o_VUf z8uPAN_<)hRPqk}9UCbGbb|NpqU~CIRgT*hIC9sajp1 zG}s4Q6c+)X1q*j+wG{ViRdx1c*S9HO1B*mx)E^W`Cl#TFETZ$A+a>9=NPen%Akczr zTo?$d^-+H5U7zeB&%!>YYDV0OM^&H_gmwh~RXb&F<#C%fQZF@Q6NLQS_y^IXGl5w& z{iX;_Hywn^Ba*rq45E$EK6S57-5e7pt~gc6V>FD~@2`+!L%3F>hC*7hFYzV28j+pk zEIiC7=L0QT(z=oylb5=vNw%kZ6ypW45(R}nw3z5AC8VAnLN_$`T#lF=Ep3~~zf=`T zBd;e?lc<2HT7pcBhvX8OiP`@R-1;}6Ki|ppvt;oUH0qnIFXqooAJtn8mG3|4?qm>! zndK>xa=EAoSx~Hda&b=aYohZpdFm~NQc7$k`CUknW4DaE245yKb75iFMXURns!afs zgCy$=P%sCh*~rm9B(9;vBf<8(C^HPFG|zl748_V#)hXNG$Uo z$iu4eQ72x9J(3l{ludHMD49T{JX@S^)r&VN5I6hev$(v6*91+Gz5T>)wdu&pfQeZa#kBXCshsN&c2|w=YS(6e=an@k)_#Am1g6ci3{Xz&6C1tgN6CQc#+Y! zU_*5yMhEL+i*BK?Ba1^JAB47~xfEz1E?>4e1M>~Ccipf)ke^-nh$8)WgF-GdCYSem+_mI5E)z+6!vZ2elYB z-66m|7|e_^BAXgbmA`web6-(e#Os=fr=p9LFyEmuLN269w9Tc;7yja1Fk!)NO?P)0 zxxTS_7fK+yxMYg_aCZp`oA_}++DNj1p9`nF3n@`1>9~_~C_&DH!0vPk7IzMVN+3i< zBVX7!*fjD8bEA94ulQYVpP@eRrUzfTdW@z+pn00;B(08EJ{Tq5`tfBQG3VvJY=(wmT0X46_(ikA5 zvJ8t@)Drtvi^-!ndFNIO!-HzgJWRbcA-yq_HlF@GmvrDpS#C$bg5vv;>4-^iIHsF* z_H?^zWYdTycXYo|SDx)>Gt>El+7FU7bnJIFfd}S*CUoP@)^x~$ROERU3JA-?Aj^Z{ zkrO7h-zHzJT6UM%>rVzpJS=O?gTIzR0XSKaNgdZq>E01L(qrqOvmk^Ai%IoF1$w*I zz&M@EfSpzpS$MN9(SH3Z2)ogKlgQ6-5)eEBkdLqhO!%HQQH_Bx0R^~rO0)BTu$=F~ z6x(*8x0Ol74p(6fUcEBJM$-m(5s?%7rl}uz=DT(TUTI5D;_C%!E6M9_qA=Sn5X|cQ zun5FVeZbc#ae8);iz^`Ut3QNfxk34%W6mF3TGC|7ER38gL5T#tK`P=I{E6e^yglc> z@gtPpD^WINS{#|Q*fJJUbOAtuK*E7`3`PMctWioonX_x+5pUaZk=*eKtAX<18m31S zQ}^v44b-OT@FWCVNBtbb23NfumhnkL0_Mq=o)nL4L?AIOG)V-%5>VP{y<3sZ*FOPf z$P=7-Bn{7tS`B)a9@7MNil~@^tF6Tc?fo2CRpb23gT}@FHRK2q7or`lZzh^iCvgCh zHIR{a+-kx~kK*17v&hvas!AnWmab|_!k6*cd0on4^b5@P+xuR2o&&&q2nN|#By*4CFds88`k z9oiO^f}oOA!#{kG85@*$T~$t77bE_1dxLi$62Qtc%2Fz*`FNnO5DNR6EH*&aOfcV? z)Yqhxn#{LW^>Bku&NQ8fP6!y+nUWs)G#7-O-xK_Af&~kj!o4U z-E86vTC-zLbZ)!oGdkYbij&!TpdiH{F#NV9b6*|cvg%Spw!qj(VNN05)=hMpg`2TI zF)y?Ib1N@Q?We}j!ZyK!z6*%&f!5ocF6xu;$z$H7NX{*%ptM?cfG=TytYn~3rIS$o zhH_WLPJV?*pZt7M*gu)T0G9vV_**AO91|$=g>uNm1;`ID7o^c-R|Df3zb572!#rA* zpvD~3Dl7}7GjO1U+s_4Omh&Vut`jDfaS~7aVvtSREZZ?LKIRdw0`B=TP|+u)fAV(E*3Ne z!W}rg9hwgW*YBxNQ**`U#Bz+fVhYVdqRIjyQaUaOB~A?3nG8vDP0QngRKVKZr{M7C zs8^WYnn@MsZz-dMZDnfu(`7D0uf|&y(L}+dGnqUoCRRfatmMBPi7`-%RCAQE!BjEH zSSlrDpIt_*)Wl@55j4-le+zrzAPF z_e0ESL?jXRZw<3uni1&rPS*!DWHeP$fyqFHV}rJyA~u>1jq?t7j|NRY)kPZY|IHc= zrM@s=bpK~MXg~uaE5xs>29ObfX5;W=Y|pc$(9djhe;?Cc9_}PIVUvnC4awP-M$(|zfU z6HUhg@9lg4&Z%XdI_!SNJ*%7+n!zA~K3mVq`enMxugcD~nVSH*1hNflm*JhmEzvP% zljNP%l*EmbPz9v0_8H`%+{t;6sWEp};uV1Qh3Pdzg1}e*LOlWff?Ct|;xys(>jAfc z?<~>3LI5Q~g3Q@6zpy$ju|OS;Q>|!tHHd4WcP+JQjg-eDU?SqPOD7N}64ysWNXTgc z+Ex&MM(diHRgdAY&+wcN!C)G>waid+uU{4QGCS7y=YoPGd=k#{$qoViqSW7+P0z|SKygN zpGlfaw87wsXaa&3Gq8fwzNAv6MMxq_(cy)urh$7`I$p*6Ni18TT}un3YTmFW&d#ig z)y?tt3P#4H9u&O^%`yRUmAW{b7_c7$C~y98y zP#DliiNquY5S6{OW!g7wUmy=DuwUEgT9Z`Cp({Qy-A6Y9L%E5}`o_QY_&;ze4h*)u z8qx&Ys_q_WsS6r<`R59$9)I!dt2tS0UPg+w&(mk#-MJ0$38r=1SDpSV(;C_Bg>%31JZGPRCn{YoNPIW{-4<@AN7nVZ7LPKJeXC7^NukemFRI=pa(o5+lV zO>$$Bli``~^~A)b{}xoqCqt~|QQZaaqmg_I(fN%momWa!^bda^G_KyOXPVKyx0FeqYcKJ6!O85<>{>HYg!oz2A zW5Kdu*j%IMUbcl8c|@Ov2q$4#9~I#9a;6Vj7bv&S2l};q$2bI={Fx5!UTr7Jd;`JW zGDuonNn}h-qnZb7T!f9u^kP*&B9{6BIOj=%VPJInN*4s}oj9_|>lrlzxJ5$Ma00ePuAr>hS1Z#T z$h8#41r=dvHlU4y#R^Fnf+E<^VJxN_)&g;mrWM^j> zp1o7`=&2gFV{IbAc!pjR0;UhM7KiPg@^EX`6VoFx`Mmwg#hl&wdy3&3Ff$A@a?xkh z1fU}k;_Kl)vCG0nDnwFLStJjucCTXXPnOCJSxYC6AuE}@@wk9t9EmV7Fu_~|d!Z+C zrjF>0#xJym4gNK*KGe}?#N;{PFM z_HlehVI`CuZjoaAX3S*qPp}RNZw=TW^ztENq1BF`W5z@Gw^}!(wrv4ztSM_m(!>lV zjf@dd#Q@Z0oKz4giAP2hK4cPvHG7LGmA-A3^b>XzA>(Pds-&!HT{S<^Ss7$oYZsMD z?W>PRASjB$3s+}_XNj)lP1XdJqd7TLX)Ko*1Gnh;$j5y)ix8$6(MtC$c8OWh<+UOS zWMA=QNp-2oVQ!wi^fLCtXNiG+x|3~0s^ z9tVzh9t==SIKj35LO)9Ii7XEJaEipjBi%X!aO~8n7-BGRJ~+r-wzUuD!;>dfx((G} zpg{LUwBQ8P(PVESkBEMJZ`wK#b=w{WxIh}OU{Cr+&+j#ZT&>ehZxT#(mI57hJj1so X7v1u{G4VYn`^2wSTe literal 0 HcmV?d00001 diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui index 94da304..8cf1803 100755 --- a/src/codex-launcher-gui +++ b/src/codex-launcher-gui @@ -15,6 +15,7 @@ CONFIG_BAK = HOME / ".codex/config.toml.launcher-bak" CLEANUP = HOME / ".local/bin/cleanup-codex-stale.sh" PROXY = HOME / ".local/bin/translate-proxy.py" ENDPOINTS_FILE = HOME / ".codex/endpoints.json" +BGP_POOLS_FILE = HOME / ".codex/bgp-pools.json" LOG_DIR = HOME / ".cache/codex-desktop" LAUNCH_LOG = LOG_DIR / "launcher.log" PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy" @@ -24,6 +25,15 @@ model_catalog_json = "" """ CHANGELOG = [ + ("2.5.0", "2026-05-20", [ + "AI BGP β€” multi-provider routing with automatic failover", + "Create BGP pools with ordered routes from any configured endpoint", + "Each route uses its own endpoint URL, API key, and model", + "Failover strategy: tries primary, falls back on error/timeout", + "BGP pools appear in endpoint dropdown with shuffle icon", + "Up/down reordering for route priority in pool editor", + "Fixed TOML config breakage from multi-line paste in fields", + ]), ("2.4.0", "2026-05-20", [ "Added OpenAdapter provider preset (api.openadapter.in)", "One API key access to 40+ models β€” GLM, DeepSeek, Kimi, Qwen, Claude, GPT, Gemini", @@ -345,6 +355,18 @@ def save_endpoints(data): ENDPOINTS_FILE.parent.mkdir(parents=True, exist_ok=True) ENDPOINTS_FILE.write_text(json.dumps(data, indent=2)) +def load_bgp_pools(): + if BGP_POOLS_FILE.exists(): + try: + return json.loads(BGP_POOLS_FILE.read_text()) + except Exception: + pass + return {"pools": []} + +def save_bgp_pools(data): + BGP_POOLS_FILE.parent.mkdir(parents=True, exist_ok=True) + BGP_POOLS_FILE.write_text(json.dumps(data, indent=2)) + def get_endpoint(name): for e in load_endpoints()["endpoints"]: if e["name"] == name: @@ -513,13 +535,15 @@ def _start_proxy_for(endpoint, logfn): pcfg_path = PROXY_CONFIG_DIR / f"proxy-{safe_name(endpoint['name'])}.json" pcfg_path.parent.mkdir(parents=True, exist_ok=True) pcfg_path.write_text(json.dumps(pcfg, indent=2)) + _start_proxy_with_config(pcfg_path, logfn) +def _start_proxy_with_config(pcfg_path, logfn): + global _proxy_proc _proxy_proc = subprocess.Popen( ["python3", str(PROXY), "--config", str(pcfg_path)], stdout=subprocess.DEVNULL, preexec_fn=os.setsid, ) - for _ in range(30): try: urllib.request.urlopen("http://127.0.0.1:8080/v1/models", timeout=2) @@ -605,12 +629,15 @@ class LauncherWin(Gtk.Window): # header row hdr = Gtk.Box(spacing=8) vbox.pack_start(hdr, False, False, 0) - lbl = Gtk.Label(label="Codex Launcher v2.4.0") + lbl = Gtk.Label(label="Codex Launcher v2.5.0") lbl.set_use_markup(True) hdr.pack_start(lbl, False, False, 0) changelog_btn = Gtk.Button(label="Changelog") changelog_btn.connect("clicked", lambda b: self._show_changelog()) hdr.pack_end(changelog_btn, False, False, 0) + bgp_btn = Gtk.Button(label="AI BGP") + bgp_btn.connect("clicked", lambda b: self._open_bgp()) + hdr.pack_end(bgp_btn, False, False, 0) mgr_btn = Gtk.Button(label="Manage Endpoints") mgr_btn.connect("clicked", lambda b: self._open_mgr()) hdr.pack_end(mgr_btn, False, False, 0) @@ -844,7 +871,10 @@ class LauncherWin(Gtk.Window): names = [e["name"] for e in self._endpoints_data["endpoints"]] for n in names: self._combo.append_text(n) - if names: + bgp_names = [p["name"] for p in load_bgp_pools().get("pools", [])] + for n in bgp_names: + self._combo.append_text(f"πŸ”€ {n}") + if names or bgp_names: default = self._endpoints_data.get("default") if default and default in names: self._combo.set_active(names.index(default)) @@ -854,9 +884,26 @@ class LauncherWin(Gtk.Window): def _on_endpoint_changed(self): name = self._combo.get_active_text() - ep = get_endpoint(name) if name else None + is_bgp = name and name.startswith("πŸ”€ ") + bgp_name = name[2:] if is_bgp else None + ep = get_endpoint(name) if name and not is_bgp else None self._model_combo.remove_all() - if ep: + if is_bgp: + pool = None + for p in load_bgp_pools().get("pools", []): + if p["name"] == bgp_name: + pool = p + break + if pool: + seen = set() + for r in pool.get("routes", []): + m = r.get("model", "") + if m and m not in seen: + self._model_combo.append_text(m) + seen.add(m) + if seen: + self._model_combo.set_active(0) + elif ep: for m in ep.get("models", []): self._model_combo.append_text(m) GLib.idle_add(self._select_default_model, ep) @@ -880,6 +927,15 @@ class LauncherWin(Gtk.Window): d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Error: {e}") d.run(); d.destroy() + def _open_bgp(self): + try: + self._bgp_window = BGPPoolMgr(self) + self._bgp_window.connect("destroy", lambda *_: setattr(self, "_bgp_window", None)) + except Exception as e: + import traceback; traceback.print_exc() + d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Error: {e}") + d.run(); d.destroy() + def _backup_profile(self): chooser = Gtk.FileChooserDialog( title="Backup Codex Profile", @@ -1067,8 +1123,7 @@ class LauncherWin(Gtk.Window): def _launch(self, target): name = self._combo.get_active_text() - ep = get_endpoint(name) if name else None - if not ep: + if not name: self.log("ERROR: no endpoint selected") return model = self._model_combo.get_active_text() @@ -1076,6 +1131,26 @@ class LauncherWin(Gtk.Window): self.log("ERROR: no model selected") return + is_bgp = name.startswith("πŸ”€ ") + if is_bgp: + pool_name = name[2:] + pool = None + for p in load_bgp_pools().get("pools", []): + if p["name"] == pool_name: + pool = p + break + if not pool: + self.log(f"ERROR: BGP pool '{pool_name}' not found") + return + self._set_busy(True) + self.log(f"=== πŸ”€ BGP: {pool_name} / {model} β†’ {'Desktop' if target == 'desktop' else 'CLI'} ===") + threading.Thread(target=self._run_bgp, args=(pool, model, target), daemon=True).start() + return + + ep = get_endpoint(name) + if not ep: + self.log("ERROR: endpoint not found") + return self._set_busy(True) self.log(f"=== {ep['name']} / {model} β†’ {'Desktop' if target == 'desktop' else 'CLI'} ===") threading.Thread(target=self._run, args=(ep, model, target), daemon=True).start() @@ -1128,6 +1203,48 @@ class LauncherWin(Gtk.Window): self._set_busy(False) self.log("Ready.") + def _run_bgp(self, pool, model, target): + try: + self.log("Cleaning up stale processes…") + _run_cleanup() + + self.log(f"Starting BGP proxy with {len(pool.get('routes', []))} routes…") + bgp_ep = { + "name": pool["name"], + "backend_type": "openai-compat", + "base_url": "http://bgp.placeholder", + "api_key": "", + "default_model": model, + "models": list(dict.fromkeys(r.get("model", model) for r in pool.get("routes", []))), + } + pcfg = { + "port": 8080, + "backend_type": "openai-compat", + "target_url": "http://bgp.placeholder", + "api_key": "", + "bgp_routes": pool.get("routes", []), + "models": [{"id": m, "object": "model", "created": 1700000000, "owned_by": "bgp"} for m in bgp_ep["models"]], + } + pcfg_path = PROXY_CONFIG_DIR / f"proxy-{safe_name(pool['name'])}.json" + pcfg_path.parent.mkdir(parents=True, exist_ok=True) + pcfg_path.write_text(json.dumps(pcfg, indent=2)) + _start_proxy_with_config(pcfg_path, self.log) + + write_config_for_translated(bgp_ep, model) + + if target == "desktop": + self._launch_desktop(bgp_ep, model) + else: + self._launch_cli(bgp_ep, model) + + except Exception as e: + self.log(f"ERROR: {e}") + finally: + _stop_proxy() + restore_config() + self._set_busy(False) + self.log("Ready.") + def _run_codex_default(self, target): try: self.log("Cleaning up stale processes…") @@ -1967,6 +2084,381 @@ class EditEndpointDialog(Gtk.Dialog): # Entry point # ═══════════════════════════════════════════════════════════════════ +# ═══════════════════════════════════════════════════════════════════ +# BGP Pool Manager +# ═══════════════════════════════════════════════════════════════════ + +class BGPPoolMgr(Gtk.Window): + def __init__(self, parent): + super().__init__(title="AI BGP β€” Pool Manager") + self.set_transient_for(parent) + self.set_default_size(620, 440) + self._parent = parent + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + vbox.set_margin_start(12) + vbox.set_margin_end(12) + vbox.set_margin_top(12) + vbox.set_margin_bottom(12) + self.add(vbox) + + hdr = Gtk.Box(spacing=8) + vbox.pack_start(hdr, False, False, 0) + hdr.pack_start(Gtk.Label(label="AI BGP Pools β€” multi-provider routing with automatic failover", use_markup=True), False, False, 0) + + self._store = Gtk.ListStore(str, str, str) + self._tree = Gtk.TreeView(model=self._store) + for i, (title, w) in enumerate([("Pool Name", 200), ("Routes", 250), ("Strategy", 100)]): + r = Gtk.CellRendererText() + c = Gtk.TreeViewColumn(title, r, text=i) + c.set_min_width(w) + self._tree.append_column(c) + self._tree.set_headers_visible(True) + sw = Gtk.ScrolledWindow() + sw.add(self._tree) + vbox.pack_start(sw, True, True, 0) + + sel = self._tree.get_selection() + sel.connect("changed", lambda *_: self._on_select()) + + bbox = Gtk.Box(spacing=8) + vbox.pack_start(bbox, False, False, 0) + self._add_btn = Gtk.Button(label="Create Pool") + self._add_btn.connect("clicked", lambda b: self._add_pool()) + bbox.pack_start(self._add_btn, True, True, 0) + self._edit_btn = Gtk.Button(label="Edit Pool") + self._edit_btn.connect("clicked", lambda b: self._edit_pool()) + self._edit_btn.set_sensitive(False) + bbox.pack_start(self._edit_btn, True, True, 0) + self._del_btn = Gtk.Button(label="Delete Pool") + self._del_btn.connect("clicked", lambda b: self._del_pool()) + self._del_btn.set_sensitive(False) + bbox.pack_start(self._del_btn, True, True, 0) + close_btn = Gtk.Button(label="Close") + close_btn.connect("clicked", lambda b: self.destroy()) + bbox.pack_start(close_btn, True, True, 0) + + self._rebuild() + self.show_all() + + def _rebuild(self): + self._store.clear() + for pool in load_bgp_pools().get("pools", []): + routes_str = " β†’ ".join(f'{r.get("name","?")}/{r.get("model","?")}' for r in pool.get("routes", [])) + self._store.append([pool["name"], routes_str, pool.get("strategy", "failover")]) + + def _selected_name(self): + sel = self._tree.get_selection() + m, i = sel.get_selected() + return self._store[i][0] if i else None + + def _on_select(self): + name = self._selected_name() + self._edit_btn.set_sensitive(bool(name)) + self._del_btn.set_sensitive(bool(name)) + + def _add_pool(self): + d = BGPPoolEditDialog(self, None) + d.connect("response", lambda *_: self._rebuild()) + + def _edit_pool(self): + name = self._selected_name() + if name: + d = BGPPoolEditDialog(self, name) + d.connect("response", lambda *_: self._rebuild()) + + def _del_pool(self): + name = self._selected_name() + if not name: + return + d = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, + f'Delete BGP pool "{name}"?') + r = d.run(); d.destroy() + if r != Gtk.ResponseType.YES: + return + data = load_bgp_pools() + data["pools"] = [p for p in data["pools"] if p["name"] != name] + save_bgp_pools(data) + self._rebuild() + self._parent._on_endpoints_updated() + + +class BGPPoolEditDialog(Gtk.Dialog): + def __init__(self, parent, existing_name): + title = "Edit BGP Pool" if existing_name else "Create BGP Pool" + Gtk.Dialog.__init__(self, title=title, parent=parent, modal=True) + self.add_button("Cancel", Gtk.ResponseType.CANCEL) + self.add_button("Save", Gtk.ResponseType.OK) + self.set_default_size(580, 480) + + self._existing_name = existing_name + self._parent_mgr = parent + + data = load_bgp_pools() + pool = None + if existing_name: + for p in data.get("pools", []): + if p["name"] == existing_name: + pool = p + break + if not pool: + pool = {"name": "", "strategy": "failover", "routes": []} + + area = self.get_content_area() + area.set_margin_start(12) + area.set_margin_end(12) + area.set_margin_top(12) + area.set_margin_bottom(12) + area.set_spacing(8) + + grid = Gtk.Grid(column_spacing=8, row_spacing=6) + area.pack_start(grid, False, False, 0) + + grid.attach(Gtk.Label(label="Pool Name:", xalign=1), 0, 0, 1, 1) + self._entry_name = Gtk.Entry(text=pool["name"]) + grid.attach(self._entry_name, 1, 0, 1, 1) + + grid.attach(Gtk.Label(label="Strategy:", xalign=1), 0, 1, 1, 1) + self._combo_strategy = Gtk.ComboBoxText() + self._combo_strategy.append("failover", "Failover (try primary, fall back on error)") + self._combo_strategy.append("race", "Race (send to all, return fastest)") + self._combo_strategy.set_active_id(pool.get("strategy", "failover")) + grid.attach(self._combo_strategy, 1, 1, 1, 1) + + area.pack_start(Gtk.Label(label="Routes (drag to reorder priority)", use_markup=True, xalign=0), False, False, 8) + + self._route_store = Gtk.ListStore(str, str, str, str, str, str) + for r in pool.get("routes", []): + self._route_store.append([ + r.get("name", ""), r.get("endpoint_name", ""), + r.get("target_url", ""), r.get("api_key", ""), + r.get("model", ""), str(r.get("priority", 99)) + ]) + + self._route_tree = Gtk.TreeView(model=self._route_store) + for i, (title, w) in enumerate([ + ("Route Name", 120), ("Endpoint", 120), ("URL", 150), + ("API Key", 80), ("Model", 120), ("Priority", 60) + ]): + renderer = Gtk.CellRendererText() + renderer.set_property("editable", False) + col = Gtk.TreeViewColumn(title, renderer, text=i) + col.set_min_width(w) + col.set_resizable(True) + self._route_tree.append_column(col) + self._route_tree.set_headers_visible(True) + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + sw.add(self._route_tree) + sw.set_min_content_height(200) + area.pack_start(sw, True, True, 0) + + bbox = Gtk.Box(spacing=6) + area.pack_start(bbox, False, False, 0) + add_r = Gtk.Button(label="Add Route") + add_r.connect("clicked", lambda b: self._add_route()) + bbox.pack_start(add_r, True, True, 0) + edit_r = Gtk.Button(label="Edit Route") + edit_r.connect("clicked", lambda b: self._edit_route()) + bbox.pack_start(edit_r, True, True, 0) + rm_r = Gtk.Button(label="Remove Route") + rm_r.connect("clicked", lambda b: self._remove_route()) + bbox.pack_start(rm_r, True, True, 0) + up_r = Gtk.Button(label="↑ Up") + up_r.connect("clicked", lambda b: self._move_route(-1)) + bbox.pack_start(up_r, True, True, 0) + down_r = Gtk.Button(label="↓ Down") + down_r.connect("clicked", lambda b: self._move_route(1)) + bbox.pack_start(down_r, True, True, 0) + + self.show_all() + + if self.run() == Gtk.ResponseType.OK: + self._save() + + self.destroy() + + def _save(self): + name = self._entry_name.get_text().strip() + if not name: + return + strategy = self._combo_strategy.get_active_id() or "failover" + routes = [] + for i, row in enumerate(self._route_store): + if not row[2]: + continue + routes.append({ + "name": row[0] or f"Route {i+1}", + "endpoint_name": row[1], + "target_url": row[2], + "api_key": row[3], + "model": row[4], + "priority": i + 1, + "reasoning_enabled": True, + "reasoning_effort": "medium", + }) + data = load_bgp_pools() + if self._existing_name: + data["pools"] = [p for p in data["pools"] if p["name"] != self._existing_name] + data["pools"].append({"name": name, "strategy": strategy, "routes": routes}) + save_bgp_pools(data) + self._parent_mgr._parent._on_endpoints_updated() + + def _add_route(self): + endpoints = load_endpoints().get("endpoints", []) + if not endpoints: + d = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, + "No endpoints configured. Add endpoints in Manage Endpoints first.") + d.run(); d.destroy() + return + d = BGPRouteDialog(self, endpoints, None) + if d.result: + r = d.result + self._route_store.append([ + r.get("name", ""), r.get("endpoint_name", ""), + r.get("target_url", ""), r.get("api_key", ""), + r.get("model", ""), str(r.get("priority", 99)) + ]) + + def _edit_route(self): + sel = self._route_tree.get_selection() + m, i = sel.get_selected() + if not i: + return + endpoints = load_endpoints().get("endpoints", []) + existing = { + "name": m[i][0], "endpoint_name": m[i][1], + "target_url": m[i][2], "api_key": m[i][3], + "model": m[i][4], "priority": int(m[i][5]) if m[i][5] else 99, + } + d = BGPRouteDialog(self, endpoints, existing) + if d.result: + r = d.result + m[i][0] = r.get("name", "") + m[i][1] = r.get("endpoint_name", "") + m[i][2] = r.get("target_url", "") + m[i][3] = r.get("api_key", "") + m[i][4] = r.get("model", "") + m[i][5] = str(r.get("priority", 99)) + + def _remove_route(self): + sel = self._route_tree.get_selection() + m, i = sel.get_selected() + if i: + self._route_store.remove(i) + + def _move_route(self, direction): + sel = self._route_tree.get_selection() + m, i = sel.get_selected() + if not i: + return + path = m.get_path(i) + idx = path.get_indices()[0] + new_idx = idx + direction + if new_idx < 0 or new_idx >= len(self._route_store): + return + row_data = [m[idx][c] for c in range(6)] + self._route_store.remove(m.get_iter(Gtk.TreePath(idx))) + new_iter = self._route_store.insert(new_idx) + for c, v in enumerate(row_data): + self._route_store.set_value(new_iter, c, v) + + +class BGPRouteDialog(Gtk.Dialog): + def __init__(self, parent, endpoints, existing): + Gtk.Dialog.__init__(self, title="BGP Route", parent=parent, modal=True) + self.add_button("Cancel", Gtk.ResponseType.CANCEL) + self.add_button("OK", Gtk.ResponseType.OK) + self.set_default_size(440, 300) + self.result = None + + area = self.get_content_area() + area.set_margin_start(12) + area.set_margin_end(12) + area.set_margin_top(12) + area.set_margin_bottom(12) + area.set_spacing(6) + + grid = Gtk.Grid(column_spacing=8, row_spacing=6) + area.pack_start(grid, False, False, 0) + + def add_row(row, label, widget): + grid.attach(Gtk.Label(label=label, xalign=1), 0, row, 1, 1) + grid.attach(widget, 1, row, 1, 1) + + self._entry_name = Gtk.Entry(text=existing.get("name", "") if existing else "") + add_row(0, "Route Name:", self._entry_name) + + self._combo_ep = Gtk.ComboBoxText() + ep_names = [e["name"] for e in endpoints] + for en in ep_names: + self._combo_ep.append(en, en) + if existing and existing.get("endpoint_name") in ep_names: + self._combo_ep.set_active_id(existing["endpoint_name"]) + elif ep_names: + self._combo_ep.set_active(0) + self._combo_ep.connect("changed", lambda b: self._on_ep_changed(endpoints)) + add_row(1, "Endpoint:", self._combo_ep) + + self._entry_url = Gtk.Entry() + add_row(2, "URL:", self._entry_url) + + self._entry_key = Gtk.Entry() + self._entry_key.set_visibility(False) + add_row(3, "API Key:", self._entry_key) + + self._combo_model = Gtk.ComboBoxText() + add_row(4, "Model:", self._combo_model) + + if existing: + self._entry_url.set_text(existing.get("target_url", "")) + self._entry_key.set_text(existing.get("api_key", "")) + self._on_ep_changed(endpoints) + if existing and existing.get("model"): + self._combo_model.set_active_id(existing["model"]) + + self.show_all() + if self.run() == Gtk.ResponseType.OK: + ep_name = self._combo_ep.get_active_text() or "" + ep = None + for e in endpoints: + if e["name"] == ep_name: + ep = e + break + self.result = { + "name": self._entry_name.get_text().strip() or ep_name, + "endpoint_name": ep_name, + "target_url": self._entry_url.get_text().strip(), + "api_key": self._entry_key.get_text().strip(), + "model": self._combo_model.get_active_text() or "", + "priority": 99, + } + if ep: + self.result["reasoning_enabled"] = ep.get("reasoning_enabled", True) + self.result["reasoning_effort"] = ep.get("reasoning_effort", "medium") + self.result["oauth_provider"] = ep.get("oauth_provider", "") + self.destroy() + + def _on_ep_changed(self, endpoints): + ep_name = self._combo_ep.get_active_text() + ep = None + for e in endpoints: + if e["name"] == ep_name: + ep = e + break + if ep: + self._entry_url.set_text(normalize_base_url(ep.get("base_url", ""))) + self._entry_key.set_text(ep.get("api_key", "")) + self._combo_model.remove_all() + for m in ep.get("models", []): + mid = normalize_model_id(m) if m else "" + self._combo_model.append(mid, m) + if ep.get("default_model"): + self._combo_model.set_active_id(normalize_model_id(ep["default_model"])) + elif len(ep.get("models", [])) > 0: + self._combo_model.set_active(0) + + def main(): for d in [LOG_DIR, PROXY_CONFIG_DIR]: d.mkdir(parents=True, exist_ok=True) diff --git a/src/translate-proxy.py b/src/translate-proxy.py index b5cbe43..6d3997b 100755 --- a/src/translate-proxy.py +++ b/src/translate-proxy.py @@ -84,23 +84,35 @@ MODELS = CONFIG["models"] CC_VERSION = CONFIG.get("cc_version", "") REASONING_ENABLED = CONFIG.get("reasoning_enabled", True) REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium") +BGP_ROUTES = CONFIG.get("bgp_routes", []) +BGP_MODELS = [] +for _r in BGP_ROUTES: + for _m in _r.get("models", [{"id": _r.get("model", "unknown")}]): + if _m.get("id", _m) not in BGP_MODELS: + BGP_MODELS.append(_m.get("id", _m) if isinstance(_m, dict) else _m) +if BGP_ROUTES and not MODELS: + MODELS = [{"id": m, "object": "model", "created": 1700000000, "owned_by": "bgp"} for m in BGP_MODELS] + CONFIG["models"] = MODELS def _refresh_oauth_token(): - if OAUTH_PROVIDER != "google": - return API_KEY + return _refresh_oauth_token_for(API_KEY, OAUTH_PROVIDER) + +def _refresh_oauth_token_for(api_key, oauth_provider): + if oauth_provider != "google": + return api_key token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", "google-oauth-token.json") if not os.path.exists(token_path): - return API_KEY + return api_key try: with open(token_path) as f: tokens = json.load(f) if tokens.get("expires_at", 0) > time.time() + 60: - return tokens.get("access_token", API_KEY) + return tokens.get("access_token", api_key) client_id = tokens.get("client_id", "") client_secret = tokens.get("client_secret", "") refresh_token = tokens.get("refresh_token", "") if not all([client_id, client_secret, refresh_token]): - return tokens.get("access_token", API_KEY) + return tokens.get("access_token", api_key) print("[oauth] refreshing Google access token...", file=sys.stderr) data = urllib.parse.urlencode({ "client_id": client_id, "client_secret": client_secret, @@ -1006,7 +1018,6 @@ class Handler(http.server.BaseHTTPRequestHandler): def _handle_openai_compat(self, body, model, stream): input_data = body.get("input", "") - # Adaptive: proactively compact if above learned Crof limit crof_limit = _crof_item_limit(model) if isinstance(input_data, list) and len(input_data) > crof_limit: print(f"[crof-adaptive] proactive compact: {len(input_data)} items > limit {crof_limit}", file=sys.stderr) @@ -1018,6 +1029,29 @@ class Handler(http.server.BaseHTTPRequestHandler): instructions = body.get("instructions", "").strip() if instructions: messages.insert(0, {"role": "system", "content": instructions}) + + if BGP_ROUTES: + self._handle_bgp(body, model, stream, messages, input_data) + else: + chat_body = self._build_chat_body(model, messages, body, stream) + target = upstream_target(TARGET_URL, "/chat/completions") + effective_key = _refresh_oauth_token() + fwd = forwarded_headers(self.headers, { + "Content-Type": "application/json", + "Authorization": f"Bearer {effective_key}", + }, browser_ua=True) + print(f"[translate-proxy] POST {target} model={model} stream={stream} items={len(input_data) if isinstance(input_data,list) else 1}", file=sys.stderr) + req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd) + try: + upstream = urllib.request.urlopen(req, timeout=180) + except urllib.error.HTTPError as e: + err = e.read().decode() + return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}}) + except Exception as e: + return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}}) + self._forward_oa_compat(upstream, stream, model, chat_body, body, input_data, fwd, target) + + def _build_chat_body(self, model, messages, body, stream): chat_body = {"model": model, "messages": messages} for k in ("temperature", "top_p"): if k in body: @@ -1034,31 +1068,63 @@ class Handler(http.server.BaseHTTPRequestHandler): chat_body["reasoning_effort"] = "none" else: chat_body["reasoning_effort"] = REASONING_EFFORT + return chat_body - target = upstream_target(TARGET_URL, "/chat/completions") - effective_key = _refresh_oauth_token() - fwd = forwarded_headers(self.headers, { - "Content-Type": "application/json", - "Authorization": f"Bearer {effective_key}", - }, browser_ua=True) - print(f"[translate-proxy] POST {target} model={model} stream={stream} items={len(input_data) if isinstance(input_data,list) else 1} ua={fwd.get('User-Agent','')[:50]}", file=sys.stderr) + def _handle_bgp(self, body, model, stream, messages, input_data): + routes = sorted(BGP_ROUTES, key=lambda r: r.get("priority", 99)) + errors = [] + for route in routes: + r_model = route.get("model", model) + r_url = route["target_url"].rstrip("/") + r_key = route.get("api_key", "") + r_reasoning = route.get("reasoning_enabled", True) + r_effort = route.get("reasoning_effort", "medium") + r_oauth = route.get("oauth_provider", "") - req = urllib.request.Request( - target, - data=json.dumps(chat_body).encode(), - headers=fwd, - ) - self._forward_oa_compat(req, stream, model, chat_body, body, input_data, fwd, target, tools) + chat_body = dict(messages=list(messages)) + chat_body["model"] = r_model + for k in ("temperature", "top_p"): + if k in body: + chat_body[k] = body[k] + chat_body["max_tokens"] = max(body.get("max_output_tokens", 0), 64000) + tools = oa_convert_tools(body.get("tools")) + if tools: + chat_body["tools"] = tools + if body.get("tool_choice"): + chat_body["tool_choice"] = body["tool_choice"] + chat_body["stream"] = stream + if not r_reasoning or r_effort == "none": + chat_body["enable_thinking"] = False + chat_body["reasoning_effort"] = "none" + else: + chat_body["reasoning_effort"] = r_effort - def _forward_oa_compat(self, req, stream, model, chat_body, body, input_data, fwd, target, tools): - try: - upstream = urllib.request.urlopen(req, timeout=180) - except urllib.error.HTTPError as e: - err = e.read().decode() - return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}}) - except Exception as e: - return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}}) + target = upstream_target(r_url, "/chat/completions") + if r_oauth == "google": + r_key = _refresh_oauth_token_for(r_key, r_oauth) + fwd = forwarded_headers(self.headers, { + "Content-Type": "application/json", + "Authorization": f"Bearer {r_key}", + }, browser_ua=True) + print(f"[bgp] trying route '{route.get('name', r_url)}' model={r_model}", file=sys.stderr) + req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd) + try: + upstream = urllib.request.urlopen(req, timeout=180) + print(f"[bgp] route '{route.get('name', r_url)}' connected OK", file=sys.stderr) + self._forward_oa_compat(upstream, stream, r_model, chat_body, body, input_data, fwd, target) + return + except urllib.error.HTTPError as e: + err = e.read().decode() + print(f"[bgp] route '{route.get('name', r_url)}' FAILED: HTTP {e.code}: {err[:200]}", file=sys.stderr) + errors.append(f"{route.get('name','?')}: HTTP {e.code}") + except Exception as e: + print(f"[bgp] route '{route.get('name', r_url)}' FAILED: {e}", file=sys.stderr) + errors.append(f"{route.get('name','?')}: {e}") + print(f"[bgp] ALL ROUTES FAILED: {errors}", file=sys.stderr) + self.send_json(502, {"error": {"type": "bgp_all_routes_failed", "message": f"All BGP routes failed: {'; '.join(errors)}"}}) + + def _forward_oa_compat(self, upstream, stream, model, chat_body, body, input_data, fwd, target): n_items = len(input_data) if isinstance(input_data, list) else 1 if stream: