Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -1263,7 +1263,7 @@
"filename": "src/backend/tests/unit/agentic/services/test_assistant_service_run_intent.py",
"hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd",
"is_verified": false,
"line_number": 369,
"line_number": 386,
"is_secret": false
}
],
Expand Down Expand Up @@ -1337,7 +1337,7 @@
"filename": "src/backend/tests/unit/agentic/services/test_provider_service.py",
"hashed_secret": "c92b9809dacd9240dc85e86da6388e9b1bfc8a7d",
"is_verified": false,
"line_number": 118,
"line_number": 126,
"is_secret": false
}
],
Expand Down
24 changes: 13 additions & 11 deletions src/backend/base/langflow/agentic/flows/flow_builder_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,13 +323,16 @@
running — otherwise the run fails with "No model selected".

Pick the model in this STRICT priority order:
1. **The model the user EXPLICITLY named WINS — always.** If the user asked
1. **The model the user EXPLICITLY named WINS unless a runtime
`[Model provider policy ...]` notice says its provider is unavailable.**
If the user asked
for a specific model ("use GPT-5.4", "use the OpenAI 5.4 model", "switch to
claude-sonnet-4-5", "troque para gemini-2.5-pro"), use THAT model — never
substitute a different version or the `preferred` model for it, even if the
requested model is not in the `[Available language models ...]` block and
even if a "preferred" model is offered. (e.g. user said "5.4" / "gpt-5.4"
and preferred is "gpt-5.5" → you MUST set the 5.4 model, NOT gpt-5.5.)
exact requested model is not listed and even if a "preferred" model is
offered. (e.g. user said "5.4" / "gpt-5.4" and preferred is "gpt-5.5"
→ you MUST set the 5.4 model, NOT gpt-5.5.) If a policy notice omits or
rejects the provider, do not discover, configure, or run it.
BUT set the **canonical model id** — the EXACT id as it appears in the
provider catalog / `describe_component` / the `[Available language models]`
block, NOT the user's loose wording. Provider model ids are CASE-SENSITIVE
Expand All @@ -343,19 +346,18 @@
`preferred`; if there is none, pick ANY provider from "providers with
credentials configured" (provider-agnostic — do NOT assume OpenAI; use
whatever the user actually has keys for, e.g. Anthropic, Google, Groq).
3. Only if no such block is present at all may you fall back to
`provider="OpenAI", name="gpt-4o-mini"`.
3. Only if neither an Available-language-model block nor a restrictive
Model-provider-policy notice is present may you preserve the historical
fallback `provider="OpenAI", name="gpt-4o-mini"`.

`configure_component(component_id="Agent-...", params='{"model": [{"provider": "<provider>", "name": "<name>"}]}')`.
Never run a flow whose Agent has no model. NEVER claim in your reply that you
used a model different from the one you actually set on the canvas — report the
EXACT model you configured.

Common providers and example model names:
- `OpenAI` — `gpt-4o`, `gpt-4o-mini`, `gpt-5`, `o1-mini`
- `Anthropic` — `claude-sonnet-4-5-20250929`, `claude-haiku-4-5`
- `Google Generative AI` — `gemini-2.5-flash`, `gemini-2.5-pro`
- `Groq`, `Azure OpenAI`, `Ollama`, `IBM WatsonX`
Provider and model names are deployment-specific. Treat the runtime
`[Available language models ...]` block as the authority; never infer that a
provider is available from these instructions or from an example below.

Add a SEPARATE model component (OpenAIModel etc.) only when the user
EXPLICITLY says "add an OpenAIModel component" / "create a model node" — never
Expand Down
30 changes: 25 additions & 5 deletions src/backend/base/langflow/agentic/services/agent_run_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,40 @@ class AgentRunModel(TypedDict):
provider: str | None
model_name: str | None
api_key_var: str | None
allow_configuration: bool


class RequestedAgentModel(TypedDict):
provider: str
model_name: str
api_key_var: str | None


_model_var: contextvars.ContextVar[AgentRunModel | None] = contextvars.ContextVar("agentic_run_model", default=None)

