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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ENV=dev # options: dev|s

# Active LLM provider. Selects which provider answers credentials,
# metadata, and default-model lookups. Leave unset to default to nv_build.
# Options: openai | anthropic | anthropic_proxy | nv_build
# Options: openai | anthropic | anthropic_proxy | vertexai | nv_build
SKILLSPECTOR_PROVIDER=

# Provider credentials — set the one matching SKILLSPECTOR_PROVIDER (or
Expand All @@ -28,6 +28,11 @@ ANTHROPIC_PROXY_API_KEY=
# ANTHROPIC_PROXY_API_VERSION=vertex-2023-10-16 # optional; defaults to vertex-2023-10-16
# SKILLSPECTOR_SSL_VERIFY=false # set to false for internal/self-signed CAs

# For SKILLSPECTOR_PROVIDER=vertexai
GOOGLE_APPLICATION_CREDENTIALS=
GOOGLE_CLOUD_PROJECT=
GOOGLE_CLOUD_LOCATION=

# SkillSpector config
SKILLSPECTOR_MODEL= # leave empty to use the active provider's bundled default (see README); set to override (e.g. gpt-5.2)
# SKILLSPECTOR_MODEL_REGISTRY=./model_registry.yaml # optional override; defaults to each provider's bundled YAML in src/skillspector/providers/
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ export SKILLSPECTOR_PROVIDER=nv_build
export NVIDIA_INFERENCE_KEY=nvapi-...
skillspector scan ./my-skill/

# VertexAI (Google Cloud)
export SKILLSPECTOR_PROVIDER=vertexai
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
export GOOGLE_CLOUD_PROJECT=your-project-id
export GOOGLE_CLOUD_LOCATION=us-central1
export SKILLSPECTOR_MODEL=gemini-2.5-pro
skillspector scan ./my-skill/

# Local Ollama or any OpenAI-compatible endpoint
export SKILLSPECTOR_PROVIDER=openai
export OPENAI_API_KEY=ollama
Expand Down
21 changes: 21 additions & 0 deletions model_registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,24 @@ models:
"openai/openai/gpt-5.3-chat":
context_length: 128000
max_output_tokens: 16384

# Google Gemini models (via VertexAI or AI Studio OpenAI-compatible endpoints)
"gemini-2.0-flash":
context_length: 1048576
max_output_tokens: 8192

"gemini-2.5-pro":
context_length: 1000000
max_output_tokens: 65536

"gemini-2.5-flash":
context_length: 1048576
max_output_tokens: 65535

"gemini-3.1-pro":
context_length: 1000000
max_output_tokens: 65536

"gemini-3.5-flash":
context_length: 1048576
max_output_tokens: 8192
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies = [
"langchain-openai>=1.1.10",
"langsmith>=0.7.30",
"yara-python>=4.5.0",
"google-auth>=2.53.0",
]

[project.optional-dependencies]
Expand Down
6 changes: 5 additions & 1 deletion src/skillspector/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ def _select_active_provider() -> LLMProvider:
from .anthropic_proxy import AnthropicProxyProvider

return AnthropicProxyProvider()
if name == "vertexai":
from .vertexai import VertexAIProvider

return VertexAIProvider()
if name == "nv_build":
return NvBuildProvider()
if name in ("nv_inference", ""):
Expand All @@ -83,7 +87,7 @@ 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, vertexai, nv_build (or unset)."
)


Expand Down
2 changes: 2 additions & 0 deletions src/skillspector/providers/nv_build/model_registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ models:
"openai/gpt-oss-120b":
context_length: 128000
max_output_tokens: 16384


21 changes: 21 additions & 0 deletions src/skillspector/providers/openai/model_registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,24 @@ models:
"gpt-5.4":
context_length: 1000000
max_output_tokens: 128000

# Google Gemini models (via VertexAI or AI Studio OpenAI-compatible endpoints)
"gemini-2.0-flash":
context_length: 1048576
max_output_tokens: 8192

