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
15 changes: 0 additions & 15 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,6 @@ repos:
- id: ruff-format
name: Run the ruff formatter

- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v4.0.0-alpha.8"
hooks:
- id: prettier
name: Pretty format files
exclude: "^daiv/static/"
files: "\\.(\
css|scss\
|js|jsx\
|json\
|md|markdown|mdown|mkdn\
|ts|tsx\
|yaml|yml\
)$"

- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.5.1"
hooks:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Support for `OpenRouter` integration, unified API for LLM providers. **Breaking change: this will be the default provider from now on as it's more reliable and has more models available.**

## [0.1.0-beta.3] - 2025-03-23

### Added
Expand Down
73 changes: 54 additions & 19 deletions daiv/automation/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from langchain_core.runnables import Runnable
from langgraph.graph.state import CompiledStateGraph

from automation.conf import settings
from core.constants import BOT_NAME

if TYPE_CHECKING:
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import BaseMessage
Expand All @@ -22,6 +25,7 @@ class ModelProvider(StrEnum):
ANTHROPIC = "anthropic"
OPENAI = "openai"
GOOGLE_GENAI = "google_genai"
OPENROUTER = "openrouter"


class ThinkingLevel(StrEnum):
Expand Down Expand Up @@ -78,10 +82,14 @@ def get_model(self, *, model: str, thinking_level: ThinkingLevel | None = None,
Returns:
BaseChatModel: The model instance
"""
model_kwargs = self.get_model_kwargs(model=model, thinking_level=thinking_level, **kwargs)
model_kwargs = self.get_model_kwargs(
model=model, model_provider=BaseAgent.get_model_provider(model), thinking_level=thinking_level, **kwargs
)
return init_chat_model(**model_kwargs)

def get_model_kwargs(self, *, thinking_level: ThinkingLevel | None = None, **kwargs) -> dict:
def get_model_kwargs(
self, *, model_provider: ModelProvider, thinking_level: ThinkingLevel | None = None, **kwargs
) -> dict:
"""
Get the keyword arguments to pass to the model.

Expand All @@ -93,28 +101,21 @@ def get_model_kwargs(self, *, thinking_level: ThinkingLevel | None = None, **kwa
"callbacks": [self.usage_handler],
"configurable_fields": ("temperature", "max_tokens"),
"model_kwargs": {},
"model_provider": model_provider,
**kwargs,
}

model_provider = BaseAgent.get_model_provider(_kwargs["model"])

if model_provider == ModelProvider.ANTHROPIC:
# As stated in docs: https://docs.anthropic.com/en/api/rate-limits#updated-rate-limits
# the OTPM is calculated based on the max_tokens. We need to use a fair value to avoid rate limiting.
# If needed, we can increase this value using the configurable field.
if thinking_level and _kwargs["model"].startswith("claude-3-7-sonnet"):
max_tokens, thinking_tokens = self._get_anthropic_thinking_tokens(thinking_level=thinking_level)
# When using thinking the temperature need to be set to 1
_kwargs["temperature"] = 1
if thinking_level == ThinkingLevel.LOW:
_kwargs["max_tokens"] = 4_096
_kwargs["thinking"] = {"type": "enabled", "budget_tokens": 2_048}
elif thinking_level == ThinkingLevel.MEDIUM:
_kwargs["max_tokens"] = 10_240
_kwargs["thinking"] = {"type": "enabled", "budget_tokens": 7_168}
elif thinking_level == ThinkingLevel.HIGH:
_kwargs["max_tokens"] = 20_480
_kwargs["thinking"] = {"type": "enabled", "budget_tokens": 16_384}
_kwargs["max_tokens"] = max_tokens
_kwargs["thinking"] = {"type": "enabled", "budget_tokens": thinking_tokens}
else:
# As stated in docs: https://docs.anthropic.com/en/api/rate-limits#updated-rate-limits
# the OTPM is calculated based on the max_tokens. We need to use a fair value to avoid rate limiting.
# If needed, we can increase this value using the configurable field.
_kwargs["max_tokens"] = 2_048

if _kwargs["model"].startswith("claude-3-7-sonnet"):
Expand All @@ -125,11 +126,42 @@ def get_model_kwargs(self, *, thinking_level: ThinkingLevel | None = None, **kwa
if thinking_level and _kwargs["model"].startswith(("o1", "o3")):
_kwargs["temperature"] = 1
_kwargs["reasoning_effort"] = thinking_level
elif model_provider in [ModelProvider.GOOGLE_GENAI]:
# otherwise google_genai will be inferred as google_vertexai
_kwargs["model_provider"] = model_provider

elif model_provider == ModelProvider.OPENROUTER:
_kwargs["model"] = _kwargs["model"].split(":")[1]
# OpenRouter is OpenAI compatible, so we need to use the OpenAI model provider
_kwargs["model_provider"] = ModelProvider.OPENAI
_kwargs["model_kwargs"]["extra_headers"] = {
"HTTP-Referer": "https://github.com/srtab/daiv",
"X-Title": BOT_NAME,
}
_kwargs["openai_api_base"] = settings.OPENROUTER_API_BASE
_kwargs["openai_api_key"] = settings.OPENROUTER_API_KEY

if _kwargs["model"].startswith("anthropic/claude-3-7-sonnet"):
_kwargs["model_kwargs"]["extra_headers"]["anthropic-beta"] = "token-efficient-tools-2025-02-19"

if thinking_level:
_kwargs["temperature"] = 1
_kwargs["extra_body"] = {"reasoning": {"effort": thinking_level}}

elif _kwargs["model"].startswith("anthropic"):
# Avoid rate limiting by setting a fair max_tokens value
_kwargs["max_tokens"] = 2_048

return _kwargs

def _get_anthropic_thinking_tokens(self, *, thinking_level: ThinkingLevel) -> tuple[int, dict]:
"""
Get the thinking tokens and max tokens for the model.
"""
if thinking_level == ThinkingLevel.LOW:
return 4_096, {"type": "enabled", "budget_tokens": 2_048}
elif thinking_level == ThinkingLevel.MEDIUM:
return 32_768, {"type": "enabled", "budget_tokens": 25_600}
elif thinking_level == ThinkingLevel.HIGH:
return 64_000, {"type": "enabled", "budget_tokens": 55_808}

def draw_mermaid(self):
"""
Draw the graph in Mermaid format.
Expand Down Expand Up @@ -194,7 +226,10 @@ def get_model_provider(model_name: str) -> ModelProvider:
model_provider: ModelProvider | None = None

if model_name.startswith("gemini"):
# The _attempt_infer_model_provider will return google_vertexai instead of google_genai
model_provider = ModelProvider.GOOGLE_GENAI
elif model_name.startswith("openrouter:"):
model_provider = ModelProvider.OPENROUTER
else:
model_provider = cast("ModelProvider | None", _attempt_infer_model_provider(model_name))

Expand Down
2 changes: 1 addition & 1 deletion daiv/automation/agents/codebase_chat/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class CodebaseChatSettings(BaseSettings):

NAME: str = Field(default="CodebaseChat", description="Name of the codebase chat agent.")
MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022, description="Model name to be used for codebase chat."
default=ModelName.CLAUDE_3_5_HAIKU, description="Model name to be used for codebase chat."
)
TEMPERATURE: float = Field(default=0.5, description="Temperature to be used for codebase chat.")

Expand Down
9 changes: 4 additions & 5 deletions daiv/automation/agents/codebase_search/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ class CodebaseSearchSettings(BaseSettings):
NAME: str = Field(default="CodebaseSearch", description="Name of the codebase search agent.")
TOP_N: int = Field(default=10, description="Number of results to return from the codebase search.")
REPHRASE_MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Model name to be used for codebase search."
default=ModelName.GPT_4O_MINI, description="Model name to be used for codebase search."
)
REPHRASE_FALLBACK_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022, description="Fallback model name to be used for codebase search."
default=ModelName.CLAUDE_3_5_HAIKU, description="Fallback model name to be used for codebase search."
)
RERANKING_MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Model name to be used for listwise reranking."
default=ModelName.GPT_4O_MINI, description="Model name to be used for listwise reranking."
)
RERANKING_FALLBACK_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022,
description="Fallback model name to be used for listwise reranking.",
default=ModelName.CLAUDE_3_5_HAIKU, description="Fallback model name to be used for listwise reranking."
)


Expand Down
24 changes: 15 additions & 9 deletions daiv/automation/agents/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@


class ModelName(StrEnum):
CLAUDE_3_7_SONNET_20250219 = "claude-3-7-sonnet-20250219"
CLAUDE_3_5_HAIKU_20241022 = "claude-3-5-haiku-20241022"
GPT_4O_2024_11_20 = "gpt-4o-2024-11-20"
GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18"
O1_2024_12_17 = "o1-2024-12-17"
O3_MINI_2025_01_31 = "o3-mini-2025-01-31"
GEMINI_2_0_FLASH = "gemini-2.0-flash"
GEMINI_2_0_FLASH_LITE = "gemini-2.0-flash-lite"
GEMINI_2_0_PRO = "gemini-2.0-pro-exp-02-05"
"""
`openrouter` provider is the default provider to use any model that is supported by OpenRouter.

You can also use `anthropic`, `google` or `openai` model providers directly to use any model that is supported
by Anthropic, Google or OpenAI.
"""

CLAUDE_3_7_SONNET = "openrouter:anthropic/claude-3-7-sonnet"
CLAUDE_3_5_HAIKU = "openrouter:anthropic/claude-3-5-haiku"
GPT_4O = "openrouter:openai/gpt-4o"
GPT_4O_MINI = "openrouter:openai/gpt-4o-mini"
O1 = "openrouter:openai/o1"
O3_MINI = "openrouter:openai/o3-mini"
GEMINI_2_0_FLASH = "openrouter:google/gemini-2.0-flash-001"
GEMINI_2_0_FLASH_LITE = "openrouter:google/gemini-2.0-flash-lite-001"
2 changes: 1 addition & 1 deletion daiv/automation/agents/image_url_extractor/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ImageURLExtractorSettings(BaseSettings):

NAME: str = Field(default="ImageURLExtractor", description="Name of the image URL extractor agent.")
MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Model name to be used for image URL extractor."
default=ModelName.GPT_4O_MINI, description="Model name to be used for image URL extractor."
)


Expand Down
4 changes: 2 additions & 2 deletions daiv/automation/agents/issue_addressor/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ class IssueAddressorSettings(BaseSettings):
NAME: str = Field(default="IssueAddressor", description="Name of the issue addressor agent.")
RECURSION_LIMIT: int = Field(default=50, description="Recursion limit for the issue addressor agent.")
ASSESSMENT_MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Model name to be used for issue assessment."
default=ModelName.GPT_4O_MINI, description="Model name to be used for issue assessment."
)
FALLBACK_ASSESSMENT_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022, description="Fallback model name to be used for issue assessment."
default=ModelName.CLAUDE_3_5_HAIKU, description="Fallback model name to be used for issue assessment."
)


Expand Down
10 changes: 5 additions & 5 deletions daiv/automation/agents/pipeline_fixer/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ class PipelineFixerSettings(BaseSettings):
NAME: str = Field(default="PipelineFixer", description="Name of the pipeline fixer agent.")
MAX_ITERATIONS: int = Field(default=20, description="Maximum number of retry iterations for pipeline fixer.")
LOG_EVALUATOR_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022, description="Model name to be used for log evaluator."
default=ModelName.CLAUDE_3_5_HAIKU, description="Model name to be used for log evaluator."
)
LOG_EVALUATOR_FALLBACK_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_7_SONNET_20250219, description="Fallback model name for log evaluator."
default=ModelName.CLAUDE_3_7_SONNET, description="Fallback model name for log evaluator."
)
TROUBLESHOOTING_MODEL_NAME: ModelName = Field(
default=ModelName.O3_MINI_2025_01_31, description="Model name to be used for pipeline fixer."
default=ModelName.O3_MINI, description="Model name to be used for pipeline fixer."
)
TROUBLESHOOTING_THINKING_LEVEL: ThinkingLevel = Field(
default=ThinkingLevel.HIGH, description="Thinking level to be used for pipeline fixer."
)
LINT_EVALUATOR_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022, description="Model name to be used for lint evaluator."
default=ModelName.CLAUDE_3_5_HAIKU, description="Model name to be used for lint evaluator."
)
LINT_EVALUATOR_FALLBACK_MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Fallback model name for lint evaluator."
default=ModelName.GPT_4O_MINI, description="Fallback model name for lint evaluator."
)


Expand Down
6 changes: 3 additions & 3 deletions daiv/automation/agents/plan_and_execute/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ class PlanAndExecuteSettings(BaseSettings):

NAME: str = Field(default="PlanAndExecute", description="Name of the plan and execute agent.")
PLANNING_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_7_SONNET_20250219, description="Model name to be used to plan tasks."
default=ModelName.CLAUDE_3_7_SONNET, description="Model name to be used to plan tasks."
)
PLANNING_THINKING_LEVEL: ThinkingLevel = Field(
default=ThinkingLevel.MEDIUM, description="Thinking level to be used to plan tasks."
)
EXECUTION_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_7_SONNET_20250219, description="Model name to be used to execute tasks."
default=ModelName.CLAUDE_3_7_SONNET, description="Model name to be used to execute tasks."
)
EXECUTION_THINKING_LEVEL: ThinkingLevel = Field(
default=ThinkingLevel.MEDIUM, description="Thinking level to be used to execute tasks."
)
PLAN_APPROVAL_MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Model name to be used to evaluate the plan approval."
default=ModelName.GPT_4O_MINI, description="Model name to be used to evaluate the plan approval."
)


Expand Down
4 changes: 2 additions & 2 deletions daiv/automation/agents/pr_describer/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class PRDescriberSettings(BaseSettings):

NAME: str = Field(default="PullRequestDescriber", description="Name of the PR describer agent.")
MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022, description="Model name to be used for PR describer."
default=ModelName.CLAUDE_3_5_HAIKU, description="Model name to be used for PR describer."
)
FALLBACK_MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Fallback model name to be used for PR describer."
default=ModelName.GPT_4O_MINI, description="Fallback model name to be used for PR describer."
)


Expand Down
9 changes: 4 additions & 5 deletions daiv/automation/agents/review_addressor/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@ class ReviewAddressorSettings(BaseSettings):

NAME: str = Field(default="ReviewAddressor", description="Name of the review addressor agent.")
ASSESSMENT_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022, description="Model name to be used for review assessment."
default=ModelName.CLAUDE_3_5_HAIKU, description="Model name to be used for review assessment."
)
FALLBACK_ASSESSMENT_MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Fallback model name to be used for review assessment."
default=ModelName.GPT_4O_MINI, description="Fallback model name to be used for review assessment."
)
REPLY_MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022,
description="Model name to be used for reply to comments or questions.",
default=ModelName.CLAUDE_3_5_HAIKU, description="Model name to be used for reply to comments or questions."
)
FALLBACK_REPLY_MODEL_NAME: ModelName = Field(
default=ModelName.GPT_4O_MINI_2024_07_18, description="Fallback model name for REPLY_MODEL_NAME."
default=ModelName.GPT_4O_MINI, description="Fallback model name for REPLY_MODEL_NAME."
)
REPLY_TEMPERATURE: float = Field(default=0.5, description="Temperature for the reply model.")

Expand Down
2 changes: 1 addition & 1 deletion daiv/automation/agents/snippet_replacer/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class SnippetReplacerSettings(BaseSettings):

NAME: str = Field(default="SnippetReplacer", description="Name of the snippet replacer agent.")
MODEL_NAME: ModelName = Field(
default=ModelName.CLAUDE_3_5_HAIKU_20241022,
default=ModelName.CLAUDE_3_5_HAIKU,
description="Model name to be used for snippet replacer. This model is used for the LLM strategy.",
)
STRATEGY: Literal["llm", "find_and_replace"] = Field(
Expand Down
6 changes: 6 additions & 0 deletions daiv/automation/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
class AutomationSettings(BaseSettings):
model_config = SettingsConfigDict(secrets_dir="/run/secrets", env_prefix="AUTOMATION_")

# OpenRouter settings
OPENROUTER_API_KEY: str | None = Field(default=None, description="OpenRouter API key", alias="OPENROUTER_API_KEY")
OPENROUTER_API_BASE: str | None = Field(
default="https://openrouter.ai/api/v1", description="OpenRouter API base url", alias="OPENROUTER_API_BASE"
)

# Web search settings
WEB_SEARCH_MAX_RESULTS: int = Field(default=5, description="Maximum number of results to return from web search")
WEB_SEARCH_ENGINE: Literal["duckduckgo", "tavily"] = Field(
Expand Down
1 change: 1 addition & 0 deletions docker/local/app/config.secrets.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CODEBASE_GITLAB_AUTH_TOKEN=
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GOOGLE_API_KEY=
OPENROUTER_API_KEY=

# Monitoring
LANGCHAIN_API_KEY=
Expand Down
Loading
Loading