WasabiDrop Claude commited on
Commit
7e1c5bd
Β·
1 Parent(s): b5d9316

🧬 Add Dynamic Model Management System

Browse files

Major new feature: Complete model management system with admin interface.

Key Features:
- Add new AI models via admin interface (e.g., gpt-4.1, o3-mini)
- Support for all providers: OpenAI, Anthropic, Google, Mistral, Groq, Cohere
- Whitelist-only security model for model access control
- Firebase persistence for custom models
- Cost tracking and usage analytics
- Beautiful admin interface with modal forms

Technical Implementation:
- Enhanced ModelFamilyManager with dynamic model CRUD operations
- New API endpoints for model management
- Comprehensive model validation and error handling
- Custom models separate from default models
- Real-time model toggling and bulk operations

Documentation:
- Created comprehensive docs/model-management.md guide
- Updated README with documentation structure
- Added docs/README.md index

Security:
- Models must exist AND be whitelisted to be used
- Non-existent models like "o3-mini" are blocked with 403
- Default models preserved and auto-whitelisted
- Admin authentication required for all model operations

πŸ€– Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

README.md CHANGED
@@ -10,7 +10,7 @@ pinned: false
10
 
11
  # 🐱 NyanProxy - Meow-nificent AI Proxy!
12
 
13
- A sophisticated AI proxy service that provides unified access to multiple AI providers with advanced features like token tracking, rate limiting, and user management.
14
 
15
  ## πŸ”₯ **IMPORTANT: Firebase Database Required!**
16
 
@@ -44,17 +44,31 @@ A sophisticated AI proxy service that provides unified access to multiple AI pro
44
 
45
  **NyanProxy is designed to be a privacy-first proxy that tracks usage without compromising your data privacy.**
46
 
47
- ## Features
48
 
 
49
  - **πŸ”₯ Firebase-Powered**: Persistent logging and user management
50
- - **πŸ€– Multi-Provider Support**: OpenAI, Anthropic, and more AI services
51
- - **πŸ“Š Advanced Analytics**: Real-time usage tracking and performance metrics
52
  - **πŸ›‘οΈ Smart Rate Limiting**: Automatic key rotation and health monitoring
53
  - **πŸ‘₯ User Management**: Individual user tokens with quotas and permissions
54
- - **🎯 Model Whitelisting**: Configurable model access controls
55
- - **πŸ“± Web Dashboard**: Beautiful admin interface for monitoring and management
56
  - **πŸ” Flexible Authentication**: Support for proxy keys or individual user tokens
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  ## πŸš€ Quick Setup Guide
59
 
60
  ### **Step 1: Firebase Database Setup (REQUIRED)**
 
10
 
11
  # 🐱 NyanProxy - Meow-nificent AI Proxy!
12
 
13
+ A sophisticated AI proxy service that provides unified access to multiple AI providers with advanced features like dynamic model management, token tracking, rate limiting, and user management.
14
 
15
  ## πŸ”₯ **IMPORTANT: Firebase Database Required!**
16
 
 
44
 
45
  **NyanProxy is designed to be a privacy-first proxy that tracks usage without compromising your data privacy.**
46
 
47
+ ## ✨ Key Features
48
 
49
+ - **🧬 Dynamic Model Management**: Add/remove AI models via admin interface without code changes
50
  - **πŸ”₯ Firebase-Powered**: Persistent logging and user management
51
+ - **πŸ€– Multi-Provider Support**: OpenAI, Anthropic, Google, Mistral, Groq, Cohere
52
+ - **πŸ“Š Advanced Analytics**: Real-time usage tracking and cost analysis
53
  - **πŸ›‘οΈ Smart Rate Limiting**: Automatic key rotation and health monitoring
54
  - **πŸ‘₯ User Management**: Individual user tokens with quotas and permissions
55
+ - **🎯 Model Whitelisting**: Secure whitelist-only model access
56
+ - **πŸ“± Beautiful Admin Dashboard**: Web interface for monitoring and management
57
  - **πŸ” Flexible Authentication**: Support for proxy keys or individual user tokens
58
 
