openbench-ai 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openbench/__init__.py +191 -0
- openbench/adapters/__init__.py +39 -0
- openbench/adapters/base.py +220 -0
- openbench/adapters/huggingface.py +158 -0
- openbench/adapters/local.py +104 -0
- openbench/adapters/mock.py +136 -0
- openbench/adapters/ollama.py +223 -0
- openbench/analytics.py +215 -0
- openbench/cache.py +315 -0
- openbench/catalog.py +185 -0
- openbench/cli.py +1665 -0
- openbench/comparison.py +394 -0
- openbench/configuration.py +490 -0
- openbench/dataset.py +646 -0
- openbench/evalstats.py +85 -0
- openbench/evaluator.py +994 -0
- openbench/exceptions.py +90 -0
- openbench/execution.py +456 -0
- openbench/experiments.py +1257 -0
- openbench/history.py +139 -0
- openbench/integrity.py +261 -0
- openbench/itemscores.py +178 -0
- openbench/lineage.py +264 -0
- openbench/metrics/__init__.py +127 -0
- openbench/metrics/accuracy.py +118 -0
- openbench/metrics/base.py +156 -0
- openbench/metrics/bleu.py +145 -0
- openbench/metrics/exact_match.py +52 -0
- openbench/metrics/f1.py +109 -0
- openbench/metrics/keyword_match.py +103 -0
- openbench/metrics/normalization.py +59 -0
- openbench/metrics/precision.py +243 -0
- openbench/metrics/recall.py +106 -0
- openbench/metrics/rouge.py +193 -0
- openbench/metrics/semantic_similarity.py +353 -0
- openbench/metrics/structured_output.py +259 -0
- openbench/paired.py +1042 -0
- openbench/performance.py +272 -0
- openbench/preferences.py +940 -0
- openbench/provenance.py +91 -0
- openbench/py.typed +0 -0
- openbench/quality.py +633 -0
- openbench/quality_notes.json +87 -0
- openbench/reporting.py +546 -0
- openbench/reporting_v040.py +391 -0
- openbench/security.py +258 -0
- openbench/stats/__init__.py +74 -0
- openbench/stats/_numeric.py +49 -0
- openbench/stats/_validation.py +24 -0
- openbench/stats/bootstrap.py +140 -0
- openbench/stats/effects.py +152 -0
- openbench/stats/equivalence.py +939 -0
- openbench/stats/results.py +214 -0
- openbench/stats/rng.py +54 -0
- openbench/stats/tests.py +323 -0
- openbench/stats/variability.py +324 -0
- openbench_ai-0.4.0.dist-info/METADATA +253 -0
- openbench_ai-0.4.0.dist-info/RECORD +62 -0
- openbench_ai-0.4.0.dist-info/WHEEL +5 -0
- openbench_ai-0.4.0.dist-info/entry_points.txt +2 -0
- openbench_ai-0.4.0.dist-info/licenses/LICENSE +21 -0
- openbench_ai-0.4.0.dist-info/top_level.txt +1 -0
openbench/__init__.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""OpenBench AI: A modular, open-source LLM evaluation, execution, and benchmarking platform."""
|
|
2
|
+
|
|
3
|
+
from openbench.adapters import (
|
|
4
|
+
PROVIDER_REGISTRY,
|
|
5
|
+
BaseModelAdapter,
|
|
6
|
+
DeterministicLookupAdapter,
|
|
7
|
+
GenerationConfig,
|
|
8
|
+
HuggingFaceAdapter,
|
|
9
|
+
LocalCallableAdapter,
|
|
10
|
+
MockModelProvider,
|
|
11
|
+
OllamaAdapter,
|
|
12
|
+
OllamaConnectionError,
|
|
13
|
+
ProviderHealthStatus,
|
|
14
|
+
create_provider,
|
|
15
|
+
list_registered_providers,
|
|
16
|
+
register_provider,
|
|
17
|
+
)
|
|
18
|
+
from openbench.analytics import BenchmarkAnalytics
|
|
19
|
+
from openbench.cache import CacheRecord, ResponseCache
|
|
20
|
+
from openbench.catalog import DatasetCatalog, DatasetCatalogEntry
|
|
21
|
+
from openbench.comparison import (
|
|
22
|
+
ComparisonResult,
|
|
23
|
+
MetricLeader,
|
|
24
|
+
ModelComparator,
|
|
25
|
+
ModelComparisonEntry,
|
|
26
|
+
)
|
|
27
|
+
from openbench.configuration import EvaluationProfile, ProfileLoader
|
|
28
|
+
from openbench.dataset import (
|
|
29
|
+
Dataset,
|
|
30
|
+
DatasetLoader,
|
|
31
|
+
DatasetRecord,
|
|
32
|
+
IdFilterSpec,
|
|
33
|
+
load_ids_file,
|
|
34
|
+
)
|
|
35
|
+
from openbench.evaluator import (
|
|
36
|
+
EvaluationConfig,
|
|
37
|
+
EvaluationResult,
|
|
38
|
+
Evaluator,
|
|
39
|
+
ItemEvaluationResult,
|
|
40
|
+
ModelResponseSet,
|
|
41
|
+
ResponseItem,
|
|
42
|
+
ResponseLoader,
|
|
43
|
+
)
|
|
44
|
+
from openbench.exceptions import (
|
|
45
|
+
ConfigurationError,
|
|
46
|
+
DatasetValidationError,
|
|
47
|
+
EmptyDatasetError,
|
|
48
|
+
IncompatibleComparisonError,
|
|
49
|
+
MetricError,
|
|
50
|
+
OpenBenchError,
|
|
51
|
+
ResponseValidationError,
|
|
52
|
+
)
|
|
53
|
+
from openbench.execution import (
|
|
54
|
+
ExecutionConfig,
|
|
55
|
+
ExecutionEngine,
|
|
56
|
+
ExecutionOutcome,
|
|
57
|
+
RetryPolicy,
|
|
58
|
+
classify_execution_error,
|
|
59
|
+
)
|
|
60
|
+
from openbench.experiments import ExperimentManager, ExperimentRecord, RepeatGroupResult
|
|
61
|
+
from openbench.history import HistoryEntry, ResultHistoryStore
|
|
62
|
+
from openbench.integrity import (
|
|
63
|
+
IntegrityCheckResult,
|
|
64
|
+
RunIntegrityVerifier,
|
|
65
|
+
compute_dict_sha256,
|
|
66
|
+
compute_file_sha256,
|
|
67
|
+
)
|
|
68
|
+
from openbench.lineage import RunLineage
|
|
69
|
+
from openbench.metrics import LexicalSimilarityMetric
|
|
70
|
+
from openbench.paired import (
|
|
71
|
+
ComparisonArm,
|
|
72
|
+
ComparisonPolicy,
|
|
73
|
+
CompatibilityReport,
|
|
74
|
+
PairedComparisonEngine,
|
|
75
|
+
PairedComparisonResult,
|
|
76
|
+
)
|
|
77
|
+
from openbench.performance import (
|
|
78
|
+
PerformanceMonitor,
|
|
79
|
+
PerformanceReport,
|
|
80
|
+
PricingConfig,
|
|
81
|
+
RequestTelemetry,
|
|
82
|
+
compute_percentile,
|
|
83
|
+
)
|
|
84
|
+
from openbench.preferences import (
|
|
85
|
+
AnnotationImporter,
|
|
86
|
+
PreferenceAnalyzer,
|
|
87
|
+
PreferenceAnnotation,
|
|
88
|
+
PreferenceTaskBuilder,
|
|
89
|
+
)
|
|
90
|
+
from openbench.quality import AuditFinding, AuditReport, DatasetAuditor, ExperimentAuditor
|
|
91
|
+
from openbench.reporting import ReportGenerator, escape_markdown_cell
|
|
92
|
+
from openbench.security import (
|
|
93
|
+
redact_prompt_if_private,
|
|
94
|
+
redact_sensitive_data,
|
|
95
|
+
resolve_env_credential,
|
|
96
|
+
sanitize_error_message,
|
|
97
|
+
validate_endpoint_url,
|
|
98
|
+
validate_safe_output_path,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
__version__ = "0.4.0"
|
|
102
|
+
|
|
103
|
+
__all__ = [
|
|
104
|
+
"__version__",
|
|
105
|
+
"BaseModelAdapter",
|
|
106
|
+
"BenchmarkAnalytics",
|
|
107
|
+
"CacheRecord",
|
|
108
|
+
"ComparisonResult",
|
|
109
|
+
"ConfigurationError",
|
|
110
|
+
"Dataset",
|
|
111
|
+
"DatasetCatalog",
|
|
112
|
+
"DatasetCatalogEntry",
|
|
113
|
+
"DatasetLoader",
|
|
114
|
+
"DatasetRecord",
|
|
115
|
+
"DatasetValidationError",
|
|
116
|
+
"DeterministicLookupAdapter",
|
|
117
|
+
"EmptyDatasetError",
|
|
118
|
+
"EvaluationConfig",
|
|
119
|
+
"EvaluationProfile",
|
|
120
|
+
"EvaluationResult",
|
|
121
|
+
"Evaluator",
|
|
122
|
+
"ExecutionConfig",
|
|
123
|
+
"ExecutionEngine",
|
|
124
|
+
"ExecutionOutcome",
|
|
125
|
+
"ExperimentManager",
|
|
126
|
+
"ExperimentRecord",
|
|
127
|
+
"GenerationConfig",
|
|
128
|
+
"HistoryEntry",
|
|
129
|
+
"HuggingFaceAdapter",
|
|
130
|
+
"IdFilterSpec",
|
|
131
|
+
"IntegrityCheckResult",
|
|
132
|
+
"ItemEvaluationResult",
|
|
133
|
+
"LexicalSimilarityMetric",
|
|
134
|
+
"LocalCallableAdapter",
|
|
135
|
+
"MetricError",
|
|
136
|
+
"MetricLeader",
|
|
137
|
+
"MockModelProvider",
|
|
138
|
+
"ModelComparator",
|
|
139
|
+
"ModelComparisonEntry",
|
|
140
|
+
"ModelResponseSet",
|
|
141
|
+
"OllamaAdapter",
|
|
142
|
+
"OllamaConnectionError",
|
|
143
|
+
"OpenBenchError",
|
|
144
|
+
"PROVIDER_REGISTRY",
|
|
145
|
+
"PerformanceMonitor",
|
|
146
|
+
"PerformanceReport",
|
|
147
|
+
"PricingConfig",
|
|
148
|
+
"ProfileLoader",
|
|
149
|
+
"ProviderHealthStatus",
|
|
150
|
+
"ReportGenerator",
|
|
151
|
+
"RequestTelemetry",
|
|
152
|
+
"ResponseCache",
|
|
153
|
+
"ResponseItem",
|
|
154
|
+
"ResponseLoader",
|
|
155
|
+
"ResponseValidationError",
|
|
156
|
+
"ResultHistoryStore",
|
|
157
|
+
"RetryPolicy",
|
|
158
|
+
"RunIntegrityVerifier",
|
|
159
|
+
"classify_execution_error",
|
|
160
|
+
"compute_dict_sha256",
|
|
161
|
+
"compute_file_sha256",
|
|
162
|
+
"compute_percentile",
|
|
163
|
+
"create_provider",
|
|
164
|
+
"escape_markdown_cell",
|
|
165
|
+
"list_registered_providers",
|
|
166
|
+
"load_ids_file",
|
|
167
|
+
"redact_prompt_if_private",
|
|
168
|
+
"redact_sensitive_data",
|
|
169
|
+
"register_provider",
|
|
170
|
+
"resolve_env_credential",
|
|
171
|
+
"sanitize_error_message",
|
|
172
|
+
"validate_endpoint_url",
|
|
173
|
+
"validate_safe_output_path",
|
|
174
|
+
# v0.4.0 additions (the 70 v0.3.1 names above are unchanged and keep their order)
|
|
175
|
+
"ComparisonArm",
|
|
176
|
+
"ComparisonPolicy",
|
|
177
|
+
"CompatibilityReport",
|
|
178
|
+
"IncompatibleComparisonError",
|
|
179
|
+
"PairedComparisonEngine",
|
|
180
|
+
"PairedComparisonResult",
|
|
181
|
+
"RepeatGroupResult",
|
|
182
|
+
"RunLineage",
|
|
183
|
+
"AnnotationImporter",
|
|
184
|
+
"PreferenceAnalyzer",
|
|
185
|
+
"PreferenceAnnotation",
|
|
186
|
+
"PreferenceTaskBuilder",
|
|
187
|
+
"AuditFinding",
|
|
188
|
+
"AuditReport",
|
|
189
|
+
"DatasetAuditor",
|
|
190
|
+
"ExperimentAuditor",
|
|
191
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Model providers and adapters package for OpenBench AI."""
|
|
2
|
+
|
|
3
|
+
from openbench.adapters.base import (
|
|
4
|
+
PROVIDER_REGISTRY,
|
|
5
|
+
BaseModelAdapter,
|
|
6
|
+
GenerationConfig,
|
|
7
|
+
ProviderHealthStatus,
|
|
8
|
+
create_provider,
|
|
9
|
+
list_registered_providers,
|
|
10
|
+
register_provider,
|
|
11
|
+
)
|
|
12
|
+
from openbench.adapters.huggingface import HuggingFaceAdapter
|
|
13
|
+
from openbench.adapters.local import DeterministicLookupAdapter, LocalCallableAdapter
|
|
14
|
+
from openbench.adapters.mock import MockModelProvider
|
|
15
|
+
from openbench.adapters.ollama import DEFAULT_OLLAMA_ENDPOINT, OllamaAdapter, OllamaConnectionError
|
|
16
|
+
|
|
17
|
+
# Register all built-in providers
|
|
18
|
+
register_provider(MockModelProvider)
|
|
19
|
+
register_provider(OllamaAdapter)
|
|
20
|
+
register_provider(HuggingFaceAdapter, aliases=["huggingface", "hf"])
|
|
21
|
+
register_provider(LocalCallableAdapter)
|
|
22
|
+
register_provider(DeterministicLookupAdapter)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BaseModelAdapter",
|
|
26
|
+
"DEFAULT_OLLAMA_ENDPOINT",
|
|
27
|
+
"DeterministicLookupAdapter",
|
|
28
|
+
"GenerationConfig",
|
|
29
|
+
"HuggingFaceAdapter",
|
|
30
|
+
"LocalCallableAdapter",
|
|
31
|
+
"MockModelProvider",
|
|
32
|
+
"OllamaAdapter",
|
|
33
|
+
"OllamaConnectionError",
|
|
34
|
+
"PROVIDER_REGISTRY",
|
|
35
|
+
"ProviderHealthStatus",
|
|
36
|
+
"create_provider",
|
|
37
|
+
"list_registered_providers",
|
|
38
|
+
"register_provider",
|
|
39
|
+
]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Base model inference adapter interface, generation config, and provider registry for OpenBench AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from openbench.dataset import Dataset, DatasetRecord
|
|
11
|
+
from openbench.evaluator import ModelResponseSet, ResponseItem
|
|
12
|
+
from openbench.exceptions import ConfigurationError
|
|
13
|
+
from openbench.security import redact_sensitive_data
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class GenerationConfig:
|
|
18
|
+
"""Standardized generation parameters passed to model providers."""
|
|
19
|
+
|
|
20
|
+
temperature: float = 0.0
|
|
21
|
+
max_tokens: int = 256
|
|
22
|
+
top_p: float = 1.0
|
|
23
|
+
seed: int | None = None
|
|
24
|
+
stop_sequences: list[str] = field(default_factory=list)
|
|
25
|
+
stream: bool = False
|
|
26
|
+
extra_options: dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict[str, Any]:
|
|
29
|
+
payload: dict[str, Any] = {
|
|
30
|
+
"temperature": self.temperature,
|
|
31
|
+
"max_tokens": self.max_tokens,
|
|
32
|
+
"top_p": self.top_p,
|
|
33
|
+
"stream": self.stream,
|
|
34
|
+
}
|
|
35
|
+
if self.seed is not None:
|
|
36
|
+
payload["seed"] = self.seed
|
|
37
|
+
if self.stop_sequences:
|
|
38
|
+
payload["stop_sequences"] = list(self.stop_sequences)
|
|
39
|
+
if self.extra_options:
|
|
40
|
+
payload["extra_options"] = redact_sensitive_data(dict(self.extra_options))
|
|
41
|
+
return payload
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ProviderHealthStatus:
|
|
46
|
+
"""Result of checking a model provider's availability and configuration."""
|
|
47
|
+
|
|
48
|
+
provider_name: str
|
|
49
|
+
available: bool
|
|
50
|
+
message: str
|
|
51
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"provider_name": self.provider_name,
|
|
56
|
+
"available": self.available,
|
|
57
|
+
"message": self.message,
|
|
58
|
+
"details": redact_sensitive_data(dict(self.details)),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BaseModelAdapter(ABC):
|
|
63
|
+
"""Abstract interface for connecting local or external language models to OpenBench AI.
|
|
64
|
+
|
|
65
|
+
Design Principles:
|
|
66
|
+
- Adapters are strictly optional; core OpenBench evaluation works directly on JSON files.
|
|
67
|
+
- Never hardcode tokens or automatically call paid APIs.
|
|
68
|
+
- Record reproducibility metadata (`model_name`, `model_version`, `model_provider`,
|
|
69
|
+
`inference_config`) alongside generated responses.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
provider_name: str = "base_adapter"
|
|
73
|
+
description: str = "Abstract base model provider adapter."
|
|
74
|
+
is_experimental: bool = False
|
|
75
|
+
is_mock: bool = False
|
|
76
|
+
optional_dependencies: tuple[str, ...] = ()
|
|
77
|
+
#: True only for adapters that place ``GenerationConfig.seed`` in the provider request
|
|
78
|
+
#: (v0.4.0). A provider may still ignore the seed or be non-deterministic.
|
|
79
|
+
supports_generation_seed: bool = False
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
model_name: str,
|
|
84
|
+
model_version: str = "1.0",
|
|
85
|
+
inference_config: dict[str, Any] | None = None,
|
|
86
|
+
generation_config: GenerationConfig | None = None,
|
|
87
|
+
timeout_seconds: float = 30.0,
|
|
88
|
+
) -> None:
|
|
89
|
+
if not model_name or not model_name.strip():
|
|
90
|
+
raise ValueError("model_name must be a non-empty string")
|
|
91
|
+
if timeout_seconds <= 0:
|
|
92
|
+
raise ConfigurationError("timeout_seconds must be positive")
|
|
93
|
+
self.model_name = model_name.strip()
|
|
94
|
+
self.model_version = model_version.strip() or "1.0"
|
|
95
|
+
self.generation_config = generation_config or GenerationConfig()
|
|
96
|
+
self.timeout_seconds = float(timeout_seconds)
|
|
97
|
+
base_inf = self.generation_config.to_dict()
|
|
98
|
+
if inference_config:
|
|
99
|
+
base_inf.update(redact_sensitive_data(dict(inference_config)))
|
|
100
|
+
self.inference_config = base_inf
|
|
101
|
+
#: Number of provider requests into which this adapter placed a generation seed.
|
|
102
|
+
self.seed_forward_count = 0
|
|
103
|
+
|
|
104
|
+
def with_generation_seed(self, seed: int | None) -> BaseModelAdapter:
|
|
105
|
+
"""Return a shallow copy of this adapter whose generation seed is ``seed`` (v0.4.0).
|
|
106
|
+
|
|
107
|
+
Used for per-repetition seeds. The copy shares any client or transport objects with
|
|
108
|
+
the original; only ``generation_config``, ``inference_config`` and the seed counter
|
|
109
|
+
are replaced.
|
|
110
|
+
"""
|
|
111
|
+
import copy
|
|
112
|
+
from dataclasses import replace
|
|
113
|
+
|
|
114
|
+
clone = copy.copy(self)
|
|
115
|
+
clone.generation_config = replace(self.generation_config, seed=seed)
|
|
116
|
+
inference = dict(self.inference_config)
|
|
117
|
+
if seed is None:
|
|
118
|
+
inference.pop("seed", None)
|
|
119
|
+
else:
|
|
120
|
+
inference["seed"] = seed
|
|
121
|
+
clone.inference_config = inference
|
|
122
|
+
clone.seed_forward_count = 0
|
|
123
|
+
return clone
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def generate_one(self, record: DatasetRecord) -> ResponseItem:
|
|
127
|
+
"""Generate a single ResponseItem for a given DatasetRecord."""
|
|
128
|
+
|
|
129
|
+
def generate_responses(self, dataset: Dataset) -> ModelResponseSet:
|
|
130
|
+
"""Run inference across all records in a Dataset and return a ModelResponseSet."""
|
|
131
|
+
items = [self.generate_one(record) for record in dataset.records]
|
|
132
|
+
return ModelResponseSet(
|
|
133
|
+
model_name=self.model_name,
|
|
134
|
+
model_version=self.model_version,
|
|
135
|
+
responses=items,
|
|
136
|
+
model_provider=self.provider_name,
|
|
137
|
+
inference_config=self.reproducibility_metadata(),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def check_connection(self) -> ProviderHealthStatus:
|
|
141
|
+
"""Verify whether the provider is reachable and properly configured."""
|
|
142
|
+
return ProviderHealthStatus(
|
|
143
|
+
provider_name=self.provider_name,
|
|
144
|
+
available=True,
|
|
145
|
+
message=f"Provider '{self.provider_name}' is ready.",
|
|
146
|
+
details={"model_name": self.model_name, "is_mock": self.is_mock},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def reproducibility_metadata(self) -> dict[str, Any]:
|
|
150
|
+
"""Return safe reproducibility metadata for reports and cards (with secrets redacted)."""
|
|
151
|
+
return redact_sensitive_data(
|
|
152
|
+
{
|
|
153
|
+
"provider": self.provider_name,
|
|
154
|
+
"model_name": self.model_name,
|
|
155
|
+
"model_version": self.model_version,
|
|
156
|
+
"experimental": self.is_experimental,
|
|
157
|
+
"is_mock": self.is_mock,
|
|
158
|
+
"timeout_seconds": self.timeout_seconds,
|
|
159
|
+
**self.inference_config,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
PROVIDER_REGISTRY: dict[str, type[BaseModelAdapter]] = {}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def register_provider(adapter_cls: type[BaseModelAdapter], aliases: list[str] | None = None) -> None:
|
|
168
|
+
"""Register a BaseModelAdapter subclass in `PROVIDER_REGISTRY`."""
|
|
169
|
+
if not isinstance(adapter_cls, type) or not issubclass(adapter_cls, BaseModelAdapter):
|
|
170
|
+
raise ConfigurationError("Provider class must inherit from BaseModelAdapter")
|
|
171
|
+
p_name = getattr(adapter_cls, "provider_name", "").strip().lower()
|
|
172
|
+
if not p_name:
|
|
173
|
+
raise ConfigurationError("Provider class must define a non-empty 'provider_name'")
|
|
174
|
+
PROVIDER_REGISTRY[p_name] = adapter_cls
|
|
175
|
+
for alias in aliases or []:
|
|
176
|
+
PROVIDER_REGISTRY[alias.strip().lower()] = adapter_cls
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def create_provider(provider_name: str, **kwargs: Any) -> BaseModelAdapter:
|
|
180
|
+
"""Instantiate a registered provider adapter by name."""
|
|
181
|
+
key = provider_name.strip().lower()
|
|
182
|
+
if key not in PROVIDER_REGISTRY:
|
|
183
|
+
available = sorted({cls.provider_name for cls in PROVIDER_REGISTRY.values()})
|
|
184
|
+
raise ConfigurationError(
|
|
185
|
+
f"Unknown model provider '{provider_name}'. Registered providers: {', '.join(available)}"
|
|
186
|
+
)
|
|
187
|
+
adapter_cls = PROVIDER_REGISTRY[key]
|
|
188
|
+
sig = inspect.signature(adapter_cls.__init__)
|
|
189
|
+
accepts_var_kw = any(
|
|
190
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
191
|
+
)
|
|
192
|
+
filtered_kwargs: dict[str, Any] = {}
|
|
193
|
+
for k, v in kwargs.items():
|
|
194
|
+
if accepts_var_kw or k in sig.parameters:
|
|
195
|
+
filtered_kwargs[k] = v
|
|
196
|
+
else:
|
|
197
|
+
raise ConfigurationError(
|
|
198
|
+
f"Provider '{provider_name}' does not accept configuration argument '{k}'"
|
|
199
|
+
)
|
|
200
|
+
return adapter_cls(**filtered_kwargs)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def list_registered_providers() -> list[dict[str, Any]]:
|
|
204
|
+
"""Return metadata dictionaries for all unique registered model providers."""
|
|
205
|
+
unique: dict[str, type[BaseModelAdapter]] = {}
|
|
206
|
+
for cls in PROVIDER_REGISTRY.values():
|
|
207
|
+
unique[cls.provider_name] = cls
|
|
208
|
+
result: list[dict[str, Any]] = []
|
|
209
|
+
for name in sorted(unique):
|
|
210
|
+
cls = unique[name]
|
|
211
|
+
result.append(
|
|
212
|
+
{
|
|
213
|
+
"provider_name": cls.provider_name,
|
|
214
|
+
"description": cls.description,
|
|
215
|
+
"is_experimental": cls.is_experimental,
|
|
216
|
+
"is_mock": cls.is_mock,
|
|
217
|
+
"optional_dependencies": list(cls.optional_dependencies),
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
return result
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Optional Hugging Face `transformers` adapter for OpenBench AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from openbench.adapters.base import BaseModelAdapter, GenerationConfig, ProviderHealthStatus
|
|
9
|
+
from openbench.dataset import DatasetRecord
|
|
10
|
+
from openbench.evaluator import ResponseItem
|
|
11
|
+
from openbench.exceptions import ConfigurationError, OpenBenchError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HuggingFaceAdapter(BaseModelAdapter):
|
|
15
|
+
"""Optional adapter for local Hugging Face `transformers` text-generation pipelines.
|
|
16
|
+
|
|
17
|
+
Status: Experimental (requires optional `transformers` and `torch` packages).
|
|
18
|
+
|
|
19
|
+
Safety & Reproducibility Guarantees:
|
|
20
|
+
- Does NOT import `transformers` at module import time.
|
|
21
|
+
- Defaults to `local_files_only=True` so weights are NEVER automatically downloaded
|
|
22
|
+
from the Hugging Face Hub unless the caller explicitly sets `allow_download=True`.
|
|
23
|
+
- Contains zero hardcoded API tokens and makes zero paid API calls.
|
|
24
|
+
- Records explicit `model_id`, `revision`, `device`, and generation parameters.
|
|
25
|
+
- Catches `MemoryError` and CUDA/CPU OOM errors with actionable remediation hints.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
provider_name = "huggingface_transformers"
|
|
29
|
+
description = "Local Hugging Face transformers text-generation pipeline adapter (Experimental)."
|
|
30
|
+
is_experimental = True
|
|
31
|
+
is_mock = False
|
|
32
|
+
optional_dependencies = ("transformers", "torch")
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
model_id: str = "gpt2",
|
|
37
|
+
revision: str = "main",
|
|
38
|
+
allow_download: bool = False,
|
|
39
|
+
device: str = "cpu",
|
|
40
|
+
max_new_tokens: int = 128,
|
|
41
|
+
temperature: float = 0.0,
|
|
42
|
+
pipeline_fn: Any | None = None,
|
|
43
|
+
generation_config: GenerationConfig | None = None,
|
|
44
|
+
timeout_seconds: float = 60.0,
|
|
45
|
+
model_name: str | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
resolved_id = (model_name or model_id).strip()
|
|
48
|
+
if not resolved_id:
|
|
49
|
+
raise ConfigurationError("An explicit Hugging Face model_id is required")
|
|
50
|
+
gen_cfg = generation_config or GenerationConfig(
|
|
51
|
+
temperature=temperature,
|
|
52
|
+
max_tokens=max_new_tokens,
|
|
53
|
+
)
|
|
54
|
+
super().__init__(
|
|
55
|
+
model_name=resolved_id,
|
|
56
|
+
model_version=revision.strip() or "main",
|
|
57
|
+
generation_config=gen_cfg,
|
|
58
|
+
timeout_seconds=timeout_seconds,
|
|
59
|
+
inference_config={
|
|
60
|
+
"model_id": resolved_id,
|
|
61
|
+
"revision": revision.strip() or "main",
|
|
62
|
+
"local_files_only": not allow_download,
|
|
63
|
+
"device": device,
|
|
64
|
+
"max_new_tokens": max_new_tokens,
|
|
65
|
+
"temperature": temperature,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
self.model_id = resolved_id
|
|
69
|
+
self.revision = revision.strip() or "main"
|
|
70
|
+
self.allow_download = allow_download
|
|
71
|
+
self.device = device
|
|
72
|
+
self.max_new_tokens = max_new_tokens
|
|
73
|
+
self.temperature = temperature
|
|
74
|
+
self._pipeline = pipeline_fn
|
|
75
|
+
|
|
76
|
+
def check_connection(self) -> ProviderHealthStatus:
|
|
77
|
+
if self._pipeline is not None:
|
|
78
|
+
return ProviderHealthStatus(
|
|
79
|
+
provider_name=self.provider_name,
|
|
80
|
+
available=True,
|
|
81
|
+
message=f"HuggingFaceAdapter ready with injected pipeline for '{self.model_id}'.",
|
|
82
|
+
details={"model_id": self.model_id, "revision": self.revision},
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
import transformers # type: ignore[import-not-found] # noqa: F401
|
|
86
|
+
except ImportError:
|
|
87
|
+
return ProviderHealthStatus(
|
|
88
|
+
provider_name=self.provider_name,
|
|
89
|
+
available=False,
|
|
90
|
+
message=(
|
|
91
|
+
"Optional dependency 'transformers' is not installed. "
|
|
92
|
+
"Install it with `pip install -e '.[huggingface]'`."
|
|
93
|
+
),
|
|
94
|
+
details={"model_id": self.model_id, "device": self.device},
|
|
95
|
+
)
|
|
96
|
+
return ProviderHealthStatus(
|
|
97
|
+
provider_name=self.provider_name,
|
|
98
|
+
available=True,
|
|
99
|
+
message=f"transformers package is installed (model='{self.model_id}', local_files_only={not self.allow_download}).",
|
|
100
|
+
details={"model_id": self.model_id, "revision": self.revision, "device": self.device},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _get_pipeline(self) -> Any:
|
|
104
|
+
if self._pipeline is not None:
|
|
105
|
+
return self._pipeline
|
|
106
|
+
try:
|
|
107
|
+
from transformers import pipeline # type: ignore[import-not-found]
|
|
108
|
+
except ImportError as exc:
|
|
109
|
+
raise ConfigurationError(
|
|
110
|
+
"Optional dependency 'transformers' is not installed. "
|
|
111
|
+
"Install it with `pip install -e '.[huggingface]'` to use HuggingFaceAdapter."
|
|
112
|
+
) from exc
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
self._pipeline = pipeline(
|
|
116
|
+
"text-generation",
|
|
117
|
+
model=self.model_id,
|
|
118
|
+
revision=self.revision,
|
|
119
|
+
device=self.device,
|
|
120
|
+
model_kwargs={"local_files_only": not self.allow_download},
|
|
121
|
+
)
|
|
122
|
+
except MemoryError as exc:
|
|
123
|
+
raise OpenBenchError(
|
|
124
|
+
f"Insufficient system memory to load Hugging Face model '{self.model_id}' on device '{self.device}'."
|
|
125
|
+
) from exc
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
raise OpenBenchError(
|
|
128
|
+
f"Failed to load Hugging Face model '{self.model_id}' (revision='{self.revision}', "
|
|
129
|
+
f"local_files_only={not self.allow_download}): {exc}"
|
|
130
|
+
) from exc
|
|
131
|
+
return self._pipeline
|
|
132
|
+
|
|
133
|
+
def generate_one(self, record: DatasetRecord) -> ResponseItem:
|
|
134
|
+
pipe = self._get_pipeline()
|
|
135
|
+
start = time.perf_counter()
|
|
136
|
+
try:
|
|
137
|
+
outputs = pipe(
|
|
138
|
+
record.prompt,
|
|
139
|
+
max_new_tokens=self.max_new_tokens,
|
|
140
|
+
do_sample=self.temperature > 0.0,
|
|
141
|
+
return_full_text=False,
|
|
142
|
+
)
|
|
143
|
+
except MemoryError as exc:
|
|
144
|
+
raise OpenBenchError(
|
|
145
|
+
f"Out of memory while generating response for '{record.id}' with model '{self.model_id}'."
|
|
146
|
+
) from exc
|
|
147
|
+
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
|
148
|
+
|
|
149
|
+
if isinstance(outputs, list) and outputs and isinstance(outputs[0], dict):
|
|
150
|
+
generated_text = str(outputs[0].get("generated_text", ""))
|
|
151
|
+
else:
|
|
152
|
+
generated_text = str(outputs)
|
|
153
|
+
|
|
154
|
+
return ResponseItem(
|
|
155
|
+
id=record.id,
|
|
156
|
+
response=generated_text,
|
|
157
|
+
latency_ms=round(elapsed_ms, 3),
|
|
158
|
+
)
|