Skip to main content

kcode_k1_chat_websearch_request/
lib.rs

1use 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}