EclipseLlmService.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Carsten Hammer
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.mining.llm;

import java.util.function.Function;

import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.sandbox.jdt.internal.ui.preferences.LlmPreferencePage;
import org.sandbox.jdt.internal.ui.preferences.LlmSecureCredentials;
import org.sandbox.jdt.triggerpattern.llm.AiRuleInferenceEngine;
import org.sandbox.jdt.triggerpattern.llm.LlmClient;
import org.sandbox.jdt.triggerpattern.llm.LlmClientFactory;
import org.sandbox.jdt.triggerpattern.llm.LlmInferenceSettings;
import org.sandbox.jdt.triggerpattern.llm.LlmProvider;

/** Eclipse-level service that owns the shared AI rule-inference engine. */
public class EclipseLlmService {

	private static final String DOCUMENTATION_SCREENSHOT_PROPERTY = "sandbox.help.screenshot.mode"; //$NON-NLS-1$
	private static final String ENV_PROVIDER = "LLM_PROVIDER"; //$NON-NLS-1$
	private static final Object INSTANCE_LOCK = new Object();

	private static volatile EclipseLlmService instance;

	private final Object lifecycleLock = new Object();

	private LlmClient llmClient;
	private AiRuleInferenceEngine engine;

	private EclipseLlmService() {
	}

	public static EclipseLlmService getInstance() {
		EclipseLlmService current = instance;
		if (current == null) {
			synchronized (INSTANCE_LOCK) {
				current = instance;
				if (current == null) {
					current = new EclipseLlmService();
					instance = current;
				}
			}
		}
		return current;
	}

	public AiRuleInferenceEngine getEngine() {
		synchronized (lifecycleLock) {
			if (engine == null) {
				llmClient = createClientFromPreferences();
				engine = new AiRuleInferenceEngine(llmClient);
			}
			return engine;
		}
	}

	/** Documentation generation must never contact or unlock an external provider. */
	public boolean isAvailable() {
		if (Boolean.getBoolean(DOCUMENTATION_SCREENSHOT_PROPERTY)) {
			return false;
		}
		IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(LlmPreferencePage.PLUGIN_ID);
		String provider = prefs.get(LlmPreferencePage.PREF_PROVIDER, ""); //$NON-NLS-1$
		String secureApiKey = provider.isBlank() ? "" : LlmSecureCredentials.loadApiKey(provider); //$NON-NLS-1$
		return hasCredentials(provider, secureApiKey, System::getenv);
	}

	/**
	 * Returns a non-secret explanation of how to make the currently selected
	 * provider available.
	 */
	public String configurationHint() {
		IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(LlmPreferencePage.PLUGIN_ID);
		String configuredProvider = prefs.get(LlmPreferencePage.PREF_PROVIDER, ""); //$NON-NLS-1$
		if (!configuredProvider.isBlank()) {
			try {
				LlmProvider provider = LlmProvider.fromString(configuredProvider);
				return "Store an API key in Java > LLM Rule Inference or set " //$NON-NLS-1$
						+ environmentVariable(provider) + "."; //$NON-NLS-1$
			} catch (IllegalArgumentException e) {
				return "Select a supported provider in Java > LLM Rule Inference."; //$NON-NLS-1$
			}
		}

		String environmentProvider = System.getenv(ENV_PROVIDER);
		if (environmentProvider != null && !environmentProvider.isBlank()) {
			try {
				LlmProvider provider = LlmProvider.fromString(environmentProvider);
				return "Set " + environmentVariable(provider) //$NON-NLS-1$
						+ " for the provider selected by LLM_PROVIDER, or configure Java > LLM Rule Inference."; //$NON-NLS-1$
			} catch (IllegalArgumentException e) {
				return "LLM_PROVIDER names an unsupported provider; configure Java > LLM Rule Inference instead."; //$NON-NLS-1$
			}
		}

		return "Configure Java > LLM Rule Inference or set a supported provider API-key environment variable."; //$NON-NLS-1$
	}

	public void shutdown() {
		synchronized (lifecycleLock) {
			if (llmClient != null) {
				llmClient.close();
				llmClient = null;
				engine = null;
			}
		}
	}

	public static void reset() {
		EclipseLlmService current;
		synchronized (INSTANCE_LOCK) {
			current = instance;
			instance = null;
		}
		if (current != null) {
			current.shutdown();
		}
	}

	static boolean hasCredentials(String configuredProvider, String secureApiKey,
			Function<String, String> environment) {
		if (configuredProvider != null && !configuredProvider.isBlank()) {
			try {
				LlmProvider provider = LlmProvider.fromString(configuredProvider);
				return (secureApiKey != null && !secureApiKey.isBlank())
						|| envSet(environmentVariable(provider), environment);
			} catch (IllegalArgumentException e) {
				return false;
			}
		}

		String environmentProvider = environment.apply(ENV_PROVIDER);
		if (environmentProvider != null && !environmentProvider.isBlank()) {
			try {
				return envSet(environmentVariable(LlmProvider.fromString(environmentProvider)), environment);
			} catch (IllegalArgumentException e) {
				return false;
			}
		}

		for (LlmProvider provider : LlmProvider.values()) {
			if (envSet(environmentVariable(provider), environment)) {
				return true;
			}
		}
		return false;
	}

	static String environmentVariable(LlmProvider provider) {
		return switch (provider) {
		case GEMINI -> "GEMINI_API_KEY"; //$NON-NLS-1$
		case OPENAI -> "OPENAI_API_KEY"; //$NON-NLS-1$
		case DEEPSEEK -> "DEEPSEEK_API_KEY"; //$NON-NLS-1$
		case QWEN -> "DASHSCOPE_API_KEY"; //$NON-NLS-1$
		case LLAMA -> "LLAMA_API_KEY"; //$NON-NLS-1$
		case MISTRAL -> "MISTRAL_API_KEY"; //$NON-NLS-1$
		};
	}

	private static LlmClient createClientFromPreferences() {
		IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(LlmPreferencePage.PLUGIN_ID);
		String provider = prefs.get(LlmPreferencePage.PREF_PROVIDER, ""); //$NON-NLS-1$
		String modelName = prefs.get(LlmPreferencePage.PREF_MODEL_NAME, ""); //$NON-NLS-1$
		LlmInferenceSettings settings = new LlmInferenceSettings(modelName, null, null);

		if (!provider.isBlank()) {
			String apiKey = LlmSecureCredentials.loadApiKey(provider);
			return LlmClientFactory.create(
					LlmProvider.fromString(provider),
					apiKey.isBlank() ? null : apiKey,
					settings);
		}
		return LlmClientFactory.createFromEnvironment(null, settings);
	}

	private static boolean envSet(String key, Function<String, String> environment) {
		String value = environment.apply(key);
		return value != null && !value.isBlank();
	}
}