diff --git a/routstr/payment/models.py b/routstr/payment/models.py
index 95d7ad89..afed3238 100644
--- a/routstr/payment/models.py
+++ b/routstr/payment/models.py
@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends
from pydantic.v1 import BaseModel
from sqlmodel.ext.asyncio.session import AsyncSession
-from ..core.db import ModelRow, get_session
+from ..core.db import ModelRow, UpstreamProviderRow, get_session
from ..core.logging import get_logger
from ..core.settings import settings
from .price import sats_usd_price
@@ -405,6 +405,76 @@ async def update_sats_pricing() -> None:
logger.error(f"Error updating sats pricing: {e}")
+class ModelTestRequest(BaseModel):
+ model_id: str
+ endpoint_type: str
+ request_data: dict
+
+
+@models_router.post("/api/models/test")
+async def test_model(
+ payload: ModelTestRequest,
+ session: AsyncSession = Depends(get_session),
+) -> dict:
+ """Test a model by sending a request through its configured upstream provider."""
+ from sqlmodel import select
+
+ result = await session.execute(
+ select(ModelRow).where(ModelRow.id == payload.model_id)
+ )
+ model_row = result.scalars().first()
+
+ if not model_row:
+ return {
+ "success": False,
+ "error": f"Model '{payload.model_id}' not found in database",
+ "status_code": 404,
+ }
+
+ provider = await session.get(UpstreamProviderRow, model_row.upstream_provider_id)
+ if not provider:
+ return {
+ "success": False,
+ "error": "Upstream provider not found",
+ "status_code": 404,
+ }
+
+ base_url = provider.base_url.rstrip("/")
+ if payload.endpoint_type == "chat-completions":
+ url = f"{base_url}/chat/completions"
+ else:
+ url = f"{base_url}/{payload.endpoint_type}"
+
+ actual_model_id = model_row.forwarded_model_id or model_row.id
+ request_data = dict(payload.request_data)
+ request_data["model"] = actual_model_id
+
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {provider.api_key}",
+ }
+
+ try:
+ async with httpx.AsyncClient(timeout=30.0) as client:
+ response = await client.post(url, json=request_data, headers=headers)
+ try:
+ response_data = response.json()
+ except Exception:
+ response_data = {"raw": response.text}
+
+ return {
+ "success": response.status_code < 400,
+ "data": response_data,
+ "status_code": response.status_code,
+ }
+ except Exception as e:
+ return {
+ "success": False,
+ "error": str(e),
+ "status_code": 500,
+ }
+
+
@models_router.get("/v1/models")
@models_router.get("/v1/models/", include_in_schema=False)
@models_router.get("/models")
diff --git a/ui/components/api-endpoint-tester.tsx b/ui/components/api-endpoint-tester.tsx
index a77b4248..dbb91266 100644
--- a/ui/components/api-endpoint-tester.tsx
+++ b/ui/components/api-endpoint-tester.tsx
@@ -471,7 +471,11 @@ export function ApiEndpointTester({ models }: ApiEndpointTesterProps) {
testEndpointMutation.mutate(requestData);
};
- const enabledModels = models.filter((model) => model.isEnabled);
+ const enabledModels = Array.from(
+ new Map(
+ models.filter((model) => model.isEnabled).map((m) => [m.id, m])
+ ).values()
+ );
const credentials = selectedModel ? getModelCredentials(selectedModel) : null;
const endpointUrl = credentials
? buildEndpointUrl(
diff --git a/ui/components/model-tester.tsx b/ui/components/model-tester.tsx
index dee500a7..3ecd43b3 100644
--- a/ui/components/model-tester.tsx
+++ b/ui/components/model-tester.tsx
@@ -197,7 +197,11 @@ export function ModelTester({ models }: ModelTesterProps) {
testModelMutation.mutate(request);
};
- const enabledModels = models.filter((model) => model.isEnabled);
+ const enabledModels = Array.from(
+ new Map(
+ models.filter((model) => model.isEnabled).map((m) => [m.id, m])
+ ).values()
+ );
const credentials = selectedModel ? getModelCredentials(selectedModel) : null;
return (
diff --git a/ui/components/models-page.tsx b/ui/components/models-page.tsx
index f8505d7f..bff3a379 100644
--- a/ui/components/models-page.tsx
+++ b/ui/components/models-page.tsx
@@ -124,12 +124,14 @@ export function ModelsPage() {
>
Basic Testing
+ {/*
API Endpoints
+ */}