diff --git a/backend/apps/settings/models.py b/backend/apps/settings/models.py index 97023155..29170683 100644 --- a/backend/apps/settings/models.py +++ b/backend/apps/settings/models.py @@ -31,10 +31,14 @@ DEFAULT_SYSTEM_PROMPT = ( ) +# Fresh-install / unset-fallback model. "opus-5" is the plain row: cc/ sub lane for subscription users, API key otherwise. +DEFAULT_MODEL = "opus-5" + + class AppSettings(BaseModel): default_system_prompt: Optional[str] = DEFAULT_SYSTEM_PROMPT default_folder: Optional[str] = None - default_model: str = "sonnet" + default_model: str = DEFAULT_MODEL default_mode: str = "agent" default_max_turns: Optional[int] = None default_thinking_level: Literal["off", "low", "medium", "high", "auto"] = "auto" diff --git a/backend/apps/subscription/free_trial.py b/backend/apps/subscription/free_trial.py index 889f2215..d6a02272 100644 --- a/backend/apps/subscription/free_trial.py +++ b/backend/apps/subscription/free_trial.py @@ -136,7 +136,8 @@ async def clear_free_trial(settings_obj) -> None: if getattr(settings_obj, "default_model", None) == "haiku" and ( has_own_model(settings_obj) or await p_has_connected_subscription() ): - settings_obj.default_model = "sonnet" + from backend.apps.settings.models import DEFAULT_MODEL + settings_obj.default_model = DEFAULT_MODEL settings_obj.free_trial_token = None await save_settings_async(settings_obj) await p_sync_routing(settings_obj) diff --git a/backend/apps/swarm/entities/SessionExportable.py b/backend/apps/swarm/entities/SessionExportable.py index b735dbf8..cf1d1140 100644 --- a/backend/apps/swarm/entities/SessionExportable.py +++ b/backend/apps/swarm/entities/SessionExportable.py @@ -14,6 +14,7 @@ from uuid import uuid4 from backend.apps.swarm.exportable import DepRef, ExportContext, RemapTable from backend.apps.swarm.models import EntityType, Requirement, RequirementKind +from backend.apps.settings.models import DEFAULT_MODEL P_BUILTIN_MODES = {"agent", "ask", "plan", "view-builder", "skill-builder"} # Transcript fields ride along so the shared agent keeps its history; ids inside (message ids, branch ids, their parent/fork refs) are self-consistent within the one session file, so they carry verbatim with no remap. @@ -95,7 +96,7 @@ class SessionExportable: "name": payload.get("name") or "Agent", "status": "completed", "provider": payload.get("provider") or "anthropic", - "model": payload.get("model") or "sonnet", + "model": payload.get("model") or DEFAULT_MODEL, "mode": payload.get("mode") or "agent", "system_prompt": payload.get("system_prompt"), "allowed_tools": payload.get("allowed_tools") or [], diff --git a/backend/apps/workflows/executor.py b/backend/apps/workflows/executor.py index 1e54b734..35cbb5f9 100644 --- a/backend/apps/workflows/executor.py +++ b/backend/apps/workflows/executor.py @@ -14,6 +14,7 @@ from typing import Optional from backend.apps.agents.core.models import AgentConfig from backend.apps.workflows.models import Workflow, WorkflowRun from backend.apps.workflows import storage +from backend.apps.settings.models import DEFAULT_MODEL logger = logging.getLogger(__name__) @@ -268,7 +269,7 @@ async def execute( resolved_allowed_tools = _resolve_allowed_tools(wf) config = AgentConfig( name=wf.title or "Workflow", - model=wf.model or "sonnet", + model=wf.model or DEFAULT_MODEL, mode=wf.mode or "agent", provider=wf.provider or "anthropic", system_prompt=_resolve_system_prompt(wf), diff --git a/backend/apps/workflows/models.py b/backend/apps/workflows/models.py index 84c176e0..0e56bd4c 100644 --- a/backend/apps/workflows/models.py +++ b/backend/apps/workflows/models.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida from typing import Optional, Literal, Any from datetime import datetime from uuid import uuid4 +from backend.apps.settings.models import DEFAULT_MODEL # Each "tier" in the permission chain: notify in app, fall through to text after N minutes if no response, then to call after a further N minutes/hours. Matches images 17 to 19 (Schedule edit). Order in the list = escalation order. @@ -97,7 +98,7 @@ class Workflow(BaseModel): # Tool names observed in the source chat when this workflow was generated. This preserves conversion context without pretending those calls map to generated workflow step ids. Explicit approval decisions still live in remembered_approvals and are the only values reused as permissions. source_tools: list[str] = Field(default_factory=list) dashboard_id: Optional[str] = None - model: str = "sonnet" + model: str = DEFAULT_MODEL mode: str = "agent" provider: str = "anthropic" created_at: datetime = Field(default_factory=datetime.now) diff --git a/backend/apps/workflows/workflows.py b/backend/apps/workflows/workflows.py index fe930c1a..ffd4b9a8 100644 --- a/backend/apps/workflows/workflows.py +++ b/backend/apps/workflows/workflows.py @@ -20,6 +20,7 @@ from backend.apps.workflows.models import ( GenerateMetadataResponse, ) from backend.apps.workflows import storage, scheduler, executor, audit, escalation +from backend.apps.settings.models import DEFAULT_MODEL logger = logging.getLogger(__name__) @@ -265,7 +266,7 @@ async def create_workflow(body: WorkflowCreate): permissions=body.permissions or [], source_session_id=body.source_session_id, dashboard_id=body.dashboard_id, - model=body.model or "sonnet", + model=body.model or DEFAULT_MODEL, mode=body.mode or "agent", provider=body.provider or "anthropic", cost_cap_usd_monthly=body.cost_cap_usd_monthly, @@ -313,7 +314,7 @@ async def create_workflow(body: WorkflowCreate): @workflows.router.post("/generate-metadata") async def generate_workflow_metadata(body: GenerateMetadataRequest) -> GenerateMetadataResponse: # Preview-time naming for the convert-to-workflow draft. Generates without persisting so the card can show a real title before the user saves. - wf = Workflow(steps=body.steps, model=body.model or "sonnet") + wf = Workflow(steps=body.steps, model=body.model or DEFAULT_MODEL) title, description, labels = await _generate_workflow_metadata(wf) return GenerateMetadataResponse(title=title, description=description, step_labels=labels) @@ -1017,7 +1018,7 @@ async def edit_agent_session(workflow_id: str): edit_dashboard_id = p_wsm.active_dashboard_id or executor.resolve_workflow_dashboard_id(wf) config = AgentConfig( name=f"Edit Agent: {wf.title}", - model=wf.model or "sonnet", + model=wf.model or DEFAULT_MODEL, mode=wf.mode or "agent", provider=wf.provider or "anthropic", system_prompt=system_prompt, @@ -1256,7 +1257,7 @@ async def test_run_workflow(workflow_id: str, body: dict): resolved_allowed_tools = executor._resolve_allowed_tools(wf) config = AgentConfig( name=f"{wf.title or 'Workflow'} (test)", - model=wf.model or "sonnet", + model=wf.model or DEFAULT_MODEL, mode=wf.mode or "agent", provider=wf.provider or "anthropic", system_prompt=executor._resolve_system_prompt(wf), @@ -1395,7 +1396,7 @@ async def schedule_agent_session(workflow_id: str): ) config = AgentConfig( name=f"Scheduling: {wf.title}", - model=wf.model or "sonnet", + model=wf.model or DEFAULT_MODEL, mode=wf.mode or "agent", provider=wf.provider or "anthropic", system_prompt=system_prompt, diff --git a/backend/main.py b/backend/main.py index 97367cd1..0366a312 100644 --- a/backend/main.py +++ b/backend/main.py @@ -503,7 +503,8 @@ async def browser_agent_run(request: Request): body = await request.json() tasks = body.get("tasks", []) - model = body.get("model", "sonnet") + from backend.apps.settings.models import DEFAULT_MODEL + model = body.get("model", DEFAULT_MODEL) dashboard_id = body.get("dashboard_id", "") pre_selected_browser_ids = body.get("pre_selected_browser_ids", []) parent_session_id = body.get("parent_session_id", "") diff --git a/backend/tests/test_free_trial.py b/backend/tests/test_free_trial.py index 4f42b8c5..5e67d092 100644 --- a/backend/tests/test_free_trial.py +++ b/backend/tests/test_free_trial.py @@ -168,13 +168,13 @@ async def test_clear_keeps_haiku_as_trial_face_unless_a_real_model_is_connected( s2 = AppSettings(connection_mode="free-trial", free_trial_token="ftk", default_model="haiku", anthropic_api_key="sk-ant-real") await clear_free_trial(s2) - assert s2.default_model == "sonnet" + assert s2.default_model == "opus-5" # A connected 9Router subscription (invisible in settings) also frees haiku -> sonnet. monkeypatch.setattr(ft, "p_has_connected_subscription", _true) s3 = AppSettings(connection_mode="free-trial", free_trial_token="ftk", default_model="haiku") await clear_free_trial(s3) - assert s3.default_model == "sonnet" + assert s3.default_model == "opus-5" # A user who deliberately picked haiku OUTSIDE free-trial mode is left alone. s4 = AppSettings(connection_mode="own_key", default_model="haiku") diff --git a/backend/tests/test_settings_migration.py b/backend/tests/test_settings_migration.py index 53d2e36b..b16dce49 100644 --- a/backend/tests/test_settings_migration.py +++ b/backend/tests/test_settings_migration.py @@ -63,7 +63,7 @@ def test_minimal_old_file_fills_missing_with_defaults(settings_file): p_write(settings_file, {"theme": "light"}) s = store.load_settings() assert s.theme == "light" - assert s.default_model == "sonnet" # filled from default + assert s.default_model == "opus-5" # filled from default assert s.auto_reveal_sub_agents is True diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e4608790..f5f40034 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "open-swarm", "version": "1.0.0", + "license": "AGPL-3.0-only", "dependencies": { "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-json": "^6.0.2", diff --git a/frontend/src/app/Main.tsx b/frontend/src/app/Main.tsx index 33109c71..a9209932 100644 --- a/frontend/src/app/Main.tsx +++ b/frontend/src/app/Main.tsx @@ -289,9 +289,9 @@ const DEFAULT_MODEL_PRIORITY: string[] = [ ]; const DEFAULT_MODEL_PICKS: Record = { - Anthropic: ['sonnet-cc', 'sonnet'], - OpenAI: ['gpt-5.4-mini', 'gpt-5.4'], - Google: ['gemini-2.5-flash', 'gemini-3-flash', 'gemini-2.5-pro'], + Anthropic: ['opus-5-cc', 'opus-5', 'opus-5-api', 'sonnet-5-cc', 'sonnet-5'], + OpenAI: ['gpt-5.6', 'gpt-5.6-api', 'gpt-5.5', 'gpt-5.5-api'], + Google: ['gemini-3.6-flash-api', 'gemini-3.5-flash-api', 'gemini-3.1-flash-lite'], 'OpenSwarm Pro': ['sonnet', 'opus'], OpenSwarm: ['gpt-5-mini', 'claude-haiku-4.5', 'gpt-4.1'], }; diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index 2fff1fca..92c828cc 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -337,7 +337,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose // Holds the last non-empty suggestions so the docked banner's exit fade renders them instead of going blank the instant the array is cleared. const mcpSnapshotRef = useRef>([]); const [mode, setMode] = useState('agent'); - const [model, setModel] = useState('sonnet'); + const [model, setModel] = useState('opus-5'); // Workflow build chat only: brief "this model now runs the workflow" notice when the user switches models, so the run-model change isn't silent. const [workflowModelNotice, setWorkflowModelNotice] = useState(null); const workflowModelNoticeTimer = useRef | null>(null); diff --git a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useChatInputModel.ts b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useChatInputModel.ts index f08b3afd..796a9845 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useChatInputModel.ts +++ b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useChatInputModel.ts @@ -3,6 +3,7 @@ import { useAppSelector } from '@/shared/hooks'; import { sortModelsForPicker } from '../model-picker/modelPicker'; const FALLBACK_MODELS = [ + { value: 'opus-5', label: 'Claude Opus 5', context_window: 1_000_000, reasoning: true }, { value: 'sonnet', label: 'Claude Sonnet 4.6', context_window: 1_000_000, reasoning: true }, { value: 'opus', label: 'Claude Opus 4.6', context_window: 1_000_000, reasoning: true }, { value: 'haiku', label: 'Claude Haiku 4.5', context_window: 200_000, reasoning: true }, diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx index 71b80e15..32d16edd 100644 --- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx @@ -84,14 +84,14 @@ const DashboardToolbar = React.forwardRef( const defaultThinkingLevel = useAppSelector((s) => s.settings.data.default_thinking_level); const settingsLoaded = useAppSelector((s) => s.settings.loaded); const [mode, setMode] = useState(defaultMode || 'agent'); - const [model, setModel] = useState(defaultModel || 'sonnet'); + const [model, setModel] = useState(defaultModel || 'opus-5'); const [thinkingLevel, setThinkingLevel] = useState<'off' | 'low' | 'medium' | 'high' | 'auto'>(defaultThinkingLevel || 'auto'); // Snap to the persisted Settings defaults as soon as they arrive from the backend. Without the settingsLoaded guard, the effect fires against the Redux initialState ('sonnet') before the real default has loaded, and the settingsApplied flag then locks out the real default for the rest of the session, so new chats spawn under the stale value. const settingsApplied = useRef(false); useEffect(() => { if (settingsLoaded && !settingsApplied.current) { setMode(defaultMode || 'agent'); - setModel(defaultModel || 'sonnet'); + setModel(defaultModel || 'opus-5'); setThinkingLevel(defaultThinkingLevel || 'auto'); settingsApplied.current = true; } @@ -126,7 +126,7 @@ const DashboardToolbar = React.forwardRef( useEffect(() => { if (settingsLoaded && inputOpen && !prevInputOpen.current) { setMode(defaultMode || 'agent'); - setModel(defaultModel || 'sonnet'); + setModel(defaultModel || 'opus-5'); setThinkingLevel(defaultThinkingLevel || 'auto'); } prevInputOpen.current = inputOpen; diff --git a/frontend/src/app/pages/Settings/Settings.tsx b/frontend/src/app/pages/Settings/Settings.tsx index 36ac5827..d675c15c 100644 --- a/frontend/src/app/pages/Settings/Settings.tsx +++ b/frontend/src/app/pages/Settings/Settings.tsx @@ -44,6 +44,7 @@ let lastOpenTab: string | null = null; // Shown only in the brief window before the live model list loads from the backend. Keep the flagship current so the default-model dropdown isn't stale. const DEFAULT_MODEL_FALLBACK = [ + { value: 'opus-5', label: 'Claude Opus 5' }, { value: 'opus-4-8', label: 'Claude Opus 4.8' }, { value: 'sonnet', label: 'Claude Sonnet 4.6' }, { value: 'opus', label: 'Claude Opus 4.6' }, diff --git a/frontend/src/shared/state/settingsSlice.ts b/frontend/src/shared/state/settingsSlice.ts index 88c0114d..40d5dae0 100644 --- a/frontend/src/shared/state/settingsSlice.ts +++ b/frontend/src/shared/state/settingsSlice.ts @@ -156,7 +156,7 @@ interface SettingsState { export const DEFAULT_SETTINGS: AppSettings = { default_system_prompt: DEFAULT_SYSTEM_PROMPT, default_folder: null, - default_model: 'sonnet', + default_model: 'opus-5', default_mode: 'agent', default_max_turns: null, default_thinking_level: 'auto',