From 887661f6dc6bdec6447f1e2caa45ae1f5420da12 Mon Sep 17 00:00:00 2001 From: whit3rabbit Date: Wed, 8 Apr 2026 13:32:21 -0500 Subject: [PATCH] feat: add managed backend types, queries, and provider field schema - Add CatalogProvider, ManagedBackend, CreateManagedBackendRequest, UpdateManagedBackendRequest to api/types.ts - Add useManagedBackends, useCreateManagedBackend, useUpdateManagedBackend, useDeleteManagedBackend hooks to api/queries.ts - Create utils/providerFields.ts with getProviderFields() mapping protocol+auth to FieldDef[] Co-Authored-By: Claude Sonnet 4.6 --- crates/proxy/admin-ui/dist/index.html | 16 +- crates/proxy/admin-ui/src/api/queries.ts | 38 ++++ crates/proxy/admin-ui/src/api/types.ts | 48 +++++ .../admin-ui/src/utils/providerFields.ts | 189 ++++++++++++++++++ 4 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 crates/proxy/admin-ui/src/utils/providerFields.ts diff --git a/crates/proxy/admin-ui/dist/index.html b/crates/proxy/admin-ui/dist/index.html index 6279ad4..7b410de 100644 --- a/crates/proxy/admin-ui/dist/index.html +++ b/crates/proxy/admin-ui/dist/index.html @@ -4,7 +4,7 @@ Proxy Admin - +PROXY_API_KEYS=my-key`})]})]})]}),ht&&f.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16,padding:"8px 12px",background:"var(--warn-dim)",borderLeft:"3px solid var(--warn)",borderRadius:"var(--r)",fontSize:13},children:[f.jsx("span",{children:"Restart the proxy for imported env vars to take effect."}),f.jsx("button",{className:"btn btn-secondary btn-sm",onClick:Ct,children:"Dismiss"})]}),f.jsxs("div",{style:{marginBottom:24},children:[f.jsx("div",{className:"section-label",style:{marginBottom:8},children:"Env File"}),f.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center"},children:[f.jsx("input",{ref:T,type:"file",accept:".env,.anyllm.env,text/plain",style:{display:"none"},onChange:Ot}),f.jsx("button",{className:"btn btn-secondary btn-sm",onClick:()=>T.current?.click(),disabled:O.isPending,children:O.isPending?"Importing…":"Import .anyllm.env"}),f.jsx("button",{className:"btn btn-secondary btn-sm",onClick:Yt,children:"Export .anyllm.env"})]}),D&&f.jsxs("div",{style:{marginTop:10},children:[f.jsxs("div",{className:"dim",style:{marginBottom:4},children:[D.applied," variable",D.applied!==1?"s":""," imported.",D.warnings.length===0&&" No issues."]}),D.warnings.length>0&&f.jsxs("div",{style:{marginTop:8,padding:"8px 12px",background:"var(--warn-dim)",borderLeft:"3px solid var(--warn)",borderRadius:"var(--r)",fontSize:12},children:[f.jsx("div",{style:{fontWeight:600,marginBottom:4},children:"Warnings"}),D.warnings.map((Q,nt)=>f.jsxs("div",{className:"mono",style:{fontSize:12},children:[Q.line!=null&&f.jsxs("span",{className:"dim",children:["[line ",Q.line,"] "]}),Q.key&&f.jsxs("span",{children:[Q.key,": "]}),Q.message]},nt))]})]}),J&&f.jsxs("div",{style:{marginTop:10,padding:"8px 12px",background:"var(--err-dim)",borderLeft:"3px solid var(--err)",borderRadius:"var(--r)",fontSize:12},children:[f.jsx("div",{style:{fontWeight:600,marginBottom:4},children:"Import rejected"}),J.hard_errors.map((Q,nt)=>f.jsx("div",{className:"mono",style:{fontSize:12},children:Q},nt)),J.warnings.length>0&&f.jsxs(f.Fragment,{children:[f.jsx("div",{style:{fontWeight:600,marginTop:8,marginBottom:4},children:"Warnings (from partial parse)"}),J.warnings.map((Q,nt)=>f.jsxs("div",{className:"mono",style:{fontSize:12},children:[Q.line!=null&&f.jsxs("span",{className:"dim",children:["[line ",Q.line,"] "]}),Q.message]},nt))]})]}),G&&f.jsxs("div",{style:{marginTop:10,padding:"8px 12px",background:"var(--err-dim)",borderLeft:"3px solid var(--err)",borderRadius:"var(--r)",fontSize:12},children:["Export failed: ",G]})]}),f.jsx(Rl,{loading:o,error:r?.message}),i&&f.jsx("div",{children:i.entries.map(Q=>f.jsxs("div",{className:"form-group",children:[f.jsx("div",{className:"form-label",children:Q.key}),f.jsxs("div",{className:"form-row",children:[f.jsx("input",{value:p[Q.key]??Q.value,onChange:nt=>U(gt=>({...gt,[Q.key]:nt.target.value}))}),f.jsx("button",{className:"btn btn-primary btn-sm",onClick:()=>ct(Q.key,Q.value),children:"Save"}),f.jsx("button",{className:"btn btn-secondary btn-sm",onClick:()=>_.mutate(Q.key),children:"Reset"})]})]},Q.key))}),h&&f.jsxs("div",{className:"readonly-section",style:{marginTop:16},children:[f.jsx("div",{className:"section-label",children:"Environment"}),f.jsx("div",{style:{display:"grid",gridTemplateColumns:"220px 1fr",gap:"4px 12px",marginTop:8,fontSize:12},children:Object.entries(h).map(([Q,nt])=>f.jsxs(Z.Fragment,{children:[f.jsx("span",{className:"dim",children:Q}),f.jsx("span",{className:"mono",children:nt})]},Q))})]})]})}const zp={ok:"var(--ok)",warn:"var(--warn)",err:"var(--err)",dim:"var(--text-3)"};function bv({status:s,pulse:i}){return f.jsx("span",{style:{display:"inline-block",width:7,height:7,borderRadius:"50%",background:zp[s],animation:i?"pulse 2s ease-in-out infinite":void 0,verticalAlign:"middle",marginRight:6}})}function Ap(){const{data:s,isLoading:i,error:o}=lp();return f.jsxs("div",{children:[f.jsx(Rl,{loading:i,error:o?.message,empty:s?.length===0}),f.jsx("div",{className:"backend-cards",children:s?.map(r=>f.jsxs("div",{className:"card",children:[f.jsxs("div",{className:"card-header",children:[f.jsx("span",{className:"card-name",children:r.name}),f.jsx(bv,{status:r.status==="ok"?"ok":"err",pulse:r.status==="ok"})]}),f.jsxs("div",{className:"card-body",children:[f.jsx("div",{className:"mono",children:r.model}),f.jsxs("div",{style:{marginTop:6,display:"grid",gridTemplateColumns:"1fr 1fr",gap:4},children:[f.jsx("span",{className:"dim",children:"Requests"}),f.jsx("span",{className:"mono",children:r.requests_total}),f.jsx("span",{className:"dim",children:"P50"}),f.jsxs("span",{className:"mono",children:[r.p50_ms,"ms"]}),f.jsx("span",{className:"dim",children:"P95"}),f.jsxs("span",{className:"mono",children:[r.p95_ms,"ms"]}),f.jsx("span",{className:"dim",children:"Errors"}),f.jsx("span",{className:"mono",style:{color:r.requests_err>0?"var(--err)":void 0},children:r.requests_err})]})]})]},r.name))})]})}function Mp({variant:s}){return f.jsx("span",{className:`badge badge-${s}`,children:s})}function Rp({spent:s,limit:i}){if(!i)return f.jsx("span",{className:"dim",children:"—"});const o=Math.min(s/i*100,100),r=o>=95?"danger":o>=80?"warn":"";return f.jsxs("div",{children:[f.jsx("div",{className:"budget-bar",children:f.jsx("div",{className:`budget-bar-fill${r?` ${r}`:""}`,style:{width:`${o}%`}})}),f.jsxs("span",{className:"dim",style:{fontSize:10},children:["$",s.toFixed(4)," / $",i.toFixed(2)]})]})}function Cp({onCreated:s}){const i=Pg(),[o,r]=Z.useState(""),[h,S]=Z.useState(""),[_,O]=Z.useState("");function T(){i.mutate({description:o||null,spend_limit:h?Number(h):null,rpm_limit:_?Number(_):null},{onSuccess:p=>{r(""),S(""),O(""),s(p.key)}})}return f.jsxs("div",{className:"form-group",children:[f.jsx("div",{className:"form-label",children:"Create Key"}),f.jsx("form",{onSubmit:p=>{p.preventDefault(),T()},children:f.jsxs("div",{className:"form-row",style:{flexWrap:"wrap"},children:[f.jsx("input",{placeholder:"Description",value:o,onChange:p=>r(p.target.value)}),f.jsx("input",{placeholder:"Spend limit USD",type:"number",value:h,onChange:p=>S(p.target.value),style:{width:120}}),f.jsx("input",{placeholder:"RPM limit",type:"number",value:_,onChange:p=>O(p.target.value),style:{width:100}}),f.jsx("button",{type:"submit",className:"btn btn-primary",disabled:i.isPending,children:i.isPending?"Creating…":"Create"})]})})]})}function Dp({vk:s,onClose:i}){const o=tp(),r=ep(),[h,S]=Z.useState(s.description??""),[_,O]=Z.useState(s.spend_limit?.toString()??""),[T,p]=Z.useState(s.rpm_limit?.toString()??"");function U(){o.mutate({id:s.id,body:{description:h||null,spend_limit:_?Number(_):null,rpm_limit:T?Number(T):null}},{onSuccess:i})}function D(){confirm("Revoke this key?")&&r.mutate(s.id,{onSuccess:i})}return f.jsx("div",{className:"modal-backdrop",onClick:i,children:f.jsxs("div",{className:"modal",onClick:Y=>Y.stopPropagation(),children:[f.jsxs("div",{className:"modal-title",children:["Edit Key — ",s.key_prefix,"…"]}),f.jsxs("div",{className:"form-group",children:[f.jsx("div",{className:"form-label",children:"Description"}),f.jsx("input",{value:h,onChange:Y=>S(Y.target.value),style:{width:"100%"}})]}),f.jsxs("div",{className:"form-group",children:[f.jsx("div",{className:"form-label",children:"Spend limit (USD)"}),f.jsx("input",{value:_,onChange:Y=>O(Y.target.value),type:"number",min:"0",step:"0.01"})]}),f.jsxs("div",{className:"form-group",children:[f.jsx("div",{className:"form-label",children:"RPM limit"}),f.jsx("input",{value:T,onChange:Y=>p(Y.target.value),type:"number",min:"0"})]}),f.jsxs("div",{className:"form-row",children:[f.jsx("button",{className:"btn btn-primary",onClick:U,children:"Save"}),f.jsx("button",{className:"btn btn-secondary",onClick:i,children:"Cancel"}),f.jsx("button",{className:"btn btn-danger",style:{marginLeft:"auto"},onClick:D,children:"Revoke"})]})]})})}function Up(){const{data:s,isLoading:i,error:o}=Ig(),[r,h]=Z.useState(null),[S,_]=Z.useState(null);return f.jsxs("div",{children:[f.jsx(Cp,{onCreated:h}),r&&f.jsxs("div",{className:"key-result",children:[f.jsx("div",{className:"key-result-label",children:"New key (copy now — not shown again)"}),r]}),f.jsx(Rl,{loading:i,error:o?.message,empty:s?.length===0,message:"No keys"}),s&&s.length>0&&f.jsxs("table",{className:"keys-grid",children:[f.jsx("thead",{children:f.jsxs("tr",{children:[f.jsx("th",{children:"Prefix"}),f.jsx("th",{children:"Description"}),f.jsx("th",{children:"Status"}),f.jsx("th",{children:"Spend"}),f.jsx("th",{children:"Requests"}),f.jsx("th",{children:"Created"})]})}),f.jsx("tbody",{children:s.map(O=>f.jsxs("tr",{style:{cursor:"pointer"},onClick:()=>_(O),children:[f.jsxs("td",{className:"mono",children:[O.key_prefix,"…"]}),f.jsx("td",{className:"dim",children:O.description??"—"}),f.jsx("td",{children:f.jsx(Mp,{variant:O.status})}),f.jsx("td",{children:f.jsx(Rp,{spent:O.total_spend,limit:O.spend_limit})}),f.jsx("td",{className:"mono",children:O.total_requests.toLocaleString()}),f.jsx("td",{className:"mono dim",children:O.created_at.slice(0,10)})]},O.id))})]}),S&&f.jsx(Dp,{vk:S,onClose:()=>_(null)},S.id)]})}const Xm={openrouter:{text:"Public, no key needed",needsKey:!1},deepinfra:{text:"Public, no key needed",needsKey:!1},ollama:{text:"No key needed (local)",needsKey:!1},configured:{text:"API key required",needsKey:!0},custom:{text:"API key may be required",needsKey:!0}};function qp(){return f.jsx("svg",{width:"12",height:"12",viewBox:"0 0 16 16",fill:"none",style:{verticalAlign:"-1px",marginRight:3},children:f.jsx("path",{d:"M10.5 1a4.5 4.5 0 0 0-4.1 6.35L2 11.75V15h3.25v-2H7v-1.75h1.75L9.65 10.4A4.5 4.5 0 1 0 10.5 1zm1 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2z",fill:"currentColor"})})}function Hp(){const{data:s,isLoading:i,error:o}=sp(),r=cp(),h=fp(),S=rp(),[_,O]=Z.useState(""),[T,p]=Z.useState(""),[U,D]=Z.useState("openai"),[Y,J]=Z.useState("openrouter"),[$,G]=Z.useState(""),P=Xm[Y]??Xm.custom;function ht(){S.mutate({source:Y,...Y==="custom"?{url:$}:{}})}return f.jsxs("div",{children:[f.jsxs("div",{style:{marginBottom:20},children:[f.jsx("div",{className:"section-label",style:{marginBottom:8},children:"Discover Models"}),f.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[f.jsxs("select",{value:Y,onChange:k=>{J(k.target.value),S.reset()},children:[f.jsx("option",{value:"openrouter",children:"OpenRouter"}),f.jsx("option",{value:"deepinfra",children:"DeepInfra"}),f.jsx("option",{value:"ollama",children:"Ollama (local)"}),f.jsx("option",{value:"configured",children:"Configured backend"}),f.jsx("option",{value:"custom",children:"Custom URL"})]}),Y==="custom"&&f.jsx("input",{placeholder:"https://api.example.com",value:$,onChange:k=>G(k.target.value),style:{minWidth:220}}),f.jsx("button",{className:"btn btn-secondary",onClick:ht,disabled:S.isPending||Y==="custom"&&!$,children:S.isPending?"Fetching...":"Fetch"}),f.jsxs("span",{className:"dim",style:{fontSize:12},children:[P.needsKey&&f.jsx(qp,{}),P.text]})]}),S.isError&&f.jsx("div",{style:{marginTop:8,padding:"6px 10px",background:"var(--err-dim)",borderLeft:"3px solid var(--err)",borderRadius:"var(--r)",fontSize:12},children:S.error.message}),S.data&&S.data.models.length>0&&f.jsxs("div",{style:{marginTop:8},children:[f.jsxs("div",{className:"dim",style:{fontSize:12,marginBottom:4},children:[S.data.models.length," model",S.data.models.length!==1?"s":""," found. Click to populate the form below."]}),f.jsx("div",{style:{maxHeight:200,overflowY:"auto",border:"1px solid var(--border)",borderRadius:"var(--r)",fontSize:12},children:S.data.models.map(k=>f.jsxs("div",{onClick:()=>p(k.id),style:{padding:"4px 8px",cursor:"pointer",borderBottom:"1px solid var(--border)",background:T===k.id?"var(--accent-dim)":void 0},onMouseEnter:ct=>{ct.target.style.background="var(--surface-2)"},onMouseLeave:ct=>{ct.target.style.background=T===k.id?"var(--accent-dim)":""},children:[f.jsx("span",{className:"mono",children:k.id}),k.name&&k.name!==k.id&&f.jsx("span",{className:"dim",style:{marginLeft:8},children:k.name})]},k.id))})]}),S.data&&S.data.models.length===0&&f.jsx("div",{className:"dim",style:{marginTop:8,fontSize:12},children:"No models returned."})]}),f.jsxs("div",{className:"form-group",children:[f.jsx("div",{className:"form-label",children:"Add Model"}),f.jsxs("div",{className:"form-row",style:{flexWrap:"wrap"},children:[f.jsx("input",{placeholder:"Virtual name",value:_,onChange:k=>O(k.target.value)}),f.jsx("input",{placeholder:"Model ID",value:T,onChange:k=>p(k.target.value)}),f.jsxs("select",{value:U,onChange:k=>D(k.target.value),children:[f.jsx("option",{value:"openai",children:"openai"}),f.jsx("option",{value:"anthropic",children:"anthropic"}),f.jsx("option",{value:"gemini",children:"gemini"}),f.jsx("option",{value:"vertex",children:"vertex"}),f.jsx("option",{value:"azure",children:"azure"}),f.jsx("option",{value:"bedrock",children:"bedrock"})]}),f.jsx("button",{className:"btn btn-primary",onClick:()=>r.mutate({name:_,model:T,provider:U}),disabled:!_||!T||r.isPending,children:"Add"})]})]}),f.jsx(Rl,{loading:i,error:o?.message}),s&&f.jsxs("table",{className:"route-table",children:[f.jsx("thead",{children:f.jsxs("tr",{children:[f.jsx("th",{children:"Virtual Name"}),f.jsx("th",{children:"Model"}),f.jsx("th",{children:"Provider"}),f.jsx("th",{children:"Strategy"}),f.jsx("th",{})]})}),f.jsx("tbody",{children:s.models.map(k=>f.jsxs("tr",{children:[f.jsx("td",{className:"mono",children:k.name}),f.jsx("td",{className:"mono",children:k.model}),f.jsx("td",{className:"dim",children:k.provider}),f.jsx("td",{className:"dim",children:s.routing_strategy}),f.jsx("td",{children:f.jsx("button",{className:"btn btn-danger btn-sm",onClick:()=>h.mutate(k.name),children:"Remove"})})]},`${k.name}-${k.model}`))})]})]})}function Bp(){const[s,i]=Z.useState(1),{data:o,isLoading:r,error:h}=op({page:s,page_size:50});return f.jsxs("div",{children:[f.jsx(Rl,{loading:r,error:h?.message}),o&&f.jsxs(f.Fragment,{children:[f.jsxs("table",{className:"route-table",children:[f.jsx("thead",{children:f.jsxs("tr",{children:[f.jsx("th",{children:"Time"}),f.jsx("th",{children:"Action"}),f.jsx("th",{children:"Target"}),f.jsx("th",{children:"Detail"}),f.jsx("th",{children:"IP"})]})}),f.jsx("tbody",{children:o.entries.map(S=>f.jsxs("tr",{children:[f.jsx("td",{className:"mono dim",children:S.timestamp.slice(0,19)}),f.jsx("td",{className:"mono",children:S.action}),f.jsxs("td",{className:"dim",children:[S.target_type,S.target_id?` #${S.target_id}`:""]}),f.jsx("td",{className:"dim",style:{maxWidth:300,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:S.detail??"—"}),f.jsx("td",{className:"mono dim",children:S.source_ip??"—"})]},S.id))})]}),f.jsx(pv,{page:s,hasMore:o.has_more,onPrev:()=>i(S=>Math.max(1,S-1)),onNext:()=>i(S=>S+1)})]})]})}function Qp({routes:s}){const i=[...s].sort((o,r)=>r.requests_per_min-o.requests_per_min);return f.jsxs("table",{className:"route-table",children:[f.jsx("thead",{children:f.jsxs("tr",{children:[f.jsx("th",{children:"Route"}),f.jsx("th",{children:"Req/min"}),f.jsx("th",{children:"Error rate"}),f.jsx("th",{children:"Avg latency"}),f.jsx("th",{children:"P95 latency"}),f.jsx("th",{children:"Total"})]})}),f.jsx("tbody",{children:i.map(o=>f.jsxs("tr",{children:[f.jsx("td",{className:"mono",children:o.path}),f.jsx("td",{className:"mono",children:o.requests_per_min.toFixed(2)}),f.jsxs("td",{className:"mono",style:{color:o.error_rate>.05?"var(--err)":o.error_rate>.01?"var(--warn)":void 0},children:[(o.error_rate*100).toFixed(1),"%"]}),f.jsxs("td",{className:"mono",children:[o.avg_latency_ms.toFixed(0),"ms"]}),f.jsxs("td",{className:"mono",children:[o.p95_latency_ms,"ms"]}),f.jsx("td",{className:"mono",children:o.total_requests.toLocaleString()})]},o.path))})]})}const Km=["#e8a030","#d4922b","#c07820","#a86015","#8c500a"],Zm=["#6eb5c0","#5aa0ab","#468b96","#327681","#1e616c"];function Yp(){const[s,i]=Z.useState(6),{data:o,isLoading:r,error:h}=dp(s),S=o?.routes??[],_=S.slice(0,5).map((O,T)=>{const p=(o?.series??[]).filter(U=>U.path===O.path).map(U=>U.requests);return{label:O.path,color:Km[T%Km.length],data:p}});return f.jsxs("div",{children:[f.jsxs("div",{className:"section-header",children:[f.jsx("span",{className:"section-label",children:"Traffic"}),f.jsxs("select",{value:s,onChange:O=>i(Number(O.target.value)),children:[f.jsx("option",{value:1,children:"Last 1 hour"}),f.jsx("option",{value:6,children:"Last 6 hours"}),f.jsx("option",{value:24,children:"Last 24 hours"})]})]}),f.jsx(Rl,{loading:r,error:h?.message}),o&&f.jsxs(f.Fragment,{children:[f.jsx(Qp,{routes:o.routes}),f.jsxs("div",{className:"operator-grid",style:{marginTop:16},children:[f.jsxs("div",{className:"chart-card",children:[f.jsx("div",{className:"chart-header",children:f.jsxs("div",{children:[f.jsx("div",{className:"chart-title",children:"Requests / min by route"}),f.jsx("div",{className:"chart-subtitle",children:"Stacked over time window"})]})}),f.jsx(vs,{series:_})]}),f.jsxs("div",{className:"chart-card",children:[f.jsx("div",{className:"chart-header",children:f.jsxs("div",{children:[f.jsx("div",{className:"chart-title",children:"Avg latency per route"}),f.jsx("div",{className:"chart-subtitle",children:"ms"})]})}),S.length===0?f.jsx("div",{className:"empty",children:"No routes"}):f.jsx("div",{style:{display:"flex",flexDirection:"column",gap:8,paddingTop:8},children:S.slice(0,5).map((O,T)=>{const p=Math.max(...S.slice(0,5).map(D=>D.avg_latency_ms),1),U=O.avg_latency_ms/p*100;return f.jsxs("div",{children:[f.jsxs("div",{style:{display:"flex",justifyContent:"space-between",fontSize:11,marginBottom:2},children:[f.jsx("span",{className:"mono dim",style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"70%"},children:O.path}),f.jsxs("span",{className:"mono",children:[O.avg_latency_ms.toFixed(0),"ms"]})]}),f.jsx("div",{style:{height:6,background:"var(--border)",borderRadius:0},children:f.jsx("div",{style:{height:"100%",width:`${U}%`,background:Zm[T%Zm.length],borderRadius:0}})})]},O.path)})})]})]})]})]})}function Lp(s){const i=Math.floor(Date.now()/1e3-s),o=Math.floor(i/86400),r=Math.floor(i%86400/3600),h=Math.floor(i%3600/60);return o>0?`${o}d ${r}h ${h}m`:r>0?`${r}h ${h}m`:`${h}m`}function Gp({proxy:s}){return f.jsxs("div",{className:"uptime-proxy",children:[f.jsxs("div",{className:"uptime-proxy-stats",children:[f.jsxs("div",{children:[f.jsx("div",{className:"section-label",children:"Uptime (30d)"}),f.jsxs("div",{className:"uptime-pct",children:[s.uptime_pct_30d.toFixed(2),"%"]})]}),f.jsxs("div",{children:[f.jsx("div",{className:"section-label",children:"Running"}),f.jsx("div",{className:"stat-value",style:{fontSize:16},children:Lp(s.started_at)})]})]}),f.jsx("div",{className:"section-label",style:{marginBottom:4},children:"30-day history"}),f.jsx("div",{className:"history-bar",children:s.history.map(i=>f.jsx("div",{className:`history-day ${i.status}`,title:`${i.date}: ${i.status}`},i.date))})]})}function Xp({b:s}){const i=s.status==="up"?"ok":s.status==="down"?"err":"dim",o=s.last_checked_at?new Date(s.last_checked_at*1e3).toLocaleTimeString():"—";return f.jsxs("tr",{children:[f.jsx("td",{className:"mono",children:s.name}),f.jsxs("td",{children:[f.jsx(bv,{status:i,pulse:s.status==="up"}),s.status]}),f.jsxs("td",{className:"mono",children:[s.uptime_pct_30d.toFixed(2),"%"]}),f.jsx("td",{className:"mono dim",children:o}),f.jsx("td",{className:"mono dim",children:s.last_latency_ms!=null?`${s.last_latency_ms}ms`:"—"}),f.jsx("td",{children:f.jsx("div",{className:"history-bar",style:{height:12},children:s.history.map(r=>f.jsx("div",{className:`history-day ${r.status}`,title:`${r.date}: ${r.status}`},r.date))})})]})}function Kp(){const{data:s,isLoading:i,error:o}=hp();return f.jsxs("div",{children:[f.jsx(Rl,{loading:i,error:o?.message}),s&&f.jsxs(f.Fragment,{children:[f.jsx(Gp,{proxy:s.proxy}),f.jsx("div",{className:"section-label",style:{marginTop:16,marginBottom:8},children:"Backend Availability"}),f.jsxs("table",{className:"backend-health-table",children:[f.jsx("thead",{children:f.jsxs("tr",{children:[f.jsx("th",{children:"Backend"}),f.jsx("th",{children:"Status"}),f.jsx("th",{children:"Uptime (30d)"}),f.jsx("th",{children:"Last checked"}),f.jsx("th",{children:"Latency"}),f.jsx("th",{children:"History"})]})}),f.jsx("tbody",{children:s.backends.slice().sort((r,h)=>r.name.localeCompare(h.name)).map(r=>f.jsx(Xp,{b:r},r.name))})]})]})]})}function Zp(){const s=Al(p=>p.token),i=Al(p=>p.login),o=ha(p=>p.lastEvent),r=nl(),[h,S]=Z.useState("dashboard"),[_,O]=Z.useState(!0),{data:T}=kg(!!s);return Z.useEffect(()=>{const U=new URLSearchParams(location.search).get("token");U&&!s?fetch("/admin/api/metrics",{headers:{Authorization:`Bearer ${U}`}}).then(D=>{D.ok&&(i(U),history.replaceState(null,"",location.pathname))}).catch(()=>{}).finally(()=>O(!1)):O(!1)},[]),Z.useEffect(()=>{s?Vg():wg()},[s]),Z.useEffect(()=>{s&&T&&!T.configured&&S("settings")},[T?.configured,s]),Z.useEffect(()=>{o&&(o.type==="metrics_snapshot"?r.setQueryData(["metrics"],o.data):o.type==="backend_health_changed"&&r.invalidateQueries({queryKey:["uptime"]}))},[o,r]),_?null:s?f.jsxs("div",{children:[f.jsx(pp,{activeTab:h,onTabChange:S}),f.jsxs("div",{className:"tab-content",children:[h==="dashboard"&&f.jsx(Tp,{}),h==="requests"&&f.jsx(_p,{}),h==="settings"&&f.jsx(Np,{configured:T?.configured??!0}),h==="backends"&&f.jsx(Ap,{}),h==="keys"&&f.jsx(Up,{}),h==="models"&&f.jsx(Hp,{}),h==="audit"&&f.jsx(Bp,{}),h==="traffic"&&f.jsx(Yp,{}),h==="uptime"&&f.jsx(Kp,{})]})]}):f.jsx(yp,{})}const Vp=new Tg({defaultOptions:{queries:{retry:1,refetchOnWindowFocus:!1}}});P0.createRoot(document.getElementById("root")).render(f.jsx(Z.StrictMode,{children:f.jsx(_g,{client:Vp,children:f.jsx(Zp,{})})})); diff --git a/crates/proxy/admin-ui/src/api/queries.ts b/crates/proxy/admin-ui/src/api/queries.ts index ddce420..d95679e 100644 --- a/crates/proxy/admin-ui/src/api/queries.ts +++ b/crates/proxy/admin-ui/src/api/queries.ts @@ -6,6 +6,7 @@ import type { Backend, ConfigResponse, ObservabilityResponse, ModelsResponse, AuditResponse, TrafficResponse, UptimeResponse, EnvImportResponse, ProxyStatus, DiscoverResponse, + ManagedBackend, CreateManagedBackendRequest, UpdateManagedBackendRequest, } from './types' // ── Status ─────────────────────────────────────────────────────────────────── @@ -232,6 +233,43 @@ export function useImportEnv() { }) } +// ── Managed backends ────────────────────────────────────────────────────────── + +export function useManagedBackends() { + return useQuery<{ backends: ManagedBackend[] }>({ + queryKey: ['managed-backends'], + queryFn: () => apiFetch('/admin/api/backends/managed'), + staleTime: Infinity, + }) +} + +export function useCreateManagedBackend() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (data) => + mutatingFetch('POST', '/admin/api/backends/managed', data), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['managed-backends'] }) }, + }) +} + +export function useUpdateManagedBackend() { + const qc = useQueryClient() + return useMutation({ + mutationFn: ({ name, data }) => + mutatingFetch('PUT', `/admin/api/backends/managed/${name}`, data), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['managed-backends'] }) }, + }) +} + +export function useDeleteManagedBackend() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (name) => + mutatingFetch('DELETE', `/admin/api/backends/managed/${name}`), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['managed-backends'] }) }, + }) +} + /** * Download the current effective env as a .anyllm.env file. * Not a hook — call directly from an event handler. diff --git a/crates/proxy/admin-ui/src/api/types.ts b/crates/proxy/admin-ui/src/api/types.ts index 17e6863..6598304 100644 --- a/crates/proxy/admin-ui/src/api/types.ts +++ b/crates/proxy/admin-ui/src/api/types.ts @@ -239,6 +239,54 @@ export interface EnvImportError { warnings: EnvWarning[] } +// --- Provider catalog --- + +export interface CatalogProvider { + id: string + name: string + protocol: string + auth: string + default_base_url: string + env_vars: string[] +} + +// --- Managed backends --- + +export interface ManagedBackend { + id: string + name: string + provider_id: string + api_key_set: boolean + aws_creds_set: boolean + api_base: string | null + deployment: string | null + api_version: string | null + project: string | null + region: string | null + rpm: number | null + tpm: number | null + created_at: string + updated_at: string +} + +export interface CreateManagedBackendRequest { + name: string + provider_id: string + api_key?: string + api_base?: string + deployment?: string + api_version?: string + project?: string + region?: string + aws_access_key_id?: string + aws_secret_access_key?: string + aws_session_token?: string + rpm?: number + tpm?: number +} + +export type UpdateManagedBackendRequest = Partial> + // --- WebSocket events --- export type WSEvent = diff --git a/crates/proxy/admin-ui/src/utils/providerFields.ts b/crates/proxy/admin-ui/src/utils/providerFields.ts new file mode 100644 index 0000000..5360626 --- /dev/null +++ b/crates/proxy/admin-ui/src/utils/providerFields.ts @@ -0,0 +1,189 @@ +import { CatalogProvider } from '../api/types' + +export type FieldGroup = 'auth' | 'endpoint' | 'limits' + +export interface FieldDef { + name: string + label: string + type: 'text' | 'password' | 'url' | 'number' + required: boolean + placeholder?: string + hint?: string + group: FieldGroup +} + +export function getProviderFields(provider: CatalogProvider): FieldDef[] { + const fields: FieldDef[] = [] + const firstEnvVar = provider.env_vars.length > 0 ? provider.env_vars[0] : null + + const { protocol, auth, default_base_url } = provider + + if (protocol === 'openai_compat' && auth === 'bearer') { + fields.push({ + name: 'api_key', + label: 'API Key', + type: 'password', + required: true, + group: 'auth', + ...(firstEnvVar ? { hint: `or set ${firstEnvVar} env var` } : {}), + }) + fields.push({ + name: 'api_base', + label: 'API Base URL', + type: 'url', + required: false, + group: 'endpoint', + ...(default_base_url ? { placeholder: default_base_url } : {}), + }) + } else if (protocol === 'openai_compat' && auth === 'none') { + fields.push({ + name: 'api_base', + label: 'API Base URL', + type: 'url', + required: false, + group: 'endpoint', + ...(default_base_url ? { placeholder: default_base_url } : {}), + }) + } else if (protocol === 'azure_openai' && auth === 'azure_api_key') { + fields.push({ + name: 'api_key', + label: 'Azure API Key', + type: 'password', + required: true, + group: 'auth', + }) + fields.push({ + name: 'api_base', + label: 'Endpoint URL', + type: 'url', + required: true, + placeholder: 'https://.openai.azure.com', + group: 'endpoint', + }) + fields.push({ + name: 'deployment', + label: 'Deployment Name', + type: 'text', + required: true, + group: 'endpoint', + }) + fields.push({ + name: 'api_version', + label: 'API Version', + type: 'text', + required: true, + placeholder: '2024-08-01-preview', + group: 'endpoint', + }) + } else if (protocol === 'vertex_ai' && auth === 'google_api_key') { + fields.push({ + name: 'api_key', + label: 'API Key', + type: 'password', + required: true, + group: 'auth', + }) + fields.push({ + name: 'project', + label: 'GCP Project ID', + type: 'text', + required: true, + group: 'endpoint', + }) + fields.push({ + name: 'region', + label: 'GCP Region', + type: 'text', + required: true, + placeholder: 'us-central1', + group: 'endpoint', + }) + } else if (protocol === 'bedrock_native' && auth === 'aws_sigv4') { + fields.push({ + name: 'aws_access_key_id', + label: 'AWS Access Key ID', + type: 'text', + required: true, + group: 'auth', + }) + fields.push({ + name: 'aws_secret_access_key', + label: 'AWS Secret Access Key', + type: 'password', + required: true, + group: 'auth', + }) + fields.push({ + name: 'aws_session_token', + label: 'AWS Session Token', + type: 'password', + required: false, + group: 'auth', + }) + fields.push({ + name: 'region', + label: 'AWS Region', + type: 'text', + required: true, + placeholder: 'us-east-1', + group: 'endpoint', + }) + } else if ((protocol === 'gemini_openai' || protocol === 'gemini_native') && auth === 'google_api_key') { + fields.push({ + name: 'api_key', + label: 'API Key', + type: 'password', + required: true, + group: 'auth', + ...(firstEnvVar ? { hint: `or set ${firstEnvVar} env var` } : {}), + }) + } else if (protocol === 'anthropic_native' && auth === 'bearer') { + fields.push({ + name: 'api_key', + label: 'API Key', + type: 'password', + required: true, + group: 'auth', + ...(firstEnvVar ? { hint: `or set ${firstEnvVar} env var` } : {}), + }) + } else { + // Fallback + if (auth.includes('bearer')) { + fields.push({ + name: 'api_key', + label: 'API Key', + type: 'password', + required: true, + group: 'auth', + }) + } + if (auth === 'none' || auth.includes('bearer')) { + fields.push({ + name: 'api_base', + label: 'API Base URL', + type: 'url', + required: false, + group: 'endpoint', + ...(default_base_url ? { placeholder: default_base_url } : {}), + }) + } + } + + // All providers get rate/token limits + fields.push({ + name: 'rpm', + label: 'Rate Limit (req/min)', + type: 'number', + required: false, + group: 'limits', + }) + fields.push({ + name: 'tpm', + label: 'Token Limit (tokens/min)', + type: 'number', + required: false, + group: 'limits', + }) + + return fields +}