initial commit

This commit is contained in:
Radek Davidek 2026-03-23 14:10:15 +01:00
commit 04d294c65b
14 changed files with 1909 additions and 0 deletions

5
.gitignore vendored Executable file
View File

@ -0,0 +1,5 @@
target
.classpath
.project
.vscode
.claude

6
config.properties Normal file
View File

@ -0,0 +1,6 @@
#Llama Runner Configuration
#Mon Mar 23 12:24:12 CET 2026
windowHeight=854
windowWidth=587
windowX=1973
windowY=546

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cz.kamma</groupId>
<artifactId>llama-runner</artifactId>
<name>Llama Runner</name>
<version>1.0-SNAPSHOT</version>
<description>GUI application for running llama-server with customizable parameters</description>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>cz.kamma.llamarunner.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

62
pom.xml Normal file
View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cz.kamma</groupId>
<artifactId>llama-runner</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Llama Runner</name>
<description>GUI application for running llama-server with customizable parameters</description>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>cz.kamma.llamarunner.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

97
profiles.json Normal file
View File

@ -0,0 +1,97 @@
{
"Nemotron Cascade Q8 160k": {
"host": "0.0.0.0",
"port": 3080,
"parallel": 1,
"threads": 99,
"flashAttention": true,
"kvUnified": true,
"cacheTypeK": "q8_0",
"cacheTypeV": "q8_0",
"temperature": 0.6,
"topP": 0.95,
"topK": 20,
"minP": 0.0,
"ctxSize": 160000,
"enableThinking": false,
"modelPath": "/home/kamma/models/Nemotron-Cascade-2-30B-A3B.Q8_0.gguf",
"chatTemplateKwargs": "{\"enable_thinking\": false}",
"ngl": -1
},
"Qwen3.5-q6k-180k": {
"host": "0.0.0.0",
"port": 3080,
"parallel": 1,
"threads": 99,
"flashAttention": true,
"kvUnified": true,
"cacheTypeK": "q8_0",
"cacheTypeV": "q8_0",
"temperature": 0.6,
"topP": 0.95,
"topK": 20,
"minP": 0.0,
"ctxSize": 180000,
"enableThinking": false,
"modelPath": "/home/kamma/models/Qwen3.5-35B-A3B-Q6_K.gguf",
"chatTemplateKwargs": "{\"enable_thinking\": false}",
"ngl": 999
},
"QwenCoderNext-160k": {
"host": "0.0.0.0",
"port": 3080,
"parallel": 1,
"threads": 99,
"flashAttention": true,
"kvUnified": true,
"cacheTypeK": "q8_0",
"cacheTypeV": "q8_0",
"temperature": 0.6,
"topP": 0.95,
"topK": 20,
"minP": 0.0,
"ctxSize": 160000,
"enableThinking": false,
"modelPath": "/home/kamma/models/Qwen3-Coder-Next-UD-Q2_K_XL.gguf",
"chatTemplateKwargs": "{\"enable_thinking\": false}",
"ngl": 999
},
"Nemotron Cascade 180k": {
"host": "0.0.0.0",
"port": 3080,
"parallel": 1,
"threads": 99,
"flashAttention": true,
"kvUnified": true,
"cacheTypeK": "q8_0",
"cacheTypeV": "q8_0",
"temperature": 0.6,
"topP": 0.95,
"topK": 20,
"minP": 0.0,
"ctxSize": 180000,
"enableThinking": false,
"modelPath": "/home/kamma/models/Nemotron-Cascade-2-30B-A3B.Q5_K_M.gguf",
"chatTemplateKwargs": "{\"enable_thinking\": false}",
"ngl": 999
},
"Qwen3.5 q6xl 160k": {
"host": "0.0.0.0",
"port": 3080,
"parallel": 1,
"threads": 99,
"flashAttention": true,
"kvUnified": true,
"cacheTypeK": "q8_0",
"cacheTypeV": "q8_0",
"temperature": 0.6,
"topP": 0.95,
"topK": 20,
"minP": 0.0,
"ctxSize": 160000,
"enableThinking": false,
"modelPath": "/home/kamma/models/Qwen3.5-35B-A3B-UD-Q6_K_XL.gguf",
"chatTemplateKwargs": "{\"enable_thinking\": false}",
"ngl": -1
}
}

