Skip to main content

edgecrab_types/
config.rs

1//! API modes, platform identifiers, and constants.
2
3use serde::{Deserialize, Serialize};
4
5/// Default model when none is specified.
6pub const DEFAULT_MODEL: &str = "anthropic/claude-sonnet-4-20250514";
7pub const OPENROUTER_BASE_URL: &str = "https://openrouter.ai/api/v1";
8
9/// API protocol variant — determines how requests/responses are shaped.
10///
11/// ```text
12///   ChatCompletions   ── OpenAI / OpenRouter standard
13///   AnthropicMessages ── Direct Anthropic API
14///   CodexResponses    ── OpenAI Codex Responses API
15/// ```
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum ApiMode {
18    ChatCompletions,
19    AnthropicMessages,
20    CodexResponses,
21}
22
23impl ApiMode {
24    /// Auto-detect API mode from base URL and model name.
25    pub fn detect(base_url: &str, model: &str) -> Self {
26        if base_url.contains("api.anthropic.com") {
27            ApiMode::AnthropicMessages
28        } else if base_url.contains("api.openai.com") && model.contains("codex") {
29            ApiMode::CodexResponses
30        } else {
31            ApiMode::ChatCompletions
32        }
33    }
34}
35
36/// Platform the agent is running on — affects prompt hints and tool availability.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
38#[serde(rename_all = "lowercase")]
39pub enum Platform {
40    #[default]
41    Cli,
42    Telegram,
43    Discord,
44    Slack,
45    Whatsapp,
46    Feishu,
47    Wecom,
48    Signal,
49    Email,
50    Matrix,
51    Mattermost,
52    DingTalk,
53    Sms,
54    Webhook,
55    Api,
56    HomeAssistant,
57    Acp,
58    BlueBubbles,
59    Weixin,
60    /// Scheduled cron job — no interactive user present.
61    Cron,
62}
63
64impl std::fmt::Display for Platform {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        let s = match self {
67            Platform::Cli => "cli",
68            Platform::Telegram => "telegram",
69            Platform::Discord => "discord",
70            Platform::Slack => "slack",
71            Platform::Whatsapp => "whatsapp",
72            Platform::Feishu => "feishu",
73            Platform::Wecom => "wecom",
74            Platform::Signal => "signal",
75            Platform::Email => "email",
76            Platform::Matrix => "matrix",
77            Platform::Mattermost => "mattermost",
78            Platform::DingTalk => "dingtalk",
79            Platform::Sms => "sms",
80            Platform::Webhook => "webhook",
81            Platform::Api => "api",
82            Platform::HomeAssistant => "homeassistant",
83            Platform::Acp => "acp",
84            Platform::BlueBubbles => "bluebubbles",
85            Platform::Weixin => "weixin",
86            Platform::Cron => "cron",
87        };
88        write!(f, "{s}")
89    }
90}
91
92/// Origin chat metadata for gateway-backed sessions.
93///
94/// WHY a named struct: `(String, String)` obscures which value is the platform
95/// name and which is the chat identifier. A shared value type makes call sites
96/// self-documenting across edgecrab-core, edgecrab-tools, and edgecrab-gateway.
97#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
98pub struct OriginChat {
99    pub platform: String,
100    pub chat_id: String,
101}
102
103impl OriginChat {
104    pub fn new(platform: impl Into<String>, chat_id: impl Into<String>) -> Self {
105        Self {
106            platform: platform.into(),
107            chat_id: chat_id.into(),
108        }
109    }
110
111    pub fn session_key(&self) -> String {
112        format!("{}:{}", self.platform, self.chat_id)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn api_mode_detect_anthropic() {
122        assert_eq!(
123            ApiMode::detect("https://api.anthropic.com/v1", "claude-4"),
124            ApiMode::AnthropicMessages
125        );
126    }
127
128    #[test]
129    fn api_mode_detect_codex() {
130        assert_eq!(
131            ApiMode::detect("https://api.openai.com/v1", "codex-mini"),
132            ApiMode::CodexResponses
133        );
134    }
135
136    #[test]
137    fn api_mode_detect_default() {
138        assert_eq!(
139            ApiMode::detect("https://openrouter.ai/api/v1", "anthropic/claude-4"),
140            ApiMode::ChatCompletions
141        );
142    }
143
144    #[test]
145    fn platform_display() {
146        assert_eq!(format!("{}", Platform::Cli), "cli");
147        assert_eq!(format!("{}", Platform::Telegram), "telegram");
148    }
149
150    #[test]
151    fn platform_serde_roundtrip() {
152        for p in [
153            Platform::Cli,
154            Platform::Telegram,
155            Platform::Discord,
156            Platform::Slack,
157            Platform::Feishu,
158            Platform::Wecom,
159        ] {
160            let json = serde_json::to_string(&p).expect("serialize");
161            let deser: Platform = serde_json::from_str(&json).expect("deserialize");
162            assert_eq!(p, deser);
163        }
164    }
165
166    #[test]
167    fn origin_chat_session_key() {
168        let origin = OriginChat::new("telegram", "chat-123");
169        assert_eq!(origin.session_key(), "telegram:chat-123");
170    }
171
172    #[test]
173    fn origin_chat_serde_roundtrip() {
174        let origin = OriginChat::new("discord", "chan-456");
175        let json = serde_json::to_string(&origin).expect("serialize");
176        let deser: OriginChat = serde_json::from_str(&json).expect("deserialize");
177        assert_eq!(origin, deser);
178    }
179}