Files
ollama4j/src/test/java/io/github/ollama4j/integrationtests/WithAuth.java
Amith Koujalgi 074ac712ca Refactor Ollama API to use ThinkMode enum for "think" parameter
- Addresses #231
- Updated Ollama class and related methods to replace boolean "think" with ThinkMode enum for better clarity and control over thinking levels.
- Modified MetricsRecorder to accept ThinkMode instead of boolean for metrics recording.
- Adjusted OllamaChatRequest and OllamaGenerateRequest to utilize ThinkMode, including serialization support.
- Updated integration and unit tests to reflect changes in the "think" parameter handling.
- Introduced ThinkMode and ThinkModeSerializer classes to manage the new thinking parameter structure.
2025-11-07 15:17:22 +05:30

228 lines
8.8 KiB
Java

/*
* Ollama4j - Java library for interacting with Ollama server.
* Copyright (c) 2025 Amith Koujalgi and contributors.
*
* Licensed under the MIT License (the "License");
* you may not use this file except in compliance with the License.
*
*/
package io.github.ollama4j.integrationtests;
import static org.junit.jupiter.api.Assertions.*;
import io.github.ollama4j.Ollama;
import io.github.ollama4j.exceptions.OllamaException;
import io.github.ollama4j.models.generate.OllamaGenerateRequest;
import io.github.ollama4j.models.generate.OllamaGenerateStreamObserver;
import io.github.ollama4j.models.request.ThinkMode;
import io.github.ollama4j.models.response.OllamaResult;
import io.github.ollama4j.samples.AnnotatedTool;
import io.github.ollama4j.tools.annotations.OllamaToolService;
import io.github.ollama4j.utils.OptionsBuilder;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.NginxContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.ollama.OllamaContainer;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
@OllamaToolService(providers = {AnnotatedTool.class})
@TestMethodOrder(OrderAnnotation.class)
@SuppressWarnings({
"HttpUrlsUsage",
"SpellCheckingInspection",
"resource",
"ResultOfMethodCallIgnored"
})
public class WithAuth {
private static final Logger LOG = LoggerFactory.getLogger(WithAuth.class);
private static final int NGINX_PORT = 80;
private static final int OLLAMA_INTERNAL_PORT = 11434;
private static final String OLLAMA_VERSION = "0.6.1";
private static final String NGINX_VERSION = "nginx:1.23.4-alpine";
private static final String BEARER_AUTH_TOKEN = "secret-token";
private static final String GENERAL_PURPOSE_MODEL = "gemma3:270m";
private static OllamaContainer ollama;
private static GenericContainer<?> nginx;
private static Ollama api;
@BeforeAll
static void setUp() {
ollama = createOllamaContainer();
ollama.start();
nginx = createNginxContainer(ollama.getMappedPort(OLLAMA_INTERNAL_PORT));
nginx.start();
LOG.info("Using Testcontainer Ollama host...");
api = new Ollama("http://" + nginx.getHost() + ":" + nginx.getMappedPort(NGINX_PORT));
api.setRequestTimeoutSeconds(120);
api.setNumberOfRetriesForModelPull(3);
String ollamaUrl =
"http://" + ollama.getHost() + ":" + ollama.getMappedPort(OLLAMA_INTERNAL_PORT);
String nginxUrl = "http://" + nginx.getHost() + ":" + nginx.getMappedPort(NGINX_PORT);
LOG.info(
"The Ollama service is now accessible via the Nginx proxy with bearer-auth"
+ " authentication mode.\n"
+ "→ Ollama URL: {}\n"
+ "→ Proxy URL: {}",
ollamaUrl,
nginxUrl);
LOG.info("Ollama initialized with bearer auth token: {}", BEARER_AUTH_TOKEN);
}
private static OllamaContainer createOllamaContainer() {
return new OllamaContainer("ollama/ollama:" + OLLAMA_VERSION)
.withExposedPorts(OLLAMA_INTERNAL_PORT);
}
private static String generateNginxConfig(int ollamaPort) {
return String.format(
"events {}\n"
+ "\n"
+ "http {\n"
+ " server {\n"
+ " listen 80;\n"
+ "\n"
+ " location / {\n"
+ " set $auth_header $http_authorization;\n"
+ "\n"
+ " if ($auth_header != \"Bearer secret-token\") {\n"
+ " return 401;\n"
+ " }\n"
+ "\n"
+ " proxy_pass http://host.docker.internal:%s/;\n"
+ " proxy_set_header Host $host;\n"
+ " proxy_set_header X-Real-IP $remote_addr;\n"
+ " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n"
+ " proxy_set_header X-Forwarded-Proto $scheme;\n"
+ " }\n"
+ " }\n"
+ "}\n",
ollamaPort);
}
public static GenericContainer<?> createNginxContainer(int ollamaPort) {
File nginxConf;
try {
File tempDir = new File(System.getProperty("java.io.tmpdir"), "nginx-auth");
if (!tempDir.exists()) tempDir.mkdirs();
nginxConf = new File(tempDir, "nginx.conf");
try (FileWriter writer = new FileWriter(nginxConf)) {
writer.write(generateNginxConfig(ollamaPort));
}
return new NginxContainer<>(DockerImageName.parse(NGINX_VERSION))
.withExposedPorts(NGINX_PORT)
.withCopyFileToContainer(
MountableFile.forHostPath(nginxConf.getAbsolutePath()),
"/etc/nginx/nginx.conf")
.withExtraHost("host.docker.internal", "host-gateway")
.waitingFor(
Wait.forHttp("/")
.forStatusCode(401)
.withStartupTimeout(Duration.ofSeconds(30)));
} catch (IOException e) {
throw new RuntimeException("Failed to create nginx.conf", e);
}
}
@Test
@Order(1)
void testOllamaBehindProxy() {
api.setBearerAuth(BEARER_AUTH_TOKEN);
try {
assertTrue(
api.ping(),
"Expected Ollama to successfully ping through NGINX with valid auth token.");
} catch (Exception e) {
fail("Exception occurred while pinging Ollama through NGINX: " + e.getMessage(), e);
}
}
@Test
@Order(1)
void testWithWrongToken() {
api.setBearerAuth("wrong-token");
try {
assertFalse(
api.ping(),
"Expected Ollama ping to fail through NGINX with an invalid auth token.");
} catch (Exception e) {
// If an exception is thrown, that's also an expected failure for a wrong token
// (e.g., OllamaBaseException or IOException)
// Optionally, you can assert the type/message of the exception if needed
// For now, we treat any exception as a pass for this negative test
}
}
@Test
@Order(2)
void testAskModelWithStructuredOutput() throws OllamaException, IOException {
api.setBearerAuth(BEARER_AUTH_TOKEN);
String model = GENERAL_PURPOSE_MODEL;
api.pullModel(model);
String prompt =
"The sun is shining brightly and is directly overhead at the zenith, casting my"
+ " shadow over my foot, so it must be noon.";
Map<String, Object> format = new HashMap<>();
format.put("type", "object");
format.put(
"properties",
new HashMap<String, Object>() {
{
put(
"isNoon",
new HashMap<String, Object>() {
{
put("type", "boolean");
}
});
}
});
format.put("required", List.of("isNoon"));
OllamaGenerateRequest request =
OllamaGenerateRequest.builder()
.withModel(model)
.withPrompt(prompt)
.withRaw(false)
.withThink(ThinkMode.DISABLED)
.withStreaming(false)
.withImages(new ArrayList<>())
.withOptions(new OptionsBuilder().build())
.withFormat(format)
.build();
OllamaGenerateStreamObserver handler = null;
OllamaResult result = api.generate(request, handler);
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
assertNotNull(result.getStructuredResponse().get("isNoon"));
}
}