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
2 changes: 2 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
- name: Run docs quality checks
run: npm run ci:check
working-directory: docs-fumadocs
env:
GITHUB_TOKEN: ${{ github.token }}

- name: Upload docs site
uses: actions/upload-artifact@v4
Expand Down
10 changes: 10 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
Portions of this software are licensed as follows:

* All content that resides under the "enterprise/" directory of this repository,
if that directory exists, is licensed under the license defined in
"enterprise/LICENSE.md".
* Content outside of the above mentioned directories or restrictions above is
available under the MIT license as defined below.

---

MIT License

Copyright (c) 2025 EfficientAI.tech
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -983,4 +983,11 @@ See `CONTRIBUTING.md` for PR format, review expectations, and release label conv

## 📄 License

MIT License - see LICENSE file for details
The open-source core is licensed under the **MIT License** — see [LICENSE](LICENSE).

Content under the `enterprise/` directory is licensed separately under
[enterprise/LICENSE.md](enterprise/LICENSE.md). Enterprise product capabilities
(unlimited usage history, call imports, voice playground, alerts, and more) require
a valid **`EFFICIENTAI_LICENSE`** JWT key. Contact
[sales@efficientai.com](mailto:sales@efficientai.com) or visit
[efficientai.cloud](https://efficientai.cloud).
2 changes: 2 additions & 0 deletions app/api/v1/routes/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from app.dependencies import get_db, get_organization_id, get_workspace_id, get_api_key
from app.core.api_rate_limit import enforce_resource_create_rate_limit
from app.core.oss_quotas import enforce_oss_quota
from app.core.auth import Principal
from app.services.billing.flexprice_service import record_agent_test_setup_generated
from app.models.database import (
Expand Down Expand Up @@ -597,6 +598,7 @@ async def create_agent(
The agent is stamped with the active workspace from the
``X-Workspace-Id`` header (falling back to the org's Default).
"""
enforce_oss_quota(db, organization_id, "agents")
# Validate phone_number is provided when call_medium is phone_call
if agent.call_medium == CallMediumEnumSchema.PHONE_CALL and not agent.phone_number:
raise HTTPException(
Expand Down
157 changes: 144 additions & 13 deletions app/api/v1/routes/aiproviders.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import desc, func
from sqlalchemy.orm import Session
from typing import List, Optional
from typing import Any, List, Optional
from uuid import UUID

from app.dependencies import get_db, get_organization_id
Expand Down Expand Up @@ -89,25 +89,117 @@ def _scrub_for_response(
)


def _assert_gateway_fields_allowed(
organization_id: UUID,
*,
gateway_model: Optional[str] = None,
gateway_interface: Optional[str] = None,
gateway_base_url: Optional[str] = None,
gateway_auth_header: Optional[str] = None,
gateway_auth_secret_env: Optional[str] = None,
gateway_auth_secret: Optional[str] = None,
gateway_extra_headers: Optional[dict] = None,
) -> None:
from app.core.license import has_valid_license
from app.services.ai.llm_gateway_settings import assert_llm_gateway_entitlement

if has_valid_license(organization_id):
return

has_gateway_fields = any(
[
(gateway_model or "").strip(),
gateway_interface not in (None, "", "inherit"),
(gateway_base_url or "").strip(),
(gateway_auth_header or "").strip(),
(gateway_auth_secret_env or "").strip(),
(gateway_auth_secret or "").strip(),
bool(gateway_extra_headers),
]
)
if has_gateway_fields:
assert_llm_gateway_entitlement(organization_id)


def _normalize_gateway_interface(value: Any) -> Optional[str]:
if value is None:
return None
return value.value if hasattr(value, "value") else str(value)


def _assert_gateway_update_allowed(
organization_id: UUID,
db_aiprovider: AIProvider,
update_data: dict,
) -> None:
"""Reject new gateway configuration on OSS; allow clears and unrelated edits."""
from app.core.license import has_valid_license
from app.services.ai.llm_gateway_settings import assert_llm_gateway_entitlement

if has_valid_license(organization_id):
return

def _enabling(field: str, value: Any) -> bool:
if field == "gateway_interface":
normalized = (_normalize_gateway_interface(value) or "").strip().lower()
return normalized not in ("", "inherit")
if field == "gateway_extra_headers":
return bool(value)
return bool((value or "").strip())

checks: list[tuple[str, Any]] = [
("gateway_model", db_aiprovider.gateway_model),
("gateway_interface", db_aiprovider.gateway_interface),
("gateway_base_url", db_aiprovider.gateway_base_url),
("gateway_auth_header", db_aiprovider.gateway_auth_header),
("gateway_auth_secret_env", db_aiprovider.gateway_auth_secret_env),
("gateway_extra_headers", db_aiprovider.gateway_extra_headers),
]

for field, stored in checks:
if field not in update_data:
continue
new_value = update_data[field]
if not _enabling(field, new_value):
continue
stored_cmp = stored
if field == "gateway_interface":
new_value = _normalize_gateway_interface(new_value)
stored_cmp = _normalize_gateway_interface(stored)
if new_value != stored_cmp:
assert_llm_gateway_entitlement(organization_id)
return

if update_data.get("gateway_auth_secret"):
assert_llm_gateway_entitlement(organization_id)


def _validate_routing_and_api_key(
*,
organization_id: UUID,
routing_mode: CredentialRoutingMode,
api_key: Optional[str],
gateway_model: Optional[str],
has_existing_key: bool = False,
check_routing_entitlement: bool = True,
) -> None:
from app.services.ai.llm_gateway_settings import assert_credential_routing_allowed

if check_routing_entitlement:
assert_credential_routing_allowed(organization_id, routing_mode)

mode = routing_mode.value if hasattr(routing_mode, "value") else str(routing_mode)
trimmed_key = (api_key or "").strip()

if mode == CredentialRoutingMode.GATEWAY.value:
return

if mode == CredentialRoutingMode.DIRECT.value and not trimmed_key and not has_existing_key:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="api_key is required when routing_mode is direct.",
)

if mode == CredentialRoutingMode.GATEWAY.value:
return

if mode == CredentialRoutingMode.INHERIT.value:
if not trimmed_key and not has_existing_key:
if settings.LLM_GATEWAY_PASSTHROUGH_PROVIDER_KEYS:
Expand Down Expand Up @@ -152,11 +244,38 @@ async def create_aiprovider(
"""Create a new AI Provider credential row."""
provider_value = aiprovider.provider.value if hasattr(aiprovider.provider, 'value') else aiprovider.provider

from app.core.license import has_valid_license

if str(provider_value).lower() == "custom" and not has_valid_license(organization_id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"error": "enterprise_license_required",
"message": (
"Custom Bifrost model integrations require a valid "
"EFFICIENTAI_LICENSE key."
),
},
)

_validate_routing_and_api_key(
organization_id=organization_id,
routing_mode=aiprovider.routing_mode,
api_key=aiprovider.api_key,
gateway_model=aiprovider.gateway_model,
)
_assert_gateway_fields_allowed(
organization_id,
gateway_model=aiprovider.gateway_model,
gateway_interface=aiprovider.gateway_interface.value
if hasattr(aiprovider.gateway_interface, "value")
else aiprovider.gateway_interface,
gateway_base_url=aiprovider.gateway_base_url,
gateway_auth_header=aiprovider.gateway_auth_header,
gateway_auth_secret_env=aiprovider.gateway_auth_secret_env,
gateway_auth_secret=aiprovider.gateway_auth_secret,
gateway_extra_headers=aiprovider.gateway_extra_headers,
)

existing_default = db.query(AIProvider).filter(
AIProvider.organization_id == organization_id,
Expand Down Expand Up @@ -278,24 +397,36 @@ async def update_aiprovider(
)

update_data = aiprovider_update.model_dump(exclude_unset=True)
next_routing_mode = update_data.get(
"routing_mode",
CredentialRoutingMode(db_aiprovider.routing_mode),
)
stored_routing_mode = CredentialRoutingMode(db_aiprovider.routing_mode)
next_routing_mode = update_data.get("routing_mode", stored_routing_mode)
next_gateway_model = update_data.get("gateway_model", db_aiprovider.gateway_model)
next_api_key = update_data.get("api_key")
has_existing_key = (
bool(db_aiprovider.api_key)
and not is_gateway_managed_stored_key(db_aiprovider.api_key)
)

if "routing_mode" in update_data or "api_key" in update_data or "gateway_model" in update_data:
if "routing_mode" in update_data:
_validate_routing_and_api_key(
organization_id=organization_id,
routing_mode=next_routing_mode,
api_key=next_api_key,
gateway_model=next_gateway_model,
has_existing_key=(
bool(db_aiprovider.api_key)
and not is_gateway_managed_stored_key(db_aiprovider.api_key)
),
has_existing_key=has_existing_key,
check_routing_entitlement=True,
)
elif "api_key" in update_data:
_validate_routing_and_api_key(
organization_id=organization_id,
routing_mode=stored_routing_mode,
api_key=next_api_key,
gateway_model=next_gateway_model,
has_existing_key=has_existing_key,
check_routing_entitlement=False,
)

_assert_gateway_update_allowed(organization_id, db_aiprovider, update_data)

skip_fields = {
"api_key",
"routing_mode",
Expand Down
8 changes: 6 additions & 2 deletions app/api/v1/routes/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from datetime import datetime, timezone

from app.database import get_db
from app.dependencies import get_organization_id
from app.dependencies import get_organization_id, require_enterprise_feature
from app.models.database import Alert, AlertHistory
from app.models.enums import AlertStatus, AlertHistoryStatus
from app.models.schemas import (
Expand All @@ -22,7 +22,11 @@
from app.services.alerts.alert_evaluation_service import alert_evaluation_service
from app.services.alerts.alert_notification_service import alert_notification_service

router = APIRouter(prefix="/alerts", tags=["alerts"])
router = APIRouter(
prefix="/alerts",
tags=["alerts"],
dependencies=[Depends(require_enterprise_feature("alerts"))],
)


# ============================================
Expand Down
2 changes: 2 additions & 0 deletions app/api/v1/routes/iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import secrets

from app.dependencies import get_db, get_organization_id, get_api_key
from app.core.oss_quotas import enforce_oss_quota
from app.core.auth import Principal, get_principal
from app.core.auth.rbac import require_admin
from app.models.database import (
Expand Down Expand Up @@ -282,6 +283,7 @@ async def invite_user(
Invite a user to the organization.
Requires ADMIN role.
"""
enforce_oss_quota(db, organization_id, "org_members")
# Check if user already exists
existing_user = db.query(User).filter(User.email == invitation_data.email).first()

Expand Down
9 changes: 9 additions & 0 deletions app/api/v1/routes/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ async def create_integration(
Requires at least WRITER role.
"""
from sqlalchemy import func
from app.models.enums import CredentialRoutingMode
from app.services.ai.llm_gateway_settings import assert_credential_routing_allowed

assert_credential_routing_allowed(organization_id, integration_data.routing_mode)

platform_value = integration_data.platform.value if hasattr(integration_data.platform, 'value') else integration_data.platform

user_details = None
Expand Down Expand Up @@ -282,6 +287,10 @@ async def update_integration(
integration.is_active = integration_update.is_active

if integration_update.routing_mode is not None:
from app.models.enums import CredentialRoutingMode
from app.services.ai.llm_gateway_settings import assert_credential_routing_allowed

assert_credential_routing_allowed(organization_id, integration_update.routing_mode)
integration.routing_mode = integration_update.routing_mode.value

db.commit()
Expand Down
13 changes: 11 additions & 2 deletions app/api/v1/routes/metric_studio.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
from sqlalchemy.orm import Session

from app.database import get_db
from app.dependencies import get_api_key, get_organization_id, get_workspace_id
from app.dependencies import (
get_api_key,
get_organization_id,
get_workspace_id,
require_enterprise_feature,
)
from app.models.database import (
Metric,
MetricStudioRun,
Expand All @@ -28,7 +33,11 @@
from app.services.metric_studio.run_rollup import rollup_metric_studio_run
from app.services.metric_studio.source_resolver import resolve_source

router = APIRouter(prefix="/metric-studio", tags=["metric-studio"])
router = APIRouter(
prefix="/metric-studio",
tags=["metric-studio"],
dependencies=[Depends(require_enterprise_feature("metric_studio"))],
)


def _serialize_run(run: MetricStudioRun) -> MetricStudioRunResponse:
Expand Down
10 changes: 10 additions & 0 deletions app/api/v1/routes/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from app.database import get_db
from app.dependencies import get_organization_id, get_api_key, get_workspace_id
from app.core.oss_quotas import enforce_oss_quota
from app.services.billing.flexprice_service import record_metrics_llm_assist
from app.models.database import Metric, MetricCategory, MetricType, MetricTrigger, ModelProvider
from app.models.schemas import (
Expand Down Expand Up @@ -240,6 +241,7 @@ def create_metric(
``parent_metric_id`` is set so a stale UI can't accidentally split
a tree across workspaces or scopes.
"""
enforce_oss_quota(db, organization_id, "metrics")
_validate_hierarchy_fields(
organization_id,
db,
Expand Down Expand Up @@ -359,6 +361,7 @@ def create_metric_draft(
db: Session = Depends(get_db),
):
"""Create a draft metric for Metrics Studio (hidden from production flows)."""
enforce_oss_quota(db, organization_id, "metrics")
_validate_hierarchy_fields(
organization_id,
db,
Expand Down Expand Up @@ -455,6 +458,12 @@ def _create_metric_with_children(
lifecycle: str = "active",
studio_notes: Optional[str] = None,
) -> Metric:
enforce_oss_quota(
db,
organization_id,
"metrics",
additional=1 + len(payload.children),
)
if payload.selection_mode not in _VALID_SELECTION_MODES:
raise HTTPException(
status_code=400,
Expand Down Expand Up @@ -686,6 +695,7 @@ def add_metric_child(
db: Session = Depends(get_db),
):
"""Append a new child sub-metric under an existing parent."""
enforce_oss_quota(db, organization_id, "metrics")

parent = (
db.query(Metric)
Expand Down
Loading
Loading