-
Notifications
You must be signed in to change notification settings - Fork 8.4k
feat: Add full provider variable metadata and multi-variable support #11446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
c7cd237
72ebfe9
da39a1b
e7c46a4
c4ca6a3
ee21c9c
5875b9d
75915a4
dbd3d20
77a8a5b
f8363e8
fd51e27
8b64cb2
f642379
d4860a5
29fe393
13251f9
0fafd24
04b7e2e
1fc5afd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
+191
to
206
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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() |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -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
|
||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused variable The variable 🐛 Proposed fix- provider_mapping = get_model_provider_variable_mapping()If 📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Actions: Ruff Style Check[error] 40-40: Ruff/Flake8: F841 Local variable 🪛 GitHub Check: Ruff Style Check (3.13)[failure] 40-40: Ruff (F841) 🤖 Prompt for AI Agents |
||||
| # 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: rg -n 'get_model_provider_variable_mapping' src/backend/base/langflow/services/variable/service.pyRepository: langflow-ai/langflow Length of output: 191 Remove the unused import and variable assignment. The 🧰 Tools🪛 GitHub Actions: Ruff Style Check[error] 40-40: Ruff/Flake8: F841 Local variable 🪛 GitHub Check: Ruff Style Check (3.13)[failure] 40-40: Ruff (F841) 🤖 Prompt for AI Agents |
||||
|
|
||||
|
Comment on lines
+35
to
54
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused 🧹 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 🪛 GitHub Check: Ruff Style Check (3.13)[failure] 40-40: Ruff (F841) 🤖 Prompt for AI Agents |
||||
| for var_name in self.settings_service.settings.variables_to_get_from_environment: | ||||
| # Check if session is still usable before processing each variable | ||||
|
|
@@ -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" | ||||
|
|
@@ -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}") | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
|
||||||||||||||
| # 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 |
Check failure on line 483 in src/backend/tests/unit/api/v1/test_models_enabled_providers.py
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`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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) todict[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.