Showing posts with label Google. Show all posts
Showing posts with label Google. Show all posts

Friday, 13 March 2026

The Tail at Scale


 When engineers talk about latency, they almost always talk about average latency. P50. The median experience. It's a comfortable metric — it responds to optimization, it's easy to visualize, and it makes dashboards look good. The trouble is that in any non-trivial distributed system, average latency is nearly irrelevant to whether your system actually feels fast.

A 2013 paper from Google, The Tail at Scale, reframed the entire conversation. The insight was simple: in a system where a single user request fans out to hundreds of backend machines, the response time is determined not by the average machine but by the slowest machine in the fan-out. The 99th percentile is not a corner case. It is, structurally, the common case for any sufficiently large request graph.

This is the founding observation behind Google's latency engineering philosophy — and it reshapes how you think about almost every architectural decision in a shared environment.

"In a large enough system, the tail is the average. The stragglers are not noise — they are the signal."

Why Shared Environments Are Inherently Hostile to Latency

A shared environment — whether it's a multi-tenant cluster, a distributed storage layer, or a cloud runtime shared across teams — introduces a class of latency that has nothing to do with your code. It comes from contention: for CPU time, for memory bandwidth, for network queues, for disk I/O. These are resources that your workload competes for with processes it has no visibility into and no control over.

Result is what the paper calls "variability amplification." Even a single machine exhibiting transient slowness — a GC pause, a cache eviction storm, a background compaction job — introduces latency that propagates through the system in ways that are entirely disproportionate to the duration of the original event. A 50ms hiccup on a single shard becomes a 200ms tail for every request that happened to touch that shard during that window.



This is the fundamental problem. And the conventional response — "profile and optimize the slow path" — doesn't work, because the slowness is not in the code. It's in the environment. You cannot optimize away a garbage collector running on a machine you don't control, or a noisy neighbor saturating the memory bus two NUMA nodes away.


Hedged Requests

The first and most important pattern the paper describes is the hedged request. The idea is counterintuitive enough that it's worth stating plainly: rather than waiting for a slow server to respond, you send the same request to a second server after a short delay — and take whichever response arrives first.

The delay is critical. You don't want to double your load by default. Instead, you observe your system's typical P95 response time and use that as the hedge threshold. If a request hasn't completed within that window, you issue an identical request to a different replica and race them. The moment either one responds, you cancel the other.



The practical effect is remarkable. Measurements at Google showed that hedging could reduce 99.9th percentile latency by an order of magnitude while increasing load by only a few percent — because most requests don't trigger the hedge at all, and those that do are precisely the ones stuck on slow replicas.

Hedged requests trade a small amount of extra load for a large reduction in tail latency. Load amplification is bounded by the fraction of requests that exceed your hedge threshold — which, by definition, is a small minority if you set the threshold near P95.

What makes this pattern powerful in shared environments specifically is that it sidesteps the cause of slowness entirely. You don't need to know why Replica 1 is slow. You don't need to detect it, alert on it, or drain it. You just race around it.

Tied Requests and Cancellation

Hedged requests have a subtle problem: if both replicas are fast, you've wasted work on both. Tied request pattern refines this by introducing coordination between the two requests. When you issue a hedge, you attach a "cancellation token" that the replicas share. Whichever replica starts processing the request first notifies the other to cancel, and proceeds alone.

This is particularly valuable when requests are expensive to process — when the work itself consumes significant CPU or I/O on the backend. Instead of duplicating work silently, tied requests minimize wasted computation by communicating intent across the request boundary.

The implementation requires some infrastructure: replicas need to be aware of each other's state for a given request, which typically means either a shared coordination layer or an out-of-band cancellation channel. In Google's architecture, this was handled via internal RPC cancellation propagation. In most systems, you can approximate it with request-scoped context cancellation — the Go context.Context model being a modern analogue of this idea.


Micro-Partitioning and Fine-Grained Load Balancing

