or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
mavenpkg:maven/io.quarkiverse.langchain4j/quarkus-langchain4j-core@1.5.x

docs

index.md
tile.json

tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-core

tessl install tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-core@1.5.0

Quarkus LangChain4j Core provides runtime integration for LangChain4j with the Quarkus framework, enabling declarative AI service creation through CDI annotations.

authentication.mddocs/reference/

Authentication

Authentication support enables custom credential providers for model API calls, with CDI-aware resolution and per-model authentication.

Capabilities

ModelAuthProvider Interface

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);
}

Implementation Examples

Simple Bearer Token Provider

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/v1

Dynamic Token Provider with Refresh

import 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.

Request-Aware Auth Provider

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.

Multi-Provider Auth with Named Models

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=llama2

Rotating Credentials Provider

import 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.

Vault-Based Auth Provider

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) {}
}

Usage with AI Services

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:

  1. AI service method is invoked
  2. Framework determines which model to use (from modelName)
  3. Framework calls ModelAuthProvider.resolve(modelName)
  4. CDI resolves provider with matching @ModelName qualifier
  5. Provider's getAuthorization() is called before HTTP request
  6. Authorization header is added to request
  7. Request is sent to model API

Programmatic Resolution

Resolve 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
    }
}

Advanced Patterns

Pattern: Fallback Credentials

@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;
    }
}

Pattern: Per-Tenant Authentication

@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;
    }
}

Pattern: Signature-Based Auth

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);
        }
    }
}

Pattern: Header-Based Auth (Non-Authorization)

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.

Error Handling

Handling Auth Failures

@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:

  • Exceptions from getAuthorization() propagate to the model request
  • Request fails immediately without being sent
  • Error is visible to the AI service caller
  • No automatic retry by the framework

Retry Logic with Circuit Breaker

import 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=3

Use Cases

Custom authentication is useful for:

  • Token Refresh: Automatically refresh expired tokens
  • Multi-Provider: Different auth schemes for different model providers
  • Enterprise Integration: Integrate with corporate auth systems (OAuth2, SAML, etc.)
  • Request Signing: Sign requests with HMAC or other schemes
  • Rate Limiting: Track usage per API key, rotate keys to distribute load
  • Audit Logging: Log which credentials are used for each request
  • Fallback Credentials: Try multiple credentials if one fails
  • Environment-Specific: Different credentials for dev/staging/prod
  • Multi-Tenancy: Different credentials per tenant/customer
  • Secret Management: Integration with vault services (HashiCorp Vault, AWS Secrets Manager)
  • Cost Tracking: Use different keys per department for cost allocation
  • Geographic Routing: Different keys for different regions/endpoints

Testing

Unit Testing Auth Providers

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(); }
        };
    }
}

Integration Testing with Mock Provider

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";
        }
    }
}

Security Best Practices

  1. Never Hardcode Credentials: Always use configuration or secret management
  2. Use Encrypted Configuration: Encrypt API keys in configuration files
  3. Rotate Credentials Regularly: Implement token refresh and key rotation
  4. Limit Credential Scope: Use least-privilege credentials
  5. Monitor for Leaks: Watch for credentials in logs or error messages
  6. Implement Rate Limiting: Protect against credential abuse
  7. Use Short-Lived Tokens: Prefer tokens with expiration over long-lived keys
  8. Audit Access: Log credential usage for security auditing
  9. Secure Storage: Use secure vaults for credential storage
  10. Fail Securely: Don't expose credential details in error messages

Performance Considerations

  • Caching: Cache tokens with appropriate TTL to reduce auth service calls
  • Async Refresh: Refresh tokens proactively before expiration
  • Connection Pooling: Reuse connections to auth services
  • Circuit Breakers: Protect against auth service failures
  • Timeout Configuration: Set appropriate timeouts for auth calls
  • Thread Safety: Ensure providers are thread-safe for concurrent access

Troubleshooting

Issue: "No ModelAuthProvider found"

Cause: No CDI bean with matching @ModelName qualifier Solution: Ensure provider class has @ApplicationScoped and correct @ModelName

Issue: "Authorization header rejected by API"

Cause: Incorrect header format or expired token Solution: Check API documentation for correct format, implement token refresh

Issue: "Auth provider returns null"

Cause: Provider returned null or empty string Solution: Ensure provider always returns valid authorization value, or handle missing credentials

Issue: "Slow model requests"

Cause: Auth provider making synchronous calls to external services Solution: Implement caching, use async refresh, optimize auth service calls

Issue: "Different models using same credentials"

Cause: No model-specific auth provider, default provider used Solution: Create separate providers with appropriate @ModelName qualifiers