1use serde::{Deserialize, Serialize};
4
5pub const DEFAULT_MODEL: &str = "anthropic/claude-sonnet-4-20250514";
7pub const OPENROUTER_BASE_URL: &str = "https://openrouter.ai/api/v1";
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum ApiMode {
18 ChatCompletions,
19 AnthropicMessages,
20 CodexResponses,
21}
22
23impl ApiMode {
24 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#[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 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#[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}