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
1 change: 1 addition & 0 deletions core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ class ProvidersConfig(_Base):
openrouter: ProviderConfig = Field(default_factory=ProviderConfig)
forge: ProviderConfig = Field(default_factory=ProviderConfig)
requesty: ProviderConfig = Field(default_factory=ProviderConfig)
bedrock: ProviderConfig = Field(default_factory=ProviderConfig)
anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
openai: ProviderConfig = Field(default_factory=ProviderConfig)
deepseek: ProviderConfig = Field(default_factory=ProviderConfig)
Expand Down
20 changes: 20 additions & 0 deletions core/providers/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ class ModelInfo:
mandatory=True,
)

# Bedrock exposes the same Claude release through direct, geography-scoped,
# and global inference IDs. Keep these exact spellings in the offline catalog:
# the dots are part of the IDs and cannot be removed by the slash normalizer.
_BEDROCK_CLAUDE_SONNET_46_IDS = (
"anthropic.claude-sonnet-4-6",
"us.anthropic.claude-sonnet-4-6",
"eu.anthropic.claude-sonnet-4-6",
"au.anthropic.claude-sonnet-4-6",
"jp.anthropic.claude-sonnet-4-6",
"global.anthropic.claude-sonnet-4-6",
)

# --------------------------------------------------------------------------
# Seed catalog — exact ids DeepCode targets, curated from models.dev.
# Context/output are the vendor-published limits; costs are list price per 1M.
Expand Down Expand Up @@ -186,6 +198,14 @@ class ModelInfo:
"claude-haiku-4-5": ModelInfo(
"claude-haiku-4-5", 200_000, 64_000, 1.0, 5.0, reasoning=_REASONING_ANTHROPIC
),
# Amazon Bedrock Claude Sonnet 4.6 inference IDs. Bedrock publishes a 1M
# context window for this release. Pricing is deliberately absent because
# it is account/region/service-tier dependent, and the OpenAI-compatible
# surface does not publish DeepCode's semantic reasoning-effort ladder.
**{
model_id: ModelInfo(model_id, 1_000_000, 64_000)
for model_id in _BEDROCK_CLAUDE_SONNET_46_IDS
},
# Dotted spellings of the same Anthropic releases. Listed rather than
# normalised away, because the dot is meaningful elsewhere in this table
# (``gpt-5.4``, ``kimi-k2.5`` are distinct models, not separator noise).
Expand Down
8 changes: 8 additions & 0 deletions core/providers/catalog_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,14 @@ def _looks_native(model_id: str, provider_name: str) -> bool:
markers = {
"openai": ("gpt-", "o1", "o3", "o4"),
"anthropic": ("claude-",),
"bedrock": (
"anthropic.",
"us.anthropic.",
"eu.anthropic.",
"au.anthropic.",
"jp.anthropic.",
"global.anthropic.",
),
"gemini": ("gemini-",),
"deepseek": ("deepseek-",),
"dashscope": ("qwen",),
Expand Down
12 changes: 12 additions & 0 deletions core/providers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ def label(self) -> str:
# OpenRouter-style gateways above.
strip_model_prefix=True,
),
ProviderSpec(
name="bedrock",
keywords=("bedrock",),
env_key="AWS_BEARER_TOKEN_BEDROCK",
display_name="Amazon Bedrock",
backend="openai_compat",
is_gateway=True,
detect_by_base_keyword="bedrock-runtime",
# Bedrock Runtime endpoints are region-specific, so there is no safe
# registry default. The connection supplies its own ``/openai/v1`` URL.
requires_api_base=True,
),
ProviderSpec(
name="anthropic",
keywords=("anthropic", "claude"),
Expand Down
24 changes: 24 additions & 0 deletions docs/guide/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ Connections live under `providers` in `~/.deepcode/deepcode_config.json`
work). Project-level config deliberately cannot add connections — credentials
stay user-scoped.

### Amazon Bedrock

Bedrock's OpenAI-compatible endpoint is region-specific. Supply its
`/openai/v1` base URL and expose an Amazon Bedrock API key through the standard
`AWS_BEARER_TOKEN_BEDROCK` environment variable:

```console
deepcode provider set work-bedrock --template bedrock \
--api-base https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1 \
--api-key-env AWS_BEARER_TOKEN_BEDROCK
deepcode provider models work-bedrock --refresh
deepcode provider test work-bedrock --model us.anthropic.claude-sonnet-4-6
```

Replace `us-east-1` with the Region that serves your account and choose a model
ID returned by the refresh command. Bedrock may return an in-Region ID such as
`anthropic.claude-sonnet-4-6`, a geography-scoped ID such as
`us.anthropic.claude-sonnet-4-6` or `eu.anthropic.claude-sonnet-4-6`, or a
`global.` ID depending on model and Region.

This first integration sends the configured value as a bearer token through
Bedrock's OpenAI-compatible Chat Completions API. It does **not** implement AWS
SigV4 signing, IAM role/profile discovery, SSO, or the native Converse API.

## Declared models

When a gateway serves a model the catalog doesn't know — or knows wrongly —
Expand Down
194 changes: 194 additions & 0 deletions tests/test_bedrock_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""Amazon Bedrock's bounded OpenAI-compatible provider integration."""

from __future__ import annotations

import sys
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

