Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c7cd237
Add full provider variable metadata and multi-variable support
edwinjosechittilappilly Jan 26, 2026
72ebfe9
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
da39a1b
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Jan 26, 2026
e7c46a4
Merge branch 'main' into EJ/add-multi-api-support
edwinjosechittilappilly Jan 26, 2026
c4ca6a3
Refactor provider variable saving to batch mode
edwinjosechittilappilly Jan 26, 2026
ee21c9c
Merge branch 'EJ/add-multi-api-support' of https://github.com/langflo…
edwinjosechittilappilly Jan 26, 2026
5875b9d
use decrypt only for is secret
edwinjosechittilappilly Jan 26, 2026
75915a4
Update unified_models.py
edwinjosechittilappilly Jan 26, 2026
dbd3d20
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
77a8a5b
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Jan 26, 2026
f8363e8
Refactor model provider metadata and validation logic
edwinjosechittilappilly Jan 26, 2026
fd51e27
Merge branch 'EJ/add-multi-api-support' of https://github.com/langflo…
edwinjosechittilappilly Jan 26, 2026
8b64cb2
Refactor provider param mappings for language models
edwinjosechittilappilly Jan 26, 2026
f642379
Update providerConstants.ts
edwinjosechittilappilly Jan 26, 2026
d4860a5
Refactor provider config check to include all required variables
edwinjosechittilappilly Jan 26, 2026
29fe393
Fixed overflow
lucaseduoli Jan 26, 2026
13251f9
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
0fafd24
Fix provider enablement to check all required variables
edwinjosechittilappilly Jan 26, 2026
04b7e2e
Refactor language model config for provider flexibility
edwinjosechittilappilly Jan 26, 2026
1fc5afd
Merge branch 'main' into EJ/add-multi-api-support
edwinjosechittilappilly Jan 26, 2026
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
17 changes: 15 additions & 2 deletions src/backend/base/langflow/api/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from fastapi import APIRouter, Depends, HTTPException, Query
from lfx.base.models.unified_models import (
get_model_provider_metadata,
get_model_provider_variable_mapping,
get_model_providers,
get_unified_models_detailed,
Expand Down Expand Up @@ -186,8 +187,20 @@ def sort_key(provider_dict):


@router.get("/provider-variable-mapping", status_code=200)
async def get_model_provider_mapping() -> dict[str, str]:
return get_model_provider_variable_mapping()
async def get_model_provider_mapping() -> dict[str, list[dict]]:
"""Return provider variables mapping with full variable info.

Each provider maps to a list of variable objects containing:
- variable_name: Display name shown to user
- variable_key: Environment variable key
- description: Help text for the variable
- required: Whether the variable is required
- is_secret: Whether to treat as credential
- is_list: Whether it accepts multiple values
- options: Predefined options for dropdowns
"""
metadata = get_model_provider_metadata()
return {provider: meta.get("variables", []) for provider, meta in metadata.items()}
Comment on lines +203 to +204
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint changes its response shape from dict[str, str] (provider → variable key) to dict[str, list[dict]] (provider → variable metadata list). Since the path stayed the same, existing clients expecting the prior schema will break. If backward compatibility is required, consider either (a) introducing a new endpoint (e.g., /provider-variables), (b) adding a query param (e.g., ?format=full|legacy), or (c) returning both shapes (e.g., { mapping: ..., variables: ... }) during a deprecation window.

Copilot uses AI. Check for mistakes.


Comment on lines +191 to 206
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint changes its response shape from dict[str, str] (provider → variable key) to dict[str, list[dict]] (provider → variable metadata list). Since the path stayed the same, existing clients expecting the prior schema will break. If backward compatibility is required, consider either (a) introducing a new endpoint (e.g., /provider-variables), (b) adding a query param (e.g., ?format=full|legacy), or (c) returning both shapes (e.g., { mapping: ..., variables: ... }) during a deprecation window.

Suggested change
async def get_model_provider_mapping() -> dict[str, list[dict]]:
"""Return provider variables mapping with full variable info.
Each provider maps to a list of variable objects containing:
- variable_name: Display name shown to user
- variable_key: Environment variable key
- description: Help text for the variable
- required: Whether the variable is required
- is_secret: Whether to treat as credential
- is_list: Whether it accepts multiple values
- options: Predefined options for dropdowns
"""
metadata = get_model_provider_metadata()
return {provider: meta.get("variables", []) for provider, meta in metadata.items()}
async def get_model_provider_mapping(
format: Annotated[
str,
Query(
description="Response format: 'legacy' for simple mapping, 'full' for variable metadata.",
alias="format",
),
] = "legacy",
) -> dict:
"""Return provider variables mapping.
Backwards compatible behavior:
- By default (format=legacy), returns a simple mapping of provider -> variable key,
matching the original endpoint behavior:
{
"openai": "OPENAI_API_KEY",
"anthropic": "ANTHROPIC_API_KEY",
...
}
- When format=full, returns provider -> list of variable metadata objects:
{
"openai": [
{
"variable_name": "...",
"variable_key": "...",
"description": "...",
"required": true,
"is_secret": true,
"is_list": false,
"options": [...]
},
...
],
...
}
"""
if format == "full":
metadata = get_model_provider_metadata()
return {provider: meta.get("variables", []) for provider, meta in metadata.items()}
# Default: legacy simple mapping (provider -> variable key)
return get_model_provider_variable_mapping()

Copilot uses AI. Check for mistakes.
@router.get("/enabled_providers", status_code=200)
Expand Down
65 changes: 44 additions & 21 deletions src/backend/base/langflow/services/variable/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,25 @@

# Import the provider mapping to set default_fields for known providers
try:
from lfx.base.models.unified_models import get_model_provider_variable_mapping
from lfx.base.models.unified_models import (
get_model_provider_metadata,
get_model_provider_variable_mapping,
)

provider_mapping = get_model_provider_variable_mapping()

Check failure on line 40 in src/backend/base/langflow/services/variable/service.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (F841)

src/backend/base/langflow/services/variable/service.py:40:13: F841 Local variable `provider_mapping` is assigned to but never used
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove unused variable provider_mapping — causes pipeline failure.

The variable provider_mapping is assigned but never used. This is flagged by Ruff (F841) and is causing the CI pipeline to fail.

🐛 Proposed fix
-            provider_mapping = get_model_provider_variable_mapping()

If get_model_provider_variable_mapping was intended to be used somewhere, that usage is missing. Otherwise, remove the unused import as well if it's no longer needed elsewhere in the file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
provider_mapping = get_model_provider_variable_mapping()
🧰 Tools
🪛 GitHub Actions: Ruff Style Check

[error] 40-40: Ruff/Flake8: F841 Local variable provider_mapping is assigned to but never used.

🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 40-40: Ruff (F841)
src/backend/base/langflow/services/variable/service.py:40:13: F841 Local variable provider_mapping is assigned to but never used

🤖 Prompt for AI Agents
In `@src/backend/base/langflow/services/variable/service.py` at line 40, The local
variable provider_mapping is assigned from get_model_provider_variable_mapping
but never used; remove the unused assignment (provider_mapping =
get_model_provider_variable_mapping()) and, if
get_model_provider_variable_mapping is no longer referenced anywhere in this
module, also remove its import to eliminate the Ruff F841 unused-variable error;
if the mapping was intended to be used, instead add the missing usage where
relevant (e.g., pass it into the function or return it) in the surrounding
service functions.

# Reverse the mapping to go from variable name to provider
var_to_provider = {var_name: provider for provider, var_name in provider_mapping.items()}
# Build var_to_provider from all variables in metadata (not just primary)
var_to_provider = {}
var_to_info = {} # Maps variable_key to its full info (including is_secret)
metadata = get_model_provider_metadata()
for provider, meta in metadata.items():
for var in meta.get("variables", []):
var_key = var.get("variable_key")
if var_key:
var_to_provider[var_key] = provider
var_to_info[var_key] = var
except Exception: # noqa: BLE001
var_to_provider = {}
var_to_info = {}
Comment on lines +35 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'get_model_provider_variable_mapping' src/backend/base/langflow/services/variable/service.py

Repository: langflow-ai/langflow

Length of output: 191


Remove the unused import and variable assignment.

The get_model_provider_variable_mapping import and the provider_mapping = get_model_provider_variable_mapping() assignment (line 40) should both be removed, as provider_mapping is never used after assignment.

🧰 Tools
🪛 GitHub Actions: Ruff Style Check

[error] 40-40: Ruff/Flake8: F841 Local variable provider_mapping is assigned to but never used.

🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 40-40: Ruff (F841)
src/backend/base/langflow/services/variable/service.py:40:13: F841 Local variable provider_mapping is assigned to but never used

🤖 Prompt for AI Agents
In `@src/backend/base/langflow/services/variable/service.py` around lines 35 - 53,
The code imports get_model_provider_variable_mapping and assigns
provider_mapping = get_model_provider_variable_mapping() but never uses it;
remove the unused import and the provider_mapping assignment to avoid dead code.
Update the import list to drop get_model_provider_variable_mapping and delete
the line that assigns provider_mapping (search for provider_mapping and
get_model_provider_variable_mapping in this file, e.g., in service.py) so only
metadata-related calls (get_model_provider_metadata) and the
var_to_provider/var_to_info logic remain.


Comment on lines +35 to 54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused provider_mapping to clear Ruff F841.
The local assignment isn’t used and is currently failing the Ruff check. Remove it (and the now-unused import).

🧹 Proposed fix
-            from lfx.base.models.unified_models import (
-                get_model_provider_metadata,
-                get_model_provider_variable_mapping,
-            )
+            from lfx.base.models.unified_models import get_model_provider_metadata
...
-            provider_mapping = get_model_provider_variable_mapping()
🧰 Tools
🪛 GitHub Actions: Ruff Style Check

[error] 40-40: F841 Local variable provider_mapping is assigned to but never used. (From ruff: src/backend/base/langflow/services/variable/service.py:40)

🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 40-40: Ruff (F841)
src/backend/base/langflow/services/variable/service.py:40:13: F841 Local variable provider_mapping is assigned to but never used

🤖 Prompt for AI Agents
In `@src/backend/base/langflow/services/variable/service.py` around lines 35 - 54,
Remove the unused provider_mapping local variable and its import to satisfy Ruff
F841: delete the assignment to provider_mapping and remove
get_model_provider_variable_mapping from the import list (leaving
get_model_provider_metadata), keeping the rest of the logic that builds
var_to_provider and var_to_info intact so functions like
get_model_provider_metadata, var_to_provider, and var_to_info remain unchanged.

for var_name in self.settings_service.settings.variables_to_get_from_environment:
# Check if session is still usable before processing each variable
Expand All @@ -54,8 +66,11 @@

# Skip placeholder/test values like "dummy" for API key variables only
# This prevents test environments from overwriting user-configured model provider keys
is_api_key_variable = var_name in var_to_provider
if is_api_key_variable and value.lower() == "dummy":
is_provider_variable = var_name in var_to_provider
var_info = var_to_info.get(var_name, {})
is_secret_variable = var_info.get("is_secret", False)

if is_provider_variable and is_secret_variable and value.lower() == "dummy":
await logger.adebug(
f"Skipping API key variable {var_name} with placeholder value 'dummy' "
"to preserve user configuration"
Expand All @@ -66,24 +81,32 @@
# Set default_fields if this is a known provider variable
default_fields = []
try:
if is_api_key_variable:
if is_provider_variable:
provider_name = var_to_provider[var_name]
# Validate the API key before setting default_fields
# Get the variable type from metadata
var_display_name = var_info.get("variable_name", "api_key")

# Validate secret variables (API keys) before setting default_fields
# This prevents invalid keys from enabling providers during migration
try:
from lfx.base.models.unified_models import validate_model_provider_key

validate_model_provider_key(var_name, value)
# Only set default_fields if validation passes
default_fields = [provider_name, "api_key"]
await logger.adebug(f"Validated {var_name} - provider will be enabled")
except (ValueError, Exception) as validation_error: # noqa: BLE001
# Validation failed - don't set default_fields
# This prevents the provider from appearing as "Enabled"
default_fields = []
await logger.adebug(
f"Skipping default_fields for {var_name} - validation failed: {validation_error!s}"
)
if is_secret_variable:
try:
from lfx.base.models.unified_models import validate_model_provider_key

validate_model_provider_key(var_name, value)
# Only set default_fields if validation passes
default_fields = [provider_name, var_display_name]
await logger.adebug(f"Validated {var_name} - provider will be enabled")
except (ValueError, Exception) as validation_error: # noqa: BLE001
# Validation failed - don't set default_fields
# This prevents the provider from appearing as "Enabled"
default_fields = []
await logger.adebug(
f"Skipping default_fields for {var_name} - validation failed: {validation_error!s}"
)
else:
# Non-secret variables (like project_id, url) don't need validation
default_fields = [provider_name, var_display_name]
await logger.adebug(f"Set default_fields for non-secret variable {var_name}")
existing = (await session.exec(query)).first()
except Exception as e: # noqa: BLE001
await logger.aexception(f"Error querying {var_name} variable: {e!s}")
Expand Down
94 changes: 94 additions & 0 deletions src/backend/tests/unit/api/v1/test_models_enabled_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,97 @@
if variable.get("type") == CREDENTIAL_TYPE:
# CRITICAL: Value must be None (redacted), never the original value
assert variable["value"] is None


@pytest.mark.usefixtures("active_user")
async def test_provider_variable_mapping_returns_full_variable_info(client: AsyncClient, logged_in_headers):
"""Test that provider-variable-mapping endpoint returns full variable info for each provider."""
response = await client.get("api/v1/models/provider-variable-mapping", headers=logged_in_headers)
result = response.json()

assert response.status_code == status.HTTP_200_OK
assert isinstance(result, dict)

# Check that known providers exist
assert "OpenAI" in result
assert "Anthropic" in result
assert "Google Generative AI" in result
assert "Ollama" in result
assert "IBM WatsonX" in result

# Check structure of variables for OpenAI (single variable provider)
openai_vars = result["OpenAI"]
assert isinstance(openai_vars, list)
assert len(openai_vars) >= 1

# Check each variable has required fields
for var in openai_vars:
assert "variable_name" in var
assert "variable_key" in var
assert "description" in var
assert "required" in var
assert "is_secret" in var
assert "is_list" in var
assert "options" in var

# Check OpenAI primary variable
openai_api_key_var = openai_vars[0]
assert openai_api_key_var["variable_key"] == "OPENAI_API_KEY"
Comment on lines +436 to +438
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test assumes the OpenAI API key variable is always at index 0. If the provider metadata list order changes, the test will fail even if the endpoint remains correct. Prefer locating the variable by variable_key (similar to the WatsonX assertions) to make the test order-independent.

Suggested change
# Check OpenAI primary variable
openai_api_key_var = openai_vars[0]
assert openai_api_key_var["variable_key"] == "OPENAI_API_KEY"
# Check OpenAI primary variable (order-independent)
openai_api_key_var = next((v for v in openai_vars if v["variable_key"] == "OPENAI_API_KEY"), None)
assert openai_api_key_var is not None

Copilot uses AI. Check for mistakes.
assert openai_api_key_var["required"] is True
assert openai_api_key_var["is_secret"] is True


@pytest.mark.usefixtures("active_user")
async def test_provider_variable_mapping_multi_variable_provider(client: AsyncClient, logged_in_headers):
"""Test that IBM WatsonX returns multiple required variables."""
response = await client.get("api/v1/models/provider-variable-mapping", headers=logged_in_headers)
result = response.json()

assert response.status_code == status.HTTP_200_OK

# Check IBM WatsonX has multiple variables
watsonx_vars = result.get("IBM WatsonX", [])
assert len(watsonx_vars) >= 3 # API Key, Project ID, URL

# Find each variable
var_keys = {v["variable_key"] for v in watsonx_vars}
assert "WATSONX_APIKEY" in var_keys
assert "WATSONX_PROJECT_ID" in var_keys
assert "WATSONX_URL" in var_keys

# Check API Key is secret
api_key_var = next((v for v in watsonx_vars if v["variable_key"] == "WATSONX_APIKEY"), None)
assert api_key_var is not None
assert api_key_var["is_secret"] is True
assert api_key_var["required"] is True

# Check Project ID is not secret
project_id_var = next((v for v in watsonx_vars if v["variable_key"] == "WATSONX_PROJECT_ID"), None)
assert project_id_var is not None
assert project_id_var["is_secret"] is False
assert project_id_var["required"] is True

# Check URL has options
url_var = next((v for v in watsonx_vars if v["variable_key"] == "WATSONX_URL"), None)
assert url_var is not None
assert url_var["is_secret"] is False
assert url_var["required"] is True
assert len(url_var["options"]) > 0 # Should have regional endpoint options
assert "https://us-south.ml.cloud.ibm.com" in url_var["options"]


@pytest.mark.usefixtures("active_user")
async def test_backward_compatible_variable_mapping(client: AsyncClient, logged_in_headers):

Check failure on line 483 in src/backend/tests/unit/api/v1/test_models_enabled_providers.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (ARG001)

src/backend/tests/unit/api/v1/test_models_enabled_providers.py:483:74: ARG001 Unused function argument: `logged_in_headers`

Check failure on line 483 in src/backend/tests/unit/api/v1/test_models_enabled_providers.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (ARG001)

src/backend/tests/unit/api/v1/test_models_enabled_providers.py:483:53: ARG001 Unused function argument: `client`
"""Test that get_model_provider_variable_mapping() still returns primary variable (backward compat)."""
from lfx.base.models.unified_models import get_model_provider_variable_mapping

mapping = get_model_provider_variable_mapping()

# Should return dict of provider -> primary variable key
assert isinstance(mapping, dict)
assert mapping.get("OpenAI") == "OPENAI_API_KEY"
assert mapping.get("Anthropic") == "ANTHROPIC_API_KEY"
assert mapping.get("Google Generative AI") == "GOOGLE_API_KEY"
assert mapping.get("Ollama") == "OLLAMA_BASE_URL"
# IBM WatsonX should return primary secret (API key)
assert mapping.get("IBM WatsonX") == "WATSONX_APIKEY"
Comment on lines +482 to +496
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused fixture args to satisfy linting.
client and logged_in_headers are unused here and trigger Ruff (ARG001).

🛠️ Proposed fix
-async def test_backward_compatible_variable_mapping(client: AsyncClient, logged_in_headers):
+async def test_backward_compatible_variable_mapping():
🧰 Tools
🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 483-483: Ruff (ARG001)
src/backend/tests/unit/api/v1/test_models_enabled_providers.py:483:74: ARG001 Unused function argument: logged_in_headers


[failure] 483-483: Ruff (ARG001)
src/backend/tests/unit/api/v1/test_models_enabled_providers.py:483:53: ARG001 Unused function argument: client

🤖 Prompt for AI Agents
In `@src/backend/tests/unit/api/v1/test_models_enabled_providers.py` around lines
482 - 496, The test function test_backward_compatible_variable_mapping has
unused fixture parameters client and logged_in_headers which trigger Ruff
ARG001; remove these unused args from the function signature so it only relies
on the `@pytest.mark.usefixtures`("active_user") decorator (i.e., change def
test_backward_compatible_variable_mapping(client: AsyncClient,
logged_in_headers): to a signature with no parameters), leaving the body and the
call to get_model_provider_variable_mapping() unchanged.

Comment on lines +483 to +496
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused fixtures to satisfy Ruff (ARG001).

client and logged_in_headers aren’t used in this test; drop them to avoid lint failures.

✅ Suggested fix
-async def test_backward_compatible_variable_mapping(client: AsyncClient, logged_in_headers):
+async def test_backward_compatible_variable_mapping():
🧰 Tools
🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 483-483: Ruff (ARG001)
src/backend/tests/unit/api/v1/test_models_enabled_providers.py:483:74: ARG001 Unused function argument: logged_in_headers


[failure] 483-483: Ruff (ARG001)
src/backend/tests/unit/api/v1/test_models_enabled_providers.py:483:53: ARG001 Unused function argument: client

🤖 Prompt for AI Agents
In `@src/backend/tests/unit/api/v1/test_models_enabled_providers.py` around lines
483 - 496, The test function test_backward_compatible_variable_mapping currently
accepts unused fixtures client and logged_in_headers causing Ruff ARG001; edit
the function signature for test_backward_compatible_variable_mapping to remove
the unused parameters so it has no arguments, leaving the body intact and still
importing/using get_model_provider_variable_mapping to perform the assertions.

Loading
Loading