How DictationPostProcessingService Integrates with Cloud AI Providers in FluidVoice

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 with the process(_:dictationSlot:) method:

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:

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:

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, 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

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:

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:

{
  "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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →