From 3180ebcabd5edd2fccdd841ab61c2ceae3780532 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sat, 30 Aug 2025 17:57:27 +0000 Subject: [PATCH 1/6] feat(models): add stability flags for DeepSeek models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add StabilityLevel type with stable, beta, unstable, experimental options - Add stability field to ModelDefinition interface - Mark DeepSeek V3, R1, and R1-0528 as unstable - Mark DeepSeek R1 Distill as beta - Update UI ModelSelector to show stability warnings and badges - Add stability information to API responses - Add comprehensive tests for stability flag handling - Enable previously commented nebius provider for DeepSeek R1-0528 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- apps/gateway/src/models/models.spec.ts | 70 +++++++++++++++++++ apps/gateway/src/models/models.ts | 2 + .../components/playground/model-selector.tsx | 59 +++++++++++++++- packages/models/src/models.ts | 10 +++ packages/models/src/models/deepseek.ts | 29 ++++---- 5 files changed, 155 insertions(+), 15 deletions(-) diff --git a/apps/gateway/src/models/models.spec.ts b/apps/gateway/src/models/models.spec.ts index cc8235c889..84f97bf7a2 100644 --- a/apps/gateway/src/models/models.spec.ts +++ b/apps/gateway/src/models/models.spec.ts @@ -126,4 +126,74 @@ describe("Models API", () => { "image", ]); }); + + test("GET /v1/models should include stability information for models", async () => { + const res = await app.request("/v1/models"); + expect(res.status).toBe(200); + + const json = await res.json(); + + // Check that the stability field exists in the model schema (models may or may not have it set) + const modelsWithStability = json.data.filter( + (model: any) => model.stability !== undefined, + ); + // At least one model should have stability information (our DeepSeek models) + expect(modelsWithStability.length).toBeGreaterThan(0); + + // Find DeepSeek models to test specific stability flags + const deepSeekV3 = json.data.find( + (model: any) => model.id === "deepseek-v3", + ); + const deepSeekR1 = json.data.find( + (model: any) => model.id === "deepseek-r1", + ); + const deepSeekR1Distill = json.data.find( + (model: any) => model.id === "deepseek-r1-distill-llama-70b", + ); + const deepSeekV31 = json.data.find( + (model: any) => model.id === "deepseek-v3.1", + ); + + if (deepSeekV3) { + expect(deepSeekV3.stability).toBe("unstable"); + } + + if (deepSeekR1) { + expect(deepSeekR1.stability).toBe("unstable"); + } + + if (deepSeekR1Distill) { + expect(deepSeekR1Distill.stability).toBe("beta"); + } + + // DeepSeek v3.1 should default to stable (undefined in response means stable) + if (deepSeekV31) { + expect( + deepSeekV31.stability === undefined || + deepSeekV31.stability === "stable", + ).toBe(true); + } + }); + + test("GET /v1/models should handle stability field values correctly", async () => { + const res = await app.request("/v1/models"); + expect(res.status).toBe(200); + + const json = await res.json(); + + // Validate that stability field contains only valid values + const validStabilityValues = [ + "stable", + "beta", + "unstable", + "experimental", + undefined, + ]; + + for (const model of json.data) { + if (model.stability !== undefined) { + expect(validStabilityValues).toContain(model.stability); + } + } + }); }); diff --git a/apps/gateway/src/models/models.ts b/apps/gateway/src/models/models.ts index a940d178c0..300fa3e81d 100644 --- a/apps/gateway/src/models/models.ts +++ b/apps/gateway/src/models/models.ts @@ -62,6 +62,7 @@ const modelSchema = z.object({ free: z.boolean().optional(), deprecated_at: z.string().optional(), deactivated_at: z.string().optional(), + stability: z.enum(["stable", "beta", "unstable", "experimental"]).optional(), }); const listModelsResponseSchema = z.object({ @@ -220,6 +221,7 @@ modelsApi.openapi(listModels, async (c) => { free: model.free || false, deprecated_at: model.deprecatedAt?.toISOString(), deactivated_at: model.deactivatedAt?.toISOString(), + stability: model.stability, }; }); diff --git a/apps/ui/src/components/playground/model-selector.tsx b/apps/ui/src/components/playground/model-selector.tsx index 1fc1e51ee2..e6410eb9c8 100644 --- a/apps/ui/src/components/playground/model-selector.tsx +++ b/apps/ui/src/components/playground/model-selector.tsx @@ -1,5 +1,10 @@ -import { models, providers, type ModelDefinition } from "@llmgateway/models"; -import { Check, ChevronDown } from "lucide-react"; +import { + models, + providers, + type ModelDefinition, + type StabilityLevel, +} from "@llmgateway/models"; +import { Check, ChevronDown, AlertTriangle } from "lucide-react"; import { Badge } from "@/lib/components/badge"; import { Button } from "@/lib/components/button"; @@ -19,6 +24,7 @@ interface LocalModel { id: string; name?: string; jsonOutput: boolean; + stability?: StabilityLevel; providers: Array<{ providerId: string; modelName: string; @@ -47,6 +53,35 @@ export function ModelSelector({ return providers.find((p) => p.id === providerId); }; + const getStabilityBadgeProps = (stability?: StabilityLevel) => { + switch (stability) { + case "beta": + return { + variant: "secondary" as const, + color: "text-blue-600", + label: "BETA", + }; + case "unstable": + return { + variant: "destructive" as const, + color: "text-red-600", + label: "UNSTABLE", + }; + case "experimental": + return { + variant: "destructive" as const, + color: "text-orange-600", + label: "EXPERIMENTAL", + }; + default: + return null; + } + }; + + const shouldShowStabilityWarning = (stability?: StabilityLevel) => { + return stability && ["unstable", "experimental"].includes(stability); + }; + // Group by model instead of provider to avoid duplicates const uniqueModels: LocalModel[] = models.map((model) => { const modelProviders = model.providers @@ -64,6 +99,7 @@ export function ModelSelector({ id: typedModel.id, name: typedModel.name, jsonOutput: typedModel.jsonOutput ?? false, + stability: typedModel.stability, providers: modelProviders, }; }); @@ -90,6 +126,9 @@ export function ModelSelector({ {currentModelInfo?.name || currentModelInfo?.id || selectedModel} + {shouldShowStabilityWarning(currentModelInfo?.stability) && ( + + )} @@ -113,6 +152,9 @@ export function ModelSelector({ ))} {model.name || model.id} + {shouldShowStabilityWarning(model.stability) && ( + + )} {model.id === selectedModel && ( )} @@ -144,6 +186,19 @@ export function ModelSelector({ Stream )} + {(() => { + const stabilityProps = getStabilityBadgeProps( + model.stability, + ); + return stabilityProps ? ( + + {stabilityProps.label} + + ) : null; + })()} diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index 420c7d7c6e..6592627cf1 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -86,6 +86,8 @@ export interface ProviderModelMapping { test?: "skip" | "only"; } +export type StabilityLevel = "stable" | "beta" | "unstable" | "experimental"; + export interface ModelDefinition { /** * Unique identifier for the model @@ -127,6 +129,14 @@ export interface ModelDefinition { * Output formats supported by the model (defaults to ['text'] if not specified) */ output?: ("text" | "image")[]; + /** + * Stability level of the model (defaults to 'stable' if not specified) + * - stable: Fully tested and production ready + * - beta: Generally stable but may have minor issues + * - unstable: May have significant issues or frequent changes + * - experimental: Early stage, use with caution + */ + stability?: StabilityLevel; } export const models = [ diff --git a/packages/models/src/models/deepseek.ts b/packages/models/src/models/deepseek.ts index 41934262eb..2a7b86ed74 100644 --- a/packages/models/src/models/deepseek.ts +++ b/packages/models/src/models/deepseek.ts @@ -7,6 +7,7 @@ export const deepseekModels = [ family: "deepseek", deprecatedAt: undefined, deactivatedAt: undefined, + stability: "unstable" as const, providers: [ { providerId: "cloudrift", @@ -41,6 +42,7 @@ export const deepseekModels = [ family: "deepseek", deprecatedAt: undefined, deactivatedAt: undefined, + stability: "unstable" as const, providers: [ { providerId: "cloudrift", @@ -87,6 +89,7 @@ export const deepseekModels = [ family: "deepseek", deprecatedAt: undefined, deactivatedAt: undefined, + stability: "unstable" as const, providers: [ { providerId: "cloudrift", @@ -112,19 +115,18 @@ export const deepseekModels = [ vision: false, tools: false, }, - // TODO mark as unstable - // { - // providerId: "nebius", - // modelName: "deepseek-ai/DeepSeek-R1-0528", - // inputPrice: 0.8 / 1e6, - // outputPrice: 2.4 / 1e6, - // requestPrice: 0, - // contextSize: 64000, - // maxOutput: undefined, - // streaming: true, - // vision: false, - // tools: false, - // }, + { + providerId: "nebius", + modelName: "deepseek-ai/DeepSeek-R1-0528", + inputPrice: 0.8 / 1e6, + outputPrice: 2.4 / 1e6, + requestPrice: 0, + contextSize: 64000, + maxOutput: undefined, + streaming: true, + vision: false, + tools: false, + }, ], jsonOutput: false, }, @@ -134,6 +136,7 @@ export const deepseekModels = [ family: "deepseek", deprecatedAt: undefined, deactivatedAt: undefined, + stability: "beta" as const, providers: [ { providerId: "groq", From 2672bb0f215a280a308a060e8457b080d048d6a5 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sat, 30 Aug 2025 23:46:00 +0000 Subject: [PATCH 2/6] feat(ui): add stability indicators to models list and detail pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add stability badges to models table view (new Stability column) - Add stability badges to models grid view - Add stability warnings (triangle icons) for unstable/experimental models - Add stability information to individual model detail pages - Display warning banners on unstable model detail pages - Update ModelSelector, AllModels, ModelsList, and model detail page components - Consistent color coding: blue for beta, red for unstable, orange for experimental 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- apps/ui/src/app/models/[name]/page.tsx | 85 ++++++++++++++++++- apps/ui/src/components/models/all-models.tsx | 84 +++++++++++++++++- apps/ui/src/components/models/models-list.tsx | 57 ++++++++++++- 3 files changed, 219 insertions(+), 7 deletions(-) diff --git a/apps/ui/src/app/models/[name]/page.tsx b/apps/ui/src/app/models/[name]/page.tsx index 78bb64128a..4a0137cfd0 100644 --- a/apps/ui/src/app/models/[name]/page.tsx +++ b/apps/ui/src/app/models/[name]/page.tsx @@ -1,13 +1,17 @@ import { models as modelDefinitions, providers as providerDefinitions, + type StabilityLevel, + type ModelDefinition, } from "@llmgateway/models"; +import { AlertTriangle } from "lucide-react"; import { notFound } from "next/navigation"; import Footer from "@/components/landing/footer"; import { Navbar } from "@/components/landing/navbar"; import { CopyModelName } from "@/components/models/copy-model-name"; import { ProviderCard } from "@/components/models/provider-card"; +import { Badge } from "@/lib/components/badge"; interface PageProps { params: Promise<{ name: string }>; @@ -17,12 +21,43 @@ export default async function ModelPage({ params }: PageProps) { const { name } = await params; const decodedName = decodeURIComponent(name); - const modelDef = modelDefinitions.find((m) => m.id === decodedName); + const modelDef = modelDefinitions.find( + (m) => m.id === decodedName, + ) as ModelDefinition; if (!modelDef) { notFound(); } + const getStabilityBadgeProps = (stability?: StabilityLevel) => { + switch (stability) { + case "beta": + return { + variant: "secondary" as const, + color: "text-blue-600", + label: "BETA", + }; + case "unstable": + return { + variant: "destructive" as const, + color: "text-red-600", + label: "UNSTABLE", + }; + case "experimental": + return { + variant: "destructive" as const, + color: "text-orange-600", + label: "EXPERIMENTAL", + }; + default: + return null; + } + }; + + const shouldShowStabilityWarning = (stability?: StabilityLevel) => { + return stability && ["unstable", "experimental"].includes(stability); + }; + const modelProviders = modelDef.providers.map((provider) => { const providerInfo = providerDefinitions.find( (p) => p.id === provider.providerId, @@ -39,11 +74,33 @@ export default async function ModelPage({ params }: PageProps) {
-

- {modelDef.id} -

+
+

+ {modelDef.id} +

+ {shouldShowStabilityWarning(modelDef.stability) && ( + + )} +
+ {(() => { + const stabilityProps = getStabilityBadgeProps( + modelDef.stability, + ); + return stabilityProps ? ( + + {stabilityProps.label} + + ) : ( + + STABLE + + ); + })()}
@@ -86,6 +143,26 @@ export default async function ModelPage({ params }: PageProps) { different configurations, pricing, and performance characteristics. Choose the provider that best fits your needs.

+ {shouldShowStabilityWarning(modelDef.stability) && ( +
+
+ +
+

+ {modelDef.stability === "experimental" + ? "Experimental" + : "Unstable"}{" "} + Model Warning +

+

+ This model is marked as {modelDef.stability} and may have + issues with reliability, performance, or consistency. Use + with caution in production environments. +

+
+
+
+ )}
diff --git a/apps/ui/src/components/models/all-models.tsx b/apps/ui/src/components/models/all-models.tsx index 90421d9326..2f773e38c9 100644 --- a/apps/ui/src/components/models/all-models.tsx +++ b/apps/ui/src/components/models/all-models.tsx @@ -19,6 +19,7 @@ import { ArrowDown, Play, ImagePlus, + AlertTriangle, } from "lucide-react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; @@ -60,7 +61,11 @@ import { } from "@/lib/components/tooltip"; import { cn, formatContextSize } from "@/lib/utils"; -import type { ModelDefinition, ProviderModelMapping } from "@llmgateway/models"; +import type { + ModelDefinition, + ProviderModelMapping, + StabilityLevel, +} from "@llmgateway/models"; interface ModelWithProviders extends ModelDefinition { providerDetails: Array<{ @@ -368,6 +373,35 @@ export function AllModels({ children }: { children: React.ReactNode }) { ); }; + const getStabilityBadgeProps = (stability?: StabilityLevel) => { + switch (stability) { + case "beta": + return { + variant: "secondary" as const, + color: "text-blue-600", + label: "BETA", + }; + case "unstable": + return { + variant: "destructive" as const, + color: "text-red-600", + label: "UNSTABLE", + }; + case "experimental": + return { + variant: "destructive" as const, + color: "text-orange-600", + label: "EXPERIMENTAL", + }; + default: + return null; + } + }; + + const shouldShowStabilityWarning = (stability?: StabilityLevel) => { + return stability && ["unstable", "experimental"].includes(stability); + }; + const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); @@ -752,6 +786,7 @@ export function AllModels({ children }: { children: React.ReactNode }) { Capabilities + Stability Actions @@ -762,6 +797,9 @@ export function AllModels({ children }: { children: React.ReactNode }) {
{model.name || model.id} + {shouldShowStabilityWarning(model.stability) && ( + + )} {model.free && ( + + {(() => { + const stabilityProps = getStabilityBadgeProps( + model.stability, + ); + return stabilityProps ? ( + + {stabilityProps.label} + + ) : ( + + STABLE + + ); + })()} + + {model.name || model.id} + {shouldShowStabilityWarning(model.stability) && ( + + )} {model.free && ( +
+
Stability:
+ {(() => { + const stabilityProps = getStabilityBadgeProps( + model.stability, + ); + return stabilityProps ? ( + + {stabilityProps.label} + + ) : ( + + STABLE + + ); + })()} +
+
{ + switch (stability) { + case "beta": + return { + variant: "secondary" as const, + color: "text-blue-600", + label: "BETA", + }; + case "unstable": + return { + variant: "destructive" as const, + color: "text-red-600", + label: "UNSTABLE", + }; + case "experimental": + return { + variant: "destructive" as const, + color: "text-orange-600", + label: "EXPERIMENTAL", + }; + default: + return null; + } + }; + + const shouldShowStabilityWarning = (stability?: StabilityLevel) => { + return stability && ["unstable", "experimental"].includes(stability); + }; + return (
{(models as readonly ModelDefinition[]).map((model) => ( -
{model.name || model.id}
+
+ {model.name || model.id} + {shouldShowStabilityWarning(model.stability) && ( + + )} +
Providers:
{model.providers.map((provider) => ( {provider.providerId} ))}
+
+ Stability: + {(() => { + const stabilityProps = getStabilityBadgeProps(model.stability); + return stabilityProps ? ( + + {stabilityProps.label} + + ) : ( + + STABLE + + ); + })()} +
{model.providers.map((provider) => (
From 7fc7e16e0e77ea2262a61db41d23dd4335fd780b Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sun, 31 Aug 2025 00:11:08 +0000 Subject: [PATCH 3/6] feat(models,ui): add stability levels to models and display in UI - Introduce optional stability enum (stable, beta, unstable, experimental) to model and provider schemas - Update deepseek models with stability levels - Enhance UI components (AllModels, ModelSelector) to show stability badges and warnings based on most unstable stability level - Add utility function to determine most unstable stability level from model and its providers - Improve model API to include stability information This feature enables better visibility of model stability status across the system, helping users make informed decisions. Co-authored-by: terragon-labs[bot] --- apps/gateway/src/models/models.ts | 4 ++ apps/ui/src/components/models/all-models.tsx | 36 ++++++++++++--- .../components/playground/model-selector.tsx | 44 ++++++++++++++++--- packages/models/src/models.ts | 8 ++++ packages/models/src/models/deepseek.ts | 10 +++++ 5 files changed, 90 insertions(+), 12 deletions(-) diff --git a/apps/gateway/src/models/models.ts b/apps/gateway/src/models/models.ts index 300fa3e81d..93827ad0f8 100644 --- a/apps/gateway/src/models/models.ts +++ b/apps/gateway/src/models/models.ts @@ -43,6 +43,9 @@ const modelSchema = z.object({ tools: z.boolean(), parallelToolCalls: z.boolean(), reasoning: z.boolean(), + stability: z + .enum(["stable", "beta", "unstable", "experimental"]) + .optional(), }), ), pricing: z.object({ @@ -197,6 +200,7 @@ modelsApi.openapi(listModels, async (c) => { tools: provider.tools || false, parallelToolCalls: provider.parallelToolCalls || false, reasoning: provider.reasoning || false, + stability: provider.stability || model.stability, }; }), pricing: { diff --git a/apps/ui/src/components/models/all-models.tsx b/apps/ui/src/components/models/all-models.tsx index 2f773e38c9..141ac58af9 100644 --- a/apps/ui/src/components/models/all-models.tsx +++ b/apps/ui/src/components/models/all-models.tsx @@ -402,6 +402,30 @@ export function AllModels({ children }: { children: React.ReactNode }) { return stability && ["unstable", "experimental"].includes(stability); }; + const getMostUnstableStability = (model: any): StabilityLevel | undefined => { + const stabilityLevels: StabilityLevel[] = [ + "experimental", + "unstable", + "beta", + "stable", + ]; + + // Get all stability levels (model-level and provider-level) + const allStabilities = [ + model.stability, + ...model.providers.map((p: any) => p.stability || model.stability), + ].filter(Boolean) as StabilityLevel[]; + + // Return the most unstable level + for (const level of stabilityLevels) { + if (allStabilities.includes(level)) { + return level; + } + } + + return undefined; + }; + const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); @@ -797,9 +821,9 @@ export function AllModels({ children }: { children: React.ReactNode }) {
{model.name || model.id} - {shouldShowStabilityWarning(model.stability) && ( - - )} + {shouldShowStabilityWarning( + getMostUnstableStability(model), + ) && } {model.free && ( {(() => { + const mostUnstableStability = getMostUnstableStability(model); const stabilityProps = getStabilityBadgeProps( - model.stability, + mostUnstableStability, ); return stabilityProps ? (
Stability:
{(() => { + const mostUnstableStability = getMostUnstableStability(model); const stabilityProps = getStabilityBadgeProps( - model.stability, + mostUnstableStability, ); return stabilityProps ? ( { + const stabilityLevels: StabilityLevel[] = [ + "experimental", + "unstable", + "beta", + "stable", + ]; + + // Get all stability levels (model-level and provider-level) + const allStabilities = [ + model.stability, + ...model.providers.map((p) => p.stability || model.stability), + ].filter(Boolean) as StabilityLevel[]; + + // Return the most unstable level + for (const level of stabilityLevels) { + if (allStabilities.includes(level)) { + return level; + } + } + + return undefined; + }; + // Group by model instead of provider to avoid duplicates const uniqueModels: LocalModel[] = models.map((model) => { const modelProviders = model.providers @@ -126,9 +153,10 @@ export function ModelSelector({ {currentModelInfo?.name || currentModelInfo?.id || selectedModel} - {shouldShowStabilityWarning(currentModelInfo?.stability) && ( - - )} + {currentModelInfo && + shouldShowStabilityWarning( + getMostUnstableStability(currentModelInfo), + ) && }
@@ -152,9 +180,9 @@ export function ModelSelector({ ))}
{model.name || model.id} - {shouldShowStabilityWarning(model.stability) && ( - - )} + {shouldShowStabilityWarning( + getMostUnstableStability(model), + ) && } {model.id === selectedModel && ( )} @@ -187,8 +215,10 @@ export function ModelSelector({ )} {(() => { + const mostUnstableStability = + getMostUnstableStability(model); const stabilityProps = getStabilityBadgeProps( - model.stability, + mostUnstableStability, ); return stabilityProps ? ( Date: Sun, 31 Aug 2025 01:02:38 +0000 Subject: [PATCH 4/6] feat(ui): add stability badges and warnings to provider cards and lists - Display stability badges for providers in the models list with appropriate styling. - Show stability badges and warning icons (for unstable and experimental) in provider cards. - Pass model stability info down to provider cards for consistent display. - Enhance UI to better communicate provider stability levels to users. Co-authored-by: terragon-labs[bot] --- apps/ui/src/app/models/[name]/page.tsx | 1 + apps/ui/src/components/models/models-list.tsx | 24 ++++++- .../src/components/models/provider-card.tsx | 64 +++++++++++++++++-- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/apps/ui/src/app/models/[name]/page.tsx b/apps/ui/src/app/models/[name]/page.tsx index 4a0137cfd0..892b0659d1 100644 --- a/apps/ui/src/app/models/[name]/page.tsx +++ b/apps/ui/src/app/models/[name]/page.tsx @@ -185,6 +185,7 @@ export default async function ModelPage({ params }: PageProps) { key={provider.providerId} provider={provider} modelName={decodedName} + modelStability={modelDef.stability} /> ))}
diff --git a/apps/ui/src/components/models/models-list.tsx b/apps/ui/src/components/models/models-list.tsx index d611202025..578d706edf 100644 --- a/apps/ui/src/components/models/models-list.tsx +++ b/apps/ui/src/components/models/models-list.tsx @@ -51,9 +51,27 @@ export function ModelsList() {
Providers:
- {model.providers.map((provider) => ( - {provider.providerId} - ))} + {model.providers.map((provider) => { + const providerStability = provider.stability || model.stability; + const stabilityProps = getStabilityBadgeProps(providerStability); + + return ( +
+ {provider.providerId} + {stabilityProps && ( + + {stabilityProps.label} + + )} +
+ ); + })}
Stability: diff --git a/apps/ui/src/components/models/provider-card.tsx b/apps/ui/src/components/models/provider-card.tsx index 57f49e92c5..ab1b721cb7 100644 --- a/apps/ui/src/components/models/provider-card.tsx +++ b/apps/ui/src/components/models/provider-card.tsx @@ -1,9 +1,10 @@ "use client"; -import { Copy, Check } from "lucide-react"; +import { Copy, Check, AlertTriangle } from "lucide-react"; import { useState } from "react"; import { getProviderIcon } from "@/components/ui/providers-icons"; +import { Badge } from "@/lib/components/badge"; import { Button } from "@/lib/components/button"; import { Card, CardContent } from "@/lib/components/card"; import { formatContextSize } from "@/lib/utils"; @@ -11,6 +12,7 @@ import { formatContextSize } from "@/lib/utils"; import type { ProviderModelMapping, ProviderDefinition, + StabilityLevel, } from "@llmgateway/models"; interface ProviderWithInfo extends ProviderModelMapping { @@ -20,12 +22,47 @@ interface ProviderWithInfo extends ProviderModelMapping { interface ProviderCardProps { provider: ProviderWithInfo; modelName: string; + modelStability?: StabilityLevel; } -export function ProviderCard({ provider, modelName }: ProviderCardProps) { +export function ProviderCard({ + provider, + modelName, + modelStability, +}: ProviderCardProps) { const [copied, setCopied] = useState(false); const providerModelName = `${provider.providerId}/${modelName}`; const ProviderIcon = getProviderIcon(provider.providerId); + const providerStability = provider.stability || modelStability; + + const getStabilityBadgeProps = (stability?: StabilityLevel) => { + switch (stability) { + case "beta": + return { + variant: "secondary" as const, + color: "text-blue-600", + label: "BETA", + }; + case "unstable": + return { + variant: "destructive" as const, + color: "text-red-600", + label: "UNSTABLE", + }; + case "experimental": + return { + variant: "destructive" as const, + color: "text-orange-600", + label: "EXPERIMENTAL", + }; + default: + return null; + } + }; + + const shouldShowStabilityWarning = (stability?: StabilityLevel) => { + return stability && ["unstable", "experimental"].includes(stability); + }; const copyToClipboard = async () => { try { @@ -50,9 +87,26 @@ export function ProviderCard({ provider, modelName }: ProviderCardProps) { )}
-

- {provider.providerInfo?.name || provider.providerId} -

+
+

+ {provider.providerInfo?.name || provider.providerId} +

+ {shouldShowStabilityWarning(providerStability) && ( + + )} + {(() => { + const stabilityProps = + getStabilityBadgeProps(providerStability); + return stabilityProps ? ( + + {stabilityProps.label} + + ) : null; + })()} +
{providerModelName} From 9ef430d79689521fdb7e2ac6079ec6c6981f5823 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sun, 31 Aug 2025 18:26:30 +0100 Subject: [PATCH 5/6] refactor(models): remove stability fields from deepseek definitions Simplifies deepseek models by removing unnecessary stability fields. --- packages/models/src/models/deepseek.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/models/src/models/deepseek.ts b/packages/models/src/models/deepseek.ts index ea2fe53037..a68b8194bb 100644 --- a/packages/models/src/models/deepseek.ts +++ b/packages/models/src/models/deepseek.ts @@ -7,7 +7,6 @@ export const deepseekModels = [ family: "deepseek", deprecatedAt: undefined, deactivatedAt: undefined, - stability: "unstable" as const, providers: [ { providerId: "cloudrift", @@ -20,7 +19,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "unstable" as const, }, { providerId: "nebius", @@ -33,7 +31,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "beta" as const, }, ], jsonOutput: false, @@ -44,7 +41,6 @@ export const deepseekModels = [ family: "deepseek", deprecatedAt: undefined, deactivatedAt: undefined, - stability: "unstable" as const, providers: [ { providerId: "cloudrift", @@ -57,7 +53,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "unstable" as const, }, { providerId: "deepseek", @@ -70,7 +65,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "stable" as const, }, { providerId: "nebius", @@ -83,7 +77,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "beta" as const, }, ], jsonOutput: false, @@ -94,7 +87,6 @@ export const deepseekModels = [ family: "deepseek", deprecatedAt: undefined, deactivatedAt: undefined, - stability: "unstable" as const, providers: [ { providerId: "cloudrift", @@ -107,7 +99,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "unstable" as const, }, { providerId: "deepseek", @@ -120,7 +111,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "stable" as const, }, { providerId: "nebius", @@ -133,7 +123,7 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "beta" as const, + stability: "unstable" as const, }, ], jsonOutput: false, @@ -157,7 +147,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: true, - stability: "stable" as const, }, { providerId: "nebius", @@ -170,7 +159,6 @@ export const deepseekModels = [ streaming: true, vision: false, tools: false, - stability: "beta" as const, }, ], jsonOutput: true, From 64639947489d72fc9b8b8257d67c0414506f5d6b Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sun, 31 Aug 2025 18:59:09 +0100 Subject: [PATCH 6/6] refactor(models): remove redundant stability checks in tests --- apps/gateway/src/models/models.spec.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/apps/gateway/src/models/models.spec.ts b/apps/gateway/src/models/models.spec.ts index 84f97bf7a2..d6cb4ba1b8 100644 --- a/apps/gateway/src/models/models.spec.ts +++ b/apps/gateway/src/models/models.spec.ts @@ -141,12 +141,6 @@ describe("Models API", () => { expect(modelsWithStability.length).toBeGreaterThan(0); // Find DeepSeek models to test specific stability flags - const deepSeekV3 = json.data.find( - (model: any) => model.id === "deepseek-v3", - ); - const deepSeekR1 = json.data.find( - (model: any) => model.id === "deepseek-r1", - ); const deepSeekR1Distill = json.data.find( (model: any) => model.id === "deepseek-r1-distill-llama-70b", ); @@ -154,14 +148,6 @@ describe("Models API", () => { (model: any) => model.id === "deepseek-v3.1", ); - if (deepSeekV3) { - expect(deepSeekV3.stability).toBe("unstable"); - } - - if (deepSeekR1) { - expect(deepSeekR1.stability).toBe("unstable"); - } - if (deepSeekR1Distill) { expect(deepSeekR1Distill.stability).toBe("beta"); }