A unified Java AI utility library for Jakarta EE or MicroProfile applications.
OmniHai provides a single, consistent API to interact with multiple AI providers. It achieves that by interacting with their REST API endpoints directly.
- Java 17
- Jakarta EE 10 or MicroProfile 7 (JSON-P required, CDI optional, EL optional, MP Config optional)
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnihai</artifactId>
<version>1.1</version>
</dependency>On non-Jakarta EE / non-MicroProfile runtimes such as Tomcat, you'll need to manually add JSON-P and optionally CDI / MP Config dependencies:
<!-- JSON-P implementation (required) -->
<dependency>
<groupId>org.eclipse.parsson</groupId>
<artifactId>parsson</artifactId>
<version>1.1.7</version>
</dependency>
<!-- CDI implementation (optional, for @AI injection) -->
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet-shaded</artifactId>
<version>6.0.4.Final</version>
</dependency>
<!-- MP Config implementation (optional, for ${config:...} resolution in @AI attributes) -->
<dependency>
<groupId>smallrye.config</groupId>
<artifactId>smallrye-config</artifactId>
<version>3.15.1</version>
</dependency>You can technically also use it on plain Java SE, you'll still need the JSON-P implementation, but you cannot use the CDI annotation.
| Provider | Default Model | API Key Required | Available Models |
|---|---|---|---|
| OpenAI | gpt-5.2-2025-12-11 | Yes | List |
| Anthropic | claude-sonnet-4-5-20250929 | Yes | List |
| Google AI | gemini-2.5-flash | Yes | List |
| xAI | grok-4-1-fast-reasoning | Yes | List |
| Mistral | mistral-medium-2508 | Yes | List |
| Meta AI | Llama-4-Scout-17B-16E-Instruct-FP8 | Yes | List |
| Azure OpenAI | gpt-5-mini | Yes | List |
| OpenRouter | deepseek/deepseek-v3.2 | Yes | List |
| Hugging Face | google/gemma-3-27b-it | Yes | List |
| Ollama | gemma3 | No (localhost) | List |
| Custom | - | - | - |
// Create a service instance
AIService service = AIConfig.of("your-openai-api-key").createService();
// Simple chat
String response = service.chat("What is Jakarta EE?");@Inject
@AI(provider = AIProvider.ANTHROPIC, apiKey = "your-anthropic-api-key")
private AIService claude;
// Use EL expressions for dynamic configuration
@Inject
@AI(provider = AIProvider.OPENAI,
apiKey = "#{initParam['com.example.OPENAI_API_KEY']}")
private AIService gpt;
// With MicroProfile config expressions and custom system prompt
@Inject
@AI(provider = AIProvider.GOOGLE,
apiKey = "${config:google.api-key}",
prompt = "You are a helpful assistant specialized in Jakarta EE.")
private AIService jakartaExpert;
// With different model than default
@Inject
@AI(provider = AIProvider.XAI,
apiKey = "#{configBean.xaiApiKey}",
model = "grok-2-image-1212")
private AIService imageGenerator;Need diverse perspectives? OmniHai makes it easy to query multiple providers and combine their responses:
@Inject @AI(apiKey = "#{config.openaiApiKey}")
private AIService gpt;
@Inject @AI(provider = GOOGLE, apiKey = "#{config.googleApiKey}")
private AIService gemini;
@Inject @AI(provider = XAI, apiKey = "#{config.xaiApiKey}")
private AIService grok;
public String getConsensusAnswer(String question) {
var responses = Stream.of(gpt, gemini, grok)
.parallel()
.map(ai -> ai.chat(question))
.toList();
return gpt.summarize(String.join("\n\n", responses), 200);
}This pattern is useful for reducing bias, cross-validating answers, or getting a balanced summary from multiple AI perspectives.
Synchronous:
String response = service.chat("Hello!");Asynchronous:
CompletableFuture<String> future = service.chatAsync("Hello!");With options:
String response = service.chat("Explain microservices",
ChatOptions.newBuilder()
.systemPrompt("You are a helpful software architect.")
.temperature(0.5)
.maxTokens(500)
.build());Streaming:
service.chatStream(message, token -> {
// handle partial response
System.out.print(token);
}).exceptionally(e -> {
// handle exception
System.out.println("\n\nError occurred: " + e);
}).thenRun(() -> {
// handle completion
System.out.println("\n\n");
});With file attachments:
byte[] document = Files.readAllBytes(Path.of("report.pdf"));
byte[] image = Files.readAllBytes(Path.of("chart.png"));
ChatInput input = ChatInput.newBuilder()
.message("Compare these files")
.attach(document, image)
.build();
String response = service.chat(input);Multi-turn conversation with memory:
ChatOptions options = ChatOptions.newBuilder()
.systemPrompt("You are a helpful assistant.")
.withMemory()
.build();
String response1 = service.chat("My name is Bob.", options);
String response2 = service.chat("What is my name?", options); // AI remembers: "Bob"
// Access conversation history
List<ChatInput.Message> history = options.getHistory();History is maintained as a sliding window, defaulting to 20 messages (10 conversational turns). Oldest messages are automatically evicted when the limit is exceeded. You can customize the window size:
ChatOptions options = ChatOptions.newBuilder()
.withMemory(50) // Keep up to 50 messages (25 turns)
.build();File attachments are automatically tracked in history. When you upload files in a memory-enabled chat, their references are preserved across turns so the AI can continue referencing them:
ChatOptions options = ChatOptions.newBuilder()
.withMemory()
.build();
ChatInput input = ChatInput.newBuilder()
.message("Analyze this PDF")
.attach(Files.readAllBytes(Path.of("report.pdf")))
.build();
String analysis = service.chat(input, options);
String followUp = service.chat("What's on page 2?", options); // AI still has access to the PDFWhen messages slide out of the window, their associated file references are evicted as well. Uploaded files on the provider's servers are automatically cleaned up in the background after 2 days, preventing stale file accumulation. Only files uploaded by OmniHai are cleaned up.
Note: file tracking in history requires the AI provider to support a files API. This is currently the case for OpenAI(-compatible) providers, Anthropic, and Google AI.
Get typed Java objects directly from AI responses:
// Define your response structure as a record (or bean)
record ProductReview(String sentiment, int rating, List<String> pros, List<String> cons) {}
// Get a typed response in one call
ProductReview review = service.chat("Analyze this review: " + reviewText, ProductReview.class);With options:
ChatOptions options = ChatOptions.newBuilder()
.systemPrompt("You are a product review analyzer.")
.temperature(0.3)
.build();
ProductReview review = service.chat("Analyze this review: " + reviewText, options, ProductReview.class);Under the hood, OmniHai generates a JSON schema from the class, instructs the AI to return conforming JSON, and parses the response back into the typed object. You can also do this manually if you need more control:
JsonObject schema = JsonSchemaHelper.buildJsonSchema(ProductReview.class);
ChatOptions options = ChatOptions.newBuilder().jsonSchema(schema).build();
String responseJson = service.chat("Analyze this review: " + reviewText, options);
ProductReview review = JsonSchemaHelper.fromJson(responseJson, ProductReview.class);JsonSchemaHelper supports primitive types, strings, enums, temporals, collections, arrays, maps, nested types, and Optional fields (which are excluded from "required" in JSON schema).
// Summarize text
String summary = service.summarize(longText, 100); // max 100 words
// Extract key points
List<String> points = service.extractKeyPoints(text, 5); // max 5 points// Detect language
String lang = service.detectLanguage(text); // Returns ISO 639-1 code
// Translate with auto-detection
String translated = service.translate(text, null, "es");
// Translate from specific language
String translated = service.translate(text, "en", "fr");
// Proofread text (fix grammar and spelling, preserve meaning and style)
String corrected = service.proofread(text);// Basic moderation
ModerationResult result = service.moderateContent(userInput);
if (result.isFlagged()) {
// Handle violation
}
// Custom moderation options
ModerationResult result = service.moderateContent(content,
ModerationOptions.newBuilder()
.categories(Category.HATE, Category.VIOLENCE)
.threshold(0.8)
.build());// Analyze image
byte[] imageBytes = Files.readAllBytes(imagePath);
String description = service.analyzeImage(imageBytes, "Describe the product");
// Generate alt text
String altText = service.generateAltText(imageBytes);// Generate image
byte[] image = service.generateImage("A sunset over mountains");
// With options
byte[] image = service.generateImage("A modern office",
GenerateImageOptions.newBuilder()
.size("1024x1024")
.build());// Transcribe audio
byte[] audioBytes = Files.readAllBytes(audioPath);
String transcription = service.transcribe(audioBytes);All methods have async variants returning CompletableFuture (e.g., chatAsync, summarizeAsync, translateAsync, proofreadAsync, generateImageAsync, transcribeAsync, etc.).
Implement AIService or extend BaseAIService or even OpenAIService, etc.
AIService service = AIConfig.of(MyCustomAIService.class, "api-key").createService();@Inject
@AI(serviceClass = MyCustomAIService.class, apiKey = "#{config.apiKey}")
private AIService custom;You can customize how requests are built and responses are parsed by providing custom handler implementations.
// Custom OpenAI text handler for request tracking
public class TrackingTextHandler extends OpenAITextHandler {
@Override
public JsonObject buildChatPayload(AIService service, ChatInput input, ChatOptions options, boolean streaming) {
return Json.createObjectBuilder(super.buildChatPayload(service, input, options, streaming))
.add("user", getCurrentUserId())
.build();
}
}AIStrategy strategy = AIStrategy.of(TrackingTextHandler.class);
AIService service = AIConfig.of("your-api-key").withStrategy(strategy).createService();@Inject
@AI(provider = OPENAI, apiKey = "#{config.openaiApiKey}", textHandler = TrackingTextHandler.class)
private AIService trackedService;| Aspect | OmniHai | LangChain4J | Spring AI | Jakarta Agentic |
|---|---|---|---|---|
| Target Runtime | Jakarta EE / MicroProfile | Any Java | Spring | Jakarta EE |
| Philosophy | Minimal, focused utility | Comprehensive toolkit | Spring integration | Standard specification |
| Dependencies | JSON-P only (CDI/EL/MP-config optional) | Multiple modules | Spring framework | TBD (in development) |
| Learning Curve | Low | Medium-High | Medium (if Spring-familiar) | TBD |
| Feature | OmniHai | LangChain4J | Spring AI | Jakarta Agentic |
|---|---|---|---|---|
| Chat/Completion | ✅ | ✅ | ✅ | ✅ (planned) |
| Streaming | ✅ | ✅ | ✅ | TBD |
| Structured Outputs | ✅ | ✅ | ✅ | TBD |
| File Attachments | ✅ | ✅ | ✅ | TBD |
| Function Calling | ❌ | ✅ | ✅ | TBD |
| RAG Support | ❌ | ✅ (extensive) | ✅ | TBD |
| Vector Stores | ❌ | ✅ (many) | ✅ (many) | TBD |
| Embeddings | ❌ | ✅ | ✅ | TBD |
| Image Analysis | ✅ | ✅ | ✅ | TBD |
| Image Generation | ✅ | ✅ | ✅ | TBD |
| Audio Transcription | ✅ (native + fallback) | ✅ | ✅ | TBD |
| Content Moderation | ✅ (native + fallback) | ❌ (via chat) | ❌ (via chat) | TBD |
| Translation | ✅ | ❌ (via chat) | ❌ (via chat) | TBD |
| Proofreading | ✅ | ❌ (via chat) | ❌ (via chat) | TBD |
| Summarization | ✅ | ❌ (via chat) | ❌ (via chat) | TBD |
| Memory/History | ✅ | ✅ | ✅ | TBD |
| Agents | ❌ | ✅ | ✅ | ✅ (core focus) |
| Prompt Templates | ❌ | ✅ | ✅ | TBD |
| Provider | OmniHai | LangChain4J | Spring AI |
|---|---|---|---|
| OpenAI | ✅ | ✅ | ✅ |
| Anthropic | ✅ | ✅ | ✅ |
| Google AI | ✅ | ✅ | ✅ |
| xAI (Grok) | ✅ | ❌ (via OpenAI) | ❌ (via OpenAI) |
| Mistral | ✅ | ✅ | ✅ |
| Meta AI | ✅ | ❌ (via OpenAI) | ❌ (via OpenAI) |
| Azure OpenAI | ✅ | ✅ | ✅ |
| OpenRouter | ✅ | ❌ (via OpenAI) | ❌ (via OpenAI) |
| Hugging Face | ✅ | ✅ | ✅ |
| Ollama | ✅ | ✅ | ✅ |
| AWS Bedrock | ❌ | ✅ | ✅ |
| Aspect | OmniHai | LangChain4J-CDI | Spring AI |
|---|---|---|---|
| Injection Style | @Inject @AI(...) |
@Inject + config |
@Autowired + beans |
| Qualifier-based | ✅ | ❌ | ❌ |
| EL Support | ✅ #{...}, ${...} |
❌ | ❌ (SpEL, different) |
| MP Config Support | ✅ ${config:...} |
❌ | ❌ (SpEL, different) |
- Ultra-lightweight - No external HTTP library, just
java.net.http.HttpClient. Minimal deps. Transparent gzip compression for reduced bandwidth. - Built-in text utilities - Summarization, translation, transcription, proofreading, key point extraction, moderation as first-class features (not "build your own prompt")
- Structured outputs - Get typed Java objects directly from AI responses:
service.chat(message, MyRecord.class) - File attachments - Send documents, images, and other files alongside chat messages with help of
ChatInput - Native CDI with EL -
@AI(apiKey = "#{config.openaiKey}")with expression resolution - MicroProfile Config -
@AI(apiKey = "${config:openai.key}")with expression resolution - 10 providers out of the box - Including Ollama for local/offline
- Caller-owned conversation memory - History lives in
ChatOptions, not in the service. No server-side session state, no memory leaks, no lifecycle management. The caller controls it. Sliding window keeps context manageable, and uploaded file references are tracked across turns. - Automatic file cleanup - Uploaded files on provider servers are cleaned up after 2 days in a fire-and-forget background task, preventing stale file accumulation.
- Clean exception hierarchy - Specific exceptions per HTTP status
No tools, embeddings, RAG, or agents. This isn't a gap - it's a design choice. OmniHai is a utility library, not a framework.
| Library | Analogy |
|---|---|
| LangChain4J | Full kitchen with every appliance |
| Spring AI | Full kitchen, Spring-branded appliances |
| Jakarta Agentic | Kitchen building code (specification) |
| OmniHai | Sharp chef's knife - does a few things very well |
OmniHai fills a different niche. For apps that need:
- Multi-provider chat with easy switching
- Text analysis (summarize, translate, proofread, moderate)
- Image analysis (describe, generate alt text)
- Audio analysis (transcribe)
- Minimal dependencies
- Pure Jakarta EE / MicroProfile
...without needing RAG pipelines, agent frameworks, or vector stores, OmniHai is arguably the better choice. Less to learn, less to break, fewer dependencies.
If Jakarta Agentic matures, OmniHai could potentially be a lightweight implementation of parts of that spec, or remain a complementary "just the essentials" alternative.
Yes, significantly:
- OmniHai JAR: ~175 KB vs LangChain4J: ~5-10 MB (per AI provider!) — at least 35x smaller
- 73 source files, ~11,000 lines of code (~4,600 actual code, rest is javadoc)
- Zero external runtime dependencies — uses JDK's native
java.net.http.HttpClientdirectly without any SDKs - Only one required dependency: Jakarta JSON-P (which Jakarta EE and MicroProfile runtimes already have)
- Other dependencies are optional: CDI, EL and/or MP Config APIs (which Jakarta EE resp. MicroProfile runtimes already have)
Likely yes for startup and per-request overhead:
- No classpath scanning or proxy generation at startup
- Minimal reflection — only used once during service instantiation, not per-request
- No abstraction layers around HTTP — direct
java.net.http.HttpClientusage - Simple interface dispatch, no dynamic proxies
- Services are stateless and cached via
ConcurrentHashMap
The design strongly suggests yes:
- No intermediate JSON object materialization — uses path extraction directly on
JsonObject - Conservative allocation patterns — no framework overhead creating wrapper objects
- Native
java.net.http.HttpClient— has better GC characteristics than third-party HTTP libraries - Simple POJOs and builders — no reflection-based bean creation at runtime
- Stateless services — all state lives in method parameters, no per-request object graphs
Choose OmniHai when:
- You need a lean, focused solution for Jakarta EE or MicroProfile
- Your use case is straightforward chat, translation, summarization, proofreading, or moderation
- You want minimal dependencies and a small footprint
- You prefer simplicity over feature completeness
Choose LangChain4J when:
- You're building complex AI agents with tool calling and orchestration
- You need Retrieval-Augmented Generation (RAG) or vector stores
- You want the most comprehensive feature set
- You're not tied to a specific framework
Choose Spring AI when:
- You're already in the Spring ecosystem
- You need tight Spring Boot integration
- You want auto-configuration and starters
- Your team is Spring-proficient
Choose Jakarta Agentic when:
- You need a standard specification (once finalized)
- You want vendor-neutral portability
- You're building agentic workflows
- You can wait for the specification to mature
As said, OmniHai is "a sharp chef's knife — does a few things very well" rather than being a full framework.
Bottom line: If you need a lightweight utility for AI chat/text operations in Jakarta EE or MicroProfile without framework overhead, OmniHai is dramatically smaller and should be faster with less GC pressure. If you need RAG or agent pipelines, LangChain4J's / Spring AI's larger footprint comes with those capabilities.
- OmniHai
- OmniFaces
- GitHub
- Blog post: OmniAI 1.0-M1: One API, any AI
- Blog post: OmniAI 1.0-M2: Real-time AI, your way
- Blog post: OmniHai 1.0 released!
- Blog post: OmniHai 1.1: OmniHai grows ears
This README is ~90% generated by Claude Code :)
