Skip to content

Commit 932e1f2

Browse files
NathanHBNathan Habibclaude
authored
Store provider credentials as SecretStr in model configs (#1326)
* Store provider credentials as SecretStr in model configs LiteLLMModelConfig.api_key and TGIModelConfig.inference_server_auth were plain str fields, which meant they were retained in plaintext wherever a model config gets serialized (e.g. EvaluationTracker.results). Switch both to pydantic SecretStr, which masks the value in reprs and default serialization, and additionally exclude them explicitly when building the results dict as a second layer. The real value is still unwrapped via get_secret_value() at the specific call sites that need it for the actual outgoing request. Added regression tests asserting the credential never appears in the serialized results dict. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Store JudgeLM credentials as SecretStr JudgeLM.api_key was a plain str consumed directly by several backend clients (OpenAI, AsyncInferenceClient, litellm). Wrap it in SecretStr on assignment and unwrap via get_secret_value() at each usage site, for consistency with the other model configs and to remove any reliance on incidental string formatting to keep it out of logs or serialized output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix pre-existing ruff format drift in README and docs Unrelated cleanup so CI's Quality check is green on this branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Nathan Habib <nathan_habib@Mac.lan> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 64f4f5a commit 932e1f2

7 files changed

Lines changed: 69 additions & 27 deletions

File tree

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,9 @@ MODEL_NAME = "meta-llama/Meta-Llama-3-8B-Instruct"
145145
BENCHMARKS = "gsm8k"
146146

147147
evaluation_tracker = EvaluationTracker(output_dir="./results")
148-
pipeline_params = PipelineParameters(
149-
launcher_type=ParallelismManager.NONE,
150-
max_samples=2
151-
)
148+
pipeline_params = PipelineParameters(launcher_type=ParallelismManager.NONE, max_samples=2)
152149

153-
model = AutoModelForCausalLM.from_pretrained(
154-
MODEL_NAME, device_map="auto"
155-
)
150+
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto")
156151
config = TransformersModelConfig(model_name=MODEL_NAME, batch_size=1)
157152
model = TransformersModel.from_model(model, config)
158153

docs/source/offline-evaluation.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@ from lighteval.tasks.requests import Doc
1818

1919

2020
def local_prompt(line: dict, task_name: str) -> Doc:
21-
return Doc(
22-
task_name=task_name,
23-
query=line["question"],
24-
choices=line["choices"],
25-
gold_index=line["answer"]
26-
)
21+
return Doc(task_name=task_name, query=line["question"], choices=line["choices"], gold_index=line["answer"])
2722

2823

2924
local_data = Path(__file__).parent / "samples" / "faq.jsonl"

src/lighteval/logging/evaluation_tracker.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,11 @@ def __init__(
211211
@property
212212
def results(self):
213213
config_general = asdict(self.general_config_logger)
214-
config_general["model_config"] = config_general["model_config"].model_dump()
214+
# This exclude set is defense in depth: SecretStr fields already serialize masked,
215+
# but this ensures no future plain-str secret field leaks through model_dump().
216+
config_general["model_config"] = config_general["model_config"].model_dump(
217+
exclude={"api_key", "inference_server_auth"}
218+
)
215219
results = {
216220
"config_general": config_general,
217221
"results": self.metrics_logger.metric_aggregated,

src/lighteval/metrics/utils/llm_as_judge.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from typing import Callable, Literal, Optional
3030

3131
from huggingface_hub import AsyncInferenceClient, InferenceTimeoutError
32-
from pydantic import BaseModel
32+
from pydantic import BaseModel, SecretStr
3333
from requests.exceptions import HTTPError
3434
from tqdm import tqdm
3535
from tqdm.asyncio import tqdm_asyncio
@@ -74,6 +74,7 @@ class JudgeLM:
7474
judge_backend (Literal["litellm", "openai", "transformers", "tgi", "vllm", "inference-providers"]): The backend for the judge.
7575
url (str | None): The URL for the OpenAI API.
7676
api_key (str | None): The API key for the OpenAI API (either OpenAI or HF key).
77+
Stored internally as a SecretStr so it is masked in logs, reprs, and serialized configs.
7778
max_tokens (int): The maximum number of tokens to generate. Defaults to 512.
7879
response_format (BaseModel | None): The format of the response from the API, used for the OpenAI and TGI backend.
7980
hf_provider (Literal["black-forest-labs", "cerebras", "cohere", "fal-ai", "fireworks-ai",
@@ -129,7 +130,7 @@ def __init__(
129130
self.process_judge_response = process_judge_response
130131

131132
self.url = url
132-
self.api_key = api_key
133+
self.api_key = SecretStr(api_key) if api_key is not None else None
133134
self.backend = judge_backend
134135
self.hf_provider = hf_provider
135136
self.max_tokens = max_tokens
@@ -156,7 +157,8 @@ def __lazy_load_client(self): # noqa: C901
156157
from openai import OpenAI
157158

158159
self.client = OpenAI(
159-
api_key=self.api_key if self.url is None else None, base_url=self.url if self.url else None
160+
api_key=self.api_key.get_secret_value() if self.url is None and self.api_key else None,
161+
base_url=self.url if self.url else None,
160162
)
161163
return self.__call_api_parallel
162164

@@ -195,7 +197,11 @@ def __lazy_load_client(self): # noqa: C901
195197
case "inference-providers":
196198
from huggingface_hub import AsyncInferenceClient
197199

198-
self.client = AsyncInferenceClient(token=self.api_key, base_url=self.url, provider=self.hf_provider)
200+
self.client = AsyncInferenceClient(
201+
token=self.api_key.get_secret_value() if self.api_key else None,
202+
base_url=self.url,
203+
provider=self.hf_provider,
204+
)
199205
return self.__call_hf_inference_async
200206

201207
case _:
@@ -340,7 +346,7 @@ def __call_api(prompt):
340346
if max_new_tokens is not None:
341347
kwargs["max_tokens"] = (max_new_tokens,)
342348
if self.api_key is not None:
343-
kwargs["api_key"] = self.api_key
349+
kwargs["api_key"] = self.api_key.get_secret_value()
344350
if self.url is not None:
345351
kwargs["base_url"] = self.url
346352

src/lighteval/models/endpoints/litellm_model.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from json import JSONDecodeError
2727

2828
import requests
29+
from pydantic import SecretStr
2930
from tqdm import tqdm
3031

3132
from lighteval.data import GenerativeTaskDataset
@@ -77,9 +78,10 @@ class LiteLLMModelConfig(ModelConfig):
7778
base_url (str | None):
7879
Custom base URL for the API. If None, uses provider's default URL.
7980
Useful for using custom endpoints or local deployments.
80-
api_key (str | None):
81+
api_key (SecretStr | None):
8182
API key for authentication. If None, reads from environment variables.
8283
Environment variable names are provider-specific (e.g., OPENAI_API_KEY).
84+
Stored as a SecretStr so it is masked in logs, reprs, and serialized configs.
8385
concurrent_requests (int):
8486
Maximum number of concurrent API requests to execute in parallel.
8587
Higher values can improve throughput for batch processing but may hit rate limits
@@ -121,7 +123,7 @@ class LiteLLMModelConfig(ModelConfig):
121123
model_name: str
122124
provider: str | None = None
123125
base_url: str | None = None
124-
api_key: str | None = None
126+
api_key: SecretStr | None = None
125127
concurrent_requests: int = 10
126128
verbose: bool = False
127129
max_model_length: int | None = None
@@ -144,7 +146,7 @@ def __init__(self, config: LiteLLMModelConfig) -> None:
144146
self.model = config.model_name
145147
self.provider = config.provider or config.model_name.split("/")[0]
146148
self.base_url = config.base_url
147-
self.api_key = config.api_key
149+
self.api_key = config.api_key.get_secret_value() if config.api_key is not None else None
148150
self.generation_parameters = config.generation_parameters
149151
self.concurrent_requests = config.concurrent_requests
150152
self._max_length = config.max_model_length

src/lighteval/models/endpoints/tgi_model.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import requests
2828
from huggingface_hub import TextGenerationInputGenerateParameters, TextGenerationInputGrammarType, TextGenerationOutput
29+
from pydantic import SecretStr
2930
from transformers.models.auto.tokenization_auto import AutoTokenizer
3031

3132
from lighteval.models.abstract_model import ModelConfig
@@ -65,8 +66,9 @@ class TGIModelConfig(ModelConfig):
6566
inference_server_address (str | None):
6667
Address of the TGI server. Format: "http://host:port" or "https://host:port".
6768
Example: "http://localhost:8080"
68-
inference_server_auth (str | None):
69+
inference_server_auth (SecretStr | None):
6970
Authentication token for the TGI server. If None, no authentication is used.
71+
Stored as a SecretStr so it is masked in logs, reprs, and serialized configs.
7072
model_name (str | None):
7173
Optional model name override. If None, uses the model name from server info.
7274
generation_parameters (GenerationParameters, optional, defaults to empty GenerationParameters):
@@ -91,7 +93,7 @@ class TGIModelConfig(ModelConfig):
9193
"""
9294

9395
inference_server_address: str | None = None
94-
inference_server_auth: str | None = None
96+
inference_server_auth: SecretStr | None = None
9597
model_name: str | None
9698
model_info: dict | None = None
9799
batch_size: int = 1
@@ -104,7 +106,9 @@ class ModelClient(InferenceEndpointModel):
104106

105107
def __init__(self, config: TGIModelConfig) -> None:
106108
headers = (
107-
{} if config.inference_server_auth is None else {"Authorization": f"Bearer {config.inference_server_auth}"}
109+
{}
110+
if config.inference_server_auth is None
111+
else {"Authorization": f"Bearer {config.inference_server_auth.get_secret_value()}"}
108112
)
109113

110114
self.client = AsyncClient(config.inference_server_address, headers=headers, timeout=240)

tests/unit/logging/test_evaluation_tracker.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,13 +299,11 @@ def setUp(self):
299299
"model_name": "test/case",
300300
"provider": None,
301301
"base_url": None,
302-
"api_key": None,
303302
"system_prompt": ref_system_prompt,
304303
"generation_parameters": ref_generation_parameters,
305304
} # ruff: noqa: E501
306305
self.tgi_ref_config = {
307306
"inference_server_address": None,
308-
"inference_server_auth": None,
309307
"model_name": "test/case",
310308
"model_info": None,
311309
"system_prompt": ref_system_prompt,
@@ -485,3 +483,41 @@ def test_model_config_property_with_different_model_configs(self):
485483
for k, v in ref_config.items():
486484
with self.subTest(model_config=model_config, model_property=k):
487485
self.assertEqual(results["config_general"]["model_config"][k], v)
486+
487+
def test_litellm_api_key_never_leaks_to_results(self):
488+
"""A LiteLLM api_key must never appear in the results dict or its JSON/parquet serialization."""
489+
from lighteval.logging.evaluation_tracker import EnhancedJSONEncoder
490+
from lighteval.models.endpoints.litellm_model import LiteLLMModelConfig
491+
492+
secret = "sk-super-secret-should-not-leak"
493+
model_config = LiteLLMModelConfig(model_name="test/case", api_key=secret)
494+
495+
with tempfile.TemporaryDirectory() as tmp_dir:
496+
evaluation_tracker = EvaluationTracker(output_dir=tmp_dir)
497+
evaluation_tracker.general_config_logger.log_model_info(model_config=model_config)
498+
499+
results = evaluation_tracker.results
500+
501+
self.assertNotIn("api_key", results["config_general"]["model_config"])
502+
503+
serialized = json.dumps(results, cls=EnhancedJSONEncoder)
504+
self.assertNotIn(secret, serialized)
505+
506+
def test_tgi_inference_server_auth_never_leaks_to_results(self):
507+
"""A TGI inference_server_auth token must never appear in the results dict or its JSON/parquet serialization."""
508+
from lighteval.logging.evaluation_tracker import EnhancedJSONEncoder
509+
from lighteval.models.endpoints.tgi_model import TGIModelConfig
510+
511+
secret = "tgi-bearer-token-should-not-leak"
512+
model_config = TGIModelConfig(model_name="test/case", inference_server_auth=secret)
513+
514+
with tempfile.TemporaryDirectory() as tmp_dir:
515+
evaluation_tracker = EvaluationTracker(output_dir=tmp_dir)
516+
evaluation_tracker.general_config_logger.log_model_info(model_config=model_config)
517+
518+
results = evaluation_tracker.results
519+
520+
self.assertNotIn("inference_server_auth", results["config_general"]["model_config"])
521+
522+
serialized = json.dumps(results, cls=EnhancedJSONEncoder)
523+
self.assertNotIn(secret, serialized)

0 commit comments

Comments
 (0)