omniroute 2.8.6 → 2.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/app/.next/BUILD_ID +1 -1
  2. package/app/.next/build-manifest.json +2 -2
  3. package/app/.next/prerender-manifest.json +3 -3
  4. package/app/.next/server/app/(dashboard)/dashboard/a2a/page_client-reference-manifest.js +1 -1
  5. package/app/.next/server/app/(dashboard)/dashboard/agents/page_client-reference-manifest.js +1 -1
  6. package/app/.next/server/app/(dashboard)/dashboard/analytics/page_client-reference-manifest.js +1 -1
  7. package/app/.next/server/app/(dashboard)/dashboard/api-manager/page_client-reference-manifest.js +1 -1
  8. package/app/.next/server/app/(dashboard)/dashboard/audit-log/page_client-reference-manifest.js +1 -1
  9. package/app/.next/server/app/(dashboard)/dashboard/auto-combo/page_client-reference-manifest.js +1 -1
  10. package/app/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
  11. package/app/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
  12. package/app/.next/server/app/(dashboard)/dashboard/costs/page_client-reference-manifest.js +1 -1
  13. package/app/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
  14. package/app/.next/server/app/(dashboard)/dashboard/health/page_client-reference-manifest.js +1 -1
  15. package/app/.next/server/app/(dashboard)/dashboard/limits/page_client-reference-manifest.js +1 -1
  16. package/app/.next/server/app/(dashboard)/dashboard/logs/page_client-reference-manifest.js +1 -1
  17. package/app/.next/server/app/(dashboard)/dashboard/mcp/page_client-reference-manifest.js +1 -1
  18. package/app/.next/server/app/(dashboard)/dashboard/media/page_client-reference-manifest.js +1 -1
  19. package/app/.next/server/app/(dashboard)/dashboard/onboarding/page_client-reference-manifest.js +1 -1
  20. package/app/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
  21. package/app/.next/server/app/(dashboard)/dashboard/playground/page_client-reference-manifest.js +1 -1
  22. package/app/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
  23. package/app/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
  24. package/app/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
  25. package/app/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
  26. package/app/.next/server/app/(dashboard)/dashboard/search-tools/page_client-reference-manifest.js +1 -1
  27. package/app/.next/server/app/(dashboard)/dashboard/settings/page_client-reference-manifest.js +1 -1
  28. package/app/.next/server/app/(dashboard)/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
  29. package/app/.next/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
  30. package/app/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
  31. package/app/.next/server/app/400/page_client-reference-manifest.js +1 -1
  32. package/app/.next/server/app/401/page_client-reference-manifest.js +1 -1
  33. package/app/.next/server/app/403/page_client-reference-manifest.js +1 -1
  34. package/app/.next/server/app/408/page_client-reference-manifest.js +1 -1
  35. package/app/.next/server/app/429/page_client-reference-manifest.js +1 -1
  36. package/app/.next/server/app/500/page_client-reference-manifest.js +1 -1
  37. package/app/.next/server/app/502/page_client-reference-manifest.js +1 -1
  38. package/app/.next/server/app/503/page_client-reference-manifest.js +1 -1
  39. package/app/.next/server/app/_global-error.html +2 -2
  40. package/app/.next/server/app/_global-error.rsc +1 -1
  41. package/app/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  42. package/app/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  43. package/app/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  44. package/app/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  45. package/app/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  46. package/app/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  47. package/app/.next/server/app/api/providers/test-batch/route.js +4 -4
  48. package/app/.next/server/app/api/v1/embeddings/route.js +7 -6
  49. package/app/.next/server/app/api/v1/embeddings/route.js.nft.json +1 -1
  50. package/app/.next/server/app/callback/page_client-reference-manifest.js +1 -1
  51. package/app/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  52. package/app/.next/server/app/forbidden/page_client-reference-manifest.js +1 -1
  53. package/app/.next/server/app/forgot-password/page_client-reference-manifest.js +1 -1
  54. package/app/.next/server/app/landing/page_client-reference-manifest.js +1 -1
  55. package/app/.next/server/app/login/page_client-reference-manifest.js +1 -1
  56. package/app/.next/server/app/maintenance/page_client-reference-manifest.js +1 -1
  57. package/app/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  58. package/app/.next/server/app/page_client-reference-manifest.js +1 -1
  59. package/app/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
  60. package/app/.next/server/app/status/page_client-reference-manifest.js +1 -1
  61. package/app/.next/server/app/terms/page_client-reference-manifest.js +1 -1
  62. package/app/.next/server/chunks/[root-of-the-server]__007da72e._.js +3 -0
  63. package/app/.next/server/chunks/[root-of-the-server]__09c944b3._.js +1 -1
  64. package/app/.next/server/chunks/[root-of-the-server]__7d9b23e7._.js +1 -1
  65. package/app/.next/server/chunks/[root-of-the-server]__80e3bfc3._.js +1 -1
  66. package/app/.next/server/chunks/[root-of-the-server]__84e445b2._.js +1 -1
  67. package/app/.next/server/chunks/[root-of-the-server]__92cb0def._.js +1 -1
  68. package/app/.next/server/chunks/[root-of-the-server]__d4563e10._.js +1 -1
  69. package/app/.next/server/chunks/[root-of-the-server]__db2f9fe0._.js +1 -1
  70. package/app/.next/server/chunks/[root-of-the-server]__e27a89bd._.js +1 -1
  71. package/app/.next/server/chunks/[root-of-the-server]__f31b4656._.js +1 -1
  72. package/app/.next/server/chunks/_05c48915._.js +1 -1
  73. package/app/.next/server/chunks/_2115d8de._.js +1 -1
  74. package/app/.next/server/chunks/_3ac953eb._.js +1 -1
  75. package/app/.next/server/chunks/_4b8fd853._.js +1 -1
  76. package/app/.next/server/chunks/_5677b5e2._.js +1 -1
  77. package/app/.next/server/chunks/_68683848._.js +1 -1
  78. package/app/.next/server/chunks/_b39b1914._.js +3 -0
  79. package/app/.next/server/chunks/_ee9b677b._.js +1 -1
  80. package/app/.next/server/chunks/ssr/[root-of-the-server]__9affb65e._.js +1 -1
  81. package/app/.next/server/chunks/ssr/[root-of-the-server]__a6942102._.js +1 -1
  82. package/app/.next/server/chunks/ssr/src_d3225e36._.js +1 -1
  83. package/app/.next/server/pages/500.html +2 -2
  84. package/app/.next/server/server-reference-manifest.js +1 -1
  85. package/app/.next/server/server-reference-manifest.json +1 -1
  86. package/app/.next/static/chunks/{353ef826fbae436d.js → 0c5e4a7f0f799d8c.js} +1 -1
  87. package/app/.next/static/chunks/6fecf6be49542089.js +1 -0
  88. package/app/CHANGELOG.md +32 -0
  89. package/app/docs/openapi.yaml +1 -1
  90. package/app/open-sse/services/rateLimitManager.ts +10 -5
  91. package/app/package-lock.json +2 -2
  92. package/app/package.json +1 -1
  93. package/app/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +156 -0
  94. package/app/src/app/api/providers/test-batch/route.ts +48 -8
  95. package/app/src/app/api/v1/embeddings/route.ts +33 -3
  96. package/package.json +1 -1
  97. package/app/.next/server/chunks/[root-of-the-server]__6b56b04f._.js +0 -3
  98. package/app/.next/static/chunks/91761ba00c702cff.js +0 -1
  99. /package/app/.next/static/{m9BrfW8GQTdaCjvX6OgP2 → yygubF7HqPRxaseKRZqYC}/_buildManifest.js +0 -0
  100. /package/app/.next/static/{m9BrfW8GQTdaCjvX6OgP2 → yygubF7HqPRxaseKRZqYC}/_clientMiddlewareManifest.json +0 -0
  101. /package/app/.next/static/{m9BrfW8GQTdaCjvX6OgP2 → yygubF7HqPRxaseKRZqYC}/_ssgManifest.js +0 -0
