1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#![cfg(feature = "_client")]
//! Environment-driven construction of [`AnyClient`].
//!
//! Environment variables are process-global, and tests within a single binary
//! share that process. If these scenarios were split across multiple `#[test]`
//! functions, the default parallel test runner would interleave their mutations
//! of the same four keys and produce flaky, order-dependent failures. To keep the
//! behavior deterministic, **all** environment manipulation lives in this single
//! test. The original values of every key are saved on entry and restored on exit
//! (even on panic, via a drop guard).
use rstructor::{AnyClient, ApiErrorKind, LLMClient, Provider, RStructorError};
/// The four provider API-key environment variables, in detection-precedence order.
const ENV_KEYS: [&str; 4] = [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"XAI_API_KEY",
"GEMINI_API_KEY",
];
/// Snapshot of the four keys captured at test start; restores them when dropped.
///
/// Using a drop guard guarantees the original environment is reinstated even if an
/// assertion panics partway through, so a failure here cannot poison other test
/// binaries or the developer's shell session.
struct EnvGuard {
saved: Vec<(&'static str, Option<String>)>,
}
impl EnvGuard {
fn capture() -> Self {
let saved = ENV_KEYS
.iter()
.map(|&key| (key, std::env::var(key).ok()))
.collect();
EnvGuard { saved }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, value) in &self.saved {
// SAFETY: single-threaded restore at end of the only env-mutating test.
unsafe {
match value {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
}
}
}
/// Set exactly the named keys (to a dummy value) and remove every other tracked key.
///
/// API keys are never validated at construction time (the clients only read the
/// variable's presence), so a placeholder string is sufficient to drive detection.
fn set_only(present: &[&str]) {
for &key in &ENV_KEYS {
// SAFETY: env mutation is confined to this single test (see module docs).
unsafe {
if present.contains(&key) {
std::env::set_var(key, "test-key-placeholder");
} else {
std::env::remove_var(key);
}
}
}
}
#[test]
fn anyclient_from_env_detection_precedence_and_per_provider() {
let _guard = EnvGuard::capture();
// --- from_env precedence: OpenAI > Anthropic > Grok > Gemini ---
// OpenAI + Anthropic both set -> OpenAI wins (highest precedence).
set_only(&["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]);
assert_eq!(
AnyClient::from_env().unwrap().provider(),
Provider::OpenAI,
"OpenAI must outrank Anthropic when both keys are present"
);
// All four set -> still OpenAI.
set_only(&ENV_KEYS);
assert_eq!(
AnyClient::from_env().unwrap().provider(),
Provider::OpenAI,
"OpenAI must win when every key is present"
);
// Remove OpenAI -> Anthropic wins over Grok and Gemini.
set_only(&["ANTHROPIC_API_KEY", "XAI_API_KEY", "GEMINI_API_KEY"]);
assert_eq!(
AnyClient::from_env().unwrap().provider(),
Provider::Anthropic,
"Anthropic must outrank Grok and Gemini once OpenAI is absent"
);
// Only Grok + Gemini -> Grok wins.
set_only(&["XAI_API_KEY", "GEMINI_API_KEY"]);
assert_eq!(
AnyClient::from_env().unwrap().provider(),
Provider::Grok,
"Grok must outrank Gemini"
);
// Only Grok (XAI) set -> Grok.
set_only(&["XAI_API_KEY"]);
assert_eq!(AnyClient::from_env().unwrap().provider(), Provider::Grok);
// Only Gemini set -> Gemini (lowest precedence, but the only one present).
set_only(&["GEMINI_API_KEY"]);
assert_eq!(AnyClient::from_env().unwrap().provider(), Provider::Gemini);
// Only Anthropic set -> Anthropic.
set_only(&["ANTHROPIC_API_KEY"]);
assert_eq!(
AnyClient::from_env().unwrap().provider(),
Provider::Anthropic
);
// Only OpenAI set -> OpenAI.
set_only(&["OPENAI_API_KEY"]);
assert_eq!(AnyClient::from_env().unwrap().provider(), Provider::OpenAI);
// --- from_env_for: deterministic per-provider construction ---
// With its key present each provider builds; once its key is removed it errors
// with AuthenticationFailed (precedence is irrelevant to from_env_for).
let cases = [
(Provider::OpenAI, "OPENAI_API_KEY"),
(Provider::Anthropic, "ANTHROPIC_API_KEY"),
(Provider::Grok, "XAI_API_KEY"),
(Provider::Gemini, "GEMINI_API_KEY"),
];
for (provider, key) in cases {
// Only this provider's key is present.
set_only(&[key]);
let client = AnyClient::from_env_for(provider)
.unwrap_or_else(|e| panic!("from_env_for({provider:?}) should succeed: {e}"));
assert_eq!(
client.provider(),
provider,
"from_env_for({provider:?}) must produce a client reporting that provider"
);
// Remove all keys -> from_env_for for this provider must fail with AuthenticationFailed.
// AnyClient does not implement Debug, so match instead of `expect_err`.
set_only(&[]);
match AnyClient::from_env_for(provider) {
Ok(_) => {
panic!("from_env_for({provider:?}) must fail when the provider's key is absent")
}
Err(err) => assert!(
matches!(
err.api_error_kind(),
Some(ApiErrorKind::AuthenticationFailed)
),
"from_env_for({provider:?}) without a key should be AuthenticationFailed, got {err:?}"
),
}
}
// --- from_env with NO key set -> AuthenticationFailed, provider label "AnyClient" ---
set_only(&[]);
// AnyClient does not implement Debug, so match instead of `expect_err`.
let err = match AnyClient::from_env() {
Ok(_) => panic!("from_env must fail when no provider key is set"),
Err(err) => err,
};
assert!(
matches!(
err.api_error_kind(),
Some(ApiErrorKind::AuthenticationFailed)
),
"no-key from_env should yield AuthenticationFailed, got {err:?}"
);
match &err {
RStructorError::ApiError { provider, kind } => {
assert_eq!(
provider, "AnyClient",
"no-key from_env must report the synthetic provider label \"AnyClient\""
);
assert!(matches!(kind, ApiErrorKind::AuthenticationFailed));
}
other => panic!("expected ApiError, got {other:?}"),
}
// --- provider() reporting via From<ConcreteClient> ---
// Build each concrete client (its key is present), convert with `.into()`, and
// verify the resulting AnyClient reports the matching provider.
set_only(&["OPENAI_API_KEY"]);
let openai: AnyClient = rstructor::OpenAIClient::from_env().unwrap().into();
assert_eq!(openai.provider(), Provider::OpenAI);
set_only(&["ANTHROPIC_API_KEY"]);
let anthropic: AnyClient = rstructor::AnthropicClient::from_env().unwrap().into();
assert_eq!(anthropic.provider(), Provider::Anthropic);
set_only(&["XAI_API_KEY"]);
let grok: AnyClient = rstructor::GrokClient::from_env().unwrap().into();
assert_eq!(grok.provider(), Provider::Grok);
set_only(&["GEMINI_API_KEY"]);
let gemini: AnyClient = rstructor::GeminiClient::from_env().unwrap().into();
assert_eq!(gemini.provider(), Provider::Gemini);
// `_guard` restores the original environment here.
}