Skip to content
Open
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ Wizard defaults reflect the project's per-phase recommendations (stronger reason
| `anthropic` | [console.anthropic.com](https://console.anthropic.com/settings/keys) | Reference adapter. NOT included in Claude Pro / Max subscriptions — separate billing. |
| `openai` | [platform.openai.com](https://platform.openai.com/api-keys) | NOT included in ChatGPT / Codex subscriptions — separate billing. |
| `google` | [aistudio.google.com](https://aistudio.google.com/apikey) | NOT included in Gemini Advanced — separate billing. |
| `bedrock` | — (AWS credential chain) | Claude on AWS Bedrock. No `api_key`: credentials come from `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` env vars or a `~/.aws` profile, region from `AWS_REGION`. Model IDs are inference profiles (`us.anthropic.claude-sonnet-4-6`, `global.anthropic.claude-haiku-4-5-20251001-v1:0`, ...) — enable them under "Model access" in the Bedrock console and list them with `aws bedrock list-inference-profiles`. Not offered by the setup wizard yet; configure by hand — full guide: [`utilities/llm/providers/BEDROCK.md`](libs/openant-core/utilities/llm/providers/BEDROCK.md). |

All three support tool calling, so any of them can drive the `enhance` and `verify` phases that use the agentic tool-use loop.
All four support tool calling, so any of them can drive the `enhance` and `verify` phases that use the agentic tool-use loop.

#### Quick path for Anthropic-only setups

Expand Down Expand Up @@ -216,7 +217,7 @@ openant project switch <org/repo> # switch active project

Things on the list, in no particular order:

- **More provider adapters.** Ollama (local models), vLLM, Cohere, Mistral, Groq, Amazon Bedrock, Azure OpenAI — each is a small Python adapter recipe (plus a few Go wizard/probe touch-points if you want it offered by `openant setup llm`) per the contributor guide. Lower the barrier to local / on-prem inference.
- **More provider adapters.** Ollama (local models), vLLM, Cohere, Mistral, Groq, Azure OpenAI — each is a small Python adapter recipe (plus a few Go wizard/probe touch-points if you want it offered by `openant setup llm`) per the contributor guide. Lower the barrier to local / on-prem inference.
- **Subscription-based auth.** ChatGPT / Codex, Claude Pro / Max, and Gemini Advanced subscriptions don't currently grant API quota — users have to maintain a separate API-tier key per provider. OAuth-based adapters that ride the consumer subscription would close that gap.
- **Cross-provider tool-call quirks.** All three shipped adapters support tool calling, but the long tail (parallel tool calls, strict-mode schema enforcement, retry semantics on partial JSON) behaves differently per provider. Real-world scans surface these — PRs welcome.
- **More languages.** The supported-languages list above is current coverage. Rust, Java, C#, and Swift come up frequently.
Expand Down
36 changes: 36 additions & 0 deletions config/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,42 @@
"source": "liveness contested and not verifiable in build env; asserted neither alive nor dead",
"retrieved": "2026-07-23"
},
{
"id": "us.anthropic.claude-opus-4-8", "provider": "bedrock", "status": "current",
"price": {"input": 15.00, "output": 75.00},
"source": "verified against a live Bedrock account via list-inference-profiles (us-east-1, status ACTIVE); price mirrors the anthropic claude-opus-4-8 record (Bedrock Claude token rates match the direct Anthropic API)",
"retrieved": "2026-07-31"
},
{
"id": "global.anthropic.claude-opus-4-8", "provider": "bedrock", "status": "current",
"price": {"input": 15.00, "output": 75.00},
"source": "verified against a live Bedrock account via list-inference-profiles (us-east-1, status ACTIVE); price mirrors the anthropic claude-opus-4-8 record (Bedrock Claude token rates match the direct Anthropic API)",
"retrieved": "2026-07-31"
},
{
"id": "us.anthropic.claude-sonnet-4-6", "provider": "bedrock", "status": "current",
"price": {"input": 3.00, "output": 15.00},
"source": "verified against a live Bedrock account via list-inference-profiles (us-east-1, status ACTIVE); price mirrors the anthropic claude-sonnet-4-6 record (Bedrock Claude token rates match the direct Anthropic API)",
"retrieved": "2026-07-31"
},
{
"id": "global.anthropic.claude-sonnet-4-6", "provider": "bedrock", "status": "current",
"price": {"input": 3.00, "output": 15.00},
"source": "verified against a live Bedrock account via list-inference-profiles (us-east-1, status ACTIVE); price mirrors the anthropic claude-sonnet-4-6 record (Bedrock Claude token rates match the direct Anthropic API)",
"retrieved": "2026-07-31"
},
{
"id": "us.anthropic.claude-haiku-4-5-20251001-v1:0", "provider": "bedrock", "status": "current",
"price": {"input": 1.00, "output": 5.00},
"source": "verified against a live Bedrock account via list-inference-profiles (us-east-1, status ACTIVE); price mirrors the anthropic claude-haiku-4-5-20251001 record (Bedrock Claude token rates match the direct Anthropic API)",
"retrieved": "2026-07-31"
},
{
"id": "global.anthropic.claude-haiku-4-5-20251001-v1:0", "provider": "bedrock", "status": "current",
"price": {"input": 1.00, "output": 5.00},
"source": "verified against a live Bedrock account via list-inference-profiles (us-east-1, status ACTIVE); price mirrors the anthropic claude-haiku-4-5-20251001 record (Bedrock Claude token rates match the direct Anthropic API)",
"retrieved": "2026-07-31"
},
{
"id": "gpt-4o", "provider": "openai", "status": "current",
"price": {"input": 2.50, "output": 10.00},
Expand Down
2 changes: 1 addition & 1 deletion libs/openant-core/core/model_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
_CONFIG_REL = Path("config") / "models.json"
_SEARCH_LEVELS = 6
_VALID_STATUS = frozenset({"current", "retired", "unknown"})
_VALID_PROVIDERS = frozenset({"anthropic", "openai", "google"})
_VALID_PROVIDERS = frozenset({"anthropic", "openai", "google", "bedrock"})


def _search_upward(start: Path) -> Path | None:
Expand Down
152 changes: 152 additions & 0 deletions libs/openant-core/tests/_llm_factories/bedrock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Scenario factory for the Bedrock adapter contract tests.

Each scenario builds a fake ``anthropic.AnthropicBedrock`` client wired
with the right scripted behavior, then constructs a
:class:`BedrockAdapter` over that fake. Bedrock speaks the same
Messages API through the same ``anthropic`` SDK types and exception
classes as the direct API, so the scripted responses and raised
exceptions mirror the Anthropic factory — only the client spec and the
endpoint URL differ.

See ``tests/test_llm_adapter_contract.py`` for the scenario catalogue.
"""

from __future__ import annotations

from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock

import anthropic
import httpx

from utilities.llm import LLMAdapter
from utilities.llm.providers.bedrock import BedrockAdapter


_ENDPOINT = "https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke"


def _text_block(text: str) -> SimpleNamespace:
return SimpleNamespace(type="text", text=text)


def _tool_use_block(*, id: str, name: str, input: dict) -> SimpleNamespace:
return SimpleNamespace(type="tool_use", id=id, name=name, input=input)


def _response(
*, content: list, input_tokens: int, output_tokens: int, stop_reason: str
) -> SimpleNamespace:
return SimpleNamespace(
content=content,
usage=SimpleNamespace(
input_tokens=input_tokens, output_tokens=output_tokens
),
stop_reason=stop_reason,
)


def _fake_httpx_response(status_code: int, *, retry_after: str | None = None) -> httpx.Response:
headers = {}
if retry_after is not None:
headers["retry-after"] = retry_after
return httpx.Response(
status_code=status_code,
headers=headers,
request=httpx.Request("POST", _ENDPOINT),
)


def _script_text(call_args: dict) -> SimpleNamespace:
# The contract test asserts content=="hi there", usage 3/5, end_turn.
return _response(
content=[_text_block("hi there")],
input_tokens=3,
output_tokens=5,
stop_reason="end_turn",
)


def _script_tool_use_round(call_args: dict) -> SimpleNamespace:
has_assistant = any(m.get("role") == "assistant" for m in call_args["messages"])
if not has_assistant:
return _response(
content=[
_tool_use_block(
id="toolu_test_1",
name="echo",
input={"text": "hello"},
)
],
input_tokens=10,
output_tokens=8,
stop_reason="tool_use",
)
return _response(
content=[_text_block("echoed: hello")],
input_tokens=20,
output_tokens=4,
stop_reason="end_turn",
)


def _raise_auth(_call_args: dict):
raise anthropic.AuthenticationError(
message="invalid signature",
response=_fake_httpx_response(401),
body=None,
)


def _raise_rate_limit(_call_args: dict):
# Bedrock throttling (ThrottlingException) surfaces as the SDK's
# standard 429 RateLimitError.
raise anthropic.RateLimitError(
message="too many requests, please wait before trying again",
response=_fake_httpx_response(429, retry_after="7"),
body=None,
)


def _raise_connection(_call_args: dict):
raise anthropic.APIConnectionError(
request=httpx.Request("POST", _ENDPOINT),
)


def _raise_not_found(_call_args: dict):
raise anthropic.NotFoundError(
message="model not found: ghost-model",
response=_fake_httpx_response(404),
body=None,
)


_SCENARIO_HANDLERS = {
"text": _script_text,
"tool_use_round": _script_tool_use_round,
"auth_error": _raise_auth,
"rate_limit": _raise_rate_limit,
"connection_error": _raise_connection,
"model_not_found": _raise_not_found,
"validate_ok": _script_text, # any valid response satisfies validate
"validate_auth_fail": _raise_auth, # validate is a thin wrapper over create
}


def make_adapter(scenario: str) -> LLMAdapter:
"""Build a BedrockAdapter whose SDK is scripted for ``scenario``."""
if scenario not in _SCENARIO_HANDLERS:
raise KeyError(f"Unknown scenario: {scenario!r}")

handler = _SCENARIO_HANDLERS[scenario]

def side_effect(**kwargs: Any) -> Any:
return handler(kwargs)

fake_client = MagicMock(spec=anthropic.AnthropicBedrock)
fake_client.messages = MagicMock()
fake_client.messages.create = MagicMock(side_effect=side_effect)

return BedrockAdapter(_client=fake_client)
7 changes: 7 additions & 0 deletions libs/openant-core/tests/test_llm_adapter_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,19 @@ def _google_factory():
return make_adapter


def _bedrock_factory():
from tests._llm_factories.bedrock import make_adapter

return make_adapter


# Each row: (display_name, scenario_factory_callable)
# Add a row when registering a new adapter.
ADAPTERS: list[tuple[str, Callable[[str], LLMAdapter]]] = [
("anthropic", _anthropic_factory()),
("openai", _openai_factory()),
("google", _google_factory()),
("bedrock", _bedrock_factory()),
]


Expand Down
Loading
Loading