The saas PaygChargeInterceptor classifies a request as AI / AUTOMATION /
API / BYPASSED using a fixed precedence: X-Stirling-Automation header >
@RequiresFeature(AUTOMATION) > @RequiresFeature(AI_SUPPORT) > API-key
auth > BYPASSED (manual UI).
Plug the AI surface and the automation sub-step path:
AI controllers (saas → easy import):
- AiCreateController, AiCreateInternalController, AiProxyController each
get class-level @RequiresFeature(AI_SUPPORT). The interceptor already
falls back to the bean type via AnnotationUtils.findAnnotation when no
method-level annotation is present.
Automation sub-step header (InternalApiClient in common):
- Every caller of InternalApiClient.post() is a parent automation flow
(PipelineProcessor, AiWorkflowService, PolicyExecutor) running a child
tool via loopback HTTP. Tag every dispatch with
X-Stirling-Automation: true so the child step bills as AUTOMATION
regardless of its own annotation — i.e. an AI-OCR step inside a
policy run is correctly billed AUTOMATION rather than AI.
Deviation (documented in test javadoc): PipelineController lives in
core and PolicyController lives in proprietary. Neither module can
import @RequiresFeature from saas without a forbidden upward dependency
(both build under STIRLING_FLAVOR=core/proprietary where saas is
absent). Promoting RequiresFeature + FeatureGate to common is a
non-trivial refactor (touches 18 files across multiple C-bucket
commits) and out of scope for C4. The X-Stirling-Automation header
covers sub-step dispatch via InternalApiClient; direct user-facing
calls to /api/v1/pipeline/handleData therefore bill as BYPASSED (WEB)
or API (api-key auth) rather than AUTOMATION. Annotating those
controllers is left to a future refactor that promotes the cap types
to common.
Tests:
- RequiresFeatureAnnotationRolloutTest pins the AI controller
classifications so a refactor can't silently regress to BYPASSED.
- InternalApiClientTest.postTagsRequestAsAutomation captures the
HttpHeaders submitted to RestTemplate and asserts the marker.