LLMEdit.jsx
1 import { Card, Divider, Box, Grid, TextField, Button, Typography, MenuItem, CircularProgress, Chip } from "@mui/material"; 2 import { H4 } from "app/components/Typography"; 3 import { useState, useEffect } from "react"; 4 import useAuth from "app/hooks/useAuth"; 5 import { useNavigate } from "react-router-dom"; 6 import { useTranslation } from "react-i18next"; 7 import { JsonEditor } from 'json-edit-react'; 8 import { PROVIDER_CONFIG } from '../providerConfig'; 9 import api from "app/utils/api"; 10 11 const OPENAI_COMPAT_CLASSES = new Set([ 12 "OpenAI", "OpenAILike", "LiteLLM", "vLLM", "Grok", "Gemini", "GeminiMultiModal", 13 ]); 14 15 export default function LLMEdit({ llm }) { 16 const { t } = useTranslation(); 17 const auth = useAuth(); 18 const [state, setState] = useState({}); 19 const [remoteModels, setRemoteModels] = useState(null); 20 const [loadingModels, setLoadingModels] = useState(false); 21 const navigate = useNavigate(); 22 23 const handleSubmit = (event) => { 24 event.preventDefault(); 25 26 var update = {}; 27 28 if (state.name !== llm.name) { 29 update.name = state.name; 30 } 31 if (state.class_name !== llm.class_name) { 32 update.class_name = state.class_name; 33 } 34 if (state.options !== llm.options) { 35 update.options = state.options; 36 } 37 if (state.privacy !== llm.privacy) { 38 update.privacy = state.privacy; 39 } 40 if (state.description !== llm.description) { 41 update.description = state.description; 42 } 43 if (state.input_cost !== llm.input_cost) { 44 update.input_cost = state.input_cost; 45 } 46 if (state.output_cost !== llm.output_cost) { 47 update.output_cost = state.output_cost; 48 } 49 if (state.context_window !== llm.context_window) { 50 update.context_window = parseInt(state.context_window); 51 } 52 53 api.patch("/llms/" + llm.id, update, auth.user.token) 54 .then(() => { 55 window.location.href = "/admin/llm/" + llm.id; 56 }).catch(() => {}); 57 } 58 59 const handleChange = (event) => { 60 if (event && event.persist) event.persist(); 61 setState({ ...state, [event.target.name]: event.target.value }); 62 }; 63 64 const handleListModels = async () => { 65 setLoadingModels(true); 66 setRemoteModels(null); 67 try { 68 const result = await api.get("/tools/openai-compat/models/" + llm.id, auth.user.token); 69 setRemoteModels(result.models || []); 70 } catch (err) { 71 setRemoteModels([]); 72 } finally { 73 setLoadingModels(false); 74 } 75 }; 76 77 const handleSelectModel = (modelId) => { 78 setState({ 79 ...state, 80 options: { ...state.options, model: modelId }, 81 }); 82 setRemoteModels(null); 83 }; 84 85 const canListModels = OPENAI_COMPAT_CLASSES.has(state.class_name); 86 87 useEffect(() => { 88 setState(llm); 89 }, [llm]); 90 91 return ( 92 <Card elevation={3}> 93 <H4 p={2}>{t("llms.edit.title", { name: llm.name })}</H4> 94 95 <Divider sx={{ mb: 1 }} /> 96 97 <form onSubmit={handleSubmit}> 98 <Box margin={3}> 99 <Grid container spacing={3}> 100 <Grid item sm={6} xs={12}> 101 <TextField 102 fullWidth 103 InputLabelProps={{ shrink: true }} 104 name="name" 105 label={t("llms.edit.name")} 106 variant="outlined" 107 onChange={handleChange} 108 value={state.name} 109 /> 110 </Grid> 111 112 <Grid item sm={6} xs={12}> 113 <TextField 114 fullWidth 115 select 116 InputLabelProps={{ shrink: true }} 117 name="class_name" 118 label={t("llms.edit.className")} 119 variant="outlined" 120 onChange={handleChange} 121 value={state.class_name || ""} 122 > 123 {Object.entries(PROVIDER_CONFIG).map(([key, config]) => ( 124 <MenuItem key={key} value={key}>{config.label}</MenuItem> 125 ))} 126 </TextField> 127 </Grid> 128 129 <Grid item sm={6} xs={12}> 130 <Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 1 }}> 131 <Typography variant="h6">{t("llms.edit.options")}</Typography> 132 {canListModels && ( 133 <Button 134 size="small" 135 variant="outlined" 136 onClick={handleListModels} 137 disabled={loadingModels} 138 startIcon={loadingModels ? <CircularProgress size={16} /> : null} 139 > 140 {loadingModels ? t("llms.edit.loading") : t("llms.edit.listModels")} 141 </Button> 142 )} 143 </Box> 144 145 {remoteModels && remoteModels.length > 0 && ( 146 <Box sx={{ 147 mb: 2, p: 1.5, borderRadius: 1, 148 border: "1px solid", borderColor: "divider", 149 maxHeight: 200, overflowY: "auto", 150 }}> 151 <Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 1 }}> 152 {t("llms.edit.modelsAvailable", { count: remoteModels.length })} 153 </Typography> 154 <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}> 155 {remoteModels.map((m) => ( 156 <Chip 157 key={m.id} 158 label={m.id} 159 size="small" 160 variant={state.options?.model === m.id ? "filled" : "outlined"} 161 color={state.options?.model === m.id ? "primary" : "default"} 162 onClick={() => handleSelectModel(m.id)} 163 sx={{ cursor: "pointer" }} 164 /> 165 ))} 166 </Box> 167 </Box> 168 )} 169 170 {remoteModels && remoteModels.length === 0 && !loadingModels && ( 171 <Typography variant="body2" color="error" sx={{ mb: 2 }}> 172 {t("llms.edit.modelsNone")} 173 </Typography> 174 )} 175 176 <JsonEditor 177 data={state.options || {}} 178 setData={(updatedOptions) => setState({ ...state, options: updatedOptions })} 179 restrictDelete={false} 180 rootName={t("llms.edit.options")} 181 numberType="float" 182 /> 183 </Grid> 184 185 <Grid item sm={6} xs={12}> 186 <TextField 187 fullWidth 188 select 189 InputLabelProps={{ shrink: true }} 190 name="privacy" 191 label={t("llms.edit.privacy")} 192 variant="outlined" 193 onChange={handleChange} 194 value={state.privacy || ""} 195 > 196 {["public", "private"].map((p) => ( 197 <MenuItem key={p} value={p}>{p}</MenuItem> 198 ))} 199 </TextField> 200 </Grid> 201 202 <Grid item sm={6} xs={12}> 203 <TextField 204 fullWidth 205 InputLabelProps={{ shrink: true }} 206 name="description" 207 label={t("llms.edit.description")} 208 variant="outlined" 209 onChange={handleChange} 210 value={state.description} 211 /> 212 </Grid> 213 214 <Grid item sm={6} xs={12}> 215 <TextField 216 fullWidth 217 InputLabelProps={{ shrink: true }} 218 name="input_cost" 219 label={t("llms.edit.inputCost")} 220 variant="outlined" 221 onChange={handleChange} 222 value={state.input_cost} 223 /> 224 </Grid> 225 226 <Grid item sm={6} xs={12}> 227 <TextField 228 fullWidth 229 InputLabelProps={{ shrink: true }} 230 name="output_cost" 231 label={t("llms.edit.outputCost")} 232 variant="outlined" 233 onChange={handleChange} 234 value={state.output_cost} 235 /> 236 </Grid> 237 238 <Grid item sm={6} xs={12}> 239 <TextField 240 fullWidth 241 InputLabelProps={{ shrink: true }} 242 name="context_window" 243 label={t("llms.edit.contextWindow")} 244 type="number" 245 variant="outlined" 246 onChange={handleChange} 247 value={state.context_window} 248 helperText={t("llms.edit.contextHelp")} 249 /> 250 </Grid> 251 252 <Grid item xs={12}> 253 <Button type="submit" variant="contained"> 254 {t("llms.edit.saveChanges")} 255 </Button> 256 <Button variant="outlined" sx={{ ml: 2 }} onClick={() => { navigate("/llms") }}> 257 {t("common.cancel")} 258 </Button> 259 </Grid> 260 </Grid> 261 </Box> 262 </form> 263 </Card> 264 ); 265 }