Skip to content

Commit 0d27e21

Browse files
authored
Merge pull request #72 from lfnovo/fix/config-dict-api-key-issue-68
fix: resolve config dict api_key not being unpacked in OpenAI-based providers
2 parents 5e36a1f + 9bb2bd1 commit 0d27e21

File tree

10 files changed

+173
-9
lines changed

10 files changed

+173
-9
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.17.1] - 2026-01-24
9+
10+
### Fixed
11+
12+
- **Config Dict API Key Not Unpacked** - Fixed providers ignoring `api_key` passed via config dict (#68)
13+
- Affected providers: OpenRouter, DeepSeek, xAI (LLM), Groq (STT)
14+
- These providers inherit from OpenAI-compatible parent classes and were checking for `api_key` before the config dict was unpacked
15+
- Now correctly extracts `api_key` and `base_url` from config dict before setting provider defaults
16+
- Example that now works:
17+
```python
18+
model = AIFactory.create_language(
19+
"openrouter",
20+
"anthropic/claude-3.5-sonnet",
21+
config={"api_key": "sk-or-v1-xxxxx"}
22+
)
23+
```
24+
825
## [2.17.0] - 2026-01-23
926

1027
### Added

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "esperanto"
3-
version = "2.17.0"
3+
version = "2.17.1"
44
description = "A light-weight, production-ready, unified interface for various AI model providers"
55
authors = [
66
{ name = "LUIS NOVO", email = "lfnovo@gmail.com" }

src/esperanto/providers/llm/deepseek.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ def provider(self) -> str:
2525
return "deepseek"
2626

2727
def __post_init__(self):
28+
# Extract api_key and base_url from config dict first (before parent sets OpenAI defaults)
29+
if hasattr(self, "config") and self.config:
30+
if "api_key" in self.config:
31+
self.api_key = self.config["api_key"]
32+
if "base_url" in self.config:
33+
self.base_url = self.config["base_url"]
34+
2835
# Initialize DeepSeek-specific configuration
2936
self.base_url = self.base_url or os.getenv(
3037
"DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1"
@@ -37,7 +44,7 @@ def __post_init__(self):
3744
"DeepSeek API key not found. Set the DEEPSEEK_API_KEY environment variable."
3845
)
3946

40-
# Call parent's post_init to set up normalized response handling
47+
# Call parent's post_init (won't overwrite since values are already set)
4148
super().__post_init__()
4249

4350
# DeepSeek supports JSON mode like OpenAI (handled by parent)

src/esperanto/providers/llm/openrouter.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ class OpenRouterLanguageModel(OpenAILanguageModel):
3737
api_key: Optional[str] = None # Changed type hint
3838

3939
def __post_init__(self):
40+
# Extract api_key and base_url from config dict first (before parent sets OpenAI defaults)
41+
if hasattr(self, "config") and self.config:
42+
if "api_key" in self.config:
43+
self.api_key = self.config["api_key"]
44+
if "base_url" in self.config:
45+
self.base_url = self.config["base_url"]
46+
4047
# Initialize OpenRouter-specific configuration
4148
self.base_url = self.base_url or os.getenv(
4249
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"
@@ -48,7 +55,7 @@ def __post_init__(self):
4855
"OpenRouter API key not found. Set the OPENROUTER_API_KEY environment variable."
4956
)
5057

51-
# Call parent's post_init to set up HTTP clients
58+
# Call parent's post_init (won't overwrite since values are already set)
5259
super().__post_init__()
5360

5461
def _get_headers(self) -> Dict[str, str]:

src/esperanto/providers/llm/xai.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ class XAILanguageModel(OpenAILanguageModel):
2020
api_key: Optional[str] = None # Changed type hint
2121

2222
def __post_init__(self):
23+
# Extract api_key and base_url from config dict first (before parent sets OpenAI defaults)
24+
if hasattr(self, "config") and self.config:
25+
if "api_key" in self.config:
26+
self.api_key = self.config["api_key"]
27+
if "base_url" in self.config:
28+
self.base_url = self.config["base_url"]
29+
2330
# Initialize XAI-specific configuration
2431
self.base_url = self.base_url or os.getenv(
2532
"XAI_BASE_URL", "https://api.x.ai/v1"
@@ -31,7 +38,7 @@ def __post_init__(self):
3138
"XAI API key not found. Set the XAI_API_KEY environment variable."
3239
)
3340

34-
# Call parent's post_init to set up HTTP clients
41+
# Call parent's post_init (won't overwrite since values are already set)
3542
super().__post_init__()
3643

3744

src/esperanto/providers/stt/groq.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,22 @@ class GroqSpeechToTextModel(OpenAISpeechToTextModel):
1414

1515
def __post_init__(self):
1616
"""Initialize HTTP clients with Groq configuration."""
17-
# Set Groq-specific API key and base URL before calling parent
17+
# Extract api_key and base_url from config dict first (before parent sets OpenAI defaults)
18+
if hasattr(self, "config") and self.config:
19+
if "api_key" in self.config:
20+
self.api_key = self.config["api_key"]
21+
if "base_url" in self.config:
22+
self.base_url = self.config["base_url"]
23+
24+
# Set Groq-specific API key and base URL
1825
self.api_key = self.api_key or os.getenv("GROQ_API_KEY")
1926
if not self.api_key:
2027
raise ValueError("Groq API key not found")
21-
28+
2229
# Set Groq's OpenAI-compatible base URL
2330
self.base_url = self.base_url or "https://api.groq.com/openai/v1"
24-
25-
# Call parent's post_init which will initialize HTTP clients
31+
32+
# Call parent's post_init (won't overwrite since values are already set)
2633
super().__post_init__()
2734

2835
def _get_default_model(self) -> str:

tests/providers/llm/test_deepseek_provider.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,34 @@ def test_to_langchain():
5252
assert lc is not None
5353

5454

55+
def test_initialization_with_api_key_in_config():
56+
"""Test that api_key can be passed via config dict (GitHub issue #68)."""
57+
model = DeepSeekLanguageModel(config={"api_key": "config-test-key"})
58+
assert model.api_key == "config-test-key"
59+
assert model.base_url == "https://api.deepseek.com/v1"
60+
61+
62+
def test_initialization_with_base_url_in_config():
63+
"""Test that base_url can be passed via config dict."""
64+
model = DeepSeekLanguageModel(
65+
api_key="test-key",
66+
config={"base_url": "https://custom.deepseek.com/v1"}
67+
)
68+
assert model.base_url == "https://custom.deepseek.com/v1"
69+
70+
71+
def test_initialization_with_api_key_and_base_url_in_config():
72+
"""Test that both api_key and base_url can be passed via config dict."""
73+
model = DeepSeekLanguageModel(
74+
config={
75+
"api_key": "config-test-key",
76+
"base_url": "https://custom.deepseek.com/v1"
77+
}
78+
)
79+
assert model.api_key == "config-test-key"
80+
assert model.base_url == "https://custom.deepseek.com/v1"
81+
82+
5583
# =============================================================================
5684
# Tool Calling Tests
5785
# =============================================================================

tests/providers/llm/test_openrouter_provider.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,34 @@ def test_custom_base_url():
3939
assert model.base_url == "https://custom.openrouter.ai/v1"
4040

4141

42+
def test_initialization_with_api_key_in_config():
43+
"""Test that api_key can be passed via config dict (GitHub issue #68)."""
44+
model = OpenRouterLanguageModel(config={"api_key": "config-test-key"})
45+
assert model.api_key == "config-test-key"
46+
assert model.base_url == "https://openrouter.ai/api/v1"
47+
48+
49+
def test_initialization_with_base_url_in_config():
50+
"""Test that base_url can be passed via config dict."""
51+
model = OpenRouterLanguageModel(
52+
api_key="test-key",
53+
config={"base_url": "https://custom.openrouter.ai/v1"}
54+
)
55+
assert model.base_url == "https://custom.openrouter.ai/v1"
56+
57+
58+
def test_initialization_with_api_key_and_base_url_in_config():
59+
"""Test that both api_key and base_url can be passed via config dict."""
60+
model = OpenRouterLanguageModel(
61+
config={
62+
"api_key": "config-test-key",
63+
"base_url": "https://custom.openrouter.ai/v1"
64+
}
65+
)
66+
assert model.api_key == "config-test-key"
67+
assert model.base_url == "https://custom.openrouter.ai/v1"
68+
69+
4270
# =============================================================================
4371
# Tool Calling Tests
4472
# =============================================================================

tests/providers/llm/test_xai_provider.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ def test_custom_base_url():
3838
assert model.base_url == "https://custom.x.ai/v1"
3939

4040

41+
def test_initialization_with_api_key_in_config():
42+
"""Test that api_key can be passed via config dict (GitHub issue #68)."""
43+
model = XAILanguageModel(config={"api_key": "config-test-key"})
44+
assert model.api_key == "config-test-key"
45+
assert model.base_url == "https://api.x.ai/v1"
46+
47+
48+
def test_initialization_with_base_url_in_config():
49+
"""Test that base_url can be passed via config dict."""
50+
model = XAILanguageModel(
51+
api_key="test-key",
52+
config={"base_url": "https://custom.x.ai/v1"}
53+
)
54+
assert model.base_url == "https://custom.x.ai/v1"
55+
56+
57+
def test_initialization_with_api_key_and_base_url_in_config():
58+
"""Test that both api_key and base_url can be passed via config dict."""
59+
model = XAILanguageModel(
60+
config={
61+
"api_key": "config-test-key",
62+
"base_url": "https://custom.x.ai/v1"
63+
}
64+
)
65+
assert model.api_key == "config-test-key"
66+
assert model.base_url == "https://custom.x.ai/v1"
67+
68+
4169
# =============================================================================
4270
# Tool Calling Tests
4371
# =============================================================================

tests/providers/stt/test_groq.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,48 @@ def test_factory_creates_groq_stt():
2323
"""Test that AIFactory creates Groq STT model."""
2424
from unittest.mock import patch
2525
import os
26-
26+
2727
# Mock the environment variable to provide an API key
2828
with patch.dict(os.environ, {'GROQ_API_KEY': 'test-key'}):
2929
model = AIFactory.create_speech_to_text("groq")
3030
assert isinstance(model, GroqSpeechToTextModel)
3131

3232

33+
def test_initialization_with_api_key():
34+
"""Test initialization with api_key parameter."""
35+
model = GroqSpeechToTextModel(api_key="test-key")
36+
assert model.api_key == "test-key"
37+
assert model.base_url == "https://api.groq.com/openai/v1"
38+
39+
40+
def test_initialization_with_api_key_in_config():
41+
"""Test that api_key can be passed via config dict (GitHub issue #68)."""
42+
model = GroqSpeechToTextModel(config={"api_key": "config-test-key"})
43+
assert model.api_key == "config-test-key"
44+
assert model.base_url == "https://api.groq.com/openai/v1"
45+
46+
47+
def test_initialization_with_base_url_in_config():
48+
"""Test that base_url can be passed via config dict."""
49+
model = GroqSpeechToTextModel(
50+
api_key="test-key",
51+
config={"base_url": "https://custom.groq.com/v1"}
52+
)
53+
assert model.base_url == "https://custom.groq.com/v1"
54+
55+
56+
def test_initialization_with_api_key_and_base_url_in_config():
57+
"""Test that both api_key and base_url can be passed via config dict."""
58+
model = GroqSpeechToTextModel(
59+
config={
60+
"api_key": "config-test-key",
61+
"base_url": "https://custom.groq.com/v1"
62+
}
63+
)
64+
assert model.api_key == "config-test-key"
65+
assert model.base_url == "https://custom.groq.com/v1"
66+
67+
3368
def test_groq_transcribe(audio_file):
3469
"""Test Groq transcribe method with httpx mocking."""
3570
from unittest.mock import Mock

0 commit comments

Comments
 (0)