├── .gitignore ├── Sources └── LlamaCpp │ ├── Models │ ├── LlamaConfig.swift │ └── Llama.swift │ ├── main.swift │ └── CommandLineArguments.swift ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | /Users/vb/Documents/hf-projects/lui/Sources/LlamaCpp/Resources/ 10 | -------------------------------------------------------------------------------- /Sources/LlamaCpp/Models/LlamaConfig.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct LlamaConfig { 4 | public var hfRepo: String 5 | 6 | public init(hfRepo: String) { 7 | self.hfRepo = hfRepo 8 | } 9 | 10 | func toCommandLineArguments() -> [String] { 11 | return ["-hf", hfRepo] 12 | } 13 | } -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "lui", 8 | platforms: [ 9 | .macOS(.v13) 10 | ], 11 | products: [ 12 | .executable( 13 | name: "lui", 14 | targets: ["LlamaCpp"] 15 | ) 16 | ], 17 | targets: [ 18 | .executableTarget( 19 | name: "LlamaCpp", 20 | path: "Sources/LlamaCpp", 21 | resources: [ 22 | .copy("Resources") 23 | ] 24 | ) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Sources/LlamaCpp/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @main 4 | struct LlamaCLI { 5 | static func main() async throws { 6 | let args = CommandLineArguments.shared 7 | 8 | // Create configuration 9 | let config = LlamaConfig(hfRepo: args.hfRepo) 10 | 11 | // Create Llama instance 12 | let llama = Llama(config: config) 13 | 14 | do { 15 | // Generate text 16 | let response = try await llama.generate(prompt: "Hello, how are you?") 17 | print(response) 18 | } catch { 19 | print("Error: \(error)") 20 | exit(1) 21 | } 22 | 23 | // Clean up 24 | llama.stop() 25 | } 26 | } -------------------------------------------------------------------------------- /Sources/LlamaCpp/CommandLineArguments.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CommandLineArguments { 4 | static let shared = CommandLineArguments() 5 | 6 | let hfRepo: String 7 | 8 | static func printHelp() { 9 | print(""" 10 | Usage: lui -hf 11 | 12 | Required arguments: 13 | -hf, --huggingface REPO Hugging Face repository to use 14 | 15 | Example: 16 | lui -hf mistralai/Mistral-7B-v0.1 17 | """) 18 | } 19 | 20 | init() { 21 | // Parse command line arguments 22 | let arguments = CommandLine.arguments 23 | 24 | // Default values 25 | var hfRepo = "" 26 | 27 | // Parse arguments 28 | var i = 1 29 | while i < arguments.count { 30 | switch arguments[i] { 31 | case "-hf", "--huggingface": 32 | i += 1 33 | hfRepo = arguments[i] 34 | case "-h", "--help": 35 | Self.printHelp() 36 | exit(0) 37 | default: 38 | print("Unknown argument: \(arguments[i])") 39 | Self.printHelp() 40 | exit(1) 41 | } 42 | i += 1 43 | } 44 | 45 | // Validate required arguments 46 | if hfRepo.isEmpty { 47 | print("Error: Hugging Face repository is required (-hf or --huggingface)") 48 | Self.printHelp() 49 | exit(1) 50 | } 51 | 52 | self.hfRepo = hfRepo 53 | } 54 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lui - Llama.cpp Swift CLI 2 | 3 | A Swift command-line interface for the llama.cpp binary, providing a convenient way to use the model from the terminal. 4 | 5 | ## Prerequisites 6 | 7 | You need a llama.cpp binary. You can either: 8 | 9 | 1. Build it yourself: 10 | ```bash 11 | git clone https://github.com/ggerganov/llama.cpp.git 12 | cd llama.cpp 13 | make 14 | ``` 15 | 16 | 2. Or use a pre-built binary from a trusted source. 17 | 18 | ## Binary Placement 19 | 20 | The CLI will look for the llama.cpp binary in the following locations (in order): 21 | 1. `Resources/llama` in the CLI bundle (for bundled distribution) 22 | 2. `/usr/local/bin/llama` (system-wide installation) 23 | 3. `~/bin/llama` (user's home directory) 24 | 4. `./llama` (current directory) 25 | 26 | To bundle the binary with the CLI: 27 | 1. Place the llama.cpp binary in `Sources/LlamaCpp/Resources/llama` 28 | 2. Make sure it's executable: `chmod +x Sources/LlamaCpp/Resources/llama` 29 | 30 | ## Building the CLI 31 | 32 | ```bash 33 | swift build -c release 34 | ``` 35 | 36 | The binary will be available at `.build/release/lui` 37 | 38 | ## Usage 39 | 40 | Basic usage: 41 | ```bash 42 | lui -m /path/to/model.gguf --prompt "Your prompt here" 43 | ``` 44 | 45 | ### Command Line Arguments 46 | 47 | Required arguments: 48 | - `-m, --model PATH`: Path to the model file 49 | - `--prompt TEXT`: Prompt to generate from 50 | 51 | Optional arguments: 52 | - `-c, --ctx-size N`: Context size (default: 4096) 53 | - `-b, --batch-size N`: Batch size (default: 2048) 54 | - `-t, --threads N`: Number of threads (default: -1) 55 | - `--temp N`: Temperature (default: 0.8) 56 | - `--top-k N`: Top-k sampling (default: 40) 57 | - `--top-p N`: Top-p sampling (default: 0.9) 58 | - `--repeat-penalty N`: Repeat penalty (default: 1.0) 59 | - `-s, --seed N`: Random seed (default: -1) 60 | - `-ngl, --gpu-layers N`: Number of GPU layers (default: 0) 61 | - `-dev, --device DEV`: Device to use (default: cpu) 62 | - `--mlock`: Lock model in memory 63 | - `--no-mmap`: Disable memory mapping 64 | - `--verbose-prompt`: Print verbose prompt information 65 | - `-h, --help`: Show help message 66 | 67 | ### Examples 68 | 69 | 1. Basic text generation: 70 | ```bash 71 | lui -m model.gguf --prompt "Once upon a time" 72 | ``` 73 | 74 | 2. With custom parameters: 75 | ```bash 76 | lui -m model.gguf \ 77 | --prompt "Write a story about a robot" \ 78 | --temp 0.7 \ 79 | --top-k 40 \ 80 | --top-p 0.9 \ 81 | --ctx-size 2048 82 | ``` 83 | 84 | 3. Using GPU acceleration: 85 | ```bash 86 | lui -m model.gguf \ 87 | --prompt "Explain quantum computing" \ 88 | -ngl 32 \ 89 | -dev "cuda" 90 | ``` 91 | 92 | ## Notes 93 | 94 | - The CLI will automatically find the llama.cpp binary in one of the supported locations 95 | - The CLI supports all llama.cpp parameters through command-line arguments 96 | - Use `-h` or `--help` to see all available options -------------------------------------------------------------------------------- /Sources/LlamaCpp/Models/Llama.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class Llama { 4 | private let config: LlamaConfig 5 | private var process: Process? 6 | private var outputPipe: Pipe? 7 | private var inputPipe: Pipe? 8 | 9 | public init(config: LlamaConfig) { 10 | self.config = config 11 | } 12 | 13 | private func findLlamaBinary() throws -> URL { 14 | // List of possible locations to search for the binary 15 | let possiblePaths = [ 16 | // Bundled binary location (relative to the executable) 17 | Bundle.module.bundleURL.appendingPathComponent("Resources/build/bin/llama-server"), 18 | // System-wide installation 19 | URL(fileURLWithPath: "/usr/local/bin/llama-server"), 20 | // User's home directory 21 | FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("bin/llama-server"), 22 | // Current directory 23 | URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("llama-server") 24 | ] 25 | 26 | print("Bundle.module.bundleURL: \(Bundle.module.bundleURL)") 27 | print("Current directory: \(FileManager.default.currentDirectoryPath)") 28 | 29 | // Check each location 30 | for path in possiblePaths { 31 | print("Checking path: \(path.path)") 32 | if FileManager.default.isExecutableFile(atPath: path.path) { 33 | print("Found executable at: \(path.path)") 34 | return path 35 | } 36 | } 37 | 38 | throw LlamaError.binaryNotFound 39 | } 40 | 41 | public func generate(prompt: String) async throws -> String { 42 | let process = Process() 43 | self.process = process 44 | 45 | // Find and set up the process 46 | process.executableURL = try findLlamaBinary() 47 | 48 | // Set up pipes for input/output 49 | let outputPipe = Pipe() 50 | let inputPipe = Pipe() 51 | self.outputPipe = outputPipe 52 | self.inputPipe = inputPipe 53 | 54 | process.standardOutput = outputPipe 55 | process.standardInput = inputPipe 56 | process.standardError = outputPipe 57 | 58 | // Set up arguments for the server 59 | var arguments = ["--host", "127.0.0.1", "--port", "8080"] 60 | arguments.append(contentsOf: config.toCommandLineArguments()) 61 | process.arguments = arguments 62 | 63 | // Start the process 64 | try process.run() 65 | 66 | // Wait a moment for the server to start 67 | try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second 68 | 69 | // Create URL request to the server 70 | guard let url = URL(string: "http://127.0.0.1:8080/completion") else { 71 | throw LlamaError.invalidConfiguration 72 | } 73 | 74 | var request = URLRequest(url: url) 75 | request.httpMethod = "POST" 76 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 77 | 78 | // Create request body with default values 79 | let requestBody: [String: Any] = [ 80 | "prompt": prompt, 81 | "temperature": 0.8, 82 | "top_k": 40, 83 | "top_p": 0.9, 84 | "repeat_penalty": 1.0, 85 | "n_predict": 128, // Default prediction length 86 | "stop": ["", "User:", "Assistant:"] 87 | ] 88 | 89 | request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) 90 | 91 | // Make the request 92 | let (data, _) = try await URLSession.shared.data(for: request) 93 | 94 | // Parse the response 95 | guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], 96 | let content = json["content"] as? String else { 97 | throw LlamaError.invalidOutput 98 | } 99 | 100 | return content 101 | } 102 | 103 | public func stop() { 104 | process?.terminate() 105 | process = nil 106 | } 107 | } 108 | 109 | public enum LlamaError: Error { 110 | case invalidOutput 111 | case processError 112 | case invalidConfiguration 113 | case binaryNotFound 114 | } --------------------------------------------------------------------------------