Skip to content
Open
4 changes: 4 additions & 0 deletions src/benchmark_core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ class UsagePolicyProfile(BaseModel):
default=None,
description="Budget limit in currency units",
)
budget_currency: str = Field(
default="USD",
description="Budget currency",
)
ttl_seconds: int | None = Field(
default=None,
description="Key TTL in seconds (time-to-live before expiration)",
Expand Down
19 changes: 14 additions & 5 deletions src/benchmark_core/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@

# Comprehensive SQL-based services (COE-305 implementation)
from benchmark_core.services.benchmark_metadata_service import BenchmarkMetadataService

# Unified credential service (COE-370)
from benchmark_core.services.credential_service import CredentialService
from benchmark_core.services.experiment_service import ExperimentService
from benchmark_core.services.harness_profile_service import HarnessProfileService
from benchmark_core.services.provider_service import ProviderService
Comment thread
kumanday marked this conversation as resolved.

# Sessionless proxy key services
from benchmark_core.services.proxy_key_service import (
LiteLLMAPIError,
ProxyKeyService,
ProxyKeyServiceError,
)
from benchmark_core.services.rendering import (
EnvRenderingService,
EnvSnippet,
Expand All @@ -25,11 +35,6 @@
from benchmark_core.services.task_card_service import TaskCardService
from benchmark_core.services.variant_service import VariantService

# ABC service exports - Note: old services_abc SessionService removed, use services.session_service
from benchmark_core.services_abc import (
CredentialService,
)

__all__ = [
# Core SQL-based services
"SessionService",
Expand All @@ -49,6 +54,10 @@
"RenderingError",
"ProfileValidationError",
"render_env_for_session",
# Sessionless proxy key services
"ProxyKeyService",
"ProxyKeyServiceError",
"LiteLLMAPIError",
# ABC services
"CredentialService",
]
95 changes: 95 additions & 0 deletions src/benchmark_core/services/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Shared utilities for benchmark core services.

Provides common helper functions used across multiple service classes
to avoid duplication and ensure consistent behavior.
"""

import warnings


def validate_litellm_url(litellm_base_url: str, enforce_https: bool = True) -> str:
"""Validate and normalize a LiteLLM base URL.

Strips trailing slashes and optionally enforces HTTPS in
production environments.

Args:
litellm_base_url: The raw base URL for the LiteLLM proxy API.
enforce_https: Whether to require HTTPS. Defaults to True.

Returns:
The normalized URL with trailing slash removed.

Raises:
ValueError: If HTTPS is required but the URL is not localhost
and does not start with ``https://``.
"""
normalized = litellm_base_url.rstrip("/")
if enforce_https and not normalized.startswith(
("https://", "http://localhost", "http://127.0.0.1")
):
raise ValueError(
f"LiteLLM URL must use HTTPS in production environments. Got: {litellm_base_url}"
)
return normalized


def render_env_shell(env_vars: dict[str, str]) -> str:
"""Render environment variables as shell export commands.

Variables are sorted alphabetically for deterministic output.

Args:
env_vars: Dictionary of environment variables.

Returns:
Shell export commands string.
"""
lines: list[str] = []
for key in sorted(env_vars.keys()):
value = env_vars[key]
escaped = value.replace("'", "'\\''")
lines.append(f"export {key}='{escaped}'")
return "\n".join(lines)


def render_env_dotenv(env_vars: dict[str, str]) -> str:
"""Render environment variables as dotenv file content.

Variables are sorted alphabetically for deterministic output.

Args:
env_vars: Dictionary of environment variables.

Returns:
Dotenv file content string.
"""
lines: list[str] = []
for key in sorted(env_vars.keys()):
value = env_vars[key]
if " " in value or "#" in value or "'" in value or '"' in value:
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
lines.append(f'{key}="{escaped}"')
else:
lines.append(f"{key}={value}")
return "\n".join(lines)


def warn_revoke_failure(source: str, litellm_key_id: str | None, exc: Exception) -> None:
"""Warn when a LiteLLM key revocation attempt fails.

This helper centralises the warning issued when a best-effort LiteLLM
delete call fails, so both :class:`CredentialService` and
:class:`ProxyKeyService` behave consistently.

Args:
source: Human-readable source of the revocation (e.g. ``credential``
or ``proxy_key``).
litellm_key_id: The LiteLLM key ID that could not be deleted.
exc: The original exception that caused the failure.
"""
warnings.warn(
f"LiteLLM delete failed for {source} (key_id={litellm_key_id}): {exc}. "
"Local metadata has been marked revoked but the proxy key may still be active.",
stacklevel=3,
)
Comment on lines +1 to +95
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.

🟡 Suggestion: common.py has zero direct test coverage — These are now the shared foundation for both CredentialService and ProxyKeyService, but they're only tested indirectly through service tests. A test_common.py with direct unit tests for validate_litellm_url (HTTPS enforcement, localhost exceptions, trailing slash stripping), render_env_shell/render_env_dotenv (sorting, escaping), and warn_revoke_failure would catch regressions in the shared code path that could be masked by mock boundaries in the service tests.

Comment on lines +1 to +95
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.

🟡 Suggestion: common.py has zero direct test coverage — These shared utilities (validate_litellm_url, render_env_shell, render_env_dotenv, warn_revoke_failure) are now the foundation for both services but are only tested indirectly. A test_common.py with direct unit tests would catch edge cases in shell escaping and dotenv quoting. Flagged in previous review, still unfixed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2f4a830: see commit message for details.

Loading
Loading