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
17 changes: 13 additions & 4 deletions frontier_agent/infra/anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@
import logging
import os
from collections.abc import AsyncIterator
from typing import Any
from typing import Any, Protocol

from frontier_agent.core.llm import LLMClient, LLMResponse, StreamDelta
from frontier_agent.core.messages import Message, ToolCall, text_of

logger = logging.getLogger(__name__)


class _MutableHeaders(Protocol):
def __setitem__(self, key: str, value: str) -> None: ...


class _RequestWithHeaders(Protocol):
@property
def headers(self) -> _MutableHeaders: ...


class AnthropicClient(LLMClient):
"""Non-streaming-first Anthropic adapter."""

Expand Down Expand Up @@ -243,8 +252,6 @@ def _build_bedrock_client(
else the Bedrock client gives for free is what we want — the
``/v1/messages`` → ``/model/{id}/invoke`` URL rewrite and the
``anthropic_version: bedrock-2023-05-31`` body stamp."""
import httpx

# ``anthropic.AsyncAnthropicBedrock`` is the SDK's documented entry point,
# but it is missing from the package's ``__all__``, so the checker suggests
# importing from ``anthropic._client`` instead. Keep the public path — a
Expand All @@ -255,7 +262,9 @@ def _build_bedrock_client(
bearer = api_key or ""

class _BearerBedrock(AsyncAnthropicBedrock):
async def _prepare_request(self, request: httpx.Request) -> None:
# Anthropic 1.0 migrated its transport from httpx to httpx2. Both
# request types satisfy this deliberately narrow headers protocol.
async def _prepare_request(self, request: _RequestWithHeaders) -> None:
request.headers["Authorization"] = f"Bearer {bearer}"

return _BearerBedrock(
Expand Down
6 changes: 5 additions & 1 deletion frontier_agent/infra/openai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ def __init__(
# Per-call timeout is enforced by the agent loop's
# ``asyncio.wait_for`` wrapper, not by the SDK.
self._client = AsyncOpenAI(
api_key=api_key,
# OpenAI 2.54 rejects an explicitly supplied empty key before it
# consults OPENAI_API_KEY. A cached config can legitimately hold
# the pre-environment empty value, so treat it as unspecified and
# preserve the SDK's normal environment fallback.
api_key=api_key or None,
base_url=base_url or None,
timeout=timeout,
default_headers=default_headers,
Expand Down
31 changes: 25 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ license = { file = "LICENSE" }
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.0",
"pydantic-settings>=2.0",
"pydantic-settings>=2.14.2",
"openai>=1.50",
"anthropic>=0.34",
"anthropic>=0.87.0",
"httpx>=0.27",
"trafilatura>=1.12",
"python-dotenv>=1.0",
Expand All @@ -19,7 +19,7 @@ dependencies = [
"tiktoken>=0.12.0",
"pyyaml>=6.0",
"tenacity>=9.1.4",
"Pillow>=10.0",
"Pillow>=12.3.0",
# create_file promises Office authoring in every runtime. Keep its writers
# deterministic instead of relying on an in-session pip install.
"openpyxl>=3.1",
Expand All @@ -31,7 +31,7 @@ dependencies = [
document-readers = [
"python-docx>=1.1",
"python-pptx>=0.6.23",
"pypdf>=4.0",
"pypdf>=6.15.0",
]
plugins = [
"tavily-python>=0.5.0",
Expand All @@ -52,11 +52,11 @@ eval = [
"openpyxl>=3.1",
"python-docx>=1.1",
"python-pptx>=0.6.23",
"pypdf>=4.0",
"pypdf>=6.15.0",
]
dev = [
"openpyxl>=3.1",
"pypdf>=4.0",
"pypdf>=6.15.0",
"python-docx>=1.1",
"python-pptx>=0.6.23",
"pytest>=8.3",
Expand Down Expand Up @@ -102,6 +102,25 @@ packages = ["frontier_agent", "apodex", "plugins", "workflows", "benchmarks"]
[tool.hatch.build.targets.wheel.force-include]
"frontier_agent/py.typed" = "frontier_agent/py.typed"

[tool.uv]
# Security floors for transitive packages reported by Dependabot. Keeping these
# as resolver constraints avoids promoting implementation details of optional
# dependencies to FrontierAgent's public runtime API.
constraint-dependencies = [
"aiohttp>=3.14.3",
"cryptography>=50.0.0",
"h2>=4.4.1",
"idna>=3.15",
"litellm>=1.84.0",
"lxml>=6.1.0",
"mcp>=1.28.1",
"Pygments>=2.20.0",
"PyJWT>=2.13.0",
"python-multipart>=0.0.31",
"requests>=2.33.0",
"urllib3>=2.7.0",
]

[tool.ruff]
target-version = "py312"
line-length = 100
Expand Down
26 changes: 26 additions & 0 deletions tests/test_anthropic_bedrock_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

import httpx2
import pytest

from frontier_agent.infra.anthropic_client import _build_bedrock_client


@pytest.mark.asyncio
async def test_bedrock_client_injects_bearer_auth_with_locked_transport() -> None:
client = _build_bedrock_client(
"bedrock-test-token",
"https://bedrock-runtime.us-east-1.amazonaws.com",
5.0,
)
request = httpx2.Request(
"POST",
"https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke",
)

try:
await client._prepare_request(request)
finally:
await client.close()

assert request.headers["Authorization"] == "Bearer bedrock-test-token"
7 changes: 7 additions & 0 deletions tests/test_benchmark_kernel_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@

@pytest.mark.asyncio
async def test_benchmark_session_bootstraps_team_runtime(monkeypatch: pytest.MonkeyPatch) -> None:
from frontier_agent.infra.config import get_config

# Reproduce the full-suite ordering where config is cached before a later
# bootstrap supplies credentials through the environment. OpenAI 2.54 must
# receive None, not the stale empty string, so it performs its env fallback.
cached_config = get_config(force_reload=True)
monkeypatch.setattr(cached_config, "openai_api_key", "")
monkeypatch.setenv("OPENAI_API_KEY", "test")
monkeypatch.setenv("OPENAI_BASE_URL", "http://127.0.0.1:9/v1")
monkeypatch.setenv("OPENAI_MODEL", "test")
Expand Down
Loading
Loading