1 package fr.tiogars.chat.domains.prompt.services;
2
3 import fr.tiogars.chat.domains.lmstudio.models.LmStudioSettings;
4 import fr.tiogars.chat.domains.lmstudio.services.LmStudioSettingsService;
5 import org.springframework.stereotype.Component;
6
7 import java.io.IOException;
8 import java.net.URI;
9 import java.net.http.HttpClient;
10 import java.net.http.HttpRequest;
11 import java.net.http.HttpResponse;
12 import java.time.Duration;
13
14
15
16
17
18
19 @Component
20 public class LmStudioChatClientImpl implements ChatClientAdapter {
21
22 private final LmStudioSettingsService settingsService;
23
24 public LmStudioChatClientImpl(LmStudioSettingsService settingsService) {
25 this.settingsService = settingsService;
26 }
27
28 @Override
29 public String call(String prompt) {
30 return call(null, prompt);
31 }
32
33 @Override
34 public String call(String systemPrompt, String userPrompt) {
35 LmStudioSettings settings = settingsService.requireSettings();
36 Duration timeout = Duration.ofSeconds(settings.timeoutSeconds());
37 try (HttpClient httpClient = HttpClient.newBuilder().connectTimeout(timeout).build()) {
38 String requestBody = buildRequestBody(settings.model(), systemPrompt, userPrompt);
39
40 HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
41 .uri(URI.create(settings.baseUrl().replaceAll("/+$", "") + "/v1/chat/completions"))
42 .timeout(timeout)
43 .header("Content-Type", "application/json")
44 .POST(HttpRequest.BodyPublishers.ofString(requestBody));
45 if (settings.isTokenConfigured()) {
46 requestBuilder.header("Authorization", "Bearer " + settings.token());
47 }
48 HttpRequest request = requestBuilder.build();
49
50 HttpResponse<String> response = httpClient.send(
51 request,
52 HttpResponse.BodyHandlers.ofString()
53 );
54
55 if (response.statusCode() != 200) {
56 throw new RuntimeException("LMStudio API error: " + response.statusCode());
57 }
58
59 return parseResponse(response.body());
60 } catch (InterruptedException e) {
61 Thread.currentThread().interrupt();
62 throw new RuntimeException("LMStudio API call interrupted", e);
63 } catch (IOException e) {
64 throw new RuntimeException("Error calling LMStudio API", e);
65 }
66 }
67
68 private String buildRequestBody(String model, String systemPrompt, String userPrompt) {
69 if (systemPrompt != null && !systemPrompt.isBlank()) {
70 return String.format("""
71 {
72 "model": "%s",
73 "messages": [
74 {"role": "system", "content": "%s"},
75 {"role": "user", "content": "%s"}
76 ],
77 "stream": false,
78 "temperature": 0.7
79 }
80 """, escapeJson(model), escapeJson(systemPrompt), escapeJson(userPrompt));
81 } else {
82 return String.format("""
83 {
84 "model": "%s",
85 "messages": [
86 {"role": "user", "content": "%s"}
87 ],
88 "stream": false,
89 "temperature": 0.7
90 }
91 """, escapeJson(model), escapeJson(userPrompt));
92 }
93 }
94
95 private String parseResponse(String responseBody) {
96
97
98
99 int contentIndex = responseBody.indexOf("\"content\":");
100 if (contentIndex == -1) {
101 return responseBody;
102 }
103
104 int start = responseBody.indexOf('"', contentIndex + 10) + 1;
105 int end = responseBody.lastIndexOf('"');
106
107 if (start < 0 || end < start) {
108 return responseBody;
109 }
110
111 return responseBody.substring(start, end);
112 }
113
114 private String escapeJson(String input) {
115 if (input == null) {
116 return "";
117 }
118 return input.replace("\\", "\\\\")
119 .replace("\"", "\\\"")
120 .replace("\n", "\\n")
121 .replace("\r", "\\r")
122 .replace("\t", "\\t");
123 }
124 }