LLMClient Architecture and Multi-Provider Support in FluidVoice
The LLMClient is a Swift-based communication layer that abstracts LLM interactions through a unified Config-driven interface, automatically routing requests to provider-specific endpoints while handling streaming responses, tool calls, and thinking tokens.
The LLMClient architecture in the altic-dev/FluidVoice repository provides a single entry point for all Large Language Model interactions. It eliminates vendor lock-in by separating transport logic from provider-specific parsing, allowing the app to communicate with OpenAI, Ollama, DeepSeek, or private servers without changing calling code.
Core Architecture Components
The LLMClient implementation in Sources/Fluid/Services/LLMClient.swift is built around several specialized structures that handle the complete request lifecycle.
Unified Error Handling with LLMError
All failures route through LLMError, a comprehensive enum covering URL validation issues, HTTP status codes, JSON encoding errors, timeout conditions, and custom validation failures. This ensures consistent error propagation regardless of which provider returns the failure.
Config Struct for Request Parameters
The Config struct acts as a declarative blueprint for every LLM request. It encapsulates:
- messages: The conversation history array
- model: Provider model identifier (e.g.,
gpt-5-mini,deepseek-coder) - baseURL: Endpoint root (e.g.,
https://api.openai.com/v1orhttp://localhost:11434) - apiKey: Authentication token
- streaming: Boolean flag for SSE vs. synchronous responses
- tools: Optional function definitions for tool calling
- temperature and maxTokens: Standard inference controls
- extraParameters: Provider-specific extensions injected by
ThinkingParserFactory
Smart Endpoint Selection
The shouldUseResponsesAPI(for:baseURL:) method automatically detects when to use OpenAI's newer /responses endpoint instead of the standard /chat/completions path. This logic triggers for reasoning models (o1/o3/gpt-5 series) and certain private API implementations, ensuring compatibility without manual configuration.
Response Processing Pathways
The client maintains three distinct processing pipelines in LLMClient.swift:
- processStreaming: Handles Server-Sent Events (SSE) from Chat Completions endpoints, accumulating partial deltas
- processResponsesStreaming: Parses the alternative Responses API format
- processNonStreaming: Manages synchronous JSON responses
Each pathway extracts a standardized Response object containing optional thinking content, main content text, and parsed tool calls.
Resilient Retry Logic
Transient network failures trigger executeWithRetry(request:config:), which implements exponential backoff for DNS failures, timeouts, and temporary HTTP errors. This runs automatically before surfacing permanent failures to the caller.
Multi-Provider Support Strategy
FluidVoice achieves provider agnosticism through runtime inspection of the base URL and model name rather than hardcoded vendor logic.
ThinkingParserFactory for Model Detection
Located in Sources/Fluid/Services/ThinkingParsers.swift, ThinkingParserFactory inspects the model string to determine both the parsing strategy and extra parameter injection:
- StandardThinkingParser: Handles models wrapping thinking content in
<thinking>tags - NemoThinkingParser: Detects
nemotronornemomodels to injectenable_thinking=true - SeparateFieldThinkingParser: Manages OpenAI o1/o3/gpt-5 and DeepSeek models that expose
reasoningorthoughtas distinct JSON fields
When SettingsStore.shared.isReasoningModel(model) returns true, the factory automatically appends provider-specific flags like enable_reasoning=true for DeepSeek R1 via getExtraParameters.
Dynamic Request Construction
The buildRequest(_:) method in LLMClient.swift assembles the final URLRequest by:
- Selecting the correct endpoint path
- Applying authorization headers
- Merging base Config parameters with factory-generated extras
- Encoding the JSON body with proper Swift Date formatting
This means switching from OpenAI to a local Ollama instance requires only changing the baseURL and model strings in Config—no other code modifications.
Implementation Examples
Basic OpenAI API Call
let config = LLMClient.Config(
messages: [["role": "user", "content": "Summarize this text."]],
model: "gpt-5-mini",
baseURL: "https://api.openai.com/v1",
apiKey: "<YOUR-KEY>",
streaming: true,
tools: [],
temperature: 0.7,
maxTokens: 512,
extraParameters: [:]
)
Task {
do {
let response = try await LLMClient.shared.call(config)
print("Thinking:", response.thinking ?? "none")
print("Answer:", response.content)
} catch let error as LLMError {
print("Request failed:", error.localizedDescription)
}
}
Local Ollama Server Configuration
let localConfig = LLMClient.Config(
messages: [["role": "user", "content": "Translate to French."]],
model: "deepseek-coder",
baseURL: "http://localhost:11434",
apiKey: "", // No authentication required for local instances
streaming: true,
tools: [],
temperature: nil,
maxTokens: 256,
extraParameters: [:]
)
Task {
let response = try await LLMClient.shared.call(localConfig)
print(response.content) // "Traduisez..."
}
Tool-Enabled Function Calling
let weatherTool: [[String: Any]] = [
[
"type": "function",
"function": [
"name": "get_weather",
"description": "Retrieve current weather for a location.",
"parameters": [
"type": "object",
"properties": [
"location": ["type": "string", "description": "City name"]
],
"required": ["location"]
]
]
]
]
let toolConfig = LLMClient.Config(
messages: [["role": "user", "content": "What's the weather in Tokyo?"]],
model: "gpt-5-mini",
baseURL: "https://api.openai.com/v1",
apiKey: "<YOUR-KEY>",
streaming: true,
tools: weatherTool,
temperature: 0.0,
maxTokens: nil,
extraParameters: [:]
)
Task {
let response = try await LLMClient.shared.call(toolConfig)
if let toolCall = response.toolCalls.first {
print("Function:", toolCall.name)
print("Arguments:", toolCall.arguments)
}
}
Extending the Architecture
Adding support for new providers requires minimal changes:
- Update
ThinkingParserFactory: Add model string detection logic (e.g.,modelLower.contains("newvendor")) and return the appropriate parser - Define Extra Parameters: Implement
getExtraParametersto inject provider-specific flags (e.g.,top_padjustments or reasoning toggles) - Update
SettingsStore: ExtendisReasoningModel(_:)if the new provider uses distinct reasoning logic
No modifications are needed to LLMClient.swift or consuming view models, maintaining clean separation of concerns.
Summary
- LLMClient provides a single, async-await interface for all LLM operations in
Sources/Fluid/Services/LLMClient.swift - Config structs encapsulate all request parameters including provider-specific extras generated by ThinkingParserFactory
- Multi-provider support works through runtime model inspection and configurable base URLs, supporting OpenAI, Ollama, DeepSeek, and custom endpoints without code changes
- Automatic endpoint detection via
shouldUseResponsesAPIhandles both Chat Completions and Responses API formats - Standardized Response objects normalize thinking tokens, content, and tool calls across all providers
Frequently Asked Questions
How does LLMClient handle different authentication schemes between providers?
LLMClient accepts the API key directly in the Config struct and applies standard Bearer token authentication in buildRequest(_:). For providers requiring different schemes (like query parameters or custom headers), you can extend the request builder logic or pass additional headers through the extraParameters dictionary. Local servers like Ollama simply receive an empty string for apiKey.
What triggers the use of the Responses API instead of Chat Completions?
The shouldUseResponsesAPI(for:baseURL:) method checks if the model name indicates a reasoning model (o1, o3, gpt-5 series) or if the base URL matches known private API patterns. This automatic selection ensures reasoning models receive the correct endpoint without manual configuration, while standard models continue using the classic /chat/completions path.
How are "thinking" tokens extracted from different model formats?
ThinkingParserFactory creates specialized parsers based on model name detection. Standard models use regex to extract content between <thinking> tags, while DeepSeek and OpenAI reasoning models parse separate JSON fields (reasoning, thought). Nemotron models receive special handling with enable_thinking parameters injected automatically.
Can LLMClient stream responses from local providers like Ollama?
Yes, streaming works identically for local and remote providers as long as the endpoint supports Server-Sent Events. Set streaming: true in Config, and processStreaming will handle the SSE parsing regardless of whether the baseURL points to OpenAI or localhost:11434. The client manages the partial content accumulation and completion detection automatically.
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 →