Skip to content

ref: Refactor Ollama model fetching to use async and filter capabilities - #10551

Closed
edwinjosechittilappilly wants to merge 6 commits into
mainfrom
fix-ollama-models-loading-main-update
Closed

ref: Refactor Ollama model fetching to use async and filter capabilities#10551
edwinjosechittilappilly wants to merge 6 commits into
mainfrom
fix-ollama-models-loading-main-update

Conversation

@edwinjosechittilappilly

@edwinjosechittilappilly edwinjosechittilappilly commented Nov 10, 2025

Copy link
Copy Markdown
Collaborator

Replaces synchronous requests for Ollama model fetching with asynchronous httpx calls and adds filtering to only include models with 'completion' capability. Updates the LanguageModelComponent to support async validation and fetching of Ollama models, improving reliability and accuracy of available model options.

Summary by CodeRabbit

  • New Features

    • Added asynchronous Ollama model discovery and validation for improved responsiveness
    • Improved Ollama endpoint handling with automatic URL normalization
    • Dynamic model population when selecting Ollama as provider
  • Bug Fixes

    • Enhanced error handling and logging for Ollama connections
    • Better capability filtering for available models

Replaces synchronous requests for Ollama model fetching with asynchronous httpx calls and adds filtering to only include models with 'completion' capability. Updates the LanguageModelComponent to support async validation and fetching of Ollama models, improving reliability and accuracy of available model options.
Introduces several Notion-related components for Langflow, including AddContentToPage, NotionDatabaseProperties, NotionListPages, NotionPageContent, NotionPageCreator, NotionPageUpdate, and NotionSearch. Updates the component index to register these new tools, enabling Notion API interactions such as page creation, content retrieval, database property listing, and more.
@coderabbitai

coderabbitai Bot commented Nov 10, 2025

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Converts Ollama model discovery from synchronous to asynchronous across 17 starter projects and core language model components. Introduces async helpers (is_valid_ollama_url, get_ollama_models), makes update_build_config async, replaces httpx for HTTP calls, and adds constants for HTTP/Ollama operations while maintaining existing provider support.

Changes

Cohort / File(s) Summary
Starter Projects - Async Ollama Integration
src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json, Blog Writer.json, Custom Component Generator.json, Document Q&A.json, Financial Report Parser.json, Hybrid Search RAG.json, Image Sentiment Analysis.json, Instagram Copywriter.json, Market Research.json, Meeting Summary.json, Memory Chatbot.json, Portfolio Website Code Generator.json, Research Agent.json, Research Translation Loop.json, SEO Keyword Generator.json, Twitter Thread Generator.json, Vector Store RAG.json, Youtube Analysis.json
Updated LanguageModelComponent with async Ollama support: new async methods is_valid_ollama_url() and get_ollama_models(), converted update_build_config to async, added httpx/asyncio imports, introduced HTTP/Ollama constants, enhanced URL normalization (/v1 stripping), and integrated async model discovery within async build-config flows.
Language Model Component
src/lfx/src/lfx/components/models/language_model.py
Replaced synchronous fetch_ollama_models() with async helpers; added is_valid_ollama_url() and get_ollama_models() for async Ollama endpoint validation and model fetching; converted update_build_config to async and integrated dynamic Ollama model population on provider switch; enhanced error handling, URL normalization, and logging for async workflows; updated constants and visibility logic for Ollama model naming.
Ollama Component
src/lfx/src/lfx/components/ollama/ollama.py
Added warning log when updating build config for model_name, base_url, or tool_model_enabled fields prior to Ollama URL validation; control flow unchanged otherwise.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant UpdateBuildConfig as update_build_config<br/>(async)
    participant ValidateURL as is_valid_ollama_url<br/>(async)
    participant FetchModels as get_ollama_models<br/>(async)
    participant HTTP as httpx<br/>(async HTTP)

    Caller->>UpdateBuildConfig: call with provider=Ollama<br/>or ollama_base_url change
    UpdateBuildConfig->>ValidateURL: await url validation
    ValidateURL->>HTTP: GET /api/tags
    HTTP-->>ValidateURL: HTTP 200
    ValidateURL-->>UpdateBuildConfig: valid=true
    
    rect rgb(200, 220, 240)
    Note over UpdateBuildConfig,HTTP: On valid URL
    UpdateBuildConfig->>FetchModels: await model fetch
    FetchModels->>HTTP: GET /api/tags
    HTTP-->>FetchModels: models list
    FetchModels->>HTTP: GET /api/show (per model)
    HTTP-->>FetchModels: model capabilities
    FetchModels-->>UpdateBuildConfig: filtered models
    end
    
    UpdateBuildConfig->>UpdateBuildConfig: populate model_name options
    UpdateBuildConfig-->>Caller: updated config
    
    Note over UpdateBuildConfig: Error handling:<br/>Invalid URL → ValueError<br/>HTTP error → log + ValueError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

Areas requiring extra attention:

  • Async/await correctness across all 19 files: Verify that all update_build_config call sites properly await the now-async method and that async context propagation is correct.
  • Error handling in async contexts: Examine error handling in get_ollama_models() and is_valid_ollama_url() across all files for consistency (try/except, logging, ValueError raises).
  • URL normalization logic: Review the /v1 suffix stripping, localhost transformation, and trailing-slash handling to ensure no edge cases break Ollama URL parsing or OpenAI-compatible endpoints.
  • HTTP request implementation: Validate httpx usage (timeout, connection pooling, proper async patterns) and confirm no synchronous calls remain in async methods.
  • Model filtering and capability detection: Verify that completion-capability filtering logic correctly identifies and filters Ollama models across the various provider flows.
  • Consistency across starter projects: Although changes are repetitive, spot-check 3–4 diverse starter projects to confirm identical async patterns and constants are applied uniformly.
  • Integration with existing providers: Ensure OpenAI, Anthropic, Google, and IBM watsonx.ai flows remain unchanged and not inadvertently affected by the async refactor.

Possibly related PRs

Suggested labels

enhancement, async, ollama, refactor

Suggested reviewers

  • lucaseduoli

Pre-merge checks and finishing touches

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 3 warnings)
Check name Status Explanation Resolution
Test Coverage For New Implementations ❌ Error PR adds new async Ollama model fetching methods but includes no corresponding test files for the new async functionality. Add test_language_model.py with unit tests for is_valid_ollama_url(), get_ollama_models(), and async update_build_config() covering edge cases and error handling.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Test Quality And Coverage ⚠️ Warning Test suite has significant coverage gaps for async Ollama integration including missing timeout/limit validation, no error path testing, and no backward compatibility tests. Add tests for httpx timeouts/limits, error scenarios (RequestError, raise_for_status, ValueError), backward compatibility for Ollama versions without capabilities, and timeout edge cases.
Test File Naming And Structure ⚠️ Warning PR modifies async code in language_model.py and ollama.py without adding any test files to validate the changes. Add test files following pytest conventions (test_*.py) to cover async methods, httpx configurations, error handling, and model filtering logic.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Refactor Ollama model fetching to use async and filter capabilities' accurately reflects the main changes: converting Ollama model fetching from synchronous to asynchronous operations and adding capability-based filtering across all modified files.
Excessive Mock Usage Warning ✅ Passed The PR contains no test files, only implementation files and JSON configurations, so excessive mock usage cannot be assessed.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Nov 10, 2025

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 15%
14.67% (3955/26947) 7.45% (1533/20552) 9% (532/5906)

Unit Test Results

Tests Skipped Failures Errors Time
1588 0 💤 0 ❌ 0 🔥 20.071s ⏱️

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors Ollama model fetching to use async operations and adds capability filtering to distinguish between completion and embedding models. The changes convert synchronous requests calls to asynchronous httpx calls and implement filtering logic to only return models with 'completion' capability.

Key Changes:

  • Replaced synchronous Ollama model fetching with async implementation using httpx
  • Added filtering to query model capabilities and only include completion models
  • Updated update_build_config method to be async to support new async operations

Reviewed Changes

Copilot reviewed 20 out of 23 changed files in this pull request and generated 3 comments.

