Skip to content

Commit f96350c

Browse files
author
Yauheniya
committed
test: add unit tests for base URL env var fallbacks in OpenAI, Anthropic, and Gemini clients
1 parent 499df0e commit f96350c

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""
2+
Unit tests for automatic base URL and API key env var fallbacks
3+
in OpenAI, Anthropic, and Gemini clients.
4+
"""
5+
6+
import os
7+
from unittest.mock import MagicMock, patch
8+
9+
import pytest
10+
11+
12+
# =============================================================================
13+
# OpenAI
14+
# =============================================================================
15+
16+
17+
class TestOpenAIClientBaseUrl:
18+
"""Tests for OPENAI_API_BASE env var fallback in OpenAIClient."""
19+
20+
def test_base_url_from_env(self):
21+
"""When base_url is not passed, OPENAI_API_BASE is used."""
22+
with patch("rlm.clients.openai.openai.OpenAI") as mock_openai, patch(
23+
"rlm.clients.openai.openai.AsyncOpenAI"
24+
), patch.dict(
25+
os.environ,
26+
{"OPENAI_API_BASE": "https://my-org.example.com/v1", "OPENAI_API_KEY": "test-key"},
27+
):
28+
from rlm.clients.openai import OpenAIClient
29+
30+
with patch("rlm.clients.openai.DEFAULT_OPENAI_API_BASE", "https://my-org.example.com/v1"):
31+
client = OpenAIClient(api_key="test-key")
32+
33+
_, kwargs = mock_openai.call_args
34+
assert kwargs.get("base_url") == "https://my-org.example.com/v1"
35+
36+
def test_explicit_base_url_overrides_env(self):
37+
"""An explicitly passed base_url takes precedence over OPENAI_API_BASE."""
38+
with patch("rlm.clients.openai.openai.OpenAI") as mock_openai, patch(
39+
"rlm.clients.openai.openai.AsyncOpenAI"
40+
), patch("rlm.clients.openai.DEFAULT_OPENAI_API_BASE", "https://env-base.example.com/v1"):
41+
from rlm.clients.openai import OpenAIClient
42+
43+
client = OpenAIClient(api_key="test-key", base_url="https://explicit.example.com/v1")
44+
45+
_, kwargs = mock_openai.call_args
46+
assert kwargs.get("base_url") == "https://explicit.example.com/v1"
47+
48+
def test_base_url_is_none_when_env_not_set(self):
49+
"""When OPENAI_API_BASE is not set and base_url not passed, base_url is None."""
50+
with patch("rlm.clients.openai.openai.OpenAI") as mock_openai, patch(
51+
"rlm.clients.openai.openai.AsyncOpenAI"
52+
), patch("rlm.clients.openai.DEFAULT_OPENAI_API_BASE", None):
53+
from rlm.clients.openai import OpenAIClient
54+
55+
client = OpenAIClient(api_key="test-key")
56+
57+
_, kwargs = mock_openai.call_args
58+
assert kwargs.get("base_url") is None
59+
60+
61+
# =============================================================================
62+
# Anthropic
63+
# =============================================================================
64+
65+
66+
class TestAnthropicClientEnvVars:
67+
"""Tests for ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL env var fallbacks."""
68+
69+
def test_api_key_from_env(self):
70+
"""When api_key is not passed, ANTHROPIC_API_KEY env var is used."""
71+
with patch("rlm.clients.anthropic.anthropic.Anthropic") as mock_anthropic, patch(
72+
"rlm.clients.anthropic.anthropic.AsyncAnthropic"
73+
), patch("rlm.clients.anthropic.DEFAULT_ANTHROPIC_API_KEY", "env-anthropic-key"), patch(
74+
"rlm.clients.anthropic.DEFAULT_ANTHROPIC_BASE_URL", None
75+
):
76+
from rlm.clients.anthropic import AnthropicClient
77+
78+
client = AnthropicClient()
79+
80+
_, kwargs = mock_anthropic.call_args
81+
assert kwargs.get("api_key") == "env-anthropic-key"
82+
83+
def test_raises_when_no_api_key(self):
84+
"""Raises ValueError when neither api_key arg nor ANTHROPIC_API_KEY env var is set."""
85+
with patch("rlm.clients.anthropic.DEFAULT_ANTHROPIC_API_KEY", None), patch(
86+
"rlm.clients.anthropic.DEFAULT_ANTHROPIC_BASE_URL", None
87+
):
88+
from rlm.clients.anthropic import AnthropicClient
89+
90+
with pytest.raises(ValueError, match="Anthropic API key is required"):
91+
AnthropicClient(api_key=None)
92+
93+
def test_base_url_from_env(self):
94+
"""When base_url is not passed, ANTHROPIC_BASE_URL env var is used."""
95+
with patch("rlm.clients.anthropic.anthropic.Anthropic") as mock_anthropic, patch(
96+
"rlm.clients.anthropic.anthropic.AsyncAnthropic"
97+
), patch(
98+
"rlm.clients.anthropic.DEFAULT_ANTHROPIC_API_KEY", "test-key"
99+
), patch(
100+
"rlm.clients.anthropic.DEFAULT_ANTHROPIC_BASE_URL", "https://my-org.example.com"
101+
):
102+
from rlm.clients.anthropic import AnthropicClient
103+
104+
client = AnthropicClient()
105+
106+
_, kwargs = mock_anthropic.call_args
107+
assert kwargs.get("base_url") == "https://my-org.example.com"
108+
109+
def test_explicit_base_url_overrides_env(self):
110+
"""An explicitly passed base_url takes precedence over ANTHROPIC_BASE_URL."""
111+
with patch("rlm.clients.anthropic.anthropic.Anthropic") as mock_anthropic, patch(
112+
"rlm.clients.anthropic.anthropic.AsyncAnthropic"
113+
), patch(
114+
"rlm.clients.anthropic.DEFAULT_ANTHROPIC_API_KEY", "test-key"
115+
), patch(
116+
"rlm.clients.anthropic.DEFAULT_ANTHROPIC_BASE_URL", "https://env-base.example.com"
117+
):
118+
from rlm.clients.anthropic import AnthropicClient
119+
120+
client = AnthropicClient(base_url="https://explicit.example.com")
121+
122+
_, kwargs = mock_anthropic.call_args
123+
assert kwargs.get("base_url") == "https://explicit.example.com"
124+
125+
def test_base_url_not_passed_when_env_not_set(self):
126+
"""When ANTHROPIC_BASE_URL is not set, base_url key is absent from client kwargs."""
127+
with patch("rlm.clients.anthropic.anthropic.Anthropic") as mock_anthropic, patch(
128+
"rlm.clients.anthropic.anthropic.AsyncAnthropic"
129+
), patch(
130+
"rlm.clients.anthropic.DEFAULT_ANTHROPIC_API_KEY", "test-key"
131+
), patch(
132+
"rlm.clients.anthropic.DEFAULT_ANTHROPIC_BASE_URL", None
133+
):
134+
from rlm.clients.anthropic import AnthropicClient
135+
136+
client = AnthropicClient()
137+
138+
_, kwargs = mock_anthropic.call_args
139+
assert "base_url" not in kwargs
140+
141+
142+
# =============================================================================
143+
# Gemini
144+
# =============================================================================
145+
146+
147+
class TestGeminiClientBaseUrl:
148+
"""Tests for GEMINI_API_BASE env var fallback in GeminiClient."""
149+
150+
def test_base_url_from_env(self):
151+
"""When base_url is not passed, GEMINI_API_BASE is used in HttpOptions."""
152+
with patch("rlm.clients.gemini.genai.Client"), patch(
153+
"rlm.clients.gemini.types.HttpOptions"
154+
) as mock_http_options, patch(
155+
"rlm.clients.gemini.DEFAULT_GEMINI_API_BASE", "https://my-org.example.com"
156+
):
157+
from rlm.clients.gemini import GeminiClient
158+
159+
client = GeminiClient(api_key="test-key")
160+
161+
_, kwargs = mock_http_options.call_args
162+
assert kwargs.get("base_url") == "https://my-org.example.com"
163+
164+
def test_explicit_base_url_overrides_env(self):
165+
"""An explicitly passed base_url takes precedence over GEMINI_API_BASE."""
166+
with patch("rlm.clients.gemini.genai.Client"), patch(
167+
"rlm.clients.gemini.types.HttpOptions"
168+
) as mock_http_options, patch(
169+
"rlm.clients.gemini.DEFAULT_GEMINI_API_BASE", "https://env-base.example.com"
170+
):
171+
from rlm.clients.gemini import GeminiClient
172+
173+
client = GeminiClient(api_key="test-key", base_url="https://explicit.example.com")
174+
175+
_, kwargs = mock_http_options.call_args
176+
assert kwargs.get("base_url") == "https://explicit.example.com"
177+
178+
def test_base_url_absent_when_env_not_set(self):
179+
"""When GEMINI_API_BASE is not set, base_url is not passed to HttpOptions."""
180+
with patch("rlm.clients.gemini.genai.Client"), patch(
181+
"rlm.clients.gemini.types.HttpOptions"
182+
) as mock_http_options, patch(
183+
"rlm.clients.gemini.DEFAULT_GEMINI_API_BASE", None
184+
):
185+
from rlm.clients.gemini import GeminiClient
186+
187+
client = GeminiClient(api_key="test-key")
188+
189+
_, kwargs = mock_http_options.call_args
190+
assert "base_url" not in kwargs

0 commit comments

Comments
 (0)