Appearance
ADR-005: Provider Decomposition Template
Status
Accepted (v0.6)
Context
OpenAIProvider (904 lines) and AnthropicProvider (1,052 lines) each contained five interleaved concerns: request building, response parsing, SSE streaming, error classification, and retry integration. This monolithic structure caused several problems:
- Testing difficulty: Testing SSE reconnection logic required mocking the entire provider, including unrelated request-building code.
- Bug isolation: An SSE parsing bug in
AnthropicProviderwas initially misdiagnosed as a request-building issue because both concerns lived in the same class. - Code duplication: Both providers independently implemented nearly identical SSE subscriber logic and error classification patterns.
- Onboarding friction: Adding a new provider (e.g., Google Gemini) required understanding and replicating all five concerns from scratch.
Decision
Decompose each provider into a 5-piece template:
RequestBuilder— Transforms Kairo'sModelConfig+ messages into the provider's native HTTP request format. Pure function, no I/O.ResponseParser— Converts the provider's response JSON into Kairo'sModelResponse. Pure function, no I/O.SseSubscriber— Manages the SSE connection lifecycle: connection, reconnection, backpressure, and partial-event assembly. Reactive I/O only.ErrorClassifier— Maps HTTP status codes and provider-specific error bodies into Kairo's exception hierarchy (retryable vs. fatal). Pure function.ProviderFacade— Thin orchestrator that wires the above four components together and exposes theModelProviderSPI contract.
Reuse existing ReactiveRetryPolicy — no new retry abstraction is introduced. The ProviderFacade delegates retry decisions to the existing policy, using ErrorClassifier output to determine retryability.
The pattern was piloted on OpenAIProvider first, then applied to AnthropicProvider using the same template.
Consequences
- Positive: Each concern is independently testable.
SseSubscribertests don't need request-building setup. - Positive: New providers follow a clear template: implement 4 components + 1 facade. Estimated effort for a new provider drops from ~3 days to ~1 day.
- Positive: SSE subscriber bugs are isolated from request building — no more misdiagnosis.
- Positive: No new retry abstraction — reuses
ReactiveRetryPolicy, keeping the abstraction count stable. - Negative: Five classes per provider increases file count (2 files → 10 files for two providers). Mitigated by clear naming convention:
OpenAiRequestBuilder,OpenAiResponseParser, etc. - Negative: Cross-cutting changes (e.g., adding a new header to all requests) now require touching
RequestBuilderin each provider.
References
ReactiveRetryPolicy.java— Existing retry policy reused by all providersModelProvider.java— SPI contract implemented byProviderFacade- ADR-001 — Similar decomposition pattern applied to
ReActLoop