From 09a9c241618b98c6e1d7c9c3a8f7cdb56b4c19d8 Mon Sep 17 00:00:00 2001 From: Logan Solonche Date: Tue, 2 Jun 2026 16:33:36 -0400 Subject: [PATCH 1/5] Added VertexAI support, including GCP ADC Resolution --- .env.example | 7 +- README.md | 8 ++ model_registry.yaml | 21 ++++ pyproject.toml | 1 + src/skillspector/providers/__init__.py | 6 +- .../providers/nv_build/model_registry.yaml | 21 ++++ .../providers/openai/model_registry.yaml | 21 ++++ .../providers/vertexai/__init__.py | 20 ++++ .../providers/vertexai/model_registry.yaml | 30 +++++ .../providers/vertexai/provider.py | 107 ++++++++++++++++++ 10 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 src/skillspector/providers/vertexai/__init__.py create mode 100644 src/skillspector/providers/vertexai/model_registry.yaml create mode 100644 src/skillspector/providers/vertexai/provider.py diff --git a/.env.example b/.env.example index 24ad4f5..01b60fb 100644 --- a/.env.example +++ b/.env.example @@ -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 | nv_build +# Options: openai | anthropic | vertexai | nv_build SKILLSPECTOR_PROVIDER= # Provider credentials — set the one matching SKILLSPECTOR_PROVIDER (or @@ -21,6 +21,11 @@ OPENAI_BASE_URL= # For SKILLSPECTOR_PROVIDER=anthropic. ANTHROPIC_API_KEY= +# 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/ diff --git a/README.md b/README.md index ab84623..c9038fd 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,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 diff --git a/model_registry.yaml b/model_registry.yaml index e1c2b8c..f528891 100644 --- a/model_registry.yaml +++ b/model_registry.yaml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 9cd0e53..0c469f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dependencies = [ "langchain-openai>=1.1.10", "langsmith>=0.7.30", "yara-python>=4.5.0", + "google-auth>=2.53.0", ] [project.optional-dependencies] diff --git a/src/skillspector/providers/__init__.py b/src/skillspector/providers/__init__.py index 78bdd17..0c26fa0 100644 --- a/src/skillspector/providers/__init__.py +++ b/src/skillspector/providers/__init__.py @@ -49,6 +49,10 @@ def _select_active_provider() -> ModelMetadataProvider: from .anthropic import AnthropicProvider return AnthropicProvider() + if name == "vertexai": + from .vertexai import VertexAIProvider + + return VertexAIProvider() if name == "nv_build": return NvBuildProvider() if name in ("nv_inference", ""): @@ -63,7 +67,7 @@ def _select_active_provider() -> ModelMetadataProvider: raise ValueError( f"Unknown SKILLSPECTOR_PROVIDER: {name!r}. " - "Expected one of: openai, anthropic, nv_build (or unset)." + "Expected one of: openai, anthropic, vertexai, nv_build (or unset)." ) diff --git a/src/skillspector/providers/nv_build/model_registry.yaml b/src/skillspector/providers/nv_build/model_registry.yaml index aeba04e..2485b6f 100644 --- a/src/skillspector/providers/nv_build/model_registry.yaml +++ b/src/skillspector/providers/nv_build/model_registry.yaml @@ -26,3 +26,24 @@ models: "openai/gpt-oss-120b": 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 diff --git a/src/skillspector/providers/openai/model_registry.yaml b/src/skillspector/providers/openai/model_registry.yaml index a4d2606..3145e61 100644 --- a/src/skillspector/providers/openai/model_registry.yaml +++ b/src/skillspector/providers/openai/model_registry.yaml @@ -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 diff --git a/src/skillspector/providers/vertexai/__init__.py b/src/skillspector/providers/vertexai/__init__.py new file mode 100644 index 0000000..0099188 --- /dev/null +++ b/src/skillspector/providers/vertexai/__init__.py @@ -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"] diff --git a/src/skillspector/providers/vertexai/model_registry.yaml b/src/skillspector/providers/vertexai/model_registry.yaml new file mode 100644 index 0000000..3fafa8c --- /dev/null +++ b/src/skillspector/providers/vertexai/model_registry.yaml @@ -0,0 +1,30 @@ +# Token-budget metadata for the VertexAIProvider. Bundled with the +# package; consulted whenever the active provider is VertexAIProvider. +# +# Format: +# models: +# "": +# context_length: # total context window in tokens (required) +# max_output_tokens: # 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 diff --git a/src/skillspector/providers/vertexai/provider.py b/src/skillspector/providers/vertexai/provider.py new file mode 100644 index 0000000..1982614 --- /dev/null +++ b/src/skillspector/providers/vertexai/provider.py @@ -0,0 +1,107 @@ +# 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 skillspector.providers import registry + +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] = {} + + + 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 project cannot be determined or 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 + + # If we get here, the user explicitly configured VertexAI, + # so let authentication errors propagate for debugging + + + credentials, default_project = google.auth.default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + + project = project_id or default_project + if not project: + raise ValueError( + "Could not determine GCP project. Ensure GOOGLE_CLOUD_PROJECT " + "is set or the credentials file contains a project ID." + ) + + 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." + ) + + # Construct the VertexAI OpenAI-compatible base URL + base_url = ( + f"https://{location}-aiplatform.googleapis.com/v1beta1/" + f"projects/{project}/locations/{location}/endpoints/openapi" + ) + + return access_token, base_url + + 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 \ No newline at end of file From 3188053c5f9e8cc0ef50efd9d6e6a8f527eb2965 Mon Sep 17 00:00:00 2001 From: Logan Solonche Date: Thu, 25 Jun 2026 13:28:08 -0400 Subject: [PATCH 2/5] Remove gemini model registry from nv_build provider --- .../providers/nv_build/model_registry.yaml | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/skillspector/providers/nv_build/model_registry.yaml b/src/skillspector/providers/nv_build/model_registry.yaml index 2485b6f..97be064 100644 --- a/src/skillspector/providers/nv_build/model_registry.yaml +++ b/src/skillspector/providers/nv_build/model_registry.yaml @@ -27,23 +27,4 @@ models: 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 + From 5f8db1ecf483c68bb7b4aba9d3033d67ca44f444 Mon Sep 17 00:00:00 2001 From: Logan Solonche Date: Thu, 25 Jun 2026 13:36:15 -0400 Subject: [PATCH 3/5] Resolve prefix + add tests --- .../providers/vertexai/provider.py | 26 +++++ tests/unit/test_providers.py | 95 +++++++++++++++++++ uv.lock | 38 +++++++- 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/src/skillspector/providers/vertexai/provider.py b/src/skillspector/providers/vertexai/provider.py index 1982614..760b9ba 100644 --- a/src/skillspector/providers/vertexai/provider.py +++ b/src/skillspector/providers/vertexai/provider.py @@ -29,7 +29,10 @@ 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")) @@ -40,6 +43,7 @@ class VertexAIProvider: 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. @@ -95,6 +99,28 @@ def resolve_credentials(self) -> tuple[str, str | None] | None: 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) diff --git a/tests/unit/test_providers.py b/tests/unit/test_providers.py index 2886d4e..3fc93c4 100644 --- a/tests/unit/test_providers.py +++ b/tests/unit/test_providers.py @@ -23,6 +23,7 @@ from __future__ import annotations import sys +from unittest.mock import MagicMock, patch import pytest from langchain_anthropic import ChatAnthropic @@ -40,6 +41,7 @@ from skillspector.providers.chat_models import create_openai_compatible_chat_model from skillspector.providers.nv_build import BUILD_BASE_URL, NvBuildProvider from skillspector.providers.openai import OpenAIProvider +from skillspector.providers.vertexai import VertexAIProvider try: from skillspector.providers.nv_inference import ( @@ -69,6 +71,9 @@ def _clean_provider_env(monkeypatch: pytest.MonkeyPatch): monkeypatch.delenv("SKILLSPECTOR_MODEL", raising=False) monkeypatch.delenv("SKILLSPECTOR_MODEL_REGISTRY", raising=False) monkeypatch.delenv("SKILLSPECTOR_PROVIDER", raising=False) + monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_LOCATION", raising=False) registry._load.cache_clear() yield registry._load.cache_clear() @@ -270,6 +275,96 @@ def test_metadata_known_models(self) -> None: assert provider.get_context_length("claude-sonnet-4-6") == 1_000_000 +class TestVertexAIProvider: + """VertexAI provider — credentials, model prefix, and bundled YAML metadata.""" + + def test_returns_none_without_env_vars(self) -> None: + assert VertexAIProvider().resolve_credentials() is None + + def test_returns_none_with_partial_env(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "my-project") + assert VertexAIProvider().resolve_credentials() is None + + @patch("skillspector.providers.vertexai.provider.google.auth.default") + @patch("skillspector.providers.vertexai.provider.google.auth.transport.requests.Request") + def test_resolves_credentials( + self, + mock_request: MagicMock, + mock_auth_default: MagicMock, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + mock_creds = MagicMock() + mock_creds.token = "fake-access-token" + mock_auth_default.return_value = (mock_creds, "default-project") + monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "my-project") + monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "us-central1") + + creds = VertexAIProvider().resolve_credentials() + assert creds is not None + token, base_url = creds + assert token == "fake-access-token" + assert "us-central1" in base_url + assert "my-project" in base_url + assert base_url.endswith("/endpoints/openapi") + + @patch("skillspector.providers.vertexai.provider.google.auth.default") + @patch("skillspector.providers.vertexai.provider.google.auth.transport.requests.Request") + def test_create_chat_model_prefixes_model_with_google( + self, + mock_request: MagicMock, + mock_auth_default: MagicMock, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + mock_creds = MagicMock() + mock_creds.token = "fake-access-token" + mock_auth_default.return_value = (mock_creds, "default-project") + monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "my-project") + monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "us-central1") + + llm = VertexAIProvider().create_chat_model("gemini-2.5-flash", max_tokens=123) + assert isinstance(llm, ChatOpenAI) + assert llm.model_name == "google/gemini-2.5-flash" + assert llm.max_tokens == 123 + + @patch("skillspector.providers.vertexai.provider.google.auth.default") + @patch("skillspector.providers.vertexai.provider.google.auth.transport.requests.Request") + def test_create_chat_model_does_not_double_prefix( + self, + mock_request: MagicMock, + mock_auth_default: MagicMock, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + mock_creds = MagicMock() + mock_creds.token = "fake-access-token" + mock_auth_default.return_value = (mock_creds, "default-project") + monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "my-project") + monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "us-central1") + + llm = VertexAIProvider().create_chat_model("google/gemini-2.5-flash", max_tokens=123) + assert isinstance(llm, ChatOpenAI) + assert llm.model_name == "google/gemini-2.5-flash" + + def test_create_chat_model_returns_none_without_credentials(self) -> None: + assert VertexAIProvider().create_chat_model("gemini-2.5-flash", max_tokens=123) is None + + def test_metadata_known_model_from_bundled_yaml(self) -> None: + provider = VertexAIProvider() + assert provider.get_context_length("gemini-2.5-flash") == 1_048_576 + assert provider.get_max_output_tokens("gemini-2.5-flash") == 65_535 + + def test_metadata_unknown_model_returns_none(self) -> None: + provider = VertexAIProvider() + assert provider.get_context_length("unknown-model") is None + assert provider.get_max_output_tokens("unknown-model") is None + + def test_resolve_model_default_when_no_env(self) -> None: + assert VertexAIProvider().resolve_model() == "gemini-2.5-flash" + + def test_resolve_model_env_overrides_default(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("SKILLSPECTOR_MODEL", "gemini-2.5-pro") + assert VertexAIProvider().resolve_model() == "gemini-2.5-pro" + + class TestOpenAICompatibleConstructor: """The shared OpenAI-compatible chat-model constructor.""" diff --git a/uv.lock b/uv.lock index b214e86..e1914bf 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12, <3.14" [[package]] @@ -441,6 +441,19 @@ version = "0.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e6/79/d4f20e91327c98096d605646bdc6a5ffedae820f38d378d3515c42ec5e60/forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253", size = 43756, upload-time = "2021-01-16T21:03:35.401Z" } +[[package]] +name = "google-auth" +version = "2.55.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/1c/70b23fc52b2bb3c70b379f3bd05c4a60ab3a873e30c6bd21c57e0154848a/google_auth-2.55.0.tar.gz", hash = "sha256:fcd3a130f575fa36403d38774af1c64a4fbfbca09215f0589d2372b5119697cb", size = 349379, upload-time = "2026-06-15T22:33:16.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/71/c0321dc6d63d99946da45f7c06299b934e4f7f7da5c4f14d101bcb39adf1/google_auth-2.55.0-py3-none-any.whl", hash = "sha256:a17cef9dedf98c4ebae2fb0c48c8f75952c877cbc2efe09f329ef16c2783d88a", size = 252400, upload-time = "2026-06-15T22:33:14.992Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.72.0" @@ -1476,6 +1489,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -2017,6 +2051,7 @@ name = "skillspector" version = "2.3.7" source = { editable = "." } dependencies = [ + { name = "google-auth" }, { name = "httpx" }, { name = "langchain-anthropic" }, { name = "langchain-core" }, @@ -2051,6 +2086,7 @@ mcp = [ [package.metadata] requires-dist = [ { name = "build", marker = "extra == 'dev'", specifier = ">=1.4.0" }, + { name = "google-auth", specifier = ">=2.53.0" }, { name = "httpx", specifier = ">=0.28.0" }, { name = "langchain-anthropic", specifier = ">=1.4.5" }, { name = "langchain-core", specifier = ">=1.2.17" }, From c9da077009f6311817b04a5aa9fac93b0eb5543d Mon Sep 17 00:00:00 2001 From: Logan Solonche Date: Thu, 25 Jun 2026 13:37:02 -0400 Subject: [PATCH 4/5] Fix linting errors --- src/skillspector/providers/vertexai/provider.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/skillspector/providers/vertexai/provider.py b/src/skillspector/providers/vertexai/provider.py index 760b9ba..f66ea2a 100644 --- a/src/skillspector/providers/vertexai/provider.py +++ b/src/skillspector/providers/vertexai/provider.py @@ -28,7 +28,6 @@ import google.auth import google.auth.transport.requests - from langchain_core.language_models.chat_models import BaseChatModel from skillspector.providers import registry @@ -59,7 +58,7 @@ def resolve_credentials(self) -> tuple[str, str | None] | None: are configured but invalid or malformed. ValueError: When project cannot be determined or token refresh fails. """ - + project_id = os.environ.get("GOOGLE_CLOUD_PROJECT", "").strip() location = os.environ.get("GOOGLE_CLOUD_LOCATION", "").strip() @@ -69,7 +68,7 @@ def resolve_credentials(self) -> tuple[str, str | None] | None: # If we get here, the user explicitly configured VertexAI, # so let authentication errors propagate for debugging - + credentials, default_project = google.auth.default( scopes=["https://www.googleapis.com/auth/cloud-platform"] ) @@ -130,4 +129,4 @@ def get_max_output_tokens(self, model: str) -> int | None: 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 \ No newline at end of file + return user_input or self.SLOT_DEFAULTS.get(slot, "") or self.DEFAULT_MODEL From 8eedd7e1b3892b4f1566bd60f5288142d48d0ec3 Mon Sep 17 00:00:00 2001 From: Logan Solonche Date: Thu, 25 Jun 2026 13:40:56 -0400 Subject: [PATCH 5/5] Remove dead branch --- .../providers/vertexai/provider.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/skillspector/providers/vertexai/provider.py b/src/skillspector/providers/vertexai/provider.py index f66ea2a..7660467 100644 --- a/src/skillspector/providers/vertexai/provider.py +++ b/src/skillspector/providers/vertexai/provider.py @@ -56,7 +56,7 @@ def resolve_credentials(self) -> tuple[str, str | None] | None: Raises: google.auth.exceptions.DefaultCredentialsError: When credentials are configured but invalid or malformed. - ValueError: When project cannot be determined or token refresh fails. + ValueError: When token refresh fails. """ project_id = os.environ.get("GOOGLE_CLOUD_PROJECT", "").strip() @@ -65,21 +65,10 @@ def resolve_credentials(self) -> tuple[str, str | None] | None: if not project_id or not location: return None - # If we get here, the user explicitly configured VertexAI, - # so let authentication errors propagate for debugging - - - credentials, default_project = google.auth.default( + credentials, _ = google.auth.default( scopes=["https://www.googleapis.com/auth/cloud-platform"] ) - project = project_id or default_project - if not project: - raise ValueError( - "Could not determine GCP project. Ensure GOOGLE_CLOUD_PROJECT " - "is set or the credentials file contains a project ID." - ) - credentials.refresh(google.auth.transport.requests.Request()) access_token = credentials.token @@ -90,10 +79,9 @@ def resolve_credentials(self) -> tuple[str, str | None] | None: "service account key file." ) - # Construct the VertexAI OpenAI-compatible base URL base_url = ( f"https://{location}-aiplatform.googleapis.com/v1beta1/" - f"projects/{project}/locations/{location}/endpoints/openapi" + f"projects/{project_id}/locations/{location}/endpoints/openapi" ) return access_token, base_url