From 1231226205f14bf78b30ed74a32e7b8852529ec0 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Tue, 21 Jul 2026 22:45:12 -0400 Subject: [PATCH] feat: add OcrSettings scope configuration --- src/polygateway/config.py | 26 ++++++++++++++++++++++ tests/unit/test_config.py | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/polygateway/config.py b/src/polygateway/config.py index 32de89e..6908c66 100644 --- a/src/polygateway/config.py +++ b/src/polygateway/config.py @@ -407,3 +407,29 @@ class EmbeddingSettings: normalize=bool(_cast(norm[1], "bool", norm[0])) if norm else False, expected_dim=expected_dim, ) + + +@dataclass(frozen=True) +class OcrSettings: + """OCR scope 装配配置(M3 设计 §3.4): 复用 GatewaySettings,无 OCR 专用键。 + + cache/structured/pricing 键对 OCR 无意义,装配时忽略;TPM 闸不启用 + (tpm=0)、看门狗键不配(非流式)由源键缺省自然达成。api_key 惯例填 + "none"(MonkeyOCR 无鉴权,SourceConfig 非空校验用占位)。 + """ + + gateway: GatewaySettings + + @classmethod + def from_env( + cls, + scope: str = "OCR", + env: Mapping[str, str] | None = None, + *, + env_file: str = ".env", + ) -> OcrSettings: + if env is None: + env = { + k: v for k, v in {**dotenv_values(env_file), **os.environ}.items() if v is not None + } + return cls(gateway=GatewaySettings.from_env(scope.upper(), env=env)) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 6322bae..5d513ad 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -274,3 +274,48 @@ class TestAssemblyGuards: env_ok = dict(env) env_ok["LLM__BACKPRESSURE__STALL_WINDOW_S"] = "60" assert GatewaySettings.from_env("LLM", env=env_ok).backpressure.stall_window_s == 60.0 + + +class TestOcrSettings: + """M3 OcrSettings(设计 §3.4): 复用 GatewaySettings,无 OCR 专用键。""" + + _OCR_ENV = { + "OCR__MONKEY__1__BASE_URL": "http://10.77.0.20:7866", + "OCR__MONKEY__1__API_KEY": "none", # 无鉴权占位惯例 + "OCR__MONKEY__1__MODEL": "monkey-ocr", + "OCR__MONKEY__1__TIMEOUT_S": "120", + "LLM_MAX_RETRIES": "3", + "LLM_RETRY_BASE_DELAY": "2.0", + "LLM_RETRY_MAX_DELAY": "30.0", + "LLM_CIRCUIT_BREAKER_THRESHOLD": "5", + "LLM_CIRCUIT_BREAKER_COOLDOWN": "60", + "PGW_CACHE_BACKEND": "none", + "PGW_TELEMETRY_BACKEND": "none", + } + + def test_minimal_ocr_scope(self): + from polygateway.config import OcrSettings + + settings = OcrSettings.from_env("OCR", env=dict(self._OCR_ENV)) + gw = settings.gateway + assert gw.scope == "ocr" # GatewaySettings 统一小写归一(既有约定) + assert gw.sources[0].name == "monkey_1" + assert gw.sources[0].provider == "monkey" + assert gw.sources[0].trust_env is True + + def test_scope_resilience_override(self): + from polygateway.config import OcrSettings + + env = dict(self._OCR_ENV) + env["OCR__RETRY__MAX_ATTEMPTS"] = "5" + env["OCR__MONKEY__1__TRUST_ENV"] = "false" + settings = OcrSettings.from_env("OCR", env=env) + assert settings.gateway.retry.max_attempts == 5 + assert settings.gateway.sources[0].trust_env is False + + def test_missing_base_url_fails(self): + from polygateway.config import OcrSettings + + env = {k: v for k, v in self._OCR_ENV.items() if k != "OCR__MONKEY__1__BASE_URL"} + with pytest.raises(ValueError): + OcrSettings.from_env("OCR", env=env)