1use std::borrow::Cow;
8
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11use serde_json::{Map, Value};
12
13use super::mapping::Mapping;
14use super::{CacheRetention, GenerationOptions, OnUnsupported, UnsupportedOption};
15use crate::completion::provider_options::SHARED;
16use crate::completion::{CompletionRequest, ReplayTarget};
17use crate::error::EncodeError;
18use crate::providers::openai::wire::BodyRewrite;
19use crate::wire::Body;
20
21#[derive(Clone, Debug, PartialEq, Serialize)]
25#[serde(transparent)]
26pub struct FinalBody(Map<String, Value>);
27
28impl FinalBody {
29 pub fn is_empty(&self) -> bool {
31 self.0.is_empty()
32 }
33
34 pub fn get(&self, key: &str) -> Option<&Value> {
36 self.0.get(key)
37 }
38
39 pub fn pointer(&self, pointer: &str) -> Option<&Value> {
41 let path = pointer.strip_prefix('/')?;
42 let (head, rest) = path.split_once('/').unwrap_or((path, ""));
43 let value = self.0.get(&head.replace("~1", "/").replace("~0", "~"))?;
44 if rest.is_empty() {
45 Some(value)
46 } else {
47 value.pointer(&format!("/{rest}"))
48 }
49 }
50
51 pub fn into_body(self) -> Body {
54 Body::Bytes(Value::Object(self.0).to_string().into_bytes())
55 }
56
57 pub fn deserialize<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
64 let fields = self.0.iter().map(|(key, value)| (key.as_str(), value));
66 T::deserialize(serde::de::value::MapDeserializer::new(fields))
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72#[non_exhaustive]
73pub enum RawAt {
74 Top,
76 Under(&'static str),
79 Split {
82 top: &'static [&'static str],
84 rest: &'static str,
86 },
87 #[doc(hidden)]
92 Ignored(&'static str),
93}
94
95#[derive(Clone, Debug, PartialEq)]
100#[non_exhaustive]
101pub enum Rewrite {
102 #[doc(hidden)]
107 OutputCapRename,
108 #[doc(hidden)]
113 DropUnboundThinking,
114 #[doc(hidden)]
119 AnthropicVersionInBody(String),
120 #[doc(hidden)]
125 ToolChoiceNeedsTools,
126 Stream(bool),
128 NoStream,
130 #[doc(hidden)]
135 NoBackground,
136 StreamUsage,
139 #[doc(hidden)]
144 ReasoningCiphertext(bool),
145 #[doc(hidden)]
149 CodexStore,
150 #[doc(hidden)]
154 ChatDialect(BodyRewrite),
155 #[doc(hidden)]
162 GeminiCachedContent(Option<String>),
163 #[doc(hidden)]
168 MoveLast(&'static str),
169}
170
171pub struct BaseInput<'a> {
174 target: &'a dyn ReplayTarget,
175 request: &'a CompletionRequest,
176 cache: Option<CacheRetention>,
177 upper: &'a Map<String, Value>,
178 raw_tools: Option<Value>,
179}
180
181impl BaseInput<'_> {
182 pub fn cache(&self) -> Option<CacheRetention> {
185 self.cache
186 }
187
188 pub fn refuse_cache(&mut self, reason: impl Into<String>) -> Result<(), EncodeError> {
196 refuse(self.target, self.request, "cache", reason.into())
197 }
198
199 pub fn param(&self, key: &str) -> Option<&Value> {
204 self.upper.get(key)
205 }
206
207 pub fn raw_tools(&mut self) -> Result<Vec<Value>, EncodeError> {
215 match self.raw_tools.take() {
216 None | Some(Value::Null) => Ok(Vec::new()),
217 Some(Value::Array(tools)) => Ok(tools),
218 Some(other) => Err(EncodeError::request(format!(
219 "`additional_params.tools` must be an array, got {other}"
220 ))),
221 }
222 }
223}
224
225#[derive(Default)]
227struct Settled {
228 sends: Vec<Value>,
230 cache: Option<CacheRetention>,
232 ignored: Vec<&'static str>,
234}
235
236fn model_of<'a>(target: &'a dyn ReplayTarget, request: &'a CompletionRequest) -> &'a str {
238 request
239 .model
240 .as_deref()
241 .filter(|model| !model.is_empty())
242 .unwrap_or_else(|| target.model())
243}
244
245fn refuse(
247 target: &dyn ReplayTarget,
248 request: &CompletionRequest,
249 option: impl Into<Cow<'static, str>>,
250 reason: String,
251) -> Result<(), EncodeError> {
252 let option = option.into();
253 let provider = target.provider();
254 let model = model_of(target, request);
255 if collected(|| UnsupportedOption::new(option.clone(), provider, model, reason.clone())) {
256 return Ok(());
257 }
258 match request.options.unsupported_policy() {
259 OnUnsupported::Error => Err(EncodeError::unsupported(UnsupportedOption::new(
260 option, provider, model, reason,
261 ))),
262 OnUnsupported::Ignore => {
263 tracing::warn!(
264 option = option.as_ref(),
265 provider,
266 model,
267 reason = reason.as_str(),
268 "option skipped: the provider cannot honour it"
269 );
270 Ok(())
271 }
272 }
273}
274
275fn settle(target: &dyn ReplayTarget, request: &CompletionRequest) -> Result<Settled, EncodeError> {
278 let fields = request.options.fields();
279 let set = fields.set();
280 let map = target.map_options(request, fields);
281 let provider = target.provider();
282 let mut settled = Settled::default();
283 for ((option, mapping), set) in map.into_slots().into_iter().zip(set) {
284 let fail = |what: String| Err(EncodeError::request(format!("{provider} {what}")));
285 match (mapping, set) {
286 (Mapping::Nothing, false) => {}
287 (Mapping::Nothing, true) => {
288 return fail(format!(
289 "answered `Mapping::Nothing` for the option `{option}`, which the request sets"
290 ));
291 }
292 (_, false) => {
293 return fail(format!(
294 "answered the option `{option}`, which the request does not set; \
295 an unset option answers `Mapping::Nothing` (see `Mapping::of`)"
296 ));
297 }
298 (Mapping::Send(value), true) => {
299 if !value.is_object() {
300 return fail(format!(
301 "sent a value that is not a JSON object for the option `{option}`"
302 ));
303 }
304 settled.sends.push(value);
305 if option == "cache" {
306 settled.cache = request.options.cache;
307 }
308 }
309 (Mapping::Omit(reason), true) => {
310 tracing::debug!(
311 option,
312 provider,
313 reason,
314 "option honoured by sending nothing"
315 );
316 }
317 (Mapping::Place, true) if option == "cache" => settled.cache = request.options.cache,
318 (Mapping::Place, true) => {
319 return fail(format!(
320 "answered `Mapping::Place` for the option `{option}`; only `cache` places markers"
321 ));
322 }
323 (Mapping::Unsupported(reason), true) => {
324 refuse(target, request, option, reason)?;
325 settled.ignored.push(option);
326 }
327 }
328 }
329 Ok(settled)
330}
331
332fn clear(options: &mut GenerationOptions, ignored: &[&str]) {
334 let GenerationOptions {
335 reasoning,
336 cache,
337 service_tier,
338 verbosity,
339 parallel_tool_calls,
340 top_p,
341 seed,
342 stop,
343 cache_key,
344 on_unsupported: _,
345 } = options;
346 let off = |name: &str| ignored.contains(&name);
347 if off("reasoning") {
348 *reasoning = None;
349 }
350 if off("cache") {
351 *cache = None;
352 }
353 if off("service_tier") {
354 *service_tier = None;
355 }
356 if off("verbosity") {
357 *verbosity = None;
358 }
359 if off("parallel_tool_calls") {
360 *parallel_tool_calls = None;
361 }
362 if off("top_p") {
363 *top_p = None;
364 }
365 if off("seed") {
366 *seed = None;
367 }
368 if off("stop") {
369 stop.clear();
370 }
371 if off("cache_key") {
372 *cache_key = None;
373 }
374}
375
376pub fn check(
389 target: &dyn ReplayTarget,
390 request: &mut CompletionRequest,
391) -> Result<(), EncodeError> {
392 unserialized(request)?;
393 let settled = settle(target, request)?;
394 clear(&mut request.options, &settled.ignored);
395 model_rules(target, request)?;
396 let layer = provider_layer(target, request);
397 for refusal in &layer.refused {
398 refuse(
399 target,
400 request,
401 refusal.name(target),
402 refusal.reason.clone(),
403 )?;
404 }
405 let api = target.api();
406 for refusal in layer.refused {
407 request.provider_options.remove_field(
408 target.provider(),
409 &[SHARED, api.as_str()],
410 refusal.field,
411 );
412 }
413 Ok(())
414}
415
416fn model_rules(
422 target: &dyn ReplayTarget,
423 request: &mut CompletionRequest,
424) -> Result<(), EncodeError> {
425 let facts = target
426 .facts()
427 .unwrap_or_else(|| crate::catalog::ModelFacts::builtin());
428 let Some(spec) = facts.for_model(target.provider(), model_of(target, request)) else {
429 return Ok(());
430 };
431 let refusals = spec.request_rules(request);
432 let mut ignored = Vec::new();
433 for CatalogRefusal { field, reason } in refusals {
434 refuse(target, request, field, reason)?;
435 ignored.push(field);
436 }
437 if ignored.contains(&"temperature") {
438 request.temperature = None;
439 }
440 clear(&mut request.options, &ignored);
441 Ok(())
442}
443
444thread_local! {
445 static COLLECTED: std::cell::RefCell<Option<Vec<UnsupportedOption>>> =
448 const { std::cell::RefCell::new(None) };
449}
450
451pub(crate) fn collect_refusals<T>(encode: impl FnOnce() -> T) -> (T, Vec<UnsupportedOption>) {
457 struct Scope(Option<Vec<UnsupportedOption>>);
459 impl Drop for Scope {
460 fn drop(&mut self) {
461 let outer = self.0.take();
462 COLLECTED.with(|collected| collected.replace(outer));
463 }
464 }
465 let outer = COLLECTED.with(|collected| collected.replace(Some(Vec::new())));
466 let scope = Scope(outer);
467 let value = encode();
468 let refusals = COLLECTED
469 .with(|collected| collected.borrow_mut().take())
470 .unwrap_or_default();
471 drop(scope);
472 (value, refusals)
473}
474
475fn collected(make: impl FnOnce() -> UnsupportedOption) -> bool {
478 COLLECTED.with(|collected| match collected.borrow_mut().as_mut() {
479 Some(refusals) => {
480 refusals.push(make());
481 true
482 }
483 None => false,
484 })
485}
486
487pub(crate) struct CatalogRefusal {
492 pub(crate) field: &'static str,
493 pub(crate) reason: String,
494}
495
496pub(crate) fn catalog_refusals(
513 target: &dyn ReplayTarget,
514 request: &CompletionRequest,
515 body: FinalBody,
516 refusals: Vec<CatalogRefusal>,
517) -> Result<FinalBody, EncodeError> {
518 let provider = target.provider();
519 let model = model_of(target, request);
520 if request.options.is_default() {
521 for refusal in &refusals {
522 tracing::debug!(
523 option = refusal.field,
524 provider,
525 model,
526 reason = refusal.reason.as_str(),
527 "catalog refusal not applied: the request sets no generation options"
528 );
529 }
530 return Ok(body);
531 }
532 let FinalBody(mut body) = body;
533 for CatalogRefusal { field, reason } in refusals {
534 let collecting =
535 collected(|| UnsupportedOption::new(field, provider, model, reason.clone()));
536 if !collecting && request.options.unsupported_policy() == OnUnsupported::Error {
537 return Err(EncodeError::unsupported(UnsupportedOption::new(
538 field, provider, model, reason,
539 )));
540 }
541 let raw = request
542 .additional_params
543 .as_ref()
544 .and_then(|params| params.get(field));
545 let typed = field != "tools" && raw.is_none_or(|raw| body.get(field) != Some(raw));
546 if typed {
547 body.shift_remove(field);
548 }
549 if collecting {
550 continue;
551 }
552 tracing::warn!(
553 option = field,
554 provider,
555 model,
556 reason = reason.as_str(),
557 "{}",
558 match typed {
559 true => "option skipped: the model's catalog entry refuses it",
560 false => "catalog refusal ignored: sent as written",
561 }
562 );
563 }
564 Ok(FinalBody(body))
565}
566
567struct Refused {
570 field: &'static str,
571 section: String,
572 reason: String,
573}
574
575impl Refused {
576 fn name(&self, target: &dyn ReplayTarget) -> String {
578 format!("{}.{}.{}", target.provider(), self.section, self.field)
579 }
580}
581
582struct ProviderLayer {
585 fields: Map<String, Value>,
586 refused: Vec<Refused>,
587}
588
589fn provider_layer(target: &dyn ReplayTarget, request: &CompletionRequest) -> ProviderLayer {
594 let provider = target.provider();
595 let api = target.api();
596 let mut layer = ProviderLayer {
597 fields: Map::new(),
598 refused: Vec::new(),
599 };
600 let Some(sections) = request.provider_options.sections(provider) else {
601 return layer;
602 };
603 let mut route = None;
604 for (name, section) in sections {
605 let Value::Object(section) = section else {
606 continue;
607 };
608 if name == SHARED {
609 deep_merge(&mut layer.fields, section.clone());
610 } else if name == api.as_str() {
611 route = Some(section);
612 } else {
613 tracing::debug!(
614 provider,
615 section = name.as_str(),
616 api = api.as_str(),
617 "provider options section skipped: the request takes another route"
618 );
619 }
620 }
621 if let Some(route) = route {
622 deep_merge(&mut layer.fields, route.clone());
623 }
624 for (field, reason) in request.provider_options.refusals(provider, target, request) {
625 layer.refuse(target, request, field, reason);
626 }
627 layer
628}
629
630impl ProviderLayer {
631 fn refuse(
635 &mut self,
636 target: &dyn ReplayTarget,
637 request: &CompletionRequest,
638 field: &'static str,
639 reason: String,
640 ) {
641 if self.fields.shift_remove(field).is_none() {
642 return;
643 }
644 let api = target.api();
645 let in_route = request
646 .provider_options
647 .sections(target.provider())
648 .and_then(|sections| sections.get(api.as_str()))
649 .and_then(Value::as_object)
650 .is_some_and(|route| route.contains_key(field));
651 let section = if in_route { api.as_str() } else { SHARED };
652 self.refused.push(Refused {
653 field,
654 section: section.to_owned(),
655 reason,
656 });
657 }
658}
659
660fn unserialized(request: &CompletionRequest) -> Result<(), EncodeError> {
665 match request.provider_options.failure() {
666 Some(error) => Err(EncodeError::request(error.clone())),
667 None => Ok(()),
668 }
669}
670
671fn raw_layer(request: &CompletionRequest) -> Result<Map<String, Value>, EncodeError> {
673 match &request.additional_params {
674 None | Some(Value::Null) => Ok(Map::new()),
675 Some(Value::Object(params)) => Ok(params.clone()),
676 Some(_) => Err(EncodeError::request(
677 "`additional_params` must be a JSON object",
678 )),
679 }
680}
681
682fn placed(raw: Map<String, Value>, raw_at: RawAt) -> Map<String, Value> {
684 match raw_at {
685 RawAt::Top => raw,
686 RawAt::Under(_) if raw.is_empty() => Map::new(),
687 RawAt::Under(pointer) => {
688 let mut value = Value::Object(raw);
689 for key in pointer.rsplit('/').filter(|key| !key.is_empty()) {
690 value = Value::Object(Map::from_iter([(key.to_owned(), value)]));
691 }
692 match value {
693 Value::Object(map) => map,
694 _ => Map::new(),
695 }
696 }
697 RawAt::Split { top, rest } => {
698 let mut body = Map::new();
699 let mut under = Map::new();
700 for (key, value) in raw {
701 if key == rest || top.contains(&key.as_str()) {
702 deep_merge(&mut body, Map::from_iter([(key, value)]));
703 } else {
704 under.insert(key, value);
705 }
706 }
707 if !under.is_empty() {
708 deep_merge(
709 &mut body,
710 Map::from_iter([(rest.to_owned(), Value::Object(under))]),
711 );
712 }
713 body
714 }
715 RawAt::Ignored(warning) => {
716 if !raw.is_empty() {
717 tracing::warn!("{warning}");
718 }
719 Map::new()
720 }
721 }
722}
723
724pub(crate) fn deep_merge(body: &mut Map<String, Value>, upper: Map<String, Value>) {
727 for (key, value) in upper {
728 match (body.get_mut(&key), value) {
729 (Some(Value::Object(lower)), Value::Object(upper)) => deep_merge(lower, upper),
730 (_, value) => {
731 body.insert(key, value);
732 }
733 }
734 }
735}
736
737fn upper_layers(
740 sends: &[Value],
741 provider: &Map<String, Value>,
742 raw: &Map<String, Value>,
743) -> Map<String, Value> {
744 let mut upper = Map::new();
745 for send in sends {
746 if let Value::Object(send) = send {
747 deep_merge(&mut upper, send.clone());
748 }
749 }
750 deep_merge(&mut upper, provider.clone());
751 deep_merge(&mut upper, raw.clone());
752 upper
753}
754
755pub fn param(target: &dyn ReplayTarget, request: &CompletionRequest, key: &str) -> Option<Value> {
763 let map = target.map_options(request, request.options.fields());
764 let mut upper = Map::new();
765 for (_, mapping) in map.into_slots() {
766 if let Mapping::Send(Value::Object(send)) = mapping {
767 deep_merge(&mut upper, send);
768 }
769 }
770 deep_merge(&mut upper, provider_layer(target, request).fields);
771 if let Some(Value::Object(raw)) = &request.additional_params {
772 deep_merge(&mut upper, raw.clone());
773 }
774 upper.shift_remove(key)
775}
776
777pub fn request_params(
793 target: &dyn ReplayTarget,
794 request: &CompletionRequest,
795 base: impl FnOnce(&mut BaseInput<'_>) -> Result<Map<String, Value>, EncodeError>,
796 raw_at: RawAt,
797 rewrites: &[Rewrite],
798) -> Result<FinalBody, EncodeError> {
799 unserialized(request)?;
800 let settled = settle(target, request)?;
801 let mut provider = provider_layer(target, request);
802 if rewrites.contains(&Rewrite::NoBackground) {
805 provider.refuse(
806 target,
807 request,
808 "background",
809 "a WebSocket session takes no `background`".to_owned(),
810 );
811 }
812 for refusal in provider.refused {
813 refuse(target, request, refusal.name(target), refusal.reason)?;
814 }
815 let provider = provider.fields;
816 let mut raw = raw_layer(request)?;
817 let raw_tools = match raw_at {
818 RawAt::Top | RawAt::Split { .. } => raw.shift_remove("tools"),
819 RawAt::Under(_) | RawAt::Ignored(_) => None,
820 };
821 let mut handles = Vec::new();
822 if rewrites
823 .iter()
824 .any(|rewrite| matches!(rewrite, Rewrite::GeminiCachedContent(_)))
825 {
826 for spelling in ["cachedContent", "cached_content"] {
827 match raw.shift_remove(spelling) {
828 None => {}
829 Some(Value::String(name)) => handles.push(name),
830 Some(other) => {
831 return Err(EncodeError::request(format!(
832 "Gemini `additional_params.{spelling}` should be a string, got {other}"
833 )));
834 }
835 }
836 }
837 if raw.get("generationConfig").is_some_and(Value::is_null) {
840 raw.shift_remove("generationConfig");
841 }
842 }
843 let raw = placed(raw, raw_at);
844 let upper = upper_layers(&settled.sends, &provider, &raw);
845 let mut input = BaseInput {
846 target,
847 request,
848 cache: settled.cache,
849 upper: &upper,
850 raw_tools,
851 };
852 let mut body = base(&mut input)?;
853 for send in settled.sends {
854 if let Value::Object(send) = send {
855 deep_merge(&mut body, send);
856 }
857 }
858 deep_merge(&mut body, provider);
859 deep_merge(&mut body, raw);
860 for rewrite in rewrites {
861 apply(target, rewrite, &mut body, &handles)?;
862 }
863 Ok(FinalBody(body))
864}
865
866fn apply(
868 target: &dyn ReplayTarget,
869 rewrite: &Rewrite,
870 body: &mut Map<String, Value>,
871 handles: &[String],
872) -> Result<(), EncodeError> {
873 match rewrite {
874 Rewrite::OutputCapRename => {
875 let reasoning = body
878 .get("model")
879 .and_then(Value::as_str)
880 .is_some_and(|model| {
881 let facts = target
882 .facts()
883 .unwrap_or_else(|| crate::catalog::ModelFacts::builtin());
884 !model.contains('/')
885 && crate::providers::openai::options::reasons(facts, model) == Some(true)
886 });
887 if reasoning && let Some(max_tokens) = body.shift_remove("max_tokens") {
888 body.entry("max_completion_tokens").or_insert(max_tokens);
889 }
890 }
891 Rewrite::DropUnboundThinking => {
892 let adaptive = body.get("thinking").is_none_or(|thinking| {
893 thinking.get("type").and_then(Value::as_str) == Some("adaptive")
894 });
895 if adaptive {
896 crate::providers::anthropic::completion::drop_unbound_thinking(body);
897 }
898 }
899 Rewrite::AnthropicVersionInBody(version) => {
900 body.shift_remove("model");
901 body.insert("anthropic_version".to_owned(), version.as_str().into());
902 }
903 Rewrite::ToolChoiceNeedsTools => {
904 let has_tools = body
905 .get("tools")
906 .and_then(Value::as_array)
907 .is_some_and(|tools| !tools.is_empty());
908 if has_tools {
909 body.entry("tool_choice")
910 .or_insert_with(|| serde_json::json!({ "type": "auto" }));
911 } else {
912 body.shift_remove("tool_choice");
913 }
914 }
915 Rewrite::Stream(stream) => {
916 body.insert("stream".to_owned(), Value::Bool(*stream));
917 }
918 Rewrite::NoStream => {
919 body.shift_remove("stream");
920 }
921 Rewrite::NoBackground => {
922 body.shift_remove("background");
923 }
924 Rewrite::StreamUsage => {
925 if let Some(options) = body
926 .entry("stream_options")
927 .or_insert_with(|| Value::Object(Map::new()))
928 .as_object_mut()
929 {
930 options.entry("include_usage").or_insert(Value::Bool(true));
931 }
932 }
933 Rewrite::ReasoningCiphertext(always) => {
934 let wanted = *always
935 || body.get("reasoning").is_some()
936 || body.get("store") == Some(&Value::Bool(false));
937 if wanted {
938 crate::providers::openai::responses_api::include_ciphertext(body);
939 }
940 }
941 Rewrite::CodexStore => {
942 body.insert("store".to_owned(), Value::Bool(false));
943 }
944 Rewrite::ChatDialect(kind) => {
945 crate::providers::openai::wire::chat::rewrite_body(*kind, body)?;
946 }
947 Rewrite::GeminiCachedContent(handle) => {
948 for name in handles.iter().chain(handle) {
949 crate::providers::gemini::completion::with_cached_content(body, name)?;
950 }
951 }
952 Rewrite::MoveLast(key) => {
953 if let Some(value) = body.shift_remove(*key) {
954 body.insert((*key).to_owned(), value);
955 }
956 }
957 }
958 Ok(())
959}