This commit is contained in:
Nitin Krishna Mucheli 2026-08-26 07:34:37 +01:00 committed by GitHub
commit 7adefa0ac4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 94 additions and 18 deletions

View file

@ -673,6 +673,7 @@ wandb_models: Set = set(WANDB_MODELS)
ovhcloud_models: Set = set()
ovhcloud_embedding_models: Set = set()
lemonade_models: Set = set()
lm_studio_models: Set = set() # mutable-ok: dynamic-discovery-only, like lemonade_models
docker_model_runner_models: Set = set()
amazon_nova_models: Set = set()
stability_models: Set = set()
@ -1191,6 +1192,7 @@ def _build_models_by_provider() -> dict:
"wandb": wandb_models,
"ovhcloud": ovhcloud_models | ovhcloud_embedding_models,
"lemonade": lemonade_models,
"lm_studio": lm_studio_models,
"clarifai": clarifai_models,
"amazon_nova": amazon_nova_models,
"stability": stability_models,

View file

@ -19,6 +19,25 @@ class LMStudioChatConfig(OpenAIGPTConfig):
) # LM Studio does not require an api key, but OpenAI client requires non-None value
return api_base, dynamic_api_key
def get_models(self, api_key: str | None = None, api_base: str | None = None) -> list[str]:
"""
Calls LM Studio's `/v1/models` endpoint and returns the list of models,
prefixed with "lm_studio/" (matching the OllamaModelInfo convention)
so discovered ids are directly callable without the caller having to
add the provider prefix themselves.
Reuses the same api_base/api_key resolution as chat completions
(LM_STUDIO_API_BASE / LM_STUDIO_API_KEY env vars, "fake-api-key"
fallback) instead of OpenAIGPTConfig's default of
https://api.openai.com, which would be wrong for a local/self-hosted
LM Studio server.
"""
api_base, api_key = self._get_openai_compatible_provider_info(api_base=api_base, api_key=api_key)
models: Final = super().get_models(api_key=api_key, api_base=api_base)
return [ # mutable-ok: matches OllamaModelInfo.get_models' list-of-prefixed-ids contract
m if m.startswith("lm_studio/") else f"lm_studio/{m}" for m in models
]
def map_openai_params(
self,
non_default_params: dict,

View file

@ -8664,24 +8664,31 @@ class ProviderConfigManager:
model: str | None,
provider: LlmProviders,
) -> BaseLLMModelInfo | None:
if LlmProviders.FIREWORKS_AI == provider:
return litellm.FireworksAIConfig()
elif LlmProviders.OPENAI == provider:
return litellm.OpenAIGPTConfig()
elif LlmProviders.GEMINI == provider:
return litellm.GeminiModelInfo()
# Providers whose BaseLLMModelInfo needs no local import and no `model`
# arg: a single dict lookup instead of N `elif` branches keeps this
# dispatcher's cyclomatic complexity from growing with every provider
# added here (see the elif chain below for providers that DO need a
# local import or the `model` arg).
simple_provider_configs: Final[
dict[LlmProviders, Callable[[], BaseLLMModelInfo]]
] = { # mutable-ok: built once per call, read-only lookup table replacing 9 trivial elif branches
LlmProviders.FIREWORKS_AI: litellm.FireworksAIConfig,
LlmProviders.OPENAI: litellm.OpenAIGPTConfig,
LlmProviders.GEMINI: litellm.GeminiModelInfo,
LlmProviders.LITELLM_PROXY: litellm.LiteLLMProxyChatConfig,
LlmProviders.TOPAZ: litellm.TopazModelInfo,
LlmProviders.ANTHROPIC: litellm.AnthropicModelInfo,
LlmProviders.XAI: litellm.XAIModelInfo,
LlmProviders.LEMONADE: litellm.LemonadeChatConfig,
LlmProviders.CLARIFAI: litellm.ClarifaiConfig,
LlmProviders.LM_STUDIO: litellm.LMStudioChatConfig,
}
if provider in simple_provider_configs:
return simple_provider_configs[provider]()
elif LlmProviders.VERTEX_AI == provider:
from litellm.llms.vertex_ai.common_utils import VertexAIModelInfo
return VertexAIModelInfo()
elif LlmProviders.LITELLM_PROXY == provider:
return litellm.LiteLLMProxyChatConfig()
elif LlmProviders.TOPAZ == provider:
return litellm.TopazModelInfo()
elif LlmProviders.ANTHROPIC == provider:
return litellm.AnthropicModelInfo()
elif LlmProviders.XAI == provider:
return litellm.XAIModelInfo()
elif LlmProviders.OLLAMA == provider or LlmProviders.OLLAMA_CHAT == provider:
# Dynamic model listing for Ollama server
from litellm.llms.ollama.common_utils import OllamaModelInfo
@ -8693,10 +8700,6 @@ class ProviderConfigManager:
)
return VLLMModelInfo()
elif LlmProviders.LEMONADE == provider:
return litellm.LemonadeChatConfig()
elif LlmProviders.CLARIFAI == provider:
return litellm.ClarifaiConfig()
elif LlmProviders.BEDROCK == provider:
from litellm.llms.bedrock.common_utils import BedrockModelInfo

View file

@ -2887,6 +2887,58 @@ class TestGetValidModelsWithCLI:
assert headers.get("Authorization") == "Bearer sk-test-cli-key-123"
class TestGetValidModelsLMStudio:
"""Test get_valid_models(check_provider_endpoint=True) for lm_studio.
get_provider_model_info() previously had no LM_STUDIO branch, so
discovery silently returned an empty list regardless of
check_provider_endpoint; and lm_studio was missing from
models_by_provider, so the proxy's wildcard-model expansion
(get_provider_models) bailed out before ever calling get_valid_models.
"""
def test_get_valid_models_lm_studio_discovery(self):
"""Discovery hits LM Studio's own /v1/models, not OpenAI's default api_base, and prefixes results with lm_studio/."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": [
{"id": "qwen/qwen3-a3b", "object": "model"},
{"id": "lm_studio/already-prefixed", "object": "model"},
]
}
with patch.object(
litellm.module_level_client, "get", return_value=mock_response
) as mock_get:
result = litellm.get_valid_models(
check_provider_endpoint=True,
custom_llm_provider="lm_studio",
api_key="lm-studio-key",
api_base="http://my-lm-studio-host:1234",
)
assert isinstance(result, list)
assert result == [
"lm_studio/qwen/qwen3-a3b",
"lm_studio/already-prefixed",
]
mock_get.assert_called_once()
_, call_kwargs = mock_get.call_args
# Must hit the passed-in LM Studio host, not OpenAIGPTConfig's
# default of https://api.openai.com
assert call_kwargs["url"] == "http://my-lm-studio-host:1234/v1/models"
assert call_kwargs["headers"]["Authorization"] == "Bearer lm-studio-key"
def test_lm_studio_in_models_by_provider(self):
"""lm_studio must be a key in models_by_provider or the proxy's
wildcard-model expansion (get_provider_models) bails out before
ever reaching get_valid_models/check_provider_endpoint."""
assert "lm_studio" in litellm.models_by_provider
class TestIsCachedMessage:
"""Test is_cached_message function for context caching detection.