Skip to main content

iicp_client/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2use serde::{Deserialize, Serialize};
3
4/// Client configuration (SDK-04: timeout_ms enforced at construction time).
5#[derive(Debug, Clone)]
6pub struct ClientConfig {
7    pub directory_url: String,
8    /// Maximum request timeout in milliseconds. Must be ≤ 120 000 (SDK-04).
9    pub timeout_ms: u64,
10    pub region: Option<String>,
11    pub node_token: Option<String>,
12    /// IICP-CX S.16: encrypt task payloads when the node advertises cx_public_key. Default: false.
13    pub use_confidentiality: bool,
14}
15
16impl Default for ClientConfig {
17    fn default() -> Self {
18        Self {
19            directory_url: "https://iicp.network/api".into(),
20            timeout_ms: 30_000,
21            region: None,
22            node_token: None,
23            use_confidentiality: false,
24        }
25    }
26}
27
28/// Options for `discover()` calls.
29#[derive(Debug, Default, Clone)]
30pub struct DiscoverOptions {
31    pub region: Option<String>,
32    pub model: Option<String>,
33    pub min_reputation: Option<f64>,
34    pub limit: Option<u32>,
35}
36
37/// X25519 public key advertised by a CX-Provider node (IICP-CX S.16 §3.1).
38#[derive(Debug, Clone, Deserialize)]
39pub struct CxPublicKey {
40    pub algorithm: String,
41    /// Base64url-encoded 32-byte X25519 public key.
42    pub key: String,
43    /// 8-hex-byte key identifier.
44    pub key_id: String,
45}
46
47/// A single IICP node returned by `/v1/discover`.
48#[derive(Debug, Clone, Deserialize)]
49pub struct Node {
50    pub node_id: String,
51    pub endpoint: String,
52    pub score: f64,
53    pub available: bool,
54    pub region: String,
55    pub models: Option<Vec<String>>,
56    pub cip_policy: Option<CipPolicy>,
57    /// ADR-044 composed health label (healthy/degraded/impaired/critical/offline).
58    /// `None` against a directory predating v1.10.0.
59    #[serde(default)]
60    pub health_label: Option<String>,
61    /// ADR-043 8-category network exposure classification. `None` if unset.
62    #[serde(default)]
63    pub exposure_mode: Option<String>,
64    /// IICP-CX S.16 §3.1 — X25519 public key for E2E payload confidentiality.
65    /// Present only when the node registered with cx_public_key (directory v1.10.7+).
66    #[serde(default)]
67    pub cx_public_key: Option<CxPublicKey>,
68}
69
70/// CIP policy block from the discover response.
71#[derive(Debug, Clone, Deserialize)]
72pub struct CipPolicy {
73    pub allow_remote_inference: bool,
74}
75
76/// Response from `/v1/discover`.
77#[derive(Debug, Clone, Deserialize)]
78pub struct NodeList {
79    pub nodes: Vec<Node>,
80    pub count: u32,
81}
82
83/// IICP task request body.
84#[derive(Debug, Clone, Serialize)]
85pub struct TaskRequest {
86    pub task_id: String,
87    pub intent: String,
88    pub payload: serde_json::Value,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub constraints: Option<TaskConstraints>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub auth: Option<TaskAuth>,
93}
94
95/// Constraints block for a task request.
96#[derive(Debug, Clone, Serialize)]
97pub struct TaskConstraints {
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub timeout_ms: Option<u64>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub max_tokens: Option<u32>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub model: Option<String>,
104}
105
106/// Auth block for a task request.
107#[derive(Debug, Clone, Serialize)]
108pub struct TaskAuth {
109    pub token: String,
110}
111
112/// Response from `POST /v1/task`.
113#[derive(Debug, Clone, Deserialize)]
114pub struct TaskResponse {
115    pub task_id: String,
116    pub status: String,
117    pub result: Option<serde_json::Value>,
118    pub metrics: Option<TaskMetrics>,
119}
120
121/// Task execution metrics.
122#[derive(Debug, Clone, Deserialize)]
123pub struct TaskMetrics {
124    pub latency_ms: Option<f64>,
125    pub tokens_used: Option<u32>,
126    pub node_id: Option<String>,
127}
128
129/// A single chat message (role + content).
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ChatMessage {
132    pub role: String,
133    pub content: String,
134}
135
136/// Options for `chat()` calls.
137#[derive(Debug, Default, Clone)]
138pub struct ChatOptions {
139    pub model: Option<String>,
140    pub max_tokens: Option<u32>,
141    pub timeout_ms: Option<u64>,
142    pub temperature: Option<f64>,
143}
144
145/// OpenAI-compatible chat completion response.
146#[derive(Debug, Clone, Deserialize, Default)]
147pub struct ChatResponse {
148    pub choices: Vec<ChatChoice>,
149    pub usage: Option<ChatUsage>,
150    /// Task ID from the IICP task response (correlation handle).
151    #[serde(default)]
152    pub task_id: String,
153    /// IICP node that served this request — from task metrics.
154    #[serde(default)]
155    pub node_id: Option<String>,
156}
157
158/// A single choice in a chat response.
159#[derive(Debug, Clone, Deserialize)]
160pub struct ChatChoice {
161    pub message: ChatMessage,
162    pub finish_reason: Option<String>,
163}
164
165/// Token usage from a chat completion.
166#[derive(Debug, Clone, Deserialize)]
167pub struct ChatUsage {
168    pub total_tokens: Option<u32>,
169    pub prompt_tokens: Option<u32>,
170    pub completion_tokens: Option<u32>,
171}
172
173#[cfg(test)]
174mod tests {
175    use super::Node;
176
177    // ADR-044 — discover Node parses the composed health_label + exposure_mode.
178    #[test]
179    fn node_parses_health_label_and_exposure_mode() {
180        let json = r#"{"node_id":"n1","endpoint":"https://x","score":0.9,"available":true,"region":"eu","health_label":"healthy","exposure_mode":"ipv4_public_direct"}"#;
181        let n: Node = serde_json::from_str(json).unwrap();
182        assert_eq!(n.health_label.as_deref(), Some("healthy"));
183        assert_eq!(n.exposure_mode.as_deref(), Some("ipv4_public_direct"));
184    }
185
186    // A directory predating v1.10.0 omits the fields; parsing must not break.
187    #[test]
188    fn node_health_fields_default_none_for_old_directory() {
189        let json =
190            r#"{"node_id":"n1","endpoint":"https://x","score":0.5,"available":true,"region":"eu"}"#;
191        let n: Node = serde_json::from_str(json).unwrap();
192        assert!(n.health_label.is_none());
193        assert!(n.exposure_mode.is_none());
194    }
195}