# How DictationPostProcessingService Integrates with Cloud AI Providers in FluidVoice

> Discover how FluidVoice's DictationPostProcessingService seamlessly integrates with cloud AI providers. Learn how it routes transcripts, constructs requests, and executes them for enhanced dictation.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: how-to-guide
- Published: 2026-07-01

---

**The DictationPostProcessingService routes raw dictation transcripts to cloud AI providers by resolving user-configured credentials, constructing OpenAI-compatible chat completion requests, and executing them through a provider-agnostic HTTP layer.**

The DictationPostProcessingService is the central orchestration component in the **altic-dev/FluidVoice** repository that transforms raw speech-to-text output into polished prose. When AI-enhanced dictation is enabled, this Swift service abstracts the complexity of credential management, request formatting, and cloud provider integration, allowing the dictation UI to remain agnostic of whether the backend is OpenAI, Anthropic, or a local model.

## Entry Point and Provider Resolution

The integration begins in [`Sources/Fluid/Services/DictationPostProcessingService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/DictationPostProcessingService.swift) with the `process(_:dictationSlot:)` method:

```swift
func process(_ inputText: String,
             dictationSlot: SettingsStore.DictationShortcutSlot = .primary) async throws -> Result

```

This method first trims the input and aborts early for empty strings. It then reads the global `SettingsStore` to determine which provider configuration applies to the requested slot.

### Resolving the Provider

The `resolveProvider(settings:dictationSlot:)` method implements a four-tier resolution strategy:

**1. Private AI (Local LLM)**
- **Condition**: `settings.dictationPromptSelection == .privateAI` and a verified model ID exists
- **Implementation**: Returns a `ResolvedProvider` with an empty API key and the local model path, routing to the internal "private-ai" provider

**2. Saved Custom Provider**
- **Condition**: The selected provider ID matches an entry in `SettingsStore.savedProviders`
- **Implementation**: Returns a `ResolvedProvider` with a "custom:" key prefix, using the stored `baseURL`, `model`, and API key from the user's settings file

**3. Built-in Cloud Provider**
- **Condition**: `ModelRepository.shared.isBuiltIn(providerID)` returns true (e.g., "openai", "anthropic", "cohere")
- **Implementation**: Uses the default base URL from `ModelRepository`, the selected model, and any globally stored API key

**4. Fallback**
- **Condition**: Any unmatched provider ID
- **Implementation**: Returns a provider with raw user-supplied data, enabling support for arbitrary OpenAI-compatible endpoints

The resulting `ResolvedProvider` struct contains all necessary network credentials: `providerID`, `providerKey`, `baseURL`, `model`, and `apiKey`.

## Special Provider Handlers

Before constructing HTTP requests, the service checks for two special integration paths that bypass the standard cloud flow.

### Apple Intelligence Integration

When the resolved `providerID` equals `"apple-intelligence"`, the service routes the request to `AppleIntelligenceProvider` (available on macOS 26+). This path uses the local macOS FoundationModels framework via an in-process call, requiring no HTTP request or external API key.

### Private AI Integration

When **Private-AI** mode is active, the service forwards the request to `PrivateAIIntegrationService.enhanceDictation(...)`. This handles locally-hosted or self-hosted LLM instances, keeping all inference on-device or on the local network.

## Building Cloud AI Requests

For standard cloud providers, the service constructs a request for `LLMClient` in [`Sources/Fluid/Services/LLMClient.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/LLMClient.swift):

**Reasoning Parameters**: Optional parameters like `enable_thinking` are appended if the model's reasoning configuration is enabled.

**Message Payload**: The service creates a chat completion payload with an empty system prompt and a user message containing the rendered dictation text.

**LLMClient Configuration**:

```swift
var config = LLMClient.Config(
    messages: messages,
    model: resolved.model,
    baseURL: resolved.baseURL,
    apiKey: resolved.apiKey,
    streaming: false,
    tools: [],
    temperature: settings.isTemperatureUnsupported(resolved.model) ? nil : 0.2,
    extraParameters: extraParams
)

let response = try await LLMClient.shared.call(config)

```

After receiving the response, the service applies `ASRService.applyGAAVFormatting` to strip unwanted formatting before returning the final `Result` object.

## HTTP Layer and OpenAI Compatibility