@@ -0,0 +1 @@
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,710864,e=>{"use strict";var t=e.i(843476),r=e.i(271645),a=e.i(627949),s=e.i(745009),l=e.i(618566),i=e.i(522016),o=e.i(657688),n=e.i(948148);e.i(565650);var d=e.i(11477),c=e.i(474078),p=e.i(628029),m=e.i(339971),u=e.i(87855),x=e.i(454149),h=e.i(116016),h=h,f=e.i(567955),f=f,g=e.i(419947),g=g,b=e.i(148943),v=e.i(961430),y=e.i(105370),j=e.i(623249);e.i(313705);var N=e.i(25230),C=e.i(556938);function k(e){let t=e&&"object"==typeof e&&!Array.isArray(e)?e:{};return{use5h:"boolean"!=typeof t.use5h||t.use5h,useWeekly:"boolean"!=typeof t.useWeekly||t.useWeekly}}function w({t:e,normalizeToolCallId:a,preserveDeveloperRole:s,showDeveloperToggle:l=!0,onNormalizeChange:i,onPreserveChange:o,disabled:n}){let[d,c]=(0,r.useState)(!1),p=(0,r.useRef)(null);return(0,r.useEffect)(()=>{if(!d)return;let e=e=>{p.current&&!p.current.contains(e.target)&&c(!1)};return document.addEventListener("mousedown",e),()=>document.removeEventListener("mousedown",e)},[d]),(0,t.jsxs)("div",{className:"relative inline-block",ref:p,children:[(0,t.jsxs)("button",{type:"button",onClick:()=>c(e=>!e),disabled:n,className:"inline-flex items-center gap-1 px-2 py-1 text-xs rounded-md border border-border bg-sidebar/50 hover:bg-sidebar text-text-muted hover:text-text-main disabled:opacity-50",title:e("compatAdjustmentsTitle"),children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:"tune"}),e("compatButtonLabel")]}),d&&(0,t.jsxs)("div",{className:"absolute left-0 top-full mt-1 z-50 min-w-[200px] p-3 rounded-lg border border-border bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-black/5 dark:ring-white/10",children:[(0,t.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wide text-text-muted mb-2",children:e("compatAdjustmentsTitle")}),(0,t.jsxs)("div",{className:"flex flex-col gap-3",children:[(0,t.jsx)(b.Toggle,{size:"sm",label:e("compatToolIdShort"),title:e("normalizeToolCallIdLabel"),checked:a,onChange:i,disabled:n}),l&&(0,t.jsx)(b.Toggle,{size:"sm",label:e("compatDoNotPreserveDeveloper"),title:e("preserveDeveloperRoleLabel"),checked:!1===s,onChange:e=>o(!e),disabled:n})]})]})]})}function T(){let e=(0,l.useParams)(),s=(0,l.useRouter)(),p=e.id,[m,b]=(0,r.useState)([]),[v,w]=(0,r.useState)(!0),[T,R]=(0,r.useState)(null),[E,O]=(0,r.useState)(!1),[U,_]=(0,r.useState)(!1),[F,z]=(0,r.useState)(!1),[K,q]=(0,r.useState)(!1),[B,J]=(0,r.useState)(null),[W,V]=(0,r.useState)(null),[H,G]=(0,r.useState)(!1),[Y,Q]=(0,r.useState)(null),[X,Z]=(0,r.useState)({}),[ee,et]=(0,r.useState)(!1),{copied:er,copy:ea}=(0,C.useCopyToClipboard)(),es=(0,n.useTranslations)("providers"),el=(0,a.useNotificationStore)(),ei=(0,r.useRef)(!1),eo=(0,r.useRef)(!1),[en,ed]=(0,r.useState)(null),[ec,ep]=(0,r.useState)(null),[em,eu]=(0,r.useState)(!1),[ex,eh]=(0,r.useState)(!1),[ef,eg]=(0,r.useState)({current:0,total:0,phase:"idle",status:"",logs:[],error:"",importedCount:0}),[eb,ev]=(0,r.useState)({customModels:[],modelCompatOverrides:[]}),[ey,ej]=(0,r.useState)(null),eN=T?{id:T.id,name:T.name||("anthropic-compatible"===T.type?es("anthropicCompatibleName"):es("openaiCompatibleName")),color:"anthropic-compatible"===T.type?"#D97757":"#10A37F",textIcon:"anthropic-compatible"===T.type?"AC":"OC",apiType:T.apiType,baseUrl:T.baseUrl,type:T.type}:j.FREE_PROVIDERS[p]||j.OAUTH_PROVIDERS[p]||j.APIKEY_PROVIDERS[p],eC=!!j.FREE_PROVIDERS[p]||!!j.OAUTH_PROVIDERS[p],ek=(0,N.getModelsByProviderId)(p),ew=(0,j.getProviderAlias)(p),eT=(0,j.isOpenAICompatibleProvider)(p),eS=(0,j.isAnthropicCompatibleProvider)(p),eP=eT||eS,eR=p.endsWith("-search"),eA=eP?p:ew,eI=eP?T?.prefix||p:ew,eE=(0,r.useCallback)(async()=>{try{let e=await fetch("/api/models/alias"),t=await e.json();e.ok&&Z(t.aliases||{})}catch(e){console.log("Error fetching aliases:",e)}},[]),eO=(0,r.useCallback)(async()=>{if(!eR)try{let e=await fetch(`/api/provider-models?provider=${encodeURIComponent(p)}`,{cache:"no-store"});if(!e.ok)return;let t=await e.json();ev({customModels:t.models||[],modelCompatOverrides:t.modelCompatOverrides||[]})}catch(e){console.error("fetchProviderModelMeta",e)}},[p,eR]),eD=(0,r.useCallback)(async()=>{try{let[e,t]=await Promise.all([fetch("/api/providers",{cache:"no-store"}),fetch("/api/provider-nodes",{cache:"no-store"})]),r=await e.json(),a=await t.json();if(e.ok){let e=(r.connections||[]).filter(e=>e.provider===p);b(e)}if(t.ok){let e=(a.nodes||[]).find(e=>e.id===p)||null;if(!e&&eP)for(let t=0;t<3;t+=1){await new Promise(e=>setTimeout(e,150));let t=await fetch("/api/provider-nodes",{cache:"no-store"});if(t.ok&&(e=((await t.json()).nodes||[]).find(e=>e.id===p)||null))break}R(e)}}catch(e){console.log("Error fetching connections:",e)}finally{w(!1)}},[p,eP]),eM=async e=>{try{let t=await fetch(`/api/provider-nodes/${p}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),r=await t.json();t.ok&&(R(r.node),await eD(),q(!1))}catch(e){console.log("Error updating provider node:",e)}};(0,r.useEffect)(()=>{eD(),eE(),fetch("/api/settings/proxy").then(e=>e.ok?e.json():null).then(e=>ep(e)).catch(()=>{})},[eD,eE]),(0,r.useEffect)(()=>{v||eR||eO()},[v,eR,eO]),(0,r.useEffect)(()=>{v||0!==m.length||!eN||eP||ei.current||eo.current||(ei.current=!0,eC?O(!0):_(!0))},[v]);let eU=async(e,t,r=ew)=>{let a=`${r}/${e}`;try{let e=await fetch("/api/models/alias",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:a,alias:t})});if(e.ok)await eE();else{let t=await e.json();alert(t.error||es("failedSetAlias"))}}catch(e){console.log("Error setting alias:",e)}},e$=async e=>{try{(await fetch(`/api/models/alias?alias=${encodeURIComponent(e)}`,{method:"DELETE"})).ok&&await eE()}catch(e){console.log("Error deleting alias:",e)}},eL=async e=>{if(confirm(es("deleteConnectionConfirm")))try{(await fetch(`/api/providers/${e}`,{method:"DELETE"})).ok&&b(m.filter(t=>t.id!==e))}catch(e){console.log("Error deleting connection:",e)}},e_=(0,r.useCallback)(()=>{eD(),O(!1)},[eD]),eF=async e=>{try{let t=await fetch("/api/providers",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:p,...e})});if(t.ok)return await eD(),_(!1),null;let r=await t.json().catch(()=>({}));return r.error?.message||r.error||es("failedSaveConnection")}catch(e){return console.log("Error saving connection:",e),es("failedSaveConnectionRetry")}},ez=async e=>{try{let t=await fetch(`/api/providers/${B.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(t.ok)return await eD(),z(!1),null;let r=await t.json().catch(()=>({}));return r.error?.message||r.error||es("failedSaveConnection")}catch(e){return console.log("Error updating connection:",e),es("failedSaveConnectionRetry")}},eK=async(e,t)=>{try{(await fetch(`/api/providers/${e}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({isActive:t})})).ok&&b(r=>r.map(r=>r.id===e?{...r,isActive:t}:r))}catch(e){console.log("Error updating connection status:",e)}},eq=async(e,t)=>{try{(await fetch("/api/rate-limits",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({connectionId:e,enabled:t})})).ok&&b(r=>r.map(r=>r.id===e?{...r,rateLimitProtection:t}:r))}catch(e){console.error("Error toggling rate limit:",e)}},eB=async(e,t,r)=>{try{let a=m.find(t=>t.id===e);if(!a)return;let s=a.providerSpecificData&&"object"==typeof a.providerSpecificData?a.providerSpecificData:{},l=s.codexLimitPolicy&&"object"==typeof s.codexLimitPolicy?s.codexLimitPolicy:{},i={...k(l),[t]:r},o=await fetch(`/api/providers/${e}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({providerSpecificData:{...s,codexLimitPolicy:i}})});if(!o.ok){let e=await o.json().catch(()=>({}));el.error(e.error||"Failed to update Codex limit policy");return}b(t=>t.map(t=>t.id===e?{...t,providerSpecificData:{...t.providerSpecificData||{},codexLimitPolicy:i}}:t)),el.success("Codex limit policy updated")}catch(e){console.error("Error toggling Codex quota policy:",e),el.error("Failed to update Codex limit policy")}},eJ=async e=>{if(e&&!W){V(e);try{let t=await fetch(`/api/providers/${e}/test`,{method:"POST"});if(!t.ok){let e=await t.json().catch(()=>({}));alert(e.error||es("failedRetestConnection"));return}await eD()}catch(e){console.error("Error retesting connection:",e)}finally{V(null)}}},eW=async()=>{if(H||0===m.length)return;G(!0),Q(null);let e=new AbortController,t=setTimeout(()=>e.abort(),12e4);try{let t,r=await fetch("/api/providers/test-batch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mode:"provider",providerId:p}),signal:e.signal});try{t=await r.json()}catch{t={error:es("providerTestFailed"),results:[],summary:null}}if(Q({...t,error:t.error?"object"==typeof t.error?t.error.message||t.error.error||JSON.stringify(t.error):String(t.error):null}),t?.summary){let{passed:e,failed:r,total:a}=t.summary;0===r?el.success(es("allTestsPassed",{total:a})):el.warning(es("testSummary",{passed:e,failed:r,total:a}))}await eD()}catch(t){let e=t?.name==="AbortError"?es("providerTestTimeout"):es("providerTestFailed");Q({error:e,results:[],summary:null}),el.error(e)}finally{clearTimeout(t),G(!1)}},[eV,eH]=(0,r.useState)(null),eG=async e=>{if(!eV){eH(e);try{let t=await fetch(`/api/providers/${e}/refresh`,{method:"POST"}),r=await t.json().catch(()=>({}));t.ok&&r.success?(el.success(es("tokenRefreshed")),await eD()):el.error(r.error||es("tokenRefreshFailed"))}catch(e){console.error("Error refreshing token:",e),el.error(es("tokenRefreshFailed"))}finally{eH(null)}}},eY=async(e,t)=>{if(e&&t)try{let r=t.priority,a=e.priority;r===a&&(r=m.indexOf(e)>m.indexOf(t)?t.priority-.5:t.priority+.5),await Promise.all([fetch(`/api/providers/${e.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({priority:r})}),fetch(`/api/providers/${t.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({priority:a})})]),await eD()}catch(e){console.log("Error swapping priority:",e)}},eQ=async()=>{if(em)return;let e=m.find(e=>!1!==e.isActive);if(e){eu(!0),eh(!0),eg({current:0,total:0,phase:"fetching",status:es("fetchingModels"),logs:[],error:"",importedCount:0});try{let t=await fetch(`/api/providers/${e.id}/models`),r=await t.json();if(!t.ok)return void eg(e=>({...e,phase:"error",status:es("failedFetchModels"),error:r.error||es("failedImportModels")}));let a=r.models||[];if(0===a.length)return void eg(e=>({...e,phase:"done",status:es("noModelsFound"),logs:[es("noModelsReturnedFromEndpoint")]}));eg(e=>({...e,phase:"importing",total:a.length,status:es("importingModelsProgress",{current:0,total:a.length}),logs:[es("foundModelsStartingImport",{count:a.length})]}));let s=0;for(let e=0;e<a.length;e++){let t=a[e],r=t.id||t.name||t.model;if(!r)continue;let l=r.split("/"),i=l[l.length-1];eg(t=>({...t,current:e+1,status:es("importingModelsProgress",{current:e+1,total:a.length}),logs:[...t.logs,es("importingModelById",{modelId:r})]})),await fetch("/api/provider-models",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:p,modelId:r,modelName:t.name||r,source:"imported"})}),X[i]||await eU(r,i,eA),s+=1}await eE(),eg(e=>({...e,phase:"done",current:a.length,status:s>0?es("importSuccessCount",{count:s}):es("noNewModelsAddedExisting"),logs:[...e.logs,s>0?es("importDoneCount",{count:s}):es("noNewModelsAdded")],importedCount:s})),s>0&&setTimeout(()=>{window.location.reload()},2e3)}catch(e){console.log("Error importing models:",e),eg(t=>({...t,phase:"error",status:es("importFailed"),error:e instanceof Error?e.message:es("unexpectedErrorOccurred")}))}finally{eu(!1)}}},eX=async(e,t)=>{eh(!0),eg({current:0,total:0,phase:"fetching",status:es("fetchingModels"),logs:[],error:"",importedCount:0});try{let r=(await e()).models||[];if(0===r.length)return void eg(e=>({...e,phase:"done",status:es("noModelsFound"),logs:[es("noModelsReturnedFromEndpoint")]}));eg(e=>({...e,phase:"importing",total:r.length,status:es("importingModelsProgress",{current:0,total:r.length}),logs:[es("foundModelsStartingImport",{count:r.length})]}));let a=0;for(let e=0;e<r.length;e++){let s=r[e],l=s.id||s.name||s.model;l&&(eg(t=>({...t,current:e+1,status:es("importingModelsProgress",{current:e+1,total:r.length}),logs:[...t.logs,es("importingModelById",{modelId:l})]})),await t(s)&&(a+=1))}eg(e=>({...e,phase:"done",current:r.length,status:a>0?es("importSuccessCount",{count:a}):es("noNewModelsAdded"),logs:[...e.logs,a>0?es("importDoneCount",{count:a}):es("noNewModelsAdded")],importedCount:a})),a>0&&setTimeout(()=>{window.location.reload()},2e3)}catch(e){console.log("Error importing models:",e),eg(t=>({...t,phase:"error",status:es("importFailed"),error:e instanceof Error?e.message:es("unexpectedErrorOccurred")}))}},eZ=m.some(e=>!1!==e.isActive),e0=e=>{let t=eb.customModels.find(t=>t.id===e);if(t)return!!t.normalizeToolCallId;let r=eb.modelCompatOverrides.find(t=>t.id===e);return!!r?.normalizeToolCallId},e1=e=>{let t=eb.customModels.find(t=>t.id===e);if(t&&Object.prototype.hasOwnProperty.call(t,"preserveOpenAIDeveloperRole"))return!!t.preserveOpenAIDeveloperRole;let r=eb.modelCompatOverrides.find(t=>t.id===e);return!(r&&Object.prototype.hasOwnProperty.call(r,"preserveOpenAIDeveloperRole"))||!!r.preserveOpenAIDeveloperRole},e5=async(e,t)=>{ej(e);try{let r,a=eb.customModels.find(t=>t.id===e);r=a?{provider:p,modelId:e,modelName:a.name||e,source:a.source||"manual",apiFormat:a.apiFormat||"chat-completions",supportedEndpoints:Array.isArray(a.supportedEndpoints)&&a.supportedEndpoints.length?a.supportedEndpoints:["chat"],normalizeToolCallId:void 0!==t.normalizeToolCallId?t.normalizeToolCallId:!!a.normalizeToolCallId,preserveOpenAIDeveloperRole:void 0!==t.preserveOpenAIDeveloperRole?t.preserveOpenAIDeveloperRole:!Object.prototype.hasOwnProperty.call(a,"preserveOpenAIDeveloperRole")||!!a.preserveOpenAIDeveloperRole}:{provider:p,modelId:e,...t},(await fetch("/api/provider-models",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)})).ok?await eO():el.error(es("failedSaveCustomModel"))}catch{el.error(es("failedSaveCustomModel"))}finally{ej(null)}};return v?(0,t.jsxs)("div",{className:"flex flex-col gap-8",children:[(0,t.jsx)(x.CardSkeleton,{}),(0,t.jsx)(x.CardSkeleton,{})]}):eN?(0,t.jsxs)("div",{className:"flex flex-col gap-8",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)(i.default,{href:"/dashboard/providers",className:"inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary transition-colors mb-4",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-lg",children:"arrow_back"}),es("backToProviders")]}),(0,t.jsxs)("div",{className:"flex items-center gap-4",children:[(0,t.jsx)("div",{className:"rounded-lg flex items-center justify-center",style:{backgroundColor:`${eN.color}15`},children:ee?(0,t.jsx)("span",{className:"text-sm font-bold",style:{color:eN.color},children:eN.textIcon||eN.id.slice(0,2).toUpperCase()}):(0,t.jsx)(o.default,{src:eT&&eN.apiType?"responses"===eN.apiType?"/providers/oai-r.png":"/providers/oai-cc.png":eS?"/providers/anthropic-m.png":`/providers/${eN.id}.png`,alt:eN.name,width:48,height:48,className:"object-contain rounded-lg max-w-[48px] max-h-[48px]",sizes:"48px",onError:()=>et(!0)})}),(0,t.jsxs)("div",{children:[eN.website?(0,t.jsxs)("a",{href:eN.website,target:"_blank",rel:"noopener noreferrer",className:"text-3xl font-semibold tracking-tight hover:underline inline-flex items-center gap-2",style:{color:eN.color},children:[eN.name,(0,t.jsx)("span",{className:"material-symbols-outlined text-lg opacity-60",children:"open_in_new"})]}):(0,t.jsx)("h1",{className:"text-3xl font-semibold tracking-tight",children:eN.name}),(0,t.jsx)("p",{className:"text-text-muted",children:es("connectionCountLabel",{count:m.length})})]})]})]}),eP&&T&&(0,t.jsxs)(d.Card,{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("h2",{className:"text-lg font-semibold",children:eS?es("anthropicCompatibleDetails"):es("openaiCompatibleDetails")}),(0,t.jsxs)("p",{className:"text-sm text-text-muted",children:[eS?es("messagesApi"):"responses"===T.apiType?es("responsesApi"):es("chatCompletions")," ","· ",(T.baseUrl||"").replace(/\/$/,""),"/",eS?es("messagesPath"):"responses"===T.apiType?es("responsesPath"):es("chatCompletionsPath")]})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(c.Button,{size:"sm",icon:"add",onClick:()=>_(!0),disabled:m.length>0,children:es("add")}),(0,t.jsx)(c.Button,{size:"sm",variant:"secondary",icon:"edit",onClick:()=>q(!0),children:es("edit")}),(0,t.jsx)(c.Button,{size:"sm",variant:"secondary",icon:"delete",onClick:async()=>{if(confirm(es("deleteCompatibleNodeConfirm",{type:eS?es("anthropic"):es("openai")})))try{(await fetch(`/api/provider-nodes/${p}`,{method:"DELETE"})).ok&&s.push("/dashboard/providers")}catch(e){console.log("Error deleting provider node:",e)}},children:es("delete")})]})]}),m.length>0&&(0,t.jsx)("p",{className:"text-sm text-text-muted",children:es("singleConnectionPerCompatible")})]}),(0,t.jsxs)(d.Card,{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-4",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)("h2",{className:"text-lg font-semibold",children:es("connections")}),(0,t.jsxs)("button",{onClick:()=>ed({level:"provider",id:p,label:eN?.name||p}),className:`inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all ${ec?.providers?.[p]?"bg-amber-500/15 text-amber-500 hover:bg-amber-500/25":"bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"}`,title:ec?.providers?.[p]?es("providerProxyTitleConfigured",{host:ec.providers[p].host||es("configured")}):es("providerProxyConfigureHint"),children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-[14px]",children:"vpn_lock"}),ec?.providers?.[p]&&ec.providers[p].host||es("providerProxy")]})]}),m.length>1&&(0,t.jsxs)("button",{onClick:eW,disabled:H||!!W,className:`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${H?"bg-primary/20 border-primary/40 text-primary animate-pulse":"bg-bg-subtle border-border text-text-muted hover:text-text-primary hover:border-primary/40"}`,title:es("testAll"),"aria-label":es("testAll"),children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-[14px]",children:H?"sync":"play_arrow"}),H?es("testing"):es("testAll")]}),eP?0===m.length&&(0,t.jsx)(c.Button,{size:"sm",icon:"add",onClick:()=>_(!0),children:es("add")}):(0,t.jsx)(c.Button,{size:"sm",icon:"add",onClick:()=>eC?O(!0):_(!0),children:es("add")})]}),0===m.length?(0,t.jsxs)("div",{className:"text-center py-12",children:[(0,t.jsx)("div",{className:"inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4",children:(0,t.jsx)("span",{className:"material-symbols-outlined text-[32px]",children:eC?"lock":"key"})}),(0,t.jsx)("p",{className:"text-text-main font-medium mb-1",children:es("noConnectionsYet")}),(0,t.jsx)("p",{className:"text-sm text-text-muted mb-4",children:es("addFirstConnectionHint")}),!eP&&(0,t.jsx)(c.Button,{icon:"add",onClick:()=>eC?O(!0):_(!0),children:es("addConnection")})]}):(0,t.jsx)("div",{className:"flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]",children:m.sort((e,t)=>(e.priority||0)-(t.priority||0)).map((e,r)=>(0,t.jsx)(D,{connection:e,isOAuth:eC,isFirst:0===r,isLast:r===m.length-1,onMoveUp:()=>eY(e,m[r-1]),onMoveDown:()=>eY(e,m[r+1]),onToggleActive:t=>eK(e.id,t),onToggleRateLimit:t=>eq(e.id,t),isCodex:"codex"===p,onToggleCodex5h:t=>eB(e.id,"use5h",t),onToggleCodexWeekly:t=>eB(e.id,"useWeekly",t),onRetest:()=>eJ(e.id),isRetesting:W===e.id,onEdit:()=>{J(e),z(!0)},onDelete:()=>eL(e.id),onReauth:eC?()=>O(!0):void 0,onRefreshToken:eC?()=>eG(e.id):void 0,isRefreshing:eV===e.id,onProxy:()=>ed({level:"key",id:e.id,label:e.name||e.email||e.id}),hasProxy:!!(ec?.keys?.[e.id]||ec?.providers?.[p]||ec?.global),proxySource:ec?.keys?.[e.id]?"key":ec?.providers?.[p]?"provider":ec?.global?"global":null,proxyHost:(ec?.keys?.[e.id]||ec?.providers?.[p]||ec?.global)?.host||null},e.id))})]}),!eR&&(0,t.jsxs)(d.Card,{children:[(0,t.jsx)("h2",{className:"text-lg font-semibold mb-4",children:es("availableModels")}),(()=>{if(eP)return(0,t.jsx)(I,{providerStorageAlias:eA,providerDisplayAlias:eI,modelAliases:X,copied:er,onCopy:ea,onSetAlias:eU,onDeleteAlias:e$,connections:m,isAnthropic:eS,onImportWithProgress:eX,t:es,effectiveModelNormalize:e0,effectiveModelPreserveDeveloper:e1,saveModelCompatFlags:e5,compatSavingModelId:ey,onModelsChanged:eO});if(eN.passthroughModels)return(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-4",children:[(0,t.jsx)(c.Button,{size:"sm",variant:"secondary",icon:"download",onClick:eQ,disabled:!eZ||em,children:em?es("importingModels"):es("importFromModels")}),!eZ&&(0,t.jsx)("span",{className:"text-xs text-text-muted",children:es("addConnectionToImport")})]}),(0,t.jsx)(P,{providerAlias:ew,modelAliases:X,copied:er,onCopy:ea,onSetAlias:eU,onDeleteAlias:e$,t:es,effectiveModelNormalize:e0,effectiveModelPreserveDeveloper:e1,saveModelCompatFlags:e5,compatSavingModelId:ey})]});let e=(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-4",children:[(0,t.jsx)(c.Button,{size:"sm",variant:"secondary",icon:"download",onClick:eQ,disabled:!eZ||em,children:em?es("importingModels"):es("importFromModels")}),!eZ&&(0,t.jsx)("span",{className:"text-xs text-text-muted",children:es("addConnectionToImport")})]});return 0===ek.length?(0,t.jsxs)("div",{children:[e,(0,t.jsx)("p",{className:"text-sm text-text-muted",children:es("noModelsConfigured")})]}):(0,t.jsxs)("div",{children:[e,(0,t.jsx)("div",{className:"flex flex-wrap gap-3",children:ek.map(e=>{let r=`${eA}/${e.id}`,a=`${p}/${e.id}`,s=Object.entries(X).find(([,e])=>e===r||e===a)?.[0];return(0,t.jsx)(S,{model:e,fullModel:`${eI}/${e.id}`,alias:s,copied:er,onCopy:ea,t:es,showDeveloperToggle:!0,normalizeToolCallId:e0(e.id),preserveDeveloperRole:e1(e.id),onNormalizeChange:t=>e5(e.id,{normalizeToolCallId:t}),onPreserveChange:t=>e5(e.id,{preserveOpenAIDeveloperRole:t}),compatDisabled:ey===e.id},e.id)})})]})})(),!eP&&(0,t.jsx)(A,{providerId:p,providerAlias:eI,copied:er,onCopy:ea,onModelsChanged:eO})]}),eR&&(0,t.jsxs)(d.Card,{children:[(0,t.jsx)("h2",{className:"text-lg font-semibold mb-4",children:es("searchProvider")||"Search Provider"}),(0,t.jsx)("p",{className:"text-sm text-text-muted",children:es("searchProviderDesc")||"This provider is used for web search via POST /v1/search. No model configuration needed — search providers are ready to use once an API key is connected."}),"perplexity-search"===p&&(0,t.jsxs)("div",{className:"mt-3 flex items-center gap-2 px-3 py-2 rounded-lg bg-blue-500/10 border border-blue-500/20",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-sm text-blue-400",children:"link"}),(0,t.jsxs)("p",{className:"text-xs text-blue-300",children:["Uses the same API key as ",(0,t.jsx)("strong",{children:"Perplexity"})," (chat provider). If you already have Perplexity configured, no additional setup is needed."]})]})]}),"kiro"===p?(0,t.jsx)(f.default,{isOpen:E,providerInfo:eN,onSuccess:e_,onClose:()=>{eo.current=!0,O(!1)}}):"cursor"===p?(0,t.jsx)(g.default,{isOpen:E,onSuccess:e_,onClose:()=>{eo.current=!0,O(!1)}}):(0,t.jsx)(h.default,{isOpen:E,provider:p,providerInfo:eN,onSuccess:e_,onClose:()=>{eo.current=!0,O(!1)}}),(0,t.jsx)(M,{isOpen:U,provider:p,providerName:eN.name,isCompatible:eP,isAnthropic:eS,onSave:eF,onClose:()=>_(!1)}),(0,t.jsx)($,{isOpen:F,connection:B,onSave:ez,onClose:()=>z(!1)}),eP&&(0,t.jsx)(L,{isOpen:K,node:T,onSave:eM,onClose:()=>q(!1),isAnthropic:eS}),Y&&(0,t.jsxs)("div",{className:"fixed inset-0 z-50 flex items-start justify-center pt-[10vh]",onClick:()=>Q(null),children:[(0,t.jsx)("div",{className:"absolute inset-0 bg-black/60 backdrop-blur-sm"}),(0,t.jsxs)("div",{className:"relative bg-bg-primary border border-border rounded-xl w-full max-w-[600px] max-h-[80vh] overflow-y-auto shadow-2xl",onClick:e=>e.stopPropagation(),children:[(0,t.jsxs)("div",{className:"sticky top-0 z-10 flex items-center justify-between px-5 py-3 border-b border-border bg-bg-primary/95 backdrop-blur-sm rounded-t-xl",children:[(0,t.jsx)("h3",{className:"font-semibold",children:es("testResults")}),(0,t.jsx)("button",{onClick:()=>Q(null),className:"p-1 rounded-lg hover:bg-bg-subtle text-text-muted hover:text-text-primary transition-colors","aria-label":"Close",children:(0,t.jsx)("span",{className:"material-symbols-outlined text-lg",children:"close"})})]}),(0,t.jsx)("div",{className:"p-5",children:Y.error&&(!Y.results||0===Y.results.length)?(0,t.jsxs)("div",{className:"text-center py-6",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-red-500 text-[32px] mb-2 block",children:"error"}),(0,t.jsx)("p",{className:"text-sm text-red-400",children:String(Y.error)})]}):(0,t.jsxs)("div",{className:"flex flex-col gap-3",children:[Y.summary&&(0,t.jsxs)("div",{className:"flex items-center gap-3 text-xs mb-1",children:[(0,t.jsx)("span",{className:"text-text-muted",children:eN?.name||p}),(0,t.jsx)("span",{className:"px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400 font-medium",children:es("passedCount",{count:Y.summary.passed})}),Y.summary.failed>0&&(0,t.jsx)("span",{className:"px-2 py-0.5 rounded bg-red-500/15 text-red-400 font-medium",children:es("failedCount",{count:Y.summary.failed})}),(0,t.jsx)("span",{className:"text-text-muted ml-auto",children:es("testedCount",{count:Y.summary.total})})]}),(Y.results||[]).map((e,r)=>(0,t.jsxs)("div",{className:"flex items-center gap-2 text-xs px-3 py-2 rounded-lg bg-black/[0.03] dark:bg-white/[0.03]",children:[(0,t.jsx)("span",{className:`material-symbols-outlined text-[16px] ${e.valid?"text-emerald-500":"text-red-500"}`,children:e.valid?"check_circle":"error"}),(0,t.jsx)("div",{className:"flex-1 min-w-0",children:(0,t.jsx)("span",{className:"font-medium",children:e.connectionName})}),void 0!==e.latencyMs&&(0,t.jsx)("span",{className:"text-text-muted font-mono tabular-nums",children:es("millisecondsAbbr",{value:e.latencyMs})}),(0,t.jsx)("span",{className:`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${e.valid?"bg-emerald-500/15 text-emerald-400":"bg-red-500/15 text-red-400"}`,children:e.valid?es("okShort"):e.diagnosis?.type||es("errorShort")})]},e.connectionId||r)),(!Y.results||0===Y.results.length)&&(0,t.jsx)("div",{className:"text-center py-4 text-text-muted text-sm",children:es("noActiveConnectionsInGroup")})]})})]})]}),en&&(0,t.jsx)(y.ProxyConfigModal,{isOpen:!!en,onClose:()=>ed(null),level:en.level,levelId:en.id,levelLabel:en.label}),(0,t.jsx)(u.Modal,{isOpen:ex,onClose:()=>{("done"===ef.phase||"error"===ef.phase)&&eh(!1)},title:es("importingModelsTitle"),size:"md",closeOnOverlay:!1,showCloseButton:"done"===ef.phase||"error"===ef.phase,children:(0,t.jsxs)("div",{className:"flex flex-col gap-4",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:["fetching"===ef.phase&&(0,t.jsx)("span",{className:"material-symbols-outlined text-primary animate-spin",children:"progress_activity"}),"importing"===ef.phase&&(0,t.jsx)("span",{className:"material-symbols-outlined text-primary animate-spin",children:"progress_activity"}),"done"===ef.phase&&(0,t.jsx)("span",{className:"material-symbols-outlined text-green-500",children:"check_circle"}),"error"===ef.phase&&(0,t.jsx)("span",{className:"material-symbols-outlined text-red-500",children:"error"}),(0,t.jsx)("span",{className:"text-sm font-medium text-text-main",children:ef.status})]}),("importing"===ef.phase||"done"===ef.phase)&&ef.total>0&&(0,t.jsxs)("div",{className:"w-full",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-1",children:[(0,t.jsxs)("span",{className:"text-xs text-text-muted",children:[ef.current," / ",ef.total]}),(0,t.jsxs)("span",{className:"text-xs text-text-muted",children:[Math.round(ef.current/ef.total*100),"%"]})]}),(0,t.jsx)("div",{className:"w-full h-2.5 bg-black/10 dark:bg-white/10 rounded-full overflow-hidden",children:(0,t.jsx)("div",{className:"h-full rounded-full transition-all duration-300 ease-out",style:{width:`${ef.current/ef.total*100}%`,background:"done"===ef.phase?"linear-gradient(90deg, #22c55e, #16a34a)":"linear-gradient(90deg, var(--color-primary), var(--color-primary-hover, var(--color-primary)))"}})})]}),"fetching"===ef.phase&&(0,t.jsx)("div",{className:"w-full h-2.5 bg-black/10 dark:bg-white/10 rounded-full overflow-hidden",children:(0,t.jsx)("div",{className:"h-full rounded-full animate-pulse",style:{width:"60%",background:"linear-gradient(90deg, var(--color-primary), var(--color-primary-hover, var(--color-primary)))"}})}),"error"===ef.phase&&ef.error&&(0,t.jsx)("div",{className:"p-3 rounded-lg bg-red-500/10 border border-red-500/20",children:(0,t.jsx)("p",{className:"text-sm text-red-400",children:ef.error})}),ef.logs.length>0&&(0,t.jsx)("div",{className:"max-h-48 overflow-y-auto rounded-lg bg-black/5 dark:bg-white/5 p-3 border border-black/5 dark:border-white/5",children:(0,t.jsx)("div",{className:"flex flex-col gap-1",children:ef.logs.map((e,r)=>(0,t.jsx)("p",{className:`text-xs font-mono ${e.startsWith("✓")?"text-green-500 font-semibold":"text-text-muted"}`,children:e},r))})}),"done"===ef.phase&&ef.importedCount>0&&(0,t.jsx)("p",{className:"text-xs text-text-muted text-center animate-pulse",children:es("pageAutoRefresh")})]})})]}):(0,t.jsxs)("div",{className:"text-center py-20",children:[(0,t.jsx)("p",{className:"text-text-muted",children:es("providerNotFound")}),(0,t.jsx)(i.default,{href:"/dashboard/providers",className:"text-primary mt-4 inline-block",children:es("backToProviders")})]})}function S({model:e,fullModel:r,alias:a,copied:s,onCopy:l,t:i,showDeveloperToggle:o=!0,normalizeToolCallId:n,preserveDeveloperRole:d,onNormalizeChange:c,onPreserveChange:p,compatDisabled:m}){return(0,t.jsxs)("div",{className:"flex flex-col px-3 py-2 rounded-lg border border-border hover:bg-sidebar/50 min-w-[220px] max-w-md",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 flex-wrap",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-base text-text-muted shrink-0",children:"smart_toy"}),(0,t.jsx)("code",{className:"text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded",children:r}),(0,t.jsx)("button",{onClick:()=>l(r,`model-${e.id}`),className:"p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary",title:i("copyModel"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:s===`model-${e.id}`?"check":"content_copy"})})]}),(0,t.jsx)(w,{t:i,normalizeToolCallId:!!n,preserveDeveloperRole:d,showDeveloperToggle:o,onNormalizeChange:c,onPreserveChange:p,disabled:m})]})}function P({providerAlias:e,modelAliases:a,copied:s,onCopy:l,onSetAlias:i,onDeleteAlias:o,t:n,effectiveModelNormalize:d,effectiveModelPreserveDeveloper:p,saveModelCompatFlags:m,compatSavingModelId:u}){let[x,h]=(0,r.useState)(""),[f,g]=(0,r.useState)(!1),b=Object.entries(a).filter(([,t])=>t.startsWith(`${e}/`)).map(([t,r])=>({modelId:r.replace(`${e}/`,""),fullModel:r,alias:t})),v=async()=>{let e;if(!x.trim()||f)return;let t=x.trim(),r=(e=t.split("/"))[e.length-1];if(a[r])return void alert(n("aliasExistsAlert",{alias:r}));g(!0);try{await i(t,r),h("")}catch(e){console.log("Error adding model:",e)}finally{g(!1)}};return(0,t.jsxs)("div",{className:"flex flex-col gap-4",children:[(0,t.jsx)("p",{className:"text-sm text-text-muted",children:n("openRouterAnyModelHint")}),(0,t.jsxs)("div",{className:"flex items-end gap-2",children:[(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsx)("label",{htmlFor:"new-model-input",className:"text-xs text-text-muted mb-1 block",children:n("modelIdFromOpenRouter")}),(0,t.jsx)("input",{id:"new-model-input",type:"text",value:x,onChange:e=>h(e.target.value),onKeyDown:e=>"Enter"===e.key&&v(),placeholder:n("openRouterModelPlaceholder"),className:"w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"})]}),(0,t.jsx)(c.Button,{size:"sm",icon:"add",onClick:v,disabled:!x.trim()||f,children:f?n("adding"):n("add")})]}),b.length>0&&(0,t.jsx)("div",{className:"flex flex-col gap-3",children:b.map(({modelId:e,fullModel:r,alias:a})=>(0,t.jsx)(R,{modelId:e,fullModel:r,copied:s,onCopy:l,onDeleteAlias:()=>o(a),t:n,showDeveloperToggle:!0,normalizeToolCallId:d(e),preserveDeveloperRole:p(e),onNormalizeChange:t=>m(e,{normalizeToolCallId:t}),onPreserveChange:t=>m(e,{preserveOpenAIDeveloperRole:t}),compatDisabled:u===e},r))})]})}function R({modelId:e,fullModel:r,copied:a,onCopy:s,onDeleteAlias:l,t:i,showDeveloperToggle:o=!0,normalizeToolCallId:n,preserveDeveloperRole:d,onNormalizeChange:c,onPreserveChange:p,compatDisabled:m}){return(0,t.jsxs)("div",{className:"flex flex-col gap-0 p-3 rounded-lg border border-border hover:bg-sidebar/50",children:[(0,t.jsxs)("div",{className:"flex items-start gap-3",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-base text-text-muted shrink-0",children:"smart_toy"}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("p",{className:"text-sm font-medium truncate",children:e}),(0,t.jsxs)("div",{className:"flex items-center gap-1 mt-1 flex-wrap",children:[(0,t.jsx)("code",{className:"text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded",children:r}),(0,t.jsx)("button",{onClick:()=>s(r,`model-${e}`),className:"p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary",title:i("copyModel"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:a===`model-${e}`?"check":"content_copy"})})]})]}),(0,t.jsx)("button",{onClick:l,className:"p-1 hover:bg-red-50 rounded text-red-500 shrink-0",title:i("removeModel"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:"delete"})})]}),(0,t.jsx)("div",{className:"pl-9",children:(0,t.jsx)(w,{t:i,normalizeToolCallId:!!n,preserveDeveloperRole:d,showDeveloperToggle:o,onNormalizeChange:c,onPreserveChange:p,disabled:m})})]})}function A({providerId:e,providerAlias:s,copied:l,onCopy:i,onModelsChanged:o}){let d=(0,n.useTranslations)("providers"),p=(0,a.useNotificationStore)(),[m,u]=(0,r.useState)([]),[x,h]=(0,r.useState)(""),[f,g]=(0,r.useState)(""),[b,v]=(0,r.useState)("chat-completions"),[y,j]=(0,r.useState)(["chat"]),[N,C]=(0,r.useState)(!1),[k,T]=(0,r.useState)(!0),[S,P]=(0,r.useState)(null),[R,A]=(0,r.useState)("chat-completions"),[I,E]=(0,r.useState)(["chat"]),[O,D]=(0,r.useState)(!1),[M,U]=(0,r.useState)(!1),[$,L]=(0,r.useState)(null),_=(0,r.useCallback)(async()=>{try{let t=await fetch(`/api/provider-models?provider=${encodeURIComponent(e)}`);if(t.ok){let e=await t.json();u(e.models||[])}}catch(e){console.error("Failed to fetch custom models:",e)}finally{T(!1)}},[e]);(0,r.useEffect)(()=>{_()},[_]);let F=async()=>{if(x.trim()&&!N){C(!0);try{(await fetch("/api/provider-models",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:e,modelId:x.trim(),modelName:f.trim()||void 0,apiFormat:b,supportedEndpoints:y})})).ok&&(h(""),g(""),v("chat-completions"),j(["chat"]),await _(),o?.())}catch(e){console.error("Failed to add custom model:",e)}finally{C(!1)}}},z=async t=>{try{await fetch(`/api/provider-models?provider=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}`,{method:"DELETE"}),await _(),o?.()}catch(e){console.error("Failed to remove custom model:",e)}},K=()=>{P(null),A("chat-completions"),E(["chat"]),D(!1),U(!0),L(null)},q=async t=>{if(S&&S===t){if(!I.length)return void p.error("Select at least one supported endpoint");L(t);try{let r=m.find(e=>e.id===t);if(!(await fetch("/api/provider-models",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:e,modelId:t,modelName:r?.name||t,source:r?.source||"manual",apiFormat:R,supportedEndpoints:I,normalizeToolCallId:O,preserveOpenAIDeveloperRole:M})})).ok)throw Error("Failed to save model endpoint settings");await _(),o?.(),p.success("Saved model endpoint settings"),K()}catch(e){console.error("Failed to save custom model:",e),p.error("Failed to save model endpoint settings")}finally{L(null)}}};return(0,t.jsxs)("div",{className:"mt-6 pt-6 border-t border-border",children:[(0,t.jsxs)("h3",{className:"text-sm font-semibold mb-3 flex items-center gap-2",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-base text-primary",children:"tune"}),d("customModels")]}),(0,t.jsx)("p",{className:"text-xs text-text-muted mb-3",children:d("customModelsHint")}),(0,t.jsxs)("div",{className:"flex flex-col gap-3 mb-3",children:[(0,t.jsxs)("div",{className:"flex items-end gap-2",children:[(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsx)("label",{htmlFor:"custom-model-id",className:"text-xs text-text-muted mb-1 block",children:d("modelId")}),(0,t.jsx)("input",{id:"custom-model-id",type:"text",value:x,onChange:e=>h(e.target.value),onKeyDown:e=>"Enter"===e.key&&F(),placeholder:d("customModelPlaceholder"),className:"w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"})]}),(0,t.jsxs)("div",{className:"w-40",children:[(0,t.jsx)("label",{htmlFor:"custom-model-name",className:"text-xs text-text-muted mb-1 block",children:d("displayName")}),(0,t.jsx)("input",{id:"custom-model-name",type:"text",value:f,onChange:e=>g(e.target.value),onKeyDown:e=>"Enter"===e.key&&F(),placeholder:d("optional"),className:"w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"})]}),(0,t.jsx)(c.Button,{size:"sm",icon:"add",onClick:F,disabled:!x.trim()||N,children:N?d("adding"):d("add")})]}),(0,t.jsxs)("div",{className:"flex items-end gap-4 flex-wrap",children:[(0,t.jsxs)("div",{className:"w-48",children:[(0,t.jsx)("label",{htmlFor:"custom-api-format",className:"text-xs text-text-muted mb-1 block",children:"API Format"}),(0,t.jsxs)("select",{id:"custom-api-format",value:b,onChange:e=>v(e.target.value),className:"w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary",children:[(0,t.jsx)("option",{value:"chat-completions",children:"Chat Completions"}),(0,t.jsx)("option",{value:"responses",children:"Responses API"})]})]}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsx)("span",{className:"text-xs text-text-muted mb-1 block",children:"Supported Endpoints"}),(0,t.jsx)("div",{className:"flex items-center gap-3",children:["chat","embeddings","images","audio"].map(e=>(0,t.jsxs)("label",{className:"flex items-center gap-1.5 text-xs text-text-main cursor-pointer",children:[(0,t.jsx)("input",{type:"checkbox",checked:y.includes(e),onChange:t=>{t.target.checked?j(t=>[...t,e]):j(t=>t.filter(t=>t!==e))},className:"rounded border-border"}),"chat"===e?"💬 Chat":"embeddings"===e?"📐 Embeddings":"images"===e?"🖼️ Images":"🔊 Audio"]},e))})]})]})]}),k?(0,t.jsx)("p",{className:"text-xs text-text-muted",children:d("loading")}):m.length>0?(0,t.jsx)("div",{className:"flex flex-col gap-2",children:m.map(e=>{let r=`${s}/${e.id}`,a=`custom-${e.id}`;return(0,t.jsxs)("div",{className:"flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-sidebar/50",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-base text-primary",children:"tune"}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("p",{className:"text-sm font-medium truncate",children:e.name||e.id}),(0,t.jsxs)("div",{className:"flex items-center gap-1 mt-1 flex-wrap",children:[(0,t.jsx)("code",{className:"text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded",children:r}),(0,t.jsx)("button",{onClick:()=>i(r,a),className:"p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary",title:d("copyModel"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:l===a?"check":"content_copy"})}),"responses"===e.apiFormat&&(0,t.jsx)("span",{className:"text-[10px] px-1.5 py-0.5 rounded-full bg-blue-500/15 text-blue-400 font-medium",children:"Responses"}),e.supportedEndpoints?.includes("embeddings")&&(0,t.jsx)("span",{className:"text-[10px] px-1.5 py-0.5 rounded-full bg-purple-500/15 text-purple-400 font-medium",children:"📐 Embed"}),e.supportedEndpoints?.includes("images")&&(0,t.jsx)("span",{className:"text-[10px] px-1.5 py-0.5 rounded-full bg-amber-500/15 text-amber-400 font-medium",children:"🖼️ Images"}),e.supportedEndpoints?.includes("audio")&&(0,t.jsx)("span",{className:"text-[10px] px-1.5 py-0.5 rounded-full bg-green-500/15 text-green-400 font-medium",children:"🔊 Audio"}),e.normalizeToolCallId&&(0,t.jsx)("span",{className:"text-[10px] px-1.5 py-0.5 rounded-full bg-slate-500/15 text-slate-400 font-medium",title:d("normalizeToolCallIdLabel"),children:"ID×9"}),!1===e.preserveOpenAIDeveloperRole&&(0,t.jsx)("span",{className:"text-[10px] px-1.5 py-0.5 rounded-full bg-cyan-500/15 text-cyan-400 font-medium",title:d("compatDoNotPreserveDeveloper"),children:d("compatBadgeNoPreserve")})]}),S===e.id&&(0,t.jsxs)("div",{className:"mt-3 p-3 rounded-lg border border-border bg-sidebar/40",children:[(0,t.jsxs)("div",{className:"flex items-end gap-3 flex-wrap",children:[(0,t.jsxs)("div",{className:"w-44",children:[(0,t.jsx)("label",{className:"text-xs text-text-muted mb-1 block",children:"API Format"}),(0,t.jsxs)("select",{value:R,onChange:e=>A(e.target.value),className:"w-full px-2.5 py-2 text-xs border border-border rounded-lg bg-background focus:outline-none focus:border-primary",children:[(0,t.jsx)("option",{value:"chat-completions",children:"Chat Completions"}),(0,t.jsx)("option",{value:"responses",children:"Responses API"})]})]}),(0,t.jsxs)("div",{className:"flex-1 min-w-[240px]",children:[(0,t.jsx)("span",{className:"text-xs text-text-muted mb-1 block",children:"Supported Endpoints"}),(0,t.jsx)("div",{className:"flex items-center gap-3 flex-wrap",children:["chat","embeddings","images","audio"].map(e=>(0,t.jsxs)("label",{className:"flex items-center gap-1.5 text-xs text-text-main cursor-pointer",children:[(0,t.jsx)("input",{type:"checkbox",checked:I.includes(e),onChange:t=>{t.target.checked?E(t=>t.includes(e)?t:[...t,e]):E(t=>t.filter(t=>t!==e))},className:"rounded border-border"}),"chat"===e?"💬 Chat":"embeddings"===e?"📐 Embeddings":"images"===e?"🖼️ Images":"🔊 Audio"]},e))})]})]}),(0,t.jsx)("div",{className:"mt-3 pt-3 border-t border-border/80 w-full",children:(0,t.jsx)(w,{t:d,normalizeToolCallId:O,preserveDeveloperRole:M,showDeveloperToggle:!0,onNormalizeChange:D,onPreserveChange:U,disabled:$===e.id})}),(0,t.jsxs)("div",{className:"mt-3 flex items-center gap-2",children:[(0,t.jsx)(c.Button,{size:"sm",onClick:()=>q(e.id),disabled:$===e.id,children:$===e.id?d("saving"):d("save")}),(0,t.jsx)(c.Button,{size:"sm",variant:"ghost",onClick:K,children:d("cancel")})]})]})]}),(0,t.jsxs)("div",{className:"flex items-center gap-1",children:[(0,t.jsx)("button",{onClick:()=>{P(e.id),A(e.apiFormat||"chat-completions"),E(Array.isArray(e.supportedEndpoints)&&e.supportedEndpoints.length?e.supportedEndpoints:["chat"]),D(!!e.normalizeToolCallId),U(!Object.prototype.hasOwnProperty.call(e,"preserveOpenAIDeveloperRole")||!!e.preserveOpenAIDeveloperRole)},className:"p-1 hover:bg-sidebar rounded text-text-muted hover:text-primary",title:d("edit"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:"edit"})}),(0,t.jsx)("button",{onClick:()=>z(e.id),className:"p-1 hover:bg-red-50 rounded text-red-500",title:d("removeCustomModel"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:"delete"})})]})]},e.id)})}):(0,t.jsx)("p",{className:"text-xs text-text-muted",children:d("noCustomModels")})]})}function I({providerStorageAlias:e,providerDisplayAlias:s,modelAliases:l,copied:i,onCopy:o,onSetAlias:n,onDeleteAlias:d,connections:p,isAnthropic:m,onImportWithProgress:u,t:x,effectiveModelNormalize:h,effectiveModelPreserveDeveloper:f,saveModelCompatFlags:g,compatSavingModelId:b,onModelsChanged:v}){let[y,j]=(0,r.useState)(""),[N,C]=(0,r.useState)(!1),[k,w]=(0,r.useState)(!1),T=(0,a.useNotificationStore)(),S=Object.entries(l).filter(([,t])=>t.startsWith(`${e}/`)).map(([t,r])=>({modelId:r.replace(`${e}/`,""),fullModel:r,alias:t})),P=e=>{let t,r=(t=e.split("/"))[t.length-1];if(!l[r])return r;let a=`${s}-${r}`;return l[a]?null:a},A=async()=>{if(!y.trim()||N)return;let t=y.trim(),r=P(t);if(!r)return void T.error(x("allSuggestedAliasesExist"));C(!0);try{let a=await fetch("/api/provider-models",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:e,modelId:t,modelName:t,source:"manual"})});if(!a.ok){let e={};try{e=await a.json()}catch(e){console.error("Failed to parse error response from custom model API:",e)}throw Error(e.error?.message||x("failedSaveCustomModel"))}await n(t,r,e),j(""),T.success(x("modelAddedSuccess",{modelId:t})),v?.()}catch(e){console.error("Error adding model:",e),T.error(e instanceof Error?e.message:x("failedAddModelTryAgain"))}finally{C(!1)}},I=async()=>{if(k)return;let t=p.find(e=>!1!==e.isActive);if(t){w(!0);try{await u(async()=>{let e=await fetch(`/api/providers/${t.id}/models`),r=await e.json();if(!e.ok)throw Error(r.error||x("failedImportModels"));return r},async t=>{let r=t.id||t.name||t.model;if(!r)return!1;let a=P(r);return!!a&&((await fetch("/api/provider-models",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:e,modelId:r,modelName:t.name||r,source:"imported"})})).ok?(await n(r,a,e),!0):(T.error(x("failedSaveImportedModel")),!1))})}catch(e){console.error("Error importing models:",e),T.error(x("failedImportModelsTryAgain"))}finally{w(!1)}}},E=p.some(e=>!1!==e.isActive),O=async(t,r)=>{try{if(!(await fetch(`/api/provider-models?provider=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}`,{method:"DELETE"})).ok)throw Error(x("failedRemoveModelFromDatabase"));await d(r),T.success(x("modelRemovedSuccess")),v?.()}catch(e){console.error("Error deleting model:",e),T.error(e instanceof Error?e.message:x("failedDeleteModelTryAgain"))}};return(0,t.jsxs)("div",{className:"flex flex-col gap-4",children:[(0,t.jsx)("p",{className:"text-sm text-text-muted",children:x("compatibleModelsDescription",{type:m?x("anthropic"):x("openai")})}),(0,t.jsxs)("div",{className:"flex items-end gap-2 flex-wrap",children:[(0,t.jsxs)("div",{className:"flex-1 min-w-[240px]",children:[(0,t.jsx)("label",{htmlFor:"new-compatible-model-input",className:"text-xs text-text-muted mb-1 block",children:x("modelId")}),(0,t.jsx)("input",{id:"new-compatible-model-input",type:"text",value:y,onChange:e=>j(e.target.value),onKeyDown:e=>"Enter"===e.key&&A(),placeholder:m?x("anthropicCompatibleModelPlaceholder"):x("openaiCompatibleModelPlaceholder"),className:"w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"})]}),(0,t.jsx)(c.Button,{size:"sm",icon:"add",onClick:A,disabled:!y.trim()||N,children:N?x("adding"):x("add")}),(0,t.jsx)(c.Button,{size:"sm",variant:"secondary",icon:"download",onClick:I,disabled:!E||k,children:k?x("importingModels"):x("importFromModels")})]}),!E&&(0,t.jsx)("p",{className:"text-xs text-text-muted",children:x("addConnectionToImport")}),S.length>0&&(0,t.jsx)("div",{className:"flex flex-col gap-3",children:S.map(({modelId:e,fullModel:r,alias:a})=>(0,t.jsx)(R,{modelId:e,fullModel:`${s}/${e}`,copied:i,onCopy:o,onDeleteAlias:()=>O(e,a),t:x,showDeveloperToggle:!m,normalizeToolCallId:h(e),preserveDeveloperRole:f(e),onNormalizeChange:t=>g(e,{normalizeToolCallId:t}),onPreserveChange:t=>g(e,{preserveOpenAIDeveloperRole:t}),compatDisabled:b===e},r))})]})}function E({until:e}){let[a,s]=(0,r.useState)("");return((0,r.useEffect)(()=>{let t=()=>{let t=new Date(e).getTime()-Date.now();if(t<=0)return void s("");let r=Math.floor(t/1e3);if(r<60)s(`${r}s`);else if(r<3600)s(`${Math.floor(r/60)}m ${r%60}s`);else{let e=Math.floor(r/3600),t=Math.floor(r%3600/60);s(`${e}h ${t}m`)}};t();let r=setInterval(t,1e3);return()=>clearInterval(r)},[e]),a)?(0,t.jsxs)("span",{className:"text-xs text-orange-500 font-mono",children:["⏱ ",a]}):null}S.propTypes={model:s.default.shape({id:s.default.string.isRequired}).isRequired,fullModel:s.default.string.isRequired,alias:s.default.string,copied:s.default.string,onCopy:s.default.func.isRequired,t:s.default.func,showDeveloperToggle:s.default.bool,normalizeToolCallId:s.default.bool,preserveDeveloperRole:s.default.bool,onNormalizeChange:s.default.func,onPreserveChange:s.default.func,compatDisabled:s.default.bool},P.propTypes={providerAlias:s.default.string.isRequired,modelAliases:s.default.object.isRequired,copied:s.default.string,onCopy:s.default.func.isRequired,onSetAlias:s.default.func.isRequired,onDeleteAlias:s.default.func.isRequired,t:s.default.func.isRequired,effectiveModelNormalize:s.default.func.isRequired,effectiveModelPreserveDeveloper:s.default.func.isRequired,saveModelCompatFlags:s.default.func.isRequired,compatSavingModelId:s.default.string},R.propTypes={modelId:s.default.string.isRequired,fullModel:s.default.string.isRequired,copied:s.default.string,onCopy:s.default.func.isRequired,onDeleteAlias:s.default.func.isRequired,t:s.default.func,showDeveloperToggle:s.default.bool,normalizeToolCallId:s.default.bool,preserveDeveloperRole:s.default.bool,onNormalizeChange:s.default.func,onPreserveChange:s.default.func,compatDisabled:s.default.bool},A.propTypes={providerId:s.default.string.isRequired,providerAlias:s.default.string.isRequired,copied:s.default.string,onCopy:s.default.func.isRequired,onModelsChanged:s.default.func},I.propTypes={providerStorageAlias:s.default.string.isRequired,providerDisplayAlias:s.default.string.isRequired,modelAliases:s.default.object.isRequired,copied:s.default.string,onCopy:s.default.func.isRequired,onSetAlias:s.default.func.isRequired,onDeleteAlias:s.default.func.isRequired,connections:s.default.arrayOf(s.default.shape({id:s.default.string,isActive:s.default.bool})).isRequired,isAnthropic:s.default.bool,onImportWithProgress:s.default.func.isRequired,t:s.default.func.isRequired,effectiveModelNormalize:s.default.func.isRequired,effectiveModelPreserveDeveloper:s.default.func.isRequired,saveModelCompatFlags:s.default.func.isRequired,compatSavingModelId:s.default.string,onModelsChanged:s.default.func},E.propTypes={until:s.default.string.isRequired};let O={runtime_error:{labelKey:"errorTypeRuntime",variant:"warning"},upstream_auth_error:{labelKey:"errorTypeUpstreamAuth",variant:"error"},auth_missing:{labelKey:"errorTypeMissingCredential",variant:"warning"},token_refresh_failed:{labelKey:"errorTypeRefreshFailed",variant:"warning"},token_expired:{labelKey:"errorTypeTokenExpired",variant:"warning"},upstream_rate_limited:{labelKey:"errorTypeRateLimited",variant:"warning"},upstream_unavailable:{labelKey:"errorTypeUpstreamUnavailable",variant:"error"},network_error:{labelKey:"errorTypeNetworkError",variant:"warning"},unsupported:{labelKey:"errorTypeTestUnsupported",variant:"default"},upstream_error:{labelKey:"errorTypeUpstreamError",variant:"error"}};function D({connection:e,isOAuth:a,isCodex:s,isFirst:l,isLast:i,onMoveUp:o,onMoveDown:d,onToggleActive:m,onToggleRateLimit:u,onToggleCodex5h:x,onToggleCodexWeekly:h,onRetest:f,isRetesting:g,onEdit:v,onDelete:y,onReauth:j,onProxy:N,hasProxy:C,proxySource:w,proxyHost:T,onRefreshToken:S,isRefreshing:P}){let R,A=(0,n.useTranslations)("providers"),I=a?e.name||e.email||e.displayName||A("oauthAccount"):e.name,[D,M]=(0,r.useState)(!1),[U,$]=(0,r.useState)(()=>a&&e.expiresAt?Math.floor((new Date(e.expiresAt).getTime()-Date.now())/6e4):null);(0,r.useEffect)(()=>{if(!a||!e.expiresAt)return;let t=setInterval(()=>{$(Math.floor((new Date(e.expiresAt).getTime()-Date.now())/6e4))},3e4);return()=>clearInterval(t)},[a,e.expiresAt]),(0,r.useEffect)(()=>{let t=()=>{M(e.rateLimitedUntil&&new Date(e.rateLimitedUntil).getTime()>Date.now())};t();let r=e.rateLimitedUntil?setInterval(t,1e3):null;return()=>{r&&clearInterval(r)}},[e.rateLimitedUntil]);let L="unavailable"!==e.testStatus||D?e.testStatus:"active",_=function(e,t,r,a){if(!1===e.isActive)return{statusVariant:"default",statusLabel:a("statusDisabled"),errorType:null,errorBadge:null,errorTextClass:"text-text-muted"};if("active"===t||"success"===t)return{statusVariant:"success",statusLabel:a("statusConnected"),errorType:null,errorBadge:null,errorTextClass:"text-text-muted"};let s=function(e,t){if(t)return"upstream_rate_limited";if(e.lastErrorType)return e.lastErrorType;let r=Number(e.errorCode);if(401===r||403===r)return"upstream_auth_error";if(429===r)return"upstream_rate_limited";if(r>=500)return"upstream_unavailable";let a=(e.lastError||"").toLowerCase();return a?a.includes("runtime")||a.includes("not runnable")||a.includes("not installed")||a.includes("healthcheck")?"runtime_error":a.includes("refresh failed")?"token_refresh_failed":a.includes("token expired")||a.includes("expired")?"token_expired":a.includes("invalid api key")||a.includes("token invalid")||a.includes("revoked")||a.includes("access denied")||a.includes("unauthorized")?"upstream_auth_error":a.includes("rate limit")||a.includes("quota")||a.includes("too many requests")||a.includes("429")?"upstream_rate_limited":a.includes("fetch failed")||a.includes("network")||a.includes("timeout")||a.includes("econn")||a.includes("enotfound")?"network_error":a.includes("not supported")?"unsupported":"upstream_error":null}(e,r),l=s&&O[s]||null;return"runtime_error"===s?{statusVariant:"warning",statusLabel:a("statusRuntimeIssue"),errorType:s,errorBadge:l,errorTextClass:"text-yellow-600 dark:text-yellow-400"}:"upstream_auth_error"===s||"auth_missing"===s||"token_refresh_failed"===s||"token_expired"===s?{statusVariant:"error",statusLabel:a("statusAuthFailed"),errorType:s,errorBadge:l,errorTextClass:"text-red-500"}:"upstream_rate_limited"===s?{statusVariant:"warning",statusLabel:a("statusRateLimited"),errorType:s,errorBadge:l,errorTextClass:"text-yellow-600 dark:text-yellow-400"}:"network_error"===s?{statusVariant:"warning",statusLabel:a("statusNetworkIssue"),errorType:s,errorBadge:l,errorTextClass:"text-yellow-600 dark:text-yellow-400"}:"unsupported"===s?{statusVariant:"default",statusLabel:a("statusTestUnsupported"),errorType:s,errorBadge:l,errorTextClass:"text-text-muted"}:{statusVariant:"error",statusLabel:({unavailable:a("statusUnavailable"),failed:a("statusFailed"),error:a("statusError")})[t]||t||a("statusError"),errorType:s,errorBadge:l,errorTextClass:"text-red-500"}}(e,L,D,A),F=!!e.rateLimitProtection,z=k(e.providerSpecificData&&"object"==typeof e.providerSpecificData&&e.providerSpecificData.codexLimitPolicy&&"object"==typeof e.providerSpecificData.codexLimitPolicy?e.providerSpecificData.codexLimitPolicy:{}),K=z.use5h,q=z.useWeekly;return(0,t.jsxs)("div",{className:`group flex items-center justify-between p-3 rounded-lg hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${!1===e.isActive?"opacity-60":""}`,children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 flex-1 min-w-0",children:[(0,t.jsxs)("div",{className:"flex flex-col",children:[(0,t.jsx)("button",{onClick:o,disabled:l,className:`p-0.5 rounded ${l?"text-text-muted/30 cursor-not-allowed":"hover:bg-sidebar text-text-muted hover:text-primary"}`,children:(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:"keyboard_arrow_up"})}),(0,t.jsx)("button",{onClick:d,disabled:i,className:`p-0.5 rounded ${i?"text-text-muted/30 cursor-not-allowed":"hover:bg-sidebar text-text-muted hover:text-primary"}`,children:(0,t.jsx)("span",{className:"material-symbols-outlined text-sm",children:"keyboard_arrow_down"})})]}),(0,t.jsx)("span",{className:"material-symbols-outlined text-base text-text-muted",children:a?"lock":"key"}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("p",{className:"text-sm font-medium truncate",children:I}),(0,t.jsxs)("div",{className:"flex items-center gap-2 mt-1 flex-wrap",children:[(0,t.jsx)(p.Badge,{variant:_.statusVariant,size:"sm",dot:!0,children:_.statusLabel}),null!==U&&(U<0?(0,t.jsxs)("span",{className:"inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-xs font-medium bg-red-500/15 text-red-500",title:`Token expired: ${e.expiresAt}`,children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-[11px]",children:"error"}),"expired"]}):U<30?(0,t.jsxs)("span",{className:"inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-xs font-medium bg-amber-500/15 text-amber-500",title:`Token expires in ${U}m`,children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-[11px]",children:"warning"}),`~${U}m`]}):null),D&&!1!==e.isActive&&(0,t.jsx)(E,{until:e.rateLimitedUntil}),_.errorBadge&&!1!==e.isActive&&(0,t.jsx)(p.Badge,{variant:_.errorBadge.variant,size:"sm",children:A(_.errorBadge.labelKey)}),e.lastError&&!1!==e.isActive&&(0,t.jsx)("span",{className:`text-xs truncate max-w-[300px] ${_.errorTextClass}`,title:e.lastError,children:e.lastError}),(0,t.jsxs)("span",{className:"text-xs text-text-muted",children:["#",e.priority]}),e.globalPriority&&(0,t.jsx)("span",{className:"text-xs text-text-muted",children:A("autoPriority",{priority:e.globalPriority})}),(0,t.jsx)("span",{className:"text-text-muted/30 select-none",children:"|"}),(0,t.jsxs)("button",{onClick:()=>u(!F),className:`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${F?"bg-emerald-500/15 text-emerald-500 hover:bg-emerald-500/25":"bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"}`,title:A(F?"disableRateLimitProtection":"enableRateLimitProtection"),children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-[13px]",children:"shield"}),A(F?"rateLimitProtected":"rateLimitUnprotected")]}),s&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"text-text-muted/30 select-none",children:"|"}),(0,t.jsxs)("button",{onClick:()=>x?.(!K),className:`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${K?"bg-blue-500/15 text-blue-500 hover:bg-blue-500/25":"bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"}`,title:"Toggle Codex 5h limit policy",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-[13px]",children:"timer"}),"5h ",K?"ON":"OFF"]}),(0,t.jsxs)("button",{onClick:()=>h?.(!q),className:`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${q?"bg-violet-500/15 text-violet-500 hover:bg-violet-500/25":"bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"}`,title:"Toggle Codex weekly limit policy",children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-[13px]",children:"date_range"}),"Weekly ",q?"ON":"OFF"]})]}),C&&(R=A("global"===w?"proxySourceGlobal":"provider"===w?"proxySourceProvider":"proxySourceKey"),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"text-text-muted/30 select-none",children:"|"}),(0,t.jsxs)("span",{className:`inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-xs font-medium ${"global"===w?"bg-emerald-500/15 text-emerald-500":"provider"===w?"bg-amber-500/15 text-amber-500":"bg-blue-500/15 text-blue-500"}`,title:A("proxyConfiguredBySource",{source:R,host:T||A("configured")}),children:[(0,t.jsx)("span",{className:"material-symbols-outlined text-[13px]",children:"vpn_lock"}),T||A("proxy")]})]}))]})]})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(c.Button,{size:"sm",variant:"ghost",icon:"refresh",loading:g,disabled:!1===e.isActive,onClick:f,className:"!h-7 !px-2 text-xs",title:A("retestAuthentication"),children:A("retest")}),S&&(0,t.jsx)(c.Button,{size:"sm",variant:"ghost",icon:"token",loading:P,disabled:!1===e.isActive||P,onClick:S,className:"!h-7 !px-2 text-xs text-amber-500 hover:text-amber-400",title:"Refresh OAuth token manually",children:"Token"}),(0,t.jsx)(b.Toggle,{size:"sm",checked:e.isActive??!0,onChange:m,title:A(e.isActive??!0?"disableConnection":"enableConnection")}),(0,t.jsxs)("div",{className:"flex gap-1 ml-1 transition-opacity",children:[j&&(0,t.jsx)("button",{onClick:j,className:"p-2 hover:bg-amber-500/10 rounded text-amber-600 hover:text-amber-500",title:A("reauthenticateConnection"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-[18px]",children:"passkey"})}),(0,t.jsx)("button",{onClick:v,className:"p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary",title:A("edit"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-[18px]",children:"edit"})}),(0,t.jsx)("button",{onClick:N,className:"p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary",title:A("proxyConfig"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-[18px]",children:"vpn_lock"})}),(0,t.jsx)("button",{onClick:y,className:"p-2 hover:bg-red-500/10 rounded text-red-500",title:A("delete"),children:(0,t.jsx)("span",{className:"material-symbols-outlined text-[18px]",children:"delete"})})]})]})]})}function M({isOpen:e,provider:a,providerName:s,isCompatible:l,isAnthropic:i,onSave:o,onClose:d}){let x=(0,n.useTranslations)("providers"),h="bailian-coding-plan"===a,f="https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1",[g,b]=(0,r.useState)({name:"",apiKey:"",priority:1,baseUrl:h?f:""}),[v,y]=(0,r.useState)(!1),[j,N]=(0,r.useState)(null),[C,k]=(0,r.useState)(!1),[w,T]=(0,r.useState)(null),S=async()=>{y(!0),T(null);try{let e=await fetch("/api/providers/validate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:a,apiKey:g.apiKey})}),t=await e.json();N(t.valid?"success":"failed")}catch{N("failed")}finally{y(!1)}},P=async()=>{if(a&&g.apiKey){k(!0),T(null);try{let e=null;if(h){let t=U(g.baseUrl,f);if(t.error)return void T(t.error);e=t.value}let t=!1;try{y(!0),N(null);let e=await fetch("/api/providers/validate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:a,apiKey:g.apiKey})});t=!!(await e.json()).valid,N(t?"success":"failed")}catch{N("failed")}finally{y(!1)}if(!t)return void T(x("apiKeyValidationFailed"));let r={name:g.name,apiKey:g.apiKey,priority:g.priority,testStatus:"active",providerSpecificData:void 0};h&&(r.providerSpecificData={baseUrl:e});let s=await o(r);s&&T("string"==typeof s?s:x("failedSaveConnection"))}finally{k(!1)}}};return a?(0,t.jsx)(u.Modal,{isOpen:e,title:x("addProviderApiKeyTitle",{provider:s||a}),onClose:d,children:(0,t.jsxs)("div",{className:"flex flex-col gap-4",children:[(0,t.jsx)(m.Input,{label:x("nameLabel"),value:g.name,onChange:e=>b({...g,name:e.target.value}),placeholder:x("productionKey")}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)(m.Input,{label:x("apiKeyLabel"),type:"password",value:g.apiKey,onChange:e=>b({...g,apiKey:e.target.value}),className:"flex-1"}),(0,t.jsx)("div",{className:"pt-6",children:(0,t.jsx)(c.Button,{onClick:S,disabled:!g.apiKey||v||C,variant:"secondary",children:v?x("checking"):x("check")})})]}),j&&(0,t.jsx)(p.Badge,{variant:"success"===j?"success":"error",children:"success"===j?x("valid"):x("invalid")}),w&&(0,t.jsx)("div",{className:"text-sm text-red-500 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2",children:w}),l&&(0,t.jsx)("p",{className:"text-xs text-text-muted",children:i?x("validationChecksAnthropicCompatible",{provider:s||x("anthropicCompatibleName")}):x("validationChecksOpenAiCompatible",{provider:s||x("openaiCompatibleName")})}),(0,t.jsx)(m.Input,{label:x("priorityLabel"),type:"number",value:g.priority,onChange:e=>b({...g,priority:Number.parseInt(e.target.value)||1})}),h&&(0,t.jsx)(m.Input,{label:"Base URL",value:g.baseUrl,onChange:e=>b({...g,baseUrl:e.target.value}),placeholder:f,hint:"Optional: Custom base URL for bailian-coding-plan provider"}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)(c.Button,{onClick:P,fullWidth:!0,disabled:!g.name||!g.apiKey||C,children:C?x("saving"):x("save")}),(0,t.jsx)(c.Button,{onClick:d,variant:"ghost",fullWidth:!0,children:x("cancel")})]})]})}):null}function U(e,t){let r=("string"==typeof e?e.trim():"")||t;try{let e=new URL(r);if("http:"!==e.protocol&&"https:"!==e.protocol)return{value:null,error:"Base URL must use http or https"};return{value:r,error:null}}catch{return{value:null,error:"Base URL must be a valid URL"}}}function $({isOpen:e,connection:a,onSave:s,onClose:l}){let i=(0,n.useTranslations)("providers"),[o,d]=(0,r.useState)({name:"",priority:1,apiKey:"",healthCheckInterval:60,baseUrl:""}),[x,h]=(0,r.useState)(!1),[f,g]=(0,r.useState)(null),[b,v]=(0,r.useState)(!1),[y,N]=(0,r.useState)(null),[C,k]=(0,r.useState)(!1),[w,T]=(0,r.useState)(null),[S,P]=(0,r.useState)([]),[R,A]=(0,r.useState)(""),I=a?.provider==="bailian-coding-plan",E="https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1";(0,r.useEffect)(()=>{if(a){let e=a.providerSpecificData?.baseUrl;d({name:a.name||"",priority:a.priority||1,apiKey:"",healthCheckInterval:a.healthCheckInterval??60,baseUrl:e||(I?E:"")});let t=a.providerSpecificData?.extraApiKeys;P(Array.isArray(t)?t:[]),A(""),g(null),N(null),T(null)}},[a,I]);let D=async()=>{if(a?.provider){h(!0),g(null);try{let e=await fetch(`/api/providers/${a.id}/test`,{method:"POST"}),t=await e.json();g({valid:!!t.valid,diagnosis:t.diagnosis||null,message:t.error||null})}catch{g({valid:!1,diagnosis:{type:"network_error"},message:i("failedTestConnection")})}finally{h(!1)}}},M=async()=>{if(a?.provider&&o.apiKey){v(!0),N(null);try{let e=await fetch("/api/providers/validate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:a.provider,apiKey:o.apiKey})}),t=await e.json();N(t.valid?"success":"failed")}catch{N("failed")}finally{v(!1)}}},$=async()=>{k(!0),T(null);try{let e={name:o.name,priority:o.priority,healthCheckInterval:o.healthCheckInterval},t=null;if(I){let e=U(o.baseUrl,E);if(e.error)return void T(e.error);t=e.value}if(!L&&o.apiKey){e.apiKey=o.apiKey;let t="success"===y;if(!t)try{v(!0),N(null);let e=await fetch("/api/providers/validate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:a.provider,apiKey:o.apiKey})});t=!!(await e.json()).valid,N(t?"success":"failed")}catch{N("failed")}finally{v(!1)}t&&(e.testStatus="active",e.lastError=null,e.lastErrorAt=null,e.lastErrorType=null,e.lastErrorSource=null,e.errorCode=null,e.rateLimitedUntil=null)}!L&&(e.providerSpecificData={...a.providerSpecificData||{},extraApiKeys:S.filter(e=>e.trim().length>0)},I&&(e.providerSpecificData.baseUrl=t));let r=await s(e);r&&T("string"==typeof r?r:i("failedSaveConnection"))}finally{k(!1)}};if(!a)return null;let L="oauth"===a.authType,_=(0,j.isOpenAICompatibleProvider)(a.provider)||(0,j.isAnthropicCompatibleProvider)(a.provider),F=!f?.valid&&f?.diagnosis?.type&&O[f.diagnosis.type]||null;return(0,t.jsx)(u.Modal,{isOpen:e,title:i("editConnection"),onClose:l,children:(0,t.jsxs)("div",{className:"flex flex-col gap-4",children:[(0,t.jsx)(m.Input,{label:i("nameLabel"),value:o.name,onChange:e=>d({...o,name:e.target.value}),placeholder:L?i("accountName"):i("productionKey")}),L&&a.email&&(0,t.jsxs)("div",{className:"bg-sidebar/50 p-3 rounded-lg",children:[(0,t.jsx)("p",{className:"text-sm text-text-muted mb-1",children:i("email")}),(0,t.jsx)("p",{className:"font-medium",children:a.email})]}),L&&(0,t.jsx)(m.Input,{label:i("healthCheckMinutes"),type:"number",value:o.healthCheckInterval,onChange:e=>d({...o,healthCheckInterval:Math.max(0,Number.parseInt(e.target.value)||0)}),hint:i("healthCheckHint")}),(0,t.jsx)(m.Input,{label:i("priorityLabel"),type:"number",value:o.priority,onChange:e=>d({...o,priority:Number.parseInt(e.target.value)||1})}),!L&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)(m.Input,{label:i("apiKeyLabel"),type:"password",value:o.apiKey,onChange:e=>d({...o,apiKey:e.target.value}),placeholder:i("enterNewApiKey"),hint:i("leaveBlankKeepCurrentApiKey"),className:"flex-1"}),(0,t.jsx)("div",{className:"pt-6",children:(0,t.jsx)(c.Button,{onClick:M,disabled:!o.apiKey||b||C,variant:"secondary",children:b?i("checking"):i("check")})})]}),y&&(0,t.jsx)(p.Badge,{variant:"success"===y?"success":"error",children:"success"===y?i("valid"):i("invalid")}),w&&(0,t.jsx)("div",{className:"text-sm text-red-500 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2",children:w})]}),I&&(0,t.jsx)(m.Input,{label:"Base URL",value:o.baseUrl,onChange:e=>d({...o,baseUrl:e.target.value}),placeholder:E,hint:"Custom base URL for bailian-coding-plan provider"}),!L&&(0,t.jsxs)("div",{className:"flex flex-col gap-2",children:[(0,t.jsxs)("label",{className:"text-sm font-medium text-text-main",children:["Extra API Keys",(0,t.jsx)("span",{className:"ml-2 text-[11px] font-normal text-text-muted",children:"(round-robin rotation — optional)"})]}),S.length>0&&(0,t.jsx)("div",{className:"flex flex-col gap-1.5",children:S.map((e,r)=>(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"flex-1 font-mono text-xs bg-sidebar/50 px-3 py-2 rounded border border-border text-text-muted truncate",children:`Key #${r+2}: ${e.slice(0,6)}...${e.slice(-4)}`}),(0,t.jsx)("button",{onClick:()=>P(S.filter((e,t)=>t!==r)),className:"p-1.5 rounded hover:bg-red-500/10 text-red-400 hover:text-red-500",title:"Remove this key",children:(0,t.jsx)("span",{className:"material-symbols-outlined text-[16px]",children:"close"})})]},r))}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)("input",{type:"password",value:R,onChange:e=>A(e.target.value),placeholder:"Add another API key...",className:"flex-1 text-sm bg-sidebar/50 border border-border rounded px-3 py-2 text-text-main placeholder:text-text-muted focus:ring-1 focus:ring-primary outline-none",onKeyDown:e=>{"Enter"===e.key&&R.trim()&&(P([...S,R.trim()]),A(""))}}),(0,t.jsx)("button",{onClick:()=>{R.trim()&&(P([...S,R.trim()]),A(""))},disabled:!R.trim(),className:"px-3 py-2 rounded bg-primary/10 text-primary hover:bg-primary/20 disabled:opacity-40 text-sm font-medium",children:"Add"})]}),S.length>0&&(0,t.jsxs)("p",{className:"text-[11px] text-text-muted",children:[S.length+1," keys total — rotating round-robin on each request."]})]}),!_&&(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)(c.Button,{onClick:D,variant:"secondary",disabled:x,children:x?i("testing"):i("testConnection")}),f&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(p.Badge,{variant:f.valid?"success":"error",children:f.valid?i("valid"):i("failed")}),F&&(0,t.jsx)(p.Badge,{variant:F.variant,children:i(F.labelKey)})]})]}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)(c.Button,{onClick:$,fullWidth:!0,disabled:C,children:C?i("saving"):i("save")}),(0,t.jsx)(c.Button,{onClick:l,variant:"ghost",fullWidth:!0,children:i("cancel")})]})]})})}function L({isOpen:e,node:a,onSave:s,onClose:l,isAnthropic:i}){let o=(0,n.useTranslations)("providers"),[d,x]=(0,r.useState)({name:"",prefix:"",apiType:"chat",baseUrl:"https://api.openai.com/v1",chatPath:"",modelsPath:""}),[h,f]=(0,r.useState)(!1),[g,b]=(0,r.useState)(""),[y,j]=(0,r.useState)(!1),[N,C]=(0,r.useState)(null),[k,w]=(0,r.useState)(!1);(0,r.useEffect)(()=>{a&&(x({name:a.name||"",prefix:a.prefix||"",apiType:a.apiType||"chat",baseUrl:a.baseUrl||(i?"https://api.anthropic.com/v1":"https://api.openai.com/v1"),chatPath:a.chatPath||"",modelsPath:a.modelsPath||""}),w(!!(a.chatPath||a.modelsPath)))},[a,i]);let T=[{value:"chat",label:o("chatCompletions")},{value:"responses",label:o("responsesApi")}],S=async()=>{if(d.name.trim()&&d.prefix.trim()&&d.baseUrl.trim()){f(!0);try{let e={name:d.name,prefix:d.prefix,baseUrl:d.baseUrl,chatPath:d.chatPath||"",modelsPath:d.modelsPath||""};i||(e.apiType=d.apiType),await s(e)}finally{f(!1)}}},P=async()=>{j(!0);try{let e=await fetch("/api/provider-nodes/validate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({baseUrl:d.baseUrl,apiKey:g,type:i?"anthropic-compatible":"openai-compatible",modelsPath:d.modelsPath||""})}),t=await e.json();C(t.valid?"success":"failed")}catch{C("failed")}finally{j(!1)}};return a?(0,t.jsx)(u.Modal,{isOpen:e,title:o("editCompatibleTitle",{type:o(i?"anthropic":"openai")}),onClose:l,children:(0,t.jsxs)("div",{className:"flex flex-col gap-4",children:[(0,t.jsx)(m.Input,{label:o("nameLabel"),value:d.name,onChange:e=>x({...d,name:e.target.value}),placeholder:o("compatibleProdPlaceholder",{type:o(i?"anthropic":"openai")}),hint:o("nameHint")}),(0,t.jsx)(m.Input,{label:o("prefixLabel"),value:d.prefix,onChange:e=>x({...d,prefix:e.target.value}),placeholder:o(i?"anthropicPrefixPlaceholder":"openaiPrefixPlaceholder"),hint:o("prefixHint")}),!i&&(0,t.jsx)(v.Select,{label:o("apiTypeLabel"),options:T,value:d.apiType,onChange:e=>x({...d,apiType:e.target.value})}),(0,t.jsx)(m.Input,{label:o("baseUrlLabel"),value:d.baseUrl,onChange:e=>x({...d,baseUrl:e.target.value}),placeholder:o(i?"anthropicBaseUrlPlaceholder":"openaiBaseUrlPlaceholder"),hint:o("compatibleBaseUrlHint",{type:o(i?"anthropic":"openai")})}),(0,t.jsxs)("button",{type:"button",className:"text-sm text-text-muted hover:text-text-primary flex items-center gap-1",onClick:()=>w(!k),"aria-expanded":k,"aria-controls":"advanced-settings",children:[(0,t.jsx)("span",{className:`transition-transform ${k?"rotate-90":""}`,"aria-hidden":"true",children:"▶"}),o("advancedSettings")]}),k&&(0,t.jsxs)("div",{id:"advanced-settings",className:"flex flex-col gap-3 pl-2 border-l-2 border-border",children:[(0,t.jsx)(m.Input,{label:o("chatPathLabel"),value:d.chatPath,onChange:e=>x({...d,chatPath:e.target.value}),placeholder:i?"/messages":o("chatPathPlaceholder"),hint:o("chatPathHint")}),(0,t.jsx)(m.Input,{label:o("modelsPathLabel"),value:d.modelsPath,onChange:e=>x({...d,modelsPath:e.target.value}),placeholder:o("modelsPathPlaceholder"),hint:o("modelsPathHint")})]}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)(m.Input,{label:o("apiKeyForCheck"),type:"password",value:g,onChange:e=>b(e.target.value),className:"flex-1"}),(0,t.jsx)("div",{className:"pt-6",children:(0,t.jsx)(c.Button,{onClick:P,disabled:!g||y||!d.baseUrl.trim(),variant:"secondary",children:o(y?"checking":"check")})})]}),N&&(0,t.jsx)(p.Badge,{variant:"success"===N?"success":"error",children:o("success"===N?"valid":"invalid")}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)(c.Button,{onClick:S,fullWidth:!0,disabled:!d.name.trim()||!d.prefix.trim()||!d.baseUrl.trim()||h,children:o(h?"saving":"save")}),(0,t.jsx)(c.Button,{onClick:l,variant:"ghost",fullWidth:!0,children:o("cancel")})]})]})}):null}D.propTypes={connection:s.default.shape({id:s.default.string,name:s.default.string,email:s.default.string,displayName:s.default.string,rateLimitedUntil:s.default.string,rateLimitProtection:s.default.bool,testStatus:s.default.string,isActive:s.default.bool,priority:s.default.number,lastError:s.default.string,lastErrorType:s.default.string,lastErrorSource:s.default.string,errorCode:s.default.oneOfType([s.default.string,s.default.number]),globalPriority:s.default.number,providerSpecificData:s.default.object}).isRequired,isOAuth:s.default.bool.isRequired,isCodex:s.default.bool,isFirst:s.default.bool.isRequired,isLast:s.default.bool.isRequired,onMoveUp:s.default.func.isRequired,onMoveDown:s.default.func.isRequired,onToggleActive:s.default.func.isRequired,onToggleRateLimit:s.default.func.isRequired,onToggleCodex5h:s.default.func,onToggleCodexWeekly:s.default.func,onRetest:s.default.func.isRequired,isRetesting:s.default.bool,onEdit:s.default.func.isRequired,onDelete:s.default.func.isRequired,onReauth:s.default.func},M.propTypes={isOpen:s.default.bool.isRequired,provider:s.default.string,providerName:s.default.string,isCompatible:s.default.bool,isAnthropic:s.default.bool,onSave:s.default.func.isRequired,onClose:s.default.func.isRequired},$.propTypes={isOpen:s.default.bool.isRequired,connection:s.default.shape({id:s.default.string,name:s.default.string,email:s.default.string,priority:s.default.number,authType:s.default.string,provider:s.default.string}),onSave:s.default.func.isRequired,onClose:s.default.func.isRequired},L.propTypes={isOpen:s.default.bool.isRequired,node:s.default.shape({id:s.default.string,name:s.default.string,prefix:s.default.string,apiType:s.default.string,baseUrl:s.default.string,chatPath:s.default.string,modelsPath:s.default.string}),onSave:s.default.func.isRequired,onClose:s.default.func.isRequired,isAnthropic:s.default.bool},e.s(["default",()=>T],710864)}]);
package/app/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.8.8] — 2026-03-20
8
+
9
+ > Sprint: Fix OAuth batch test crash, add "Test All" button to individual provider pages.
10
+
11
+ ### Bug Fixes
12
+
13
+ - **OAuth batch test crash** (ERR_CONNECTION_REFUSED): Replaced sequential for-loop with 5-connection concurrency limit + 30s per-connection timeout via `Promise.race()` + `Promise.allSettled()`. Prevents server crash when testing large OAuth provider groups (~30+ connections).
14
+
15
+ ### Features
16
+
17
+ - **"Test All" button on provider pages**: Individual provider pages (e.g., `/providers/codex`) now show a "Test All" button in the Connections header when there are 2+ connections. Uses `POST /api/providers/test-batch` with `{mode: "provider", providerId}`. Results displayed in a modal with pass/fail summary and per-connection diagnosis.
18
+
19
+ ---
20
+
21
+ ## [2.8.7] — 2026-03-20
22
+
23
+ > Sprint: Merge PR #495 (Bottleneck 429 drop), fix #496 (custom embedding providers), triage features.
24
+
25
+ ### Bug Fixes
26
+
27
+ - **Bottleneck 429 infinite wait** (PR #495 by @xandr0s): On 429, `limiter.stop({ dropWaitingJobs: true })` immediately fails all queued requests so upstream callers can trigger fallback. Limiter is deleted from Map so next request creates a fresh instance.
28
+ - **Custom embedding models unresolvable** (#496): `POST /v1/embeddings` now resolves custom embedding models from ALL provider_nodes (not just localhost). Enables models like `google/gemini-embedding-001` added via dashboard.
29
+
30
+ ### Issues Responded
31
+
32
+ - **#452** — Per-API-key request-count limits (acknowledged, on roadmap)
33
+ - **#464** — Auto-issue API keys with provider/account limits (needs more detail)
34
+ - **#488** — Auto-update model lists (acknowledged, on roadmap)
35
+ - **#496** — Custom embedding provider resolution (fixed)
36
+
37
+ ---
38
+
7
39
  ## [2.8.6] — 2026-03-20
8
40
 
9
41
  > Sprint: Merge PR #494 (MiniMax role fix), fix KIRO MITM dashboard, triage 8 issues.
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: OmniRoute API
4
- version: 2.8.6
4
+ version: 2.8.8
5
5
  description: |
6
6
  OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
7
7
  endpoint that routes requests to multiple AI providers with load balancing,
@@ -339,14 +339,19 @@ export function updateFromHeaders(provider, connectionId, headers, status, model
339
339
  // Handle 429 — rate limited
340
340
  if (status === 429) {
341
341
  const retryAfterMs = parseResetTime(retryAfterStr) || 60000; // Default 60s
342
+ const counts = limiter.counts();
343
+ const limiterKey = `${provider}:${connectionId}`;
342
344
  console.log(
343
- `🚫 [RATE-LIMIT] ${provider}:${connectionId.slice(0, 8)} — 429 received, pausing for ${Math.ceil(retryAfterMs / 1000)}s`
345
+ `🚫 [RATE-LIMIT] ${provider}:${connectionId.slice(0, 8)} — 429 received, pausing for ${Math.ceil(retryAfterMs / 1000)}s, dropping ${counts.QUEUED} queued request(s)`
344
346
  );
345
347
 
346
- limiter.updateSettings({
347
- reservoir: 0,
348
- reservoirRefreshAmount: limit || 60,
349
- reservoirRefreshInterval: retryAfterMs,
348
+ // Stop the limiter and drop all waiting jobs so they fail immediately
349
+ // instead of hanging in the queue until reservoir refreshes (which can
350
+ // be hours for providers like Codex with long rate limit windows).
351
+ // This lets upstream callers (e.g. LiteLLM) trigger fallback to other providers.
352
+ // After stop, delete from Map so getLimiter() creates a fresh instance.
353
+ limiter.stop({ dropWaitingJobs: true }).finally(() => {
354
+ limiters.delete(limiterKey);
350
355
  });
351
356
  return;
352
357
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omniroute",
3
- "version": "2.8.6",
3
+ "version": "2.8.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omniroute",
9
- "version": "2.8.6",
9
+ "version": "2.8.8",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "workspaces": [
package/app/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniroute",
3
- "version": "2.8.6",
3
+ "version": "2.8.8",
4
4
  "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -128,6 +128,8 @@ export default function ProviderDetailPage() {
128
128
  const [showEditNodeModal, setShowEditNodeModal] = useState(false);
129
129
  const [selectedConnection, setSelectedConnection] = useState(null);
130
130
  const [retestingId, setRetestingId] = useState(null);
131
+ const [batchTesting, setBatchTesting] = useState(false);
132
+ const [batchTestResults, setBatchTestResults] = useState<any>(null);
131
133
  const [modelAliases, setModelAliases] = useState({});
132
134
  const [headerImgError, setHeaderImgError] = useState(false);
133
135
  const { copied, copy } = useCopyToClipboard();
@@ -506,6 +508,52 @@ export default function ProviderDetailPage() {
506
508
  }
507
509
  };
508
510
 
511
+ // Batch test all connections for this provider
512
+ const handleBatchTestAll = async () => {
513
+ if (batchTesting || connections.length === 0) return;
514
+ setBatchTesting(true);
515
+ setBatchTestResults(null);
516
+ const controller = new AbortController();
517
+ const timeoutId = setTimeout(() => controller.abort(), 120_000); // 2min max
518
+ try {
519
+ const res = await fetch("/api/providers/test-batch", {
520
+ method: "POST",
521
+ headers: { "Content-Type": "application/json" },
522
+ body: JSON.stringify({ mode: "provider", providerId }),
523
+ signal: controller.signal,
524
+ });
525
+ let data: any;
526
+ try {
527
+ data = await res.json();
528
+ } catch {
529
+ data = { error: t("providerTestFailed"), results: [], summary: null };
530
+ }
531
+ setBatchTestResults({
532
+ ...data,
533
+ error: data.error
534
+ ? typeof data.error === "object"
535
+ ? data.error.message || data.error.error || JSON.stringify(data.error)
536
+ : String(data.error)
537
+ : null,
538
+ });
539
+ if (data?.summary) {
540
+ const { passed, failed, total } = data.summary;
541
+ if (failed === 0) notify.success(t("allTestsPassed", { total }));
542
+ else notify.warning(t("testSummary", { passed, failed, total }));
543
+ }
544
+ // Refresh connections to update statuses
545
+ await fetchConnections();
546
+ } catch (error: any) {
547
+ const isAbort = error?.name === "AbortError";
548
+ const msg = isAbort ? t("providerTestTimeout") : t("providerTestFailed");
549
+ setBatchTestResults({ error: msg, results: [], summary: null });
550
+ notify.error(msg);
551
+ } finally {
552
+ clearTimeout(timeoutId);
553
+ setBatchTesting(false);
554
+ }
555
+ };
556
+
509
557
  // T12: Manual token refresh
510
558
  const [refreshingId, setRefreshingId] = useState<string | null>(null);
511
559
  const handleRefreshToken = async (connectionId: string) => {
@@ -1160,6 +1208,24 @@ export default function ProviderDetailPage() {
1160
1208
  : t("providerProxy")}
1161
1209
  </button>
1162
1210
  </div>
1211
+ {connections.length > 1 && (
1212
+ <button
1213
+ onClick={handleBatchTestAll}
1214
+ disabled={batchTesting || !!retestingId}
1215
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
1216
+ batchTesting
1217
+ ? "bg-primary/20 border-primary/40 text-primary animate-pulse"
1218
+ : "bg-bg-subtle border-border text-text-muted hover:text-text-primary hover:border-primary/40"
1219
+ }`}
1220
+ title={t("testAll")}
1221
+ aria-label={t("testAll")}
1222
+ >
1223
+ <span className="material-symbols-outlined text-[14px]">
1224
+ {batchTesting ? "sync" : "play_arrow"}
1225
+ </span>
1226
+ {batchTesting ? t("testing") : t("testAll")}
1227
+ </button>
1228
+ )}
1163
1229
  {!isCompatible ? (
1164
1230
  <Button
1165
1231
  size="sm"
@@ -1356,6 +1422,96 @@ export default function ProviderDetailPage() {
1356
1422
  isAnthropic={isAnthropicCompatible}
1357
1423
  />
1358
1424
  )}
1425
+ {/* Batch Test Results Modal */}
1426
+ {batchTestResults && (
1427
+ <div
1428
+ className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh]"
1429
+ onClick={() => setBatchTestResults(null)}
1430
+ >
1431
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
1432
+ <div
1433
+ className="relative bg-bg-primary border border-border rounded-xl w-full max-w-[600px] max-h-[80vh] overflow-y-auto shadow-2xl"
1434
+ onClick={(e) => e.stopPropagation()}
1435
+ >
1436
+ <div className="sticky top-0 z-10 flex items-center justify-between px-5 py-3 border-b border-border bg-bg-primary/95 backdrop-blur-sm rounded-t-xl">
1437
+ <h3 className="font-semibold">{t("testResults")}</h3>
1438
+ <button
1439
+ onClick={() => setBatchTestResults(null)}
1440
+ className="p-1 rounded-lg hover:bg-bg-subtle text-text-muted hover:text-text-primary transition-colors"
1441
+ aria-label="Close"
1442
+ >
1443
+ <span className="material-symbols-outlined text-lg">close</span>
1444
+ </button>
1445
+ </div>
1446
+ <div className="p-5">
1447
+ {batchTestResults.error &&
1448
+ (!batchTestResults.results || batchTestResults.results.length === 0) ? (
1449
+ <div className="text-center py-6">
1450
+ <span className="material-symbols-outlined text-red-500 text-[32px] mb-2 block">
1451
+ error
1452
+ </span>
1453
+ <p className="text-sm text-red-400">{String(batchTestResults.error)}</p>
1454
+ </div>
1455
+ ) : (
1456
+ <div className="flex flex-col gap-3">
1457
+ {batchTestResults.summary && (
1458
+ <div className="flex items-center gap-3 text-xs mb-1">
1459
+ <span className="text-text-muted">{providerInfo?.name || providerId}</span>
1460
+ <span className="px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400 font-medium">
1461
+ {t("passedCount", { count: batchTestResults.summary.passed })}
1462
+ </span>
1463
+ {batchTestResults.summary.failed > 0 && (
1464
+ <span className="px-2 py-0.5 rounded bg-red-500/15 text-red-400 font-medium">
1465
+ {t("failedCount", { count: batchTestResults.summary.failed })}
1466
+ </span>
1467
+ )}
1468
+ <span className="text-text-muted ml-auto">
1469
+ {t("testedCount", { count: batchTestResults.summary.total })}
1470
+ </span>
1471
+ </div>
1472
+ )}
1473
+ {(batchTestResults.results || []).map((r: any, i: number) => (
1474
+ <div
1475
+ key={r.connectionId || i}
1476
+ className="flex items-center gap-2 text-xs px-3 py-2 rounded-lg bg-black/[0.03] dark:bg-white/[0.03]"
1477
+ >
1478
+ <span
1479
+ className={`material-symbols-outlined text-[16px] ${
1480
+ r.valid ? "text-emerald-500" : "text-red-500"
1481
+ }`}
1482
+ >
1483
+ {r.valid ? "check_circle" : "error"}
1484
+ </span>
1485
+ <div className="flex-1 min-w-0">
1486
+ <span className="font-medium">{r.connectionName}</span>
1487
+ </div>
1488
+ {r.latencyMs !== undefined && (
1489
+ <span className="text-text-muted font-mono tabular-nums">
1490
+ {t("millisecondsAbbr", { value: r.latencyMs })}
1491
+ </span>
1492
+ )}
1493
+ <span
1494
+ className={`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${
1495
+ r.valid
1496
+ ? "bg-emerald-500/15 text-emerald-400"
1497
+ : "bg-red-500/15 text-red-400"
1498
+ }`}
1499
+ >
1500
+ {r.valid ? t("okShort") : r.diagnosis?.type || t("errorShort")}
1501
+ </span>
1502
+ </div>
1503
+ ))}
1504
+ {(!batchTestResults.results || batchTestResults.results.length === 0) && (
1505
+ <div className="text-center py-4 text-text-muted text-sm">
1506
+ {t("noActiveConnectionsInGroup")}
1507
+ </div>
1508
+ )}
1509
+ </div>
1510
+ )}
1511
+ </div>
1512
+ </div>
1513
+ </div>
1514
+ )}
1359
1515
  {/* Proxy Config Modal */}
1360
1516
  {proxyTarget && (
1361
1517
  <ProxyConfigModal
@@ -90,13 +90,23 @@ export async function POST(request) {
90
90
  });
91
91
  }
92
92
 
93
- // Test each connection sequentially via direct function call (no HTTP self-call)
94
- const results = [];
93
+ // Test each connection with timeout and concurrency limits (prevents server crash on large groups)
94
+ const PER_CONNECTION_TIMEOUT = 30_000; // 30s per connection
95
+ const CONCURRENCY = 5; // max parallel tests
95
96
 
96
- for (const conn of connectionsToTest) {
97
+ const testOne = async (conn) => {
97
98
  try {
98
- const data = await testSingleConnection(conn.id);
99
- results.push({
99
+ const result = await Promise.race([
100
+ testSingleConnection(conn.id),
101
+ new Promise((_, reject) =>
102
+ setTimeout(
103
+ () => reject(new Error("Connection test timed out after 30s")),
104
+ PER_CONNECTION_TIMEOUT
105
+ )
106
+ ),
107
+ ]);
108
+ const data = result as any;
109
+ return {
100
110
  provider: conn.provider,
101
111
  connectionId: conn.id,
102
112
  connectionName: conn.name || conn.email || conn.provider,
@@ -107,9 +117,9 @@ export async function POST(request) {
107
117
  diagnosis: data.diagnosis || null,
108
118
  statusCode: data.statusCode || null,
109
119
  testedAt: data.testedAt || new Date().toISOString(),
110
- });
120
+ };
111
121
  } catch (error) {
112
- results.push({
122
+ return {
113
123
  provider: conn.provider,
114
124
  connectionId: conn.id,
115
125
  connectionName: conn.name || conn.email || conn.provider,
@@ -120,7 +130,37 @@ export async function POST(request) {
120
130
  diagnosis: { type: "network_error", source: "local", code: null, message: error.message },
121
131
  statusCode: null,
122
132
  testedAt: new Date().toISOString(),
123
- });
133
+ };
134
+ }
135
+ };
136
+
137
+ // Execute with concurrency limit
138
+ const results = [];
139
+ for (let i = 0; i < connectionsToTest.length; i += CONCURRENCY) {
140
+ const batch = connectionsToTest.slice(i, i + CONCURRENCY);
141
+ const batchResults = await Promise.allSettled(batch.map(testOne));
142
+ for (const r of batchResults) {
143
+ results.push(
144
+ r.status === "fulfilled"
145
+ ? r.value
146
+ : {
147
+ provider: "unknown",
148
+ connectionId: "unknown",
149
+ connectionName: "unknown",
150
+ authType: "unknown",
151
+ valid: false,
152
+ latencyMs: 0,
153
+ error: r.reason?.message || "Test failed",
154
+ diagnosis: {
155
+ type: "network_error",
156
+ source: "local",
157
+ code: null,
158
+ message: r.reason?.message || "Test failed",
159
+ },
160
+ statusCode: null,
161
+ testedAt: new Date().toISOString(),
162
+ }
163
+ );
124
164
  }
125
165
  }
126
166
 
@@ -12,6 +12,7 @@ import {
12
12
  getEmbeddingProvider,
13
13
  buildDynamicEmbeddingProvider,
14
14
  type EmbeddingProviderNodeRow,
15
+ type EmbeddingProvider,
15
16
  } from "@omniroute/open-sse/config/embeddingRegistry.ts";
16
17
  import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
17
18
  import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
@@ -116,9 +117,9 @@ export async function POST(request) {
116
117
  // Load local provider_nodes for embedding routing (only localhost — prevents auth bypass/SSRF)
117
118
  let dynamicProviders: ReturnType<typeof buildDynamicEmbeddingProvider>[] = [];
118
119
  try {
119
- const nodes = await getProviderNodes();
120
+ const nodes = (await getProviderNodes()) as unknown as EmbeddingProviderNodeRow[];
120
121
  dynamicProviders = (Array.isArray(nodes) ? nodes : [])
121
- .filter((n: EmbeddingProviderNodeRow) => {
122
+ .filter((n) => {
122
123
  // provider_nodes apiType is "chat" or "responses" (not "embeddings") — local OpenAI-compatible
123
124
  // backends expose /embeddings under the same base URL as chat, so we build the URL as baseUrl + /embeddings.
124
125
  if (n.apiType !== "chat" && n.apiType !== "responses") return false;
@@ -157,9 +158,38 @@ export async function POST(request) {
157
158
  }
158
159
 
159
160
  // Resolve provider config — dynamic first (local override), then hardcoded
160
- const providerConfig =
161
+ let providerConfig: EmbeddingProvider | null =
161
162
  dynamicProviders.find((dp) => dp.id === provider) || getEmbeddingProvider(provider) || null;
162
163
 
164
+ // #496: Fallback — resolve from ALL provider_nodes (not just localhost)
165
+ // This enables custom embedding models (e.g. google/gemini-embedding-001) whose
166
+ // providers have remote baseUrls. Safe because getProviderCredentials() authenticates.
167
+ if (!providerConfig) {
168
+ try {
169
+ const allNodes = (await getProviderNodes()) as unknown as EmbeddingProviderNodeRow[];
170
+ const matchingNode = (Array.isArray(allNodes) ? allNodes : []).find(
171
+ (n) =>
172
+ n.prefix === provider && (n.apiType === "chat" || n.apiType === "responses") && n.baseUrl
173
+ );
174
+ if (matchingNode) {
175
+ const baseUrl = String(matchingNode.baseUrl).replace(/\/+$/, "");
176
+ providerConfig = {
177
+ id: matchingNode.prefix,
178
+ baseUrl: `${baseUrl}/embeddings`,
179
+ authType: "apikey",
180
+ authHeader: "bearer",
181
+ models: [],
182
+ };
183
+ log.info(
184
+ "EMBED",
185
+ `Resolved custom embedding provider: ${provider} → ${providerConfig.baseUrl}`
186
+ );
187
+ }
188
+ } catch (err) {
189
+ log.error("EMBED", `Failed to resolve custom embedding provider ${provider}: ${err}`);
190
+ }
191
+ }
192
+
163
193
  if (!providerConfig) {
164
194
  return errorResponse(
165
195
  HTTP_STATUS.BAD_REQUEST,