SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
11.2 KB

# Workstream B — Models (2026-09-11)

Model picker, badges, profile, catalog, public SEO pages, capability-aware settings and presets. Everything derives from real registry data (PolyModel); nothing is invented — unknown release dates and cutoffs render as "—".

# Files

# New — pure libraries (src/lib/models/*, client- and server-safe, unit-tested)

File Exports
badges.ts buildBadgeContext(models, now) (pricing quantiles, "new" window), deriveBadges(model, allModels | ctx), lifecycleStatus(model, {connected, ctx, now}), isFastModel/isCheapModel/isCodingModel/isOpenWeightsModel/isLongContextModel/isNewModel, speedTier, blendedPrice, shutdownDateOf, retiresSoon, sortWeightOf, regexes FAST_RE/FRONTIER_RE/CODING_RE/OPEN_WEIGHTS_RE
search.ts parseSearchQuery(q) → SearchIntent (brands, capabilities, min context, max $/M, open weights, coding, newest, quality, sort, chips), searchModels(models, q | intent, ctx) → SearchResult[] (hard filters + ranked text/intent score), isEmptyIntent
slug.ts slugify, modelSlug, compareSlug, parseCompareSlug, findModelBySlug, resolveCompareSlug, MAX_COMPARE
params.ts PARAM_DEFS (17 unified settings × group × supports(model) × detail), SETTINGS_GROUPS, groupsFor, supportsParam, isSetValue, unsupportedSettings, presetCompatibility, presetToSettings, pruneSettingsForModel
sections.ts buildSelectorSections (Favorites, Recent, Recommended, Fast, Best reasoning, Cheapest, Largest context, Vision, Coding, Open source, New) and recommendedModels (wraps routeModels)
format.ts formatPrice, formatPricePair, formatContext, formatIsoDate
public-data.ts server-only: getPublicModels() (React cache, graceful ok:false when DB is down), flagshipModels, popularComparisons

# New — components

  • src/components/models/model-profile.tsx — ModelProfile (ResponsiveDialog, snap full): header + user label (set / remove), lifecycle & derived badges, 12 facts (context, max out, in/out/cached $, long-context tier, speed tier, release date, knowledge cutoff, provider listing date, first seen, prices as of), capability grid, supported parameters grouped with ranges/levels, lifecycle block, aliases/source. Footer: Compare (/app/models/compare?m=), Arena (/app/arena?models=key), Use in chat (or Connect).
  • src/components/models/compare-table.tsx — CompareTable (server-safe, no hooks): header cards, spec rows with meters (context, max out, input/output $), capability matrix, parameter matrix. Desktop grid / phone stacked cards + compact check grid.
  • src/components/models/meter.tsx — single-hue 4 px magnitude bar (values always printed beside it).
  • src/components/models/badge-chips.tsx — BadgeChips, LifecycleChip (presentational, shared by app and public pages).

