diff --git a/config.properties b/config.properties index 5686d48..84b526f 100644 --- a/config.properties +++ b/config.properties @@ -1,5 +1,5 @@ #Llama Runner Configuration -#Tue May 12 20:14:02 CEST 2026 +#Tue May 12 20:45:12 CEST 2026 lastProfile=Qwen3.6-35B-A3B-UD-Q5_K_XL-FULL windowHeight=1271 windowWidth=665 diff --git a/profiles.json b/profiles.json index 656198d..7a8eea9 100644 --- a/profiles.json +++ b/profiles.json @@ -16,6 +16,7 @@ "ctxSize": 160000, "enableThinking": false, "modelPath": "/home/kamma/models/Nemotron-Cascade-2-30B-A3B.Q8_0.gguf", + "mmprojPath": "", "chatTemplateKwargs": "{\"enable_thinking\": false}", "ngl": -1, "fit": false, @@ -38,6 +39,7 @@ "ctxSize": 160000, "enableThinking": false, "modelPath": "/home/kamma/models/Qwen3-Coder-Next-UD-Q2_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "{\"enable_thinking\": false}", "ngl": 999, "fit": false, @@ -60,6 +62,7 @@ "ctxSize": 180000, "enableThinking": false, "modelPath": "/home/kamma/models/Nemotron-Cascade-2-30B-A3B.Q5_K_M.gguf", + "mmprojPath": "", "chatTemplateKwargs": "{\"enable_thinking\": false}", "ngl": 999, "fit": false, @@ -82,6 +85,7 @@ "ctxSize": 160000, "enableThinking": false, "modelPath": "/home/kamma/models/gpt-oss-20b-F16.gguf", + "mmprojPath": "", "chatTemplateKwargs": "{\"enable_thinking\": false}", "ngl": 999, "fit": false, @@ -104,6 +108,7 @@ "ctxSize": 131072, "enableThinking": false, "modelPath": "/home/kamma/models/gpt-oss-120b-F16.gguf", + "mmprojPath": "", "chatTemplateKwargs": "{\"enable_thinking\": true}", "ngl": -1, "fit": true, @@ -126,6 +131,7 @@ "ctxSize": 180000, "enableThinking": false, "modelPath": "/home/kamma/models/GLM-4.7-Flash-UD-Q6_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "{\"enable_thinking\": false}", "ngl": -1, "fit": true, @@ -148,6 +154,7 @@ "ctxSize": 160000, "enableThinking": false, "modelPath": "/home/kamma/models/Qwen3-Coder-Next-UD-Q3_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "", "ngl": -1, "fit": true, @@ -170,6 +177,7 @@ "ctxSize": 160000, "enableThinking": false, "modelPath": "/home/kamma/models/Qwen3-Coder-Next-UD-Q4_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "", "ngl": -1, "fit": true, @@ -192,6 +200,7 @@ "ctxSize": 54000, "enableThinking": false, "modelPath": "/home/kamma/models/gemma-4-31B-it-UD-Q6_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "", "ngl": 99, "fit": false, @@ -214,6 +223,7 @@ "ctxSize": 180000, "enableThinking": false, "modelPath": "/home/kamma/models/gemma-4-26B-A4B-it-UD-Q8_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "", "ngl": 99, "fit": false, @@ -236,6 +246,7 @@ "ctxSize": 112000, "enableThinking": false, "modelPath": "/home/kamma/models/gemma-4-31B-it-Q6_K.gguf", + "mmprojPath": "", "chatTemplateKwargs": "{\"enable_thinking\": true}", "ngl": 99, "fit": false, @@ -258,6 +269,7 @@ "ctxSize": 131072, "enableThinking": false, "modelPath": "/home/kamma/models/EXAONE-4.0-32B-GGUF-Q6_K.gguf", + "mmprojPath": "", "chatTemplateKwargs": "", "ngl": 99, "fit": false, @@ -280,6 +292,7 @@ "ctxSize": 180000, "enableThinking": false, "modelPath": "/home/kamma/models/Qwen3.6-35B-A3B-UD-Q6_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "", "ngl": 99, "fit": false, @@ -302,6 +315,7 @@ "ctxSize": 260000, "enableThinking": false, "modelPath": "/home/kamma/models/Qwen3.6-35B-A3B-UD-Q5_K_XL.gguf", + "mmprojPath": "/home/kamma/models/Qwen3.6-35B-A3B-UD.mmproj", "chatTemplateKwargs": "", "ngl": 99, "fit": false, @@ -324,6 +338,7 @@ "ctxSize": 180000, "enableThinking": false, "modelPath": "/home/kamma/models/Qwen3.6-27B-UD-Q6_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "{\"preserve_thinking\":true,\"enable_thinking\":true}", "ngl": 99, "fit": false, @@ -346,6 +361,7 @@ "ctxSize": 180000, "enableThinking": false, "modelPath": "/home/kamma/models/Qwen3.6-27B-UD-Q6_K_XL.gguf", + "mmprojPath": "", "chatTemplateKwargs": "", "ngl": 99, "fit": false, diff --git a/src/main/java/cz/kamma/llamarunner/CommandBuilder.java b/src/main/java/cz/kamma/llamarunner/CommandBuilder.java index acbadc3..a93cad8 100644 --- a/src/main/java/cz/kamma/llamarunner/CommandBuilder.java +++ b/src/main/java/cz/kamma/llamarunner/CommandBuilder.java @@ -49,6 +49,11 @@ public class CommandBuilder { String modelPath = buildModelPath(config.getModelPath(), modelsDirPath); cmd.append(" -m ").append(modelPath); + String mmprojPath = buildModelPath(config.getMmprojPath(), modelsDirPath); + if (!mmprojPath.isEmpty()) { + cmd.append(" --mmproj ").append(mmprojPath); + } + String kwargsText = config.getChatTemplateKwargs(); if (kwargsText != null && !kwargsText.trim().isEmpty()) { cmd.append(" --chat-template-kwargs \""); diff --git a/src/main/java/cz/kamma/llamarunner/ConfigValidation.java b/src/main/java/cz/kamma/llamarunner/ConfigValidation.java index 4d75b95..aa76aba 100644 --- a/src/main/java/cz/kamma/llamarunner/ConfigValidation.java +++ b/src/main/java/cz/kamma/llamarunner/ConfigValidation.java @@ -69,6 +69,18 @@ public class ConfigValidation { return "Model path is not a file: " + config.getModelPath(); } + // Validate optional mmproj path + String mmprojPath = config.getMmprojPath(); + if (mmprojPath != null && !mmprojPath.trim().isEmpty()) { + File mmprojFile = new File(mmprojPath); + if (!mmprojFile.exists()) { + return "MMProj file does not exist: " + mmprojPath; + } + if (!mmprojFile.isFile()) { + return "MMProj path is not a file: " + mmprojPath; + } + } + // Validate JSON kwargs String json = config.getChatTemplateKwargs(); if (json != null && !json.trim().isEmpty()) { diff --git a/src/main/java/cz/kamma/llamarunner/Main.java b/src/main/java/cz/kamma/llamarunner/Main.java index 21aacdb..c61f452 100644 --- a/src/main/java/cz/kamma/llamarunner/Main.java +++ b/src/main/java/cz/kamma/llamarunner/Main.java @@ -69,6 +69,7 @@ public class Main extends JFrame { private JTextField kwargsField; private JTextField nglField; private JComboBox modelComboBox; + private JComboBox mmprojComboBox; private JTextArea logArea; private JTextArea commandPreviewArea; private JButton browseModelsButton; @@ -179,14 +180,12 @@ public class Main extends JFrame { gbc.insets = new Insets(0, 0, 0, 0); gbc.anchor = GridBagConstraints.WEST; - // Label gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE; panel.add(new JLabel("Model:"), gbc); - // ComboBox gbc.gridx = 1; gbc.weightx = 1.0; gbc.fill = GridBagConstraints.HORIZONTAL; @@ -194,9 +193,27 @@ public class Main extends JFrame { modelComboBox.setEditable(true); modelComboBox.setMinimumSize(new Dimension(200, 25)); modelComboBox.setPreferredSize(new Dimension(500, 25)); + increaseComboBoxHeight(modelComboBox); modelComboBox.addActionListener(e -> updateCommandPreview()); panel.add(modelComboBox, gbc); + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0; + gbc.fill = GridBagConstraints.NONE; + panel.add(new JLabel("MMProj:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + mmprojComboBox = new JComboBox<>(); + mmprojComboBox.setEditable(true); + mmprojComboBox.setMinimumSize(new Dimension(200, 25)); + mmprojComboBox.setPreferredSize(new Dimension(500, 25)); + increaseComboBoxHeight(mmprojComboBox); + mmprojComboBox.addActionListener(e -> updateCommandPreview()); + panel.add(mmprojComboBox, gbc); + // Buttons panel JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); browseModelsButton = new JButton("Browse"); @@ -208,6 +225,8 @@ public class Main extends JFrame { buttonPanel.add(refreshModelsButton); gbc.gridx = 2; + gbc.gridy = 0; + gbc.gridheight = 2; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE; panel.add(buttonPanel, gbc); @@ -236,6 +255,7 @@ public class Main extends JFrame { profileComboBox.setEditable(true); profileComboBox.setMinimumSize(new Dimension(200, 25)); profileComboBox.setPreferredSize(new Dimension(500, 25)); + increaseComboBoxHeight(profileComboBox); profileChangeListener = e -> profileComboBoxChanged(); profileComboBox.addActionListener(profileChangeListener); panel.add(profileComboBox, gbc); @@ -268,9 +288,12 @@ public class Main extends JFrame { } private void loadModels() { - String selectedItem = (String) modelComboBox.getSelectedItem(); + String selectedModel = (String) modelComboBox.getSelectedItem(); + String selectedMmproj = (String) mmprojComboBox.getSelectedItem(); modelComboBox.removeAllItems(); + mmprojComboBox.removeAllItems(); + mmprojComboBox.addItem(""); File modelsDir = new File(modelsDirPath); if (modelsDir.exists() && modelsDir.isDirectory()) { @@ -279,25 +302,79 @@ public class Main extends JFrame { List sortedFiles = new ArrayList<>(Arrays.asList(modelFiles)); sortedFiles.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName())); for (File file : sortedFiles) { - double sizeInGb = (double) file.length() / (1024 * 1024 * 1024); - String displayName = String.format("%s (%.2f GB)", file.getName(), sizeInGb); + String displayName = formatDisplayName(file); modelComboBox.addItem(displayName); } } + + File[] mmprojFiles = modelsDir.listFiles((dir, name) -> name.endsWith(".mmproj")); + if (mmprojFiles != null) { + List sortedFiles = new ArrayList<>(Arrays.asList(mmprojFiles)); + sortedFiles.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName())); + for (File file : sortedFiles) { + mmprojComboBox.addItem(formatDisplayName(file)); + } + } } - if (selectedItem != null && modelComboBox.getItemCount() > 0) { - boolean found = false; - for (int i = 0; i < modelComboBox.getItemCount(); i++) { - if (modelComboBox.getItemAt(i).equals(selectedItem)) { - found = true; - break; - } - } - if (!found) { - modelComboBox.addItem(selectedItem); + restoreSelection(modelComboBox, selectedModel, false); + restoreSelection(mmprojComboBox, selectedMmproj, true); + } + + private String formatDisplayName(File file) { + double sizeInGb = (double) file.length() / (1024 * 1024 * 1024); + return String.format("%s (%.2f GB)", file.getName(), sizeInGb); + } + + private void restoreSelection(JComboBox comboBox, String selectedItem, boolean allowEmpty) { + if (selectedItem == null || (allowEmpty && selectedItem.isEmpty())) { + return; + } + + boolean found = false; + for (int i = 0; i < comboBox.getItemCount(); i++) { + if (comboBox.getItemAt(i).equals(selectedItem)) { + found = true; + break; } } + if (!found) { + comboBox.addItem(selectedItem); + } + comboBox.setSelectedItem(selectedItem); + } + + private String getSelectedFileName(JComboBox comboBox) { + String displayName = (String) comboBox.getSelectedItem(); + if (displayName == null) { + return ""; + } + if (displayName.contains(" (") && displayName.endsWith(" GB)")) { + return displayName.substring(0, displayName.lastIndexOf(" (")); + } + return displayName; + } + + private String buildSelectedPath(JComboBox comboBox) { + String fileName = getSelectedFileName(comboBox); + return fileName != null && !fileName.isEmpty() ? new File(modelsDirPath, fileName).getAbsolutePath() : ""; + } + + private String buildDisplayNameForPath(String path) { + String fileName = path != null ? new File(path).getName() : ""; + if (fileName.isEmpty()) { + return ""; + } + + File file = new File(modelsDirPath, fileName); + return file.exists() ? formatDisplayName(file) : fileName; + } + + private void increaseComboBoxHeight(JComboBox comboBox) { + Dimension minimumSize = comboBox.getMinimumSize(); + Dimension preferredSize = comboBox.getPreferredSize(); + comboBox.setMinimumSize(new Dimension(minimumSize.width, minimumSize.height + 5)); + comboBox.setPreferredSize(new Dimension(preferredSize.width, preferredSize.height + 5)); } private void loadProfiles() { @@ -405,16 +482,8 @@ public class Main extends JFrame { config.setTopK(Integer.parseInt(topKField.getText())); config.setMinP(Double.parseDouble(minPField.getText())); config.setCtxSize(Integer.parseInt(ctxSizeField.getText())); - String displayName = (String) modelComboBox.getSelectedItem(); - String fileName = ""; - if (displayName != null) { - if (displayName.contains(" (") && displayName.endsWith(" GB)")) { - fileName = displayName.substring(0, displayName.lastIndexOf(" (")); - } else { - fileName = displayName; - } - } - config.setModelPath(fileName != null && !fileName.isEmpty() ? new File(modelsDirPath, fileName).getAbsolutePath() : ""); + config.setModelPath(buildSelectedPath(modelComboBox)); + config.setMmprojPath(buildSelectedPath(mmprojComboBox)); config.setChatTemplateKwargs(kwargsField.getText()); config.setNgl(Integer.parseInt(nglField.getText())); return config; @@ -445,17 +514,8 @@ public class Main extends JFrame { topKField.setText(String.valueOf(config.getTopK())); minPField.setText(String.valueOf(config.getMinP())); ctxSizeField.setText(String.valueOf(config.getCtxSize())); - String modelName = config.getModelPath() != null - ? new File(config.getModelPath()).getName() - : ""; - if (!modelName.isEmpty()) { - File modelFile = new File(modelsDirPath, modelName); - if (modelFile.exists()) { - double sizeInGb = (double) modelFile.length() / (1024 * 1024 * 1024); - modelName = String.format("%s (%.2f GB)", modelName, sizeInGb); - } - } - modelComboBox.setSelectedItem(modelName); + modelComboBox.setSelectedItem(buildDisplayNameForPath(config.getModelPath())); + mmprojComboBox.setSelectedItem(buildDisplayNameForPath(config.getMmprojPath())); kwargsField.setText(config.getChatTemplateKwargs()); setNglFieldText(String.valueOf(config.getNgl())); @@ -479,15 +539,7 @@ public class Main extends JFrame { String defaultName = (currentProfile != null && !currentProfile.isEmpty()) ? currentProfile : ""; if (defaultName.isEmpty()) { - String displayName = (String) modelComboBox.getSelectedItem(); - String modelName = ""; - if (displayName != null) { - if (displayName.contains(" (") && displayName.endsWith(" GB)")) { - modelName = displayName.substring(0, displayName.lastIndexOf(" (")); - } else { - modelName = displayName; - } - } + String modelName = getSelectedFileName(modelComboBox); defaultName = modelName != null ? modelName : ""; } @@ -764,6 +816,7 @@ public class Main extends JFrame { gbc.weightx = 1.0; cacheTypeKComboBox = new JComboBox<>(new String[] { "bf16", "f16", "f32", "f8", "q8_0", "turbo3", "turbo4" }); cacheTypeKComboBox.setSelectedIndex(0); + increaseComboBoxHeight(cacheTypeKComboBox); cacheTypeKComboBox.addActionListener(e -> updateCommandPreview()); panel.add(cacheTypeKComboBox, gbc); @@ -776,6 +829,7 @@ public class Main extends JFrame { gbc.weightx = 1.0; cacheTypeVComboBox = new JComboBox<>(new String[] { "bf16", "f16", "f32", "f8", "q8_0", "turbo3", "turbo4" }); cacheTypeVComboBox.setSelectedIndex(0); + increaseComboBoxHeight(cacheTypeVComboBox); cacheTypeVComboBox.addActionListener(e -> updateCommandPreview()); panel.add(cacheTypeVComboBox, gbc); @@ -945,14 +999,13 @@ public class Main extends JFrame { try { String error = validateCurrentConfig(); if (error != null) { - JOptionPane.showMessageDialog(this, error, "Validation Error", JOptionPane.ERROR_MESSAGE); + commandPreviewArea.setText(error); return; } String command = buildCommand(); commandPreviewArea.setText(command); } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(this, "Please enter valid numeric values.", "Validation Error", - JOptionPane.ERROR_MESSAGE); + commandPreviewArea.setText("Please enter valid numeric values."); } } diff --git a/src/main/java/cz/kamma/llamarunner/ModelConfig.java b/src/main/java/cz/kamma/llamarunner/ModelConfig.java index 6454f0a..f2d613c 100644 --- a/src/main/java/cz/kamma/llamarunner/ModelConfig.java +++ b/src/main/java/cz/kamma/llamarunner/ModelConfig.java @@ -25,6 +25,7 @@ public class ModelConfig implements Serializable { private int ctxSize; private boolean enableThinking; private String modelPath; + private String mmprojPath; private String chatTemplateKwargs; private int ngl; private boolean fit; @@ -47,6 +48,7 @@ public class ModelConfig implements Serializable { this.ctxSize = 180000; this.enableThinking = false; this.modelPath = ""; + this.mmprojPath = ""; this.chatTemplateKwargs = ""; this.ngl = 999; this.fit = false; @@ -102,6 +104,9 @@ public class ModelConfig implements Serializable { public String getModelPath() { return modelPath; } public void setModelPath(String modelPath) { this.modelPath = modelPath; } + public String getMmprojPath() { return mmprojPath; } + public void setMmprojPath(String mmprojPath) { this.mmprojPath = mmprojPath; } + public String getChatTemplateKwargs() { return chatTemplateKwargs; } public void setChatTemplateKwargs(String chatTemplateKwargs) { this.chatTemplateKwargs = chatTemplateKwargs; } @@ -133,6 +138,7 @@ public class ModelConfig implements Serializable { ", ctxSize=" + ctxSize + ", enableThinking=" + enableThinking + ", modelPath='" + modelPath + '\'' + + ", mmprojPath='" + mmprojPath + '\'' + ", chatTemplateKwargs='" + chatTemplateKwargs + '\'' + ", ngl=" + ngl + ", fit=" + fit +