ref: Refactor Ollama model fetching to use async and filter capabilities - #10551
ref: Refactor Ollama model fetching to use async and filter capabilities#10551edwinjosechittilappilly wants to merge 6 commits into
Conversation
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.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughConverts Ollama model discovery from synchronous to asynchronous across 17 starter projects and core language model components. Introduces async helpers ( Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45–75 minutes Areas requiring extra attention:
Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touchesImportant Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 3 warnings)
✅ Passed checks (3 passed)
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. Comment |
There was a problem hiding this comment.
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_configmethod 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.
| if asyncio.iscoroutine(models): | ||
| models = await models |
There was a problem hiding this comment.
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.
| if asyncio.iscoroutine(json_data): | ||
| json_data = await json_data |
There was a problem hiding this comment.
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.
| 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( |
There was a problem hiding this comment.
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.
| logger.debug( |
There was a problem hiding this comment.
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 callsself.fetch_ibm_models(), a synchronous static method usingrequests.get(), from within an async method. This will block the event loop. Consider makingfetch_ibm_modelsasync 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_modelsto be async usinghttpx.AsyncClient.
2076-2100: Remove unnecessaryasyncio.iscoroutinechecks onresponse.json()calls.httpx.Response.json() is synchronous and returns a dict, not a coroutine. The checks
if asyncio.iscoroutine(models):andif asyncio.iscoroutine(json_data):will always beFalseand should be removed.Fix in:
src/lfx/src/lfx/components/ollama/ollama.pysrc/lfx/src/lfx/components/models/language_model.pyRemove 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_datasrc/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 exposetext. This will KeyError when switching mode to Parser.- "value": "Text: {dt}" + "value": "Text: {text}"Optionally make DataFrame formatting use
format_mapwith a default dict to avoid KeyErrors. Based on learningssrc/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 providersThis 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 providersThe 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_ibmMirror the dependency metadata fix from Blog Writer to list both packages and bump total_dependencies.
1055-1089: Expose IBM/Ollama in provider dropdownUpdate 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 metadataAdd langchain_ollama and langchain_ibm (and adjust total_dependencies).
1035-1069: Provider dropdown needs IBM/OllamaAdd "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_MODELSAnd 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, theupdate_build_configmethod attempts to accessbuild_config["base_url_ibm_watsonx"],build_config["project_id"], andbuild_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
KeyErrorwhen 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 learningssrc/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 checksPlease 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 concernsApply 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 fixThe 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 unnecessaryasyncio.iscoroutine()defensive checks.Lines within
get_ollama_modelscheck iftags_response.json()andshow_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 unnecessaryasyncio.iscoroutine()checks in model fetching.Lines 2602–2603 and 2620–2621 contain unreachable checks:
asyncio.iscoroutine()will never beTrueformodelsorjson_datasincehttpx.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_datasrc/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
awaitkeyword. If they are coroutines, all must be awaited.Can you confirm whether
logger.adebug(),logger.aexception(),logger.ainfo(), andlogger.awarning()are coroutines that requireawait? 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 providerThis 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 normalizedThen use it consistently across all three locations.
2628-2680: Verify error handling and logging inget_ollama_models()provides sufficient context.In the error handler for
get_ollama_models(), the exception is caught as(httpx.RequestError, ValueError)and logged withawait logger.aexception(msg). While this captures exceptions, ensure:
- The error message includes enough context (e.g., the URL that failed, the response status code if available).
- Network timeouts or connection refusals are clearly communicated to users.
- 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:
- Whether to log this at
warningorerrorlevel- Whether users need more context (e.g., "Could not connect to Ollama at {url}")
- 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). Withhttpx.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/showcalls.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 forlangchain_ollama.Component imports
langchain_ollamabut it isn’t listed inmetadata.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: Addlangchain_ollamato 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()returnsFalseon any request error (safe default)get_ollama_models()raisesValueErrorwhich is caught and logged inupdate_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
| "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" | ||
| }, |
There was a problem hiding this comment.
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
LimitsandTimeout. fetch_ibm_modelsusesrequests.getbut is awaited indirectly from asyncupdate_build_config→ block. Make it async httpx andawaitit.- Sequential per‑model
/api/showfan‑out. Use boundedgather.
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.
| "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" | ||
| }, |
There was a problem hiding this comment.
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.
| "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" | ||
| }, |
There was a problem hiding this comment.
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 modelsThe 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.
| "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" | ||
| }, |
There was a problem hiding this comment.
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.
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.
| "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" | ||
| }, |
There was a problem hiding this comment.
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.
| 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"] = [] |
There was a problem hiding this comment.
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 Report✅ All modified and coverable lines are covered by tests. ❌ 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@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
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
Bug Fixes