# Rewritten

  • src/components/chat/model-selector.tsx — same export/props (value, onChange, className, size, allowDisconnected, multiple, selected, onToggle, buttonLabel) + showAuto?, trigger?, draft?. Phone: BottomSheet snap full with fixed search header; desktop: dialog (680 px). Sections + "All models" by provider, always virtualized (@tanstack/react-virtual, rows 56 px, dynamic measure). AUTO card with router mode Segmented (persisted localStorage["polyllm:router-mode"] through useLocalStorage → JSON string, e.g. "balanced"). Intent search with "Understood:" chips. Rows: provider icon, name + accent label, lifecycle chip, badges, ctx · $in/$out, capability glyphs, star (animate-pop), "…" menu (desktop DropdownMenu / phone long-press or "…" → ActionSheet): Favorite, Set/Change/Remove label (PromptDialog → store.setLabel), View profile, Compare. Keyboard ↑↓↵ (combobox pattern, aria-activedescendant). ⌘/ toggles.
  • src/components/chat/model-badges.tsx — CAPABILITY_GLYPHS, CapabilityGlyphs, ModelBadges (NEW/REASONING/VISION/CODING/FAST/CHEAP/LONG CONTEXT), StatusBadge({ model, connected, ctx, showActive }) (New / Active / Preview / Deprecated / Retiring < 90 d / Unavailable / Unverified), useBadgeContext(); kept capabilityList, CapabilityBadges, PriceLabel, isFastModel, retiresSoon for existing callers (empty-state, arena/types).
  • src/components/chat/model-config.tsx — same props (model, settings, onChange, systemPrompt, onSystemPromptChange, trigger, allowSavePreset). ResponsiveDialog: phone full sheet, desktop right panel. Collapsible groups Generation / Reasoning / Output / Tools / Advanced built from groupsFor(model); sliders with numeric inputs + per-control reset; Segmented for small enums; Reset to defaults; Save preset (PromptDialog → POST /api/presets?kind=model); Apply preset (sheet listing presets, compatible first; incompatible ones list every reason from presetCompatibility). countActiveSettings kept.
  • src/app/app/models/page.tsx — catalog: mobile 48 px bar (hamburger, refresh), intent search, ChipRow filters (Reasoning, Vision, Tools, JSON, Web, PDF, Fast, Cheap, Open source, New), provider chips, status Segmented (Deprecated fetches /api/models?deprecated=1), Connected only / Favorites switches, sortable columns (Model, Provider, Input, Output, Context, Max out, Reasoning, Vision, Tools, Speed tier, Status; aria-sort), phone sort Select, virtualized rows (stacked below md, grid table from md, extra columns from lg), inline price/context meters, select up to 4 → compare bar → /app/models/compare?m=a,b,c, row → ModelProfile. Sync/refresh + favorites unchanged.
  • src/app/app/presets/page.tsx — editor moved to ResponsiveDialog (sheet on phones), pruning now uses pruneSettingsForModel (effort-level aware). components/presets/* unchanged.

# New — pages

  • src/app/app/models/compare/page.tsx — arbitrary sets (?m=key1,key2[,…], max 4), add/remove via ModelSelector multiple allowDisconnected, CompareTable, "Try in Arena".
  • src/app/(marketing)/models/page.tsx — public catalog (server component, force-dynamic, no auth): per-provider tables (desktop) / rows (mobile), badges, lifecycle, popular comparisons, sign-up CTA, honest empty state when the registry is unreachable.
  • src/app/(marketing)/compare/[slug]/page.tsx — gpt-5.5-vs-claude-sonnet-5 (2–4 parts, ids or display names, provider-prefixed ids for gateways), generateMetadata (title/description/canonical/OG), notFound() on unknown/duplicate, CompareTable, related comparisons, "Try both in Arena" → /app/arena?models=….

# Additive

  • src/lib/ai/registry/index.ts — rowToModel now emits metadata.firstSeenAt (ISO) next to lastSeenAt; flows through GET /api/models and listRegistryModels() unchanged.

# Tests (tests/unit/)

models-search.test.ts (intent parsing + ranking, 11), models-badges.test.ts (quantile badges, lifecycle, parameter sheet/presets, 9), models-slug.test.ts (4). pnpm test → 125/125.

# API changes

  • GET /api/models — additive: every model's metadata.firstSeenAt (ISO string). No new routes.
  • Presets: reuses POST /api/presets?kind=model { name, modelKey, systemPrompt, parameters, tools:{builtin} } from the settings panel.

# Integration notes for other areas

  • A (chat): pass showAuto (and optionally draft) to ModelSelector once the composer handles AUTO_MODEL_KEY; trigger lets the active-model pill open the picker. Router mode is in localStorage["polyllm:router-mode"] (JSON string via useLocalStorage; export ROUTER_MODE_KEY from model-selector.tsx). "Compare" actions navigate to /app/models/compare?m=…; profile "Arena" links to /app/arena?models=key — C should read ?models= (comma-separated keys).
  • F (sitemap): add /models and the popular /compare/<slug> pages (popularComparisons() in lib/models/public-data.ts) to app/sitemap.ts; consider linking "Models" in the marketing header/footer.
  • E (custom endpoints): lib/models/* never branches on provider ids except params.ts code-execution (OpenAI/Anthropic/Gemini, mirroring the previous UI) — set metadata.codeExecution explicitly on custom models to override.

# Heuristics worth knowing

  • CHEAP = blended (input + output $/1M) ≤ 25th percentile of the models passed in; FAST = fast-tier name regex or output price ≤ min($2, 35th percentile); LONG CONTEXT ≥ 400K; CODING = metadata.coding or codestral/coder/devstral/codex/"code"; OPEN SOURCE = metadata.openWeights/openSource or a vendor/name regex (llama, qwen, gemma, gpt-oss, deepseek, kimi…).
  • NEW = metadata.firstSeenAt within 30 days unless more than half the registry is that recent (fresh registry ⇒ falls back to provider createdAt/releaseDate only).
  • Lifecycle priority: Retiring (shutdown < 90 d) > Deprecated > Unavailable (no key) > Preview > New > Unverified > Active.

# Coming soon / not done

  • No <ComingSoon /> placeholders were needed; every control works.
  • /models/compare?m= outside the app is not a route (the public URL form is /compare/[slug]; the app form is /app/models/compare?m=).
  • Public pages show list prices from the last catalog audit; there is no per-user pricing.

# Visual QA checklist (375 / 390 / 393 / 430 / 1440)

  1. Chat header → model pill → sheet opens full height, search input fixed, keyboard does not cover the list; scroll the list, pull down while scrolled (must not dismiss), pull down at top (dismisses). Desktop: ⌘/ opens dialog, input focused, ↑↓↵ pick, Esc closes.
  2. Type "cheap vision", "1M context", "under $1/M", "fastest gemini", "json schema", "open source", "sonet": "Understood" chips appear, results ranked; empty state text on nonsense.
  3. Sections: Favorites / Recent appear only when non-empty; "Show all N" expands; "All models" grouped by provider; badges and status chips do not wrap rows (row stays 56 px).
  4. Star toggles with pop animation; long-press a row (touch) → ActionSheet; "…" on desktop → menu; Set label → accent label shows in row, trigger pill and profile; Remove label works.
  5. AUTO card (with showAuto): Segmented modes scroll horizontally on phones, choice persists after reload; "Right now → model · reason" updates with mode.
  6. Profile sheet: facts show "—" for missing release date / cutoff; capability grid, parameter groups, footer buttons on one row at 375 px (labels truncate, not overflow).
  7. Settings panel: phone sheet / desktop right panel; groups collapse; slider + numeric input stay in sync; Reset disabled at 0 settings; Save preset creates a row (check /app/presets); Apply preset lists incompatible reasons and disables Apply.
  8. Catalog: no horizontal overflow at 375; filters toggle; sort select on phone; desktop headers sort with arrows; select 2–4 → compare bar above bottom nav → compare page; Deprecated status loads extra models.
  9. /app/models/compare?m=openai/gpt-5.5,anthropic/claude-sonnet-5: chips, add via picker (max 4), spec cards stacked on phone, grid on desktop, meters proportional.
  10. Public /models and /compare/gpt-5.5-vs-claude-sonnet-5 unauthenticated: header/footer from the marketing layout, metadata title/description, 404 on /compare/foo-vs-bar, CTAs go to /signup and /app/arena?models=….
  11. Dark mode and prefers-reduced-motion (pop animation disabled by the global rule) on all of the above.