feat: Add OpenRouter component for multi-provider model access (#5271)
* feat: add OpenRouter component for multi-provider model access - Add OpenRouter integration with provider-based model selection - Support dynamic model fetching from OpenRouter API - Implement temperature and max tokens controls - Add site URL and app name for rankings - Include model tooltips with context length and descriptions * fix: restore SambaNova component in models __init__.py * refactor: simplify OpenRouter build_model method * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
68deca4feb
commit
9a8f721515
6 changed files with 234 additions and 0 deletions
|
|
@ -13,6 +13,7 @@ from .mistral import MistralAIModelComponent
|
||||||
from .nvidia import NVIDIAModelComponent
|
from .nvidia import NVIDIAModelComponent
|
||||||
from .ollama import ChatOllamaComponent
|
from .ollama import ChatOllamaComponent
|
||||||
from .openai import OpenAIModelComponent
|
from .openai import OpenAIModelComponent
|
||||||
|
from .openrouter import OpenRouterComponent
|
||||||
from .perplexity import PerplexityComponent
|
from .perplexity import PerplexityComponent
|
||||||
from .sambanova import SambaNovaComponent
|
from .sambanova import SambaNovaComponent
|
||||||
from .vertexai import ChatVertexAIComponent
|
from .vertexai import ChatVertexAIComponent
|
||||||
|
|
@ -33,6 +34,7 @@ __all__ = [
|
||||||
"MistralAIModelComponent",
|
"MistralAIModelComponent",
|
||||||
"NVIDIAModelComponent",
|
"NVIDIAModelComponent",
|
||||||
"OpenAIModelComponent",
|
"OpenAIModelComponent",
|
||||||
|
"OpenRouterComponent",
|
||||||
"PerplexityComponent",
|
"PerplexityComponent",
|
||||||
"QianfanChatEndpointComponent",
|
"QianfanChatEndpointComponent",
|
||||||
"SambaNovaComponent",
|
"SambaNovaComponent",
|
||||||
|
|
|
||||||
197
src/backend/base/langflow/components/models/openrouter.py
Normal file
197
src/backend/base/langflow/components/models/openrouter.py
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from pydantic.v1 import SecretStr
|
||||||
|
|
||||||
|
from langflow.base.models.model import LCModelComponent
|
||||||
|
from langflow.field_typing import LanguageModel
|
||||||
|
from langflow.field_typing.range_spec import RangeSpec
|
||||||
|
from langflow.inputs import (
|
||||||
|
DropdownInput,
|
||||||
|
IntInput,
|
||||||
|
SecretStrInput,
|
||||||
|
SliderInput,
|
||||||
|
StrInput,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenRouterComponent(LCModelComponent):
|
||||||
|
"""OpenRouter API component for language models."""
|
||||||
|
|
||||||
|
display_name = "OpenRouter"
|
||||||
|
description = (
|
||||||
|
"OpenRouter provides unified access to multiple AI models " "from different providers through a single API."
|
||||||
|
)
|
||||||
|
icon = "OpenRouter"
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
*LCModelComponent._base_inputs,
|
||||||
|
SecretStrInput(
|
||||||
|
name="api_key", display_name="OpenRouter API Key", required=True, info="Your OpenRouter API key"
|
||||||
|
),
|
||||||
|
StrInput(
|
||||||
|
name="site_url",
|
||||||
|
display_name="Site URL",
|
||||||
|
info="Your site URL for OpenRouter rankings",
|
||||||
|
advanced=True,
|
||||||
|
),
|
||||||
|
StrInput(
|
||||||
|
name="app_name",
|
||||||
|
display_name="App Name",
|
||||||
|
info="Your app name for OpenRouter rankings",
|
||||||
|
advanced=True,
|
||||||
|
),
|
||||||
|
DropdownInput(
|
||||||
|
name="provider",
|
||||||
|
display_name="Provider",
|
||||||
|
info="The AI model provider",
|
||||||
|
options=["Loading providers..."],
|
||||||
|
value="Loading providers...",
|
||||||
|
real_time_refresh=True,
|
||||||
|
),
|
||||||
|
DropdownInput(
|
||||||
|
name="model_name",
|
||||||
|
display_name="Model",
|
||||||
|
info="The model to use for chat completion",
|
||||||
|
options=["Select a provider first"],
|
||||||
|
value="Select a provider first",
|
||||||
|
real_time_refresh=True,
|
||||||
|
),
|
||||||
|
SliderInput(
|
||||||
|
name="temperature", display_name="Temperature", value=0.7, range_spec=RangeSpec(min=0, max=2, step=0.01)
|
||||||
|
),
|
||||||
|
IntInput(
|
||||||
|
name="max_tokens",
|
||||||
|
display_name="Max Tokens",
|
||||||
|
info="Maximum number of tokens to generate",
|
||||||
|
advanced=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def fetch_models(self) -> dict[str, list]:
|
||||||
|
"""Fetch available models from OpenRouter API and organize them by provider."""
|
||||||
|
url = "https://openrouter.ai/api/v1/models"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
models_data = response.json().get("data", [])
|
||||||
|
provider_models = defaultdict(list)
|
||||||
|
|
||||||
|
for model in models_data:
|
||||||
|
model_id = model.get("id", "")
|
||||||
|
if "/" in model_id:
|
||||||
|
provider = model_id.split("/")[0].title()
|
||||||
|
provider_models[provider].append(
|
||||||
|
{
|
||||||
|
"id": model_id,
|
||||||
|
"name": model.get("name", ""),
|
||||||
|
"description": model.get("description", ""),
|
||||||
|
"context_length": model.get("context_length", 0),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return dict(provider_models)
|
||||||
|
|
||||||
|
except httpx.HTTPError as e:
|
||||||
|
self.log(f"Error fetching models: {e!s}")
|
||||||
|
return {"Error": [{"id": "error", "name": f"Error fetching models: {e!s}"}]}
|
||||||
|
|
||||||
|
def build_model(self) -> LanguageModel:
|
||||||
|
"""Build and return the OpenRouter language model."""
|
||||||
|
model_not_selected = "Please select a model"
|
||||||
|
api_key_required = "API key is required"
|
||||||
|
|
||||||
|
if not self.model_name or self.model_name == "Select a provider first":
|
||||||
|
raise ValueError(model_not_selected)
|
||||||
|
|
||||||
|
if not self.api_key:
|
||||||
|
raise ValueError(api_key_required)
|
||||||
|
|
||||||
|
api_key = SecretStr(self.api_key).get_secret_value()
|
||||||
|
|
||||||
|
# Build base configuration
|
||||||
|
kwargs: dict[str, Any] = {
|
||||||
|
"model": self.model_name,
|
||||||
|
"openai_api_key": api_key,
|
||||||
|
"openai_api_base": "https://openrouter.ai/api/v1",
|
||||||
|
"temperature": self.temperature if self.temperature is not None else 0.7,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add optional parameters
|
||||||
|
if self.max_tokens:
|
||||||
|
kwargs["max_tokens"] = self.max_tokens
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if self.site_url:
|
||||||
|
headers["HTTP-Referer"] = self.site_url
|
||||||
|
if self.app_name:
|
||||||
|
headers["X-Title"] = self.app_name
|
||||||
|
|
||||||
|
if headers:
|
||||||
|
kwargs["default_headers"] = headers
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ChatOpenAI(**kwargs)
|
||||||
|
except (ValueError, httpx.HTTPError) as err:
|
||||||
|
error_msg = f"Failed to build model: {err!s}"
|
||||||
|
self.log(error_msg)
|
||||||
|
raise ValueError(error_msg) from err
|
||||||
|
|
||||||
|
def _get_exception_message(self, e: Exception) -> str | None:
|
||||||
|
"""Get a message from an OpenRouter exception.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e (Exception): The exception to get the message from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: The message from the exception, or None if no specific message can be extracted.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from openai import BadRequestError
|
||||||
|
|
||||||
|
if isinstance(e, BadRequestError):
|
||||||
|
message = e.body.get("message")
|
||||||
|
if message:
|
||||||
|
return message
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:
|
||||||
|
"""Update build configuration based on field updates."""
|
||||||
|
try:
|
||||||
|
if field_name is None or field_name == "provider":
|
||||||
|
provider_models = self.fetch_models()
|
||||||
|
build_config["provider"]["options"] = sorted(provider_models.keys())
|
||||||
|
if build_config["provider"]["value"] not in provider_models:
|
||||||
|
build_config["provider"]["value"] = build_config["provider"]["options"][0]
|
||||||
|
|
||||||
|
if field_name == "provider" and field_value in self.fetch_models():
|
||||||
|
provider_models = self.fetch_models()
|
||||||
|
models = provider_models[field_value]
|
||||||
|
|
||||||
|
build_config["model_name"]["options"] = [model["id"] for model in models]
|
||||||
|
if models:
|
||||||
|
build_config["model_name"]["value"] = models[0]["id"]
|
||||||
|
|
||||||
|
tooltips = {
|
||||||
|
model["id"]: (
|
||||||
|
f"{model['name']}\n" f"Context Length: {model['context_length']}\n" f"{model['description']}"
|
||||||
|
)
|
||||||
|
for model in models
|
||||||
|
}
|
||||||
|
build_config["model_name"]["tooltips"] = tooltips
|
||||||
|
|
||||||
|
except httpx.HTTPError as e:
|
||||||
|
self.log(f"Error updating build config: {e!s}")
|
||||||
|
build_config["provider"]["options"] = ["Error loading providers"]
|
||||||
|
build_config["provider"]["value"] = "Error loading providers"
|
||||||
|
build_config["model_name"]["options"] = ["Error loading models"]
|
||||||
|
build_config["model_name"]["value"] = "Error loading models"
|
||||||
|
|
||||||
|
return build_config
|
||||||
23
src/frontend/src/icons/OpenRouter/OpenRouterIcon.jsx
Normal file
23
src/frontend/src/icons/OpenRouter/OpenRouterIcon.jsx
Normal file
File diff suppressed because one or more lines are too long
9
src/frontend/src/icons/OpenRouter/index.tsx
Normal file
9
src/frontend/src/icons/OpenRouter/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
import SvgOpenRouter from "./OpenRouterIcon";
|
||||||
|
|
||||||
|
export const OpenRouterIcon = forwardRef<
|
||||||
|
SVGSVGElement,
|
||||||
|
React.PropsWithChildren<{}>
|
||||||
|
>((props, ref) => {
|
||||||
|
return <SvgOpenRouter ref={ref} {...props} />;
|
||||||
|
});
|
||||||
1
src/frontend/src/icons/OpenRouter/openrouter.svg
Normal file
1
src/frontend/src/icons/OpenRouter/openrouter.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -282,6 +282,7 @@ import { NotionIcon } from "../icons/Notion";
|
||||||
import { NvidiaIcon } from "../icons/Nvidia";
|
import { NvidiaIcon } from "../icons/Nvidia";
|
||||||
import { OllamaIcon } from "../icons/Ollama";
|
import { OllamaIcon } from "../icons/Ollama";
|
||||||
import { OpenAiIcon } from "../icons/OpenAi";
|
import { OpenAiIcon } from "../icons/OpenAi";
|
||||||
|
import { OpenRouterIcon } from "../icons/OpenRouter";
|
||||||
import { OpenSearch } from "../icons/OpenSearch";
|
import { OpenSearch } from "../icons/OpenSearch";
|
||||||
import { PineconeIcon } from "../icons/Pinecone";
|
import { PineconeIcon } from "../icons/Pinecone";
|
||||||
import { PostgresIcon } from "../icons/Postgres";
|
import { PostgresIcon } from "../icons/Postgres";
|
||||||
|
|
@ -666,6 +667,7 @@ export const nodeIconsLucide: iconsType = {
|
||||||
ChatOpenAI: OpenAiIcon,
|
ChatOpenAI: OpenAiIcon,
|
||||||
AzureChatOpenAI: OpenAiIcon,
|
AzureChatOpenAI: OpenAiIcon,
|
||||||
OpenAI: OpenAiIcon,
|
OpenAI: OpenAiIcon,
|
||||||
|
OpenRouter: OpenRouterIcon,
|
||||||
OpenAIEmbeddings: OpenAiIcon,
|
OpenAIEmbeddings: OpenAiIcon,
|
||||||
Pinecone: PineconeIcon,
|
Pinecone: PineconeIcon,
|
||||||
Qdrant: QDrantIcon,
|
Qdrant: QDrantIcon,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue