diff options
| author | Michael Peter Christen <mc@yacy.net> | 2026-03-29 17:33:49 +0200 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2026-03-29 17:33:49 +0200 |
| commit | 7b303b6f82b1eee57c05e984ce7f4f42fc992405 (patch) | |
| tree | 73ce9bf6bcb1b6527a4590d987e3c247b2b214a2 /source/net | |
| parent | f0464e7fbcfcb69127f0325910f92f113ce23677 (diff) | |
Add thinking/tooling/vision/format model capability tests and suppress thinking across LLM calls
Diffstat (limited to 'source/net')
| -rw-r--r-- | source/net/yacy/ai/LLM.java | 75 | ||||
| -rw-r--r-- | source/net/yacy/ai/ToolCallProtocol.java | 11 | ||||
| -rw-r--r-- | source/net/yacy/htroot/LLMSelection_p.java | 18 |
3 files changed, 80 insertions, 24 deletions
diff --git a/source/net/yacy/ai/LLM.java b/source/net/yacy/ai/LLM.java index 5a32b6592..7f74943d0 100644 --- a/source/net/yacy/ai/LLM.java +++ b/source/net/yacy/ai/LLM.java @@ -43,6 +43,7 @@ import net.yacy.search.Switchboard; public class LLM { + private static final String MODEL_CAPABILITIES_CONFIG = "ai.model_capabilities"; private static String[] STOPTOKENS = new String[]{"[/INST]", "<|im_end|>", "<|end_of_turn|>", "<|eot_id|>", "<|end_header_id|>", "<EOS_TOKEN>", "</s>", "<|end|>"}; public static enum LLMType { @@ -71,10 +72,12 @@ public class LLM { public LLM llm; public String model; public boolean tooling; - public LLMModel(LLM llm, String model, boolean tooling) { + public boolean thinking; + public LLMModel(LLM llm, String model, boolean tooling, boolean thinking) { this.llm = llm; this.model = model; this.tooling = tooling; + this.thinking = thinking; } } @@ -98,13 +101,7 @@ public class LLM { public static LLMModel llmFromUsage(LLMUsage llmUsage) { Switchboard sb = Switchboard.getSwitchboard(); String pms = sb.getConfig("ai.production_models", "[]"); - String mcs = sb.getConfig("ai.model_capabilities", "{}"); - JSONObject model_capabilities = new JSONObject(true); - try { - model_capabilities = new JSONObject(new JSONTokener(mcs)); - } catch (JSONException e) { - model_capabilities = new JSONObject(true); - } + JSONObject model_capabilities = readModelCapabilities(); try { JSONArray production_models = new JSONArray(new JSONTokener(pms)); // got through all the selected models to find which one has the wanted usage flag switched on @@ -117,15 +114,18 @@ public class LLM { final String api_key = row.optString("api_key", ""); final int max_tokens = Integer.parseInt(row.optString("max_tokens", "4096")); final String model = row.optString("model", ""); + final LLMType type = LLMType.valueOf(row.optString("service", "OLLAMA")); boolean tooling = row.optBoolean("tooling", false); - if (!tooling) { - final String capabilityKey = row.optString("service", "OLLAMA") + "|" + hoststub.replaceAll("/+$", "") + "|" + model; - final JSONObject capabilityEntry = model_capabilities.optJSONObject(capabilityKey); - tooling = capabilityEntry != null && "supported".equals(capabilityEntry.optString("tooling", "")); + boolean thinking = row.optBoolean("thinking", false); + if (!tooling || !thinking) { + final JSONObject capabilityEntry = model_capabilities.optJSONObject(capabilityKey(type, hoststub, model)); + if (capabilityEntry != null) { + if (!tooling) tooling = "supported".equals(capabilityEntry.optString("tooling", "")); + if (!thinking) thinking = "supported".equals(capabilityEntry.optString("thinking", "")); + } } - final LLMType type = LLMType.valueOf(row.optString("service", "OLLAMA")); LLM llm = new LLM(hoststub, api_key, max_tokens, type); - LLMModel llmmodel = new LLMModel(llm, model, tooling); + LLMModel llmmodel = new LLMModel(llm, model, tooling, thinking); return llmmodel; } } @@ -140,6 +140,47 @@ public class LLM { return this.hoststub; } + public static String capabilityKey(final LLMType type, final String hoststub, final String model) { + final String normalizedType = type == null ? "" : type.name(); + final String normalizedHoststub = hoststub == null ? "" : hoststub.replaceAll("/+$", ""); + final String normalizedModel = model == null ? "" : model.trim(); + return normalizedType + "|" + normalizedHoststub + "|" + normalizedModel; + } + + private static JSONObject readModelCapabilities() { + final Switchboard sb = Switchboard.getSwitchboard(); + if (sb == null) return new JSONObject(true); + final String capabilitiesJson = sb.getConfig(MODEL_CAPABILITIES_CONFIG, "{}"); + try { + return new JSONObject(new JSONTokener(capabilitiesJson)); + } catch (JSONException e) { + return new JSONObject(true); + } + } + + public static boolean isCapabilitySupported(final LLMType type, final String hoststub, final String model, final String capabilityName) { + if (capabilityName == null || capabilityName.isEmpty()) return false; + final JSONObject modelCapabilities = readModelCapabilities(); + final JSONObject capabilityEntry = modelCapabilities.optJSONObject(capabilityKey(type, hoststub, model)); + return capabilityEntry != null && "supported".equalsIgnoreCase(capabilityEntry.optString(capabilityName, "")); + } + + public static void applyNoThinkingParameters(final JSONObject data) { + if (data == null) return; + try { + data.put("reasoning_effort", "none"); + data.put("enable_thinking", false); + } catch (JSONException e) { + } + } + + public void applyNoThinkingParametersIfNeeded(final String model, final JSONObject data) { + final String normalizedModel = model == null ? "" : model.toLowerCase(); + if (normalizedModel.contains("qwen3.5") || isCapabilitySupported(this.type, this.hoststub, model, "thinking")) { + applyNoThinkingParameters(data); + } + } + // API Helper Methods @@ -287,11 +328,7 @@ public class LLM { data.put("messages", context); data.put("stop", new JSONArray(STOPTOKENS)); data.put("stream", false); - - if (model.toLowerCase().contains("qwen3.5")) { // we don't think - data.put("reasoning_effort", "none"); - data.put("enable_thinking", false); - } + applyNoThinkingParametersIfNeeded(model, data); if (schema != null) { System.out.println(schema.toString()); diff --git a/source/net/yacy/ai/ToolCallProtocol.java b/source/net/yacy/ai/ToolCallProtocol.java index b545c85b7..2eab71687 100644 --- a/source/net/yacy/ai/ToolCallProtocol.java +++ b/source/net/yacy/ai/ToolCallProtocol.java @@ -88,11 +88,6 @@ public final class ToolCallProtocol { try { final JSONObject prepared = body == null ? new JSONObject(true) : new JSONObject(body.toString()); if (forceStream) prepared.put("stream", true); - final String model = prepared.optString("model", ""); - if (model.toLowerCase().contains("qwen3.5")) { - prepared.put("reasoning_effort", "none"); - prepared.put("enable_thinking", false); - } if (toolingEnabled) net.yacy.ai.ToolProvider.ensureTools(prepared); return prepared; } catch (JSONException e) { @@ -123,6 +118,9 @@ public final class ToolCallProtocol { */ public static int proxyToolLifecycle(ServletOutputStream out, LLM.LLMModel llm4Chat, JSONObject originalBody, JSONArray messages, JSONObject initialMetadata) throws IOException { final JSONObject preparedBody = prepareToolRequestBody(originalBody, false, llm4Chat != null && llm4Chat.tooling); + if (llm4Chat != null && llm4Chat.thinking) { + LLM.applyNoThinkingParameters(preparedBody); + } final HttpURLConnection conn = openChatCompletionConnection(llm4Chat, preparedBody); final int status = conn.getResponseCode(); //final String message = conn.getResponseMessage(); @@ -284,6 +282,9 @@ public final class ToolCallProtocol { // Build follow-up completion request from original body template. final JSONObject followup = prepareToolRequestBody(originalBody, true, llm4Chat != null && llm4Chat.tooling); + if (llm4Chat != null && llm4Chat.thinking) { + LLM.applyNoThinkingParameters(followup); + } followup.put("messages", newMessages); final HttpURLConnection followConn = openChatCompletionConnection(llm4Chat, followup); if (followConn.getResponseCode() != 200) { diff --git a/source/net/yacy/htroot/LLMSelection_p.java b/source/net/yacy/htroot/LLMSelection_p.java index 4a5955b32..03229a9ce 100644 --- a/source/net/yacy/htroot/LLMSelection_p.java +++ b/source/net/yacy/htroot/LLMSelection_p.java @@ -52,11 +52,15 @@ public class LLMSelection_p { final JSONObject entry = source.optJSONObject(key); final JSONObject normalizedEntry = new JSONObject(true); if (entry != null) { + normalizedEntry.put("thinking", normalizeCapabilityStatus(entry.opt("thinking"))); normalizedEntry.put("tooling", normalizeCapabilityStatus(entry.opt("tooling"))); normalizedEntry.put("vision", normalizeCapabilityStatus(entry.opt("vision"))); + normalizedEntry.put("format", normalizeCapabilityStatus(entry.opt("format"))); } else { + normalizedEntry.put("thinking", "unknown"); normalizedEntry.put("tooling", "unknown"); normalizedEntry.put("vision", "unknown"); + normalizedEntry.put("format", "unknown"); } normalized.put(key, normalizedEntry); } @@ -88,8 +92,10 @@ public class LLMSelection_p { normalized.put("qapairs", false); normalized.put("tldr", row.optBoolean("tldr", false)); + normalized.put("thinking", row.optBoolean("thinking", false)); normalized.put("tooling", row.optBoolean("tooling", false)); normalized.put("vision", row.optBoolean("vision", false)); + normalized.put("format", row.optBoolean("format", false)); return normalized; } @@ -183,14 +189,22 @@ public class LLMSelection_p { final String key = capabilityKey(row); JSONObject capabilityEntry = key.isEmpty() ? null : capabilities.optJSONObject(key); + String thinkingStatus = capabilityEntry == null ? "unknown" : normalizeCapabilityStatus(capabilityEntry.opt("thinking")); String toolingStatus = capabilityEntry == null ? "unknown" : normalizeCapabilityStatus(capabilityEntry.opt("tooling")); String visionStatus = capabilityEntry == null ? "unknown" : normalizeCapabilityStatus(capabilityEntry.opt("vision")); + String formatStatus = capabilityEntry == null ? "unknown" : normalizeCapabilityStatus(capabilityEntry.opt("format")); + if (row.optBoolean("thinking", false)) thinkingStatus = "supported"; if (row.optBoolean("tooling", false)) toolingStatus = "supported"; if (row.optBoolean("vision", false)) visionStatus = "supported"; + if (row.optBoolean("format", false)) formatStatus = "supported"; + prop.put("productionmodels_" + i + "_thinking", + "supported".equals(thinkingStatus) ? "yes" : "unsupported".equals(thinkingStatus) ? "no" : "?"); prop.put("productionmodels_" + i + "_tooling", "supported".equals(toolingStatus) ? "yes" : "unsupported".equals(toolingStatus) ? "no" : "?"); prop.put("productionmodels_" + i + "_vision", "supported".equals(visionStatus) ? "yes" : "unsupported".equals(visionStatus) ? "no" : "?"); + prop.put("productionmodels_" + i + "_format", + "supported".equals(formatStatus) ? "yes" : "unsupported".equals(formatStatus) ? "no" : "?"); } prop.put("productionmodels", production_models.length()); } catch (JSONException e) { @@ -206,12 +220,16 @@ public class LLMSelection_p { JSONObject entry = capabilities.optJSONObject(key); if (entry == null) { entry = new JSONObject(true); + entry.put("thinking", "unknown"); entry.put("tooling", "unknown"); entry.put("vision", "unknown"); + entry.put("format", "unknown"); capabilities.put(key, entry); } + if (row.optBoolean("thinking", false)) entry.put("thinking", "supported"); if (row.optBoolean("tooling", false)) entry.put("tooling", "supported"); if (row.optBoolean("vision", false)) entry.put("vision", "supported"); + if (row.optBoolean("format", false)) entry.put("format", "supported"); } } prop.putHTML("model_capabilities", capabilities.toString()); |
