Skip to main content

Core Design Patterns

1. Singleton Services

All core services (llmService, activeModelService, generationService, imageGenerationService) are singleton instances exported directly from their modules. Why: Prevents duplicate model loading, concurrent inference conflicts, memory leaks from orphaned contexts, and state desynchronization.
Benefits:
  • Thread safety: Promise deduplication ensures only one load operation at a time
  • Consistency: All callers see the same loaded model state
  • Resource safety: Only one native context exists at a time

2. Background-Safe Orchestration

generationService and imageGenerationService maintain state independently of React component lifecycle. Generation continues even when the user navigates away. Implementation: Listener pattern with immediate state delivery on subscription.
Key Points:
  • Services hold state in private fields (not React state)
  • Listeners are weakly held via cleanup functions
  • Subscribers receive current state immediately on mount
  • No memory leaks — cleanup functions remove listeners on unmount
Flow Example:
  1. User starts generation in ChatScreen
  2. ChatScreen subscribes to generationService
  3. User navigates to HomeScreen → ChatScreen unmounts, unsubscribes
  4. Generation continues, service maintains state
  5. HomeScreen mounts, subscribes, immediately receives current state (progress 15/20)
  6. User navigates back to ChatScreen
  7. ChatScreen re-subscribes, receives current state (progress 18/20)
  8. Generation completes, all subscribers notified

3. Memory-First Loading Strategy

All model loads check available RAM before proceeding. Prevents OOM crashes by blocking loads that would exceed safe limits. RAM Budget: 60% of device RAM Estimation Multipliers:
  • Text models: fileSize × 1.5 (KV cache + activations)
  • Vision models: (modelFileSize + mmProjSize) × 1.5
  • Image models: fileSize × 1.8 (MNN/QNN runtime overhead)
User-Friendly Messages:
RAM-Aware Runtime Safeguards: On low-RAM devices (≤4GB), llama.cpp can call abort() during Metal/OpenCL allocation, killing the app before JavaScript catches the error. To prevent this:

4. Combined Asset Tracking

Vision models track both main GGUF and mmproj files as a single logical unit. Why: mmproj files (100-700MB) are downloaded separately but required for vision inference. Memory estimates must include both.
Download Flow:
  1. User selects vision model (e.g., SmolVLM-500M)
  2. System downloads main GGUF file
  3. System detects vision capability, downloads mmproj automatically
  4. Both files linked in store: mmProjPath, mmProjFileSize
  5. On load, both passed to llmService

5. State Cleanup Patterns

After prompt enhancement (which uses llmService), explicit cleanup ensures text generation doesn’t hang. Why: Prompt enhancement runs a separate LLM generation to expand simple prompts (“a dog” → detailed 75-word description). Without cleanup, the LLM service remains in “generating” state, blocking subsequent text generation.
Key Decision: KV cache is NOT cleared after enhancement. Clearing the cache would invalidate all cached tokens, forcing full re-computation on the next vision inference. For large vision models, this adds 30-60 seconds to inference time. Trade-off:
  • ✅ Vision inference stays fast (cached tokens reused)
  • ⚠️ KV cache grows over time (cleared manually via settings or on model unload)

Token Batching (Performance Optimization)

Streaming token generation produces 10-50 tokens/second. Updating React state on every token causes excessive renders and frame drops. Solution: Batch tokens and flush to UI at a controlled rate (50ms intervals = ~20 updates/second).
Before:
  • 30 tok/s × 60fps = 1800 unnecessary renders/minute
  • Janky scrolling, high CPU usage
After:
  • 20 UI updates/second regardless of generation speed
  • Smooth scrolling, low overhead

Message Queue (Non-Blocking Input)

Users can send messages while the LLM is still generating. Messages are queued and processed automatically after the current generation completes.
Flow:
  1. User sends “What is AI?” → Generation starts
  2. User sends “Explain neural networks” → Added to queue (count: 1)
  3. User sends “Give me an example” → Added to queue (count: 2)
  4. First generation completes
  5. Queue processor aggregates messages:
  6. Combined message sent to LLM
UI Indicators:
  • Send button stays active during generation
  • Stop button visible alongside send button
  • Queue count badge shows “2 queued messages”
  • Tap badge to clear queue