mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user