Skip to content
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,8 @@ database:
# Optional novelty filtering with Gemini embeddings
embedding_model: "gemini-embedding-001"
similarity_threshold: 0.99
# Or any OpenAI-compatible embedding endpoint (OpenRouter, local servers):
# embedding_api_base: "https://openrouter.ai/api/v1" # or OPENAI_EMBEDDING_BASE_URL

evaluator:
enable_artifacts: true # Error feedback to LLM
Expand Down
1 change: 1 addition & 0 deletions configs/default_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ random_seed: 42 # Random seed for reproducibility (null =
# Evolution settings
diff_based_evolution: true # Use diff-based evolution (true) or full rewrites (false)
max_code_length: 10000 # Maximum allowed code length in characters
enforce_evolve_blocks: false # Revert LLM edits outside EVOLVE-BLOCK-START/END markers

# Early stopping settings
early_stopping_patience: null # Stop after N iterations without improvement (null = disabled)
Expand Down
2 changes: 1 addition & 1 deletion openevolve/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information for openevolve package."""

__version__ = "0.3.2"
__version__ = "0.4.0"
5 changes: 5 additions & 0 deletions openevolve/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,9 @@ class DatabaseConfig:

novelty_llm: Optional["LLMInterface"] = None
embedding_model: Optional[str] = None
# OpenAI-compatible base URL for embeddings (e.g. OpenRouter or a local server);
# falls back to the OPENAI_EMBEDDING_BASE_URL environment variable
embedding_api_base: Optional[str] = None
similarity_threshold: float = 0.99


Expand Down Expand Up @@ -442,6 +445,8 @@ class Config:
# Evolution settings
diff_based_evolution: bool = True
max_code_length: int = 10000
# Revert any LLM edits outside the EVOLVE-BLOCK-START/END regions
enforce_evolve_blocks: bool = False
diff_pattern: str = r"<<<<<<< SEARCH\n(.*?)=======\n(.*?)>>>>>>> REPLACE"

# Early stopping settings
Expand Down
4 changes: 3 additions & 1 deletion openevolve/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ def __init__(

self.novelty_llm = config.novelty_llm
self.embedding_client = (
EmbeddingClient(config.embedding_model) if config.embedding_model else None
EmbeddingClient(config.embedding_model, config.embedding_api_base)
if config.embedding_model
else None
)
self.similarity_threshold = config.similarity_threshold

Expand Down
23 changes: 17 additions & 6 deletions openevolve/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import logging
import os
from typing import List, Union
from typing import List, Optional, Union

import openai

Expand Down Expand Up @@ -34,17 +34,28 @@


class EmbeddingClient:
def __init__(self, model_name: str = "text-embedding-3-small"):
def __init__(self, model_name: str = "text-embedding-3-small", api_base: Optional[str] = None):
"""
Initialize the EmbeddingClient.

