kcode_k1_chat_websearch_request/
lib.rs1use kcode_k1_codex_websearch::Request;
2use serde::{Deserialize, Deserializer};
3use std::time::Instant;
4
5const INVALID_ARGUMENTS: &str = "WebSearch failed: invalid arguments";
6const UNSUPPORTED_ROUTE: &str =
7 "WebSearch failed: unsupported model route; expected model to begin with \"codex/\"";
8
9#[derive(Deserialize)]
10#[serde(deny_unknown_fields)]
11struct PublicWebSearch {
12 query: String,
13 model: String,
14 #[serde(default, deserialize_with = "present_string")]
15 reasoning_effort: Option<String>,
16}
17
18pub fn parse(arguments: &str, deadline: Instant) -> Result<Request, String> {
19 let public: PublicWebSearch =
20 serde_json::from_str(arguments).map_err(|_| INVALID_ARGUMENTS.to_owned())?;
21 let model = public
22 .model
23 .strip_prefix("codex/")
24 .ok_or_else(|| UNSUPPORTED_ROUTE.to_owned())?;
25 Ok(Request {
26 query: public.query,
27 model: model.to_owned(),
28 reasoning_effort: public
29 .reasoning_effort
30 .unwrap_or_else(|| "medium".to_owned()),
31 deadline,
32 })
33}
34
35fn present_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
36 String::deserialize(deserializer).map(Some)
37}
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42 use std::time::Duration;
43
44 #[test]
45 fn valid_values_are_preserved_and_effort_defaults() {
46 let deadline = Instant::now() + Duration::from_secs(10);
47 let request = parse(
48 r#"{"query":" exact\nquery ","model":"codex/model","reasoning_effort":"high"}"#,
49 deadline,
50 )
51 .unwrap();
52 assert_eq!(request.query, " exact\nquery ");
53 assert_eq!(request.model, "model");
54 assert_eq!(request.reasoning_effort, "high");
55 assert_eq!(request.deadline, deadline);
56
57 let request = parse(r#"{"query":"","model":"codex/m"}"#, deadline).unwrap();
58 assert_eq!(request.query, "");
59 assert_eq!(request.reasoning_effort, "medium");
60 }
61
62 #[test]
63 fn invalid_shapes_are_rejected_uniformly() {
64 for arguments in [
65 "null",
66 "[]",
67 "{}",
68 r#"{"query":"q"}"#,
69 r#"{"model":"codex/m"}"#,
70 r#"{"query":null,"model":"codex/m"}"#,
71 r#"{"query":"q","model":7}"#,
72 r#"{"query":"q","model":"codex/m","reasoning_effort":null}"#,
73 r#"{"query":"q","model":"codex/m","reasoning_effort":7}"#,
74 r#"{"query":"q","model":"codex/m","extra":true}"#,
75 r#"{"question":"q","model":"codex/m"}"#,
76 ] {
77 let error = match parse(arguments, Instant::now()) {
78 Err(error) => error,
79 Ok(_) => panic!("invalid WebSearch arguments were accepted"),
80 };
81 assert_eq!(error, INVALID_ARGUMENTS);
82 }
83 }
84
85 #[test]
86 fn model_route_is_exact_case_sensitive_and_stripped_once() {
87 for model in ["model", "Codex/model", "codexx/model"] {
88 let arguments = format!(r#"{{"query":"q","model":"{model}"}}"#);
89 let error = match parse(&arguments, Instant::now()) {
90 Err(error) => error,
91 Ok(_) => panic!("unsupported WebSearch route was accepted"),
92 };
93 assert_eq!(error, UNSUPPORTED_ROUTE);
94 }
95
96 let request = parse(
97 r#"{"query":"q","model":"codex/codex/model"}"#,
98 Instant::now(),
99 )
100 .unwrap();
101 assert_eq!(request.model, "codex/model");
102 }
103}