1use crate::completion::history::Replay;
15use crate::completion::options::{
16 BaseInput, CacheRetention, FinalBody, RawAt, Rewrite, request_params,
17};
18use crate::error::EncodeError;
19use crate::json_utils;
20use crate::json_utils::Lenient;
21use crate::message::{
22 AssistantContent, Document, DocumentData, DocumentMediaType, DocumentSourceKind, Message,
23 MimeType, ToolResultContent, UserContent,
24};
25use crate::providers::internal::object;
26use crate::providers::internal::wire_ids::WireIds;
27use crate::wire::Mode;
28use crate::{completion, message};
29use serde::{Deserialize, Serialize, Serializer};
30use serde_json::{Map, Value, json};
31use std::sync::Arc;
32
33pub mod streaming;
34#[cfg(feature = "websocket")]
35#[cfg_attr(docsrs, doc(cfg(feature = "websocket")))]
36pub mod websocket;
37pub mod wire;
38
39fn image_part(image: &message::Image) -> Option<Value> {
43 let (key, source) = match &image.data {
44 DocumentSourceKind::Base64(data) => (
45 "image_url",
46 format!(
47 "data:{};base64,{data}",
48 image.media_type.as_ref()?.to_mime_type()
49 ),
50 ),
51 DocumentSourceKind::Url(url) => ("image_url", url.clone()),
52 DocumentSourceKind::FileId(file_id) => ("file_id", file_id.clone()),
53 _ => return None,
54 };
55 let mut part =
56 json!({"type": "input_image", "detail": image.detail.clone().unwrap_or_default()});
57 set(&mut part, key, Value::String(source));
58 Some(part)
59}
60
61fn document_part(document: &Document) -> Option<Value> {
65 let source = match &document.data {
66 DocumentData::Text(text) => return Some(json!({"type": "input_text", "text": text})),
67 DocumentData::File(source) => source,
68 };
69 Some(match source {
70 DocumentSourceKind::FileId(file_id) => json!({"type": "input_file", "file_id": file_id}),
71 DocumentSourceKind::Url(url) => json!({"type": "input_file", "file_url": url}),
73 DocumentSourceKind::Base64(data) if document.media_type == Some(DocumentMediaType::PDF) => {
74 object! {
75 "type": "input_file",
76 "file_data": format!("data:application/pdf;base64,{data}"),
77 "filename": "document.pdf",
78 }
79 }
80 _ => return None,
81 })
82}
83
84fn set(value: &mut Value, key: &str, field: Value) {
86 if let Some(fields) = value.as_object_mut() {
87 fields.insert(key.to_owned(), field);
88 }
89}
90
91fn unsendable(part: &str) -> EncodeError {
93 EncodeError::request(format!(
94 "the Responses API cannot carry this {part}; prepare the request first"
95 ))
96}
97
98fn result_output(content: &[ToolResultContent]) -> Result<Value, EncodeError> {
101 let mut parts = content
102 .iter()
103 .map(|part| match part {
104 ToolResultContent::Text(text) => Ok(json!({"type": "input_text", "text": text.text})),
105 ToolResultContent::Json { value } => {
106 Ok(json!({"type": "input_text", "text": value.to_string()}))
107 }
108 ToolResultContent::Image(image) => {
109 image_part(image).ok_or_else(|| unsendable("tool-result image"))
110 }
111 })
112 .collect::<Result<Vec<_>, _>>()?;
113 Ok(match parts.as_mut_slice() {
114 [part] if part.str("type") == Some("input_text") => {
115 part.get_mut("text").map(Value::take).unwrap_or_default()
116 }
117 _ => Value::Array(parts),
118 })
119}
120
121struct Custom {
125 tools: std::collections::HashSet<String>,
127 calls: std::collections::HashMap<String, bool>,
129}
130
131fn input(
143 history: &[Arc<Message>],
144 target: &wire::Responses,
145 model: &str,
146 custom: &mut Custom,
147 stateless: bool,
148) -> Result<(Vec<Value>, Layout), EncodeError> {
149 let ids = WireIds::for_target(history, target, model);
150 let mut items = Vec::new();
151 let mut layout = Layout::default();
152 for (position, message) in history.iter().enumerate() {
153 if layout.after.is_empty() && !matches!(&**message, Message::System { .. }) {
154 layout.after.push(items.len());
155 }
156 let mut parts = Vec::new();
157 match &**message {
158 Message::System { content } => items.push(json!({
159 "type": "message",
160 "role": target.provider.dialect.quirks.responses.system_role,
161 "content": [{"type": "input_text", "text": content}],
162 })),
163 Message::User { content } => {
164 for part in content {
165 parts.push(items.len());
166 let part = match part {
167 UserContent::Text(text) if text.text.trim().is_empty() => continue,
169 UserContent::Text(text) => json!({"type": "input_text", "text": text.text}),
170 UserContent::ToolResult(result) => {
173 let call_id = ids.spell(&result.call);
174 let output = result_output(&result.content)?;
175 items.push(
176 if custom.calls.get(&call_id).copied().unwrap_or_else(|| {
177 custom.tools.contains(result.name.as_str())
178 }) {
179 object! {"type": "custom_tool_call_output", "call_id": call_id, "output": output}
180 } else {
181 object! {"type": "function_call_output", "call_id": call_id, "output": output, "status": "completed"}
182 },
183 );
184 continue;
185 }
186 UserContent::Image(image) => {
187 image_part(image).ok_or_else(|| unsendable("image"))?
188 }
189 UserContent::Document(document) => {
190 document_part(document).ok_or_else(|| unsendable("document"))?
191 }
192 UserContent::Audio(_) => return Err(unsendable("audio")),
193 UserContent::Video(_) => return Err(unsendable("video")),
194 };
195 items.push(object! {"type": "message", "role": "user", "content": vec![part]});
196 }
197 }
198 Message::Assistant(turn) => {
199 let mut texts = 0usize;
200 let mut unpaired = false;
201 for block in &turn.content {
202 parts.push(items.len());
203 let mut replay = block.replay(target, &ids);
204 if unpaired && !matches!(block, AssistantContent::Reasoning(_)) {
205 unpaired = false;
206 replay = Replay::Rebuild;
207 }
208 let ciphertext = |item: &Map<String, Value>| {
209 item.get("encrypted_content")
210 .and_then(Value::as_str)
211 .is_some_and(|cipher| !cipher.is_empty())
212 };
213 let unresolvable = stateless
214 && target
215 .provider
216 .dialect
217 .quirks
218 .responses
219 .reasoning_ciphertext
220 && matches!(block, AssistantContent::Reasoning(_))
221 && match &replay {
222 Replay::Item(item) => !item.as_object().is_some_and(ciphertext),
223 Replay::Identity(identity) => !ciphertext(identity),
224 Replay::Rebuild => false,
225 };
226 if unresolvable {
227 unpaired = true;
228 continue;
229 }
230 let identity = match replay {
231 Replay::Item(item) => {
232 if let Some(call_id) = item.str("call_id") {
233 let kind = item.str("type");
234 if matches!(kind, Some("custom_tool_call" | "function_call")) {
235 custom.calls.insert(
236 call_id.to_owned(),
237 kind == Some("custom_tool_call"),
238 );
239 }
240 }
241 items.push(item.into_owned());
242 continue;
243 }
244 Replay::Identity(identity) => identity,
245 Replay::Rebuild => Map::new(),
246 };
247 let id = |prefix: &str| {
248 identity
249 .get("id")
250 .and_then(Value::as_str)
251 .filter(|id| !id.is_empty() && id.starts_with(prefix) && id.len() <= 64)
252 .map(str::to_owned)
253 };
254 let item = match block {
255 AssistantContent::Text(text) => {
256 let synthetic = match texts {
257 0 => format!("msg_rig_{position}"),
258 n => format!("msg_rig_{position}_{n}"),
259 };
260 texts += 1;
261 let mut item = json!({
262 "type": "message",
263 "role": "assistant",
264 "content": [{"type": "output_text", "text": text.text, "annotations": []}],
265 "status": "completed",
266 "id": id("").unwrap_or(synthetic),
267 });
268 if let Some(phase) =
269 identity.get("phase").filter(|phase| phase.is_string())
270 {
271 set(&mut item, "phase", phase.clone());
272 }
273 items.push(item);
274 continue;
275 }
276 AssistantContent::ToolCall(call) => {
277 let call_id = ids.spell(&call.id);
278 let name = call.function.name.as_str();
279 let kind = identity.get("type").and_then(Value::as_str);
280 let arguments = || {
281 serde_json::to_string(&call.function.arguments).unwrap_or_default()
282 };
283 let (kind, key, payload, prefix) = if kind == Some("custom_tool_call")
284 || (kind.is_none() && custom.tools.contains(name))
285 {
286 custom.calls.insert(call_id.clone(), true);
287 let input =
288 call.function.arguments.get("input").and_then(Value::as_str);
289 let input = input.map_or_else(arguments, str::to_owned);
290 ("custom_tool_call", "input", input, "ctc_")
291 } else {
292 custom.calls.insert(call_id.clone(), false);
293 ("function_call", "arguments", arguments(), "fc_")
294 };
295 let mut item = json!({"type": kind, "call_id": call_id, "name": name});
296 set(&mut item, key, Value::String(payload));
297 if let Some(id) = id(prefix) {
298 set(&mut item, "id", json!(id));
299 }
300 item
301 }
302 AssistantContent::Reasoning(reasoning) => {
305 let Some(rs) = id("") else {
306 continue;
307 };
308 let summary: Vec<Value> = (!reasoning.text.is_empty())
309 .then(|| json!({"type": "summary_text", "text": reasoning.text}))
310 .into_iter()
311 .collect();
312 let mut item =
313 object! {"type": "reasoning", "id": rs, "summary": summary};
314 if let Some(ciphertext) = identity.get("encrypted_content") {
315 set(&mut item, "encrypted_content", ciphertext.clone());
316 }
317 item
318 }
319 AssistantContent::Opaque(opaque) if opaque.replay => opaque.item.clone(),
320 AssistantContent::Opaque(_) => continue,
321 AssistantContent::Image(_) => return Err(unsendable("assistant image")),
322 };
323 items.push(item);
324 }
325 }
326 }
327 if !matches!(&**message, Message::System { .. }) {
328 layout.after.push(items.len());
329 layout.parts.push(parts);
330 }
331 }
332 if layout.after.is_empty() {
333 layout.after.push(items.len());
334 }
335 Ok((items, layout))
336}
337
338#[derive(Default)]
341struct Layout {
342 after: Vec<usize>,
345 parts: Vec<Vec<usize>>,
347}
348
349impl Layout {
350 fn boundary(&self, point: completion::CachePoint) -> Option<usize> {
352 use completion::CachePoint;
353 match point {
354 CachePoint::Tools => None,
355 CachePoint::System => self.after.first().copied(),
356 CachePoint::Messages(turns) => self.after.get(turns).copied(),
357 CachePoint::Within { message, items: 0 } => self.after.get(message).copied(),
358 CachePoint::Within { message, items } => self
359 .parts
360 .get(message)
361 .and_then(|parts| parts.get(items))
362 .or_else(|| self.after.get(message.checked_add(1)?))
363 .copied(),
364 }
365 }
366}
367
368fn tool_choice(choice: message::ToolChoice) -> Result<Value, EncodeError> {
370 Ok(match choice {
371 message::ToolChoice::Auto => json!("auto"),
372 message::ToolChoice::None => json!("none"),
373 message::ToolChoice::Required => json!("required"),
374 message::ToolChoice::Specific { function_names } => match function_names.as_slice() {
375 [] => {
376 return Err(EncodeError::request(
377 "ToolChoice::Specific requires at least one function name",
378 ));
379 }
380 [name] => json!({"type": "function", "name": name}),
381 names => json!({
382 "type": "allowed_tools",
383 "mode": "required",
384 "tools": names.iter().map(|name| json!({"type": "function", "name": name})).collect::<Vec<_>>(),
385 }),
386 },
387 })
388}
389
390pub(crate) fn include_ciphertext(body: &mut Map<String, Value>) {
393 const CIPHERTEXT: &str = "reasoning.encrypted_content";
394 let mut include = body
395 .get("include")
396 .and_then(Value::as_array)
397 .cloned()
398 .unwrap_or_default();
399 if !include.iter().any(|item| item == CIPHERTEXT) {
400 include.push(json!(CIPHERTEXT));
401 }
402 body.insert("include".to_owned(), Value::Array(include));
403}
404
405#[derive(Clone, Copy, Debug, PartialEq, Eq)]
408pub(crate) enum Delivery {
409 Http(Mode),
411 WebSocket,
413}
414
415impl wire::Responses {
416 pub(crate) fn responses_request(
422 &self,
423 request: &completion::CompletionRequest,
424 delivery: Delivery,
425 ) -> Result<FinalBody, EncodeError> {
426 let codex =
427 self.provider.dialect.quirks.responses.contract == wire::ResponsesContract::Codex;
428 let mut rewrites = Vec::new();
429 match delivery {
430 Delivery::Http(Mode::Streaming) => rewrites.push(Rewrite::Stream(true)),
431 Delivery::Http(Mode::Unary) | Delivery::WebSocket => rewrites.push(Rewrite::NoStream),
432 }
433 if delivery == Delivery::WebSocket {
434 rewrites.push(Rewrite::NoBackground);
435 }
436 if codex {
437 rewrites.push(Rewrite::CodexStore);
438 }
439 if self.provider.dialect.quirks.responses.reasoning_ciphertext {
441 rewrites.push(Rewrite::ReasoningCiphertext(codex));
442 }
443 rewrites.push(Rewrite::MoveLast("input"));
448 rewrites.push(Rewrite::MoveLast("max_output_tokens"));
449 let body = request_params(
450 self,
451 request,
452 |layers| self.base(request, codex, layers),
453 RawAt::Top,
454 &rewrites,
455 )?;
456 crate::providers::openai::options::check_body(
457 self,
458 &self.facts,
459 request,
460 body,
461 crate::providers::openai::options::Endpoint::Responses,
462 )
463 }
464
465 fn base(
470 &self,
471 request: &completion::CompletionRequest,
472 codex: bool,
473 layers: &mut BaseInput<'_>,
474 ) -> Result<Map<String, Value>, EncodeError> {
475 let model = request.model.clone().unwrap_or_else(|| self.model.clone());
476 let mut tools: Vec<ResponsesToolDefinition> = request
477 .tools
478 .iter()
479 .map(|tool| ResponsesToolDefinition::from(completion::ToolDefinition::clone(tool)))
480 .collect();
481 let extra = layers.raw_tools()?;
482 if !extra.is_empty() {
483 tools.extend(
484 serde_json::from_value::<Vec<ResponsesToolDefinition>>(Value::Array(extra))
485 .map_err(|err| {
486 EncodeError::request(format!(
487 "Invalid OpenAI Responses tools payload in additional_params: {err}"
488 ))
489 })?,
490 );
491 }
492 tools.extend(self.tools.iter().cloned());
493 if self.strict_tools {
494 tools = tools
495 .into_iter()
496 .map(ResponsesToolDefinition::with_strict)
497 .collect();
498 }
499 let mut custom = Custom {
500 tools: tools
501 .iter()
502 .filter(|tool| tool.kind == "custom")
503 .map(|tool| tool.name.clone())
504 .collect(),
505 calls: Default::default(),
506 };
507 let stateless = codex || layers.param("store") == Some(&json!(false));
508 let caller = match layers.claim("input") {
509 None | Some(Value::Null) => Vec::new(),
510 Some(Value::Array(items)) => items,
511 Some(other) => {
512 return Err(EncodeError::request(format!(
513 "the OpenAI Responses `input` option must be an array of items, got {other}"
514 )));
515 }
516 };
517 for item in &caller {
519 if let (Some(kind @ ("function_call" | "custom_tool_call")), Some(call_id)) =
520 (item.str("type"), item.str("call_id"))
521 {
522 custom
523 .calls
524 .insert(call_id.to_owned(), kind == "custom_tool_call");
525 }
526 }
527 let (items, layout) = input(&request.chat_history, self, &model, &mut custom, stateless)?;
528
529 let system = |item: &Value| {
530 (item.str("role") == Some(self.provider.dialect.quirks.responses.system_role)).then(
531 || {
532 item.at("/content/0/text")
533 .and_then(Value::as_str)
534 .unwrap_or_default()
535 .to_owned()
536 },
537 )
538 };
539 let lift: Vec<bool> = match self.system_instructions {
540 SystemInstructionsPlacement::Instructions => {
543 let leading = items
544 .iter()
545 .take_while(|item| system(item).is_some())
546 .count();
547 let leading = if leading < items.len() || !caller.is_empty() {
548 leading
549 } else {
550 0
551 };
552 (0..items.len()).map(|at| at < leading).collect()
553 }
554 SystemInstructionsPlacement::AllInstructions => {
555 items.iter().map(|item| system(item).is_some()).collect()
556 }
557 SystemInstructionsPlacement::InputSystemMessages => vec![false; items.len()],
558 };
559 let mut lifted = Vec::new();
560 let mut sent = caller;
561 let offset = sent.len();
562 for (item, lift) in items.into_iter().zip(&lift) {
563 if *lift {
564 lifted.extend(system(&item));
565 } else {
566 sent.push(item);
567 }
568 }
569 if sent.is_empty() {
570 return Err(EncodeError::request(if lifted.is_empty() {
571 "OpenAI Responses request input must contain at least one item"
572 } else {
573 "OpenAI Responses request input must contain at least one non-system item \
574 (system messages were lifted into the top-level `instructions` field)"
575 }));
576 }
577 if self.marks_cache_points(&model) && layers.cache() != Some(CacheRetention::None) {
578 let at =
580 |count: usize| offset + lift.iter().take(count).filter(|lifted| !**lifted).count();
581 let points = request
582 .cache_points
583 .iter()
584 .filter_map(|point| layout.boundary(*point))
585 .map(at);
586 mark_cache_points(&mut sent, offset, points);
587 }
588 let lifted: Vec<&str> = lifted
589 .iter()
590 .map(|text| text.trim())
591 .filter(|text| !text.is_empty())
592 .collect();
593 let lifted = lifted.join("\n\n");
594 let instructions = match &self.provider.instructions {
596 Some(gateway) if lifted.is_empty() => Some(gateway.clone()),
597 Some(gateway) if !lifted.contains(gateway.as_str()) => {
598 Some(format!("{gateway}\n\n{lifted}"))
599 }
600 _ => (!lifted.is_empty()).then_some(lifted),
601 };
602 let text = request
603 .output_schema
604 .clone()
605 .filter(|_| !codex)
606 .map(|schema| {
607 let (name, schema) = super::structured_output_schema(schema);
608 json!({"format": {"type": "json_schema", "name": name, "schema": schema, "strict": true}})
609 });
610
611 Ok(crate::providers::internal::set_fields([
612 ("model", Some(Value::String(model))),
613 ("input", Some(Value::Array(sent))),
614 ("instructions", instructions.map(Value::from)),
615 (
616 "max_output_tokens",
617 request.max_tokens.filter(|_| !codex).map(Value::from),
618 ),
619 (
620 "temperature",
621 request.temperature.filter(|_| !codex).map(Value::from),
622 ),
623 (
624 "tool_choice",
625 request.tool_choice.clone().map(tool_choice).transpose()?,
626 ),
627 ("tools", (!tools.is_empty()).then(|| json!(tools))),
628 ("text", text),
629 ]))
630 }
631}
632
633impl wire::Responses {
634 fn marks_cache_points(&self, model: &str) -> bool {
637 self.provider.dialect.name == super::wire::OPENAI.name
638 && super::options::caches_by_options(&self.facts, model)
639 }
640}
641
642fn mark_cache_points(items: &mut [Value], held: usize, ends: impl Iterator<Item = usize>) {
647 for end in ends {
648 let last = (held..end.min(items.len()))
649 .rev()
650 .find(|at| items.get(*at).is_some_and(markable));
651 if let Some(item) = last.and_then(|at| items.get_mut(at)) {
652 mark(item);
653 }
654 }
655}
656
657fn parts_field(item: &Value) -> Option<&'static str> {
660 match item.str("type") {
661 Some("message") if item.str("role") != Some("assistant") => Some("content"),
662 Some("function_call_output" | "custom_tool_call_output") => Some("output"),
663 _ => None,
664 }
665}
666
667fn takes_breakpoint(part: &Value) -> bool {
669 part.str("type")
670 .is_some_and(|kind| ["input_text", "input_image", "input_file"].contains(&kind))
671}
672
673fn breakpoint_parts(item: &Value) -> impl Iterator<Item = &Value> {
675 parts_field(item)
676 .and_then(|field| item.get(field))
677 .and_then(Value::as_array)
678 .into_iter()
679 .flatten()
680 .filter(|part| takes_breakpoint(part))
681}
682
683fn markable(item: &Value) -> bool {
686 breakpoint_parts(item).next().is_some()
687 || (parts_field(item) == Some("output") && item.str("output").is_some())
688}
689
690fn mark(item: &mut Value) {
694 if parts_field(item) == Some("output")
695 && let Some(text) = item.str("output").map(str::to_owned)
696 {
697 set(
698 item,
699 "output",
700 json!([{"type": "input_text", "text": text}]),
701 );
702 }
703 parts_field(item)
704 .and_then(|field| item.get_mut(field))
705 .and_then(Value::as_array_mut)
706 .into_iter()
707 .filter_map(|parts| parts.iter_mut().rev().find(|part| takes_breakpoint(part)))
708 .for_each(|part| set(part, "prompt_cache_breakpoint", json!({"mode": "explicit"})));
709}
710
711#[derive(Debug, Deserialize, Clone, PartialEq)]
713pub struct ResponsesToolDefinition {
714 #[serde(rename = "type")]
716 pub kind: String,
717 #[serde(default)]
719 pub name: String,
720 #[serde(default)]
723 pub parameters: serde_json::Value,
724 #[serde(default, deserialize_with = "json_utils::null_or_default")]
732 pub strict: bool,
733 #[serde(default, deserialize_with = "json_utils::null_or_default")]
735 pub description: String,
736 #[serde(flatten, default)]
738 pub config: Map<String, Value>,
739}
740
741impl Serialize for ResponsesToolDefinition {
742 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
743 where
744 S: Serializer,
745 {
746 use serde::ser::SerializeMap;
747
748 let mut map = serializer.serialize_map(None)?;
749 map.serialize_entry("type", &self.kind)?;
750 if !self.name.is_empty() {
751 map.serialize_entry("name", &self.name)?;
752 }
753 if !self.parameters.is_null() {
754 map.serialize_entry("parameters", &self.parameters)?;
755 }
756 if self.kind == "function" {
757 map.serialize_entry("strict", &self.strict)?;
758 }
759 if !self.description.is_empty() {
760 map.serialize_entry("description", &self.description)?;
761 }
762 for (key, value) in &self.config {
763 map.serialize_entry(key, value)?;
764 }
765 map.end()
766 }
767}
768
769impl ResponsesToolDefinition {
770 pub fn function(
772 name: impl Into<String>,
773 description: impl Into<String>,
774 parameters: serde_json::Value,
775 ) -> Self {
776 Self {
777 kind: "function".to_string(),
778 name: name.into(),
779 parameters,
780 strict: false,
781 description: description.into(),
782 config: Map::new(),
783 }
784 }
785
786 pub fn strict_function(
791 name: impl Into<String>,
792 description: impl Into<String>,
793 parameters: serde_json::Value,
794 ) -> Self {
795 Self::function(name, description, parameters).with_strict()
796 }
797
798 pub fn with_strict(mut self) -> Self {
803 if self.kind == "function" {
804 super::sanitize_schema(&mut self.parameters);
805 self.strict = true;
806 }
807 self
808 }
809
810 pub fn hosted(kind: impl Into<String>) -> Self {
812 Self {
813 kind: kind.into(),
814 name: String::new(),
815 parameters: Value::Null,
816 strict: false,
817 description: String::new(),
818 config: Map::new(),
819 }
820 }
821
822 pub fn web_search() -> Self {
824 Self::hosted("web_search")
825 }
826
827 pub fn file_search() -> Self {
829 Self::hosted("file_search")
830 }
831
832 pub fn computer_use() -> Self {
834 Self::hosted("computer_use")
835 }
836
837 pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
839 self.config.insert(key.into(), value);
840 self
841 }
842}
843
844impl From<completion::ToolDefinition> for ResponsesToolDefinition {
845 fn from(value: completion::ToolDefinition) -> Self {
846 let completion::ToolDefinition {
848 name,
849 parameters,
850 description,
851 annotations: _,
852 } = value;
853
854 Self::function(name, description, parameters)
855 }
856}
857
858#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
863#[serde(rename_all = "snake_case")]
864pub enum SystemInstructionsPlacement {
865 #[default]
870 Instructions,
871 AllInstructions,
876 InputSystemMessages,
881}
882
883#[cfg(test)]
884mod history_tests;
885#[cfg(test)]
886mod tests;