diff --git a/application/single_app/config.py b/application/single_app/config.py index 47dc23320..6cd524ec4 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -97,7 +97,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.250.065" +VERSION = "0.250.066" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/application/single_app/functions_model_capabilities.py b/application/single_app/functions_model_capabilities.py new file mode 100644 index 000000000..aecb13dab --- /dev/null +++ b/application/single_app/functions_model_capabilities.py @@ -0,0 +1,53 @@ +# functions_model_capabilities.py + +import re +from collections.abc import Mapping + + +MODEL_IDENTIFIER_SEPARATOR_PATTERN = re.compile(r"[\s_.]+") +GPT_VISION_MODEL_PATTERN = re.compile(r"(?:^|-)gpt-(?:[5-9]|\d{2,})(?:-|$)") +O_SERIES_MODEL_PATTERN = re.compile(r"(?:^|-)o\d+(?:-|$)") +MODEL_IDENTIFIER_FIELDS = ( + "modelName", + "displayName", + "deploymentName", + "deployment", + "name", +) + + +def _normalize_model_identifier(value): + return MODEL_IDENTIFIER_SEPARATOR_PATTERN.sub( + "-", + str(value or "").strip().lower(), + ) + + +def is_vision_capable_model_name(*model_names): + """Return whether any supplied identifier names a supported vision model.""" + for model_name in model_names: + normalized_name = _normalize_model_identifier(model_name) + if ( + "vision" in normalized_name + or "gpt-4o" in normalized_name + or "gpt-4-1" in normalized_name + or "gpt-4-5" in normalized_name + or GPT_VISION_MODEL_PATTERN.search(normalized_name) + or O_SERIES_MODEL_PATTERN.search(normalized_name) + ): + return True + + return False + + +def is_vision_capable_model(model): + """Return whether a model record or identifier names a supported vision model.""" + if isinstance(model, str): + return is_vision_capable_model_name(model) + + if isinstance(model, Mapping): + model_names = [model.get(field_name) for field_name in MODEL_IDENTIFIER_FIELDS] + else: + model_names = [getattr(model, field_name, None) for field_name in MODEL_IDENTIFIER_FIELDS] + + return is_vision_capable_model_name(*model_names) \ No newline at end of file diff --git a/application/single_app/route_frontend_admin_settings.py b/application/single_app/route_frontend_admin_settings.py index 878c2abd8..366a34c32 100644 --- a/application/single_app/route_frontend_admin_settings.py +++ b/application/single_app/route_frontend_admin_settings.py @@ -27,6 +27,7 @@ from functions_notifications import broadcast_system_notification from functions_logging import * from functions_document_actions import normalize_document_action_capabilities +from functions_model_capabilities import is_vision_capable_model from functions_terms_of_use import ( TERMS_OF_USE_DEFAULT_REDIRECT, TERMS_OF_USE_MAX_BUTTON_TEXT_LENGTH, @@ -596,7 +597,8 @@ def admin_settings(): chunk_size_cap=get_chunk_size_cap(settings), chunk_size_effective=get_chunk_size_config(settings), audio_runtime_capabilities=audio_runtime_capabilities, - source_review_runtime_capabilities=source_review_runtime_capabilities + source_review_runtime_capabilities=source_review_runtime_capabilities, + is_vision_capable_model=is_vision_capable_model, # You don't need to pass deployments separately if they are added to settings['..._model']['all'] # gpt_deployments=gpt_deployments, # embedding_deployments=embedding_deployments, diff --git a/application/single_app/static/js/admin/admin_settings.js b/application/single_app/static/js/admin/admin_settings.js index b5e996ac6..ad47f3a16 100644 --- a/application/single_app/static/js/admin/admin_settings.js +++ b/application/single_app/static/js/admin/admin_settings.js @@ -8364,18 +8364,22 @@ const visionToggle = document.getElementById('enable_multimodal_vision'); const visionModelDiv = document.getElementById('multimodal_vision_model_settings'); const visionSelect = document.getElementById('multimodal_vision_model'); -function isVisionCapableModelName(modelName) { - const modelNameLower = (modelName || '').toLowerCase(); - return ( - modelNameLower.includes('vision') || - modelNameLower.includes('gpt-4o') || - modelNameLower.includes('gpt-4.1') || - modelNameLower.includes('gpt-4.5') || - modelNameLower.includes('gpt-5') || - /^o\d+/.test(modelNameLower) || - modelNameLower.includes('o1-') || - modelNameLower.includes('o3-') - ); +function isVisionCapableModelName(...modelNames) { + return modelNames.some(modelName => { + const normalizedName = String(modelName || '') + .trim() + .toLowerCase() + .replace(/[\s_.]+/g, '-'); + + return ( + normalizedName.includes('vision') || + normalizedName.includes('gpt-4o') || + normalizedName.includes('gpt-4-1') || + normalizedName.includes('gpt-4-5') || + /(?:^|-)gpt-(?:[5-9]|\d{2,})(?:-|$)/.test(normalizedName) || + /(?:^|-)o\d+(?:-|$)/.test(normalizedName) + ); + }); } function getSelectedVisionModelOption() { @@ -8401,10 +8405,18 @@ function populateVisionModels() { .filter(ep => ep && ep.enabled) .forEach(ep => { (ep.models || []) - .filter(m => m && m.enabled && isVisionCapableModelName(m.modelName || m.displayName)) + .filter(m => m && m.enabled && isVisionCapableModelName( + m.modelName, + m.displayName, + m.deploymentName, + m.deployment, + m.name + )) .forEach(m => { const value = m.deploymentName; - const label = `${m.displayName || m.deploymentName} (${m.modelName})`; + const label = m.modelName + ? `${m.displayName || m.deploymentName} (${m.modelName})` + : (m.displayName || m.deploymentName); const opt = new Option(label, value); opt.dataset.endpointId = ep.id || ''; opt.dataset.modelId = m.id || ''; diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html index 74d0fa978..8af4d70d8 100644 --- a/application/single_app/templates/admin_settings.html +++ b/application/single_app/templates/admin_settings.html @@ -8213,19 +8213,7 @@
{# Iterate all enabled endpoints and models, filter to vision-capable #} {% for endpoint in settings.model_endpoints if endpoint.enabled %} {% for m in endpoint.models if m.enabled %} - {% set model_lower = (m.modelName or m.displayName or '').lower() %} - {% set is_vision = - 'vision' in model_lower or - 'gpt-4o' in model_lower or - 'gpt-4.1' in model_lower or - 'gpt-4.5' in model_lower or - '-5' in model_lower or - model_lower.startswith('o1') or - model_lower.startswith('o3') or - 'o1-' in model_lower or - 'o3-' in model_lower - %} - {% if is_vision %} + {% if is_vision_capable_model is defined and is_vision_capable_model(m) %} {% set option_value = m.deploymentName %} {% endif %} {% endfor %} @@ -8242,19 +8230,7 @@
{# Legacy APIM-based GPT configuration #} {% for d in (settings.azure_apim_gpt_deployment or '').split(',') if d %} {% set d_trim = d.strip() %} - {% set model_lower = d_trim.lower() %} - {% set is_vision = - 'vision' in model_lower or - 'gpt-4o' in model_lower or - 'gpt-4.1' in model_lower or - 'gpt-4.5' in model_lower or - '-5' in model_lower or - model_lower.startswith('o1') or - model_lower.startswith('o3') or - 'o1-' in model_lower or - 'o3-' in model_lower - %} - {% if is_vision %} + {% if is_vision_capable_model is defined and is_vision_capable_model(d_trim) %}