1use std::future::Future;
52use std::sync::Arc;
53
54use agent_sdk_foundation::llm::{ChatOutcome, ChatRequest, ThinkingConfig};
55use anyhow::Result;
56use async_trait::async_trait;
57use futures::StreamExt;
58use tokio::sync::Mutex;
59
60use crate::model_capabilities::ModelCapabilities;
61use crate::provider::{LlmProvider, StructuredOutputSupport};
62use crate::streaming::{StreamBox, StreamDelta};
63
64pub struct RefreshingProvider<P, F> {
77 inner: Arc<Mutex<P>>,
78 refresh: Arc<F>,
79 template: P,
84 model: String,
85 provider: &'static str,
86 thinking: Option<ThinkingConfig>,
87}
88
89impl<P: Clone, F> Clone for RefreshingProvider<P, F> {
90 fn clone(&self) -> Self {
91 Self {
92 inner: Arc::clone(&self.inner),
93 refresh: Arc::clone(&self.refresh),
94 template: self.template.clone(),
95 model: self.model.clone(),
96 provider: self.provider,
97 thinking: self.thinking.clone(),
98 }
99 }
100}
101
102impl<P, F, Fut> RefreshingProvider<P, F>
103where
104 P: LlmProvider + Clone + 'static,
105 F: Fn() -> Fut + Send + Sync + 'static,
106 Fut: Future<Output = Result<P>> + Send + 'static,
107{
108 #[must_use]
116 pub fn new(inner: P, refresh: F) -> Self {
117 let model = inner.model().to_string();
118 let provider = inner.provider();
119 let thinking = inner.configured_thinking().cloned();
120 let template = inner.clone();
121 Self {
122 inner: Arc::new(Mutex::new(inner)),
123 refresh: Arc::new(refresh),
124 template,
125 model,
126 provider,
127 thinking,
128 }
129 }
130
131 async fn snapshot(&self) -> P {
132 self.inner.lock().await.clone()
133 }
134
135 async fn run_refresh(&self) -> Result<()> {
136 let fresh = (self.refresh)().await?;
137 *self.inner.lock().await = fresh;
138 Ok(())
139 }
140}
141
142#[must_use]
150pub fn is_unauthorized_error(message: &str) -> bool {
151 let lower = message.to_ascii_lowercase();
152 lower.contains(" 401")
153 || lower.contains("status=401")
154 || lower.contains("unauthorized")
155 || lower.contains("authentication")
156 || lower.contains("token_expired")
157 || lower.contains("invalid api key")
158 || lower.contains("invalid_api_key")
159}
160
161#[async_trait]
162impl<P, F, Fut> LlmProvider for RefreshingProvider<P, F>
163where
164 P: LlmProvider + Clone + 'static,
165 F: Fn() -> Fut + Send + Sync + 'static,
166 Fut: Future<Output = Result<P>> + Send + 'static,
167{
168 async fn chat(&self, request: ChatRequest) -> Result<ChatOutcome> {
169 let outcome = self.snapshot().await.chat(request.clone()).await?;
170 if let ChatOutcome::InvalidRequest(message) = &outcome
171 && is_unauthorized_error(message)
172 {
173 match self.run_refresh().await {
174 Ok(()) => return self.snapshot().await.chat(request).await,
175 Err(error) => {
176 log::warn!("RefreshingProvider refresh after 401 failed: {error:#}");
177 }
178 }
179 }
180 Ok(outcome)
181 }
182
183 fn chat_stream(&self, request: ChatRequest) -> StreamBox<'_> {
184 let this = self.clone();
185 Box::pin(async_stream::stream! {
186 let mut refreshed = false;
187 'attempts: loop {
188 let provider = this.snapshot().await;
189 let mut stream = provider.chat_stream(request.clone());
190 let mut saw_output = false;
191
192 while let Some(item) = stream.next().await {
193 match item {
194 Ok(StreamDelta::Error { message, kind })
195 if !saw_output
196 && !refreshed
197 && is_unauthorized_error(&message) =>
198 {
199 match this.run_refresh().await {
200 Ok(()) => {
201 refreshed = true;
202 continue 'attempts;
203 }
204 Err(error) => {
205 log::warn!(
206 "RefreshingProvider refresh after streaming 401 failed: {error:#}"
207 );
208 yield Ok(StreamDelta::Error { message, kind });
209 return;
210 }
211 }
212 }
213 Ok(delta) => {
214 if matches!(
215 delta,
216 StreamDelta::TextDelta { .. }
217 | StreamDelta::ThinkingDelta { .. }
218 | StreamDelta::ToolUseStart { .. }
219 | StreamDelta::ToolInputDelta { .. }
220 | StreamDelta::SignatureDelta { .. }
221 | StreamDelta::RedactedThinking { .. }
222 ) {
223 saw_output = true;
224 }
225 let done = matches!(delta, StreamDelta::Done { .. });
226 yield Ok(delta);
227 if done {
228 return;
229 }
230 }
231 Err(error)
232 if !saw_output
233 && !refreshed
234 && is_unauthorized_error(&error.to_string()) =>
235 {
236 match this.run_refresh().await {
237 Ok(()) => {
238 refreshed = true;
239 continue 'attempts;
240 }
241 Err(refresh_error) => {
242 log::warn!(
243 "RefreshingProvider refresh after stream failure failed: {refresh_error:#}"
244 );
245 yield Err(error);
246 return;
247 }
248 }
249 }
250 Err(error) => {
251 yield Err(error);
252 return;
253 }
254 }
255 }
256 return;
257 }
258 })
259 }
260
261 async fn list_models(&self) -> Result<Vec<crate::provider::ModelInfo>> {
264 self.snapshot().await.list_models().await
265 }
266
267 fn model(&self) -> &str {
268 &self.model
269 }
270
271 fn provider(&self) -> &'static str {
272 self.provider
273 }
274
275 fn configured_thinking(&self) -> Option<&ThinkingConfig> {
276 self.thinking.as_ref()
277 }
278
279 fn capabilities(&self) -> Option<&'static ModelCapabilities> {
286 self.template.capabilities()
287 }
288
289 fn validate_thinking_config(&self, thinking: Option<&ThinkingConfig>) -> Result<()> {
290 self.template.validate_thinking_config(thinking)
291 }
292
293 fn default_max_tokens(&self) -> u32 {
294 self.template.default_max_tokens()
295 }
296
297 fn structured_output_support(&self) -> StructuredOutputSupport {
298 self.template.structured_output_support()
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 use std::collections::VecDeque;
307 use std::sync::Mutex as StdMutex;
308 use std::sync::atomic::{AtomicUsize, Ordering};
309
310 use agent_sdk_foundation::llm::{ChatResponse, ContentBlock, StopReason, Usage};
311 use anyhow::Context;
312
313 use crate::streaming::StreamErrorKind;
314
315 #[derive(Clone)]
316 enum MockStreamItem {
317 Ok(StreamDelta),
318 Err(String),
319 }
320
321 #[derive(Clone)]
322 struct MockProvider {
323 model: String,
324 provider_name: &'static str,
325 outcomes: Arc<StdMutex<VecDeque<ChatOutcome>>>,
326 stream_batches: Arc<StdMutex<VecDeque<Vec<MockStreamItem>>>>,
327 chat_calls: Arc<AtomicUsize>,
328 stream_calls: Arc<AtomicUsize>,
329 }
330
331 impl MockProvider {
332 fn new() -> Self {
333 Self {
334 model: "mock-model".to_string(),
335 provider_name: "mock",
336 outcomes: Arc::new(StdMutex::new(VecDeque::new())),
337 stream_batches: Arc::new(StdMutex::new(VecDeque::new())),
338 chat_calls: Arc::new(AtomicUsize::new(0)),
339 stream_calls: Arc::new(AtomicUsize::new(0)),
340 }
341 }
342
343 fn queue_chat(&self, outcome: ChatOutcome) -> Result<()> {
344 self.outcomes
345 .lock()
346 .ok()
347 .context("outcomes lock poisoned")?
348 .push_back(outcome);
349 Ok(())
350 }
351
352 fn queue_stream(&self, batch: Vec<MockStreamItem>) -> Result<()> {
353 self.stream_batches
354 .lock()
355 .ok()
356 .context("stream_batches lock poisoned")?
357 .push_back(batch);
358 Ok(())
359 }
360
361 fn chat_call_count(&self) -> usize {
362 self.chat_calls.load(Ordering::SeqCst)
363 }
364
365 fn stream_call_count(&self) -> usize {
366 self.stream_calls.load(Ordering::SeqCst)
367 }
368 }
369
370 #[async_trait]
371 impl LlmProvider for MockProvider {
372 async fn chat(&self, _request: ChatRequest) -> Result<ChatOutcome> {
373 self.chat_calls.fetch_add(1, Ordering::SeqCst);
374 let mut queue = self
375 .outcomes
376 .lock()
377 .ok()
378 .context("outcomes lock poisoned")?;
379 queue.pop_front().context("MockProvider: no queued outcome")
380 }
381
382 async fn list_models(&self) -> Result<Vec<crate::provider::ModelInfo>> {
383 Ok(vec![crate::provider::ModelInfo {
384 id: "mock-discovered-model".to_owned(),
385 display_name: None,
386 context_window: None,
387 max_output_tokens: None,
388 }])
389 }
390
391 fn chat_stream(&self, _request: ChatRequest) -> StreamBox<'_> {
392 self.stream_calls.fetch_add(1, Ordering::SeqCst);
393 let batch: Vec<MockStreamItem> = self
394 .stream_batches
395 .lock()
396 .ok()
397 .and_then(|mut q| q.pop_front())
398 .unwrap_or_else(|| vec![MockStreamItem::Err("no queued stream batch".into())]);
399 Box::pin(async_stream::stream! {
400 for item in batch {
401 match item {
402 MockStreamItem::Ok(delta) => yield Ok(delta),
403 MockStreamItem::Err(msg) => {
404 yield Err(anyhow::anyhow!(msg));
405 return;
406 }
407 }
408 }
409 })
410 }
411
412 fn model(&self) -> &str {
413 &self.model
414 }
415
416 fn provider(&self) -> &'static str {
417 self.provider_name
418 }
419
420 fn default_max_tokens(&self) -> u32 {
423 32_000
424 }
425
426 fn structured_output_support(&self) -> StructuredOutputSupport {
427 StructuredOutputSupport::Native
428 }
429
430 fn validate_thinking_config(&self, thinking: Option<&ThinkingConfig>) -> Result<()> {
431 if thinking.is_some() {
432 Err(anyhow::anyhow!("mock rejects thinking"))
433 } else {
434 Ok(())
435 }
436 }
437 }
438
439 fn success_response() -> ChatResponse {
440 ChatResponse {
441 id: "msg_test".to_string(),
442 content: vec![ContentBlock::Text {
443 text: "ok".to_string(),
444 }],
445 model: "mock-model".to_string(),
446 stop_reason: Some(StopReason::EndTurn),
447 usage: Usage {
448 input_tokens: 1,
449 output_tokens: 1,
450 cached_input_tokens: 0,
451 cache_creation_input_tokens: 0,
452 },
453 }
454 }
455
456 fn empty_request() -> ChatRequest {
457 ChatRequest {
458 system: String::new(),
459 messages: Vec::new(),
460 tools: None,
461 max_tokens: 100,
462 max_tokens_explicit: false,
463 session_id: None,
464 cached_content: None,
465 thinking: None,
466 tool_choice: None,
467 response_format: None,
468 cache: None,
469 }
470 }
471
472 type BoxedFut = std::pin::Pin<Box<dyn Future<Output = Result<MockProvider>> + Send>>;
473 type RefreshFn = Box<dyn Fn() -> BoxedFut + Send + Sync + 'static>;
474 type Wrapped = RefreshingProvider<MockProvider, RefreshFn>;
475
476 fn wrap_success(mock: &MockProvider, counter: &Arc<AtomicUsize>) -> Wrapped {
477 let counter = Arc::clone(counter);
478 let template = mock.clone();
479 let cb: RefreshFn = Box::new(move || {
480 counter.fetch_add(1, Ordering::SeqCst);
481 let provider = template.clone();
482 Box::pin(async move { Ok(provider) })
483 });
484 RefreshingProvider::new(mock.clone(), cb)
485 }
486
487 fn wrap_failure(
488 mock: &MockProvider,
489 counter: &Arc<AtomicUsize>,
490 error: &'static str,
491 ) -> Wrapped {
492 let counter = Arc::clone(counter);
493 let cb: RefreshFn = Box::new(move || {
494 counter.fetch_add(1, Ordering::SeqCst);
495 Box::pin(async move { Err(anyhow::anyhow!(error)) })
496 });
497 RefreshingProvider::new(mock.clone(), cb)
498 }
499
500 #[test]
502 fn wrapper_delegates_capability_overrides_to_inner() {
503 let mock = MockProvider::new();
504 let refresh_count = Arc::new(AtomicUsize::new(0));
505 let wrapped = wrap_success(&mock, &refresh_count);
506
507 assert_eq!(wrapped.default_max_tokens(), 32_000);
510 assert_eq!(
511 wrapped.structured_output_support(),
512 StructuredOutputSupport::Native
513 );
514 assert!(
515 wrapped
516 .validate_thinking_config(Some(&ThinkingConfig::adaptive()))
517 .is_err()
518 );
519 assert!(wrapped.validate_thinking_config(None).is_ok());
520 }
521
522 #[tokio::test]
523 async fn wrapper_delegates_list_models_to_inner_snapshot() -> Result<()> {
524 let mock = MockProvider::new();
525 let refresh_count = Arc::new(AtomicUsize::new(0));
526 let wrapped = wrap_success(&mock, &refresh_count);
527
528 let models = wrapped.list_models().await?;
531 assert_eq!(models.len(), 1);
532 assert_eq!(models[0].id, "mock-discovered-model");
533 Ok(())
534 }
535
536 #[test]
538 fn is_unauthorized_error_matches_expected_strings() {
539 assert!(is_unauthorized_error("HTTP 401"));
540 assert!(is_unauthorized_error("status=401 Unauthorized"));
541 assert!(is_unauthorized_error("Invalid API key"));
542 assert!(is_unauthorized_error("invalid_api_key"));
543 assert!(is_unauthorized_error("token_expired"));
544 assert!(is_unauthorized_error("Authentication failed"));
545 assert!(is_unauthorized_error("UNAUTHORIZED"));
546
547 assert!(!is_unauthorized_error("rate limited"));
548 assert!(!is_unauthorized_error("network error"));
549 assert!(!is_unauthorized_error(""));
550 assert!(!is_unauthorized_error("internal server error"));
551 }
552
553 #[tokio::test]
555 async fn chat_successful_pass_through_does_not_refresh() -> Result<()> {
556 let mock = MockProvider::new();
557 mock.queue_chat(ChatOutcome::Success(success_response()))?;
558
559 let refresh_count = Arc::new(AtomicUsize::new(0));
560 let wrapped = wrap_success(&mock, &refresh_count);
561
562 let outcome = wrapped.chat(empty_request()).await?;
563 assert!(matches!(outcome, ChatOutcome::Success(_)));
564 assert_eq!(refresh_count.load(Ordering::SeqCst), 0);
565 assert_eq!(mock.chat_call_count(), 1);
566 Ok(())
567 }
568
569 #[tokio::test]
571 async fn chat_401_triggers_refresh_and_retries() -> Result<()> {
572 let mock = MockProvider::new();
573 mock.queue_chat(ChatOutcome::InvalidRequest("401 Unauthorized".into()))?;
574 mock.queue_chat(ChatOutcome::Success(success_response()))?;
575
576 let refresh_count = Arc::new(AtomicUsize::new(0));
577 let wrapped = wrap_success(&mock, &refresh_count);
578
579 let outcome = wrapped.chat(empty_request()).await?;
580 assert!(matches!(outcome, ChatOutcome::Success(_)));
581 assert_eq!(refresh_count.load(Ordering::SeqCst), 1);
582 assert_eq!(mock.chat_call_count(), 2);
583 Ok(())
584 }
585
586 #[tokio::test]
588 async fn chat_surfaces_original_401_when_refresh_fails() -> Result<()> {
589 let mock = MockProvider::new();
590 mock.queue_chat(ChatOutcome::InvalidRequest(
591 "status=401 Unauthorized".into(),
592 ))?;
593
594 let refresh_count = Arc::new(AtomicUsize::new(0));
595 let wrapped = wrap_failure(&mock, &refresh_count, "refresh callback failed");
596
597 let outcome = wrapped.chat(empty_request()).await?;
598 match outcome {
599 ChatOutcome::InvalidRequest(msg) => assert!(
600 msg.contains("401"),
601 "expected original 401 message, got {msg}"
602 ),
603 other => panic!("expected InvalidRequest, got {other:?}"),
604 }
605 assert_eq!(refresh_count.load(Ordering::SeqCst), 1);
606 assert_eq!(mock.chat_call_count(), 1);
607 Ok(())
608 }
609
610 async fn drain(mut stream: StreamBox<'_>) -> Vec<Result<StreamDelta>> {
611 let mut out = Vec::new();
612 while let Some(item) = stream.next().await {
613 out.push(item);
614 }
615 out
616 }
617
618 #[tokio::test]
620 async fn chat_stream_successful_pass_through() -> Result<()> {
621 let mock = MockProvider::new();
622 mock.queue_stream(vec![
623 MockStreamItem::Ok(StreamDelta::TextDelta {
624 delta: "hi".into(),
625 block_index: 0,
626 }),
627 MockStreamItem::Ok(StreamDelta::Done {
628 stop_reason: Some(StopReason::EndTurn),
629 }),
630 ])?;
631
632 let refresh_count = Arc::new(AtomicUsize::new(0));
633 let wrapped = wrap_success(&mock, &refresh_count);
634
635 let deltas = drain(wrapped.chat_stream(empty_request())).await;
636 assert_eq!(deltas.len(), 2);
637 assert!(matches!(
638 deltas[0].as_ref().ok(),
639 Some(StreamDelta::TextDelta { delta, .. }) if delta == "hi"
640 ));
641 assert!(matches!(
642 deltas[1].as_ref().ok(),
643 Some(StreamDelta::Done { .. })
644 ));
645 assert_eq!(refresh_count.load(Ordering::SeqCst), 0);
646 assert_eq!(mock.stream_call_count(), 1);
647 Ok(())
648 }
649
650 #[tokio::test]
652 async fn chat_stream_401_before_output_retries() -> Result<()> {
653 let mock = MockProvider::new();
654 mock.queue_stream(vec![MockStreamItem::Ok(StreamDelta::Error {
655 message: "status=401 Unauthorized".into(),
656 kind: StreamErrorKind::InvalidRequest,
657 })])?;
658 mock.queue_stream(vec![
659 MockStreamItem::Ok(StreamDelta::TextDelta {
660 delta: "retried".into(),
661 block_index: 0,
662 }),
663 MockStreamItem::Ok(StreamDelta::Done {
664 stop_reason: Some(StopReason::EndTurn),
665 }),
666 ])?;
667
668 let refresh_count = Arc::new(AtomicUsize::new(0));
669 let wrapped = wrap_success(&mock, &refresh_count);
670
671 let deltas = drain(wrapped.chat_stream(empty_request())).await;
672 assert_eq!(deltas.len(), 2);
674 assert!(matches!(
675 deltas[0].as_ref().ok(),
676 Some(StreamDelta::TextDelta { delta, .. }) if delta == "retried"
677 ));
678 assert!(matches!(
679 deltas[1].as_ref().ok(),
680 Some(StreamDelta::Done { .. })
681 ));
682 assert_eq!(refresh_count.load(Ordering::SeqCst), 1);
683 assert_eq!(mock.stream_call_count(), 2);
684 Ok(())
685 }
686
687 #[tokio::test]
689 async fn chat_stream_401_after_output_does_not_retry() -> Result<()> {
690 let mock = MockProvider::new();
691 mock.queue_stream(vec![
692 MockStreamItem::Ok(StreamDelta::TextDelta {
693 delta: "partial".into(),
694 block_index: 0,
695 }),
696 MockStreamItem::Ok(StreamDelta::Error {
697 message: "401 Unauthorized".into(),
698 kind: StreamErrorKind::InvalidRequest,
699 }),
700 ])?;
701
702 let refresh_count = Arc::new(AtomicUsize::new(0));
703 let wrapped = wrap_success(&mock, &refresh_count);
704
705 let deltas = drain(wrapped.chat_stream(empty_request())).await;
706 assert_eq!(deltas.len(), 2);
707 assert!(matches!(
708 deltas[0].as_ref().ok(),
709 Some(StreamDelta::TextDelta { delta, .. }) if delta == "partial"
710 ));
711 assert!(matches!(
712 deltas[1].as_ref().ok(),
713 Some(StreamDelta::Error { message, .. }) if message.contains("401")
714 ));
715 assert_eq!(refresh_count.load(Ordering::SeqCst), 0);
716 assert_eq!(mock.stream_call_count(), 1);
717 Ok(())
718 }
719
720 #[tokio::test]
722 async fn chat_stream_only_one_retry_per_call() -> Result<()> {
723 let mock = MockProvider::new();
724 mock.queue_stream(vec![MockStreamItem::Ok(StreamDelta::Error {
725 message: "status=401 Unauthorized".into(),
726 kind: StreamErrorKind::InvalidRequest,
727 })])?;
728 mock.queue_stream(vec![MockStreamItem::Ok(StreamDelta::Error {
729 message: "still 401 Unauthorized".into(),
730 kind: StreamErrorKind::InvalidRequest,
731 })])?;
732
733 let refresh_count = Arc::new(AtomicUsize::new(0));
734 let wrapped = wrap_success(&mock, &refresh_count);
735
736 let deltas = drain(wrapped.chat_stream(empty_request())).await;
737 assert_eq!(deltas.len(), 1);
738 assert!(matches!(
739 deltas[0].as_ref().ok(),
740 Some(StreamDelta::Error { message, .. }) if message == "still 401 Unauthorized"
741 ));
742 assert_eq!(refresh_count.load(Ordering::SeqCst), 1);
743 assert_eq!(mock.stream_call_count(), 2);
744 Ok(())
745 }
746
747 #[derive(Clone)]
753 struct ConcurrentMock {
754 model: String,
755 provider_name: &'static str,
756 total_calls: Arc<AtomicUsize>,
757 initial_barrier: Arc<tokio::sync::Barrier>,
758 }
759
760 type CMFut = std::pin::Pin<Box<dyn Future<Output = Result<ConcurrentMock>> + Send>>;
761 type CMRefresh = Box<dyn Fn() -> CMFut + Send + Sync + 'static>;
762
763 #[async_trait]
764 impl LlmProvider for ConcurrentMock {
765 async fn chat(&self, _request: ChatRequest) -> Result<ChatOutcome> {
766 let call_index = self.total_calls.fetch_add(1, Ordering::SeqCst);
767 if call_index < 2 {
768 self.initial_barrier.wait().await;
769 Ok(ChatOutcome::InvalidRequest("401 Unauthorized".into()))
770 } else {
771 Ok(ChatOutcome::Success(success_response()))
772 }
773 }
774
775 fn chat_stream(&self, _request: ChatRequest) -> StreamBox<'_> {
776 Box::pin(async_stream::stream! {
777 yield Err(anyhow::anyhow!("chat_stream not used in this test"));
778 })
779 }
780
781 fn model(&self) -> &str {
782 &self.model
783 }
784
785 fn provider(&self) -> &'static str {
786 self.provider_name
787 }
788 }
789
790 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
792 async fn chat_concurrent_callers_share_refresh() -> Result<()> {
793 let mock = ConcurrentMock {
794 model: "mock-model".to_string(),
795 provider_name: "mock",
796 total_calls: Arc::new(AtomicUsize::new(0)),
797 initial_barrier: Arc::new(tokio::sync::Barrier::new(2)),
798 };
799 let call_count = Arc::clone(&mock.total_calls);
800 let refresh_count = Arc::new(AtomicUsize::new(0));
801 let refresh_counter = Arc::clone(&refresh_count);
802 let template = mock.clone();
803
804 let cb: CMRefresh = Box::new(move || {
805 refresh_counter.fetch_add(1, Ordering::SeqCst);
806 let provider = template.clone();
807 Box::pin(async move { Ok(provider) })
808 });
809 let wrapped = RefreshingProvider::new(mock, cb);
810
811 let a = wrapped.clone();
812 let b = wrapped.clone();
813 let task_a = tokio::spawn(async move { a.chat(empty_request()).await });
814 let task_b = tokio::spawn(async move { b.chat(empty_request()).await });
815
816 let outcome_a = task_a.await.context("task_a join")??;
817 let outcome_b = task_b.await.context("task_b join")??;
818
819 assert!(matches!(outcome_a, ChatOutcome::Success(_)));
820 assert!(matches!(outcome_b, ChatOutcome::Success(_)));
821 assert_eq!(call_count.load(Ordering::SeqCst), 4);
822 let refreshes = refresh_count.load(Ordering::SeqCst);
823 assert!(
824 refreshes <= 2,
825 "expected at most 2 refresh calls (one per caller), got {refreshes}"
826 );
827 Ok(())
828 }
829}