Skip to main content

imagegen_bridge_core/
error.rs

1//! Stable error types shared by providers and transports.
2
3use std::collections::BTreeMap;
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9/// Stable machine-readable error classification.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11#[serde(rename_all = "snake_case")]
12pub enum ErrorCode {
13    /// A request failed intrinsic validation.
14    InvalidRequest,
15    /// The selected provider cannot honor the request.
16    UnsupportedCapability,
17    /// Configuration is absent or invalid.
18    Configuration,
19    /// Provider authentication is absent, expired, or rejected.
20    Authentication,
21    /// The caller is authenticated but not entitled to the operation.
22    PermissionDenied,
23    /// A safety system rejected the request.
24    SafetyRejected,
25    /// A provider or bridge rate limit was reached.
26    RateLimited,
27    /// Admission control rejected the request because capacity is exhausted.
28    Overloaded,
29    /// The operation exceeded its deadline.
30    Timeout,
31    /// The caller or runtime cancelled the operation.
32    Cancelled,
33    /// An upstream provider returned an invalid or unsuccessful response.
34    Upstream,
35    /// The upstream protocol no longer matches the supported schema.
36    Protocol,
37    /// An input could not be loaded or validated.
38    Input,
39    /// A generated artifact could not be verified or published.
40    Artifact,
41    /// A requested session does not exist or cannot be accessed.
42    Session,
43    /// An idempotency key conflicts with a different request.
44    IdempotencyConflict,
45    /// An unexpected internal failure occurred.
46    Internal,
47}
48
49/// Public, redaction-safe error returned by the bridge.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Error)]
51#[error("{code:?}: {message}")]
52#[serde(deny_unknown_fields)]
53pub struct BridgeError {
54    /// Stable machine-readable code.
55    pub code: ErrorCode,
56    /// Human-readable message safe to show to a client.
57    pub message: String,
58    /// Whether retrying later may succeed without changing the request.
59    pub retryable: bool,
60    /// Optional provider name, never a credential or account identifier.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub provider: Option<String>,
63    /// Safe provider correlation ID when one exists.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub upstream_request_id: Option<String>,
66    /// Structured, redaction-safe diagnostic values.
67    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
68    pub details: BTreeMap<String, serde_json::Value>,
69}
70
71impl BridgeError {
72    /// Creates a non-retryable error without provider details.
73    #[must_use]
74    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
75        Self {
76            code,
77            message: message.into(),
78            retryable: false,
79            provider: None,
80            upstream_request_id: None,
81            details: BTreeMap::new(),
82        }
83    }
84
85    /// Creates a safety rejection with stable, non-bypass recovery guidance.
86    #[must_use]
87    pub fn safety_rejected(message: impl Into<String>) -> Self {
88        Self::new(ErrorCode::SafetyRejected, message)
89            .with_detail("safety_category", "content_policy")
90            .with_detail("recovery", "revise_prompt_or_inputs")
91            .with_detail("retry_same_request", false)
92            .with_detail("safety_controls_relaxed", false)
93    }
94
95    /// Marks the error as potentially retryable.
96    #[must_use]
97    pub const fn retryable(mut self, value: bool) -> Self {
98        self.retryable = value;
99        self
100    }
101
102    /// Attaches a safe provider name.
103    #[must_use]
104    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
105        self.provider = Some(provider.into());
106        self
107    }
108
109    /// Attaches a redaction-safe structured detail.
110    #[must_use]
111    pub fn with_detail(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
112        if let Ok(value) = serde_json::to_value(value) {
113            self.details.insert(key.into(), value);
114        }
115        self
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn safety_rejections_include_actionable_non_bypass_guidance() {
125        let error = BridgeError::safety_rejected("request rejected");
126        assert_eq!(error.code, ErrorCode::SafetyRejected);
127        assert!(!error.retryable);
128        assert_eq!(error.details["recovery"], "revise_prompt_or_inputs");
129        assert_eq!(error.details["retry_same_request"], false);
130        assert_eq!(error.details["safety_controls_relaxed"], false);
131    }
132}