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}