The `LLMClient` delegates the actual HTTP execution to an `AIProvider` implementation. The default implementation is `OpenAICompatibleProvider` in [`Sources/Fluid/Networking/OpenAICompatibleProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Networking/OpenAICompatibleProvider.swift), which standardizes communication with any OpenAI-compatible endpoint:

**Endpoint Construction**: Automatically appends `/chat/completions` unless the base URL already contains a path component.

**Local Endpoint Detection**: Detects private IP ranges (`localhost`, `127.*`, `10.*`, `192.168.*`, `172.16-31.*`) and omits the `Authorization` header for these endpoints, enabling seamless local LLM usage without API keys.

**Groq-Specific Handling**: Adds the `reasoning_effort` parameter for Groq "gpt-oss" models.

**Request Execution**: Serializes the `ChatRequest` struct to JSON and executes a `URLSession` POST request.

**Response Parsing**: Extracts the first `content` field from the returned `ChatResponse` JSON.

This architecture allows FluidVoice to support any cloud AI provider that implements the standard chat completions API schema without code changes.

## Error Handling Strategy

The service defines specific `AIProcessingError` variants to surface configuration issues:

- **`noVerifiedProvider`**: Thrown when provider resolution fails
- **`missingModel`**: Thrown when no model is configured for the resolved provider
- **`missingAPIKey`**: Thrown when a cloud provider requires an API key but none is stored
- **`emptyResponse`**: Returned when the AI provider returns a success status but no content

These typed errors allow the UI to present specific remediation steps, such as prompting the user to configure their API key in Settings.

## Practical Implementation Examples

### Calling the Service from a View Model

```swift
import Fluid

func enhanceDictation(_ transcript: String) async {
    do {
        let result = try await DictationPostProcessingService.shared.process(transcript)
        print("Enhanced text:", result.text)
    } catch let error as AIProcessingError {
        print("Configuration error:", error.localizedDescription)
    } catch {
        print("Network failure:", error)
    }
}

```

This invocation automatically uses whichever provider the user selected in the Settings UI, whether Apple Intelligence, Private AI, or a cloud LLM.

### Checking Provider Configuration

Before enabling AI features, verify that the user has configured a valid provider:

```swift
if DictationAIPostProcessingGate.isProviderConfigured() {
    // Enable "AI-enhanced dictation" toggle
} else {
    // Fallback to raw transcript only
}

```

### Example Settings Configuration

The following JSON fragment from `SettingsStore` demonstrates how a custom OpenAI configuration persists:

```json
{
  "selectedProviderID": "openai",
  "providerAPIKeys": {
    "openai": "sk-YOUR_OPENAI_KEY"
  },
  "selectedModelByProvider": {
    "openai": "gpt-4o-mini"
  },
  "savedProviders": [
    {
      "id": "openai",
      "baseURL": "https://api.openai.com/v1",
      "models": ["gpt-4o-mini", "gpt-4o"]
    }
  ]
}

```

When the user selects OpenAI, `DictationPostProcessingService` resolves these values and constructs the appropriate request for `OpenAICompatibleProvider`.

## Summary

- **DictationPostProcessingService** acts as a unified abstraction layer for AI-enhanced dictation in FluidVoice
- Provider resolution supports **four distinct paths**: Private AI, custom saved providers, built-in cloud providers, and raw fallback configurations
- **OpenAICompatibleProvider** handles HTTP execution for any OpenAI-compatible endpoint, with automatic local-network detection that skips authorization headers
- **Special handlers** exist for Apple Intelligence (macOS FoundationModels) and Private AI (local/self-hosted models)
- The **LLMClient** standardizes request construction with support for reasoning parameters and temperature controls
- **Explicit error types** (`AIProcessingError`) distinguish between configuration failures and network outages

## Frequently Asked Questions

### How does FluidVoice handle API keys for different cloud providers?

API keys are stored in `SettingsStore.providerAPIKeys` as a dictionary keyed by provider ID. When `resolveProvider` identifies a built-in or custom cloud provider, it injects the corresponding key into the `ResolvedProvider` struct. For local endpoints (detected by IP range), the `Authorization` header is automatically omitted, allowing keyless access to local LLM servers like Ollama or LM Studio.

### Can DictationPostProcessingService work with local AI models?

Yes. The service supports two local pathways: **Apple Intelligence** (using `AppleIntelligenceProvider` for macOS 26+ FoundationModels) and **Private AI** (routing to `PrivateAIIntegrationService` for self-hosted or local-network LLMs). Both paths bypass the HTTP layer entirely, keeping inference on-device or within the local network.

### What happens if the configured cloud provider is unreachable?

The service propagates `URLSession` errors through the `LLMClient` to the caller. If the provider returns a successful HTTP status but empty content, the service throws `AIProcessingError.emptyResponse`. These errors surface in the UI as retryable failures, while the raw dictation transcript remains available as a fallback.

### How does the service support providers like Groq or Cohere that offer OpenAI-compatible endpoints?

The `OpenAICompatibleProvider` implementation in [`Sources/Fluid/Networking/OpenAICompatibleProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Networking/OpenAICompatibleProvider.swift) works with any provider implementing the standard chat completions schema. For Groq specifically, it automatically injects the `reasoning_effort` parameter when using "gpt-oss" models. Custom providers are saved with their specific `baseURL` values in `SettingsStore.savedProviders`, allowing the resolution logic to route requests correctly.