59
+ ## πŸ“š Documentation
60
+
61
+ ### πŸ“– Detailed Guides
62
+ - **[🧬 Model Management](docs/model-management.md)** - Add/configure AI models dynamically
63
+ - **[πŸ” Authentication Setup](AUTHENTICATION.md)** - User management and authentication modes
64
+
65
+ ### 🎯 Quick Links
66
+ - **[πŸ’‘ Usage Examples](#-usage-examples)** - API integration examples
67
+ - **[πŸ”§ API Endpoints](#-api-endpoints)** - Complete endpoint reference
68
+ - **[βš™οΈ Environment Variables](#step-2-api-keys-setup)** - Configuration guide
69
+
70
+ *More documentation coming soon as features are added!*
71
+
72
  ## πŸš€ Quick Setup Guide
73
 
74
  ### **Step 1: Firebase Database Setup (REQUIRED)**
docs/README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ“š NyanProxy Documentation
2
+
3
+ Welcome to the NyanProxy documentation! Here you'll find detailed guides and tutorials for all features.
4
+
5
+ ## πŸ“– Available Guides
6
+
7
+ ### 🧬 [Model Management](model-management.md)
8
+ Learn how to dynamically add, configure, and manage AI models through the admin interface.
9
+
10
+ - Adding new models (like gpt-4.1, o1-preview, etc.)
11
+ - Configuring model properties and costs
12
+ - Managing model whitelisting and security
13
+ - Understanding usage analytics
14
+
15
+ ### πŸ” [Authentication Setup](../AUTHENTICATION.md)
16
+ Comprehensive guide to user management and authentication modes.
17
+
18
+ - Setting up user tokens
19
+ - Configuring authentication modes
20
+ - Managing user quotas and permissions
21
+
22
+ ## 🎯 Quick Reference
23
+
24
+ ### Essential Links
25
+ - **[Main README](../README.md)** - Project overview and quick start
26
+ - **[Admin Dashboard](../README.md#-api-endpoints)** - Management interface guide
27
+ - **[API Endpoints](../README.md#-api-endpoints)** - Complete API reference
28
+
29
+ ### Common Tasks
30
+ - **Add New Model**: [Model Management Guide](model-management.md#adding-a-new-model)
31
+ - **Configure Authentication**: [Authentication Setup](../AUTHENTICATION.md)
32
+ - **Set Up Firebase**: [Main README](../README.md#step-1-firebase-database-setup-required)
33
+
34
+ ## πŸš€ Getting Started
35
+
36
+ 1. **[Set up Firebase](../README.md#step-1-firebase-database-setup-required)** - Required for persistence
37
+ 2. **[Configure API Keys](../README.md#step-2-api-keys-setup)** - Add your AI service keys
38
+ 3. **[Deploy to HF](../README.md#step-3-local-development)** - Run locally or deploy
39
+ 4. **[Manage Models](model-management.md)** - Add and configure AI models
40
+ 5. **[Set up Users](../AUTHENTICATION.md)** - Configure authentication
41
+
42
+ ## πŸ†˜ Need Help?
43
+
44
+ If you can't find what you're looking for:
45
+ 1. Check the [main README](../README.md) for basic setup
46
+ 2. Look through the specific guide for your feature
47
+ 3. Create an issue on the repository with your question
48
+
49
+ ---
50
+
51
+ *Documentation is continuously updated as new features are added!*
docs/model-management.md ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🧬 Model Management Guide
2
+
3
+ NyanProxy provides a comprehensive model management system that allows you to dynamically add, configure, and control AI models without modifying code or environment variables.
4
+
5
+ ## 🎯 Overview
6
+
7
+ The Model Management system provides:
8
+ - **Dynamic Model Addition**: Add new models via admin interface
9
+ - **Provider Support**: OpenAI, Anthropic, Google, Mistral, Groq, Cohere
10
+ - **Security**: Whitelist-only access control
11
+ - **Cost Tracking**: Per-model usage and cost analytics
12
+ - **Firebase Persistence**: Automatic synchronization across deployments
13
+
14
+ ## πŸš€ Quick Start
15
+
16
+ ### Accessing Model Management
17
+
18
+ 1. Navigate to your NyanProxy admin panel: `/admin/login`
19
+ 2. Enter your admin credentials
20
+ 3. Click "🧬 Model Families" in the navigation menu
21
+
22
+ ### Adding a New Model
23
+
24
+ 1. Click **"βž• Add New Model"** button
25
+ 2. Fill in the model details:
26
+ - **Model ID**: `gpt-4.1-turbo` (API identifier)
27
+ - **Provider**: Select from dropdown (OpenAI, Anthropic, etc.)
28
+ - **Display Name**: `GPT-4.1 Turbo` (human-readable name)
29
+ - **Description**: Brief description of capabilities
30
+ - **Costs**: Input/output cost per 1K tokens
31
+ - **Context Length**: Maximum tokens (e.g., 128000)
32
+ - **Cat Personality**: Fun theme (curious, playful, smart, etc.)
33
+ - **Capabilities**: Streaming, function calling, premium status
34
+ 3. Check **"Auto-enable when added"** to immediately whitelist the model
35
+ 4. Click **"Add Model"**
36
+
37
+ The model is now available for API requests!
38
+
39
+ ## πŸ”§ Managing Models
40
+
41
+ ### Default Models
42
+
43
+ NyanProxy comes with these pre-configured models:
44
+
45
+ **OpenAI Models:**
46
+ - `gpt-4o` - Most advanced GPT-4 model
47
+ - `gpt-4o-mini` - Smaller, faster GPT-4 model
48
+ - `gpt-3.5-turbo` - Fast and efficient for most tasks
49
+
50
+ **Anthropic Models:**
51
+ - `claude-3-5-sonnet-20241022` - Most intelligent Claude model
52
+ - `claude-3-haiku-20240307` - Fastest Claude model
53
+
54
+ **Google Models:**
55
+ - `gemini-1.5-pro` - Google's most capable model
56
+ - `gemini-1.5-flash` - Fast and efficient Gemini model
57
+
58
+ ### Model Controls
59
+
60
+ **Individual Model Toggle:**
61
+ - Use the toggle switches to enable/disable specific models
62
+ - Changes are applied immediately
63
+
64
+ **Bulk Operations:**
65
+ - **Enable All Models**: Enable all models across all providers
66
+ - **Disable All Models**: Disable all models (use with caution!)
67
+ - **Provider Controls**: Enable/disable all models for a specific provider
68
+
69
+ ### Custom Models Management
70
+
71
+ 1. Click **"πŸ”§ Manage Custom Models"** to view all custom models
72
+ 2. See detailed information: costs, context length, status
73
+ 3. **Delete**: Remove custom models you no longer need
74
+
75
+ ## πŸ”’ Security & Whitelisting
76
+
77
+ ### How Whitelisting Works
78
+
79
+ NyanProxy uses a **whitelist-only** security model:
80
+
81
+ 1. **Model Must Exist**: The model must be defined in the system
82
+ 2. **Model Must Be Whitelisted**: The model must be explicitly enabled
83
+ 3. **Request Validation**: Every API request checks both conditions
84
+
85
+ ### Security Examples
86
+
87
+ ```
88
+ βœ… ALLOWED: "gpt-4o" (exists + whitelisted)
89
+ ❌ BLOCKED: "o3-mini" (doesn't exist)
90
+ ❌ BLOCKED: "custom-model" (exists but not whitelisted)
91
+ ```
92
+
93
+ ### API Response for Blocked Models
94
+
95
+ ```json
96
+ {
97
+ "error": {
98
+ "message": "Model 'o3-mini' is not whitelisted for use",
99
+ "type": "model_not_allowed"
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## πŸ“Š Usage Analytics
105
+
106
+ ### Cost Tracking
107
+
108
+ Each model tracks:
109
+ - **Total Requests**: Number of API calls
110
+ - **Token Usage**: Input/output tokens consumed
111
+ - **Cost**: Calculated cost based on token usage
112
+ - **Success Rate**: Percentage of successful requests
113
+
114
+ ### Viewing Analytics
115
+
116
+ 1. Navigate to **"πŸ“Š Usage Analytics"** from the Model Families page
117
+ 2. View:
118
+ - Top models by cost
119
+ - Top models by requests
120
+ - Per-model performance metrics
121
+ - Cost breakdown by provider
122
+
123
+ ## πŸ”§ Advanced Configuration
124
+
125
+ ### Model Properties
126
+
127
+ When adding a model, you can configure:
128
+
129
+ **Required Fields:**
130
+ - `model_id`: API identifier (e.g., "gpt-4.1-turbo")
131
+ - `provider`: AI service provider
132
+ - `display_name`: Human-readable name
133
+ - `description`: Model capabilities description
134
+
135
+ **Optional Fields:**
136
+ - `input_cost_per_1k`: Cost per 1K input tokens ($)
137
+ - `output_cost_per_1k`: Cost per 1K output tokens ($)
138
+ - `context_length`: Maximum context window (default: 4096)
139
+ - `supports_streaming`: Real-time response support
140
+ - `supports_function_calling`: Function/tool calling capability
141
+ - `is_premium`: Premium model designation
142
+ - `cat_personality`: Theme for fun categorization
143
+
144
+ ### Cat Personalities
145
+
146
+ Choose from these fun themes:
147
+ - πŸ™€ **Curious**: Inquisitive models
148
+ - 😸 **Playful**: Fun, creative models
149
+ - 😴 **Sleepy**: Relaxed, casual models
150
+ - 😾 **Grumpy**: Serious, no-nonsense models
151
+ - πŸ€“ **Smart**: Intelligent, analytical models
152
+ - πŸ’¨ **Fast**: Quick, efficient models
153
+
154
+ ## πŸ—„οΈ Data Persistence
155
+
156
+ ### Firebase Integration
157
+
158
+ When Firebase is configured:
159
+ - Custom models sync automatically across deployments
160
+ - Model configurations persist through restarts
161
+ - Real-time updates across multiple instances
162
+
163
+ ### Local File Fallback
164
+
165
+ Without Firebase:
166
+ - Models saved to `custom_models.json`
167
+ - Configuration persists locally
168
+ - Manual backup recommended
169
+
170
+ ## πŸ› οΈ Troubleshooting
171
+
172
+ ### Common Issues
173
+
174
+ **Model Not Appearing:**
175
+ 1. Check if model was added successfully
176
+ 2. Verify model is whitelisted (toggle on)
177
+ 3. Refresh the page
178
+
179
+ **API Requests Failing:**
180
+ 1. Confirm model exists in system
181
+ 2. Verify model is whitelisted
182
+ 3. Check API key is configured for the provider
183
+
184
+ **Permission Errors:**
185
+ 1. Ensure you're logged in as admin
186
+ 2. Check admin key is correct
187
+ 3. Verify session hasn't expired
188
+
189
+ ### Getting Help
190
+
191
+ If you encounter issues:
192
+ 1. Check the browser console for JavaScript errors
193
+ 2. Review the NyanProxy logs for backend errors
194
+ 3. Verify your Firebase configuration (if using)
195
+ 4. Test with default models first
196
+
197
+ ## πŸŽ‰ Best Practices
198
+
199
+ ### Model Naming
200
+
201
+ - Use clear, descriptive model IDs
202
+ - Follow provider conventions (e.g., `gpt-4.1-turbo`)
203
+ - Avoid special characters or spaces
204
+
205
+ ### Cost Management
206
+
207
+ - Set realistic cost estimates for budget tracking
208
+ - Monitor usage analytics regularly
209
+ - Consider token limits for expensive models
210
+
211
+ ### Security
212
+
213
+ - Only whitelist models you trust
214
+ - Regularly review enabled models
215
+ - Use the "Disable All" feature for maintenance
216
+
217
+ ### Performance
218
+
219
+ - Enable streaming for better user experience
220
+ - Set appropriate context lengths
221
+ - Monitor response times via analytics
222
+
223
+ ---
224
+
225
+ *Need help? Check out the [main documentation](../README.md) or create an issue on the repository.*
pages/admin/model_families.html CHANGED
@@ -160,6 +160,151 @@
160
  font-size: 1.2em;
161
  margin-right: 5px;
162
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  </style>
164
  {% endblock %}
165
 
@@ -190,9 +335,11 @@
190
 
191
  <!-- Action Buttons -->
192
  <div style="margin-bottom: 20px;">
 
193
  <button onclick="enableAllModels()" class="btn btn-success">🐱 Enable All Models</button>
194
  <button onclick="disableAllModels()" class="btn btn-warning">😾 Disable All Models</button>
195
  <a href="{{ url_for('model_families.usage_analytics') }}" class="btn btn-info">πŸ“Š Usage Analytics</a>
 
196
  </div>
197
 
198
  <!-- Provider Cards -->
@@ -251,6 +398,111 @@
251
  </div>
252
  </div>
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  <script>
255
  function toggleModel(provider, modelId, checkbox) {
256
  fetch(`/admin/model-families/api/model/${modelId}/toggle`, {
@@ -398,5 +650,173 @@ function showNotification(message, type) {
398
  notification.remove();
399
  }, 3000);
400
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  </script>
402
  {% endblock %}
 
160
  font-size: 1.2em;
161
  margin-right: 5px;
162
  }
163
+
164
+ .modal {
165
+ display: none;
166
+ position: fixed;
167
+ z-index: 1000;
168
+ left: 0;
169
+ top: 0;
170
+ width: 100%;
171
+ height: 100%;
172
+ background-color: rgba(0,0,0,0.5);
173
+ }
174
+
175
+ .modal-content {
176
+ background-color: white;
177
+ margin: 5% auto;
178
+ padding: 20px;
179
+ border-radius: 10px;
180
+ width: 80%;
181
+ max-width: 500px;
182
+ max-height: 80vh;
183
+ overflow-y: auto;
184
+ }
185
+
186
+ .close {
187
+ color: #aaa;
188
+ float: right;
189
+ font-size: 28px;
190
+ font-weight: bold;
191
+ cursor: pointer;
192
+ }
193
+
194
+ .close:hover {
195
+ color: black;
196
+ }
197
+
198
+ .form-group {
199
+ margin-bottom: 15px;
200
+ }
201
+
202
+ .form-group label {
203
+ display: block;
204
+ margin-bottom: 5px;
205
+ font-weight: bold;
206
+ }
207
+
208
+ .form-group input,
209
+ .form-group select,
210
+ .form-group textarea {
211
+ width: 100%;
212
+ padding: 8px;
213
+ border: 1px solid #ddd;
214
+ border-radius: 4px;
215
+ font-size: 14px;
216
+ }
217
+
218
+ .form-group textarea {
219
+ height: 80px;
220
+ resize: vertical;
221
+ }
222
+
223
+ .error {
224
+ background-color: #f8d7da;
225
+ color: #721c24;
226
+ padding: 10px;
227
+ border-radius: 4px;
228
+ margin-bottom: 15px;
229
+ }
230
+
231
+ .success {
232
+ background-color: #d4edda;
233
+ color: #155724;
234
+ padding: 10px;
235
+ border-radius: 4px;
236
+ margin-bottom: 15px;
237
+ }
238
+
239
+ .custom-model-item {
240
+ border: 1px solid #ddd;
241
+ border-radius: 8px;
242
+ padding: 15px;
243
+ margin-bottom: 10px;
244
+ background: #f9f9f9;
245
+ }
246
+
247
+ .custom-model-header {
248
+ display: flex;
249
+ justify-content: space-between;
250
+ align-items: center;
251
+ margin-bottom: 10px;
252
+ }
253
+
254
+ .custom-model-name {
255
+ font-weight: bold;
256
+ font-size: 1.1em;
257
+ }
258
+
259
+ .custom-model-actions {
260
+ display: flex;
261
+ gap: 10px;
262
+ }
263
+
264
+ .btn {
265
+ padding: 8px 16px;
266
+ border: none;
267
+ border-radius: 4px;
268
+ cursor: pointer;
269
+ font-size: 14px;
270
+ text-decoration: none;
271
+ display: inline-block;
272
+ }
273
+
274
+ .btn-primary {
275
+ background-color: #007bff;
276
+ color: white;
277
+ }
278
+
279
+ .btn-secondary {
280
+ background-color: #6c757d;
281
+ color: white;
282
+ }
283
+
284
+ .btn-success {
285
+ background-color: #28a745;
286
+ color: white;
287
+ }
288
+
289
+ .btn-warning {
290
+ background-color: #ffc107;
291
+ color: black;
292
+ }
293
+
294
+ .btn-danger {
295
+ background-color: #dc3545;
296
+ color: white;
297
+ }
298
+
299
+ .btn-info {
300
+ background-color: #17a2b8;
301
+ color: white;
302
+ }
303
+
304
+ .btn-sm {
305
+ padding: 4px 8px;
306
+ font-size: 12px;
307
+ }
308
  </style>
309
  {% endblock %}
310
 
 
335
 
336
  <!-- Action Buttons -->
337
  <div style="margin-bottom: 20px;">
338
+ <button onclick="showAddModelModal()" class="btn btn-primary">βž• Add New Model</button>
339
  <button onclick="enableAllModels()" class="btn btn-success">🐱 Enable All Models</button>
340
  <button onclick="disableAllModels()" class="btn btn-warning">😾 Disable All Models</button>
341
  <a href="{{ url_for('model_families.usage_analytics') }}" class="btn btn-info">πŸ“Š Usage Analytics</a>
342
+ <button onclick="showCustomModels()" class="btn btn-secondary">πŸ”§ Manage Custom Models</button>
343
  </div>
344
 
345
  <!-- Provider Cards -->
 
398
  </div>
399
  </div>
400
 
401
+ <!-- Add New Model Modal -->
402
+ <div id="addModelModal" class="modal" style="display: none;">
403
+ <div class="modal-content" style="max-width: 600px;">
404
+ <span class="close" onclick="hideModal('addModelModal')">&times;</span>
405
+ <h3>🐱 Add New Model</h3>
406
+ <form id="addModelForm">
407
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
408
+ <div class="form-group">
409
+ <label for="model_id">Model ID *</label>
410
+ <input type="text" id="model_id" name="model_id" required
411
+ placeholder="e.g., gpt-4.1-turbo">
412
+ </div>
413
+ <div class="form-group">
414
+ <label for="provider">Provider *</label>
415
+ <select id="provider" name="provider" required>
416
+ <option value="">Select provider...</option>
417
+ <option value="openai">OpenAI</option>
418
+ <option value="anthropic">Anthropic</option>
419
+ <option value="google">Google</option>
420
+ <option value="mistral">Mistral</option>
421
+ <option value="groq">Groq</option>
422
+ <option value="cohere">Cohere</option>
423
+ </select>
424
+ </div>
425
+ <div class="form-group">
426
+ <label for="display_name">Display Name *</label>
427
+ <input type="text" id="display_name" name="display_name" required
428
+ placeholder="e.g., GPT-4.1 Turbo">
429
+ </div>
430
+ <div class="form-group">
431
+ <label for="cat_personality">Cat Personality</label>
432
+ <select id="cat_personality" name="cat_personality">
433
+ <option value="curious">Curious πŸ™€</option>
434
+ <option value="playful">Playful 😸</option>
435
+ <option value="sleepy">Sleepy 😴</option>
436
+ <option value="grumpy">Grumpy 😾</option>
437
+ <option value="smart">Smart πŸ€“</option>
438
+ <option value="fast">Fast πŸ’¨</option>
439
+ </select>
440
+ </div>
441
+ <div class="form-group">
442
+ <label for="input_cost_per_1k">Input Cost per 1K tokens ($)</label>
443
+ <input type="number" id="input_cost_per_1k" name="input_cost_per_1k"
444
+ step="0.001" min="0" placeholder="0.000">
445
+ </div>
446
+ <div class="form-group">
447
+ <label for="output_cost_per_1k">Output Cost per 1K tokens ($)</label>
448
+ <input type="number" id="output_cost_per_1k" name="output_cost_per_1k"
449
+ step="0.001" min="0" placeholder="0.000">
450
+ </div>
451
+ <div class="form-group">
452
+ <label for="context_length">Context Length</label>
453
+ <input type="number" id="context_length" name="context_length"
454
+ min="1" value="4096" placeholder="4096">
455
+ </div>
456
+ <div class="form-group">
457
+ <label>
458
+ <input type="checkbox" id="supports_streaming" name="supports_streaming" checked>
459
+ Supports Streaming
460
+ </label>
461
+ </div>
462
+ <div class="form-group">
463
+ <label>
464
+ <input type="checkbox" id="supports_function_calling" name="supports_function_calling">
465
+ Supports Function Calling
466
+ </label>
467
+ </div>
468
+ <div class="form-group">
469
+ <label>
470
+ <input type="checkbox" id="is_premium" name="is_premium">
471
+ Premium Model
472
+ </label>
473
+ </div>
474
+ <div class="form-group">
475
+ <label>
476
+ <input type="checkbox" id="auto_whitelist" name="auto_whitelist" checked>
477
+ Auto-enable when added
478
+ </label>
479
+ </div>
480
+ </div>
481
+ <div class="form-group">
482
+ <label for="description">Description *</label>
483
+ <textarea id="description" name="description" required
484
+ placeholder="Brief description of the model capabilities..."></textarea>
485
+ </div>
486
+ <div id="addModelErrors" class="error" style="display: none;"></div>
487
+ <div style="margin-top: 20px;">
488
+ <button type="submit" class="btn btn-primary">Add Model</button>
489
+ <button type="button" class="btn btn-secondary" onclick="hideModal('addModelModal')">Cancel</button>
490
+ </div>
491
+ </form>
492
+ </div>
493
+ </div>
494
+
495
+ <!-- Custom Models Management Modal -->
496
+ <div id="customModelsModal" class="modal" style="display: none;">
497
+ <div class="modal-content" style="max-width: 800px;">
498
+ <span class="close" onclick="hideModal('customModelsModal')">&times;</span>
499
+ <h3>πŸ”§ Custom Models Management</h3>
500
+ <div id="customModelsList">
501
+ <div class="loading">Loading custom models...</div>
502
+ </div>
503
+ </div>
504
+ </div>
505
+
506
  <script>
507
  function toggleModel(provider, modelId, checkbox) {
508
  fetch(`/admin/model-families/api/model/${modelId}/toggle`, {
 
650
  notification.remove();
651
  }, 3000);
652
  }
653
+
654
+ function showModal(modalId) {
655
+ document.getElementById(modalId).style.display = 'block';
656
+ }
657
+
658
+ function hideModal(modalId) {
659
+ document.getElementById(modalId).style.display = 'none';
660
+ }
661
+
662
+ function showAddModelModal() {
663
+ document.getElementById('addModelForm').reset();
664
+ document.getElementById('addModelErrors').style.display = 'none';
665
+ showModal('addModelModal');
666
+ }
667
+
668
+ function showCustomModels() {
669
+ showModal('customModelsModal');
670
+ loadCustomModels();
671
+ }
672
+
673
+ document.getElementById('addModelForm').addEventListener('submit', function(e) {
674
+ e.preventDefault();
675
+
676
+ const formData = new FormData(e.target);
677
+ const data = {};
678
+
679
+ // Convert form data to object
680
+ for (let [key, value] of formData.entries()) {
681
+ if (key.startsWith('supports_') || key.startsWith('is_') || key === 'auto_whitelist') {
682
+ data[key] = true; // Checkbox is checked
683
+ } else {
684
+ data[key] = value;
685
+ }
686
+ }
687
+
688
+ // Set unchecked checkboxes to false
689
+ const checkboxes = ['supports_streaming', 'supports_function_calling', 'is_premium', 'auto_whitelist'];
690
+ checkboxes.forEach(cb => {
691
+ if (!data[cb]) {
692
+ data[cb] = false;
693
+ }
694
+ });
695
+
696
+ // Submit the form
697
+ fetch('/admin/model-families/api/models', {
698
+ method: 'POST',
699
+ headers: {
700
+ 'Content-Type': 'application/json',
701
+ 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
702
+ },
703
+ body: JSON.stringify(data)
704
+ })
705
+ .then(response => response.json())
706
+ .then(result => {
707
+ if (result.success) {
708
+ showNotification(result.message, 'success');
709
+ hideModal('addModelModal');
710
+ // Refresh the page to show the new model
711
+ location.reload();
712
+ } else {
713
+ const errorDiv = document.getElementById('addModelErrors');
714
+ if (result.field_errors) {
715
+ let errorText = 'Validation errors:\n';
716
+ for (const [field, error] of Object.entries(result.field_errors)) {
717
+ errorText += `${field}: ${error}\n`;
718
+ }
719
+ errorDiv.textContent = errorText;
720
+ } else {
721
+ errorDiv.textContent = result.error || 'Unknown error occurred';
722
+ }
723
+ errorDiv.style.display = 'block';
724
+ }
725
+ })
726
+ .catch(error => {
727
+ console.error('Error:', error);
728
+ showNotification('Network error occurred', 'error');
729
+ });
730
+ });
731
+
732
+ function loadCustomModels() {
733
+ const customModelsList = document.getElementById('customModelsList');
734
+ customModelsList.innerHTML = '<div class="loading">Loading custom models...</div>';
735
+
736
+ fetch('/admin/model-families/api/models/custom')
737
+ .then(response => response.json())
738
+ .then(result => {
739
+ if (result.success) {
740
+ renderCustomModels(result.models);
741
+ } else {
742
+ customModelsList.innerHTML = '<div class="error">Error loading custom models: ' + result.error + '</div>';
743
+ }
744
+ })
745
+ .catch(error => {
746
+ console.error('Error:', error);
747
+ customModelsList.innerHTML = '<div class="error">Network error occurred</div>';
748
+ });
749
+ }
750
+
751
+ function renderCustomModels(models) {
752
+ const customModelsList = document.getElementById('customModelsList');
753
+
754
+ if (models.length === 0) {
755
+ customModelsList.innerHTML = '<div class="info">No custom models found. Add your first custom model using the "Add New Model" button!</div>';
756
+ return;
757
+ }
758
+
759
+ let html = '';
760
+ models.forEach(model => {
761
+ html += `
762
+ <div class="custom-model-item">
763
+ <div class="custom-model-header">
764
+ <div class="custom-model-name">
765
+ ${model.cat_emoji} ${model.display_name}
766
+ ${model.is_premium ? '<span class="premium-badge">PREMIUM</span>' : ''}
767
+ </div>
768
+ <div class="custom-model-actions">
769
+ <button class="btn btn-sm btn-danger" onclick="deleteCustomModel('${model.model_id}')">Delete</button>
770
+ </div>
771
+ </div>
772
+ <div style="font-size: 0.9em; color: #666;">
773
+ <strong>ID:</strong> ${model.model_id}<br>
774
+ <strong>Provider:</strong> ${model.provider.toUpperCase()}<br>
775
+ <strong>Description:</strong> ${model.description}<br>
776
+ <strong>Costs:</strong> $${model.input_cost_per_1k.toFixed(3)}/1K in, $${model.output_cost_per_1k.toFixed(3)}/1K out<br>
777
+ <strong>Context:</strong> ${model.context_length.toLocaleString()} tokens<br>
778
+ <strong>Status:</strong> ${model.is_whitelisted ? '<span style="color: green;">Enabled</span>' : '<span style="color: red;">Disabled</span>'}
779
+ </div>
780
+ </div>
781
+ `;
782
+ });
783
+
784
+ customModelsList.innerHTML = html;
785
+ }
786
+
787
+ function deleteCustomModel(modelId) {
788
+ if (!confirm('Are you sure you want to delete this custom model? This action cannot be undone.')) {
789
+ return;
790
+ }
791
+
792
+ fetch(`/admin/model-families/api/models/${modelId}`, {
793
+ method: 'DELETE',
794
+ headers: {
795
+ 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
796
+ }
797
+ })
798
+ .then(response => response.json())
799
+ .then(result => {
800
+ if (result.success) {
801
+ showNotification(result.message, 'success');
802
+ loadCustomModels(); // Refresh the list
803
+ // Also refresh the main page after a short delay
804
+ setTimeout(() => location.reload(), 1000);
805
+ } else {
806
+ showNotification(result.error || 'Failed to delete model', 'error');
807
+ }
808
+ })
809
+ .catch(error => {
810
+ console.error('Error:', error);
811
+ showNotification('Network error occurred', 'error');
812
+ });
813
+ }
814
+
815
+ // Close modal when clicking outside
816
+ window.onclick = function(event) {
817
+ if (event.target.classList.contains('modal')) {
818
+ event.target.style.display = 'none';
819
+ }
820
+ }
821
  </script>
822
  {% endblock %}
src/routes/model_families_admin.py CHANGED
@@ -10,7 +10,7 @@ from typing import Dict, Any, List
10
  import json
11
 
12
  from ..middleware.auth import require_admin_session
13
- from ..services.model_families import model_manager, AIProvider
14
  from ..services.firebase_logger import structured_logger
15
 
16
  model_families_bp = Blueprint('model_families', __name__, url_prefix='/admin/model-families')
@@ -246,4 +246,195 @@ def usage_analytics():
246
  cost_analysis=cost_analysis,
247
  top_models_by_cost=top_models_by_cost,
248
  top_models_by_requests=top_models_by_requests,
249
- global_stats=global_stats)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  import json
11
 
12
  from ..middleware.auth import require_admin_session
13
+ from ..services.model_families import model_manager, AIProvider, ModelInfo
14
  from ..services.firebase_logger import structured_logger
15
 
16
  model_families_bp = Blueprint('model_families', __name__, url_prefix='/admin/model-families')
 
246
  cost_analysis=cost_analysis,
247
  top_models_by_cost=top_models_by_cost,
248
  top_models_by_requests=top_models_by_requests,
249
+ global_stats=global_stats)
250
+
251
+ @model_families_bp.route('/api/models', methods=['POST'])
252
+ @require_admin_session
253
+ def add_custom_model():
254
+ """Add a new custom model"""
255
+ data = request.get_json()
256
+
257
+ if not data:
258
+ return jsonify({'error': 'No data provided'}), 400
259
+
260
+ # Validate the model data
261
+ errors = model_manager.validate_model_data(data)
262
+ if errors:
263
+ return jsonify({'error': 'Validation failed', 'field_errors': errors}), 400
264
+
265
+ try:
266
+ # Create ModelInfo object
267
+ model_info = ModelInfo(
268
+ model_id=data['model_id'],
269
+ provider=AIProvider(data['provider'].lower()),
270
+ display_name=data['display_name'],
271
+ description=data['description'],
272
+ input_cost_per_1k=float(data.get('input_cost_per_1k', 0)),
273
+ output_cost_per_1k=float(data.get('output_cost_per_1k', 0)),
274
+ context_length=int(data.get('context_length', 4096)),
275
+ supports_streaming=bool(data.get('supports_streaming', True)),
276
+ supports_function_calling=bool(data.get('supports_function_calling', False)),
277
+ is_premium=bool(data.get('is_premium', False)),
278
+ cat_personality=data.get('cat_personality', 'curious')
279
+ )
280
+
281
+ # Add the model
282
+ auto_whitelist = data.get('auto_whitelist', True)
283
+ success = model_manager.add_custom_model(model_info, auto_whitelist)
284
+
285
+ if success:
286
+ # Log admin action
287
+ structured_logger.log_user_action(
288
+ user_token='admin',
289
+ action='custom_model_added',
290
+ details={
291
+ 'model_id': model_info.model_id,
292
+ 'provider': model_info.provider.value,
293
+ 'display_name': model_info.display_name,
294
+ 'auto_whitelisted': auto_whitelist
295
+ },
296
+ admin_user='admin'
297
+ )
298
+
299
+ return jsonify({
300
+ 'success': True,
301
+ 'message': f'Model {model_info.display_name} added successfully',
302
+ 'model_id': model_info.model_id,
303
+ 'whitelisted': auto_whitelist
304
+ })
305
+ else:
306
+ return jsonify({'error': 'Failed to add model (already exists?)'}), 400
307
+
308
+ except ValueError as e:
309
+ return jsonify({'error': f'Invalid data: {str(e)}'}), 400
310
+ except Exception as e:
311
+ return jsonify({'error': f'Server error: {str(e)}'}), 500
312
+
313
+ @model_families_bp.route('/api/models/<model_id>', methods=['PUT'])
314
+ @require_admin_session
315
+ def update_custom_model(model_id: str):
316
+ """Update an existing custom model"""
317
+ data = request.get_json()
318
+
319
+ if not data:
320
+ return jsonify({'error': 'No data provided'}), 400
321
+
322
+ # Remove model_id from validation data to prevent uniqueness check
323
+ validation_data = data.copy()
324
+ validation_data.pop('model_id', None)
325
+
326
+ # Validate the model data (excluding model_id)
327
+ errors = model_manager.validate_model_data(validation_data)
328
+ if errors:
329
+ return jsonify({'error': 'Validation failed', 'field_errors': errors}), 400
330
+
331
+ try:
332
+ # Convert numeric fields
333
+ updates = {}
334
+ for field in ['input_cost_per_1k', 'output_cost_per_1k', 'context_length']:
335
+ if field in data:
336
+ updates[field] = float(data[field]) if field != 'context_length' else int(data[field])
337
+
338
+ # Convert boolean fields
339
+ for field in ['supports_streaming', 'supports_function_calling', 'is_premium']:
340
+ if field in data:
341
+ updates[field] = bool(data[field])
342
+
343
+ # Copy string fields
344
+ for field in ['display_name', 'description', 'cat_personality']:
345
+ if field in data:
346
+ updates[field] = data[field]
347
+
348
+ success = model_manager.update_model_info(model_id, updates)
349
+
350
+ if success:
351
+ # Log admin action
352
+ structured_logger.log_user_action(
353
+ user_token='admin',
354
+ action='custom_model_updated',
355
+ details={
356
+ 'model_id': model_id,
357
+ 'updates': updates
358
+ },
359
+ admin_user='admin'
360
+ )
361
+
362
+ return jsonify({
363
+ 'success': True,
364
+ 'message': f'Model {model_id} updated successfully'
365
+ })
366
+ else:
367
+ return jsonify({'error': 'Model not found'}), 404
368
+
369
+ except Exception as e:
370
+ return jsonify({'error': f'Server error: {str(e)}'}), 500
371
+
372
+ @model_families_bp.route('/api/models/<model_id>', methods=['DELETE'])
373
+ @require_admin_session
374
+ def delete_custom_model(model_id: str):
375
+ """Delete a custom model"""
376
+ try:
377
+ # Get model info for logging
378
+ model_info = model_manager.get_model_info(model_id)
379
+ if not model_info:
380
+ return jsonify({'error': 'Model not found'}), 404
381
+
382
+ success = model_manager.remove_custom_model(model_id)
383
+
384
+ if success:
385
+ # Log admin action
386
+ structured_logger.log_user_action(
387
+ user_token='admin',
388
+ action='custom_model_deleted',
389
+ details={
390
+ 'model_id': model_id,
391
+ 'display_name': model_info.display_name,
392
+ 'provider': model_info.provider.value
393
+ },
394
+ admin_user='admin'
395
+ )
396
+
397
+ return jsonify({
398
+ 'success': True,
399
+ 'message': f'Model {model_info.display_name} deleted successfully'
400
+ })
401
+ else:
402
+ return jsonify({'error': 'Failed to delete model'}), 500
403
+
404
+ except Exception as e:
405
+ return jsonify({'error': f'Server error: {str(e)}'}), 500
406
+
407
+ @model_families_bp.route('/api/models/custom')
408
+ @require_admin_session
409
+ def get_custom_models():
410
+ """Get all custom models"""
411
+ try:
412
+ custom_models = model_manager.get_custom_models()
413
+
414
+ models_data = []
415
+ for model in custom_models:
416
+ model_dict = {
417
+ 'model_id': model.model_id,
418
+ 'provider': model.provider.value,
419
+ 'display_name': model.display_name,
420
+ 'description': model.description,
421
+ 'input_cost_per_1k': model.input_cost_per_1k,
422
+ 'output_cost_per_1k': model.output_cost_per_1k,
423
+ 'context_length': model.context_length,
424
+ 'supports_streaming': model.supports_streaming,
425
+ 'supports_function_calling': model.supports_function_calling,
426
+ 'is_premium': model.is_premium,
427
+ 'cat_personality': model.cat_personality,
428
+ 'cat_emoji': model.get_cat_emoji(),
429
+ 'is_whitelisted': model_manager.is_model_whitelisted(model.provider, model.model_id)
430
+ }
431
+ models_data.append(model_dict)
432
+
433
+ return jsonify({
434
+ 'success': True,
435
+ 'models': models_data,
436
+ 'count': len(models_data)
437
+ })
438
+
439
+ except Exception as e:
440
+ return jsonify({'error': f'Server error: {str(e)}'}), 500
src/services/model_families.py CHANGED
@@ -149,6 +149,9 @@ class ModelFamilyManager:
149
 
150
  # Load configuration
151
  self._load_configuration()
 
 
 
152
 
153
  def _initialize_default_models(self):
154
  """Initialize with default model configurations"""
@@ -496,6 +499,196 @@ class ModelFamilyManager:
496
  'by_provider': by_provider,
497
  'currency': 'USD'
498
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
  # Global instance
501
  model_manager = ModelFamilyManager()
 
149
 
150
  # Load configuration
151
  self._load_configuration()
152
+
153
+ # Load custom models
154
+ self._load_custom_models()
155
 
156
  def _initialize_default_models(self):
157
  """Initialize with default model configurations"""
 
499
  'by_provider': by_provider,
500
  'currency': 'USD'
501
  }
502
+
503
+ def add_custom_model(self, model_info: ModelInfo, auto_whitelist: bool = True) -> bool:
504
+ """Add a custom model to the system"""
505
+ with self.lock:
506
+ # Check if model already exists
507
+ if model_info.model_id in self.models:
508
+ return False
509
+
510
+ # Add the model
511
+ self.models[model_info.model_id] = model_info
512
+
513
+ # Auto-whitelist if requested
514
+ if auto_whitelist:
515
+ provider = model_info.provider
516
+ if provider not in self.whitelisted_models:
517
+ self.whitelisted_models[provider] = set()
518
+ self.whitelisted_models[provider].add(model_info.model_id)
519
+
520
+ # Save configuration
521
+ self._save_configuration()
522
+
523
+ # Save custom models to Firebase/file
524
+ self._save_custom_models()
525
+
526
+ return True
527
+
528
+ def remove_custom_model(self, model_id: str) -> bool:
529
+ """Remove a custom model from the system"""
530
+ with self.lock:
531
+ if model_id not in self.models:
532
+ return False
533
+
534
+ model_info = self.models[model_id]
535
+
536
+ # Remove from whitelist first
537
+ provider = model_info.provider
538
+ if provider in self.whitelisted_models:
539
+ self.whitelisted_models[provider].discard(model_id)
540
+
541
+ # Remove from models
542
+ del self.models[model_id]
543
+
544
+ # Save configuration
545
+ self._save_configuration()
546
+
547
+ # Save custom models to Firebase/file
548
+ self._save_custom_models()
549
+
550
+ return True
551
+
552
+ def get_custom_models(self) -> List[ModelInfo]:
553
+ """Get all custom (non-default) models"""
554
+ with self.lock:
555
+ # Default models are those defined in _initialize_default_models
556
+ default_model_ids = {
557
+ "gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo",
558
+ "claude-3-5-sonnet-20241022", "claude-3-haiku-20240307",
559
+ "gemini-1.5-pro", "gemini-1.5-flash"
560
+ }
561
+
562
+ return [
563
+ model for model_id, model in self.models.items()
564
+ if model_id not in default_model_ids
565
+ ]
566
+
567
+ def update_model_info(self, model_id: str, updates: Dict[str, Any]) -> bool:
568
+ """Update an existing model's information"""
569
+ with self.lock:
570
+ if model_id not in self.models:
571
+ return False
572
+
573
+ model_info = self.models[model_id]
574
+
575
+ # Update allowed fields
576
+ allowed_fields = {
577
+ 'display_name', 'description', 'input_cost_per_1k', 'output_cost_per_1k',
578
+ 'context_length', 'supports_streaming', 'supports_function_calling',
579
+ 'is_premium', 'cat_personality'
580
+ }
581
+
582
+ for field, value in updates.items():
583
+ if field in allowed_fields and hasattr(model_info, field):
584
+ setattr(model_info, field, value)
585
+
586
+ # Save configuration
587
+ self._save_configuration()
588
+ self._save_custom_models()
589
+
590
+ return True
591
+
592
+ def _save_custom_models(self):
593
+ """Save custom models to persistent storage"""
594
+ custom_models = self.get_custom_models()
595
+
596
+ # Convert to serializable format
597
+ models_data = []
598
+ for model in custom_models:
599
+ model_dict = asdict(model)
600
+ model_dict['provider'] = model.provider.value # Convert enum to string
601
+ models_data.append(model_dict)
602
+
603
+ config = {
604
+ 'custom_models': models_data,
605
+ 'last_updated': datetime.now().isoformat()
606
+ }
607
+
608
+ if self.firebase_db:
609
+ try:
610
+ custom_models_ref = self.firebase_db.child('custom_models')
611
+ custom_models_ref.set(config)
612
+ except Exception as e:
613
+ print(f"Failed to save custom models to Firebase: {e}")
614
+ else:
615
+ try:
616
+ with open("custom_models.json", 'w') as f:
617
+ json.dump(config, f, indent=2)
618
+ except Exception as e:
619
+ print(f"Failed to save custom models to file: {e}")
620
+
621
+ def _load_custom_models(self):
622
+ """Load custom models from persistent storage"""
623
+ if self.firebase_db:
624
+ try:
625
+ custom_models_ref = self.firebase_db.child('custom_models')
626
+ config = custom_models_ref.get()
627
+
628
+ if config and 'custom_models' in config:
629
+ for model_data in config['custom_models']:
630
+ # Convert provider string back to enum
631
+ model_data['provider'] = AIProvider(model_data['provider'])
632
+ model_info = ModelInfo(**model_data)
633
+ self.models[model_info.model_id] = model_info
634
+
635
+ except Exception as e:
636
+ print(f"Failed to load custom models from Firebase: {e}")
637
+ else:
638
+ try:
639
+ if os.path.exists("custom_models.json"):
640
+ with open("custom_models.json", 'r') as f:
641
+ config = json.load(f)
642
+
643
+ if 'custom_models' in config:
644
+ for model_data in config['custom_models']:
645
+ # Convert provider string back to enum
646
+ model_data['provider'] = AIProvider(model_data['provider'])
647
+ model_info = ModelInfo(**model_data)
648
+ self.models[model_info.model_id] = model_info
649
+
650
+ except Exception as e:
651
+ print(f"Failed to load custom models from file: {e}")
652
+
653
+ def validate_model_data(self, model_data: Dict[str, Any]) -> Dict[str, str]:
654
+ """Validate model data and return any errors"""
655
+ errors = {}
656
+
657
+ # Required fields
658
+ required_fields = ['model_id', 'provider', 'display_name', 'description']
659
+ for field in required_fields:
660
+ if not model_data.get(field):
661
+ errors[field] = f"{field} is required"
662
+
663
+ # Validate provider
664
+ if model_data.get('provider'):
665
+ try:
666
+ AIProvider(model_data['provider'].lower())
667
+ except ValueError:
668
+ errors['provider'] = "Invalid AI provider"
669
+
670
+ # Validate numeric fields
671
+ numeric_fields = ['input_cost_per_1k', 'output_cost_per_1k', 'context_length']
672
+ for field in numeric_fields:
673
+ if field in model_data:
674
+ try:
675
+ float(model_data[field])
676
+ if float(model_data[field]) < 0:
677
+ errors[field] = f"{field} must be non-negative"
678
+ except (ValueError, TypeError):
679
+ errors[field] = f"{field} must be a valid number"
680
+
681
+ # Validate model_id uniqueness
682
+ if model_data.get('model_id') and model_data['model_id'] in self.models:
683
+ errors['model_id'] = "Model ID already exists"
684
+
685
+ # Validate cat personality
686
+ if model_data.get('cat_personality'):
687
+ valid_personalities = {"curious", "playful", "sleepy", "grumpy", "smart", "fast"}
688
+ if model_data['cat_personality'] not in valid_personalities:
689
+ errors['cat_personality'] = f"Invalid cat personality. Must be one of: {', '.join(valid_personalities)}"
690
+
691
+ return errors
692
 
693
  # Global instance
694
  model_manager = ModelFamilyManager()