From e88fbf615204c8554b28fcdcfe4ea7d9997eb8d0 Mon Sep 17 00:00:00 2001 From: majdyz Date: Wed, 15 Apr 2026 14:26:45 +0700 Subject: [PATCH] refactor(backend/copilot): remove LD per-user model override machinery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop Flag.COPILOT_MODEL, env_flag_string_override, _resolve_user_model_override and all associated tests — the UI toggle makes per-user LD targeting unnecessary. --- .../backend/backend/copilot/sdk/service.py | 47 +-------- .../copilot/sdk/service_helpers_test.py | 96 ------------------- .../backend/backend/util/feature_flag.py | 24 ----- .../backend/backend/util/feature_flag_test.py | 43 --------- 4 files changed, 3 insertions(+), 207 deletions(-) diff --git a/autogpt_platform/backend/backend/copilot/sdk/service.py b/autogpt_platform/backend/backend/copilot/sdk/service.py index 26a9971240..e3cf3dcf0a 100644 --- a/autogpt_platform/backend/backend/copilot/sdk/service.py +++ b/autogpt_platform/backend/backend/copilot/sdk/service.py @@ -54,11 +54,6 @@ from backend.copilot.transcript_builder import TranscriptBuilder from backend.data.redis_client import get_redis_async from backend.executor.cluster_lock import AsyncClusterLock from backend.util.exceptions import NotFoundError -from backend.util.feature_flag import ( - Flag, - env_flag_string_override, - get_feature_flag_value, -) from backend.util.settings import Settings from ..config import ChatConfig, CopilotLlmModel, CopilotMode @@ -684,39 +679,15 @@ def _resolve_fallback_model() -> str | None: return _normalize_model_name(raw) -async def _resolve_user_model_override(user_id: str) -> str | None: - """Resolve a per-user model override from LaunchDarkly or env vars. - - Checks ``Flag.COPILOT_MODEL`` via LaunchDarkly for the given user. - Returns the normalized model string (e.g. ``"claude-opus-4-6"``) when - an override is configured, or ``None`` to fall through to the global - ``config.model`` / ``sdk_model`` default. - - Local dev: set ``FORCE_FLAG_COPILOT_MODEL=anthropic/claude-opus-4-6`` - (or with ``NEXT_PUBLIC_FORCE_FLAG_`` prefix) to test without LD. - """ - # Env override takes precedence — avoids an LD round-trip in local dev. - env_override = env_flag_string_override(Flag.COPILOT_MODEL) - if env_override: - return _normalize_model_name(env_override) - - raw = await get_feature_flag_value(Flag.COPILOT_MODEL.value, user_id, default=None) - if not raw or not isinstance(raw, str): - return None - return _normalize_model_name(raw) - - async def _resolve_model_and_multiplier( model: "CopilotLlmModel | None", - user_id: str | None, session_id: str, ) -> tuple[str | None, float]: """Resolve the SDK model string and rate-limit cost multiplier for a turn. Priority (highest first): 1. Explicit per-request ``model`` tier from the frontend toggle. - 2. Per-user LaunchDarkly model override (``Flag.COPILOT_MODEL``). - 3. Global config default (``_resolve_sdk_model()``). + 2. Global config default (``_resolve_sdk_model()``). Returns a ``(sdk_model, cost_multiplier)`` pair. ``sdk_model`` is ``None`` when the Claude Code subscription default applies. @@ -724,18 +695,6 @@ async def _resolve_model_and_multiplier( """ sdk_model = _resolve_sdk_model() - if user_id: - user_model_override = await _resolve_user_model_override(user_id) - if user_model_override: - logger.info( - "[SDK] [%s] Per-user model override for user %s: %s (was: %s)", - session_id[:12] if session_id else "?", - user_id[:8], - user_model_override, - sdk_model or "subscription-default", - ) - sdk_model = user_model_override - if model == "advanced": sdk_model = _normalize_model_name("anthropic/claude-opus-4-6") logger.info( @@ -2581,9 +2540,9 @@ async def stream_chat_completion_sdk( mcp_server = create_copilot_mcp_server(use_e2b=use_e2b) - # Resolve model and cost multiplier (LD per-user → request tier → config). + # Resolve model and cost multiplier (request tier → config default). sdk_model, model_cost_multiplier = await _resolve_model_and_multiplier( - model, user_id, session_id + model, session_id ) # Track SDK-internal compaction (PreCompact hook → start, next msg → end) diff --git a/autogpt_platform/backend/backend/copilot/sdk/service_helpers_test.py b/autogpt_platform/backend/backend/copilot/sdk/service_helpers_test.py index 3348418fcd..cc015259a5 100644 --- a/autogpt_platform/backend/backend/copilot/sdk/service_helpers_test.py +++ b/autogpt_platform/backend/backend/copilot/sdk/service_helpers_test.py @@ -22,7 +22,6 @@ from .service import ( _iter_sdk_messages, _normalize_model_name, _reduce_context, - _resolve_user_model_override, ) # --------------------------------------------------------------------------- @@ -354,101 +353,6 @@ class TestIsParallelContinuation: assert _is_tool_only_message(msg) is True -# --------------------------------------------------------------------------- -# _resolve_user_model_override -# --------------------------------------------------------------------------- - - -class TestResolveUserModelOverride: - @pytest.mark.asyncio - async def test_no_env_no_ld_returns_none(self, monkeypatch: pytest.MonkeyPatch): - """When no env override and LD returns None, result is None.""" - monkeypatch.delenv("FORCE_FLAG_COPILOT_MODEL", raising=False) - monkeypatch.delenv("NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", raising=False) - with patch( - "backend.copilot.sdk.service.get_feature_flag_value", - new=AsyncMock(return_value=None), - ): - result = await _resolve_user_model_override("user-123") - assert result is None - - @pytest.mark.asyncio - async def test_env_override_bypasses_ld(self, monkeypatch: pytest.MonkeyPatch): - """FORCE_FLAG_COPILOT_MODEL short-circuits the LD call.""" - monkeypatch.setenv("FORCE_FLAG_COPILOT_MODEL", "anthropic/claude-opus-4-6") - ld_mock = AsyncMock(return_value=None) - with patch("backend.copilot.sdk.service.get_feature_flag_value", new=ld_mock): - result = await _resolve_user_model_override("user-123") - # LD should not be called - ld_mock.assert_not_called() - # Model name is normalized (OpenRouter prefix stripped) - assert result == "claude-opus-4-6" - - @pytest.mark.asyncio - async def test_ld_returns_model_string(self, monkeypatch: pytest.MonkeyPatch): - """When LD returns a model string, it is normalized and returned.""" - monkeypatch.delenv("FORCE_FLAG_COPILOT_MODEL", raising=False) - monkeypatch.delenv("NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", raising=False) - with patch( - "backend.copilot.sdk.service.get_feature_flag_value", - new=AsyncMock(return_value="anthropic/claude-opus-4-6"), - ): - result = await _resolve_user_model_override("user-123") - assert result == "claude-opus-4-6" - - @pytest.mark.asyncio - async def test_ld_returns_non_string_returns_none( - self, monkeypatch: pytest.MonkeyPatch - ): - """When LD returns a non-string (e.g. True), result is None.""" - monkeypatch.delenv("FORCE_FLAG_COPILOT_MODEL", raising=False) - monkeypatch.delenv("NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", raising=False) - with patch( - "backend.copilot.sdk.service.get_feature_flag_value", - new=AsyncMock(return_value=True), - ): - result = await _resolve_user_model_override("user-123") - assert result is None - - @pytest.mark.asyncio - async def test_ld_returns_empty_string_returns_none( - self, monkeypatch: pytest.MonkeyPatch - ): - """When LD returns an empty string, result is None.""" - monkeypatch.delenv("FORCE_FLAG_COPILOT_MODEL", raising=False) - monkeypatch.delenv("NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", raising=False) - with patch( - "backend.copilot.sdk.service.get_feature_flag_value", - new=AsyncMock(return_value=""), - ): - result = await _resolve_user_model_override("user-123") - assert result is None - - @pytest.mark.asyncio - async def test_already_normalized_model_unchanged( - self, monkeypatch: pytest.MonkeyPatch - ): - """A model name without the OpenRouter prefix is returned as-is.""" - monkeypatch.delenv("FORCE_FLAG_COPILOT_MODEL", raising=False) - monkeypatch.delenv("NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", raising=False) - with patch( - "backend.copilot.sdk.service.get_feature_flag_value", - new=AsyncMock(return_value="claude-opus-4-6"), - ): - result = await _resolve_user_model_override("user-123") - assert result == "claude-opus-4-6" - - @pytest.mark.asyncio - async def test_ld_call_uses_correct_flag_key(self, monkeypatch: pytest.MonkeyPatch): - """get_feature_flag_value is called with Flag.COPILOT_MODEL and the user_id.""" - monkeypatch.delenv("FORCE_FLAG_COPILOT_MODEL", raising=False) - monkeypatch.delenv("NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", raising=False) - ld_mock = AsyncMock(return_value=None) - with patch("backend.copilot.sdk.service.get_feature_flag_value", new=ld_mock): - await _resolve_user_model_override("user-abc") - ld_mock.assert_called_once_with("copilot-model", "user-abc", default=None) - - # --------------------------------------------------------------------------- # _normalize_model_name — used by per-request model override # --------------------------------------------------------------------------- diff --git a/autogpt_platform/backend/backend/util/feature_flag.py b/autogpt_platform/backend/backend/util/feature_flag.py index 689d0ab23e..27121304ca 100644 --- a/autogpt_platform/backend/backend/util/feature_flag.py +++ b/autogpt_platform/backend/backend/util/feature_flag.py @@ -46,7 +46,6 @@ class Flag(str, Enum): STRIPE_PRICE_PRO = "stripe-price-id-pro" STRIPE_PRICE_BUSINESS = "stripe-price-id-business" GRAPHITI_MEMORY = "graphiti-memory" - COPILOT_MODEL = "copilot-model" def is_configured() -> bool: @@ -195,29 +194,6 @@ def _env_flag_override(flag_key: Flag) -> bool | None: return None -def env_flag_string_override(flag_key: Flag) -> str | None: - """Return a local string override for ``flag_key`` from the environment. - - Set ``FORCE_FLAG_=`` (``NAME`` = flag value with - ``-`` → ``_``, upper-cased) to bypass LaunchDarkly for a string flag - in local dev or tests. Returns ``None`` when no override is configured - or the value is blank. - - Accepts the ``NEXT_PUBLIC_FORCE_FLAG_`` prefix so a single shared env - var works across backend and frontend services. - - Example: ``FORCE_FLAG_COPILOT_MODEL=anthropic/claude-opus-4-6`` routes - all users to Opus in a local dev environment. - """ - suffix = flag_key.value.upper().replace("-", "_") - for prefix in ("FORCE_FLAG_", "NEXT_PUBLIC_FORCE_FLAG_"): - raw = os.environ.get(prefix + suffix) - if raw is not None: - stripped = raw.strip() - return stripped if stripped else None - return None - - async def is_feature_enabled( flag_key: Flag, user_id: str, diff --git a/autogpt_platform/backend/backend/util/feature_flag_test.py b/autogpt_platform/backend/backend/util/feature_flag_test.py index ae199a4898..9a11256ef8 100644 --- a/autogpt_platform/backend/backend/util/feature_flag_test.py +++ b/autogpt_platform/backend/backend/util/feature_flag_test.py @@ -5,7 +5,6 @@ from ldclient import LDClient from backend.util.feature_flag import ( Flag, _env_flag_override, - env_flag_string_override, feature_flag, is_feature_enabled, mock_flag_variation, @@ -169,45 +168,3 @@ class TestEnvFlagOverride: def test_case_insensitive_value(self, monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("FORCE_FLAG_CHAT", "TRUE") assert _env_flag_override(Flag.CHAT) is True - - -class TestEnvFlagOverrideString: - def test_returns_model_string(self, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setenv("FORCE_FLAG_COPILOT_MODEL", "anthropic/claude-opus-4-6") - assert ( - env_flag_string_override(Flag.COPILOT_MODEL) == "anthropic/claude-opus-4-6" - ) - - def test_next_public_prefix_accepted(self, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setenv( - "NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", "anthropic/claude-opus-4-6" - ) - assert ( - env_flag_string_override(Flag.COPILOT_MODEL) == "anthropic/claude-opus-4-6" - ) - - def test_unset_returns_none(self, monkeypatch: pytest.MonkeyPatch): - monkeypatch.delenv("FORCE_FLAG_COPILOT_MODEL", raising=False) - monkeypatch.delenv("NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", raising=False) - assert env_flag_string_override(Flag.COPILOT_MODEL) is None - - def test_blank_value_returns_none(self, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setenv("FORCE_FLAG_COPILOT_MODEL", " ") - assert env_flag_string_override(Flag.COPILOT_MODEL) is None - - def test_force_flag_takes_precedence_over_next_public( - self, monkeypatch: pytest.MonkeyPatch - ): - monkeypatch.setenv("FORCE_FLAG_COPILOT_MODEL", "anthropic/claude-opus-4-6") - monkeypatch.setenv( - "NEXT_PUBLIC_FORCE_FLAG_COPILOT_MODEL", "anthropic/claude-sonnet-4-6" - ) - assert ( - env_flag_string_override(Flag.COPILOT_MODEL) == "anthropic/claude-opus-4-6" - ) - - def test_whitespace_is_stripped(self, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setenv("FORCE_FLAG_COPILOT_MODEL", " anthropic/claude-opus-4-6 ") - assert ( - env_flag_string_override(Flag.COPILOT_MODEL) == "anthropic/claude-opus-4-6" - )