View File

@ -0,0 +1,102 @@
package cz.kamma.llamarunner;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
/**
* Application configuration management for storing window size and position.
*/
public class AppConfig {
private static final int DEFAULT_WIDTH = 900;
private static final int DEFAULT_HEIGHT = 750;
private static final int DEFAULT_X = -1; // -1 means center on screen
private static final int DEFAULT_Y = -1;
private int windowWidth;
private int windowHeight;
private int windowX;
private int windowY;
private final ConfigLocation configLocation;
public AppConfig() {
this(new ConfigLocation());
}
public AppConfig(ConfigLocation configLocation) {
this.configLocation = configLocation;
windowWidth = DEFAULT_WIDTH;
windowHeight = DEFAULT_HEIGHT;
windowX = DEFAULT_X;
windowY = DEFAULT_Y;
load();
}
private void load() {
if (!configLocation.getConfigPropertiesFile().exists()) {
return;
}
Properties props = new Properties();
try (FileInputStream fis = new FileInputStream(configLocation.getConfigPropertiesFile())) {
props.load(fis);
windowWidth = Integer.parseInt(props.getProperty("windowWidth", String.valueOf(DEFAULT_WIDTH)));
windowHeight = Integer.parseInt(props.getProperty("windowHeight", String.valueOf(DEFAULT_HEIGHT)));
windowX = Integer.parseInt(props.getProperty("windowX", String.valueOf(DEFAULT_X)));
windowY = Integer.parseInt(props.getProperty("windowY", String.valueOf(DEFAULT_Y)));
} catch (IOException | NumberFormatException e) {
// Use default values on error
}
}
public void save() {
configLocation.ensureDirectoryExists();
Properties props = new Properties();
props.setProperty("windowWidth", String.valueOf(windowWidth));
props.setProperty("windowHeight", String.valueOf(windowHeight));
props.setProperty("windowX", String.valueOf(windowX));
props.setProperty("windowY", String.valueOf(windowY));
try (FileOutputStream fos = new FileOutputStream(configLocation.getConfigPropertiesFile())) {
props.store(fos, "Llama Runner Configuration");
} catch (IOException e) {
e.printStackTrace();
}
}
public int getWindowWidth() {
return windowWidth;
}
public void setWindowWidth(int windowWidth) {
this.windowWidth = windowWidth;
}
public int getWindowHeight() {
return windowHeight;
}
public void setWindowHeight(int windowHeight) {
this.windowHeight = windowHeight;
}
public int getWindowX() {
return windowX;
}
public void setWindowX(int windowX) {
this.windowX = windowX;
}
public int getWindowY() {
return windowY;
}
public void setWindowY(int windowY) {
this.windowY = windowY;
}
}

View File

@ -0,0 +1,74 @@
package cz.kamma.llamarunner;
import java.io.File;
/**
* Builds llama-server command lines from ModelConfig.
*/
public class CommandBuilder {
/**
* Builds the command string from a ModelConfig.
*
* @param config the model configuration
* @param modelsDirPath the path to the models directory
* @return the complete command string
*/
public String buildCommand(ModelConfig config, String modelsDirPath) {
StringBuilder cmd = new StringBuilder("llama-server");
cmd.append(" --host ").append(config.getHost());
cmd.append(" --port ").append(config.getPort());
cmd.append(" --parallel ").append(config.getParallel());
cmd.append(" -t ").append(config.getThreads());
cmd.append(" -fa ").append(config.isFlashAttention() ? "on" : "off");
cmd.append(" --temp ").append(config.getTemperature());
cmd.append(" --top-p ").append(config.getTopP());
cmd.append(" --top-k ").append(config.getTopK());
cmd.append(" --min-p ").append(config.getMinP());
if (config.isKvUnified()) {
cmd.append(" --kv-unified");
}
cmd.append(" --cache-type-k ").append(config.getCacheTypeK());
cmd.append(" --cache-type-v ").append(config.getCacheTypeV());
cmd.append(" --ctx-size ").append(config.getCtxSize());
if (!config.isFit()) {
cmd.append(" -ngl ").append(config.getNgl());
}
if (config.isFit()) {
cmd.append(" --fit on");
}
String modelPath = buildModelPath(config.getModelPath(), modelsDirPath);
cmd.append(" -m ").append(modelPath);
String kwargsText = config.getChatTemplateKwargs();
if (kwargsText != null && !kwargsText.trim().isEmpty()) {
cmd.append(" --chat-template-kwargs \"");
cmd.append(kwargsText.replace("\"", "\\\""));
cmd.append("\"");
}
return cmd.toString();
}
/**
* Builds the full model path.
*/
private String buildModelPath(String modelPath, String modelsDirPath) {
if (modelPath == null || modelPath.trim().isEmpty()) {
return "";
}
File modelFile = new File(modelPath);
if (modelFile.isAbsolute()) {
return modelPath;
}
return new File(modelsDirPath, modelPath).getAbsolutePath();
}
}

View File

@ -0,0 +1,48 @@
package cz.kamma.llamarunner;
import java.io.File;
/**
* Manages configuration file locations in the user's home directory.
*/
public class ConfigLocation {
private final File appDir;
private final File configPropertiesFile;
private final File profilesJsonFile;
public ConfigLocation() {
appDir = new File(".");
configPropertiesFile = new File(appDir, "config.properties");
profilesJsonFile = new File(appDir, "profiles.json");
}
/**
* Ensures the application directory exists.
*/
public void ensureDirectoryExists() {
if (!appDir.exists()) {
appDir.mkdirs();
}
}
public File getAppDir() {
return appDir;
}
public File getConfigPropertiesFile() {
return configPropertiesFile;
}
public File getProfilesJsonFile() {
return profilesJsonFile;
}
public String getConfigPropertiesFilePath() {
return configPropertiesFile.getAbsolutePath();
}
public String getProfilesJsonFilePath() {
return profilesJsonFile.getAbsolutePath();
}
}

View File

@ -0,0 +1,123 @@
package cz.kamma.llamarunner;
import java.io.File;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* Validation utilities for configuration inputs.
*/
public class ConfigValidation {
private static final Gson GSON = new Gson();
/**
* Validates numeric fields for valid ranges.
*
* @return a list of error messages (empty if valid)
*/
public static java.util.List<String> validateNumericInputs(
String host,
int port,
int parallel,
int threads,
double temperature,
double topP,
int topK,
double minP,
int ctxSize,
int ngl) {
java.util.List<String> errors = new java.util.ArrayList<>();
// Validate port
if (port < 1 || port > 65535) {
errors.add("Port must be between 1 and 65535");
}
// Validate parallel
if (parallel < 1) {
errors.add("Parallel must be at least 1");
}
// Validate threads
if (threads < 1) {
errors.add("Threads must be at least 1");
}
// Validate temperature
if (temperature < 0) {
errors.add("Temperature must be non-negative");
}
// Validate topP
if (topP < 0 || topP > 1) {
errors.add("Top P must be between 0 and 1");
}
// Validate topK
if (topK < 0) {
errors.add("Top K must be non-negative");
}
// Validate minP
if (minP < 0 || minP > 1) {
errors.add("Min P must be between 0 and 1");
}
// Validate ctxSize
if (ctxSize < 0) {
errors.add("Context size must be non-negative");
}
// Validate ngl
if (ngl < 0) {
errors.add("GPU layers must be non-negative");
}
return errors;
}
/**
* Validates JSON string for chatTemplateKwargs.
*
* @param json the JSON string to validate
* @return error message if invalid, null if valid
*/
public static String validateJsonKwargs(String json) {
if (json == null || json.trim().isEmpty()) {
return null;
}
try {
GSON.fromJson(json, Object.class);
return null;
} catch (JsonSyntaxException e) {
return "Invalid JSON format in kwargs";
}
}
/**
* Validates model path exists.
*
* @param modelPath the model file path
* @return error message if file doesn't exist, null if valid
*/
public static String validateModelPath(String modelPath) {
if (modelPath == null || modelPath.trim().isEmpty()) {
return "Model path cannot be empty";
}
File modelFile = new File(modelPath);
if (!modelFile.exists()) {
return "Model file does not exist: " + modelPath;
}
if (!modelFile.isFile()) {
return "Model path is not a file: " + modelPath;
}
return null;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,129 @@
package cz.kamma.llamarunner;
import java.io.Serializable;
/**
* Class for storing llama-server configuration.
*/
public class ModelConfig implements Serializable {
private static final long serialVersionUID = 1L;
private String host;
private int port;
private int parallel;
private int threads;
private boolean flashAttention;
private boolean kvUnified;
private String cacheTypeK;
private String cacheTypeV;
private double temperature;
private double topP;
private int topK;
private double minP;
private int ctxSize;
private boolean enableThinking;
private String modelPath;
private String chatTemplateKwargs;
private int ngl;
private boolean fit;
public ModelConfig() {
this.host = "0.0.0.0";
this.port = 3080;
this.parallel = 1;
this.threads = 99;
this.flashAttention = true;
this.kvUnified = true;
this.cacheTypeK = "bf16";
this.cacheTypeV = "bf16";
this.temperature = 0.6;
this.topP = 0.95;
this.topK = 20;
this.minP = 0.00;
this.ctxSize = 180000;
this.enableThinking = false;
this.modelPath = "";
this.chatTemplateKwargs = "";
this.ngl = 999;
this.fit = false;
}
// Getters and setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public int getParallel() { return parallel; }
public void setParallel(int parallel) { this.parallel = parallel; }
public int getThreads() { return threads; }
public void setThreads(int threads) { this.threads = threads; }
public boolean isFlashAttention() { return flashAttention; }
public void setFlashAttention(boolean flashAttention) { this.flashAttention = flashAttention; }
public boolean isKvUnified() { return kvUnified; }
public void setKvUnified(boolean kvUnified) { this.kvUnified = kvUnified; }
public String getCacheTypeK() { return cacheTypeK; }
public void setCacheTypeK(String cacheTypeK) { this.cacheTypeK = cacheTypeK; }
public String getCacheTypeV() { return cacheTypeV; }
public void setCacheTypeV(String cacheTypeV) { this.cacheTypeV = cacheTypeV; }
public double getTemperature() { return temperature; }
public void setTemperature(double temperature) { this.temperature = temperature; }
public double getTopP() { return topP; }
public void setTopP(double topP) { this.topP = topP; }
public int getTopK() { return topK; }
public void setTopK(int topK) { this.topK = topK; }
public double getMinP() { return minP; }
public void setMinP(double minP) { this.minP = minP; }
public int getCtxSize() { return ctxSize; }
public void setCtxSize(int ctxSize) { this.ctxSize = ctxSize; }
public boolean isEnableThinking() { return enableThinking; }
public void setEnableThinking(boolean enableThinking) { this.enableThinking = enableThinking; }
public String getModelPath() { return modelPath; }
public void setModelPath(String modelPath) { this.modelPath = modelPath; }
public String getChatTemplateKwargs() { return chatTemplateKwargs; }
public void setChatTemplateKwargs(String chatTemplateKwargs) { this.chatTemplateKwargs = chatTemplateKwargs; }
public int getNgl() { return ngl; }
public void setNgl(int ngl) { this.ngl = ngl; }
public boolean isFit() { return fit; }
public void setFit(boolean fit) { this.fit = fit; }
@Override
public String toString() {
return "ModelConfig{" +
"host='" + host + '\'' +
", port=" + port +
", parallel=" + parallel +
", threads=" + threads +
", flashAttention=" + flashAttention +
", kvUnified=" + kvUnified +
", cacheTypeK='" + cacheTypeK + '\'' +
", cacheTypeV='" + cacheTypeV + '\'' +
", temperature=" + temperature +
", topP=" + topP +
", topK=" + topK +
", minP=" + minP +
", ctxSize=" + ctxSize +
", enableThinking=" + enableThinking +
", modelPath='" + modelPath + '\'' +
", chatTemplateKwargs='" + chatTemplateKwargs + '\'' +
", ngl=" + ngl +
", fit=" + fit +
'}';
}
}

View File

@ -0,0 +1,137 @@
package cz.kamma.llamarunner;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
/**
* Profile management using JSON file in user's home directory.
*/
public class ProfileManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private final ConfigLocation configLocation;
public ProfileManager() {
this(new ConfigLocation());
}
public ProfileManager(ConfigLocation configLocation) {
this.configLocation = configLocation;
}
/**
* Saves profile to JSON file.
*/
public void saveProfile(String name, ModelConfig config) throws IOException {
configLocation.ensureDirectoryExists();
Map<String, ModelConfig> profiles = loadAllProfiles();
profiles.put(name, config);
try (FileWriter writer = new FileWriter(configLocation.getProfilesJsonFile())) {
GSON.toJson(profiles, writer);
}
}
/**
* Loads profile from JSON file.
*/
public ModelConfig loadProfile(String name) throws IOException {
Map<String, ModelConfig> profiles = loadAllProfiles();
ModelConfig config = profiles.get(name);
if (config == null) {
throw new IOException("Profile does not exist: " + name);
}
return config;
}
/**
* Deletes profile from JSON file.
*/
public void deleteProfile(String name) throws IOException {
Map<String, ModelConfig> profiles = loadAllProfiles();
if (!profiles.containsKey(name)) {
throw new IOException("Profile does not exist: " + name);
}
profiles.remove(name);
try (FileWriter writer = new FileWriter(configLocation.getProfilesJsonFile())) {
GSON.toJson(profiles, writer);
}
}
/**
* Returns the path to the profiles file.
*/
public String getProfilesFilePath() {
return configLocation.getProfilesJsonFilePath();
}
/**
* Returns the list of profile names.
*/
public List<String> listProfiles() throws IOException {
Map<String, ModelConfig> profiles = loadAllProfiles();
return profiles != null ? new ArrayList<>(profiles.keySet()) : new ArrayList<>();
}
/**
* Checks if profile exists.
*/
public boolean profileExists(String name) {
try {
Map<String, ModelConfig> profiles = loadAllProfiles();
return profiles.containsKey(name);
} catch (IOException e) {
return false;
}
}
/**
* Renames a profile from oldName to newName.
*/
public void renameProfile(String oldName, String newName) throws IOException {
Map<String, ModelConfig> profiles = loadAllProfiles();
if (!profiles.containsKey(oldName)) {
throw new IOException("Profile does not exist: " + oldName);
}
if (oldName.equals(newName)) {
throw new IOException("Old and new names must be different.");
}
if (profiles.containsKey(newName)) {
throw new IOException("Profile already exists: " + newName);
}
ModelConfig config = profiles.remove(oldName);
profiles.put(newName, config);
try (FileWriter writer = new FileWriter(configLocation.getProfilesJsonFile())) {
GSON.toJson(profiles, writer);
}
}
/**
* Loads all profiles from JSON file.
*/
private Map<String, ModelConfig> loadAllProfiles() throws IOException {
if (!configLocation.getProfilesJsonFile().exists()) {
return new HashMap<>();
}
try (FileReader reader = new FileReader(configLocation.getProfilesJsonFile())) {
java.lang.reflect.Type mapType = new TypeToken<Map<String, ModelConfig>>() {}.getType();
Map<String, ModelConfig> profiles = GSON.fromJson(reader, mapType);
return profiles != null ? profiles : new HashMap<>();
} catch (Exception e) {
return new HashMap<>();
}
}
}

View File

@ -0,0 +1,51 @@
package cz.kamma.llamarunner;
import java.util.regex.Pattern;
/**
* Validation utilities for profile names.
*/
public class ProfileValidator {
private static final int MAX_NAME_LENGTH = 100;
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
/**
* Validates a profile name.
*
* @param name the profile name to validate
* @return error message if invalid, null if valid
*/
public static String validateProfileName(String name) {
if (name == null || name.trim().isEmpty()) {
return "Profile name cannot be empty";
}
String trimmedName = name.trim();
if (trimmedName.length() > MAX_NAME_LENGTH) {
return "Profile name must be at most " + MAX_NAME_LENGTH + " characters";
}
if (!VALID_NAME_PATTERN.matcher(trimmedName).matches()) {
return "Profile name can only contain letters, numbers, underscores, and hyphens";
}
return null;
}
/**
* Checks if a profile name is unique among existing profiles.
*
* @param newName the new profile name
* @param existingProfiles list of existing profile names
* @return true if unique, false otherwise
*/
public static boolean isProfileNameUnique(String newName, java.util.List<String> existingProfiles) {
if (existingProfiles == null) {
return true;
}
return !existingProfiles.stream()
.anyMatch(name -> name.equalsIgnoreCase(newName));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB