CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-ollama-deployment

Quarkus extension deployment module for integrating Ollama LLM models with Quarkus applications through the LangChain4j framework

Overview
Eval results
Files

native-image-support.mddocs/

Native Image Support

The Quarkus LangChain4j Ollama Deployment module configures GraalVM native image compilation support through the nativeSupport build step. This ensures that all necessary classes, service providers, and reflection configurations are properly registered for native image compilation.

Native Support Build Step

package io.quarkiverse.langchain4j.ollama.deployment;

import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.smallrye.config.ConfigSourceInterceptor;

public class OllamaProcessor {
    @BuildStep
    void nativeSupport(
        BuildProducer<ServiceProviderBuildItem> serviceProviderProducer,
        BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer,
        BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchyProducer
    ) {
        // Register all ConfigSourceInterceptor service providers
        serviceProviderProducer.produce(
            ServiceProviderBuildItem.allProvidersFromClassPath(
                ConfigSourceInterceptor.class.getName()
            )
        );

        // Register OllamaChatRequest for reflection
        reflectiveHierarchyProducer.produce(
            ReflectiveHierarchyBuildItem.builder(
                "dev.langchain4j.model.ollama.OllamaChatRequest"
            )
            .source(getClass().getSimpleName())
            .build()
        );

        // Register OllamaChatResponse for reflection
        reflectiveHierarchyProducer.produce(
            ReflectiveHierarchyBuildItem.builder(
                "dev.langchain4j.model.ollama.OllamaChatResponse"
            )
            .source(getClass().getSimpleName())
            .ignoreNested(false)
            .build()
        );

        // Register serializers/deserializers for reflection
        reflectiveClassProducer.produce(
            ReflectiveClassBuildItem.builder(
                "dev.langchain4j.model.ollama.FormatSerializer",
                "dev.langchain4j.model.ollama.OllamaDateDeserializer"
            )
            .constructors()
            .methods(false)
            .fields(false)
            .build()
        );
    }
}

Purpose: Registers classes, service providers, and hierarchies for GraalVM native image compilation.

Produces:

  • ServiceProviderBuildItem - Service provider registrations
  • ReflectiveClassBuildItem - Individual class registrations
  • ReflectiveHierarchyBuildItem - Class hierarchy registrations

Service Provider Registration

ConfigSourceInterceptor Providers

serviceProviderProducer.produce(
    ServiceProviderBuildItem.allProvidersFromClassPath(
        ConfigSourceInterceptor.class.getName()
    )
);

Target Interface: io.smallrye.config.ConfigSourceInterceptor

Purpose: Registers all implementations of ConfigSourceInterceptor found on the classpath.

Description: This registration ensures that all configuration source interceptors are available in the native image. Configuration source interceptors are used by SmallRye Config to intercept and modify configuration values during runtime. Without this registration, custom configuration interceptors would not be discovered in native images.

Why Needed: Service providers in native images must be explicitly registered because native image compilation does not support classpath scanning. This registration makes the service provider mechanism work in native mode.

Reflective Hierarchy Registration

OllamaChatRequest

reflectiveHierarchyProducer.produce(
    ReflectiveHierarchyBuildItem.builder(
        "dev.langchain4j.model.ollama.OllamaChatRequest"
    )
    .source(getClass().getSimpleName())
    .build()
);

Class: dev.langchain4j.model.ollama.OllamaChatRequest

Hierarchy: Full class hierarchy (all fields, methods, constructors)

Nested Types: Default behavior (nested types included)

Purpose: Enables reflection access to the Ollama chat request class and its hierarchy.

Description: The OllamaChatRequest class represents the request payload sent to the Ollama API. It needs reflection support because:

  1. JSON Serialization: Jackson serializes this class to JSON when making API requests
  2. Dynamic Access: The serializer uses reflection to access fields and nested objects
  3. Type Information: Generics and type parameters need to be preserved

Why Full Hierarchy: The request class contains nested objects and complex type hierarchies that must be accessible via reflection for proper serialization.

OllamaChatResponse

