From 356754bdbeb19767ff297eceece7dd03d87ecde3 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Wed, 11 Feb 2026 23:24:31 +0200 Subject: [PATCH 01/11] SDK-89: Use huggingface_hub for model access validation --- hirundo/_model_access.py | 132 +++++++++++++++++++++++++++++++++++ hirundo/llm_behavior_eval.py | 23 ++++++ hirundo/unlearning_llm.py | 13 +++- pyproject.toml | 2 + tests/test_model_access.py | 84 ++++++++++++++++++++++ uv.lock | 6 +- 6 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 hirundo/_model_access.py create mode 100644 tests/test_model_access.py diff --git a/hirundo/_model_access.py b/hirundo/_model_access.py new file mode 100644 index 00000000..a8361d9a --- /dev/null +++ b/hirundo/_model_access.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +from pathlib import Path + +from huggingface_hub import HfApi +from huggingface_hub.errors import GatedRepoError, RepositoryNotFoundError +from requests import HTTPError + +from hirundo._hirundo_error import HirundoError +from hirundo.logger import get_logger + +logger = get_logger(__name__) + + +def _build_huggingface_access_message( + model_name: str, + model_role: str, + hint: str, + token_provided: bool, +) -> str: + message_prefix = f"The {model_role} model '{model_name}'" + + if hint == "gated": + if token_provided: + return ( + f"{message_prefix} is gated and the provided HuggingFace token does not " + "have access. Please request access or use a different token." + ) + return f"{message_prefix} is gated. Please provide a HuggingFace token with access." + + if hint == "private": + if token_provided: + return ( + f"{message_prefix} is private and the provided HuggingFace token does not " + "have access. Please use a token with access or a different model." + ) + return f"{message_prefix} is private. Please provide a HuggingFace token with access." + + if hint == "not_found": + if token_provided: + return ( + f"{message_prefix} was not found or is private/gated for the provided " + "token. Please verify the model ID or token access." + ) + return ( + f"{message_prefix} was not found or is private/gated. Please provide a " + "HuggingFace token or verify the model ID." + ) + + if hint == "unauthorized": + if token_provided: + return ( + f"{message_prefix} could not be accessed with the provided HuggingFace " + "token. Please verify token permissions or use a different model." + ) + return ( + f"{message_prefix} could not be accessed without a HuggingFace token. " + "Please provide a token or use a public model." + ) + + return ( + f"{message_prefix} could not be accessed. Please verify the model ID or provide " + "a HuggingFace token with access." + ) + + +def _is_local_model_path(path_or_repo_id: str) -> bool: + potential_path = Path(path_or_repo_id).expanduser() + return potential_path.exists() + + +def validate_huggingface_model_access( + model_name: str, + token: str | None, + model_role: str, +) -> None: + huggingface_api = HfApi(token=token) + token_provided = token is not None + + try: + huggingface_api.model_info(repo_id=model_name) + except GatedRepoError as exception: + raise HirundoError( + _build_huggingface_access_message( + model_name=model_name, + model_role=model_role, + hint="gated", + token_provided=token_provided, + ) + ) from exception + except RepositoryNotFoundError as exception: + raise HirundoError( + _build_huggingface_access_message( + model_name=model_name, + model_role=model_role, + hint="not_found", + token_provided=token_provided, + ) + ) from exception + except HTTPError as exception: + if exception.response is not None and exception.response.status_code in { + 401, + 403, + }: + hint = "unauthorized" + else: + hint = "generic" + logger.debug( + "HuggingFace access validation failed for %s model '%s' with status %s.", + model_role, + model_name, + exception.response.status_code if exception.response is not None else None, + ) + raise HirundoError( + _build_huggingface_access_message( + model_name=model_name, + model_role=model_role, + hint=hint, + token_provided=token_provided, + ) + ) from exception + + +def validate_judge_model_access(path_or_repo_id: str, token: str | None) -> None: + if _is_local_model_path(path_or_repo_id): + return + + validate_huggingface_model_access( + model_name=path_or_repo_id, + token=token, + model_role="judge", + ) diff --git a/hirundo/llm_behavior_eval.py b/hirundo/llm_behavior_eval.py index 561204f2..41cbec87 100644 --- a/hirundo/llm_behavior_eval.py +++ b/hirundo/llm_behavior_eval.py @@ -15,6 +15,10 @@ from hirundo._http import raise_for_status_with_reason, requests from hirundo._iter_sse_retrying import aiter_sse_retrying, iter_sse_retrying from hirundo._llm_sources import HuggingFaceTransformersModelOutput, LlmSourcesOutput +from hirundo._model_access import ( + validate_huggingface_model_access, + validate_judge_model_access, +) from hirundo._run_checking import ( DEFAULT_MAX_RETRIES, STATUS_TO_PROGRESS_MAP, @@ -29,6 +33,7 @@ from hirundo.llm_behavior_eval_results import LlmBehaviorEvalResults from hirundo.llm_bias_type import BBQBiasType, UnqoverBiasType from hirundo.logger import get_logger +from hirundo.unlearning_llm import LlmModel from hirundo.unzip import download_and_extract_llm_behavior_eval_zip logger = get_logger(__name__) @@ -143,6 +148,22 @@ class LlmBehaviorEval: def __init__(self, run_id: str | None = None): self.run_id = run_id + @staticmethod + def _validate_model_access(model_or_run: ModelOrRun, run_info: EvalRunInfo) -> None: + if run_info.judge_model is not None: + validate_judge_model_access( + path_or_repo_id=run_info.judge_model.path_or_repo_id, + token=run_info.judge_model.token, + ) + if model_or_run == ModelOrRun.MODEL and run_info.model_id is not None: + llm_model = LlmModel.get_by_id(run_info.model_id) + if isinstance(llm_model.model_source, HuggingFaceTransformersModelOutput): + validate_huggingface_model_access( + model_name=llm_model.model_source.model_name, + token=None, + model_role="LLM", + ) + @staticmethod def _parse_eval_run_record(response_payload: dict) -> EvalRunRecord: model_payload = response_payload.get("model") @@ -219,6 +240,8 @@ def launch_eval_run( else: model_or_run_value = model_or_run + LlmBehaviorEval._validate_model_access(model_or_run_value, run_info) + response = requests.post( f"{API_HOST}/llm-behavior-eval/run/{model_or_run_value.value}", json=run_info.model_dump(mode="json"), diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 00375dc1..448a6923 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -12,7 +12,12 @@ from hirundo._headers import get_headers from hirundo._http import raise_for_status_with_reason, requests from hirundo._llm_pipeline import get_hf_pipeline_for_run_given_model -from hirundo._llm_sources import LlmSources, LlmSourcesOutput +from hirundo._llm_sources import ( + HuggingFaceTransformersModel, + LlmSources, + LlmSourcesOutput, +) +from hirundo._model_access import validate_huggingface_model_access from hirundo._run_checking import ( STATUS_TO_PROGRESS_MAP, aiter_run_events, @@ -49,6 +54,12 @@ def create( self, replace_if_exists: bool = False, ) -> int: + if isinstance(self.model_source, HuggingFaceTransformersModel): + validate_huggingface_model_access( + model_name=self.model_source.model_name, + token=self.model_source.token, + model_role="LLM", + ) llm_model_response = requests.post( f"{API_HOST}/unlearning-llm/llm/", json={ diff --git a/pyproject.toml b/pyproject.toml index 1541b3d9..ebcd5f96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "stamina>=24.2.0", "httpx-sse>=0.4.0", "tqdm>=4.66.5", + "huggingface-hub>=0.35.3", "h11>=0.16.0", # ⬆️ Required to fix vulnerability GHSA-vqfr-h8mv-ghfj "requests>=2.32.4", @@ -62,6 +63,7 @@ dev = [ "httpx>=0.27.0", "stamina>=24.2.0", "httpx-sse>=0.4.0", + "huggingface-hub>=0.35.3", "pytest>=8.2.0", "pytest-asyncio>=0.23.6", "uv>=0.9.6", diff --git a/tests/test_model_access.py b/tests/test_model_access.py new file mode 100644 index 00000000..b8e0c592 --- /dev/null +++ b/tests/test_model_access.py @@ -0,0 +1,84 @@ +import secrets +from pathlib import Path +from unittest.mock import patch + +import pytest +from hirundo._hirundo_error import HirundoError +from hirundo._model_access import ( + validate_huggingface_model_access, + validate_judge_model_access, +) +from huggingface_hub.errors import GatedRepoError, RepositoryNotFoundError +from requests import HTTPError, Response + + +def _build_http_error(status_code: int) -> HTTPError: + response_object = Response() + response_object.status_code = status_code + return HTTPError(response=response_object) + + +def test_validate_huggingface_model_access_allows_public_model() -> None: + with patch("hirundo._model_access.HfApi.model_info") as mock_model_info: + validate_huggingface_model_access( + model_name="some/model", + token=None, + model_role="LLM", + ) + + mock_model_info.assert_called_once_with(repo_id="some/model") + + +def test_validate_huggingface_model_access_raises_gated_message_without_token() -> None: + with patch( + "hirundo._model_access.HfApi.model_info", + side_effect=GatedRepoError("gated"), + ): + with pytest.raises(HirundoError, match="is gated"): + validate_huggingface_model_access( + model_name="some/model", + token=None, + model_role="judge", + ) + + +def test_validate_huggingface_model_access_raises_not_found_message_with_token() -> ( + None +): + user_access_secret = secrets.token_hex(8) + with patch( + "hirundo._model_access.HfApi.model_info", + side_effect=RepositoryNotFoundError("missing"), + ): + with pytest.raises(HirundoError, match="provided token"): + validate_huggingface_model_access( + model_name="missing/model", + token=user_access_secret, + model_role="LLM", + ) + + +def test_validate_huggingface_model_access_raises_unauthorized_message() -> None: + user_access_secret = secrets.token_hex(8) + with patch( + "hirundo._model_access.HfApi.model_info", + side_effect=_build_http_error(status_code=401), + ): + with pytest.raises(HirundoError, match="provided HuggingFace token"): + validate_huggingface_model_access( + model_name="private/model", + token=user_access_secret, + model_role="judge", + ) + + +def test_validate_judge_model_access_skips_local_path(tmp_path: Path) -> None: + local_model_path = tmp_path / "local_model" + local_model_path.mkdir() + + with patch( + "hirundo._model_access.validate_huggingface_model_access" + ) as mock_validate: + validate_judge_model_access(str(local_model_path), token=None) + + mock_validate.assert_not_called() diff --git a/uv.lock b/uv.lock index f0073fdf..b40295f3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13'", @@ -553,6 +553,7 @@ dependencies = [ { name = "h11" }, { name = "httpx" }, { name = "httpx-sse" }, + { name = "huggingface-hub" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "pyyaml" }, @@ -576,6 +577,7 @@ dev = [ { name = "filelock" }, { name = "httpx" }, { name = "httpx-sse" }, + { name = "huggingface-hub" }, { name = "jinja2" }, { name = "marshmallow" }, { name = "platformdirs" }, @@ -641,6 +643,8 @@ requires-dist = [ { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" }, { name = "httpx-sse", specifier = ">=0.4.0" }, { name = "httpx-sse", marker = "extra == 'dev'", specifier = ">=0.4.0" }, + { name = "huggingface-hub", specifier = ">=0.35.3" }, + { name = "huggingface-hub", marker = "extra == 'dev'", specifier = ">=0.35.3" }, { name = "jinja2", marker = "extra == 'dev'", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'docs'", specifier = ">=3.1.6" }, { name = "markupsafe", marker = "extra == 'docs'", specifier = ">=3.0.2" }, From 4d84127e942d526e88073899113ea18d11ba8b8a Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 12 Feb 2026 17:15:48 +0200 Subject: [PATCH 02/11] Bump `huggingface-hub` version to minimum 1.0.0 (for compatibility issues) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19b4ff06..b9c3a80a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "stamina>=24.2.0", "httpx-sse>=0.4.0", "tqdm>=4.66.5", - "huggingface-hub>=0.35.3", + "huggingface-hub>=1.0.0", "h11>=0.16.0", # ⬆️ Required to fix vulnerability GHSA-vqfr-h8mv-ghfj "requests>=2.32.4", diff --git a/uv.lock b/uv.lock index a0996d36..a5ecf025 100644 --- a/uv.lock +++ b/uv.lock @@ -735,7 +735,7 @@ requires-dist = [ { name = "h11", specifier = ">=0.16.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "httpx-sse", specifier = ">=0.4.0" }, - { name = "huggingface-hub", specifier = ">=0.35.3" }, + { name = "huggingface-hub", specifier = ">=1.0.0" }, { name = "pandas", marker = "extra == 'pandas'", specifier = ">=2.2.3" }, { name = "peft", marker = "extra == 'transformers'", specifier = ">=0.18.1" }, { name = "polars", marker = "extra == 'polars'", specifier = ">=1.0.0" }, From 5bb021147d48ae11a7ff595b53149ffb7a9232cc Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Sun, 15 Feb 2026 11:23:08 +0200 Subject: [PATCH 03/11] Fix agent PR comments Hardcoded token=None ignores model's stored token & Unreachable hint="private" message branch is dead code --- hirundo/_llm_sources.py | 1 + hirundo/_model_access.py | 8 ----- hirundo/llm_behavior_eval.py | 2 +- tests/test_llm_behavior_eval_model_access.py | 37 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 tests/test_llm_behavior_eval_model_access.py diff --git a/hirundo/_llm_sources.py b/hirundo/_llm_sources.py index 03e1a112..d51bde5e 100644 --- a/hirundo/_llm_sources.py +++ b/hirundo/_llm_sources.py @@ -28,6 +28,7 @@ class HuggingFaceTransformersModelOutput(BaseModel): ModelSourceType.HUGGINGFACE_TRANSFORMERS ) model_name: str + token: str | None = None class LocalTransformersModel(BaseModel): diff --git a/hirundo/_model_access.py b/hirundo/_model_access.py index a8361d9a..5e46c568 100644 --- a/hirundo/_model_access.py +++ b/hirundo/_model_access.py @@ -28,14 +28,6 @@ def _build_huggingface_access_message( ) return f"{message_prefix} is gated. Please provide a HuggingFace token with access." - if hint == "private": - if token_provided: - return ( - f"{message_prefix} is private and the provided HuggingFace token does not " - "have access. Please use a token with access or a different model." - ) - return f"{message_prefix} is private. Please provide a HuggingFace token with access." - if hint == "not_found": if token_provided: return ( diff --git a/hirundo/llm_behavior_eval.py b/hirundo/llm_behavior_eval.py index 41cbec87..dbf24165 100644 --- a/hirundo/llm_behavior_eval.py +++ b/hirundo/llm_behavior_eval.py @@ -160,7 +160,7 @@ def _validate_model_access(model_or_run: ModelOrRun, run_info: EvalRunInfo) -> N if isinstance(llm_model.model_source, HuggingFaceTransformersModelOutput): validate_huggingface_model_access( model_name=llm_model.model_source.model_name, - token=None, + token=llm_model.model_source.token, model_role="LLM", ) diff --git a/tests/test_llm_behavior_eval_model_access.py b/tests/test_llm_behavior_eval_model_access.py new file mode 100644 index 00000000..9947b97c --- /dev/null +++ b/tests/test_llm_behavior_eval_model_access.py @@ -0,0 +1,37 @@ +import secrets +from unittest.mock import patch + +from hirundo._llm_sources import HuggingFaceTransformersModelOutput +from hirundo.llm_behavior_eval import EvalRunInfo, LlmBehaviorEval, ModelOrRun + + +def test_validate_model_access_forwards_huggingface_token() -> None: + run_info = EvalRunInfo(model_id=123) + user_access_secret = secrets.token_hex(8) + llm_model_output = type( + "LlmModelStub", + (), + { + "model_source": HuggingFaceTransformersModelOutput( + model_name="org/private-model", + token=user_access_secret, + ) + }, + )() + + with ( + patch( + "hirundo.llm_behavior_eval.LlmModel.get_by_id", + return_value=llm_model_output, + ), + patch( + "hirundo.llm_behavior_eval.validate_huggingface_model_access" + ) as mock_validate_huggingface_model_access, + ): + LlmBehaviorEval._validate_model_access(ModelOrRun.MODEL, run_info) + + mock_validate_huggingface_model_access.assert_called_once_with( + model_name="org/private-model", + token=user_access_secret, + model_role="LLM", + ) From d236474f8f5c416f1d4f88ba04608e779f5272c5 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Sun, 15 Feb 2026 11:23:37 +0200 Subject: [PATCH 04/11] Fix Pydantic model configuration --- hirundo/llm_behavior_eval.py | 8 +++++++- hirundo/llm_behavior_eval_results.py | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/hirundo/llm_behavior_eval.py b/hirundo/llm_behavior_eval.py index dbf24165..c2fa4ee6 100644 --- a/hirundo/llm_behavior_eval.py +++ b/hirundo/llm_behavior_eval.py @@ -69,6 +69,8 @@ class JudgeModel(BaseModel): class EvalRunInfo(BaseModel): + model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump")) + organization_id: int | None = None name: str | None = None model_id: int | None = None @@ -80,7 +82,9 @@ class EvalRunInfo(BaseModel): class OutputLlm(BaseModel): - model_config = {"extra": "allow"} + model_config = ConfigDict( + extra="allow", protected_namespaces=("model_validate", "model_dump") + ) id: int organization_id: int @@ -121,6 +125,8 @@ class LlmEvalMetrics(BaseModel): class EvalRunRecord(BaseModel): + model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump")) + id: int name: str model_id: int | None diff --git a/hirundo/llm_behavior_eval_results.py b/hirundo/llm_behavior_eval_results.py index f13e9e0b..65f2e0f6 100644 --- a/hirundo/llm_behavior_eval_results.py +++ b/hirundo/llm_behavior_eval_results.py @@ -1,13 +1,16 @@ import typing from pathlib import Path -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict T = typing.TypeVar("T") class LlmBehaviorEvalResults(BaseModel, typing.Generic[T]): - model_config = {"arbitrary_types_allowed": True} + model_config = ConfigDict( + arbitrary_types_allowed=True, + protected_namespaces=("model_validate", "model_dump"), + ) cached_zip_path: Path """ From dc4df91374eb85247265da4069923552065d9dda Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 12 Mar 2026 11:34:58 +0200 Subject: [PATCH 05/11] Update `authlib` to `1.6.7` to fix vulnerability Thank you Dependabot --- pyproject.toml | 4 +-- uv.lock | 79 ++++++++++++++++++++++++++------------------------ 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9c3a80a..ff0cec33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,8 +67,8 @@ dev = [ "basedpyright==1.37.1", "virtualenv>=20.36.1", # ⬆️ Needed for `pre-commit` version fix for vulnerability GHSA-rqc4-2hc7-8c8v - "authlib>=1.6.6", - # ⬆️ Required to fix vulnerability CVE-2025-68158 + "authlib>=1.6.7", + # ⬆️ Required to fix vulnerability CVE-2026-28802 "ruff>=0.12.0", "bumpver>=2025.1131", "platformdirs>=4.3.6", diff --git a/uv.lock b/uv.lock index a5ecf025..5dfab3a6 100644 --- a/uv.lock +++ b/uv.lock @@ -83,14 +83,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.6" +version = "1.6.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, ] [[package]] @@ -624,31 +624,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.2.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, - { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, - { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, - { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/68/01/928fd82663fb0ab455551a178303a2960e65029da66b21974594f3a20a94/hf_xet-1.4.0.tar.gz", hash = "sha256:48e6ba7422b0885c9bbd8ac8fdf5c4e1306c3499b82d489944609cc4eae8ecbd", size = 660350, upload-time = "2026-03-11T18:50:03.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4b/2351e30dddc6f3b47b3da0a0693ec1e82f8303b1a712faa299cf3552002b/hf_xet-1.4.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:76725fcbc5f59b23ac778f097d3029d6623e3cf6f4057d99d1fce1a7e3cff8fc", size = 3796397, upload-time = "2026-03-11T18:49:47.382Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/3db90ec0afb4e26e3330b1346b89fe0e9a3b7bfc2d6a2b2262787790d25f/hf_xet-1.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:76f1f73bee81a6e6f608b583908aa24c50004965358ac92c1dc01080a21bcd09", size = 3556235, upload-time = "2026-03-11T18:49:45.785Z" }, + { url = "https://files.pythonhosted.org/packages/57/6e/2a662af2cbc6c0a64ebe9fcdb8faf05b5205753d45a75a3011bb2209d0b4/hf_xet-1.4.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1818c2e5d6f15354c595d5111c6eb0e5a30a6c5c1a43eeaec20f19607cff0b34", size = 4213145, upload-time = "2026-03-11T18:49:38.009Z" }, + { url = "https://files.pythonhosted.org/packages/b9/4a/47c129affb540767e0e3e101039a95f4a73a292ec689c26e8f0c5b633f9d/hf_xet-1.4.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:70764d295f485db9cc9a6af76634ea00ec4f96311be7485f8f2b6144739b4ccf", size = 3991951, upload-time = "2026-03-11T18:49:36.396Z" }, + { url = "https://files.pythonhosted.org/packages/76/81/ec516cfc6281cfeef027b0919166b2fe11ab61fbe6131a2c43fafbed8b68/hf_xet-1.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d3bd2a1e289f772c715ca88cdca8ceb3d8b5c9186534d5925410e531d849a3e", size = 4193205, upload-time = "2026-03-11T18:49:54.415Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/0945b5e542ed6c6ce758b589b27895a449deab630dfcdee5a6ee0f699d21/hf_xet-1.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:06da3797f1fdd9a8f8dbc8c1bddfa0b914789b14580c375d29c32ee35c2c66ca", size = 4431022, upload-time = "2026-03-11T18:49:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ad/a4859c55ab4b67a4fde2849be8bde81917f54062050419b821071f199a9c/hf_xet-1.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:30b9d8f384ccec848124d51d883e91f3c88d430589e02a7b6d867730ab8d53ac", size = 3674977, upload-time = "2026-03-11T18:50:06.369Z" }, + { url = "https://files.pythonhosted.org/packages/4b/17/5bf3791e3a53e597913c2a775a48a98aaded9c2ddb5d1afaedabb55e2ed8/hf_xet-1.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:07ffdbf7568fa3245b24d949f0f3790b5276fb7293a5554ac4ec02e5f7e2b38d", size = 3536778, upload-time = "2026-03-11T18:50:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a1/05a7f9d6069bf78405d3fc2464b6c76b167128501e13b4f1d6266e1d1f54/hf_xet-1.4.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e2731044f3a18442f9f7a3dcf03b96af13dee311f03846a1df1f0553a3ea0fc6", size = 3796727, upload-time = "2026-03-11T18:49:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8a/67abc642c2b32efcb7a257cdad8555c2904e23f18a1b4fec3aef1ebfe0fc/hf_xet-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b6f3729335fbc4baef60fe14fe32ef13ac9d377bdc898148c541e20c6056b504", size = 3555869, upload-time = "2026-03-11T18:49:51.313Z" }, + { url = "https://files.pythonhosted.org/packages/19/3d/4765367c64ee70db15fa771d5b94bf12540b85076a1d3210ebbfec42d477/hf_xet-1.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c0c9f052738a024073d332c573275c8e33697a3ef3f5dd2fb4ef98216e1e74a", size = 4212980, upload-time = "2026-03-11T18:49:44.21Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bf/6ad99ee0e7ca2318f912a87318e493d82d8f9aace6be81f774bd14b996df/hf_xet-1.4.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f44b2324be75bfa399735996ac299fd478684c48ce47d12a42b5f24b1a99ccb8", size = 3991136, upload-time = "2026-03-11T18:49:42.512Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/932e25c69699076088f57e3c14f83ccae87bac25e755994f3362acc908d5/hf_xet-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:01de78b1ceddf8b38da001f7cc728b3bc3eb956948b18e8a1997ad6fc80fbe9d", size = 4192676, upload-time = "2026-03-11T18:50:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0a/5e41339a294fd3450948989a47ecba9824d5bc1950cf767f928ecaf53a55/hf_xet-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cac8616e7a974105c3494735313f5ab0fb79b5accadec1a7a992859a15536a9", size = 4430729, upload-time = "2026-03-11T18:50:01.923Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c1/c3d8ed9b7118e9166b0cf71dfd501da82f1abe306387e34e0f3ee59553ec/hf_xet-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3a5d9cb25095ceb3beab4843ae2d1b3e5746371ddbf2e5849f7be6a7d6f44df4", size = 3674989, upload-time = "2026-03-11T18:50:12.633Z" }, + { url = "https://files.pythonhosted.org/packages/65/bc/ea26cf774063cb09d7aaaa6cba9d341fb72b42ea99b8a94ca254dbafbbb0/hf_xet-1.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9b777674499dc037317db372c90a2dd91329b5f1ee93c645bb89155bb974f5bf", size = 3536805, upload-time = "2026-03-11T18:50:11.082Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f9/a0b01945726aea81d2f213457cd5f5102a51e6fd1ca9f9769f561fb57501/hf_xet-1.4.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:981d2b5222c3baadf9567c135cf1d1073786f546b7745686978d46b5df179e16", size = 3799223, upload-time = "2026-03-11T18:49:49.884Z" }, + { url = "https://files.pythonhosted.org/packages/5d/30/ee62b0c00412f49a7e6f509f0104ee8808692278d247234336df48029349/hf_xet-1.4.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:cc8bd050349d0d7995ce7b3a3a18732a2a8062ce118a82431602088abb373428", size = 3560682, upload-time = "2026-03-11T18:49:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/93/d0/0fe5c44dbced465a651a03212e1135d0d7f95d19faada692920cb56f8e38/hf_xet-1.4.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5d0c38d2a280d814280b8c15eead4a43c9781e7bf6fc37843cffab06dcdc76b9", size = 4218323, upload-time = "2026-03-11T18:49:40.921Z" }, + { url = "https://files.pythonhosted.org/packages/73/df/7b3c99a4e50442039eae498e5c23db634538eb3e02214109880cf1165d4c/hf_xet-1.4.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a883f0250682ea888a1bd0af0631feda377e59ad7aae6fb75860ecee7ae0f93", size = 3997156, upload-time = "2026-03-11T18:49:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/a9/26/47dfedf271c21d95346660ae1698e7ece5ab10791fa6c4f20c59f3713083/hf_xet-1.4.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:99e1d9255fe8ecdf57149bb0543d49e7b7bd8d491ddf431eb57e114253274df5", size = 4199052, upload-time = "2026-03-11T18:49:57.097Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c0/346b9aad1474e881e65f998d5c1981695f0af045bc7a99204d9d86759a89/hf_xet-1.4.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b25f06ce42bd2d5f2e79d4a2d72f783d3ac91827c80d34a38cf8e5290dd717b0", size = 4434346, upload-time = "2026-03-11T18:49:58.67Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d6/88ce9d6caa397c3b935263d5bcbe3ebf6c443f7c76098b8c523d206116b9/hf_xet-1.4.0-cp37-abi3-win_amd64.whl", hash = "sha256:8d6d7816d01e0fa33f315c8ca21b05eca0ce4cdc314f13b81d953e46cc6db11d", size = 3678921, upload-time = "2026-03-11T18:50:09.496Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/17d99ed253b28a9550ca479867c66a8af4c9bcd8cdc9a26b0c8007c2000a/hf_xet-1.4.0-cp37-abi3-win_arm64.whl", hash = "sha256:cb8d9549122b5b42f34b23b14c6b662a88a586a919d418c774d8dbbc4b3ce2aa", size = 3541054, upload-time = "2026-03-11T18:50:07.963Z" }, ] [[package]] @@ -760,7 +763,7 @@ deploy = [ { name = "twine", specifier = ">=5.0.0" }, ] dev = [ - { name = "authlib", specifier = ">=1.6.6" }, + { name = "authlib", specifier = ">=1.6.7" }, { name = "basedpyright", specifier = "==1.37.1" }, { name = "bumpver", specifier = ">=2025.1131" }, { name = "cryptography", specifier = ">=46.0.5" }, @@ -833,21 +836,22 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.36.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "requests" }, { name = "tqdm" }, + { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7a/304cec37112382c4fe29a43bcb0d5891f922785d18745883d2aa4eb74e4b/huggingface_hub-1.6.0.tar.gz", hash = "sha256:d931ddad8ba8dfc1e816bf254810eb6f38e5c32f60d4184b5885662a3b167325", size = 717071, upload-time = "2026-03-06T14:19:18.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/92/e3/e3a44f54c8e2f28983fcf07f13d4260b37bd6a0d3a081041bc60b91d230e/huggingface_hub-1.6.0-py3-none-any.whl", hash = "sha256:ef40e2d5cb85e48b2c067020fa5142168342d5108a1b267478ed384ecbf18961", size = 612874, upload-time = "2026-03-06T14:19:16.844Z" }, ] [[package]] @@ -2713,24 +2717,23 @@ wheels = [ [[package]] name = "transformers" -version = "4.57.6" +version = "5.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, { name = "huggingface-hub" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, - { name = "requests" }, { name = "safetensors" }, { name = "tokenizers" }, { name = "tqdm" }, + { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, + { url = "https://files.pythonhosted.org/packages/b8/88/ae8320064e32679a5429a2c9ebbc05c2bf32cefb6e076f9b07f6d685a9b4/transformers-5.3.0-py3-none-any.whl", hash = "sha256:50ac8c89c3c7033444fb3f9f53138096b997ebb70d4b5e50a2e810bf12d3d29a", size = 10661827, upload-time = "2026-03-04T17:41:42.722Z" }, ] [[package]] From 1e5c8cab6ce32607122fadcf656e9aad81768d51 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 12 Mar 2026 12:43:15 +0200 Subject: [PATCH 06/11] Add docstring to new validation functions --- hirundo/_model_access.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hirundo/_model_access.py b/hirundo/_model_access.py index 5e46c568..dca91dc6 100644 --- a/hirundo/_model_access.py +++ b/hirundo/_model_access.py @@ -66,6 +66,13 @@ def validate_huggingface_model_access( token: str | None, model_role: str, ) -> None: + """Validate that a Hugging Face model can be accessed. + + Args: + model_name: Hugging Face repository ID for the model to validate. + token: Optional Hugging Face access token used for authenticated access. + model_role: Human-readable role for the model in error messages. + """ huggingface_api = HfApi(token=token) token_provided = token is not None @@ -114,6 +121,14 @@ def validate_huggingface_model_access( def validate_judge_model_access(path_or_repo_id: str, token: str | None) -> None: + """Validate that a judge model can be accessed. + + Args: + path_or_repo_id: Local filesystem path or Hugging Face repository ID for the + judge model. + token: Optional Hugging Face access token used when the judge model is hosted + on Hugging Face. + """ if _is_local_model_path(path_or_repo_id): return From 9c6bc5959cf0456282d824e3c0f0f0fdbe1ee081 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 12 Mar 2026 12:51:07 +0200 Subject: [PATCH 07/11] Change `test_llm_behavior_eval_model_access.py` and `test_model_access.py` to use `pytest` instead of `unittest` --- AGENTS.md | 2 + tests/test_llm_behavior_eval_model_access.py | 52 ++++-- tests/test_model_access.py | 161 +++++++++++++------ 3 files changed, 146 insertions(+), 69 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7382123b..b8133f66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,8 @@ Activate the local virtualenv before running any Python/uv commands: `source .ve - Frameworks: `pytest` and `pytest-asyncio`. - Place tests in `tests/`; name files `test_*.py`. +- Write tests in `pytest` style; do not add new `unittest`-based tests or `unittest` + assertions/fixtures. - Run locally with `pytest` before opening a PR (CI runs lint + integration tests). ## Pull Request Guidelines diff --git a/tests/test_llm_behavior_eval_model_access.py b/tests/test_llm_behavior_eval_model_access.py index 9947b97c..f5218e6d 100644 --- a/tests/test_llm_behavior_eval_model_access.py +++ b/tests/test_llm_behavior_eval_model_access.py @@ -1,11 +1,14 @@ import secrets -from unittest.mock import patch +import hirundo.llm_behavior_eval as llm_behavior_eval_module +import pytest from hirundo._llm_sources import HuggingFaceTransformersModelOutput from hirundo.llm_behavior_eval import EvalRunInfo, LlmBehaviorEval, ModelOrRun -def test_validate_model_access_forwards_huggingface_token() -> None: +def test_validate_model_access_forwards_huggingface_token( + monkeypatch: pytest.MonkeyPatch, +) -> None: run_info = EvalRunInfo(model_id=123) user_access_secret = secrets.token_hex(8) llm_model_output = type( @@ -18,20 +21,35 @@ def test_validate_model_access_forwards_huggingface_token() -> None: ) }, )() + captured_call: dict[str, str | None] = {} - with ( - patch( - "hirundo.llm_behavior_eval.LlmModel.get_by_id", - return_value=llm_model_output, - ), - patch( - "hirundo.llm_behavior_eval.validate_huggingface_model_access" - ) as mock_validate_huggingface_model_access, - ): - LlmBehaviorEval._validate_model_access(ModelOrRun.MODEL, run_info) - - mock_validate_huggingface_model_access.assert_called_once_with( - model_name="org/private-model", - token=user_access_secret, - model_role="LLM", + monkeypatch.setattr( + llm_behavior_eval_module.LlmModel, + "get_by_id", + lambda model_id: llm_model_output, ) + + def fake_validate_huggingface_model_access( + model_name: str, token: str | None, model_role: str + ) -> None: + captured_call.update( + { + "model_name": model_name, + "token": token, + "model_role": model_role, + } + ) + + monkeypatch.setattr( + llm_behavior_eval_module, + "validate_huggingface_model_access", + fake_validate_huggingface_model_access, + ) + + LlmBehaviorEval._validate_model_access(ModelOrRun.MODEL, run_info) + + assert captured_call == { + "model_name": "org/private-model", + "token": user_access_secret, + "model_role": "LLM", + } diff --git a/tests/test_model_access.py b/tests/test_model_access.py index b8e0c592..08b05e4b 100644 --- a/tests/test_model_access.py +++ b/tests/test_model_access.py @@ -1,14 +1,20 @@ import secrets from pathlib import Path -from unittest.mock import patch +from typing import Any, TypeVar, cast +import hirundo._model_access as model_access_module +import httpx import pytest from hirundo._hirundo_error import HirundoError from hirundo._model_access import ( validate_huggingface_model_access, validate_judge_model_access, ) -from huggingface_hub.errors import GatedRepoError, RepositoryNotFoundError +from huggingface_hub.errors import ( + GatedRepoError, + HfHubHTTPError, + RepositoryNotFoundError, +) from requests import HTTPError, Response @@ -18,67 +24,118 @@ def _build_http_error(status_code: int) -> HTTPError: return HTTPError(response=response_object) -def test_validate_huggingface_model_access_allows_public_model() -> None: - with patch("hirundo._model_access.HfApi.model_info") as mock_model_info: +def _build_huggingface_error_response(status_code: int) -> httpx.Response: + return httpx.Response( + status_code=status_code, + request=httpx.Request("GET", "https://huggingface.co/api/models/test"), + ) + + +ExceptionType = TypeVar("ExceptionType", bound=HfHubHTTPError) + + +def _build_huggingface_hub_error( + exception_type: type[ExceptionType], + message: str, + status_code: int, +) -> ExceptionType: + return cast(Any, exception_type)( + message, + response=_build_huggingface_error_response(status_code=status_code), + ) + + +def test_validate_huggingface_model_access_allows_public_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured_repo_ids: list[str] = [] + + def fake_model_info(_self: object, *, repo_id: str) -> None: + captured_repo_ids.append(repo_id) + + monkeypatch.setattr(model_access_module.HfApi, "model_info", fake_model_info) + + validate_huggingface_model_access( + model_name="some/model", + token=None, + model_role="LLM", + ) + + assert captured_repo_ids == ["some/model"] + + +def test_validate_huggingface_model_access_raises_gated_message_without_token( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def fake_model_info(_self: object, *, repo_id: str) -> None: + raise _build_huggingface_hub_error(GatedRepoError, "gated", status_code=403) + + monkeypatch.setattr(model_access_module.HfApi, "model_info", fake_model_info) + + with pytest.raises(HirundoError, match="is gated"): validate_huggingface_model_access( model_name="some/model", token=None, - model_role="LLM", + model_role="judge", ) - mock_model_info.assert_called_once_with(repo_id="some/model") +def test_validate_huggingface_model_access_raises_not_found_message_with_token( + monkeypatch: pytest.MonkeyPatch, +) -> None: + user_access_secret = secrets.token_hex(8) -def test_validate_huggingface_model_access_raises_gated_message_without_token() -> None: - with patch( - "hirundo._model_access.HfApi.model_info", - side_effect=GatedRepoError("gated"), - ): - with pytest.raises(HirundoError, match="is gated"): - validate_huggingface_model_access( - model_name="some/model", - token=None, - model_role="judge", - ) + def fake_model_info(_self: object, *, repo_id: str) -> None: + raise _build_huggingface_hub_error( + RepositoryNotFoundError, "missing", status_code=404 + ) + monkeypatch.setattr(model_access_module.HfApi, "model_info", fake_model_info) -def test_validate_huggingface_model_access_raises_not_found_message_with_token() -> ( - None -): - user_access_secret = secrets.token_hex(8) - with patch( - "hirundo._model_access.HfApi.model_info", - side_effect=RepositoryNotFoundError("missing"), - ): - with pytest.raises(HirundoError, match="provided token"): - validate_huggingface_model_access( - model_name="missing/model", - token=user_access_secret, - model_role="LLM", - ) - - -def test_validate_huggingface_model_access_raises_unauthorized_message() -> None: + with pytest.raises(HirundoError, match="provided token"): + validate_huggingface_model_access( + model_name="missing/model", + token=user_access_secret, + model_role="LLM", + ) + + +def test_validate_huggingface_model_access_raises_unauthorized_message( + monkeypatch: pytest.MonkeyPatch, +) -> None: user_access_secret = secrets.token_hex(8) - with patch( - "hirundo._model_access.HfApi.model_info", - side_effect=_build_http_error(status_code=401), - ): - with pytest.raises(HirundoError, match="provided HuggingFace token"): - validate_huggingface_model_access( - model_name="private/model", - token=user_access_secret, - model_role="judge", - ) - - -def test_validate_judge_model_access_skips_local_path(tmp_path: Path) -> None: + + def fake_model_info(_self: object, *, repo_id: str) -> None: + raise _build_http_error(status_code=401) + + monkeypatch.setattr(model_access_module.HfApi, "model_info", fake_model_info) + + with pytest.raises(HirundoError, match="provided HuggingFace token"): + validate_huggingface_model_access( + model_name="private/model", + token=user_access_secret, + model_role="judge", + ) + + +def test_validate_judge_model_access_skips_local_path( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: local_model_path = tmp_path / "local_model" local_model_path.mkdir() + validate_calls: list[tuple[str, str | None, str]] = [] + + def fake_validate_huggingface_model_access( + model_name: str, token: str | None, model_role: str + ) -> None: + validate_calls.append((model_name, token, model_role)) + + monkeypatch.setattr( + model_access_module, + "validate_huggingface_model_access", + fake_validate_huggingface_model_access, + ) - with patch( - "hirundo._model_access.validate_huggingface_model_access" - ) as mock_validate: - validate_judge_model_access(str(local_model_path), token=None) + validate_judge_model_access(str(local_model_path), token=None) - mock_validate.assert_not_called() + assert validate_calls == [] From ce40b0a1ab344e4fbb0fb70f1c3a2fcf178cb838 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 12 Mar 2026 12:54:30 +0200 Subject: [PATCH 08/11] Fix Ruff error --- tests/test_model_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_model_access.py b/tests/test_model_access.py index 08b05e4b..0a759923 100644 --- a/tests/test_model_access.py +++ b/tests/test_model_access.py @@ -39,7 +39,7 @@ def _build_huggingface_hub_error( message: str, status_code: int, ) -> ExceptionType: - return cast(Any, exception_type)( + return cast("Any", exception_type)( message, response=_build_huggingface_error_response(status_code=status_code), ) From b09582d4b81304b4dbe8df0dd37962fcc1bb9555 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Fri, 13 Mar 2026 18:39:56 +0200 Subject: [PATCH 09/11] Fix Codex's PR comment about model error --- hirundo/_model_access.py | 24 +++++++++++++++++------- tests/test_model_access.py | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/hirundo/_model_access.py b/hirundo/_model_access.py index dca91dc6..fb913c0e 100644 --- a/hirundo/_model_access.py +++ b/hirundo/_model_access.py @@ -3,7 +3,11 @@ from pathlib import Path from huggingface_hub import HfApi -from huggingface_hub.errors import GatedRepoError, RepositoryNotFoundError +from huggingface_hub.errors import ( + GatedRepoError, + HfHubHTTPError, + RepositoryNotFoundError, +) from requests import HTTPError from hirundo._hirundo_error import HirundoError @@ -61,6 +65,13 @@ def _is_local_model_path(path_or_repo_id: str) -> bool: return potential_path.exists() +def _get_huggingface_error_status_code( + exception: HfHubHTTPError | HTTPError, +) -> int | None: + response = exception.response + return response.status_code if response is not None else None + + def validate_huggingface_model_access( model_name: str, token: str | None, @@ -96,11 +107,10 @@ def validate_huggingface_model_access( token_provided=token_provided, ) ) from exception - except HTTPError as exception: - if exception.response is not None and exception.response.status_code in { - 401, - 403, - }: + except (HfHubHTTPError, HTTPError) as exception: + status_code = _get_huggingface_error_status_code(exception) + + if status_code in {401, 403}: hint = "unauthorized" else: hint = "generic" @@ -108,7 +118,7 @@ def validate_huggingface_model_access( "HuggingFace access validation failed for %s model '%s' with status %s.", model_role, model_name, - exception.response.status_code if exception.response is not None else None, + status_code, ) raise HirundoError( _build_huggingface_access_message( diff --git a/tests/test_model_access.py b/tests/test_model_access.py index 0a759923..b8cf1d84 100644 --- a/tests/test_model_access.py +++ b/tests/test_model_access.py @@ -105,6 +105,26 @@ def test_validate_huggingface_model_access_raises_unauthorized_message( ) -> None: user_access_secret = secrets.token_hex(8) + def fake_model_info(_self: object, *, repo_id: str) -> None: + raise _build_huggingface_hub_error( + HfHubHTTPError, "unauthorized", status_code=401 + ) + + monkeypatch.setattr(model_access_module.HfApi, "model_info", fake_model_info) + + with pytest.raises(HirundoError, match="provided HuggingFace token"): + validate_huggingface_model_access( + model_name="private/model", + token=user_access_secret, + model_role="judge", + ) + + +def test_validate_huggingface_model_access_keeps_legacy_http_error_support( + monkeypatch: pytest.MonkeyPatch, +) -> None: + user_access_secret = secrets.token_hex(8) + def fake_model_info(_self: object, *, repo_id: str) -> None: raise _build_http_error(status_code=401) From 8287c9e8f77878ede7c3a7c5fc68bc0e855c6d11 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Mon, 30 Mar 2026 01:20:35 +0300 Subject: [PATCH 10/11] Fix Baz comment about validation when setting model to run LLM behavior eval with a model ID --- hirundo/llm_behavior_eval.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hirundo/llm_behavior_eval.py b/hirundo/llm_behavior_eval.py index c2fa4ee6..c020b382 100644 --- a/hirundo/llm_behavior_eval.py +++ b/hirundo/llm_behavior_eval.py @@ -161,6 +161,11 @@ def _validate_model_access(model_or_run: ModelOrRun, run_info: EvalRunInfo) -> N path_or_repo_id=run_info.judge_model.path_or_repo_id, token=run_info.judge_model.token, ) + if model_or_run == ModelOrRun.MODEL and run_info.model_id is None: + raise HirundoLlmBehaviorEvalError( + "model_id is required when model_or_run is 'model'" + ) + if model_or_run == ModelOrRun.MODEL and run_info.model_id is not None: llm_model = LlmModel.get_by_id(run_info.model_id) if isinstance(llm_model.model_source, HuggingFaceTransformersModelOutput): From 8616eab33961b72b2d04e57110acb1480e390656 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Mon, 30 Mar 2026 11:59:56 +0300 Subject: [PATCH 11/11] Refactor to replace boolean environment variable checks with a function & allow skipping HuggingFace validation --- hirundo/_env.py | 13 +++ hirundo/unlearning_llm.py | 12 +- tests/dataset_qa_shared.py | 8 +- .../llm_behavior_eval_test.py | 4 +- tests/test_unlearning_llm_model_create.py | 106 ++++++++++++++++++ .../unlearn_llm_behavior_test.py | 4 +- 6 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 tests/test_unlearning_llm_model_create.py diff --git a/hirundo/_env.py b/hirundo/_env.py index 6ff99f4e..07d613c5 100644 --- a/hirundo/_env.py +++ b/hirundo/_env.py @@ -19,6 +19,19 @@ class EnvLocation(enum.Enum): API_KEY = os.getenv("API_KEY") +def get_env_bool(variable_name: str, default: bool = False) -> bool: + variable_value = os.getenv(variable_name) + if variable_value is None: + return default + + normalized_value = variable_value.strip().lower() + if normalized_value in {"1", "true", "yes", "on"}: + return True + if normalized_value in {"0", "false", "no", "off"}: + return False + return default + + def check_api_key(): if not API_KEY: raise ValueError( diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 448a6923..060e4ecf 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -8,7 +8,7 @@ from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm -from hirundo._env import API_HOST +from hirundo._env import API_HOST, get_env_bool from hirundo._headers import get_headers from hirundo._http import raise_for_status_with_reason, requests from hirundo._llm_pipeline import get_hf_pipeline_for_run_given_model @@ -53,8 +53,16 @@ class LlmModel(BaseModel): def create( self, replace_if_exists: bool = False, + validate_hf_access: bool = True, ) -> int: - if isinstance(self.model_source, HuggingFaceTransformersModel): + should_validate_hf_access = validate_hf_access or get_env_bool( + "HIRUNDO_VALIDATE_HF_ACCESS", + default=True, + ) + + if should_validate_hf_access and isinstance( + self.model_source, HuggingFaceTransformersModel + ): validate_huggingface_model_access( model_name=self.model_source.model_name, token=self.model_source.token, diff --git a/tests/dataset_qa_shared.py b/tests/dataset_qa_shared.py index 014b3957..6c9c0ba5 100644 --- a/tests/dataset_qa_shared.py +++ b/tests/dataset_qa_shared.py @@ -1,4 +1,3 @@ -import os from collections import defaultdict from contextlib import contextmanager @@ -9,6 +8,7 @@ RunArgs, StorageConfig, ) +from hirundo._env import get_env_bool from hirundo._run_status import RunStatus from hirundo.logger import get_logger @@ -168,8 +168,8 @@ def dataset_qa_sync_test( run_args: RunArgs | None = None, ): logger.info("Sync: Finished cleanup") - if (os.getenv("FULL_TEST", "false") == "true" and sanity) or ( - alternative_env and os.getenv(alternative_env, "false") == "true" + if (get_env_bool("FULL_TEST") and sanity) or ( + alternative_env and get_env_bool(alternative_env) ): run_id = test_dataset.run_qa(replace_dataset_if_exists=True, run_args=run_args) logger.info("Sync: Started dataset QA run with run ID %s", run_id) @@ -189,7 +189,7 @@ async def dataset_qa_async_test( run_args: RunArgs | None = None, ): logger.info("Async: Finished cleanup") - if os.getenv(env, "false") == "true": + if get_env_bool(env): run_id = test_dataset.run_qa(replace_dataset_if_exists=True, run_args=run_args) logger.info("Async: Started dataset QA run with run ID %s", run_id) events_generator = test_dataset.acheck_run() diff --git a/tests/llm-behavior-eval/llm_behavior_eval_test.py b/tests/llm-behavior-eval/llm_behavior_eval_test.py index 090d1a82..c0668afd 100644 --- a/tests/llm-behavior-eval/llm_behavior_eval_test.py +++ b/tests/llm-behavior-eval/llm_behavior_eval_test.py @@ -1,5 +1,4 @@ import logging -import os from hirundo import ( BBQBiasType, @@ -10,6 +9,7 @@ ModelOrRun, PresetType, ) +from hirundo._env import get_env_bool from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) @@ -32,7 +32,7 @@ def test_llm_behavior_eval(): bias_type=BBQBiasType.ALL, ) assert llm_id is not None - if os.getenv("FULL_TEST", "false") == "true": + if get_env_bool("FULL_TEST"): run_id = LlmBehaviorEval.launch_eval_run(ModelOrRun.MODEL, run_info) assert run_id is not None results = LlmBehaviorEval.check_run_by_id(run_id) diff --git a/tests/test_unlearning_llm_model_create.py b/tests/test_unlearning_llm_model_create.py new file mode 100644 index 00000000..508e2b42 --- /dev/null +++ b/tests/test_unlearning_llm_model_create.py @@ -0,0 +1,106 @@ +import secrets + +import hirundo.unlearning_llm as unlearning_llm_module +import pytest +from hirundo._llm_sources import HuggingFaceTransformersModel +from hirundo.unlearning_llm import LlmModel + + +class _FakeResponse: + def __init__(self, model_id: int) -> None: + self._model_id = model_id + + def json(self) -> dict[str, int]: + return {"id": self._model_id} + + +def _build_huggingface_llm_model() -> LlmModel: + user_access_secret = secrets.token_hex(8) + return LlmModel( + model_name="test-llm", + model_source=HuggingFaceTransformersModel( + model_name="org/private-model", + token=user_access_secret, + ), + ) + + +def _stub_create_dependencies(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + unlearning_llm_module.requests, + "post", + lambda *args, **kwargs: _FakeResponse(model_id=123), + ) + monkeypatch.setattr( + unlearning_llm_module, + "raise_for_status_with_reason", + lambda _response: None, + ) + + +def test_llm_model_create_skips_hf_access_validation_by_default( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation_calls: list[tuple[str, str | None, str]] = [] + + monkeypatch.setenv("HIRUNDO_VALIDATE_HF_ACCESS", "false") + _stub_create_dependencies(monkeypatch) + monkeypatch.setattr( + unlearning_llm_module, + "validate_huggingface_model_access", + lambda model_name, token, model_role: validation_calls.append( + (model_name, token, model_role) + ), + ) + + _build_huggingface_llm_model().create() + + assert validation_calls == [] + + +def test_llm_model_create_validates_hf_access_when_parameter_enabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation_calls: list[tuple[str, str | None, str]] = [] + + monkeypatch.setenv("HIRUNDO_VALIDATE_HF_ACCESS", "false") + _stub_create_dependencies(monkeypatch) + monkeypatch.setattr( + unlearning_llm_module, + "validate_huggingface_model_access", + lambda model_name, token, model_role: validation_calls.append( + (model_name, token, model_role) + ), + ) + + llm_model = _build_huggingface_llm_model() + llm_model.create(validate_hf_access=True) + + assert isinstance(llm_model.model_source, HuggingFaceTransformersModel) + assert validation_calls == [ + ("org/private-model", llm_model.model_source.token, "LLM") + ] + + +def test_llm_model_create_validates_hf_access_when_env_enabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation_calls: list[tuple[str, str | None, str]] = [] + + monkeypatch.setenv("HIRUNDO_VALIDATE_HF_ACCESS", "true") + _stub_create_dependencies(monkeypatch) + monkeypatch.setattr( + unlearning_llm_module, + "validate_huggingface_model_access", + lambda model_name, token, model_role: validation_calls.append( + (model_name, token, model_role) + ), + ) + + llm_model = _build_huggingface_llm_model() + llm_model.create() + + assert isinstance(llm_model.model_source, HuggingFaceTransformersModel) + assert validation_calls == [ + ("org/private-model", llm_model.model_source.token, "LLM") + ] diff --git a/tests/unlearning-llm/unlearn_llm_behavior_test.py b/tests/unlearning-llm/unlearn_llm_behavior_test.py index 3506c982..a405f228 100644 --- a/tests/unlearning-llm/unlearn_llm_behavior_test.py +++ b/tests/unlearning-llm/unlearn_llm_behavior_test.py @@ -1,5 +1,4 @@ import logging -import os from hirundo import ( BBQBiasType, @@ -8,6 +7,7 @@ LlmModel, LlmUnlearningRun, ) +from hirundo._env import get_env_bool from tests.testing_utils import get_unique_id from transformers.pipelines.base import Pipeline @@ -28,7 +28,7 @@ def test_unlearn_llm_behavior(): bias_type=BBQBiasType.ALL, ) assert llm_id is not None - if os.getenv("FULL_TEST", "false") == "true": + if get_env_bool("FULL_TEST"): run_id = LlmUnlearningRun.launch( llm_id, run_info,