Both hedging and tying are reactive: they respond to latency after it has occurred. Complementary proactive strategy is micro-partitioning — dividing work into far more partitions than you have machines, so that load imbalance between logical partitions can be corrected by reassigning partitions rather than migrating state.

Intuition is straightforward. If you have 100 machines and partition your keyspace into 100 shards, a hot key on one shard means one machine is overloaded and there's nowhere to move it without a full reshard. If instead you have 10,000 virtual partitions distributed across 100 machines, a hot partition can be migrated to a less-loaded machine in seconds, with minimal disruption.



This is less a trick and more a structural principle: partition granularity determines your ability to respond to imbalance. Google's Bigtable uses this extensively — tablet splits are designed to be cheap precisely so that hot tablets can be redistributed across tablet servers without downtime.

Good Citizens and Background Throttling

In any shared environment, there are two classes of work: foreground requests with latency SLOs that users directly feel, and background work — compaction, replication sync, index rebuilds, garbage collection — that has no user-visible deadline but consumes the same physical resources. The conflict between these two classes is one of the most consistent sources of latency spikes in production systems.

Solution is conceptually simple: background tasks must be "good citizens." They should yield CPU and I/O to foreground work when demand is detected. In practice, this means implementing throttle mechanisms that observe system load indicators — request queue depth, disk I/O wait, CPU steal — and automatically back off when those indicators cross a threshold.

Google's approach to this problem includes priority queues in their RPC layer, where foreground traffic can preempt background work mid-execution. Bigtable's compaction scheduler monitors foreground request rates and adjusts compaction aggressiveness in real time. The principle is that background jobs should "earn" their CPU time during slack periods, not consume it as a fixed entitlement.

What's important here is that this isn't optional in a shared environment — it's a contract. If your background jobs don't throttle, you are imposing your latency cost on every other workload sharing your infrastructure. In large organizations, this becomes a coordination problem: the team running the nightly reindex doesn't know which other team's latency SLO they're violating at 2am.

Selective Replication of Hot Data

The patterns above all treat slowness as something to route around or absorb. This final pattern takes a different approach: eliminate the bottleneck entirely for the data that matters most.

In most systems, data access follows a power-law distribution. A small number of keys — a viral post, a high-traffic configuration value, a globally shared counter — account for a disproportionate fraction of reads. These hot items are precisely the ones most likely to create queuing delays, cache evictions, and server-level saturation.

Solution is selective, on-the-fly replication of hot items. Rather than replicating everything uniformly, the system detects hot keys — through access frequency monitoring or explicit client hints — and creates additional in-memory replicas across multiple servers. Reads are then distributed across those replicas, reducing per-server load for the items that need it most.



This pattern is now standard in systems like Memcached (Facebook's lease mechanism was a direct response to this problem), Redis Cluster, and modern distributed caches. The underlying principle — don't treat all data as equally hot, and adapt replication depth to observed access patterns — generalizes far beyond caching.


Latency-Aware Load Balancing

Round-robin load balancing assumes that all backend servers are equivalent and equally available. In a shared environment, this assumption fails constantly. A server experiencing memory pressure or CPU saturation will accept requests at the same rate as a healthy one — queuing them invisibly while the client believes load is distributed evenly. The result is that round-robin actively routes traffic into latency holes it cannot see.

Latency-aware load balancing corrects this by making routing decisions based on observed response times rather than theoretical capacity. Client maintains a rolling measurement of each backend's recent latency and biases requests toward the faster ones. The simplest version is the "power of two choices" algorithm: rather than picking randomly from all backends, pick two at random and route to whichever has the lower current latency. The probabilistic gain is disproportionate to the cost — two random samples are enough to avoid the worst servers most of the time.



Elegance of this approach is that it requires no central coordinator and no global view of server health. Each client maintains its own local latency measurements independently. The collective effect of many clients doing this converges on a system-wide load distribution that naturally isolates slow servers — without any explicit health-checking infrastructure.

Google's gRPC and Envoy proxy both implement variants of this. Netflix's Ribbon client-side load balancer added latency-based weighting as a core feature after observing that round-robin was systematically directing traffic into degraded nodes during partial cluster failures.

Request Deadline Propagation

Every distributed request has an implicit budget: the maximum time the user is willing to wait before the response becomes useless. A search result that arrives after the user has navigated away is not a slow success — it is a waste of resources that could have been spent on a fresher request. Yet most systems treat their internal RPC calls as if they exist outside of time, with no awareness of how much of the outer deadline has already been consumed.

Deadline propagation makes the remaining time budget explicit and transmits it across every service boundary. When a frontend handler receives a request with a 200ms SLO and spends 30ms doing authentication, the downstream RPC it issues should carry a deadline of 170ms — not an unconstrained call that could block for seconds. Each hop in the call graph receives a shrinking time window, and each service is expected to abandon work and return an error rather than continuing once that window closes.

Deadline propagation transforms timeout from a per-hop configuration into a system-wide invariant. Instead of each service having its own independently configured timeout — which can add up to far more than the user's actual patience — the deadline flows through the entire call graph as a shared, decrementing constraint.

Without deadline propagation, a slow backend continues burning CPU on a request whose answer will never be seen. The frontend has already returned an error to the user, but the downstream services don't know this — they keep working, consuming resources that could serve other requests. With deadline propagation, a cancelled frontend request immediately cancels the entire downstream tree. The work stops the moment it becomes irrelevant.



Go's context.Context is the most widely adopted implementation of this idea in modern systems. Passing a context with a deadline through every function call is the idiomatic Go way of expressing exactly this contract. The Dapper tracing system and gRPC's deadline mechanism implement the same principle at the RPC layer.


Probabilistic Early Completion

There is a class of read-heavy workload where the question "what is the correct answer?" is less important than "what is a good enough answer, returned quickly?" Search ranking, recommendation feeds, autocomplete suggestions, approximate analytics — in each of these, the value of the response degrades gradually with quality, not catastrophically. A slightly stale recommendation list is almost as useful as a fresh one. A search result that includes 98% of relevant documents is indistinguishable from one with 100%, from the user's perspective.

Probabilistic early completion exploits this tolerance by allowing a request to return as soon as it has gathered "enough" signal, rather than waiting for every shard to respond. The coordinator tracks how many responses have arrived and, once a statistically sufficient fraction of shards have replied, returns the aggregated result rather than waiting for the stragglers. The remaining responses, when they eventually arrive, are discarded.

The fraction required is a tunable parameter that encodes the application's quality-vs-latency tradeoff. Setting it at 90% means the request finishes when 9 of 10 shards have responded — the one slow shard no longer determines the outcome. The quality loss is bounded by the fraction omitted, and in practice for approximate workloads the loss is negligible while the latency gain is substantial.

Probabilistic early completion only makes sense when partial results are semantically valid. It is appropriate for search, recommendations, aggregated metrics, and autocomplete. It is inappropriate for financial transactions, inventory updates, authentication checks, or any computation where partial data produces incorrect rather than merely approximate output.

Overload Admission Control

All of the patterns discussed so far are concerned with how an individual request navigates a slow or overloaded system. This one operates at a different level: preventing the system from accepting more work than it can complete within latency bounds in the first place.

The counterintuitive observation is that queueing is not a buffer — it is a latency amplifier. When a service is operating at capacity and accepts additional requests into a queue, those requests do not get served "slightly later." They get served much later, because every subsequent request must wait behind everything already in the queue. The 99th percentile latency of a system at 95% utilization can be ten times worse than the same system at 80% utilization, even though the throughput difference is modest.



Admission control accepts this reality and acts on it. Rather than allowing the queue to grow unbounded during traffic spikes, the system measures current utilization — active request count, queue depth, recent latency percentiles — and explicitly rejects incoming requests when those indicators cross a threshold. The rejected requests receive an immediate error rather than a delayed one. From the client's perspective, a fast rejection is often preferable to a timeout: it can retry against a different backend, fail fast, or serve from cache, rather than hanging indefinitely.

Google's internal systems use a technique called "client-side throttling" where the client itself participates in admission control: it tracks its own recent reject rate and probabilistically drops requests before sending them, reducing load on an already-stressed backend without requiring the backend to process and reject each request individually. Netflix's Concurrency Limits library implements a similar adaptive mechanism based on TCP congestion control algorithms — treating the request queue like a network pipe and backing off as soon as it detects queuing delay increasing.


Latency SLO Budgeting Across Teams

The patterns above are all technical. This last one is organizational, but its absence makes every technical pattern less effective.

In a large engineering organization, a single user-facing latency SLO — say, P99 < 300ms — is actually a composite of dozens of internal service SLOs. The frontend has a budget. The auth service has a budget. The ranking service has a budget. The storage layer has a budget. When these budgets are implicit, undocumented, or uncoordinated, teams make local decisions that are individually reasonable but collectively catastrophic. The auth team tightens its internal retry logic, adding 20ms to every call. The indexing team adds a synchronous cache warm-up step. Neither change violates any documented contract, and neither team knows what the other did. The cumulative effect is a P99 regression that shows up in the frontend SLO and takes weeks to attribute.

Latency budgeting makes these implicit contracts explicit. Each service in a call graph is assigned a latency budget — its maximum allowed contribution to the end-to-end P99 — derived from the top-level SLO. Changes that affect that budget require coordination across the services that share the call path. The budget is measured, reported, and treated as a first-class engineering constraint, like memory or CPU quota.

This is less a distributed systems pattern and more a systems-thinking pattern. Latency is a shared resource in the same way that bandwidth or storage is a shared resource. The only difference is that it is invisible until the moment it fails, at which point attribution is painful and slow. Making latency budgets explicit — even approximately — transforms latency from an emergent surprise into a managed constraint.

Embracing Stochastic Reality

What's important about these patterns, taken together, is what they have in common. None of them attempt to eliminate variability from the system. None of them assume that the environment can be made deterministic, or that every machine can be made equally fast, or that background noise can be suppressed. They all start from the premise that variability is irreducible — that shared environments will always produce straggler events — and design around it rather than against it.

"You cannot engineer your way to a deterministic distributed system. You can only engineer your way to one that degrades gracefully in the face of guaranteed non-determinism."

Old engineering intuition was that good infrastructure means predictable infrastructure — every component behaves the same way every time. New intuition is that good infrastructure means resilient infrastructure — every request completes within acceptable bounds, regardless of what any individual component is doing.

Each pattern acknowledges that something will go wrong and designs so that something going wrong in one place cannot become everything going wrong everywhere.

The question worth sitting with, for any system you're currently building, is not "what is our average latency?" It is: "when something goes wrong on one machine, where does that pain go?" If the answer is "it propagates to every user touching that partition," you have a tail latency problem, and the patterns above are where the solution starts.

Further reading: The ideas in this post are drawn from The Tail at Scale — Communications of the ACM, February 2013. Worth reading in full if any of this resonated.

Saturday, 5 April 2025

Beyond the Turing Test for todays LLM

This post is continuation of Teaching LLM to reason

In 1950, Alan Turing proposed a deceptively simple test to determine if a machine could think: if a human evaluator cannot reliably distinguish between responses from a computer and a human during a text-based conversation, the machine could be considered "intelligent." For decades, this Turing Test has remained the gold standard for evaluating artificial intelligence.

But as we approach 2025, leading AI companies have developed new capabilities that fundamentally change how we should think about machine intelligence. Both Anthropic's Claude with "Extended Thinking" and OpenAI's O-Series "Reasoning Models" have introduced features that allow AI systems to "think before they answer" — a significant evolution that may render the traditional Turing Test insufficient.

The Evolution of AI Thinking

The classic approach to large language models has been to generate responses directly from prompts. While these systems have demonstrated impressive capabilities, they've struggled with complex reasoning tasks requiring multi-step thinking.

The newest generation of AI models takes a fundamentally different approach:

Claude's Extended Thinking creates explicit thinking blocks where it outputs its internal reasoning. These blocks provide transparency into Claude's step-by-step thought process before it delivers its final answer.

OpenAI's O-Series Reasoning Models (o1 and o3-mini) generate internal reasoning tokens that are completely invisible to users but inform the final response. These models are "trained with reinforcement learning to perform complex reasoning."

In both cases, the AIs are doing something remarkable: they're creating space between the input they receive and the output they produce. This space — filled with reasoning tokens — allows them to work through problems incrementally rather than jumping directly to conclusions.

Other company are also catching up, google has Gemini 2.0 flash , DeepSeek has R1 series 




How These Systems "Think"

Despite their different approaches to visibility, all systems share key architectural similarities:

  1. Internal Reasoning Process: Generate reasoning content that explores different approaches, examines assumptions, and follows chains of thought.
  2. Token Economy: The thinking/reasoning content consumes tokens that users pay for, even though they might not see this content directly (in OpenAI's case).
  3. Budgeting Thought: Users can control how much "thinking" the AI does through parameters like Claude's "budget_tokens" or OpenAI's "reasoning_effort" settings.
  4. Context Management: All systems have mechanisms to manage reasoning content across multi-turn conversations, preventing it from consuming the entire context window.

The key difference lies in transparency: while OpenAI's reasoning happens entirely behind the scenes and other models show the working.

Real-World Applications

These advances in AI thinking enable new classes of applications:

  • Complex Problem Solving: Tasks requiring multi-step reasoning, like mathematical proofs or scientific analysis
  • Careful Code Generation: Writing and refactoring code with attention to edge cases
  • Constraint Optimization: Balancing multiple competing factors in decision-making
  • Data Validation: Detecting inconsistencies and anomalies in datasets
  • Process Creation: Developing workflows and routines from high-level instructions

What makes these applications possible is the AI's ability to break down complex problems, consider multiple approaches, and show its work — just as a human expert would.

Implications for the Turing Test

These developments challenge the basic premise of the Turing Test in several ways:

1. Process vs. Output Evaluation

The original Turing Test only evaluates the final output of a conversation. But with models that can now show their reasoning process, we can evaluate not just what they conclude, but how they reach those conclusions.

Some models  approach of exposing its thinking makes the test more transparent. We can see its reasoning chain, which might make it easier to identify "machine-ness" but also builds more trust in its conclusions. By contrast, OpenAI's hidden reasoning maintains the black-box nature of the original test.

2. Human-like Problem Solving

Both approaches enable more human-like problem-solving behavior. Humans rarely reach complex conclusions instantly; we work through problems step by step. This incremental approach to reasoning makes AI responses more believable to human evaluators.

As Anthropic's documentation states: "Claude often performs better with high-level instructions to just think deeply about a task rather than step-by-step prescriptive guidance." This mirrors human cognition, where experts can often solve problems with minimal guidance by applying their intuition and experience.

3. Tackling Sophisticated Reasoning

These advances allow AI to handle increasingly sophisticated questions that require multi-step thinking — a critical capability for passing ever more demanding Turing Test scenarios.

The models can now perform better on tasks involving:

  • Mathematical problem-solving
  • Scientific reasoning
  • Multi-step planning
  • Constraint satisfaction
  • Code generation and debugging

4. The Economics of Thinking

Perhaps most interestingly, all systems implement economic controls around "thinking," which is fundamentally different from human cognition. Humans don't have token budgets for our thoughts or explicit parameters to control how deeply we ponder.

OpenAI's documentation even describes this trade-off explicitly: "low will favor speed and economical token usage, and high will favor more complete reasoning at the cost of more tokens generated and slower responses."

This commodification of thought creates a new dimension that wasn't considered in Turing's original formulation.

Beyond the Turing Test: New Evaluation Frameworks

Given these developments, we need new frameworks to evaluate AI systems that go beyond the simple pass/fail binary of the Turing Test, it might look into some of these aspect

1. The Reasoning Transparency Test

Can an AI system not only produce human-like outputs but also demonstrate human-like reasoning processes? This evaluates not just the answer but the path to that answer.

Some system approach of showing its reasoning steps provides a window into this, while OpenAI's invisible reasoning would require specific prompting to elicit the thinking process.

2. The Resource Efficiency Test

Can an AI system allocate its "thinking resources" adaptively rather than having fixed budgets? Humans naturally devote more mental effort to difficult problems and less to simple ones.

The ideal system would automatically determine how much reasoning is required for a given task rather than relying on predetermined parameters.

3. The Memory Integration Test

Can an AI retain and utilize its previous reasoning seamlessly across conversations? All systems currently discard reasoning tokens between turns (though they keep final outputs).

A truly human-like system would build on its previous thought processes rather than starting fresh each time.

4. The Self-Correction Test

Can the AI identify errors in its own reasoning and correct its course? All systems show some capability for this, with documentation highlighting how they can "reflect on and check their work for improved consistency and error handling."

This self-reflective capacity is a hallmark of human intelligence that goes beyond the output-focused Turing Test.




The Future of AI Evaluation

As AI systems continue to evolve, we may need to reconceptualize what it means for a machine to "think." Perhaps rather than asking if an AI can fool a human evaluator, we should ask:

  • Can it reason transparently and explain its conclusions?
  • Can it adapt its thinking process to different types of problems?
  • Can it build on previous reasoning across multiple interactions?
  • Can it identify and correct flaws in its own thinking?

The parallel development of extended thinking capabilities of leading model represents a significant step toward AI systems that can reason through complex problems in increasingly human-like ways. While these systems might pass the original Turing Test in many scenarios, their underlying mechanisms reveal important differences from human cognition.

As Alan Turing himself might acknowledge if he were here today, the conversation has evolved beyond simply determining if machines can think. Now we must ask more nuanced questions about how they think and what that means for our understanding of intelligence itself.

In this new landscape, perhaps the most valuable insights will come not from whether AIs can fool us, but from how their thinking processes compare to and differ from our own human cognition.

Wednesday, 5 March 2025

Building a Universal Java Client for Large Language Models

Building a Universal Java Client for Large Language Models

In today's rapidly evolving AI landscape, developers often need to work with multiple Large Language Model (LLM) providers to find the best solution for their specific use case. Whether you're exploring OpenAI's GPT models, Anthropic's Claude, or running local models via Ollama, having a unified interface can significantly simplify development and make it easier to switch between providers.

The Java LLM Client project provides exactly this: a clean, consistent API for interacting with various LLM providers through a single library. Let's explore how this library works and how you can use it in your Java applications.

Core Features

The library offers several key features that make working with LLMs easier:

  1. Unified Interface: Interact with different LLM providers through a consistent API
  2. Multiple Provider Support: Currently supports OpenAI, Anthropic, Google, Groq, and Ollama
  3. Chat Completions: Send messages and receive responses from language models
  4. Embeddings: Generate vector representations of text where supported
  5. Factory Pattern: Easily create service instances for different providers

Architecture Overview

The library is built around a few key interfaces and classes:

  • GenerativeAIService: The main interface for interacting with LLMs
  • GenerativeAIFactory: Factory interface for creating service instances
  • GenerativeAIDriverManager: Registry that manages available services
  • Provider-specific implementations in separate packages

This design follows the classic factory pattern, allowing you to:

  1. Register service factories with the GenerativeAIDriverManager
  2. Create service instances through the manager
  3. Use a consistent API to interact with different providers

Getting Started

To use the library, first add it to your Maven project:

xml
<dependency> <groupId>org.llm</groupId> <artifactId>llmapi</artifactId> <version>1.0.0</version> </dependency>


Basic Usage Example

Here's how to set up and use the library:

java
// Register service providers GenerativeAIDriverManager.registerService(OpenAIFactory.NAME, new OpenAIFactory()); GenerativeAIDriverManager.registerService(AnthropicAIFactory.NAME, new AnthropicAIFactory()); // Register more providers as needed // Create an OpenAI service Map<String, Object> properties = Map.of("apiKey", System.getenv("gpt_key")); var service = GenerativeAIDriverManager.create( OpenAIFactory.NAME, "https://api.openai.com/", properties ); // Create and send a chat request var message = new ChatMessage("user", "Hello, how are you?"); var conversation = new ChatRequest("gpt-4o-mini", List.of(message)); var reply = service.chat(conversation); System.out.println(reply.message()); // Generate embeddings var vector = service.embedding( new EmbeddingRequest("text-embedding-3-small", "How are you") ); System.out.println(Arrays.toString(vector.embedding()));

Working with Different Providers

OpenAI

java
Map<String, Object> properties = Map.of("apiKey", System.getenv("gpt_key")); var service = GenerativeAIDriverManager.create( OpenAIFactory.NAME, "https://api.openai.com/", properties ); // Chat with GPT-4o mini var conversation = new ChatRequest("gpt-4o-mini", List.of(new ChatMessage("user", "Hello, how are you?"))); var reply = service.chat(conversation);

Anthropic

java
Map<String, Object> properties = Map.of("apiKey", System.getenv("ANTHROPIC_API_KEY")); var service = GenerativeAIDriverManager.create( AnthropicAIFactory.NAME, "https://api.anthropic.com", properties ); // Chat with Claude var conversation = new ChatRequest("claude-3-7-sonnet-20250219", List.of(new ChatMessage("user", "Hello, how are you?"))); var reply = service.chat(conversation);

Ollama (Local Models)

java
// No API key needed for local models Map<String, Object> properties = Map.of(); var service = GenerativeAIDriverManager.create( OllamaFactory.NAME, "http://localhost:11434", properties ); // Chat with locally hosted Llama model var conversation = new ChatRequest("llama3.2", List.of(new ChatMessage("user", "Hello, how are you?"))); var reply = service.chat(conversation);

Under the Hood

The library uses an RPC (Remote Procedure Call) client to handle the HTTP communication with various APIs. Each provider's implementation:

  1. Creates appropriate request objects with the required format
  2. Sends requests to the corresponding API endpoints
  3. Parses responses into a consistent format
  4. Handles errors gracefully

The RpcBuilder creates proxy instances of service interfaces, handling the HTTP communication details so you don't have to.

Supported Models

The library currently supports several models across different providers:

  • OpenAI: all
  • Anthropic: all
  • Google: gemini-2.0-flash
  • Groq: all
  • Ollama: any other model you have locally

Extending the Library

One of the strengths of this design is how easily it can be extended to support new providers or features:

  1. Create a new implementation of GenerativeAIFactory
  2. Implement GenerativeAIService for the new provider
  3. Create necessary request/response models
  4. Register the new factory with GenerativeAIDriverManager

Conclusion

The Java LLM Client provides a clean, consistent way to work with multiple LLM providers in Java applications. By abstracting away the differences between APIs, it allows developers to focus on their application logic rather than the details of each provider's implementation.

Whether you're building a chatbot, generating embeddings for semantic search, or experimenting with different LLM providers, this library offers a straightforward way to integrate these capabilities into your Java applications.

The project's use of standard Java patterns like factories and interfaces makes it easy to understand and extend, while its modular design allows you to use only the providers you need. As the LLM ecosystem continues to evolve, this type of abstraction layer will become increasingly valuable for developers looking to build flexible, future-proof applications.


Link to github project - llmapi