diff --git a/backend/apps/workflows/default_model.py b/backend/apps/workflows/default_model.py new file mode 100644 index 00000000..8be7f88e --- /dev/null +++ b/backend/apps/workflows/default_model.py @@ -0,0 +1,42 @@ +"""Which model a workflow runs on when nobody picked one. + +`DEFAULT_MODEL` is `opus-5`, and that is Anthropic's API-KEY lane: the Claude subscription lane is a +separate id (`opus-5-cc`). Defaulting every workflow to the literal therefore billed an API key a +subscriber may not even have, and named a model a Codex or Gemini subscriber cannot run at all. +Lives in its own module because both the routes and the executor need it, and importing one from the +other is a cycle. +""" + +import logging +from typing import Optional + +from typeguard import typechecked + +from backend.apps.settings.models import DEFAULT_MODEL + +logger = logging.getLogger(__name__) + + +@typechecked +def user_default_model() -> str: + """The model this user actually configured, never a vendor literal.""" + try: + from backend.apps.settings.settings import load_settings + chosen = (getattr(load_settings(), "default_model", "") or "").strip() + return chosen or DEFAULT_MODEL + except Exception: + logger.debug("could not read the user's default model", exc_info=True) + return DEFAULT_MODEL + + +@typechecked +def provider_for_model(model: Optional[str]) -> str: + """Derive the provider from the model instead of assuming Anthropic.""" + if not model: + return "anthropic" + try: + from backend.apps.agents.providers.registry import get_api_type + return get_api_type(model) or "anthropic" + except Exception: + logger.debug("could not derive a provider for %s", model, exc_info=True) + return "anthropic" diff --git a/backend/apps/workflows/executor.py b/backend/apps/workflows/executor.py index a5229e1a..4efa0aa9 100644 --- a/backend/apps/workflows/executor.py +++ b/backend/apps/workflows/executor.py @@ -14,7 +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 +from backend.apps.workflows.default_model import provider_for_model, user_default_model logger = logging.getLogger(__name__) @@ -302,9 +302,9 @@ async def execute( resolved_allowed_tools = _resolve_allowed_tools(wf) config = AgentConfig( name=wf.title or "Workflow", - model=wf.model or DEFAULT_MODEL, + model=wf.model or user_default_model(), mode=wf.mode or "agent", - provider=wf.provider or "anthropic", + provider=wf.provider or provider_for_model(wf.model or user_default_model()), system_prompt=_resolve_system_prompt(wf), # None when the user has not frozen the Actions set, which means the workflow runs with the mode's full surface exactly like a chat does. allowed_tools=resolved_allowed_tools, diff --git a/backend/apps/workflows/workflows.py b/backend/apps/workflows/workflows.py index 03592cdf..d3349fed 100644 --- a/backend/apps/workflows/workflows.py +++ b/backend/apps/workflows/workflows.py @@ -22,6 +22,7 @@ from backend.apps.workflows.models import ( from backend.apps.workflows import storage, scheduler, executor, audit, escalation from backend.apps.workflows.cloud.handover import release_before_removing from backend.apps.settings.models import DEFAULT_MODEL +from backend.apps.workflows.default_model import provider_for_model, user_default_model logger = logging.getLogger(__name__) @@ -292,9 +293,9 @@ 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 DEFAULT_MODEL, + model=body.model or user_default_model(), mode=body.mode or "agent", - provider=body.provider or "anthropic", + provider=body.provider or provider_for_model(body.model or user_default_model()), cost_cap_usd_monthly=body.cost_cap_usd_monthly, auto_named=body.auto_named, unsaved=body.unsaved,