from core.config import DeepCodeConfig, ProvidersConfig # noqa: E402
from core.providers.catalog import resolve_model_info # noqa: E402
from core.providers.catalog_service import ModelCatalogService # noqa: E402
from core.providers.credentials import CredentialStore # noqa: E402
from core.providers.openai_compat import OpenAICompatProvider # noqa: E402
from core.providers.profiles import ConnectionResolver # noqa: E402
from core.providers.registry import find_by_name # noqa: E402


BEDROCK_MODEL_IDS = (
"anthropic.claude-sonnet-4-6",
"us.anthropic.claude-sonnet-4-6",
"eu.anthropic.claude-sonnet-4-6",
"au.anthropic.claude-sonnet-4-6",
"jp.anthropic.claude-sonnet-4-6",
"global.anthropic.claude-sonnet-4-6",
)


def _connection(tmp_path: Path):
credentials = CredentialStore(tmp_path / "credentials.json")
credentials.set("work-bedrock", "bedrock-test-secret")
config = DeepCodeConfig.model_validate(
{
"providers": {
"profiles": {
"work-bedrock": {
"template": "bedrock",
"apiBase": (
"https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1"
),
}
}
}
}
)
return ConnectionResolver(config, credentials).resolve_connection("work-bedrock")


def test_bedrock_is_a_region_scoped_openai_compatible_template() -> None:
spec = find_by_name("bedrock")

assert spec is not None
assert spec.display_name == "Amazon Bedrock"
assert spec.backend == "openai_compat"
assert spec.is_gateway is True
assert spec.requires_api_base is True
assert spec.default_api_base == ""
assert spec.detect_by_base_keyword == "bedrock-runtime"
assert spec.env_key == "AWS_BEARER_TOKEN_BEDROCK"
assert hasattr(ProvidersConfig(), "bedrock")


def test_bedrock_profile_keeps_the_region_url_and_bearer_credential(
tmp_path: Path,
) -> None:
connection = _connection(tmp_path)

assert connection.provider_name == "bedrock"
assert connection.adapter == "openai_compat"
assert connection.api_base == (
"https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1"
)
assert connection.api_key == "bedrock-test-secret"
assert connection.model_catalog == "openai"
assert connection.is_usable is True


def test_bedrock_requires_a_user_supplied_region_url(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "bedrock-test-secret")
connection = ConnectionResolver(
DeepCodeConfig(), CredentialStore(tmp_path / "credentials.json")
).resolve_connection("bedrock")

assert connection.api_key == "bedrock-test-secret"
assert connection.api_base is None
assert connection.is_configured is False
assert connection.is_usable is False


def test_bedrock_catalog_uses_models_endpoint_and_bearer_auth(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
connection = _connection(tmp_path)
seen: dict[str, object] = {}

class Response:
def raise_for_status(self) -> None:
return None

def json(self) -> dict[str, object]:
return {"data": [{"id": "us.anthropic.claude-sonnet-4-6"}]}

class Client:
def __init__(self, **kwargs: object) -> None:
seen["client"] = kwargs

def __enter__(self):
return self

def __exit__(self, *_args: object) -> None:
return None

def get(self, url: str, *, headers: dict[str, str]):
seen["url"] = url
seen["headers"] = headers
return Response()

monkeypatch.setattr("core.providers.catalog_service.httpx.Client", Client)
result = ModelCatalogService(tmp_path / "cache.json").list_models(
connection, refresh=True
)

assert result.source == "remote"
assert [model.id for model in result.models] == ["us.anthropic.claude-sonnet-4-6"]
assert seen["url"] == (
"https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1/models"
)
assert seen["headers"] == {"Authorization": "Bearer bedrock-test-secret"}


def test_bedrock_catalog_fallback_lists_exact_inference_ids(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
connection = _connection(tmp_path)
service = ModelCatalogService(tmp_path / "cache.json")

def offline(_connection):
raise OSError("network unavailable")

monkeypatch.setattr(service, "_fetch", offline)
result = service.list_models(connection, refresh=True)

assert result.source == "fallback"
assert result.stale is True
assert tuple(model.id for model in result.models) == tuple(
sorted(BEDROCK_MODEL_IDS)
)
assert {model.context_window for model in result.models} == {1_000_000}
assert {model.max_output_tokens for model in result.models} == {64_000}
assert all(model.reasoning is None for model in result.models)


def test_bedrock_chat_request_preserves_the_dotted_model_id() -> None:
provider = OpenAICompatProvider(
api_key="bedrock-test-secret",
api_base="https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1",
default_model="us.anthropic.claude-sonnet-4-6",
spec=find_by_name("bedrock"),
)

kwargs = provider._build_kwargs(
[{"role": "user", "content": "Hello"}],
tools=None,
model=None,
max_tokens=512,
temperature=0.2,
reasoning_effort=None,
tool_choice=None,
)

assert kwargs["model"] == "us.anthropic.claude-sonnet-4-6"
assert kwargs["max_tokens"] == 512
assert "reasoning_effort" not in kwargs
assert provider._should_use_responses_api(None, None) is False


@pytest.mark.parametrize("model_id", BEDROCK_MODEL_IDS)
def test_bedrock_model_ids_have_conservative_offline_metadata(model_id: str) -> None:
info = resolve_model_info(model_id)

assert info.id == model_id
assert info.source == "seed"
assert info.context_window == 1_000_000
assert info.max_output_tokens == 64_000
assert info.reasoning is None
Loading