Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/lfx/src/lfx/_assets/component_index.json

Large diffs are not rendered by default.

198 changes: 163 additions & 35 deletions src/lfx/src/lfx/components/models/language_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import asyncio
from typing import Any
from urllib.parse import urljoin

import httpx
import requests
from langchain_anthropic import ChatAnthropic
from langchain_ibm import ChatWatsonx
Expand Down Expand Up @@ -32,6 +34,14 @@
"https://ca-tor.ml.cloud.ibm.com",
]

# Ollama API constants
HTTP_STATUS_OK = 200
JSON_MODELS_KEY = "models"
JSON_NAME_KEY = "name"
JSON_CAPABILITIES_KEY = "capabilities"
DESIRED_CAPABILITY = "completion"
DEFAULT_OLLAMA_URL = "http://localhost:11434"


class LanguageModelComponent(LCModelComponent):
display_name = "Language Model"
Expand All @@ -56,27 +66,85 @@
logger.exception("Error fetching IBM watsonx models. Using default models.")
return IBM_WATSONX_DEFAULT_MODELS

@staticmethod
def fetch_ollama_models(base_url: str) -> list[str]:
"""Fetch available models from the Ollama API."""
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:
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:
logger.debug(f"Invalid Ollama URL: {url}")
return False

async def get_ollama_models(self, base_url_value: str) -> list[str]:
"""Fetch available completion models from the Ollama API.

Filters out embedding models and only returns models with completion capability.

Args:
base_url_value (str): The base URL of the Ollama API.

Returns:
list[str]: A sorted list of model names that support completion.

Raises:
ValueError: If there is an issue with the API request or response.
"""
try:
# Strip /v1 suffix if present, as Ollama API endpoints are at root level
base_url = base_url.rstrip("/").removesuffix("/v1")
base_url = base_url_value.rstrip("/").removesuffix("/v1")
if not base_url.endswith("/"):
base_url = base_url + "/"
base_url = transform_localhost_url(base_url)

# Ollama REST API to return models
tags_url = urljoin(base_url, "api/tags")

response = requests.get(tags_url, timeout=10)
response.raise_for_status()
data = response.json()
models = [model["name"] for model in data.get("models", [])]
return sorted(models)
except Exception: # noqa: BLE001
logger.exception("Error fetching Ollama models. Returning empty list.")
return []
# Ollama REST API to return model capabilities
show_url = urljoin(base_url, "api/show")

async with httpx.AsyncClient() 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
Comment on lines +117 to +118

Copilot AI Nov 10, 2025

Copy link

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
await logger.adebug(f"Available models: {models}")

# Filter models that are NOT embedding 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
Comment on lines +133 to +134

Copilot AI Nov 10, 2025

Copy link

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.

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)

return sorted(model_ids)

except (httpx.RequestError, ValueError) as e:
msg = "Could not get model names from Ollama."
await logger.aexception(msg)
raise ValueError(msg) from e

