diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index aca29594..ae676584 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -62,14 +62,18 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: '18'
- cache: 'npm'
+ node-version: "18"
+ cache: "npm"
cache-dependency-path: ui/package-lock.json
- name: Install UI dependencies
working-directory: ./ui
run: npm ci
+ - name: Run UI format check
+ working-directory: ./ui
+ run: npm run format-check
+
- name: Run UI linting
working-directory: ./ui
run: npm run lint
diff --git a/routstr/algorithm.py b/routstr/algorithm.py
index a1b6babd..acd059eb 100644
--- a/routstr/algorithm.py
+++ b/routstr/algorithm.py
@@ -150,18 +150,6 @@ def should_prefer_model(
# Prefer lower adjusted cost
should_replace = candidate_adjusted < current_adjusted
- # Log provider changes when candidate wins
- if should_replace:
- candidate_provider_name = getattr(
- candidate_provider, "provider_type", "unknown"
- )
- current_provider_name = getattr(current_provider, "provider_type", "unknown")
- logger.debug(
- f"Model selection for alias '{alias}': choosing {candidate_provider_name} "
- f"(cost: ${candidate_adjusted:.6f}) over {current_provider_name} "
- f"(cost: ${current_adjusted:.6f})"
- )
-
return should_replace
diff --git a/routstr/core/db.py b/routstr/core/db.py
index 96fe91d0..f40212b3 100644
--- a/routstr/core/db.py
+++ b/routstr/core/db.py
@@ -149,8 +149,6 @@ def run_migrations() -> None:
import pathlib
try:
- logger.info("Starting database migrations")
-
# Get the path to the alembic.ini file
project_root = pathlib.Path(__file__).resolve().parents[2]
alembic_ini_path = project_root / "alembic.ini"
@@ -167,7 +165,6 @@ def run_migrations() -> None:
alembic_cfg.set_main_option("sqlalchemy.url", DATABASE_URL)
# Run migrations to the latest revision
- logger.info("Running migrations to latest revision")
command.upgrade(alembic_cfg, "head")
logger.info("Database migrations completed successfully")
diff --git a/routstr/core/logging.py b/routstr/core/logging.py
index 65b6a5c0..00474949 100644
--- a/routstr/core/logging.py
+++ b/routstr/core/logging.py
@@ -338,6 +338,11 @@ def setup_logging() -> None:
"handlers": ["console"] if console_enabled else [],
"propagate": False,
},
+ "openai": {
+ "level": "WARNING",
+ "handlers": ["console"] if console_enabled else [],
+ "propagate": False,
+ },
"httpcore": {
"level": "WARNING",
"handlers": ["console"] if console_enabled else [],
@@ -360,6 +365,11 @@ def setup_logging() -> None:
},
"watchfiles.main": {"level": "WARNING", "handlers": [], "propagate": False},
"aiosqlite": {"level": "ERROR", "handlers": [], "propagate": False},
+ "alembic": {
+ "level": "WARNING",
+ "handlers": ["console"] if console_enabled else [],
+ "propagate": False,
+ },
},
"root": {
"level": log_level,
diff --git a/routstr/core/main.py b/routstr/core/main.py
index 06b86d65..d2974d4a 100644
--- a/routstr/core/main.py
+++ b/routstr/core/main.py
@@ -54,9 +54,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
try:
# Run database migrations on startup
- # This ensures the database schema is always up-to-date in production
- # Migrations are idempotent - running them multiple times is safe
- logger.info("Running database migrations")
run_migrations()
# Initialize database connection pools
@@ -104,6 +101,9 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
yield
+ except asyncio.CancelledError:
+ # Expected during shutdown
+ pass
except Exception as e:
logger.error(
"Application startup failed",
diff --git a/routstr/payment/models.py b/routstr/payment/models.py
index 4e21f91e..a7bc5cf5 100644
--- a/routstr/payment/models.py
+++ b/routstr/payment/models.py
@@ -2,7 +2,6 @@ import asyncio
import json
import random
from pathlib import Path
-from typing import Final
from urllib.request import urlopen
import httpx
@@ -20,15 +19,6 @@ logger = get_logger(__name__)
models_router = APIRouter()
-DEFAULT_EXCLUDED_MODEL_IDS: Final[set[str]] = {
- "openrouter/auto",
- "openrouter/bodybuilder",
- "google/gemini-2.5-pro-exp-03-25",
- "opengvlab/internvl3-78b",
- "openrouter/sonoma-dusk-alpha",
- "openrouter/sonoma-sky-alpha",
-}
-
class Architecture(BaseModel):
modality: str
@@ -45,6 +35,8 @@ class Pricing(BaseModel):
image: float = 0.0
web_search: float = 0.0
internal_reasoning: float = 0.0
+ input_cache_read: float = 0.0
+ input_cache_write: float = 0.0
max_prompt_cost: float = 0.0 # in sats not msats
max_completion_cost: float = 0.0 # in sats not msats
max_cost: float = 0.0 # in sats not msats
@@ -76,6 +68,27 @@ class Model(BaseModel):
return hash(self.id)
+def _has_valid_pricing(model: dict) -> bool:
+ """Check if model has valid pricing (not free, no negative values)."""
+ pricing = model.get("pricing", {})
+ if not pricing:
+ return False
+
+ try:
+ prompt = float(pricing.get("prompt", 0))
+ completion = float(pricing.get("completion", 0))
+ except (ValueError, TypeError):
+ return False
+
+ if prompt < 0 or completion < 0:
+ return False
+
+ if prompt == 0 and completion == 0:
+ return False
+
+ return True
+
+
def fetch_openrouter_models(source_filter: str | None = None) -> list[dict]:
"""Fetches model information from OpenRouter API."""
base_url = "https://openrouter.ai/api/v1"
@@ -97,10 +110,10 @@ def fetch_openrouter_models(source_filter: str | None = None) -> list[dict]:
model["id"] = model_id[len(source_prefix) :]
model_id = model["id"]
- if (
- "(free)" in model.get("name", "")
- or model_id in DEFAULT_EXCLUDED_MODEL_IDS
- ):
+ if "(free)" in model.get("name", ""):
+ continue
+
+ if not _has_valid_pricing(model):
continue
models_data.append(model)
@@ -134,10 +147,10 @@ async def async_fetch_openrouter_models(source_filter: str | None = None) -> lis
model["id"] = model_id[len(source_prefix) :]
model_id = model["id"]
- if (
- "(free)" in model.get("name", "")
- or model_id in DEFAULT_EXCLUDED_MODEL_IDS
- ):
+ if "(free)" in model.get("name", ""):
+ continue
+
+ if not _has_valid_pricing(model):
continue
models_data.append(model)
@@ -201,7 +214,22 @@ def load_models() -> list[Model]:
return []
logger.info(f"Successfully fetched {len(models_data)} models from OpenRouter API")
- return [Model(**model) for model in models_data] # type: ignore
+
+ valid_models = []
+ for model_data in models_data:
+ try:
+ model = Model(**model_data) # type: ignore
+ valid_models.append(model)
+ except Exception as e:
+ model_id = model_data.get("id", "unknown")
+ logger.warning(f"Skipping model {model_id} - validation failed: {e}")
+
+ if len(valid_models) != len(models_data):
+ logger.warning(
+ f"Filtered out {len(models_data) - len(valid_models)} models with incomplete data"
+ )
+
+ return valid_models
def _row_to_model(
diff --git a/routstr/payment/price.py b/routstr/payment/price.py
index c20ac34e..9011ce1d 100644
--- a/routstr/payment/price.py
+++ b/routstr/payment/price.py
@@ -110,10 +110,6 @@ async def _update_prices() -> None:
return
BTC_USD_PRICE = btc_price
SATS_USD_PRICE = btc_price / 100_000_000
- logger.info(
- "Updated BTC/USD price",
- extra={"btc_usd": btc_price, "sats_usd": SATS_USD_PRICE},
- )
def btc_usd_price() -> float:
diff --git a/routstr/upstream/base.py b/routstr/upstream/base.py
index 64eb6a4c..7726b5f6 100644
--- a/routstr/upstream/base.py
+++ b/routstr/upstream/base.py
@@ -1726,7 +1726,6 @@ class BaseUpstreamProvider:
Returns:
List of Model objects with pricing
"""
- logger.debug(f"Fetching models for {self.provider_type or self.base_url}")
try:
or_models, provider_models_response = await asyncio.gather(
@@ -1753,18 +1752,9 @@ class BaseUpstreamProvider:
else:
not_found_models.append(model_id)
- logger.info(
- "Fetched models for provider",
- extra={
- "provider": self.provider_type or self.base_url,
- "found_count": len(found_models),
- "not_found_count": len(not_found_models),
- },
- )
-
if not_found_models:
logger.debug(
- "Models not found in OpenRouter",
+ f"({len(not_found_models)}/{len(provider_model_ids)}) unmatched models for {self.provider_type or self.base_url}",
extra={"not_found_models": not_found_models},
)
@@ -1832,10 +1822,7 @@ class BaseUpstreamProvider:
self._models_cache = models_with_fees
self._models_by_id = {m.id: m for m in self._models_cache}
- logger.info(
- f"Refreshed models cache for {self.provider_type or self.base_url}",
- extra={"model_count": len(models)},
- )
+
except Exception as e:
logger.error(
f"Failed to refresh models cache for {self.provider_type or self.base_url}",
diff --git a/routstr/upstream/helpers.py b/routstr/upstream/helpers.py
index 3d91550f..b453af1f 100644
--- a/routstr/upstream/helpers.py
+++ b/routstr/upstream/helpers.py
@@ -193,7 +193,7 @@ async def init_upstreams() -> list[BaseUpstreamProvider]:
if provider:
await provider.refresh_models_cache()
upstreams.append(provider)
- logger.info(
+ logger.debug(
f"Initialized {provider_row.provider_type} provider",
extra={
"base_url": provider_row.base_url,
diff --git a/routstr/upstream/ppqai.py b/routstr/upstream/ppqai.py
index 24013edb..0b4dfb08 100644
--- a/routstr/upstream/ppqai.py
+++ b/routstr/upstream/ppqai.py
@@ -102,11 +102,6 @@ class PPQAIUpstreamProvider(BaseUpstreamProvider):
url = f"{self.base_url}/models"
headers = {"Authorization": f"Bearer {self.api_key}"}
- logger.debug(
- "Fetching models from PPQ.AI",
- extra={"url": url, "has_api_key": bool(self.api_key)},
- )
-
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(url, headers=headers)
@@ -114,10 +109,6 @@ class PPQAIUpstreamProvider(BaseUpstreamProvider):
data = response.json()
models_data = data.get("data", [])
- logger.info(
- "Fetched models from PPQ.AI",
- extra={"model_count": len(models_data)},
- )
or_models = [
Model(**model) # type: ignore
@@ -198,15 +189,6 @@ class PPQAIUpstreamProvider(BaseUpstreamProvider):
return models
- except httpx.HTTPStatusError as e:
- logger.error(
- "HTTP error fetching models from PPQ.AI",
- extra={
- "status_code": e.response.status_code,
- "error": str(e),
- },
- )
- return []
except Exception as e:
logger.error(
"Error fetching models from PPQ.AI",
diff --git a/tests/integration/test_performance_load.py b/tests/integration/test_performance_load.py
index 581553b9..e5e5c3ce 100644
--- a/tests/integration/test_performance_load.py
+++ b/tests/integration/test_performance_load.py
@@ -142,46 +142,6 @@ class TestPerformanceBaseline:
f" P99: {sorted(response_times)[int(len(response_times) * 0.99)]:.2f}ms"
)
- @pytest.mark.asyncio
- async def test_database_query_performance(
- self, integration_session: Any, db_snapshot: Any
- ) -> None:
- """Test database operation performance"""
- from sqlmodel import select
-
- from routstr.core.db import ApiKey
-
- # Create test data
- for i in range(100):
- key = ApiKey(
- hashed_key=f"test_key_{i}",
- balance=1000000,
- total_spent=0,
- total_requests=0,
- )
- integration_session.add(key)
- await integration_session.commit()
-
- # Test query performance
- query_times = []
-
- for _ in range(100):
- start = time.time()
- result = await integration_session.execute(
- select(ApiKey).where(ApiKey.balance > 0) # type: ignore[arg-type]
- )
- _ = result.all()
- duration = (time.time() - start) * 1000
- query_times.append(duration)
-
- # All queries should complete < 100ms
- assert max(query_times) < 100, (
- f"Max query time {max(query_times)}ms exceeds 100ms limit"
- )
- print("\nDatabase query performance:")
- print(f" Mean: {statistics.mean(query_times):.2f}ms")
- print(f" Max: {max(query_times):.2f}ms")
-
@pytest.mark.integration
@pytest.mark.slow
diff --git a/ui/app/balances/page.tsx b/ui/app/balances/page.tsx
index b5213287..89e44f3a 100644
--- a/ui/app/balances/page.tsx
+++ b/ui/app/balances/page.tsx
@@ -29,9 +29,7 @@ export default function BalancesPage() {
-
- Balances
-
+
Balances
Monitor and manage wallet balances
diff --git a/ui/app/login/page.tsx b/ui/app/login/page.tsx
index 7df50995..ba47a120 100644
--- a/ui/app/login/page.tsx
+++ b/ui/app/login/page.tsx
@@ -78,8 +78,8 @@ export default function AdminLoginPage(): ReactElement {
};
return (
-
-
+
+
Admin Login
diff --git a/ui/app/logs/log-details-dialog.tsx b/ui/app/logs/log-details-dialog.tsx
index 852aa4a8..5c0193ce 100644
--- a/ui/app/logs/log-details-dialog.tsx
+++ b/ui/app/logs/log-details-dialog.tsx
@@ -97,7 +97,7 @@ export function LogDetailsDialog({
Message
@@ -116,7 +116,12 @@ export function LogDetailsDialog({
copyToClipboard(String(log[field as keyof LogEntry] || ''), field)}
+ onClick={() =>
+ copyToClipboard(
+ String(log[field as keyof LogEntry] || ''),
+ field
+ )
+ }
className='h-6 flex-shrink-0 px-2'
>
{copiedField === field ? (
@@ -175,7 +180,9 @@ export function LogDetailsDialog({
copyToClipboard(JSON.stringify(log, null, 2), 'json')}
+ onClick={() =>
+ copyToClipboard(JSON.stringify(log, null, 2), 'json')
+ }
className='h-6 px-2'
>
{copiedField === 'json' ? (
diff --git a/ui/app/model/page.tsx b/ui/app/model/page.tsx
index 6c415827..6f523cf7 100644
--- a/ui/app/model/page.tsx
+++ b/ui/app/model/page.tsx
@@ -205,32 +205,45 @@ export default function ModelsPage() {
{totalModels === 0 && (
-
+
No models found for this provider
-
-
Common issues:
-
+
+
+ Common issues:
+
+
- API credentials: Check if the API key is correct and has the right permissions
+ API credentials: {' '}
+ Check if the API key is correct
+ and has the right permissions
- Base URL: Verify the base URL is correct for your provider
+ Base URL: Verify
+ the base URL is correct for your
+ provider
- Network access: Ensure the server can reach the provider's API endpoint
+ Network access: {' '}
+ Ensure the server can reach the
+ provider's API endpoint
- Provider status: The upstream provider might be temporarily unavailable
+ Provider status: {' '}
+ The upstream provider might be
+ temporarily unavailable
{groupData?.group_url && (
-
- Current endpoint: {groupData.group_url}
+
+ Current endpoint:{' '}
+
+ {groupData.group_url}
+
)}
diff --git a/ui/app/page.tsx b/ui/app/page.tsx
index 041512eb..ba5dff37 100644
--- a/ui/app/page.tsx
+++ b/ui/app/page.tsx
@@ -53,7 +53,7 @@ export default function DashboardPage() {
window.removeEventListener('storage', syncAuthState);
};
}, []);
-
+
const { data: btcUsdPrice } = useQuery({
queryKey: ['btc-usd-price'],
queryFn: fetchBtcUsdPrice,
@@ -131,8 +131,13 @@ export default function DashboardPage() {
-
Dashboard
-
+
+ Dashboard
+
+
@@ -140,8 +145,9 @@ export default function DashboardPage() {
Usage Analytics
-
- Monitor requests, errors, and revenue over the last {timeRange} hours
+
+ Monitor requests, errors, and revenue over the last {timeRange}{' '}
+ hours
@@ -176,27 +182,31 @@ export default function DashboardPage() {
{summaryLoading ? (
-
Loading summary...
+
Loading summary...
) : summaryData ? (
) : null}
{metricsLoading ? (
-
+
Loading metrics...
) : metricsData && metricsData.metrics.length > 0 ? (
<>
-
+
({
- ...m,
- revenue_sats: m.revenue_msats / 1000,
- refunds_sats: m.refunds_msats / 1000,
- net_revenue_sats:
- (m.revenue_msats - m.refunds_msats) / 1000,
- })) as Array & { timestamp: string }>}
+ data={
+ metricsData.metrics.map((m) => ({
+ ...m,
+ revenue_sats: m.revenue_msats / 1000,
+ refunds_sats: m.refunds_msats / 1000,
+ net_revenue_sats:
+ (m.revenue_msats - m.refunds_msats) / 1000,
+ })) as Array<
+ Record & { timestamp: string }
+ >
+ }
title='Revenue Over Time (sats)'
dataKeys={[
{
@@ -218,7 +228,11 @@ export default function DashboardPage() {
/>
& { timestamp: string }>}
+ data={
+ metricsData.metrics as Array<
+ Record & { timestamp: string }
+ >
+ }
title='Request Volume'
dataKeys={[
{
@@ -239,7 +253,11 @@ export default function DashboardPage() {
]}
/>
& { timestamp: string }>}
+ data={
+ metricsData.metrics as Array<
+ Record & { timestamp: string }
+ >
+ }
title='Error Tracking'
dataKeys={[
{
@@ -260,7 +278,11 @@ export default function DashboardPage() {
]}
/>
& { timestamp: string }>}
+ data={
+ metricsData.metrics as Array<
+ Record & { timestamp: string }
+ >
+ }
title='Payment Activity'
dataKeys={[
{
@@ -270,7 +292,7 @@ export default function DashboardPage() {
},
]}
/>
-
+
{summaryData && summaryData.unique_models.length > 0 && (
@@ -306,7 +328,9 @@ export default function DashboardPage() {
key={type}
className='flex items-center justify-between'
>
- {type}
+
+ {type}
+
{count}
@@ -335,16 +359,18 @@ export default function DashboardPage() {
{revenueByModelLoading ? (
-
Loading revenue by model...
+
+ Loading revenue by model...
+
) : revenueByModelData && revenueByModelData.models.length > 0 ? (
-
) : null}
{errorLoading ? (
-
Loading errors...
+
Loading errors...
) : errorData ? (
) : null}
diff --git a/ui/app/providers/page.tsx b/ui/app/providers/page.tsx
index f431f101..4624f26a 100644
--- a/ui/app/providers/page.tsx
+++ b/ui/app/providers/page.tsx
@@ -76,7 +76,11 @@ function ProviderBalance({
);
const queryClient = useQueryClient();
- const { data: balanceData, isLoading, error } = useQuery({
+ const {
+ data: balanceData,
+ isLoading,
+ error,
+ } = useQuery({
queryKey: ['provider-balance', providerId],
queryFn: () => AdminService.getProviderBalance(providerId),
refetchInterval: 30000,
@@ -108,7 +112,10 @@ function ProviderBalance({
mutationFn: async (amount: number) => {
console.log('Calling top-up API with:', { providerId, amount });
try {
- const result = await AdminService.initiateProviderTopup(providerId, amount);
+ const result = await AdminService.initiateProviderTopup(
+ providerId,
+ amount
+ );
console.log('API returned:', result);
return result;
} catch (err) {
@@ -120,11 +127,8 @@ function ProviderBalance({
console.log('Top-up response:', data);
console.log('Type of data:', typeof data);
console.log('Keys in data:', Object.keys(data || {}));
-
- if (
- data?.topup_data?.payment_request &&
- data?.topup_data?.invoice_id
- ) {
+
+ if (data?.topup_data?.payment_request && data?.topup_data?.invoice_id) {
setInvoiceData({
payment_request: data.topup_data.payment_request as string,
invoice_id: data.topup_data.invoice_id as string,
@@ -172,19 +176,23 @@ function ProviderBalance({
// The backend throws 500/400 if not implemented.
// A better approach is to check if we have a platform URL and maybe redirect there
// if we know it's not supported.
-
+
// BUT, we don't know for sure if it's supported without checking metadata or trying.
// Let's rely on the "can_topup" metadata if available, but currently we only have "can_show_balance".
-
+
// Simple heuristic: If platformUrl exists and we suspect no direct topup, redirect?
// Actually, let's try to open the dialog, but if it's OpenRouter/OpenAI, maybe we just redirect?
// The user specifically mentioned "like in openrouter".
-
- if (platformUrl && (platformUrl.includes('openrouter.ai') || platformUrl.includes('openai.com'))) {
- window.open(platformUrl, '_blank');
- return;
+
+ if (
+ platformUrl &&
+ (platformUrl.includes('openrouter.ai') ||
+ platformUrl.includes('openai.com'))
+ ) {
+ window.open(platformUrl, '_blank');
+ return;
}
-
+
setIsTopupDialogOpen(true);
};
@@ -266,9 +274,7 @@ function ProviderBalance({
-
- Top-up successful!
-
+ Top-up successful!
) : invoiceData ? (
@@ -482,7 +488,9 @@ export default function ProvidersPage() {
...formData,
api_key: String(response.account_data.api_key),
});
- toast.success('Account created successfully! API key has been filled in.');
+ toast.success(
+ 'Account created successfully! API key has been filled in.'
+ );
} else {
toast.success('Account created, but no API key returned.');
}
@@ -778,8 +786,7 @@ export default function ProvidersPage() {
)}
/>
- 1.01 means +1% e.g. currency exchange, card
- fees, etc.
+ 1.01 means +1% e.g. currency exchange, card fees, etc.
@@ -856,9 +863,11 @@ export default function ProvidersPage() {
{canShowBalance(provider.provider_type) &&
provider.api_key && (
-
)}
- No models configured. Add custom models to use this provider.
+ No models configured. Add custom models
+ to use this provider.
- handleAddModel(provider.id)}>
+
+ handleAddModel(provider.id)
+ }
+ >
Add Custom Model
@@ -970,7 +986,12 @@ export default function ProvidersPage() {
variant='ghost'
size='icon'
className='h-8 w-8'
- onClick={() => handleEditModel(provider.id, model)}
+ onClick={() =>
+ handleEditModel(
+ provider.id,
+ model
+ )
+ }
>
@@ -1027,10 +1048,17 @@ export default function ProvidersPage() {
{providerModels.db_models.length > 0 && (
- Custom models override or extend the provider's catalog.
+ Custom models override or extend the
+ provider's catalog.
)}
-
handleAddModel(provider.id)}>
+
+ handleAddModel(provider.id)
+ }
+ >
Add
@@ -1079,7 +1107,12 @@ export default function ProvidersPage() {
variant='ghost'
size='icon'
className='h-8 w-8'
- onClick={() => handleEditModel(provider.id, model)}
+ onClick={() =>
+ handleEditModel(
+ provider.id,
+ model
+ )
+ }
>
@@ -1126,7 +1159,12 @@ export default function ProvidersPage() {
variant='outline'
size='sm'
className='h-7 text-xs'
- onClick={() => handleOverrideModel(provider.id, model)}
+ onClick={() =>
+ handleOverrideModel(
+ provider.id,
+ model
+ )
+ }
>
Override
@@ -1245,7 +1283,7 @@ export default function ProvidersPage() {
)}
-
@@ -1277,8 +1315,7 @@ export default function ProvidersPage() {
)}
/>
- 1.01 means +1% e.g. currency exchange, card
- fees, etc.
+ 1.01 means +1% e.g. currency exchange, card fees, etc.
@@ -1303,7 +1340,9 @@ export default function ProvidersPage() {
setModelDialogState((prev) => ({ ...prev, isOpen: false }))}
+ onClose={() =>
+ setModelDialogState((prev) => ({ ...prev, isOpen: false }))
+ }
onSuccess={() => {
queryClient.invalidateQueries({
queryKey: ['provider-models', modelDialogState.providerId],
diff --git a/ui/components/AddProviderModelDialog.tsx b/ui/components/AddProviderModelDialog.tsx
index 1bc17154..301682f4 100644
--- a/ui/components/AddProviderModelDialog.tsx
+++ b/ui/components/AddProviderModelDialog.tsx
@@ -102,7 +102,8 @@ export function AddProviderModelDialog({
}: AddProviderModelDialogProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [isPresetOpen, setIsPresetOpen] = useState(false);
- const [selectedPresetLabel, setSelectedPresetLabel] = useState('Select a preset');
+ const [selectedPresetLabel, setSelectedPresetLabel] =
+ useState('Select a preset');
const form = useForm({
resolver: zodResolver(FormSchema) as never,
@@ -149,7 +150,10 @@ export function AddProviderModelDialog({
if (initialData) {
const architecture = initialData.architecture as Record;
const pricing = initialData.pricing as Record;
- const topProvider = initialData.top_provider as Record | null;
+ const topProvider = initialData.top_provider as Record<
+ string,
+ unknown
+ > | null;
form.reset({
id: initialData.id,
@@ -250,7 +254,9 @@ export function AddProviderModelDialog({
form.setValue('context_length', model.context_length);
form.setValue(
'modality',
- typeof architecture?.modality === 'string' ? architecture.modality : 'text'
+ typeof architecture?.modality === 'string'
+ ? architecture.modality
+ : 'text'
);
form.setValue(
'input_modalities_raw',
@@ -318,7 +324,10 @@ export function AddProviderModelDialog({
setIsSubmitting(true);
try {
let perRequestLimits: Record | null = null;
- if (data.per_request_limits_raw && data.per_request_limits_raw.trim().length) {
+ if (
+ data.per_request_limits_raw &&
+ data.per_request_limits_raw.trim().length
+ ) {
try {
perRequestLimits = JSON.parse(data.per_request_limits_raw);
} catch {
@@ -336,7 +345,9 @@ export function AddProviderModelDialog({
context_length: data.context_length,
architecture: {
modality: data.modality,
- input_modalities: listFromString(data.input_modalities_raw || data.modality),
+ input_modalities: listFromString(
+ data.input_modalities_raw || data.modality
+ ),
output_modalities: listFromString(
data.output_modalities_raw || data.modality
),
@@ -366,10 +377,9 @@ export function AddProviderModelDialog({
is_moderated: data.top_provider_is_moderated,
}
: null,
- upstream_provider_id:
- data.upstream_provider_id?.trim().length
- ? data.upstream_provider_id.trim()
- : providerId,
+ upstream_provider_id: data.upstream_provider_id?.trim().length
+ ? data.upstream_provider_id.trim()
+ : providerId,
canonical_slug: data.canonical_slug?.trim() || null,
alias_ids: listFromString(data.alias_ids_raw || ''),
enabled: data.enabled,
@@ -416,7 +426,7 @@ export function AddProviderModelDialog({
{description}
{!isEdit && !isOverride && (
-
+
Presets
@@ -425,23 +435,25 @@ export function AddProviderModelDialog({
variant='outline'
role='combobox'
aria-expanded={isPresetOpen}
- className='w-full justify-between text-left text-sm overflow-hidden'
+ className='w-full justify-between overflow-hidden text-left text-sm'
>
- {isLoadingPresets ? 'Loading presets...' : selectedPresetLabel}
+ {isLoadingPresets
+ ? 'Loading presets...'
+ : selectedPresetLabel}
- e.preventDefault()}
>
- e.stopPropagation()}
>
@@ -475,8 +487,9 @@ export function AddProviderModelDialog({
-
- Prefill fields from a preset model definition, then adjust as needed.
+
+ Prefill fields from a preset model definition, then adjust as
+ needed.
)}
@@ -496,7 +509,9 @@ export function AddProviderModelDialog({
disabled={isOverride || isEdit}
/>
-
Unique identifier for the model
+
+ Unique identifier for the model
+
)}
@@ -524,7 +539,11 @@ export function AddProviderModelDialog({
Description
-
+
@@ -575,10 +594,7 @@ export function AddProviderModelDialog({
Input Modalities
-
+
Comma-separated list
@@ -593,10 +609,7 @@ export function AddProviderModelDialog({
Output Modalities
-
+
Comma-separated list
@@ -643,7 +656,10 @@ export function AddProviderModelDialog({
Canonical Slug
-
+
@@ -697,15 +713,19 @@ export function AddProviderModelDialog({
rows={4}
/>
- JSON object; leave empty for none
+
+ JSON object; leave empty for none
+
)}
/>
-
-
Pricing (USD per 1M tokens)
+
+
+ Pricing (USD per 1M tokens)
+
-
+
Top Provider (optional)
(
- Is Moderated
- Whether provider enforces moderation
+
+ Is Moderated
+
+
+ Whether provider enforces moderation
+
-
+
)}
@@ -892,7 +919,10 @@ export function AddProviderModelDialog({
Enable this model for use
-
+
)}
@@ -908,8 +938,14 @@ export function AddProviderModelDialog({
Cancel
- {isSubmitting && }
- {isEdit ? 'Save Changes' : isOverride ? 'Create Override' : 'Create Model'}
+ {isSubmitting && (
+
+ )}
+ {isEdit
+ ? 'Save Changes'
+ : isOverride
+ ? 'Create Override'
+ : 'Create Model'}
@@ -918,4 +954,3 @@ export function AddProviderModelDialog({
);
}
-
diff --git a/ui/components/ModelSelector.tsx b/ui/components/ModelSelector.tsx
index 07ac3700..fc62ff2a 100644
--- a/ui/components/ModelSelector.tsx
+++ b/ui/components/ModelSelector.tsx
@@ -2,11 +2,12 @@
import React, { useState, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { type Model, type GroupSettings } from '@/lib/api/schemas/models';
import {
- type Model,
- type GroupSettings,
-} from '@/lib/api/schemas/models';
-import { AdminService, type AdminModelGroup, type AdminModel } from '@/lib/api/services/admin';
+ AdminService,
+ type AdminModelGroup,
+ type AdminModel,
+} from '@/lib/api/services/admin';
type ModelGroup = AdminModelGroup;
import { AddProviderModelDialog } from '@/components/AddProviderModelDialog';
import { EditGroupForm } from '@/components/EditGroupForm';
@@ -514,7 +515,7 @@ export function ModelSelector({
return;
}
const providerId = parseInt(model.provider_id);
-
+
// Construct AdminModel from Model
const adminModel: AdminModel = {
id: model.id,
@@ -557,7 +558,7 @@ export function ModelSelector({
return;
}
const providerId = parseInt(model.provider_id);
-
+
// Construct AdminModel from Model
const adminModel: AdminModel = {
id: model.id,
@@ -901,10 +902,10 @@ export function ModelSelector({
)}
{/* Model Management Actions */}
{groupData && (
-
handleAddModelClick(parseInt(groupData.id))}
+ handleAddModelClick(parseInt(groupData.id))}
className='gap-2'
- variant="outline"
+ variant='outline'
>
Add Custom Model
@@ -1306,7 +1307,9 @@ export function ModelSelector({
setModelDialogState((prev) => ({ ...prev, isOpen: false }))}
+ onClose={() =>
+ setModelDialogState((prev) => ({ ...prev, isOpen: false }))
+ }
onSuccess={handleModelUpdate}
initialData={modelDialogState.initialData}
mode={modelDialogState.mode}
diff --git a/ui/components/currency-toggle.tsx b/ui/components/currency-toggle.tsx
index 14045e5c..7fac8437 100644
--- a/ui/components/currency-toggle.tsx
+++ b/ui/components/currency-toggle.tsx
@@ -48,20 +48,26 @@ export function CurrencyToggle() {
return (
-
-
- {getLabel(displayUnit)}
- {displayUnit}
+
+
+
+ {getLabel(displayUnit)}
+
+ {displayUnit}
-
+
setDisplayUnit('msat')}>
Millisatoshis (mSAT)
setDisplayUnit('sat')}>
Satoshis (sat)
- setDisplayUnit('usd')}
disabled={!usdPerSat}
>
@@ -71,4 +77,3 @@ export function CurrencyToggle() {
);
}
-
diff --git a/ui/components/dashboard-balance-summary.tsx b/ui/components/dashboard-balance-summary.tsx
index 2921c891..9f1521df 100644
--- a/ui/components/dashboard-balance-summary.tsx
+++ b/ui/components/dashboard-balance-summary.tsx
@@ -96,4 +96,3 @@ export function DashboardBalanceSummary({
);
}
-
diff --git a/ui/components/error-details-table.tsx b/ui/components/error-details-table.tsx
index 5e51e810..d71b3172 100644
--- a/ui/components/error-details-table.tsx
+++ b/ui/components/error-details-table.tsx
@@ -24,7 +24,7 @@ export function ErrorDetailsTable({ errors }: ErrorDetailsTableProps) {
Recent Errors
-
+
No errors found in the selected time period
diff --git a/ui/components/revenue-by-model-table.tsx b/ui/components/revenue-by-model-table.tsx
index 20a2c8b6..bea8e28b 100644
--- a/ui/components/revenue-by-model-table.tsx
+++ b/ui/components/revenue-by-model-table.tsx
@@ -41,8 +41,11 @@ export function RevenueByModelTable({
Revenue by Model
-
- Total Revenue: {formatAmount(totalRevenue)}
+
+ Total Revenue:{' '}
+
+ {formatAmount(totalRevenue)}
+
@@ -50,48 +53,62 @@ export function RevenueByModelTable({
Model
- Requests
- Successful
- Failed
- Revenue
- Share
- Refunds
- Net Revenue
- Avg/Request
+ Requests
+ Successful
+ Failed
+ Revenue
+ Share
+ Refunds
+ Net Revenue
+ Avg/Request
{models.length === 0 ? (
-
+
No model data available
) : (
models.map((model) => {
- const share = totalRevenue > 0 ? (model.revenue_sats / totalRevenue) * 100 : 0;
+ const share =
+ totalRevenue > 0
+ ? (model.revenue_sats / totalRevenue) * 100
+ : 0;
return (
- {model.model}
- {model.requests}
- {model.successful}
- {model.failed}
-
+ {model.model}
+
+ {model.requests}
+
+
+ {model.successful}
+
+
+ {model.failed}
+
+
{formatAmount(model.revenue_sats)}
-
-
-
{share.toFixed(0)}%
+
+
+
+ {share.toFixed(0)}%
+
-
+
{formatAmount(model.refunds_sats)}
-
+
{formatAmount(model.net_revenue_sats)}
-
+
{formatAmount(model.avg_revenue_per_request)}
diff --git a/ui/components/usage-metrics-chart.tsx b/ui/components/usage-metrics-chart.tsx
index 588371e0..3239f549 100644
--- a/ui/components/usage-metrics-chart.tsx
+++ b/ui/components/usage-metrics-chart.tsx
@@ -90,7 +90,11 @@ export function UsageMetricsChart({
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
}}
itemStyle={{ fontSize: '12px' }}
- labelStyle={{ fontSize: '12px', color: 'hsl(var(--muted-foreground))', marginBottom: '8px' }}
+ labelStyle={{
+ fontSize: '12px',
+ color: 'hsl(var(--muted-foreground))',
+ marginBottom: '8px',
+ }}
/>
{dataKeys.map((dataKey) => (
diff --git a/ui/components/usage-summary-cards.tsx b/ui/components/usage-summary-cards.tsx
index 88224957..08b0b07a 100644
--- a/ui/components/usage-summary-cards.tsx
+++ b/ui/components/usage-summary-cards.tsx
@@ -114,15 +114,19 @@ export function UsageSummaryCards({ summary }: UsageSummaryCardsProps) {
return (
{cards.map((card) => (
-
+
- {card.title}
-
-
+
+ {card.title}
+
+
+
- {card.value}
+
+ {card.value}
+
))}
diff --git a/ui/lib/api/services/admin.ts b/ui/lib/api/services/admin.ts
index 42740521..39269ee6 100644
--- a/ui/lib/api/services/admin.ts
+++ b/ui/lib/api/services/admin.ts
@@ -816,7 +816,9 @@ export class AdminService {
if (search) params.append('search', search);
params.append('limit', limit.toString());
- return await apiClient.get
(`/admin/api/logs?${params.toString()}`);
+ return await apiClient.get(
+ `/admin/api/logs?${params.toString()}`
+ );
}
static async getLogDates(): Promise<{ dates: string[] }> {
@@ -862,9 +864,7 @@ export class AdminService {
);
}
- static async createProviderAccountByType(
- providerType: string
- ): Promise<{
+ static async createProviderAccountByType(providerType: string): Promise<{
ok: boolean;
account_data: Record;
message: string;
@@ -881,7 +881,11 @@ export class AdminService {
static async initiateProviderTopup(
providerId: number,
amount: number
- ): Promise<{ ok: boolean; topup_data: Record; message: string }> {
+ ): Promise<{
+ ok: boolean;
+ topup_data: Record;
+ message: string;
+ }> {
return await apiClient.post<{
ok: boolean;
topup_data: Record;
@@ -901,9 +905,10 @@ export class AdminService {
}>(`/admin/api/upstream-providers/${providerId}/topup/${invoiceId}/status`);
}
- static async getProviderBalance(
- providerId: number
- ): Promise<{ ok: boolean; balance_data: number | null | Record }> {
+ static async getProviderBalance(providerId: number): Promise<{
+ ok: boolean;
+ balance_data: number | null | Record;
+ }> {
return await apiClient.get<{
ok: boolean;
balance_data: number | null | Record;
diff --git a/ui/lib/stores/currency.ts b/ui/lib/stores/currency.ts
index 38d78c05..f83634d9 100644
--- a/ui/lib/stores/currency.ts
+++ b/ui/lib/stores/currency.ts
@@ -18,4 +18,3 @@ export const useCurrencyStore = create()(
}
)
);
-