How DictationPostProcessingService Integrates with AI Providers in FluidVoice
The DictationPostProcessingService acts as a unified abstraction layer that routes raw dictation transcripts to Apple Intelligence, local private AI models, or any cloud-based LLM through an OpenAI-compatible interface, automatically handling provider resolution, credential management, and HTTP request construction.
The DictationPostProcessingService is the central orchestration component in the FluidVoice macOS application that transforms raw speech-to-text output into polished, context-aware text. When users enable AI-enhanced dictation, this Swift service coordinates between multiple AI backends—from local macOS FoundationModels to remote OpenAI, Anthropic, or custom endpoints—without requiring changes to the core dictation logic.
Provider Resolution Architecture
The integration begins in Sources/Fluid/Services/DictationPostProcessingService.swift, where the process(_:dictationSlot:) method serves as the primary entry point. This method first validates the input text, then delegates provider selection to a dedicated resolution engine.
The resolveProvider Method
The resolveProvider(settings:dictationSlot:) method implements a four-tier fallback strategy to determine which AI backend handles the request:
// Returns ResolvedProvider containing providerID, providerKey, baseURL, model, apiKey
func resolveProvider(settings: SettingsStore,
dictationSlot: DictationShortcutSlot) -> ResolvedProvider
Private AI (Local LLM) – When settings.dictationPromptSelection == .privateAI and a verified model ID exists, the service returns a ResolvedProvider with an empty API key and the local model path. This triggers the PrivateAIIntegrationService pathway instead of a network call.
Saved Custom Provider – If the selected provider ID matches an entry in SettingsStore.savedProviders, the service extracts the custom baseURL, model, and stored API key, returning a provider prefixed with "custom:".
Built-in Cloud Provider – For recognized providers like OpenAI, Anthropic, or Cohere (verified via ModelRepository.shared.isBuiltIn(providerID)), the service uses default base URLs from ModelRepository alongside the selected model and globally stored API keys.
Fallback Handler – Any unrecognized provider ID passes through with raw user-supplied data, enabling integration with experimental or niche LLM endpoints.
Special Case Handlers
The service recognizes two provider types that bypass the standard HTTP flow entirely.
Apple Intelligence Integration
When the resolved providerID equals "apple-intelligence", the service instantiates AppleIntelligenceProvider and processes the request through the local macOS FoundationModels framework (available on macOS 26+). This pathway executes entirely in-process without generating HTTP requests, preserving privacy and reducing latency.
Private AI Local Models
For the "private-ai" selection, the service forwards the transcript to PrivateAIIntegrationService.enhanceDictation(...). This method handles locally-hosted or self-hosted LLM instances, managing the inference pipeline independently of the cloud-based LLMClient architecture.
Cloud Provider Request Flow
When the resolved provider represents a remote LLM, the service constructs a standardized request through three layers.
Building the LLMClient Configuration
The service prepares a LLMClient.Config struct with the following parameters:
var config = LLMClient.Config(
messages: [
.init(role: .system, content: ""),
.init(role: .user, content: promptRenderedText)
],
model: resolved.model,
baseURL: resolved.baseURL,
apiKey: resolved.apiKey,
streaming: false,
tools: [],
temperature: settings.isTemperatureUnsupported(resolved.model) ? nil : 0.2,
extraParameters: extraParams
)
Reasoning model parameters – If the selected model supports reasoning (e.g., Groq's "gpt-oss" models), the service injects additional parameters like enable_thinking or reasoning_effort into the extraParameters dictionary.
OpenAICompatibleProvider Implementation
LLMClient.shared.call(config) delegates to an AIProvider implementation. The default OpenAICompatibleProvider located in Sources/Fluid/Networking/OpenAICompatibleProvider.swift handles the actual HTTP interaction:
- Endpoint construction – Automatically appends
/chat/completionsunless the base URL already contains a path - Local endpoint detection – Skips the
Authorizationheader for local addresses (localhost,127.*,10.*,192.168.*,172.16-31.*) to support self-hosted models without API key requirements - Groq-specific handling – Adds
reasoning_effortparameters for Groq reasoning models - Serialization – Encodes the request into a
ChatRequestJSON payload and executes viaURLSession - Response parsing – Extracts the first
contentfield from theChatResponseJSON structure
Error Handling and Validation
The service enforces strict validation before network requests:
- Missing credentials – Throws
AIProcessingError.noVerifiedProviderwhen no provider is configured,AIProcessingError.missingModelwhen the model identifier is empty, andAIProcessingError.missingAPIKeywhen the API key is required but absent - Empty responses – Converts empty HTTP responses or malformed JSON into
AIProcessingError.emptyResponse - Formatting cleanup – After receiving text from any provider, the service strips formatting artifacts through
ASRService.applyGAAVFormattingbefore returning the finalResultobject
Complete Implementation Example
The following pattern demonstrates how to invoke the service from a view model with proper provider checking:
import Fluid
class DictationViewModel {
func processTranscript(_ transcript: String) async {
// Verify configuration before attempting enhancement
guard DictationAIPostProcessingGate.isProviderConfigured() else {
print("No AI provider configured")
return
}
do {
let result = try await DictationPostProcessingService.shared.process(
transcript,
dictationSlot: .primary
)
print("Enhanced text:", result.text)
} catch let error as AIProcessingError {
switch error {
case .missingAPIKey:
print("API key not found")
case .emptyResponse:
print("Model returned empty content")
default:
print("Processing failed:", error)
}
} catch {
print("Network or system error:", error)
}
}
}
Configuration storage in SettingsStore follows this structure:
{
"selectedProviderID": "openai",
"providerAPIKeys": {
"openai": "sk-YOUR_OPENAI_KEY",
"anthropic": "sk-ant-..."
},
"selectedModelByProvider": {
"openai": "gpt-4o-mini",
"anthropic": "claude-3-5-sonnet-20241022"
},
"savedProviders": [
{
"id": "custom-local",
"baseURL": "http://localhost:11434/v1",
"models": ["llama3.1", "mistral"]
}
]
}
Summary
- DictationPostProcessingService orchestrates all AI-enhanced dictation through a single
process(_:dictationSlot:)method that abstracts provider-specific complexity - Four-tier resolution handles Private AI, custom providers, built-in cloud providers, and fallback configurations through
resolveProvider(settings:dictationSlot:) - Dual local pathways support Apple Intelligence (macOS FoundationModels) and Private AI (self-hosted models) without HTTP overhead
- OpenAI-compatible abstraction enables integration with OpenAI, Anthropic, Cohere, Groq, and custom endpoints through standardized chat completion requests
- Automatic local detection in
OpenAICompatibleProviderremoves authentication requirements for local network addresses (192.168.x.x,10.x.x.x, etc.) - Comprehensive error handling through
AIProcessingErrorvariants ensures the UI can distinguish between configuration, network, and model-specific failures
Frequently Asked Questions
How does DictationPostProcessingService handle API keys for different providers?
The service retrieves API keys from SettingsStore.providerAPIKeys using the resolved providerID as the lookup key. For built-in providers like OpenAI or Anthropic, it accesses the global key store. For custom providers saved in SettingsStore.savedProviders, it uses the key stored within that specific provider entry. Local endpoints detected by OpenAICompatibleProvider automatically skip authentication headers entirely.
Can I use a local LLM like Ollama with DictationPostProcessingService?
Yes. Add your local endpoint (e.g., http://localhost:11434/v1) as a custom provider in SettingsStore.savedProviders. The OpenAICompatibleProvider detects local IP addresses (127.*, 192.168.*, etc.) and omits the Authorization header, allowing Ollama, LM Studio, or other OpenAI-compatible local servers to work without API keys.
What happens when Apple Intelligence is selected as the provider?
When selectedProviderID equals "apple-intelligence", the service instantiates AppleIntelligenceProvider and processes the transcript through the macOS FoundationModels framework. This bypasses LLMClient entirely, executing the inference in-process on macOS 26 or later without network requests, providing lower latency and complete privacy for the dictation text.
How does the service format the final output from AI providers?
After receiving the raw text from any provider (cloud or local), the service passes the result through ASRService.applyGAAVFormatting to remove formatting artifacts, markdown syntax, or unwanted whitespace. This ensures consistent text output regardless of whether the underlying provider was OpenAI, a local Ollama instance, or Apple Intelligence.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →