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");
}
}