fix: consistent model numbering between /model and /model-show for Ol… (#371)

fix: consistent model numbering between /model and /model-show for Ollama

When using OLLAMA_API_BASE, model selection by number was incorrect.
/model-show displayed Ollama models with certain numbers, but /model <number>
selected different models from LiteLLM instead.

This happened because both commands used separate caches with inconsistent
numbering. Now they share a global cache that loads models in consistent order:
predefined → LiteLLM → Ollama.

Fixes model number mismatch when selecting Ollama models by number.
This commit is contained in:
pzabalegui 2025-12-10 12:41:35 +01:00 committed by GitHub
parent e932e30850
commit 7c2267d2b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 137 additions and 142 deletions

View File

@ -22,6 +22,10 @@ LITELLM_URL = (
"model_prices_and_context_window.json" "model_prices_and_context_window.json"
) )
# Global cache shared between /model and /model-show commands
_GLOBAL_MODEL_CACHE = []
_GLOBAL_MODEL_NUMBERS = {}
def get_predefined_model_categories() -> Dict[str, List[Dict[str, str]]]: def get_predefined_model_categories() -> Dict[str, List[Dict[str, str]]]:
"""Get the predefined model categories as the single source of truth. """Get the predefined model categories as the single source of truth.
@ -155,6 +159,43 @@ def get_predefined_model_names() -> List[str]:
return [model["name"] for model in get_all_predefined_models()] return [model["name"] for model in get_all_predefined_models()]
def load_all_available_models() -> tuple[List[str], List[Dict[str, Any]]]:
"""Load all available models (predefined + LiteLLM + Ollama) in consistent order.
This ensures /model and /model-show use the same numbering.
Returns:
Tuple of (all_model_names, ollama_models_data)
"""
# Predefined models
predefined = [model["name"] for model in get_all_predefined_models()]
# LiteLLM models
litellm_names = []
try:
response = requests.get(LITELLM_URL, timeout=5)
if response.status_code == 200:
litellm_names = sorted(response.json().keys())
except Exception: # pylint: disable=broad-except
pass
# Ollama models
ollama_data = []
ollama_names = []
try:
api_base = get_ollama_api_base()
response = requests.get(f"{api_base.replace('/v1', '')}/api/tags", timeout=1)
if response.status_code == 200:
data = response.json()
ollama_data = data.get('models', data.get('items', []))
ollama_names = [m.get('name', '') for m in ollama_data if m.get('name')]
except Exception: # pylint: disable=broad-except
pass
all_models = predefined + litellm_names + ollama_names
return all_models, ollama_data
class ModelCommand(Command): class ModelCommand(Command):
"""Command for viewing and changing the current LLM model.""" """Command for viewing and changing the current LLM model."""
@ -196,29 +237,21 @@ class ModelCommand(Command):
Returns: Returns:
bool: True if the model was changed successfully bool: True if the model was changed successfully
""" """
# Get all predefined models from the shared source of truth # Load all models with consistent numbering and update global cache
# pylint: disable=invalid-name global _GLOBAL_MODEL_CACHE, _GLOBAL_MODEL_NUMBERS
ALL_MODELS = get_all_predefined_models() _GLOBAL_MODEL_CACHE, ollama_models_data = load_all_available_models()
_GLOBAL_MODEL_NUMBERS = {
# Also fetch LiteLLM model names to make numbering consistent with /model-show
litellm_model_names = []
try:
response = requests.get(LITELLM_URL, timeout=5)
if response.status_code == 200:
litellm_data = response.json()
# Add LiteLLM model names (sorted for consistency with /model-show)
litellm_model_names = sorted(litellm_data.keys())
except Exception: # pylint: disable=broad-except
# Silently fail if LiteLLM is not available
pass
# Update cached models to include all models for number selection (consistent with /model-show)
predefined_model_names = [model["name"] for model in ALL_MODELS]
self.cached_models = predefined_model_names + litellm_model_names
self.cached_model_numbers = {
str(i): model_name str(i): model_name
for i, model_name in enumerate(self.cached_models, 1) for i, model_name in enumerate(_GLOBAL_MODEL_CACHE, 1)
} }
self.cached_models = _GLOBAL_MODEL_CACHE
self.cached_model_numbers = _GLOBAL_MODEL_NUMBERS
# Get predefined and litellm counts for display
ALL_MODELS = get_all_predefined_models()
predefined_model_names = [model["name"] for model in ALL_MODELS]
litellm_model_names = [m for m in self.cached_models[len(predefined_model_names):]
if m not in [d.get('name') for d in ollama_models_data]]
if not args: # pylint: disable=too-many-nested-blocks if not args: # pylint: disable=too-many-nested-blocks
# Display current model # Display current model
@ -272,61 +305,35 @@ class ModelCommand(Command):
model["description"] model["description"]
) )
# Ollama models (if available) # Ollama models (display from already loaded data)
# pylint: disable=too-many-nested-blocks if ollama_models_data:
try: start_index = len(predefined_model_names) + len(litellm_model_names) + 1
# Get Ollama models with a short timeout to prevent hanging for i, model in enumerate(ollama_models_data, start_index):
api_base = get_ollama_api_base() model_name = model.get('name', '')
ollama_base = api_base.replace('/v1', '') model_size = model.get('size', 0)
response = requests.get( size_str = ""
f"{ollama_base}/api/tags", if model_size:
timeout=1 size_mb = model_size / (1024 * 1024)
) if model_size < 1024 * 1024 * 1024:
size_str = f"{size_mb:.1f} MB"
if response.status_code == 200: else:
data = response.json() size_gb = size_mb / 1024
ollama_models = [] size_str = f"{size_gb:.1f} GB"
if 'models' in data: model_description = "Local model"
ollama_models = data['models'] if size_str:
else: model_description += f" ({size_str})"
# Fallback for older Ollama versions
ollama_models = data.get('items', []) model_table.add_row(
str(i),
# Add Ollama models to the table with continuing numbers model_name,
# (after predefined models + LiteLLM models in numbering) "Ollama",
start_index = len(predefined_model_names) + len(litellm_model_names) + 1 "Local",
for i, model in enumerate(ollama_models, start_index): "Free",
model_name = model.get('name', '') "Free",
model_size = model.get('size', 0) model_description
# Convert size to human-readable format )
size_str = "" else: # pylint: disable=broad-except
if model_size:
size_mb = model_size / (1024 * 1024)
if model_size < 1024 * 1024 * 1024:
size_str = f"{size_mb:.1f} MB"
else:
size_gb = size_mb / 1024
size_str = f"{size_gb:.1f} GB"
# Ollama models are free to use locally
model_description = "Local model"
if size_str:
model_description += f" ({size_str})"
model_table.add_row(
str(i),
model_name,
"Ollama",
"Local",
"Free",
"Free",
model_description
)
# Add to cached models for numeric selection
self.cached_models.append(model_name)
self.cached_model_numbers[str(i)] = model_name
except Exception: # pylint: disable=broad-except
# Add a note about Ollama if we couldn't fetch models # Add a note about Ollama if we couldn't fetch models
start_index = len(predefined_model_names) + len(litellm_model_names) + 1 start_index = len(predefined_model_names) + len(litellm_model_names) + 1
model_table.add_row( model_table.add_row(
@ -436,6 +443,15 @@ class ModelShowCommand(Command):
if args: # If there are still args left, use as search term if args: # If there are still args left, use as search term
search_term = args[0].lower() search_term = args[0].lower()
# Load all models and update global cache for consistent numbering with /model
global _GLOBAL_MODEL_CACHE, _GLOBAL_MODEL_NUMBERS
all_model_names, ollama_models_data = load_all_available_models()
_GLOBAL_MODEL_CACHE = all_model_names
_GLOBAL_MODEL_NUMBERS = {
str(i): model_name
for i, model_name in enumerate(_GLOBAL_MODEL_CACHE, 1)
}
# Fetch model pricing data from LiteLLM GitHub repository # Fetch model pricing data from LiteLLM GitHub repository
try: try:
with console.status( with console.status(
@ -482,10 +498,14 @@ class ModelShowCommand(Command):
# Count models for summary # Count models for summary
total_models = 0 total_models = 0
displayed_models = 0 displayed_models = 0
model_index = 1
# Process and display models # Process and display models (use global cache for numbering)
for model_name, model_info in sorted(model_data.items()): for model_name, model_info in sorted(model_data.items()):
# Find the model index from global cache
try:
model_index = _GLOBAL_MODEL_CACHE.index(model_name) + 1
except ValueError:
continue # Model not in cache, skip
total_models += 1 total_models += 1
# Skip if showing only supported models and no function calling # Skip if showing only supported models and no function calling
@ -569,71 +589,46 @@ class ModelShowCommand(Command):
features_str features_str
) )
model_index += 1 # Add Ollama models to the table (already loaded in global cache)
for model in ollama_models_data:
# Now add Ollama models if available model_name = model.get('name', '')
try:
# Get Ollama models with a short timeout # Skip if search term provided and not in model name
api_base = get_ollama_api_base() if search_term and search_term not in model_name.lower():
api_tags = f"{api_base.replace('/v1', '')}/api/tags" continue
ollama_response = requests.get(api_tags, timeout=1)
# Find index from global cache
if ollama_response.status_code == 200: try:
ollama_data = ollama_response.json() model_index = _GLOBAL_MODEL_CACHE.index(model_name) + 1
ollama_models = [] except ValueError:
continue
if 'models' in ollama_data:
ollama_models = ollama_data['models'] total_models += 1
displayed_models += 1
model_size = model.get('size', 0)
size_str = ""
if model_size:
size_mb = model_size / (1024 * 1024)
if model_size < 1024 * 1024 * 1024:
size_str = f"{size_mb:.1f} MB"
else: else:
# Fallback for older Ollama versions size_gb = size_mb / 1024
ollama_models = ollama_data.get('items', []) size_str = f"{size_gb:.1f} GB"
# Add Ollama models to the table model_description = "Local model"
for model in ollama_models: if size_str:
model_name = model.get('name', '') model_description += f" ({size_str})"
# Skip if search term provided and not in model name model_table.add_row(
if (search_term and str(model_index),
search_term not in model_name.lower()): model_name,
continue "Ollama",
"Varies",
total_models += 1 "Free",
displayed_models += 1 "Free",
model_description
model_size = model.get('size', 0) )
# Convert size to human-readable format
size_str = ""
if model_size:
size_mb = model_size / (1024 * 1024)
if model_size < 1024 * 1024 * 1024:
size_str = f"{size_mb:.1f} MB"
else:
size_gb = size_mb / 1024
size_str = f"{size_gb:.1f} GB"
# Add row to table
model_description = "Local model"
if size_str:
model_description += f" ({size_str})"
model_table.add_row(
str(model_index),
model_name,
"Ollama",
"Varies",
"Free",
"Free",
model_description
)
model_index += 1
except Exception: # pylint: disable=broad-except
# Silently fail if Ollama is not available
# This is acceptable as Ollama is optional and we don't want to
# disrupt the user experience if it's not running
console.print(
"[dim]Ollama models not available[/dim]",
style="dim")
# Display the table # Display the table
console.print(model_table) console.print(model_table)