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