Skip to content

Commit 40b6384

Browse files
committed
updates
1 parent 15826d5 commit 40b6384

File tree

6 files changed

+366
-18
lines changed

6 files changed

+366
-18
lines changed
Binary file not shown.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"filename": "backup_unicorn_db_20260218_172922.sql.gz",
3+
"timestamp": "20260218_172922",
4+
"created_at": "2026-02-18T17:29:23.301259",
5+
"database": "unicorn_db",
6+
"size_bytes": 71224,
7+
"size_mb": 0.07,
8+
"description": "Automated scheduled backup",
9+
"compressed": true
10+
}

backend/litellm_routing_api.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,15 @@ class ModelCreate(BaseModel):
263263
metadata: Dict = Field(default_factory=dict)
264264

265265

266+
class ModelUpdate(BaseModel):
267+
display_name: Optional[str] = None
268+
cost_per_1m_input_tokens: Optional[Decimal] = None
269+
cost_per_1m_output_tokens: Optional[Decimal] = None
270+
context_length: Optional[int] = None
271+
enabled: Optional[bool] = None
272+
metadata: Optional[Dict] = None
273+
274+
266275
class ModelResponse(BaseModel):
267276
id: str
268277
provider_id: str
@@ -771,6 +780,110 @@ async def create_model(model: ModelCreate):
771780
conn.close()
772781

773782

