tessl install tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-core@1.5.0Quarkus LangChain4j Core provides runtime integration for LangChain4j with the Quarkus framework, enabling declarative AI service creation through CDI annotations.
Authentication support enables custom credential providers for model API calls, with CDI-aware resolution and per-model authentication.
Interface for supplying authentication credentials to model API calls.
// Package: io.quarkiverse.langchain4j.auth
/**
* Provider for model API authentication.
* Implement to supply custom Authorization header values.
*
* Providers are CDI beans qualified with @ModelName.
* One provider can service multiple models or one provider per model.
*
* Thread Safety: Implementations must be thread-safe as they may be
* called concurrently from multiple threads.
*
* Lifecycle: Providers are invoked before each HTTP request to the model API.
* They should be fast and avoid blocking operations when possible.
*/
public interface ModelAuthProvider {
/**
* Get Authorization header value for the request.
*
* Return value format depends on the authentication scheme:
* - Bearer tokens: "Bearer <token>"
* - API keys: "Api-Key <key>" or "X-API-Key <key>"
* - Basic auth: "Basic <base64credentials>"
* - Custom schemes: "<scheme> <credentials>"
*
* Exceptions thrown from this method will fail the model request.
* Consider catching and handling exceptions internally for resilience.
*
* @param input HTTP request information (method, URI, headers)
* @return Authorization header value (complete header value, not just the token)
* @throws RuntimeException if authentication cannot be provided
*/
String getAuthorization(Input input);
/**
* HTTP request input information.
* Provides context about the outgoing request to help determine authentication.
*/
interface Input {
/**
* HTTP method (GET, POST, etc.)
* Most model APIs use POST for completions.
*
* @return HTTP method string
*/
String method();
/**
* Request URI including scheme, host, port, and path.
* Query parameters may or may not be included.
*
* Example: "https://api.openai.com/v1/chat/completions"
*
* @return Full request URI
*/
URI uri();
/**
* Request headers already set on the request.
* Does not include the Authorization header (that's what we're providing).
*
* Header names are case-insensitive.
* Header values are lists to support multiple values.
*
* @return Immutable map of headers
*/
Map<String, List<String>> headers();
}
/**
* Resolve ModelAuthProvider for a named model.
* Uses CDI to find provider with @ModelName qualifier.
*
* Resolution order:
* 1. Provider with exact @ModelName match
* 2. Default provider (no @ModelName qualifier)
* 3. Throws exception if none found
*
* This method is primarily used internally by the framework.
* Application code typically doesn't need to call this.
*
* @param modelName The model name to resolve provider for
* @return The authentication provider for this model
* @throws IllegalStateException if no provider found
*/
static ModelAuthProvider resolve(String modelName);
}import io.quarkiverse.langchain4j.ModelName;
import io.quarkiverse.langchain4j.auth.ModelAuthProvider;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@ApplicationScoped
@ModelName("openai-gpt4")
public class OpenAIAuthProvider implements ModelAuthProvider {
@ConfigProperty(name = "openai.api.key")
String apiKey;
@Override
public String getAuthorization(Input input) {
return "Bearer " + apiKey;
}
}Configuration:
openai.api.key=${OPENAI_API_KEY}Model Configuration:
quarkus.langchain4j.openai.openai-gpt4.model-name=gpt-4
quarkus.langchain4j.openai.openai-gpt4.base-url=https://api.openai.com/v1import io.quarkiverse.langchain4j.ModelName;
import io.quarkiverse.langchain4j.auth.ModelAuthProvider;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.Instant;
import java.util.concurrent.locks.ReentrantLock;
@ApplicationScoped
@ModelName("enterprise-model")
public class RefreshableAuthProvider implements ModelAuthProvider {
private volatile String cachedToken;
private volatile Instant tokenExpiry;
private final ReentrantLock refreshLock = new ReentrantLock();
@Override
public String getAuthorization(Input input) {
// Check if token is expired or will expire soon (5 minute buffer)
if (tokenExpiry == null || Instant.now().plusSeconds(300).isAfter(tokenExpiry)) {
refreshToken();
}
return "Bearer " + cachedToken;
}
private void refreshToken() {
// Only one thread should refresh at a time
refreshLock.lock();
try {
// Double-check after acquiring lock
if (tokenExpiry == null || Instant.now().plusSeconds(300).isAfter(tokenExpiry)) {
TokenResponse response = fetchNewToken();
this.cachedToken = response.token();
this.tokenExpiry = Instant.now().plusSeconds(response.expiresIn());
}
} finally {
refreshLock.unlock();
}
}
private TokenResponse fetchNewToken() {
// Call authentication service to get new token
// This would typically call an OAuth2 token endpoint
try {
// Example: RestClient call to auth service
return authClient.getToken(clientId, clientSecret);
} catch (Exception e) {
throw new RuntimeException("Failed to refresh authentication token", e);
}
}
record TokenResponse(String token, long expiresIn) {}
}Use Case: Enterprise systems with short-lived OAuth2 tokens that need automatic refresh.
Error Handling: If token refresh fails, the exception propagates and the model request fails. Consider implementing fallback logic or circuit breaker patterns.
import io.quarkiverse.langchain4j.auth.ModelAuthProvider;
import jakarta.enterprise.context.ApplicationScoped;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ApplicationScoped
public class RequestAwareAuthProvider implements ModelAuthProvider {
private final Map<String, String> pathToKeyMap = new ConcurrentHashMap<>();
public RequestAwareAuthProvider() {
// Initialize path-based API keys
pathToKeyMap.put("/v1/completions", getConfig("completions.api.key"));
pathToKeyMap.put("/v1/embeddings", getConfig("embeddings.api.key"));
pathToKeyMap.put("/v1/chat/completions", getConfig("chat.api.key"));
}
@Override
public String getAuthorization(Input input) {
URI uri = input.uri();
String path = uri.getPath();
// Select API key based on endpoint
String apiKey = pathToKeyMap.entrySet().stream()
.filter(e -> path.contains(e.getKey()))
.map(Map.Entry::getValue)
.findFirst()
.orElse(getConfig("default.api.key"));
// Check request method for special handling
if ("GET".equals(input.method())) {
// Some APIs use different auth for GET requests
return "Api-Key " + apiKey;
}
return "Bearer " + apiKey;
}
private String getConfig(String key) {
return ConfigProvider.getConfig().getValue("auth." + key, String.class);
}
}Use Case: APIs that require different credentials for different endpoints or operations.
import io.quarkiverse.langchain4j.ModelName;
import io.quarkiverse.langchain4j.auth.ModelAuthProvider;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
// OpenAI Provider
@ApplicationScoped
@ModelName("openai")
public class OpenAIAuthProvider implements ModelAuthProvider {
@ConfigProperty(name = "openai.api.key")
String apiKey;
@Override
public String getAuthorization(Input input) {
return "Bearer " + apiKey;
}
}
// Anthropic Provider
@ApplicationScoped
@ModelName("anthropic")
public class AnthropicAuthProvider implements ModelAuthProvider {
@ConfigProperty(name = "anthropic.api.key")
String apiKey;
@Override
public String getAuthorization(Input input) {
// Anthropic uses x-api-key header scheme
return "x-api-key " + apiKey;
}
}
// Azure OpenAI Provider
@ApplicationScoped
@ModelName("azure")
public class AzureAuthProvider implements ModelAuthProvider {
@ConfigProperty(name = "azure.api.key")
String apiKey;
@Override
public String getAuthorization(Input input) {
// Azure uses api-key header scheme
return "api-key " + apiKey;
}
}
// Custom/Local Model Provider
@ApplicationScoped
@ModelName("local")
public class LocalAuthProvider implements ModelAuthProvider {
@Override
public String getAuthorization(Input input) {
// Local model doesn't require authentication
return null; // or return empty string
}
}Configuration:
openai.api.key=${OPENAI_API_KEY}
anthropic.api.key=${ANTHROPIC_API_KEY}
azure.api.key=${AZURE_API_KEY}
quarkus.langchain4j.openai.openai.model-name=gpt-4
quarkus.langchain4j.anthropic.anthropic.model-name=claude-3-opus-20240229
quarkus.langchain4j.azure.azure.model-name=gpt-4
quarkus.langchain4j.ollama.local.model-name=llama2import io.quarkiverse.langchain4j.ModelName;
import io.quarkiverse.langchain4j.auth.ModelAuthProvider;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@ApplicationScoped
@ModelName("rotating")
public class RotatingAuthProvider implements ModelAuthProvider {
private final List<String> apiKeys;
private final AtomicInteger currentIndex = new AtomicInteger(0);
public RotatingAuthProvider() {
// Load multiple API keys for rotation
apiKeys = List.of(
getConfig("api.key.1"),
getConfig("api.key.2"),
getConfig("api.key.3")
);
}
@Override
public String getAuthorization(Input input) {
// Round-robin through API keys to distribute load/rate limits
int index = currentIndex.getAndUpdate(i -> (i + 1) % apiKeys.size());
String apiKey = apiKeys.get(index);
return "Bearer " + apiKey;
}
private String getConfig(String key) {
return ConfigProvider.getConfig().getValue(key, String.class);
}
}Use Case: Distribute requests across multiple API keys to avoid rate limiting on a single key.
Rate Limiting Considerations: This approach helps with per-key rate limits but doesn't track which key was used for retry logic.
import io.quarkiverse.langchain4j.ModelName;
import io.quarkiverse.langchain4j.auth.ModelAuthProvider;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
@ModelName("secure")
public class VaultAuthProvider implements ModelAuthProvider {
@Inject
VaultService vaultService; // Hypothetical secret management service
@Override
public String getAuthorization(Input input) {
try {
// Retrieve secret from vault on each request
String apiKey = vaultService.getSecret("ai-model-api-key");
return "Bearer " + apiKey;
} catch (Exception e) {
throw new RuntimeException("Failed to retrieve API key from vault", e);
}
}
}Use Case: Enterprise environments with centralized secret management (HashiCorp Vault, AWS Secrets Manager, etc.)
Performance: Fetching from vault on every request adds latency. Consider caching with TTL:
@ApplicationScoped
@ModelName("secure-cached")
public class CachedVaultAuthProvider implements ModelAuthProvider {
@Inject
VaultService vaultService;
private volatile CachedSecret cachedSecret;
private static final long CACHE_TTL_SECONDS = 300; // 5 minutes
@Override
public String getAuthorization(Input input) {
CachedSecret current = cachedSecret;
long now = System.currentTimeMillis();
if (current == null || (now - current.timestamp) > (CACHE_TTL_SECONDS * 1000)) {
synchronized (this) {
current = cachedSecret;
if (current == null || (now - current.timestamp) > (CACHE_TTL_SECONDS * 1000)) {
String secret = vaultService.getSecret("ai-model-api-key");
cachedSecret = new CachedSecret(secret, now);
current = cachedSecret;
}
}
}
return "Bearer " + current.secret;
}
private static record CachedSecret(String secret, long timestamp) {}
}Auth providers are automatically resolved based on model name:
import io.quarkiverse.langchain4j.RegisterAiService;
// This will use the OpenAIAuthProvider
@RegisterAiService(modelName = "openai")
public interface OpenAIAssistant {
String chat(String message);
}
// This will use the AnthropicAuthProvider
@RegisterAiService(modelName = "anthropic")
public interface AnthropicAssistant {
String chat(String message);
}
// This will use the AzureAuthProvider
@RegisterAiService(modelName = "azure")
public interface AzureAssistant {
String chat(String message);
}Resolution Process:
modelName)ModelAuthProvider.resolve(modelName)@ModelName qualifiergetAuthorization() is called before HTTP requestResolve auth providers programmatically:
import io.quarkiverse.langchain4j.auth.ModelAuthProvider;
import java.net.URI;
import java.util.List;
import java.util.Map;
public class AuthResolver {
public void useAuthProvider() {
ModelAuthProvider provider = ModelAuthProvider.resolve("openai");
// Create input
ModelAuthProvider.Input input = new ModelAuthProvider.Input() {
@Override
public String method() { return "POST"; }
@Override
public URI uri() {
return URI.create("https://api.openai.com/v1/chat/completions");
}
@Override
public Map<String, List<String>> headers() {
return Map.of("Content-Type", List.of("application/json"));
}
};
// Get authorization
String authHeader = provider.getAuthorization(input);
// Use in HTTP request
makeRequest(authHeader);
}
private void makeRequest(String authHeader) {
// HTTP client code
}
}@ApplicationScoped
@ModelName("resilient")
public class FallbackAuthProvider implements ModelAuthProvider {
@Inject
@ConfigProperty(name = "primary.api.key")
Optional<String> primaryKey;
@Inject
@ConfigProperty(name = "backup.api.key")
String backupKey;
@Override
public String getAuthorization(Input input) {
String apiKey = primaryKey.orElse(backupKey);
return "Bearer " + apiKey;
}
}@ApplicationScoped
public class MultiTenantAuthProvider implements ModelAuthProvider {
@Inject
TenantResolver tenantResolver;
@Inject
TenantCredentialStore credentialStore;
@Override
public String getAuthorization(Input input) {
// Extract tenant from context (e.g., thread local, request scope)
String tenantId = tenantResolver.getCurrentTenant();
// Get tenant-specific credentials
String apiKey = credentialStore.getApiKey(tenantId);
return "Bearer " + apiKey;
}
}import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@ApplicationScoped
@ModelName("signed")
public class SignatureAuthProvider implements ModelAuthProvider {
@ConfigProperty(name = "api.secret")
String apiSecret;
@ConfigProperty(name = "api.key.id")
String apiKeyId;
@Override
public String getAuthorization(Input input) {
try {
// Create signature: HMAC-SHA256 of method + path + timestamp
long timestamp = System.currentTimeMillis() / 1000;
String toSign = input.method() + ":" + input.uri().getPath() + ":" + timestamp;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(apiSecret.getBytes(), "HmacSHA256");
mac.init(secretKey);
byte[] signature = mac.doFinal(toSign.getBytes());
String encodedSignature = Base64.getEncoder().encodeToString(signature);
// Return custom auth header: "Signature keyId:timestamp:signature"
return String.format("Signature %s:%d:%s", apiKeyId, timestamp, encodedSignature);
} catch (Exception e) {
throw new RuntimeException("Failed to generate request signature", e);
}
}
}Some APIs use custom headers instead of Authorization header:
// Note: This is a limitation - ModelAuthProvider only supports Authorization header
// For custom headers, you need to use a different approach:
@ApplicationScoped
public class CustomHeaderModelConfigurer {
// Use Quarkus REST Client filter or HTTP client interceptor
// to add custom headers to requests
@RegisterProvider
public class CustomHeaderFilter implements ClientHeadersFactory {
@Override
public MultivaluedMap<String, String> update(
MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders) {
clientOutgoingHeaders.add("X-API-Key", apiKey);
return clientOutgoingHeaders;
}
}
}Note: If the model API uses a custom header name (not "Authorization"), you'll need to configure this at the HTTP client level rather than using ModelAuthProvider.
@ApplicationScoped
@ModelName("robust")
public class RobustAuthProvider implements ModelAuthProvider {
private static final Logger LOG = Logger.getLogger(RobustAuthProvider.class);
@Override
public String getAuthorization(Input input) {
try {
String token = fetchToken();
if (token == null || token.isEmpty()) {
LOG.error("Failed to obtain authentication token");
throw new RuntimeException("Authentication token not available");
}
return "Bearer " + token;
} catch (Exception e) {
LOG.error("Authentication provider error", e);
throw new RuntimeException("Failed to provide authentication", e);
}
}
private String fetchToken() {
// Token fetching logic
return "token";
}
}Impact of Exceptions:
getAuthorization() propagate to the model requestimport io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@ApplicationScoped
@ModelName("resilient")
public class CircuitBreakerAuthProvider implements ModelAuthProvider {
@CircuitBreaker(name = "auth-service")
@Override
public String getAuthorization(Input input) {
return "Bearer " + fetchTokenFromExternalService();
}
private String fetchTokenFromExternalService() {
// Call to external auth service
// Circuit breaker will open if this fails repeatedly
return authService.getToken();
}
}Configuration:
quarkus.resilience4j.circuit-breaker.auth-service.failure-rate-threshold=50
quarkus.resilience4j.circuit-breaker.auth-service.wait-duration-in-open-state=10s
quarkus.resilience4j.circuit-breaker.auth-service.permitted-number-of-calls-in-half-open-state=3Custom authentication is useful for:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class OpenAIAuthProviderTest {
@Test
void testAuthorizationFormat() {
OpenAIAuthProvider provider = new OpenAIAuthProvider();
provider.apiKey = "test-key-123";
ModelAuthProvider.Input input = createInput("POST", "https://api.openai.com/v1/chat/completions");
String auth = provider.getAuthorization(input);
assertEquals("Bearer test-key-123", auth);
assertTrue(auth.startsWith("Bearer "));
}
private ModelAuthProvider.Input createInput(String method, String uri) {
return new ModelAuthProvider.Input() {
@Override
public String method() { return method; }
@Override
public URI uri() { return URI.create(uri); }
@Override
public Map<String, List<String>> headers() { return Map.of(); }
};
}
}import io.quarkus.test.junit.QuarkusTest;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
@QuarkusTest
class AssistantIntegrationTest {
@Inject
Assistant assistant;
@Test
void testWithMockAuth() {
// MockAuthProvider will be used instead of real provider
String result = assistant.chat("test");
assertNotNull(result);
}
@Alternative
@Priority(1)
@ApplicationScoped
@ModelName("openai")
public static class MockAuthProvider implements ModelAuthProvider {
@Override
public String getAuthorization(Input input) {
return "Bearer mock-token-for-testing";
}
}
}Cause: No CDI bean with matching @ModelName qualifier
Solution: Ensure provider class has @ApplicationScoped and correct @ModelName
Cause: Incorrect header format or expired token Solution: Check API documentation for correct format, implement token refresh
Cause: Provider returned null or empty string Solution: Ensure provider always returns valid authorization value, or handle missing credentials
Cause: Auth provider making synchronous calls to external services Solution: Implement caching, use async refresh, optimize auth service calls
Cause: No model-specific auth provider, default provider used
Solution: Create separate providers with appropriate @ModelName qualifiers