diff --git a/pom.xml b/pom.xml
index 6a6dc21..bfe96b5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,10 @@
com.fasterxml.jackson.core
jackson-core
+
+ io.micrometer
+ micrometer-registry-prometheus
+
diff --git a/src/main/java/com/example/blast_radius/infra/GroqApiException.java b/src/main/java/com/example/blast_radius/infra/GroqApiException.java
new file mode 100644
index 0000000..3350af2
--- /dev/null
+++ b/src/main/java/com/example/blast_radius/infra/GroqApiException.java
@@ -0,0 +1,29 @@
+package com.example.blast_radius.infra;
+
+/**
+ * Thrown when the Groq API call fails due to network errors, HTTP errors,
+ * or an unparseable response envelope.
+ */
+public class GroqApiException extends Exception {
+
+ private final int httpStatus;
+
+ public GroqApiException(String message) {
+ super(message);
+ this.httpStatus = -1;
+ }
+
+ public GroqApiException(String message, int httpStatus) {
+ super(message);
+ this.httpStatus = httpStatus;
+ }
+
+ public GroqApiException(String message, Throwable cause) {
+ super(message, cause);
+ this.httpStatus = -1;
+ }
+
+ public int getHttpStatus() {
+ return httpStatus;
+ }
+}
diff --git a/src/main/java/com/example/blast_radius/infra/GroqClient.java b/src/main/java/com/example/blast_radius/infra/GroqClient.java
index becc870..59c586d 100644
--- a/src/main/java/com/example/blast_radius/infra/GroqClient.java
+++ b/src/main/java/com/example/blast_radius/infra/GroqClient.java
@@ -5,11 +5,14 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.*;
import org.springframework.stereotype.Component;
-import org.springframework.web.client.RestTemplate;
-import java.util.HashMap;
+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;
import java.util.Map;
@Component
@@ -17,7 +20,11 @@ public class GroqClient {
private static final Logger log = LoggerFactory.getLogger(GroqClient.class);
- private final RestTemplate restTemplate;
+ private static final int MAX_ATTEMPTS = 2;
+ private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
+ private static final Duration READ_TIMEOUT = Duration.ofSeconds(20);
+
+ private final HttpClient httpClient;
private final ObjectMapper objectMapper;
@Value("${groq.api.key}")
@@ -30,59 +37,108 @@ public class GroqClient {
private String model;
public GroqClient() {
- // TODO: Configure timeouts on RestTemplate (e.g., 30s connect, 60s read)
- // SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
- // factory.setConnectTimeout(Duration.ofSeconds(30));
- // factory.setReadTimeout(Duration.ofSeconds(60));
- // this.restTemplate = new RestTemplate(factory);
- this.restTemplate = new RestTemplate();
+ this.httpClient = HttpClient.newBuilder()
+ .connectTimeout(CONNECT_TIMEOUT)
+ .build();
this.objectMapper = new ObjectMapper();
}
/**
* Sends a prompt to the Groq chat completions API and returns the assistant's
* message content as a raw string.
+ * Retries up to MAX_ATTEMPTS on network errors or 5xx responses.
+ * Does not retry on 4xx (client errors).
*/
- public String callChatApi(String promptPayload) throws Exception {
- Map body = new HashMap<>();
- body.put("model", model);
- body.put("messages", new Object[]{
- Map.of("role", "user", "content", promptPayload)
- });
- body.put("response_format", Map.of("type", "json_object"));
-
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_JSON);
- headers.setBearerAuth(apiKey);
-
- HttpEntity