Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/skillspector/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@

Selection happens via the ``SKILLSPECTOR_PROVIDER`` env var:

openai → OpenAIProvider (api.openai.com)
anthropic → AnthropicProvider (api.anthropic.com)
anthropic_proxy → AnthropicProxyProvider (Vertex-style raw-predict proxy)
nv_build → NvBuildProvider (build.nvidia.com)
openai → OpenAIProvider (api.openai.com)
anthropic → AnthropicProvider (api.anthropic.com)
anthropic_proxy → AnthropicProxyProvider (Vertex-style raw-predict proxy)
nv_build → NvBuildProvider (build.nvidia.com)
ollama → OllamaProvider (local Ollama instance)
azure_openai → AzureOpenAIProvider (Azure OpenAI Service)
openai_compatible → OpenAICompatibleProvider (Groq, Together AI, Mistral, etc.)

When unset, the selector defaults to ``nv_build``.
"""
Expand All @@ -44,6 +47,7 @@
"No LLM API key configured. Set the credential env var for the "
"active provider, or set OPENAI_API_KEY (and optionally "
"OPENAI_BASE_URL) to use a standard OpenAI-compatible endpoint. "
"For local models, try SKILLSPECTOR_PROVIDER=ollama. "
"Use --no-llm to skip LLM analysis and run static checks only."
)

Expand All @@ -69,6 +73,18 @@ def _select_active_provider() -> LLMProvider:
from .anthropic_proxy import AnthropicProxyProvider

return AnthropicProxyProvider()
if name == "ollama":
from .ollama import OllamaProvider

return OllamaProvider()
if name == "azure_openai":
from .azure_openai import AzureOpenAIProvider

return AzureOpenAIProvider()
if name == "openai_compatible":
from .openai_compatible import OpenAICompatibleProvider

return OpenAICompatibleProvider()
if name == "nv_build":
return NvBuildProvider()
if name in ("nv_inference", ""):
Expand All @@ -83,7 +99,8 @@ def _select_active_provider() -> LLMProvider:

raise ValueError(
f"Unknown SKILLSPECTOR_PROVIDER: {name!r}. "
"Expected one of: openai, anthropic, anthropic_proxy, nv_build (or unset)."
"Expected one of: openai, anthropic, anthropic_proxy, ollama, "
"azure_openai, openai_compatible, nv_build (or unset)."
)


Expand Down
20 changes: 20 additions & 0 deletions src/skillspector/providers/azure_openai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Azure OpenAI provider package (Azure-hosted OpenAI deployments)."""

from .provider import REGISTRY_PATH, AzureOpenAIProvider