_requested_model_var: contextvars.ContextVar[AgentRunModel | None] = contextvars.ContextVar(
_requested_model_var: contextvars.ContextVar[RequestedAgentModel | None] = contextvars.ContextVar(
"agentic_requested_model", default=None
)

_iterations_var: contextvars.ContextVar[int | None] = contextvars.ContextVar("agentic_run_iterations", default=None)


def set_agent_run_model(provider: str | None, model_name: str | None, api_key_var: str | None) -> None:
"""Bind the request's provider/model/api-key to the current context."""
_model_var.set({"provider": provider, "model_name": model_name, "api_key_var": api_key_var})
def set_agent_run_model(
provider: str | None,
model_name: str | None,
api_key_var: str | None,
*,
allow_configuration: bool = True,
) -> None:
"""Bind the request model and whether it may be injected into the canvas."""
_model_var.set(
{
"provider": provider,
"model_name": model_name,
"api_key_var": api_key_var,
"allow_configuration": allow_configuration,
}
)


def current_agent_run_model() -> AgentRunModel | None:
Expand Down Expand Up @@ -83,7 +103,7 @@ def set_requested_agent_model(provider: str | None, model_name: str | None, api_
_requested_model_var.set(None)


def current_requested_agent_model() -> AgentRunModel | None:
def current_requested_agent_model() -> RequestedAgentModel | None:
"""Return the user's explicitly-requested model (or ``None`` when unset)."""
return _requested_model_var.get()

Expand Down
80 changes: 69 additions & 11 deletions src/backend/base/langflow/agentic/services/assistant_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from fastapi import HTTPException
from lfx.base.models.model_remediation import cached_overrides, find_remediation, remember, restore_overrides
from lfx.base.models.provider_registry import get_registry_snapshot
from lfx.graph.flow_builder.flow import flow_to_spec_summary
from lfx.log.logger import logger
from lfx.mcp.flow_builder_tools import (
Expand All @@ -23,6 +24,7 @@
set_propose_existing_edits,
)
from lfx.mcp.tool_cache import reset_tool_cache
from lfx.services.model_provider_policy import ModelProviderPolicyPurpose, aresolve_model_provider_policy

from langflow.agentic.helpers.code_extraction import extract_component_code, extract_flow_json
from langflow.agentic.helpers.code_security import scan_code_security
Expand Down Expand Up @@ -718,29 +720,79 @@ def _complete(data: dict) -> str:
f"{current_input}"
)

# Tell the agent which language model(s) it can safely put on any Agent
# it builds — building an Agent without a model makes the run fail with
# "No model selected". The PREFERRED one is the model the assistant
# Tell the agent which language model(s) it can safely put on any Agent it
# builds — building an Agent without a model makes the run fail with
# "No model selected".
# Resolve one CONFIGURE snapshot before exposing names or binding the
# classifier's requested model; this keeps the assistant from reintroducing
# a provider hidden by governance through prompt context or deterministic
# run-model injection.
# The PREFERRED one is the model the assistant
# itself runs with (key guaranteed). We also list every provider whose
# API key is configured (provider-agnostic, detected from the env-built
# global variables — NO OpenAI bias). Omitted (input byte-identical to
# before) only when neither is available.
from langflow.agentic.services.flow_preparation import available_model_providers

_available_provider_names = available_model_providers(global_variables)
_registered_provider_names = sorted(
descriptor.name for descriptor in get_registry_snapshot().descriptors_by_id.values()
)
_policy_candidates = list(
dict.fromkeys(
[
*_registered_provider_names,
*_available_provider_names,
*([provider] if provider else []),
*([intent_result.requested_provider] if intent_result.requested_provider else []),
]
)
)
try:
_provider_policy = await aresolve_model_provider_policy(
user_id=user_id,
providers=_policy_candidates,
purpose=ModelProviderPolicyPurpose.CONFIGURE,
)
except BaseException:
# The current canvas was already seeded above, but the main request
# try/finally has not started yet. A fail-closed policy error or
# cancellation must not leak that ContextVar state to the next request.
reset_working_flow()
raise
_requested_provider = intent_result.requested_provider
_requested_provider_allowed = not _requested_provider or _provider_policy.allows(_requested_provider)
_allowed_configuration_providers = _provider_policy.filter(_registered_provider_names)
_catalog_is_restricted = any(not _provider_policy.allows(name) for name in _policy_candidates)

_model_parts: list[str] = []
if provider and model_name:
if provider and model_name and _provider_policy.allows(provider):
_model_parts.append(f"preferred: provider={provider!r}, name={model_name!r}")
_avail = available_model_providers(global_variables)
_avail = _provider_policy.filter(_available_provider_names)
if _avail:
_model_parts.append("providers with credentials configured: " + ", ".join(_avail))
if _model_parts:
current_input = (
f"[Available language models — these are a DEFAULT only. If the user explicitly named a "
f"model, set EXACTLY that model (verbatim) and IGNORE this block. ONLY when the user did "
f"NOT name a model, configure an Agent's `model` field with the one marked `preferred` "
f"(else any listed provider) so the flow can run: "
f"model and no Model provider policy notice rejects it, set EXACTLY that model and IGNORE "
f"this block. ONLY when the user did NOT name a model, configure an Agent's `model` field "
f"with the one marked `preferred` (else any listed provider) so the flow can run: "
f"{'; '.join(_model_parts)}]\n\n{current_input}"
)
if _catalog_is_restricted or not _requested_provider_allowed:
allowed_notice = (
"Configure only these providers: " + ", ".join(_allowed_configuration_providers) + ". "
if _allowed_configuration_providers
else "No model providers are available for configuration. "
)
requested_notice = (
"The explicitly requested provider is unavailable. " if not _requested_provider_allowed else ""
)
current_input = (
f"[Model provider policy: {requested_notice}{allowed_notice}"
"Do not discover, configure, or run any other provider.]\n\n"
f"{current_input}"
)

# Headless callers (MCP) have no review UI, so steer the agent away from a
# "proposed/pending approval" narration the user can never act on (#13641).
Expand Down Expand Up @@ -829,16 +881,22 @@ async def check_cancelled() -> bool:
set_current_user_id(user_id)
# The generate_component tool re-runs the component-gen LLM flow
# mid-loop and needs the same provider/model the request used.
set_agent_run_model(provider, model_name, api_key_var)
set_agent_run_model(
provider,
model_name,
api_key_var,
allow_configuration=bool(provider and _provider_policy.allows(provider)),
)
set_agent_run_iterations(_iterations_from_globals(global_variables))
# If the user EXPLICITLY named a model (e.g. "use the OpenAI gpt-5.4
# model"), bind it so the run-time injector ENFORCES it on the Agent —
# the canvas must show exactly what the user asked for, never the
# assistant's own runtime model. Same-provider runs reuse the verified
# api_key_var; a different provider falls back to its default var.
_req_provider = intent_result.requested_provider
_req_provider = intent_result.requested_provider if _requested_provider_allowed else None
_req_api_key_var = api_key_var if (_req_provider and provider and _req_provider == provider) else None
set_requested_agent_model(_req_provider, intent_result.requested_model, _req_api_key_var)
_req_model = intent_result.requested_model if _req_provider else None
set_requested_agent_model(_req_provider, _req_model, _req_api_key_var)

# max_retries=0 means 1 attempt (no retries), matching non-streaming semantics
total_attempts = max_retries + 1
Expand Down
12 changes: 8 additions & 4 deletions src/backend/base/langflow/agentic/services/provider_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@

from lfx.base.models.model_metadata import CONDITIONAL_LIVE_MODEL_PROVIDERS, LIVE_MODEL_PROVIDERS
from lfx.base.models.model_utils import get_live_models_for_provider
from lfx.base.models.provider_registry import is_api_key_optional
from lfx.base.models.provider_registry import get_registry_snapshot, is_api_key_optional
from lfx.base.models.unified_models import (
get_model_provider_variable_mapping,
get_model_providers,
get_provider_required_variable_keys,
get_unified_models_detailed,
)
Expand Down Expand Up @@ -45,6 +44,11 @@
"""


def _get_registered_provider_names() -> list[str]:
"""Return provider names without executing extension catalog loaders."""
return sorted(descriptor.name for descriptor in get_registry_snapshot().descriptors_by_id.values())


async def get_enabled_providers_for_user(
user_id: UUID | str,
session: AsyncSession,
Expand All @@ -64,7 +68,7 @@ async def get_enabled_providers_for_user(
all_variable_names = {var.name for var in all_variables}

provider_variable_map = get_model_provider_variable_mapping()
registered_providers = get_model_providers()
registered_providers = _get_registered_provider_names()
provider_candidates = [
*provider_variable_map,
*(
Expand All @@ -76,7 +80,7 @@ async def get_enabled_providers_for_user(
provider_policy = resolve_model_provider_policy(
user_id=user_id,
providers=[*registered_providers, *provider_candidates],
purpose=ModelProviderPolicyPurpose.USE,
purpose=ModelProviderPolicyPurpose.CONFIGURE,
)

enabled_providers = []
Expand Down
Loading
Loading