PolyLLM — UI brief for contributors (humans and agents)
PolyLLM (www.polyllm.io) is a universal control center for AI models: users bring their own API keys (OpenAI, Anthropic, Gemini, xAI), discover current models, configure them precisely, chat with real streaming, compare models in the Arena and track usage/costs. Tagline: "Your models. Your keys. One workspace."
Stack & conventions
- Next.js 16 App Router, React 19, TypeScript strict, Tailwind v4 (tokens in
src/app/globals.css), shadcn-style primitives insrc/components/ui/*(button, input/textarea/label/field, badge, dialog, dropdown-menu, popover, select, slider, switch, tabs, tooltip, toast (toast.success/error/info), misc: Card/CardHeader/CardBody, Stat, PageHeader, EmptyState, Skeleton, CopyButton, Kbd, Spinner, Divider). Icons:lucide-react. Motion:motion/react(sparingly). - Client data:
useApi<T>(url)(SWR) andapi<T>(url, { method, json })fromsrc/lib/client/api.ts;streamEvents(url, body, onEvent, signal)for SSE. Types:src/lib/client/types.ts. Provider metadata:src/lib/client/providers.ts(PROVIDERS,PROVIDER_ORDER,providerName). Brand:src/components/brand/logo.tsx(Logo,LogoMark),src/components/brand/provider-icon.tsx(ProviderIcon). - App state:
useApp()fromsrc/components/app/store.tsx→{ user, preferences, updatePreferences, models, modelsByKey, favorites, recents, connectedProviders, connections, folders, toggleFavorite, refreshModels, refreshConnections, selectedModelKey, setSelectedModelKey, setPaletteOpen }. Everything under/app/*is wrapped by the shell (sidebar + command palette) insrc/app/app/layout.tsx; pages render inside<div className="relative flex min-w-0 flex-1 flex-col">. A page should typically be<main className="min-h-0 flex-1 overflow-y-auto scrollbar-thin"><div className="mx-auto w-full max-w-5xl px-4 py-6 sm:px-6 lg:px-8">…</div></main>. - Auth client:
authClient,signIn,signUp,signOut,useSessionfromsrc/lib/auth-client.ts(Better Auth 1.7). Server:requireUser()/getSession()insrc/lib/session.ts. - Formatting helpers:
formatUsd,formatTokens,formatMs,formatRelative,formatNumber,cninsrc/lib/utils.ts. - Never render key material. Connections only expose
keyHintlikesk-••••••••9A2K. - React Compiler lint rules apply: no setState synchronously in effects without the eslint-disable, no components defined
inside render, derive state with useMemo. Run
pnpm lintandpnpm typecheck.
Design language (must feel premium, unique, not a template)
- Minimal, dense-when-useful, excellent typography (Geist Sans/Mono via
font-sans/font-mono), subtle borders (border-border), surfacesbg-bg / bg-bg-subtle / bg-bg-muted / bg-bg-elevated, texttext-fg / text-fg-muted / text-fg-subtle, accenttext-accent / bg-accent / bg-accent-soft, semanticsuccess/warning/danger/info(+-soft). Radiirounded-md/lg/xl. Provider colors:text-openai,text-anthropic,text-gemini,text-xaiorstyle={{color: PROVIDERS[p].colorVar}}. - Dark and light must both look great. No cheap gradients, no childish AI imagery, no huge empty whitespace, no boxed-card overload.
A single restrained gradient (
.text-gradient) is allowed for hero words. Use.dot-grid/.glassutilities sparingly. - Mobile first: no horizontal overflow, 16px inputs on mobile (
text-[15px] sm:text-smis already in Input), safe-area aware. - Keyboard-first: ⌘K palette, ⌘N new chat, ⌘Enter send, Esc stop, ⌘/ model selector, ⌘B sidebar.
- Accessibility: semantic HTML, labels, visible focus, aria on icon buttons, reduced motion respected.
API routes (all JSON unless noted; all under a verified session)
GET /api/models→{ models: PolyModel[], favorites: string[], recents: string[], connectedProviders };POST /api/models {action:"toggle-favorite", modelKey};POST /api/models/sync {provider?}→{results: SyncResult[]}.GET /api/providers→{connections: PublicConnection[]};PUT /api/providers {provider, apiKey}→{ok, error?, modelsAvailable?, connections};POST /api/providers {action:"validate", provider};DELETE /api/providers?provider=.GET /api/conversations?q&provider&model&folder&archived=1&pinned=1&since&until&limit&cursor;POST /api/conversations;GET|PATCH|DELETE /api/conversations/:id;POST /api/conversations/:id/actions {action: duplicate|branch|export|share|unshare|share-status|delete-message}.POST /api/chat(SSE) bodyChatRequestInput→ eventsChatStreamEvent(seesrc/lib/client/types.ts).GET /api/folders,POST {name,color},PATCH {id,…},DELETE ?id=.POST /api/attachments(multipartfile) →{attachment};GET /api/attachments?id=streams the file.GET /api/usage?range=today|7d|30d|all→{ totals, series[{bucket,requests,inputTokens,outputTokens,costUsd}], byProvider[], byModel[], recent[] }.GET /api/presets→{modelPresets, promptPresets};POST /api/presets?kind=model|prompt;PATCH ?kind&id;DELETE ?kind&id.GET /api/preferences/PATCH /api/preferences(theme, defaultModelKey, defaultSystemPrompt, enterToSend, streaming, codeWrap, showReasoning, showCosts, autoTitle, onboardingCompleted).GET /api/arena→{sessions};POST /api/arena {prompt, systemPrompt?, modelKeys[1..4], settings?, attachmentIds?}→{session};POST /api/arena/stream {sessionId, modelKey}(SSE: meta → text-delta/reasoning-delta/citation → done|error);PATCH /api/arena {responseId, ratings}.GET /api/account→{audit};GET /api/account?export=1downloads the full account export.GET /api/search?q=.- Admin:
GET /api/admin/providers,POST /api/admin/providers {action:"sync", provider?}(owner only,user.isAdmin). - Better Auth:
/api/auth/*(client SDK). Email verification is mandatory: unverified users are redirected to/verify-email.
Model capability sheet (drives every configuration UI)
PolyModel.capabilities {text, vision, audioInput, audioOutput, imageGeneration, video, reasoning, tools, structuredOutput,
streaming, files, webSearch}; parameters {temperature, topP, topK, maxTokens, reasoningEffort, reasoningEffortLevels[],
thinkingBudget, thinkingBudgetRange, stop, seed, frequencyPenalty, presencePenalty, verbosity, temperatureRange};
limits {contextTokens, maxOutputTokens}; pricing {inputPerMillion, cachedInputPerMillion, outputPerMillion};
status active|preview|deprecated|unknown; metadata.shutdownDate, metadata.aliases, etc. Never show a control the model does not support.