diff --git a/crates/proxy/admin-ui/dist/index.html b/crates/proxy/admin-ui/dist/index.html index 355312d..b82ac94 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`})]})]})]}),ft&&c.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:[c.jsx("span",{children:"Restart the proxy for imported env vars to take effect."}),c.jsx("button",{className:"btn btn-secondary btn-sm",onClick:Z,children:"Dismiss"})]}),c.jsxs("div",{style:{marginBottom:24},children:[c.jsx("div",{className:"section-label",style:{marginBottom:8},children:"Env File"}),c.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center"},children:[c.jsx("input",{ref:_,type:"file",accept:".env,.anyllm.env,text/plain",style:{display:"none"},onChange:pt}),c.jsx("button",{className:"btn btn-secondary btn-sm",onClick:()=>_.current?.click(),disabled:N.isPending,children:N.isPending?"Importing…":"Import .anyllm.env"}),c.jsx("button",{className:"btn btn-secondary btn-sm",onClick:qt,children:"Export .anyllm.env"})]}),R&&c.jsxs("div",{style:{marginTop:10},children:[c.jsxs("div",{className:"dim",style:{marginBottom:4},children:[R.applied," variable",R.applied!==1?"s":""," imported.",R.warnings.length===0&&" No issues."]}),R.warnings.length>0&&c.jsxs("div",{style:{marginTop:8,padding:"8px 12px",background:"var(--warn-dim)",borderLeft:"3px solid var(--warn)",borderRadius:"var(--r)",fontSize:12},children:[c.jsx("div",{style:{fontWeight:600,marginBottom:4},children:"Warnings"}),R.warnings.map((B,it)=>c.jsxs("div",{className:"mono",style:{fontSize:12},children:[B.line!=null&&c.jsxs("span",{className:"dim",children:["[line ",B.line,"] "]}),B.key&&c.jsxs("span",{children:[B.key,": "]}),B.message]},it))]})]}),V&&c.jsxs("div",{style:{marginTop:10,padding:"8px 12px",background:"var(--err-dim)",borderLeft:"3px solid var(--err)",borderRadius:"var(--r)",fontSize:12},children:[c.jsx("div",{style:{fontWeight:600,marginBottom:4},children:"Import rejected"}),V.hard_errors.map((B,it)=>c.jsx("div",{className:"mono",style:{fontSize:12},children:B},it)),V.warnings.length>0&&c.jsxs(c.Fragment,{children:[c.jsx("div",{style:{fontWeight:600,marginTop:8,marginBottom:4},children:"Warnings (from partial parse)"}),V.warnings.map((B,it)=>c.jsxs("div",{className:"mono",style:{fontSize:12},children:[B.line!=null&&c.jsxs("span",{className:"dim",children:["[line ",B.line,"] "]}),B.message]},it))]})]}),Y&&c.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: ",Y]})]}),c.jsx(sl,{loading:o,error:f?.message}),u&&c.jsx("div",{children:u.entries.map(B=>c.jsxs("div",{className:"form-group",children:[c.jsx("div",{className:"form-label",children:B.key}),c.jsxs("div",{className:"form-row",children:[c.jsx("input",{value:b[B.key]??B.value,onChange:it=>U(vt=>({...vt,[B.key]:it.target.value}))}),c.jsx("button",{className:"btn btn-primary btn-sm",onClick:()=>ut(B.key,B.value),children:"Save"}),c.jsx("button",{className:"btn btn-secondary btn-sm",onClick:()=>E.mutate(B.key),children:"Reset"})]})]},B.key))}),h&&c.jsxs("div",{className:"readonly-section",style:{marginTop:16},children:[c.jsx("div",{className:"section-label",children:"Environment"}),c.jsx("div",{style:{display:"grid",gridTemplateColumns:"220px 1fr",gap:"4px 12px",marginTop:8,fontSize:12},children:Object.entries(h).map(([B,it])=>c.jsxs(G.Fragment,{children:[c.jsx("span",{className:"dim",children:B}),c.jsx("span",{className:"mono",children:it})]},B))})]})]})}const B0={ok:"var(--ok)",warn:"var(--warn)",err:"var(--err)",dim:"var(--text-3)"};function Ev({status:s,pulse:u}){return c.jsx("span",{style:{display:"inline-block",width:7,height:7,borderRadius:"50%",background:B0[s],animation:u?"pulse 2s ease-in-out infinite":void 0,verticalAlign:"middle",marginRight:6}})}function H0(){const{data:s,isLoading:u,error:o}=gv();return c.jsxs("div",{children:[c.jsx(sl,{loading:u,error:o?.message,empty:s?.length===0}),c.jsx("div",{className:"backend-cards",children:s?.map(f=>c.jsxs("div",{className:"card",children:[c.jsxs("div",{className:"card-header",children:[c.jsx("span",{className:"card-name",children:f.name}),c.jsx(Ev,{status:f.status==="ok"?"ok":"err",pulse:f.status==="ok"})]}),c.jsxs("div",{className:"card-body",children:[c.jsx("div",{className:"mono",children:f.model}),c.jsxs("div",{style:{marginTop:6,display:"grid",gridTemplateColumns:"1fr 1fr",gap:4},children:[c.jsx("span",{className:"dim",children:"Requests"}),c.jsx("span",{className:"mono",children:f.requests_total}),c.jsx("span",{className:"dim",children:"P50"}),c.jsxs("span",{className:"mono",children:[f.p50_ms,"ms"]}),c.jsx("span",{className:"dim",children:"P95"}),c.jsxs("span",{className:"mono",children:[f.p95_ms,"ms"]}),c.jsx("span",{className:"dim",children:"Errors"}),c.jsx("span",{className:"mono",style:{color:f.requests_err>0?"var(--err)":void 0},children:f.requests_err})]})]})]},f.name))})]})}function Q0({variant:s}){return c.jsx("span",{className:`badge badge-${s}`,children:s})}function L0({spent:s,limit:u}){if(!u)return c.jsx("span",{className:"dim",children:"—"});const o=Math.min(s/u*100,100),f=o>=95?"danger":o>=80?"warn":"";return c.jsxs("div",{children:[c.jsx("div",{className:"budget-bar",children:c.jsx("div",{className:`budget-bar-fill${f?` ${f}`:""}`,style:{width:`${o}%`}})}),c.jsxs("span",{className:"dim",style:{fontSize:10},children:["$",s.toFixed(4)," / $",u.toFixed(2)]})]})}function Y0({onCreated:s}){const u=a0(),[o,f]=G.useState(""),[h,g]=G.useState(""),[E,N]=G.useState("");function _(){u.mutate({description:o||null,spend_limit:h?Number(h):null,rpm_limit:E?Number(E):null},{onSuccess:b=>{f(""),g(""),N(""),s(b.key)}})}return c.jsxs("div",{className:"form-group",children:[c.jsx("div",{className:"form-label",children:"Create Key"}),c.jsx("form",{onSubmit:b=>{b.preventDefault(),_()},children:c.jsxs("div",{className:"form-row",style:{flexWrap:"wrap"},children:[c.jsx("input",{placeholder:"Description",value:o,onChange:b=>f(b.target.value)}),c.jsx("input",{placeholder:"Spend limit USD",type:"number",value:h,onChange:b=>g(b.target.value),style:{width:120}}),c.jsx("input",{placeholder:"RPM limit",type:"number",value:E,onChange:b=>N(b.target.value),style:{width:100}}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:u.isPending,children:u.isPending?"Creating…":"Create"})]})})]})}function G0({vk:s,onClose:u}){const o=n0(),f=i0(),[h,g]=G.useState(s.description??""),[E,N]=G.useState(s.spend_limit?.toString()??""),[_,b]=G.useState(s.rpm_limit?.toString()??"");function U(){o.mutate({id:s.id,body:{description:h||null,spend_limit:E?Number(E):null,rpm_limit:_?Number(_):null}},{onSuccess:u})}function R(){confirm("Revoke this key?")&&f.mutate(s.id,{onSuccess:u})}return c.jsx("div",{className:"modal-backdrop",onClick:u,children:c.jsxs("div",{className:"modal",onClick:q=>q.stopPropagation(),children:[c.jsxs("div",{className:"modal-title",children:["Edit Key — ",s.key_prefix,"…"]}),c.jsxs("div",{className:"form-group",children:[c.jsx("div",{className:"form-label",children:"Description"}),c.jsx("input",{value:h,onChange:q=>g(q.target.value),style:{width:"100%"}})]}),c.jsxs("div",{className:"form-group",children:[c.jsx("div",{className:"form-label",children:"Spend limit (USD)"}),c.jsx("input",{value:E,onChange:q=>N(q.target.value),type:"number",min:"0",step:"0.01"})]}),c.jsxs("div",{className:"form-group",children:[c.jsx("div",{className:"form-label",children:"RPM limit"}),c.jsx("input",{value:_,onChange:q=>b(q.target.value),type:"number",min:"0"})]}),c.jsxs("div",{className:"form-row",children:[c.jsx("button",{className:"btn btn-primary",onClick:U,children:"Save"}),c.jsx("button",{className:"btn btn-secondary",onClick:u,children:"Cancel"}),c.jsx("button",{className:"btn btn-danger",style:{marginLeft:"auto"},onClick:R,children:"Revoke"})]})]})})}function X0(){const{data:s,isLoading:u,error:o}=l0(),[f,h]=G.useState(null),[g,E]=G.useState(null);return c.jsxs("div",{children:[c.jsx(Y0,{onCreated:h}),f&&c.jsxs("div",{className:"key-result",children:[c.jsx("div",{className:"key-result-label",children:"New key (copy now — not shown again)"}),f]}),c.jsx(sl,{loading:u,error:o?.message,empty:s?.length===0,message:"No keys"}),s&&s.length>0&&c.jsxs("table",{className:"keys-grid",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Prefix"}),c.jsx("th",{children:"Description"}),c.jsx("th",{children:"Status"}),c.jsx("th",{children:"Spend"}),c.jsx("th",{children:"Requests"}),c.jsx("th",{children:"Created"})]})}),c.jsx("tbody",{children:s.map(N=>c.jsxs("tr",{style:{cursor:"pointer"},onClick:()=>E(N),children:[c.jsxs("td",{className:"mono",children:[N.key_prefix,"…"]}),c.jsx("td",{className:"dim",children:N.description??"—"}),c.jsx("td",{children:c.jsx(Q0,{variant:N.status})}),c.jsx("td",{children:c.jsx(L0,{spent:N.total_spend,limit:N.spend_limit})}),c.jsx("td",{className:"mono",children:N.total_requests.toLocaleString()}),c.jsx("td",{className:"mono dim",children:N.created_at.slice(0,10)})]},N.id))})]}),g&&c.jsx(G0,{vk:g,onClose:()=>E(null)},g.id)]})}const Km={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 K0(){return c.jsx("svg",{width:"12",height:"12",viewBox:"0 0 16 16",fill:"none",style:{verticalAlign:"-1px",marginRight:3},children:c.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 Z0(){const{data:s,isLoading:u,error:o}=f0(),f=o0(),h=d0(),g=h0(),{data:E}=gv(),{data:N}=Sv(),[_,b]=G.useState(""),[U,R]=G.useState(""),[q,V]=G.useState("openai"),[W,Y]=G.useState(""),[I,ft]=G.useState("openrouter"),[yt,ut]=G.useState(""),pt=Km[I]??Km.custom;function qt(){g.mutate({source:I,...I==="custom"?{url:yt}:{}})}return c.jsxs("div",{children:[c.jsxs("div",{style:{marginBottom:20},children:[c.jsx("div",{className:"section-label",style:{marginBottom:8},children:"Discover Models"}),c.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[c.jsxs("select",{value:I,onChange:Z=>{ft(Z.target.value),g.reset()},children:[c.jsx("option",{value:"openrouter",children:"OpenRouter"}),c.jsx("option",{value:"deepinfra",children:"DeepInfra"}),c.jsx("option",{value:"ollama",children:"Ollama (local)"}),c.jsx("option",{value:"configured",children:"Configured backend"}),c.jsx("option",{value:"custom",children:"Custom URL"})]}),I==="custom"&&c.jsx("input",{placeholder:"https://api.example.com",value:yt,onChange:Z=>ut(Z.target.value),style:{minWidth:220}}),c.jsx("button",{className:"btn btn-secondary",onClick:qt,disabled:g.isPending||I==="custom"&&!yt,children:g.isPending?"Fetching...":"Fetch"}),c.jsxs("span",{className:"dim",style:{fontSize:12},children:[pt.needsKey&&c.jsx(K0,{}),pt.text]})]}),g.isError&&c.jsx("div",{style:{marginTop:8,padding:"6px 10px",background:"var(--err-dim)",borderLeft:"3px solid var(--err)",borderRadius:"var(--r)",fontSize:12},children:g.error.message}),g.data&&g.data.models.length>0&&c.jsxs("div",{style:{marginTop:8},children:[c.jsxs("div",{className:"dim",style:{fontSize:12,marginBottom:4},children:[g.data.models.length," model",g.data.models.length!==1?"s":""," found. Click to populate the form below."]}),c.jsx("div",{style:{maxHeight:200,overflowY:"auto",border:"1px solid var(--border)",borderRadius:"var(--r)",fontSize:12},children:g.data.models.map(Z=>c.jsxs("div",{onClick:()=>R(Z.id),style:{padding:"4px 8px",cursor:"pointer",borderBottom:"1px solid var(--border)",background:U===Z.id?"var(--accent-dim)":void 0},onMouseEnter:B=>{B.target.style.background="var(--surface-2)"},onMouseLeave:B=>{B.target.style.background=U===Z.id?"var(--accent-dim)":""},children:[c.jsx("span",{className:"mono",children:Z.id}),Z.name&&Z.name!==Z.id&&c.jsx("span",{className:"dim",style:{marginLeft:8},children:Z.name})]},Z.id))})]}),g.data&&g.data.models.length===0&&c.jsx("div",{className:"dim",style:{marginTop:8,fontSize:12},children:"No models returned."})]}),c.jsxs("datalist",{id:"backends-list",children:[E?.map(Z=>c.jsx("option",{value:Z.name,children:Z.name},Z.name)),N?.backends.map(Z=>c.jsxs("option",{value:Z.name,children:[Z.name," (managed)"]},`managed-${Z.name}`))]}),c.jsxs("div",{className:"form-group",children:[c.jsx("div",{className:"form-label",children:"Add Model"}),c.jsxs("div",{className:"form-row",style:{flexWrap:"wrap"},children:[c.jsx("input",{placeholder:"Virtual name",value:_,onChange:Z=>b(Z.target.value)}),c.jsx("input",{placeholder:"Model ID",value:U,onChange:Z=>R(Z.target.value)}),c.jsxs("select",{value:q,onChange:Z=>V(Z.target.value),children:[c.jsx("option",{value:"openai",children:"openai"}),c.jsx("option",{value:"anthropic",children:"anthropic"}),c.jsx("option",{value:"gemini",children:"gemini"}),c.jsx("option",{value:"vertex",children:"vertex"}),c.jsx("option",{value:"azure",children:"azure"}),c.jsx("option",{value:"bedrock",children:"bedrock"})]}),c.jsx("input",{placeholder:"Backend (optional)",value:W,onChange:Z=>Y(Z.target.value),list:"backends-list"}),c.jsx("button",{className:"btn btn-primary",onClick:()=>f.mutate({name:_,model:U,provider:q,...W?{backend_name:W}:{}}),disabled:!_||!U||f.isPending,children:"Add"})]})]}),c.jsx(sl,{loading:u,error:o?.message}),s&&c.jsxs("table",{className:"route-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Virtual Name"}),c.jsx("th",{children:"Model"}),c.jsx("th",{children:"Provider"}),c.jsx("th",{children:"Strategy"}),c.jsx("th",{})]})}),c.jsx("tbody",{children:s.models.map(Z=>c.jsxs("tr",{children:[c.jsx("td",{className:"mono",children:Z.name}),c.jsx("td",{className:"mono",children:Z.model}),c.jsx("td",{className:"dim",children:Z.provider}),c.jsx("td",{className:"dim",children:s.routing_strategy}),c.jsx("td",{children:c.jsx("button",{className:"btn btn-danger btn-sm",onClick:()=>h.mutate(Z.name),children:"Remove"})})]},`${Z.name}-${Z.model}`))})]})]})}function w0(){const[s,u]=G.useState(1),{data:o,isLoading:f,error:h}=m0({page:s,page_size:50});return c.jsxs("div",{children:[c.jsx(sl,{loading:f,error:h?.message}),o&&c.jsxs(c.Fragment,{children:[c.jsxs("table",{className:"route-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Time"}),c.jsx("th",{children:"Action"}),c.jsx("th",{children:"Target"}),c.jsx("th",{children:"Detail"}),c.jsx("th",{children:"IP"})]})}),c.jsx("tbody",{children:o.entries.map(g=>c.jsxs("tr",{children:[c.jsx("td",{className:"mono dim",children:g.timestamp.slice(0,19)}),c.jsx("td",{className:"mono",children:g.action}),c.jsxs("td",{className:"dim",children:[g.target_type,g.target_id?` #${g.target_id}`:""]}),c.jsx("td",{className:"dim",style:{maxWidth:300,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:g.detail??"—"}),c.jsx("td",{className:"mono dim",children:g.source_ip??"—"})]},g.id))})]}),c.jsx(jv,{page:s,hasMore:o.has_more,onPrev:()=>u(g=>Math.max(1,g-1)),onNext:()=>u(g=>g+1)})]})]})}function V0({routes:s}){const u=[...s].sort((o,f)=>f.requests_per_min-o.requests_per_min);return c.jsxs("table",{className:"route-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Route"}),c.jsx("th",{children:"Req/min"}),c.jsx("th",{children:"Error rate"}),c.jsx("th",{children:"Avg latency"}),c.jsx("th",{children:"P95 latency"}),c.jsx("th",{children:"Total"})]})}),c.jsx("tbody",{children:u.map(o=>c.jsxs("tr",{children:[c.jsx("td",{className:"mono",children:o.path}),c.jsx("td",{className:"mono",children:o.requests_per_min.toFixed(2)}),c.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),"%"]}),c.jsxs("td",{className:"mono",children:[o.avg_latency_ms.toFixed(0),"ms"]}),c.jsxs("td",{className:"mono",children:[o.p95_latency_ms,"ms"]}),c.jsx("td",{className:"mono",children:o.total_requests.toLocaleString()})]},o.path))})]})}const Zm=["#e8a030","#d4922b","#c07820","#a86015","#8c500a"],wm=["#6eb5c0","#5aa0ab","#468b96","#327681","#1e616c"];function J0(){const[s,u]=G.useState(6),{data:o,isLoading:f,error:h}=v0(s),g=o?.routes??[],E=g.slice(0,5).map((N,_)=>{const b=(o?.series??[]).filter(U=>U.path===N.path).map(U=>U.requests);return{label:N.path,color:Zm[_%Zm.length],data:b}});return c.jsxs("div",{children:[c.jsxs("div",{className:"section-header",children:[c.jsx("span",{className:"section-label",children:"Traffic"}),c.jsxs("select",{value:s,onChange:N=>u(Number(N.target.value)),children:[c.jsx("option",{value:1,children:"Last 1 hour"}),c.jsx("option",{value:6,children:"Last 6 hours"}),c.jsx("option",{value:24,children:"Last 24 hours"})]})]}),c.jsx(sl,{loading:f,error:h?.message}),o&&c.jsxs(c.Fragment,{children:[c.jsx(V0,{routes:o.routes}),c.jsxs("div",{className:"operator-grid",style:{marginTop:16},children:[c.jsxs("div",{className:"chart-card",children:[c.jsx("div",{className:"chart-header",children:c.jsxs("div",{children:[c.jsx("div",{className:"chart-title",children:"Requests / min by route"}),c.jsx("div",{className:"chart-subtitle",children:"Stacked over time window"})]})}),c.jsx(vs,{series:E})]}),c.jsxs("div",{className:"chart-card",children:[c.jsx("div",{className:"chart-header",children:c.jsxs("div",{children:[c.jsx("div",{className:"chart-title",children:"Avg latency per route"}),c.jsx("div",{className:"chart-subtitle",children:"ms"})]})}),g.length===0?c.jsx("div",{className:"empty",children:"No routes"}):c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:8,paddingTop:8},children:g.slice(0,5).map((N,_)=>{const b=Math.max(...g.slice(0,5).map(R=>R.avg_latency_ms),1),U=N.avg_latency_ms/b*100;return c.jsxs("div",{children:[c.jsxs("div",{style:{display:"flex",justifyContent:"space-between",fontSize:11,marginBottom:2},children:[c.jsx("span",{className:"mono dim",style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"70%"},children:N.path}),c.jsxs("span",{className:"mono",children:[N.avg_latency_ms.toFixed(0),"ms"]})]}),c.jsx("div",{style:{height:6,background:"var(--border)",borderRadius:0},children:c.jsx("div",{style:{height:"100%",width:`${U}%`,background:wm[_%wm.length],borderRadius:0}})})]},N.path)})})]})]})]})]})}function k0(s){const u=Math.floor(Date.now()/1e3-s),o=Math.floor(u/86400),f=Math.floor(u%86400/3600),h=Math.floor(u%3600/60);return o>0?`${o}d ${f}h ${h}m`:f>0?`${f}h ${h}m`:`${h}m`}function F0({proxy:s}){return c.jsxs("div",{className:"uptime-proxy",children:[c.jsxs("div",{className:"uptime-proxy-stats",children:[c.jsxs("div",{children:[c.jsx("div",{className:"section-label",children:"Uptime (30d)"}),c.jsxs("div",{className:"uptime-pct",children:[s.uptime_pct_30d.toFixed(2),"%"]})]}),c.jsxs("div",{children:[c.jsx("div",{className:"section-label",children:"Running"}),c.jsx("div",{className:"stat-value",style:{fontSize:16},children:k0(s.started_at)})]})]}),c.jsx("div",{className:"section-label",style:{marginBottom:4},children:"30-day history"}),c.jsx("div",{className:"history-bar",children:s.history.map(u=>c.jsx("div",{className:`history-day ${u.status}`,title:`${u.date}: ${u.status}`},u.date))})]})}function W0({b:s}){const u=s.status==="up"?"ok":s.status==="down"?"err":"dim",o=s.last_checked_at?new Date(s.last_checked_at*1e3).toLocaleTimeString():"—";return c.jsxs("tr",{children:[c.jsx("td",{className:"mono",children:s.name}),c.jsxs("td",{children:[c.jsx(Ev,{status:u,pulse:s.status==="up"}),s.status]}),c.jsxs("td",{className:"mono",children:[s.uptime_pct_30d.toFixed(2),"%"]}),c.jsx("td",{className:"mono dim",children:o}),c.jsx("td",{className:"mono dim",children:s.last_latency_ms!=null?`${s.last_latency_ms}ms`:"—"}),c.jsx("td",{children:c.jsx("div",{className:"history-bar",style:{height:12},children:s.history.map(f=>c.jsx("div",{className:`history-day ${f.status}`,title:`${f.date}: ${f.status}`},f.date))})})]})}function $0(){const{data:s,isLoading:u,error:o}=y0();return c.jsxs("div",{children:[c.jsx(sl,{loading:u,error:o?.message}),s&&c.jsxs(c.Fragment,{children:[c.jsx(F0,{proxy:s.proxy}),c.jsx("div",{className:"section-label",style:{marginTop:16,marginBottom:8},children:"Backend Availability"}),c.jsxs("table",{className:"backend-health-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Backend"}),c.jsx("th",{children:"Status"}),c.jsx("th",{children:"Uptime (30d)"}),c.jsx("th",{children:"Last checked"}),c.jsx("th",{children:"Latency"}),c.jsx("th",{children:"History"})]})}),c.jsx("tbody",{children:s.backends.slice().sort((f,h)=>f.name.localeCompare(h.name)).map(f=>c.jsx(W0,{b:f},f.name))})]})]})]})}function I0(){const s=Rl(b=>b.token),u=Rl(b=>b.login),o=ma(b=>b.lastEvent),f=Ue(),[h,g]=G.useState("dashboard"),[E,N]=G.useState(!0),{data:_}=Ig(!!s);return G.useEffect(()=>{const U=new URLSearchParams(location.search).get("token");U&&!s?fetch("/admin/api/metrics",{headers:{Authorization:`Bearer ${U}`}}).then(R=>{R.ok&&(u(U),history.replaceState(null,"",location.pathname))}).catch(()=>{}).finally(()=>N(!1)):N(!1)},[]),G.useEffect(()=>{s?Fg():Wg()},[s]),G.useEffect(()=>{s&&_&&!_.configured&&g("settings")},[_?.configured,s]),G.useEffect(()=>{o&&(o.type==="metrics_snapshot"?f.setQueryData(["metrics"],o.data):o.type==="backend_health_changed"&&f.invalidateQueries({queryKey:["uptime"]}))},[o,f]),E?null:s?c.jsxs("div",{children:[c.jsx(_0,{activeTab:h,onTabChange:g}),c.jsxs("div",{className:"tab-content",children:[h==="dashboard"&&c.jsx(M0,{}),h==="requests"&&c.jsx(C0,{}),h==="settings"&&c.jsx(q0,{configured:_?.configured??!0}),h==="backends"&&c.jsx(H0,{}),h==="keys"&&c.jsx(X0,{}),h==="models"&&c.jsx(Z0,{}),h==="audit"&&c.jsx(w0,{}),h==="traffic"&&c.jsx(J0,{}),h==="uptime"&&c.jsx($0,{})]})]}):c.jsx(j0,{})}const P0=new Ag({defaultOptions:{queries:{retry:1,refetchOnWindowFocus:!1}}});ag.createRoot(document.getElementById("root")).render(c.jsx(G.StrictMode,{children:c.jsx(zg,{client:P0,children:c.jsx(I0,{})})})); diff --git a/crates/proxy/admin-ui/src/api/types.ts b/crates/proxy/admin-ui/src/api/types.ts index 188b04f..055b16b 100644 --- a/crates/proxy/admin-ui/src/api/types.ts +++ b/crates/proxy/admin-ui/src/api/types.ts @@ -243,11 +243,23 @@ export interface EnvImportError { export interface CatalogProvider { id: string - name: string + display_name: string + name?: string // keep for backwards compat if anything uses it protocol: string auth: string + status: 'implemented' | 'wired' | 'stub' default_base_url: string env_vars: string[] + litellm_prefix: string + capabilities: { + chat_completions: boolean + streaming: boolean + tool_use: boolean + embeddings: boolean + vision: boolean + batch: boolean + } + model_count: number } // --- Managed backends --- diff --git a/crates/proxy/admin-ui/src/tabs/settings/BackendForm.tsx b/crates/proxy/admin-ui/src/tabs/settings/BackendForm.tsx index ca4f01b..81a423a 100644 --- a/crates/proxy/admin-ui/src/tabs/settings/BackendForm.tsx +++ b/crates/proxy/admin-ui/src/tabs/settings/BackendForm.tsx @@ -128,9 +128,17 @@ export function BackendForm({ initial, onSuccess, onCancel }: BackendFormProps) {providers.length === 0 && ( )} - {providers.map(p => ( - - ))} + {(['implemented', 'wired', 'stub'] as const).map(status => { + const group = providers.filter(p => p.status === status) + if (group.length === 0) return null + return ( + + {group.map(p => ( + + ))} + + ) + })} diff --git a/crates/proxy/src/admin/routes/catalog.rs b/crates/proxy/src/admin/routes/catalog.rs new file mode 100644 index 0000000..fadad43 --- /dev/null +++ b/crates/proxy/src/admin/routes/catalog.rs @@ -0,0 +1,133 @@ +use anyllm_providers::{ + all_providers, get_provider, list_models, + model::ModelStatus, + provider::{AuthKind, ProviderProtocol, ProviderStatus}, +}; +use axum::{extract::Path, http::StatusCode, response::IntoResponse, Json}; + +fn protocol_str(p: ProviderProtocol) -> &'static str { + match p { + ProviderProtocol::OpenAICompat => "openai_compat", + ProviderProtocol::AzureOpenAI => "azure_openai", + ProviderProtocol::VertexAI => "vertex_ai", + ProviderProtocol::GeminiOpenAI => "gemini_openai", + ProviderProtocol::GeminiNative => "gemini_native", + ProviderProtocol::AnthropicNative => "anthropic_native", + ProviderProtocol::BedrockNative => "bedrock_native", + ProviderProtocol::Custom => "custom", + } +} + +fn auth_str(a: AuthKind) -> &'static str { + match a { + AuthKind::Bearer => "bearer", + AuthKind::GoogleApiKey => "google_api_key", + AuthKind::AzureApiKey => "azure_api_key", + AuthKind::AwsSigV4 => "aws_sigv4", + AuthKind::None => "none", + } +} + +fn provider_status_str(s: ProviderStatus) -> &'static str { + match s { + ProviderStatus::Implemented => "implemented", + ProviderStatus::Wired => "wired", + ProviderStatus::Stub => "stub", + } +} + +fn model_status_str(s: ModelStatus) -> &'static str { + match s { + ModelStatus::Available => "available", + ModelStatus::Deprecated => "deprecated", + ModelStatus::Stub => "stub", + } +} + +/// GET /admin/api/catalog/providers +/// +/// Returns all registered providers with metadata from the compile-time registry. +/// No SharedState needed — data is all static. +pub(super) async fn list_providers() -> impl IntoResponse { + let providers: Vec = all_providers() + .map(|p| { + let model_count = list_models(p.id).len(); + serde_json::json!({ + "id": p.id, + "display_name": p.display_name, + "protocol": protocol_str(p.protocol), + "auth": auth_str(p.auth), + "status": provider_status_str(p.status), + "default_base_url": p.default_base_url, + "env_vars": p.env_vars, + "litellm_prefix": p.litellm_prefix, + "capabilities": { + "chat_completions": p.capabilities.chat_completions, + "streaming": p.capabilities.streaming, + "tool_use": p.capabilities.tool_use, + "embeddings": p.capabilities.embeddings, + "vision": p.capabilities.vision, + "batch": p.capabilities.batch, + }, + "model_count": model_count, + }) + }) + .collect(); + + Json(serde_json::json!({ "providers": providers })).into_response() +} + +/// GET /admin/api/catalog/providers/{id}/models +/// +/// Returns all static models for the given provider, enriched with pricing data +/// from the embedded pricing table. Returns 404 for unknown provider ids. +pub(super) async fn list_provider_models(Path(provider_id): Path) -> impl IntoResponse { + if !super::is_safe_model_name(&provider_id) { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "invalid provider id" })), + ) + .into_response(); + } + + if get_provider(&provider_id).is_none() { + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": "provider not found" })), + ) + .into_response(); + } + + let models: Vec = list_models(&provider_id) + .iter() + .map(|m| { + let pricing = crate::cost::price_per_million_for_model(m.id).map(|(inp, out)| { + serde_json::json!({ + "input_per_million_tokens": inp, + "output_per_million_tokens": out, + }) + }); + serde_json::json!({ + "id": m.id, + "context_window": m.context_window, + "max_output_tokens": m.max_output_tokens, + "status": model_status_str(m.status), + "capabilities": { + "streaming": m.capabilities.streaming, + "tool_use": m.capabilities.tool_use, + "vision": m.capabilities.vision, + "extended_thinking": m.capabilities.extended_thinking, + }, + "pricing": pricing, + }) + }) + .collect(); + + let has_models = !models.is_empty(); + Json(serde_json::json!({ + "provider_id": provider_id, + "has_models": has_models, + "models": models, + })) + .into_response() +} diff --git a/crates/proxy/src/admin/routes/mod.rs b/crates/proxy/src/admin/routes/mod.rs index 03d8635..d84e954 100644 --- a/crates/proxy/src/admin/routes/mod.rs +++ b/crates/proxy/src/admin/routes/mod.rs @@ -1,6 +1,7 @@ // Admin server routes. Served on a separate localhost-only listener. pub mod audit; +pub mod catalog; pub mod config; pub mod env; pub mod keys; @@ -420,6 +421,11 @@ pub fn admin_router(shared: SharedState, token: Arc>) "/admin/api/mcp-servers/{name}", delete(mcp::remove_mcp_server), ) + .route("/admin/api/catalog/providers", get(catalog::list_providers)) + .route( + "/admin/api/catalog/providers/:id/models", + get(catalog::list_provider_models), + ) .route("/admin/api/status", get(status::get_status)) .route("/admin/api/traffic", get(traffic::get_traffic)) .route("/admin/api/uptime", get(uptime::get_uptime)) diff --git a/crates/proxy/src/cost/mod.rs b/crates/proxy/src/cost/mod.rs index 188217c..8307e9e 100644 --- a/crates/proxy/src/cost/mod.rs +++ b/crates/proxy/src/cost/mod.rs @@ -104,6 +104,15 @@ pub fn pricing() -> &'static ModelPricing { &PRICING } +/// Return (input_per_million, output_per_million) for a model, or None if unknown. +/// Costs are scaled to per-million for human-readable display; the underlying +/// table stores per-token values. +pub fn price_per_million_for_model(model_id: &str) -> Option<(f64, f64)> { + pricing() + .price_for_model(model_id) + .map(|(i, o)| (i * 1_000_000.0, o * 1_000_000.0)) +} + /// A single pricing record loaded from the JSON pricing table. /// `model_pattern` supports exact match and longest-prefix matching. #[derive(Debug, Clone, serde::Deserialize)]