Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/backend/base/langflow/api/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
parse_value,
raise_error_if_astra_cloud_env,
remove_api_keys,
replace_api_key_with_env_var_name,
validate_is_component,
verify_public_flow_and_get_user,
)
Expand Down Expand Up @@ -75,6 +76,7 @@
"parse_value",
"raise_error_if_astra_cloud_env",
"remove_api_keys",
"replace_api_key_with_env_var_name",
"validate_is_component",
"verify_public_flow_and_get_user",
]
54 changes: 52 additions & 2 deletions src/backend/base/langflow/api/utils/core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
import uuid
from ast import literal_eval
from datetime import timedelta
Expand Down Expand Up @@ -28,6 +29,8 @@
from langflow.services.chat.service import ChatService
from langflow.services.store.schema import StoreComponentCreate

_VARIABLE_NAME_PATTERN = re.compile(r"[A-Za-z][A-Za-z0-9_]*")


API_WORDS = ["api", "key", "token"]

Expand Down Expand Up @@ -68,14 +71,61 @@
return "api" in word and ("key" in word or ("token" in word and "tokens" not in word))


def _get_provider_from_template(template: dict) -> str | None:
"""Return provider name from template's model field, if any."""
model_field = template.get("model")
if not isinstance(model_field, dict):
return None
raw = model_field.get("value")
if isinstance(raw, list) and len(raw) > 0 and isinstance(raw[0], dict):
return raw[0].get("provider")
return None


def _looks_like_variable_name(value: Any) -> bool:
"""Return True if value looks like a variable name."""
if not isinstance(value, str):
return False
stripped = value.strip()
if not stripped:
return False
return _VARIABLE_NAME_PATTERN.fullmatch(stripped) is not None


def replace_api_key_with_env_var_name(flow: dict) -> dict:
"""Normalize api_key to a variable name when possible, never export raw keys."""
nodes = flow.get("data", {}).get("nodes", [])
for node in nodes:
node_data = node.get("data")
if not isinstance(node_data, dict):
continue
node_inner = node_data.get("node")
if not isinstance(node_inner, dict):
continue
template = node_inner.get("template")
if not isinstance(template, dict):
continue
for value in template.values():
if isinstance(value, dict) and value.get("name") == "api_key" and value.get("password"):
current = value.get("value")
if _looks_like_variable_name(current):
break # keep user's custom variable name
# raw secret or other string: clear it
value["value"] = None
break
return flow


def remove_api_keys(flow: dict):
"""Remove api keys from flow data."""
"""Clear secret values from flow data."""
flow = replace_api_key_with_env_var_name(flow)
for node in flow.get("data", {}).get("nodes", []):
node_data = node.get("data").get("node")
template = node_data.get("template")
for value in template.values():
if isinstance(value, dict) and "name" in value and has_api_terms(value["name"]) and value.get("password"):
value["value"] = None
if value.get("name") != "api_key":

Check failure on line 127 in src/backend/base/langflow/api/utils/core.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (SIM102)

src/backend/base/langflow/api/utils/core.py:126:13: SIM102 Use a single `if` statement instead of nested `if` statements
value["value"] = None

return flow

Expand Down
13 changes: 12 additions & 1 deletion src/backend/base/langflow/api/v1/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@
from sqlmodel import and_, col, select
from sqlmodel.ext.asyncio.session import AsyncSession

from langflow.api.utils import CurrentActiveUser, DbSession, cascade_delete_flow, remove_api_keys, validate_is_component
from langflow.api.utils import (
CurrentActiveUser,
DbSession,
cascade_delete_flow,
remove_api_keys,
replace_api_key_with_env_var_name,
validate_is_component,
)
from langflow.api.v1.schemas import FlowListCreate
from langflow.helpers.user import get_user_by_flow_id_or_endpoint_name
from langflow.initial_setup.constants import STARTER_FOLDER_NAME
Expand Down Expand Up @@ -480,6 +487,8 @@ async def update_flow(
if flow.endpoint_name is None or flow.endpoint_name == "":
update_data["endpoint_name"] = None

# Never persist raw api_key; use env var name so import resolves from env
update_data = replace_api_key_with_env_var_name(update_data)
if settings_service.settings.remove_api_keys:
update_data = remove_api_keys(update_data)

Expand Down Expand Up @@ -672,6 +681,8 @@ async def _update_existing_flow(
if "folder_id" not in update_data or update_data.get("folder_id") is None:
update_data.pop("folder_id", None)

# Never persist raw api_key; use env var name so import resolves from env
update_data = replace_api_key_with_env_var_name(update_data)
if settings_service.settings.remove_api_keys:
update_data = remove_api_keys(update_data)

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\n\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Language Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\n real_time_refresh=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"watsonx Project ID\",\n info=\"The project ID associated with the foundation model (IBM watsonx.ai only)\",\n show=False,\n required=False,\n ),\n StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n"
"value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_model_providers,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\n\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Language Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\n real_time_refresh=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"watsonx Project ID\",\n info=\"The project ID associated with the foundation model (IBM watsonx.ai only)\",\n show=False,\n required=False,\n ),\n StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\") or \"\"\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n else:\n # No provider selected yet (e.g. new node): pre-fill api_key from first provider that has env configured\n for p in get_model_providers():\n build_config = apply_provider_variable_config_to_build_config(build_config, p)\n if build_config.get(\"api_key\", {}).get(\"value\"):\n break\n\n return build_config\n"
},
"input_value": {
"_input_type": "MessageInput",
Expand Down
Loading
Loading