07067ca6c5
- Custom Keyboard Extension with push-to-talk UI - iOS 26 SpeechAnalyzer + DictationTranscriber (iOS 18 SF fallback) - OpenAI-compatible LLM client (4 built-in providers + custom) - 3-page onboarding flow + provider config UI - App Group shared storage for cross-process config - 8s LLM timeout with raw-transcript fallback - App Store privacy manifests for both targets - SwiftLint + XcodeGen + GitHub Actions CI - Unit tests (4/4 passing)
85 lines
3.0 KiB
Swift
85 lines
3.0 KiB
Swift
// ProviderConfig.swift
|
|
// OSGKeyboard · Shared
|
|
//
|
|
// User's LLM configuration. Persisted in App Group UserDefaults so both
|
|
// the main app and keyboard extension read the same values.
|
|
|
|
import Foundation
|
|
import Combine
|
|
|
|
public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
|
public static let shared = ProviderConfig()
|
|
|
|
// Storage keys
|
|
private enum Key {
|
|
static let providerId = "config.providerId"
|
|
static let baseURL = "config.baseURL"
|
|
static let apiKey = "config.apiKey"
|
|
static let model = "config.model"
|
|
static let systemPrompt = "config.systemPrompt"
|
|
}
|
|
|
|
@Published public var providerId: String {
|
|
didSet { defaults.set(providerId, forKey: Key.providerId) }
|
|
}
|
|
|
|
@Published public var baseURL: String {
|
|
didSet { defaults.set(baseURL, forKey: Key.baseURL) }
|
|
}
|
|
|
|
@Published public var apiKey: String {
|
|
didSet { defaults.set(apiKey, forKey: Key.apiKey) }
|
|
}
|
|
|
|
@Published public var model: String {
|
|
didSet { defaults.set(model, forKey: Key.model) }
|
|
}
|
|
|
|
@Published public var systemPrompt: String {
|
|
didSet { defaults.set(systemPrompt, forKey: Key.systemPrompt) }
|
|
}
|
|
|
|
public var isConfigured: Bool {
|
|
!baseURL.isEmpty && !apiKey.isEmpty && !model.isEmpty
|
|
}
|
|
|
|
public let defaultSystemPrompt = """
|
|
You are a voice-input polishing assistant. The user has spoken informally; rewrite their dictation as clean written text:
|
|
1) Preserve the user's original intent and meaning; do not invent facts.
|
|
2) Add proper punctuation, capitalization, and paragraph breaks.
|
|
3) When the user enumerates items ("first ... second ... third"), output a markdown list.
|
|
4) Keep the output concise — do not exceed 1.5x the spoken length.
|
|
5) Output in the same language as the input.
|
|
"""
|
|
|
|
private let defaults: UserDefaults
|
|
|
|
public init(defaults: UserDefaults = AppGroup.defaults) {
|
|
self.defaults = defaults
|
|
self.providerId = defaults.string(forKey: Key.providerId) ?? "openai"
|
|
self.baseURL = defaults.string(forKey: Key.baseURL) ?? LLMProvider.provider(id: "openai").defaultBaseURL
|
|
self.apiKey = defaults.string(forKey: Key.apiKey) ?? ""
|
|
self.model = defaults.string(forKey: Key.model) ?? LLMProvider.provider(id: "openai").defaultModel
|
|
self.systemPrompt = defaults.string(forKey: Key.systemPrompt) ?? defaultSystemPrompt
|
|
}
|
|
|
|
public func apply(preset: LLMProvider) {
|
|
providerId = preset.id
|
|
if !preset.defaultBaseURL.isEmpty {
|
|
baseURL = preset.defaultBaseURL
|
|
}
|
|
if !preset.defaultModel.isEmpty {
|
|
model = preset.defaultModel
|
|
}
|
|
}
|
|
|
|
public func reset() {
|
|
providerId = "openai"
|
|
let preset = LLMProvider.provider(id: "openai")
|
|
baseURL = preset.defaultBaseURL
|
|
apiKey = ""
|
|
model = preset.defaultModel
|
|
systemPrompt = defaultSystemPrompt
|
|
}
|
|
}
|