1#[cfg(feature = "openai")]
8use crate::backend::OpenAiBackend;
9use crate::backend::{Backend, BackoffConfig, OllamaBackend};
10use crate::events::EventHandler;
11use crate::limits::PipelineLimits;
12#[allow(deprecated)]
13use crate::trace::TraceId;
14use reqwest::Client;
15use stack_ids::TraceCtx;
16use std::collections::HashMap;
17use std::sync::{
18 atomic::{AtomicBool, Ordering},
19 Arc,
20};
21use std::time::Duration;
22
23#[allow(deprecated)]
39pub struct ExecCtx {
40 pub client: Client,
42 pub base_url: String,
44 pub backend: Arc<dyn Backend>,
46 pub backoff: BackoffConfig,
48 pub vars: HashMap<String, String>,
50 pub cancellation: Option<Arc<AtomicBool>>,
52 pub event_handler: Option<Arc<dyn EventHandler>>,
54 #[deprecated(
62 note = "Use trace_ctx instead. Will be removed when all callers migrate to TraceCtx."
63 )]
64 pub trace_id: TraceId,
65 pub trace_ctx: TraceCtx,
71 pub limits: PipelineLimits,
73}
74
75#[allow(deprecated)]
76impl ExecCtx {
77 pub fn builder(base_url: impl Into<String>) -> ExecCtxBuilder {
79 ExecCtxBuilder {
80 client: None,
81 base_url: base_url.into(),
82 backend: None,
83 backoff: None,
84 vars: HashMap::new(),
85 cancellation: None,
86 event_handler: None,
87 timeout: None,
88 trace_id: None,
89 trace_ctx: None,
90 limits: None,
91 }
92 }
93
94 pub fn is_cancelled(&self) -> bool {
96 self.cancellation
97 .as_ref()
98 .is_some_and(|c| c.load(Ordering::Relaxed))
99 }
100
101 pub fn check_cancelled(&self) -> crate::error::Result<()> {
103 if self.is_cancelled() {
104 return Err(crate::PipelineError::Cancelled);
105 }
106 Ok(())
107 }
108
109 pub fn cancel_flag(&self) -> Option<&AtomicBool> {
111 self.cancellation.as_deref()
112 }
113}
114
115#[allow(deprecated)]
116impl std::fmt::Debug for ExecCtx {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("ExecCtx")
119 .field("base_url", &self.base_url)
120 .field("backend", &self.backend.name())
121 .field("backoff", &self.backoff)
122 .field("vars_count", &self.vars.len())
123 .field("has_cancellation", &self.cancellation.is_some())
124 .field("has_event_handler", &self.event_handler.is_some())
125 .field("trace_id", &self.trace_id)
126 .field("trace_ctx", &self.trace_ctx)
127 .field("limits", &self.limits)
128 .finish()
129 }
130}
131
132pub struct ExecCtxBuilder {
134 client: Option<Client>,
135 base_url: String,
136 backend: Option<Arc<dyn Backend>>,
137 backoff: Option<BackoffConfig>,
138 vars: HashMap<String, String>,
139 cancellation: Option<Arc<AtomicBool>>,
140 event_handler: Option<Arc<dyn EventHandler>>,
141 timeout: Option<Duration>,
142 trace_id: Option<TraceId>,
143 trace_ctx: Option<TraceCtx>,
144 limits: Option<PipelineLimits>,
145}
146
147#[allow(deprecated)]
148impl ExecCtxBuilder {
149 pub fn client(mut self, client: Client) -> Self {
151 self.client = Some(client);
152 self
153 }
154
155 pub fn backend(mut self, backend: Arc<dyn Backend>) -> Self {
157 self.backend = Some(backend);
158 self
159 }
160
161 #[cfg(feature = "openai")]
166 pub fn openai(mut self) -> Self {
167 self.backend = Some(Arc::new(OpenAiBackend::new()));
168 self
169 }
170
171 #[cfg(feature = "openai")]
176 pub fn openai_with_key(mut self, api_key: impl Into<String>) -> Self {
177 self.backend = Some(Arc::new(OpenAiBackend::new().with_api_key(api_key)));
178 self
179 }
180
181 pub fn backoff(mut self, config: BackoffConfig) -> Self {
183 self.backoff = Some(config);
184 self
185 }
186
187 pub fn vars(mut self, vars: HashMap<String, String>) -> Self {
189 self.vars = vars;
190 self
191 }
192
193 pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
195 self.vars.insert(key.into(), value.into());
196 self
197 }
198
199 pub fn cancellation(mut self, cancel: Option<Arc<AtomicBool>>) -> Self {
201 self.cancellation = cancel;
202 self
203 }
204
205 pub fn event_handler(mut self, handler: Arc<dyn EventHandler>) -> Self {
207 self.event_handler = Some(handler);
208 self
209 }
210
211 pub fn timeout(mut self, timeout: Duration) -> Self {
220 self.timeout = Some(timeout);
221 self
222 }
223
224 #[deprecated(
233 since = "0.6.0",
234 note = "Use with_trace_ctx() instead. This method will be removed in v1.0."
235 )]
236 pub fn with_trace_id(mut self, trace_id: TraceId) -> Self {
237 self.trace_id = Some(trace_id);
238 self
239 }
240
241 pub fn with_trace_ctx(mut self, trace_ctx: TraceCtx) -> Self {
246 self.trace_ctx = Some(trace_ctx);
247 self
248 }
249
250 pub fn with_limits(mut self, limits: PipelineLimits) -> Self {
252 self.limits = Some(limits);
253 self
254 }
255
256 pub fn build(self) -> ExecCtx {
269 let limits = self.limits.unwrap_or_default();
270 let client_timeout = self.timeout.unwrap_or(Duration::from_secs(300));
274 let client = self.client.unwrap_or_else(|| {
275 Client::builder()
276 .timeout(client_timeout)
277 .build()
278 .expect("Failed to build HTTP client")
279 });
280
281 let (trace_id, trace_ctx) = match (self.trace_ctx, self.trace_id) {
282 (Some(ctx), _) => {
284 let legacy = TraceId::from_trace_ctx(&ctx);
285 (legacy, ctx)
286 }
287 (None, Some(id)) => {
289 let ctx = id.to_trace_ctx();
290 (id, ctx)
291 }
292 (None, None) => {
294 let ctx = TraceCtx::generate();
295 let legacy = TraceId::from_trace_ctx(&ctx);
296 (legacy, ctx)
297 }
298 };
299
300 ExecCtx {
301 client,
302 base_url: normalize_base_url(&self.base_url),
303 backend: self.backend.unwrap_or_else(|| Arc::new(OllamaBackend)),
304 backoff: self.backoff.unwrap_or_else(BackoffConfig::none),
305 vars: self.vars,
306 cancellation: self.cancellation,
307 event_handler: self.event_handler,
308 trace_id,
309 trace_ctx,
310 limits,
311 }
312 }
313}
314
315fn normalize_base_url(url: &str) -> String {
320 let trimmed = url.trim_end_matches('/');
321 for suffix in &[
323 "/v1/chat/completions",
324 "/v1/chat",
325 "/v1",
326 "/api/generate",
327 "/api/chat",
328 "/api",
329 ] {
330 if let Some(stripped) = trimmed.strip_suffix(suffix) {
331 return stripped.to_string();
332 }
333 }
334 trimmed.to_string()
335}
336
337#[allow(deprecated)]
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_normalize_base_url_strips_v1() {
344 assert_eq!(
345 normalize_base_url("https://api.openai.com/v1"),
346 "https://api.openai.com"
347 );
348 assert_eq!(
349 normalize_base_url("https://api.openai.com/v1/"),
350 "https://api.openai.com"
351 );
352 }
353
354 #[test]
355 fn test_normalize_base_url_strips_api() {
356 assert_eq!(
357 normalize_base_url("http://localhost:11434/api"),
358 "http://localhost:11434"
359 );
360 assert_eq!(
361 normalize_base_url("http://localhost:11434/api/"),
362 "http://localhost:11434"
363 );
364 }
365
366 #[test]
367 fn test_normalize_base_url_preserves_clean() {
368 assert_eq!(
369 normalize_base_url("http://localhost:11434"),
370 "http://localhost:11434"
371 );
372 assert_eq!(
373 normalize_base_url("https://api.openai.com"),
374 "https://api.openai.com"
375 );
376 }
377
378 #[test]
379 fn test_normalize_base_url_strips_full_path() {
380 assert_eq!(
381 normalize_base_url("https://api.openai.com/v1/chat/completions"),
382 "https://api.openai.com"
383 );
384 }
385
386 #[test]
387 fn test_normalize_base_url_trailing_slash() {
388 assert_eq!(
389 normalize_base_url("http://localhost:11434/"),
390 "http://localhost:11434"
391 );
392 }
393
394 #[test]
395 fn test_default_timeout_applied() {
396 let _ctx = ExecCtx::builder("http://localhost:11434")
398 .timeout(Duration::from_secs(120))
399 .build();
400 }
402
403 #[test]
404 fn test_trace_ctx_generated_by_default() {
405 let ctx = ExecCtx::builder("http://localhost:11434").build();
406 assert!(!ctx.trace_id.as_str().is_empty());
408 assert!(!ctx.trace_ctx.trace_id.is_empty());
409 }
410
411 #[test]
412 fn test_trace_ctx_explicit_sets_legacy() {
413 let trace = TraceCtx::from_trace_id("0af7651916cd43dd8448eb211c80319c");
414 let ctx = ExecCtx::builder("http://localhost:11434")
415 .with_trace_ctx(trace.clone())
416 .build();
417 assert_eq!(ctx.trace_ctx.trace_id, "0af7651916cd43dd8448eb211c80319c");
418 assert_eq!(ctx.trace_id.as_str(), "0af7651916cd43dd8448eb211c80319c");
419 }
420
421 #[test]
422 fn test_legacy_trace_id_derives_trace_ctx() {
423 let id = TraceId::from_string("my-legacy-trace");
424 let ctx = ExecCtx::builder("http://localhost:11434")
425 .with_trace_id(id)
426 .build();
427 assert_eq!(ctx.trace_id.as_str(), "my-legacy-trace");
428 assert_eq!(ctx.trace_ctx.trace_id, "my-legacy-trace");
429 }
430
431 #[test]
432 fn test_trace_ctx_takes_priority_over_trace_id() {
433 let trace = TraceCtx::from_trace_id("canonical-trace");
434 let id = TraceId::from_string("legacy-trace");
435 let ctx = ExecCtx::builder("http://localhost:11434")
436 .with_trace_id(id)
437 .with_trace_ctx(trace)
438 .build();
439 assert_eq!(ctx.trace_ctx.trace_id, "canonical-trace");
441 assert_eq!(ctx.trace_id.as_str(), "canonical-trace");
442 }
443}