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