__all__ = ["REGISTRY_PATH", "AzureOpenAIProvider"]
23 changes: 23 additions & 0 deletions src/skillspector/providers/azure_openai/model_registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Token-budget metadata for the AzureOpenAIProvider (Azure-hosted OpenAI
# deployments). Bundled with the package; consulted whenever the active
# provider is AzureOpenAIProvider.
#
# Format:
# models:
# "<model-label>":
# context_length: <int> # total context window in tokens (required)
# max_output_tokens: <int> # model's max output cap (optional)

models:
"gpt-4o":
context_length: 128000
max_output_tokens: 16384
"gpt-4o-mini":
context_length: 128000
max_output_tokens: 16384
"gpt-4":
context_length: 8192
max_output_tokens: 4096
"gpt-4-turbo":
context_length: 128000
max_output_tokens: 4096
92 changes: 92 additions & 0 deletions src/skillspector/providers/azure_openai/provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Azure OpenAI provider — enterprise Azure-hosted OpenAI deployments.

Uses ``AzureChatOpenAI`` from ``langchain_openai`` which handles Azure's
deployment-based routing and ``api-version`` query parameter natively.

Required env vars:
AZURE_OPENAI_ENDPOINT — Azure resource endpoint
AZURE_OPENAI_API_KEY — Azure API key

Optional env vars:
AZURE_OPENAI_DEPLOYMENT — deployment name (defaults to model label)
AZURE_OPENAI_API_VERSION — API version (defaults to ``2024-06-01``)
"""

from __future__ import annotations

import os
from pathlib import Path

from langchain_core.language_models.chat_models import BaseChatModel
from langchain_openai import AzureChatOpenAI
from pydantic import SecretStr

from skillspector.providers import registry

REGISTRY_PATH = str(Path(__file__).with_name("model_registry.yaml"))


class AzureOpenAIProvider:
"""Azure OpenAI credentials + bundled-YAML metadata provider."""

DEFAULT_MODEL = "gpt-4o"
SLOT_DEFAULTS: dict[str, str] = {}

def resolve_credentials(self) -> tuple[str, str | None] | None:
"""Return ``(api_key, endpoint)`` from Azure OpenAI env vars."""
api_key = os.environ.get("AZURE_OPENAI_API_KEY", "").strip()
endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT", "").strip()
if not api_key or not endpoint:
return None
return api_key, endpoint

def create_chat_model(
self,
model: str,
*,
max_tokens: int,
timeout: float | None = 120,
) -> BaseChatModel | None:
"""Create ``AzureChatOpenAI`` using Azure-specific credentials."""
creds = self.resolve_credentials()
if creds is None:
return None

api_key, endpoint = creds
deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "").strip() or model
api_version = os.environ.get("AZURE_OPENAI_API_VERSION", "").strip() or "2024-06-01"

return AzureChatOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment,
api_key=SecretStr(api_key),
api_version=api_version,
max_tokens=max_tokens,
timeout=timeout,
)

def get_context_length(self, model: str) -> int | None:
return registry.lookup_context_length(REGISTRY_PATH, model)

def get_max_output_tokens(self, model: str) -> int | None:
return registry.lookup_max_output_tokens(REGISTRY_PATH, model)

def resolve_model(self, slot: str = "default") -> str:
"""Resolve model: ``SKILLSPECTOR_MODEL`` env > slot default > ``DEFAULT_MODEL``."""
user_input = os.environ.get("SKILLSPECTOR_MODEL", "").strip()
return user_input or self.SLOT_DEFAULTS.get(slot, "") or self.DEFAULT_MODEL
20 changes: 20 additions & 0 deletions src/skillspector/providers/ollama/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Ollama provider package (local/self-hosted OpenAI-compatible endpoint)."""

from .provider import OLLAMA_DEFAULT_BASE_URL, REGISTRY_PATH, OllamaProvider

__all__ = ["OLLAMA_DEFAULT_BASE_URL", "REGISTRY_PATH", "OllamaProvider"]
29 changes: 29 additions & 0 deletions src/skillspector/providers/ollama/model_registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Token-budget metadata for the OllamaProvider (local/self-hosted LLMs).
# Bundled with the package; consulted whenever the active provider is
# OllamaProvider.
#
# Format:
# models:
# "<model-label>":
# context_length: <int> # total context window in tokens (required)
# max_output_tokens: <int> # model's max output cap (optional)

models:
"llama3.1:8b":
context_length: 131072
max_output_tokens: 4096
"llama3.1:70b":
context_length: 131072
max_output_tokens: 4096
"mistral:7b":
context_length: 32768
max_output_tokens: 4096
"qwen2.5:7b":
context_length: 131072
max_output_tokens: 8192
"gemma2:9b":
context_length: 8192
max_output_tokens: 4096
"deepseek-v3:latest":
context_length: 65536
max_output_tokens: 8192
80 changes: 80 additions & 0 deletions src/skillspector/providers/ollama/provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Ollama provider — local/self-hosted LLM via OpenAI-compatible API.

Ollama serves an OpenAI-compatible endpoint at ``http://localhost:11434/v1``
by default. No API key is required; ``ChatOpenAI`` needs a non-empty
placeholder string.

Override the endpoint via ``OLLAMA_BASE_URL`` for remote Ollama instances.
"""

from __future__ import annotations

import os
from pathlib import Path

from langchain_core.language_models.chat_models import BaseChatModel

from skillspector.providers import registry
from skillspector.providers.chat_models import create_openai_compatible_chat_model

OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434/v1"

REGISTRY_PATH = str(Path(__file__).with_name("model_registry.yaml"))


class OllamaProvider:
"""Ollama credentials + bundled-YAML metadata provider."""

DEFAULT_MODEL = "llama3.1:8b"
SLOT_DEFAULTS: dict[str, str] = {}

def resolve_credentials(self) -> tuple[str, str | None] | None:
"""Return ``(api_key, base_url)`` for the Ollama endpoint.

Ollama does not require an API key but ``ChatOpenAI`` needs a
non-empty string — we supply the literal ``"ollama"``.
Always returns credentials (Ollama is assumed available when selected).
"""
base_url = os.environ.get("OLLAMA_BASE_URL", "").strip() or OLLAMA_DEFAULT_BASE_URL
return "ollama", base_url

def create_chat_model(
self,
model: str,
*,
max_tokens: int,
timeout: float | None = 120,
) -> BaseChatModel | None:
"""Create ``ChatOpenAI`` pointing at the Ollama endpoint."""
return create_openai_compatible_chat_model(
model=model,
credentials=self.resolve_credentials(),
max_tokens=max_tokens,
timeout=timeout,
)

def get_context_length(self, model: str) -> int | None:
return registry.lookup_context_length(REGISTRY_PATH, model)

def get_max_output_tokens(self, model: str) -> int | None:
return registry.lookup_max_output_tokens(REGISTRY_PATH, model)

def resolve_model(self, slot: str = "default") -> str:
"""Resolve model: ``SKILLSPECTOR_MODEL`` env > slot default > ``DEFAULT_MODEL``."""
user_input = os.environ.get("SKILLSPECTOR_MODEL", "").strip()
return user_input or self.SLOT_DEFAULTS.get(slot, "") or self.DEFAULT_MODEL
20 changes: 20 additions & 0 deletions src/skillspector/providers/openai_compatible/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Generic OpenAI-compatible provider package (Groq, Together AI, Mistral, etc.)."""

from .provider import REGISTRY_PATH, OpenAICompatibleProvider

__all__ = ["REGISTRY_PATH", "OpenAICompatibleProvider"]
43 changes: 43 additions & 0 deletions src/skillspector/providers/openai_compatible/model_registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Token-budget metadata for the OpenAICompatibleProvider (Groq, Together AI,
# Mistral, DeepInfra, Fireworks, LiteLLM, and other OpenAI-compatible
# endpoints). Bundled with the package; consulted whenever the active
# provider is OpenAICompatibleProvider.
#
# Format:
# models:
# "<model-label>":
# context_length: <int> # total context window in tokens (required)
# max_output_tokens: <int> # model's max output cap (optional)

models:
# Groq
"llama-3.1-70b-versatile":
context_length: 131072
max_output_tokens: 8192
"llama-3.1-8b-instant":
context_length: 131072
max_output_tokens: 8192
"mixtral-8x7b-32768":
context_length: 32768
max_output_tokens: 4096

# Together AI
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo":
context_length: 131072
max_output_tokens: 4096
"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo":
context_length: 131072
max_output_tokens: 4096

# Mistral
"mistral-large-latest":
context_length: 131072
max_output_tokens: 8192
"mistral-small-latest":
context_length: 131072
max_output_tokens: 8192

# DeepInfra / generic
"deepseek-ai/DeepSeek-V3":
context_length: 65536
max_output_tokens: 8192
Loading