Refactor the API Keys tab into a "Providers & Models" system with two provider types:
- Known Providers - Cloud providers (OpenAI, Anthropic, Google, etc.) with predefined models
- Custom Providers - User-configured servers with either auto-discovered or manually-defined models
Files:
pi-mono/packages/web-ui/src/dialogs/SettingsDialog.ts- ContainsApiKeysTabpi-mono/packages/web-ui/src/dialogs/ModelSelector.ts- Hard-coded Ollama discovery (lines 94-156)pi-mono/packages/web-ui/src/components/ProviderKeyInput.ts- API key input componentpi-mono/packages/ai/src/types.ts-Model<TApi>interface (lines 154-170)pi-mono/packages/ai/src/models.ts- Model registry (currently read-only)pi-mono/packages/ai/src/models.generated.ts- Generated known provider models
Problems:
- Only supports cloud providers with API keys
- Ollama is hard-coded in ModelSelector with no UI configuration
- No support for other local servers (llama.cpp, LM Studio, vLLM)
- No way to add custom models or providers
┌─ Known Providers ──────────────────────────────────────────┐
│ Cloud LLM providers with predefined models. │
│ API keys are stored locally in your browser. │
│ │
│ Anthropic │
│ API Key: [••••••••••••••••••••] [Save] │
│ Models: 12 available │
│ │
│ OpenAI │
│ API Key: [......................] [Save] │
│ Models: 8 available │
│ │
│ Google │
│ API Key: [......................] [Save] │
│ Models: 5 available │
│ │
│ (repeat for all known providers) │
└─────────────────────────────────────────────────────────────┘
┌─ Custom Providers ─────────────────────── [+ Add Provider] ┐
│ User-configured servers with auto-discovered or manually │
│ defined models. │
│ │
│ My Ollama Server │
│ • Type: Ollama │
│ • URL: http://localhost:11434 │
│ • Status: ✓ Connected │
│ • Models: 5 auto-discovered │
│ [Edit] [Refresh] [Delete] │
│ │
│ Work vLLM Cluster │
│ • Type: vLLM │
│ • URL: https://vllm.company.internal │
│ • Status: ✓ Connected │
│ • Models: 12 auto-discovered │
│ [Edit] [Refresh] [Delete] │
│ │
│ My Custom API │
│ • Type: OpenAI Responses Compatible │
│ • URL: https://api.myserver.com │
│ • Models: 2 manually defined │
│ [Edit] [Delete] │
└─────────────────────────────────────────────────────────────┘
Provider Configuration:
- Name: Display name (e.g., "My Ollama Server")
- Type: Dropdown
- Ollama (auto-discovers models)
- llama.cpp (auto-discovers models)
- vLLM (auto-discovers models)
- OpenAI Completions Compatible (manual models)
- OpenAI Responses Compatible (manual models)
- Anthropic Messages Compatible (manual models)
- Base URL: Server endpoint
- API Key: Optional (applies to all models from this provider)
For Auto-Discovery Types (Ollama/llama.cpp/vLLM):
- [Test Connection] button
- Shows discovered models with names and IDs
- User cannot edit model list (auto-managed)
For Manual Types (OpenAI/Anthropic Compatible):
- [+ Add Model] button opens model editor
- Shows list of manually-defined models
- Each model fully editable/deletable
Only shown for manual provider types. All fields map to Model<TApi> interface:
Basic:
- Model ID:
model.id(e.g., "my-llama-3.2-ft") - Display Name:
model.name(e.g., "My Fine-Tuned Llama") - API Type: Dropdown →
model.api- openai-completions
- openai-responses
- anthropic-messages
- google-generative-ai
Capabilities:
- Context Window:
model.contextWindow(number) - Max Tokens:
model.maxTokens(number) - Supports Reasoning:
model.reasoning(boolean) - Input Types:
model.input(checkboxes: text, image)
Costs ($/M tokens):
- Input:
model.cost.input - Output:
model.cost.output - Cache Read:
model.cost.cacheRead - Cache Write:
model.cost.cacheWrite
Derived Fields:
model.provider: Set from parent custom provider namemodel.baseUrl: Set from parent custom provider URL
Location: pi-mono/packages/web-ui/src/storage/stores/
// File: custom-providers-store.ts
"customProviders" → CustomProvider[]
interface CustomProvider {
id: string; // UUID
name: string; // Display name, also used as Model.provider
type: CustomProviderType;
baseUrl: string;
apiKey?: string; // Optional, applies to all models
// For manual types ONLY - models stored directly on provider
models?: Model<any>[];
}
type CustomProviderType =
| "ollama" // Auto-discovery - models fetched on-demand
| "llama.cpp" // Auto-discovery - models fetched on-demand
| "vllm" // Auto-discovery - models fetched on-demand
| "openai-completions" // Manual models - stored in provider.models
| "openai-responses" // Manual models - stored in provider.models
| "anthropic-messages"; // Manual models - stored in provider.modelsNotes:
- Single store only - no separate
customModelsstore needed CustomProvider.nameis used asModel.providerfor all models from that providerCustomProvider.baseUrlis used asModel.baseUrlfor all models from that provider- Auto-discovery types: Models are NOT stored. Fetched fresh on-demand via discovery functions.
- Manual types: Models stored in
provider.modelsarray directly
Problem: User loads/unloads models in Ollama → stale models in our UI
Solution: Always fetch fresh when needed:
- Provider dialog: "Test Connection" calls discovery function → shows preview
- Model selector: On open, calls discovery function for each auto-discovery provider → fresh list
- No storage: Only store provider config (name, type, baseUrl, apiKey) - never the models
Create: src/storage/stores/custom-providers-store.ts
- Define
CustomProviderTypetype - Define
CustomProviderinterface - Implement
CustomProvidersStoreclass - Create
getConfig()for IndexedDB schema
Update: src/storage/app-storage.ts
- Add
customProviders: CustomProvidersStore - Wire up to IndexedDB
- Increment database version
Create: src/utils/model-discovery.ts
// Auto-discovery functions - one per provider type
async function discoverOllamaModels(
baseUrl: string,
apiKey?: string
): Promise<Model<any>[]>
async function discoverLlamaCppModels(
baseUrl: string,
apiKey?: string
): Promise<Model<any>[]>
async function discoverVLLMModels(
baseUrl: string,
apiKey?: string
): Promise<Model<any>[]>
// Convenience wrapper
async function discoverModels(
type: "ollama" | "llama.cpp" | "vllm",
baseUrl: string,
apiKey?: string
): Promise<Model<any>[]>Implementation:
discoverOllamaModels():
GET ${baseUrl}/api/tags→ parse model list- Map to
Model<any>objects - Set
api: "openai-completions"by default - Special case: if
model.id.includes("gpt-oss")→api: "openai-responses" - Extract contextWindow/maxTokens if available
- Default costs to 0
discoverLlamaCppModels():
GET ${baseUrl}/v1/models→ parse OpenAI-compatible response- Map to
Model<any>objects - Set
api: "openai-completions"for all models (for now) - Extract contextWindow/maxTokens if available
- Default costs to 0
discoverVLLMModels():
GET ${baseUrl}/v1/models→ parse OpenAI-compatible response- Map to
Model<any>objects - Set
api: "openai-completions"for all models (for now) - Extract contextWindow/maxTokens if available
- Default costs to 0
Notes:
- Each function encapsulates provider-specific logic
apifield determined per-model based on provider capabilities- Start simple (default to openai-completions), add special cases as needed
- These functions used by both CustomProviderDialog AND ModelSelector
Update: src/models.ts
Make registry mutable and add:
// Register models from a custom provider (auto-discovery or manual)
export function registerProviderModels(
provider: string,
models: Model<Api>[]
): void
// Unregister all models from a provider
export function unregisterProvider(provider: string): void
// Clear all custom registrations (for app reset)
export function clearCustomModels(): voidImplementation:
- Make
modelRegistrymutable (no longeras const) registerProviderModels()adds/replaces models for a providerunregisterProvider()removes provider from registrygetProviders()andgetModels()work transparently with custom models
Create: src/components/KnownProvidersList.ts
- Loop through known providers from
getProviders() - For each provider:
- Show provider name
- Show API key input (ProviderKeyInput component)
- Show model count from
getModels(provider).length
- Always display all known providers
Create: src/components/CustomProvidersList.ts
- Load custom providers from storage
- For each provider:
- Show name, type, URL
- Show connection status (for auto-discovery types)
- Show model count
- Edit/Refresh/Delete buttons
- [+ Add Provider] button opens provider editor
Create: src/dialogs/CustomProviderDialog.ts
- Form fields: name, type dropdown, baseUrl, apiKey
- For auto-discovery types:
- [Test Connection] button
- Shows discovered models
- Save button stores provider + models
- For manual types:
- [+ Add Model] button
- Shows list of models (from customModels store filtered by providerId)
- Each model has Edit/Delete buttons
- Save/Cancel actions
Create: src/dialogs/CustomModelDialog.ts
- All fields from
Model<TApi>interface - API type dropdown determines available options
- Validation for required fields
- Save creates/updates entry in customModels store
- Cancel discards changes
Update: src/dialogs/SettingsDialog.ts
- Rename
ApiKeysTab→ProvidersModelsTab - Update
getTabName()→ "Providers & Models" - Layout:
- Section header: "Known Providers"
KnownProvidersListcomponent- Section header: "Custom Providers"
CustomProvidersListcomponent
Update: src/dialogs/ModelSelector.ts
- Remove hard-coded Ollama discovery (lines 94-156)
- Remove
ollamaModels,ollamaError,fetchOllamaModels() - Models now come from unified registry (includes custom providers)
- No code changes needed - registry handles everything
Update: src/storage/app-storage.ts
Add initialization hook:
async function initializeModelRegistry() {
const customProviders = await appStorage.customProviders.getAll();
// Register manually-defined models from manual provider types
for (const provider of customProviders) {
const isManualType = ["openai-completions", "openai-responses", "anthropic-messages"].includes(provider.type);
if (isManualType && provider.models) {
registerProviderModels(provider.name, provider.models);
}
// Auto-discovery providers: don't register anything at startup
// Models will be fetched on-demand when ModelSelector opens
}
}Call on app startup before any model selection happens.
Model Selector Loading:
- On open, check for auto-discovery providers
- Call
discoverModels()for each auto-discovery provider - Temporarily register discovered models with
registerProviderModels() - Display in selector alongside known + manual providers
Update: sitegeist/src/sidepanel.ts
- Import:
import { ProvidersModelsTab } from "@mariozechner/pi-web-ui" - Update SettingsDialog to use new tab name
Update: sitegeist/src/storage/app-storage.ts
- Ensure extended storage includes new custom provider/model stores
Known Providers:
- API key save/load for all known providers
- Model counts display correctly
- Existing functionality unchanged
Custom Providers - Auto-Discovery:
- Add Ollama server, test connection, save
- Add llama.cpp server, test connection, save
- Add vLLM server, test connection, save
- Refresh button updates model list
- Connection errors display properly
- Edit existing server updates models
- Delete server removes models from registry
Custom Providers - Manual:
- Add OpenAI-compatible provider
- Add Anthropic-compatible provider
- Add models with all field combinations
- Edit model updates registry
- Delete model removes from registry
- Provider deletion cascades to models
Model Selection:
- Known provider models appear
- Custom auto-discovered models appear
- Custom manual models appear
- Provider grouping works correctly
- Model metadata (context, cost, capabilities) correct
Storage:
- Data persists across browser reloads
- IndexedDB schema migration works
- No conflicts with existing API keys
No Breaking Changes:
- Purely additive functionality
- Existing API keys continue to work
- No changes to existing storage schema
New Dependencies:
- Ollama client already imported in ModelSelector
- Standard fetch API for other servers
Storage Migration:
- New IndexedDB object stores added
- Database version incremented
- No data migration needed
- Should auto-discovery types allow manual model editing (override detected values)?
- Should we validate model IDs for uniqueness across all providers?
- Should we periodically auto-refresh discovered models in background?
- Should connection status be shown in ModelSelector or only in settings?
- Should we allow disabling individual models without deleting them?
- Background auto-refresh of server models
- Model performance benchmarking
- Usage statistics per model
- Model tagging/categories
- Import/export configurations
- Shared configurations via URL/JSON