From 06112c136c1f10b2a007911335036b99cdd0d589 Mon Sep 17 00:00:00 2001 From: Nikan Wystaf Date: Sat, 26 Sep 2026 16:04:40 +0700 Subject: [PATCH] feat(providers): add four OpenAI-compatible aggregator providers (dahl, atria, agnes, bai) Adds Dahl Inference, Atria Dawn, Agnes AI, and B.AI following the existing registry pattern. Each uses DefaultExecutor with no custom translator needed. --- open-sse/providers/registry/agnes.js | 29 ++++ open-sse/providers/registry/atria.js | 32 ++++ open-sse/providers/registry/bai.js | 30 ++++ open-sse/providers/registry/dahl.js | 35 +++++ open-sse/providers/registry/index.js | 8 + public/providers/agnes.png | Bin 0 -> 5257 bytes public/providers/atria.png | Bin 0 -> 2449 bytes public/providers/bai.png | Bin 0 -> 6093 bytes public/providers/dahl.png | Bin 0 -> 3020 bytes src/app/api/providers/[id]/models/route.js | 7 + src/app/api/providers/[id]/test/testUtils.js | 10 ++ tests/__baseline__/alias-baseline.json | 6 + tests/__baseline__/providers-baseline.json | 20 +++ tests/unit/aggregator-providers-batch.test.js | 137 ++++++++++++++++++ 14 files changed, 314 insertions(+) create mode 100644 open-sse/providers/registry/agnes.js create mode 100644 open-sse/providers/registry/atria.js create mode 100644 open-sse/providers/registry/bai.js create mode 100644 open-sse/providers/registry/dahl.js create mode 100644 public/providers/agnes.png create mode 100644 public/providers/atria.png create mode 100644 public/providers/bai.png create mode 100644 public/providers/dahl.png create mode 100644 tests/unit/aggregator-providers-batch.test.js diff --git a/open-sse/providers/registry/agnes.js b/open-sse/providers/registry/agnes.js new file mode 100644 index 00000000..4cf2a88e --- /dev/null +++ b/open-sse/providers/registry/agnes.js @@ -0,0 +1,29 @@ +export default { + id: "agnes", + priority: 120, + alias: "agnes", + aliases: [ + "agnes-ai", + ], + uiAlias: "agnes", + display: { + name: "Agnes AI", + icon: "auto_awesome", + color: "#7C3AED", + textIcon: "AG", + website: "https://agnes-ai.com", + notice: { + text: "OpenAI-compatible gateway from Agnes AI, offering free API credits on sign-up. Accepts a bearer token or an x-api-key header.", + apiKeyUrl: "https://platform.agnes-ai.com", + }, + }, + category: "freeTier", + authType: "apikey", + transport: { + baseUrl: "https://apihub.agnes-ai.com/v1/chat/completions", + validateUrl: "https://apihub.agnes-ai.com/v1/models", + }, + // No model ids could be verified without a key, so discovery is left to the + // live endpoint and any id is accepted through passthroughModels. + passthroughModels: true, +}; diff --git a/open-sse/providers/registry/atria.js b/open-sse/providers/registry/atria.js new file mode 100644 index 00000000..ed9de421 --- /dev/null +++ b/open-sse/providers/registry/atria.js @@ -0,0 +1,32 @@ +export default { + id: "atria", + priority: 120, + alias: "atria", + aliases: [ + "atria-asi", + ], + uiAlias: "atria", + display: { + name: "Atria Dawn", + icon: "flare", + color: "#C2410C", + textIcon: "AD", + website: "https://atria-asi.ai", + notice: { + text: "OpenAI-compatible endpoint from Atria Dawn (AtomInnoLab). Currently a research preview offering a single text model, Atria-Dawn-Preview.", + apiKeyUrl: "https://api.atria-asi.ai/dashboard", + }, + }, + category: "apikey", + authType: "apikey", + transport: { + baseUrl: "https://api.atria-asi.ai/v1/chat/completions", + validateUrl: "https://api.atria-asi.ai/v1/models", + }, + // Docs pin the model field to one case-sensitive id. Text-only for now: the + // service ships a hook that blocks image/PDF input, so no vision is claimed. + models: [ + { id: "Atria-Dawn-Preview", name: "Atria Dawn Preview" }, + ], + passthroughModels: true, +}; diff --git a/open-sse/providers/registry/bai.js b/open-sse/providers/registry/bai.js new file mode 100644 index 00000000..84eb2797 --- /dev/null +++ b/open-sse/providers/registry/bai.js @@ -0,0 +1,30 @@ +export default { + id: "bai", + priority: 120, + alias: "bai", + aliases: [ + "b-ai", + ], + uiAlias: "bai", + display: { + name: "B.AI", + icon: "account_balance", + color: "#0369A1", + textIcon: "BA", + website: "https://b.ai", + notice: { + text: "OpenAI-compatible gateway with one of the larger catalogues here. Accepts a bearer token or an x-api-key header. Model ids are fetched live from the provider.", + apiKeyUrl: "https://b.ai", + }, + }, + category: "apikey", + authType: "apikey", + transport: { + baseUrl: "https://api.b.ai/v1/chat/completions", + validateUrl: "https://api.b.ai/v1/models", + }, + // No ids hardcoded: the catalogue is large and rotates, so the live endpoint + // is the source of truth and any id is accepted via passthroughModels. + modelsFetcher: { url: "https://api.b.ai/v1/models", type: "openai" }, + passthroughModels: true, +}; diff --git a/open-sse/providers/registry/dahl.js b/open-sse/providers/registry/dahl.js new file mode 100644 index 00000000..8efc0189 --- /dev/null +++ b/open-sse/providers/registry/dahl.js @@ -0,0 +1,35 @@ +export default { + id: "dahl", + priority: 120, + alias: "dahl", + aliases: [ + "dahl-inference", + ], + uiAlias: "dahl", + display: { + name: "Dahl Inference", + icon: "hub", + color: "#1E40AF", + textIcon: "DH", + website: "https://dahl.global", + notice: { + text: "OpenAI-compatible Gonka inference node. Small, fixed catalogue (GLM-5.3-Flash, DeepSeek-V4-Flash, MiniMax-M2.7) at a flat per-token rate.", + apiKeyUrl: "https://dahl.global/dashboard", + }, + }, + category: "apikey", + authType: "apikey", + transport: { + baseUrl: "https://inference.dahl.global/v1/chat/completions", + validateUrl: "https://inference.dahl.global/v1/models", + }, + // The live catalogue is public (no auth), so modelsFetcher works without a key + // and the ids below are a convenience seed rather than an exhaustive list. + models: [ + { id: "zai-org/GLM-5.3-Flash", name: "GLM-5.3 Flash" }, + { id: "deepseek-ai/DeepSeek-V4-Flash-0731", name: "DeepSeek V4 Flash 0731" }, + { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7" }, + ], + modelsFetcher: { url: "https://inference.dahl.global/v1/models", type: "openai" }, + passthroughModels: true, +}; diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index 0e8dbe2e..4318a5a1 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -125,6 +125,10 @@ import p119 from "./selfhosted-embedding.js"; import p120 from "./fish-audio.js"; import p121 from "./alitp-intl.js"; import p122 from "./xquik.js"; +import p126 from "./dahl.js"; +import p127 from "./atria.js"; +import p129 from "./agnes.js"; +import p130 from "./bai.js"; export default [ p0, p1, @@ -250,4 +254,8 @@ export default [ p120, p121, p122, + p126, + p127, + p129, + p130, ]; diff --git a/public/providers/agnes.png b/public/providers/agnes.png new file mode 100644 index 0000000000000000000000000000000000000000..8615a3134360db9486a62ecf762062aa299f7623 GIT binary patch literal 5257 zcmV;46n5*0P)^JJ4r-ARCwC$T?w>J^%_6tnkjLSQbcBvib{C(C_F?-4?TIURY|?l zO0PnAXceng8QwEQMGCDlW_Xn`m&!9FLy07nAyYKSc+da+zkT=b_|E_Cz0cnJoU`}2 z_w2pach9+J-uwG~^Y{P$p-{m)!jg*U6gGISyrhPb?vnJBq<>4ABk2=KUrYK)(r=Q^ zN)kGq(H}b?=_^UACC!#JT2enrcSvd==^~TwH2Xclkwm5!m6WACUR+XrNxdYEmz1M3 zSfqd=lJ-ejA!)3n9+GNHDyICV01Qbyvh<6|j|!4H=?vc@>68h>a&;*u={~3bpQ|%6 zV!&KbuKxP7I^n(Q>*@9nNgH)d@fTe~s9?wn*y#LFFWiN?N}nm|paC<^F^Z%}0|*5i zM&Gqp(o{(uB$YA%a-bD#*mlnN#=3OvRU{8g$W1ZB?U;pgCR$>*q!GG_veIb5p)j6o z>g|#~QXDnTbdjc%Yw#OSOPVj~R>u4Q2nLKNhpxM%^@@b0JJaKVforH7-G3>fIE)#q z?s31fqz#PexgOYaugyfR0)*C?>MZh;y-w0niwjlQ2>t@jcb=qrnPL{W)R)(tyVC|2 zCKHIC?+Ho6C6!?U5-yDKJfxl+yl*p<@m8i`o1~i!u2-SR;31ZusGNKxLz!=sTYx}} zlvI*wgF?alrjoX@lgtemZwZ9Ym~T+DAesd3(Dc?*rotdB3Mju>Kkd^Mt zugbD{8OMA`fAi+e#o)n%#k6VD#M^JbEk=wOA=ueZ~%fWx|U4~5M(^_!N69nT8T4f&S(rK z#;Y0t2Y^AxqmMr776h9mRq@*=z!z5w<@MSLrF>iH2_AjPk|o844I82ij(6nd=4!V` zj~*3QUU{Wk5UgW9L6%?4FR%X)k%=i?sQ#{9yV_be9LE5FkFa;| zUUAVy7r6ldzxG4CatiSUeKJ+;8~FgAefF7a031GiSY&5sy9GdmWemHf8U*;@W_k@< zWO`m7CSc*hg^mFLM{xW0?V?nvQf>ht7yu6G(O62%0UV67lD<&BIO72@XwaY-$Igil zFl*K%=eu`+o_+I%}nqFda8fO`7Y1^`H0BM`wnguGpRLbr>ZI)jk@ zZUOTPe1|dofwp!X!Z4HmPXhoXw^7v0Gzec>c+23$IwQqG-^dfoRsOnHCikFJ3&^3817z1bCteOC|(yzeDdI|JSHdL-gy{ zPb^!uOdL3HKuZE4IkaNM3Q?_EHLpMb9v+ue(+UJZ)-OSIPxnc}XU?1{e*5jWn2aFZ z5(rQPTCiY&A6mdPI|$WJW6t&j_TPC1uvoET;`!&F7pG63jxrpr;79@Ubnvlb$3*q& z)l-=fH0l}n+^k#-M05?9@4Nn~k&pY{HDd`E#D= znKy4(Zks#V3og9lx80!^7RC0U(7Zp=UnuK59gae?}GCjFyFkB-|6ssaO= z;@M}P^#cHi?Eb|vzR^2Dv!{a=uqI8Kh?6HzT1r1Ufs8aj_wL=@tO!b$AFx~v0~_eQ zWI^)BruwIb4?C^kKm71Rv1JG_wL=>lKEit_uqf7 z*?M^Yc${(o$OL*^9O4zijvN3%ySGi%PqF(?KmBB>`T-1@3i9f!uSOq32>9){-)e~? z4?{q!T>J?skeJvX&-duj!*2k@)CHaj z?7y|@Cuc4m^CA7z`;Uf?IdkSj-^aBEHHP>Q0T?-gJwy7wdO-wW67ta^4>gRla{k)2 zYp1=BKtLnm)c4W?kxMVV)K4dn^Lh{gU~ZrRjO~7{TD8Rf{rh7i+)DMo^Ugcb&!}6s zuEuQM4B}H2&?*w3@L$_eGXQ|WukjK$zb*#DtJhsHdQ&w2gz*Zm9uRRDY(()Y^^asbTfd;Iao zZRPo)*0*ijCd!sA8^hT5-+$i_05na+#|S_L0FC(5HcbxZ{pgIN*c~lhcDsAXQRGae znLjl9hrb552myW80MPbS0S>%>FoQ#z4a1mpnt zG8B0Hd5oxBxw59p?Wle};*BB&wKW`}BR+?RfK{qgNhbj83T3kD2NP(9KP}dD#w&j0V9iwTJW9L7PxsfJVvq90KM8z?n>6y8YgJ@7ZqbakK76=i=7V|E-c8HE%m9GLYS*qEvpih& z9qdZK2LJ&8dKB6 z9;^ajs1}g`Bw9^m zr2%NHY{!lr#T#$Dq17a=y6UP_2^Dz!6`vBa!mS^Gi$KMcUcGwx=Hzf`Y}~kUv>fYX z7Z1+BJ$v@V_1a15iD~QCuQ#nA;xXD+=!z?@5ar947nfgtxfV{6X`x0ET4#tVBpuX- zmcqV4@c2ExGH8rj0N~=>bkj{*bu0tl41JHC?^3&p`4A~0tRSY1 zr&HK8%GU(6;{c$!@+aLmg3tgnX3WSZJ*fl%ufJ1Gp294sd^gO8MAFa){@cNXwr$%+ zJA_G6vDnjzZvkMc?*s7m?c1ky6ML|iz+p)jt7GEUJ2eBiIHUz&bI~J5PE9IKgxh+e z`|rO$x-o-OXHRAR{Q2_@CwucwA8PBitMcst0^d!;Bx&pU6h~9w?T1D)f<`+90DdU# zeS^V>I3Y@j!fQ^(38{1Vsp__P@nT16*h=lMTenWL^=1qJ=tx3g``vyg7?1apZvn|q zvFTLpe^CyA_D=l({8&>U{XqS+ORl@7}kSN50+KU^KB-&^fVQA8%Nip``!)c+^1X5g`7K%ZU zQ=FIqTrfhhQOu2tLj@9wmyIM_CG|UK=rXOc1>;pIa&v$YZD7Dcm8bNyxEp1 z!1IR=9nwO)i!Z*|kK`G6`jXM4Q2cW{LrcW24=`=oG*fBd%%a9invO*qD4BgGRm^C# zI%UolEm{<_zD}LHn~3C-3AyE#TU@nh0jNED_KXVvocoVI{;0K`$J)w?6@v2x#A?w zr$i27?L<3sA;n~_)E)pQ%_JVGe@gyTjkk9YKrq{mGY9}sp~Fq?-HQXvCn=;vPAb~6 zKmGJm!}G`g3l}c*69C}hBs)8Y;mLM(GyqNrW!H290MAFzI)7Bxk{N5`vtD}XCByTF z3Hbc;&;6>Bf`4b!*x$~vBAzn{08phYodCdd1hg0fd-Wze(SpzG(xrt^v5zQ);(m%a+mKm@28`+s5+@r3wHN4B9~2V*sd)A6afTk8WpB zM>ZUyXjU)x%GTq1DJhkmot<*#t015T5iK+S(e%>;MEdy%37*gkgo~wX@8)JcJ~an0zf0HF*3N&4?OUICuTq`rIPI( zHEL9}|G?|UQ~+S=)T#N-9~HwFUwqNaY9DxX0A0UMb`Cc)1;fpe{Jt28rQZ z)zkl!U_m9%OQ|;!0BR$H^GUxG%-^+ZSG4xwvGn&%MEZ?t1e?sF0!RVvf&pJ386e;p zL*6(^ojc8&H;-Y0U+w6;E}-rrLeA*5*Iw(T?dMy<6iGz#jkLh6`WHm3Ou=1u-6c+( zI1wYYNbhpu4J07Eo=ZAzsct`R+&Hb}+o&%v9fnI12=2S@K5ZouX#{Ek_rdWabA63l z$1ffTMp_k$zzmw6fhn6tQGp5svkDsJ?%#d)U9E%+&pw?PZ`8;V?=cUg|DVZF{br`1 zq@*=gQ2|bv7(}g8%FP=m$n*|Wvq!GlGsR;{8vdK_TsvivOG z`aX}%5_qvNQK!#NRB)A~qgGi$HS1 zrE{L(p$y-n%*>`e^Ld^CegW-Gd?YXlj#WO5?rY65eBn?5JHjkASxfg`f$l&X7)w2_ z%VJ5F`H|sEWrR>nZ!J5+PB_p8d3GL*KOyN!mf0)t%;$aqPUc2Q8z{0E%B1%YnT3@ky zSz(s?c-jC+Z!T$xViyZb#Grzj{+}B<-?$mh1gW><8_@A*Ngpwd5GlY>gUa|bI^%CN z(S{j)s2AcUNz+5Idm2UxKvwY_(_DNm|70fdM!*ja6ChNVG*HqO#T)<(rt1_uvgr}V zbQK8e^_e_X0>&o}gc6ee6v}IV+G(P0&ob{i*`UDZ%&3QP*o_ zNgYFZg{S$thWIrF>i)RC%j#@BhtW2a*A8d&I|Igh(3NafAfd-=NV-|l-y{u^G(plF zNgs#u_RIZ3-#L4iPG9PaU)Skcg{Er!M^gV#-ewvt^p2+@7B!Co$@KpNO{|+YjTDOh P00000NkvXXu0mjfwsZO; literal 0 HcmV?d00001 diff --git a/public/providers/atria.png b/public/providers/atria.png new file mode 100644 index 0000000000000000000000000000000000000000..8ef034e30829e735534285b09f4291a18daec10f GIT binary patch literal 2449 zcmV;C32yd@P)w<=UHuOFZ8pFh8T{d#wIcYS^R_8L@zZSlp$h5v8(LqwlGeOeMwMbI-;M-ZAHKYlC;C?@C-vZD_l zJ}dwV==S!OtOgnQ% z_AoeK0HWQ8%SgRwDtkba#J5)bz9|7S`$ba#0P{%QDT(I{guOaK+y--ojJTi8!VfJ&tYDUkEB0jcxDZyEOb{rmSQ&iwlI>y%V% zP@$}s6)yyxMS&*q{iBSH-$(FfSm`|(Rd3Q~U0q$BkpxKg2wNXbasSjp#F&zF&Lj$0`jeao1_cw z`o{z`iT-hnA8m2;zkuiA;Q`;U=?=+LOxeJ=rx5fG#5RkLjK;@5fh(h+!4ZBLrZb}l zg_V7|1mKgMyNPJ5|HBlgAQRbEI6j{&?4(O^eYOBZngOhx3On{xSRDTL?b|8WRj1QJ zY9DNB0I=84;C@fPeECx2L<-Z$UgH&o$Br}lSOV%F07EmR-j2sclDYv-!rg&9>9Q2h zQw6zJ@^p{(A_KQyFJ8Q;36~V18J^p%P}3EF)WIlCO6$6MW^P@fV>LfJx2N>n%g2u& z74?QbCHpY|;N4!oeqE}U(?)Z-omAxJ`UC_6it?LkCksQm9m3srA5&O@J^?{*3Ggk5 zMSkI);mu+YexSZeb9ex-k7?m0D+e{47y_WklJcq9g8=})LRT!*8NOl{C;Dv}(3@hpfVSfaGPJ$!XTIx%F?aY43b}pKA;bN#RuWJ%-Jxe+RB2R< z%V)k5Ax*)Ebs8QvMrXBBB7Z>8n}z?HWmt`|x`WO)0Z&nneIQ>dVDn}n)= z509~9KJmCx)78ROQM*ome)oH}|5<6FsGH9O54{8w4VZ~w*z+hR*Fw>PW!a^RWtJ$^#b zQMhx{RTTlS-vSv=2HJ zhKGFfUu{bH{&XuU~D|JRW=up?FPFC!C+h=w`VVav1RJh@;M>>XhUfSrRn% z@2ferJinj5PWDeqkJd;ffH}j(gE6i0iP%dR5}$J26^JUD08rXyGDEi6JS6tg?7-V= zZz&Q2Y8QEJDxazEQphC`LlqwZ5GLe{v@Y2?KjG5nR6b1gEju|sax|F+FZsJuqruK$ z)P5Ao=69nsV#JqWcd0z#NR2|@(*Y@K2BHHqykZdcd5~e|$Fo3(lRb?n&OBN~t zqo+BE*1XBWa@~*($H=hzA#JwBB;e9-KzqUXdtLr%o=u)eJtc-Uvq^yr0MSeZMG)ZP z^)P7U!~Ib6V)ap4v5zRY5xpj+(KXXC%O>0YAM983Fi9yb3vdWVI}9Y>%ooNL10Kot zu=QqtQq8g9O*I9Z0o~gFU{k1km|+J+c}b}kRHKE$sNMXQDvt+v6DFsfKM1u_WcK|NA4G0$en7k<4c*L_)oH*GSDNk0RDCjU^3NB5{=SO6uLa zca`dRFSP)x(C#*8EB(X!`@8~(GKpmX_&-yd;&8c4n-Hvm&flG!gc&W;&G;(HlKKxs zslkvnAg%mUMj29Sb{wdzIoC?$;TS~4)yU+)Pw#)btvSilB+~N%P7yIyuGB%|ui$av z&k*}r&tye|Z3?qp+*q#S5 zW@sOC=P92lf*Ns^lNR+wd?&sc+k~|N5)(#o1-QK|zoK4E=2cTH&b>3(`Rx;7(e4NQ z#+MJ}bwS%IJXYv|0Reuu)Nr+q_%*99)SBobx6`;bbgS)~`u+g^tt6R3+vm>NtxcW# zYKEVPum5oRnVJ`;=d{;H6r*#r0nb(iEm{C902VC(766MD01JRcLIC~;j@(NbU=-^Y P00000NkvXXu0mjfQu3lS literal 0 HcmV?d00001 diff --git a/public/providers/bai.png b/public/providers/bai.png new file mode 100644 index 0000000000000000000000000000000000000000..878045c5f908059681a35eeb94b83a13dd171e13 GIT binary patch literal 6093 zcmV;;7c%IHP)g~-7~8=C@?3`Nd4I|wkLumO_`5e*0j z1f~F426ReDLRko8Nq|L=z!r#TurWD<$T<`OBXSZT-R}SA_m=LZ_1W7$e{V0$>~2-p z|F`LOrh9sNI=t@Dqw#1w8jr@K7LQz&)Oggp;Iy{37HPnM0ZpVCGiH2`g!`V8CQVw5 z#CuETJxxmbJq;o2g2#vvBbFRJdh~J^U3AgXVAxXp8%$XO&n=E?FEUXX*4a#l z!6l(e3Q!rrX*^sXoJ3xS45J@4a!qJ?_`0;80t+FjxelC&u2?5QuLL zKyCv!kwKX7JSfKBQ3{c9ix8g6Tf%cKB)Ko>XXX2g0zg3^LGX&C!ly!}!leSmdl6KA zJ{HaODehmz^#RZ<&_rq;2+4626ufsseAhDNw2IguJ=>vi{C;rovJL#6lP!#D?IemV zQ7{t1mlUiP6(qjT3XW#5Wp+-{N@QM5y@o&=ytwt)>X zDGNn4$DJqy?G_*-I3mjOMS$(a6P)SH+T+}2*!QghI0)f}R0No7ggV$EH3q+0O;I{#+hzmF&GD)qWlL$po zQ&2-d5MmLAd6?)Ik!uxIs3M8}ajh^$jcoj~do zQe#UULVUkEg-CjwQ@?-}E)kd(D@X~SLWc{CYz>htj^|+`>}gI5xnR747~~2xf@mRE z5&}*L8HxLAFd6&ddK~B^Ft0CNzo2vR_XTtbNlDY(A@*Zc4qh35m@e?>-9 z82Fo1^YC7^i~eTOB2ha9A;NDf$Q<2_-?6IIq9J$%`v`L@O;phn$H~V8{Dhygp$?&%J`5!YvU#^>+V?MaN>xShgK{JfkNUZUXI zIrMrP;DmzatG~U%Bows(C%uZnFB11p<`rlN*hcoM3F7cG&CSiLF!IPi5@@PKIb)40 zm?L0Zg*M1@JHlMOP0ghNrx0=kZkOOt#chC-h`~>{TS#YSF;2CMbAx@MSi}MB#9{hc)MY>zoQd?0!u2%g19mTfz zT;l{;gJLQK5TZAc->_~MEQs0~f)Id>dGtyP4p9i?*m$5vlAqJ<#Tu|;ON_vni-KT$ z70yCHt%=Xi!91?qO&5flSCA_MgL+K%$V!ut7i)N;uc}XtR2@53@mdgs=@4uRcUEnX z=LrNtaJA1`K(~SBG_Ro92IfwlJbBq_kA&St`&q{h)7CJ?H_TQTXVC6DIw%y}7SquXyM}Px|#6OXiDyGID z-un(cZ@wOZT(ly_INCLVR_6uubML%_M(WRT$q`Un0QZ0P-Y;HZQJi?iOe^oHTHD~6~AbWSkd%`ndSV*A7n_7Rgw*7a5hFo4U` zLc~_$nGX=4Es)Hf8kl8HSRUt{W;8hhySNyG<_!FShKWPd0y-XOyHzkRb6t{WlWRAyuOV%n@j0h{r;6Og@(p zz?5UMrjK3jt*2C1Fa|Tr)e%Cssf3gd4owKmX<#23f{p>R`;xYmcl#dGo-m*VF8iK2 z6?};OZ}X6ZKoXi`aIGUWbp+3=5XegK*)Y_2$x;~K+m-wQCUNFN zynh*=6K8BtWZx`>q$X$Xb^N`F3W3L=V=Dl2T?7F4A8Ku(*TQdn;*)6IVjnDIOrd7B z%u%XU3=Ex*0>I*w_Z>CBTvkGPF960Jx*WEGC&a>aO`eRjs1%0b)fyx*DcoLY1)YO| z0|)*PV@NBcDcS%K`d0+etcUlN#cB{@Pu~L4{aISHunam!LBh;mKoCY+K}f>78Ia0c zchiFS?K>F8r=0>GGs2w<<}8d!I5JDRRHFVq7Kaf-rnB+f@eCxjG~BQt~ZiW-N=nVLPDr6k)Ra+L4`B!2z~1Yrzo520bUVfB&g)?hS8hoOa< zNa+j*$5@_K%XrU_Ad_WG66r8;i&j%?S;Fa|Fi@jtnYRek$AT$vEoi8qh8}=(__nzJ z5pj;wCe~_?_ShM^f}#1=VX};aQ!Em}?h|OX)VClB7~~*{e^*s;YlT689wLkNr+^WH z-_t5cA;buH7uPws{tMSDpg9KP{&d{$5AWqqlfaNxAZ%q&7WI$cHOOK0YgW(#Q$`*n z;#khu1ZPv?39v*4)(}-R5-p*SF|meP6=qf;X04F${SXM^5Z_M003AY%0sCR`uA8Kq(`892L*q!^A!? z1+&}W`A^jNbcE6)?OvAxkW`}rO5K}KahT}5#pyB7Ze0x&shT0u#u#AytxReG<6jf| zA#yDc#b>XoTjsZ=4SIZ&U$`k9BZPaSI@L43@u@4ys0D=Vbfi$V;(L$~E$O89L&6c6*Z|Z1j2talCe@(2ybttFK zF}@B>uoCxL2*vK8--6C$`#NcB_Zt3gQNfU?Wvg)*)eL4{_WjQppJ=H58Vf}d4AM2E z+P80C0z<9~A@~h7hqRsyaw6ZPwnDDBz=tCvXR^Tcac4N|BIz_E?h z-@_m9Cz~X~a&PtkfG~WN1r{z*0Mi2PvPX|lK%?u$F{N7w@4FQ7c&Tf);tVuXn`82?@D_qCl^f`R$}Tn>l4@lm-+YXNXASYu0|Va ztl$A;#gf+XpPdu}Xo8LKKi+kzhtyL6wt-JWPXw*Gf!5aXiLZeNSI5MqY?$xla8lVcC>C$Ok1pq=cyRJ2W_5)0$gN#QswgLK9Yb+?CnD zWW3ZhvIg<^q>H$W0PHNO;i=y*l|e_>)i$72xjm$a5{$o+&KKL8uLG-vQjI)Q51@=E zYRM>~ex-Bz%iLigZcegLnjqDom~{7Wn+=82+|@D z3H?6DC?Ji{#P845qQRyChj=vj+CaP4WB=S8gMOCMFGZpqspdOt^bu&&OG2~ZdN*?fdCX+(U((rB1xQt?vl_nl^%U=5EU2lx4>MOlna|0^ARm$= zRH2=v;oni;e4dQf6)oCD4atwFX7Qe@#Cdci!@LBtDyMQy``BDN+w5NBvJLPzQvgFV z1al^Ix-V4SN8ofouxNpZIX5H$u*^SAk|*M7j6CD;i8Xy&a_l~mYM~)`?Qk-hhGmL@mJADb2ym1! z1W$JG3g+dcF(H_ffI$N}SG--B_j(HuC7HSW`&mrtQ1`zw3X{28QazRl3+K97Kc$_L2497kzzyD?0*u-} zqe|veOGNxEK_l=zQ}Oxr?ZhMuv|KlD8?%-A7aRoO&QPrVBdP#~EUJ4LYq40ww-GeL zqKvi2D(@-*V9J=*C00TT6(7U{-ZqRdk+7|qKbI96)}%?z)ea)%0!6+7W;0XBK>!Nk zGQQhO0Z`RUqYR9HHcOVQ9Onvxv@xB9oW|GQ zA~s#oS{fl%$gLv5i2_r9&!R8Ro{mI$@M)|OU zKX7aeWmSK%H!^%EKl4f}+SV><)h?#kfv(>jJT?AuxwK0!1HZtZh)$-`~3} z(d4glN5&=_#~G_t0wV7qyg4Qo-)U?_AYuFQ|!fo$Zj{rULLd z=u`qgu#}5x6@Om|n#D#8TzzZ_$3zIwCwTo!0ypXjL(937%;Wzj1GS?E-y|=!M7CeBS=CXkyNdBtKtM?`l&7(6z4tC~Q{{4nYx0MxQ_DC`iZV`n?X|@?wpme2 z+c2*s(ebNbtnny;6xP|j1dCP60wd2X=RfJ(`R)SGnf`rqG9-Q~&QFVJ=iVI|SCdYn zL^%=d_?SlbD&PqDX8;njkH%7#il2~41)S<$wBqO7?r75(yD1k$#7hjoJzc;L>wxhE zVZ64MZ(#u|5yO2hWpnZXW=Q=Gh2mrzdF=7Au6R5@2?3o=EFea3sct3$IMLw*;4_o$ zX(6xCJ%ynW&n91g6m*v21qQk5%T=gi!1p_V9(E}clgtz?csPLuG_=-5&2A)U4RNmT7Xt~z?V7`$O21fy_-MXj}pw22yI8l!#& z5v->P88;~n@78aI=iUlGiX-k2!w%Ar^ZK~A- z8yY2PN{Y1F^p&PYo7%)a(i*J_+Y}?MR0{G~DPmh-SyoUAB4~t$SY(UZRa)HTk*EPg zcIM99$KU@y=ic3g!0z4d%zokdQ09A5 zuCbMD$tiiNLrFhxDPS2KvFTY!59L%*$%CkyRivYnLX0Q<1$wl?*O4E=FDZI`R*30E zDVKXnwgY}3itk$z3QC#_U>S4>NEf6N(hcc|bmd67qA0|SK%c7nVD44<^n6%|nt~Md zMN8)BO&v@6IZJsRmU2<(HIM$72t2(2^H9qACKhy6G$OzRPX@CRpvnB^*Y4A88g(j6Y@iMI?6 z=@JL@ankJtAqc;LluSih-VT&s z26Qn`$&(^S$G!mTd&9(@>2@C+=2nK+K0bpSOVkZ?AO2r_F)g=tTV2Gr$ z<=_g&${K*GN+T(RA=Vba9$pLf$V*Yi47}8ihWrOU2ze}xB*A zZ#n?}x);G;|I)z9x);EI{8{kop8)5Md!g`!*^sZPg&cvPRB9lIAplk<0N&Cjfai|_ zY<(TznKuSrwjT!A`IfncU2Wj6-2=|;KLLBhXk!t`8ZZFm0st2{EZG3?_x}KFZUNYG zXb|LI08J>H4;mpTUN`~Xnq5%1=6VA`WksTWgLnh*`;7oQ{%e%b{OaHfUB{+Z5X!^) zx}2N889-3U0AvDyP{uQ^wkLfe zF%ba+Q0fE`C5OKLy`WZ5#Qdk9*neyb*wvSGHMcSV8390tgZ~D9 z<3GW^@}q$js9*px1pt+bF*RJA`Q_OA0SrLd@&U)d`Pz2^0IC>(vI9V)uB39P-w7f2 zt_NaY9ZUxR6nmGtBN*rL&W6yw!a%=+p`f>LwMhuE6If0FOyb2~zqh0U8(%gZ$tlJ?U;Yf_ht>qeA&UVhIRH(j1nbr}m^%E3T3oy_y#F$+2JFkPfWnMx!MXE( z^Eq@KQ6Zq~rB<=O`~&P!mjxL?2B16uJcj^0xYFEYYRT%pl&il&OTqBDFljRQe`rM5 zjszXS5h4MamhicNpx5YXc!~paG0Br@XosFN_-pe$A^M50@i|@hakZ;|^0Hgu{@(J)W>@j1}yWfXi zerwO+skQ*e!2Qv$*#}4s0C;V=s%r3l_oV*Z9;X2fKs&fUt!Ez~6#$S_nrXoO@q>DF z_X+@Z3A?{q$^fJWfFL9+&ijEb_Vf(Ec{vIOAVmP&I9HEu{rNpE6u}13->&6MV0r*B zpI1yCE^S~k{57LXni06`M{~4=A>oN52utDP zb?gKtd1)Ape0%H!Yy@4W*C9q)6-3L+x8dsI#sm1f<9TUV;ueS2;NsBB#zJBGXTkaQ z-QYj9H&_?g5`9(B_BX+Acpk55BxeE>xjcNs%ckpxmWOX^jkYNCLK{X_4~g|gQM^G! zPa8zgJRY7{)|U+;8iORz&mWHcp256DOxd=G!6eXvJ9v+v>}?Vi>=O3eTX=%VO$
    a3p&FbLPiB`^Ny3-29upfxRbogH7dR z+F~{Vqn`mNrSrGvgkCVhr-x?^05biQ`dz(Kw~fVlYj_5roaY2yHz!eZKLbz-*>u1h z4Rg=pL}dLKfD)#9Vbd$RA=UfSR;>CBOPuLv0Qx)fG3NKzqSt@Lgv6W_&H$8Rz7I|W z^H=Wxd%~o|dHu-(V95q^a8uZrInYAjkx-N87PIG%ZuY!y8Q3E)?OKeIQ02!Q014jG zCexNp8#0x;>o?6py+los;fGQOOn$zv!@($2TV% zLb@iMlkSBrWdoRRx_omo1At39ccfe;-cw=$m^Ew;-<-^dLjar=;#P55h>4DpZI1B+ zcp6Cm{O^&jQQArOf*v>xEGH{~USW}MQp(Lg>R9Vq%9SE~ped+xl{%g0Xdw08e~$Vr zlsbc>|7xv#5W=EoEPvJq&LH~&>2?0}++UDlQK#k=&`+DTdW2D;)D*t>4kdkQ#!G4-AY|Iy*Rnp-QFl~CfPE?ZKRX9L7eZC&WflzbolBI3sDnE`Q0HvQS`bf z#0*RsuE5-JJH082?_oEs) { const result = await resolveKimchiModels({ diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index b81294d4..90e19d83 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -696,6 +696,16 @@ async function testApiKeyConnection(connection, effectiveProxy = null) { const res = await fetchWithConnectionProxy("https://api.hyperbolic.xyz/v1/models", { headers: { Authorization: `Bearer ${connection.apiKey}` } }, effectiveProxy); return { valid: res.ok, error: res.ok ? null : "Invalid API key" }; } + // OpenAI-compatible aggregators. All standard Bearer + OpenAI shape, so the + // same /v1/models probe applies to each. + case "dahl": + case "atria": + case "agnes": + case "bai": { + const cfg = PROVIDERS[connection.provider]; + const res = await fetchWithConnectionProxy(cfg.validateUrl, { headers: { Authorization: `Bearer ${connection.apiKey}` } }, effectiveProxy); + return { valid: res.ok, error: res.ok ? null : "Invalid API key" }; + } case "ollama": { const res = await fetch("https://ollama.com/api/tags", { headers: { Authorization: `Bearer ${connection.apiKey}` } }); return { valid: res.ok, error: res.ok ? null : "Invalid API key" }; diff --git a/tests/__baseline__/alias-baseline.json b/tests/__baseline__/alias-baseline.json index a387cb7f..547f0c0e 100644 --- a/tests/__baseline__/alias-baseline.json +++ b/tests/__baseline__/alias-baseline.json @@ -119,6 +119,7 @@ "morphllm": "morph" }, "idToAlias": { + "agnes": "agnes", "alicode": "alicode", "alicode-intl": "alicode-intl", "alims-intl": "alims-intl", @@ -127,7 +128,9 @@ "antigravity": "ag", "api-airforce": "af", "assemblyai": "assemblyai", + "atria": "atria", "azure": "azure", + "bai": "bai", "baidu": "qianfan", "bazaarlink": "bzl", "blackbox": "blackbox", @@ -145,6 +148,7 @@ "cohere": "cohere", "commandcode": "commandcode", "cursor": "cu", + "dahl": "dahl", "deepgram": "deepgram", "deepseek": "deepseek", "featherless": "featherless", @@ -212,6 +216,7 @@ "alitp-intl", "anthropic", "assemblyai", + "atria", "black-forest-labs", "blackbox", "bm", @@ -229,6 +234,7 @@ "commandcode", "cu", "cx", + "dahl", "deepgram", "deepseek", "edge-tts", diff --git a/tests/__baseline__/providers-baseline.json b/tests/__baseline__/providers-baseline.json index bab11342..85a3ee85 100644 --- a/tests/__baseline__/providers-baseline.json +++ b/tests/__baseline__/providers-baseline.json @@ -1085,5 +1085,25 @@ "preserveCacheControl": true }, "format": "openai" + }, + "dahl": { + "baseUrl": "https://inference.dahl.global/v1/chat/completions", + "validateUrl": "https://inference.dahl.global/v1/models", + "format": "openai" + }, + "atria": { + "baseUrl": "https://api.atria-asi.ai/v1/chat/completions", + "validateUrl": "https://api.atria-asi.ai/v1/models", + "format": "openai" + }, + "agnes": { + "baseUrl": "https://apihub.agnes-ai.com/v1/chat/completions", + "validateUrl": "https://apihub.agnes-ai.com/v1/models", + "format": "openai" + }, + "bai": { + "baseUrl": "https://api.b.ai/v1/chat/completions", + "validateUrl": "https://api.b.ai/v1/models", + "format": "openai" } } \ No newline at end of file diff --git a/tests/unit/aggregator-providers-batch.test.js b/tests/unit/aggregator-providers-batch.test.js new file mode 100644 index 00000000..c270e93b --- /dev/null +++ b/tests/unit/aggregator-providers-batch.test.js @@ -0,0 +1,137 @@ +import { describe, expect, it } from "vitest"; +import { existsSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +import REGISTRY from "../../open-sse/providers/registry/index.js"; +import { PROVIDERS } from "../../open-sse/providers/index.js"; +import { getExecutor } from "../../open-sse/executors/index.js"; +import { DefaultExecutor } from "../../open-sse/executors/default.js"; + +/** + * OpenAI-compatible aggregator providers. Each was verified by probing the + * live /v1/models endpoint: a 401 with a structured error body confirms a + * real API behind the host, and Dahl/Kira answer 200 with no credentials. + */ +const BATCH = [ + { + id: "dahl", + category: "apikey", + baseUrl: "https://inference.dahl.global/v1/chat/completions", + modelsUrl: "https://inference.dahl.global/v1/models", + aliases: ["dahl-inference"], + }, + { + id: "atria", + category: "apikey", + baseUrl: "https://api.atria-asi.ai/v1/chat/completions", + modelsUrl: "https://api.atria-asi.ai/v1/models", + aliases: ["atria-asi"], + }, + { + id: "agnes", + category: "freeTier", + baseUrl: "https://apihub.agnes-ai.com/v1/chat/completions", + modelsUrl: "https://apihub.agnes-ai.com/v1/models", + aliases: ["agnes-ai"], + }, + { + id: "bai", + category: "apikey", + baseUrl: "https://api.b.ai/v1/chat/completions", + modelsUrl: "https://api.b.ai/v1/models", + aliases: ["b-ai"], + }, +]; + +describe.each(BATCH)("$id provider", (p) => { + const entry = REGISTRY.find((e) => e.id === p.id); + + it("is registered with the expected category and base URL", () => { + expect(entry).toBeDefined(); + expect(entry.category).toBe(p.category); + expect(PROVIDERS[p.id].baseUrl).toBe(p.baseUrl); + expect(PROVIDERS[p.id].format).toBe("openai"); + }); + + it("exposes its aliases and a UI display name", () => { + for (const a of p.aliases) expect(entry.aliases).toContain(a); + expect(entry.display?.name).toBeTruthy(); + expect(entry.display?.textIcon).toBeTruthy(); + }); + + it("routes through the shared DefaultExecutor", () => { + expect(getExecutor(p.id)).toBeInstanceOf(DefaultExecutor); + }); + + it("accepts arbitrary model ids via passthrough", () => { + expect(entry.passthroughModels).toBe(true); + }); +}); + +describe("Atria Dawn specifics", () => { + const entry = REGISTRY.find((e) => e.id === "atria"); + + it("is named after the service, not just the host", () => { + expect(entry.display.name).toBe("Atria Dawn"); + }); + + it("pins the single documented preview model", () => { + expect(entry.models.map((m) => m.id)).toEqual(["Atria-Dawn-Preview"]); + }); +}); + +describe("provider icons", () => { + // This file lives in tests/unit/, so resolve icons against the repo root. + const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + + it("ships a /public/providers/{id}.png for every provider in the batch", () => { + // getProviderIconSrc() resolves /providers/{id}.png and falls back to the + // textIcon tile when the file 404s, so a missing icon is silent in the UI. + for (const p of BATCH.map((x) => x.id)) { + expect(existsSync(join(REPO_ROOT, "public", "providers", `${p}.png`))).toBe(true); + } + }); +}); + +describe("authenticated model discovery", () => { + const ROUTE = join( + dirname(fileURLToPath(import.meta.url)), "..", "..", + "src", "app", "api", "providers", "[id]", "models", "route.js" + ); + const source = readFileSync(ROUTE, "utf8"); + + it("registers every batch provider in the /models resolver", () => { + // Without an entry here the route answers + // 400 "Provider X does not support models listing" and live discovery + // silently fails once a key is saved. + for (const p of BATCH.map((x) => x.id)) { + expect(source).toContain(`${p}: createOpenAIModelsConfig(`); + } + }); + + it("points each entry at that provider's own /models URL", () => { + for (const p of BATCH) { + const line = source.split("\n").find((l) => l.trim().startsWith(`${p.id}: createOpenAIModelsConfig(`)); + expect(line, `no models entry for ${p.id}`).toBeTruthy(); + expect(line).toContain(p.modelsUrl); + } + }); +}); + +describe("batch invariants", () => { + it("keeps every registry id unique", () => { + const ids = REGISTRY.map((e) => e.id); + expect(new Set(ids).size).toBe(ids.length); + }); + + it("does not claim vision for any provider in the batch", () => { + // These aggregators relay third-party models; none of them has documented + // image input, so no registry entry may assert it. + for (const p of BATCH) { + const entry = REGISTRY.find((e) => e.id === p.id); + expect(entry.serviceKinds ?? ["llm"]).toContain("llm"); + expect(entry.imageToTextConfig).toBeUndefined(); + } + }); +});