Skip to content

Commit 9a90a91

Browse files
authored
fix: redact provider credentials from repr (#285)
1 parent 7d3e2ba commit 9a90a91

11 files changed

Lines changed: 93 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
`anthropic` provider at MiniMax's Anthropic-protocol API via `base_url`, with
2727
the regional (international / mainland China) URLs. (#278)
2828

29+
### Fixed
30+
31+
- **Provider credentials are redacted from object representations.** API keys
32+
and configuration dictionaries no longer appear in dataclass `repr` output,
33+
preventing failed requests and test tracebacks from leaking credentials.
34+
2935
## [2.26.0] - 2026-07-29
3036

3137
### Added

src/esperanto/providers/embedding/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ class EmbeddingModel(HttpConnectionMixin, ABC):
2323
# breaks otherwise-correct user code (ARCHITECTURE.md "Hot-Swap-First Defaults").
2424
MAX_BATCH_SIZE: ClassVar[int] = 0
2525

26-
api_key: Optional[str] = None
26+
api_key: Optional[str] = field(default=None, repr=False)
2727
base_url: Optional[str] = None
2828
model_name: Optional[str] = None
2929
organization: Optional[str] = None
30-
config: Optional[Dict[str, Any]] = None
31-
_config: Dict[str, Any] = field(default_factory=dict)
30+
config: Optional[Dict[str, Any]] = field(default=None, repr=False)
31+
_config: Dict[str, Any] = field(default_factory=dict, repr=False)
3232

3333
def __post_init__(self):
3434
"""Initialize configuration after dataclass initialization."""

src/esperanto/providers/embedding/openrouter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""OpenRouter embedding model implementation."""
22

33
import os
4-
from dataclasses import dataclass
4+
from dataclasses import dataclass, field
55
from typing import ClassVar, Dict, List, Optional
66

77
from esperanto.common_types import Model
@@ -16,7 +16,7 @@ class OpenRouterEmbeddingModel(OpenAIEmbeddingModel):
1616
MAX_BATCH_SIZE: ClassVar[int] = 96
1717

1818
base_url: Optional[str] = None
19-
api_key: Optional[str] = None
19+
api_key: Optional[str] = field(default=None, repr=False)
2020

2121
def __post_init__(self):
2222
"""Initialize OpenRouter-specific configuration."""

src/esperanto/providers/llm/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
class LanguageModel(HttpConnectionMixin, ABC):
1414
"""Base class for all language models."""
1515

16-
api_key: Optional[str] = None
16+
api_key: Optional[str] = field(default=None, repr=False)
1717
base_url: Optional[str] = None
1818
model_name: Optional[str] = None
1919
max_tokens: int = 850
@@ -22,12 +22,12 @@ class LanguageModel(HttpConnectionMixin, ABC):
2222
top_p: float = 0.9
2323
structured: Optional[Dict[str, Any]] = None
2424
organization: Optional[str] = None
25-
config: Optional[Dict[str, Any]] = None
25+
config: Optional[Dict[str, Any]] = field(default=None, repr=False)
2626
# Tool-related fields
2727
tools: Optional[List[Tool]] = None
2828
tool_choice: Optional[Union[str, Dict[str, Any]]] = None
2929
parallel_tool_calls: Optional[bool] = None
30-
_config: Dict[str, Any] = field(default_factory=dict)
30+
_config: Dict[str, Any] = field(default_factory=dict, repr=False)
3131

3232
@property
3333
def models(self) -> List[Model]:

src/esperanto/providers/llm/openai_compatible.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""OpenAI-compatible language model implementation."""
22

3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
44
from typing import (
55
TYPE_CHECKING,
66
Any,
@@ -39,7 +39,7 @@ class OpenAICompatibleLanguageModel(ProfileAwareMixin, OpenAILanguageModel):
3939
"""OpenAI-compatible language model implementation for custom endpoints."""
4040

4141
base_url: Optional[str] = None
42-
api_key: Optional[str] = None
42+
api_key: Optional[str] = field(default=None, repr=False)
4343

4444
def __post_init__(self):
4545
"""Initialize OpenAI-compatible configuration."""

src/esperanto/providers/llm/openrouter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""OpenRouter language model implementation."""
22

33
import os
4-
from dataclasses import dataclass
4+
from dataclasses import dataclass, field
55
from typing import (
66
TYPE_CHECKING,
77
Any,
@@ -39,7 +39,7 @@ class OpenRouterLanguageModel(OpenAILanguageModel):
3939
"""OpenRouter language model implementation using OpenAI-compatible API."""
4040

4141
base_url: Optional[str] = None # Changed type hint
42-
api_key: Optional[str] = None # Changed type hint
42+
api_key: Optional[str] = field(default=None, repr=False)
4343

4444
def __post_init__(self):
4545
# Extract api_key and base_url from config dict first (before parent sets OpenAI defaults)

src/esperanto/providers/llm/perplexity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class PerplexityLanguageModel(LanguageModel):
4848
"""Perplexity AI language model implementation using httpx."""
4949

5050
base_url: Optional[str] = None
51-
api_key: Optional[str] = None
51+
api_key: Optional[str] = field(default=None, repr=False)
5252
search_domain_filter: Optional[List[str]] = field(default=None)
5353
return_images: Optional[bool] = field(default=None)
5454
return_related_questions: Optional[bool] = field(default=None)

src/esperanto/providers/reranker/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
class RerankerModel(HttpConnectionMixin, ABC):
1515
"""Base class for all reranker providers."""
1616

17-
api_key: Optional[str] = None
17+
api_key: Optional[str] = field(default=None, repr=False)
1818
base_url: Optional[str] = None
1919
model_name: Optional[str] = None
20-
config: Optional[Dict[str, Any]] = None
21-
_config: Dict[str, Any] = field(default_factory=dict)
20+
config: Optional[Dict[str, Any]] = field(default=None, repr=False)
21+
_config: Dict[str, Any] = field(default_factory=dict, repr=False)
2222

2323
def __post_init__(self):
2424
"""Initialize configuration after dataclass initialization."""
@@ -221,4 +221,4 @@ def _clean_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
221221
Returns:
222222
Cleaned configuration dictionary.
223223
"""
224-
return {k: v for k, v in config.items() if v is not None}
224+
return {k: v for k, v in config.items() if v is not None}

src/esperanto/providers/stt/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@ class SpeechToTextModel(HttpConnectionMixin, ABC):
147147
"""
148148

149149
model_name: Optional[str] = None
150-
api_key: Optional[str] = None
150+
api_key: Optional[str] = field(default=None, repr=False)
151151
base_url: Optional[str] = None
152-
config: Optional[Dict[str, Any]] = None
152+
config: Optional[Dict[str, Any]] = field(default=None, repr=False)
153153
timeout: Optional[float] = None
154154
_config: Dict[str, Any] = field(init=False, repr=False)
155155

src/esperanto/providers/tts/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ class TextToSpeechModel(HttpConnectionMixin, ABC):
2424
"""
2525

2626
model_name: Optional[str] = None
27-
api_key: Optional[str] = None
27+
api_key: Optional[str] = field(default=None, repr=False)
2828
base_url: Optional[str] = None
29-
config: Optional[Dict[str, Any]] = None
29+
config: Optional[Dict[str, Any]] = field(default=None, repr=False)
3030
timeout: Optional[float] = None
3131
_config: Dict[str, Any] = field(init=False, repr=False)
3232

0 commit comments

Comments
 (0)