mirror of https://github.com/aliasrobotics/cai.git
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:
parent
e932e30850
commit
7c2267d2b9
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue