import Foundation import JavaScriptCore enum ModelCatalogLoader { static let defaultPath: String = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path private static let logger = Logger(subsystem: "com.clawdbot", category: "models") static func load(from path: String) async throws -> [ModelChoice] { let expanded = (path as NSString).expandingTildeInPath self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: expanded).lastPathComponent)") let source = try String(contentsOfFile: expanded, encoding: .utf8) let sanitized = self.sanitize(source: source) let ctx = JSContext() ctx?.exceptionHandler = { _, exception in if let exception { self.logger.warning("model catalog JS exception: \(exception)") } } ctx?.evaluateScript(sanitized) guard let rawModels = ctx?.objectForKeyedSubscript("MODELS")?.toDictionary() as? [String: Any] else { self.logger.error("model catalog parse failed: MODELS missing") throw NSError( domain: "ModelCatalogLoader", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to parse models.generated.ts"]) } var choices: [ModelChoice] = [] for (provider, value) in rawModels { guard let models = value as? [String: Any] else { continue } for (id, payload) in models { guard let dict = payload as? [String: Any] else { continue } let name = dict["name"] as? String ?? id let ctxWindow = dict["contextWindow"] as? Int choices.append(ModelChoice(id: id, name: name, provider: provider, contextWindow: ctxWindow)) } } let sorted = choices.sorted { lhs, rhs in if lhs.provider == rhs.provider { return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending } return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending } self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)") return sorted } private static func sanitize(source: String) -> String { guard let exportRange = source.range(of: "export const MODELS"), let firstBrace = source[exportRange.upperBound...].firstIndex(of: "{"), let lastBrace = source.lastIndex(of: "}") else { return "var MODELS = {}" } var body = String(source[firstBrace...lastBrace]) body = body.replacingOccurrences( of: #"(?m)\bsatisfies\s+[^,}\n]+"#, with: "", options: .regularExpression) body = body.replacingOccurrences( of: #"(?m)\bas\s+[^;,\n]+"#, with: "", options: .regularExpression) return "var MODELS = \(body);" } }