@mmmbuto/anthmorph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.rs ADDED
@@ -0,0 +1,120 @@
1
+ mod config;
2
+ mod error;
3
+ mod models;
4
+ mod proxy;
5
+ mod transform;
6
+
7
+ use axum::{routing::post, Extension, Router};
8
+ use clap::Parser;
9
+ use config::BackendProfile;
10
+ use proxy::{build_cors_layer, Config};
11
+ use reqwest::Client;
12
+ use std::sync::Arc;
13
+ use tower_http::trace::TraceLayer;
14
+ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
15
+
16
+ #[derive(Parser, Debug)]
17
+ #[command(name = "anthmorph")]
18
+ #[command(about = "Anthropic to OpenAI-compatible proxy")]
19
+ struct Cli {
20
+ #[arg(long, default_value = "3000")]
21
+ port: Option<u16>,
22
+ #[arg(long)]
23
+ backend_url: Option<String>,
24
+ #[arg(long)]
25
+ model: Option<String>,
26
+ #[arg(long)]
27
+ reasoning_model: Option<String>,
28
+ #[arg(long)]
29
+ api_key: Option<String>,
30
+ #[arg(long, value_enum)]
31
+ backend_profile: Option<BackendProfile>,
32
+ #[arg(long)]
33
+ ingress_api_key: Option<String>,
34
+ #[arg(long)]
35
+ allow_origin: Vec<String>,
36
+ }
37
+
38
+ #[tokio::main]
39
+ async fn main() -> anyhow::Result<()> {
40
+ let cli = Cli::parse();
41
+
42
+ let mut config = Config::from_env();
43
+
44
+ if let Some(port) = cli.port {
45
+ config.port = port;
46
+ }
47
+ if let Some(url) = cli.backend_url {
48
+ config.backend_url = url;
49
+ }
50
+ if let Some(m) = cli.model {
51
+ config.model = m;
52
+ }
53
+ if let Some(m) = cli.reasoning_model {
54
+ config.reasoning_model = Some(m);
55
+ }
56
+ if let Some(k) = cli.api_key {
57
+ config.api_key = Some(k);
58
+ }
59
+ if let Some(profile) = cli.backend_profile {
60
+ config.backend_profile = profile;
61
+ }
62
+ if let Some(k) = cli.ingress_api_key {
63
+ config.ingress_api_key = Some(k);
64
+ }
65
+ if !cli.allow_origin.is_empty() {
66
+ config.allow_origins = cli.allow_origin;
67
+ }
68
+
69
+ tracing_subscriber::registry()
70
+ .with(
71
+ tracing_subscriber::EnvFilter::try_from_default_env()
72
+ .unwrap_or_else(|_| "anthmorph=debug".into()),
73
+ )
74
+ .with(tracing_subscriber::fmt::layer())
75
+ .init();
76
+
77
+ tracing::info!("AnthMorph v{}", env!("CARGO_PKG_VERSION"));
78
+ tracing::info!("Backend URL: {}", config.backend_url);
79
+ tracing::info!("Backend Profile: {}", config.backend_profile.as_str());
80
+ tracing::info!("Model: {}", config.model);
81
+ if let Some(ref m) = config.reasoning_model {
82
+ tracing::info!("Reasoning Model: {}", m);
83
+ }
84
+ tracing::info!("Port: {}", config.port);
85
+
86
+ let client = Client::builder()
87
+ .timeout(std::time::Duration::from_secs(300))
88
+ .connect_timeout(std::time::Duration::from_secs(10))
89
+ .pool_max_idle_per_host(10)
90
+ .build()?;
91
+
92
+ let config = Arc::new(config);
93
+
94
+ let app = Router::new()
95
+ .route("/v1/messages", post(proxy::proxy_handler))
96
+ .route("/health", axum::routing::get(health_handler))
97
+ .layer(Extension(config.clone()))
98
+ .layer(Extension(client))
99
+ .layer(TraceLayer::new_for_http());
100
+
101
+ let app = if let Some(cors) = build_cors_layer(&config)? {
102
+ app.layer(cors)
103
+ } else {
104
+ app
105
+ };
106
+
107
+ let addr = format!("0.0.0.0:{}", config.port);
108
+ let listener = tokio::net::TcpListener::bind(&addr).await?;
109
+
110
+ tracing::info!("Listening on {}", addr);
111
+ tracing::info!("Proxy ready");
112
+
113
+ axum::serve(listener, app).await?;
114
+
115
+ Ok(())
116
+ }
117
+
118
+ async fn health_handler() -> &'static str {
119
+ "OK"
120
+ }
@@ -0,0 +1,274 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ // ============================================================================
4
+ // Anthropic Request
5
+ // ============================================================================
6
+
7
+ #[derive(Debug, Clone, Deserialize)]
8
+ pub struct AnthropicRequest {
9
+ pub model: String,
10
+ pub messages: Vec<Message>,
11
+ #[serde(default)]
12
+ pub system: Option<SystemPrompt>,
13
+ #[serde(default)]
14
+ pub stream: Option<bool>,
15
+ #[serde(default)]
16
+ pub max_tokens: usize,
17
+ #[serde(default)]
18
+ pub temperature: Option<f32>,
19
+ #[serde(default)]
20
+ pub top_p: Option<f32>,
21
+ #[serde(default)]
22
+ pub top_k: Option<i32>,
23
+ #[serde(default)]
24
+ pub tools: Option<Vec<Tool>>,
25
+ #[serde(default)]
26
+ pub stop_sequences: Option<Vec<String>>,
27
+ #[serde(flatten)]
28
+ pub extra: serde_json::Map<String, serde_json::Value>,
29
+ }
30
+
31
+ #[derive(Debug, Clone)]
32
+ pub enum SystemPrompt {
33
+ Single(String),
34
+ Multiple(Vec<SystemMessage>),
35
+ }
36
+
37
+ impl<'de> Deserialize<'de> for SystemPrompt {
38
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39
+ where
40
+ D: serde::Deserializer<'de>,
41
+ {
42
+ let value = serde_json::Value::deserialize(deserializer)?;
43
+ match value {
44
+ serde_json::Value::String(s) => Ok(SystemPrompt::Single(s)),
45
+ serde_json::Value::Array(arr) => {
46
+ let messages: Vec<SystemMessage> =
47
+ serde_json::from_value(serde_json::Value::Array(arr))
48
+ .map_err(|e| serde::de::Error::custom(e))?;
49
+ Ok(SystemPrompt::Multiple(messages))
50
+ }
51
+ serde_json::Value::Object(obj) => {
52
+ // Check if it's the "Single" or "Multiple" tagged format
53
+ if let Some(text) = obj.get("Single").and_then(|v| v.as_str()) {
54
+ Ok(SystemPrompt::Single(text.to_string()))
55
+ } else if let Some(arr) = obj.get("Multiple").and_then(|v| v.as_array()) {
56
+ let messages: Vec<SystemMessage> =
57
+ serde_json::from_value(serde_json::Value::Array(arr.clone()))
58
+ .map_err(|e| serde::de::Error::custom(e))?;
59
+ Ok(SystemPrompt::Multiple(messages))
60
+ } else {
61
+ Err(serde::de::Error::custom("expected Single or Multiple"))
62
+ }
63
+ }
64
+ _ => Err(serde::de::Error::custom("expected string or array")),
65
+ }
66
+ }
67
+ }
68
+
69
+ #[derive(Debug, Clone, Serialize, Deserialize)]
70
+ pub struct SystemMessage {
71
+ pub text: String,
72
+ }
73
+
74
+ #[derive(Debug, Clone, Deserialize)]
75
+ pub struct Message {
76
+ pub role: String,
77
+ pub content: MessageContent,
78
+ }
79
+
80
+ #[derive(Debug, Clone)]
81
+ pub enum MessageContent {
82
+ Text(String),
83
+ Blocks(Vec<ContentBlock>),
84
+ }
85
+
86
+ impl<'de> Deserialize<'de> for MessageContent {
87
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88
+ where
89
+ D: serde::Deserializer<'de>,
90
+ {
91
+ let value = serde_json::Value::deserialize(deserializer)?;
92
+ match value {
93
+ serde_json::Value::String(s) => Ok(MessageContent::Text(s)),
94
+ serde_json::Value::Array(arr) => {
95
+ let blocks: Vec<ContentBlock> =
96
+ match serde_json::from_value(serde_json::Value::Array(arr.clone())) {
97
+ Ok(b) => b,
98
+ Err(_) => {
99
+ // If array doesn't match ContentBlock variants, treat as text
100
+ let text = serde_json::to_string(&arr).unwrap_or_default();
101
+ return Ok(MessageContent::Text(text));
102
+ }
103
+ };
104
+ Ok(MessageContent::Blocks(blocks))
105
+ }
106
+ _ => Err(serde::de::Error::custom("expected string or array")),
107
+ }
108
+ }
109
+ }
110
+
111
+ #[derive(Debug, Clone, Deserialize)]
112
+ #[serde(tag = "type")]
113
+ pub enum ContentBlock {
114
+ #[serde(rename = "text")]
115
+ Text { text: String },
116
+ #[serde(rename = "image")]
117
+ Image { source: ImageSource },
118
+ #[serde(rename = "tool_use")]
119
+ ToolUse {
120
+ id: String,
121
+ name: String,
122
+ input: serde_json::Value,
123
+ },
124
+ #[serde(rename = "tool_result")]
125
+ ToolResult {
126
+ tool_use_id: String,
127
+ content: ToolResultContent,
128
+ #[serde(default)]
129
+ is_error: Option<bool>,
130
+ },
131
+ #[serde(rename = "thinking")]
132
+ Thinking { thinking: String },
133
+ #[serde(other)]
134
+ Other,
135
+ }
136
+
137
+ #[derive(Debug, Clone, Deserialize)]
138
+ pub struct ImageSource {
139
+ pub media_type: String,
140
+ pub data: String,
141
+ }
142
+
143
+ #[derive(Debug, Clone, Deserialize)]
144
+ pub struct Tool {
145
+ pub name: String,
146
+ pub description: Option<String>,
147
+ #[serde(rename = "input_schema")]
148
+ pub input_schema: serde_json::Value,
149
+ #[serde(rename = "type")]
150
+ pub tool_type: Option<String>,
151
+ }
152
+
153
+ #[derive(Debug, Clone, Deserialize)]
154
+ #[serde(untagged)]
155
+ pub enum ToolResultContent {
156
+ Text(String),
157
+ Blocks(Vec<serde_json::Value>),
158
+ }
159
+
160
+ // ============================================================================
161
+ // Anthropic Response
162
+ // ============================================================================
163
+
164
+ #[derive(Debug, Clone, Serialize)]
165
+ pub struct AnthropicResponse {
166
+ pub id: String,
167
+ #[serde(rename = "type")]
168
+ pub response_type: String,
169
+ pub role: String,
170
+ pub content: Vec<ResponseContent>,
171
+ pub model: String,
172
+ #[serde(rename = "stop_reason")]
173
+ pub stop_reason: Option<String>,
174
+ #[serde(rename = "stop_sequence")]
175
+ pub stop_sequence: Option<()>,
176
+ pub usage: Usage,
177
+ }
178
+
179
+ #[derive(Debug, Clone, Serialize)]
180
+ #[serde(tag = "type")]
181
+ pub enum ResponseContent {
182
+ #[serde(rename = "text")]
183
+ Text { text: String },
184
+ #[serde(rename = "thinking")]
185
+ Thinking { thinking: String },
186
+ #[serde(rename = "tool_use")]
187
+ ToolUse {
188
+ id: String,
189
+ name: String,
190
+ input: serde_json::Value,
191
+ },
192
+ }
193
+
194
+ #[derive(Debug, Clone, Serialize, Deserialize)]
195
+ pub struct Usage {
196
+ #[serde(rename = "input_tokens")]
197
+ pub input_tokens: usize,
198
+ #[serde(rename = "output_tokens")]
199
+ pub output_tokens: usize,
200
+ }
201
+
202
+ // ============================================================================
203
+ // Anthropic SSE Events
204
+ // ============================================================================
205
+
206
+ #[derive(Debug, Clone, Serialize)]
207
+ #[serde(tag = "type")]
208
+ pub enum StreamEvent {
209
+ #[serde(rename = "message_start")]
210
+ MessageStart { message: MessageStartData },
211
+ #[serde(rename = "content_block_start")]
212
+ ContentBlockStart {
213
+ index: usize,
214
+ content_block: ContentBlockStartData,
215
+ },
216
+ #[serde(rename = "content_block_delta")]
217
+ ContentBlockDelta {
218
+ index: usize,
219
+ delta: ContentBlockDeltaData,
220
+ },
221
+ #[serde(rename = "content_block_stop")]
222
+ ContentBlockStop { index: usize },
223
+ #[serde(rename = "message_delta")]
224
+ MessageDelta {
225
+ delta: MessageDeltaData,
226
+ #[serde(skip_serializing_if = "Option::is_none")]
227
+ usage: Option<MessageDeltaUsage>,
228
+ },
229
+ #[serde(rename = "message_stop")]
230
+ MessageStop,
231
+ }
232
+
233
+ #[derive(Debug, Clone, Serialize)]
234
+ pub struct MessageStartData {
235
+ pub id: String,
236
+ #[serde(rename = "type")]
237
+ pub message_type: String,
238
+ pub role: String,
239
+ pub model: String,
240
+ pub usage: Usage,
241
+ }
242
+
243
+ #[derive(Debug, Clone, Serialize)]
244
+ #[serde(tag = "type", content = "content_block")]
245
+ pub enum ContentBlockStartData {
246
+ #[serde(rename = "text")]
247
+ Text { text: String },
248
+ #[serde(rename = "thinking")]
249
+ Thinking { thinking: String },
250
+ #[serde(rename = "tool_use")]
251
+ ToolUse { id: String, name: String },
252
+ }
253
+
254
+ #[derive(Debug, Clone, Serialize)]
255
+ #[serde(tag = "type", rename_all = "snake_case")]
256
+ pub enum ContentBlockDeltaData {
257
+ TextDelta { text: String },
258
+ ThinkingDelta { thinking: String },
259
+ InputJsonDelta { partial_json: String },
260
+ }
261
+
262
+ #[derive(Debug, Clone, Serialize)]
263
+ pub struct MessageDeltaData {
264
+ #[serde(rename = "stop_reason")]
265
+ pub stop_reason: Option<String>,
266
+ #[serde(rename = "stop_sequence")]
267
+ pub stop_sequence: (),
268
+ }
269
+
270
+ #[derive(Debug, Clone, Serialize)]
271
+ pub struct MessageDeltaUsage {
272
+ #[serde(rename = "output_tokens")]
273
+ pub output_tokens: usize,
274
+ }
@@ -0,0 +1,2 @@
1
+ pub mod anthropic;
2
+ pub mod openai;
@@ -0,0 +1,230 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ // ============================================================================
4
+ // OpenAI Request
5
+ // ============================================================================
6
+
7
+ #[derive(Debug, Clone, Serialize)]
8
+ pub struct OpenAIRequest {
9
+ pub model: String,
10
+ pub messages: Vec<Message>,
11
+ #[serde(skip_serializing_if = "Option::is_none")]
12
+ pub max_tokens: Option<usize>,
13
+ #[serde(skip_serializing_if = "Option::is_none")]
14
+ pub temperature: Option<f32>,
15
+ #[serde(skip_serializing_if = "Option::is_none")]
16
+ pub top_p: Option<f32>,
17
+ #[serde(skip_serializing_if = "Option::is_none")]
18
+ pub top_k: Option<i32>,
19
+ #[serde(skip_serializing_if = "Option::is_none")]
20
+ pub stop: Option<Vec<String>>,
21
+ #[serde(skip_serializing_if = "Option::is_none")]
22
+ pub stream: Option<bool>,
23
+ #[serde(skip_serializing_if = "Option::is_none")]
24
+ pub tools: Option<Vec<Tool>>,
25
+ #[serde(skip_serializing_if = "Option::is_none")]
26
+ pub tool_choice: Option<ToolChoice>,
27
+ }
28
+
29
+ #[derive(Debug, Clone, Serialize)]
30
+ pub struct Message {
31
+ pub role: String,
32
+ #[serde(skip_serializing_if = "Option::is_none")]
33
+ pub content: Option<MessageContent>,
34
+ #[serde(skip_serializing_if = "Option::is_none")]
35
+ pub name: Option<String>,
36
+ #[serde(skip_serializing_if = "Option::is_none")]
37
+ pub tool_calls: Option<Vec<ToolCall>>,
38
+ #[serde(skip_serializing_if = "Option::is_none")]
39
+ pub tool_call_id: Option<String>,
40
+ }
41
+
42
+ #[derive(Debug, Clone)]
43
+ pub enum MessageContent {
44
+ Text(String),
45
+ Parts(Vec<ContentPart>),
46
+ }
47
+
48
+ impl Serialize for MessageContent {
49
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50
+ where
51
+ S: serde::Serializer,
52
+ {
53
+ match self {
54
+ MessageContent::Text(s) => s.serialize(serializer),
55
+ MessageContent::Parts(parts) => parts.serialize(serializer),
56
+ }
57
+ }
58
+ }
59
+
60
+ #[derive(Debug, Clone)]
61
+ pub enum ContentPart {
62
+ Text { data: String },
63
+ ImageUrl { image_url: ImageUrl },
64
+ }
65
+
66
+ impl Serialize for ContentPart {
67
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
68
+ where
69
+ S: serde::Serializer,
70
+ {
71
+ #[derive(Serialize)]
72
+ struct TextPart {
73
+ #[serde(rename = "type")]
74
+ part_type: &'static str,
75
+ text: String,
76
+ }
77
+ #[derive(Serialize)]
78
+ struct ImageUrlPart {
79
+ #[serde(rename = "type")]
80
+ part_type: &'static str,
81
+ image_url: ImageUrl,
82
+ }
83
+
84
+ match self {
85
+ ContentPart::Text { data } => TextPart {
86
+ part_type: "text",
87
+ text: data.clone(),
88
+ }
89
+ .serialize(serializer),
90
+ ContentPart::ImageUrl { image_url } => ImageUrlPart {
91
+ part_type: "image_url",
92
+ image_url: image_url.clone(),
93
+ }
94
+ .serialize(serializer),
95
+ }
96
+ }
97
+ }
98
+
99
+ #[derive(Debug, Clone, Serialize)]
100
+ pub struct ImageUrl {
101
+ pub url: String,
102
+ }
103
+
104
+ #[derive(Debug, Clone, Serialize, Deserialize)]
105
+ pub struct Tool {
106
+ #[serde(rename = "type")]
107
+ pub tool_type: String,
108
+ pub function: Function,
109
+ }
110
+
111
+ #[derive(Debug, Clone, Serialize, Deserialize)]
112
+ pub struct Function {
113
+ pub name: String,
114
+ pub description: Option<String>,
115
+ pub parameters: serde_json::Value,
116
+ }
117
+
118
+ #[derive(Debug, Clone, Serialize, Deserialize)]
119
+ pub struct ToolCall {
120
+ pub id: String,
121
+ #[serde(rename = "type")]
122
+ pub call_type: String,
123
+ pub function: FunctionCall,
124
+ }
125
+
126
+ #[derive(Debug, Clone, Serialize, Deserialize)]
127
+ pub struct FunctionCall {
128
+ pub name: String,
129
+ pub arguments: String,
130
+ }
131
+
132
+ #[derive(Debug, Clone, Serialize, Deserialize)]
133
+ #[serde(untagged)]
134
+ pub enum ToolChoice {
135
+ String(String),
136
+ Object {
137
+ #[serde(rename = "type")]
138
+ tool_type: String,
139
+ function: ToolChoiceFunction,
140
+ },
141
+ }
142
+
143
+ #[derive(Debug, Clone, Serialize, Deserialize)]
144
+ pub struct ToolChoiceFunction {
145
+ pub name: String,
146
+ }
147
+
148
+ // ============================================================================
149
+ // OpenAI Response
150
+ // ============================================================================
151
+
152
+ #[derive(Debug, Clone, Deserialize)]
153
+ pub struct OpenAIResponse {
154
+ pub id: Option<String>,
155
+ pub model: Option<String>,
156
+ pub choices: Vec<Choice>,
157
+ pub usage: Usage,
158
+ }
159
+
160
+ #[derive(Debug, Clone, Deserialize)]
161
+ pub struct Choice {
162
+ pub message: ChoiceMessage,
163
+ #[serde(rename = "finish_reason")]
164
+ pub finish_reason: Option<String>,
165
+ }
166
+
167
+ #[derive(Debug, Clone, Deserialize)]
168
+ pub struct ChoiceMessage {
169
+ pub content: Option<String>,
170
+ #[serde(rename = "tool_calls")]
171
+ pub tool_calls: Option<Vec<ToolCall>>,
172
+ #[serde(rename = "reasoning_content")]
173
+ pub reasoning_content: Option<String>,
174
+ }
175
+
176
+ #[derive(Debug, Clone, Deserialize)]
177
+ pub struct Usage {
178
+ #[serde(rename = "prompt_tokens")]
179
+ pub prompt_tokens: usize,
180
+ #[serde(rename = "completion_tokens")]
181
+ pub completion_tokens: usize,
182
+ }
183
+
184
+ // ============================================================================
185
+ // OpenAI Streaming Chunk
186
+ // ============================================================================
187
+
188
+ #[derive(Debug, Clone, Deserialize)]
189
+ pub struct StreamChunk {
190
+ pub id: Option<String>,
191
+ pub model: Option<String>,
192
+ pub choices: Vec<StreamChoice>,
193
+ pub usage: Option<StreamUsage>,
194
+ }
195
+
196
+ #[derive(Debug, Clone, Deserialize)]
197
+ pub struct StreamChoice {
198
+ pub delta: StreamDelta,
199
+ #[serde(rename = "finish_reason")]
200
+ pub finish_reason: Option<String>,
201
+ }
202
+
203
+ #[derive(Debug, Clone, Deserialize)]
204
+ pub struct StreamDelta {
205
+ #[serde(default)]
206
+ pub content: Option<String>,
207
+ #[serde(default)]
208
+ pub reasoning: Option<String>,
209
+ #[serde(rename = "tool_calls")]
210
+ pub tool_calls: Option<Vec<StreamToolCall>>,
211
+ }
212
+
213
+ #[derive(Debug, Clone, Deserialize)]
214
+ pub struct StreamToolCall {
215
+ pub index: Option<usize>,
216
+ pub id: Option<String>,
217
+ pub function: Option<StreamFunctionCall>,
218
+ }
219
+
220
+ #[derive(Debug, Clone, Deserialize)]
221
+ pub struct StreamFunctionCall {
222
+ pub name: Option<String>,
223
+ pub arguments: Option<String>,
224
+ }
225
+
226
+ #[derive(Debug, Clone, Deserialize)]
227
+ pub struct StreamUsage {
228
+ #[serde(rename = "completion_tokens")]
229
+ pub completion_tokens: Option<usize>,
230
+ }