diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index e9f99ad..8386773 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -64,6 +64,11 @@ public class OllamaAPI { @Setter private long requestTimeoutSeconds = 10; + @Setter + private int imageURLReadTimeoutSeconds = 10; + + @Setter + private int imageURLConnectTimeoutSeconds = 10; /** * The maximum number of retries for tool calls during chat interactions. *

@@ -821,7 +826,7 @@ public class OllamaAPI { encodedImages.add(encodeByteArrayToBase64((byte[]) image)); } else if (image instanceof String) { LOG.debug("Using image URL: {}", image); - encodedImages.add(encodeByteArrayToBase64(Utils.loadImageBytesFromUrl((String) image))); + encodedImages.add(encodeByteArrayToBase64(Utils.loadImageBytesFromUrl((String) image, imageURLConnectTimeoutSeconds, imageURLReadTimeoutSeconds))); } else { throw new OllamaBaseException("Unsupported image type. Please provide a File, byte[], or a URL String."); } diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java index 38ff63a..b540beb 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java @@ -7,7 +7,6 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; -import java.net.URISyntaxException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; @@ -21,6 +20,19 @@ public class OllamaChatRequestBuilder { private static final Logger LOG = LoggerFactory.getLogger(OllamaChatRequestBuilder.class); + private int imageURLConnectTimeoutSeconds = 10; + private int imageURLReadTimeoutSeconds = 10; + + public OllamaChatRequestBuilder withImageURLConnectTimeoutSeconds(int imageURLConnectTimeoutSeconds) { + this.imageURLConnectTimeoutSeconds = imageURLConnectTimeoutSeconds; + return this; + } + + public OllamaChatRequestBuilder withImageURLReadTimeoutSeconds(int imageURLReadTimeoutSeconds) { + this.imageURLReadTimeoutSeconds = imageURLReadTimeoutSeconds; + return this; + } + private OllamaChatRequestBuilder(String model, List messages) { request = new OllamaChatRequest(model, false, messages); } @@ -72,11 +84,11 @@ public class OllamaChatRequestBuilder { binaryImages = new ArrayList<>(); for (String imageUrl : imageUrls) { try { - binaryImages.add(Utils.loadImageBytesFromUrl(imageUrl)); - } catch (URISyntaxException e) { - LOG.warn("URL '{}' could not be accessed, will not add to message!", imageUrl, e); + binaryImages.add(Utils.loadImageBytesFromUrl(imageUrl, imageURLConnectTimeoutSeconds, imageURLReadTimeoutSeconds)); } catch (IOException e) { LOG.warn("Content of URL '{}' could not be read, will not add to message!", imageUrl, e); + } catch (InterruptedException e) { + LOG.warn("Loading image from URL '{}' was interrupted, will not add to message!", imageUrl, e); } } } diff --git a/src/main/java/io/github/ollama4j/utils/Utils.java b/src/main/java/io/github/ollama4j/utils/Utils.java index 6d2aa5e..0c6f000 100644 --- a/src/main/java/io/github/ollama4j/utils/Utils.java +++ b/src/main/java/io/github/ollama4j/utils/Utils.java @@ -2,17 +2,20 @@ package io.github.ollama4j.utils; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Objects; public class Utils { + private static final Logger LOG = LoggerFactory.getLogger(Utils.class); private static ObjectMapper objectMapper; @@ -24,21 +27,32 @@ public class Utils { return objectMapper; } - public static byte[] loadImageBytesFromUrl(String imageUrl) - throws IOException, URISyntaxException { - URL url = new URI(imageUrl).toURL(); - try (InputStream in = url.openStream(); - ByteArrayOutputStream out = new ByteArrayOutputStream()) { - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - return out.toByteArray(); + public static byte[] loadImageBytesFromUrl(String imageUrl, int connectTimeoutSeconds, int readTimeoutSeconds) + throws IOException, InterruptedException { + LOG.debug("Attempting to load image from URL: {} (connectTimeout={}s, readTimeout={}s)", imageUrl, connectTimeoutSeconds, readTimeoutSeconds); + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(connectTimeoutSeconds)) + .build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(imageUrl)) + .timeout(Duration.ofSeconds(readTimeoutSeconds)) + .header("User-Agent", "Mozilla/5.0") + .GET() + .build(); + LOG.debug("Sending HTTP GET request to {}", imageUrl); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); + LOG.debug("Received HTTP response with status code: {}", response.statusCode()); + if (response.statusCode() >= 200 && response.statusCode() < 300) { + LOG.debug("Successfully loaded image from URL: {} ({} bytes)", imageUrl, response.body().length); + return response.body(); + } else { + LOG.error("Failed to load image from URL: {}. HTTP status: {}", imageUrl, response.statusCode()); + throw new IOException("Failed to load image: HTTP " + response.statusCode()); } } public static File getFileFromClasspath(String fileName) { + LOG.debug("Trying to load file from classpath: {}", fileName); ClassLoader classLoader = Utils.class.getClassLoader(); return new File(Objects.requireNonNull(classLoader.getResource(fileName)).getFile()); } diff --git a/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java index 20ab81c..6c06864 100644 --- a/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java +++ b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java @@ -33,17 +33,22 @@ class TestOllamaChatRequestBuilder { @Test void testImageUrlFailuresAreIgnoredAndDoNotBreakBuild() { - // Provide clearly invalid URL, builder logs a warning and continues - OllamaChatRequest req = OllamaChatRequestBuilder.getInstance("m") - .withMessage(OllamaChatMessageRole.USER, "hi", Collections.emptyList(), - "ht!tp://invalid url \n not a uri") + // Provide a syntactically invalid URL, but catch the expected exception to verify builder robustness + OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance("m"); + try { + builder.withMessage(OllamaChatMessageRole.USER, "hi", Collections.emptyList(), + "ht!tp://invalid url \n not a uri"); + fail("Expected IllegalArgumentException due to malformed URL"); + } catch (IllegalArgumentException e) { + // Expected: malformed URL should throw IllegalArgumentException + } + // The builder should still be usable after the exception + OllamaChatRequest req = builder.withMessage(OllamaChatMessageRole.USER, "hello", Collections.emptyList()) .build(); assertNotNull(req.getMessages()); assertEquals(1, req.getMessages().size()); OllamaChatMessage msg = req.getMessages().get(0); - // images list will be initialized only if any valid URL was added; for invalid URL list can be null - // We just assert that builder didn't crash and message is present with content - assertEquals("hi", msg.getContent()); + assertEquals("hello", msg.getContent()); } }