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
216
217
218
219
220
//! Composable provider wrappers.
//!
//! Each wrapper implements [`ChatProvider`](crate::ChatProvider),
//! [`EmbeddingProvider`](crate::EmbeddingProvider), and
//! [`DecisionProvider`](crate::DecisionProvider) over an inner provider and
//! composes with the others. Recommended order, outermost first:
//!
//! ```rust,ignore
//! TracingProvider::new(TimeoutProvider::new(
//! FallbackProvider::new(
//! RetryingProvider::new(TimeoutProvider::new(primary, per_attempt), RetryPolicy::default()),
//! RetryingProvider::new(TimeoutProvider::new(backup, per_attempt), RetryPolicy::default()),
//! ),
//! deadline,
//! ))
//! ```
//!
//! - Tracing outermost, so one span covers retries and failover.
//! - A deadline on the whole call ([`TimeoutProvider`]) just inside
//! tracing: a [`request`](TimeoutPolicy::request) bound, which also
//! bounds stream setup unless [`stream_setup`](TimeoutPolicy::stream_setup)
//! sets a bound of its own. Retry and fallback hand a stream over only at
//! its first output, so stream setup here is the wait for that output
//! across every attempt and backoff, the deadline a server needs before
//! it sends a status. Not [`stream_total`](TimeoutPolicy::stream_total) in an
//! interactive stack: it would cut off long answers after they start.
//! Inside tracing, because a deadline outside it drops the traced call,
//! whose span then records a cancellation instead of a timeout.
//! - Fallback around retry, so each backend gets its own retries.
//! [`RetryingProvider`] waits out a provider's `Retry-After` hint up to
//! its [`max_retry_after`](RetryPolicy::max_retry_after) and gives up on
//! longer ones, so the rate limit reaches the fallback instead of an early
//! retry. The default, 120 s, suits batch work; an interactive server sets
//! a few seconds. Each `RetryingProvider` owns a [`RetryBudget`] that
//! bounds its retries across calls during an outage, so each side drains
//! its own; share one across both sides explicitly with
//! [`RetryPolicy::budget`].
//! - Timeout ([`TimeoutProvider`]) inside retry, so each attempt is bounded
//! and a timed-out attempt (including a stream that stalls before its
//! first output) is retried.
//! - Request transforms ([`MapRequestProvider`]) whose result the span
//! should show, such as redaction or sanitization, **outside** tracing:
//! `MapRequestProvider::new(TracingProvider::new(p), redact)`. A span
//! records the request `TracingProvider` is given, so inside it, the span
//! shows the caller's request, before the transform: captured content
//! would hold the text the transform removes.
//! - Model mapping ([`MapModelProvider`]) around the one provider whose
//! catalog it targets, typically a fallback side:
//! `FallbackProvider::new(primary, MapModelProvider::new(backup, map))`.
//! - Recording ([`RecordingProvider`]) outermost to log what the application
//! saw, or around a single provider to log every attempt.
//!
//! `TimeoutProvider` adds workload-specific bounds on top of the transport
//! defaults of the HTTP provider crates (a connect timeout, a whole-call
//! timeout for unary requests, and generous setup and idle bounds for
//! streams, all in `allama-http`). Both fail with
//! [`Error::Timeout`](crate::Error::Timeout) phrased `"<phase> exceeded
//! <limit>"`; whichever bound is shorter fires first. Tighten with a
//! `TimeoutProvider`; loosen or disable the transport bounds in the
//! provider's builder (`timeout`, `stream_timeouts`).
//!
//! # Constructing wrappers
//!
//! Every wrapper is built the same way: `new` takes the wrapped provider and
//! the configuration that is the wrapper's purpose, and optional behaviour
//! is a chained setter. So [`RetryingProvider::new(inner, policy)`](RetryingProvider::new)
//! and [`TimeoutProvider::new(inner, policy)`](TimeoutProvider::new) read
//! alike (`RetryPolicy::default()` is the retry default),
//! [`FallbackProvider::new(primary, fallback)`](FallbackProvider::new) takes
//! both sides, and a wrapper with nothing it must be told
//! ([`ObserveProvider`], `TracingProvider`) takes only the provider and
//! offers its hooks and options as setters. A setter that registers a
//! callback the wrapper runs as calls happen is named `on_*`
//! ([`ObserveProvider::on_outcome`], `TracingProvider::on_span`); one that
//! sets configuration is named `with_*` or after the setting.
//!
//! # Writing a wrapper
//!
//! A wrapper is transparent: it changes how calls run, not who answers them.
//! Write the operations (`chat`, `chat_stream`, `embed`, `decide`) and
//! forward every question about the provider with
//! [`forward_provider_identity!`], [`forward_chat_questions!`],
//! [`forward_embedding_questions!`] and [`forward_decision_questions!`],
//! naming the field that holds the wrapped provider. A question added to a
//! provider trait later is added to its macro in the same release, so a
//! wrapper that uses them forwards it by being recompiled. They forward:
//!
//! - [`provider_name`](crate::ProviderIdentity::provider_name) and
//! [`downcast_any`](crate::ProviderIdentity::downcast_any)
//! (`self.inner.downcast_any(id)`), so callers can still reach the
//! provider behind it with
//! [`downcast_ref`](crate::ProviderIdentityExt::downcast_ref);
//! - the capability answers (`chat_capability`, `embedding_capability`,
//! `decision_capability`);
//! - the limits ([`embedding_limits`](crate::EmbeddingProvider::embedding_limits),
//! [`decision_limits`](crate::DecisionProvider::decision_limits)) and
//! request validation (`validate_chat_request`,
//! `validate_chat_stream_request`, `validate_embedding_request`,
//! `validate_decision_request`), whose defaults are right for providers
//! but wrong for wrappers: a wrapper that does not forward them reports
//! the default `Capabilities` coverage instead of the adapter's dry run.
//!
//! A wrapper that adds warnings of its own builds on
//! [`chat::prepare`](crate::adapter::chat::prepare) and passes the inner response
//! through [`Prepared::finish`](crate::adapter::chat::Prepared::finish) (or
//! `finish_stream`): warnings the inner provider already emitted as warning
//! events are not emitted again, so `finish` is safe to call at every layer.
//! Give a warning that recurs with the request's shape a
//! [code](crate::Warning::with_code), so its event goes out at `WARN` once
//! rather than on every call. A response cache or replay layer belongs
//! outside such a wrapper: the warnings of a deserialized response are
//! unmarked, so a wrapper's `finish` emits them again on every hit.
//!
//! Where a wrapper's annotations go:
//!
//! - what it changed is a coded warning of the matching kind: a redaction
//! wrapper that rewrites the prompt adds an
//! [`Approximated`](crate::WarningKind::Approximated) warning with code
//! `approximated.redacted_content`;
//! - pure observations (timings, cache hits, what a lookup found) go to
//! `tracing` or the application's own channel;
//! - it never writes provider data: not under
//! [`ALLAMA_NAMESPACE`](crate::provider_data::ALLAMA_NAMESPACE), which
//! is reserved for conventions allama publishes, not under a provider's
//! namespace, whose state that provider reads back on replay, and not
//! on blocks. [`FallbackAttempts`] is the one reserved exception;
//! - an annotation that is not a degradation has no portable place on the
//! response yet;
//! - a wrapper that rewrites or annotates responses beyond adding warnings
//! is not transparent: it runs the identity contract
//! (`allama_conformance::assert_transparent_chat_wrapper`), not the data
//! path one.
//!
//! [`RetryingProvider`] runs each attempt in an attempt scope and commits
//! the attempt the caller gets, so the warnings of discarded attempts are
//! not logged. The scope is internal in this release: a third-party wrapper
//! that calls its inner provider more than once for one call (retries,
//! hedging) logs the warnings of every attempt. [`ObserveProvider`] hands
//! every warning to a callback, for metrics.
//!
//! The name and capability methods are required, so the compiler catches a
//! wrapper that forgets them; `allama_conformance::assert_wrapper` (and its
//! per-trait variants) checks all of them, and that the provider's
//! responses, stream events and errors pass through unchanged. Wrappers that combine
//! providers ([`FallbackProvider`]), map models ([`MapModelProvider`]), or
//! transform requests ([`MapRequestProvider`]) document how they answer.
//!
//! [`RetryingProvider`] and [`FallbackProvider`] report what they did
//! through [`call_scope`] rather than recording span fields, so
//! `TracingProvider` (feature `tracing`) puts it in the call's summary. A
//! third-party resilience wrapper (hedging, a circuit breaker) appears there
//! as the attempts made under it ([What is public](call_scope#what-is-public)).
//!
//! Middleware is a wrapper too. [`MapRequestProvider`] covers a synchronous
//! request rewrite; for an async request hook (a memory or retrieval
//! lookup) or for watching responses and streams, write a wrapper. The
//! [`middleware` example]
//! is the recipe: it awaits a lookup before each call,
//! observes responses and errors, observes streams through a boxed
//! [`ChatStream`](crate::ChatStream), forwards everything above, and runs
//! `assert_wrapper` in its tests. Every provider's stream is `'static`
//! ([`ChatProvider::Stream`](crate::ChatProvider::Stream)), so a wrapper
//! boxes the inner stream with no extra bound.
//!
pub use crate::;
pub use CapabilityOverrideProvider;
pub use ;
pub use MapModelProvider;
pub use MapRequestProvider;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use UnsupportedPolicyProvider;
/// `otel_genai_provider_name`: the provider name attempt events carry,
/// also without the `tracing` feature.
/// A custom error classifier, set with [`RetryPolicy::retry_if`] or
/// [`FallbackProvider::fallback_if`].
type Classifier = Arc;