LmStudioChatClientImpl.java

package fr.tiogars.chat.domains.prompt.services;

import fr.tiogars.chat.domains.lmstudio.models.LmStudioSettings;
import fr.tiogars.chat.domains.lmstudio.services.LmStudioSettingsService;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

/**
 * Implementation of ChatClientAdapter for LMStudio.
 * This uses direct HTTP calls to LMStudio's API until Spring AI is available.
 * Connection settings are persisted by the dedicated LM Studio domain.
 */
@Component
public class LmStudioChatClientImpl implements ChatClientAdapter {

    private final LmStudioSettingsService settingsService;

    public LmStudioChatClientImpl(LmStudioSettingsService settingsService) {
        this.settingsService = settingsService;
    }

    @Override
    public String call(String prompt) {
        return call(null, prompt);
    }

    @Override
    public String call(String systemPrompt, String userPrompt) {
        LmStudioSettings settings = settingsService.requireSettings();
        Duration timeout = Duration.ofSeconds(settings.timeoutSeconds());
        try (HttpClient httpClient = HttpClient.newBuilder().connectTimeout(timeout).build()) {
            String requestBody = buildRequestBody(settings.model(), systemPrompt, userPrompt);
            
            HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
                    .uri(URI.create(settings.baseUrl().replaceAll("/+$", "") + "/v1/chat/completions"))
                    .timeout(timeout)
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(requestBody));
            if (settings.isTokenConfigured()) {
                requestBuilder.header("Authorization", "Bearer " + settings.token());
            }
            HttpRequest request = requestBuilder.build();

            HttpResponse<String> response = httpClient.send(
                    request, 
                    HttpResponse.BodyHandlers.ofString()
            );

            if (response.statusCode() != 200) {
                throw new RuntimeException("LMStudio API error: " + response.statusCode());
            }

            return parseResponse(response.body());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("LMStudio API call interrupted", e);
        } catch (IOException e) {
            throw new RuntimeException("Error calling LMStudio API", e);
        }
    }

    private String buildRequestBody(String model, String systemPrompt, String userPrompt) {
        if (systemPrompt != null && !systemPrompt.isBlank()) {
            return String.format("""
                {
                    "model": "%s",
                    "messages": [
                        {"role": "system", "content": "%s"},
                        {"role": "user", "content": "%s"}
                    ],
                    "stream": false,
                    "temperature": 0.7
                }
                """, escapeJson(model), escapeJson(systemPrompt), escapeJson(userPrompt));
        } else {
            return String.format("""
                {
                    "model": "%s",
                    "messages": [
                        {"role": "user", "content": "%s"}
                    ],
                    "stream": false,
                    "temperature": 0.7
                }
                """, escapeJson(model), escapeJson(userPrompt));
        }
    }

    private String parseResponse(String responseBody) {
        // Simple parsing - in a real implementation, use JSON parsing
        // This is a simplified version that extracts the content from the response
        // Expected format: {"choices":[{"message":{"content":"..."}}]}
        int contentIndex = responseBody.indexOf("\"content\":");
        if (contentIndex == -1) {
            return responseBody;
        }
        
        int start = responseBody.indexOf('"', contentIndex + 10) + 1;
        int end = responseBody.lastIndexOf('"');
        
        if (start < 0 || end < start) {
            return responseBody;
        }
        
        return responseBody.substring(start, end);
    }

    private String escapeJson(String input) {
        if (input == null) {
            return "";
        }
        return input.replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
    }
}