/ frontend / src / app / views / llms / components / LLMEdit.jsx
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  }