diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c3b1f..c1614a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ the GitHub Release body, so a release with no entry here fails. Versioning follows [docs/versioning.md](docs/versioning.md). +## [0.11.1] - 2026-09-24 + +### Fixed + +- `build_protocol_client` now forwards `cfg["default_headers"]` to + `AnthropicClient` for both `protocol: anthropic` and `protocol: bedrock`. + Previously the headers were silently dropped, so gateway routing/auth headers + never reached those requests. Consumers that shimmed the builder to work + around this (ApodexHarness `miroharness.infra.protocol_client`) can revert to + a pure alias after upgrading. +- Anthropic/Bedrock clients now also send `X-Title`, matching the Responses + client. +- The `protocol: responses` builder now merges `cfg["default_headers"]` with + its `X-Title` header instead of discarding them. + ## [0.11.0] - 2026-09-14 ### Added diff --git a/agent_core/providers/protocol_client.py b/agent_core/providers/protocol_client.py index 8fd6271..a6cce77 100644 --- a/agent_core/providers/protocol_client.py +++ b/agent_core/providers/protocol_client.py @@ -110,7 +110,9 @@ def _effort_str(cfg: dict[str, Any]) -> str: return effort.strip() if isinstance(effort, str) else "" -def _build_anthropic(cfg: dict[str, Any], *, bedrock: bool = False) -> LLMClient: +def _build_anthropic( + cfg: dict[str, Any], title: str, *, bedrock: bool = False +) -> LLMClient: """Native Anthropic Messages API with extended thinking. Responses carry thinking + signature blocks, kept verbatim @@ -119,7 +121,8 @@ def _build_anthropic(cfg: dict[str, Any], *, bedrock: bool = False) -> LLMClient ``{base_url}/v1/messages`` (direct) or ``{base_url}/model/{id}/invoke`` (``bedrock=True``, AWS Bedrock runtime, Bearer API-key auth + the ``anthropic_version`` body stamp). Optional ``effort`` → - ``output_config.effort``. + ``output_config.effort``. ``default_headers`` (gateway routing / auth + headers) is merged over ``X-Title``, as in the Responses builder. ``thinking_type`` selects the request shape (default ``adaptive``). Live- verified against api.anthropic.com + Bedrock 2026-07-09 (see @@ -169,6 +172,7 @@ def _build_anthropic(cfg: dict[str, Any], *, bedrock: bool = False) -> LLMClient max_tokens=max_tokens, thinking=thinking, effort=effort, + default_headers={"X-Title": title, **(cfg.get("default_headers") or {})}, bedrock=bedrock, ) @@ -227,7 +231,7 @@ def _build_responses(cfg: dict[str, Any], title: str) -> LLMClient: base_url=cfg.get("base_url"), temperature=cfg.get("temperature"), max_output_tokens=cfg.get("max_tokens", 32768), - default_headers={"X-Title": title}, + default_headers={"X-Title": title, **(cfg.get("default_headers") or {})}, reasoning=reasoning or None, store=False, ) @@ -245,9 +249,9 @@ def build_protocol_client(cfg: dict[str, Any], *, title: str) -> LLMClient | Non """ protocol = protocol_of(cfg) if protocol == "anthropic": - return _build_anthropic(cfg) + return _build_anthropic(cfg, title) if protocol == "bedrock": - return _build_anthropic(cfg, bedrock=True) + return _build_anthropic(cfg, title, bedrock=True) if protocol == "responses": return _build_responses(cfg, title) return None diff --git a/pyproject.toml b/pyproject.toml index a833224..5522835 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "apodex-agent-core" -version = "0.11.0" +version = "0.11.1" description = "Shared, product-neutral runtime primitives for Apodex agents" readme = "README.md" license = "Apache-2.0" diff --git a/tests/test_provider_protocol_reasoning.py b/tests/test_provider_protocol_reasoning.py index 673f499..c4e745d 100644 --- a/tests/test_provider_protocol_reasoning.py +++ b/tests/test_provider_protocol_reasoning.py @@ -527,3 +527,46 @@ def test_absent_or_valid_protocol_is_not_logged(cfg, caplog): with caplog.at_level(logging.WARNING): protocol_of(cfg) assert "unknown llm.protocol" not in caplog.text + + +@pytest.mark.parametrize("protocol", ["anthropic", "bedrock"]) +def test_anthropic_builders_forward_default_headers(monkeypatch, protocol): + # Regression: the builder used to drop cfg["default_headers"], so gateway + # routing/auth headers never reached Anthropic or Bedrock requests. + import agent_core.providers.anthropic as anthropic_mod + + seen = {} + + def fake_init(self, *args, **kwargs): + seen.update(kwargs) + + monkeypatch.setattr(anthropic_mod.AnthropicClient, "__init__", fake_init) + headers = {"X-Route": "gw-a"} + build_protocol_client( + {"model": "claude-x", "protocol": protocol, "api_key": "k", + "default_headers": headers}, title="T") + assert seen["default_headers"] == {"X-Title": "T", "X-Route": "gw-a"} + assert headers == {"X-Route": "gw-a"} + assert seen["bedrock"] is (protocol == "bedrock") + + build_protocol_client({"model": "claude-x", "protocol": protocol}, title="T") + assert seen["default_headers"] == {"X-Title": "T"} + + +def test_openai_responses_builder_merges_default_headers(monkeypatch): + # Gateway headers from cfg must reach the Responses client alongside X-Title. + import agent_core.providers.openai_responses as responses_mod + + seen = {} + + def fake_init(self, *args, **kwargs): + seen.update(kwargs) + + monkeypatch.setattr(responses_mod.OpenAIResponsesClient, "__init__", fake_init) + build_protocol_client( + {"model": "gpt-x", "protocol": "responses", "api_key": "k", + "default_headers": {"X-Route": "gw-a"}}, title="T") + assert seen["default_headers"] == {"X-Title": "T", "X-Route": "gw-a"} + + build_protocol_client({"model": "gpt-x", "protocol": "responses"}, title="T") + assert seen["default_headers"] == {"X-Title": "T"} diff --git a/uv.lock b/uv.lock index 2cad586..cf914dd 100644 --- a/uv.lock +++ b/uv.lock @@ -50,7 +50,7 @@ wheels = [ [[package]] name = "apodex-agent-core" -version = "0.11.0" +version = "0.11.1" source = { editable = "." } dependencies = [ { name = "anthropic", extra = ["bedrock"] },