From 4ed86b0091d546c4f435d76345ac2ee3f07e3371 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Mon, 22 Dec 2025 21:40:06 +0100 Subject: [PATCH] update model mapping to the ui --- routstr/core/admin.py | 119 +++++++++++++- routstr/model_mappings.json | 2 +- ui/app/model/page.tsx | 224 ++++++++++++++++++++++++++- ui/lib/api/services/modelMappings.ts | 78 ++++++++++ 4 files changed, 419 insertions(+), 4 deletions(-) create mode 100644 ui/lib/api/services/modelMappings.ts diff --git a/routstr/core/admin.py b/routstr/core/admin.py index 29ac8319..1ad21518 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -5,7 +5,7 @@ from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import HTMLResponse, RedirectResponse -from pydantic import BaseModel +from pydantic import BaseModel, Field from sqlmodel import select from ..payment.models import _row_to_model, list_models @@ -3165,3 +3165,120 @@ async def get_log_dates_api(request: Request) -> dict[str, object]: continue return {"dates": dates} + + +class ModelMappingRequest(BaseModel): + from_model: str = Field(..., alias="from") + to: str + + +class ModelMappingUpdateRequest(BaseModel): + to: str + + +@admin_router.get("/api/model-mappings", dependencies=[Depends(require_admin_api)]) +async def get_model_mappings(request: Request) -> dict[str, str]: + from ..proxy import _manual_model_mappings + return _manual_model_mappings + + +@admin_router.post("/api/model-mappings", dependencies=[Depends(require_admin_api)]) +async def create_model_mapping(request: Request, mapping: ModelMappingRequest) -> dict[str, str]: + import json + import os + from ..proxy import _manual_model_mappings, load_manual_model_mappings + + mappings_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "model_mappings.json") + + try: + if os.path.exists(mappings_file): + with open(mappings_file, "r") as f: + data = json.load(f) + else: + data = {"manual_model_mappings": {"mappings": {}}} + + data["manual_model_mappings"]["mappings"][mapping.from_model.lower()] = mapping.to.lower() + + with open(mappings_file, "w") as f: + json.dump(data, f, indent=2) + + load_manual_model_mappings() + + return _manual_model_mappings + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to create mapping: {str(e)}") + + +@admin_router.put("/api/model-mappings/{from_model}", dependencies=[Depends(require_admin_api)]) +async def update_model_mapping(request: Request, from_model: str, mapping: ModelMappingUpdateRequest) -> dict[str, str]: + import json + import os + from ..proxy import _manual_model_mappings, load_manual_model_mappings + + mappings_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "model_mappings.json") + + try: + if os.path.exists(mappings_file): + with open(mappings_file, "r") as f: + data = json.load(f) + else: + data = {"manual_model_mappings": {"mappings": {}}} + + if from_model.lower() not in data["manual_model_mappings"]["mappings"]: + raise HTTPException(status_code=404, detail="Mapping not found") + + data["manual_model_mappings"]["mappings"][from_model.lower()] = mapping.to.lower() + + with open(mappings_file, "w") as f: + json.dump(data, f, indent=2) + + load_manual_model_mappings() + + return _manual_model_mappings + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to update mapping: {str(e)}") + + +@admin_router.delete("/api/model-mappings/{from_model}", dependencies=[Depends(require_admin_api)]) +async def delete_model_mapping(request: Request, from_model: str) -> dict[str, str]: + import json + import os + from ..proxy import _manual_model_mappings, load_manual_model_mappings + + mappings_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "model_mappings.json") + + try: + if os.path.exists(mappings_file): + with open(mappings_file, "r") as f: + data = json.load(f) + else: + data = {"manual_model_mappings": {"mappings": {}}} + + if from_model.lower() not in data["manual_model_mappings"]["mappings"]: + raise HTTPException(status_code=404, detail="Mapping not found") + + del data["manual_model_mappings"]["mappings"][from_model.lower()] + + with open(mappings_file, "w") as f: + json.dump(data, f, indent=2) + + load_manual_model_mappings() + + return _manual_model_mappings + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to delete mapping: {str(e)}") + + +@admin_router.post("/api/model-mappings/reload", dependencies=[Depends(require_admin_api)]) +async def reload_model_mappings(request: Request) -> dict[str, object]: + from ..proxy import load_manual_model_mappings, _manual_model_mappings + + try: + load_manual_model_mappings() + return {"ok": True, "mappings": _manual_model_mappings} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to reload mappings: {str(e)}") diff --git a/routstr/model_mappings.json b/routstr/model_mappings.json index e5e9b72c..fcbd4e3b 100644 --- a/routstr/model_mappings.json +++ b/routstr/model_mappings.json @@ -4,4 +4,4 @@ "text-embedding-ada-002-v2": "text-embedding-ada-002" } } -} +} \ No newline at end of file diff --git a/ui/app/model/page.tsx b/ui/app/model/page.tsx index 6f523cf7..5450b320 100644 --- a/ui/app/model/page.tsx +++ b/ui/app/model/page.tsx @@ -10,16 +10,26 @@ import { SiteHeader } from '@/components/site-header'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useQuery } from '@tanstack/react-query'; import { AdminService } from '@/lib/api/services/admin'; +import { ModelMappingService } from '@/lib/api/services/modelMappings'; import { Skeleton } from '@/components/ui/skeleton'; import { AlertCircle, Users, Globe } from 'lucide-react'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; -import { useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import type { Model } from '@/lib/api/schemas/models'; import { groupAndSortModelsByProvider } from '@/lib/utils/modelSort'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Trash2, Plus, Edit2, Save, X } from 'lucide-react'; export default function ModelsPage() { const [filteredModels, setFilteredModels] = useState([]); + const [modelMappings, setModelMappings] = useState>( + {} + ); + const [editingMapping, setEditingMapping] = useState(null); + const [newMapping, setNewMapping] = useState({ from: '', to: '' }); const { data: modelsData, @@ -31,6 +41,23 @@ export default function ModelsPage() { refetchOnWindowFocus: false, }); + const { + data: mappingsData, + isLoading: isLoadingMappings, + error: mappingsError, + refetch: refetchMappings, + } = useQuery({ + queryKey: ['model-mappings'], + queryFn: () => ModelMappingService.getModelMappings(), + refetchOnWindowFocus: false, + }); + + React.useEffect(() => { + if (mappingsData) { + setModelMappings(mappingsData); + } + }, [mappingsData]); + const { models = [], groups = [] } = modelsData || {}; const groupedModels = useMemo(() => { @@ -67,6 +94,40 @@ export default function ModelsPage() { }); }, [groupedModels, groupDataMap, groups]); + const handleAddMapping = async () => { + if (!newMapping.from || !newMapping.to) return; + + try { + await ModelMappingService.createModelMapping({ + from: newMapping.from, + to: newMapping.to, + }); + setNewMapping({ from: '', to: '' }); + refetchMappings(); + } catch (error) { + console.error('Failed to add mapping:', error); + } + }; + + const handleDeleteMapping = async (from: string) => { + try { + await ModelMappingService.deleteModelMapping(from); + refetchMappings(); + } catch (error) { + console.error('Failed to delete mapping:', error); + } + }; + + const handleUpdateMapping = async (from: string, to: string) => { + try { + await ModelMappingService.updateModelMapping(from, { to }); + setEditingMapping(null); + refetchMappings(); + } catch (error) { + console.error('Failed to update mapping:', error); + } + }; + return ( @@ -81,8 +142,9 @@ export default function ModelsPage() { - + Manage Models + Model Mappings {/*Basic Testing API Endpoints */} @@ -267,6 +329,164 @@ export default function ModelsPage() { )} + +
+ Manage model ID mappings to redirect requests from one model + to another. This is useful for maintaining compatibility with + legacy model names or creating aliases. +
+ + {isLoadingMappings ? ( +
+ +
+ ) : mappingsError ? ( + + + + Failed to load model mappings. Please try refreshing the + page. + + + ) : ( +
+ + + + + Add New Model Mapping + + + +
+ + setNewMapping({ + ...newMapping, + from: e.target.value, + }) + } + /> + + setNewMapping({ + ...newMapping, + to: e.target.value, + }) + } + /> + +
+
+
+ + + + Current Model Mappings + + + {Object.keys(modelMappings).length === 0 ? ( +
+ No model mappings configured +
+ ) : ( +
+ {Object.entries(modelMappings).map(([from, to]) => ( +
+
+
+ +
+ {from} +
+
+
+ + {editingMapping === from ? ( +
+ + + +
+ ) : ( +
+ {to} +
+ )} +
+
+ {editingMapping !== from && ( +
+ + +
+ )} +
+ ))} +
+ )} +
+
+
+ )} +
+
Test model credentials and connectivity with basic chat diff --git a/ui/lib/api/services/modelMappings.ts b/ui/lib/api/services/modelMappings.ts new file mode 100644 index 00000000..d20e28c5 --- /dev/null +++ b/ui/lib/api/services/modelMappings.ts @@ -0,0 +1,78 @@ +import { apiClient } from '../client'; +import { z } from 'zod'; + +export const ModelMappingSchema = z.object({ + from: z.string(), + to: z.string(), +}); + +export const CreateModelMappingSchema = z.object({ + from: z.string(), + to: z.string(), +}); + +export const UpdateModelMappingSchema = z.object({ + to: z.string(), +}); + +export const ModelMappingsResponseSchema = z.record(z.string()); + +export const ReloadMappingsResponseSchema = z.object({ + ok: z.boolean(), + mappings: z.record(z.string()), +}); + +export type ModelMapping = z.infer; +export type CreateModelMapping = z.infer; +export type UpdateModelMapping = z.infer; +export type ModelMappingsResponse = z.infer; +export type ReloadMappingsResponse = z.infer< + typeof ReloadMappingsResponseSchema +>; + +export class ModelMappingService { + static async getModelMappings(): Promise { + return await apiClient.get( + '/admin/api/model-mappings' + ); + } + + static async createModelMapping( + data: CreateModelMapping + ): Promise { + return await apiClient.post( + '/admin/api/model-mappings', + { + from: data.from, + to: data.to, + } + ); + } + + static async updateModelMapping( + fromModel: string, + data: UpdateModelMapping + ): Promise { + return await apiClient.put( + `/admin/api/model-mappings/${encodeURIComponent(fromModel)}`, + { + to: data.to, + } + ); + } + + static async deleteModelMapping( + fromModel: string + ): Promise { + return await apiClient.delete( + `/admin/api/model-mappings/${encodeURIComponent(fromModel)}` + ); + } + + static async reloadModelMappings(): Promise { + return await apiClient.post( + '/admin/api/model-mappings/reload', + {} + ); + } +}