#![allow(dead_code)]
use axum::{
extract::State,
http::{header, StatusCode},
response::IntoResponse,
routing::post,
Router,
};
use serde_json::json;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
};
#[derive(Clone)]
pub struct MockState {
pub call_count: Arc<AtomicUsize>,
pub distill_call_count: Arc<AtomicUsize>,
pub received_distill_body: Arc<Mutex<Option<serde_json::Value>>>,
}
fn extract_first_path(body: &serde_json::Value) -> Option<String> {
let messages = body.get("messages").and_then(|m| m.as_array())?;
for msg in messages {
let content: String = {
if let Some(s) = msg.get("content").and_then(|c| c.as_str()) {
s.to_string()
} else if let Some(blocks) = msg.get("content").and_then(|c| c.as_array()) {
blocks
.iter()
.filter_map(|b| b.get("text").and_then(|t| t.as_str()))
.collect::<Vec<_>>()
.join("\n")
} else {
continue;
}
};
let mut in_files = false;
for line in content.lines() {
if line.trim() == "Files:" {
in_files = true;
continue;
}
if in_files {
let trimmed = line.trim();
if trimmed.starts_with('/') {
return Some(trimmed.to_string());
} else if !trimmed.is_empty() {
in_files = false;
}
}
}
}
None
}
fn is_distill_call(body: &serde_json::Value) -> bool {
if body.get("tools").is_some() {
return false;
}
const DISTILL_SIG: &str = "You are summarizing a chunk of a source code file";
let messages = match body.get("messages").and_then(|m| m.as_array()) {
Some(m) => m,
None => return false,
};
for msg in messages {
if let Some(content) = msg.get("content").and_then(|c| c.as_str()) {
if content.contains(DISTILL_SIG) {
return true;
}
}
}
false
}
async fn openai_distill_handler(
State(state): State<MockState>,
body: axum::body::Bytes,
) -> impl IntoResponse {
let req_value = match serde_json::from_slice::<serde_json::Value>(&body) {
Ok(v) => v,
Err(e) => {
eprintln!("[compile_loop_distill_mock/openai] bad request body: {e}");
let err_body = json!({ "error": format!("bad request: {e}") }).to_string();
return (
StatusCode::BAD_REQUEST,
[(header::CONTENT_TYPE, "application/json")],
err_body,
);
}
};
let prev = state.call_count.fetch_add(1, Ordering::SeqCst);
if is_distill_call(&req_value) {
state.distill_call_count.fetch_add(1, Ordering::SeqCst);
{
let mut guard = state.received_distill_body.lock().unwrap();
*guard = Some(req_value.clone());
}
let response_json = json!({
"id": format!("chatcmpl-distill-{}", prev + 1),
"object": "chat.completion",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "This chunk defines utility functions and constants used throughout the module."
},
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 50, "completion_tokens": 20, "total_tokens": 70 }
});
return (
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
response_json.to_string(),
);
}
let has_tool_results = req_value
.get("messages")
.and_then(|m| m.as_array())
.map(|msgs| {
msgs.iter().any(|msg| {
msg.get("role").and_then(|r| r.as_str()) == Some("tool")
})
})
.unwrap_or(false);
let response_json = if !has_tool_results {
let path = extract_first_path(&req_value).unwrap_or_else(|| "/unknown/path".to_string());
json!({
"id": format!("chatcmpl-main-turn0-{}", prev + 1),
"object": "chat.completion",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_read_file_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": format!("{{\"path\":\"{}\"}}", path)
}
}]
},
"finish_reason": "tool_calls"
}],
"usage": { "prompt_tokens": 30, "completion_tokens": 15, "total_tokens": 45 }
})
} else {
let path = extract_first_path(&req_value).unwrap_or_else(|| "/unknown/path".to_string());
let sr_text = format!(
"<<< path={path} >>>\n<<<<<<< SEARCH\n-- marker: REPLACE_ME\n=======\n-- marker: DONE\n>>>>>>> REPLACE"
);
json!({
"id": format!("chatcmpl-main-turn1-{}", prev + 1),
"object": "chat.completion",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": sr_text
},
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 60, "completion_tokens": 30, "total_tokens": 90 }
})
};
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
response_json.to_string(),
)
}
async fn anthropic_distill_handler(
State(state): State<MockState>,
body: axum::body::Bytes,
) -> impl IntoResponse {
let req_value = match serde_json::from_slice::<serde_json::Value>(&body) {
Ok(v) => v,
Err(e) => {
eprintln!("[compile_loop_distill_mock/anthropic] bad request body: {e}");
let err_body = json!({ "error": format!("bad request: {e}") }).to_string();
return (
StatusCode::BAD_REQUEST,
[(header::CONTENT_TYPE, "application/json")],
err_body,
);
}
};
let prev = state.call_count.fetch_add(1, Ordering::SeqCst);
if is_distill_call(&req_value) {
state.distill_call_count.fetch_add(1, Ordering::SeqCst);
{
let mut guard = state.received_distill_body.lock().unwrap();
*guard = Some(req_value.clone());
}
let response_json = json!({
"id": format!("msg_distill_{}", prev + 1),
"type": "message",
"role": "assistant",
"content": [{
"type": "text",
"text": "This chunk defines utility functions and constants used throughout the module."
}],
"model": "claude-haiku-mock",
"stop_reason": "end_turn",
"usage": { "input_tokens": 50, "output_tokens": 20 }
});
return (
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
response_json.to_string(),
);
}
let has_tool_results = req_value
.get("messages")
.and_then(|m| m.as_array())
.map(|msgs| {
msgs.iter().any(|msg| {
if msg.get("role").and_then(|r| r.as_str()) != Some("user") {
return false;
}
msg.get("content")
.and_then(|c| c.as_array())
.map(|blocks| {
blocks
.iter()
.any(|b| b.get("type").and_then(|t| t.as_str()) == Some("tool_result"))
})
.unwrap_or(false)
})
})
.unwrap_or(false);
let response_json = if !has_tool_results {
let path = extract_first_path(&req_value).unwrap_or_else(|| "/unknown/path".to_string());
json!({
"id": format!("msg_main_turn0_{}", prev + 1),
"type": "message",
"role": "assistant",
"content": [{
"type": "tool_use",
"id": "toolu_read_file_1",
"name": "read_file",
"input": { "path": path }
}],
"model": "claude-haiku-mock",
"stop_reason": "tool_use",
"usage": { "input_tokens": 30, "output_tokens": 15 }
})
} else {
let path = extract_first_path(&req_value).unwrap_or_else(|| "/unknown/path".to_string());
let sr_text = format!(
"<<< path={path} >>>\n<<<<<<< SEARCH\n-- marker: REPLACE_ME\n=======\n-- marker: DONE\n>>>>>>> REPLACE"
);
json!({
"id": format!("msg_main_turn1_{}", prev + 1),
"type": "message",
"role": "assistant",
"content": [{ "type": "text", "text": sr_text }],
"model": "claude-haiku-mock",
"stop_reason": "end_turn",
"usage": { "input_tokens": 60, "output_tokens": 30 }
})
};
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
response_json.to_string(),
)
}
async fn anthropic_range_handler(
State(state): State<MockState>,
body: axum::body::Bytes,
) -> impl IntoResponse {
let req_value = match serde_json::from_slice::<serde_json::Value>(&body) {
Ok(v) => v,
Err(e) => {
eprintln!("[compile_loop_distill_mock/range] bad request body: {e}");
let err_body = json!({ "error": format!("bad request: {e}") }).to_string();
return (
StatusCode::BAD_REQUEST,
[(header::CONTENT_TYPE, "application/json")],
err_body,
);
}
};
let prev = state.call_count.fetch_add(1, Ordering::SeqCst);
let has_tool_results = req_value
.get("messages")
.and_then(|m| m.as_array())
.map(|msgs| {
msgs.iter().any(|msg| {
if msg.get("role").and_then(|r| r.as_str()) != Some("user") {
return false;
}
msg.get("content")
.and_then(|c| c.as_array())
.map(|blocks| {
blocks
.iter()
.any(|b| b.get("type").and_then(|t| t.as_str()) == Some("tool_result"))
})
.unwrap_or(false)
})
})
.unwrap_or(false);
let response_json = if !has_tool_results {
let path = extract_first_path(&req_value).unwrap_or_else(|| "/unknown/path".to_string());
json!({
"id": format!("msg_range_turn0_{}", prev + 1),
"type": "message",
"role": "assistant",
"content": [{
"type": "tool_use",
"id": "toolu_range_1",
"name": "read_file_range",
"input": { "path": path, "line_start": 10, "line_end": 20 }
}],
"model": "claude-haiku-mock",
"stop_reason": "tool_use",
"usage": { "input_tokens": 20, "output_tokens": 10 }
})
} else {
let path = extract_first_path(&req_value).unwrap_or_else(|| "/unknown/path".to_string());
let sr_text = format!(
"<<< path={path} >>>\n<<<<<<< SEARCH\n-- marker: REPLACE_ME\n=======\n-- marker: DONE\n>>>>>>> REPLACE"
);
json!({
"id": format!("msg_range_turn1_{}", prev + 1),
"type": "message",
"role": "assistant",
"content": [{ "type": "text", "text": sr_text }],
"model": "claude-haiku-mock",
"stop_reason": "end_turn",
"usage": { "input_tokens": 40, "output_tokens": 20 }
})
};
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
response_json.to_string(),
)
}
pub async fn spawn_range_mock() -> (std::net::SocketAddr, Arc<MockState>) {
let state = Arc::new(MockState {
call_count: Arc::new(AtomicUsize::new(0)),
distill_call_count: Arc::new(AtomicUsize::new(0)),
received_distill_body: Arc::new(Mutex::new(None)),
});
let router = Router::new()
.route("/v1/messages", post(anthropic_range_handler))
.with_state((*state).clone());
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral port for compile_loop range mock");
let addr = listener.local_addr().expect("local_addr");
tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
(addr, state)
}
pub async fn spawn_distill_mock(provider: &str) -> (std::net::SocketAddr, Arc<MockState>) {
let state = Arc::new(MockState {
call_count: Arc::new(AtomicUsize::new(0)),
distill_call_count: Arc::new(AtomicUsize::new(0)),
received_distill_body: Arc::new(Mutex::new(None)),
});
let router = match provider {
"anthropic" => Router::new()
.route("/v1/messages", post(anthropic_distill_handler))
.with_state((*state).clone()),
_ => {
Router::new()
.route("/chat/completions", post(openai_distill_handler))
.with_state((*state).clone())
}
};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral port for compile_loop distill mock");
let addr = listener.local_addr().expect("local_addr");
tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
(addr, state)
}