feat: ollama official support (#5509)

This commit is contained in:
Wenxi authored and GitHub committed 2025-10-02 10:47:16 -07:00
1 parent a13db828f3
commit ccf55136be
15 files changed
+895 -437

No files matched your search

@@ -0,0 +1,37 @@
"""Add image input support to model config
Revision ID: 64bd5677aeb6
Revises: b30353be4eec
Create Date: 2025-09-28 15:48:12.003612
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "64bd5677aeb6"
down_revision = "b30353be4eec"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"model_configuration",
sa.Column("supports_image_input", sa.Boolean(), nullable=True),
)
# Seems to be left over from when model visibility was introduced and a nullable field.
# Set any null is_visible values to False
connection = op.get_bind()
connection.execute(
sa.text(
"UPDATE model_configuration SET is_visible = false WHERE is_visible IS NULL"
)
)
def downgrade() -> None:
op.drop_column("model_configuration", "supports_image_input")
+1
View File
@@ -112,6 +112,7 @@ def upsert_llm_provider(
name=model_configuration.name,
is_visible=model_configuration.is_visible,
max_input_tokens=model_configuration.max_input_tokens,
supports_image_input=model_configuration.supports_image_input,
)
.on_conflict_do_nothing()
)
+2
View File
@@ -2353,6 +2353,8 @@ class ModelConfiguration(Base):
# - The end-user is configuring a model and chooses not to set a max-input-tokens limit.
max_input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
supports_image_input: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
llm_provider: Mapped["LLMProvider"] = relationship(
"LLMProvider",
back_populates="model_configurations",
+29 -11
View File
@@ -1,8 +1,5 @@
from typing import Any
from onyx.chat.models import PersonaOverrideConfig
from onyx.configs.app_configs import DISABLE_GENERATIVE_AI
from onyx.configs.model_configs import GEN_AI_MODEL_FALLBACK_MAX_TOKENS
from onyx.configs.model_configs import GEN_AI_TEMPERATURE
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.llm import fetch_default_provider
@@ -13,6 +10,8 @@ from onyx.db.models import Persona
from onyx.llm.chat_llm import DefaultMultiLLM
from onyx.llm.exceptions import GenAIDisabledException
from onyx.llm.interfaces import LLM
from onyx.llm.llm_provider_options import OLLAMA_API_KEY_CONFIG_KEY
from onyx.llm.llm_provider_options import OLLAMA_PROVIDER_NAME
from onyx.llm.override_models import LLMOverride
from onyx.llm.utils import get_max_input_tokens_from_llm_provider
from onyx.llm.utils import model_supports_image_input
@@ -24,13 +23,22 @@ from onyx.utils.long_term_log import LongTermLogger
logger = setup_logger()
def _build_extra_model_kwargs(provider: str) -> dict[str, Any]:
"""Ollama requires us to specify the max context window.
def _build_provider_extra_headers(
provider: str, custom_config: dict[str, str] | None
) -> dict[str, str]:
if provider != OLLAMA_PROVIDER_NAME or not custom_config:
return {}
For now, just using the GEN_AI_MODEL_FALLBACK_MAX_TOKENS value.
TODO: allow model-specific values to be configured via the UI.
"""
return {"num_ctx": GEN_AI_MODEL_FALLBACK_MAX_TOKENS} if provider == "ollama" else {}
raw_api_key = custom_config.get(OLLAMA_API_KEY_CONFIG_KEY)
api_key = raw_api_key.strip() if raw_api_key else None
if not api_key:
return {}
if not api_key.lower().startswith("bearer "):
api_key = f"Bearer {api_key}"
return {"Authorization": api_key}
def get_main_llm_from_tuple(
@@ -272,6 +280,16 @@ def get_llm(
) -> LLM:
if temperature is None:
temperature = GEN_AI_TEMPERATURE
extra_headers = build_llm_extra_headers(additional_headers)
# NOTE: this is needed since Ollama API key is optional
# User may access Ollama cloud via locally hosted instance (logged in)
# or just via the cloud API (not logged in, using API key)
provider_extra_headers = _build_provider_extra_headers(provider, custom_config)
if provider_extra_headers:
extra_headers.update(provider_extra_headers)
return DefaultMultiLLM(
model_provider=provider,
model_name=model,
@@ -282,8 +300,8 @@ def get_llm(
timeout=timeout,
temperature=temperature,
custom_config=custom_config,
extra_headers=build_llm_extra_headers(additional_headers),
model_kwargs=_build_extra_model_kwargs(provider),
extra_headers=extra_headers,
model_kwargs={},
long_term_logger=long_term_logger,
max_input_tokens=max_input_tokens,
)
+30 -3
View File
@@ -39,6 +39,7 @@ class WellKnownLLMProviderDescriptor(BaseModel):
model_configurations: list[ModelConfigurationView]
default_model: str | None = None
default_fast_model: str | None = None
default_api_base: str | None = None
# set for providers like Azure, which require a deployment name.
deployment_name_required: bool = False
# set for providers like Azure, which support a single model per deployment.
@@ -95,7 +96,9 @@ BEDROCK_MODEL_NAMES = [
for model in list(litellm.bedrock_models.union(litellm.bedrock_converse_models))
if "/" not in model and "embed" not in model
][::-1]
BEDROCK_DEFAULT_MODEL = "anthropic.claude-3-5-sonnet-20241022-v2:0"
OLLAMA_PROVIDER_NAME = "ollama"
OLLAMA_API_KEY_CONFIG_KEY = "OLLAMA_API_KEY"
IGNORABLE_ANTHROPIC_MODELS = [
"claude-2",
@@ -160,13 +163,15 @@ _PROVIDER_TO_MODELS_MAP = {
BEDROCK_PROVIDER_NAME: BEDROCK_MODEL_NAMES,
ANTHROPIC_PROVIDER_NAME: ANTHROPIC_MODEL_NAMES,
VERTEXAI_PROVIDER_NAME: VERTEXAI_MODEL_NAMES,
OLLAMA_PROVIDER_NAME: [],
}
_PROVIDER_TO_VISIBLE_MODELS_MAP = {
OPENAI_PROVIDER_NAME: OPEN_AI_VISIBLE_MODEL_NAMES,
BEDROCK_PROVIDER_NAME: [BEDROCK_DEFAULT_MODEL],
BEDROCK_PROVIDER_NAME: [],
ANTHROPIC_PROVIDER_NAME: ANTHROPIC_VISIBLE_MODEL_NAMES,
VERTEXAI_PROVIDER_NAME: VERTEXAI_VISIBLE_MODEL_NAMES,
OLLAMA_PROVIDER_NAME: [],
}
@@ -185,6 +190,28 @@ def fetch_available_well_known_llms() -> list[WellKnownLLMProviderDescriptor]:
default_model="gpt-4o",
default_fast_model="gpt-4o-mini",
),
WellKnownLLMProviderDescriptor(
name=OLLAMA_PROVIDER_NAME,
display_name="Ollama",
api_key_required=False,
api_base_required=True,
api_version_required=False,
custom_config_keys=[
CustomConfigKey(
name=OLLAMA_API_KEY_CONFIG_KEY,
display_name="Ollama API Key",
description="Optional API key used when connecting to Ollama Cloud (i.e. API base is https://ollama.com).",
is_required=False,
is_secret=True,
)
],
model_configurations=fetch_model_configurations_for_provider(
OLLAMA_PROVIDER_NAME
),
default_model=None,
default_fast_model=None,
default_api_base="http://127.0.0.1:11434",
),
WellKnownLLMProviderDescriptor(
name=ANTHROPIC_PROVIDER_NAME,
display_name="Anthropic",
@@ -248,7 +275,7 @@ def fetch_available_well_known_llms() -> list[WellKnownLLMProviderDescriptor]:
model_configurations=fetch_model_configurations_for_provider(
BEDROCK_PROVIDER_NAME
),
default_model=BEDROCK_DEFAULT_MODEL,
default_model=None,
default_fast_model=None,
),
WellKnownLLMProviderDescriptor(
+28
View File
@@ -16,6 +16,7 @@ from langchain.schema.messages import AIMessage
from langchain.schema.messages import BaseMessage
from langchain.schema.messages import HumanMessage
from langchain.schema.messages import SystemMessage
from sqlalchemy import select
from onyx.configs.app_configs import LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS
from onyx.configs.app_configs import MAX_TOKENS_FOR_FULL_INCLUSION
@@ -26,6 +27,9 @@ from onyx.configs.model_configs import DOC_EMBEDDING_CONTEXT_SIZE
from onyx.configs.model_configs import GEN_AI_MAX_TOKENS
from onyx.configs.model_configs import GEN_AI_MODEL_FALLBACK_MAX_TOKENS
from onyx.configs.model_configs import GEN_AI_NUM_RESERVED_OUTPUT_TOKENS
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.models import LLMProvider
from onyx.db.models import ModelConfiguration
from onyx.file_store.models import ChatFileType
from onyx.file_store.models import InMemoryChatFile
from onyx.llm.interfaces import LLM
@@ -640,6 +644,30 @@ def get_max_input_tokens_from_llm_provider(
def model_supports_image_input(model_name: str, model_provider: str) -> bool:
# TODO: Add support to check model config for any provider
# TODO: Circular import means OLLAMA_PROVIDER_NAME is not available here
if model_provider == "ollama":
try:
with get_session_with_current_tenant() as db_session:
model_config = db_session.scalar(
select(ModelConfiguration)
.join(
LLMProvider,
ModelConfiguration.llm_provider_id == LLMProvider.id,
)
.where(
ModelConfiguration.name == model_name,
LLMProvider.provider == model_provider,
)
)
if model_config and model_config.supports_image_input is not None:
return model_config.supports_image_input
except Exception as e:
logger.warning(
f"Failed to query database for {model_provider} model {model_name} image support: {e}"
)
model_map = get_model_map()
try:
model_obj = find_model_obj(
+103
View File
@@ -4,6 +4,7 @@ from datetime import datetime
from datetime import timezone
import boto3
import httpx
from botocore.exceptions import BotoCoreError
from botocore.exceptions import ClientError
from botocore.exceptions import NoCredentialsError
@@ -11,10 +12,12 @@ from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Query
from pydantic import ValidationError
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_chat_accessible_user
from onyx.configs.model_configs import GEN_AI_MODEL_FALLBACK_MAX_TOKENS
from onyx.db.engine.sql_engine import get_session
from onyx.db.llm import fetch_existing_llm_provider
from onyx.db.llm import fetch_existing_llm_providers
@@ -40,6 +43,9 @@ from onyx.server.manage.llm.models import LLMProviderDescriptor
from onyx.server.manage.llm.models import LLMProviderUpsertRequest
from onyx.server.manage.llm.models import LLMProviderView
from onyx.server.manage.llm.models import ModelConfigurationUpsertRequest
from onyx.server.manage.llm.models import OllamaFinalModelResponse
from onyx.server.manage.llm.models import OllamaModelDetails
from onyx.server.manage.llm.models import OllamaModelsRequest
from onyx.server.manage.llm.models import TestLLMRequest
from onyx.server.manage.llm.models import VisionProviderResponse
from onyx.utils.logger import setup_logger
@@ -474,3 +480,100 @@ def get_bedrock_available_models(
raise HTTPException(
status_code=500, detail=f"Unexpected error fetching Bedrock models: {e}"
)
def _get_ollama_available_model_names(api_base: str) -> set[str]:
"""Fetch available model names from Ollama server."""
tags_url = f"{api_base}/api/tags"
try:
response = httpx.get(tags_url, timeout=5.0)
response.raise_for_status()
response_json = response.json()
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Failed to fetch Ollama models: {e}",
)
models = response_json.get("models", [])
return {model.get("name") for model in models if model.get("name")}
@admin_router.post("/ollama/available-models")
def get_ollama_available_models(
request: OllamaModelsRequest,
_: User | None = Depends(current_admin_user),
) -> list[OllamaFinalModelResponse]:
"""Fetch the list of available models from an Ollama server."""
cleaned_api_base = request.api_base.strip().rstrip("/")
if not cleaned_api_base:
raise HTTPException(
status_code=400, detail="API base URL is required to fetch Ollama models."
)
model_names = _get_ollama_available_model_names(cleaned_api_base)
if not model_names:
raise HTTPException(
status_code=400,
detail="No models found from your Ollama server",
)
all_models_with_context_size_and_vision: list[OllamaFinalModelResponse] = []
show_url = f"{cleaned_api_base}/api/show"
for model_name in model_names:
context_limit: int | None = None
supports_image_input: bool | None = None
try:
show_response = httpx.post(
show_url,
json={"model": model_name},
timeout=5.0,
)
show_response.raise_for_status()
show_response_json = show_response.json()
# Parse the response into the expected format
ollama_model_details = OllamaModelDetails.model_validate(show_response_json)
# Check if this model supports completion/chat
if not ollama_model_details.supports_completion():
continue
# Optimistically access. Context limit is stored as "model_architecture.context" = int
architecture = ollama_model_details.model_info.get(
"general.architecture", ""
)
context_limit = ollama_model_details.model_info.get(
architecture + ".context_length", None
)
supports_image_input = ollama_model_details.supports_image_input()
except ValidationError as e:
logger.warning(
"Invalid model details from Ollama server",
extra={"model": model_name, "validation_error": str(e)},
)
except Exception as e:
logger.warning(
"Failed to fetch Ollama model details",
extra={"model": model_name, "error": str(e)},
)
# If we fail at any point attempting to extract context limit,
# still allow this model to be used with a fallback max context size
if not context_limit:
context_limit = GEN_AI_MODEL_FALLBACK_MAX_TOKENS
if not supports_image_input:
supports_image_input = False
all_models_with_context_size_and_vision.append(
OllamaFinalModelResponse(
name=model_name,
max_input_tokens=context_limit,
supports_image_input=supports_image_input,
)
)
return all_models_with_context_size_and_vision
+30 -2
View File
@@ -1,3 +1,4 @@
from typing import Any
from typing import TYPE_CHECKING
from pydantic import BaseModel
@@ -138,8 +139,9 @@ class LLMProviderView(LLMProvider):
class ModelConfigurationUpsertRequest(BaseModel):
name: str
is_visible: bool | None = False
is_visible: bool
max_input_tokens: int | None = None
supports_image_input: bool | None = None
@classmethod
def from_model(
@@ -149,12 +151,13 @@ class ModelConfigurationUpsertRequest(BaseModel):
name=model_configuration_model.name,
is_visible=model_configuration_model.is_visible,
max_input_tokens=model_configuration_model.max_input_tokens,
supports_image_input=model_configuration_model.supports_image_input,
)
class ModelConfigurationView(BaseModel):
name: str
is_visible: bool | None = False
is_visible: bool
max_input_tokens: int | None = None
supports_image_input: bool
@@ -196,3 +199,28 @@ class BedrockModelsRequest(BaseModel):
aws_secret_access_key: str | None = None
aws_bearer_token_bedrock: str | None = None
provider_name: str | None = None # Optional: to save models to existing provider
class OllamaModelsRequest(BaseModel):
api_base: str
class OllamaFinalModelResponse(BaseModel):
name: str
max_input_tokens: int
supports_image_input: bool
class OllamaModelDetails(BaseModel):
"""Response model for Ollama /api/show endpoint"""
model_info: dict[str, Any]
capabilities: list[str] = []
def supports_completion(self) -> bool:
"""Check if this model supports completion/chat"""
return "completion" in self.capabilities
def supports_image_input(self) -> bool:
"""Check if this model supports image input"""
return "vision" in self.capabilities
@@ -92,31 +92,22 @@ def assert_response_is_equivalent(
)
],
),
# Test the case in which the basic model-configuration is passed, but its visibility is not
# specified (and thus defaulted to False).
# In this case, since the one model-configuration is also the default-model-name, its
# visibility should be overriden to True.
(
"gpt-4",
[ModelConfigurationUpsertRequest(name="gpt-4")],
[ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True)],
),
# Test the case in which multiple model-configuration are passed.
(
"gpt-4",
[
ModelConfigurationUpsertRequest(name="gpt-4"),
ModelConfigurationUpsertRequest(name="gpt-4o"),
ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True),
ModelConfigurationUpsertRequest(name="gpt-4o", is_visible=True),
],
[
ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True),
ModelConfigurationUpsertRequest(name="gpt-4o"),
ModelConfigurationUpsertRequest(name="gpt-4o", is_visible=True),
],
),
# Test the case in which duplicate model-configuration are passed.
(
"gpt-4",
[ModelConfigurationUpsertRequest(name="gpt-4")] * 4,
[ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True)] * 4,
[ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True)],
),
# Test the case in which no model-configurations are passed.
@@ -132,10 +123,16 @@ def assert_response_is_equivalent(
# (`ModelConfiguration(name="gpt-4", is_visible=True, max_input_tokens=None)`).
(
"gpt-4",
[ModelConfigurationUpsertRequest(name="gpt-4o", max_input_tokens=4096)],
[
ModelConfigurationUpsertRequest(
name="gpt-4o", is_visible=True, max_input_tokens=4096
)
],
[
ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True),
ModelConfigurationUpsertRequest(name="gpt-4o", max_input_tokens=4096),
ModelConfigurationUpsertRequest(
name="gpt-4o", is_visible=True, max_input_tokens=4096
),
],
),
],
@@ -182,7 +179,11 @@ def test_create_llm_provider(
(
(
"gpt-4",
[ModelConfigurationUpsertRequest(name="gpt-4", max_input_tokens=4096)],
[
ModelConfigurationUpsertRequest(
name="gpt-4", is_visible=True, max_input_tokens=4096
)
],
),
[
ModelConfigurationUpsertRequest(
@@ -191,7 +192,7 @@ def test_create_llm_provider(
],
(
"gpt-4",
[ModelConfigurationUpsertRequest(name="gpt-4")],
[ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True)],
),
[ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True)],
),
@@ -201,19 +202,25 @@ def test_create_llm_provider(
(
"gpt-4",
[
ModelConfigurationUpsertRequest(name="gpt-4"),
ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True),
ModelConfigurationUpsertRequest(
name="gpt-4o", max_input_tokens=4096
name="gpt-4o", is_visible=True, max_input_tokens=4096
),
],
),
[
ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True),
ModelConfigurationUpsertRequest(name="gpt-4o", max_input_tokens=4096),
ModelConfigurationUpsertRequest(
name="gpt-4o", is_visible=True, max_input_tokens=4096
),
],
(
"gpt-4",
[ModelConfigurationUpsertRequest(name="gpt-4", max_input_tokens=4096)],
[
ModelConfigurationUpsertRequest(
name="gpt-4", is_visible=True, max_input_tokens=4096
)
],
),
[
ModelConfigurationUpsertRequest(
@@ -328,8 +335,8 @@ def test_update_model_configurations(
(
"gpt-4",
[
ModelConfigurationUpsertRequest(name="gpt-4o"),
ModelConfigurationUpsertRequest(name="gpt-4"),
ModelConfigurationUpsertRequest(name="gpt-4o", is_visible=True),
ModelConfigurationUpsertRequest(name="gpt-4", is_visible=True),
],
),
],
@@ -176,16 +176,16 @@ export function ConfiguredLLMProviderDisplay({
// then the provider is custom - don't use the default
// provider descriptor
llmProviderDescriptor={
isSubset(
defaultProviderDesciptor
? defaultProviderDesciptor.model_configurations.map(
(model_configuration) => model_configuration.name
)
: [],
provider.model_configurations.map(
(model_configuration) => model_configuration.name
)
)
defaultProviderDesciptor &&
(defaultProviderDesciptor.model_configurations.length === 0 ||
isSubset(
defaultProviderDesciptor.model_configurations.map(
(model_configuration) => model_configuration.name
),
provider.model_configurations.map(
(model_configuration) => model_configuration.name
)
))
? defaultProviderDesciptor
: null
}
@@ -5,6 +5,7 @@ import Text from "@/components/ui/text";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Form, Formik } from "formik";
import type { FormikProps } from "formik";
import { FiTrash } from "react-icons/fi";
import { LLM_PROVIDERS_ADMIN_URL } from "./constants";
import {
@@ -13,18 +14,79 @@ import {
MultiSelectField,
FileUploadFormField,
} from "@/components/Field";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useSWRConfig } from "swr";
import {
LLMProviderView,
ModelConfigurationUpsertRequest,
ModelConfiguration,
WellKnownLLMProviderDescriptor,
} from "./interfaces";
import { dynamicProviderConfigs, fetchModels } from "./utils";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import * as Yup from "yup";
import isEqual from "lodash/isEqual";
import { IsPublicGroupSelector } from "@/components/IsPublicGroupSelector";
function AutoFetchModelsOnEdit({
llmProviderDescriptor,
existingLlmProvider,
values,
setFieldValue,
setIsFetchingModels,
setFetchModelsError,
setPopup,
}: {
llmProviderDescriptor: WellKnownLLMProviderDescriptor;
existingLlmProvider?: LLMProviderView;
values: any;
setFieldValue: FormikProps<any>["setFieldValue"];
setIsFetchingModels: (loading: boolean) => void;
setFetchModelsError: (error: string) => void;
setPopup?: (popup: PopupSpec) => void;
}) {
const hasAutoFetchedRef = useRef(false);
useEffect(() => {
if (!existingLlmProvider) {
return;
}
const config = dynamicProviderConfigs[llmProviderDescriptor.name];
if (!config) {
return;
}
if (hasAutoFetchedRef.current) {
return;
}
if (config.isDisabled(values)) {
return;
}
hasAutoFetchedRef.current = true;
fetchModels(
llmProviderDescriptor,
existingLlmProvider,
values,
setFieldValue,
setIsFetchingModels,
setFetchModelsError,
setPopup
);
}, [
existingLlmProvider,
llmProviderDescriptor,
setFieldValue,
setFetchModelsError,
setIsFetchingModels,
setPopup,
values,
]);
return null;
}
export function LLMProviderUpdateForm({
llmProviderDescriptor,
onClose,
@@ -53,12 +115,22 @@ export function LLMProviderUpdateForm({
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
// Helper function to get current model configurations
const getCurrentModelConfigurations = (values: any): ModelConfiguration[] => {
return values.fetched_model_configurations?.length > 0
? values.fetched_model_configurations
: llmProviderDescriptor.model_configurations;
};
// Define the initial values based on the provider's requirements
const initialValues = {
name:
existingLlmProvider?.name || (firstTimeConfiguration ? "Default" : ""),
api_key: existingLlmProvider?.api_key ?? "",
api_base: existingLlmProvider?.api_base ?? "",
api_base:
existingLlmProvider?.api_base ??
llmProviderDescriptor.default_api_base ??
"",
api_version: existingLlmProvider?.api_version ?? "",
// For Azure OpenAI, combine api_base and api_version into target_uri
target_uri:
@@ -100,6 +172,9 @@ export function LLMProviderUpdateForm({
.filter((modelConfiguration) => modelConfiguration.is_visible)
.map((modelConfiguration) => modelConfiguration.name) as string[]),
// Store fetched model configurations in form state instead of mutating props
fetched_model_configurations: [] as ModelConfiguration[],
// Helper field to force re-renders when model list updates
_modelListUpdated: 0,
};
@@ -174,6 +249,7 @@ export function LLMProviderUpdateForm({
is_public: Yup.boolean().required(),
groups: Yup.array().of(Yup.number()),
selected_model_names: Yup.array().of(Yup.string()),
fetched_model_configurations: Yup.array(),
});
const customLinkRenderer = ({ href, children }: any) => {
@@ -184,99 +260,6 @@ export function LLMProviderUpdateForm({
);
};
const fetchBedrockModels = async (values: any, setFieldValue: any) => {
if (llmProviderDescriptor.name !== "bedrock") {
return;
}
setIsFetchingModels(true);
setFetchModelsError("");
try {
const response = await fetch("/api/admin/llm/bedrock/available-models", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
aws_region_name: values.custom_config?.AWS_REGION_NAME,
aws_access_key_id: values.custom_config?.AWS_ACCESS_KEY_ID,
aws_secret_access_key: values.custom_config?.AWS_SECRET_ACCESS_KEY,
aws_bearer_token_bedrock:
values.custom_config?.AWS_BEARER_TOKEN_BEDROCK,
provider_name: existingLlmProvider?.name, // Save models to existing provider if editing
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to fetch models");
}
const availableModels: string[] = await response.json();
// Update the model configurations with the fetched models
const updatedModelConfigs = availableModels.map((modelName) => {
// Find existing configuration to preserve is_visible setting
const existingConfig = llmProviderDescriptor.model_configurations.find(
(config) => config.name === modelName
);
return {
name: modelName,
is_visible: existingConfig?.is_visible ?? false, // Preserve existing visibility or default to false
max_input_tokens: null,
supports_image_input: false, // Will be determined by the backend
};
});
// Update the descriptor and form values
llmProviderDescriptor.model_configurations = updatedModelConfigs;
// Update selected model names to only include previously visible models that are available
const previouslySelectedModels = values.selected_model_names || [];
const stillAvailableSelectedModels = previouslySelectedModels.filter(
(modelName: string) => availableModels.includes(modelName)
);
setFieldValue("selected_model_names", stillAvailableSelectedModels);
// Set a default model if none is set
if (
(!values.default_model_name ||
!availableModels.includes(values.default_model_name)) &&
availableModels.length > 0
) {
setFieldValue("default_model_name", availableModels[0]);
}
// Clear fast model if it's not in the new list
if (
values.fast_default_model_name &&
!availableModels.includes(values.fast_default_model_name)
) {
setFieldValue("fast_default_model_name", null);
}
// Force a re-render by updating a timestamp or counter
setFieldValue("_modelListUpdated", Date.now());
setPopup?.({
message: `Successfully fetched ${availableModels.length} models for the selected region (including cross-region inference models).`,
type: "success",
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
setFetchModelsError(errorMessage);
setPopup?.({
message: `Failed to fetch models: ${errorMessage}`,
type: "error",
});
} finally {
setIsFetchingModels(false);
}
};
return (
<Formik
initialValues={initialValues}
@@ -314,11 +297,12 @@ export function LLMProviderUpdateForm({
api_base: finalApiBase,
api_version: finalApiVersion,
api_key_changed: values.api_key !== initialValues.api_key,
model_configurations: llmProviderDescriptor.model_configurations.map(
(modelConfiguration): ModelConfigurationUpsertRequest => ({
model_configurations: getCurrentModelConfigurations(values).map(
(modelConfiguration): ModelConfiguration => ({
name: modelConfiguration.name,
is_visible: visibleModels.includes(modelConfiguration.name),
max_input_tokens: null,
max_input_tokens: modelConfiguration.max_input_tokens ?? null,
supports_image_input: modelConfiguration.supports_image_input,
})
),
};
@@ -422,175 +406,165 @@ export function LLMProviderUpdateForm({
setSubmitting(false);
}}
>
{(formikProps) => (
<Form className="gap-y-4 items-stretch mt-6">
{!firstTimeConfiguration && (
<TextFormField
name="name"
label="Display Name"
subtext="A name which you can use to identify this provider when selecting it in the UI."
placeholder="Display Name"
disabled={existingLlmProvider ? true : false}
{(formikProps) => {
// Get current model configurations for this render
const currentModelConfigurations = getCurrentModelConfigurations(
formikProps.values
);
const dynamicConfig =
dynamicProviderConfigs[llmProviderDescriptor.name];
return (
<Form className="gap-y-4 items-stretch mt-6">
<AutoFetchModelsOnEdit
llmProviderDescriptor={llmProviderDescriptor}
existingLlmProvider={existingLlmProvider}
values={formikProps.values}
setFieldValue={formikProps.setFieldValue}
setIsFetchingModels={setIsFetchingModels}
setFetchModelsError={setFetchModelsError}
setPopup={setPopup}
/>
)}
{!firstTimeConfiguration && (
<TextFormField
name="name"
label="Display Name"
subtext="A name which you can use to identify this provider when selecting it in the UI."
placeholder="Display Name"
disabled={existingLlmProvider ? true : false}
/>
)}
{llmProviderDescriptor.api_key_required && (
<TextFormField
small={firstTimeConfiguration}
name="api_key"
label="API Key"
placeholder="API Key"
type="password"
/>
)}
{llmProviderDescriptor.api_key_required && (
<TextFormField
small={firstTimeConfiguration}
name="api_key"
label="API Key"
placeholder="API Key"
type="password"
/>
)}
{llmProviderDescriptor.name === "azure" ? (
<TextFormField
small={firstTimeConfiguration}
name="target_uri"
label="Target URI"
placeholder="https://your-resource.cognitiveservices.azure.com/openai/deployments/deployment-name/chat/completions?api-version=2025-01-01-preview"
subtext="The complete Azure OpenAI endpoint URL including the API version as a query parameter"
/>
) : (
<>
{llmProviderDescriptor.api_base_required && (
<TextFormField
small={firstTimeConfiguration}
name="api_base"
label="API Base"
placeholder="API Base"
/>
)}
{llmProviderDescriptor.api_version_required && (
<TextFormField
small={firstTimeConfiguration}
name="api_version"
label="API Version"
placeholder="API Version"
/>
)}
</>
)}
{llmProviderDescriptor.custom_config_keys?.map((customConfigKey) => {
if (customConfigKey.key_type === "text_input") {
return (
<div key={customConfigKey.name}>
{llmProviderDescriptor.name === "azure" ? (
<TextFormField
small={firstTimeConfiguration}
name="target_uri"
label="Target URI"
placeholder="https://your-resource.cognitiveservices.azure.com/openai/deployments/deployment-name/chat/completions?api-version=2025-01-01-preview"
subtext="The complete Azure OpenAI endpoint URL including the API version as a query parameter"
/>
) : (
<>
{llmProviderDescriptor.api_base_required && (
<TextFormField
small={firstTimeConfiguration}
name={`custom_config.${customConfigKey.name}`}
optional={!customConfigKey.is_required}
label={customConfigKey.display_name}
subtext={
<ReactMarkdown components={{ a: customLinkRenderer }}>
{customConfigKey.description}
</ReactMarkdown>
}
placeholder={customConfigKey.default_value || undefined}
type={customConfigKey.is_secret ? "password" : "text"}
name="api_base"
label="API Base"
placeholder="API Base"
/>
</div>
);
} else if (customConfigKey.key_type === "file_input") {
return (
<FileUploadFormField
key={customConfigKey.name}
name={`custom_config.${customConfigKey.name}`}
label={customConfigKey.display_name}
subtext={customConfigKey.description || undefined}
/>
);
} else {
throw new Error("Unreachable; there should only exist 2 options");
}
})}
{/* Bedrock-specific fetch models button */}
{llmProviderDescriptor.name === "bedrock" && (
<div className="flex flex-col gap-2">
<Button
type="button"
onClick={() =>
fetchBedrockModels(
formikProps.values,
formikProps.setFieldValue
)
}
disabled={
isFetchingModels ||
!formikProps.values.custom_config?.AWS_REGION_NAME
}
className="w-fit"
>
{isFetchingModels ? (
<>
<LoadingAnimation size="text-sm" />
<span className="ml-2">Fetching Models...</span>
</>
) : (
"Fetch Available Models for Region"
)}
</Button>
{fetchModelsError && (
<Text className="text-red-600 text-sm">{fetchModelsError}</Text>
)}
{llmProviderDescriptor.api_version_required && (
<TextFormField
small={firstTimeConfiguration}
name="api_version"
label="API Version"
placeholder="API Version"
/>
)}
</>
)}
<Text className="text-sm text-gray-600">
Enter your AWS region, then click this button to fetch available
Bedrock models.
<br />
If you&apos;re updating your existing provider, you&apos;ll need
to click this button to fetch the latest models.
</Text>
</div>
)}
{!firstTimeConfiguration && (
<>
<Separator />
{llmProviderDescriptor.model_configurations.length > 0 ? (
<SelectorFormField
name="default_model_name"
subtext="The model to use by default for this provider unless otherwise specified."
label="Default Model"
options={llmProviderDescriptor.model_configurations.map(
(modelConfiguration) => ({
// don't clean up names here to give admins descriptive names / handle duplicates
// like us.anthropic.claude-3-7-sonnet-20250219-v1:0 and anthropic.claude-3-7-sonnet-20250219-v1:0
name: modelConfiguration.name,
value: modelConfiguration.name,
})
{llmProviderDescriptor.custom_config_keys?.map(
(customConfigKey) => {
if (customConfigKey.key_type === "text_input") {
return (
<div key={customConfigKey.name}>
<TextFormField
small={firstTimeConfiguration}
name={`custom_config.${customConfigKey.name}`}
optional={!customConfigKey.is_required}
label={customConfigKey.display_name}
subtext={
<ReactMarkdown components={{ a: customLinkRenderer }}>
{customConfigKey.description}
</ReactMarkdown>
}
placeholder={customConfigKey.default_value || undefined}
type={customConfigKey.is_secret ? "password" : "text"}
/>
</div>
);
} else if (customConfigKey.key_type === "file_input") {
return (
<FileUploadFormField
key={customConfigKey.name}
name={`custom_config.${customConfigKey.name}`}
label={customConfigKey.display_name}
subtext={customConfigKey.description || undefined}
/>
);
} else {
throw new Error(
"Unreachable; there should only exist 2 options"
);
}
}
)}
{/* Fetch models button - automatically shows for supported providers */}
{dynamicConfig && (
<div className="flex flex-col gap-2">
<Button
type="button"
onClick={() =>
fetchModels(
llmProviderDescriptor,
existingLlmProvider,
formikProps.values,
formikProps.setFieldValue,
setIsFetchingModels,
setFetchModelsError,
setPopup
)
}
disabled={
isFetchingModels ||
dynamicConfig.isDisabled(formikProps.values)
}
className="w-fit"
>
{isFetchingModels ? (
<>
<LoadingAnimation size="text-sm" />
<span className="ml-2">Fetching models...</span>
</>
) : (
"Fetch Available Models"
)}
maxHeight="max-h-56"
/>
) : (
<TextFormField
name="default_model_name"
subtext="The model to use by default for this provider unless otherwise specified."
label="Default Model"
placeholder="E.g. gpt-4"
/>
)}
</Button>
{llmProviderDescriptor.deployment_name_required && (
<TextFormField
name="deployment_name"
label="Deployment Name"
placeholder="Deployment Name"
/>
)}
{fetchModelsError && (
<Text className="text-red-600 text-sm">
{fetchModelsError}
</Text>
)}
{!llmProviderDescriptor.single_model_supported &&
(llmProviderDescriptor.model_configurations.length > 0 ? (
<Text className="text-sm text-gray-600">
Retrieve the latest available models for this provider.
</Text>
</div>
)}
{!firstTimeConfiguration && (
<>
<Separator />
{currentModelConfigurations.length > 0 ? (
<SelectorFormField
name="fast_default_model_name"
subtext="The model to use for lighter flows like `LLM Chunk Filter` for this provider. If not set, will use the Default Model configured above."
label="[Optional] Fast Model"
options={llmProviderDescriptor.model_configurations.map(
name="default_model_name"
subtext="The model to use by default for this provider unless otherwise specified."
label="Default Model"
options={currentModelConfigurations.map(
(modelConfiguration) => ({
// don't clean up names here to give admins descriptive names / handle duplicates
// like us.anthropic.claude-3-7-sonnet-20250219-v1:0 and anthropic.claude-3-7-sonnet-20250219-v1:0
@@ -598,127 +572,161 @@ export function LLMProviderUpdateForm({
value: modelConfiguration.name,
})
)}
includeDefault
maxHeight="max-h-56"
/>
) : (
<TextFormField
name="fast_default_model_name"
subtext="The model to use for lighter flows like `LLM Chunk Filter` for this provider. If not set, will use the Default Model configured above."
label="[Optional] Fast Model"
name="default_model_name"
subtext="The model to use by default for this provider unless otherwise specified."
label="Default Model"
placeholder="E.g. gpt-4"
/>
))}
<>
<Separator />
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}
/>
{showAdvancedOptions && (
<>
{llmProviderDescriptor.model_configurations.length > 0 && (
<div className="w-full">
<MultiSelectField
selectedInitially={
formikProps.values.selected_model_names ?? []
}
name="selected_model_names"
label="Display Models"
subtext="Select the models to make available to users. Unselected models will not be available."
options={llmProviderDescriptor.model_configurations.map(
(modelConfiguration) => ({
value: modelConfiguration.name,
// don't clean up names here to give admins descriptive names / handle duplicates
// like us.anthropic.claude-3-7-sonnet-20250219-v1:0 and anthropic.claude-3-7-sonnet-20250219-v1:0
label: modelConfiguration.name,
})
)}
onChange={(selected) =>
formikProps.setFieldValue(
"selected_model_names",
selected
)
}
/>
</div>
)}
<IsPublicGroupSelector
formikProps={formikProps}
objectName="LLM Provider"
publicToWhom="Users"
enforceGroupSelection={true}
/>
</>
)}
{llmProviderDescriptor.deployment_name_required && (
<TextFormField
name="deployment_name"
label="Deployment Name"
placeholder="Deployment Name"
/>
)}
{!llmProviderDescriptor.single_model_supported &&
(currentModelConfigurations.length > 0 ? (
<SelectorFormField
name="fast_default_model_name"
subtext="The model to use for lighter flows like `LLM Chunk Filter` for this provider. If not set, will use the Default Model configured above."
label="[Optional] Fast Model"
options={currentModelConfigurations.map(
(modelConfiguration) => ({
// don't clean up names here to give admins descriptive names / handle duplicates
// like us.anthropic.claude-3-7-sonnet-20250219-v1:0 and anthropic.claude-3-7-sonnet-20250219-v1:0
name: modelConfiguration.name,
value: modelConfiguration.name,
})
)}
includeDefault
maxHeight="max-h-56"
/>
) : (
<TextFormField
name="fast_default_model_name"
subtext="The model to use for lighter flows like `LLM Chunk Filter` for this provider. If not set, will use the Default Model configured above."
label="[Optional] Fast Model"
placeholder="E.g. gpt-4"
/>
))}
<>
<Separator />
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}
/>
{showAdvancedOptions && (
<>
{currentModelConfigurations.length > 0 && (
<div className="w-full">
<MultiSelectField
selectedInitially={
formikProps.values.selected_model_names ?? []
}
name="selected_model_names"
label="Display Models"
subtext="Select the models to make available to users. Unselected models will not be available."
options={currentModelConfigurations.map(
(modelConfiguration) => ({
value: modelConfiguration.name,
// don't clean up names here to give admins descriptive names / handle duplicates
// like us.anthropic.claude-3-7-sonnet-20250219-v1:0 and anthropic.claude-3-7-sonnet-20250219-v1:0
label: modelConfiguration.name,
})
)}
onChange={(selected) =>
formikProps.setFieldValue(
"selected_model_names",
selected
)
}
/>
</div>
)}
<IsPublicGroupSelector
formikProps={formikProps}
objectName="LLM Provider"
publicToWhom="Users"
enforceGroupSelection={true}
/>
</>
)}
</>
</>
</>
)}
)}
{/* NOTE: this is above the test button to make sure it's visible */}
{testError && <Text className="text-error mt-2">{testError}</Text>}
{/* NOTE: this is above the test button to make sure it's visible */}
{testError && <Text className="text-error mt-2">{testError}</Text>}
<div className="flex w-full mt-4">
<Button type="submit" variant="submit">
{isTesting ? (
<LoadingAnimation text="Testing" />
) : existingLlmProvider ? (
"Update"
) : (
"Enable"
)}
</Button>
{existingLlmProvider && (
<Button
type="button"
variant="destructive"
className="ml-3"
icon={FiTrash}
onClick={async () => {
const response = await fetch(
`${LLM_PROVIDERS_ADMIN_URL}/${existingLlmProvider.id}`,
{
method: "DELETE",
<div className="flex w-full mt-4">
<Button type="submit" variant="submit">
{isTesting ? (
<LoadingAnimation text="Testing" />
) : existingLlmProvider ? (
"Update"
) : (
"Enable"
)}
</Button>
{existingLlmProvider && (
<Button
type="button"
variant="destructive"
className="ml-3"
icon={FiTrash}
onClick={async () => {
const response = await fetch(
`${LLM_PROVIDERS_ADMIN_URL}/${existingLlmProvider.id}`,
{
method: "DELETE",
}
);
if (!response.ok) {
const errorMsg = (await response.json()).detail;
alert(`Failed to delete provider: ${errorMsg}`);
return;
}
);
if (!response.ok) {
const errorMsg = (await response.json()).detail;
alert(`Failed to delete provider: ${errorMsg}`);
return;
}
// If the deleted provider was the default, set the first remaining provider as default
const remainingProvidersResponse = await fetch(
LLM_PROVIDERS_ADMIN_URL
);
if (remainingProvidersResponse.ok) {
const remainingProviders =
await remainingProvidersResponse.json();
// If the deleted provider was the default, set the first remaining provider as default
const remainingProvidersResponse = await fetch(
LLM_PROVIDERS_ADMIN_URL
);
if (remainingProvidersResponse.ok) {
const remainingProviders =
await remainingProvidersResponse.json();
if (remainingProviders.length > 0) {
const setDefaultResponse = await fetch(
`${LLM_PROVIDERS_ADMIN_URL}/${remainingProviders[0].id}/default`,
{
method: "POST",
if (remainingProviders.length > 0) {
const setDefaultResponse = await fetch(
`${LLM_PROVIDERS_ADMIN_URL}/${remainingProviders[0].id}/default`,
{
method: "POST",
}
);
if (!setDefaultResponse.ok) {
console.error("Failed to set new default provider");
}
);
if (!setDefaultResponse.ok) {
console.error("Failed to set new default provider");
}
}
}
mutate(LLM_PROVIDERS_ADMIN_URL);
onClose();
}}
>
Delete
</Button>
)}
</div>
</Form>
)}
mutate(LLM_PROVIDERS_ADMIN_URL);
onClose();
}}
>
Delete
</Button>
)}
</div>
</Form>
);
}}
</Formik>
);
}
@@ -1,3 +1,5 @@
import { PopupSpec } from "@/components/admin/connectors/Popup";
export interface CustomConfigKey {
name: string;
display_name: string;
@@ -10,14 +12,11 @@ export interface CustomConfigKey {
export type CustomConfigKeyType = "text_input" | "file_input";
export interface ModelConfigurationUpsertRequest {
export interface ModelConfiguration {
name: string;
is_visible: boolean;
max_input_tokens: number | null;
}
export interface ModelConfiguration extends ModelConfigurationUpsertRequest {
supports_image_input: boolean;
supports_image_input: boolean | null;
}
export interface WellKnownLLMProviderDescriptor {
@@ -34,6 +33,7 @@ export interface WellKnownLLMProviderDescriptor {
model_configurations: ModelConfiguration[];
default_model: string | null;
default_fast_model: string | null;
default_api_base: string | null;
is_public: boolean;
groups: number[];
}
@@ -81,3 +81,28 @@ export interface LLMProviderDescriptor {
groups: number[];
model_configurations: ModelConfiguration[];
}
export interface OllamaModelResponse {
name: string;
max_input_tokens: number;
supports_image_input: boolean;
}
export interface DynamicProviderConfig<
TApiResponse = any,
TProcessedResponse = ModelConfiguration,
> {
endpoint: string;
isDisabled: (values: any) => boolean;
disabledReason: string;
buildRequestBody: (args: {
values: any;
existingLlmProvider?: LLMProviderView;
}) => Record<string, any>;
processResponse: (
data: TApiResponse,
llmProviderDescriptor: WellKnownLLMProviderDescriptor
) => TProcessedResponse[];
getModelNames: (data: TApiResponse) => string[];
successMessage: (count: number) => string;
}
@@ -11,6 +11,14 @@ import {
OpenAISVG,
QwenIcon,
} from "@/components/icons/icons";
import {
WellKnownLLMProviderDescriptor,
LLMProviderView,
DynamicProviderConfig,
OllamaModelResponse,
ModelConfiguration,
} from "./interfaces";
import { PopupSpec } from "@/components/admin/connectors/Popup";
export const getProviderIcon = (
providerName: string,
@@ -62,3 +70,158 @@ export const getProviderIcon = (
export const isAnthropic = (provider: string, modelName: string) =>
provider === "anthropic" || modelName.toLowerCase().includes("claude");
export const dynamicProviderConfigs: Record<
string,
DynamicProviderConfig<any, ModelConfiguration>
> = {
bedrock: {
endpoint: "/api/admin/llm/bedrock/available-models",
isDisabled: (values) => !values.custom_config?.AWS_REGION_NAME,
disabledReason: "AWS region is required to fetch Bedrock models",
buildRequestBody: ({ values, existingLlmProvider }) => ({
aws_region_name: values.custom_config?.AWS_REGION_NAME,
aws_access_key_id: values.custom_config?.AWS_ACCESS_KEY_ID,
aws_secret_access_key: values.custom_config?.AWS_SECRET_ACCESS_KEY,
aws_bearer_token_bedrock: values.custom_config?.AWS_BEARER_TOKEN_BEDROCK,
provider_name: existingLlmProvider?.name,
}),
processResponse: (data: string[], llmProviderDescriptor) =>
data.map((modelName) => {
const existingConfig = llmProviderDescriptor.model_configurations.find(
(config) => config.name === modelName
);
return {
name: modelName,
is_visible: existingConfig?.is_visible ?? false,
max_input_tokens: null,
supports_image_input: existingConfig?.supports_image_input ?? null,
};
}),
getModelNames: (data: string[]) => data,
successMessage: (count: number) =>
`Successfully fetched ${count} models for the selected region (including cross-region inference models).`,
},
ollama: {
endpoint: "/api/admin/llm/ollama/available-models",
isDisabled: (values) => !values.api_base,
disabledReason: "API Base is required to fetch Ollama models",
buildRequestBody: ({ values }) => ({
api_base: values.api_base,
}),
processResponse: (data: OllamaModelResponse[], llmProviderDescriptor) =>
data.map((modelData) => {
const existingConfig = llmProviderDescriptor.model_configurations.find(
(config) => config.name === modelData.name
);
return {
name: modelData.name,
is_visible: existingConfig?.is_visible ?? true,
max_input_tokens: modelData.max_input_tokens,
supports_image_input: modelData.supports_image_input,
};
}),
getModelNames: (data: OllamaModelResponse[]) =>
data.map((model) => model.name),
successMessage: (count: number) =>
`Successfully fetched ${count} models from Ollama.`,
},
};
export const fetchModels = async (
llmProviderDescriptor: WellKnownLLMProviderDescriptor,
existingLlmProvider: LLMProviderView | undefined,
values: any,
setFieldValue: any,
setIsFetchingModels: (loading: boolean) => void,
setFetchModelsError: (error: string) => void,
setPopup?: (popup: PopupSpec) => void
) => {
const config = dynamicProviderConfigs[llmProviderDescriptor.name];
if (!config) {
return;
}
if (config.isDisabled(values)) {
setFetchModelsError(config.disabledReason);
return;
}
setIsFetchingModels(true);
setFetchModelsError("");
try {
const response = await fetch(config.endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
config.buildRequestBody({ values, existingLlmProvider })
),
});
if (!response.ok) {
let errorMessage = "Failed to fetch models";
try {
const errorData = await response.json();
errorMessage = errorData.detail || errorMessage;
} catch {
// ignore JSON parsing errors and use the fallback message
}
throw new Error(errorMessage);
}
const availableModels = await response.json();
const updatedModelConfigs = config.processResponse(
availableModels,
llmProviderDescriptor
);
const availableModelNames = config.getModelNames(availableModels);
// Store the updated model configurations in form state instead of mutating props
setFieldValue("fetched_model_configurations", updatedModelConfigs);
// Update selected model names to only include previously visible models that are available
const previouslySelectedModels = values.selected_model_names || [];
const stillAvailableSelectedModels = previouslySelectedModels.filter(
(modelName: string) => availableModelNames.includes(modelName)
);
setFieldValue("selected_model_names", stillAvailableSelectedModels);
// Set a default model if none is set
if (
(!values.default_model_name ||
!availableModelNames.includes(values.default_model_name)) &&
availableModelNames.length > 0
) {
setFieldValue("default_model_name", availableModelNames[0]);
}
// Clear fast model if it's not in the new list
if (
values.fast_default_model_name &&
!availableModelNames.includes(values.fast_default_model_name)
) {
setFieldValue("fast_default_model_name", null);
}
// Force a re-render by updating a timestamp or counter
setFieldValue("_modelListUpdated", Date.now());
setPopup?.({
message: config.successMessage(availableModelNames.length),
type: "success",
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
setFetchModelsError(errorMessage);
setPopup?.({
message: `Failed to fetch models: ${errorMessage}`,
type: "error",
});
} finally {
setIsFetchingModels(false);
}
};
@@ -660,7 +660,7 @@ export const ChatInputBar = React.memo(function ChatInputBar({
<LLMPopover
llmProviders={llmProviders}
llmManager={llmManager}
requiresImageGeneration={true}
requiresImageGeneration={llmManager.imageFilesPresent}
currentAssistant={selectedAssistant}
/>
+34 -23
View File
@@ -955,6 +955,40 @@ const MODEL_DISPLAY_NAMES: { [key: string]: string } = {
"ai21.jamba-instruct-v1:0": "Jamba Instruct",
"ai21.j2-ultra-v1": "J2 Ultra",
"ai21.j2-mid-v1": "J2 Mid",
// Ollama cloud models
"gpt-oss:20b-cloud": "gpt-oss 20B Cloud",
"gpt-oss:120b-cloud": "gpt-oss 120B Cloud",
"deepseek-v3.1:671b-cloud": "DeepSeek-v3.1 671B Cloud",
"kimi-k2:1t": "Kimi K2 1T Cloud",
"qwen3-coder:480b-cloud": "Qwen3-Coder 480B Cloud",
// Ollama models in litellm map (disjoint from ollama's supported model list)
// https://models.litellm.ai --> provider ollama
codegeex4: "CodeGeeX 4",
codegemma: "CodeGemma",
codellama: "CodeLLama",
"deepseek-coder-v2-base": "DeepSeek-Coder-v2 Base",
"deepseek-coder-v2-instruct": "DeepSeek-Coder-v2 Instruct",
"deepseek-coder-v2-lite-base": "DeepSeek-Coder-v2 Lite Base",
"deepseek-coder-v2-lite-instruct": "DeepSeek-Coder-v2 Lite Instruct",
"internlm2_5-20b-chat": "InternLM 2.5 20B Chat",
llama2: "Llama 2",
"llama2-uncensored": "Llama 2 Uncensored",
"llama2:13b": "Llama 2 13B",
"llama2:70b": "Llama 2 70B",
"llama2:7b": "Llama 2 7B",
llama3: "Llama 3",
"llama3:70b": "Llama 3 70B",
"llama3:8b": "Llama 3 8B",
mistral: "Mistral", // Mistral 7b
"mistral-7B-Instruct-v0.1": "Mistral 7B Instruct v0.1",
"mistral-7B-Instruct-v0.2": "Mistral 7B Instruct v0.2",
"mistral-large-instruct-2407": "Mistral Large Instruct 24.07",
"mixtral-8x22B-Instruct-v0.1": "Mixtral 8x22B Instruct v0.1",
"mixtral8x7B-Instruct-v0.1": "Mixtral 8x7B Instruct v0.1",
"orca-mini": "Orca Mini",
vicuna: "Vicuna",
};
export function getDisplayNameForModel(modelName: string): string {
@@ -972,29 +1006,6 @@ export function getDisplayNameForModel(modelName: string): string {
return MODEL_DISPLAY_NAMES[modelName] || modelName;
}
export const defaultModelsByProvider: { [name: string]: string[] } = {
openai: [
"gpt-4",
"gpt-4o",
"gpt-4o-mini",
"gpt-4.1",
"o3-mini",
"o1-mini",
"o1",
"o4-mini",
"o3",
],
bedrock: [
"meta.llama3-1-70b-instruct-v1:0",
"meta.llama3-1-8b-instruct-v1:0",
"anthropic.claude-3-opus-20240229-v1:0",
"mistral.mistral-large-2402-v1:0",
"anthropic.claude-3-5-sonnet-20241022-v2:0",
"anthropic.claude-3-7-sonnet-20250219-v1:0",
],
anthropic: ["claude-3-opus-20240229", "claude-3-5-sonnet-20241022"],
};
// Get source metadata for configured sources - deduplicated by source type
function getConfiguredSources(
availableSources: ValidSources[]