Skip to main content

node_app_sdk_rust/llm/
api_store_client.rs

1//! Ergonomic shortcuts for building public LLM call requests (feature 472,
2//! T1012). Resolves UI finding U2 — single-line construction of a request
3//! with a specific routing intent.
4//!
5//! ```ignore
6//! use node_app_sdk_rust::llm::{ExecuteChatRequest, ChatMessage};
7//!
8//! let req = ExecuteChatRequest {
9//!     messages: vec![ChatMessage { role: "user".into(), content: "hi".into() }],
10//!     model: Some("gpt-4o".into()),
11//!     ..Default::default()
12//! }
13//! .private();
14//! ```
15//!
16//! **Honest framing** (per S5/U4): Private mode isolates the upstream
17//! provider's API credentials on the peer's host and routes egress through a
18//! separate device; it does NOT mask prompt contents from the upstream
19//! provider or anonymize the user.
20
21use super::types::{ExecuteBestModelRequest, ExecuteChatRequest, LlmRoute};
22
23// Provide a Default impl so callers can build via the
24// `ExecuteChatRequest { messages, ..Default::default() }.private()` idiom.
25impl Default for ExecuteChatRequest {
26    fn default() -> Self {
27        Self {
28            messages: Vec::new(),
29            model: None,
30            platform: None,
31            route: None,
32            execution_preference: None,
33            max_output_tokens: None,
34            correlation_id: None,
35            tools: None,
36            tool_choice: None,
37            temperature: None,
38            response_format: None,
39        }
40    }
41}
42
43impl Default for ExecuteBestModelRequest {
44    fn default() -> Self {
45        Self {
46            messages: Vec::new(),
47            route: None,
48            execution_preference: None,
49            correlation_id: None,
50        }
51    }
52}
53
54/// Routing-intent shortcuts.
55///
56/// Implemented on every public-LLM request shape. Mirrors the TypeScript
57/// SDK's `.route('public' | 'private' | 'auto')` ergonomics with idiomatic
58/// Rust verbs (`.public()`, `.private()`, `.auto()`).
59pub trait WithRoute: Sized {
60    /// Set [`LlmRoute::Public`] — force direct upstream-provider execution,
61    /// skipping any custodial-hardware hop even on peers that have it
62    /// configured.
63    fn public(self) -> Self;
64
65    /// Set [`LlmRoute::Private`] — force the custodial-hardware path. Hard-
66    /// fails with [`crate::llm::PrivateUnavailableError`] when the selected
67    /// executor has no hardware path.
68    ///
69    /// **Honest framing**: Private mode isolates the upstream provider's API
70    /// credentials on the peer's host and routes egress through a separate
71    /// device; it does NOT mask prompt contents from the upstream provider
72    /// or anonymize the user.
73    fn private(self) -> Self;
74
75    /// Set [`LlmRoute::Auto`] — pick the more-isolated path when available,
76    /// never hard-fail because of routing. The response's `execution_route`
77    /// is the truthful echo of what actually ran.
78    fn auto(self) -> Self;
79}
80
81impl WithRoute for ExecuteChatRequest {
82    fn public(mut self) -> Self {
83        self.route = Some(LlmRoute::Public);
84        self
85    }
86    fn private(mut self) -> Self {
87        self.route = Some(LlmRoute::Private);
88        self
89    }
90    fn auto(mut self) -> Self {
91        self.route = Some(LlmRoute::Auto);
92        self
93    }
94}
95
96// `StreamChatRequest = ExecuteChatRequest` and `ExecuteRequest =
97// ExecuteChatRequest` (type aliases), so the impl above already covers them
98// transparently — no separate impl block needed.
99
100impl WithRoute for ExecuteBestModelRequest {
101    fn public(mut self) -> Self {
102        self.route = Some(LlmRoute::Public);
103        self
104    }
105    fn private(mut self) -> Self {
106        self.route = Some(LlmRoute::Private);
107        self
108    }
109    fn auto(mut self) -> Self {
110        self.route = Some(LlmRoute::Auto);
111        self
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::llm::{ChatMessage, StreamChatRequest};
119
120    fn msg() -> Vec<ChatMessage> {
121        vec![ChatMessage {
122            role: "user".into(),
123            content: "hi".into(),
124        }]
125    }
126
127    // ── T1012 — `.public()` / `.private()` / `.auto()` shortcuts ───────────
128
129    #[test]
130    fn execute_chat_request_public_shortcut_sets_route() {
131        let req = ExecuteChatRequest {
132            messages: msg(),
133            ..Default::default()
134        }
135        .public();
136        assert_eq!(req.route, Some(LlmRoute::Public));
137    }
138
139    #[test]
140    fn execute_chat_request_private_shortcut_sets_route() {
141        let req = ExecuteChatRequest {
142            messages: msg(),
143            ..Default::default()
144        }
145        .private();
146        assert_eq!(req.route, Some(LlmRoute::Private));
147    }
148
149    #[test]
150    fn execute_chat_request_auto_shortcut_sets_route() {
151        let req = ExecuteChatRequest {
152            messages: msg(),
153            ..Default::default()
154        }
155        .auto();
156        assert_eq!(req.route, Some(LlmRoute::Auto));
157    }
158
159    #[test]
160    fn shortcut_serializes_route_as_wire_field_route_mode() {
161        // Wire-form contract: `route_mode` snake_case, not `route`.
162        let req = ExecuteChatRequest {
163            messages: msg(),
164            ..Default::default()
165        }
166        .private();
167        let json = serde_json::to_string(&req).expect("serialize");
168        assert!(
169            json.contains("\"route_mode\":\"private\""),
170            "wire field MUST be `route_mode`, got: {}",
171            json
172        );
173        assert!(
174            !json.contains("\"route\":"),
175            "`route` is the Rust field name; the wire form must use route_mode"
176        );
177    }
178
179    #[test]
180    fn shortcuts_are_chainable_and_last_wins() {
181        let req = ExecuteChatRequest {
182            messages: msg(),
183            ..Default::default()
184        }
185        .public()
186        .auto()
187        .private();
188        assert_eq!(req.route, Some(LlmRoute::Private));
189    }
190
191    #[test]
192    fn execute_best_model_request_shortcuts() {
193        let public_req = ExecuteBestModelRequest {
194            messages: msg(),
195            ..Default::default()
196        }
197        .public();
198        assert_eq!(public_req.route, Some(LlmRoute::Public));
199
200        let private_req = ExecuteBestModelRequest {
201            messages: msg(),
202            ..Default::default()
203        }
204        .private();
205        assert_eq!(private_req.route, Some(LlmRoute::Private));
206
207        let auto_req = ExecuteBestModelRequest {
208            messages: msg(),
209            ..Default::default()
210        }
211        .auto();
212        assert_eq!(auto_req.route, Some(LlmRoute::Auto));
213    }
214
215    #[test]
216    fn shortcut_works_on_stream_chat_request_alias() {
217        // StreamChatRequest is a type alias for ExecuteChatRequest, so the
218        // shortcut impl applies transparently. Lock that with a test so a
219        // future refactor that breaks the alias also breaks visibly.
220        let req: StreamChatRequest = ExecuteChatRequest {
221            messages: msg(),
222            ..Default::default()
223        }
224        .private();
225        assert_eq!(req.route, Some(LlmRoute::Private));
226    }
227}