reflectiveHierarchyProducer.produce(
    ReflectiveHierarchyBuildItem.builder(
        "dev.langchain4j.model.ollama.OllamaChatResponse"
    )
    .source(getClass().getSimpleName())
    .ignoreNested(false)
    .build()
);

Class: dev.langchain4j.model.ollama.OllamaChatResponse

Hierarchy: Full class hierarchy (all fields, methods, constructors)

Nested Types: Explicitly included (ignoreNested(false))

Purpose: Enables reflection access to the Ollama chat response class and all nested types.

Description: The OllamaChatResponse class represents the response payload received from the Ollama API. It needs reflection support because:

  1. JSON Deserialization: Jackson deserializes JSON responses into this class
  2. Dynamic Access: The deserializer uses reflection to instantiate and populate objects
  3. Nested Objects: The response contains nested objects that must be accessible
  4. Type Preservation: Complex type hierarchies need reflection support

Nested Types Handling: The explicit ignoreNested(false) ensures that all nested classes within the response hierarchy are also registered for reflection. This is critical because API responses often contain nested data structures.

Reflective Class Registration

Serializers and Deserializers

reflectiveClassProducer.produce(
    ReflectiveClassBuildItem.builder(
        "dev.langchain4j.model.ollama.FormatSerializer",
        "dev.langchain4j.model.ollama.OllamaDateDeserializer"
    )
    .constructors()
    .methods(false)
    .fields(false)
    .build()
);

Classes:

  • dev.langchain4j.model.ollama.FormatSerializer
  • dev.langchain4j.model.ollama.OllamaDateDeserializer

Reflection Scope:

  • Constructors: Enabled - Required for instantiation
  • Methods: Disabled - Not accessed via reflection
  • Fields: Disabled - Not accessed via reflection

Purpose: Enables reflection access to Jackson custom serializers and deserializers.

Description: These classes provide custom serialization/deserialization logic for specific fields in Ollama request/response objects:

  1. FormatSerializer: Handles custom serialization of the format field in requests
  2. OllamaDateDeserializer: Handles custom deserialization of date/time fields in responses

Why Constructor-Only: These classes are instantiated by Jackson's serialization framework, which needs to call their constructors via reflection. However, the actual serialization/deserialization logic is called directly (not via reflection), so method and field access is not needed.

Build Item Types

ServiceProviderBuildItem

// Produced by nativeSupport build step
ServiceProviderBuildItem.allProvidersFromClassPath(String serviceInterfaceName)

Purpose: Registers service provider implementations for native image.

Parameters:

  • serviceInterfaceName - Fully qualified name of the service interface

Effect: All implementations found via META-INF/services/ files are registered in the native image.

ReflectiveClassBuildItem

// Builder pattern for reflective class registration
ReflectiveClassBuildItem.builder(String... classNames)
    .constructors()      // Enable constructor reflection
    .methods(boolean)    // Enable/disable method reflection
    .fields(boolean)     // Enable/disable field reflection
    .build()

Purpose: Registers specific classes for reflection with fine-grained control.

Options:

  • constructors() - Enable constructor reflection
  • methods(boolean) - Enable/disable method reflection
  • fields(boolean) - Enable/disable field reflection

Use Case: For classes that need selective reflection support (e.g., only constructors).

ReflectiveHierarchyBuildItem

// Builder pattern for reflective hierarchy registration
ReflectiveHierarchyBuildItem.builder(String className)
    .source(String)           // Source identifier for debugging
    .ignoreNested(boolean)    // Include/exclude nested types
    .build()

Purpose: Registers a class and its entire hierarchy for reflection.

Options:

  • source(String) - Identifies the source of the registration for debugging
  • ignoreNested(boolean) - Controls whether nested types are included

Use Case: For complex data classes with nested objects and type hierarchies.

Native Image Reflection Configuration

The build items produced by the nativeSupport build step result in the following native image reflection configuration:

Request/Response Classes

[
  {
    "name": "dev.langchain4j.model.ollama.OllamaChatRequest",
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true,
    "allDeclaredFields": true,
    "allPublicConstructors": true,
    "allPublicMethods": true,
    "allPublicFields": true
  },
  {
    "name": "dev.langchain4j.model.ollama.OllamaChatResponse",
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true,
    "allDeclaredFields": true,
    "allPublicConstructors": true,
    "allPublicMethods": true,
    "allPublicFields": true
  }
]

Serializers/Deserializers

[
  {
    "name": "dev.langchain4j.model.ollama.FormatSerializer",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true
  },
  {
    "name": "dev.langchain4j.model.ollama.OllamaDateDeserializer",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true
  }
]

Service Providers

{
  "services": [
    {
      "interface": "io.smallrye.config.ConfigSourceInterceptor",
      "providers": [
        "... all implementations found on classpath ..."
      ]
    }
  ]
}

Why Native Image Configuration Is Needed

Reflection Limitations

GraalVM native image compilation performs static analysis at build time to determine which classes and methods are reachable. Reflection usage breaks this static analysis because:

  1. Dynamic Class Loading: Classes loaded via Class.forName() are not statically visible
  2. Dynamic Method Invocation: Methods called via Method.invoke() are not statically visible
  3. Dynamic Field Access: Fields accessed via Field.get/set() are not statically visible

JSON Serialization Requirements

Jackson (the JSON library used by Ollama REST client) heavily uses reflection for:

  1. Object Instantiation: Creating instances via constructors
  2. Field Access: Reading and writing object fields
  3. Method Invocation: Calling getters and setters
  4. Type Introspection: Discovering types and annotations

Without proper reflection configuration, Jackson would fail at runtime in native images.

Service Provider Limitations

The Java Service Provider Interface (SPI) uses META-INF/services/ files to discover implementations. In native images:

  1. No Classpath Scanning: Native images don't support runtime classpath scanning
  2. Static Registration: All service providers must be registered at build time
  3. Explicit Declaration: Each implementation must be explicitly listed

Testing Native Image Support

To verify native image support works correctly:

  1. Build a native image with the Ollama extension:
mvn clean package -Pnative
  1. Run the native executable:
./target/my-app-1.0.0-runner
  1. Test Ollama functionality:

    • Verify chat models can be created
    • Verify embedding models can be created
    • Verify API requests/responses work correctly
    • Verify configuration is loaded properly
  2. Check for reflection errors:

    • No ClassNotFoundException at runtime
    • No NoSuchMethodException at runtime
    • No IllegalAccessException at runtime

Common Issues and Solutions

Issue: Missing Reflection Configuration

Symptom: ClassNotFoundException or NoSuchMethodException at runtime in native image

Solution: Add the missing class to the reflection configuration in the nativeSupport build step

Example:

reflectiveClassProducer.produce(
    ReflectiveClassBuildItem.builder("com.example.MissingClass")
        .constructors()
        .methods()
        .fields()
        .build()
);

Issue: Service Provider Not Found

Symptom: Service provider implementations not discovered in native image

Solution: Ensure service providers are registered via ServiceProviderBuildItem

Example:

serviceProviderProducer.produce(
    ServiceProviderBuildItem.allProvidersFromClassPath(
        "com.example.ServiceInterface"
    )
);

Issue: Nested Types Not Accessible

Symptom: IllegalAccessException for nested classes in native image

Solution: Use ReflectiveHierarchyBuildItem with ignoreNested(false)

Example:

reflectiveHierarchyProducer.produce(
    ReflectiveHierarchyBuildItem.builder("com.example.ParentClass")
        .ignoreNested(false)
        .build()
);

Notes

  • Native image reflection configuration is only needed for classes accessed via reflection
  • Quarkus automatically handles most reflection configuration for standard patterns
  • Custom serializers/deserializers often need explicit registration
  • Service providers must always be explicitly registered in native images
  • The source() parameter helps debug which extension registered a class for reflection
  • Over-registering classes for reflection increases native image size but is safe
  • Under-registering classes causes runtime failures that are hard to debug

Install with Tessl CLI

npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-ollama-deployment@1.7.0

docs

architecture.md

build-step-processing.md

build-time-configuration.md

devservices.md

index.md

native-image-support.md

runtime-configuration.md

runtime-model-types.md

synthetic-beans.md

tile.json