* feat: introduce hindsight-api-slim and hindsight-all-slim packages Closes #552 - Move all source code from hindsight-api/ to new hindsight-api-slim/ - hindsight-api-slim has heavy ML deps (torch, sentence-transformers, transformers, einops, flashrank, mlx, mlx-lm, safetensors) and pg0-embedded as optional extras: [local-ml], [embedded-db], [all] - hindsight-api becomes a zero-code meta-package depending on hindsight-api-slim[all] for full backward compatibility - Add hindsight-all-slim meta-package: hindsight-api-slim + client + embed - hindsight-all updated to depend on hindsight-api-slim[all] - pg0.py: lazy-import pg0 with clear ImportError pointing to [embedded-db] - Dockerfile: replace sed hack with proper uv sync --extra flags - Update release.yml, test.yml, lint.sh, release.sh, CLAUDE.md and all path references throughout the repo * refactor: rename hindsight/ directory to hindsight-all/ * docs: document hindsight-api-slim and hindsight-all-slim package variants Add package variants table and extras explanation to installation.md * docs: remove emojis from installation.md, use professional tone * docs: link Docker slim variant to pip package variants section * docs: consolidate Docker image variants into single table * ci: fix working-directory paths after package restructure - Replace all hindsight-api → hindsight-api-slim in test.yml - Replace hindsight → hindsight-all in test.yml - Add --extra embedded-db to test-embed API install step * ci: add local-ml and embedded-db extras to API sync steps These extras were previously implicit in the old hindsight-api package (which bundled everything). Now that hindsight-api-slim uses optional extras, we must explicitly request local-ml and embedded-db in CI. * ci: add API install step with embedded-db to test-embed smoke test The smoke test starts hindsight-api as a daemon, which requires pg0-embedded. Add a dedicated install step for hindsight-api-slim with embedded-db extra so the daemon can start successfully. * ci: remove --no-install-project when using optional extras When --no-install-project is combined with --extra, the optional deps are not installed because extras require the project to be active. Remove --no-install-project from steps that need local-ml or embedded-db. * ci: fix ordering of uv sync steps to preserve optional extras When uv sync runs for a different workspace member, it removes optional extras installed for other members. Fix by always running extra-requiring API sync last, after other workspace member syncs. Also remove --no-install-project from embedded-db sync in test-embed, as --no-install-project prevents optional extras from being active. * ci: add local-ml extra to test-embed API install for smoke test The smoke test starts the full API server which needs sentence-transformers for local embeddings (default provider). Add local-ml extra to the install. * ci: simplify extras with --all-extras and add slim pip smoke test - Replace explicit --extra local-ml --extra embedded-db with --all-extras for cleaner, more maintainable sync steps - Add test-pip-slim job: tests hindsight-api-slim[embedded-db] without local ML models, using Cohere for embeddings/reranking (mirrors Docker slim smoke test approach) * ci: simplify slim smoke test to health check only (mirrors Docker test)
245 lines
7.6 KiB
Python
245 lines
7.6 KiB
Python
"""
|
|
Test Vertex AI provider integration using native genai SDK.
|
|
"""
|
|
|
|
import os
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# Skip all tests if google-auth not available
|
|
pytest.importorskip("google.auth")
|
|
|
|
|
|
def test_llm_wrapper_vertexai_missing_dependency():
|
|
"""Test error when google-auth is not available and service account key is set."""
|
|
from hindsight_api.engine import llm_wrapper
|
|
|
|
# VERTEXAI_AVAILABLE only matters when a service account key is provided
|
|
original_available = llm_wrapper.VERTEXAI_AVAILABLE
|
|
try:
|
|
llm_wrapper.VERTEXAI_AVAILABLE = False
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID": "test-project",
|
|
"HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY": "/path/to/key.json",
|
|
},
|
|
clear=False,
|
|
):
|
|
from hindsight_api.config import clear_config_cache
|
|
|
|
clear_config_cache()
|
|
|
|
with pytest.raises(ValueError, match="google-auth"):
|
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
|
|
|
LLMProvider(
|
|
provider="vertexai",
|
|
api_key="",
|
|
base_url="",
|
|
model="google/gemini-2.0-flash-001",
|
|
)
|
|
|
|
clear_config_cache()
|
|
finally:
|
|
llm_wrapper.VERTEXAI_AVAILABLE = original_available
|
|
|
|
|
|
def test_llm_wrapper_vertexai_missing_project_id():
|
|
"""Test error when project ID is not configured."""
|
|
with patch.dict(os.environ, {"HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID": ""}, clear=False):
|
|
from hindsight_api.config import clear_config_cache
|
|
|
|
clear_config_cache()
|
|
|
|
with pytest.raises(ValueError, match="HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID"):
|
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
|
|
|
LLMProvider(
|
|
provider="vertexai",
|
|
api_key="",
|
|
base_url="",
|
|
model="google/gemini-2.0-flash-001",
|
|
)
|
|
|
|
clear_config_cache()
|
|
|
|
|
|
def test_llm_wrapper_vertexai_adc_auth():
|
|
"""Test Vertex AI with ADC authentication creates native genai client."""
|
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID": "test-project",
|
|
"HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY": "", # Clear SA key to test ADC path
|
|
},
|
|
clear=False,
|
|
):
|
|
from hindsight_api.config import clear_config_cache
|
|
|
|
clear_config_cache()
|
|
|
|
# genai.Client handles ADC internally — just verify it creates the client
|
|
with patch("google.genai.Client") as mock_client_cls:
|
|
mock_client_cls.return_value = MagicMock()
|
|
|
|
provider = LLMProvider(
|
|
provider="vertexai",
|
|
api_key="",
|
|
base_url="",
|
|
model="google/gemini-2.0-flash-001",
|
|
)
|
|
|
|
assert provider.provider == "vertexai"
|
|
assert provider.model == "gemini-2.0-flash-001" # google/ prefix stripped
|
|
assert provider._gemini_client is not None
|
|
|
|
# Verify genai.Client was called with vertexai=True
|
|
call_kwargs = mock_client_cls.call_args.kwargs
|
|
assert call_kwargs["vertexai"] is True
|
|
assert call_kwargs["project"] == "test-project"
|
|
assert call_kwargs["location"] == "us-central1"
|
|
|
|
clear_config_cache()
|
|
|
|
|
|
def test_llm_wrapper_vertexai_sa_auth():
|
|
"""Test Vertex AI with service account authentication passes credentials to genai client."""
|
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
|
|
|
mock_credentials = MagicMock()
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID": "test-project",
|
|
"HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY": "/path/to/key.json",
|
|
},
|
|
clear=False,
|
|
):
|
|
from hindsight_api.config import clear_config_cache
|
|
|
|
clear_config_cache()
|
|
|
|
with patch(
|
|
"google.oauth2.service_account.Credentials.from_service_account_file",
|
|
return_value=mock_credentials,
|
|
):
|
|
with patch("google.genai.Client") as mock_client_cls:
|
|
mock_client_cls.return_value = MagicMock()
|
|
|
|
provider = LLMProvider(
|
|
provider="vertexai",
|
|
api_key="",
|
|
base_url="",
|
|
model="google/gemini-2.0-flash-001",
|
|
)
|
|
|
|
assert provider.provider == "vertexai"
|
|
assert provider._gemini_client is not None
|
|
|
|
# Verify credentials were passed to genai.Client
|
|
call_kwargs = mock_client_cls.call_args.kwargs
|
|
assert call_kwargs["vertexai"] is True
|
|
assert call_kwargs["project"] == "test-project"
|
|
assert call_kwargs["location"] == "us-central1"
|
|
assert call_kwargs["credentials"] is mock_credentials
|
|
|
|
clear_config_cache()
|
|
|
|
|
|
def test_llm_wrapper_vertexai_strips_google_prefix():
|
|
"""Test that google/ prefix is stripped from model name for native SDK."""
|
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{"HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID": "test-project"},
|
|
clear=False,
|
|
):
|
|
from hindsight_api.config import clear_config_cache
|
|
|
|
clear_config_cache()
|
|
|
|
with patch("google.genai.Client") as mock_client_cls:
|
|
mock_client_cls.return_value = MagicMock()
|
|
|
|
provider = LLMProvider(
|
|
provider="vertexai",
|
|
api_key="",
|
|
base_url="",
|
|
model="google/gemini-2.0-flash-lite-001",
|
|
)
|
|
|
|
assert provider.model == "gemini-2.0-flash-lite-001"
|
|
|
|
clear_config_cache()
|
|
|
|
|
|
def test_llm_wrapper_vertexai_no_prefix_model():
|
|
"""Test that model without google/ prefix is unchanged."""
|
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{"HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID": "test-project"},
|
|
clear=False,
|
|
):
|
|
from hindsight_api.config import clear_config_cache
|
|
|
|
clear_config_cache()
|
|
|
|
with patch("google.genai.Client") as mock_client_cls:
|
|
mock_client_cls.return_value = MagicMock()
|
|
|
|
provider = LLMProvider(
|
|
provider="vertexai",
|
|
api_key="",
|
|
base_url="",
|
|
model="gemini-2.0-flash-001",
|
|
)
|
|
|
|
assert provider.model == "gemini-2.0-flash-001"
|
|
|
|
clear_config_cache()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
not os.getenv("HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID"),
|
|
reason="Vertex AI integration tests require HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID",
|
|
)
|
|
async def test_vertexai_integration_actual_api():
|
|
"""
|
|
Integration test with actual Vertex AI API.
|
|
|
|
Requires:
|
|
- HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID
|
|
- ADC or HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY
|
|
"""
|
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
|
|
|
provider = LLMProvider(
|
|
provider="vertexai",
|
|
api_key="",
|
|
base_url="",
|
|
model="google/gemini-2.0-flash-001",
|
|
)
|
|
|
|
try:
|
|
# Simple test call
|
|
response = await provider.call(
|
|
messages=[{"role": "user", "content": "Say 'ok' and nothing else"}],
|
|
max_completion_tokens=10,
|
|
)
|
|
|
|
assert response is not None
|
|
assert isinstance(response, str)
|
|
assert len(response) > 0
|
|
|
|
finally:
|
|
await provider.cleanup()
|