Architectural decision TL;DR
- LLM as a slice: yes. Mirror
Audio – give LLM a proper vertical slice.
- Providers as infra: all concrete LLM providers (OpenAI, Anthropic, local OpenAI-compatible, etc.) stay in
Infra.Llm.
- Setup as a feature: Local LLM setup is a feature slice (or sub-slice of existing
Setup), bootstrapped via MediatR, orchestrating infra services and scripts.
So:
Features/Audio → ffmpeg, Whisper, commands/queries for recording/STT
Features/Llm → summarization, templates, config, setup flows (MediatR)
Infra/Llm → provider implementations (OpenAiLlmProvider, AnthropicLlmProvider, LocalOpenAiCompatibleLlmProvider)
This keeps the domain-y flows (summarization, setup UX) in slices and the “talk to remote things over HTTP / spawn processes” in infra, which is consistent with how Audio is isolated.
Summary
Add a local LLM provider that uses an OpenAI-compatible HTTP API (llama.cpp server, Ollama, LM Studio, etc.) and expose it via a dedicated LLM feature slice.
Key points:
- Keep Whisper.cpp inside the
Audio slice for STT.
- Introduce an LLM slice that owns summarization workflows, templates, and setup commands.
- Keep concrete LLM providers (OpenAI, Anthropic, Local/OpenAI-compatible) in
Infra.Llm.
- Add guided local LLM setup wired through MediatR just like other feature slices.
Goals
- Promote LLM behavior to a first-class feature slice, not a random infra detail.
- Add a local LLM provider using OpenAI-compatible HTTP APIs.
- Provide MediatR-based setup flows for local LLM:
- Detect llama.cpp (or compatible server).
- Optionally download & configure a default GGUF model.
- Wire config into the LLM slice.
- Maintain clear Infra vs Slice boundaries:
- Slices express “what the app does”.
- Infra expresses “how we talk to the outside world / system”.
Slices & Layout
New/Refined Structure
Features/
Audio/
Commands/
RecordAudioCommand.cs
TranscribeAudioCommand.cs
...
Llm/
Commands/
SummarizeNoteCommand.cs
SetupLocalLlmCommand.cs
Queries/
GetLlmStatusQuery.cs
Templates/
NoteSummaries/
DefaultNoteSummaryTemplate.txt (or similar)
...
Setup/
Commands/
RunInitialSetupWizardCommand.cs
RunAudioSetupCommand.cs
RunLocalLlmSetupCommand.cs // orchestrates Llm.SetupLocalLlmCommand
Infra/
Audio/
FfmpegRecorder.cs
WhisperCppSttProvider.cs
OpenAiWhisperSttProvider.cs
Llm/
OpenAiLlmProvider.cs
AnthropicLlmProvider.cs
LocalOpenAiCompatibleLlmProvider.cs
LlmProviderFactory.cs
Scripts/
LocalLlmModelDownloader.cs (invokes bash/pwsh or raw HTTP download)
ProcessRunner.cs
Config/
LlmOptions.cs
AudioOptions.cs
SetupOptions.cs
Architecture Decision: Slice vs Infra
Why LLM belongs in a slice (like Audio)
-
Audio today:
- Feature slice coordinates “record audio”, “transcribe audio”, etc.
- Infra does the ugly bits (ffmpeg, Whisper.cpp CLI).
-
LLM should mirror that:
- Slice coordinates “summarize note”, “generate tags”, “check LLM status”, “setup local LLM”.
- Infra does HTTP calls and process spawning for different providers.
Current smell: LLM is mostly hiding in infra as LlmProvider implementations. That makes summarization feel like an infra concern instead of a first-class behavior of the app.
Refactor direction:
- Lift or re-home LLM-specific behaviors (summarization, template selection, provider selection) into
Features/Llm.
- Keep raw provider details (
OpenAiLlmProvider, AnthropicLlmProvider, LocalOpenAiCompatibleLlmProvider) in Infra.Llm.
Result: app logic = slices; IO / external calls = infra. Symmetric with Audio.
Feature Design
1. LLM Slice: Core Responsibilities
Features/Llm should own:
The LLM slice should not know:
- How HTTP is executed.
- How to spawn llama.cpp.
- How exactly OpenAI/Anthropic JSON is structured.
All that lives in infra.
2. Infra LLM Provider: LocalOpenAiCompatibleLlmProvider
In Infra/Llm:
Config
public sealed class OpenAiCompatibleLocalLlmProviderConfig
{
public string BaseUrl { get; init; } = "http://127.0.0.1:11434/v1";
public string Model { get; init; } = "local-llama";
public string? ApiKey { get; init; } = null;
public int TimeoutSeconds { get; init; } = 60;
}
Provider Type Enum
public enum LlmProviderType
{
OpenAi,
Anthropic,
OpenAiCompatibleLocal
}
Provider Implementation
LocalOpenAiCompatibleLlmProvider:
This preserves your existing provider abstraction and simply adds a new variant.
3. LLM Slice: Summarization Flow
Command example:
public sealed record SummarizeNoteCommand(Guid NoteId) : IRequest<SummarizeNoteResult>;
Handler responsibilities:
-
Load note & transcript from persistence.
-
Compose prompt using a template (e.g., via INoteSummaryTemplateRenderer).
-
Select provider (via ILlmOrchestrator / ILlmProviderSelector):
- Try
defaultProvider.
- Fall back according to
LlmOptions.FallbackOrder.
-
Call ILlmProvider.CompleteAsync(...).
-
Persist summary + tags.
-
Return result.
The handler doesn’t know if it’s OpenAI, Anthropic, or local llama – it just uses the abstraction.
4. Setup Flow via MediatR
We reuse the existing pattern: Setup is a slice that orchestrates other slices via MediatR commands.
4.1. Commands
In Features/Llm/Commands/SetupLocalLlmCommand.cs:
public sealed record SetupLocalLlmCommand(bool ForceRedownload = false) : IRequest<SetupLocalLlmResult>;
Handler steps (LLM slice):
-
Ask an infra service to check for llama.cpp:
-
If no binary:
- Return result indicating missing dependency (so Setup slice / CLI can show instructions).
-
If no model:
- Emit a
SetupLocalLlmModelRequired state for CLI to prompt user for consent to download.
-
If user consents (CLI passes a new SetupLocalLlmCommand with a flag or additional info):
- Delegate to
ILocalLlmModelInstaller (infra) to download & register the model.
-
Update LlmOptions (or your config storage) to point to the new model & provider.
-
Optionally run a TestLlmProviderCommand to verify.
In Features/Setup/Commands/RunLocalLlmSetupCommand.cs:
This follows the same pattern as other feature slices that use MediatR for setup flows.
4.2. Infra Services for Setup
In Infra/Llm (or Infra/Scripts):
This keeps all OS/process ugliness out of the LLM slice and Setup slice.
5. Homebrew + Dependency Story
Update sirkirby/homebrew-ten-second-tom:
LLM setup flow:
-
brew install sirkirby/tap/ten-second-tom
-
ten-second-tom initial run:
-
If accepted:
- Dispatch
RunLocalLlmSetupCommand → SetupLocalLlmCommand → LocalLlmModelInstaller.
6. Config Model
LlmOptions (in Config):
public sealed class LlmOptions
{
public string DefaultProvider { get; init; } = "local-llama";
public IReadOnlyList<string> FallbackOrder { get; init; } = new[] { "local-llama", "openai", "anthropic" };
public Dictionary<string, LlmProviderConfig> Providers { get; init; } = new();
}
public sealed class LlmProviderConfig
{
public string Type { get; init; } = default!; // "openai", "anthropic", "openai-compatible-local", etc.
public object RawConfig { get; init; } = default!;
}
The LLM slice consumes LlmOptions. The Infra factory turns them into concrete ILlmProvider instances.
Testing
LLM Slice
Infra
-
LocalOpenAiCompatibleLlmProviderTests:
- Use test HTTP server to emulate OpenAI API.
- Validate request/response handling + error behavior.
-
LocalLlmEnvironmentCheckerTests:
- With
IFileSystem / IProcessRunner fakes.
-
LocalLlmModelInstallerTests:
- Validate path selection.
- Validate configuration update hooks.
Acceptance Criteria
So:
Features/Audio→ ffmpeg, Whisper, commands/queries for recording/STTFeatures/Llm→ summarization, templates, config, setup flows (MediatR)Infra/Llm→ provider implementations (OpenAiLlmProvider,AnthropicLlmProvider,LocalOpenAiCompatibleLlmProvider)This keeps the domain-y flows (summarization, setup UX) in slices and the “talk to remote things over HTTP / spawn processes” in infra, which is consistent with how Audio is isolated.
Summary
Add a local LLM provider that uses an OpenAI-compatible HTTP API (llama.cpp server, Ollama, LM Studio, etc.) and expose it via a dedicated LLM feature slice.
Key points:
Audioslice for STT.Infra.Llm.Goals
Slices & Layout
New/Refined Structure
Architecture Decision: Slice vs Infra
Why LLM belongs in a slice (like Audio)
Audio today:
LLM should mirror that:
Current smell: LLM is mostly hiding in infra as
LlmProviderimplementations. That makes summarization feel like an infra concern instead of a first-class behavior of the app.Refactor direction:
Features/Llm.OpenAiLlmProvider,AnthropicLlmProvider,LocalOpenAiCompatibleLlmProvider) inInfra.Llm.Result: app logic = slices; IO / external calls = infra. Symmetric with Audio.
Feature Design
1. LLM Slice: Core Responsibilities
Features/Llmshould own:Commands/queries describing intent:
SummarizeNoteCommandSummarizeTranscriptCommandSetupLocalLlmCommand(driven by Setup slice)TestLlmProviderCommandGetLlmStatusQueryTemplates & policies:
Config mapping:
LlmOptionsand decide which provider to use viaILlmProviderFactory.The LLM slice should not know:
All that lives in infra.
2. Infra LLM Provider:
LocalOpenAiCompatibleLlmProviderIn
Infra/Llm:Config
Provider Type Enum
Provider Implementation
LocalOpenAiCompatibleLlmProvider:Implements
ILlmProvider.Accepts
OpenAiCompatibleLocalLlmProviderConfig.Sends OpenAI-style
POST /v1/chat/completions(or/v1/completions) toBaseUrl.Used for:
This preserves your existing provider abstraction and simply adds a new variant.
3. LLM Slice: Summarization Flow
Command example:
Handler responsibilities:
Load note & transcript from persistence.
Compose prompt using a template (e.g., via
INoteSummaryTemplateRenderer).Select provider (via
ILlmOrchestrator/ILlmProviderSelector):defaultProvider.LlmOptions.FallbackOrder.Call
ILlmProvider.CompleteAsync(...).Persist summary + tags.
Return result.
The handler doesn’t know if it’s OpenAI, Anthropic, or local llama – it just uses the abstraction.
4. Setup Flow via MediatR
We reuse the existing pattern: Setup is a slice that orchestrates other slices via MediatR commands.
4.1. Commands
In
Features/Llm/Commands/SetupLocalLlmCommand.cs:Handler steps (LLM slice):
Ask an infra service to check for llama.cpp:
ILocalLlmEnvironmentChecker→LocalLlmEnvironmentStatusHasLlamaBinaryHasConfiguredModelConfiguredModelPathIf no binary:
If no model:
SetupLocalLlmModelRequiredstate for CLI to prompt user for consent to download.If user consents (CLI passes a new
SetupLocalLlmCommandwith a flag or additional info):ILocalLlmModelInstaller(infra) to download & register the model.Update
LlmOptions(or your config storage) to point to the new model & provider.Optionally run a
TestLlmProviderCommandto verify.In
Features/Setup/Commands/RunLocalLlmSetupCommand.cs:Orchestrates the human-facing sequence in CLI:
SetupLocalLlmCommand.This follows the same pattern as other feature slices that use MediatR for setup flows.
4.2. Infra Services for Setup
In
Infra/Llm(orInfra/Scripts):LocalLlmEnvironmentChecker:Knows how to check for:
llama-server/llama-clion PATH.LocalLlmModelInstaller:Either:
ProcessRunner).Writes model to:
~/Library/Application Support/ten-second-tom/models~/.local/share/ten-second-tom/modelsReturns installed path & model metadata.
This keeps all OS/process ugliness out of the LLM slice and Setup slice.
5. Homebrew + Dependency Story
Update
sirkirby/homebrew-ten-second-tom:Add dependency:
Ensure
llama-server/llama-cliis installed on PATH on macOS.LLM setup flow:
brew install sirkirby/tap/ten-second-tomten-second-tominitial run:Setup slice runs
RunInitialSetupWizardCommand.That wizard includes an option:
If accepted:
RunLocalLlmSetupCommand→SetupLocalLlmCommand→LocalLlmModelInstaller.6. Config Model
LlmOptions(in Config):The LLM slice consumes
LlmOptions. The Infra factory turns them into concreteILlmProviderinstances.Testing
LLM Slice
SummarizeNoteCommandHandlerTests:Uses a fake
ILlmProviderandILlmProviderSelector.Asserts:
DefaultProvider+FallbackOrder.SetupLocalLlmCommandHandlerTests:Fake
ILocalLlmEnvironmentChecker+ILocalLlmModelInstaller.Covers:
Infra
LocalOpenAiCompatibleLlmProviderTests:LocalLlmEnvironmentCheckerTests:IFileSystem/IProcessRunnerfakes.LocalLlmModelInstallerTests:Acceptance Criteria
Architecture
Features/Llmslice exists and owns summarization and LLM setup commands/queries.Infra/Llmowns all concrete provider implementations and setup helpers.Local Provider
LocalOpenAiCompatibleLlmProviderimplemented.Setup via MediatR
SetupLocalLlmCommandin LLM slice.RunLocalLlmSetupCommandin Setup slice orchestrating CLI UX.Homebrew
llama.cpp.Config
LlmOptionssupportsDefaultProvider,FallbackOrder, and provider registry.Fallback Behavior
FallbackOrder.Docs
Updated documentation explaining:
openai-compatible-local).