mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 12:43:16 +00:00
Some checks are pending
CI / Lint (push) Waiting to run
CI / Format check (push) Waiting to run
CI / Typecheck (push) Waiting to run
CI / Unit tests (push) Waiting to run
CI / Playwright E2E (push) Waiting to run
CI / Docker build smoke (push) Waiting to run
CI / Triage ledger up to date (push) Waiting to run
CI / ui bun.lock in sync (push) Waiting to run
* feat(models): re-add Fable 5 with admin disable toggle (port of upstream #1677) * refactor(models): convert re-added Fable 5 to Bedrock model IDs * fix(open-swe): correct Fable copy to describe provider data sharing, not ZDR The ported admin toggle description and code comments described Fable 5 as incompatible with Zero Data Retention. That is backwards: Fable 5 requires the account to opt into Bedrock provider_data_share — prompts/completions are retained and shared with Anthropic (up to 30 days, incl. human review). The old UI copy would lead an admin to believe the opposite of what enabling the toggle does. Reword the toggle description and the gate_fable_model / team_settings comments accordingly. Still off by default. Refs #171.
231 lines
8.5 KiB
Python
231 lines
8.5 KiB
Python
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from langgraph.graph.state import RunnableConfig
|
|
|
|
from agent.server import get_agent
|
|
|
|
|
|
class _DummyAgent:
|
|
def with_config(self, config: RunnableConfig) -> "_DummyAgent":
|
|
self.config = config
|
|
return self
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agent_uses_profile_subagent_model_override() -> None:
|
|
config: RunnableConfig = {
|
|
"configurable": {
|
|
"__is_for_execution__": True,
|
|
"thread_id": "thread-123",
|
|
"github_login": "octocat",
|
|
},
|
|
"metadata": {},
|
|
}
|
|
main_model = MagicMock(name="main_model")
|
|
subagent_model = MagicMock(name="subagent_model")
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_create_deep_agent(**kwargs: object) -> _DummyAgent:
|
|
captured.update(kwargs)
|
|
return _DummyAgent()
|
|
|
|
with (
|
|
patch(
|
|
"agent.server.resolve_github_token",
|
|
new_callable=AsyncMock,
|
|
return_value=("ghp", None),
|
|
),
|
|
patch("agent.server.resolve_triggering_user_identity", return_value=None),
|
|
patch(
|
|
"agent.server.ensure_sandbox_for_thread",
|
|
new_callable=AsyncMock,
|
|
return_value=MagicMock(),
|
|
),
|
|
patch(
|
|
"agent.server.aresolve_sandbox_work_dir",
|
|
new_callable=AsyncMock,
|
|
return_value="/workspace",
|
|
),
|
|
patch(
|
|
"agent.server.get_team_default_model_pair",
|
|
new_callable=AsyncMock,
|
|
return_value=(
|
|
("bedrock_converse:us.anthropic.claude-opus-4-8", "medium"),
|
|
("fireworks:accounts/fireworks/models/deepseek-v4-pro", "low"),
|
|
),
|
|
),
|
|
patch(
|
|
"agent.server.load_profile",
|
|
new_callable=AsyncMock,
|
|
return_value={
|
|
"default_model": "bedrock_converse:us.anthropic.claude-opus-4-8",
|
|
"reasoning_effort": "high",
|
|
"default_subagent_model": "fireworks:accounts/fireworks/models/deepseek-v4-pro",
|
|
"subagent_reasoning_effort": "xhigh",
|
|
},
|
|
),
|
|
patch("agent.server.fallback_model_id_for", return_value=None),
|
|
patch(
|
|
"agent.utils.deferred_model.make_model", side_effect=[main_model, subagent_model]
|
|
) as make_model,
|
|
patch("agent.server.construct_system_prompt", return_value="prompt"),
|
|
patch("agent.server.create_deep_agent", side_effect=fake_create_deep_agent),
|
|
):
|
|
await get_agent(config)
|
|
|
|
assert captured["model"] is main_model
|
|
subagents = captured["subagents"]
|
|
assert isinstance(subagents, list)
|
|
assert subagents[0]["name"] == "general-purpose"
|
|
assert subagents[0]["model"] is subagent_model
|
|
|
|
main_call = make_model.call_args_list[0]
|
|
assert main_call.args == ("bedrock_converse:us.anthropic.claude-opus-4-8",)
|
|
assert main_call.kwargs["additional_model_request_fields"] == {
|
|
"thinking": {"type": "adaptive", "display": "summarized"},
|
|
"output_config": {"effort": "high"},
|
|
}
|
|
|
|
subagent_call = make_model.call_args_list[1]
|
|
assert subagent_call.args == ("fireworks:accounts/fireworks/models/deepseek-v4-pro",)
|
|
assert subagent_call.kwargs["model_kwargs"] == {"reasoning_effort": "xhigh"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agent_subagent_inherits_profile_model_override_without_explicit_pair() -> None:
|
|
config: RunnableConfig = {
|
|
"configurable": {
|
|
"__is_for_execution__": True,
|
|
"thread_id": "thread-123",
|
|
"github_login": "octocat",
|
|
},
|
|
"metadata": {},
|
|
}
|
|
main_model = MagicMock(name="main_model")
|
|
subagent_model = MagicMock(name="subagent_model")
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_create_deep_agent(**kwargs: object) -> _DummyAgent:
|
|
captured.update(kwargs)
|
|
return _DummyAgent()
|
|
|
|
with (
|
|
patch(
|
|
"agent.server.resolve_github_token",
|
|
new_callable=AsyncMock,
|
|
return_value=("ghp", None),
|
|
),
|
|
patch("agent.server.resolve_triggering_user_identity", return_value=None),
|
|
patch(
|
|
"agent.server.ensure_sandbox_for_thread",
|
|
new_callable=AsyncMock,
|
|
return_value=MagicMock(),
|
|
),
|
|
patch(
|
|
"agent.server.aresolve_sandbox_work_dir",
|
|
new_callable=AsyncMock,
|
|
return_value="/workspace",
|
|
),
|
|
patch(
|
|
"agent.server.get_team_default_model_pair",
|
|
new_callable=AsyncMock,
|
|
return_value=(
|
|
("bedrock_converse:us.anthropic.claude-opus-4-8", "medium"),
|
|
("fireworks:accounts/fireworks/models/deepseek-v4-pro", "low"),
|
|
),
|
|
),
|
|
patch(
|
|
"agent.server.load_profile",
|
|
new_callable=AsyncMock,
|
|
return_value={
|
|
"default_model": "bedrock_converse:us.anthropic.claude-opus-4-8",
|
|
"reasoning_effort": "high",
|
|
},
|
|
),
|
|
patch("agent.server.fallback_model_id_for", return_value=None),
|
|
patch(
|
|
"agent.utils.deferred_model.make_model", side_effect=[main_model, subagent_model]
|
|
) as make_model,
|
|
patch("agent.server.construct_system_prompt", return_value="prompt"),
|
|
patch("agent.server.create_deep_agent", side_effect=fake_create_deep_agent),
|
|
):
|
|
await get_agent(config)
|
|
|
|
subagents = captured["subagents"]
|
|
assert isinstance(subagents, list)
|
|
assert subagents[0]["model"] is subagent_model
|
|
assert make_model.call_args_list[0].args == ("bedrock_converse:us.anthropic.claude-opus-4-8",)
|
|
assert make_model.call_args_list[1].args == ("bedrock_converse:us.anthropic.claude-opus-4-8",)
|
|
assert make_model.call_args_list[1].kwargs["additional_model_request_fields"] == {
|
|
"thinking": {"type": "adaptive", "display": "summarized"},
|
|
"output_config": {"effort": "high"},
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agent_gate_swaps_disabled_fable_profile_to_opus() -> None:
|
|
config: RunnableConfig = {
|
|
"configurable": {
|
|
"__is_for_execution__": True,
|
|
"thread_id": "thread-123",
|
|
"github_login": "octocat",
|
|
},
|
|
"metadata": {},
|
|
}
|
|
main_model = MagicMock(name="main_model")
|
|
subagent_model = MagicMock(name="subagent_model")
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_create_deep_agent(**kwargs: object) -> _DummyAgent:
|
|
captured.update(kwargs)
|
|
return _DummyAgent()
|
|
|
|
with (
|
|
patch(
|
|
"agent.server.resolve_github_token", new_callable=AsyncMock, return_value=("ghp", None)
|
|
),
|
|
patch("agent.server.resolve_triggering_user_identity", return_value=None),
|
|
patch(
|
|
"agent.server.ensure_sandbox_for_thread",
|
|
new_callable=AsyncMock,
|
|
return_value=MagicMock(),
|
|
),
|
|
patch(
|
|
"agent.server.aresolve_sandbox_work_dir",
|
|
new_callable=AsyncMock,
|
|
return_value="/workspace",
|
|
),
|
|
patch(
|
|
"agent.server.get_team_default_model_pair",
|
|
new_callable=AsyncMock,
|
|
return_value=(
|
|
("bedrock_converse:us.anthropic.claude-opus-4-8", "medium"),
|
|
("bedrock_converse:us.anthropic.claude-opus-4-8", "low"),
|
|
),
|
|
),
|
|
# Profile selected Fable back when it was allowed; it's now disabled.
|
|
patch(
|
|
"agent.server.load_profile",
|
|
new_callable=AsyncMock,
|
|
return_value={
|
|
"default_model": "bedrock_converse:us.anthropic.claude-fable-5",
|
|
"reasoning_effort": "high",
|
|
},
|
|
),
|
|
patch("agent.server.get_team_fable_enabled", new_callable=AsyncMock, return_value=False),
|
|
patch("agent.server.fallback_model_id_for", return_value=None),
|
|
patch(
|
|
"agent.utils.deferred_model.make_model", side_effect=[main_model, subagent_model]
|
|
) as make_model,
|
|
patch("agent.server.construct_system_prompt", return_value="prompt"),
|
|
patch("agent.server.create_deep_agent", side_effect=fake_create_deep_agent),
|
|
):
|
|
await get_agent(config)
|
|
|
|
# Fable was scrubbed to Opus for both main and subagent; effort preserved.
|
|
assert make_model.call_args_list[0].args == ("bedrock_converse:us.anthropic.claude-opus-4-8",)
|
|
assert make_model.call_args_list[0].kwargs["additional_model_request_fields"][
|
|
"output_config"
|
|
] == {"effort": "high"}
|
|
assert make_model.call_args_list[1].args == ("bedrock_converse:us.anthropic.claude-opus-4-8",)
|