Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions agent_core/providers/protocol_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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,
)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
43 changes: 43 additions & 0 deletions tests/test_provider_protocol_reasoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading