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// Default impls are now provided by #[derive(Default)] on the struct definitions.
24// The `..Default::default()` idiom still works the same way.
25
26/// Routing-intent shortcuts.
27///
28/// Implemented on every public-LLM request shape. Mirrors the TypeScript
29/// SDK's `.route('public' | 'private' | 'auto')` ergonomics with idiomatic
30/// Rust verbs (`.public()`, `.private()`, `.auto()`).
31pub trait WithRoute: Sized {
32    /// Set [`LlmRoute::Public`] — force direct upstream-provider execution,
33    /// skipping any custodial-hardware hop even on peers that have it
34    /// configured.
35    fn public(self) -> Self;
36
37    /// Set [`LlmRoute::Private`] — force the custodial-hardware path. Hard-
38    /// fails with [`crate::llm::PrivateUnavailableError`] when the selected
39    /// executor has no hardware path.
40    ///
41    /// **Honest framing**: Private mode isolates the upstream provider's API
42    /// credentials on the peer's host and routes egress through a separate
43    /// device; it does NOT mask prompt contents from the upstream provider
44    /// or anonymize the user.
45    fn private(self) -> Self;
46
47    /// Set [`LlmRoute::Auto`] — pick the more-isolated path when available,
48    /// never hard-fail because of routing. The response's `execution_route`
49    /// is the truthful echo of what actually ran.
50    fn auto(self) -> Self;
51}
52
53impl WithRoute for ExecuteChatRequest {
54    fn public(mut self) -> Self {
55        self.route = Some(LlmRoute::Public);
56        self
57    }
58    fn private(mut self) -> Self {
59        self.route = Some(LlmRoute::Private);
60        self
61    }
62    fn auto(mut self) -> Self {
63        self.route = Some(LlmRoute::Auto);
64        self
65    }
66}
67
68// `StreamChatRequest = ExecuteChatRequest` and `ExecuteRequest =
69// ExecuteChatRequest` (type aliases), so the impl above already covers them
70// transparently — no separate impl block needed.
71
72impl WithRoute for ExecuteBestModelRequest {
73    fn public(mut self) -> Self {
74        self.route = Some(LlmRoute::Public);
75        self
76    }
77    fn private(mut self) -> Self {
78        self.route = Some(LlmRoute::Private);
79        self
80    }
81    fn auto(mut self) -> Self {
82        self.route = Some(LlmRoute::Auto);
83        self
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::llm::{ChatMessage, StreamChatRequest};
91
92    fn msg() -> Vec<ChatMessage> {
93        vec![ChatMessage {
94            role: "user".into(),
95            content: "hi".into(),
96        }]
97    }
98
99    // ── T1012 — `.public()` / `.private()` / `.auto()` shortcuts ───────────
100
101    #[test]
102    fn execute_chat_request_public_shortcut_sets_route() {
103        let req = ExecuteChatRequest {
104            messages: msg(),
105            ..Default::default()
106        }
107        .public();
108        assert_eq!(req.route, Some(LlmRoute::Public));
109    }
110
111    #[test]
112    fn execute_chat_request_private_shortcut_sets_route() {
113        let req = ExecuteChatRequest {
114            messages: msg(),
115            ..Default::default()
116        }
117        .private();
118        assert_eq!(req.route, Some(LlmRoute::Private));
119    }
120
121    #[test]
122    fn execute_chat_request_auto_shortcut_sets_route() {
123        let req = ExecuteChatRequest {
124            messages: msg(),
125            ..Default::default()
126        }
127        .auto();
128        assert_eq!(req.route, Some(LlmRoute::Auto));
129    }
130
131    #[test]
132    fn shortcut_serializes_route_as_wire_field_route_mode() {
133        // Wire-form contract: `route_mode` snake_case, not `route`.
134        let req = ExecuteChatRequest {
135            messages: msg(),
136            ..Default::default()
137        }
138        .private();
139        let json = serde_json::to_string(&req).expect("serialize");
140        assert!(
141            json.contains("\"route_mode\":\"private\""),
142            "wire field MUST be `route_mode`, got: {}",
143            json
144        );
145        assert!(
146            !json.contains("\"route\":"),
147            "`route` is the Rust field name; the wire form must use route_mode"
148        );
149    }
150
151    #[test]
152    fn shortcuts_are_chainable_and_last_wins() {
153        let req = ExecuteChatRequest {
154            messages: msg(),
155            ..Default::default()
156        }
157        .public()
158        .auto()
159        .private();
160        assert_eq!(req.route, Some(LlmRoute::Private));
161    }
162
163    #[test]
164    fn execute_best_model_request_shortcuts() {
165        let public_req = ExecuteBestModelRequest {
166            messages: msg(),
167            ..Default::default()
168        }
169        .public();
170        assert_eq!(public_req.route, Some(LlmRoute::Public));
171
172        let private_req = ExecuteBestModelRequest {
173            messages: msg(),
174            ..Default::default()
175        }
176        .private();
177        assert_eq!(private_req.route, Some(LlmRoute::Private));
178
179        let auto_req = ExecuteBestModelRequest {
180            messages: msg(),
181            ..Default::default()
182        }
183        .auto();
184        assert_eq!(auto_req.route, Some(LlmRoute::Auto));
185    }
186
187    #[test]
188    fn shortcut_works_on_stream_chat_request_alias() {
189        // StreamChatRequest is a type alias for ExecuteChatRequest, so the
190        // shortcut impl applies transparently. Lock that with a test so a
191        // future refactor that breaks the alias also breaks visibly.
192        let req: StreamChatRequest = ExecuteChatRequest {
193            messages: msg(),
194            ..Default::default()
195        }
196        .private();
197        assert_eq!(req.route, Some(LlmRoute::Private));
198    }
199}