import { useEffect, useState } from "react" import { Cable, Check, ListChecks, ListPlus, Loader2, Plus, RefreshCw, Repeat, Server, Star, Trash2, } from "lucide-react" import { statusClass } from "@/App" import type { AdminConfig, LocalProviderStatus, OpenAICompatibleInstance, } from "@/lib/types" import { statusDotClass } from "@/lib/status" import { cn } from "@/lib/utils" import { Badge } from "./ui/badge" import { Button } from "./ui/button" import { Card, CardContent } from "./ui/card" import { Checkbox } from "./ui/checkbox" import { Input } from "./ui/input" import { Label } from "./ui/label" const INSTANCES_FIELD_KEY = "OPENAI_COMPATIBLE_INSTANCES" interface OpenAICompatibleViewProps { config: AdminConfig values: Record onValuesChange: (values: Record) => void localStatus: Record onTestProvider: (providerId: string, done?: () => void) => void onFetchModels: (providerId: string) => Promise onAddModels: (providerId: string, models: string[]) => void onMessage: (text: string, kind?: string) => void } interface InstanceStatus { status: string label: string baseUrl: string } function parseInstances(raw: string | undefined): OpenAICompatibleInstance[] { if (!raw) return [] try { const parsed: unknown = JSON.parse(raw) if (!Array.isArray(parsed)) return [] return parsed.filter( (entry): entry is OpenAICompatibleInstance => entry !== null && typeof entry === "object", ) } catch { return [] } } function providerIdFor(index: number): string { return `openai_compatible_${index + 1}` } function statusFor( providerId: string, config: AdminConfig, localStatus: Record, ): InstanceStatus { const tested = localStatus[providerId] if (tested) { return { status: tested.status, label: tested.label, baseUrl: tested.base_url ?? "", } } const descriptor = config.provider_status.find( (candidate) => candidate.provider_id === providerId, ) return { status: descriptor?.status ?? "unknown", label: descriptor?.label ?? "Not configured", baseUrl: descriptor?.base_url ?? "", } } interface InstanceCardProps { index: number instance: OpenAICompatibleInstance status: InstanceStatus locked: boolean testing: boolean fetching: boolean models: string[] selected: string[] currentDefault: string onPatch: (patch: Partial) => void onRemove: () => void onTest: () => void onFetch: () => void onClearModels: () => void onAddModels: (models: string[]) => void onToggleModel: (model: string) => void onUseModel: (model: string) => void } function InstanceCard({ index, instance, status, locked, testing, fetching, models, selected, currentDefault, onPatch, onRemove, onTest, onFetch, onClearModels, onAddModels, onToggleModel, onUseModel, }: InstanceCardProps) { const providerId = providerIdFor(index) const canProbe = !locked && Boolean(instance.base_url.trim()) const [modelDraft, setModelDraft] = useState("") const addFromDraft = () => { const ids = modelDraft .split(",") .map((id) => id.trim()) .filter(Boolean) if (ids.length === 0) return onAddModels(ids) setModelDraft("") } return (
{index + 1}

Endpoint {index + 1}

{providerId}/<model>

{status.label}
onPatch({ base_url: event.target.value })} />
onPatch({ api_keys: event.target.value })} />

Comma-separated keys rotate round-robin with failover. Leave empty for keyless local servers.

onPatch({ proxy: event.target.value })} />

{models.length === 0 ? "No model ids yet" : `${models.length} model id${models.length === 1 ? "" : "s"}`}

{selected.length > 0 ? ( {selected.length} selected ) : null}
{models.length > 0 ? ( ) : null}
setModelDraft(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault() addFromDraft() } }} />
{models.length > 0 ? (
{models.map((model) => { // The prefix is optional: a bare id also matches the default. const isDefault = model === currentDefault || `${providerId}/${model}` === currentDefault return (
onToggleModel(model)} /> {isDefault ? ( ) : null} {model} {isDefault ? ( default ) : null}
) })}
) : (

Add model ids manually, or use Fetch models below to pull them from GET {`{base}`}/models.

)}

