Skip to main content

Service Entry and Registry

This page answers one of the most central engineering questions of the Core SDK: when you actually start wiring providers, switching models, and adding capabilities, where should your code enter.

1. Keep two main lines in mind first​

Single-instance line​

Configuration
-> AiService
-> IChatService / IResponsesService / IEmbeddingService / ...

This is the most common first wiring chain.

Multi-instance line​

Configuration + AiConfig.platforms
-> DefaultAiServiceRegistry.from(...)
-> AiServiceRegistry
-> get(id)
-> registration.platformType + registration.aiService

This chain fits multi-account, multi-tenant, and multi-provider configurations that coexist.

Both lines are public, real, and extensible entry points. The docs do not promote ChatClient.openAi(...) or a hidden Ai4j.chat() as the main entry, because they obscure the boundaries of Tool, MCP, RAG, Memory, Responses, and custom network stacks.

2. What AiService is actually responsible for​

AiService is not an entry point only for Chat; it is the unified capability factory of the current Core SDK.

From the implementation side, it currently owns:

  • getChatService(...)
  • getResponsesService(...)
  • getEmbeddingService(...)
  • getAudioService(...)
  • getRealtimeService(...)
  • getImageService(...)
  • getRerankService(...)
  • getSystemOneService(...)
  • getRagService(...)
  • getIngestionPipeline(...)
  • getModelReranker(...)
  • getAgentFlow(...)
  • webSearchEnhance(...)

This means it is not simply a "provider chooser" — it funnels model access, retrieval augmentation, and some composition capabilities into a single entry object.

Diagram: AiService fan-out — Configuration+PlatformType into the factory → 10 service interfaces (Chat/Responses/Messages/media/Embedding/Rerank) to provider implementations, plus web-search enhance and RAG helpers.

Open the full-screen diagram in a new tab

3. AiService is an explicit factory today, not dynamic discovery​

This fact must be stated up front.

Internally, AiService currently decides which implementation class to create through a set of switch(platform) statements. So today's provider/service capability matrix is not auto-discovered; it is explicitly maintained.

This has two consequences:

  • The support matrix is clear.
  • Adding a new provider or a new service surface requires editing the factory's main chain.

This is exactly why AiService itself is the first entry point for understanding the support matrix and the cost of extension.

4. What AiServiceRegistry actually adds​

AiServiceRegistry is not simply a Map<String, AiService>.

What it formally adds is:

  • Managing multiple registration entries by id
  • Each entry is bound to a PlatformType
  • Exposing convenience methods that fetch Chat / Responses / Embedding / RAG / Ingestion / Reranker by id directly

In other words, it does not merely store objects for you; it turns the "multi-instance capability entry point" itself into a formal abstraction.

5. The real behavior of DefaultAiServiceRegistry​

The most notable thing about this implementation is not "it can register", but how it registers.

It will:

  1. Read AiConfig.platforms
  2. Validate each AiPlatform.id
  3. Resolve platform -> PlatformType
  4. Copy the base Configuration
  5. Override only the configuration fields belonging to the current instance's provider
  6. Construct a scoped AiService via AiServiceFactory.create(...)
  7. Produce an AiServiceRegistration

This means multi-instance is not a set of fully independent containers; it is closer to:

  • Sharing the underlying base configuration
  • Each id owning its own provider-scoped configuration and AiService

Diagram: registry assembly and id lookup — DefaultAiServiceRegistry.from resolves each AiPlatform, builds a scoped Configuration, and registers one AiService per id via AiServiceFactory; find(id) then routes through getXxxService(platformType).

Open the full-screen diagram in a new tab

6. The role of FreeAiService​

FreeAiService is currently a compatibility layer, not a new main-line entry point.

It retains:

  • The static getChatService(id)
  • The static getEmbeddingService(id)
  • The static getResponsesService(id) and other legacy entry points

But the more robust way to understand it in the docs is:

  • Main-line entry: AiService
  • Formal multi-instance abstraction: AiServiceRegistry
  • Legacy compatibility shell: FreeAiService

7. Where to place an OpenAI-compatible profile​

Do not create a new provider type first for an OpenAI-compatible relay platform. Typically you configure it as platform: openai, then distinguish it with a different id and api-host:

ai:
platforms:
- id: trovebox-low-cost
platform: openai
api-key: ${TROVEBOX_API_KEY}
api-host: https://codex.trovebox.online/
IChatService chatService = aiServiceRegistry.getChatService("trovebox-low-cost");

Here trovebox-low-cost is a business profile id; openai indicates the underlying protocol adaptation. For the full recipe, see OpenAI-compatible and TroveBox.

7.1 Credential references: api-key-env​

api-key takes a plaintext key. For shared configuration, CI, or anywhere you don't want keys on disk, use api-key-env to declare an environment variable name that the registry resolves at assembly time:

ai:
platforms:
- id: deepseek-main
platform: deepseek
api-key-env: DEEPSEEK_API_KEY
api-host: https://api.deepseek.com/

Rules:

  • When api-key-env is declared it takes precedence over plaintext api-key — if both are present, the env value wins.
  • Missing or empty variable → fail-fast at assembly time; the error names the platform id and the variable name and never prints any key value.
  • Resolution happens in DefaultAiServiceRegistry.from (the AiConfig assembly funnel), so Spring binding, programmatic AiConfig, and any config path that maps onto AiPlatform behave identically; the caller's AiPlatform object is not mutated.
  • It does not conflict with Spring's ${ENV} placeholders: ${VAR} is expanded by Spring at bind time, while api-key-env is resolved by the SDK at assembly time — the latter also covers non-Spring usage and produces clearer errors.
tip

For team-shared config repositories, prefer api-key-env uniformly so plaintext keys never get committed.

8. How this page divides labor with adjacent pages​

  • service-entry-and-registry covers "where to enter the capabilities"
  • platform-service-matrix covers "which services each platform supports"
  • model-access covers "how to model request semantics once inside a service"
  • extension covers "which line to extend along when the default entry points are not enough"

9. A few facts most easily overlooked​

Service objects are not cached by default​

The AiService code still shows traces of having considered caching chatService / embeddingService, but the current implementation does not enable it. This means get*Service() creates a concrete service instance per call by default.

AiServiceRegistry.get(id) throws directly on unknown ids​

This makes the registry better suited as a formal multi-instance entry point, rather than a loose lookup tool that "might or might not have it".

PlatformType.getPlatform(...) is tolerant by design​

warning

This method falls back to OPENAI for unknown values, while DefaultAiServiceRegistry.resolvePlatformType(...) throws explicitly on unknown platforms. Formal multi-instance configuration should rely on the latter's strict behavior.

10. Conclusion of this page​

AI4J's current service entry system is explicit and layered: AiService is the single-instance unified capability factory, AiServiceRegistry handles formal multi-instance registration and routing, and FreeAiService only plays the role of a compatibility shell. Understanding this entry chain matters more than memorizing any single provider's API, because the support matrix, extension cost, and upper-layer wiring all build on top of it.

11. API Javadoc​

→ AiService · AiServiceRegistry