# How DictationPostProcessingService Integrates with AI Providers in FluidVoice

> Learn how FluidVoice's DictationPostProcessingService seamlessly integrates with AI providers like Apple Intelligence and cloud LLMs. Explore unified abstraction for dictation processing.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: internals
- Published: 2026-07-03

---

**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`](https://github.com/altic-dev/FluidVoice/blob/main/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:

```swift
// 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:

```swift
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`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Networking/OpenAICompatibleProvider.swift) handles the actual HTTP interaction:

- **Endpoint construction** – Automatically appends `/chat/completions` unless the base URL already contains a path
- **Local endpoint detection** – Skips the `Authorization` header 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_effort` parameters for Groq reasoning models
- **Serialization** – Encodes the request into a `ChatRequest` JSON payload and executes via `URLSession`
- **Response parsing** – Extracts the first `content` field from the `ChatResponse` JSON structure

## Error Handling and Validation

The service enforces strict validation before network requests:

- **Missing credentials** – Throws `AIProcessingError.noVerifiedProvider` when no provider is configured, `AIProcessingError.missingModel` when the model identifier is empty, and `AIProcessingError.missingAPIKey` when 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.applyGAAVFormatting` before returning the final `Result` object

## Complete Implementation Example

The following pattern demonstrates how to invoke the service from a view model with proper provider checking:

```swift
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:

```json
{
  "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 `OpenAICompatibleProvider` removes authentication requirements for local network addresses (`192.168.x.x`, `10.x.x.x`, etc.)
- **Comprehensive error handling** through `AIProcessingError` variants 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.