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    /// #397 — transport protocols the node speaks (e.g. ["https","iicp-native"]).
69    /// Empty/absent against a directory predating the field.
70    #[serde(default)]
71    pub transport: Vec<String>,
72}
73
74/// CIP policy block from the discover response.
75#[derive(Debug, Clone, Deserialize)]
76pub struct CipPolicy {
77    pub allow_remote_inference: bool,
78}
79
80/// Response from `/v1/discover`.
81#[derive(Debug, Clone, Deserialize)]
82pub struct NodeList {
83    pub nodes: Vec<Node>,
84    pub count: u32,
85}
86
87/// IICP task request body.
88#[derive(Debug, Clone, Serialize)]
89pub struct TaskRequest {
90    pub task_id: String,
91    pub intent: String,
92    pub payload: serde_json::Value,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub constraints: Option<TaskConstraints>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub auth: Option<TaskAuth>,
97}
98
99/// Constraints block for a task request.
100#[derive(Debug, Clone, Serialize)]
101pub struct TaskConstraints {
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub timeout_ms: Option<u64>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub max_tokens: Option<u32>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub model: Option<String>,
108}
109
110/// Auth block for a task request.
111#[derive(Debug, Clone, Serialize)]
112pub struct TaskAuth {
113    pub token: String,
114}
115
116/// Response from `POST /v1/task`.
117#[derive(Debug, Clone, Deserialize)]
118pub struct TaskResponse {
119    pub task_id: String,
120    pub status: String,
121    pub result: Option<serde_json::Value>,
122    pub metrics: Option<TaskMetrics>,
123}
124
125/// Task execution metrics.
126#[derive(Debug, Clone, Deserialize)]
127pub struct TaskMetrics {
128    pub latency_ms: Option<f64>,
129    pub tokens_used: Option<u32>,
130    pub node_id: Option<String>,
131}
132
133/// A single chat message (role + content).
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ChatMessage {
136    pub role: String,
137    pub content: String,
138}
139
140/// Options for `chat()` calls.
141#[derive(Debug, Default, Clone)]
142pub struct ChatOptions {
143    pub model: Option<String>,
144    pub max_tokens: Option<u32>,
145    pub timeout_ms: Option<u64>,
146    pub temperature: Option<f64>,
147}
148
149/// OpenAI-compatible chat completion response.
150#[derive(Debug, Clone, Deserialize, Default)]
151pub struct ChatResponse {
152    pub choices: Vec<ChatChoice>,
153    pub usage: Option<ChatUsage>,
154    /// Task ID from the IICP task response (correlation handle).
155    #[serde(default)]
156    pub task_id: String,
157    /// IICP node that served this request — from task metrics.
158    #[serde(default)]
159    pub node_id: Option<String>,
160}
161
162/// A single choice in a chat response.
163#[derive(Debug, Clone, Deserialize)]
164pub struct ChatChoice {
165    pub message: ChatMessage,
166    pub finish_reason: Option<String>,
167}
168
169/// Token usage from a chat completion.
170#[derive(Debug, Clone, Deserialize)]
171pub struct ChatUsage {
172    pub total_tokens: Option<u32>,
173    pub prompt_tokens: Option<u32>,
174    pub completion_tokens: Option<u32>,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::Node;
180
181    // ADR-044 — discover Node parses the composed health_label + exposure_mode.
182    #[test]
183    fn node_parses_health_label_and_exposure_mode() {
184        let json = r#"{"node_id":"n1","endpoint":"https://x","score":0.9,"available":true,"region":"eu","health_label":"healthy","exposure_mode":"ipv4_public_direct","transport":["https","iicp-native"]}"#;
185        let n: Node = serde_json::from_str(json).unwrap();
186        assert_eq!(n.health_label.as_deref(), Some("healthy"));
187        assert_eq!(n.exposure_mode.as_deref(), Some("ipv4_public_direct"));
188        // #397 — transport parses from discover.
189        assert_eq!(n.transport, vec!["https", "iicp-native"]);
190    }
191
192    // A directory predating v1.10.0 omits the fields; parsing must not break.
193    #[test]
194    fn node_health_fields_default_none_for_old_directory() {
195        let json =
196            r#"{"node_id":"n1","endpoint":"https://x","score":0.5,"available":true,"region":"eu"}"#;
197        let n: Node = serde_json::from_str(json).unwrap();
198        assert!(n.health_label.is_none());
199        assert!(n.exposure_mode.is_none());
200    }
201}