File Description
src/lfx/src/lfx/components/ollama/ollama.py Added debug logging for Ollama model fetching
src/lfx/src/lfx/components/models/language_model.py Replaced synchronous model fetching with async, added URL validation and capability filtering
src/backend/base/langflow/initial_setup/starter_projects/*.json Updated embedded code in JSON starter project files to reflect async changes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +117 to +118
if asyncio.iscoroutine(models):
models = await models

Copilot AI Nov 10, 2025

Copy link

Choose a reason for hiding this comment

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

The json() method of httpx Response is not a coroutine and will never return a coroutine. These checks for asyncio.iscoroutine() are unnecessary and should be removed. The httpx response.json() method returns the parsed JSON directly, not a coroutine.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +134
if asyncio.iscoroutine(json_data):
json_data = await json_data

Copilot AI Nov 10, 2025

Copy link

Choose a reason for hiding this comment

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

The json() method of httpx Response is not a coroutine and will never return a coroutine. These checks for asyncio.iscoroutine() are unnecessary and should be removed. The httpx response.json() method returns the parsed JSON directly, not a coroutine.

Copilot uses AI. Check for mistakes.
logger.exception("Error updating Ollama model options.")
elif field_name == "model_name" and field_value.startswith("o1") and self.provider == "OpenAI":
# Use the field_value directly since this is triggered when the field changes
logger.debug(

Copilot AI Nov 10, 2025

Copy link

Choose a reason for hiding this comment

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

This line uses synchronous logger.debug() followed by async logger.adebug() on line 424, logging essentially the same information. The synchronous call should be removed to maintain consistency with the async implementation and avoid duplicate logging.

Suggested change
logger.debug(

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (20)
src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json (2)

2396-2410: Blocking synchronous call in async method.

The update_build_config() method calls self.fetch_ibm_models(), a synchronous static method using requests.get(), from within an async method. This will block the event loop. Consider making fetch_ibm_models async or wrapping the synchronous call to run in a thread pool executor.

Location: src/lfx/src/lfx/components/models/language_model.py:413

  # Fetch IBM models when base_url changes
  try:
-     models = self.fetch_ibm_models(base_url=field_value)
+     models = await asyncio.to_thread(self.fetch_ibm_models, base_url=field_value)

Alternatively, refactor fetch_ibm_models to be async using httpx.AsyncClient.


2076-2100: Remove unnecessary asyncio.iscoroutine checks on response.json() calls.

httpx.Response.json() is synchronous and returns a dict, not a coroutine. The checks if asyncio.iscoroutine(models): and if asyncio.iscoroutine(json_data): will always be False and should be removed.

Fix in:

  • src/lfx/src/lfx/components/ollama/ollama.py
  • src/lfx/src/lfx/components/models/language_model.py

Remove the redundant checks:

  models = tags_response.json()
- if asyncio.iscoroutine(models):
-     models = await models
  await logger.adebug(f"Available models: {models}")
  json_data = show_response.json()
- if asyncio.iscoroutine(json_data):
-     json_data = await json_data
src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json (2)

1471-1505: Provider list mismatch: add “IBM watsonx.ai” and “Ollama” to the template options.

The code supports IBM/Ollama, but the UI template exposes only OpenAI/Anthropic/Google, preventing users from selecting the new providers.

Apply:

- "options": ["OpenAI","Anthropic","Google"],
+ "options": ["OpenAI","Anthropic","Google","IBM watsonx.ai","Ollama"],
- "options_metadata": [{"icon":"OpenAI"},{"icon":"Anthropic"},{"icon":"Google"}],
+ "options_metadata": [{"icon":"OpenAI"},{"icon":"Anthropic"},{"icon":"GoogleGenerativeAI"},{"icon":"WatsonxAI"},{"icon":"Ollama"}],

1082-1105: Default template key {dt} likely incorrect; use {text} or handle missing keys.

The Parser template defaults to Text: {dt} but data/rows commonly expose text. This will KeyError when switching mode to Parser.

- "value": "Text: {dt}"
+ "value": "Text: {text}"

Optionally make DataFrame formatting use format_map with a default dict to avoid KeyErrors. Based on learnings

src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json (1)

1393-1400: Expose “IBM watsonx.ai” and “Ollama” in provider options.

Code supports these providers, but both LMC templates list only OpenAI/Anthropic/Google.

- "options": ["OpenAI","Anthropic","Google"],
+ "options": ["OpenAI","Anthropic","Google","IBM watsonx.ai","Ollama"],
- "options_metadata": [{"icon":"OpenAI"},{"icon":"Anthropic"},{"icon":"Google"}],
+ "options_metadata": [{"icon":"OpenAI"},{"icon":"Anthropic"},{"icon":"GoogleGenerativeAI"},{"icon":"WatsonxAI"},{"icon":"Ollama"}],

Also applies to: 1692-1700

src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json (3)

1463-1634: Hanging risk + brittle capability filter in async Ollama discovery

  • httpx calls lack timeouts; UI can hang if Ollama is slow/unreachable.
  • transform_localhost_url(None) path isn’t guarded in get_ollama_models; urljoin on None can blow up.
  • Ollama /api/show may not return "capabilities"; current logic would drop all models. Add safe fallback and a heuristic to exclude embeds by name/families.
  • Unnecessary asyncio.iscoroutine checks on Response.json().

Proposed patch (Python inside the code string):

@@
-import httpx
+import httpx
@@
-HTTP_STATUS_OK = 200
+HTTP_STATUS_OK = 200
+HTTPX_TIMEOUT = 10.0
@@
-    async def is_valid_ollama_url(self, url: str) -> bool:
+    async def is_valid_ollama_url(self, url: str) -> bool:
@@
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=HTTPX_TIMEOUT, follow_redirects=True) as client:
                 url = transform_localhost_url(url)
                 if not url:
                     return False
@@
-    async def get_ollama_models(self, base_url_value: str) -> list[str]:
+    async def get_ollama_models(self, base_url_value: str) -> list[str]:
@@
-            if not base_url.endswith(\"/\"):
+            if not base_url.endswith(\"/\"):
                 base_url = base_url + \"/\"
-            base_url = transform_localhost_url(base_url)
+            base_url = transform_localhost_url(base_url)
+            if not base_url:
+                raise ValueError(\"Invalid Ollama base URL\")
@@
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=HTTPX_TIMEOUT, follow_redirects=True) as client:
@@
-                models = tags_response.json()
-                if asyncio.iscoroutine(models):
-                    models = await models
+                models = tags_response.json()
                 await logger.adebug(f\"Available models: {models}\")
@@
-                    json_data = show_response.json()
-                    if asyncio.iscoroutine(json_data):
-                        json_data = await json_data
+                    json_data = show_response.json()
@@
-                    capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])
+                    capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])
+                    # Fallbacks: treat missing capabilities as completion-capable unless clearly an embed model
+                    details = json_data.get(\"details\", {}) or {}
+                    families = details.get(\"families\", []) or []
+                    name_lower = (model_name or \"\").lower()
+                    is_embed = (\"embed\" in name_lower) or (\"embedding\" in name_lower) or (\"embed\" in families)
@@
-                    if DESIRED_CAPABILITY in capabilities:
+                    if (DESIRED_CAPABILITY in capabilities) or (not capabilities and not is_embed):
                         model_ids.append(model_name)

Also, avoid passing temperature=None to ChatOpenAI:

@@
-            if model_name in OPENAI_REASONING_MODEL_NAMES:
-                # reasoning models do not support temperature (yet)
-                temperature = None
-
-            return ChatOpenAI(
-                model_name=model_name,
-                temperature=temperature,
-                streaming=stream,
-                openai_api_key=self.api_key,
-            )
+            params = dict(model_name=model_name, streaming=stream, openai_api_key=self.api_key)
+            if model_name not in OPENAI_REASONING_MODEL_NAMES:
+                params[\"temperature\"] = temperature
+            return ChatOpenAI(**params)

1363-1384: Missing runtime deps for added providers

This component imports langchain_ollama and langchain_ibm but they’re not listed in metadata.dependencies. Add both to avoid surprises in environments that surface these dependencies from templates.

           "dependencies": {
             "dependencies": [
               {
                 "name": "langchain_anthropic",
                 "version": "0.3.14"
               },
               {
                 "name": "langchain_google_genai",
                 "version": "2.0.6"
               },
               {
                 "name": "langchain_openai",
                 "version": "0.3.23"
               },
+              {
+                "name": "langchain_ollama",
+                "version": null
+              },
+              {
+                "name": "langchain_ibm",
+                "version": null
+              },
               {
                 "name": "lfx",
                 "version": null
               }
             ],
             "total_dependencies": 4
           },

Update total_dependencies accordingly.


1489-1564: Provider dropdown doesn’t expose new providers

The template’s provider options only list OpenAI/Anthropic/Google, so end-users can’t pick IBM or Ollama despite the code supporting them. Add both providers and icons.

-            "options": [
-              "OpenAI",
-              "Anthropic",
-              "Google"
-            ],
+            "options": [
+              "OpenAI",
+              "Anthropic",
+              "Google",
+              "IBM watsonx.ai",
+              "Ollama"
+            ],
             "options_metadata": [
               {
                 "icon": "OpenAI"
               },
               {
                 "icon": "Anthropic"
               },
               {
-                "icon": "Google"
+                "icon": "GoogleGenerativeAI"
+              },
+              {
+                "icon": "WatsonxAI"
+              },
+              {
+                "icon": "Ollama"
               }
             ],
src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json (2)

888-909: Add missing deps: langchain_ollama, langchain_ibm

Mirror the dependency metadata fix from Blog Writer to list both packages and bump total_dependencies.


1055-1089: Expose IBM/Ollama in provider dropdown

Update options and options_metadata to include "IBM watsonx.ai" and "Ollama" with icons ("WatsonxAI", "Ollama") so users can select the new providers.

src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json (2)

868-889: List new provider deps in metadata

Add langchain_ollama and langchain_ibm (and adjust total_dependencies).


1035-1069: Provider dropdown needs IBM/Ollama

Add "IBM watsonx.ai" and "Ollama" plus icons to the options. Users otherwise cannot access the new functionality.

src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json (3)

3244-3650: Do not block the event loop: replace sync IBM model fetch in async path.

update_build_config is async but calls fetch_ibm_models (sync requests.get), which will block the loop and degrade UX. Convert to async httpx or offload with asyncio.to_thread.

Apply one of these:

Option A — make fetch_ibm_models async with httpx and timeouts:

@@
-    @staticmethod
-    def fetch_ibm_models(base_url: str) -> list[str]:
+    @staticmethod
+    async def fetch_ibm_models(base_url: str) -> list[str]:
@@
-            response = requests.get(endpoint, params=params, timeout=10)
-            response.raise_for_status()
-            data = response.json()
+            async with httpx.AsyncClient(timeout=httpx.Timeout(connect=5, read=10), follow_redirects=True) as client:
+                resp = await client.get(endpoint, params=params)
+                resp.raise_for_status()
+                data = resp.json()
@@
-            logger.exception("Error fetching IBM watsonx models. Using default models.")
+            await logger.aexception("Error fetching IBM watsonx models. Using default models.")
             return IBM_WATSONX_DEFAULT_MODELS

And update call sites in update_build_config:

-                models = self.fetch_ibm_models(base_url=field_value)
+                models = await self.fetch_ibm_models(base_url=field_value)

Option B — keep fetch_ibm_models sync, but offload:

-                models = self.fetch_ibm_models(base_url=field_value)
+                models = await asyncio.to_thread(self.fetch_ibm_models, base_url=field_value)

3310-3345: Template mismatch: provider dropdown excludes IBM/Ollama; new inputs likely missing.

The code supports providers ["OpenAI","Anthropic","Google","IBM watsonx.ai","Ollama"], but this node’s template provider options show only OpenAI/Anthropic/Google. Also ensure template includes fields for base_url_ibm_watsonx, project_id, and ollama_base_url used by update_build_config; otherwise build_config lookups can fail and UI won’t expose them.

Minimal fix for provider options:

@@
-                "options": [
-                  "OpenAI",
-                  "Anthropic",
-                  "Google"
-                ],
+                "options": [
+                  "OpenAI",
+                  "Anthropic",
+                  "Google",
+                  "IBM watsonx.ai",
+                  "Ollama"
+                ],
@@
-                "options_metadata": [
+                "options_metadata": [
                   {
                     "icon": "OpenAI"
                   },
                   {
                     "icon": "Anthropic"
                   },
                   {
                     "icon": "GoogleGenerativeAI"
-                  }
+                  },
+                  {
+                    "icon": "WatsonxAI"
+                  },
+                  {
+                    "icon": "Ollama"
+                  }
                 ],

Please also add template entries for:

  • "base_url_ibm_watsonx" (DropdownInput), "project_id" (Str/MessageTextInput), both show=false by default.
  • "ollama_base_url" (MessageTextInput), show=false by default.

I can provide an exact patch if you want me to expand those blocks.


3209-3417: Template missing new provider options and input field definitions — will cause KeyError at runtime.

The JSON template at lines 3310+ has provider options ["OpenAI", "Anthropic", "Google"] only, but the embedded code defines providers including "IBM watsonx.ai" and "Ollama". More critically, the update_build_config method attempts to access build_config["base_url_ibm_watsonx"], build_config["project_id"], and build_config["ollama_base_url"] — none of which exist as input field definitions in the serialized JSON.

When a user selects the Ollama or IBM watsonx.ai provider at runtime, the code will fail with KeyError when trying to show/hide these fields.

Add missing provider options ("IBM watsonx.ai", "Ollama") and their corresponding input field definitions (base_url_ibm_watsonx, project_id, ollama_base_url) to the JSON template to match the embedded component code.

src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json (1)

2035-2070: Starter flow UI doesn’t expose IBM/Ollama providers.

The provider dropdown in this node’s template lists only OpenAI/Anthropic/Google, so users can’t select the new Ollama path (or IBM). Align the template with the component code.

   "provider": {
@@
-  "options": [
-    "OpenAI",
-    "Anthropic",
-    "Google"
-  ],
+  "options": [
+    "OpenAI",
+    "Anthropic",
+    "Google",
+    "IBM watsonx.ai",
+    "Ollama"
+  ],
   "options_metadata": [
     {
       "icon": "OpenAI"
     },
     {
       "icon": "Anthropic"
     },
     {
-      "icon": "Google"
+      "icon": "GoogleGenerativeAI"
+    },
+    {
+      "icon": "WatsonxAI"
+    },
+    {
+      "icon": "Ollama"
     }
   ],
src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json (2)

1309-1336: Expose IBM/Ollama in provider dropdown (node LanguageModelComponent-yEikN).

The template options omit IBM and Ollama, blocking the new async discovery at the UI level.

   "provider": {
@@
-  "options": [
-    "OpenAI",
-    "Anthropic",
-    "Google"
-  ],
+  "options": [
+    "OpenAI",
+    "Anthropic",
+    "Google",
+    "IBM watsonx.ai",
+    "Ollama"
+  ],
   "options_metadata": [
     { "icon": "OpenAI" },
     { "icon": "Anthropic" },
-    { "icon": "Google" }
+    { "icon": "GoogleGenerativeAI" },
+    { "icon": "WatsonxAI" },
+    { "icon": "Ollama" }
   ],

1627-1661: Expose IBM/Ollama in provider dropdown (node LanguageModelComponent-TSuC2).

Duplicate of the above: add IBM and Ollama providers to match the component behavior.

   "provider": {
@@
-  "options": [
-    "OpenAI",
-    "Anthropic",
-    "Google"
-  ],
+  "options": [
+    "OpenAI",
+    "Anthropic",
+    "Google",
+    "IBM watsonx.ai",
+    "Ollama"
+  ],
   "options_metadata": [
     { "icon": "OpenAI" },
     { "icon": "Anthropic" },
-    { "icon": "Google" }
+    { "icon": "GoogleGenerativeAI" },
+    { "icon": "WatsonxAI" },
+    { "icon": "Ollama" }
   ],
src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json (1)

1502-1830: Harden async Ollama discovery: add timeouts, JSON guards, and a fallback for missing capabilities

  • Add explicit httpx timeouts to avoid hanging UI.
  • Drop unnecessary asyncio.iscoroutine checks on Response.json().
  • Guard JSON access with .get(...) and handle cases where /api/show lacks "capabilities".
  • Optional: parallelize /api/show calls for better UX on many models.

Apply minimal robustness fixes:

@@
-    async def is_valid_ollama_url(self, url: str) -> bool:
+    async def is_valid_ollama_url(self, url: str) -> bool:
         """Check if the provided URL is a valid Ollama API endpoint."""
         try:
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(5, connect=5, read=10)) as client:
                 url = transform_localhost_url(url)
                 if not url:
                     return False
@@
-    async def get_ollama_models(self, base_url_value: str) -> list[str]:
+    async def get_ollama_models(self, base_url_value: str) -> list[str]:
@@
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(5, connect=5, read=15)) as client:
                 # Fetch available models
                 tags_response = await client.get(url=tags_url)
                 tags_response.raise_for_status()
-                models = tags_response.json()
-                if asyncio.iscoroutine(models):
-                    models = await models
-                await logger.adebug(f"Available models: {models}")
+                models = tags_response.json() or {}
+                await logger.adebug(f"Available models: {models}")
@@
-                model_ids = []
-                for model in models.get(JSON_MODELS_KEY, []):
-                    model_name = model.get(JSON_NAME_KEY)
+                model_ids: list[str] = []
+                for model in models.get(JSON_MODELS_KEY, []):
+                    model_name = model.get(JSON_NAME_KEY)
                     if not model_name:
                         continue
                     await logger.adebug(f"Checking model: {model_name}")
@@
-                    json_data = show_response.json()
-                    if asyncio.iscoroutine(json_data):
-                        json_data = await json_data
-
-                    capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])
+                    json_data = show_response.json() or {}
+                    capabilities = json_data.get(JSON_CAPABILITIES_KEY) or []
                     await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")
 
-                    if DESIRED_CAPABILITY in capabilities:
-                        model_ids.append(model_name)
+                    if capabilities:
+                        if DESIRED_CAPABILITY in capabilities:
+                            model_ids.append(model_name)
+                    else:
+                        # Fallback: treat models as completion-capable unless they look like embeddings
+                        if not any(s in model_name.lower() for s in ("embed", "embedding")):
+                            model_ids.append(model_name)
 
                 return sorted(model_ids)

Additionally, the provider dropdown options in this template do not include "IBM watsonx.ai" or "Ollama", so the new async Ollama path is unreachable from the UI. Please add both providers to the template’s provider options (and icons) so users can select them.
Based on learnings

src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json (1)

874-1164: Stabilize async Ollama model fetch; fix provider options so Ollama is selectable

  • Add httpx timeouts; remove unnecessary iscoroutine checks.
  • Guard JSON lookups and add a safe fallback when /api/show lacks "capabilities".
  • The provider dropdown in this template does not include "IBM watsonx.ai" or "Ollama", so the new async path is unreachable.

Apply minimal robustness fixes:

@@
-    async def is_valid_ollama_url(self, url: str) -> bool:
+    async def is_valid_ollama_url(self, url: str) -> bool:
         """Check if the provided URL is a valid Ollama API endpoint."""
         try:
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(5, connect=5, read=10)) as client:
@@
-    async def get_ollama_models(self, base_url_value: str) -> list[str]:
+    async def get_ollama_models(self, base_url_value: str) -> list[str]:
@@
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(5, connect=5, read=15)) as client:
                 tags_response = await client.get(url=tags_url)
                 tags_response.raise_for_status()
-                models = tags_response.json()
-                if asyncio.iscoroutine(models):
-                    models = await models
+                models = tags_response.json() or {}
@@
-                model_ids = []
-                for model in models.get(JSON_MODELS_KEY, []):
-                    model_name = model.get(JSON_NAME_KEY)
+                model_ids: list[str] = []
+                for model in models.get(JSON_MODELS_KEY, []):
+                    model_name = model.get(JSON_NAME_KEY)
                     if not model_name:
                         continue
@@
-                    json_data = show_response.json()
-                    if asyncio.iscoroutine(json_data):
-                        json_data = await json_data
-
-                    capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])
+                    json_data = show_response.json() or {}
+                    capabilities = json_data.get(JSON_CAPABILITIES_KEY) or []
@@
-                    if DESIRED_CAPABILITY in capabilities:
-                        model_ids.append(model_name)
+                    if capabilities:
+                        if DESIRED_CAPABILITY in capabilities:
+                            model_ids.append(model_name)
+                    else:
+                        if not any(s in model_name.lower() for s in ("embed", "embedding")):
+                            model_ids.append(model_name)

Also update the provider dropdown options in this template to include "IBM watsonx.ai" and "Ollama" (with matching options_metadata icons) so users can select them. This otherwise blocks the PR goal.
Based on learnings

♻️ Duplicate comments (3)
src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json (1)

988-1162: Same Ollama async issues here: timeouts, None-guard, capability fallback, and extra iscoroutine checks

Please apply the same fixes as in Blog Writer to this code block (HTTPX timeouts, guard transform_localhost_url, robust capability filtering, remove unnecessary iscoroutine checks, conditional temperature param). See the diff provided there and mirror it here.

src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json (1)

968-1143: Repeat of Ollama async concerns

Apply the same improvements as noted: httpx timeouts/follow_redirects, None-guard after transform_localhost_url, safer capability fallback, drop iscoroutine guards, and conditional temperature param for ChatOpenAI.

src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json (1)

1829-2147: Same as above block — apply the same robustness/timeouts and provider options fix

The LanguageModelComponent code here duplicates the earlier block. Apply the same changes and ensure the provider dropdown includes "IBM watsonx.ai" and "Ollama".

🧹 Nitpick comments (16)
src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json (1)

1367-1367: Remove unnecessary asyncio.iscoroutine() defensive checks.

Lines within get_ollama_models check if tags_response.json() and show_response.json() return coroutines. The httpx library's .json() method returns a dictionary directly, never a coroutine. These checks are redundant and add cognitive overhead.

  async with httpx.AsyncClient() as client:
      tags_response = await client.get(url=tags_url)
      tags_response.raise_for_status()
      models = tags_response.json()
-     if asyncio.iscoroutine(models):
-         models = await models
      await logger.adebug(f"Available models: {models}")

And similarly:

  show_response = await client.post(url=show_url, json=payload)
  show_response.raise_for_status()
  json_data = show_response.json()
- if asyncio.iscoroutine(json_data):
-     json_data = await json_data
  
  capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])
src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json (1)

2599-2625: Remove unnecessary asyncio.iscoroutine() checks in model fetching.

Lines 2602–2603 and 2620–2621 contain unreachable checks: asyncio.iscoroutine() will never be True for models or json_data since httpx.Response.json() returns a regular dict, not a coroutine. These defensive checks add noise and should be removed.

-                if asyncio.iscoroutine(models):
-                    models = await models
                 await logger.adebug(f"Available models: {models}")
                 
                 # ... later ...
                 
                 json_data = show_response.json()
-                if asyncio.iscoroutine(json_data):
-                    json_data = await json_data
src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json (3)

2628-2750: Verify proper async/await semantics in logger calls.

In the embedded code, several logger calls use async variants (e.g., logger.adebug(), logger.aexception(), logger.ainfo(), logger.awarning()) but some are not awaited. For example:

  • Line in get_ollama_models: await logger.adebug(f"Available models: {models}")
  • Line in error handler: await logger.aexception(msg)

However, ensure all async logger calls are consistently awaited. If these logger methods are not coroutines, remove the await keyword. If they are coroutines, all must be awaited.

Can you confirm whether logger.adebug(), logger.aexception(), logger.ainfo(), and logger.awarning() are coroutines that require await? This affects code correctness.


2628-2750: Refactor repeated URL normalization logic into a helper method.

The URL normalization pattern (url.rstrip("/").removesuffix("/v1")) appears multiple times:

  • In is_valid_ollama_url()
  • In get_ollama_models()
  • In build_model() for Ollama provider

This duplication increases maintenance burden and risk of inconsistency.

Extract this into a reusable static or private method:

@staticmethod
def _normalize_ollama_url(url: str) -> str:
    """Normalize Ollama API URL by removing /v1 suffix and ensuring trailing slash."""
    if not url:
        return url
    normalized = url.rstrip("/").removesuffix("/v1")
    if not normalized.endswith("/"):
        normalized = normalized + "/"
    return normalized

Then use it consistently across all three locations.


2628-2680: Verify error handling and logging in get_ollama_models() provides sufficient context.

In the error handler for get_ollama_models(), the exception is caught as (httpx.RequestError, ValueError) and logged with await logger.aexception(msg). While this captures exceptions, ensure:

  1. The error message includes enough context (e.g., the URL that failed, the response status code if available).
  2. Network timeouts or connection refusals are clearly communicated to users.
  3. A caller can distinguish between "invalid URL" and "network error" scenarios.

Consider enriching error messages with URL and HTTP status info if available.

src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json (2)

2943-2956: Ensure Ollama model fetch errors are logged with appropriate severity.

When Ollama model fetching fails (line ~2951), the code sets empty options and logs a warning. However, there's no distinction between a transient network error and a genuine "no models available" condition. The code should consider:

  1. Whether to log this at warning or error level
  2. Whether users need more context (e.g., "Could not connect to Ollama at {url}")
  3. Whether to provide a default fallback model list (like IBM watsonx does)

The current approach silently clears the model list, which might confuse users. Consider more explicit messaging.


2820-2835: Defensive iscoroutine checks appear unnecessary with httpx.

The code includes defensive checks like if asyncio.iscoroutine(models): models = await models (lines ~2887, ~2897). With httpx.AsyncClient, the response methods like .json() are not coroutines—they return the actual values directly. These checks will never be true and add unnecessary complexity.

Remove the defensive asyncio.iscoroutine() checks:

- models = tags_response.json()
- if asyncio.iscoroutine(models):
-     models = await models
- await logger.adebug(f"Available models: {models}")
+ models = tags_response.json()
+ await logger.adebug(f"Available models: {models}")
src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json (2)

1404-1405: Bound and parallelize per‑model /api/show calls.

Current loop is sequential O(n). Use bounded concurrency to reduce latency.

Example:

- model_ids = []
- for model in models.get(JSON_MODELS_KEY, []):
-     ...
-     show_response = await client.post(url=show_url, json=payload)
-     ...
-     if DESIRED_CAPABILITY in capabilities:
-         model_ids.append(model_name)
+ sem = asyncio.Semaphore(8)
+ async def probe(name: str) -> str | None:
+     async with sem:
+         r = await client.post(url=show_url, json={"model": name})
+         r.raise_for_status()
+         caps = r.json().get(JSON_CAPABILITIES_KEY, [])
+         return name if DESIRED_CAPABILITY in caps else None
+ tasks = [probe(m.get(JSON_NAME_KEY)) for m in models.get(JSON_MODELS_KEY, []) if m.get(JSON_NAME_KEY)]
+ results = await asyncio.gather(*tasks, return_exceptions=False)
+ model_ids = sorted([r for r in results if r])

1304-1325: Missing dependency metadata for langchain_ollama.

Component imports langchain_ollama but it isn’t listed in metadata.dependencies.dependencies. Add it for consistency and to avoid missing‑dep surprises.

   "dependencies": [
     {"name": "langchain_anthropic", "version": "0.3.14"},
     {"name": "langchain_google_genai", "version": "2.0.6"},
     {"name": "langchain_openai", "version": "0.3.23"},
+    {"name": "langchain_ollama", "version": null},
     {"name": "lfx", "version": null}
   ],
src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json (1)

1195-1216: Add langchain_ollama to metadata dependencies.

Imports are present; dependency metadata omits it.

   "dependencies": [
     {"name": "langchain_anthropic", "version": "0.3.14"},
     {"name": "langchain_google_genai", "version": "2.0.6"},
     {"name": "langchain_openai", "version": "0.3.23"},
+    {"name": "langchain_ollama", "version": null},
     {"name": "lfx", "version": null}
   ],

Also applies to: 1522-1543

src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json (1)

266-283: Tiny copy fix in default question

“What is this document is about?” → “What is this document about?”

src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json (3)

3244-3650: Harden and optimize Ollama calls: add timeouts, concurrency; remove no-op coroutine checks; unify URL normalization.

  • No timeouts on httpx calls (risk of hangs).
  • /api/show loop is fully sequential; parallelize with a bounded semaphore.
  • asyncio.iscoroutine checks on Response.json() are unnecessary.
  • URL normalization order differs between is_valid_ollama_url and get_ollama_models; standardize: transform then strip “/v1”.
  • Optional: validate scheme and avoid link-local/metadata host targets for basic SSRF hygiene.
@@
-    async def is_valid_ollama_url(self, url: str) -> bool:
+    async def is_valid_ollama_url(self, url: str) -> bool:
@@
-            async with httpx.AsyncClient() as client:
-                url = transform_localhost_url(url)
-                if not url:
+            # Normalize and validate base URL
+            url = transform_localhost_url(url or "")
+            if not url:
                 return False
-                # Strip /v1 suffix if present, as Ollama API endpoints are at root level
-                url = url.rstrip("/").removesuffix("/v1")
-                if not url.endswith("/"):
-                    url = url + "/"
-                return (await client.get(url=urljoin(url, "api/tags"))).status_code == HTTP_STATUS_OK
+            # Strip /v1 suffix if present, as Ollama API endpoints are at root level
+            url = url.rstrip("/").removesuffix("/v1") + "/"
+            async with httpx.AsyncClient(timeout=httpx.Timeout(connect=5, read=10), follow_redirects=True) as client:
+                resp = await client.get(url=urljoin(url, "api/tags"))
+                return resp.status_code == HTTP_STATUS_OK
         except httpx.RequestError:
             return False
@@
-    async def get_ollama_models(self, base_url_value: str) -> list[str]:
+    async def get_ollama_models(self, base_url_value: str) -> list[str]:
@@
-            # Strip /v1 suffix if present, as Ollama API endpoints are at root level
-            base_url = base_url_value.rstrip("/").removesuffix("/v1")
-            if not base_url.endswith("/"):
-                base_url = base_url + "/"
-            base_url = transform_localhost_url(base_url)
+            # Normalize base URL: transform first, then strip /v1, ensure trailing slash
+            base_url = transform_localhost_url(base_url_value or "").rstrip("/").removesuffix("/v1") + "/"
@@
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(connect=5, read=15), follow_redirects=True) as client:
                 # Fetch available models
-                tags_response = await client.get(url=tags_url)
-                tags_response.raise_for_status()
-                models = tags_response.json()
-                if asyncio.iscoroutine(models):
-                    models = await models
+                tags_response = await client.get(url=tags_url)
+                tags_response.raise_for_status()
+                models = tags_response.json()
                 await logger.adebug(f"Available models: {models}")
@@
-                model_ids = []
-                for model in models.get(JSON_MODELS_KEY, []):
-                    model_name = model.get(JSON_NAME_KEY)
-                    if not model_name:
-                        continue
-                    await logger.adebug(f"Checking model: {model_name}")
-
-                    payload = {"model": model_name}
-                    show_response = await client.post(url=show_url, json=payload)
-                    show_response.raise_for_status()
-                    json_data = show_response.json()
-                    if asyncio.iscoroutine(json_data):
-                        json_data = await json_data
-
-                    capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])
-                    await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")
-
-                    if DESIRED_CAPABILITY in capabilities:
-                        model_ids.append(model_name)
+                model_ids: list[str] = []
+                sem = asyncio.Semaphore(10)
+                async def fetch_caps(name: str) -> tuple[str, list[str]] | None:
+                    async with sem:
+                        try:
+                            await logger.adebug(f"Checking model: {name}")
+                            resp = await client.post(url=show_url, json={"model": name})
+                            resp.raise_for_status()
+                            data = resp.json()
+                            caps = data.get(JSON_CAPABILITIES_KEY, []) or []
+                            return (name, caps)
+                        except httpx.HTTPError:
+                            await logger.awarning(f"Skipping model {name} due to /api/show error")
+                            return None
+
+                tasks = [
+                    fetch_caps(m.get(JSON_NAME_KEY))
+                    for m in models.get(JSON_MODELS_KEY, [])
+                    if m.get(JSON_NAME_KEY)
+                ]
+                for res in await asyncio.gather(*tasks):
+                    if res and DESIRED_CAPABILITY in res[1]:
+                        model_ids.append(res[0])
                 return sorted(model_ids)

Optional (defense-in-depth): reject non-http(s) schemes and link-local/metadata hosts before calling httpx.


3244-3650: Minor: guard type when checking OpenAI o1 models.

Field values can be None/non‑str; add isinstance guard to avoid AttributeError on startswith.

-        elif field_name == "model_name" and field_value.startswith("o1") and self.provider == "OpenAI":
+        elif field_name == "model_name" and isinstance(field_value, str) and field_value.startswith("o1") and self.provider == "OpenAI":
@@
-        elif field_name == "model_name" and not field_value.startswith("o1") and "system_message" in build_config:
+        elif field_name == "model_name" and isinstance(field_value, str) and not field_value.startswith("o1") and "system_message" in build_config:

3244-3650: Consistency nit: use async logger variants within async methods.

You use await logger.adebug/awarning elsewhere; switch logger.warning/info in async contexts to await logger.awarning/ainfo for consistency.

src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json (1)

3076-3077: Error handling is appropriate with room for improvement.

The implementation handles errors defensively:

  • is_valid_ollama_url() returns False on any request error (safe default)
  • get_ollama_models() raises ValueError which is caught and logged in update_build_config()
  • Invalid URLs prevent unnecessary API calls

Suggestion for future improvement: Consider providing more granular error messages to distinguish between network errors, invalid endpoints, authentication failures, and malformed responses. This would help users debug connectivity issues more effectively. Currently the generic "Could not get model names from Ollama" might obscure the root cause.

Also applies to: 3403-3404

src/lfx/src/lfx/components/ollama/ollama.py (1)

352-362: Reduce log noise and log the actual URL value; add timeouts and JSON guards

  • Logging a dict at warning level on every refresh is noisy. Use debug/info and log the string value.
  • Add httpx timeouts to avoid hanging UI on bad endpoints.
  • Use .get(...) when reading JSON keys to avoid KeyError on schema changes.

Apply this diff:

@@
-        if field_name in {"model_name", "base_url", "tool_model_enabled"}:
-            logger.warning(f"Fetching Ollama models from updated URL: {build_config['base_url']}")
+        if field_name in {"model_name", "base_url", "tool_model_enabled"}:
+            url_log = (build_config.get("base_url", {}) or {}).get("value", self.base_url)
+            logger.debug("Fetching Ollama models from updated URL: %s", url_log)
@@
-    async def is_valid_ollama_url(self, url: str) -> bool:
+    async def is_valid_ollama_url(self, url: str) -> bool:
         try:
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(5, connect=5, read=10)) as client:
@@
-    async def get_models(self, base_url_value: str, *, tool_model_enabled: bool | None = None) -> list[str]:
+    async def get_models(self, base_url_value: str, *, tool_model_enabled: bool | None = None) -> list[str]:
@@
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(5, connect=5, read=15)) as client:
@@
-                models = tags_response.json()
-                if asyncio.iscoroutine(models):
-                    models = await models
+                models = tags_response.json() or {}
@@
-                for model in models[self.JSON_MODELS_KEY]:
-                    model_name = model[self.JSON_NAME_KEY]
+                for model in models.get(self.JSON_MODELS_KEY, []):
+                    model_name = model.get(self.JSON_NAME_KEY)
+                    if not model_name:
+                        continue
@@
-                    json_data = show_response.json()
-                    if asyncio.iscoroutine(json_data):
-                        json_data = await json_data
+                    json_data = show_response.json() or {}

If desired, we can parallelize /api/show requests with asyncio.gather for better responsiveness on large model lists.

Also applies to: 314-329, 403-439

Comment on lines 1299 to 1300
"value": "import asyncio\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nimport requests\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_ibm import ChatWatsonx\nfrom langchain_ollama import ChatOllama\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom lfx.base.models.anthropic_constants import ANTHROPIC_MODELS\nfrom lfx.base.models.google_generative_ai_constants import GOOGLE_GENERATIVE_AI_MODELS\nfrom lfx.base.models.google_generative_ai_model import ChatGoogleGenerativeAIFixed\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.openai_constants import OPENAI_CHAT_MODEL_NAMES, OPENAI_REASONING_MODEL_NAMES\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, MessageTextInput, StrInput\nfrom lfx.io import DropdownInput, MessageInput, MultilineInput, SecretStrInput, SliderInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# IBM watsonx.ai constants\nIBM_WATSONX_DEFAULT_MODELS = [\"ibm/granite-3-2b-instruct\", \"ibm/granite-3-8b-instruct\", \"ibm/granite-13b-instruct-v2\"]\nIBM_WATSONX_URLS = [\n \"https://us-south.ml.cloud.ibm.com\",\n \"https://eu-de.ml.cloud.ibm.com\",\n \"https://eu-gb.ml.cloud.ibm.com\",\n \"https://au-syd.ml.cloud.ibm.com\",\n \"https://jp-tok.ml.cloud.ibm.com\",\n \"https://ca-tor.ml.cloud.ibm.com\",\n]\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"completion\"\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 priority = 0 # Set priority to 0 to make it appear first\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\"version\": \"2024-09-16\", \"filters\": \"function_text_chat,!lifecycle_withdrawn\"}\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching IBM watsonx models. Using default models.\")\n return IBM_WATSONX_DEFAULT_MODELS\n\n async def is_valid_ollama_url(self, url: str) -> bool:\n \"\"\"Check if the provided URL is a valid Ollama API endpoint.\"\"\"\n try:\n async with httpx.AsyncClient() as client:\n url = transform_localhost_url(url)\n if not url:\n return False\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n url = url.rstrip(\"/\").removesuffix(\"/v1\")\n if not url.endswith(\"/\"):\n url = url + \"/\"\n return (await client.get(url=urljoin(url, \"api/tags\"))).status_code == HTTP_STATUS_OK\n except httpx.RequestError:\n return False\n\n async def get_ollama_models(self, base_url_value: str) -> list[str]:\n \"\"\"Fetch available completion models from the Ollama API.\n\n Filters out embedding models and only returns models with completion capability.\n\n Args:\n base_url_value (str): The base URL of the Ollama API.\n\n Returns:\n list[str]: A sorted list of model names that support completion.\n\n Raises:\n ValueError: If there is an issue with the API request or response.\n \"\"\"\n try:\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n base_url = base_url_value.rstrip(\"/\").removesuffix(\"/v1\")\n if not base_url.endswith(\"/\"):\n base_url = base_url + \"/\"\n base_url = transform_localhost_url(base_url)\n\n # Ollama REST API to return models\n tags_url = urljoin(base_url, \"api/tags\")\n\n # Ollama REST API to return model capabilities\n show_url = urljoin(base_url, \"api/show\")\n\n async with httpx.AsyncClient() as client:\n # Fetch available models\n tags_response = await client.get(url=tags_url)\n tags_response.raise_for_status()\n models = tags_response.json()\n if asyncio.iscoroutine(models):\n models = await models\n await logger.adebug(f\"Available models: {models}\")\n\n # Filter models that are NOT embedding models\n model_ids = []\n for model in models.get(JSON_MODELS_KEY, []):\n model_name = model.get(JSON_NAME_KEY)\n if not model_name:\n continue\n await logger.adebug(f\"Checking model: {model_name}\")\n\n payload = {\"model\": model_name}\n show_response = await client.post(url=show_url, json=payload)\n show_response.raise_for_status()\n json_data = show_response.json()\n if asyncio.iscoroutine(json_data):\n json_data = await json_data\n\n capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])\n await logger.adebug(f\"Model: {model_name}, Capabilities: {capabilities}\")\n\n if DESIRED_CAPABILITY in capabilities:\n model_ids.append(model_name)\n\n return sorted(model_ids)\n\n except (httpx.RequestError, ValueError) as e:\n msg = \"Could not get model names from Ollama.\"\n await logger.aexception(msg)\n raise ValueError(msg) from e\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Anthropic\", \"Google\", \"IBM watsonx.ai\", \"Ollama\"],\n value=\"OpenAI\",\n info=\"Select the model provider\",\n real_time_refresh=True,\n options_metadata=[\n {\"icon\": \"OpenAI\"},\n {\"icon\": \"Anthropic\"},\n {\"icon\": \"GoogleGenerativeAI\"},\n {\"icon\": \"WatsonxAI\"},\n {\"icon\": \"Ollama\"},\n ],\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n options=OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES,\n value=OPENAI_CHAT_MODEL_NAMES[0],\n info=\"Select the model to use\",\n real_time_refresh=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\n real_time_refresh=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 MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=\"Endpoint of the Ollama API (Ollama only). Defaults to http://localhost:11434\",\n value=\"http://localhost:11434\",\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 ]\n\n def build_model(self) -> LanguageModel:\n provider = self.provider\n model_name = self.model_name\n temperature = self.temperature\n stream = self.stream\n\n if provider == \"OpenAI\":\n if not self.api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n\n if model_name in OPENAI_REASONING_MODEL_NAMES:\n # reasoning models do not support temperature (yet)\n temperature = None\n\n return ChatOpenAI(\n model_name=model_name,\n temperature=temperature,\n streaming=stream,\n openai_api_key=self.api_key,\n )\n if provider == \"Anthropic\":\n if not self.api_key:\n msg = \"Anthropic API key is required when using Anthropic provider\"\n raise ValueError(msg)\n return ChatAnthropic(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n anthropic_api_key=self.api_key,\n )\n if provider == \"Google\":\n if not self.api_key:\n msg = \"Google API key is required when using Google provider\"\n raise ValueError(msg)\n return ChatGoogleGenerativeAIFixed(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n google_api_key=self.api_key,\n )\n if provider == \"IBM watsonx.ai\":\n if not self.api_key:\n msg = \"IBM API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.base_url_ibm_watsonx:\n msg = \"IBM watsonx API Endpoint is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.project_id:\n msg = \"IBM watsonx Project ID is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n return ChatWatsonx(\n apikey=SecretStr(self.api_key).get_secret_value(),\n url=self.base_url_ibm_watsonx,\n project_id=self.project_id,\n model_id=model_name,\n params={\n \"temperature\": temperature,\n },\n streaming=stream,\n )\n if provider == \"Ollama\":\n if not self.ollama_base_url:\n msg = \"Ollama API URL is required when using Ollama provider\"\n raise ValueError(msg)\n if not model_name:\n msg = \"Model name is required when using Ollama provider\"\n raise ValueError(msg)\n\n transformed_base_url = transform_localhost_url(self.ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return ChatOllama(\n base_url=transformed_base_url,\n model=model_name,\n temperature=temperature,\n )\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model_name\"][\"options\"] = OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES\n build_config[\"model_name\"][\"value\"] = OPENAI_CHAT_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Anthropic\":\n build_config[\"model_name\"][\"options\"] = ANTHROPIC_MODELS\n build_config[\"model_name\"][\"value\"] = ANTHROPIC_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Anthropic API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Google\":\n build_config[\"model_name\"][\"options\"] = GOOGLE_GENERATIVE_AI_MODELS\n build_config[\"model_name\"][\"value\"] = GOOGLE_GENERATIVE_AI_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Google API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model_name\"][\"options\"] = IBM_WATSONX_DEFAULT_MODELS\n build_config[\"model_name\"][\"value\"] = IBM_WATSONX_DEFAULT_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Ollama\":\n # Fetch Ollama models from the API\n ollama_url = build_config[\"ollama_base_url\"].get(\"value\", \"http://localhost:11434\")\n if await self.is_valid_ollama_url(ollama_url):\n try:\n models = await self.get_ollama_models(base_url_value=ollama_url)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama models. Setting empty options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = True\n elif (\n field_name == \"base_url_ibm_watsonx\"\n and field_value\n and hasattr(self, \"provider\")\n and self.provider == \"IBM watsonx.ai\"\n ):\n # Fetch IBM models when base_url changes\n try:\n models = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else IBM_WATSONX_DEFAULT_MODELS[0]\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n logger.info(info_message)\n except Exception: # noqa: BLE001\n logger.exception(\"Error updating IBM model options.\")\n elif (\n field_name == \"ollama_base_url\" and field_value and hasattr(self, \"provider\") and self.provider == \"Ollama\"\n ):\n # Fetch Ollama models when ollama_base_url changes\n if await self.is_valid_ollama_url(field_value):\n try:\n models = await self.get_ollama_models(base_url_value=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n await logger.ainfo(info_message)\n except ValueError:\n await logger.awarning(\"Error updating Ollama model options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n elif field_name == \"model_name\" and field_value.startswith(\"o1\") and self.provider == \"OpenAI\":\n # Hide system_message for o1 models - currently unsupported\n if \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = False\n elif field_name == \"model_name\" and not field_value.startswith(\"o1\") and \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = True\n return build_config\n"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add HTTP timeouts/limits; avoid sync requests inside async flow; bound concurrency.

Same issues as in the other starter:

  • No httpx timeouts/limits on Ollama calls. Add Limits and Timeout.
  • fetch_ibm_models uses requests.get but is awaited indirectly from async update_build_config → block. Make it async httpx and await it.
  • Sequential per‑model /api/show fan‑out. Use bounded gather.

See previous comment’s diffs; apply in both LanguageModelComponent code blocks here. Based on learnings

Also applies to: 1619-1627

🤖 Prompt for AI Agents
In src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search
RAG.json around lines 1299-1300 (and also apply same changes to 1619-1627), the
Ollama and IBM fetch code lacks httpx timeouts/limits, fetch_ibm_models is
synchronous (requests) causing blocking inside async flow, and get_ollama_models
performs sequential per-model /api/show calls; fix by converting
fetch_ibm_models to an async function using httpx.AsyncClient with a configured
Timeout and Limits, replace the synchronous requests.get usage with an awaited
httpx request and raise_for_status handling, add Timeout and Limits to all
AsyncClient creations used for Ollama calls (and use transform_localhost_url as
before), and change get_ollama_models to concurrently fetch per-model /api/show
using bounded concurrency (e.g., an asyncio.Semaphore or httpx.Limits with a
limited worker gather) so you run N requests in parallel but capped; ensure
exceptions are caught and logged similarly and preserve existing return values
and error handling semantics.

Comment on lines 1235 to 1236
"value": "import asyncio\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nimport requests\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_ibm import ChatWatsonx\nfrom langchain_ollama import ChatOllama\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom lfx.base.models.anthropic_constants import ANTHROPIC_MODELS\nfrom lfx.base.models.google_generative_ai_constants import GOOGLE_GENERATIVE_AI_MODELS\nfrom lfx.base.models.google_generative_ai_model import ChatGoogleGenerativeAIFixed\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.openai_constants import OPENAI_CHAT_MODEL_NAMES, OPENAI_REASONING_MODEL_NAMES\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, MessageTextInput, StrInput\nfrom lfx.io import DropdownInput, MessageInput, MultilineInput, SecretStrInput, SliderInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# IBM watsonx.ai constants\nIBM_WATSONX_DEFAULT_MODELS = [\"ibm/granite-3-2b-instruct\", \"ibm/granite-3-8b-instruct\", \"ibm/granite-13b-instruct-v2\"]\nIBM_WATSONX_URLS = [\n \"https://us-south.ml.cloud.ibm.com\",\n \"https://eu-de.ml.cloud.ibm.com\",\n \"https://eu-gb.ml.cloud.ibm.com\",\n \"https://au-syd.ml.cloud.ibm.com\",\n \"https://jp-tok.ml.cloud.ibm.com\",\n \"https://ca-tor.ml.cloud.ibm.com\",\n]\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"completion\"\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 priority = 0 # Set priority to 0 to make it appear first\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\"version\": \"2024-09-16\", \"filters\": \"function_text_chat,!lifecycle_withdrawn\"}\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching IBM watsonx models. Using default models.\")\n return IBM_WATSONX_DEFAULT_MODELS\n\n async def is_valid_ollama_url(self, url: str) -> bool:\n \"\"\"Check if the provided URL is a valid Ollama API endpoint.\"\"\"\n try:\n async with httpx.AsyncClient() as client:\n url = transform_localhost_url(url)\n if not url:\n return False\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n url = url.rstrip(\"/\").removesuffix(\"/v1\")\n if not url.endswith(\"/\"):\n url = url + \"/\"\n return (await client.get(url=urljoin(url, \"api/tags\"))).status_code == HTTP_STATUS_OK\n except httpx.RequestError:\n return False\n\n async def get_ollama_models(self, base_url_value: str) -> list[str]:\n \"\"\"Fetch available completion models from the Ollama API.\n\n Filters out embedding models and only returns models with completion capability.\n\n Args:\n base_url_value (str): The base URL of the Ollama API.\n\n Returns:\n list[str]: A sorted list of model names that support completion.\n\n Raises:\n ValueError: If there is an issue with the API request or response.\n \"\"\"\n try:\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n base_url = base_url_value.rstrip(\"/\").removesuffix(\"/v1\")\n if not base_url.endswith(\"/\"):\n base_url = base_url + \"/\"\n base_url = transform_localhost_url(base_url)\n\n # Ollama REST API to return models\n tags_url = urljoin(base_url, \"api/tags\")\n\n # Ollama REST API to return model capabilities\n show_url = urljoin(base_url, \"api/show\")\n\n async with httpx.AsyncClient() as client:\n # Fetch available models\n tags_response = await client.get(url=tags_url)\n tags_response.raise_for_status()\n models = tags_response.json()\n if asyncio.iscoroutine(models):\n models = await models\n await logger.adebug(f\"Available models: {models}\")\n\n # Filter models that are NOT embedding models\n model_ids = []\n for model in models.get(JSON_MODELS_KEY, []):\n model_name = model.get(JSON_NAME_KEY)\n if not model_name:\n continue\n await logger.adebug(f\"Checking model: {model_name}\")\n\n payload = {\"model\": model_name}\n show_response = await client.post(url=show_url, json=payload)\n show_response.raise_for_status()\n json_data = show_response.json()\n if asyncio.iscoroutine(json_data):\n json_data = await json_data\n\n capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])\n await logger.adebug(f\"Model: {model_name}, Capabilities: {capabilities}\")\n\n if DESIRED_CAPABILITY in capabilities:\n model_ids.append(model_name)\n\n return sorted(model_ids)\n\n except (httpx.RequestError, ValueError) as e:\n msg = \"Could not get model names from Ollama.\"\n await logger.aexception(msg)\n raise ValueError(msg) from e\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Anthropic\", \"Google\", \"IBM watsonx.ai\", \"Ollama\"],\n value=\"OpenAI\",\n info=\"Select the model provider\",\n real_time_refresh=True,\n options_metadata=[\n {\"icon\": \"OpenAI\"},\n {\"icon\": \"Anthropic\"},\n {\"icon\": \"GoogleGenerativeAI\"},\n {\"icon\": \"WatsonxAI\"},\n {\"icon\": \"Ollama\"},\n ],\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n options=OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES,\n value=OPENAI_CHAT_MODEL_NAMES[0],\n info=\"Select the model to use\",\n real_time_refresh=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\n real_time_refresh=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 MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=\"Endpoint of the Ollama API (Ollama only). Defaults to http://localhost:11434\",\n value=\"http://localhost:11434\",\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 ]\n\n def build_model(self) -> LanguageModel:\n provider = self.provider\n model_name = self.model_name\n temperature = self.temperature\n stream = self.stream\n\n if provider == \"OpenAI\":\n if not self.api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n\n if model_name in OPENAI_REASONING_MODEL_NAMES:\n # reasoning models do not support temperature (yet)\n temperature = None\n\n return ChatOpenAI(\n model_name=model_name,\n temperature=temperature,\n streaming=stream,\n openai_api_key=self.api_key,\n )\n if provider == \"Anthropic\":\n if not self.api_key:\n msg = \"Anthropic API key is required when using Anthropic provider\"\n raise ValueError(msg)\n return ChatAnthropic(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n anthropic_api_key=self.api_key,\n )\n if provider == \"Google\":\n if not self.api_key:\n msg = \"Google API key is required when using Google provider\"\n raise ValueError(msg)\n return ChatGoogleGenerativeAIFixed(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n google_api_key=self.api_key,\n )\n if provider == \"IBM watsonx.ai\":\n if not self.api_key:\n msg = \"IBM API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.base_url_ibm_watsonx:\n msg = \"IBM watsonx API Endpoint is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.project_id:\n msg = \"IBM watsonx Project ID is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n return ChatWatsonx(\n apikey=SecretStr(self.api_key).get_secret_value(),\n url=self.base_url_ibm_watsonx,\n project_id=self.project_id,\n model_id=model_name,\n params={\n \"temperature\": temperature,\n },\n streaming=stream,\n )\n if provider == \"Ollama\":\n if not self.ollama_base_url:\n msg = \"Ollama API URL is required when using Ollama provider\"\n raise ValueError(msg)\n if not model_name:\n msg = \"Model name is required when using Ollama provider\"\n raise ValueError(msg)\n\n transformed_base_url = transform_localhost_url(self.ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return ChatOllama(\n base_url=transformed_base_url,\n model=model_name,\n temperature=temperature,\n )\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model_name\"][\"options\"] = OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES\n build_config[\"model_name\"][\"value\"] = OPENAI_CHAT_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Anthropic\":\n build_config[\"model_name\"][\"options\"] = ANTHROPIC_MODELS\n build_config[\"model_name\"][\"value\"] = ANTHROPIC_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Anthropic API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Google\":\n build_config[\"model_name\"][\"options\"] = GOOGLE_GENERATIVE_AI_MODELS\n build_config[\"model_name\"][\"value\"] = GOOGLE_GENERATIVE_AI_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Google API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model_name\"][\"options\"] = IBM_WATSONX_DEFAULT_MODELS\n build_config[\"model_name\"][\"value\"] = IBM_WATSONX_DEFAULT_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Ollama\":\n # Fetch Ollama models from the API\n ollama_url = build_config[\"ollama_base_url\"].get(\"value\", \"http://localhost:11434\")\n if await self.is_valid_ollama_url(ollama_url):\n try:\n models = await self.get_ollama_models(base_url_value=ollama_url)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama models. Setting empty options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = True\n elif (\n field_name == \"base_url_ibm_watsonx\"\n and field_value\n and hasattr(self, \"provider\")\n and self.provider == \"IBM watsonx.ai\"\n ):\n # Fetch IBM models when base_url changes\n try:\n models = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else IBM_WATSONX_DEFAULT_MODELS[0]\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n logger.info(info_message)\n except Exception: # noqa: BLE001\n logger.exception(\"Error updating IBM model options.\")\n elif (\n field_name == \"ollama_base_url\" and field_value and hasattr(self, \"provider\") and self.provider == \"Ollama\"\n ):\n # Fetch Ollama models when ollama_base_url changes\n if await self.is_valid_ollama_url(field_value):\n try:\n models = await self.get_ollama_models(base_url_value=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n await logger.ainfo(info_message)\n except ValueError:\n await logger.awarning(\"Error updating Ollama model options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n elif field_name == \"model_name\" and field_value.startswith(\"o1\") and self.provider == \"OpenAI\":\n # Hide system_message for o1 models - currently unsupported\n if \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = False\n elif field_name == \"model_name\" and not field_value.startswith(\"o1\") and \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = True\n return build_config\n"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same Ollama hardening needed here: add timeouts + capability fallback.

Mirror the changes from the other file in this embedded LanguageModelComponent.

-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=5.0)) as client:
                 url = transform_localhost_url(url)
@@
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=5.0)) as client:
                 tags_response = await client.get(url=tags_url)
                 tags_response.raise_for_status()
                 models = tags_response.json()
-                if asyncio.iscoroutine(models):
-                    models = await models
@@
-                    json_data = show_response.json()
-                    if asyncio.iscoroutine(json_data):
-                        json_data = await json_data
-
-                    capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])
-                    await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")
-
-                    if DESIRED_CAPABILITY in capabilities:
-                        model_ids.append(model_name)
+                    json_data = show_response.json()
+                    capabilities = json_data.get(JSON_CAPABILITIES_KEY)
+                    families = json_data.get("families", []) or json_data.get("family", [])
+                    is_embedding = any(s in model_name.lower() for s in ("embed", "embedding")) or \
+                                   ("embedding" in " ".join(map(str, families)).lower())
+                    if capabilities:
+                        await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")
+                        if DESIRED_CAPABILITY in capabilities and not is_embedding:
+                            model_ids.append(model_name)
+                    else:
+                        await logger.adebug(f"Model: {model_name}, no capabilities in /api/show; fallback include (not embedding).")
+                        if not is_embedding:
+                            model_ids.append(model_name)
  • Optional: parallelize /api/show with asyncio.gather + semaphore.
  • Optional: remove unnecessary asyncio.iscoroutine checks.

Comment on lines 3076 to 3077
"value": "import asyncio\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nimport requests\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_ibm import ChatWatsonx\nfrom langchain_ollama import ChatOllama\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom lfx.base.models.anthropic_constants import ANTHROPIC_MODELS\nfrom lfx.base.models.google_generative_ai_constants import GOOGLE_GENERATIVE_AI_MODELS\nfrom lfx.base.models.google_generative_ai_model import ChatGoogleGenerativeAIFixed\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.openai_constants import OPENAI_CHAT_MODEL_NAMES, OPENAI_REASONING_MODEL_NAMES\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, MessageTextInput, StrInput\nfrom lfx.io import DropdownInput, MessageInput, MultilineInput, SecretStrInput, SliderInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# IBM watsonx.ai constants\nIBM_WATSONX_DEFAULT_MODELS = [\"ibm/granite-3-2b-instruct\", \"ibm/granite-3-8b-instruct\", \"ibm/granite-13b-instruct-v2\"]\nIBM_WATSONX_URLS = [\n \"https://us-south.ml.cloud.ibm.com\",\n \"https://eu-de.ml.cloud.ibm.com\",\n \"https://eu-gb.ml.cloud.ibm.com\",\n \"https://au-syd.ml.cloud.ibm.com\",\n \"https://jp-tok.ml.cloud.ibm.com\",\n \"https://ca-tor.ml.cloud.ibm.com\",\n]\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"completion\"\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 priority = 0 # Set priority to 0 to make it appear first\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\"version\": \"2024-09-16\", \"filters\": \"function_text_chat,!lifecycle_withdrawn\"}\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching IBM watsonx models. Using default models.\")\n return IBM_WATSONX_DEFAULT_MODELS\n\n async def is_valid_ollama_url(self, url: str) -> bool:\n \"\"\"Check if the provided URL is a valid Ollama API endpoint.\"\"\"\n try:\n async with httpx.AsyncClient() as client:\n url = transform_localhost_url(url)\n if not url:\n return False\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n url = url.rstrip(\"/\").removesuffix(\"/v1\")\n if not url.endswith(\"/\"):\n url = url + \"/\"\n return (await client.get(url=urljoin(url, \"api/tags\"))).status_code == HTTP_STATUS_OK\n except httpx.RequestError:\n return False\n\n async def get_ollama_models(self, base_url_value: str) -> list[str]:\n \"\"\"Fetch available completion models from the Ollama API.\n\n Filters out embedding models and only returns models with completion capability.\n\n Args:\n base_url_value (str): The base URL of the Ollama API.\n\n Returns:\n list[str]: A sorted list of model names that support completion.\n\n Raises:\n ValueError: If there is an issue with the API request or response.\n \"\"\"\n try:\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n base_url = base_url_value.rstrip(\"/\").removesuffix(\"/v1\")\n if not base_url.endswith(\"/\"):\n base_url = base_url + \"/\"\n base_url = transform_localhost_url(base_url)\n\n # Ollama REST API to return models\n tags_url = urljoin(base_url, \"api/tags\")\n\n # Ollama REST API to return model capabilities\n show_url = urljoin(base_url, \"api/show\")\n\n async with httpx.AsyncClient() as client:\n # Fetch available models\n tags_response = await client.get(url=tags_url)\n tags_response.raise_for_status()\n models = tags_response.json()\n if asyncio.iscoroutine(models):\n models = await models\n await logger.adebug(f\"Available models: {models}\")\n\n # Filter models that are NOT embedding models\n model_ids = []\n for model in models.get(JSON_MODELS_KEY, []):\n model_name = model.get(JSON_NAME_KEY)\n if not model_name:\n continue\n await logger.adebug(f\"Checking model: {model_name}\")\n\n payload = {\"model\": model_name}\n show_response = await client.post(url=show_url, json=payload)\n show_response.raise_for_status()\n json_data = show_response.json()\n if asyncio.iscoroutine(json_data):\n json_data = await json_data\n\n capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])\n await logger.adebug(f\"Model: {model_name}, Capabilities: {capabilities}\")\n\n if DESIRED_CAPABILITY in capabilities:\n model_ids.append(model_name)\n\n return sorted(model_ids)\n\n except (httpx.RequestError, ValueError) as e:\n msg = \"Could not get model names from Ollama.\"\n await logger.aexception(msg)\n raise ValueError(msg) from e\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Anthropic\", \"Google\", \"IBM watsonx.ai\", \"Ollama\"],\n value=\"OpenAI\",\n info=\"Select the model provider\",\n real_time_refresh=True,\n options_metadata=[\n {\"icon\": \"OpenAI\"},\n {\"icon\": \"Anthropic\"},\n {\"icon\": \"GoogleGenerativeAI\"},\n {\"icon\": \"WatsonxAI\"},\n {\"icon\": \"Ollama\"},\n ],\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n options=OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES,\n value=OPENAI_CHAT_MODEL_NAMES[0],\n info=\"Select the model to use\",\n real_time_refresh=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\n real_time_refresh=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 MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=\"Endpoint of the Ollama API (Ollama only). Defaults to http://localhost:11434\",\n value=\"http://localhost:11434\",\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 ]\n\n def build_model(self) -> LanguageModel:\n provider = self.provider\n model_name = self.model_name\n temperature = self.temperature\n stream = self.stream\n\n if provider == \"OpenAI\":\n if not self.api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n\n if model_name in OPENAI_REASONING_MODEL_NAMES:\n # reasoning models do not support temperature (yet)\n temperature = None\n\n return ChatOpenAI(\n model_name=model_name,\n temperature=temperature,\n streaming=stream,\n openai_api_key=self.api_key,\n )\n if provider == \"Anthropic\":\n if not self.api_key:\n msg = \"Anthropic API key is required when using Anthropic provider\"\n raise ValueError(msg)\n return ChatAnthropic(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n anthropic_api_key=self.api_key,\n )\n if provider == \"Google\":\n if not self.api_key:\n msg = \"Google API key is required when using Google provider\"\n raise ValueError(msg)\n return ChatGoogleGenerativeAIFixed(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n google_api_key=self.api_key,\n )\n if provider == \"IBM watsonx.ai\":\n if not self.api_key:\n msg = \"IBM API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.base_url_ibm_watsonx:\n msg = \"IBM watsonx API Endpoint is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.project_id:\n msg = \"IBM watsonx Project ID is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n return ChatWatsonx(\n apikey=SecretStr(self.api_key).get_secret_value(),\n url=self.base_url_ibm_watsonx,\n project_id=self.project_id,\n model_id=model_name,\n params={\n \"temperature\": temperature,\n },\n streaming=stream,\n )\n if provider == \"Ollama\":\n if not self.ollama_base_url:\n msg = \"Ollama API URL is required when using Ollama provider\"\n raise ValueError(msg)\n if not model_name:\n msg = \"Model name is required when using Ollama provider\"\n raise ValueError(msg)\n\n transformed_base_url = transform_localhost_url(self.ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return ChatOllama(\n base_url=transformed_base_url,\n model=model_name,\n temperature=temperature,\n )\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model_name\"][\"options\"] = OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES\n build_config[\"model_name\"][\"value\"] = OPENAI_CHAT_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Anthropic\":\n build_config[\"model_name\"][\"options\"] = ANTHROPIC_MODELS\n build_config[\"model_name\"][\"value\"] = ANTHROPIC_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Anthropic API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Google\":\n build_config[\"model_name\"][\"options\"] = GOOGLE_GENERATIVE_AI_MODELS\n build_config[\"model_name\"][\"value\"] = GOOGLE_GENERATIVE_AI_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Google API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model_name\"][\"options\"] = IBM_WATSONX_DEFAULT_MODELS\n build_config[\"model_name\"][\"value\"] = IBM_WATSONX_DEFAULT_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Ollama\":\n # Fetch Ollama models from the API\n ollama_url = build_config[\"ollama_base_url\"].get(\"value\", \"http://localhost:11434\")\n if await self.is_valid_ollama_url(ollama_url):\n try:\n models = await self.get_ollama_models(base_url_value=ollama_url)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama models. Setting empty options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = True\n elif (\n field_name == \"base_url_ibm_watsonx\"\n and field_value\n and hasattr(self, \"provider\")\n and self.provider == \"IBM watsonx.ai\"\n ):\n # Fetch IBM models when base_url changes\n try:\n models = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else IBM_WATSONX_DEFAULT_MODELS[0]\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n logger.info(info_message)\n except Exception: # noqa: BLE001\n logger.exception(\"Error updating IBM model options.\")\n elif (\n field_name == \"ollama_base_url\" and field_value and hasattr(self, \"provider\") and self.provider == \"Ollama\"\n ):\n # Fetch Ollama models when ollama_base_url changes\n if await self.is_valid_ollama_url(field_value):\n try:\n models = await self.get_ollama_models(base_url_value=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n await logger.ainfo(info_message)\n except ValueError:\n await logger.awarning(\"Error updating Ollama model options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n elif field_name == \"model_name\" and field_value.startswith(\"o1\") and self.provider == \"OpenAI\":\n # Hide system_message for o1 models - currently unsupported\n if \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = False\n elif field_name == \"model_name\" and not field_value.startswith(\"o1\") and \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = True\n return build_config\n"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unnecessary and likely incorrect asyncio.iscoroutine() checks after .json() calls.

In the get_ollama_models method, there are checks like:

models = tags_response.json()
if asyncio.iscoroutine(models):
    models = await models

The httpx.Response.json() method is synchronous and returns a dictionary, not a coroutine. These checks will always evaluate to False, making them dead code. Remove these unnecessary checks to simplify the code and avoid confusion.

- models = tags_response.json()
- if asyncio.iscoroutine(models):
-     models = await models
+ models = tags_response.json()

Apply the same fix to all occurrences in the method (around the show_response.json() call as well).

Also applies to: 3403-3404

🤖 Prompt for AI Agents
In src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json
around lines 3076-3077 (and also remove the same dead checks around lines
~3403-3404), remove the unnecessary asyncio.iscoroutine() checks after calling
httpx.Response.json() in get_ollama_models (i.e., delete the if
asyncio.iscoroutine(models): models = await models and the similar block for
json_data after show_response.json()); treat .json() as synchronous, stop
awaiting non-coroutines, and simplify the flow to use the returned dicts
directly.

Comment on lines 1404 to 1405
"value": "import asyncio\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nimport requests\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_ibm import ChatWatsonx\nfrom langchain_ollama import ChatOllama\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom lfx.base.models.anthropic_constants import ANTHROPIC_MODELS\nfrom lfx.base.models.google_generative_ai_constants import GOOGLE_GENERATIVE_AI_MODELS\nfrom lfx.base.models.google_generative_ai_model import ChatGoogleGenerativeAIFixed\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.openai_constants import OPENAI_CHAT_MODEL_NAMES, OPENAI_REASONING_MODEL_NAMES\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, MessageTextInput, StrInput\nfrom lfx.io import DropdownInput, MessageInput, MultilineInput, SecretStrInput, SliderInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# IBM watsonx.ai constants\nIBM_WATSONX_DEFAULT_MODELS = [\"ibm/granite-3-2b-instruct\", \"ibm/granite-3-8b-instruct\", \"ibm/granite-13b-instruct-v2\"]\nIBM_WATSONX_URLS = [\n \"https://us-south.ml.cloud.ibm.com\",\n \"https://eu-de.ml.cloud.ibm.com\",\n \"https://eu-gb.ml.cloud.ibm.com\",\n \"https://au-syd.ml.cloud.ibm.com\",\n \"https://jp-tok.ml.cloud.ibm.com\",\n \"https://ca-tor.ml.cloud.ibm.com\",\n]\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"completion\"\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 priority = 0 # Set priority to 0 to make it appear first\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\"version\": \"2024-09-16\", \"filters\": \"function_text_chat,!lifecycle_withdrawn\"}\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching IBM watsonx models. Using default models.\")\n return IBM_WATSONX_DEFAULT_MODELS\n\n async def is_valid_ollama_url(self, url: str) -> bool:\n \"\"\"Check if the provided URL is a valid Ollama API endpoint.\"\"\"\n try:\n async with httpx.AsyncClient() as client:\n url = transform_localhost_url(url)\n if not url:\n return False\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n url = url.rstrip(\"/\").removesuffix(\"/v1\")\n if not url.endswith(\"/\"):\n url = url + \"/\"\n return (await client.get(url=urljoin(url, \"api/tags\"))).status_code == HTTP_STATUS_OK\n except httpx.RequestError:\n return False\n\n async def get_ollama_models(self, base_url_value: str) -> list[str]:\n \"\"\"Fetch available completion models from the Ollama API.\n\n Filters out embedding models and only returns models with completion capability.\n\n Args:\n base_url_value (str): The base URL of the Ollama API.\n\n Returns:\n list[str]: A sorted list of model names that support completion.\n\n Raises:\n ValueError: If there is an issue with the API request or response.\n \"\"\"\n try:\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n base_url = base_url_value.rstrip(\"/\").removesuffix(\"/v1\")\n if not base_url.endswith(\"/\"):\n base_url = base_url + \"/\"\n base_url = transform_localhost_url(base_url)\n\n # Ollama REST API to return models\n tags_url = urljoin(base_url, \"api/tags\")\n\n # Ollama REST API to return model capabilities\n show_url = urljoin(base_url, \"api/show\")\n\n async with httpx.AsyncClient() as client:\n # Fetch available models\n tags_response = await client.get(url=tags_url)\n tags_response.raise_for_status()\n models = tags_response.json()\n if asyncio.iscoroutine(models):\n models = await models\n await logger.adebug(f\"Available models: {models}\")\n\n # Filter models that are NOT embedding models\n model_ids = []\n for model in models.get(JSON_MODELS_KEY, []):\n model_name = model.get(JSON_NAME_KEY)\n if not model_name:\n continue\n await logger.adebug(f\"Checking model: {model_name}\")\n\n payload = {\"model\": model_name}\n show_response = await client.post(url=show_url, json=payload)\n show_response.raise_for_status()\n json_data = show_response.json()\n if asyncio.iscoroutine(json_data):\n json_data = await json_data\n\n capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])\n await logger.adebug(f\"Model: {model_name}, Capabilities: {capabilities}\")\n\n if DESIRED_CAPABILITY in capabilities:\n model_ids.append(model_name)\n\n return sorted(model_ids)\n\n except (httpx.RequestError, ValueError) as e:\n msg = \"Could not get model names from Ollama.\"\n await logger.aexception(msg)\n raise ValueError(msg) from e\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Anthropic\", \"Google\", \"IBM watsonx.ai\", \"Ollama\"],\n value=\"OpenAI\",\n info=\"Select the model provider\",\n real_time_refresh=True,\n options_metadata=[\n {\"icon\": \"OpenAI\"},\n {\"icon\": \"Anthropic\"},\n {\"icon\": \"GoogleGenerativeAI\"},\n {\"icon\": \"WatsonxAI\"},\n {\"icon\": \"Ollama\"},\n ],\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n options=OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES,\n value=OPENAI_CHAT_MODEL_NAMES[0],\n info=\"Select the model to use\",\n real_time_refresh=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\n real_time_refresh=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 MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=\"Endpoint of the Ollama API (Ollama only). Defaults to http://localhost:11434\",\n value=\"http://localhost:11434\",\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 ]\n\n def build_model(self) -> LanguageModel:\n provider = self.provider\n model_name = self.model_name\n temperature = self.temperature\n stream = self.stream\n\n if provider == \"OpenAI\":\n if not self.api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n\n if model_name in OPENAI_REASONING_MODEL_NAMES:\n # reasoning models do not support temperature (yet)\n temperature = None\n\n return ChatOpenAI(\n model_name=model_name,\n temperature=temperature,\n streaming=stream,\n openai_api_key=self.api_key,\n )\n if provider == \"Anthropic\":\n if not self.api_key:\n msg = \"Anthropic API key is required when using Anthropic provider\"\n raise ValueError(msg)\n return ChatAnthropic(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n anthropic_api_key=self.api_key,\n )\n if provider == \"Google\":\n if not self.api_key:\n msg = \"Google API key is required when using Google provider\"\n raise ValueError(msg)\n return ChatGoogleGenerativeAIFixed(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n google_api_key=self.api_key,\n )\n if provider == \"IBM watsonx.ai\":\n if not self.api_key:\n msg = \"IBM API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.base_url_ibm_watsonx:\n msg = \"IBM watsonx API Endpoint is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.project_id:\n msg = \"IBM watsonx Project ID is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n return ChatWatsonx(\n apikey=SecretStr(self.api_key).get_secret_value(),\n url=self.base_url_ibm_watsonx,\n project_id=self.project_id,\n model_id=model_name,\n params={\n \"temperature\": temperature,\n },\n streaming=stream,\n )\n if provider == \"Ollama\":\n if not self.ollama_base_url:\n msg = \"Ollama API URL is required when using Ollama provider\"\n raise ValueError(msg)\n if not model_name:\n msg = \"Model name is required when using Ollama provider\"\n raise ValueError(msg)\n\n transformed_base_url = transform_localhost_url(self.ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return ChatOllama(\n base_url=transformed_base_url,\n model=model_name,\n temperature=temperature,\n )\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model_name\"][\"options\"] = OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES\n build_config[\"model_name\"][\"value\"] = OPENAI_CHAT_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Anthropic\":\n build_config[\"model_name\"][\"options\"] = ANTHROPIC_MODELS\n build_config[\"model_name\"][\"value\"] = ANTHROPIC_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Anthropic API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Google\":\n build_config[\"model_name\"][\"options\"] = GOOGLE_GENERATIVE_AI_MODELS\n build_config[\"model_name\"][\"value\"] = GOOGLE_GENERATIVE_AI_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Google API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model_name\"][\"options\"] = IBM_WATSONX_DEFAULT_MODELS\n build_config[\"model_name\"][\"value\"] = IBM_WATSONX_DEFAULT_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Ollama\":\n # Fetch Ollama models from the API\n ollama_url = build_config[\"ollama_base_url\"].get(\"value\", \"http://localhost:11434\")\n if await self.is_valid_ollama_url(ollama_url):\n try:\n models = await self.get_ollama_models(base_url_value=ollama_url)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama models. Setting empty options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = True\n elif (\n field_name == \"base_url_ibm_watsonx\"\n and field_value\n and hasattr(self, \"provider\")\n and self.provider == \"IBM watsonx.ai\"\n ):\n # Fetch IBM models when base_url changes\n try:\n models = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else IBM_WATSONX_DEFAULT_MODELS[0]\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n logger.info(info_message)\n except Exception: # noqa: BLE001\n logger.exception(\"Error updating IBM model options.\")\n elif (\n field_name == \"ollama_base_url\" and field_value and hasattr(self, \"provider\") and self.provider == \"Ollama\"\n ):\n # Fetch Ollama models when ollama_base_url changes\n if await self.is_valid_ollama_url(field_value):\n try:\n models = await self.get_ollama_models(base_url_value=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n await logger.ainfo(info_message)\n except ValueError:\n await logger.awarning(\"Error updating Ollama model options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n elif field_name == \"model_name\" and field_value.startswith(\"o1\") and self.provider == \"OpenAI\":\n # Hide system_message for o1 models - currently unsupported\n if \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = False\n elif field_name == \"model_name\" and not field_value.startswith(\"o1\") and \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = True\n return build_config\n"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add HTTP timeouts and connection limits to all httpx calls.

All AsyncClient calls lack timeouts/limits; a slow/blocked Ollama endpoint can hang the event loop and UI. Add sane per‑request timeouts and client limits.

Apply along these lines:

-    async with httpx.AsyncClient() as client:
+    limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
+    timeout = httpx.Timeout(connect=5.0, read=10.0, write=10.0, pool=5.0)
+    async with httpx.AsyncClient(limits=limits, timeout=timeout) as client:
         # ...
-        tags_response = await client.get(url=tags_url)
+        tags_response = await client.get(url=tags_url, timeout=timeout)
         # ...
-        show_response = await client.post(url=show_url, json=payload)
+        show_response = await client.post(url=show_url, json=payload, timeout=timeout)

Committable suggestion skipped: line range outside the PR's diff.


⚠️ Potential issue | 🟠 Major

Avoid blocking requests in async path for IBM models.

fetch_ibm_models uses requests.get and is called from async update_build_config, blocking the loop.

Convert to async httpx or offload:

-@staticmethod
-def fetch_ibm_models(base_url: str) -> list[str]:
+@staticmethod
+async def fetch_ibm_models(base_url: str) -> list[str]:
     try:
-        response = requests.get(endpoint, params=params, timeout=10)
+        limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
+        timeout = httpx.Timeout(connect=5.0, read=10.0)
+        async with httpx.AsyncClient(limits=limits, timeout=timeout) as client:
+            response = await client.get(endpoint, params=params)
         # ...
     except Exception:
         # ...

And in update_build_config:

- models = self.fetch_ibm_models(base_url=field_value)
+ models = await self.fetch_ibm_models(base_url=field_value)
🤖 Prompt for AI Agents
In src/backend/base/langflow/initial_setup/starter_projects/Research Translation
Loop.json around lines 1404-1405, fetch_ibm_models blocks the event loop because
it uses synchronous requests.get and is called from the async
update_build_config; make it non-blocking by either (A) converting
fetch_ibm_models to an async function using httpx.AsyncClient (await the GET,
raise_for_status, parse json, return sorted list) and update all callers to
await it, or (B) wrap the existing synchronous fetch_ibm_models call in
update_build_config with asyncio.to_thread (or loop.run_in_executor) so the
blocking call runs off the event loop; also adapt exception handling/logging to
use async logger methods when used from async context and preserve the same
fallback IBM_WATSONX_DEFAULT_MODELS behavior.

Comment on lines 1969 to 1970
"value": "import asyncio\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nimport requests\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_ibm import ChatWatsonx\nfrom langchain_ollama import ChatOllama\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom lfx.base.models.anthropic_constants import ANTHROPIC_MODELS\nfrom lfx.base.models.google_generative_ai_constants import GOOGLE_GENERATIVE_AI_MODELS\nfrom lfx.base.models.google_generative_ai_model import ChatGoogleGenerativeAIFixed\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.openai_constants import OPENAI_CHAT_MODEL_NAMES, OPENAI_REASONING_MODEL_NAMES\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, MessageTextInput, StrInput\nfrom lfx.io import DropdownInput, MessageInput, MultilineInput, SecretStrInput, SliderInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# IBM watsonx.ai constants\nIBM_WATSONX_DEFAULT_MODELS = [\"ibm/granite-3-2b-instruct\", \"ibm/granite-3-8b-instruct\", \"ibm/granite-13b-instruct-v2\"]\nIBM_WATSONX_URLS = [\n \"https://us-south.ml.cloud.ibm.com\",\n \"https://eu-de.ml.cloud.ibm.com\",\n \"https://eu-gb.ml.cloud.ibm.com\",\n \"https://au-syd.ml.cloud.ibm.com\",\n \"https://jp-tok.ml.cloud.ibm.com\",\n \"https://ca-tor.ml.cloud.ibm.com\",\n]\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"completion\"\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 priority = 0 # Set priority to 0 to make it appear first\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\"version\": \"2024-09-16\", \"filters\": \"function_text_chat,!lifecycle_withdrawn\"}\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching IBM watsonx models. Using default models.\")\n return IBM_WATSONX_DEFAULT_MODELS\n\n async def is_valid_ollama_url(self, url: str) -> bool:\n \"\"\"Check if the provided URL is a valid Ollama API endpoint.\"\"\"\n try:\n async with httpx.AsyncClient() as client:\n url = transform_localhost_url(url)\n if not url:\n return False\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n url = url.rstrip(\"/\").removesuffix(\"/v1\")\n if not url.endswith(\"/\"):\n url = url + \"/\"\n return (await client.get(url=urljoin(url, \"api/tags\"))).status_code == HTTP_STATUS_OK\n except httpx.RequestError:\n return False\n\n async def get_ollama_models(self, base_url_value: str) -> list[str]:\n \"\"\"Fetch available completion models from the Ollama API.\n\n Filters out embedding models and only returns models with completion capability.\n\n Args:\n base_url_value (str): The base URL of the Ollama API.\n\n Returns:\n list[str]: A sorted list of model names that support completion.\n\n Raises:\n ValueError: If there is an issue with the API request or response.\n \"\"\"\n try:\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n base_url = base_url_value.rstrip(\"/\").removesuffix(\"/v1\")\n if not base_url.endswith(\"/\"):\n base_url = base_url + \"/\"\n base_url = transform_localhost_url(base_url)\n\n # Ollama REST API to return models\n tags_url = urljoin(base_url, \"api/tags\")\n\n # Ollama REST API to return model capabilities\n show_url = urljoin(base_url, \"api/show\")\n\n async with httpx.AsyncClient() as client:\n # Fetch available models\n tags_response = await client.get(url=tags_url)\n tags_response.raise_for_status()\n models = tags_response.json()\n if asyncio.iscoroutine(models):\n models = await models\n await logger.adebug(f\"Available models: {models}\")\n\n # Filter models that are NOT embedding models\n model_ids = []\n for model in models.get(JSON_MODELS_KEY, []):\n model_name = model.get(JSON_NAME_KEY)\n if not model_name:\n continue\n await logger.adebug(f\"Checking model: {model_name}\")\n\n payload = {\"model\": model_name}\n show_response = await client.post(url=show_url, json=payload)\n show_response.raise_for_status()\n json_data = show_response.json()\n if asyncio.iscoroutine(json_data):\n json_data = await json_data\n\n capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])\n await logger.adebug(f\"Model: {model_name}, Capabilities: {capabilities}\")\n\n if DESIRED_CAPABILITY in capabilities:\n model_ids.append(model_name)\n\n return sorted(model_ids)\n\n except (httpx.RequestError, ValueError) as e:\n msg = \"Could not get model names from Ollama.\"\n await logger.aexception(msg)\n raise ValueError(msg) from e\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Anthropic\", \"Google\", \"IBM watsonx.ai\", \"Ollama\"],\n value=\"OpenAI\",\n info=\"Select the model provider\",\n real_time_refresh=True,\n options_metadata=[\n {\"icon\": \"OpenAI\"},\n {\"icon\": \"Anthropic\"},\n {\"icon\": \"GoogleGenerativeAI\"},\n {\"icon\": \"WatsonxAI\"},\n {\"icon\": \"Ollama\"},\n ],\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n options=OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES,\n value=OPENAI_CHAT_MODEL_NAMES[0],\n info=\"Select the model to use\",\n real_time_refresh=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\n real_time_refresh=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 MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=\"Endpoint of the Ollama API (Ollama only). Defaults to http://localhost:11434\",\n value=\"http://localhost:11434\",\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 ]\n\n def build_model(self) -> LanguageModel:\n provider = self.provider\n model_name = self.model_name\n temperature = self.temperature\n stream = self.stream\n\n if provider == \"OpenAI\":\n if not self.api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n\n if model_name in OPENAI_REASONING_MODEL_NAMES:\n # reasoning models do not support temperature (yet)\n temperature = None\n\n return ChatOpenAI(\n model_name=model_name,\n temperature=temperature,\n streaming=stream,\n openai_api_key=self.api_key,\n )\n if provider == \"Anthropic\":\n if not self.api_key:\n msg = \"Anthropic API key is required when using Anthropic provider\"\n raise ValueError(msg)\n return ChatAnthropic(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n anthropic_api_key=self.api_key,\n )\n if provider == \"Google\":\n if not self.api_key:\n msg = \"Google API key is required when using Google provider\"\n raise ValueError(msg)\n return ChatGoogleGenerativeAIFixed(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n google_api_key=self.api_key,\n )\n if provider == \"IBM watsonx.ai\":\n if not self.api_key:\n msg = \"IBM API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.base_url_ibm_watsonx:\n msg = \"IBM watsonx API Endpoint is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.project_id:\n msg = \"IBM watsonx Project ID is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n return ChatWatsonx(\n apikey=SecretStr(self.api_key).get_secret_value(),\n url=self.base_url_ibm_watsonx,\n project_id=self.project_id,\n model_id=model_name,\n params={\n \"temperature\": temperature,\n },\n streaming=stream,\n )\n if provider == \"Ollama\":\n if not self.ollama_base_url:\n msg = \"Ollama API URL is required when using Ollama provider\"\n raise ValueError(msg)\n if not model_name:\n msg = \"Model name is required when using Ollama provider\"\n raise ValueError(msg)\n\n transformed_base_url = transform_localhost_url(self.ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return ChatOllama(\n base_url=transformed_base_url,\n model=model_name,\n temperature=temperature,\n )\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model_name\"][\"options\"] = OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES\n build_config[\"model_name\"][\"value\"] = OPENAI_CHAT_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Anthropic\":\n build_config[\"model_name\"][\"options\"] = ANTHROPIC_MODELS\n build_config[\"model_name\"][\"value\"] = ANTHROPIC_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Anthropic API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Google\":\n build_config[\"model_name\"][\"options\"] = GOOGLE_GENERATIVE_AI_MODELS\n build_config[\"model_name\"][\"value\"] = GOOGLE_GENERATIVE_AI_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Google API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model_name\"][\"options\"] = IBM_WATSONX_DEFAULT_MODELS\n build_config[\"model_name\"][\"value\"] = IBM_WATSONX_DEFAULT_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Ollama\":\n # Fetch Ollama models from the API\n ollama_url = build_config[\"ollama_base_url\"].get(\"value\", \"http://localhost:11434\")\n if await self.is_valid_ollama_url(ollama_url):\n try:\n models = await self.get_ollama_models(base_url_value=ollama_url)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama models. Setting empty options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = True\n elif (\n field_name == \"base_url_ibm_watsonx\"\n and field_value\n and hasattr(self, \"provider\")\n and self.provider == \"IBM watsonx.ai\"\n ):\n # Fetch IBM models when base_url changes\n try:\n models = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else IBM_WATSONX_DEFAULT_MODELS[0]\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n logger.info(info_message)\n except Exception: # noqa: BLE001\n logger.exception(\"Error updating IBM model options.\")\n elif (\n field_name == \"ollama_base_url\" and field_value and hasattr(self, \"provider\") and self.provider == \"Ollama\"\n ):\n # Fetch Ollama models when ollama_base_url changes\n if await self.is_valid_ollama_url(field_value):\n try:\n models = await self.get_ollama_models(base_url_value=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n await logger.ainfo(info_message)\n except ValueError:\n await logger.awarning(\"Error updating Ollama model options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n elif field_name == \"model_name\" and field_value.startswith(\"o1\") and self.provider == \"OpenAI\":\n # Hide system_message for o1 models - currently unsupported\n if \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = False\n elif field_name == \"model_name\" and not field_value.startswith(\"o1\") and \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = True\n return build_config\n"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden Ollama discovery: add timeouts and backward‑compat for older servers.

  • httpx calls have no timeouts; async update paths can hang the UI.
  • Filtering assumes /api/show returns "capabilities"; older Ollama may omit it, yielding empty model lists.

Apply this minimal patch inside LanguageModelComponent:

-    async def is_valid_ollama_url(self, url: str) -> bool:
+    async def is_valid_ollama_url(self, url: str) -> bool:
         """Check if the provided URL is a valid Ollama API endpoint."""
         try:
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=5.0)) as client:
                 url = transform_localhost_url(url)
                 if not url:
                     return False
                 # Strip /v1 suffix if present, as Ollama API endpoints are at root level
                 url = url.rstrip("/").removesuffix("/v1")
                 if not url.endswith("/"):
                     url = url + "/"
                 return (await client.get(url=urljoin(url, "api/tags"))).status_code == HTTP_STATUS_OK
         except httpx.RequestError:
             return False
@@
-    async def get_ollama_models(self, base_url_value: str) -> list[str]:
+    async def get_ollama_models(self, base_url_value: str) -> list[str]:
         """Fetch available completion models from the Ollama API.
@@
-            async with httpx.AsyncClient() as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=5.0)) as client:
                 # Fetch available models
                 tags_response = await client.get(url=tags_url)
                 tags_response.raise_for_status()
                 models = tags_response.json()
-                if asyncio.iscoroutine(models):
-                    models = await models
                 await logger.adebug(f"Available models: {models}")
@@
-                    json_data = show_response.json()
-                    if asyncio.iscoroutine(json_data):
-                        json_data = await json_data
-
-                    capabilities = json_data.get(JSON_CAPABILITIES_KEY, [])
-                    await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")
-
-                    if DESIRED_CAPABILITY in capabilities:
-                        model_ids.append(model_name)
+                    json_data = show_response.json()
+                    capabilities = json_data.get(JSON_CAPABILITIES_KEY)
+                    families = json_data.get("families", []) or json_data.get("family", [])
+                    # Backward-compat: some Ollama versions don't expose "capabilities".
+                    # Treat models as completion unless clearly embedding by name/family.
+                    is_embedding = any(s in model_name.lower() for s in ("embed", "embedding")) or \
+                                   ("embedding" in " ".join(map(str, families)).lower())
+                    if capabilities:
+                        await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")
+                        if DESIRED_CAPABILITY in capabilities and not is_embedding:
+                            model_ids.append(model_name)
+                    else:
+                        await logger.adebug(f"Model: {model_name}, no capabilities in /api/show; fallback include (not embedding).")
+                        if not is_embedding:
+                            model_ids.append(model_name)
  • Optional: parallelize /api/show with a bounded semaphore to improve latency when many tags exist. I can provide a small gather-based refactor if desired.
  • Optional: the asyncio.iscoroutine checks around Response.json() are unnecessary; httpx returns a concrete object.
🤖 Prompt for AI Agents
In src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread
Generator.json around lines 1969-1970, the Ollama discovery code uses httpx
calls without timeouts and assumes the /api/show response always contains
"capabilities", which can hang the UI and drop models for older servers; update
get_ollama_models and is_valid_ollama_url to pass explicit timeouts to httpx
requests (e.g., timeout=10) and catch httpx.TimeoutException/RequestError, treat
missing "capabilities" as an empty list when filtering models, and remove the
unnecessary asyncio.iscoroutine checks around response.json(); optionally bound
concurrent /api/show POSTs with an asyncio.Semaphore (or a small gather with
limited concurrency) to avoid overwhelming the server.

Comment on lines +423 to +438
logger.debug(f"Fetching Ollama models from updated URL: {build_config['ollama_base_url']} and value {self.ollama_base_url}")
await logger.adebug(f"Fetching Ollama models from updated URL: {self.ollama_base_url}")
if await self.is_valid_ollama_url(self.ollama_base_url):
try:
models = await self.get_ollama_models(base_url_value=self.ollama_base_url)
build_config["model_name"]["options"] = models
build_config["model_name"]["value"] = models[0] if models else ""
info_message = f"Updated model options: {len(models)} models found in {self.ollama_base_url}"
await logger.ainfo(info_message)
except ValueError:
await logger.awarning("Error updating Ollama model options.")
build_config["model_name"]["options"] = []
build_config["model_name"]["value"] = ""
else:
await logger.awarning(f"Invalid Ollama URL: {self.ollama_base_url}")
build_config["model_name"]["options"] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the freshly supplied Ollama URL when refreshing model options.

We enter update_build_config precisely because the user changed ollama_base_url, but this block still validates/fetches against self.ollama_base_url. On the first edit the attribute still carries the previous value, so the UI keeps querying the old host and never refreshes model options for the newly entered endpoint. At the same time the 136-character f-string here is the Ruff E501 failure breaking CI. Please normalize the new value (with the same fallback logic used when switching providers), feed it into validation/fetch, and shorten the log line. For example:

-            logger.debug(f"Fetching Ollama models from updated URL: {build_config['ollama_base_url']} and value {self.ollama_base_url}")
-            await logger.adebug(f"Fetching Ollama models from updated URL: {self.ollama_base_url}")
-            if await self.is_valid_ollama_url(self.ollama_base_url):
+            normalized_url = field_value or getattr(self, "ollama_base_url", DEFAULT_OLLAMA_URL)
+            if (
+                normalized_url
+                and isinstance(normalized_url, str)
+                and normalized_url.isupper()
+                and "_" in normalized_url
+            ):
+                await logger.adebug(
+                    f"Config value appears to be a variable reference: {normalized_url}, using default"
+                )
+                normalized_url = DEFAULT_OLLAMA_URL
+            logger.debug("Fetching Ollama models from updated URL: %s", normalized_url)
+            await logger.adebug(f"Fetching Ollama models from updated URL: {normalized_url}")
+            if await self.is_valid_ollama_url(normalized_url):
                 try:
-                    models = await self.get_ollama_models(base_url_value=self.ollama_base_url)
+                    models = await self.get_ollama_models(base_url_value=normalized_url)
                     build_config["model_name"]["options"] = models
                     build_config["model_name"]["value"] = models[0] if models else ""
                 except ValueError:
                     await logger.awarning("Error updating Ollama model options.")
                     build_config["model_name"]["options"] = []
                     build_config["model_name"]["value"] = ""
             else:
-                await logger.awarning(f"Invalid Ollama URL: {self.ollama_base_url}")
+                await logger.awarning(f"Invalid Ollama URL: {normalized_url}")
                 build_config["model_name"]["options"] = []
                 build_config["model_name"]["value"] = ""

This keeps the dropdown in sync with the user's latest URL and resolves the lint failure.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: Ruff Style Check

[error] 423-423: E501 Line too long (136 > 120). Ruff check failed. File line exceeds max length. Command: 'uv run --only-dev ruff check --output-format=github .'

🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 423-423: Ruff (E501)
src/lfx/src/lfx/components/models/language_model.py:423:121: E501 Line too long (136 > 120)

🤖 Prompt for AI Agents
In src/lfx/src/lfx/components/models/language_model.py around lines 423 to 438,
the code still validates and fetches Ollama models using self.ollama_base_url
and logs a very long f-string; instead normalize and derive the new URL value
(using the same fallback logic used when switching providers) from the incoming
build_config/new input, assign it to a local variable (e.g. new_ollama_url),
shorten the log message, then call is_valid_ollama_url(new_ollama_url) and
get_ollama_models(base_url_value=new_ollama_url) so validation/fetch use the
freshly supplied URL; on success update build_config["model_name"]["options"]
and ["value"] based on models, and on ValueError or invalid URL set
options/value to []/"" and log a concise warning.

@codecov

codecov Bot commented Nov 10, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 39.37%. Comparing base (403b939) to head (ce93808).
⚠️ Report is 1 commits behind head on main.

❌ Your project status has failed because the head coverage (39.35%) is below the target coverage (60.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #10551      +/-   ##
==========================================
+ Coverage   38.90%   39.37%   +0.46%     
==========================================
  Files        1477     1476       -1     
  Lines       85270    82169    -3101     
  Branches    10240     8983    -1257     
==========================================
- Hits        33175    32350     -825     
+ Misses      51048    48912    -2136     
+ Partials     1047      907     -140     
Flag Coverage Δ
frontend 13.57% <ø> (-0.78%) ⬇️
lfx 39.35% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 86 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants