Architecture
Understand Conduit's protocol hierarchy, type system, and design patterns.
Overview
Conduit is built around a set of protocols that define provider capabilities. Every provider is an actor for thread-safe concurrent use, and all public types are Sendable. This architecture lets you write provider-agnostic app code and swap providers by changing a single initializer.
Protocol Hierarchy
All providers conform to one or more of these protocols:
TextGenerator— Text generation with streaming and non-streaming methods. Every provider implements this.EmbeddingGenerator— Vector embeddings for semantic search and similarity.Transcriber— Audio-to-text transcription.ImageGenerator— Text-to-image generation.TokenCounter— Token counting, encoding, and decoding.AIProvider— Actor-based umbrella protocol with availability checks and cancellation.
TextGenerator
The core protocol. All providers implement it.
public protocol TextGenerator: Sendable {
associatedtype ModelID: ModelIdentifying
func generate(_ prompt: String, model: ModelID, config: GenerateConfig) async throws -> String
func generate(messages: [Message], model: ModelID, config: GenerateConfig) async throws -> GenerationResult
func stream(_ prompt: String, model: ModelID, config: GenerateConfig) -> AsyncThrowingStream<String, Error>
func streamWithMetadata(messages: [Message], model: ModelID, config: GenerateConfig) -> AsyncThrowingStream<GenerationChunk, Error>
}Write generic functions against TextGenerator to support any provider:
func summarize<P: TextGenerator>(with provider: P, model: P.ModelID, text: String) async throws -> String {
try await provider.generate("Summarize: \(text)", model: model, config: .default.maxTokens(200))
}Actor-Based Providers
Every provider is an actor, ensuring thread-safe concurrent use:
public actor AnthropicProvider: AIProvider, TextGenerator { ... }
public actor OpenAIProvider: AIProvider, TextGenerator, EmbeddingGenerator, TokenCounter, ImageGenerator { ... }
public actor MLXProvider: AIProvider, TextGenerator, TokenCounter { ... }You can call providers from any task or actor without data races.
Model Identifiers
Conduit uses type-safe model identifiers. Each provider defines its own ModelID type:
AnthropicModelID—.claudeOpus45,.claudeSonnet45,.claude35Sonnet,.claude3HaikuOpenAIModelID—.gpt4o,.gpt4oMini,.o1,.textEmbedding3Small,.dallE3ModelIdentifier—.mlx("repo/model"),.huggingFace("repo/model"),.foundationModels
Provider-specific model IDs have static constants for common models:
// Anthropic
let response = try await anthropic.generate("Hello", model: .claudeSonnet45)
// OpenAI
let response = try await openai.generate("Hello", model: .gpt4o)
// MLX
let response = try await mlx.generate("Hello", model: .llama3_2_1b)
// HuggingFace — any model by repository name
let response = try await hf.generate("Hello", model: .huggingFace("meta-llama/Llama-3.1-70B-Instruct"))GenerateConfig
GenerateConfig controls generation behavior with a fluent API:
// Presets
let config = GenerateConfig.default // temperature: 0.7, topP: 0.9
let config = GenerateConfig.creative // temperature: 1.0, topP: 0.95
let config = GenerateConfig.precise // temperature: 0.3, topP: 0.8
let config = GenerateConfig.code // temperature: 0.2, topP: 0.9
// Fluent chaining
let config = GenerateConfig.default
.temperature(0.8)
.maxTokens(500)
.topP(0.9)
.stopSequences(["END"])Messages and MessageBuilder
The Message type represents conversation turns. Use the Messages result builder for ergonomic construction:
let messages = Messages {
Message.system("You are a Swift expert.")
Message.user("What are actors?")
}
let result = try await provider.generate(
messages: messages,
model: .claudeSonnet45,
config: .default
)The builder supports conditionals and loops:
let messages = Messages {
Message.system("You are a helpful assistant.")
if includeContext {
Message.user("Context: \(context)")
}
for question in questions {
Message.user(question)
}
}Multimodal content uses ContentPart:
let messages = Messages {
Message.user([
.text("What's in this image?"),
.image(base64Data: imageData, mimeType: "image/jpeg")
])
}Conditional Compilation Traits
Providers are gated behind Swift package traits. Guard new provider code with the appropriate flag:
#if CONDUIT_TRAIT_ANTHROPIC
let provider = AnthropicProvider(apiKey: "...")
#endif
#if CONDUIT_TRAIT_MLX
let provider = MLXProvider()
#endifSome providers also use #if canImport(...) for platform detection:
#if canImport(FoundationModels)
let provider = FoundationModelsProvider()
#endif