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
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ When defining a model, use the following schema. The key format `<provider>:<mod

```json
"<provider>:<model_name>": {
"provider": "<provider>", // Provider name (e.g., openai, anthropic, ollama)
"provider": "<provider>", // Provider name (e.g., openai, anthropic, bedrock, ollama)
"model_name": "<model_name>", // Model identifier (e.g., claude-sonnet-4-5, gpt-4)
"api_key": "<api_key>", // API key (optional if ENV var is set, but recommended to add here)
"base_url": "<base_url>", // Base URL for custom gateways or providers (e.g., Ollama)
Expand Down Expand Up @@ -312,6 +312,7 @@ Models are specified using the format `<provider>:<model_name>` 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

Expand All @@ -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`)
Expand All @@ -333,6 +335,58 @@ 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": "<your bedrock 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.

#### 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='<your bedrock api key>'
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:
Expand Down
5 changes: 2 additions & 3 deletions cli/deadend_cli/components/ConfigSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,7 +26,7 @@ export function ConfigSetup({ onComplete }: ConfigSetupProps) {
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(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) => {
Expand Down Expand Up @@ -334,4 +334,3 @@ max_history = 100
</Box>
);
}

1 change: 1 addition & 0 deletions cli/deadend_cli/components/LlmSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
};
Expand Down
2 changes: 1 addition & 1 deletion cli/deadend_cli/runtime/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion cli/deadend_cli/types/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
52 changes: 52 additions & 0 deletions deadend_cli/deadend_agent/src/deadend_agent/utils/provider_env.py
Original file line number Diff line number Diff line change
@@ -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)
65 changes: 65 additions & 0 deletions deadend_cli/deadend_agent/tests/test_provider_env.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions deadend_cli/src/deadend_cli/component_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down