1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7
8const OPENAI_PROMPT_CACHE_KEY_LIMIT: usize = 64;
9
10#[must_use]
12pub fn format_openai_prompt_cache_key(affinity_id: &str) -> Option<String> {
13 let mut key = String::from("sw_");
14 for ch in affinity_id.trim().chars() {
15 if key.len() >= OPENAI_PROMPT_CACHE_KEY_LIMIT {
16 break;
17 }
18 key.push(
19 if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.') {
20 ch
21 } else {
22 '_'
23 },
24 );
25 }
26 (key.len() > "sw_".len()).then_some(key)
27}
28
29fn normalized_openai_model_name(model_name: &str) -> String {
30 let model_name = model_name.trim().to_ascii_lowercase();
31 let after_namespace = model_name
32 .rsplit_once(':')
33 .map_or(model_name.as_str(), |(_, model)| model);
34 after_namespace
35 .rsplit_once('/')
36 .map_or(after_namespace, |(_, model)| model)
37 .to_string()
38}
39
40#[must_use]
42pub fn supports_automatic_openai_prompt_cache_key(model_name: &str) -> bool {
43 let model = normalized_openai_model_name(model_name);
44 model.starts_with("gpt-")
45 || model.starts_with("chatgpt-")
46 || model.starts_with("o1")
47 || model.starts_with("o3")
48 || model.starts_with("o4")
49}
50
51#[must_use]
53pub fn supports_openai_prompt_cache_breakpoints(model_name: &str) -> bool {
54 let model_name = normalized_openai_model_name(model_name);
55 model_name.starts_with("gpt-5.6") || model_name.starts_with("gpt-5-6")
56}
57
58#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
60pub struct ModelSettings {
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub max_tokens: Option<u32>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub temperature: Option<f64>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub top_p: Option<f64>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub top_k: Option<u32>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub timeout_ms: Option<u64>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub parallel_tool_calls: Option<bool>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub tool_choice: Option<ToolChoice>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub seed: Option<u64>,
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub stop_sequences: Vec<String>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub presence_penalty: Option<f64>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub frequency_penalty: Option<f64>,
97 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
99 pub logit_bias: BTreeMap<String, i32>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub thinking: Option<ThinkingSettings>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub service_tier: Option<ServiceTier>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub provider_replay: Option<ProviderReplaySettings>,
109 #[serde(default, skip_serializing_if = "ProviderSettings::is_empty")]
111 pub provider_settings: ProviderSettings,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub provider_options: Option<Value>,
115 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
117 pub extra_headers: BTreeMap<String, String>,
118 #[serde(default, skip_serializing_if = "Map::is_empty")]
120 pub extra_body: Map<String, Value>,
121}
122
123impl ModelSettings {
124 #[must_use]
126 pub fn merge(&self, overlay: &Self) -> Self {
127 Self {
128 max_tokens: overlay.max_tokens.or(self.max_tokens),
129 temperature: overlay.temperature.or(self.temperature),
130 top_p: overlay.top_p.or(self.top_p),
131 top_k: overlay.top_k.or(self.top_k),
132 timeout_ms: overlay.timeout_ms.or(self.timeout_ms),
133 parallel_tool_calls: overlay.parallel_tool_calls.or(self.parallel_tool_calls),
134 tool_choice: overlay
135 .tool_choice
136 .clone()
137 .or_else(|| self.tool_choice.clone()),
138 seed: overlay.seed.or(self.seed),
139 stop_sequences: if overlay.stop_sequences.is_empty() {
140 self.stop_sequences.clone()
141 } else {
142 overlay.stop_sequences.clone()
143 },
144 presence_penalty: overlay.presence_penalty.or(self.presence_penalty),
145 frequency_penalty: overlay.frequency_penalty.or(self.frequency_penalty),
146 logit_bias: if overlay.logit_bias.is_empty() {
147 self.logit_bias.clone()
148 } else {
149 let mut logit_bias = self.logit_bias.clone();
150 logit_bias.extend(overlay.logit_bias.clone());
151 logit_bias
152 },
153 thinking: overlay.thinking.clone().or_else(|| self.thinking.clone()),
154 service_tier: overlay
155 .service_tier
156 .clone()
157 .or_else(|| self.service_tier.clone()),
158 provider_settings: self.provider_settings.merge(&overlay.provider_settings),
159 provider_replay: overlay
160 .provider_replay
161 .clone()
162 .or_else(|| self.provider_replay.clone()),
163 provider_options: overlay
164 .provider_options
165 .clone()
166 .or_else(|| self.provider_options.clone()),
167 extra_headers: if overlay.extra_headers.is_empty() {
168 self.extra_headers.clone()
169 } else {
170 overlay_headers_case_insensitive(&self.extra_headers, &overlay.extra_headers)
171 },
172 extra_body: if overlay.extra_body.is_empty() {
173 self.extra_body.clone()
174 } else {
175 let mut body = self.extra_body.clone();
176 body.extend(overlay.extra_body.clone());
177 body
178 },
179 }
180 }
181}
182
183#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
185pub struct ProviderSettings {
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub openai_chat: Option<OpenAiChatSettings>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub openai_responses: Option<OpenAiResponsesSettings>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub anthropic: Option<AnthropicSettings>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub google: Option<GoogleSettings>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub bedrock: Option<BedrockSettings>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub codex: Option<CodexSettings>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub gateway: Option<GatewaySettings>,
207}
208
209impl ProviderSettings {
210 #[must_use]
212 pub const fn is_empty(&self) -> bool {
213 self.openai_chat.is_none()
214 && self.openai_responses.is_none()
215 && self.anthropic.is_none()
216 && self.google.is_none()
217 && self.bedrock.is_none()
218 && self.codex.is_none()
219 && self.gateway.is_none()
220 }
221
222 #[must_use]
224 pub fn merge(&self, overlay: &Self) -> Self {
225 Self {
226 openai_chat: merge_openai_chat(self.openai_chat.as_ref(), overlay.openai_chat.as_ref()),
227 openai_responses: merge_openai_responses(
228 self.openai_responses.as_ref(),
229 overlay.openai_responses.as_ref(),
230 ),
231 anthropic: merge_anthropic(self.anthropic.as_ref(), overlay.anthropic.as_ref()),
232 google: merge_google(self.google.as_ref(), overlay.google.as_ref()),
233 bedrock: merge_bedrock(self.bedrock.as_ref(), overlay.bedrock.as_ref()),
234 codex: merge_codex(self.codex.as_ref(), overlay.codex.as_ref()),
235 gateway: merge_gateway(self.gateway.as_ref(), overlay.gateway.as_ref()),
236 }
237 }
238}
239
240fn merge_option<T, F>(base: Option<&T>, overlay: Option<&T>, merge: F) -> Option<T>
241where
242 T: Clone,
243 F: FnOnce(&T, &T) -> T,
244{
245 match (base, overlay) {
246 (Some(base), Some(overlay)) => Some(merge(base, overlay)),
247 (Some(base), None) => Some(base.clone()),
248 (None, Some(overlay)) => Some(overlay.clone()),
249 (None, None) => None,
250 }
251}
252
253fn overlay_vec<T: Clone>(base: &[T], overlay: &[T]) -> Vec<T> {
254 if overlay.is_empty() {
255 base.to_vec()
256 } else {
257 overlay.to_vec()
258 }
259}
260
261fn overlay_headers_case_insensitive(
262 base: &BTreeMap<String, String>,
263 overlay: &BTreeMap<String, String>,
264) -> BTreeMap<String, String> {
265 let mut merged = base.clone();
266 for (key, value) in overlay {
267 merged.retain(|existing, _| !existing.eq_ignore_ascii_case(key));
268 merged.insert(key.clone(), value.clone());
269 }
270 merged
271}
272
273fn merge_openai_chat(
274 base: Option<&OpenAiChatSettings>,
275 overlay: Option<&OpenAiChatSettings>,
276) -> Option<OpenAiChatSettings> {
277 merge_option(base, overlay, |base, overlay| OpenAiChatSettings {
278 user: overlay.user.clone().or_else(|| base.user.clone()),
279 store: overlay.store.or(base.store),
280 logprobs: overlay.logprobs.or(base.logprobs),
281 top_logprobs: overlay.top_logprobs.or(base.top_logprobs),
282 prediction: overlay
283 .prediction
284 .clone()
285 .or_else(|| base.prediction.clone()),
286 prompt_cache_key: overlay
287 .prompt_cache_key
288 .clone()
289 .or_else(|| base.prompt_cache_key.clone()),
290 prompt_cache_retention: overlay
291 .prompt_cache_retention
292 .clone()
293 .or_else(|| base.prompt_cache_retention.clone()),
294 prompt_cache_options: overlay
295 .prompt_cache_options
296 .clone()
297 .or_else(|| base.prompt_cache_options.clone()),
298 })
299}
300
301fn merge_openai_responses(
302 base: Option<&OpenAiResponsesSettings>,
303 overlay: Option<&OpenAiResponsesSettings>,
304) -> Option<OpenAiResponsesSettings> {
305 merge_option(base, overlay, |base, overlay| OpenAiResponsesSettings {
306 store: overlay.store.or(base.store),
307 user: overlay.user.clone().or_else(|| base.user.clone()),
308 truncation: overlay
309 .truncation
310 .clone()
311 .or_else(|| base.truncation.clone()),
312 text_verbosity: overlay
313 .text_verbosity
314 .clone()
315 .or_else(|| base.text_verbosity.clone()),
316 context_management: overlay
317 .context_management
318 .clone()
319 .or_else(|| base.context_management.clone()),
320 include: overlay_vec(&base.include, &overlay.include),
321 prompt_cache_key: overlay
322 .prompt_cache_key
323 .clone()
324 .or_else(|| base.prompt_cache_key.clone()),
325 prompt_cache_retention: overlay
326 .prompt_cache_retention
327 .clone()
328 .or_else(|| base.prompt_cache_retention.clone()),
329 prompt_cache_options: overlay
330 .prompt_cache_options
331 .clone()
332 .or_else(|| base.prompt_cache_options.clone()),
333 stream_transport: overlay.stream_transport.or(base.stream_transport),
334 })
335}
336
337fn merge_anthropic(
338 base: Option<&AnthropicSettings>,
339 overlay: Option<&AnthropicSettings>,
340) -> Option<AnthropicSettings> {
341 merge_option(base, overlay, |base, overlay| AnthropicSettings {
342 metadata: overlay.metadata.clone().or_else(|| base.metadata.clone()),
343 betas: overlay_vec(&base.betas, &overlay.betas),
344 context_management: overlay
345 .context_management
346 .clone()
347 .or_else(|| base.context_management.clone()),
348 container: overlay.container.clone().or_else(|| base.container.clone()),
349 service_tier: overlay
350 .service_tier
351 .clone()
352 .or_else(|| base.service_tier.clone()),
353 })
354}
355
356fn merge_google(
357 base: Option<&GoogleSettings>,
358 overlay: Option<&GoogleSettings>,
359) -> Option<GoogleSettings> {
360 merge_option(base, overlay, |base, overlay| GoogleSettings {
361 safety_settings: overlay
362 .safety_settings
363 .clone()
364 .or_else(|| base.safety_settings.clone()),
365 cached_content: overlay
366 .cached_content
367 .clone()
368 .or_else(|| base.cached_content.clone()),
369 labels: overlay.labels.clone().or_else(|| base.labels.clone()),
370 response_logprobs: overlay.response_logprobs.or(base.response_logprobs),
371 logprobs: overlay.logprobs.or(base.logprobs),
372 service_tier: overlay
373 .service_tier
374 .clone()
375 .or_else(|| base.service_tier.clone()),
376 cloud_service_tier: overlay
377 .cloud_service_tier
378 .clone()
379 .or_else(|| base.cloud_service_tier.clone()),
380 })
381}
382
383fn merge_bedrock(
384 base: Option<&BedrockSettings>,
385 overlay: Option<&BedrockSettings>,
386) -> Option<BedrockSettings> {
387 merge_option(base, overlay, |base, overlay| BedrockSettings {
388 guardrail_config: overlay
389 .guardrail_config
390 .clone()
391 .or_else(|| base.guardrail_config.clone()),
392 performance_config: overlay
393 .performance_config
394 .clone()
395 .or_else(|| base.performance_config.clone()),
396 request_metadata: overlay
397 .request_metadata
398 .clone()
399 .or_else(|| base.request_metadata.clone()),
400 additional_model_response_field_paths: overlay_vec(
401 &base.additional_model_response_field_paths,
402 &overlay.additional_model_response_field_paths,
403 ),
404 prompt_variables: overlay
405 .prompt_variables
406 .clone()
407 .or_else(|| base.prompt_variables.clone()),
408 additional_model_request_fields: overlay
409 .additional_model_request_fields
410 .clone()
411 .or_else(|| base.additional_model_request_fields.clone()),
412 inference_profile: overlay
413 .inference_profile
414 .clone()
415 .or_else(|| base.inference_profile.clone()),
416 })
417}
418
419fn merge_codex(
420 base: Option<&CodexSettings>,
421 overlay: Option<&CodexSettings>,
422) -> Option<CodexSettings> {
423 merge_option(base, overlay, |base, overlay| CodexSettings {
424 session_id: overlay
425 .session_id
426 .clone()
427 .or_else(|| base.session_id.clone()),
428 thread_id: overlay.thread_id.clone().or_else(|| base.thread_id.clone()),
429 })
430}
431
432fn merge_gateway(
433 base: Option<&GatewaySettings>,
434 overlay: Option<&GatewaySettings>,
435) -> Option<GatewaySettings> {
436 merge_option(base, overlay, |base, overlay| GatewaySettings {
437 x_session_id: overlay
438 .x_session_id
439 .clone()
440 .or_else(|| base.x_session_id.clone()),
441 extra_headers: overlay_headers_case_insensitive(
442 &base.extra_headers,
443 &overlay.extra_headers,
444 ),
445 })
446}
447
448#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
450#[serde(rename_all = "snake_case")]
451pub enum OpenAiPromptCacheMode {
452 Implicit,
454 Explicit,
456}
457
458#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
460pub enum OpenAiPromptCacheTtl {
461 #[serde(rename = "30m")]
463 ThirtyMinutes,
464}
465
466#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
468pub struct OpenAiPromptCacheOptions {
469 pub mode: OpenAiPromptCacheMode,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub ttl: Option<OpenAiPromptCacheTtl>,
474}
475
476#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
478pub struct OpenAiChatSettings {
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub user: Option<String>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub store: Option<bool>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
487 pub logprobs: Option<bool>,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub top_logprobs: Option<u32>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub prediction: Option<Value>,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub prompt_cache_key: Option<String>,
497 #[serde(default, skip_serializing_if = "Option::is_none")]
499 pub prompt_cache_retention: Option<String>,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub prompt_cache_options: Option<OpenAiPromptCacheOptions>,
503}
504
505#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
507#[serde(rename_all = "snake_case")]
508pub enum ResponseStreamTransport {
509 Http,
511 WebSocket,
513 Auto,
515}
516
517#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
519pub struct OpenAiResponsesSettings {
520 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub store: Option<bool>,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub user: Option<String>,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub truncation: Option<String>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub text_verbosity: Option<String>,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub context_management: Option<Value>,
535 #[serde(default, skip_serializing_if = "Vec::is_empty")]
537 pub include: Vec<String>,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub prompt_cache_key: Option<String>,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub prompt_cache_retention: Option<String>,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub prompt_cache_options: Option<OpenAiPromptCacheOptions>,
547 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub stream_transport: Option<ResponseStreamTransport>,
550}
551
552#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
554pub struct AnthropicSettings {
555 #[serde(default, skip_serializing_if = "Option::is_none")]
557 pub metadata: Option<Value>,
558 #[serde(default, skip_serializing_if = "Vec::is_empty")]
560 pub betas: Vec<String>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub context_management: Option<Value>,
564 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub container: Option<String>,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
569 pub service_tier: Option<String>,
570}
571
572#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
574pub struct GoogleSettings {
575 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub safety_settings: Option<Value>,
578 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub cached_content: Option<String>,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub labels: Option<Value>,
584 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub response_logprobs: Option<bool>,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
589 pub logprobs: Option<u32>,
590 #[serde(default, skip_serializing_if = "Option::is_none")]
592 pub service_tier: Option<String>,
593 #[serde(default, skip_serializing_if = "Option::is_none")]
595 pub cloud_service_tier: Option<GoogleCloudServiceTier>,
596}
597
598#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
600#[serde(rename_all = "snake_case")]
601pub enum GoogleCloudServiceTier {
602 PtThenOnDemand,
604 PtOnly,
606 PtThenFlex,
608 PtThenPriority,
610 OnDemand,
612 FlexOnly,
614 PriorityOnly,
616}
617
618#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
620pub struct BedrockSettings {
621 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub guardrail_config: Option<Value>,
624 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub performance_config: Option<Value>,
627 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub request_metadata: Option<Value>,
630 #[serde(default, skip_serializing_if = "Vec::is_empty")]
632 pub additional_model_response_field_paths: Vec<String>,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub prompt_variables: Option<Value>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub additional_model_request_fields: Option<Value>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub inference_profile: Option<String>,
642}
643
644#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
646pub struct CodexSettings {
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub session_id: Option<String>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub thread_id: Option<String>,
653}
654
655#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
657pub struct GatewaySettings {
658 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub x_session_id: Option<String>,
661 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
663 pub extra_headers: BTreeMap<String, String>,
664}
665
666#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
668pub struct ProviderReplaySettings {
669 #[serde(default, skip_serializing_if = "Option::is_none")]
671 pub previous_response_id: Option<String>,
672 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub conversation_id: Option<String>,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
677 pub send_item_ids: Option<bool>,
678 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub include_encrypted_reasoning: Option<bool>,
681}
682
683#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
685#[serde(rename_all = "snake_case", tag = "kind")]
686pub enum ToolChoice {
687 Auto,
689 None,
691 Required,
693 Tools {
695 names: Vec<String>,
697 },
698 ToolOrOutput {
700 function_tools: Vec<String>,
702 },
703 Tool {
705 name: String,
707 },
708}
709
710#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
712pub struct ThinkingSettings {
713 pub effort: String,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub budget_tokens: Option<u32>,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
720 pub mode: Option<String>,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub include_thoughts: Option<bool>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
726 pub summary: Option<String>,
727}
728
729#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
731#[serde(rename_all = "snake_case")]
732pub enum ServiceTier {
733 Auto,
735 Default,
737 Flex,
739 Priority,
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746
747 #[test]
748 fn merges_overlay_fields() {
749 let base = ModelSettings {
750 max_tokens: Some(128),
751 temperature: Some(0.2),
752 stop_sequences: vec!["base".to_string()],
753 ..ModelSettings::default()
754 };
755 let overlay = ModelSettings {
756 temperature: Some(0.7),
757 stop_sequences: vec!["overlay".to_string()],
758 ..ModelSettings::default()
759 };
760
761 let merged = base.merge(&overlay);
762
763 assert_eq!(merged.max_tokens, Some(128));
764 assert_eq!(merged.temperature, Some(0.7));
765 assert_eq!(merged.stop_sequences, vec!["overlay"]);
766 }
767}