From 152fa264edce88fee061a32c59eedd4a30115889 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Fri, 31 Jul 2026 21:12:06 -0400 Subject: [PATCH] feat: parse EXTRA_BODY as a JSON object per source --- src/polygateway/config.py | 8 ++++++++ tests/unit/test_config.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/polygateway/config.py b/src/polygateway/config.py index 5cfbc67..cee290f 100644 --- a/src/polygateway/config.py +++ b/src/polygateway/config.py @@ -11,6 +11,7 @@ fail-loud 校验语义与 pydantic-settings 一致。 from __future__ import annotations +import json import os from dataclasses import dataclass from typing import TYPE_CHECKING @@ -44,6 +45,7 @@ _SOURCE_FIELDS: dict[str, tuple[str, str]] = { "ENABLE_THINKING": ("enable_thinking", "bool"), "MISSING_DONE": ("missing_done", "str"), "TRUST_ENV": ("trust_env", "bool"), + "EXTRA_BODY": ("extra_body", "json"), } _RESERVED_SEGMENTS = frozenset({"GLOBAL", "RETRY", "BREAKER", "BACKPRESSURE"}) _SELECTORS = frozenset({"round_robin", "least_inflight", "health_aware"}) @@ -74,6 +76,12 @@ def _cast(raw: str, kind: str, key: str) -> object: if lowered in ("0", "false", "no", "off"): return False raise ValueError(f"非法布尔值: {raw!r}") + if kind == "json": + # JSONDecodeError 是 ValueError 子类,复用下方的统一包装 + parsed = json.loads(raw) + if not isinstance(parsed, dict): + raise ValueError(f"必须是 JSON 对象(而非数组/标量): {raw!r}") + return parsed return raw except ValueError as exc: raise ValueError(f"配置 {key} 解析失败: {exc}") from exc diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 7ab9a60..579af0c 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -103,6 +103,35 @@ class TestSourceAggregation: GatewaySettings.from_env("LLM", env=env) +class TestExtraBodyParsing: + """配置级采样参数的 env 解析(issue #4 Task 2)。""" + + def test_json_object_parsed(self): + env = _env(**{"LLM__QWEN__1__EXTRA_BODY": '{"temperature": 0, "seed": 42}'}) + s = GatewaySettings.from_env("LLM", env=env) + assert s.sources[0].extra_body == {"temperature": 0, "seed": 42} + + def test_absent_defaults_to_empty(self): + assert GatewaySettings.from_env("LLM", env=_env()).sources[0].extra_body == {} + + def test_invalid_json_fails_loudly(self): + env = _env(**{"LLM__QWEN__1__EXTRA_BODY": "{invalid"}) + with pytest.raises(ValueError, match="EXTRA_BODY"): + GatewaySettings.from_env("LLM", env=env) + + def test_non_object_json_fails(self): + """数组/标量都不是请求体片段,静默接受会让参数悄悄不生效。""" + env = _env(**{"LLM__QWEN__1__EXTRA_BODY": "[1, 2]"}) + with pytest.raises(ValueError, match="JSON 对象"): + GatewaySettings.from_env("LLM", env=env) + + def test_protected_key_rejected_through_assembly(self): + """校验确实挂在装配路径上(而非只在 types.py 里孤立存在)。""" + env = _env(**{"LLM__QWEN__1__EXTRA_BODY": '{"model": "sneaky"}'}) + with pytest.raises(ValueError, match="model"): + GatewaySettings.from_env("LLM", env=env) + + class TestResilienceKeys: def test_flat_legacy_keys(self): s = GatewaySettings.from_env("LLM", env=_env())