refactor(backend/copilot): remove LD per-user model override machinery

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.
This commit is contained in:
majdyz
2026-04-15 14:26:45 +07:00
parent 8810e0170f
commit e88fbf6152
4 changed files with 3 additions and 207 deletions

View File

@@ -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)

View File

@@ -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
# ---------------------------------------------------------------------------

View File

@@ -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>=<value>`` (``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,

View File

@@ -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"
)