"gemini-2.5-pro":
context_length: 1000000
max_output_tokens: 65536

"gemini-2.5-flash":
context_length: 1048576
max_output_tokens: 65535

"gemini-3.1-pro":
context_length: 1000000
max_output_tokens: 65536

"gemini-3.5-flash":
context_length: 1048576
max_output_tokens: 8192
20 changes: 20 additions & 0 deletions src/skillspector/providers/vertexai/__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.

"""VertexAI provider package (VertexAI OpenAI-compatibility endpoint)."""

from .provider import REGISTRY_PATH, VertexAIProvider

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

models:
# Google Gemini models (via VertexAI)
"gemini-2.0-flash":
context_length: 1048576
max_output_tokens: 8192

"gemini-2.5-pro":
context_length: 1000000
max_output_tokens: 65536

"gemini-2.5-flash":
context_length: 1048576
max_output_tokens: 65535

"gemini-3.1-pro":
context_length: 1000000
max_output_tokens: 65536

"gemini-3.5-flash":
context_length: 1048576
max_output_tokens: 8192
120 changes: 120 additions & 0 deletions src/skillspector/providers/vertexai/provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# 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.

"""VertexAI provider — Gemini models via VertexAI OpenAI-compatible endpoint.

Reads ``GOOGLE_APPLICATION_CREDENTIALS``, ``GOOGLE_CLOUD_PROJECT``, and
``GOOGLE_CLOUD_LOCATION`` for credentials and constructs the VertexAI
OpenAI-compatible endpoint URL. Uses Google Cloud Application Default
Credentials (ADC) to generate access tokens. Defaults to Gemini 2.5 Flash.
"""

from __future__ import annotations

import os
from pathlib import Path

import google.auth
import google.auth.transport.requests
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

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


class VertexAIProvider:
"""Stock VertexAI credentials + bundled-YAML metadata provider."""

DEFAULT_MODEL = "gemini-2.5-flash"
SLOT_DEFAULTS: dict[str, str] = {}

WIRE_MODEL_PREFIX = "google/"

def resolve_credentials(self) -> tuple[str, str | None] | None:
"""Return ``(access_token, base_url)`` from Google Cloud credentials.

Uses Application Default Credentials (ADC) via ``google.auth.default()``.
The access token is refreshed from the credentials object and returned
as the API key for the OpenAI-compatible client.

Returns ``None`` when required environment variables are not set.

Raises:
google.auth.exceptions.DefaultCredentialsError: When credentials
are configured but invalid or malformed.
ValueError: When token refresh fails.
"""

project_id = os.environ.get("GOOGLE_CLOUD_PROJECT", "").strip()
location = os.environ.get("GOOGLE_CLOUD_LOCATION", "").strip()

if not project_id or not location:
return None

credentials, _ = google.auth.default(
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)

credentials.refresh(google.auth.transport.requests.Request())

access_token = credentials.token
if not access_token:
raise ValueError(
"Failed to obtain access token from Google Cloud credentials. "
"Ensure GOOGLE_APPLICATION_CREDENTIALS points to a valid "
"service account key file."
)

base_url = (
f"https://{location}-aiplatform.googleapis.com/v1beta1/"
f"projects/{project_id}/locations/{location}/endpoints/openapi"
)

return access_token, base_url

def create_chat_model(
self,
model: str,
*,
max_tokens: int,
timeout: float | None = 120,
) -> BaseChatModel | None:
"""Create ``ChatOpenAI`` for the VertexAI OpenAI-compatible endpoint.

The endpoint requires model names prefixed with ``google/``
(e.g. ``google/gemini-2.5-flash``). The prefix is applied here
at the wire boundary so that registry lookups and token-budget
calculations continue to use bare model labels.
"""
wire_model = model if model.startswith(self.WIRE_MODEL_PREFIX) else f"{self.WIRE_MODEL_PREFIX}{model}"
return create_openai_compatible_chat_model(
model=wire_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
Loading