{status.baseUrl ? `Resolves to ${status.baseUrl}` : "Not checked yet"}

) } export function OpenAICompatibleView({ config, values, onValuesChange, localStatus, onTestProvider, onFetchModels, onAddModels, onMessage, }: OpenAICompatibleViewProps) { const field = config.fields.find( (candidate) => candidate.key === INSTANCES_FIELD_KEY, ) const raw = values[INSTANCES_FIELD_KEY] ?? field?.value ?? "[]" const [instances, setInstances] = useState(() => parseInstances(raw), ) const [testingId, setTestingId] = useState(null) const [fetchingId, setFetchingId] = useState(null) const [modelsByProvider, setModelsByProvider] = useState>( {}, ) useEffect(() => { setInstances(parseInstances(raw)) }, [raw]) const commit = (next: OpenAICompatibleInstance[]) => { setInstances(next) onValuesChange({ ...values, [INSTANCES_FIELD_KEY]: JSON.stringify(next) }) } const updateInstance = (index: number, patch: Partial) => { commit( instances.map((instance, i) => i === index ? { ...instance, ...patch } : instance, ), ) } const addInstance = () => { commit([...instances, { base_url: "", api_keys: "", proxy: "" }]) } const removeInstance = (index: number) => { commit(instances.filter((_, i) => i !== index)) } const fetchModels = async (index: number) => { const providerId = providerIdFor(index) setFetchingId(providerId) try { const models = await onFetchModels(providerId) if (models.length === 0) { onMessage(`No models found at ${providerId}.`, "warn") return } setModelsByProvider((prev) => ({ ...prev, [providerId]: [...new Set([...(prev[providerId] ?? []), ...models])], })) onMessage(`Fetched ${models.length} models from ${providerId}.`, "ok") } finally { setFetchingId(null) } } const addModels = (index: number, models: string[]) => { const providerId = providerIdFor(index) setModelsByProvider((prev) => ({ ...prev, [providerId]: [...new Set([...(prev[providerId] ?? []), ...models])], })) // Manually added ids are deliberately typed, so apply them immediately. const current = instances[index]?.models ?? [] updateInstance(index, { models: [...new Set([...current, ...models])] }) onAddModels(providerId, models) onMessage( `Added ${models.length} model id${models.length === 1 ? "" : "s"} to ${providerId}. Apply to save.`, "ok", ) } const toggleModel = (index: number, model: string) => { const current = instances[index]?.models ?? [] updateInstance( index, current.includes(model) ? { models: current.filter((candidate) => candidate !== model) } : { models: [...current, model] }, ) } const useModelAsDefault = (index: number, model: string) => { // Set the bare id: the prefix is optional, so MODEL=deepseek routes to // whatever endpoint advertises it (round-robin across duplicates). onValuesChange({ ...values, MODEL: model }) const current = instances[index]?.models ?? [] if (!current.includes(model)) { updateInstance(index, { models: [...current, model] }) } onMessage(`Default model set to ${model}. Apply to save.`, "ok") } if (!field) { return (

OpenAI-Compatible endpoint management is unavailable in this config.

) } const legacyBaseUrl = values["OPENAI_COMPATIBLE_BASE_URL"] ?? "" const legacyKey = values["OPENAI_COMPATIBLE_API_KEY"] ?? "" const cardModels = (index: number): string[] => { const providerId = providerIdFor(index) return [ ...new Set([ ...(instances[index]?.models ?? []), ...(modelsByProvider[providerId] ?? []), ]), ] } const keyCountFor = (index: number): number => (instances[index]?.api_keys ?? "") .split(",") .filter((key) => key.trim()).length const totalModels = instances.reduce( (sum, _instance, index) => sum + cardModels(index).length, 0, ) // Model ids served by more than one endpoint rotate provider + key pool // round-robin at runtime; surface that so it is never a surprise. const duplicateModelIds = (() => { const byModel = new Map() instances.forEach((_instance, index) => { cardModels(index).forEach((model) => { byModel.set(model, [...(byModel.get(model) ?? []), index]) }) }) return [...byModel.entries()] .filter(([, indexes]) => indexes.length > 1) .sort(([a], [b]) => a.localeCompare(b)) })() return (

Endpoints

Add any OpenAI-compatible server (vLLM, LM Studio, Ollama, Together, or your own gateway). Each endpoint becomes a numbered provider:{" "} openai_compatible_1/<model>,{" "} openai_compatible_2/<model>, … The prefix is optional — a model id also works bare (MODEL=deepseek) and resolves to whichever endpoint advertises it; when the same id exists on several endpoints, requests rotate round-robin across providers and key pools. Add as many model ids as you want per endpoint, comma-separated, or use Fetch models to pull them from the endpoint's /models route. Tick the models you want applied — selected ids are saved with the endpoint on{" "} Apply and appear in the Model Config dropdowns after a reload.

{instances.length} endpoint{instances.length === 1 ? "" : "s"} {totalModels} model id{totalModels === 1 ? "" : "s"}
{duplicateModelIds.length > 0 ? (

Model ids shared across endpoints rotate round-robin

    {duplicateModelIds.map(([model, indexes]) => (
  • {model} {" "} is served by{" "} {indexes .map( (index) => `Endpoint ${index + 1} (${keyCountFor(index)} key${ keyCountFor(index) === 1 ? "" : "s" })`, ) .join(", ")}{" "} — requests cycle across providers and key pools.
  • ))}
) : null} {instances.length === 0 && legacyBaseUrl.trim() ? (

You already have the legacy OPENAI_COMPATIBLE_BASE_URL{" "} endpoint configured. Import it as endpoint 1, or keep it and add more endpoints below.

) : null} {instances.length === 0 ? (

No endpoints yet. Add your first OpenAI-compatible endpoint.

) : (
{instances.map((instance, index) => { const providerId = providerIdFor(index) return ( updateInstance(index, patch)} onRemove={() => removeInstance(index)} onTest={() => { setTestingId(providerId) onTestProvider(providerId, () => setTestingId(null)) }} onFetch={() => void fetchModels(index)} onClearModels={() => { setModelsByProvider((prev) => ({ ...prev, [providerId]: [] })) updateInstance(index, { models: [] }) }} onAddModels={(models) => addModels(index, models)} onToggleModel={(model) => toggleModel(index, model)} onUseModel={(model) => useModelAsDefault(index, model)} /> ) })}
)}
) }