From 4c338dcde4e8f24551dc2621fcb0ffe172b0f939 Mon Sep 17 00:00:00 2001 From: alex-dev1 Date: Mon, 6 Apr 2026 09:46:28 -0400 Subject: [PATCH 1/2] Add AWS Bedrock bearer auth support --- README.md | 23 ++++++- cli/deadend_cli/components/ConfigSetup.tsx | 5 +- cli/deadend_cli/components/LlmSelector.tsx | 1 + cli/deadend_cli/runtime/settings.ts | 2 +- cli/deadend_cli/types/rpc.ts | 2 +- .../src/deadend_agent/config/settings.py | 3 + .../deadend_agent/core_agent/core_agent.py | 16 +++++ .../deadend_agent/core_agent/rlm_runner.py | 6 ++ .../src/deadend_agent/models/registry.py | 6 ++ .../src/deadend_agent/utils/provider_env.py | 52 +++++++++++++++ .../deadend_agent/tests/test_provider_env.py | 65 +++++++++++++++++++ .../src/deadend_cli/component_manager.py | 4 +- 12 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 deadend_cli/deadend_agent/src/deadend_agent/utils/provider_env.py create mode 100644 deadend_cli/deadend_agent/tests/test_provider_env.py diff --git a/README.md b/README.md index abdace99..39e982c9 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ When defining a model, use the following schema. The key format `::": { - "provider": "", // Provider name (e.g., openai, anthropic, ollama) + "provider": "", // Provider name (e.g., openai, anthropic, bedrock, ollama) "model_name": "", // Model identifier (e.g., claude-sonnet-4-5, gpt-4) "api_key": "", // API key (optional if ENV var is set, but recommended to add here) "base_url": "", // Base URL for custom gateways or providers (e.g., Ollama) @@ -312,6 +312,7 @@ Models are specified using the format `:` in both `config. **Examples:** - `anthropic:claude-sonnet-4-5` - Anthropic's Claude Sonnet 4.5 - `openai:gpt-4` - OpenAI's GPT-4 +- `bedrock:us.anthropic.claude-3-5-haiku-20241022-v1:0` - Anthropic Claude via Amazon Bedrock - `openrouter:qwen/qwen3-embedding-8b` - Qwen embedding model via OpenRouter - `ollama:llama3` - Llama 3 via local Ollama instance @@ -322,6 +323,7 @@ Deadend CLI supports all providers compatible with LiteLLM. For a complete list **Popular providers include:** - **OpenAI**: `openai:gpt-4`, `openai:gpt-3.5-turbo`, `openai:gpt-4o`, etc. - **Anthropic**: `anthropic:claude-3-opus`, `anthropic:claude-sonnet-4-5`, `anthropic:claude-3-haiku`, etc. +- **AWS Bedrock**: `bedrock:us.anthropic.claude-3-5-haiku-20241022-v1:0`, `bedrock:amazon.nova-lite-v1:0`, etc. - **Ollama**: `ollama:llama3`, `ollama:mistral`, `ollama:codellama`, etc. (requires `base_url` in config) - **OpenRouter**: `openrouter:meta-llama/llama-3-70b-instruct`, `openrouter:google/gemini-pro`, etc. - **HuggingFace**: `huggingface/meta-llama/Llama-2-7b-chat-hf` (requires `base_url`) @@ -333,6 +335,25 @@ Deadend CLI supports all providers compatible with LiteLLM. For a complete list > **Note**: Some providers may require additional configuration such as `base_url` or specific API key formats. Refer to the [LiteLLM Provider Documentation](https://docs.litellm.ai/docs/providers) for provider-specific setup instructions. +#### AWS Bedrock Bearer Auth + +Deadend CLI accepts Bedrock API keys in the normal `api_key` field for `bedrock:*` models and maps them to the AWS bearer token flow at runtime. + +```json +{ + "bedrock:us.anthropic.claude-3-5-haiku-20241022-v1:0": { + "provider": "bedrock", + "model_name": "us.anthropic.claude-3-5-haiku-20241022-v1:0", + "api_key": "", + "base_url": "https://bedrock-runtime.us-east-1.amazonaws.com", + "type_model": null, + "vec_dim": null + } +} +``` + +`base_url` is optional, but recommended because Deadend can infer `AWS_DEFAULT_REGION` from a standard Bedrock endpoint like `https://bedrock-runtime.us-east-1.amazonaws.com`. If you omit it, set `AWS_DEFAULT_REGION` yourself. + ### CLI Interface Settings (`settings.json`) The CLI interface uses a separate `settings.json` file located at `~/.cache/deadend/settings.json` to store default preferences and UI settings. This file contains: diff --git a/cli/deadend_cli/components/ConfigSetup.tsx b/cli/deadend_cli/components/ConfigSetup.tsx index 8d0cba9e..eeab069f 100644 --- a/cli/deadend_cli/components/ConfigSetup.tsx +++ b/cli/deadend_cli/components/ConfigSetup.tsx @@ -7,7 +7,7 @@ interface ConfigSetupProps { onComplete: () => void; } -type ModelProvider = "openai" | "anthropic" | "gemini" | "openrouter" | "local"; +type ModelProvider = "openai" | "anthropic" | "gemini" | "bedrock" | "openrouter" | "local"; interface ModelConfig { provider: ModelProvider; @@ -26,7 +26,7 @@ export function ConfigSetup({ onComplete }: ConfigSetupProps) { const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); - const modelProviders: ModelProvider[] = ["openai", "anthropic", "gemini", "openrouter", "local"]; + const modelProviders: ModelProvider[] = ["openai", "anthropic", "gemini", "bedrock", "openrouter", "local"]; const toggleModel = useCallback((provider: ModelProvider) => { setSelectedModels((prev) => { @@ -334,4 +334,3 @@ max_history = 100 ); } - diff --git a/cli/deadend_cli/components/LlmSelector.tsx b/cli/deadend_cli/components/LlmSelector.tsx index 40db7f95..87c88dcc 100644 --- a/cli/deadend_cli/components/LlmSelector.tsx +++ b/cli/deadend_cli/components/LlmSelector.tsx @@ -126,6 +126,7 @@ export function LlmSelector({ rpcClient, onComplete, onCancel }: LlmSelectorProp openai: { apiKey: "OPENAI_API_KEY", model: "OPENAI_MODEL" }, anthropic: { apiKey: "ANTHROPIC_API_KEY", model: "ANTHROPIC_MODEL" }, gemini: { apiKey: "GEMINI_API_KEY", model: "GEMINI_MODEL" }, + bedrock: { apiKey: "AWS_BEARER_TOKEN_BEDROCK", model: "BEDROCK_MODEL" }, openrouter: { apiKey: "OPEN_ROUTER_API_KEY", model: "OPEN_ROUTER_MODEL" }, local: { apiKey: "LOCAL_API_KEY", model: "LOCAL_MODEL", baseUrl: "LOCAL_BASE_URL" }, }; diff --git a/cli/deadend_cli/runtime/settings.ts b/cli/deadend_cli/runtime/settings.ts index 8f6d9f7e..56b532a4 100644 --- a/cli/deadend_cli/runtime/settings.ts +++ b/cli/deadend_cli/runtime/settings.ts @@ -10,7 +10,7 @@ import { logger } from "./logger.ts"; export interface CliSettings { - /** Default LLM provider (openai, anthropic, gemini, openrouter, local) */ + /** Default LLM provider (openai, anthropic, gemini, bedrock, openrouter, local) */ provider?: string; /** Default model name */ model?: string; diff --git a/cli/deadend_cli/types/rpc.ts b/cli/deadend_cli/types/rpc.ts index 10d016aa..f26c54fa 100644 --- a/cli/deadend_cli/types/rpc.ts +++ b/cli/deadend_cli/types/rpc.ts @@ -313,7 +313,7 @@ export interface RunTaskParams { openapi_spec?: unknown; knowledge_base?: string; mode?: "yolo" | "safe" | "supervisor"; - /** LLM provider to use (openai, anthropic, gemini, openrouter, local) */ + /** LLM provider to use (openai, anthropic, gemini, bedrock, openrouter, local) */ provider?: string; /** Model name to use (overrides default for provider) */ model?: string; diff --git a/deadend_cli/deadend_agent/src/deadend_agent/config/settings.py b/deadend_cli/deadend_agent/src/deadend_agent/config/settings.py index debd79f2..c68b0330 100644 --- a/deadend_cli/deadend_agent/src/deadend_agent/config/settings.py +++ b/deadend_cli/deadend_agent/src/deadend_agent/config/settings.py @@ -174,6 +174,7 @@ class Config: - OpenAI: Uses OPENAI_API_KEY and OPENAI_MODEL - Anthropic: Uses ANTHROPIC_API_KEY and ANTHROPIC_MODEL - Google Gemini: Uses GEMINI_API_KEY and GEMINI_MODEL + - AWS Bedrock: Uses AWS_BEARER_TOKEN_BEDROCK and BEDROCK_MODEL - OpenRouter: Uses OPEN_ROUTER_API_KEY and OPEN_ROUTER_MODEL (supports multiple providers) - Local/Self-hosted: Uses LOCAL_API_KEY, LOCAL_MODEL, and LOCAL_BASE_URL @@ -193,6 +194,8 @@ class Config: anthropic_model_name : str | None = _cfg("ANTHROPIC_MODEL") gemini_api_key: str | None = _cfg("GEMINI_API_KEY") gemini_model_name : str | None = _cfg("GEMINI_MODEL", "gemini-2.5-pro") + bedrock_api_key: str | None = _cfg("AWS_BEARER_TOKEN_BEDROCK") + bedrock_model_name: str | None = _cfg("BEDROCK_MODEL") open_router_key: str | None = _cfg("OPEN_ROUTER_API_KEY") open_router_model: str | None = _cfg("OPEN_ROUTER_MODEL", "anthropic/claude-4.5-opus") local_model: str | None = _cfg("LOCAL_MODEL", "Kimi-K2-Thinking") diff --git a/deadend_cli/deadend_agent/src/deadend_agent/core_agent/core_agent.py b/deadend_cli/deadend_agent/src/deadend_agent/core_agent/core_agent.py index 7ab46dcc..1a1d1fdf 100644 --- a/deadend_cli/deadend_agent/src/deadend_agent/core_agent/core_agent.py +++ b/deadend_cli/deadend_agent/src/deadend_agent/core_agent/core_agent.py @@ -38,6 +38,7 @@ ContentPolicyViolationError, ) from deadend_agent.logging import get_module_logger +from deadend_agent.utils.provider_env import configure_litellm_provider_env from . import ( UsageLimitExceeded, LLMError, @@ -803,6 +804,11 @@ def log_retry(retry_state): before_sleep=log_retry, ) async def _call(): + configure_litellm_provider_env( + model=self.model, + api_key=self.api_key, + api_base=self.api_base, + ) kwargs = { "model": self.model, "messages": messages, @@ -1151,6 +1157,11 @@ async def _extract_structured(self, messages: list[dict]) -> BaseModel: # First try Instructor if available if self.instructor_client: try: + configure_litellm_provider_env( + model=self.model, + api_key=self.api_key, + api_base=self.api_base, + ) kwargs = { "model": self.model, "messages": messages, @@ -1276,6 +1287,11 @@ async def _extract_structured_manual(self, messages: list[dict]) -> BaseModel: extraction_messages = messages.copy() extraction_messages.append({"role": "user", "content": json_prompt}) + configure_litellm_provider_env( + model=self.model, + api_key=self.api_key, + api_base=self.api_base, + ) kwargs = { "model": self.model, "messages": extraction_messages, diff --git a/deadend_cli/deadend_agent/src/deadend_agent/core_agent/rlm_runner.py b/deadend_cli/deadend_agent/src/deadend_agent/core_agent/rlm_runner.py index 140617ea..637960bb 100644 --- a/deadend_cli/deadend_agent/src/deadend_agent/core_agent/rlm_runner.py +++ b/deadend_cli/deadend_agent/src/deadend_agent/core_agent/rlm_runner.py @@ -40,6 +40,7 @@ ) from deadend_agent.rlm.memory import RLMFileMemory, SUPPORTED_EXTENSIONS from deadend_agent.tools.python_interpreter.python_interpreter import PythonInterpreter +from deadend_agent.utils.provider_env import configure_litellm_provider_env try: from litellm import acompletion @@ -675,6 +676,11 @@ async def _call_model( if not LITELLM_AVAILABLE or acompletion is None: raise RuntimeError("litellm is required to run SandboxedRLMRunner model calls") + configure_litellm_provider_env( + model=model, + api_key=api_key, + api_base=api_base, + ) kwargs: dict[str, Any] = { "model": model, "messages": messages, diff --git a/deadend_cli/deadend_agent/src/deadend_agent/models/registry.py b/deadend_cli/deadend_agent/src/deadend_agent/models/registry.py index 798edb10..6eb9fa68 100644 --- a/deadend_cli/deadend_agent/src/deadend_agent/models/registry.py +++ b/deadend_cli/deadend_agent/src/deadend_agent/models/registry.py @@ -16,6 +16,7 @@ from pydantic import BaseModel from deadend_agent.config.settings import Config, ModelSpec, EmbeddingSpec, ProvidersList from deadend_agent.logging import logger +from deadend_agent.utils.provider_env import configure_litellm_provider_env class EmbedderClient: """Client for generating embeddings using various embedding API providers. @@ -67,6 +68,11 @@ async def batch_embed(self, input_texts: list[str]) -> list[dict]: ValueError: If the embedding call fails or returns an unexpected structure. """ try: + configure_litellm_provider_env( + model=self.model, + api_key=self.api_key, + api_base=self.base_url, + ) # Delegate embedding generation to LiteLLM's async embedding helper. # # NOTE: diff --git a/deadend_cli/deadend_agent/src/deadend_agent/utils/provider_env.py b/deadend_cli/deadend_agent/src/deadend_agent/utils/provider_env.py new file mode 100644 index 00000000..86cdea4f --- /dev/null +++ b/deadend_cli/deadend_agent/src/deadend_agent/utils/provider_env.py @@ -0,0 +1,52 @@ +# Copyright (C) 2025 Yassine Bargach +# Licensed under the GNU Affero General Public License v3 +# See LICENSE file for full license information. + +"""Provider-specific environment helpers for LiteLLM-backed calls.""" + +from __future__ import annotations + +import os +import re +from urllib.parse import urlparse + +BEDROCK_PROVIDER_PREFIX = "bedrock/" +BEDROCK_BEARER_ENV = "AWS_BEARER_TOKEN_BEDROCK" +AWS_DEFAULT_REGION_ENV = "AWS_DEFAULT_REGION" +AWS_REGION_ENV = "AWS_REGION" + +_BEDROCK_REGION_PATTERN = re.compile( + r"^bedrock(?:-runtime)?[.-]([a-z0-9-]+)\.amazonaws\.com(?:\.[a-z]{2})?$" +) + + +def infer_bedrock_region_from_base_url(api_base: str | None) -> str | None: + """Infer the AWS region from a Bedrock endpoint URL when possible.""" + if not api_base: + return None + + parsed = urlparse(api_base) + hostname = parsed.hostname or "" + match = _BEDROCK_REGION_PATTERN.match(hostname) + if match: + return match.group(1) + return None + + +def configure_litellm_provider_env( + *, + model: str, + api_key: str | None, + api_base: str | None, +) -> None: + """Apply provider-specific environment variables before a LiteLLM call.""" + if not model.startswith(BEDROCK_PROVIDER_PREFIX): + return + + if api_key: + os.environ[BEDROCK_BEARER_ENV] = api_key + + inferred_region = infer_bedrock_region_from_base_url(api_base) + if inferred_region: + os.environ.setdefault(AWS_DEFAULT_REGION_ENV, inferred_region) + os.environ.setdefault(AWS_REGION_ENV, inferred_region) diff --git a/deadend_cli/deadend_agent/tests/test_provider_env.py b/deadend_cli/deadend_agent/tests/test_provider_env.py new file mode 100644 index 00000000..d4fc8bd7 --- /dev/null +++ b/deadend_cli/deadend_agent/tests/test_provider_env.py @@ -0,0 +1,65 @@ +import os + +from deadend_agent.utils.provider_env import ( + AWS_DEFAULT_REGION_ENV, + AWS_REGION_ENV, + BEDROCK_BEARER_ENV, + configure_litellm_provider_env, + infer_bedrock_region_from_base_url, +) + + +def test_infer_bedrock_region_from_runtime_endpoint() -> None: + assert ( + infer_bedrock_region_from_base_url("https://bedrock-runtime.us-east-1.amazonaws.com") + == "us-east-1" + ) + + +def test_infer_bedrock_region_from_standard_endpoint() -> None: + assert ( + infer_bedrock_region_from_base_url("https://bedrock.eu-west-1.amazonaws.com") + == "eu-west-1" + ) + + +def test_configure_litellm_provider_env_sets_bedrock_bearer_and_region(monkeypatch) -> None: + monkeypatch.delenv(BEDROCK_BEARER_ENV, raising=False) + monkeypatch.delenv(AWS_DEFAULT_REGION_ENV, raising=False) + monkeypatch.delenv(AWS_REGION_ENV, raising=False) + + configure_litellm_provider_env( + model="bedrock/us.anthropic.claude-3-5-haiku-20241022-v1:0", + api_key="bedrock-api-key", + api_base="https://bedrock-runtime.us-east-1.amazonaws.com", + ) + + assert os.environ[BEDROCK_BEARER_ENV] == "bedrock-api-key" + assert os.environ[AWS_DEFAULT_REGION_ENV] == "us-east-1" + assert os.environ[AWS_REGION_ENV] == "us-east-1" + + +def test_configure_litellm_provider_env_does_not_override_existing_region(monkeypatch) -> None: + monkeypatch.setenv(AWS_DEFAULT_REGION_ENV, "ca-central-1") + monkeypatch.setenv(AWS_REGION_ENV, "ca-central-1") + + configure_litellm_provider_env( + model="bedrock/us.amazon.nova-lite-v1:0", + api_key=None, + api_base="https://bedrock-runtime.us-east-1.amazonaws.com", + ) + + assert os.environ[AWS_DEFAULT_REGION_ENV] == "ca-central-1" + assert os.environ[AWS_REGION_ENV] == "ca-central-1" + + +def test_configure_litellm_provider_env_ignores_non_bedrock_models(monkeypatch) -> None: + monkeypatch.delenv(BEDROCK_BEARER_ENV, raising=False) + + configure_litellm_provider_env( + model="anthropic/claude-sonnet-4-5", + api_key="not-used", + api_base="https://example.com", + ) + + assert os.getenv(BEDROCK_BEARER_ENV) is None diff --git a/deadend_cli/src/deadend_cli/component_manager.py b/deadend_cli/src/deadend_cli/component_manager.py index a8205a95..c37c2320 100644 --- a/deadend_cli/src/deadend_cli/component_manager.py +++ b/deadend_cli/src/deadend_cli/component_manager.py @@ -654,7 +654,7 @@ def get_model(self, provider: str | None = None, model_name: str | None = None): """Get a model instance from the model registry. Args: - provider: The LLM provider to use (openai, anthropic, gemini, openrouter, local). + provider: The LLM provider to use (openai, anthropic, gemini, bedrock, openrouter, local). If None, uses the current_llm_provider. model_name: Optional model name override. If provided, creates a model instance with this specific model name. @@ -684,7 +684,7 @@ def set_llm_provider(self, provider: str) -> None: """Set the current LLM provider. Args: - provider: The provider name (openai, anthropic, gemini, openrouter, local) + provider: The provider name (openai, anthropic, gemini, bedrock, openrouter, local) Raises: ValueError: If provider is not configured or not available From 80e06ae058805f71bca5029a03fc09cbcf0ebbcd Mon Sep 17 00:00:00 2001 From: alex-dev1 Date: Mon, 6 Apr 2026 09:58:07 -0400 Subject: [PATCH 2/2] Document Bedrock setup flow --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 39e982c9..52746c36 100644 --- a/README.md +++ b/README.md @@ -354,6 +354,39 @@ Deadend CLI accepts Bedrock API keys in the normal `api_key` field for `bedrock: `base_url` is optional, but recommended because Deadend can infer `AWS_DEFAULT_REGION` from a standard Bedrock endpoint like `https://bedrock-runtime.us-east-1.amazonaws.com`. If you omit it, set `AWS_DEFAULT_REGION` yourself. +#### AWS Bedrock Setup In The CLI + +If you are using the interactive setup wizard, enter the following values for the main model: + +1. `Provider`: `bedrock` +2. `Model`: use a Bedrock inference profile ID such as `us.anthropic.claude-sonnet-4-6` +3. `API key`: your Bedrock bearer token +4. `Base URL`: `https://bedrock-runtime.us-east-1.amazonaws.com` + +For the embedding model, configure a separate provider. Bedrock chat support does not remove the requirement for an embedding model in Deadend. + +Known working example: + +1. `Embedding provider`: `openrouter` +2. `Embedding model`: `qwen/qwen3-embedding-8b` +3. `Embedding API key`: your OpenRouter key +4. `Embedding base URL`: `https://openrouter.ai/api/v1` +5. `Vector dimension`: `4096` + +If you prefer environment variables for the main model, set: + +```bash +export AWS_BEARER_TOKEN_BEDROCK='' +export AWS_DEFAULT_REGION='us-east-1' +export BEDROCK_MODEL='us.anthropic.claude-sonnet-4-6' +``` + +Important notes: + +- Use an inference profile ID like `us.anthropic.claude-sonnet-4-6`, not the base model ID `anthropic.claude-sonnet-4-6`, for models that do not support on-demand throughput. +- If you change the Bedrock model after setup, also update `~/.cache/deadend/settings.json` so the default `model` matches the configured entry. +- If Deadend fails with `Embedder client could not be initialized: None`, the embedding model is missing from `config.json` or does not include `type_model: "embeddings"`. + ### CLI Interface Settings (`settings.json`) The CLI interface uses a separate `settings.json` file located at `~/.cache/deadend/settings.json` to store default preferences and UI settings. This file contains: