feat: add Vision monthly cap, WIF auth, and cloud-primary hybrid engine (refs #127)

- Add VISION_MONTHLY_LIMIT config setting (default 1000)
- Update CloudEngine to use WIF credential config via ADC
- Rewrite HybridEngine to support cloud-primary with Redis counter
- Pass monthly_limit through engine factory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-09 20:50:02 -06:00
parent 4412700e12
commit 4abd7d8d5b
4 changed files with 225 additions and 29 deletions

View File

@@ -15,8 +15,8 @@ from app.engines.base_engine import (
logger = logging.getLogger(__name__)
# Default path for Google Vision service account key (Docker secret mount)
_DEFAULT_KEY_PATH = "/run/secrets/google-vision-key.json"
# Default path for Google WIF credential config (Docker secret mount)
_DEFAULT_KEY_PATH = "/run/secrets/google-wif-config.json"
class CloudEngine(OcrEngine):
@@ -42,25 +42,33 @@ class CloudEngine(OcrEngine):
# ------------------------------------------------------------------
def _get_client(self) -> Any:
"""Create the Vision client on first use."""
"""Create the Vision client on first use.
Uses Application Default Credentials (ADC) pointed at a WIF
credential config file. The WIF config references an executable
that fetches an Auth0 M2M JWT.
"""
if self._client is not None:
return self._client
# Verify credentials file exists
# Verify credentials config exists
if not os.path.isfile(self._key_path):
raise EngineUnavailableError(
f"Google Vision key not found at {self._key_path}. "
f"Google Vision credential config not found at {self._key_path}. "
"Set GOOGLE_VISION_KEY_PATH or mount the secret."
)
try:
from google.cloud import vision # type: ignore[import-untyped]
# Point the SDK at the service account key
# Point ADC at the WIF credential config
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = self._key_path
# Required for executable-sourced credentials
os.environ["GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"] = "1"
self._client = vision.ImageAnnotatorClient()
logger.info(
"Google Vision client initialized (key: %s)", self._key_path
"Google Vision client initialized via WIF (config: %s)",
self._key_path,
)
return self._client
except ImportError as exc: