node_app_sdk_rust/llm/
errors.rs1use serde::{Deserialize, Serialize};
9
10use super::types::LlmRoute;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum PrivateUnavailableReason {
16 NoEsp32Endpoint,
17 FirmwareUpgradeRequired,
18 PeerOffline,
19 PlatformNotEnabled,
20}
21
22#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
30#[error("private routing unavailable: {reason:?} (fallback_available={fallback_available})")]
31pub struct PrivateUnavailableError {
32 pub reason: PrivateUnavailableReason,
33 pub fallback_available: bool,
34}
35
36#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
40#[error("invalid route_mode + execution_preference combination: route_mode={route_mode:?}, execution_preference={execution_preference:?} โ {hint}")]
41pub struct InvalidRouteModeCombinationError {
42 pub route_mode: LlmRoute,
43 pub execution_preference: Option<String>,
44 pub hint: String,
45}
46
47#[derive(Debug, Clone, Deserialize)]
49pub struct PrivateUnavailableBody {
50 pub code: String,
51 pub requested_route: String,
52 pub reason: PrivateUnavailableReason,
53 pub fallback_available: bool,
54}
55
56impl From<PrivateUnavailableBody> for PrivateUnavailableError {
57 fn from(body: PrivateUnavailableBody) -> Self {
58 Self {
59 reason: body.reason,
60 fallback_available: body.fallback_available,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Deserialize)]
68pub struct InvalidRouteModeCombinationBody {
69 pub code: String,
70 pub route_mode: LlmRoute,
71 #[serde(default)]
72 pub execution_preference: Option<String>,
73 pub hint: String,
74}
75
76impl From<InvalidRouteModeCombinationBody> for InvalidRouteModeCombinationError {
77 fn from(body: InvalidRouteModeCombinationBody) -> Self {
78 Self {
79 route_mode: body.route_mode,
80 execution_preference: body.execution_preference,
81 hint: body.hint,
82 }
83 }
84}
85
86pub fn map_api_error_body(bytes: &[u8]) -> Option<Box<dyn std::error::Error + Send + Sync>> {
90 let v: serde_json::Value = serde_json::from_slice(bytes).ok()?;
91 let code = v.get("code")?.as_str()?;
92 match code {
93 "api_store.private_unavailable" => {
94 let body: PrivateUnavailableBody = serde_json::from_value(v).ok()?;
95 Some(Box::new(PrivateUnavailableError::from(body)))
96 }
97 "api_store.invalid_route_mode_combination" => {
98 let body: InvalidRouteModeCombinationBody = serde_json::from_value(v).ok()?;
99 Some(Box::new(InvalidRouteModeCombinationError::from(body)))
100 }
101 _ => None,
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn decodes_private_unavailable() {
111 let body = br#"{
112 "code": "api_store.private_unavailable",
113 "requested_route": "private",
114 "reason": "no_esp32_endpoint",
115 "fallback_available": true
116 }"#;
117 let err = map_api_error_body(body).expect("typed err");
118 let downcast = err
119 .downcast_ref::<PrivateUnavailableError>()
120 .expect("PrivateUnavailableError");
121 assert_eq!(downcast.reason, PrivateUnavailableReason::NoEsp32Endpoint);
122 assert!(downcast.fallback_available);
123 }
124
125 #[test]
126 fn unknown_code_returns_none() {
127 assert!(map_api_error_body(br#"{ "code": "api_store.other" }"#).is_none());
128 }
129
130 #[test]
131 fn invalid_route_mode_combination_decodes() {
132 let body = br#"{
133 "code": "api_store.invalid_route_mode_combination",
134 "route_mode": "private",
135 "execution_preference": "network_only",
136 "hint": "fix it"
137 }"#;
138 let err = map_api_error_body(body).expect("typed err");
139 let downcast = err
140 .downcast_ref::<InvalidRouteModeCombinationError>()
141 .expect("InvalidRouteModeCombinationError");
142 assert_eq!(downcast.route_mode, LlmRoute::Private);
143 assert_eq!(downcast.execution_preference.as_deref(), Some("network_only"));
144 }
145}