1use serde::{Deserialize, Serialize, de::DeserializeOwned};
4use serde_json::{Map, Value};
5use thiserror::Error;
6
7use crate::e2ee::{E2eeCodec, E2eeCodecError};
8use crate::util::json_kind;
9
10#[derive(Debug, Clone, PartialEq)]
12pub struct ChatCompletionRequest {
13 pub model: String,
14 pub messages: Vec<NormalizedChatMessage>,
15 pub stream: bool,
16 pub stream_options: OpenAiStreamOptions,
17 pub venice_parameters: VeniceParameters,
18 pub passthrough: OpenAiPassthroughFields,
19 pub reasoning: Option<ReasoningOptions>,
20 pub reasoning_effort: Option<String>,
21 pub tools: Vec<ChatToolDefinition>,
22 pub tool_choice: ChatToolChoice,
23 pub parallel_tool_calls: Option<bool>,
24}
25
26impl ChatCompletionRequest {
27 pub fn parse(value: &Value) -> Result<Self, ChatRequestError> {
29 let object = value
30 .as_object()
31 .ok_or_else(|| ChatRequestError::invalid("request body must be a JSON object"))?;
32 reject_unknown_fields(
33 object,
34 &[
35 "model",
36 "messages",
37 "stream",
38 "stream_options",
39 "temperature",
40 "top_p",
41 "max_tokens",
42 "max_completion_tokens",
43 "stop",
44 "reasoning",
45 "reasoning_effort",
46 "tools",
47 "tool_choice",
48 "parallel_tool_calls",
49 "metadata",
50 "venice_parameters",
51 ],
52 "request",
53 )?;
54
55 validate_ignored_client_only_fields(object)?;
56
57 let model = required_non_empty_string(object, "model")?.to_owned();
58 let messages_value = object
59 .get("messages")
60 .ok_or(ChatRequestError::MissingField { field: "messages" })?;
61 let messages = normalize_messages(messages_value)?;
62 let stream = optional_bool(object, "stream")?.unwrap_or(false);
63 let stream_options = OpenAiStreamOptions::parse(object.get("stream_options"))?;
64 let venice_parameters = VeniceParameters::parse(object.get("venice_parameters"))?;
65 let passthrough = OpenAiPassthroughFields::parse(object)?;
66 let reasoning = ReasoningOptions::parse(object.get("reasoning"))?;
67 let reasoning_effort = optional_non_empty_string(object, "reasoning_effort")?;
68 validate_reasoning_effort_consistency(reasoning.as_ref(), reasoning_effort.as_deref())?;
69 let tools = parse_tools(object.get("tools"))?;
70 validate_tools(&tools)?;
71 let tool_choice = parse_tool_choice(object.get("tool_choice"))?;
72 validate_tool_choice(&tool_choice)?;
73 let parallel_tool_calls = optional_bool(object, "parallel_tool_calls")?;
74
75 Ok(Self {
76 model,
77 messages,
78 stream,
79 stream_options,
80 venice_parameters,
81 passthrough,
82 reasoning,
83 reasoning_effort,
84 tools,
85 tool_choice,
86 parallel_tool_calls,
87 })
88 }
89
90 pub fn to_venice_e2ee_request(
92 &self,
93 codec: &E2eeCodec,
94 model_public_key_hex: &str,
95 ) -> Result<PreparedVeniceChatRequest, ChatConstructionError> {
96 self.to_venice_e2ee_request_with_messages(codec, model_public_key_hex, &[], &[])
97 }
98
99 pub fn to_venice_e2ee_request_with_messages(
101 &self,
102 codec: &E2eeCodec,
103 model_public_key_hex: &str,
104 prefix_messages: &[NormalizedChatMessage],
105 suffix_messages: &[NormalizedChatMessage],
106 ) -> Result<PreparedVeniceChatRequest, ChatConstructionError> {
107 let encrypted_messages = prefix_messages
108 .iter()
109 .chain(self.messages.iter())
110 .chain(suffix_messages.iter())
111 .map(|message| {
112 let content = codec
113 .encrypt_content(&message.content, model_public_key_hex)
114 .map_err(ChatConstructionError::E2ee)?
115 .into_hex();
116 Ok(NormalizedChatMessage::new(message.role.clone(), content))
117 })
118 .collect::<Result<Vec<_>, ChatConstructionError>>()?;
119
120 Ok(PreparedVeniceChatRequest {
121 client_stream: self.stream,
122 upstream: VeniceE2eeChatRequest {
123 model: self.model.clone(),
124 messages: encrypted_messages,
125 stream: true,
126 stream_options: VeniceStreamOptions {
127 include_usage: self.stream_options.include_usage.unwrap_or(true),
128 },
129 venice_parameters: self.venice_parameters.clone(),
130 temperature: self.passthrough.temperature.clone(),
131 top_p: self.passthrough.top_p.clone(),
132 max_tokens: self.passthrough.max_tokens,
133 max_completion_tokens: self.passthrough.max_completion_tokens,
134 stop: self.passthrough.stop.clone(),
135 reasoning: self.reasoning.clone(),
136 reasoning_effort: self.reasoning_effort.clone(),
137 },
138 })
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct NormalizedChatMessage {
145 pub role: String,
146 pub content: String,
147}
148
149impl NormalizedChatMessage {
150 pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
152 Self {
153 role: role.into(),
154 content: content.into(),
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
162pub enum ChatToolDefinition {
163 Function {
164 function: ChatToolFunctionDefinition,
165 },
166}
167
168impl ChatToolDefinition {
169 pub fn function(&self) -> &ChatToolFunctionDefinition {
171 match self {
172 Self::Function { function } => function,
173 }
174 }
175
176 pub fn name(&self) -> &str {
178 &self.function().name
179 }
180
181 pub fn parameters_schema(&self) -> Option<&Map<String, Value>> {
183 self.function().parameters.as_ref().map(JsonSchema::as_map)
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189#[serde(deny_unknown_fields)]
190pub struct ChatToolFunctionDefinition {
191 pub name: String,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub description: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub parameters: Option<JsonSchema>,
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200#[serde(transparent)]
201pub struct JsonSchema(Map<String, Value>);
202
203impl JsonSchema {
204 pub fn as_map(&self) -> &Map<String, Value> {
206 &self.0
207 }
208}
209
210#[derive(Debug, Clone, Default, PartialEq, Eq)]
212pub enum ChatToolChoice {
213 #[default]
214 Auto,
215 None,
216 Required,
217 Function {
218 name: String,
219 },
220}
221
222impl<'de> Deserialize<'de> for ChatToolChoice {
223 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225 where
226 D: serde::Deserializer<'de>,
227 {
228 ChatToolChoiceWire::deserialize(deserializer).map(Self::from)
229 }
230}
231
232impl From<ChatToolChoiceWire> for ChatToolChoice {
233 fn from(value: ChatToolChoiceWire) -> Self {
235 match value {
236 ChatToolChoiceWire::Mode(ChatToolChoiceMode::Auto) => Self::Auto,
237 ChatToolChoiceWire::Mode(ChatToolChoiceMode::None) => Self::None,
238 ChatToolChoiceWire::Mode(ChatToolChoiceMode::Required) => Self::Required,
239 ChatToolChoiceWire::Object(ChatToolChoiceObject::Function { function }) => {
240 Self::Function {
241 name: function.name,
242 }
243 }
244 }
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
250#[serde(untagged)]
251enum ChatToolChoiceWire {
252 Mode(ChatToolChoiceMode),
253 Object(ChatToolChoiceObject),
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
258#[serde(rename_all = "snake_case")]
259enum ChatToolChoiceMode {
260 Auto,
261 None,
262 Required,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
267#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
268enum ChatToolChoiceObject {
269 Function { function: ChatToolChoiceFunction },
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
274#[serde(deny_unknown_fields)]
275struct ChatToolChoiceFunction {
276 name: String,
277}
278
279#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
281#[serde(deny_unknown_fields)]
282pub struct OpenAiStreamOptions {
283 #[serde(default, deserialize_with = "deserialize_optional_bool_reject_null")]
284 pub include_usage: Option<bool>,
285}
286
287#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(deny_unknown_fields)]
290pub struct ReasoningOptions {
291 #[serde(
292 default,
293 deserialize_with = "deserialize_optional_bool_reject_null",
294 skip_serializing_if = "Option::is_none"
295 )]
296 pub enabled: Option<bool>,
297 #[serde(
298 default,
299 deserialize_with = "deserialize_optional_non_empty_string_reject_null",
300 skip_serializing_if = "Option::is_none"
301 )]
302 pub effort: Option<String>,
303}
304
305impl ReasoningOptions {
306 fn parse(value: Option<&Value>) -> Result<Option<Self>, ChatRequestError> {
308 match value {
309 None => Ok(None),
310 Some(value) => deserialize_typed_value("reasoning", value).map(Some),
311 }
312 }
313}
314
315impl OpenAiStreamOptions {
316 fn parse(value: Option<&Value>) -> Result<Self, ChatRequestError> {
318 let Some(value) = value else {
319 return Ok(Self::default());
320 };
321 deserialize_typed_value("stream_options", value)
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
327pub struct VeniceParameters {
328 pub enable_e2ee: bool,
329 pub include_venice_system_prompt: bool,
330 pub strip_thinking_response: bool,
331 pub disable_thinking: bool,
332 pub enable_web_search: String,
333}
334
335impl Default for VeniceParameters {
336 fn default() -> Self {
338 Self {
339 enable_e2ee: true,
340 include_venice_system_prompt: false,
341 strip_thinking_response: false,
342 disable_thinking: false,
343 enable_web_search: "off".to_owned(),
344 }
345 }
346}
347
348impl VeniceParameters {
349 fn parse(value: Option<&Value>) -> Result<Self, ChatRequestError> {
351 let Some(value) = value else {
352 return Ok(Self::default());
353 };
354 let raw: RawVeniceParameters = deserialize_typed_value("venice_parameters", value)?;
355 let enable_e2ee = raw.enable_e2ee.unwrap_or(true);
356
357 if !enable_e2ee {
358 return Err(ChatRequestError::UnsupportedVeniceParameter {
359 field: "venice_parameters.enable_e2ee",
360 message: "Venice E2EE must remain enabled for encrypted proxy requests".to_owned(),
361 });
362 }
363
364 let include_venice_system_prompt = raw.include_venice_system_prompt.unwrap_or(false);
365
366 if include_venice_system_prompt {
367 return Err(ChatRequestError::UnsupportedVeniceParameter {
368 field: "venice_parameters.include_venice_system_prompt",
369 message: "Venice system prompt injection is disabled for E2EE requests".to_owned(),
370 });
371 }
372
373 let strip_thinking_response = raw.strip_thinking_response.unwrap_or(false);
374 let disable_thinking = raw.disable_thinking.unwrap_or(false);
375
376 let enable_web_search = match raw.enable_web_search {
377 None => "off".to_owned(),
378 Some(RawVeniceWebSearch::String(value)) if value == "off" => "off".to_owned(),
379 Some(RawVeniceWebSearch::Bool(false)) => "off".to_owned(),
380 Some(RawVeniceWebSearch::String(value)) => {
381 return Err(ChatRequestError::UnsupportedVeniceParameter {
382 field: "venice_parameters.enable_web_search",
383 message: format!(
384 "Venice web search is out of scope for E2EE requests; expected \"off\", got {value:?}"
385 ),
386 });
387 }
388 Some(RawVeniceWebSearch::Bool(true)) => {
389 return Err(ChatRequestError::UnsupportedVeniceParameter {
390 field: "venice_parameters.enable_web_search",
391 message: "Venice web search is out of scope for E2EE requests".to_owned(),
392 });
393 }
394 };
395
396 Ok(Self {
397 enable_e2ee,
398 include_venice_system_prompt,
399 strip_thinking_response,
400 disable_thinking,
401 enable_web_search,
402 })
403 }
404}
405
406#[derive(Debug, Clone, Default, PartialEq)]
408pub struct OpenAiPassthroughFields {
409 pub temperature: Option<Value>,
410 pub top_p: Option<Value>,
411 pub max_tokens: Option<u64>,
412 pub max_completion_tokens: Option<u64>,
413 pub stop: Option<StopSequence>,
414}
415
416impl OpenAiPassthroughFields {
417 fn parse(object: &Map<String, Value>) -> Result<Self, ChatRequestError> {
419 Ok(Self {
420 temperature: optional_number(object, "temperature")?,
421 top_p: optional_number(object, "top_p")?,
422 max_tokens: optional_u64(object, "max_tokens")?,
423 max_completion_tokens: optional_u64(object, "max_completion_tokens")?,
424 stop: optional_stop(object)?,
425 })
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
431pub struct PreparedVeniceChatRequest {
432 pub client_stream: bool,
433 pub upstream: VeniceE2eeChatRequest,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
438pub struct VeniceE2eeChatRequest {
439 pub model: String,
440 pub messages: Vec<NormalizedChatMessage>,
441 pub stream: bool,
442 pub stream_options: VeniceStreamOptions,
443 pub venice_parameters: VeniceParameters,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub temperature: Option<Value>,
446 #[serde(skip_serializing_if = "Option::is_none")]
447 pub top_p: Option<Value>,
448 #[serde(skip_serializing_if = "Option::is_none")]
449 pub max_tokens: Option<u64>,
450 #[serde(skip_serializing_if = "Option::is_none")]
451 pub max_completion_tokens: Option<u64>,
452 #[serde(skip_serializing_if = "Option::is_none")]
453 pub stop: Option<StopSequence>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 pub reasoning: Option<ReasoningOptions>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 pub reasoning_effort: Option<String>,
458}
459
460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462#[serde(untagged)]
463pub enum StopSequence {
464 String(String),
465 Strings(Vec<String>),
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
470pub struct VeniceStreamOptions {
471 pub include_usage: bool,
472}
473
474#[derive(Debug, Clone, Default, Deserialize)]
476#[serde(deny_unknown_fields)]
477struct RawVeniceParameters {
478 #[serde(default, deserialize_with = "deserialize_optional_bool_reject_null")]
479 enable_e2ee: Option<bool>,
480 #[serde(default, deserialize_with = "deserialize_optional_bool_reject_null")]
481 include_venice_system_prompt: Option<bool>,
482 #[serde(default, deserialize_with = "deserialize_optional_bool_reject_null")]
483 strip_thinking_response: Option<bool>,
484 #[serde(default, deserialize_with = "deserialize_optional_bool_reject_null")]
485 disable_thinking: Option<bool>,
486 #[serde(
487 default,
488 deserialize_with = "deserialize_optional_web_search_reject_null"
489 )]
490 enable_web_search: Option<RawVeniceWebSearch>,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
495#[serde(untagged)]
496enum RawVeniceWebSearch {
497 String(String),
498 Bool(bool),
499}
500
501#[derive(Debug, Clone, Deserialize)]
503#[serde(deny_unknown_fields)]
504struct RawAssistantToolCall {
505 id: String,
506 #[serde(rename = "type")]
507 kind: String,
508 function: RawAssistantToolFunction,
509}
510
511#[derive(Debug, Clone, Deserialize)]
513#[serde(deny_unknown_fields)]
514struct RawAssistantToolFunction {
515 name: String,
516 arguments: String,
517}
518
519#[derive(Debug, Clone, Deserialize)]
521#[serde(deny_unknown_fields)]
522struct RawTextContentPart {
523 #[serde(rename = "type")]
525 _kind: TextContentPartType,
526 text: String,
527}
528
529#[derive(Debug, Clone, Deserialize)]
531enum TextContentPartType {
532 #[serde(rename = "text")]
533 Text,
534}
535
536#[derive(Debug, Error)]
538pub enum ChatRequestError {
539 #[error("missing required field {field}")]
540 MissingField { field: &'static str },
541 #[error("invalid request: {message}")]
542 InvalidRequest { message: String },
543 #[error("invalid field {field}: {message}")]
544 InvalidField {
545 field: &'static str,
546 message: String,
547 },
548 #[error("unsupported request field {field}")]
549 UnsupportedField { field: String },
550 #[error("unsupported message role {role:?}")]
551 UnsupportedMessageRole { role: String },
552 #[error("unsupported message content at {path}: {message}")]
553 UnsupportedMessageContent { path: String, message: String },
554 #[error("invalid assistant tool-call history: {message}")]
555 InvalidToolCallHistory { message: String },
556 #[error("unsupported Venice parameter {field}: {message}")]
557 UnsupportedVeniceParameter {
558 field: &'static str,
559 message: String,
560 },
561}
562
563impl ChatRequestError {
564 pub fn api_error_code(&self) -> &'static str {
566 match self {
567 Self::MissingField { .. } | Self::InvalidRequest { .. } | Self::InvalidField { .. } => {
568 "invalid_request"
569 }
570 Self::UnsupportedField { .. } => "unsupported_request_field",
571 Self::UnsupportedMessageRole { .. } => "unsupported_message_role",
572 Self::UnsupportedMessageContent { .. } => "unsupported_message_content",
573 Self::InvalidToolCallHistory { .. } => "invalid_tool_call_history",
574 Self::UnsupportedVeniceParameter { .. } => "unsupported_venice_parameter",
575 }
576 }
577
578 pub(crate) fn invalid(message: impl Into<String>) -> Self {
580 Self::InvalidRequest {
581 message: message.into(),
582 }
583 }
584
585 pub(crate) fn invalid_field(field: &'static str, message: impl Into<String>) -> Self {
587 Self::InvalidField {
588 field,
589 message: message.into(),
590 }
591 }
592
593 pub(crate) fn unsupported_content(path: impl Into<String>, message: impl Into<String>) -> Self {
595 Self::UnsupportedMessageContent {
596 path: path.into(),
597 message: message.into(),
598 }
599 }
600
601 pub(crate) fn invalid_tool_history(message: impl Into<String>) -> Self {
603 Self::InvalidToolCallHistory {
604 message: message.into(),
605 }
606 }
607}
608
609#[derive(Debug, Error)]
611pub enum ChatConstructionError {
612 #[error(transparent)]
613 E2ee(#[from] E2eeCodecError),
614}
615
616impl ChatConstructionError {
617 pub fn api_error_code(&self) -> &'static str {
619 match self {
620 Self::E2ee(_) => "e2ee_request_encryption_failed",
621 }
622 }
623}
624
625fn normalize_messages(value: &Value) -> Result<Vec<NormalizedChatMessage>, ChatRequestError> {
627 let messages = value
628 .as_array()
629 .ok_or_else(|| ChatRequestError::invalid_field("messages", "messages must be an array"))?;
630 if messages.is_empty() {
631 return Err(ChatRequestError::invalid_field(
632 "messages",
633 "messages must include at least one message",
634 ));
635 }
636
637 messages
638 .iter()
639 .enumerate()
640 .map(|(index, value)| normalize_message(index, value))
641 .collect()
642}
643
644fn normalize_message(
646 index: usize,
647 value: &Value,
648) -> Result<NormalizedChatMessage, ChatRequestError> {
649 let object = value.as_object().ok_or_else(|| {
650 ChatRequestError::invalid_field("messages", format!("message {index} must be an object"))
651 })?;
652 let role = required_non_empty_string(object, "role")?;
653
654 match role {
655 "system" | "developer" | "user" => {
656 reject_unknown_fields(object, &["role", "content"], "message")?;
657 let content = required_content_text(
658 object.get("content"),
659 &format!("messages[{index}].content"),
660 )?;
661 Ok(NormalizedChatMessage::new(role, content))
662 }
663 "assistant" => normalize_assistant_message(index, object),
664 "tool" => normalize_tool_result_message(index, object),
665 other => Err(ChatRequestError::UnsupportedMessageRole {
666 role: other.to_owned(),
667 }),
668 }
669}
670
671fn normalize_assistant_message(
673 index: usize,
674 object: &Map<String, Value>,
675) -> Result<NormalizedChatMessage, ChatRequestError> {
676 reject_unknown_fields(
677 object,
678 &["role", "content", "tool_calls"],
679 "assistant message",
680 )?;
681 let content = optional_content_text(
682 object.get("content"),
683 &format!("messages[{index}].content"),
684 true,
685 )?;
686 let tool_calls = normalize_assistant_tool_calls(object.get("tool_calls"))?;
687
688 if content.as_deref().unwrap_or_default().is_empty() && tool_calls.is_none() {
689 return Err(ChatRequestError::invalid_field(
690 "messages",
691 "assistant messages must include string content or a supported tool_calls history entry",
692 ));
693 }
694
695 let normalized_content = match (content, tool_calls) {
696 (Some(content), Some(tool_calls)) if !content.is_empty() => {
697 format!("{content}\n\n{tool_calls}")
698 }
699 (Some(content), _) => content,
700 (None, Some(tool_calls)) => tool_calls,
701 (None, None) => unreachable!("empty assistant messages are rejected above"),
702 };
703
704 Ok(NormalizedChatMessage::new("assistant", normalized_content))
705}
706
707fn normalize_tool_result_message(
709 index: usize,
710 object: &Map<String, Value>,
711) -> Result<NormalizedChatMessage, ChatRequestError> {
712 reject_unknown_fields(object, &["role", "tool_call_id", "content"], "tool message")?;
713 let tool_call_id = required_non_empty_string(object, "tool_call_id")?;
714 let content =
715 required_content_text(object.get("content"), &format!("messages[{index}].content"))?;
716 let normalized = format!(
717 "<tool_result id=\"{}\">\n{}\n</tool_result>\n\nUse the tool result above to continue the answer.",
718 xml_escape_attr(tool_call_id),
719 content,
720 );
721
722 Ok(NormalizedChatMessage::new("user", normalized))
724}
725
726fn normalize_assistant_tool_calls(
728 value: Option<&Value>,
729) -> Result<Option<String>, ChatRequestError> {
730 let Some(value) = value else {
731 return Ok(None);
732 };
733
734 if !value.is_array() {
735 return Err(ChatRequestError::invalid_tool_history(
736 "assistant tool_calls must be an array",
737 ));
738 }
739 let tool_calls: Vec<RawAssistantToolCall> = serde_json::from_value(value.clone()).map_err(
740 |source| {
741 ChatRequestError::invalid_tool_history(format!(
742 "assistant tool_calls must be an array of supported function tool call objects: {source}"
743 ))
744 },
745 )?;
746
747 if tool_calls.is_empty() {
748 return Err(ChatRequestError::invalid_tool_history(
749 "assistant tool_calls must not be empty when provided",
750 ));
751 }
752
753 let rendered = tool_calls
754 .iter()
755 .map(render_assistant_tool_call)
756 .collect::<Result<Vec<String>, ChatRequestError>>()?;
757
758 Ok(Some(rendered.join("\n")))
759}
760
761fn render_assistant_tool_call(
763 tool_call: &RawAssistantToolCall,
764) -> Result<String, ChatRequestError> {
765 let id = non_empty_typed_string(&tool_call.id, "tool_call.id")?;
766
767 if tool_call.kind != "function" {
768 return Err(ChatRequestError::invalid_tool_history(format!(
769 "only function tool calls are supported, got {:?}",
770 tool_call.kind
771 )));
772 }
773
774 let name = non_empty_typed_string(&tool_call.function.name, "tool_call.function.name")?;
775 let arguments = non_empty_typed_string(
776 &tool_call.function.arguments,
777 "tool_call.function.arguments",
778 )?;
779 let parsed_arguments: Value = serde_json::from_str(arguments).map_err(|source| {
780 ChatRequestError::invalid_tool_history(format!(
781 "tool_call.function.arguments must be valid JSON: {source}"
782 ))
783 })?;
784 let canonical_arguments = serde_json::to_string(&parsed_arguments).map_err(|source| {
785 ChatRequestError::invalid_tool_history(format!(
786 "tool_call.function.arguments could not be serialized as JSON: {source}"
787 ))
788 })?;
789
790 Ok(format!(
791 "<previous_tool_call id=\"{}\" name=\"{}\">\n{}\n</previous_tool_call>",
792 xml_escape_attr(id),
793 xml_escape_attr(name),
794 canonical_arguments,
795 ))
796}
797
798fn required_content_text(value: Option<&Value>, path: &str) -> Result<String, ChatRequestError> {
800 optional_content_text(value, path, false)?.ok_or_else(|| {
801 ChatRequestError::unsupported_content(path, "content is required and must not be null")
802 })
803}
804
805fn optional_content_text(
807 value: Option<&Value>,
808 path: &str,
809 allow_null: bool,
810) -> Result<Option<String>, ChatRequestError> {
811 match value {
812 Some(Value::String(content)) => Ok(Some(content.clone())),
813 Some(Value::Null) if allow_null => Ok(None),
814 Some(Value::Null) => Err(ChatRequestError::unsupported_content(
815 path,
816 "null content is only supported for assistant messages with tool_calls",
817 )),
818 Some(Value::Array(parts)) => normalize_text_parts(parts, path).map(Some),
819 Some(other) => Err(ChatRequestError::unsupported_content(
820 path,
821 format!(
822 "expected a string or text-only content parts array, got {}",
823 json_kind(other)
824 ),
825 )),
826 None if allow_null => Ok(None),
827 None => Err(ChatRequestError::unsupported_content(
828 path,
829 "content is required",
830 )),
831 }
832}
833
834fn normalize_text_parts(parts: &[Value], path: &str) -> Result<String, ChatRequestError> {
836 if parts.is_empty() {
837 return Err(ChatRequestError::unsupported_content(
838 path,
839 "content parts array must not be empty",
840 ));
841 }
842
843 let mut text = String::new();
844 for (index, part) in parts.iter().enumerate() {
845 let part: RawTextContentPart = serde_json::from_value(part.clone()).map_err(|source| {
846 ChatRequestError::unsupported_content(
847 format!("{path}[{index}]"),
848 format!("text content part must match {{type:\"text\", text:string}}: {source}"),
849 )
850 })?;
851 text.push_str(&part.text);
852 }
853 Ok(text)
854}
855
856fn parse_tools(value: Option<&Value>) -> Result<Vec<ChatToolDefinition>, ChatRequestError> {
858 match value {
859 None => Ok(Vec::new()),
860 Some(value) => deserialize_typed_value("tools", value),
861 }
862}
863
864fn parse_tool_choice(value: Option<&Value>) -> Result<ChatToolChoice, ChatRequestError> {
866 let Some(value) = value else {
867 return Ok(ChatToolChoice::default());
868 };
869 deserialize_typed_value::<Option<ChatToolChoice>>("tool_choice", value)
870 .map(|choice| choice.unwrap_or_default())
871}
872
873fn validate_tools(tools: &[ChatToolDefinition]) -> Result<(), ChatRequestError> {
875 if tools.iter().any(|tool| tool.name().trim().is_empty()) {
876 return Err(ChatRequestError::invalid_field(
877 "tools",
878 "function tool names must not be empty",
879 ));
880 }
881 Ok(())
882}
883
884fn validate_tool_choice(tool_choice: &ChatToolChoice) -> Result<(), ChatRequestError> {
886 if let ChatToolChoice::Function { name } = tool_choice
887 && name.trim().is_empty()
888 {
889 return Err(ChatRequestError::invalid_field(
890 "tool_choice",
891 "function tool_choice name must not be empty",
892 ));
893 }
894 Ok(())
895}
896
897fn validate_reasoning_effort_consistency(
899 reasoning: Option<&ReasoningOptions>,
900 reasoning_effort: Option<&str>,
901) -> Result<(), ChatRequestError> {
902 let Some(nested_effort) = reasoning.and_then(|reasoning| reasoning.effort.as_deref()) else {
903 return Ok(());
904 };
905 let Some(flat_effort) = reasoning_effort else {
906 return Ok(());
907 };
908 if nested_effort != flat_effort {
909 return Err(ChatRequestError::invalid_field(
910 "reasoning_effort",
911 "must match reasoning.effort when both are provided",
912 ));
913 }
914 Ok(())
915}
916
917fn validate_ignored_client_only_fields(
919 object: &Map<String, Value>,
920) -> Result<(), ChatRequestError> {
921 if let Some(metadata) = object.get("metadata")
922 && !(metadata.is_object() || metadata.is_null())
923 {
924 return Err(ChatRequestError::invalid_field(
925 "metadata",
926 "metadata must be an object when provided",
927 ));
928 }
929 Ok(())
930}
931
932fn non_empty_typed_string<'a>(
934 value: &'a str,
935 field: &'static str,
936) -> Result<&'a str, ChatRequestError> {
937 if value.trim().is_empty() {
938 return Err(ChatRequestError::invalid_tool_history(format!(
939 "{field} must not be empty"
940 )));
941 }
942 Ok(value)
943}
944
945fn required_non_empty_string<'a>(
947 object: &'a Map<String, Value>,
948 field: &'static str,
949) -> Result<&'a str, ChatRequestError> {
950 let value = object
951 .get(field)
952 .ok_or(ChatRequestError::MissingField { field })?;
953 let string = value.as_str().ok_or_else(|| {
954 ChatRequestError::invalid_field(field, format!("expected string, got {}", json_kind(value)))
955 })?;
956 if string.trim().is_empty() {
957 return Err(ChatRequestError::invalid_field(field, "must not be empty"));
958 }
959 Ok(string)
960}
961
962fn optional_bool(
964 object: &Map<String, Value>,
965 field: &'static str,
966) -> Result<Option<bool>, ChatRequestError> {
967 object
968 .get(field)
969 .map(|value| {
970 value.as_bool().ok_or_else(|| {
971 ChatRequestError::invalid_field(
972 field,
973 format!("expected boolean, got {}", json_kind(value)),
974 )
975 })
976 })
977 .transpose()
978}
979
980fn optional_non_empty_string(
982 object: &Map<String, Value>,
983 field: &'static str,
984) -> Result<Option<String>, ChatRequestError> {
985 match object.get(field) {
986 None | Some(Value::Null) => Ok(None),
987 Some(Value::String(value)) if value.trim().is_empty() => {
988 Err(ChatRequestError::invalid_field(field, "must not be empty"))
989 }
990 Some(Value::String(value)) => Ok(Some(value.clone())),
991 Some(value) => Err(ChatRequestError::invalid_field(
992 field,
993 format!("expected string, got {}", json_kind(value)),
994 )),
995 }
996}
997
998fn optional_number(
1000 object: &Map<String, Value>,
1001 field: &'static str,
1002) -> Result<Option<Value>, ChatRequestError> {
1003 match object.get(field) {
1004 None | Some(Value::Null) => Ok(None),
1005 Some(value) => {
1006 let number = deserialize_typed_value::<serde_json::Number>(field, value)?;
1007 Ok(Some(Value::Number(number)))
1008 }
1009 }
1010}
1011
1012fn optional_u64(
1014 object: &Map<String, Value>,
1015 field: &'static str,
1016) -> Result<Option<u64>, ChatRequestError> {
1017 match object.get(field) {
1018 None | Some(Value::Null) => Ok(None),
1019 Some(value) => deserialize_typed_value(field, value).map(Some),
1020 }
1021}
1022
1023fn optional_stop(object: &Map<String, Value>) -> Result<Option<StopSequence>, ChatRequestError> {
1025 match object.get("stop") {
1026 None | Some(Value::Null) => Ok(None),
1027 Some(value) => deserialize_typed_value("stop", value).map(Some),
1028 }
1029}
1030
1031fn deserialize_typed_value<T>(field: &'static str, value: &Value) -> Result<T, ChatRequestError>
1033where
1034 T: DeserializeOwned,
1035{
1036 serde_json::from_value(value.clone()).map_err(|source| {
1037 ChatRequestError::invalid_field(
1038 field,
1039 format!(
1040 "expected supported shape, got {}: {source}",
1041 json_kind(value)
1042 ),
1043 )
1044 })
1045}
1046
1047fn deserialize_optional_bool_reject_null<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
1049where
1050 D: serde::Deserializer<'de>,
1051{
1052 let value = Value::deserialize(deserializer)?;
1053 match value {
1054 Value::Bool(value) => Ok(Some(value)),
1055 other => Err(serde::de::Error::custom(format!(
1056 "expected boolean, got {}",
1057 json_kind(&other)
1058 ))),
1059 }
1060}
1061
1062fn deserialize_optional_non_empty_string_reject_null<'de, D>(
1064 deserializer: D,
1065) -> Result<Option<String>, D::Error>
1066where
1067 D: serde::Deserializer<'de>,
1068{
1069 let value = Value::deserialize(deserializer)?;
1070 match value {
1071 Value::String(value) if value.trim().is_empty() => {
1072 Err(serde::de::Error::custom("must not be empty"))
1073 }
1074 Value::String(value) => Ok(Some(value)),
1075 other => Err(serde::de::Error::custom(format!(
1076 "expected string, got {}",
1077 json_kind(&other)
1078 ))),
1079 }
1080}
1081
1082fn deserialize_optional_web_search_reject_null<'de, D>(
1084 deserializer: D,
1085) -> Result<Option<RawVeniceWebSearch>, D::Error>
1086where
1087 D: serde::Deserializer<'de>,
1088{
1089 let value = Value::deserialize(deserializer)?;
1090 match value {
1091 Value::String(value) => Ok(Some(RawVeniceWebSearch::String(value))),
1092 Value::Bool(value) => Ok(Some(RawVeniceWebSearch::Bool(value))),
1093 other => Err(serde::de::Error::custom(format!(
1094 "expected string or boolean, got {}",
1095 json_kind(&other)
1096 ))),
1097 }
1098}
1099
1100fn reject_unknown_fields(
1102 object: &Map<String, Value>,
1103 allowed: &[&str],
1104 _context: &str,
1105) -> Result<(), ChatRequestError> {
1106 if let Some(field) = object
1107 .keys()
1108 .find(|field| !allowed.contains(&field.as_str()))
1109 {
1110 return Err(ChatRequestError::UnsupportedField {
1111 field: field.clone(),
1112 });
1113 }
1114 Ok(())
1115}
1116
1117fn xml_escape_attr(value: &str) -> String {
1119 let mut escaped = String::new();
1120 for ch in value.chars() {
1121 match ch {
1122 '&' => escaped.push_str("&"),
1123 '"' => escaped.push_str("""),
1124 '<' => escaped.push_str("<"),
1125 '>' => escaped.push_str(">"),
1126 _ => escaped.push(ch),
1127 }
1128 }
1129 escaped
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135 use k256::{SecretKey, elliptic_curve::sec1::ToEncodedPoint};
1136 use serde_json::json;
1137
1138 fn parse(value: Value) -> ChatCompletionRequest {
1139 ChatCompletionRequest::parse(&value).expect("request should parse")
1140 }
1141
1142 fn model_public_key_hex(secret_key: &SecretKey) -> String {
1143 let public_key = secret_key.public_key();
1144 hex::encode(public_key.to_encoded_point(false).as_bytes())
1145 }
1146
1147 #[test]
1148 fn normalizes_system_user_and_assistant_text_messages() {
1149 let request = parse(json!({
1150 "model": "e2ee-test",
1151 "messages": [
1152 {"role": "system", "content": "You are concise."},
1153 {"role": "user", "content": [{"type":"text", "text":"Hello"}]},
1154 {"role": "assistant", "content": "Hi"}
1155 ]
1156 }));
1157
1158 assert_eq!(
1159 request.messages,
1160 vec![
1161 NormalizedChatMessage::new("system", "You are concise."),
1162 NormalizedChatMessage::new("user", "Hello"),
1163 NormalizedChatMessage::new("assistant", "Hi"),
1164 ]
1165 );
1166 }
1167
1168 #[test]
1169 fn normalizes_assistant_tool_call_history() {
1170 let request = parse(json!({
1171 "model": "e2ee-test",
1172 "messages": [
1173 {
1174 "role": "assistant",
1175 "content": null,
1176 "tool_calls": [{
1177 "id": "call_abc",
1178 "type": "function",
1179 "function": {
1180 "name": "search_web",
1181 "arguments": "{\"query\":\"Venice E2EE\"}"
1182 }
1183 }]
1184 }
1185 ]
1186 }));
1187
1188 assert_eq!(
1189 request.messages[0],
1190 NormalizedChatMessage::new(
1191 "assistant",
1192 "<previous_tool_call id=\"call_abc\" name=\"search_web\">\n{\"query\":\"Venice E2EE\"}\n</previous_tool_call>",
1193 )
1194 );
1195 }
1196
1197 #[test]
1198 fn normalizes_parallel_assistant_tool_call_history() {
1199 let request = parse(json!({
1200 "model": "e2ee-test",
1201 "messages": [
1202 {
1203 "role": "assistant",
1204 "content": null,
1205 "tool_calls": [
1206 {
1207 "id": "call_one",
1208 "type": "function",
1209 "function": {
1210 "name": "search_web",
1211 "arguments": "{\"query\":\"Venice E2EE\"}"
1212 }
1213 },
1214 {
1215 "id": "call_two",
1216 "type": "function",
1217 "function": {
1218 "name": "get_weather",
1219 "arguments": "{\"city\":\"Venice\"}"
1220 }
1221 }
1222 ]
1223 }
1224 ]
1225 }));
1226
1227 assert_eq!(
1228 request.messages[0],
1229 NormalizedChatMessage::new(
1230 "assistant",
1231 "<previous_tool_call id=\"call_one\" name=\"search_web\">\n{\"query\":\"Venice E2EE\"}\n</previous_tool_call>\n<previous_tool_call id=\"call_two\" name=\"get_weather\">\n{\"city\":\"Venice\"}\n</previous_tool_call>",
1232 )
1233 );
1234 }
1235
1236 #[test]
1237 fn normalizes_tool_result_messages_as_user_context() {
1238 let request = parse(json!({
1239 "model": "e2ee-test",
1240 "messages": [
1241 {"role": "tool", "tool_call_id": "call_abc", "content": "result text"}
1242 ]
1243 }));
1244
1245 assert_eq!(
1246 request.messages[0],
1247 NormalizedChatMessage::new(
1248 "user",
1249 "<tool_result id=\"call_abc\">\nresult text\n</tool_result>\n\nUse the tool result above to continue the answer.",
1250 )
1251 );
1252 }
1253
1254 #[test]
1255 fn parses_tools_into_typed_function_envelopes() {
1256 let request = parse(json!({
1257 "model": "e2ee-test",
1258 "messages": [{"role":"user", "content":"hi"}],
1259 "tools": [{
1260 "type": "function",
1261 "function": {
1262 "name": "search_web",
1263 "description": "Search the web",
1264 "parameters": {
1265 "type": "object",
1266 "properties": {"query": {"type": "string"}},
1267 "required": ["query"]
1268 }
1269 }
1270 }]
1271 }));
1272
1273 assert_eq!(request.tools.len(), 1);
1274 let tool = &request.tools[0];
1275 let function = tool.function();
1276 assert_eq!(tool.name(), "search_web");
1277 assert_eq!(function.description.as_deref(), Some("Search the web"));
1278 assert_eq!(
1279 tool.parameters_schema()
1280 .and_then(|schema| schema.get("required")),
1281 Some(&json!(["query"]))
1282 );
1283 assert_eq!(
1284 serde_json::to_value(tool).expect("tool should serialize"),
1285 json!({
1286 "type": "function",
1287 "function": {
1288 "name": "search_web",
1289 "description": "Search the web",
1290 "parameters": {
1291 "type": "object",
1292 "properties": {"query": {"type": "string"}},
1293 "required": ["query"]
1294 }
1295 }
1296 })
1297 );
1298 }
1299
1300 #[test]
1301 fn parses_tool_choice_into_typed_shapes() {
1302 let required = parse(json!({
1303 "model": "e2ee-test",
1304 "messages": [{"role":"user", "content":"hi"}],
1305 "tool_choice": "required"
1306 }));
1307 assert_eq!(required.tool_choice, ChatToolChoice::Required);
1308
1309 let specific = parse(json!({
1310 "model": "e2ee-test",
1311 "messages": [{"role":"user", "content":"hi"}],
1312 "tool_choice": {"type":"function", "function":{"name":"search_web"}}
1313 }));
1314 assert_eq!(
1315 specific.tool_choice,
1316 ChatToolChoice::Function {
1317 name: "search_web".to_owned()
1318 }
1319 );
1320
1321 let null_choice = parse(json!({
1322 "model": "e2ee-test",
1323 "messages": [{"role":"user", "content":"hi"}],
1324 "tool_choice": null
1325 }));
1326 assert_eq!(null_choice.tool_choice, ChatToolChoice::Auto);
1327 }
1328
1329 #[test]
1330 fn rejects_invalid_tool_and_tool_choice_shapes() {
1331 for body in [
1332 json!({
1333 "model": "e2ee-test",
1334 "messages": [{"role":"user", "content":"hi"}],
1335 "tools": ["not an object"]
1336 }),
1337 json!({
1338 "model": "e2ee-test",
1339 "messages": [{"role":"user", "content":"hi"}],
1340 "tools": [{"type":"web_search", "function":{"name":"search_web"}}]
1341 }),
1342 json!({
1343 "model": "e2ee-test",
1344 "messages": [{"role":"user", "content":"hi"}],
1345 "tools": [{"type":"function", "function":{"name":"search_web", "description": 42}}]
1346 }),
1347 json!({
1348 "model": "e2ee-test",
1349 "messages": [{"role":"user", "content":"hi"}],
1350 "tools": [{"type":"function", "function":{"name":"search_web", "parameters": []}}]
1351 }),
1352 json!({
1353 "model": "e2ee-test",
1354 "messages": [{"role":"user", "content":"hi"}],
1355 "tools": [{"type":"function", "function":{}}]
1356 }),
1357 json!({
1358 "model": "e2ee-test",
1359 "messages": [{"role":"user", "content":"hi"}],
1360 "tools": [{"type":"function", "function":{"name":""}}]
1361 }),
1362 json!({
1363 "model": "e2ee-test",
1364 "messages": [{"role":"user", "content":"hi"}],
1365 "tools": [{"type":"function", "function":{"name":"search_web", "extra": true}}]
1366 }),
1367 json!({
1368 "model": "e2ee-test",
1369 "messages": [{"role":"user", "content":"hi"}],
1370 "tool_choice": 42
1371 }),
1372 json!({
1373 "model": "e2ee-test",
1374 "messages": [{"role":"user", "content":"hi"}],
1375 "tool_choice": "always"
1376 }),
1377 json!({
1378 "model": "e2ee-test",
1379 "messages": [{"role":"user", "content":"hi"}],
1380 "tool_choice": {"type":"web_search", "function":{"name":"search_web"}}
1381 }),
1382 json!({
1383 "model": "e2ee-test",
1384 "messages": [{"role":"user", "content":"hi"}],
1385 "tool_choice": {"type":"function", "function":{"name":""}}
1386 }),
1387 ] {
1388 let error = ChatCompletionRequest::parse(&body)
1389 .expect_err("invalid tool shape should be rejected");
1390 assert_eq!(error.api_error_code(), "invalid_request");
1391 }
1392 }
1393
1394 #[test]
1395 fn rejects_unsupported_roles_and_content_shapes() {
1396 let role_error = ChatCompletionRequest::parse(&json!({
1397 "model": "e2ee-test",
1398 "messages": [{"role":"function", "content":"legacy"}]
1399 }))
1400 .expect_err("legacy function role should be rejected");
1401 assert_eq!(role_error.api_error_code(), "unsupported_message_role");
1402
1403 let content_error = ChatCompletionRequest::parse(&json!({
1404 "model": "e2ee-test",
1405 "messages": [{"role":"user", "content":[{"type":"image_url", "image_url":{"url":"x"}}]}]
1406 }))
1407 .expect_err("image content should be rejected");
1408 assert_eq!(
1409 content_error.api_error_code(),
1410 "unsupported_message_content"
1411 );
1412
1413 let assistant_error = ChatCompletionRequest::parse(&json!({
1414 "model": "e2ee-test",
1415 "messages": [{"role":"assistant", "content": null}]
1416 }))
1417 .expect_err("assistant null content without tool call should be rejected");
1418 assert_eq!(assistant_error.api_error_code(), "invalid_request");
1419 }
1420
1421 #[test]
1422 fn rejects_unsupported_top_level_fields_and_unsafe_venice_parameters() {
1423 let field_error = ChatCompletionRequest::parse(&json!({
1424 "model": "e2ee-test",
1425 "messages": [{"role":"user", "content":"hi"}],
1426 "file_ids": ["file_1"]
1427 }))
1428 .expect_err("unsupported top-level field should be rejected");
1429 assert_eq!(field_error.api_error_code(), "unsupported_request_field");
1430
1431 let web_search_error = ChatCompletionRequest::parse(&json!({
1432 "model": "e2ee-test",
1433 "messages": [{"role":"user", "content":"hi"}],
1434 "venice_parameters": {"enable_web_search": "on"}
1435 }))
1436 .expect_err("web search should be rejected for E2EE");
1437 assert_eq!(
1438 web_search_error.api_error_code(),
1439 "unsupported_venice_parameter"
1440 );
1441
1442 let mismatched_reasoning_effort = ChatCompletionRequest::parse(&json!({
1443 "model": "e2ee-test",
1444 "messages": [{"role":"user", "content":"hi"}],
1445 "reasoning": {"effort": "low"},
1446 "reasoning_effort": "high"
1447 }))
1448 .expect_err("mismatched reasoning efforts should be rejected");
1449 assert_eq!(
1450 mismatched_reasoning_effort.api_error_code(),
1451 "invalid_request"
1452 );
1453 }
1454
1455 #[test]
1456 fn rejects_null_or_invalid_typed_subfields_without_silent_option_coercion() {
1457 let stream_options_null = ChatCompletionRequest::parse(&json!({
1458 "model": "e2ee-test",
1459 "messages": [{"role":"user", "content":"hi"}],
1460 "stream_options": null
1461 }))
1462 .expect_err("stream_options null should be rejected");
1463 assert_eq!(stream_options_null.api_error_code(), "invalid_request");
1464
1465 let include_usage_null = ChatCompletionRequest::parse(&json!({
1466 "model": "e2ee-test",
1467 "messages": [{"role":"user", "content":"hi"}],
1468 "stream_options": {"include_usage": null}
1469 }))
1470 .expect_err("stream_options.include_usage null should be rejected");
1471 assert_eq!(include_usage_null.api_error_code(), "invalid_request");
1472
1473 let venice_params_null = ChatCompletionRequest::parse(&json!({
1474 "model": "e2ee-test",
1475 "messages": [{"role":"user", "content":"hi"}],
1476 "venice_parameters": null
1477 }))
1478 .expect_err("venice_parameters null should be rejected");
1479 assert_eq!(venice_params_null.api_error_code(), "invalid_request");
1480
1481 let invalid_stop = ChatCompletionRequest::parse(&json!({
1482 "model": "e2ee-test",
1483 "messages": [{"role":"user", "content":"hi"}],
1484 "stop": ["ok", 42]
1485 }))
1486 .expect_err("mixed stop array should be rejected");
1487 assert_eq!(invalid_stop.api_error_code(), "invalid_request");
1488 }
1489
1490 #[test]
1491 fn serde_layer_rejects_unknown_nested_fields_and_wrong_types() {
1492 let stream_options_unknown = ChatCompletionRequest::parse(&json!({
1493 "model": "e2ee-test",
1494 "messages": [{"role":"user", "content":"hi"}],
1495 "stream_options": {"include_usage": true, "extra": 1}
1496 }))
1497 .expect_err("unknown stream_options field should be rejected");
1498 assert_eq!(stream_options_unknown.api_error_code(), "invalid_request");
1499 assert!(
1500 stream_options_unknown
1501 .to_string()
1502 .contains("unknown field `extra`"),
1503 "unexpected message: {stream_options_unknown}"
1504 );
1505
1506 let include_usage_string = ChatCompletionRequest::parse(&json!({
1507 "model": "e2ee-test",
1508 "messages": [{"role":"user", "content":"hi"}],
1509 "stream_options": {"include_usage": "yes"}
1510 }))
1511 .expect_err("non-boolean include_usage should be rejected");
1512 assert_eq!(include_usage_string.api_error_code(), "invalid_request");
1513 assert!(
1514 include_usage_string
1515 .to_string()
1516 .contains("expected boolean, got string"),
1517 "unexpected message: {include_usage_string}"
1518 );
1519
1520 let venice_unknown = ChatCompletionRequest::parse(&json!({
1521 "model": "e2ee-test",
1522 "messages": [{"role":"user", "content":"hi"}],
1523 "venice_parameters": {"unknown_param": true}
1524 }))
1525 .expect_err("unknown venice_parameters field should be rejected");
1526 assert_eq!(venice_unknown.api_error_code(), "invalid_request");
1527 assert!(
1528 venice_unknown
1529 .to_string()
1530 .contains("unknown field `unknown_param`"),
1531 "unexpected message: {venice_unknown}"
1532 );
1533
1534 let enable_e2ee_string = ChatCompletionRequest::parse(&json!({
1535 "model": "e2ee-test",
1536 "messages": [{"role":"user", "content":"hi"}],
1537 "venice_parameters": {"enable_e2ee": "yes"}
1538 }))
1539 .expect_err("non-boolean enable_e2ee should be rejected");
1540 assert_eq!(enable_e2ee_string.api_error_code(), "invalid_request");
1541 assert!(
1542 enable_e2ee_string
1543 .to_string()
1544 .contains("expected boolean, got string"),
1545 "unexpected message: {enable_e2ee_string}"
1546 );
1547
1548 let web_search_number = ChatCompletionRequest::parse(&json!({
1549 "model": "e2ee-test",
1550 "messages": [{"role":"user", "content":"hi"}],
1551 "venice_parameters": {"enable_web_search": 42}
1552 }))
1553 .expect_err("non-string/boolean enable_web_search should be rejected");
1554 assert_eq!(web_search_number.api_error_code(), "invalid_request");
1555 assert!(
1556 web_search_number
1557 .to_string()
1558 .contains("expected string or boolean, got number"),
1559 "unexpected message: {web_search_number}"
1560 );
1561
1562 let null_enable_e2ee = ChatCompletionRequest::parse(&json!({
1563 "model": "e2ee-test",
1564 "messages": [{"role":"user", "content":"hi"}],
1565 "venice_parameters": {"enable_e2ee": null}
1566 }))
1567 .expect_err("null enable_e2ee should be rejected");
1568 assert_eq!(null_enable_e2ee.api_error_code(), "invalid_request");
1569
1570 let content_part_unknown = ChatCompletionRequest::parse(&json!({
1571 "model": "e2ee-test",
1572 "messages": [{"role":"user", "content":[{"type":"text", "text":"hi", "extra":1}]}]
1573 }))
1574 .expect_err("unknown content part field should be rejected");
1575 assert_eq!(
1576 content_part_unknown.api_error_code(),
1577 "unsupported_message_content"
1578 );
1579 assert!(
1580 content_part_unknown
1581 .to_string()
1582 .contains("unknown field `extra`"),
1583 "unexpected message: {content_part_unknown}"
1584 );
1585
1586 let content_part_non_object = ChatCompletionRequest::parse(&json!({
1587 "model": "e2ee-test",
1588 "messages": [{"role":"user", "content":["plain string part"]}]
1589 }))
1590 .expect_err("non-object content part should be rejected");
1591 assert_eq!(
1592 content_part_non_object.api_error_code(),
1593 "unsupported_message_content"
1594 );
1595 }
1596
1597 #[test]
1598 fn parses_and_forwards_reasoning_controls() {
1599 let model_key = SecretKey::random(&mut rand_core::OsRng);
1600 let model_public_key = model_public_key_hex(&model_key);
1601 let codec = E2eeCodec::default();
1602 let request = parse(json!({
1603 "model": "e2ee-test",
1604 "messages": [{"role":"user", "content":"hi"}],
1605 "reasoning": {"enabled": true, "effort": "high"},
1606 "reasoning_effort": "high",
1607 "venice_parameters": {
1608 "strip_thinking_response": true,
1609 "disable_thinking": false
1610 }
1611 }));
1612
1613 let reasoning = request.reasoning.as_ref().expect("reasoning should parse");
1614 assert_eq!(reasoning.enabled, Some(true));
1615 assert_eq!(reasoning.effort.as_deref(), Some("high"));
1616 assert_eq!(request.reasoning_effort.as_deref(), Some("high"));
1617 assert!(request.venice_parameters.strip_thinking_response);
1618 assert!(!request.venice_parameters.disable_thinking);
1619
1620 let prepared = request
1621 .to_venice_e2ee_request(&codec, &model_public_key)
1622 .expect("request should encrypt");
1623 assert_eq!(prepared.upstream.reasoning, request.reasoning);
1624 assert_eq!(prepared.upstream.reasoning_effort.as_deref(), Some("high"));
1625 assert!(prepared.upstream.venice_parameters.strip_thinking_response);
1626 assert!(!prepared.upstream.venice_parameters.disable_thinking);
1627
1628 let upstream =
1629 serde_json::to_value(&prepared.upstream).expect("upstream request should serialize");
1630 assert_eq!(upstream["reasoning"]["enabled"], true);
1631 assert_eq!(upstream["reasoning"]["effort"], "high");
1632 assert_eq!(upstream["reasoning_effort"], "high");
1633 assert_eq!(
1634 upstream["venice_parameters"]["strip_thinking_response"],
1635 true
1636 );
1637 assert_eq!(upstream["venice_parameters"]["disable_thinking"], false);
1638 }
1639
1640 #[test]
1641 fn constructs_encrypted_request_for_non_streaming_mode() {
1642 let model_key = SecretKey::random(&mut rand_core::OsRng);
1643 let model_public_key = model_public_key_hex(&model_key);
1644 let codec = E2eeCodec::default();
1645 let request = parse(json!({
1646 "model": "e2ee-test",
1647 "messages": [{"role":"user", "content":"hi"}],
1648 "stream": false,
1649 "temperature": 0.2,
1650 "max_tokens": 64,
1651 "stop": ["END"],
1652 "venice_parameters": {
1653 "include_venice_system_prompt": false,
1654 "enable_web_search": "off"
1655 }
1656 }));
1657
1658 let prepared = request
1659 .to_venice_e2ee_request(&codec, &model_public_key)
1660 .expect("request should encrypt");
1661
1662 assert!(!prepared.client_stream);
1663 assert!(prepared.upstream.stream);
1664 assert!(prepared.upstream.stream_options.include_usage);
1665 assert_eq!(prepared.upstream.temperature, Some(json!(0.2)));
1666 assert_eq!(prepared.upstream.max_tokens, Some(64));
1667 assert_eq!(
1668 prepared.upstream.stop,
1669 Some(StopSequence::Strings(vec!["END".to_owned()]))
1670 );
1671 assert_eq!(
1672 prepared.upstream.venice_parameters,
1673 VeniceParameters::default()
1674 );
1675
1676 assert_eq!(prepared.upstream.messages.len(), request.messages.len());
1677 assert_eq!(prepared.upstream.messages[0].role, "user");
1678 assert_ne!(
1679 prepared.upstream.messages[0].content,
1680 request.messages[0].content
1681 );
1682 let payload =
1683 crate::e2ee::EncryptedPayload::from_hex(&prepared.upstream.messages[0].content)
1684 .expect("message content should be encrypted hex");
1685 let plaintext = codec
1686 .decrypt_content(&payload, &model_key)
1687 .expect("test model key should decrypt message content");
1688 assert_eq!(plaintext, request.messages[0].content);
1689 }
1690
1691 #[test]
1692 fn constructs_encrypted_request_for_streaming_mode_and_usage_option() {
1693 let model_key = SecretKey::random(&mut rand_core::OsRng);
1694 let model_public_key = model_public_key_hex(&model_key);
1695 let codec = E2eeCodec::default();
1696 let request = parse(json!({
1697 "model": "e2ee-test",
1698 "messages": [{"role":"user", "content":"hi"}],
1699 "stream": true,
1700 "stream_options": {"include_usage": false}
1701 }));
1702
1703 let prepared = request
1704 .to_venice_e2ee_request(&codec, &model_public_key)
1705 .expect("request should encrypt");
1706
1707 assert!(prepared.client_stream);
1708 assert!(prepared.upstream.stream);
1709 assert!(!prepared.upstream.stream_options.include_usage);
1710 }
1711
1712 #[test]
1713 fn constructs_encrypted_request_with_tool_controller_and_retry_prompt() {
1714 let model_key = SecretKey::random(&mut rand_core::OsRng);
1715 let model_public_key = model_public_key_hex(&model_key);
1716 let codec = E2eeCodec::default();
1717 let request = parse(json!({
1718 "model": "e2ee-test",
1719 "messages": [{"role":"user", "content":"hi"}],
1720 "tools": [{"type":"function", "function":{"name":"search_web", "parameters":{"type":"object"}}}],
1721 "tool_choice": "required"
1722 }));
1723 let controller = NormalizedChatMessage::new("system", "controller prompt");
1724 let correction = NormalizedChatMessage::new("system", "retry prompt");
1725
1726 let prepared = request
1727 .to_venice_e2ee_request_with_messages(
1728 &codec,
1729 &model_public_key,
1730 std::slice::from_ref(&controller),
1731 std::slice::from_ref(&correction),
1732 )
1733 .expect("request should encrypt");
1734
1735 assert_eq!(prepared.upstream.messages.len(), 3);
1736 assert_eq!(prepared.upstream.messages[0].role, "system");
1737 assert_eq!(prepared.upstream.messages[1].role, "user");
1738 assert_eq!(prepared.upstream.messages[2].role, "system");
1739
1740 let decrypted = prepared
1741 .upstream
1742 .messages
1743 .iter()
1744 .map(|message| {
1745 let payload = crate::e2ee::EncryptedPayload::from_hex(&message.content)
1746 .expect("message content should be encrypted hex");
1747 codec
1748 .decrypt_content(&payload, &model_key)
1749 .expect("test model key should decrypt message content")
1750 })
1751 .collect::<Vec<_>>();
1752 assert_eq!(decrypted, vec!["controller prompt", "hi", "retry prompt"]);
1753 assert!(
1754 !serde_json::to_value(&prepared.upstream)
1755 .expect("upstream request should serialize")
1756 .as_object()
1757 .expect("upstream request should be object")
1758 .contains_key("tools")
1759 );
1760 }
1761}