Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion frontend/src/components/ui/ModelCard.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { getModelLogo } from '../../lib/modelLogos';
import { getModelMetricsUrl } from '../../lib/modelMetrics';
import { getModelMetricsUrl, getModelTier } from '../../lib/modelMetrics';

interface ModelCardProps {
entry: {
Expand All @@ -19,6 +19,7 @@
// Get the logo URL for this model
const logoUrl = getModelLogo(entry.data.title);
const metricsUrl = getModelMetricsUrl(entry.data.title);
const tier = getModelTier(entry.data.title);

let copied = false;

Expand Down Expand Up @@ -82,6 +83,21 @@ class="relative group flex flex-nowrap py-3 px-4 pr-10 rounded-lg border border-
Metrics
</a>
{/if}
{#if tier === "L2"}
<span
class="uptime-badge"
title="This service is running on CSCS L2 Kubernetes"
>
24/7
</span>
{:else if tier === "slurm"}
<span
class="slurm-badge"
title="Model-launch Slurm job"
>
Slurm
</span>
{/if}
{#if entry.data.instanceCount > 1}
<span class="instance-count" title="Number of launched instances for higher throughput">
x{entry.data.instanceCount}
Expand Down Expand Up @@ -133,6 +149,28 @@ class="relative group flex flex-nowrap py-3 px-4 pr-10 rounded-lg border border-
background-color: #15803d;
}

.uptime-badge {
background-color: #2563eb;
color: white;
font-weight: bold;
font-size: 0.75em;
padding: 0 6px;
border-radius: 4px;
flex-shrink: 0;
cursor: help;
}

.slurm-badge {
background-color: #9333ea;
color: white;
font-weight: bold;
font-size: 0.75em;
padding: 0 6px;
border-radius: 4px;
flex-shrink: 0;
cursor: help;
}

@keyframes check-bounce {
0% {
transform: scale(1);
Expand Down
102 changes: 99 additions & 3 deletions frontend/src/components/ui/ModelList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
import { onMount } from "svelte";
import ModelCard from "./ModelCard.svelte";
import { getApiUrl } from "../../lib/config";
import { getModelTier } from "../../lib/modelMetrics";

let models = [];
let modelCount = 0;
let loading = true;
let error = null;

let search = "";
let activeFilter = "all"; // "all" | "24/7" | "slurm"

onMount(async () => {
try {
const response = await fetch(`${getApiUrl()}/v1/models_detailed`);
Expand Down Expand Up @@ -45,6 +50,20 @@
loading = false;
}
});

$: filteredModels = models.filter((m) => {
const title = m.data.title;

if (search.trim()) {
const q = search.trim().toLowerCase();
const haystack = (title + " " + m.data.devices.join(" ")).toLowerCase();
if (!haystack.includes(q)) return false;
}

if (activeFilter === "24/7") return getModelTier(title) === "L2";
if (activeFilter === "slurm") return getModelTier(title) === "slurm";
return true;
});
</script>

<div>
Expand All @@ -59,6 +78,35 @@
Access state-of-the-art language models from leading AI research organizations
</p>
</div>

{#if !loading && !error}
<div class="mb-6 flex flex-col sm:flex-row gap-3 sm:items-center sm:justify-between">
<div class="flex flex-wrap gap-2">
<button
class="pill"
class:active={activeFilter === "all"}
on:click={() => (activeFilter = "all")}
>All</button>
<button
class="pill"
class:active={activeFilter === "24/7"}
on:click={() => (activeFilter = "24/7")}
>24/7</button>
<button
class="pill"
class:active={activeFilter === "slurm"}
on:click={() => (activeFilter = "slurm")}
>Slurm</button>
</div>
<input
type="text"
bind:value={search}
placeholder="Search by model name or GPU..."
class="search-input"
/>
</div>
{/if}

{#if loading}
<div class="loading">Loading...</div>
{:else if error}
Expand All @@ -67,13 +115,61 @@
</div>
{:else}
<div class="model-list space-y-2">
{#each models as model}
{#each filteredModels as model (model.data.title)}
<ModelCard entry={model} />
{/each}
{#if filteredModels.length === 0}
<div class="text-center text-slate-500 dark:text-slate-400 py-6">
No models match your filters.
</div>
{/if}
</div>
{/if}
</div>

<style>
/* Optional styling */
</style>
.search-input {
width: 100%;
max-width: 420px;
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.15);
background-color: transparent;
color: inherit;
font-size: 0.875rem;
outline: none;
transition: border-color 0.15s ease;
}
:global(.dark) .search-input {
border-color: rgba(255, 255, 255, 0.2);
}
.search-input:focus {
border-color: #6366f1;
}

.pill {
padding: 0.3rem 0.75rem;
border-radius: 9999px;
font-size: 0.8rem;
font-weight: 600;
border: 1px solid rgba(0, 0, 0, 0.15);
background-color: transparent;
color: inherit;
cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
:global(.dark) .pill {
border-color: rgba(255, 255, 255, 0.2);
}
.pill:hover {
background-color: rgba(0, 0, 0, 0.05);
}
:global(.dark) .pill:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.pill.active {
background-color: #6366f1;
border-color: #6366f1;
color: white;
}
</style>
42 changes: 21 additions & 21 deletions frontend/src/lib/modelMetrics.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
const SGLANG_BASE = "https://metrics.swissai.svc.cscs.ch/d/sglang-monitoring/sglang-monitoring?orgId=1&from=now-15m&to=now&timezone=browser&refresh=5s&var-model=";
const VLLM_BASE = "https://metrics.swissai.svc.cscs.ch/d/vllm-master-v2/vllm-monitoring-v2?orgId=1&from=now-15m&to=now&timezone=browser&refresh=5s&var-model_name=";
const METRICS_BASE = "https://metrics.swissai.svc.cscs.ch/d/inference-unified/inference-monitoring-vllm-2b-sglang?orgId=1&from=now-15m&to=now&timezone=browser&var-datasource=PBFA97CFB590B2093&refresh=30s&var-model_name=";

type Engine = "sglang" | "vllm";
export type HostingTier = "L2" | "slurm";

const modelEngines: Record<string, Engine> = {
"swiss-ai/Apertus-8B-Instruct-2509": "sglang",
"zai-org/GLM-4.7-Flash": "sglang",
"Snowflake/snowflake-arctic-embed-l-v2.0": "vllm",
"cais/HarmBench-Llama-2-13b-cls": "vllm",
"meta-llama/Llama-3.3-70B-Instruct": "sglang",
"meta-llama/Llama-Guard-4-12B": "vllm",
"swiss-ai/Apertus-70B-Instruct-2509": "vllm",
"Qwen/Qwen3.5-27B": "vllm",
type ModelConfig = {
metrics?: boolean;
tier?: HostingTier;
};

const models: Record<string, ModelConfig> = {
"swiss-ai/Apertus-8B-Instruct-2509": { tier: "L2" },
"zai-org/GLM-4.7-Flash": { tier: "L2" },
"Snowflake/snowflake-arctic-embed-l-v2.0": { tier: "L2" },
"cais/HarmBench-Llama-2-13b-cls": { tier: "L2" },
"meta-llama/Llama-3.3-70B-Instruct": { tier: "L2" },
"meta-llama/Llama-Guard-4-12B": { tier: "L2" },
"swiss-ai/Apertus-70B-Instruct-2509": { tier: "L2" },
"Qwen/Qwen3.5-27B": { tier: "L2" },
};

/**
* Get the metrics dashboard URL for a model, or null if none exists
*/
export function getModelMetricsUrl(modelName: string): string | null {
const engine = modelEngines[modelName];
if (!engine) return null;
if (models[modelName]?.metrics === false) return null;
return `${METRICS_BASE}${encodeURIComponent(modelName)}`;
}

const encoded = encodeURIComponent(modelName);
return engine === "sglang"
? `${SGLANG_BASE}${encoded}`
: `${VLLM_BASE}${encoded}`;
export function getModelTier(modelName: string): HostingTier {
return models[modelName]?.tier ?? "slurm";
}
Loading