From 1a2311bb9c6b0c33f85153953bd2a554b4a24f8b Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Thu, 24 Sep 2026 09:53:39 +0800 Subject: [PATCH 1/4] fix(providers): forward default_headers to Anthropic/Bedrock clients _build_anthropic dropped cfg['default_headers'] even though AnthropicClient accepts and forwards them for both the direct and Bedrock transports. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 11 +++++++++++ agent_core/providers/protocol_client.py | 4 +++- tests/test_provider_protocol_reasoning.py | 24 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c3b1f..5d72477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ the GitHub Release body, so a release with no entry here fails. Versioning follows [docs/versioning.md](docs/versioning.md). +## [Unreleased] + +### 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. + ## [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..5af2c9a 100644 --- a/agent_core/providers/protocol_client.py +++ b/agent_core/providers/protocol_client.py @@ -119,7 +119,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 forwarded to the client verbatim. ``thinking_type`` selects the request shape (default ``adaptive``). Live- verified against api.anthropic.com + Bedrock 2026-07-09 (see @@ -169,6 +170,7 @@ def _build_anthropic(cfg: dict[str, Any], *, bedrock: bool = False) -> LLMClient max_tokens=max_tokens, thinking=thinking, effort=effort, + default_headers=dict(cfg.get("default_headers") or {}), bedrock=bedrock, ) diff --git a/tests/test_provider_protocol_reasoning.py b/tests/test_provider_protocol_reasoning.py index 673f499..0e4f5b5 100644 --- a/tests/test_provider_protocol_reasoning.py +++ b/tests/test_provider_protocol_reasoning.py @@ -527,3 +527,27 @@ 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-Route": "gw-a"} + assert seen["default_headers"] is not headers + assert seen["bedrock"] is (protocol == "bedrock") + + build_protocol_client({"model": "claude-x", "protocol": protocol}, title="T") + assert seen["default_headers"] == {} From 88dcc90548243124a1da06b18d2d53c8b0c5a6b6 Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Thu, 24 Sep 2026 10:06:59 +0800 Subject: [PATCH 2/4] fix(providers): merge default_headers into Responses client headers Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 ++ agent_core/providers/protocol_client.py | 2 +- tests/test_provider_protocol_reasoning.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d72477..c205551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Versioning follows [docs/versioning.md](docs/versioning.md). 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. +- The `protocol: responses` builder now merges `cfg["default_headers"]` with + its `X-Title` header instead of discarding them. ## [0.11.0] - 2026-09-14 diff --git a/agent_core/providers/protocol_client.py b/agent_core/providers/protocol_client.py index 5af2c9a..f423c8a 100644 --- a/agent_core/providers/protocol_client.py +++ b/agent_core/providers/protocol_client.py @@ -229,7 +229,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, ) diff --git a/tests/test_provider_protocol_reasoning.py b/tests/test_provider_protocol_reasoning.py index 0e4f5b5..e5c5d01 100644 --- a/tests/test_provider_protocol_reasoning.py +++ b/tests/test_provider_protocol_reasoning.py @@ -551,3 +551,22 @@ def fake_init(self, *args, **kwargs): build_protocol_client({"model": "claude-x", "protocol": protocol}, title="T") assert seen["default_headers"] == {} + + +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"} From f92ad837fa619875a1d196766420d29b37129096 Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Thu, 24 Sep 2026 10:08:43 +0800 Subject: [PATCH 3/4] fix(providers): send X-Title from Anthropic/Bedrock clients Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 ++ agent_core/providers/protocol_client.py | 12 +++++++----- tests/test_provider_protocol_reasoning.py | 6 +++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c205551..4679e09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Versioning follows [docs/versioning.md](docs/versioning.md). 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. diff --git a/agent_core/providers/protocol_client.py b/agent_core/providers/protocol_client.py index f423c8a..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 @@ -120,7 +122,7 @@ def _build_anthropic(cfg: dict[str, Any], *, bedrock: bool = False) -> LLMClient (``bedrock=True``, AWS Bedrock runtime, Bearer API-key auth + the ``anthropic_version`` body stamp). Optional ``effort`` → ``output_config.effort``. ``default_headers`` (gateway routing / auth - headers) is forwarded to the client verbatim. + 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 @@ -170,7 +172,7 @@ def _build_anthropic(cfg: dict[str, Any], *, bedrock: bool = False) -> LLMClient max_tokens=max_tokens, thinking=thinking, effort=effort, - default_headers=dict(cfg.get("default_headers") or {}), + default_headers={"X-Title": title, **(cfg.get("default_headers") or {})}, bedrock=bedrock, ) @@ -247,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/tests/test_provider_protocol_reasoning.py b/tests/test_provider_protocol_reasoning.py index e5c5d01..c4e745d 100644 --- a/tests/test_provider_protocol_reasoning.py +++ b/tests/test_provider_protocol_reasoning.py @@ -545,12 +545,12 @@ def fake_init(self, *args, **kwargs): build_protocol_client( {"model": "claude-x", "protocol": protocol, "api_key": "k", "default_headers": headers}, title="T") - assert seen["default_headers"] == {"X-Route": "gw-a"} - assert seen["default_headers"] is not headers + 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"] == {} + assert seen["default_headers"] == {"X-Title": "T"} def test_openai_responses_builder_merges_default_headers(monkeypatch): From c42a9070c837c947549675d84a495ab1cfa84888 Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Thu, 24 Sep 2026 10:11:23 +0800 Subject: [PATCH 4/4] chore(release): set version to 0.11.1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4679e09..c1614a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ the GitHub Release body, so a release with no entry here fails. Versioning follows [docs/versioning.md](docs/versioning.md). -## [Unreleased] +## [0.11.1] - 2026-09-24 ### Fixed 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/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"] },