A unified Swift package for connecting to various Large Language Model (LLM) backend services with a focus on character cards and AI interactions. SwiftLLMSDK provides a clean, type-safe interface for working with different LLM providers while maintaining character card compatibility.
- đź”— Unified API Interface: Single interface for multiple LLM providers
- 🤖 Character Card Support: Built-in support for character cards with system prompts, personalities, and scenarios
- 📥 ChubAI Import: Direct import of character cards from Chub.ai and CharacterHub.org
- ⚡ Async/Await: Modern Swift concurrency with async/await
- 🛡️ Type Safety: Strong typing with Result types for error handling
- đź”§ Flexible Configuration: Extensive customization options for model parameters
- Purpose: Access to multiple LLM models through a single API
- Features: Model listing, API key validation, chat completions
- Authentication: API key required
- Purpose: Local LLM inference server
- Features: Direct HTTP connection to local instances
- Authentication: No API key required (local connection)
- iOS 16.0+ / macOS 13.0+
- Swift 6.0+
Add SwiftLLMSDK to your project using Xcode:
- Go to File → Add Package Dependencies
- Enter the repository URL:
https://github.com/yourusername/SwiftLLMSDK - Click Add Package
Or add it to your Package.swift file:
dependencies: [
.package(url: "https://github.com/yourusername/SwiftLLMSDK", from: "1.0.0")
]import SwiftLLMSDK
// Initialize OpenRouter API
let openRouterAPI = OpenRouterAPI(
selectedModel: "openai/gpt-4",
apiKey: "your-api-key-here"
)
// Create API manager
let apiManager = APIManager(forService: openRouterAPI)
// Test connection
let connectionResult = await apiManager.connect()
switch connectionResult {
case .success(let keyLabel):
print("Connected successfully: \(keyLabel)")
case .failure(let error):
print("Connection failed: \(error)")
}
// Send a message (builder-based API)
let requestBuilder = OpenRouterRequestBuilder(
model: "openai/gpt-4",
messages: [
RequestBodyMessages(role: .user, message: "Hello, how are you?")
]
)
let response = await apiManager.sendMessage(builder: requestBuilder)
switch response {
case .success(let modelResponse):
print("Response: \(modelResponse.text ?? "No response")")
case .failure(let error):
print("Error: \(error)")
}import SwiftLLMSDK
import SwiftUI
let openRouterAPI = OpenRouterAPI(
selectedModel: "openai/gpt-4o-mini",
apiKey: "your-api-key-here"
)
let apiManager = APIManager(forService: openRouterAPI)
let builder = OpenRouterRequestBuilder(
model: "openai/gpt-4o-mini",
messages: [
RequestBodyMessages(role: .user, message: "Stream this response?")
],
stream: true
)
// SwiftUI example
struct ChatView: View {
@State private var assistantText = ""
var body: some View {
ScrollView {
Text(assistantText)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
.task {
for await result in apiManager.streamMessage(builder: builder) {
switch result {
case .success(let model):
await MainActor.run { assistantText = model.text ?? "" }
case .failure(let error):
print("stream error: \(error)")
}
}
}
}
}NOTE: Streaming with koboldCPP uses the same manager method, simply pass the correct request builder.
import SwiftLLMSDK
// Initialize KoboldCPP API (local server)
let koboldAPI = KoboldAPI(
host: "localhost",
port: 5001
)
// Create API manager
let apiManager = APIManager(forService: koboldAPI)
// Test connection
let connectionResult = await apiManager.connect()
switch connectionResult {
case .success(let modelName):
print("Connected to model: \(modelName)")
case .failure(let error):
print("Connection failed: \(error)")
}
// Send a prompt (builder-based API)
let requestBuilder = KoboldRequestBuilder(
prompt: "Once upon a time",
maxLength: 100,
temperature: 0.8
)
let response = await apiManager.sendMessage(builder: requestBuilder)
switch response {
case .success(let modelResponse):
print("Generated text: \(modelResponse.text ?? "No response")")
case .failure(let error):
print("Error: \(error)")
}import SwiftLLMSDK
// Access raw openrouter response
if let openRouterResponse: OpenRouterAPIResponse = modelResponse.getRawResponse() {
print("Model used: \(openRouterResponse.model ?? "Unknown")")
}
// Access raw kobold API response
if let koboldAPIResponse: KoboldAPIResponse = modelResponse.getRawResponse() {
print("Kobold response: \(koboldAPIResponse.results.first?.text ?? "Unknown")")
}import SwiftLLMSDK
// Set up character information
let requestBuilder = OpenRouterRequestBuilder(
model: "openai/gpt-4",
messages: [
RequestBodyMessages(role: .user, message: "Hello there!")
],
systemPromptTemplate: nil,
characterDescription: "A friendly AI assistant who loves to help with coding questions.",
characterPersonality: "Enthusiastic, patient, and knowledgeable about programming.",
characterScenario: "You are helping a developer learn Swift programming."
)
let response = await apiManager.sendMessage(builder: requestBuilder)import SwiftLLMSDK
// For KoboldCPP, character info goes into memory and prompt
let requestBuilder = KoboldRequestBuilder(
prompt: "User: Hello there!\nAssistant:",
memory: "You are a friendly AI assistant who loves to help with coding questions. You are enthusiastic, patient, and knowledgeable about programming.",
maxLength: 150,
temperature: 0.7,
stopSequence: ["\nUser:", "\nAssistant:"]
)
let response = await apiManager.sendMessage(builder: requestBuilder)Import character cards directly from Chub.ai or CharacterHub.org:
import SwiftLLMSDK
// Initialize the importer
let chubImporter = ChubImporter(urlSession: URLSession.shared)
// Import a character card from URL
let characterURL = URL(string: "https://chub.ai/characters/author/character-name")!
do {
let result = try await chubImporter.getCardViaURL(characterURL)
switch result {
case .success(let characterCard):
print("Character imported: \(characterCard.data?.name ?? "Unknown")")
print("Description: \(characterCard.data?.description ?? "No description")")
// Use the character card with your LLM
let requestBuilder = RequestBodyBuilder(
selectedModel: "openai/gpt-4",
messages: [
RequestBodyMessages(role: .user, message: "Hello!")
],
characterDescription: characterCard.data?.description,
characterPersonality: characterCard.data?.personality,
characterScenario: characterCard.data?.scenario
)
case .failure(let error):
print("Import failed: \(error)")
}
} catch {
print("Import error: \(error)")
}let modelsResult = await apiManager.getAvailableModels()
switch modelsResult {
case .success(let models):
for model in models {
print("Model: \(model.name) - ID: \(model.id)")
print("Context Length: \(model.contextLength ?? 0)")
print("Description: \(model.description)")
}
case .failure(let error):
print("Failed to fetch models: \(error)")
}let requestBuilder = OpenRouterRequestBuilder(
model: "anthropic/claude-3-haiku",
messages: [
RequestBodyMessages(role: .system, message: "You are a helpful assistant."),
RequestBodyMessages(role: .user, message: "Explain quantum computing")
],
maxTokens: 500,
temperature: 0.7,
topP: 0.9,
topK: 40,
stop: ["\n\nHuman:", "\n\nAssistant:"],
frequencyPenalty: 0.1,
presencePenalty: 0.1
)let response = await apiManager.sendMessage(builder: requestBuilder)
switch response {
case .success(let modelResponse):
print("Success!")
print("Response: \(modelResponse.text ?? "")")
print("Tokens used: \(modelResponse.responseTokens ?? 0)")
// Access raw response if needed
if let openRouterResponse: OpenRouterAPIResponse = modelResponse.getRawResponse() {
print("Model used: \(openRouterResponse.model ?? "Unknown")")
}
case .failure(let error):
switch error {
case .invalidURL:
print("Invalid URL configuration")
case .serverError(let code):
print("Server error with code: \(code)")
case .timeout:
print("Request timed out")
case .decodingError:
print("Failed to decode response")
default:
print("Other error: \(error.localizedDescription)")
}
}Generic manager for handling API calls to different LLM services.
Methods:
sendMessage(builder: OpenRouterRequestBuilder) async -> Result<ModelResponse, APIError>sendMessage(builder: KoboldRequestBuilder) async -> Result<ModelResponse, APIError>streamMessage(builder: OpenRouterRequestBuilder) -> AsyncStream<Result<ModelResponse, APIError>>streamMessage(builder: KoboldRequestBuilder) -> AsyncStream<Result<ModelResponse, APIError>>connect() async -> Result<String, APIError>(OpenRouter/Kobold specific)getAvailableModels() async -> Result<[OpenRouterModel], APIError>(OpenRouter only)
OpenRouterRequestBuilder: Build chat-completion requests for OpenRouter.- Fields include
model,messages,stop,temperature,topP,minP,topA,topK,maxTokens,repetitionPenalty,frequencyPenalty,presencePenalty,stream, and optional system/character fields.
- Fields include
KoboldRequestBuilder: Build text-generation requests for KoboldCPP.- Fields include
prompt,memory,maxContextLength,maxLength,temperature,tfs,topA,topK,topP,minP,typical,repetitionPenalty,repetitionRange,repetitionSlope,stopSequence,trimStop,samplerOrder,promptTemplate.
- Fields include
Compatibility: The legacy RequestBodyBuilder remains available but new code should prefer provider-specific builders.
Unified response structure containing:
text: String?- Generated text responserole: String?- Response role (usually "assistant")responseTokens: Int?- Tokens used in responsepromptTokens: Int?- Tokens used in promptstreaming: Bool? - If the response is currently streaming datarawResponse: Codable- Original API response
Note: Streaming uses AsyncStream<Result<ModelResponse, APIError>>. Each emission contains the accumulated text so far; the last emission is the final content.
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
This project is licensed under the MIT License - see the LICENSE file for details.