1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap},
3 env,
4 sync::{Arc, Mutex, OnceLock, PoisonError},
5 time::Duration,
6};
7
8use http::{HeaderMap, HeaderName, HeaderValue};
9use oven_sdk::{
10 AdapterId, ApiEndpoint, HeaderConfig, HeaderContext, HeaderOverrides, HeaderProvider,
11 LanguageModel, ModelCapabilities, ModelConfig, ModelDeclaration, ModelError, ModelId,
12 ProviderConfig, ProviderId, ResourceId, SecretString,
13};
14use oven_sdk_anthropic::{
15 AnthropicAuth, AnthropicCacheControl, AnthropicCacheTtl, AnthropicCompatibleAuth,
16 AnthropicCompatibleModel, AnthropicCompatibleSettings, AnthropicModel,
17 AnthropicProtocolSettings, AnthropicRequestOptions, AnthropicSettings, AnthropicThinking,
18 AnthropicThinkingSupport, AnthropicTimeouts,
19};
20use oven_sdk_azure::{
21 AzureApiRoute, AzureApiVersion, AzureMaxTokensField, AzureOpenAiAuth, AzureOpenAiChatModel,
22 AzureOpenAiChatOptions, AzureOpenAiChatSettings, AzureOpenAiCompletionsConfig,
23 AzureOpenAiResponsesCompaction, AzureOpenAiResponsesModel, AzureOpenAiResponsesOptions,
24 AzureOpenAiResponsesSettings, AzureOpenAiRevision, AzureOpenAiTimeouts, AzureReasoningField,
25 AzureStructuredOutputSupport, AzureSystemMessageRole,
26};
27use oven_sdk_bedrock::{
28 AwsCredentials, BedrockAuth, BedrockConverseSettings, BedrockEventStreamLimits,
29 BedrockGuardrailConfig, BedrockModel, BedrockReasoningWireFormat, BedrockRequestOptions,
30 BedrockS3LocationOptions, BedrockStructuredOutput, BedrockTimeouts,
31};
32use oven_sdk_cohere::{
33 CohereAuth, CohereDocument, CohereModel, CohereRequestOptions, CohereSettings, CohereThinking,
34 CohereTimeouts,
35};
36use oven_sdk_google::{
37 GoogleApiKeyAuth, GoogleGenerateContentSettings, GoogleModel, GoogleProviderTool,
38 GoogleRequestOptions, GoogleSafetySetting, GoogleThinkingConfig, GoogleThinkingSettings,
39 GoogleTimeouts, GoogleToolSettings,
40};
41use oven_sdk_google_vertex::{
42 GoogleVertexMediaSettings, GoogleVertexModel, GoogleVertexProviderTool,
43 GoogleVertexRequestOptions, GoogleVertexResource, GoogleVertexSafetySetting,
44 GoogleVertexSettings, GoogleVertexThinkingConfig, GoogleVertexThinkingMode,
45 GoogleVertexTimeouts, GoogleVertexToolSettings, VertexAuth, google_vertex_native_context_scope,
46};
47use oven_sdk_openai::{
48 CompatibleChatOptions, MaxTokensField, OpenAiAuth, OpenAiChatModel, OpenAiChatOptions,
49 OpenAiChatSettings, OpenAiCompatibleAuth, OpenAiCompatibleChatModel,
50 OpenAiCompatibleChatSettings, OpenAiResponsesCompaction, OpenAiResponsesModel,
51 OpenAiResponsesOptions, OpenAiResponsesSettings, OpenAiTimeouts, ReasoningField,
52 StructuredOutputSupport, SystemMessageRole,
53};
54use serde::{Deserialize, Serialize};
55use serde_json::{Map, Value, json};
56use sha2::{Digest as _, Sha256};
57use thiserror::Error;
58use zeroize::Zeroize as _;
59
60use crate::ConstructedAdapter;
61
62#[derive(Clone, Serialize)]
64pub struct ConcreteModel {
65 pub provider_id: String,
67 pub model_id: String,
69 pub endpoint: String,
71 pub auth: AuthConfig,
73 #[serde(default)]
75 pub headers: BTreeMap<String, String>,
76 pub capabilities: ModelCapabilities,
79 #[serde(default)]
81 pub defaults: CommonDefaults,
82 #[serde(flatten)]
84 pub adapter: AdapterConfig,
85}
86
87impl std::fmt::Debug for ConcreteModel {
88 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 formatter
90 .debug_struct("ConcreteModel")
91 .field("provider_id", &self.provider_id)
92 .field("model_id", &self.model_id)
93 .field("endpoint", &"<redacted>")
94 .field("auth", &"<redacted>")
95 .field("header_names", &self.headers.keys().collect::<Vec<_>>())
96 .field("capabilities", &self.capabilities)
97 .field("defaults", &self.defaults)
98 .field("adapter", &self.adapter)
99 .finish()
100 }
101}
102
103impl ConcreteModel {
104 pub fn build(&self) -> Result<ConstructedAdapter, ModelBuildError> {
106 let provider = self.provider()?;
107 let header_routing_discriminator =
108 (!self.headers.is_empty()).then(|| header_routing_discriminator(&self.headers));
109 let mut capabilities = self.capabilities.clone();
110 capabilities.compaction = match &self.adapter {
111 AdapterConfig::OpenaiResponses { settings, .. }
112 if matches!(
113 settings.compaction,
114 OpenAiResponsesCompactionConfig::OpenAiResponsesCompact
115 ) =>
116 {
117 oven_sdk::CompactionCapability::Native
118 }
119 AdapterConfig::AzureResponses { settings, .. }
120 if matches!(
121 settings.compaction,
122 AzureResponsesCompactionConfig::AzureResponsesCompact { .. }
123 ) =>
124 {
125 oven_sdk::CompactionCapability::Native
126 }
127 _ => oven_sdk::CompactionCapability::Unsupported,
128 };
129 let declaration = ModelDeclaration::new(ModelId::new(self.model_id.clone()), capabilities)?;
130 let (model, provider_options): (Arc<dyn LanguageModel>, BTreeMap<String, Value>) =
131 match &self.adapter {
132 AdapterConfig::Anthropic { settings, options } => (
133 Arc::new(AnthropicModel::new(ModelConfig::new(
134 provider.with_auth(self.anthropic_auth()?),
135 declaration,
136 settings.to_oven()?,
137 ))?),
138 namespace("anthropic", options.to_oven()?)?,
139 ),
140 AdapterConfig::AnthropicCompatible { settings, options } => (
141 Arc::new(AnthropicCompatibleModel::new(ModelConfig::new(
142 provider.with_auth(self.anthropic_compatible_auth()?),
143 declaration,
144 settings.to_oven()?,
145 ))?),
146 namespace("anthropic", options.to_oven()?)?,
147 ),
148 AdapterConfig::OpenaiChat { settings, options }
149 if matches!(self.auth, AuthConfig::None) =>
150 {
151 (
152 Arc::new(OpenAiChatModel::new_no_auth(ModelConfig::new(
153 provider.with_auth(()),
154 declaration,
155 settings.to_openai_chat(header_routing_discriminator.as_deref()),
156 ))?),
157 namespace("openai", json!({ "chat": options }))?,
158 )
159 }
160 AdapterConfig::OpenaiChat { settings, options } => (
161 Arc::new(OpenAiChatModel::new(ModelConfig::new(
162 provider.with_auth(self.openai_auth()?),
163 declaration,
164 settings.to_openai_chat(header_routing_discriminator.as_deref()),
165 ))?),
166 namespace("openai", json!({ "chat": options }))?,
167 ),
168 AdapterConfig::OpenaiResponses { settings, options } => (
169 Arc::new(OpenAiResponsesModel::new(ModelConfig::new(
170 provider.with_auth(self.openai_auth()?),
171 declaration,
172 settings.to_openai_responses(header_routing_discriminator.as_deref()),
173 ))?),
174 namespace("openai", json!({ "responses": options }))?,
175 ),
176 AdapterConfig::CompatibleResponses {
177 adapter_id,
178 settings,
179 options,
180 } => (
181 Arc::new(OpenAiResponsesModel::new_compatible(
182 ModelConfig::new(
183 provider.with_auth(self.compatible_auth()?),
184 declaration,
185 settings.to_openai_responses(header_routing_discriminator.as_deref()),
186 ),
187 oven_sdk::AdapterId::new(adapter_id.clone()),
188 )?),
189 namespace("openai", json!({ "responses": options }))?,
190 ),
191 AdapterConfig::OpenaiCompatible { settings, options } => (
192 Arc::new(OpenAiCompatibleChatModel::new(ModelConfig::new(
193 provider.with_auth(self.compatible_auth()?),
194 declaration,
195 settings.to_oven(header_routing_discriminator.as_deref()),
196 ))?),
197 namespace(
198 "openai_compatible",
199 CompatibleChatOptions {
200 extra_body: options.extra_body.clone(),
201 },
202 )?,
203 ),
204 AdapterConfig::Google { settings, options } => (
205 Arc::new(GoogleModel::new(ModelConfig::new(
206 provider.with_auth(self.google_auth()?),
207 declaration,
208 settings.to_oven(),
209 ))?),
210 namespace("google", options.to_oven())?,
211 ),
212 AdapterConfig::Vertex { settings, options } => {
213 let provider = provider.with_auth(self.vertex_auth()?);
214 let settings = settings.to_oven(&provider, &declaration)?;
215 (
216 Arc::new(GoogleVertexModel::new(ModelConfig::new(
217 provider,
218 declaration,
219 settings,
220 ))?),
221 namespace("google_vertex", options.to_oven())?,
222 )
223 }
224 AdapterConfig::Bedrock { settings, options } => (
225 Arc::new(BedrockModel::new(ModelConfig::new(
226 provider.with_auth(self.bedrock_auth()?),
227 declaration,
228 settings.to_oven(),
229 ))?),
230 namespace("bedrock", options.to_oven())?,
231 ),
232 AdapterConfig::AzureChat { settings, options } => (
233 Arc::new(AzureOpenAiChatModel::new(ModelConfig::new(
234 provider.with_auth(self.azure_auth()?),
235 declaration,
236 settings.to_chat()?,
237 ))?),
238 namespace("azure_openai", json!({ "chat": options }))?,
239 ),
240 AdapterConfig::AzureResponses { settings, options } => (
241 Arc::new(AzureOpenAiResponsesModel::new(ModelConfig::new(
242 provider.with_auth(self.azure_auth()?),
243 declaration,
244 settings.to_responses()?,
245 ))?),
246 namespace("azure_openai", json!({ "responses": options }))?,
247 ),
248 AdapterConfig::Cohere { settings, options } => (
249 Arc::new(CohereModel::new(ModelConfig::new(
250 provider.with_auth(self.cohere_auth()?),
251 declaration,
252 settings.to_oven(),
253 ))?),
254 namespace("cohere", options.to_oven())?,
255 ),
256 };
257 Ok(ConstructedAdapter {
258 model,
259 provider_options,
260 })
261 }
262
263 fn provider(&self) -> Result<CommonProvider, ModelBuildError> {
264 Ok(CommonProvider {
265 id: ProviderId::new(self.provider_id.clone()),
266 api: ApiEndpoint::parse(&self.endpoint)?,
267 headers: header_config(&self.headers)?,
268 })
269 }
270
271 fn anthropic_auth(&self) -> Result<AnthropicAuth, ModelBuildError> {
272 match &self.auth {
273 AuthConfig::None => Ok(AnthropicAuth::None),
274 AuthConfig::ApiKey { value } => Ok(AnthropicAuth::ApiKey(secret(value))),
275 _ => Err(wrong_auth("anthropic", "none or api_key")),
276 }
277 }
278
279 fn anthropic_compatible_auth(&self) -> Result<AnthropicCompatibleAuth, ModelBuildError> {
280 match &self.auth {
281 AuthConfig::None => Ok(AnthropicCompatibleAuth::None),
282 AuthConfig::ApiKey { value } => Ok(AnthropicCompatibleAuth::ApiKey(secret(value))),
283 AuthConfig::Bearer { token } => Ok(AnthropicCompatibleAuth::Bearer(secret(token))),
284 _ => Err(wrong_auth(
285 "Anthropic-compatible",
286 "none, api_key, or bearer",
287 )),
288 }
289 }
290
291 fn openai_auth(&self) -> Result<OpenAiAuth, ModelBuildError> {
292 match &self.auth {
293 AuthConfig::Openai {
294 api_key,
295 organization,
296 project,
297 } => Ok(OpenAiAuth {
298 api_key: secret(api_key),
299 organization: organization.clone(),
300 project: project.clone(),
301 }),
302 _ => Err(wrong_auth("OpenAI", "openai")),
303 }
304 }
305
306 fn compatible_auth(&self) -> Result<OpenAiCompatibleAuth, ModelBuildError> {
307 match &self.auth {
308 AuthConfig::None => Ok(OpenAiCompatibleAuth::none()),
309 AuthConfig::Bearer { token } => Ok(OpenAiCompatibleAuth::bearer(secret(token))),
310 AuthConfig::HeaderApiKey { name, value } => Ok(OpenAiCompatibleAuth::headers(
311 Arc::new(SecretHeaderProvider {
312 name: name.clone(),
313 value: secret(value),
314 }),
315 )),
316 _ => Err(wrong_auth(
317 "OpenAI-compatible",
318 "none, bearer, or header_api_key",
319 )),
320 }
321 }
322
323 fn google_auth(&self) -> Result<GoogleApiKeyAuth, ModelBuildError> {
324 match &self.auth {
325 AuthConfig::ApiKey { value } => Ok(GoogleApiKeyAuth::new(value.clone())),
326 _ => Err(wrong_auth("Google", "api_key")),
327 }
328 }
329
330 fn vertex_auth(&self) -> Result<VertexAuth, ModelBuildError> {
331 match &self.auth {
332 AuthConfig::AccessToken { token } => Ok(VertexAuth::AccessToken(secret(token))),
333 _ => Err(wrong_auth("Vertex", "access_token")),
334 }
335 }
336
337 fn bedrock_auth(&self) -> Result<BedrockAuth, ModelBuildError> {
338 match &self.auth {
339 AuthConfig::AwsStatic {
340 access_key_id,
341 secret_access_key,
342 session_token,
343 } => Ok(BedrockAuth::Static(AwsCredentials {
344 access_key_id: access_key_id.clone(),
345 secret_access_key: secret_access_key.clone(),
346 session_token: session_token.clone(),
347 })),
348 _ => Err(wrong_auth("Bedrock", "aws_static")),
349 }
350 }
351
352 fn azure_auth(&self) -> Result<AzureOpenAiAuth, ModelBuildError> {
353 match &self.auth {
354 AuthConfig::ApiKey { value } => Ok(AzureOpenAiAuth::ApiKey(secret(value))),
355 AuthConfig::Bearer { token } => {
356 let token = token.clone();
357 Ok(AzureOpenAiAuth::Entra(Arc::new(move || {
358 let token = token.clone();
359 Box::pin(async move { Ok(token) })
360 })))
361 }
362 _ => Err(wrong_auth("Azure OpenAI", "api_key or bearer")),
363 }
364 }
365
366 fn cohere_auth(&self) -> Result<CohereAuth, ModelBuildError> {
367 match &self.auth {
368 AuthConfig::Bearer { token } => Ok(CohereAuth::bearer(secret(token))),
369 _ => Err(wrong_auth("Cohere", "bearer")),
370 }
371 }
372}
373
374#[derive(Clone)]
375struct CommonProvider {
376 id: ProviderId,
377 api: ApiEndpoint,
378 headers: HeaderConfig,
379}
380
381impl CommonProvider {
382 fn with_auth<A>(&self, auth: A) -> ProviderConfig<A> {
383 ProviderConfig::new(
384 self.id.clone(),
385 self.api.clone(),
386 auth,
387 self.headers.clone(),
388 )
389 .expect("common provider identity was validated")
390 }
391}
392
393#[derive(Clone, Deserialize, Serialize)]
395#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
396pub enum AuthConfig {
397 None,
399 ApiKey { value: String },
401 Bearer { token: String },
403 HeaderApiKey { name: String, value: String },
405 Openai {
407 api_key: String,
408 organization: Option<String>,
409 project: Option<String>,
410 },
411 AccessToken { token: String },
413 AwsStatic {
415 access_key_id: String,
416 secret_access_key: String,
417 session_token: Option<String>,
418 },
419}
420
421impl Drop for AuthConfig {
422 fn drop(&mut self) {
423 match self {
424 Self::None => {}
425 Self::ApiKey { value } => value.zeroize(),
426 Self::Bearer { token } | Self::AccessToken { token } => token.zeroize(),
427 Self::HeaderApiKey { value, .. } => value.zeroize(),
428 Self::Openai { api_key, .. } => api_key.zeroize(),
429 Self::AwsStatic {
430 access_key_id,
431 secret_access_key,
432 session_token,
433 } => {
434 access_key_id.zeroize();
435 secret_access_key.zeroize();
436 if let Some(session_token) = session_token {
437 session_token.zeroize();
438 }
439 }
440 }
441 }
442}
443
444#[derive(Clone)]
445struct SecretHeaderProvider {
446 name: String,
447 value: SecretString,
448}
449
450impl HeaderProvider for SecretHeaderProvider {
451 fn headers(&self, _context: &HeaderContext) -> Result<HeaderOverrides, ModelError> {
452 let name = HeaderName::from_bytes(self.name.as_bytes())
453 .map_err(|_| ModelError::invalid_request("invalid API-key header name"))?;
454 let value = HeaderValue::from_str(self.value.expose_secret())
455 .map_err(|_| ModelError::invalid_request("invalid API-key header value"))?;
456 Ok(HeaderOverrides::new(HeaderMap::from_iter([(name, value)])))
457 }
458}
459
460#[derive(Clone)]
461pub(crate) struct TemplateHeaderProvider {
462 templates: Vec<(HeaderName, HeaderTemplate)>,
463}
464
465#[derive(Clone)]
466enum HeaderTemplate {
467 Static(Option<HeaderValue>),
468 Dynamic(Vec<HeaderTemplateSegment>),
469}
470
471#[derive(Clone)]
472enum HeaderTemplateSegment {
473 Literal(String),
474 SessionId,
475 ParentSessionId,
476 Env {
477 name: String,
478 default: Option<String>,
479 },
480}
481
482impl TemplateHeaderProvider {
483 pub(crate) fn new(values: &BTreeMap<String, String>) -> Result<Self, ModelBuildError> {
484 let templates = values
485 .iter()
486 .map(|(name, value)| {
487 let header_name = HeaderName::from_bytes(name.as_bytes())
488 .map_err(|_| ModelBuildError::HeaderName(name.clone()))?;
489 let template = parse_header_template(value)
490 .map_err(|()| ModelBuildError::HeaderTemplate(name.clone()))?;
491 let template = match template {
492 ParsedHeaderTemplate::Static(value) => HeaderTemplate::Static(
493 (!value.is_empty())
494 .then(|| HeaderValue::from_str(&value))
495 .transpose()
496 .map_err(|_| ModelBuildError::HeaderValue(name.clone()))?,
497 ),
498 ParsedHeaderTemplate::Dynamic(segments) => HeaderTemplate::Dynamic(segments),
499 };
500 Ok((header_name, template))
501 })
502 .collect::<Result<_, ModelBuildError>>()?;
503 Ok(Self { templates })
504 }
505
506 pub(crate) fn resolved_headers(
507 &self,
508 context: &HeaderContext,
509 ) -> Result<HeaderMap, &'static str> {
510 let mut headers = HeaderMap::new();
511 for (name, template) in &self.templates {
512 let value = match template {
513 HeaderTemplate::Static(value) => value.clone(),
514 HeaderTemplate::Dynamic(segments) => {
515 let value = resolve_header_template(segments, context)?;
516 (!value.is_empty())
517 .then(|| HeaderValue::from_str(&value))
518 .transpose()
519 .map_err(|_| "invalid configured header value")?
520 }
521 };
522 if let Some(value) = value {
523 headers.insert(name.clone(), value);
524 }
525 }
526 Ok(headers)
527 }
528}
529
530impl HeaderProvider for TemplateHeaderProvider {
531 fn headers(&self, context: &HeaderContext) -> Result<HeaderOverrides, ModelError> {
532 self.resolved_headers(context)
533 .map(HeaderOverrides::new)
534 .map_err(ModelError::invalid_request)
535 }
536}
537
538enum ParsedHeaderTemplate {
539 Static(String),
540 Dynamic(Vec<HeaderTemplateSegment>),
541}
542
543fn parse_header_template(template: &str) -> Result<ParsedHeaderTemplate, ()> {
544 let mut segments = Vec::new();
545 let mut literal = String::new();
546 let mut cursor = 0;
547 let mut dynamic = false;
548 let push_literal = |segments: &mut Vec<HeaderTemplateSegment>, literal: &mut String| {
549 if !literal.is_empty() {
550 segments.push(HeaderTemplateSegment::Literal(std::mem::take(literal)));
551 }
552 };
553 while cursor < template.len() {
554 let remaining = &template[cursor..];
555 if let Some(text) = remaining.strip_prefix("$$") {
556 literal.push('$');
557 cursor = template.len() - text.len();
558 } else if let Some(text) = remaining.strip_prefix("${session_id}") {
559 push_literal(&mut segments, &mut literal);
560 segments.push(HeaderTemplateSegment::SessionId);
561 dynamic = true;
562 cursor = template.len() - text.len();
563 } else if let Some(text) = remaining.strip_prefix("${parent_session_id}") {
564 push_literal(&mut segments, &mut literal);
565 segments.push(HeaderTemplateSegment::ParentSessionId);
566 dynamic = true;
567 cursor = template.len() - text.len();
568 } else if let Some(expression) = remaining.strip_prefix("${env:") {
569 let end = expression.find('}').ok_or(())?;
570 let expression_body = &expression[..end];
571 let (name, default) = expression_body
572 .split_once(":-")
573 .map_or((expression_body, None), |(name, default)| {
574 (name, Some(default))
575 });
576 if !valid_env_name(name) {
577 return Err(());
578 }
579 push_literal(&mut segments, &mut literal);
580 segments.push(HeaderTemplateSegment::Env {
581 name: name.to_owned(),
582 default: default.map(str::to_owned),
583 });
584 dynamic = true;
585 cursor += 6 + end + 1;
586 } else {
587 let character = remaining.chars().next().expect("nonempty remainder");
588 literal.push(character);
589 cursor += character.len_utf8();
590 }
591 }
592 if !dynamic {
593 return Ok(ParsedHeaderTemplate::Static(literal));
594 }
595 push_literal(&mut segments, &mut literal);
596 Ok(ParsedHeaderTemplate::Dynamic(segments))
597}
598
599fn resolve_header_template(
600 segments: &[HeaderTemplateSegment],
601 context: &HeaderContext,
602) -> Result<String, &'static str> {
603 let mut output = String::new();
604 for segment in segments {
605 match segment {
606 HeaderTemplateSegment::Literal(literal) => output.push_str(literal),
607 HeaderTemplateSegment::SessionId => output.push_str(&context.session_id),
608 HeaderTemplateSegment::ParentSessionId => {
609 output.push_str(context.parent_session_id.as_deref().unwrap_or_default());
610 }
611 HeaderTemplateSegment::Env { name, default } => match env::var(name) {
612 Ok(value) => output.push_str(&value),
613 Err(env::VarError::NotPresent) => {
614 output.push_str(
615 default
616 .as_deref()
617 .ok_or("missing header environment variable")?,
618 );
619 }
620 Err(env::VarError::NotUnicode(_)) => {
621 return Err("header environment variable is not UTF-8");
622 }
623 },
624 }
625 }
626 Ok(output)
627}
628
629fn valid_env_name(name: &str) -> bool {
630 !name.is_empty()
631 && (name.as_bytes()[0].is_ascii_uppercase() || name.as_bytes()[0] == b'_')
632 && name
633 .bytes()
634 .skip(1)
635 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
636}
637
638#[derive(Clone, Debug, Default, Deserialize, Serialize)]
640#[serde(deny_unknown_fields)]
641pub struct CommonDefaults {
642 pub max_output_tokens: Option<u64>,
643 pub temperature: Option<f64>,
644 pub top_p: Option<f64>,
645 pub reasoning_effort: Option<String>,
646 #[serde(default)]
647 pub include_raw: bool,
648}
649
650#[derive(Clone, Debug, Deserialize, Serialize)]
655#[serde(tag = "adaptor", rename_all = "kebab-case", deny_unknown_fields)]
656pub enum AdapterConfig {
657 Anthropic {
658 settings: AnthropicSettingsConfig,
659 #[serde(default)]
660 options: AnthropicOptionsConfig,
661 },
662 AnthropicCompatible {
663 settings: AnthropicCompatibleSettingsConfig,
664 #[serde(default)]
665 options: AnthropicOptionsConfig,
666 },
667 OpenaiChat {
668 settings: OpenAiChatSettingsConfig,
669 #[serde(default)]
670 options: OpenAiChatOptionsConfig,
671 },
672 OpenaiResponses {
673 #[serde(default)]
674 settings: OpenAiResponsesSettingsConfig,
675 #[serde(default)]
676 options: OpenAiResponsesOptionsConfig,
677 },
678 OpenaiCompatible {
679 settings: CompatibleSettingsConfig,
680 #[serde(default)]
681 options: CompatibleOptionsConfig,
682 },
683 CompatibleResponses {
684 adapter_id: String,
685 #[serde(default)]
686 settings: OpenAiResponsesSettingsConfig,
687 #[serde(default)]
688 options: OpenAiResponsesOptionsConfig,
689 },
690 Google {
691 settings: GoogleSettingsConfig,
692 #[serde(default)]
693 options: GoogleOptionsConfig,
694 },
695 Vertex {
696 settings: VertexSettingsConfig,
697 #[serde(default)]
698 options: VertexOptionsConfig,
699 },
700 Bedrock {
701 settings: BedrockSettingsConfig,
702 #[serde(default)]
703 options: BedrockOptionsConfig,
704 },
705 AzureChat {
706 settings: AzureChatSettingsConfig,
707 #[serde(default)]
708 options: AzureChatOptionsConfig,
709 },
710 AzureResponses {
711 settings: AzureResponsesSettingsConfig,
712 #[serde(default)]
713 options: AzureResponsesOptionsConfig,
714 },
715 Cohere {
716 settings: CohereSettingsConfig,
717 #[serde(default)]
718 options: CohereOptionsConfig,
719 },
720}
721
722#[derive(Error)]
724pub enum ModelBuildError {
725 #[error("invalid model configuration: {0}")]
726 Oven(#[source] Box<ModelError>),
727 #[error("invalid HTTP header name `{0}`")]
728 HeaderName(String),
729 #[error("invalid HTTP header value for `{0}`")]
730 HeaderValue(String),
731 #[error("invalid HTTP header template for `{0}`")]
732 HeaderTemplate(String),
733 #[error("{adapter} adapter requires auth.type = {expected}")]
734 WrongAuth {
735 adapter: &'static str,
736 expected: &'static str,
737 },
738 #[error("could not encode provider request defaults")]
739 ProviderOptions(#[source] serde_json::Error),
740}
741
742impl From<ModelError> for ModelBuildError {
743 fn from(error: ModelError) -> Self {
744 Self::Oven(Box::new(error))
745 }
746}
747
748impl std::fmt::Debug for ModelBuildError {
749 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750 formatter
751 .debug_tuple("ModelBuildError")
752 .field(&self.to_string())
753 .finish()
754 }
755}
756
757fn wrong_auth(adapter: &'static str, expected: &'static str) -> ModelBuildError {
758 ModelBuildError::WrongAuth { adapter, expected }
759}
760
761fn secret(value: &str) -> SecretString {
762 SecretString::new(value.to_owned())
763}
764
765fn header_config(values: &BTreeMap<String, String>) -> Result<HeaderConfig, ModelBuildError> {
766 Ok(HeaderConfig {
769 static_headers: HeaderOverrides::new(HeaderMap::new()),
770 dynamic_headers: if values.is_empty() {
771 None
772 } else {
773 Some(Arc::new(TemplateHeaderProvider::new(values)?))
774 },
775 })
776}
777
778fn header_routing_discriminator(values: &BTreeMap<String, String>) -> String {
779 let mut digest = Sha256::new();
780 digest.update(b"cookie-agent/header-templates/v1\0");
781 for (name, value) in values {
782 digest.update(name.as_bytes());
783 digest.update(b"\0");
784 digest.update(value.as_bytes());
785 digest.update(b"\0");
786 }
787 format!("{:x}", digest.finalize())
788}
789
790fn combined_routing_discriminator(routing: Option<&str>, headers: Option<&str>) -> Option<String> {
791 match (routing, headers) {
792 (Some(routing), Some(headers)) => {
793 let mut digest = Sha256::new();
794 digest.update(b"cookie-agent/request-routing/v1\0");
795 digest.update(
796 serde_json::to_vec(&(routing, headers)).expect("routing strings serialize"),
797 );
798 Some(format!("{:x}", digest.finalize()))
799 }
800 (routing, headers) => routing.or(headers).map(str::to_owned),
801 }
802}
803
804fn namespace(
805 name: &str,
806 value: impl Serialize,
807) -> Result<BTreeMap<String, Value>, ModelBuildError> {
808 let value = serde_json::to_value(value).map_err(ModelBuildError::ProviderOptions)?;
809 Ok([(name.to_owned(), value)].into_iter().collect())
810}
811
812#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
813#[serde(deny_unknown_fields)]
814pub struct TimeoutsConfig {
815 #[serde(default = "default_connect")]
816 pub connect_seconds: u64,
817 #[serde(default = "default_headers")]
818 pub headers_seconds: u64,
819 #[serde(default = "default_credentials")]
820 pub credentials_seconds: u64,
821 #[serde(default = "default_stream_idle")]
822 pub stream_idle_seconds: u64,
823}
824
825impl Default for TimeoutsConfig {
826 fn default() -> Self {
827 Self {
828 connect_seconds: default_connect(),
829 headers_seconds: default_headers(),
830 credentials_seconds: default_credentials(),
831 stream_idle_seconds: default_stream_idle(),
832 }
833 }
834}
835
836const fn default_connect() -> u64 {
837 10
838}
839const fn default_headers() -> u64 {
840 300
843}
844const fn default_credentials() -> u64 {
845 30
846}
847const fn default_stream_idle() -> u64 {
848 120
849}
850
851fn shared_http_client(connect: Duration) -> Option<reqwest::Client> {
857 static CLIENTS: OnceLock<Mutex<HashMap<Duration, reqwest::Client>>> = OnceLock::new();
858 let mut clients = CLIENTS
859 .get_or_init(Default::default)
860 .lock()
861 .unwrap_or_else(PoisonError::into_inner);
862 if let Some(client) = clients.get(&connect) {
863 return Some(client.clone());
864 }
865 let client = reqwest::Client::builder()
866 .connect_timeout(connect)
867 .build()
868 .ok()?;
869 clients.insert(connect, client.clone());
870 Some(client)
871}
872
873impl TimeoutsConfig {
874 fn shared_client(self) -> Option<reqwest::Client> {
875 shared_http_client(Duration::from_secs(self.connect_seconds))
876 }
877 fn anthropic(self) -> AnthropicTimeouts {
878 AnthropicTimeouts {
879 headers: Duration::from_secs(self.headers_seconds),
880 credentials: Duration::from_secs(self.credentials_seconds),
881 stream_idle: Duration::from_secs(self.stream_idle_seconds),
882 }
883 }
884 fn openai(self) -> OpenAiTimeouts {
885 OpenAiTimeouts {
886 connect: Duration::from_secs(self.connect_seconds),
887 headers: Duration::from_secs(self.headers_seconds),
888 stream_idle: Duration::from_secs(self.stream_idle_seconds),
889 }
890 }
891 fn google(self) -> GoogleTimeouts {
892 GoogleTimeouts {
893 connect: Duration::from_secs(self.connect_seconds),
894 headers: Duration::from_secs(self.headers_seconds),
895 stream_idle: Duration::from_secs(self.stream_idle_seconds),
896 }
897 }
898 fn vertex(self) -> GoogleVertexTimeouts {
899 GoogleVertexTimeouts {
900 connect: Duration::from_secs(self.connect_seconds),
901 headers: Duration::from_secs(self.headers_seconds),
902 credentials: Duration::from_secs(self.credentials_seconds),
903 stream_idle: Duration::from_secs(self.stream_idle_seconds),
904 }
905 }
906 fn bedrock(self) -> BedrockTimeouts {
907 BedrockTimeouts {
908 connect: Duration::from_secs(self.connect_seconds),
909 headers: Duration::from_secs(self.headers_seconds),
910 credentials: Duration::from_secs(self.credentials_seconds),
911 stream_idle: Duration::from_secs(self.stream_idle_seconds),
912 }
913 }
914 fn azure(self) -> AzureOpenAiTimeouts {
915 AzureOpenAiTimeouts {
916 connect: Duration::from_secs(self.connect_seconds),
917 headers: Duration::from_secs(self.headers_seconds),
918 credentials: Duration::from_secs(self.credentials_seconds),
919 stream_idle: Duration::from_secs(self.stream_idle_seconds),
920 }
921 }
922 fn cohere(self) -> CohereTimeouts {
923 CohereTimeouts {
924 connect: Duration::from_secs(self.connect_seconds),
925 headers: Duration::from_secs(self.headers_seconds),
926 stream_idle: Duration::from_secs(self.stream_idle_seconds),
927 }
928 }
929}
930
931#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
932#[serde(rename_all = "snake_case")]
933pub enum AnthropicThinkingSupportConfig {
934 None,
935 Extended,
936 Adaptive,
937 Both,
938}
939
940impl From<AnthropicThinkingSupportConfig> for AnthropicThinkingSupport {
941 fn from(value: AnthropicThinkingSupportConfig) -> Self {
942 match value {
943 AnthropicThinkingSupportConfig::None => Self::None,
944 AnthropicThinkingSupportConfig::Extended => Self::Extended,
945 AnthropicThinkingSupportConfig::Adaptive => Self::Adaptive,
946 AnthropicThinkingSupportConfig::Both => Self::Both,
947 }
948 }
949}
950
951#[derive(Clone, Debug, Deserialize, Serialize)]
952#[serde(deny_unknown_fields)]
953pub struct AnthropicSettingsConfig {
954 #[serde(default)]
955 pub timeouts: TimeoutsConfig,
956 pub thinking: AnthropicThinkingSupportConfig,
957 pub thinking_default_active: bool,
958 pub thinking_disable_allowed: bool,
959 #[serde(default)]
960 pub thinking_disable_forbidden_efforts: BTreeSet<String>,
961 pub effort: bool,
962 pub assistant_prefill: bool,
963 pub reject_non_default_sampling: bool,
964 pub native_context_discriminator: Option<String>,
965}
966
967impl AnthropicSettingsConfig {
968 fn to_oven(&self) -> Result<AnthropicSettings, ModelBuildError> {
969 Ok(AnthropicSettings {
970 client: self.timeouts.shared_client().ok_or_else(|| {
971 ModelError::transport("could not construct Anthropic HTTP client")
972 })?,
973 timeouts: self.timeouts.anthropic(),
974 protocol: AnthropicProtocolSettings {
975 thinking: self.thinking.into(),
976 thinking_default_active: self.thinking_default_active,
977 thinking_disable_allowed: self.thinking_disable_allowed,
978 thinking_disable_forbidden_efforts: self.thinking_disable_forbidden_efforts.clone(),
979 effort: self.effort,
980 assistant_prefill: self.assistant_prefill,
981 reject_non_default_sampling: self.reject_non_default_sampling,
982 },
983 native_context_discriminator: self
984 .native_context_discriminator
985 .as_ref()
986 .map(|value| ResourceId::new(value.clone()).map_err(ModelBuildError::from))
987 .transpose()?,
988 })
989 }
990}
991
992#[derive(Clone, Debug, Deserialize, Serialize)]
993#[serde(deny_unknown_fields)]
994pub struct AnthropicCompatibleSettingsConfig {
995 pub adapter_id: String,
996 #[serde(flatten)]
997 pub protocol: AnthropicSettingsConfig,
998}
999
1000impl AnthropicCompatibleSettingsConfig {
1001 fn to_oven(&self) -> Result<AnthropicCompatibleSettings, ModelBuildError> {
1002 let settings = self.protocol.to_oven()?;
1003 Ok(AnthropicCompatibleSettings {
1004 adapter_id: AdapterId::new(self.adapter_id.clone()),
1005 client: settings.client,
1006 timeouts: settings.timeouts,
1007 protocol: settings.protocol,
1008 native_context_discriminator: settings.native_context_discriminator,
1009 })
1010 }
1011}
1012
1013#[derive(Clone, Debug, Deserialize, Serialize)]
1014#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1015pub enum AnthropicThinkingConfig {
1016 Disabled,
1017 Enabled {
1018 budget_tokens: u64,
1019 display: Option<String>,
1020 },
1021 Adaptive {
1022 display: Option<String>,
1023 },
1024}
1025
1026impl From<AnthropicThinkingConfig> for AnthropicThinking {
1027 fn from(value: AnthropicThinkingConfig) -> Self {
1028 match value {
1029 AnthropicThinkingConfig::Disabled => Self::Disabled,
1030 AnthropicThinkingConfig::Enabled {
1031 budget_tokens,
1032 display,
1033 } => Self::Enabled {
1034 budget_tokens,
1035 display,
1036 },
1037 AnthropicThinkingConfig::Adaptive { display } => Self::Adaptive { display },
1038 }
1039 }
1040}
1041
1042#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1043#[serde(rename_all = "snake_case")]
1044pub enum AnthropicCacheTtlConfig {
1045 FiveMinutes,
1046 OneHour,
1047}
1048
1049#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1050#[serde(deny_unknown_fields)]
1051pub struct AnthropicCacheStrategyConfig {
1052 pub system: Option<AnthropicCacheTtlConfig>,
1053 pub tools: Option<AnthropicCacheTtlConfig>,
1054 pub rolling: Option<AnthropicCacheTtlConfig>,
1055}
1056
1057impl From<AnthropicCacheTtlConfig> for AnthropicCacheTtl {
1058 fn from(value: AnthropicCacheTtlConfig) -> Self {
1059 match value {
1060 AnthropicCacheTtlConfig::FiveMinutes => Self::FiveMinutes,
1061 AnthropicCacheTtlConfig::OneHour => Self::OneHour,
1062 }
1063 }
1064}
1065
1066#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1067#[serde(deny_unknown_fields)]
1068pub struct AnthropicOptionsConfig {
1069 pub thinking: Option<AnthropicThinkingConfig>,
1070 pub effort: Option<String>,
1071 pub cache_ttl: Option<AnthropicCacheTtlConfig>,
1073 pub cache_strategy: Option<AnthropicCacheStrategyConfig>,
1074 pub user_id: Option<String>,
1075 #[serde(default)]
1076 pub betas: Vec<String>,
1077}
1078
1079impl AnthropicOptionsConfig {
1080 fn to_oven(&self) -> Result<Value, ModelBuildError> {
1081 let options = AnthropicRequestOptions {
1082 thinking: self.thinking.clone().map(Into::into),
1083 effort: self.effort.clone(),
1084 cache_control: self
1085 .cache_ttl
1086 .map(|ttl| AnthropicCacheControl { ttl: ttl.into() }),
1087 user_id: self.user_id.clone(),
1088 betas: self.betas.clone(),
1089 };
1090 let mut value = serde_json::to_value(options).map_err(ModelBuildError::ProviderOptions)?;
1091 if let Some(strategy) = &self.cache_strategy {
1092 value["cache_strategy"] =
1093 serde_json::to_value(strategy).map_err(ModelBuildError::ProviderOptions)?;
1094 }
1095 Ok(value)
1096 }
1097}
1098
1099#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
1100#[serde(rename_all = "snake_case")]
1101pub enum SystemRoleConfig {
1102 System,
1103 Developer,
1104 Omit,
1105}
1106
1107impl From<SystemRoleConfig> for SystemMessageRole {
1108 fn from(value: SystemRoleConfig) -> Self {
1109 match value {
1110 SystemRoleConfig::System => Self::System,
1111 SystemRoleConfig::Developer => Self::Developer,
1112 SystemRoleConfig::Omit => Self::Omit,
1113 }
1114 }
1115}
1116
1117impl From<SystemRoleConfig> for AzureSystemMessageRole {
1118 fn from(value: SystemRoleConfig) -> Self {
1119 match value {
1120 SystemRoleConfig::System => Self::System,
1121 SystemRoleConfig::Developer => Self::Developer,
1122 SystemRoleConfig::Omit => Self::Omit,
1123 }
1124 }
1125}
1126
1127#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
1128#[serde(rename_all = "snake_case")]
1129pub enum MaxTokensFieldConfig {
1130 MaxTokens,
1131 MaxCompletionTokens,
1132 Omit,
1133}
1134
1135impl From<MaxTokensFieldConfig> for MaxTokensField {
1136 fn from(value: MaxTokensFieldConfig) -> Self {
1137 match value {
1138 MaxTokensFieldConfig::MaxTokens => Self::MaxTokens,
1139 MaxTokensFieldConfig::MaxCompletionTokens => Self::MaxCompletionTokens,
1140 MaxTokensFieldConfig::Omit => Self::Omit,
1141 }
1142 }
1143}
1144
1145impl From<MaxTokensFieldConfig> for AzureMaxTokensField {
1146 fn from(value: MaxTokensFieldConfig) -> Self {
1147 match value {
1148 MaxTokensFieldConfig::MaxTokens => Self::MaxTokens,
1149 MaxTokensFieldConfig::MaxCompletionTokens => Self::MaxCompletionTokens,
1150 MaxTokensFieldConfig::Omit => Self::Omit,
1151 }
1152 }
1153}
1154
1155#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
1156#[serde(rename_all = "snake_case")]
1157pub enum StructuredOutputConfig {
1158 Unsupported,
1159 JsonObject,
1160 JsonSchema,
1161}
1162
1163impl From<StructuredOutputConfig> for StructuredOutputSupport {
1164 fn from(value: StructuredOutputConfig) -> Self {
1165 match value {
1166 StructuredOutputConfig::Unsupported => Self::Unsupported,
1167 StructuredOutputConfig::JsonObject => Self::JsonObject,
1168 StructuredOutputConfig::JsonSchema => Self::JsonSchema,
1169 }
1170 }
1171}
1172
1173impl From<StructuredOutputConfig> for AzureStructuredOutputSupport {
1174 fn from(value: StructuredOutputConfig) -> Self {
1175 match value {
1176 StructuredOutputConfig::Unsupported => Self::Unsupported,
1177 StructuredOutputConfig::JsonObject => Self::JsonObject,
1178 StructuredOutputConfig::JsonSchema => Self::JsonSchema,
1179 }
1180 }
1181}
1182
1183#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
1184#[serde(rename_all = "snake_case")]
1185pub enum ReasoningFieldConfig {
1186 None,
1187 ReasoningContent,
1188 Reasoning,
1189}
1190
1191impl From<ReasoningFieldConfig> for ReasoningField {
1192 fn from(value: ReasoningFieldConfig) -> Self {
1193 match value {
1194 ReasoningFieldConfig::None => Self::None,
1195 ReasoningFieldConfig::ReasoningContent => Self::ReasoningContent,
1196 ReasoningFieldConfig::Reasoning => Self::Reasoning,
1197 }
1198 }
1199}
1200
1201impl From<ReasoningFieldConfig> for AzureReasoningField {
1202 fn from(value: ReasoningFieldConfig) -> Self {
1203 match value {
1204 ReasoningFieldConfig::None => Self::None,
1205 ReasoningFieldConfig::ReasoningContent => Self::ReasoningContent,
1206 ReasoningFieldConfig::Reasoning => Self::Reasoning,
1207 }
1208 }
1209}
1210
1211#[derive(Clone, Debug, Deserialize, Serialize)]
1212#[serde(deny_unknown_fields)]
1213pub struct OpenAiChatSettingsConfig {
1214 pub system_message_role: SystemRoleConfig,
1215 pub max_tokens_field: MaxTokensFieldConfig,
1216 pub stream_usage: bool,
1217 pub structured_output: StructuredOutputConfig,
1218 pub reasoning_field: ReasoningFieldConfig,
1219 pub routing_discriminator: Option<String>,
1220 #[serde(default)]
1221 pub timeouts: TimeoutsConfig,
1222}
1223
1224impl OpenAiChatSettingsConfig {
1225 fn to_openai_chat(&self, header_discriminator: Option<&str>) -> OpenAiChatSettings {
1226 OpenAiChatSettings {
1227 system_message_role: self.system_message_role.into(),
1228 max_tokens_field: self.max_tokens_field.into(),
1229 stream_usage: self.stream_usage,
1230 structured_output: self.structured_output.into(),
1231 reasoning_field: self.reasoning_field.into(),
1232 routing_discriminator: combined_routing_discriminator(
1233 self.routing_discriminator.as_deref(),
1234 header_discriminator,
1235 ),
1236 client: self.timeouts.shared_client(),
1237 timeouts: self.timeouts.openai(),
1238 }
1239 }
1240}
1241
1242#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1243#[serde(deny_unknown_fields)]
1244pub struct OpenAiResponsesSettingsConfig {
1245 pub routing_discriminator: Option<String>,
1246 #[serde(default)]
1247 pub compaction: OpenAiResponsesCompactionConfig,
1248 #[serde(default)]
1249 pub timeouts: TimeoutsConfig,
1250}
1251
1252impl OpenAiResponsesSettingsConfig {
1253 fn to_openai_responses(&self, header_discriminator: Option<&str>) -> OpenAiResponsesSettings {
1254 OpenAiResponsesSettings {
1255 routing_discriminator: combined_routing_discriminator(
1256 self.routing_discriminator.as_deref(),
1257 header_discriminator,
1258 ),
1259 compaction: self.compaction.into(),
1260 client: self.timeouts.shared_client(),
1261 timeouts: self.timeouts.openai(),
1262 }
1263 }
1264}
1265
1266#[derive(Clone, Copy, Debug, Default, Serialize)]
1267pub enum OpenAiResponsesCompactionConfig {
1268 #[serde(rename = "unsupported")]
1269 #[default]
1270 Unsupported,
1271 #[serde(rename = "openai-responses-compact")]
1272 OpenAiResponsesCompact,
1273}
1274
1275impl<'de> Deserialize<'de> for OpenAiResponsesCompactionConfig {
1276 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1277 where
1278 D: serde::Deserializer<'de>,
1279 {
1280 match String::deserialize(deserializer)?.as_str() {
1281 "unsupported" => Ok(Self::Unsupported),
1282 "openai-responses-compact" => Ok(Self::OpenAiResponsesCompact),
1283 "v1" => Err(serde::de::Error::custom(
1284 "compaction = \"v1\" was renamed to compaction = \"openai-responses-compact\"",
1285 )),
1286 _ => Err(serde::de::Error::custom(
1287 "compaction must be \"unsupported\" or \"openai-responses-compact\"",
1288 )),
1289 }
1290 }
1291}
1292
1293impl From<OpenAiResponsesCompactionConfig> for OpenAiResponsesCompaction {
1294 fn from(value: OpenAiResponsesCompactionConfig) -> Self {
1295 match value {
1296 OpenAiResponsesCompactionConfig::Unsupported => Self::Unsupported,
1297 OpenAiResponsesCompactionConfig::OpenAiResponsesCompact => Self::V1,
1298 }
1299 }
1300}
1301
1302#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1303#[serde(deny_unknown_fields)]
1304pub struct OpenAiChatOptionsConfig {
1305 pub user: Option<String>,
1306 pub reasoning_effort: Option<String>,
1307 pub service_tier: Option<String>,
1308 pub verbosity: Option<String>,
1309 pub parallel_tool_calls: Option<bool>,
1310}
1311
1312impl From<&OpenAiChatOptionsConfig> for OpenAiChatOptions {
1313 fn from(value: &OpenAiChatOptionsConfig) -> Self {
1314 Self {
1315 user: value.user.clone(),
1316 reasoning_effort: value.reasoning_effort.clone(),
1317 service_tier: value.service_tier.clone(),
1318 verbosity: value.verbosity.clone(),
1319 parallel_tool_calls: value.parallel_tool_calls,
1320 prompt_cache_key: None,
1321 prompt_cache_options: None,
1322 prompt_cache_retention: None,
1323 }
1324 }
1325}
1326
1327#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1328#[serde(deny_unknown_fields)]
1329pub struct OpenAiResponsesOptionsConfig {
1330 #[serde(default)]
1331 pub include: Vec<String>,
1332 pub user: Option<String>,
1333 pub service_tier: Option<String>,
1334 pub verbosity: Option<String>,
1335 pub reasoning_summary: Option<String>,
1336 pub reasoning_mode: Option<String>,
1337 pub truncation: Option<String>,
1338 pub parallel_tool_calls: Option<bool>,
1339}
1340
1341impl From<&OpenAiResponsesOptionsConfig> for OpenAiResponsesOptions {
1342 fn from(value: &OpenAiResponsesOptionsConfig) -> Self {
1343 Self {
1344 include: value.include.clone(),
1345 user: value.user.clone(),
1346 service_tier: value.service_tier.clone(),
1347 verbosity: value.verbosity.clone(),
1348 reasoning_summary: value.reasoning_summary.clone(),
1349 reasoning_mode: value.reasoning_mode.clone(),
1350 truncation: value.truncation.clone(),
1351 parallel_tool_calls: value.parallel_tool_calls,
1352 prompt_cache_key: None,
1353 prompt_cache_options: None,
1354 prompt_cache_retention: None,
1355 }
1356 }
1357}
1358
1359#[derive(Clone, Debug, Deserialize, Serialize)]
1360#[serde(deny_unknown_fields)]
1361pub struct CompatibleSettingsConfig {
1362 pub adapter_id: String,
1363 pub system_message_role: SystemRoleConfig,
1364 pub max_tokens_field: MaxTokensFieldConfig,
1365 pub stream_usage: bool,
1366 pub structured_output: StructuredOutputConfig,
1367 pub reasoning_field: ReasoningFieldConfig,
1368 #[serde(default)]
1369 pub query: BTreeMap<String, String>,
1370 #[serde(default = "default_request_id_headers")]
1371 pub request_id_headers: Vec<String>,
1372 #[serde(default)]
1373 pub strict_sse_content_type: bool,
1374 pub routing_discriminator: Option<String>,
1375 #[serde(default)]
1376 pub timeouts: TimeoutsConfig,
1377}
1378
1379fn default_request_id_headers() -> Vec<String> {
1380 vec!["x-request-id".into()]
1381}
1382
1383impl CompatibleSettingsConfig {
1384 fn to_oven(&self, header_discriminator: Option<&str>) -> OpenAiCompatibleChatSettings {
1385 OpenAiCompatibleChatSettings {
1386 adapter_id: AdapterId::new(self.adapter_id.clone()),
1387 system_message_role: self.system_message_role.into(),
1388 max_tokens_field: self.max_tokens_field.into(),
1389 stream_usage: self.stream_usage,
1390 structured_output: self.structured_output.into(),
1391 reasoning_field: self.reasoning_field.into(),
1392 query: self
1393 .query
1394 .iter()
1395 .map(|(key, value)| (key.clone(), value.clone()))
1396 .collect(),
1397 request_id_headers: self.request_id_headers.clone(),
1398 strict_sse_content_type: self.strict_sse_content_type,
1399 routing_discriminator: combined_routing_discriminator(
1400 self.routing_discriminator.as_deref(),
1401 header_discriminator,
1402 ),
1403 client: self.timeouts.shared_client(),
1404 timeouts: self.timeouts.openai(),
1405 }
1406 }
1407}
1408
1409#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1410#[serde(deny_unknown_fields)]
1411pub struct CompatibleOptionsConfig {
1412 #[serde(default)]
1413 pub extra_body: Map<String, Value>,
1414}
1415
1416#[derive(Clone, Debug, Deserialize, Serialize)]
1417#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1418pub enum GoogleThinkingSettingsConfig {
1419 Unsupported,
1420 Budget {
1421 effort_budgets: BTreeMap<String, i64>,
1422 },
1423 Level {
1424 effort_levels: BTreeMap<String, String>,
1425 },
1426}
1427
1428impl GoogleThinkingSettingsConfig {
1429 fn to_oven(&self) -> GoogleThinkingSettings {
1430 match self {
1431 Self::Unsupported => GoogleThinkingSettings::Unsupported,
1432 Self::Budget { effort_budgets } => GoogleThinkingSettings::Budget {
1433 effort_budgets: effort_budgets.clone(),
1434 },
1435 Self::Level { effort_levels } => GoogleThinkingSettings::Level {
1436 effort_levels: effort_levels.clone(),
1437 },
1438 }
1439 }
1440}
1441
1442#[derive(Clone, Debug, Deserialize, Serialize)]
1443#[serde(deny_unknown_fields)]
1444pub struct GoogleSettingsConfig {
1445 pub model_resource: String,
1446 #[serde(default)]
1447 pub timeouts: TimeoutsConfig,
1448 pub thinking: GoogleThinkingSettingsConfig,
1449 pub strict_functions: bool,
1450 pub mixed_client_and_provider_tools: bool,
1451 pub current_turn_signature_sentinel: bool,
1452}
1453
1454impl GoogleSettingsConfig {
1455 fn to_oven(&self) -> GoogleGenerateContentSettings {
1456 GoogleGenerateContentSettings {
1457 model_resource: self.model_resource.clone(),
1458 timeouts: self.timeouts.google(),
1459 thinking: self.thinking.to_oven(),
1460 tools: GoogleToolSettings {
1461 strict_functions: self.strict_functions,
1462 mixed_client_and_provider_tools: self.mixed_client_and_provider_tools,
1463 current_turn_signature_sentinel: self.current_turn_signature_sentinel,
1464 },
1465 }
1466 }
1467}
1468
1469#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1470#[serde(deny_unknown_fields)]
1471pub struct GoogleThinkingOptionsConfig {
1472 pub thinking_budget: Option<i64>,
1473 pub thinking_level: Option<String>,
1474 pub include_thoughts: Option<bool>,
1475}
1476
1477#[derive(Clone, Debug, Deserialize, Serialize)]
1478#[serde(deny_unknown_fields)]
1479pub struct SafetySettingConfig {
1480 pub category: String,
1481 pub threshold: String,
1482}
1483
1484#[derive(Clone, Debug, Deserialize, Serialize)]
1485#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1486pub enum GoogleProviderToolConfig {
1487 GoogleSearch,
1488 UrlContext,
1489 CodeExecution,
1490 FileSearch { stores: Vec<String> },
1491 GoogleMaps,
1492}
1493
1494impl GoogleProviderToolConfig {
1495 fn to_oven(&self) -> GoogleProviderTool {
1496 match self {
1497 Self::GoogleSearch => GoogleProviderTool::GoogleSearch,
1498 Self::UrlContext => GoogleProviderTool::UrlContext,
1499 Self::CodeExecution => GoogleProviderTool::CodeExecution,
1500 Self::FileSearch { stores } => GoogleProviderTool::FileSearch {
1501 stores: stores.clone(),
1502 },
1503 Self::GoogleMaps => GoogleProviderTool::GoogleMaps,
1504 }
1505 }
1506}
1507
1508#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1509#[serde(deny_unknown_fields)]
1510pub struct GoogleOptionsConfig {
1511 pub top_k: Option<u32>,
1512 #[serde(default)]
1513 pub stop_sequences: Vec<String>,
1514 pub seed: Option<i32>,
1515 pub presence_penalty: Option<f64>,
1516 pub frequency_penalty: Option<f64>,
1517 pub thinking: Option<GoogleThinkingOptionsConfig>,
1518 #[serde(default)]
1519 pub provider_tools: Vec<GoogleProviderToolConfig>,
1520 pub service_tier: Option<String>,
1521 pub cached_content: Option<String>,
1522 #[serde(default)]
1523 pub safety_settings: Vec<SafetySettingConfig>,
1524}
1525
1526impl GoogleOptionsConfig {
1527 fn to_oven(&self) -> GoogleRequestOptions {
1528 GoogleRequestOptions {
1529 top_k: self.top_k,
1530 stop_sequences: self.stop_sequences.clone(),
1531 seed: self.seed,
1532 presence_penalty: self.presence_penalty,
1533 frequency_penalty: self.frequency_penalty,
1534 thinking_config: self.thinking.as_ref().map(|thinking| GoogleThinkingConfig {
1535 thinking_budget: thinking.thinking_budget,
1536 thinking_level: thinking.thinking_level.clone(),
1537 include_thoughts: thinking.include_thoughts,
1538 }),
1539 provider_tools: self
1540 .provider_tools
1541 .iter()
1542 .map(GoogleProviderToolConfig::to_oven)
1543 .collect(),
1544 service_tier: self.service_tier.clone(),
1545 cached_content: self.cached_content.clone(),
1546 safety_settings: self
1547 .safety_settings
1548 .iter()
1549 .map(|setting| GoogleSafetySetting {
1550 category: setting.category.clone(),
1551 threshold: setting.threshold.clone(),
1552 })
1553 .collect(),
1554 }
1555 }
1556}
1557
1558#[derive(Clone, Debug, Deserialize, Serialize)]
1559#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1560pub enum VertexResourceConfig {
1561 PublisherModel { publisher: String, model: String },
1562 Endpoint { endpoint: String },
1563}
1564
1565impl VertexResourceConfig {
1566 fn to_oven(&self) -> GoogleVertexResource {
1567 match self {
1568 Self::PublisherModel { publisher, model } => GoogleVertexResource::PublisherModel {
1569 publisher: publisher.clone(),
1570 model: model.clone(),
1571 },
1572 Self::Endpoint { endpoint } => GoogleVertexResource::Endpoint {
1573 endpoint: endpoint.clone(),
1574 },
1575 }
1576 }
1577}
1578
1579#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
1580#[serde(rename_all = "snake_case")]
1581pub enum VertexThinkingModeConfig {
1582 Unsupported,
1583 Budget,
1584 Level,
1585}
1586
1587impl From<VertexThinkingModeConfig> for GoogleVertexThinkingMode {
1588 fn from(value: VertexThinkingModeConfig) -> Self {
1589 match value {
1590 VertexThinkingModeConfig::Unsupported => Self::Unsupported,
1591 VertexThinkingModeConfig::Budget => Self::Budget,
1592 VertexThinkingModeConfig::Level => Self::Level,
1593 }
1594 }
1595}
1596
1597#[derive(Clone, Debug, Deserialize, Serialize)]
1598#[serde(deny_unknown_fields)]
1599pub struct VertexMediaConfig {
1600 pub max_images: usize,
1601 pub max_https_images: usize,
1602 pub max_documents: usize,
1603 pub max_audio: usize,
1604 pub max_videos: usize,
1605 pub max_https_videos: usize,
1606 pub max_inline_image_bytes: usize,
1607 pub max_inline_pdf_bytes: usize,
1608 pub max_inline_text_bytes: usize,
1609 pub url_schemes: Vec<String>,
1610}
1611
1612#[derive(Clone, Debug, Deserialize, Serialize)]
1613#[serde(deny_unknown_fields)]
1614pub struct VertexSettingsConfig {
1615 pub project: String,
1616 pub location: String,
1617 pub resource: VertexResourceConfig,
1618 pub thinking: VertexThinkingModeConfig,
1619 pub provider_tools: bool,
1620 pub mixed_client_and_provider_tools: bool,
1621 pub strict_functions: bool,
1622 pub stream_function_call_arguments: bool,
1623 pub media: VertexMediaConfig,
1624 #[serde(default)]
1625 pub timeouts: TimeoutsConfig,
1626}
1627
1628impl VertexSettingsConfig {
1629 fn to_oven(
1630 &self,
1631 provider: &ProviderConfig<VertexAuth>,
1632 declaration: &ModelDeclaration,
1633 ) -> Result<GoogleVertexSettings, ModelBuildError> {
1634 let resource = self.resource.to_oven();
1635 let native_context_scope = google_vertex_native_context_scope(
1636 provider.id.clone(),
1637 declaration.id.clone(),
1638 &provider.api,
1639 &self.project,
1640 &self.location,
1641 &resource,
1642 )?;
1643 Ok(GoogleVertexSettings {
1644 project: self.project.clone(),
1645 location: self.location.clone(),
1646 resource,
1647 thinking: self.thinking.into(),
1648 tools: GoogleVertexToolSettings {
1649 provider_tools: self.provider_tools,
1650 mixed_client_and_provider_tools: self.mixed_client_and_provider_tools,
1651 strict_functions: self.strict_functions,
1652 },
1653 stream_function_call_arguments: self.stream_function_call_arguments,
1654 media: GoogleVertexMediaSettings {
1655 max_images: self.media.max_images,
1656 max_https_images: self.media.max_https_images,
1657 max_documents: self.media.max_documents,
1658 max_audio: self.media.max_audio,
1659 max_videos: self.media.max_videos,
1660 max_https_videos: self.media.max_https_videos,
1661 max_inline_image_bytes: self.media.max_inline_image_bytes,
1662 max_inline_pdf_bytes: self.media.max_inline_pdf_bytes,
1663 max_inline_text_bytes: self.media.max_inline_text_bytes,
1664 url_schemes: self.media.url_schemes.clone(),
1665 },
1666 native_context_scope,
1667 client: self.timeouts.shared_client(),
1668 timeouts: self.timeouts.vertex(),
1669 })
1670 }
1671}
1672
1673#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1674#[serde(deny_unknown_fields)]
1675pub struct VertexThinkingOptionsConfig {
1676 pub thinking_budget: Option<i64>,
1677 pub thinking_level: Option<String>,
1678 pub include_thoughts: Option<bool>,
1679}
1680
1681#[derive(Clone, Debug, Deserialize, Serialize)]
1682#[serde(deny_unknown_fields)]
1683pub struct VertexSafetySettingConfig {
1684 pub category: String,
1685 pub threshold: String,
1686 pub method: Option<String>,
1687}
1688
1689#[derive(Clone, Debug, Deserialize, Serialize)]
1690#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1691pub enum VertexProviderToolConfig {
1692 GoogleSearch,
1693 UrlContext,
1694 CodeExecution,
1695 VertexRagStore {
1696 rag_corpus: String,
1697 top_k: Option<u32>,
1698 },
1699 GoogleMaps,
1700}
1701
1702impl VertexProviderToolConfig {
1703 fn to_oven(&self) -> GoogleVertexProviderTool {
1704 match self {
1705 Self::GoogleSearch => GoogleVertexProviderTool::GoogleSearch,
1706 Self::UrlContext => GoogleVertexProviderTool::UrlContext,
1707 Self::CodeExecution => GoogleVertexProviderTool::CodeExecution,
1708 Self::VertexRagStore { rag_corpus, top_k } => {
1709 GoogleVertexProviderTool::VertexRagStore {
1710 rag_corpus: rag_corpus.clone(),
1711 top_k: *top_k,
1712 }
1713 }
1714 Self::GoogleMaps => GoogleVertexProviderTool::GoogleMaps,
1715 }
1716 }
1717}
1718
1719#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1720#[serde(deny_unknown_fields)]
1721pub struct VertexOptionsConfig {
1722 pub top_k: Option<f64>,
1723 #[serde(default)]
1724 pub stop_sequences: Vec<String>,
1725 pub seed: Option<i32>,
1726 pub presence_penalty: Option<f64>,
1727 pub frequency_penalty: Option<f64>,
1728 pub thinking: Option<VertexThinkingOptionsConfig>,
1729 #[serde(default)]
1730 pub provider_tools: Vec<VertexProviderToolConfig>,
1731 pub cached_content: Option<String>,
1732 #[serde(default)]
1733 pub safety_settings: Vec<VertexSafetySettingConfig>,
1734 pub shared_request_type: Option<String>,
1735 pub request_type: Option<String>,
1736}
1737
1738impl VertexOptionsConfig {
1739 fn to_oven(&self) -> GoogleVertexRequestOptions {
1740 GoogleVertexRequestOptions {
1741 top_k: self.top_k,
1742 stop_sequences: self.stop_sequences.clone(),
1743 seed: self.seed,
1744 presence_penalty: self.presence_penalty,
1745 frequency_penalty: self.frequency_penalty,
1746 thinking_config: self
1747 .thinking
1748 .as_ref()
1749 .map(|thinking| GoogleVertexThinkingConfig {
1750 thinking_budget: thinking.thinking_budget,
1751 thinking_level: thinking.thinking_level.clone(),
1752 include_thoughts: thinking.include_thoughts,
1753 }),
1754 provider_tools: self
1755 .provider_tools
1756 .iter()
1757 .map(VertexProviderToolConfig::to_oven)
1758 .collect(),
1759 cached_content: self.cached_content.clone(),
1760 safety_settings: self
1761 .safety_settings
1762 .iter()
1763 .map(|setting| GoogleVertexSafetySetting {
1764 category: setting.category.clone(),
1765 threshold: setting.threshold.clone(),
1766 method: setting.method.clone(),
1767 })
1768 .collect(),
1769 shared_request_type: self.shared_request_type.clone(),
1770 request_type: self.request_type.clone(),
1771 }
1772 }
1773}
1774
1775#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
1776#[serde(rename_all = "snake_case")]
1777pub enum BedrockReasoningFormatConfig {
1778 Unsupported,
1779 AnthropicThinking,
1780 OpenaiReasoningEffort,
1781 BedrockReasoningConfig,
1782}
1783
1784impl From<BedrockReasoningFormatConfig> for BedrockReasoningWireFormat {
1785 fn from(value: BedrockReasoningFormatConfig) -> Self {
1786 match value {
1787 BedrockReasoningFormatConfig::Unsupported => Self::Unsupported,
1788 BedrockReasoningFormatConfig::AnthropicThinking => Self::AnthropicThinking,
1789 BedrockReasoningFormatConfig::OpenaiReasoningEffort => Self::OpenAiReasoningEffort,
1790 BedrockReasoningFormatConfig::BedrockReasoningConfig => Self::BedrockReasoningConfig,
1791 }
1792 }
1793}
1794
1795#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
1796#[serde(rename_all = "snake_case")]
1797pub enum BedrockStructuredOutputConfig {
1798 Unsupported,
1799 JsonSchema,
1800}
1801
1802impl From<BedrockStructuredOutputConfig> for BedrockStructuredOutput {
1803 fn from(value: BedrockStructuredOutputConfig) -> Self {
1804 match value {
1805 BedrockStructuredOutputConfig::Unsupported => Self::Unsupported,
1806 BedrockStructuredOutputConfig::JsonSchema => Self::JsonSchema,
1807 }
1808 }
1809}
1810
1811#[derive(Clone, Debug, Deserialize, Serialize)]
1812#[serde(deny_unknown_fields)]
1813pub struct BedrockSettingsConfig {
1814 pub region: String,
1815 pub reasoning_wire_format: BedrockReasoningFormatConfig,
1816 pub signed_reasoning: bool,
1817 pub structured_output: BedrockStructuredOutputConfig,
1818 pub max_event_message_bytes: usize,
1819 #[serde(default)]
1820 pub timeouts: TimeoutsConfig,
1821}
1822
1823impl BedrockSettingsConfig {
1824 fn to_oven(&self) -> BedrockConverseSettings {
1825 BedrockConverseSettings {
1826 region: self.region.clone(),
1827 reasoning_wire_format: self.reasoning_wire_format.into(),
1828 signed_reasoning: self.signed_reasoning,
1829 structured_output: self.structured_output.into(),
1830 event_stream: BedrockEventStreamLimits::new(self.max_event_message_bytes),
1831 timeouts: self.timeouts.bedrock(),
1832 client: self.timeouts.shared_client(),
1833 }
1834 }
1835}
1836
1837#[derive(Clone, Debug, Deserialize, Serialize)]
1838#[serde(deny_unknown_fields)]
1839pub struct BedrockGuardrailConfigInput {
1840 pub guardrail_identifier: String,
1841 pub guardrail_version: String,
1842 pub trace: Option<String>,
1843 pub stream_processing_mode: Option<String>,
1844}
1845
1846#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1847#[serde(deny_unknown_fields)]
1848pub struct BedrockOptionsConfig {
1849 pub additional_model_request_fields: Option<Value>,
1850 #[serde(default)]
1851 pub additional_model_response_field_paths: Vec<String>,
1852 pub service_tier: Option<String>,
1853 pub performance_latency: Option<String>,
1854 #[serde(default)]
1855 pub request_metadata: BTreeMap<String, String>,
1856 pub guardrail: Option<BedrockGuardrailConfigInput>,
1857 pub s3_bucket_owner: Option<String>,
1858 #[serde(default)]
1859 pub stop_sequences: Vec<String>,
1860 pub reasoning_type: Option<String>,
1861 pub reasoning_budget_tokens: Option<u64>,
1862 pub reasoning_display: Option<String>,
1863 pub max_reasoning_effort: Option<String>,
1864}
1865
1866impl BedrockOptionsConfig {
1867 fn to_oven(&self) -> BedrockRequestOptions {
1868 BedrockRequestOptions {
1869 cache: None,
1870 additional_model_request_fields: self.additional_model_request_fields.clone(),
1871 additional_model_response_field_paths: self
1872 .additional_model_response_field_paths
1873 .clone(),
1874 service_tier: self.service_tier.clone(),
1875 performance_latency: self.performance_latency.clone(),
1876 request_metadata: self.request_metadata.clone(),
1877 guardrail: self
1878 .guardrail
1879 .as_ref()
1880 .map(|guardrail| BedrockGuardrailConfig {
1881 guardrail_identifier: guardrail.guardrail_identifier.clone(),
1882 guardrail_version: guardrail.guardrail_version.clone(),
1883 trace: guardrail.trace.clone(),
1884 stream_processing_mode: guardrail.stream_processing_mode.clone(),
1885 }),
1886 s3: self
1887 .s3_bucket_owner
1888 .as_ref()
1889 .map(|owner| BedrockS3LocationOptions {
1890 bucket_owner: Some(owner.clone()),
1891 }),
1892 stop_sequences: self.stop_sequences.clone(),
1893 reasoning_type: self.reasoning_type.clone(),
1894 reasoning_budget_tokens: self.reasoning_budget_tokens,
1895 reasoning_display: self.reasoning_display.clone(),
1896 max_reasoning_effort: self.max_reasoning_effort.clone(),
1897 }
1898 }
1899}
1900
1901#[derive(Clone, Debug, Deserialize, Serialize)]
1902#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1903pub enum AzureRouteConfig {
1904 V1,
1905 V1Preview,
1906 Dated { version: String },
1907}
1908
1909impl AzureRouteConfig {
1910 fn to_oven(&self) -> Result<AzureApiRoute, ModelBuildError> {
1911 Ok(match self {
1912 Self::V1 => AzureApiRoute::V1,
1913 Self::V1Preview => AzureApiRoute::V1Preview,
1914 Self::Dated { version } => AzureApiRoute::Dated(AzureApiVersion::new(version.clone())?),
1915 })
1916 }
1917}
1918
1919#[derive(Clone, Debug, Deserialize, Serialize)]
1920#[serde(deny_unknown_fields)]
1921pub struct AzureRevisionConfig {
1922 pub model: String,
1923 pub version: String,
1924 pub deployment_type: String,
1925}
1926
1927impl AzureRevisionConfig {
1928 fn to_oven(&self) -> AzureOpenAiRevision {
1929 AzureOpenAiRevision {
1930 model: self.model.clone(),
1931 version: self.version.clone(),
1932 deployment_type: self.deployment_type.clone(),
1933 }
1934 }
1935}
1936
1937#[derive(Clone, Debug, Deserialize, Serialize)]
1938#[serde(deny_unknown_fields)]
1939pub struct AzureChatSettingsConfig {
1940 pub route: AzureRouteConfig,
1941 pub revision: Option<AzureRevisionConfig>,
1942 #[serde(default)]
1943 pub timeouts: TimeoutsConfig,
1944 pub system_role: SystemRoleConfig,
1945 pub max_tokens_field: MaxTokensFieldConfig,
1946 pub stream_usage: bool,
1947 pub structured_output: StructuredOutputConfig,
1948 pub reasoning_field: ReasoningFieldConfig,
1949 pub omit_reasoning_sampling: bool,
1950}
1951
1952impl AzureChatSettingsConfig {
1953 fn to_chat(&self) -> Result<AzureOpenAiChatSettings, ModelBuildError> {
1954 Ok(AzureOpenAiChatSettings {
1955 route: self.route.to_oven()?,
1956 revision: self.revision.as_ref().map(AzureRevisionConfig::to_oven),
1957 timeouts: self.timeouts.azure(),
1958 completions: AzureOpenAiCompletionsConfig {
1959 system_role: self.system_role.into(),
1960 max_tokens_field: self.max_tokens_field.into(),
1961 stream_usage: self.stream_usage,
1962 structured_output: self.structured_output.into(),
1963 reasoning_field: self.reasoning_field.into(),
1964 omit_reasoning_sampling: self.omit_reasoning_sampling,
1965 },
1966 })
1967 }
1968}
1969
1970#[derive(Clone, Debug, Deserialize, Serialize)]
1971#[serde(deny_unknown_fields)]
1972pub struct AzureResponsesSettingsConfig {
1973 pub route: AzureRouteConfig,
1974 pub revision: Option<AzureRevisionConfig>,
1975 #[serde(default)]
1976 pub compaction: AzureResponsesCompactionConfig,
1977 #[serde(default)]
1978 pub timeouts: TimeoutsConfig,
1979}
1980
1981impl AzureResponsesSettingsConfig {
1982 fn to_responses(&self) -> Result<AzureOpenAiResponsesSettings, ModelBuildError> {
1983 Ok(AzureOpenAiResponsesSettings {
1984 route: self.route.to_oven()?,
1985 revision: self.revision.as_ref().map(AzureRevisionConfig::to_oven),
1986 timeouts: self.timeouts.azure(),
1987 compaction: self.compaction.to_oven(),
1988 })
1989 }
1990}
1991
1992#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1993#[serde(tag = "kind", deny_unknown_fields)]
1994pub enum AzureResponsesCompactionConfig {
1995 #[serde(rename = "unsupported")]
1996 #[default]
1997 Unsupported,
1998 #[serde(rename = "azure-responses-compact")]
1999 AzureResponsesCompact { routing_discriminator: String },
2000}
2001
2002impl AzureResponsesCompactionConfig {
2003 fn to_oven(&self) -> AzureOpenAiResponsesCompaction {
2004 match self {
2005 Self::Unsupported => AzureOpenAiResponsesCompaction::Unsupported,
2006 Self::AzureResponsesCompact {
2007 routing_discriminator,
2008 } => AzureOpenAiResponsesCompaction::V1 {
2009 routing_discriminator: routing_discriminator.clone(),
2010 },
2011 }
2012 }
2013}
2014
2015#[derive(Clone, Debug, Default, Deserialize, Serialize)]
2016#[serde(deny_unknown_fields)]
2017pub struct AzureChatOptionsConfig {
2018 pub user: Option<String>,
2019 pub reasoning_effort: Option<String>,
2020 pub service_tier: Option<String>,
2021 pub verbosity: Option<String>,
2022 pub parallel_tool_calls: Option<bool>,
2023}
2024
2025impl From<&AzureChatOptionsConfig> for AzureOpenAiChatOptions {
2026 fn from(value: &AzureChatOptionsConfig) -> Self {
2027 Self {
2028 user: value.user.clone(),
2029 reasoning_effort: value.reasoning_effort.clone(),
2030 service_tier: value.service_tier.clone(),
2031 verbosity: value.verbosity.clone(),
2032 parallel_tool_calls: value.parallel_tool_calls,
2033 prompt_cache_key: None,
2034 prompt_cache_options: None,
2035 prompt_cache_retention: None,
2036 }
2037 }
2038}
2039
2040#[derive(Clone, Debug, Default, Deserialize, Serialize)]
2041#[serde(deny_unknown_fields)]
2042pub struct AzureResponsesOptionsConfig {
2043 #[serde(default)]
2044 pub include: Vec<String>,
2045 pub user: Option<String>,
2046 pub service_tier: Option<String>,
2047 pub verbosity: Option<String>,
2048 pub reasoning_summary: Option<String>,
2049 pub reasoning_mode: Option<String>,
2050 pub truncation: Option<String>,
2051 pub parallel_tool_calls: Option<bool>,
2052}
2053
2054impl From<&AzureResponsesOptionsConfig> for AzureOpenAiResponsesOptions {
2055 fn from(value: &AzureResponsesOptionsConfig) -> Self {
2056 Self {
2057 include: value.include.clone(),
2058 user: value.user.clone(),
2059 service_tier: value.service_tier.clone(),
2060 verbosity: value.verbosity.clone(),
2061 reasoning_summary: value.reasoning_summary.clone(),
2062 reasoning_mode: value.reasoning_mode.clone(),
2063 truncation: value.truncation.clone(),
2064 parallel_tool_calls: value.parallel_tool_calls,
2065 prompt_cache_key: None,
2066 prompt_cache_options: None,
2067 prompt_cache_retention: None,
2068 }
2069 }
2070}
2071
2072#[derive(Clone, Debug, Deserialize, Serialize)]
2073#[serde(deny_unknown_fields)]
2074pub struct CohereThinkingConfig {
2075 pub enabled: bool,
2076 pub token_budget: Option<u64>,
2077}
2078
2079impl CohereThinkingConfig {
2080 fn to_oven(&self) -> CohereThinking {
2081 CohereThinking {
2082 enabled: self.enabled,
2083 token_budget: self.token_budget,
2084 }
2085 }
2086}
2087
2088#[derive(Clone, Debug, Default, Deserialize, Serialize)]
2089#[serde(deny_unknown_fields)]
2090pub struct CohereSettingsConfig {
2091 #[serde(default)]
2092 pub timeouts: TimeoutsConfig,
2093 pub strict_tools: bool,
2094 pub safety_mode: Option<String>,
2095 pub thinking: Option<CohereThinkingConfig>,
2096 #[serde(default)]
2097 pub reasoning_effort: BTreeMap<String, CohereThinkingConfig>,
2098 pub top_k: Option<u32>,
2099 pub seed: Option<u64>,
2100 pub frequency_penalty: Option<f64>,
2101 pub presence_penalty: Option<f64>,
2102 #[serde(default)]
2103 pub stop_sequences: Vec<String>,
2104 pub priority: Option<i64>,
2105}
2106
2107impl CohereSettingsConfig {
2108 fn to_oven(&self) -> CohereSettings {
2109 CohereSettings {
2110 timeouts: self.timeouts.cohere(),
2111 strict_tools: self.strict_tools,
2112 safety_mode: self.safety_mode.clone(),
2113 thinking: self.thinking.as_ref().map(CohereThinkingConfig::to_oven),
2114 reasoning_effort: self
2115 .reasoning_effort
2116 .iter()
2117 .map(|(effort, thinking)| (effort.clone(), thinking.to_oven()))
2118 .collect(),
2119 top_k: self.top_k,
2120 seed: self.seed,
2121 frequency_penalty: self.frequency_penalty,
2122 presence_penalty: self.presence_penalty,
2123 stop_sequences: self.stop_sequences.clone(),
2124 priority: self.priority,
2125 }
2126 }
2127}
2128
2129#[derive(Clone, Debug, Deserialize, Serialize)]
2130#[serde(deny_unknown_fields)]
2131pub struct CohereDocumentConfig {
2132 pub id: Option<String>,
2133 pub data: Map<String, Value>,
2134}
2135
2136#[derive(Clone, Debug, Default, Deserialize, Serialize)]
2137#[serde(deny_unknown_fields)]
2138pub struct CohereOptionsConfig {
2139 #[serde(default)]
2140 pub documents: Vec<CohereDocumentConfig>,
2141 pub citation_mode: Option<String>,
2142 pub image_detail: Option<String>,
2143}
2144
2145impl CohereOptionsConfig {
2146 fn to_oven(&self) -> CohereRequestOptions {
2147 CohereRequestOptions {
2148 documents: self
2149 .documents
2150 .iter()
2151 .map(|document| CohereDocument {
2152 id: document.id.clone(),
2153 data: document.data.clone(),
2154 })
2155 .collect(),
2156 citation_mode: self.citation_mode.clone(),
2157 image_detail: self.image_detail.clone(),
2158 }
2159 }
2160}
2161
2162#[cfg(test)]
2163mod tests {
2164 use super::*;
2165
2166 #[test]
2167 fn auth_routing_discriminator_composes_with_effective_header_templates() {
2168 let settings = OpenAiResponsesSettingsConfig {
2169 routing_discriminator: Some("header:x-api-key".into()),
2170 ..OpenAiResponsesSettingsConfig::default()
2171 };
2172 let one = header_routing_discriminator(&BTreeMap::from([(
2173 "x-route".into(),
2174 "route-one-secret".into(),
2175 )]));
2176 let two = header_routing_discriminator(&BTreeMap::from([(
2177 "x-route".into(),
2178 "route-two-secret".into(),
2179 )]));
2180 let first = settings
2181 .to_openai_responses(Some(&one))
2182 .routing_discriminator
2183 .unwrap();
2184 assert_ne!(
2185 first,
2186 settings
2187 .to_openai_responses(Some(&two))
2188 .routing_discriminator
2189 .unwrap()
2190 );
2191 assert_ne!(
2192 first,
2193 settings
2194 .to_openai_responses(None)
2195 .routing_discriminator
2196 .unwrap()
2197 );
2198 assert!(!first.contains("route-one-secret"));
2199 let other_auth = OpenAiResponsesSettingsConfig {
2200 routing_discriminator: Some("header:api-key".into()),
2201 ..settings
2202 };
2203 assert_ne!(
2204 first,
2205 other_auth
2206 .to_openai_responses(Some(&one))
2207 .routing_discriminator
2208 .unwrap()
2209 );
2210 }
2211
2212 #[test]
2213 fn session_header_templates_resolve_from_request_context() {
2214 let headers = TemplateHeaderProvider::new(&BTreeMap::from([
2215 ("x-session-id".into(), "${session_id}".into()),
2216 ("x-session-parent-id".into(), "${parent_session_id}".into()),
2217 ]))
2218 .unwrap();
2219 let root = headers.headers(&HeaderContext::new("root")).unwrap();
2220 assert_eq!(
2221 root.as_map().get("x-session-id").unwrap(),
2222 &HeaderValue::from_static("root")
2223 );
2224 assert!(!root.as_map().contains_key("x-session-parent-id"));
2225 let child = headers
2226 .headers(&HeaderContext::new("child").with_parent_session_id("root"))
2227 .unwrap();
2228 assert_eq!(child.as_map()["x-session-parent-id"], "root");
2229 }
2230
2231 #[test]
2232 fn literal_header_templates_are_prevalidated_and_stored_as_values() {
2233 let headers = TemplateHeaderProvider::new(&BTreeMap::from([
2234 ("x-literal".into(), "fixed$$value".into()),
2235 ("x-empty".into(), String::new()),
2236 ]))
2237 .unwrap();
2238 assert!(
2239 headers
2240 .templates
2241 .iter()
2242 .all(|(_, template)| matches!(template, HeaderTemplate::Static(_)))
2243 );
2244 let resolved = headers.headers(&HeaderContext::new("unused")).unwrap();
2245 assert_eq!(resolved.as_map()["x-literal"], "fixed$value");
2246 assert!(!resolved.as_map().contains_key("x-empty"));
2247 }
2248
2249 #[test]
2250 fn malformed_header_templates_fail_during_model_construction() {
2251 let error = TemplateHeaderProvider::new(&BTreeMap::from([(
2252 "x-invalid".into(),
2253 "${env:UNFINISHED".into(),
2254 )]))
2255 .err()
2256 .expect("invalid template");
2257 assert!(matches!(error, ModelBuildError::HeaderTemplate(name) if name == "x-invalid"));
2258 }
2259
2260 #[test]
2261 fn responses_compaction_settings_use_explicit_adapter_names() {
2262 let openai: OpenAiResponsesSettingsConfig = serde_json::from_value(json!({
2263 "routing_discriminator": null,
2264 "compaction": "openai-responses-compact"
2265 }))
2266 .expect("OpenAI compaction setting");
2267 assert!(matches!(
2268 openai.compaction,
2269 OpenAiResponsesCompactionConfig::OpenAiResponsesCompact
2270 ));
2271 let azure: AzureResponsesCompactionConfig = serde_json::from_value(json!({
2272 "kind": "azure-responses-compact",
2273 "routing_discriminator": "stable-route"
2274 }))
2275 .expect("Azure compaction setting");
2276 assert!(matches!(
2277 azure,
2278 AzureResponsesCompactionConfig::AzureResponsesCompact { .. }
2279 ));
2280 }
2281
2282 #[test]
2283 fn legacy_v1_compaction_settings_fail_with_migration_message() {
2284 let error = serde_json::from_value::<OpenAiResponsesCompactionConfig>(json!("v1"))
2285 .expect_err("legacy OpenAI setting must fail");
2286 assert!(error.to_string().contains("openai-responses-compact"));
2287 let error = serde_json::from_value::<AzureResponsesCompactionConfig>(json!({
2288 "kind": "v1",
2289 "routing_discriminator": "stable-route"
2290 }))
2291 .expect_err("legacy Azure setting must fail");
2292 assert!(error.to_string().contains("azure-responses-compact"));
2293 }
2294}