inputs = [
DropdownInput(
Expand All @@ -101,6 +169,7 @@
value=OPENAI_CHAT_MODEL_NAMES[0],
info="Select the model to use",
real_time_refresh=True,
refresh_button=True,
),
SecretStrInput(
name="api_key",
Expand Down Expand Up @@ -129,10 +198,11 @@
MessageTextInput(
name="ollama_base_url",
display_name="Ollama API URL",
info="Endpoint of the Ollama API (Ollama only). Defaults to http://localhost:11434",
value="http://localhost:11434",
info=f"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}",
value=DEFAULT_OLLAMA_URL,
show=False,
real_time_refresh=True,
load_from_db=True,
),
MessageInput(
name="input_value",
Expand Down Expand Up @@ -252,7 +322,9 @@
msg = f"Unknown provider: {provider}"
raise ValueError(msg)

def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:
async def update_build_config(
self, build_config: dotdict, field_value: Any, field_name: str | None = None
) -> dotdict:
if field_name == "provider":
if field_value == "OpenAI":
build_config["model_name"]["options"] = OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES
Expand Down Expand Up @@ -288,14 +360,48 @@
build_config["ollama_base_url"]["show"] = False
elif field_value == "Ollama":
# Fetch Ollama models from the API
ollama_url = build_config["ollama_base_url"].get("value", "http://localhost:11434")
models = self.fetch_ollama_models(base_url=ollama_url)
build_config["model_name"]["options"] = models
build_config["model_name"]["value"] = models[0] if models else ""
build_config["api_key"]["show"] = False
build_config["base_url_ibm_watsonx"]["show"] = False
build_config["project_id"]["show"] = False
build_config["ollama_base_url"]["show"] = True
build_config["ollama_base_url"]["load_from_db"] = True

# Try multiple sources to get the URL (in order of preference):
# 1. Instance attribute (already resolved from global/db)
# 2. Build config value (may be a global variable reference)
# 3. Default value
ollama_url = getattr(self, "ollama_base_url", None)
if not ollama_url:
config_value = build_config["ollama_base_url"].get("value", DEFAULT_OLLAMA_URL)
# If config_value looks like a variable name (all caps with underscores), use default
is_variable_ref = (
config_value
and isinstance(config_value, str)
and config_value.isupper()
and "_" in config_value
)
if is_variable_ref:
await logger.adebug(
f"Config value appears to be a variable reference: {config_value}, using default"
)
ollama_url = DEFAULT_OLLAMA_URL
else:
ollama_url = config_value

await logger.adebug(f"Fetching Ollama models for provider switch. URL: {ollama_url}")
if await self.is_valid_ollama_url(ollama_url):
try:
models = await self.get_ollama_models(base_url_value=ollama_url)
build_config["model_name"]["options"] = models
build_config["model_name"]["value"] = models[0] if models else ""
except ValueError:
await logger.awarning("Failed to fetch Ollama models. Setting empty options.")
build_config["model_name"]["options"] = []
build_config["model_name"]["value"] = ""
else:
await logger.awarning(f"Invalid Ollama URL: {ollama_url}")
build_config["model_name"]["options"] = []
build_config["model_name"]["value"] = ""
elif (
field_name == "base_url_ibm_watsonx"
and field_value
Expand All @@ -311,22 +417,44 @@
logger.info(info_message)
except Exception: # noqa: BLE001
logger.exception("Error updating IBM model options.")
elif (
field_name == "ollama_base_url" and field_value and hasattr(self, "provider") and self.provider == "Ollama"
):
elif field_name == "ollama_base_url":
# Fetch Ollama models when ollama_base_url changes
try:
models = self.fetch_ollama_models(base_url=field_value)
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 {field_value}"
logger.info(info_message)
except Exception: # noqa: BLE001
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(f"Fetching Ollama models from updated URL: {build_config['ollama_base_url']} and value {self.ollama_base_url}")

Check failure on line 423 in src/lfx/src/lfx/components/models/language_model.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (E501)

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

Check failure on line 423 in src/lfx/src/lfx/components/models/language_model.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (E501)

src/lfx/src/lfx/components/models/language_model.py:423:121: E501 Line too long (136 > 120)
await logger.adebug(f"Fetching Ollama models from updated URL: {self.ollama_base_url}")
if await self.is_valid_ollama_url(self.ollama_base_url):
try:
models = await self.get_ollama_models(base_url_value=self.ollama_base_url)
build_config["model_name"]["options"] = models
build_config["model_name"]["value"] = models[0] if models else ""
info_message = f"Updated model options: {len(models)} models found in {self.ollama_base_url}"
await logger.ainfo(info_message)
except ValueError:
await logger.awarning("Error updating Ollama model options.")
build_config["model_name"]["options"] = []
build_config["model_name"]["value"] = ""
else:
await logger.awarning(f"Invalid Ollama URL: {self.ollama_base_url}")
build_config["model_name"]["options"] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the freshly supplied Ollama URL when refreshing model options.

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

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

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

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

🧰 Tools
🪛 GitHub Actions: Ruff Style Check

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

🪛 GitHub Check: Ruff Style Check (3.13)

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

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

build_config["model_name"]["value"] = ""
elif field_name == "model_name":
# Refresh Ollama models when model_name field is accessed
if hasattr(self, "provider") and self.provider == "Ollama":
ollama_url = getattr(self, "ollama_base_url", DEFAULT_OLLAMA_URL)
if await self.is_valid_ollama_url(ollama_url):
try:
models = await self.get_ollama_models(base_url_value=ollama_url)
build_config["model_name"]["options"] = models
except ValueError:
await logger.awarning("Failed to refresh Ollama models.")
build_config["model_name"]["options"] = []
else:
build_config["model_name"]["options"] = []

# Hide system_message for o1 models - currently unsupported
if "system_message" in build_config:
build_config["system_message"]["show"] = False
elif field_name == "model_name" and not field_value.startswith("o1") and "system_message" in build_config:
build_config["system_message"]["show"] = True
if field_value and field_value.startswith("o1") and hasattr(self, "provider") and self.provider == "OpenAI":
if "system_message" in build_config:
build_config["system_message"]["show"] = False
elif "system_message" in build_config:
build_config["system_message"]["show"] = True
return build_config
2 changes: 2 additions & 0 deletions src/lfx/src/lfx/components/ollama/ollama.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ async def update_build_config(self, build_config: dict, field_value: Any, field_
build_config["mirostat_tau"]["value"] = 5

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 await self.is_valid_ollama_url(self.base_url):
tool_model_enabled = build_config["tool_model_enabled"].get("value", False) or self.tool_model_enabled
build_config["model_name"]["options"] = await self.get_models(
Expand Down
Loading