Docs/Decisions/ADR-0003 Dynamic ChatClients

ADR-0003: Per-request Spring AI ChatClient construction (BYOK)

Status
accepted
Decided
2026-07; re-confirmed 2026-08-15
Deciders
Platform Engineering
Scope
core/app (CoreApplication.java), core/chat, core/providers

Context

The platform is BYOK and multi-provider: each tenant (or individual agent) activates its own provider credential — Anthropic, OpenAI, Google GenAI, OpenRouter, Grok, Groq, Mistral, DeepSeek, Azure OpenAI, AWS Bedrock, Ollama, Meta, Sarvam and the platform-hosted pool — stored encrypted in the database and decrypted at request time. There is no single platform API key for these providers; spring.ai.openai.api-key is never configured at all.

Spring AI's default model is the opposite: each provider starter registers auto-configurations that eagerly build a singleton ChatModel from a static application.yml key at context refresh. On this platform those beans can never be constructed — and would be wrong even if they could be, because "the OpenAI key" is a per-request value, not an environment variable.

Two real failures forced the full exclusion set:

Decision

CoreApplication excludes eleven Spring AI auto-configurations, and no singleton provider client exists anywhere:

flowchart LR call["Chat turn / quick task /
workflow node / assist"] --> reg["ChatClientRegistry"] reg --> fac["*AgentChatClientFactory
(per ProviderName)"] cred["ProviderCredentialService
decrypt per call"] --> fac fac --> cc["new ChatClient
+ LlmMetricsAdvisor
+ shared DefaultAgentAdvisors"]

Alternatives considered

OptionWhy it lost
Spring AI auto-configuration + static keys Structurally incapable of expressing "key is chosen per request, per tenant." Also crashes at startup because the keys it wants don't exist.
Singleton client pool, one per provider, platform key Defeats BYOK: tenant spend and tenant quotas would be untrackable, and a platform key makes the platform the billing party for every tenant's usage.
Cached per-credential clients now Deliberately deferred, documented in AnthropicAgentChatClientFactory: "worth caching (keyed by credential id + updatedAt, invalidated on key rotation) once this is under real load — not done here to avoid a caching bug masquerading as a feature this early." Reconstruction cost is acceptable at current load.

Consequences

Positive

Negative / costs

Do not remove
ToolCallbackConverterAutoConfiguration was un-excluded once and verified live to leak every tool in the context. Do not remove that exclusion again until core/mcpserver has a narrow conversion that does not route through aggregateToolCallbacks.

Evidence & references