Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions Sources/Lamp/LocalModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ struct LocalModelSpec: Identifiable, Hashable {
let sizeGB: Double
let minRAMGB: Int
let blurb: String
/// Tokens the model is given; most get Lamp's usual size, one made for long work more.
var contextSize: Int = LocalModels.contextSize
}

/// A model on disk that Lamp can run.
Expand Down Expand Up @@ -70,6 +72,12 @@ final class LocalModels: ObservableObject {
LocalModelSpec(id: "qwen3.6-35b-a3b", name: "Qwen3.6 35B-A3B", repo: "unsloth/Qwen3.6-35B-A3B-GGUF",
files: ["Qwen3.6-35B-A3B-UD-Q4_K_M.gguf", "mmproj-F16.gguf"], sizeGB: 23.0, minRAMGB: 48,
blurb: "Best local option. Fast for its size, and can see screenshots."),
LocalModelSpec(id: "flint", name: "Recursive Flint", repo: "RecursiveSI/flint-int4-gguf",
files: ["flint-int4-q4_1.gguf"], sizeGB: 26.0, minRAMGB: 48,
blurb: "Very fast and made for coding, but can't see screenshots.",
// Its own card runs it at 262K; at 128K its cache is about 3.4 GB, since all but 13 of its
// layers look back only 513 tokens. Lamp's prompt alone is over 20K, so 32K ran out in a few turns.
contextSize: 131_072),
LocalModelSpec(id: "qwen3.8-27b", name: "Qwen3.8 27B", repo: "unsloth/Qwen3.8-27B-GGUF",
files: ["Qwen3.8-27B-UD-Q4_K_M.gguf", "mmproj-F16.gguf"], sizeGB: 17.4, minRAMGB: 32,
blurb: "Strong and careful, but slower. Can see screenshots."),
Expand Down Expand Up @@ -320,6 +328,16 @@ final class LocalModels: ObservableObject {
writePiModels()
}

/// The context a model on disk is run with: its catalog entry's, else Lamp's usual size.
func contextSize(of model: InstalledModel) -> Int {
Self.catalog.first { spec in model.repo == spec.repo && model.files.contains { $0.lastPathComponent == spec.files[0] } }?
.contextSize ?? Self.contextSize
}

/// The router's presets: the context for each model. A size given to llama-server itself would win over
/// these, so it's only set here.
nonisolated static let presetsFile = Edition.supportFolder.appendingPathComponent("models.ini")

/// llama-server's models folder: one subfolder per GGUF model, symlinking the real files.
private func rebuildRouter() {
let fm = FileManager.default
Expand All @@ -333,6 +351,11 @@ final class LocalModels: ObservableObject {
withDestinationURL: file.resolvingSymlinksInPath())
}
}
var presets = "version = 1\n\n[*]\nc = \(Self.contextSize)\n"
for model in installed where model.format == .gguf && contextSize(of: model) != Self.contextSize {
presets += "\n[\(model.id)]\nc = \(contextSize(of: model))\n"
}
try? presets.write(to: Self.presetsFile, atomically: true, encoding: .utf8)
}

/// Deletes a GGUF model's files from the Hugging Face cache (blobs only if nothing else uses them).
Expand Down Expand Up @@ -564,8 +587,9 @@ final class LocalModels: ObservableObject {
rebuildRouter()
let process = Process()
process.executableURL = binary
process.arguments = ["--models-dir", Self.routerDir.path, "--jinja", "--host", "127.0.0.1", "--api-key", Self.apiKey,
"--port", String(Self.port), "-ngl", "999", "-c", String(Self.contextSize)]
process.arguments = ["--models-dir", Self.routerDir.path, "--models-preset", Self.presetsFile.path,
"--jinja", "--host", "127.0.0.1", "--api-key", Self.apiKey,
"--port", String(Self.port), "-ngl", "999"]
// The router runs each loaded model in a worker of its own, on a loopback port of its own, and leaves the key
// out of the worker's arguments; the worker inherits the environment, and reads the key from there.
var env = ProcessInfo.processInfo.environment
Expand Down Expand Up @@ -646,7 +670,7 @@ final class LocalModels: ObservableObject {
if !gguf.isEmpty {
providers[Builder.localProvider] = [
"baseUrl": "\(Self.baseURL)/v1", "api": "openai-completions", "apiKey": Self.apiKey,
"models": gguf.map { ["id": $0.id, "contextWindow": Self.contextSize,
"models": gguf.map { ["id": $0.id, "contextWindow": contextSize(of: $0),
"input": $0.vision ? ["text", "image"] : ["text"]] as [String: Any] },
]
}
Expand Down
6 changes: 3 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ PI_TARBALL="vendor/pi-$PI_VERSION-darwin-$PI_ARCH.tar.gz"
# llama.cpp's server runs downloaded models on this Mac (MIT, github.com/ggml-org/llama.cpp). Lamp doesn't carry it:
# it's signed here and zipped as an asset of the release (below), which Lamp fetches the first time a local model is
# downloaded or picked (LlamaInstall.swift).
LLAMA_BUILD="b11346"
LLAMA_BUILD="b11814"
case "$(uname -m)" in
arm64) LLAMA_ARCH="arm64"; LLAMA_SHA256="25cefd22cf708a14eacea623327a2320fa07493a29b4c37bf3376623de581553" ;;
x86_64) LLAMA_ARCH="x64"; LLAMA_SHA256="1f91f85611d1573c53409c99d96d43451cd89e928ce1882b6739fd1cf7befaf2" ;;
arm64) LLAMA_ARCH="arm64"; LLAMA_SHA256="698d16c10104ad5212fdd9cb084e7076b3ab4255c68433ee8ed70f3d041432cb" ;;
x86_64) LLAMA_ARCH="x64"; LLAMA_SHA256="cb38196573bcf57c441523f15da3129ef162f202fdacf7c14d7b41c2156cc858" ;;
esac
LLAMA_TARBALL="vendor/llama-$LLAMA_BUILD-bin-macos-$LLAMA_ARCH.tar.gz"
if [ ! -f "$LLAMA_TARBALL" ] || ! echo "$LLAMA_SHA256 $LLAMA_TARBALL" | shasum -a 256 -c - >/dev/null 2>&1; then
Expand Down
Loading