Args:
model (str): The OpenAI embedding model name to use.
api_base (str, optional): OpenAI-compatible base URL for embeddings.
Defaults to the OPENAI_EMBEDDING_BASE_URL environment variable.
"""
self.client, self.model = self._get_client_model(model_name)

def _get_client_model(self, model_name: str) -> tuple[openai.OpenAI, str]:
if model_name in OPENAI_EMBEDDING_MODELS:
self.client, self.model = self._get_client_model(model_name, api_base)

def _get_client_model(
self, model_name: str, api_base: Optional[str] = None
) -> tuple[openai.OpenAI, str]:
api_base = api_base or os.getenv("OPENAI_EMBEDDING_BASE_URL")
if api_base:
# Any OpenAI-compatible endpoint (OpenRouter, local servers, ...)
# serves whatever embedding model name it supports
embedding_api_key = os.getenv("OPENAI_EMBEDDING_API_KEY") or os.getenv("OPENAI_API_KEY")
client = openai.OpenAI(api_key=embedding_api_key, base_url=api_base)
model_to_use = model_name
elif model_name in OPENAI_EMBEDDING_MODELS:
# Use OPENAI_EMBEDDING_API_KEY if set, otherwise fall back to OPENAI_API_KEY
# This allows users to use OpenRouter for LLMs while using OpenAI for embeddings
embedding_api_key = os.getenv("OPENAI_EMBEDDING_API_KEY") or os.getenv("OPENAI_API_KEY")
Expand Down
8 changes: 7 additions & 1 deletion openevolve/llm/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,14 @@ async def _call_api(self, params: Dict[str, Any]) -> str:
# Use asyncio to run the blocking API call in a thread pool
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(
None, lambda: self.client.chat.completions.create(**params)
None, lambda: self.client.chat.completions.create(**params, stream=False)
)
if isinstance(response, str):
# Some endpoints stream Server-Sent Events even when asked not to
raise ValueError(
"LLM endpoint returned a raw string instead of a chat completion "
f"(streaming responses are not supported): {response[:200]!r}"
)
# Logging of system prompt, user message and response content
logger = logging.getLogger(__name__)
logger.debug(f"API parameters: {params}")
Expand Down
59 changes: 56 additions & 3 deletions openevolve/process_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,18 @@ def _run_iteration_worker(
# Parse response based on evolution mode
if _worker_config.diff_based_evolution:
from openevolve.utils.code_utils import (
apply_diff,
apply_diff_blocks,
extract_diffs,
format_diff_summary,
split_diffs_by_target,
)

diff_blocks = extract_diffs(llm_response, _worker_config.diff_pattern)
try:
diff_blocks = extract_diffs(llm_response, _worker_config.diff_pattern)
except ValueError as exc:
return SerializableResult(
error=str(exc), iteration=iteration, token_usage=token_usage
)
if not diff_blocks:
return SerializableResult(
error="No valid diffs found in response",
Expand Down Expand Up @@ -275,7 +279,27 @@ def _run_iteration_worker(
)
else:
# All diffs applied only to code
child_code = apply_diff(parent.code, llm_response, _worker_config.diff_pattern)
child_code, applied = apply_diff_blocks(parent.code, diff_blocks)
if applied == 0:
return SerializableResult(
error=(
f"None of the {len(diff_blocks)} SEARCH block(s) matched the "
"parent program"
),
iteration=iteration,
token_usage=token_usage,
)
if applied < len(diff_blocks):
logger.warning(
f"Iteration {iteration}: only {applied} of {len(diff_blocks)} "
"SEARCH block(s) matched the parent program"
)
if child_code == parent.code:
return SerializableResult(
error="Diff did not change the parent program",
iteration=iteration,
token_usage=token_usage,
)
changes_summary = format_diff_summary(
diff_blocks,
max_line_len=_worker_config.prompt.diff_summary_max_line_len,
Expand All @@ -292,9 +316,38 @@ def _run_iteration_worker(
token_usage=token_usage,
)

if new_code == parent.code:
return SerializableResult(
error="Rewrite is identical to the parent program",
iteration=iteration,
token_usage=token_usage,
)

child_code = new_code
changes_summary = "Full rewrite"

# Revert edits made outside the EVOLVE-BLOCK regions if configured
if _worker_config.enforce_evolve_blocks:
from openevolve.utils.code_utils import enforce_evolve_blocks

try:
enforced_code = enforce_evolve_blocks(parent.code, child_code)
except ValueError as exc:
return SerializableResult(
error=str(exc), iteration=iteration, token_usage=token_usage
)
if enforced_code != child_code:
logger.info(
f"Iteration {iteration}: reverted edits outside the EVOLVE-BLOCK regions"
)
child_code = enforced_code
if child_code == parent.code:
return SerializableResult(
error="All edits were outside the EVOLVE-BLOCK regions",
iteration=iteration,
token_usage=token_usage,
)

# Check code length
if len(child_code) > _worker_config.max_code_length:
return SerializableResult(
Expand Down
Loading
Loading