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
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,16 @@ When unsure: simpler state, fewer dependencies, smaller surface area, core-first
## Environment Variables

```bash
ANTHROPIC_API_KEY=sk-ant-... # Required for agent execution
ANTHROPIC_API_KEY=sk-ant-... # Required for Anthropic provider (default)
E2B_API_KEY=e2b_... # Required for --engine cloud
DATABASE_PATH=./codeframe.db # Optional

# LLM Provider selection (multi-provider support)
CODEFRAME_LLM_PROVIDER=anthropic # Provider: anthropic (default), openai, ollama, vllm, compatible
CODEFRAME_LLM_MODEL=gpt-4o # Model override (used with openai/ollama/vllm/compatible)
OPENAI_API_KEY=sk-... # Required for openai provider; not needed for local providers
OPENAI_BASE_URL=http://localhost:11434/v1 # Base URL override (for ollama, vllm, or custom endpoints)

Comment on lines +243 to +248

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify all valid provider types in get_provider function

# Search for provider type handling in get_provider
rg -nP -A2 'def get_provider|provider_type ==|provider_type in' codeframe/adapters/llm/__init__.py

Repository: frankbria/codeframe

Length of output: 539


🏁 Script executed:

# Verify exact content and line numbers in CLAUDE.md
sed -n '240,250p' CLAUDE.md | cat -n

Repository: frankbria/codeframe

Length of output: 764


🏁 Script executed:

# Search for other mentions of provider types in documentation files
rg -n 'anthropic.*openai.*ollama|Provider.*anthropic|CODEFRAME_LLM_PROVIDER' CLAUDE.md README.md 2>/dev/null | head -20

Repository: frankbria/codeframe

Length of output: 180


Add "mock" to the list of supported LLM providers.

Line 244 documents five provider values but the code accepts a sixth valid type: "mock". Since this is developer documentation, the supported provider list should be complete.

Suggested fix
-CODEFRAME_LLM_PROVIDER=anthropic      # Provider: anthropic (default), openai, ollama, vllm, compatible
+CODEFRAME_LLM_PROVIDER=anthropic      # Provider: anthropic (default), openai, ollama, vllm, compatible, mock
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 243 - 248, Update the documented
CODEFRAME_LLM_PROVIDER options to include the missing "mock" provider so the
README matches supported runtime values; specifically edit the block that lists
provider values (the line containing CODEFRAME_LLM_PROVIDER=anthropic and the
inline comment listing "Provider: anthropic (default), openai, ollama, vllm,
compatible") to add "mock" to that comma-separated list so it reads
"...anthropic (default), openai, ollama, vllm, compatible, mock" and ensure the
example or surrounding text references "mock" where relevant.

# Optional — Rate limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_DEFAULT=100/minute
Expand Down
30 changes: 25 additions & 5 deletions codeframe/adapters/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
)
"""

import os

from codeframe.adapters.llm.base import (
LLMProvider,
LLMResponse,
Expand Down Expand Up @@ -47,23 +49,41 @@
"get_provider",
]

_OPENAI_COMPATIBLE = {"openai", "ollama", "vllm", "compatible"}


def get_provider(provider_type: str = "anthropic") -> LLMProvider:
def get_provider(provider_type: str = "anthropic", **kwargs) -> LLMProvider:
"""Get a configured LLM provider.

Args:
provider_type: Provider type ("anthropic", "openai", or "mock")
provider_type: Provider type ("anthropic", "openai", "ollama",
"vllm", "compatible", or "mock"). OpenAI-compatible types are
all routed to OpenAIProvider.
**kwargs: Optional overrides passed to the provider constructor.
Supported keys: api_key, model, base_url.
For local providers (ollama, vllm, compatible) that don't
require authentication, api_key defaults to "not-required"
if OPENAI_API_KEY is not set.

Returns:
Configured LLMProvider instance

Raises:
ValueError: If provider type is unknown
"""
if provider_type == "anthropic":
if provider_type in _OPENAI_COMPATIBLE:
api_key = kwargs.get("api_key") or os.environ.get("OPENAI_API_KEY")
if not api_key and provider_type != "openai":
# Local providers (ollama, vllm, compatible) don't need real auth;
# the openai SDK still requires a non-empty api_key value.
api_key = "not-required"
return OpenAIProvider(
api_key=api_key,
model=kwargs.get("model", os.environ.get("CODEFRAME_LLM_MODEL", "gpt-4o")),
base_url=kwargs.get("base_url", os.environ.get("OPENAI_BASE_URL")),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
elif provider_type == "anthropic":
return AnthropicProvider()
elif provider_type == "openai":
return OpenAIProvider()
elif provider_type == "mock":
return MockProvider()
else:
Expand Down
16 changes: 10 additions & 6 deletions codeframe/core/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,15 +638,19 @@ def execute_agent(
# Resolve engine (handles "built-in" alias and CODEFRAME_ENGINE env var)
engine = resolve_engine(engine)

# Determine provider type from env var (default: anthropic)
provider_type = os.getenv("CODEFRAME_LLM_PROVIDER", "anthropic")

# External engines manage their own authentication
if not is_external_engine(engine) and not os.getenv("ANTHROPIC_API_KEY"):
raise ValueError(
"ANTHROPIC_API_KEY environment variable is required for agent execution. "
"Set it with: export ANTHROPIC_API_KEY=your-key"
)
if not is_external_engine(engine):
if provider_type == "anthropic" and not os.getenv("ANTHROPIC_API_KEY"):
raise ValueError(
"ANTHROPIC_API_KEY environment variable is required for agent execution. "
"Set it with: export ANTHROPIC_API_KEY=your-key"
)

# Only create LLM provider for builtin engines (external engines manage their own)
provider = get_provider("anthropic") if not is_external_engine(engine) else None
provider = get_provider(provider_type) if not is_external_engine(engine) else None

# Create run logger for structured logging
run_logger = RunLogger(workspace, run.id, run.task_id)
Expand Down
9 changes: 8 additions & 1 deletion docs/QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ Get your project built with AI agents in minutes.
## Prerequisites

1. **Python 3.11+** with `uv` package manager
2. **Anthropic API Key** set as environment variable:
2. **LLM Provider API Key** — Anthropic is the default:
```bash
export ANTHROPIC_API_KEY=sk-ant-...
```
To use OpenAI-compatible providers (OpenAI, Ollama, vLLM, or any compatible endpoint):
```bash
export CODEFRAME_LLM_PROVIDER=openai # or: ollama, vllm, compatible
export CODEFRAME_LLM_MODEL=gpt-4o # model name for the chosen provider
export OPENAI_API_KEY=sk-... # required for openai; not needed for local providers
export OPENAI_BASE_URL=http://localhost:11434/v1 # for local providers (ollama, vllm)
```

## The Happy Path

Expand Down
76 changes: 76 additions & 0 deletions tests/adapters/test_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
LLMResponse,
MockProvider,
ModelSelector,
OpenAIProvider,
Purpose,
ToolCall,
get_provider,
Expand Down Expand Up @@ -202,6 +203,81 @@ def test_unknown_provider_raises(self):
with pytest.raises(ValueError, match="Unknown provider"):
get_provider("unknown")

@pytest.mark.v2
def test_get_openai_provider(self, monkeypatch):
"""Returns OpenAIProvider for 'openai' type."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
provider = get_provider("openai")
assert isinstance(provider, OpenAIProvider)

@pytest.mark.v2
def test_get_ollama_provider(self, monkeypatch):
"""Returns OpenAIProvider for 'ollama' type."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
provider = get_provider("ollama")
assert isinstance(provider, OpenAIProvider)

@pytest.mark.v2
def test_get_vllm_provider(self, monkeypatch):
"""Returns OpenAIProvider for 'vllm' type."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
provider = get_provider("vllm")
assert isinstance(provider, OpenAIProvider)

@pytest.mark.v2
def test_get_compatible_provider(self, monkeypatch):
"""Returns OpenAIProvider for 'compatible' type."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
provider = get_provider("compatible")
assert isinstance(provider, OpenAIProvider)

@pytest.mark.v2
def test_codeframe_llm_model_sets_model(self, monkeypatch):
"""CODEFRAME_LLM_MODEL env var sets the OpenAI provider model."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
monkeypatch.setenv("CODEFRAME_LLM_MODEL", "qwen2.5-coder")
provider = get_provider("openai")
assert provider.model == "qwen2.5-coder"

@pytest.mark.v2
def test_openai_base_url_env_var(self, monkeypatch):
"""OPENAI_BASE_URL env var sets the base_url."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
monkeypatch.setenv("OPENAI_BASE_URL", "http://localhost:11434/v1")
provider = get_provider("openai")
assert provider.base_url == "http://localhost:11434/v1"

@pytest.mark.v2
def test_openai_default_model_is_gpt4o(self, monkeypatch):
"""Default OpenAI model is gpt-4o when CODEFRAME_LLM_MODEL not set."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
monkeypatch.delenv("CODEFRAME_LLM_MODEL", raising=False)
provider = get_provider("openai")
assert provider.model == "gpt-4o"

@pytest.mark.v2
def test_ollama_no_api_key_required(self, monkeypatch):
"""ollama provider works without OPENAI_API_KEY (uses placeholder)."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
provider = get_provider("ollama")
assert isinstance(provider, OpenAIProvider)
assert provider.api_key == "not-required"

@pytest.mark.v2
def test_vllm_no_api_key_required(self, monkeypatch):
"""vllm provider works without OPENAI_API_KEY (uses placeholder)."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
provider = get_provider("vllm")
assert isinstance(provider, OpenAIProvider)
assert provider.api_key == "not-required"

@pytest.mark.v2
def test_openai_still_requires_api_key(self, monkeypatch):
"""openai provider raises if no OPENAI_API_KEY."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
with pytest.raises(ValueError, match="OPENAI_API_KEY"):
get_provider("openai")


class TestLLMResponse:
"""Tests for LLMResponse dataclass."""
Expand Down
83 changes: 83 additions & 0 deletions tests/core/test_runtime_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,86 @@ def test_valid_engines_include_external(self):
assert "react" in VALID_ENGINES
assert "plan" in VALID_ENGINES
assert "built-in" in VALID_ENGINES


@pytest.mark.v2
class TestRuntimeProviderSelection:
"""Tests for CODEFRAME_LLM_PROVIDER env var routing in execute_agent."""

def test_openai_provider_skips_anthropic_key_check(
self, mock_workspace, mock_run
):
"""CODEFRAME_LLM_PROVIDER=openai should not raise ANTHROPIC_API_KEY error.

get_provider is mocked so this test is isolated to just the auth guard
logic in execute_agent, independent of OPENAI_API_KEY availability.
"""
from codeframe.core.runtime import execute_agent

env = {"CODEFRAME_LLM_PROVIDER": "openai"}
patches = _runtime_patches()
[p.start() for p in patches]
try:
with patch("codeframe.adapters.llm.get_provider") as mock_get_provider:
mock_get_provider.return_value = MagicMock()
with patch.dict("os.environ", env, clear=True):
import os
os.environ.pop("ANTHROPIC_API_KEY", None)
# Should not raise ValueError about ANTHROPIC_API_KEY
try:
execute_agent(mock_workspace, mock_run, engine="react")
except ValueError as exc:
assert "ANTHROPIC_API_KEY" not in str(exc), (
f"Should not require ANTHROPIC_API_KEY when provider=openai: {exc}"
)
except Exception:
pass # Non-ValueError errors are outside the scope of this test
finally:
for p in patches:
p.stop()

def test_anthropic_provider_still_required_by_default(
self, mock_workspace, mock_run
):
"""Without CODEFRAME_LLM_PROVIDER, anthropic is default and key is required."""
from codeframe.core.runtime import execute_agent

with patch.dict("os.environ", {}, clear=True):
import os
os.environ.pop("ANTHROPIC_API_KEY", None)
os.environ.pop("CODEFRAME_LLM_PROVIDER", None)

with pytest.raises(ValueError, match="ANTHROPIC_API_KEY"):
execute_agent(mock_workspace, mock_run, engine="react")

def test_get_provider_called_with_env_provider_type(
self, mock_workspace, mock_run
):
"""get_provider is called with value from CODEFRAME_LLM_PROVIDER."""
from codeframe.core.runtime import execute_agent

env = {"CODEFRAME_LLM_PROVIDER": "openai", "OPENAI_API_KEY": "test-key"}
patches = _runtime_patches()
[p.start() for p in patches]
try:
# Patch at the source module level since runtime imports locally;
# local `from X import Y` reads from module.__dict__ at call time,
# so patching the source module IS correct.
with patch("codeframe.adapters.llm.get_provider") as mock_get_provider:
mock_get_provider.return_value = MagicMock()
with patch(
"codeframe.core.engine_registry.get_builtin_adapter",
create=True,
) as mock_adapter:
mock_adapter.return_value = MagicMock()
mock_adapter.return_value.run.return_value = iter([])
with patch.dict("os.environ", env, clear=True):
try:
execute_agent(mock_workspace, mock_run, engine="react")
except Exception:
pass
assert mock_get_provider.called, "get_provider was not called"
assert mock_get_provider.call_args[0][0] == "openai"
finally:
for p in patches:
p.stop()
Loading