783+
@router.put("/models/{model_id}", response_model=Dict)
784+
async def update_model(model_id: str, model: ModelUpdate):
785+
"""
786+
Update an existing model's configuration
787+
788+
Path Parameters:
789+
model_id: Model UUID
790+
791+
Body:
792+
ModelUpdate schema (partial updates supported)
793+
794+
Returns:
795+
Updated model info
796+
"""
797+
conn = get_db_connection()
798+
cursor = conn.cursor(cursor_factory=RealDictCursor)
799+
800+
try:
801+
# Build dynamic SET clause from non-None fields
802+
updates = {}
803+
if model.display_name is not None:
804+
updates['display_name'] = model.display_name
805+
if model.cost_per_1m_input_tokens is not None:
806+
updates['cost_per_1m_input_tokens'] = model.cost_per_1m_input_tokens
807+
if model.cost_per_1m_output_tokens is not None:
808+
updates['cost_per_1m_output_tokens'] = model.cost_per_1m_output_tokens
809+
if model.context_length is not None:
810+
updates['context_length'] = model.context_length
811+
if model.enabled is not None:
812+
updates['enabled'] = model.enabled
813+
if model.metadata is not None:
814+
updates['metadata'] = Json(model.metadata)
815+
816+
if not updates:
817+
raise HTTPException(status_code=400, detail="No fields to update")
818+
819+
set_clause = ", ".join(f"{k} = %s" for k in updates.keys())
820+
values = list(updates.values())
821+
values.append(model_id)
822+
823+
cursor.execute(
824+
f"UPDATE llm_models SET {set_clause} WHERE id = %s RETURNING id, name, display_name",
825+
values
826+
)
827+
828+
result = cursor.fetchone()
829+
if not result:
830+
raise HTTPException(status_code=404, detail="Model not found")
831+
832+
conn.commit()
833+
834+
return {
835+
"id": str(result['id']),
836+
"name": result['name'],
837+
"display_name": result['display_name'],
838+
"message": "Model updated successfully"
839+
}
840+
841+
except HTTPException:
842+
raise
843+
except Exception as e:
844+
conn.rollback()
845+
logger.error(f"Failed to update model: {e}")
846+
raise HTTPException(status_code=500, detail=str(e))
847+
finally:
848+
cursor.close()
849+
conn.close()
850+
851+
852+
@router.delete("/models/{model_id}")
853+
async def delete_model(model_id: str):
854+
"""
855+
Delete a model
856+
857+
Path Parameters:
858+
model_id: Model UUID
859+
860+
Returns:
861+
Deletion confirmation
862+
"""
863+
conn = get_db_connection()
864+
cursor = conn.cursor()
865+
866+
try:
867+
cursor.execute("DELETE FROM llm_models WHERE id = %s RETURNING id", (model_id,))
868+
result = cursor.fetchone()
869+
870+
if not result:
871+
raise HTTPException(status_code=404, detail="Model not found")
872+
873+
conn.commit()
874+
return {"message": "Model deleted successfully", "id": model_id}
875+
876+
except HTTPException:
877+
raise
878+
except Exception as e:
879+
conn.rollback()
880+
logger.error(f"Failed to delete model: {e}")
881+
raise HTTPException(status_code=500, detail=str(e))
882+
finally:
883+
cursor.close()
884+
conn.close()
885+
886+
774887
# ============================================================================
775888
# Routing Rules Endpoints
776889
# ============================================================================
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import React, { useState, useEffect } from 'react';
2+
import {
3+
Dialog,
4+
DialogTitle,
5+
DialogContent,
6+
DialogActions,
7+
TextField,
8+
Button,
9+
Box,
10+
Alert,
11+
FormControlLabel,
12+
Switch
13+
} from '@mui/material';
14+
15+
/**
16+
* ModelEditModal Component
17+
*
18+
* Modal for editing an existing model's configuration
19+
*/
20+
export default function ModelEditModal({ open, onClose, onSave, model }) {
21+
const [formData, setFormData] = useState({
22+
display_name: '',
23+
context_length: 4096,
24+
cost_per_1m_input_tokens: 0,
25+
cost_per_1m_output_tokens: 0,
26+
enabled: true
27+
});
28+
const [error, setError] = useState('');
29+
const [loading, setLoading] = useState(false);
30+
31+
// Populate form when model changes or dialog opens
32+
useEffect(() => {
33+
if (model && open) {
34+
setFormData({
35+
display_name: model.display_name || model.name || '',
36+
context_length: model.context_length || 4096,
37+
cost_per_1m_input_tokens: model.cost_per_1m_input_tokens ?? (model.cost_per_input_token ? model.cost_per_input_token * 1_000_000 : 0),
38+
cost_per_1m_output_tokens: model.cost_per_1m_output_tokens ?? (model.cost_per_output_token ? model.cost_per_output_token * 1_000_000 : 0),
39+
enabled: model.enabled !== false && model.status !== 'inactive'
40+
});
41+
setError('');
42+
}
43+
}, [model, open]);
44+
45+
if (!open) return null;
46+
47+
const handleChange = (field) => (event) => {
48+
setFormData({
49+
...formData,
50+
[field]: event.target.value
51+
});
52+
};
53+
54+
const handleSwitchChange = (field) => (event) => {
55+
setFormData({
56+
...formData,
57+
[field]: event.target.checked
58+
});
59+
};
60+
61+
const handleSubmit = async () => {
62+
setError('');
63+
setLoading(true);
64+
65+
try {
66+
if (!formData.display_name) {
67+
throw new Error('Display name is required');
68+
}
69+
70+
await onSave(model, {
71+
display_name: formData.display_name,
72+
context_length: parseInt(formData.context_length, 10) || 4096,
73+
cost_per_1m_input_tokens: parseFloat(formData.cost_per_1m_input_tokens) || 0,
74+
cost_per_1m_output_tokens: parseFloat(formData.cost_per_1m_output_tokens) || 0,
75+
enabled: formData.enabled
76+
});
77+
onClose();
78+
} catch (err) {
79+
setError(err.message || 'Failed to update model');
80+
} finally {
81+
setLoading(false);
82+
}
83+
};
84+
85+
const handleClose = () => {
86+
setError('');
87+
onClose();
88+
};
89+
90+
return (
91+
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
92+
<DialogTitle>Edit Model: {model?.name || ''}</DialogTitle>
93+
<DialogContent>
94+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
95+
{error && <Alert severity="error">{error}</Alert>}
96+
97+
<TextField
98+
label="Display Name"
99+
value={formData.display_name}
100+
onChange={handleChange('display_name')}
101+
required
102+
fullWidth
103+
helperText="The name shown in the UI"
104+
/>
105+
106+
<TextField
107+
label="Context Length (tokens)"
108+
value={formData.context_length}
109+
onChange={handleChange('context_length')}
110+
type="number"
111+
fullWidth
112+
helperText="Maximum context window in tokens"
113+
/>
114+
115+
<TextField
116+
label="Cost per 1M Input Tokens ($)"
117+
value={formData.cost_per_1m_input_tokens}
118+
onChange={handleChange('cost_per_1m_input_tokens')}
119+
type="number"
120+
inputProps={{ step: '0.01' }}
121+
fullWidth
122+
helperText="e.g., 0.15 for $0.15 per 1M input tokens (0 = free)"
123+
/>
124+
125+
<TextField
126+
label="Cost per 1M Output Tokens ($)"
127+
value={formData.cost_per_1m_output_tokens}
128+
onChange={handleChange('cost_per_1m_output_tokens')}
129+
type="number"
130+
inputProps={{ step: '0.01' }}
131+
fullWidth
132+
helperText="e.g., 0.60 for $0.60 per 1M output tokens (0 = free)"
133+
/>
134+
135+
<FormControlLabel
136+
control={
137+
<Switch
138+
checked={formData.enabled}
139+
onChange={handleSwitchChange('enabled')}
140+
/>
141+
}
142+
label="Enabled"
143+
/>
144+
</Box>
145+
</DialogContent>
146+
<DialogActions>
147+
<Button onClick={handleClose}>Cancel</Button>
148+
<Button onClick={handleSubmit} variant="contained" disabled={loading}>
149+
{loading ? 'Saving...' : 'Save Changes'}
150+
</Button>
151+
</DialogActions>
152+
</Dialog>
153+
);
154+
}

0 commit comments

Comments
 (0)