1 line
44 KiB
JSON
1 line
44 KiB
JSON
{"repo": "ceasarxuu/whalecode", "files": {"archive/deprecated/2026-04-27-rust-demo/crates/whalecode-cli/tests/live_status.rs": {"c539cbe18030727ae9c48e27246c0439ad246390": {"t": 1785745863.9110484, "n": 8041, "h": "0bd158d9a0d00e9a", "c": "use std::{\n io::{Read, Write},\n net::{TcpListener, TcpStream},\n process::Command,\n thread::{self, JoinHandle},\n};\n\nuse tempfile::tempdir;\n\n#[test]\nfn whale_live_run_prints_tool_permission_patch_and_usage_status() {\n let repo = tempdir().expect(\"repo\");\n let whale_home = tempdir().expect(\"whale home\");\n std::fs::write(repo.path().join(\"README.md\"), \"Fixture\\n\").expect(\"write readme\");\n let session_path = repo.path().join(\"session.jsonl\");\n let mock = MockDeepSeek::start(vec![edit_file_tool_call_sse(), final_answer_sse()]);\n\n let output = Command::new(env!(\"CARGO_BIN_EXE_whale\"))\n .args([\n \"run\",\n \"change the README fixture text\",\n \"--cwd\",\n repo.path().to_str().expect(\"repo path\"),\n \"--session\",\n session_path.to_str().expect(\"session path\"),\n \"--allow-write\",\n \"--max-turns\",\n \"3\",\n ])\n .env(\"DEEPSEEK_API_KEY\", \"test-key\")\n .env(\"DEEPSEEK_BASE_URL\", mock.base_url())\n .env(\"WHALE_SECRET_HOME\", whale_home.path())\n .output()\n .expect(\"run whale\");\n\n mock.join();\n assert!(\n output.status.success(),\n \"stdout:\\n{}\\nstderr:\\n{}\",\n String::from_utf8_lossy(&output.stdout),\n String::from_utf8_lossy(&output.stderr)\n );\n let stdout = String::from_utf8(output.stdout).expect(\"stdout utf8\");\n assert!(stdout.contains(\"workspace:\"));\n assert!(stdout.contains(\"model: deepseek-v4-flash\"));\n assert!(stdout.contains(\"edit_file: enabled\"));\n assert!(stdout.contains(\"run_command: disabled\"));\n assert!(stdout.contains(\"session:\"));\n assert_eq!(stdout.matches(\"session:\").count(), 1);\n assert!(stdout.contains(\"tool: edit_file README.md\"));\n assert!(stdout.contains(\"permission: edit_file allowed\"));\n assert!(stdout.contains(\"modified: README.md\"));\n assert!(stdout.contains(\"tool: edit_file README.md -> patch applied\"));\n assert!(stdout.contains(\"Updated README.\"));\n assert!(stdout.contains(\"input tokens: 30\"));\n assert!(stdout.contains(\"output tokens: 5\"));\n assert!(stdout.contains(\"cached input tokens: 9\"));\n assert!(stdout.contains(\"tool calls: 1\"));\n assert!(stdout.contains(\"files changed: 1 (README.md)\"));\n assert!(stdout.contains(\"duration:\"));\n assert_eq!(\n std::fs::read_to_string(repo.path().join(\"README.md\")).expect(\"read readme\"),\n \"Changed\\n\"\n );\n}\n\n#[test]\nfn whale_live_run_can_create_file_in_empty_workspace() {\n let repo = tempdir().expect(\"repo\");\n let whale_home = tempdir().expect(\"whale home\");\n let session_path = repo.path().join(\"session.jsonl\");\n let mock = MockDeepSeek::start(vec![write_file_tool_call_sse(), final_write_answer_sse()]);\n\n let output = Command::new(env!(\"CARGO_BIN_EXE_whale\"))\n .args([\n \"run\",\n \"create a tiny html page\",\n \"--cwd\",\n repo.path().to_str().expect(\"repo path\"),\n \"--session\",\n session_path.to_str().expect(\"session path\"),\n \"--allow-write\",\n \"--max-turns\",\n \"3\",\n ])\n .env(\"DEEPSEEK_API_KEY\", \"test-key\")\n .env(\"DEEPSEEK_BASE_URL\", mock.base_url())\n .env(\"WHALE_SECRET_HOME\", whale_home.path())\n .output()\n .expect(\"run whale\");\n\n mock.join();\n assert!(\n output.status.success(),\n \"stdout:\\n{}\\nstderr:\\n{}\",\n String::from_utf8_lossy(&output.stdout),\n String::from_utf8_lossy(&output.stderr)\n );\n let stdout = String::from_utf8(output.stdout).expect(\"stdout utf8\");\n assert!(stdout.contains(\"tool: write_file index.html\"));\n assert!(stdout.contains(\"permission: write_file allowed\"));\n assert!(stdout.contains(\"modified: index.html\"));\n assert!(stdout.contains(\"Wrote index.html.\"));\n assert!(stdout.contains(\"files changed: 1 (index.html)\"));\n assert_eq!(\n std::fs::read_to_string(repo.path().join(\"index.html\")).expect(\"read index\"),\n \"<!doctype html>\\n<title>Whale</title>\\n\"\n );\n}\n\nstruct MockDeepSeek {\n base_url: String,\n handle: JoinHandle<()>,\n}\n\nimpl MockDeepSeek {\n fn start(responses: Vec<String>) -> Self {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind mock server\");\n let base_url = format!(\"http://{}\", listener.local_addr().expect(\"local addr\"));\n let handle = thread::spawn(move || {\n for response_body in responses {\n let (stream, _) = listener.accept().expect(\"accept request\");\n respond(stream, &response_body);\n }\n });\n Self { base_url, handle }\n }\n\n fn base_url(&self) -> &str {\n &self.base_url\n }\n\n fn join(self) {\n self.handle.join().expect(\"mock server thread\");\n }\n}\n\nfn respond(mut stream: TcpStream, body: &str) {\n read_http_request(&mut stream);\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\ncontent-type: text/event-stream\\r\\ncontent-length: {}\\r\\nconnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write response\");\n stream.flush().expect(\"flush response\");\n}\n\nfn read_http_request(stream: &mut TcpStream) {\n let mut request = Vec::new();\n let mut buf = [0_u8; 1024];\n let header_end = loop {\n let read = stream.read(&mut buf).expect(\"read request\");\n assert_ne!(read, 0, \"client closed request before headers completed\");\n request.extend_from_slice(&buf[..read]);\n if let Some(index) = find_header_end(&request) {\n break index;\n }\n };\n let headers = String::from_utf8_lossy(&request[..header_end]);\n let content_length = content_length(&headers);\n let body_start = header_end + 4;\n let mut body_read = request.len().saturating_sub(body_start);\n while body_read < content_length {\n let read = stream.read(&mut buf).expect(\"read request body\");\n assert_ne!(read, 0, \"client closed request before body completed\");\n body_read += read;\n }\n}\n\nfn find_header_end(request: &[u8]) -> Option<usize> {\n request.windows(4).position(|window| window == b\"\\r\\n\\r\\n\")\n}\n\nfn content_length(headers: &str) -> usize {\n headers\n .lines()\n .find_map(|line| {\n let (name, value) = line.split_once(':')?;\n name.eq_ignore_ascii_case(\"content-length\")\n .then(|| value.trim().parse::<usize>().expect(\"content-length\"))\n })\n .unwrap_or(0)\n}\n\nfn edit_file_tool_call_sse() -> String {\n r#\"data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"edit_file\",\"arguments\":\"{\\\"path\\\":\\\"README.md\\\",\\\"old_string\\\":\\\"Fixture\\\",\\\"new_string\\\":\\\"Changed\\\"}\"}}]},\"finish_reason\":null,\"index\":0}]}\n\ndata: {\"choices\":[],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":2,\"total_tokens\":12,\"prompt_cache_hit_tokens\":4}}\n\ndata: [DONE]\n\n\"#\n .to_owned()\n}\n\nfn final_answer_sse() -> String {\n r#\"data: {\"choices\":[{\"delta\":{\"content\":\"Updated README.\"},\"finish_reason\":null,\"index\":0}]}\n\ndata: {\"choices\":[],\"usage\":{\"prompt_tokens\":20,\"completion_tokens\":3,\"total_tokens\":23,\"prompt_cache_hit_tokens\":5}}\n\ndata: [DONE]\n\n\"#\n .to_owned()\n}\n\nfn write_file_tool_call_sse() -> String {\n r#\"data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"write_file\",\"arguments\":\"{\\\"path\\\":\\\"index.html\\\",\\\"content\\\":\\\"<!doctype html>\\\\n<title>Whale</title>\\\\n\\\"}\"}}]},\"finish_reason\":null,\"index\":0}]}\n\ndata: {\"choices\":[],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":2,\"total_tokens\":12,\"prompt_cache_hit_tokens\":4}}\n\ndata: [DONE]\n\n\"#\n .to_owned()\n}\n\nfn final_write_answer_sse() -> String {\n r#\"data: {\"choices\":[{\"delta\":{\"content\":\"Wrote index.html.\"},\"finish_reason\":null,\"index\":0}]}\n\ndata: {\"choices\":[],\"usage\":{\"prompt_tokens\":20,\"completion_tokens\":3,\"total_tokens\":23,\"prompt_cache_hit_tokens\":5}}\n\ndata: [DONE]\n\n\"#\n .to_owned()\n}\n"}, "5c6a2946a2c3ffd81e71e7917dd3f8a4a46d0890": {"t": 1785757378.3068004, "n": 8041, "h": "0bd158d9a0d00e9a", "c": "use std::{\n io::{Read, Write},\n net::{TcpListener, TcpStream},\n process::Command,\n thread::{self, JoinHandle},\n};\n\nuse tempfile::tempdir;\n\n#[test]\nfn whale_live_run_prints_tool_permission_patch_and_usage_status() {\n let repo = tempdir().expect(\"repo\");\n let whale_home = tempdir().expect(\"whale home\");\n std::fs::write(repo.path().join(\"README.md\"), \"Fixture\\n\").expect(\"write readme\");\n let session_path = repo.path().join(\"session.jsonl\");\n let mock = MockDeepSeek::start(vec![edit_file_tool_call_sse(), final_answer_sse()]);\n\n let output = Command::new(env!(\"CARGO_BIN_EXE_whale\"))\n .args([\n \"run\",\n \"change the README fixture text\",\n \"--cwd\",\n repo.path().to_str().expect(\"repo path\"),\n \"--session\",\n session_path.to_str().expect(\"session path\"),\n \"--allow-write\",\n \"--max-turns\",\n \"3\",\n ])\n .env(\"DEEPSEEK_API_KEY\", \"test-key\")\n .env(\"DEEPSEEK_BASE_URL\", mock.base_url())\n .env(\"WHALE_SECRET_HOME\", whale_home.path())\n .output()\n .expect(\"run whale\");\n\n mock.join();\n assert!(\n output.status.success(),\n \"stdout:\\n{}\\nstderr:\\n{}\",\n String::from_utf8_lossy(&output.stdout),\n String::from_utf8_lossy(&output.stderr)\n );\n let stdout = String::from_utf8(output.stdout).expect(\"stdout utf8\");\n assert!(stdout.contains(\"workspace:\"));\n assert!(stdout.contains(\"model: deepseek-v4-flash\"));\n assert!(stdout.contains(\"edit_file: enabled\"));\n assert!(stdout.contains(\"run_command: disabled\"));\n assert!(stdout.contains(\"session:\"));\n assert_eq!(stdout.matches(\"session:\").count(), 1);\n assert!(stdout.contains(\"tool: edit_file README.md\"));\n assert!(stdout.contains(\"permission: edit_file allowed\"));\n assert!(stdout.contains(\"modified: README.md\"));\n assert!(stdout.contains(\"tool: edit_file README.md -> patch applied\"));\n assert!(stdout.contains(\"Updated README.\"));\n assert!(stdout.contains(\"input tokens: 30\"));\n assert!(stdout.contains(\"output tokens: 5\"));\n assert!(stdout.contains(\"cached input tokens: 9\"));\n assert!(stdout.contains(\"tool calls: 1\"));\n assert!(stdout.contains(\"files changed: 1 (README.md)\"));\n assert!(stdout.contains(\"duration:\"));\n assert_eq!(\n std::fs::read_to_string(repo.path().join(\"README.md\")).expect(\"read readme\"),\n \"Changed\\n\"\n );\n}\n\n#[test]\nfn whale_live_run_can_create_file_in_empty_workspace() {\n let repo = tempdir().expect(\"repo\");\n let whale_home = tempdir().expect(\"whale home\");\n let session_path = repo.path().join(\"session.jsonl\");\n let mock = MockDeepSeek::start(vec![write_file_tool_call_sse(), final_write_answer_sse()]);\n\n let output = Command::new(env!(\"CARGO_BIN_EXE_whale\"))\n .args([\n \"run\",\n \"create a tiny html page\",\n \"--cwd\",\n repo.path().to_str().expect(\"repo path\"),\n \"--session\",\n session_path.to_str().expect(\"session path\"),\n \"--allow-write\",\n \"--max-turns\",\n \"3\",\n ])\n .env(\"DEEPSEEK_API_KEY\", \"test-key\")\n .env(\"DEEPSEEK_BASE_URL\", mock.base_url())\n .env(\"WHALE_SECRET_HOME\", whale_home.path())\n .output()\n .expect(\"run whale\");\n\n mock.join();\n assert!(\n output.status.success(),\n \"stdout:\\n{}\\nstderr:\\n{}\",\n String::from_utf8_lossy(&output.stdout),\n String::from_utf8_lossy(&output.stderr)\n );\n let stdout = String::from_utf8(output.stdout).expect(\"stdout utf8\");\n assert!(stdout.contains(\"tool: write_file index.html\"));\n assert!(stdout.contains(\"permission: write_file allowed\"));\n assert!(stdout.contains(\"modified: index.html\"));\n assert!(stdout.contains(\"Wrote index.html.\"));\n assert!(stdout.contains(\"files changed: 1 (index.html)\"));\n assert_eq!(\n std::fs::read_to_string(repo.path().join(\"index.html\")).expect(\"read index\"),\n \"<!doctype html>\\n<title>Whale</title>\\n\"\n );\n}\n\nstruct MockDeepSeek {\n base_url: String,\n handle: JoinHandle<()>,\n}\n\nimpl MockDeepSeek {\n fn start(responses: Vec<String>) -> Self {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind mock server\");\n let base_url = format!(\"http://{}\", listener.local_addr().expect(\"local addr\"));\n let handle = thread::spawn(move || {\n for response_body in responses {\n let (stream, _) = listener.accept().expect(\"accept request\");\n respond(stream, &response_body);\n }\n });\n Self { base_url, handle }\n }\n\n fn base_url(&self) -> &str {\n &self.base_url\n }\n\n fn join(self) {\n self.handle.join().expect(\"mock server thread\");\n }\n}\n\nfn respond(mut stream: TcpStream, body: &str) {\n read_http_request(&mut stream);\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\ncontent-type: text/event-stream\\r\\ncontent-length: {}\\r\\nconnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write response\");\n stream.flush().expect(\"flush response\");\n}\n\nfn read_http_request(stream: &mut TcpStream) {\n let mut request = Vec::new();\n let mut buf = [0_u8; 1024];\n let header_end = loop {\n let read = stream.read(&mut buf).expect(\"read request\");\n assert_ne!(read, 0, \"client closed request before headers completed\");\n request.extend_from_slice(&buf[..read]);\n if let Some(index) = find_header_end(&request) {\n break index;\n }\n };\n let headers = String::from_utf8_lossy(&request[..header_end]);\n let content_length = content_length(&headers);\n let body_start = header_end + 4;\n let mut body_read = request.len().saturating_sub(body_start);\n while body_read < content_length {\n let read = stream.read(&mut buf).expect(\"read request body\");\n assert_ne!(read, 0, \"client closed request before body completed\");\n body_read += read;\n }\n}\n\nfn find_header_end(request: &[u8]) -> Option<usize> {\n request.windows(4).position(|window| window == b\"\\r\\n\\r\\n\")\n}\n\nfn content_length(headers: &str) -> usize {\n headers\n .lines()\n .find_map(|line| {\n let (name, value) = line.split_once(':')?;\n name.eq_ignore_ascii_case(\"content-length\")\n .then(|| value.trim().parse::<usize>().expect(\"content-length\"))\n })\n .unwrap_or(0)\n}\n\nfn edit_file_tool_call_sse() -> String {\n r#\"data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"edit_file\",\"arguments\":\"{\\\"path\\\":\\\"README.md\\\",\\\"old_string\\\":\\\"Fixture\\\",\\\"new_string\\\":\\\"Changed\\\"}\"}}]},\"finish_reason\":null,\"index\":0}]}\n\ndata: {\"choices\":[],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":2,\"total_tokens\":12,\"prompt_cache_hit_tokens\":4}}\n\ndata: [DONE]\n\n\"#\n .to_owned()\n}\n\nfn final_answer_sse() -> String {\n r#\"data: {\"choices\":[{\"delta\":{\"content\":\"Updated README.\"},\"finish_reason\":null,\"index\":0}]}\n\ndata: {\"choices\":[],\"usage\":{\"prompt_tokens\":20,\"completion_tokens\":3,\"total_tokens\":23,\"prompt_cache_hit_tokens\":5}}\n\ndata: [DONE]\n\n\"#\n .to_owned()\n}\n\nfn write_file_tool_call_sse() -> String {\n r#\"data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"write_file\",\"arguments\":\"{\\\"path\\\":\\\"index.html\\\",\\\"content\\\":\\\"<!doctype html>\\\\n<title>Whale</title>\\\\n\\\"}\"}}]},\"finish_reason\":null,\"index\":0}]}\n\ndata: {\"choices\":[],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":2,\"total_tokens\":12,\"prompt_cache_hit_tokens\":4}}\n\ndata: [DONE]\n\n\"#\n .to_owned()\n}\n\nfn final_write_answer_sse() -> String {\n r#\"data: {\"choices\":[{\"delta\":{\"content\":\"Wrote index.html.\"},\"finish_reason\":null,\"index\":0}]}\n\ndata: {\"choices\":[],\"usage\":{\"prompt_tokens\":20,\"completion_tokens\":3,\"total_tokens\":23,\"prompt_cache_hit_tokens\":5}}\n\ndata: [DONE]\n\n\"#\n .to_owned()\n}\n"}}, "archive/deprecated/2026-04-27-rust-demo/crates/whalecode-model/src/lib.rs": {"c539cbe18030727ae9c48e27246c0439ad246390": {"t": 1785745874.806327, "n": 13207, "h": "81055e8afe6afc4a", "c": "use std::env;\n\nuse chrono::{DateTime, Utc};\nuse futures_util::StreamExt;\nuse serde::{Deserialize, Serialize};\nuse serde_json::{json, Value};\nuse thiserror::Error;\n\nmod bootstrap;\nmod secrets;\nmod tool_calls;\n\npub use bootstrap::*;\npub use secrets::*;\npub use tool_calls::*;\n\npub const DEEPSEEK_DEFAULT_BASE_URL: &str = \"https://api.deepseek.com\";\npub const DEEPSEEK_DEFAULT_MODEL: &str = \"deepseek-v4-flash\";\npub const DEEPSEEK_PRO_MODEL: &str = \"deepseek-v4-pro\";\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ModelCapabilities {\n pub model: String,\n pub context_window_tokens: u64,\n pub max_output_tokens: u64,\n pub supports_thinking: bool,\n pub supports_tool_calls: bool,\n pub observed_at: DateTime<Utc>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ModelStreamEvent {\n TextDelta(String),\n ReasoningDelta(String),\n ToolCallDelta {\n index: usize,\n id: Option<String>,\n name: String,\n arguments_delta: String,\n },\n Usage(ModelTokenUsage),\n Finished,\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct ModelTokenUsage {\n pub input_tokens: u64,\n pub output_tokens: u64,\n pub cached_input_tokens: u64,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ThinkingMode {\n Enabled,\n Disabled,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ReasoningEffort {\n Low,\n Medium,\n High,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct DeepSeekConfig {\n pub base_url: String,\n pub api_key: Option<String>,\n pub model: String,\n pub thinking: ThinkingMode,\n pub reasoning_effort: ReasoningEffort,\n}\n\nimpl DeepSeekConfig {\n pub fn from_env() -> Self {\n Self {\n base_url: env::var(\"DEEPSEEK_BASE_URL\")\n .unwrap_or_else(|_| DEEPSEEK_DEFAULT_BASE_URL.to_owned()),\n api_key: resolve_deepseek_api_key(),\n model: env::var(\"DEEPSEEK_MODEL\").unwrap_or_else(|_| DEEPSEEK_DEFAULT_MODEL.to_owned()),\n thinking: ThinkingMode::Enabled,\n reasoning_effort: ReasoningEffort::Medium,\n }\n }\n\n pub fn endpoint(&self) -> String {\n format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'))\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum ChatMessageRole {\n System,\n User,\n Assistant,\n Tool,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ChatMessage {\n pub role: ChatMessageRole,\n pub content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub reasoning_content: Option<String>,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tool_call_id: Option<String>,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tool_calls: Option<Vec<DeepSeekToolCall>>,\n}\n\nimpl ChatMessage {\n pub fn user(content: impl Into<String>) -> Self {\n Self {\n role: ChatMessageRole::User,\n content: content.into(),\n reasoning_content: None,\n tool_call_id: None,\n tool_calls: None,\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct DeepSeekToolCall {\n pub id: String,\n #[serde(rename = \"type\")]\n pub kind: String,\n pub function: DeepSeekFunctionCall,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct DeepSeekFunctionCall {\n pub name: String,\n pub arguments: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct DeepSeekChatRequest {\n pub model: String,\n pub messages: Vec<ChatMessage>,\n pub stream: bool,\n pub thinking: ThinkingControl,\n pub reasoning_effort: String,\n pub stream_options: StreamOptions,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tools: Option<Vec<Value>>,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tool_choice: Option<Value>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ThinkingControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct StreamOptions {\n pub include_usage: bool,\n}\n\nimpl DeepSeekChatRequest {\n pub fn streaming(config: &DeepSeekConfig, messages: Vec<ChatMessage>) -> Self {\n Self {\n model: config.model.clone(),\n messages,\n stream: true,\n thinking: ThinkingControl {\n kind: config.thinking.as_api_str().to_owned(),\n },\n reasoning_effort: config.reasoning_effort.as_api_str().to_owned(),\n stream_options: StreamOptions {\n include_usage: true,\n },\n tools: None,\n tool_choice: None,\n }\n }\n\n pub fn with_tools(mut self, tools: Vec<Value>) -> Self {\n self.tool_choice = Some(json!(\"auto\"));\n self.tools = Some(tools);\n self\n }\n}\n\n#[derive(Debug, Clone)]\npub struct DeepSeekClient {\n config: DeepSeekConfig,\n client: reqwest::Client,\n}\n\nimpl DeepSeekClient {\n pub fn new(config: DeepSeekConfig) -> Self {\n Self {\n config,\n client: reqwest::Client::new(),\n }\n }\n\n pub async fn stream_chat(\n &self,\n request: &DeepSeekChatRequest,\n ) -> Result<Vec<ModelStreamEvent>, ModelError> {\n self.stream_chat_with_observer(request, |_| {}).await\n }\n\n pub async fn stream_chat_with_observer<F>(\n &self,\n request: &DeepSeekChatRequest,\n mut observer: F,\n ) -> Result<Vec<ModelStreamEvent>, ModelError>\n where\n F: FnMut(&ModelStreamEvent),\n {\n let api_key = self\n .config\n .api_key\n .as_deref()\n .ok_or(ModelError::MissingApiKey)?;\n let response = self\n .client\n .post(self.config.endpoint())\n .bearer_auth(api_key)\n .json(request)\n .send()\n .await?;\n let status = response.status();\n if !status.is_success() {\n let body = response.text().await.unwrap_or_default();\n return Err(ModelError::HttpStatus {\n status: status.as_u16(),\n body,\n });\n }\n\n let mut stream = response.bytes_stream();\n let mut buffer = String::new();\n let mut events = Vec::new();\n while let Some(chunk) = stream.next().await {\n let chunk = chunk?;\n let text = std::str::from_utf8(&chunk).map_err(|source| ModelError::Utf8 {\n message: source.to_string(),\n })?;\n buffer.push_str(text);\n drain_complete_sse_frames(&mut buffer, &mut events, &mut observer)?;\n }\n if !buffer.trim().is_empty() {\n push_observed_events(&mut events, parse_sse_stream(&buffer)?, &mut observer);\n }\n Ok(events)\n }\n}\n\n#[derive(Debug, Error)]\npub enum ModelError {\n #[error(\n \"DeepSeek API key is required for live DeepSeek calls; set DEEPSEEK_API_KEY or run /apikey inside whale\"\n )]\n MissingApiKey,\n #[error(\"model http request failed: {0}\")]\n Http(#[from] reqwest::Error),\n #[error(\"model returned http {status}: {body}\")]\n HttpStatus { status: u16, body: String },\n #[error(\"malformed SSE JSON payload: {0}\")]\n Json(#[from] serde_json::Error),\n #[error(\"stream chunk is not valid UTF-8: {message}\")]\n Utf8 { message: String },\n}\n\npub fn parse_sse_stream(input: &str) -> Result<Vec<ModelStreamEvent>, ModelError> {\n parse_sse_stream_with_observer(input, |_| {})\n}\n\npub fn parse_sse_stream_with_observer<F>(\n input: &str,\n mut observer: F,\n) -> Result<Vec<ModelStreamEvent>, ModelError>\nwhere\n F: FnMut(&ModelStreamEvent),\n{\n let mut events = Vec::new();\n for frame in sse_data_frames(input) {\n if frame == \"[DONE]\" {\n push_observed_events(&mut events, vec![ModelStreamEvent::Finished], &mut observer);\n continue;\n }\n push_observed_events(\n &mut events,\n events_from_chunk(&serde_json::from_str(&frame)?),\n &mut observer,\n );\n }\n Ok(events)\n}\n\npub fn response_from_stream_events(events: Vec<ModelStreamEvent>) -> ModelResponse {\n let final_text = events\n .iter()\n .filter_map(|event| match event {\n ModelStreamEvent::TextDelta(content) => Some(content.as_str()),\n _ => None,\n })\n .collect::<String>();\n ModelResponse { events, final_text }\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekStreamChunk {\n #[serde(default)]\n choices: Vec<DeepSeekStreamChoice>,\n usage: Option<DeepSeekUsage>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekStreamChoice {\n delta: DeepSeekDelta,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekDelta {\n content: Option<String>,\n reasoning_content: Option<String>,\n tool_calls: Option<Vec<DeepSeekToolCallDelta>>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekUsage {\n prompt_tokens: u64,\n completion_tokens: u64,\n prompt_cache_hit_tokens: Option<u64>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekToolCallDelta {\n index: Option<usize>,\n id: Option<String>,\n #[serde(rename = \"type\")]\n kind: Option<String>,\n function: Option<DeepSeekFunctionDelta>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekFunctionDelta {\n name: Option<String>,\n arguments: Option<String>,\n}\n\nfn events_from_chunk(chunk: &DeepSeekStreamChunk) -> Vec<ModelStreamEvent> {\n let mut events = Vec::new();\n if let Some(usage) = &chunk.usage {\n events.push(ModelStreamEvent::Usage(ModelTokenUsage {\n input_tokens: usage.prompt_tokens,\n output_tokens: usage.completion_tokens,\n cached_input_tokens: usage.prompt_cache_hit_tokens.unwrap_or(0),\n }));\n }\n for choice in &chunk.choices {\n if let Some(reasoning) = choice.delta.reasoning_content.as_deref() {\n if !reasoning.is_empty() {\n events.push(ModelStreamEvent::ReasoningDelta(reasoning.to_owned()));\n }\n }\n if let Some(content) = choice.delta.content.as_deref() {\n if !content.is_empty() {\n events.push(ModelStreamEvent::TextDelta(content.to_owned()));\n }\n }\n for tool_call in choice.delta.tool_calls.as_deref().unwrap_or_default() {\n let Some(function) = &tool_call.function else {\n continue;\n };\n let _kind = tool_call.kind.as_deref().unwrap_or(\"function\");\n let name = function.name.clone().unwrap_or_default();\n let arguments_delta = function.arguments.clone().unwrap_or_default();\n if !name.is_empty() || !arguments_delta.is_empty() {\n events.push(ModelStreamEvent::ToolCallDelta {\n index: tool_call.index.unwrap_or(0),\n id: tool_call.id.clone(),\n name,\n arguments_delta,\n });\n }\n }\n }\n events\n}\n\nfn sse_data_frames(input: &str) -> Vec<String> {\n let normalized = input.replace(\"\\r\\n\", \"\\n\");\n normalized\n .split(\"\\n\\n\")\n .filter_map(|frame| {\n let data = frame\n .lines()\n .filter_map(|line| line.strip_prefix(\"data:\"))\n .map(|line| line.trim_start())\n .collect::<Vec<_>>()\n .join(\"\\n\");\n (!data.is_empty()).then_some(data)\n })\n .collect()\n}\n\nfn drain_complete_sse_frames(\n buffer: &mut String,\n events: &mut Vec<ModelStreamEvent>,\n observer: &mut impl FnMut(&ModelStreamEvent),\n) -> Result<(), ModelError> {\n while let Some(index) = find_frame_boundary(buffer) {\n let frame = buffer[..index].to_owned();\n let drain_to = if buffer[index..].starts_with(\"\\r\\n\\r\\n\") {\n index + 4\n } else {\n index + 2\n };\n buffer.drain(..drain_to);\n push_observed_events(events, parse_sse_stream(&frame)?, observer);\n }\n Ok(())\n}\n\nfn push_observed_events(\n events: &mut Vec<ModelStreamEvent>,\n new_events: Vec<ModelStreamEvent>,\n observer: &mut impl FnMut(&ModelStreamEvent),\n) {\n for event in new_events {\n observer(&event);\n events.push(event);\n }\n}\n\nfn find_frame_boundary(buffer: &str) -> Option<usize> {\n match (buffer.find(\"\\n\\n\"), buffer.find(\"\\r\\n\\r\\n\")) {\n (Some(a), Some(b)) => Some(a.min(b)),\n (Some(a), None) => Some(a),\n (None, Some(b)) => Some(b),\n (None, None) => None,\n }\n}\n\nimpl ThinkingMode {\n fn as_api_str(&self) -> &'static str {\n match self {\n ThinkingMode::Enabled => \"enabled\",\n ThinkingMode::Disabled => \"disabled\",\n }\n }\n}\n\nimpl ReasoningEffort {\n fn as_api_str(&self) -> &'static str {\n match self {\n ReasoningEffort::Low => \"low\",\n ReasoningEffort::Medium => \"medium\",\n ReasoningEffort::High => \"high\",\n }\n }\n}\n"}, "5c6a2946a2c3ffd81e71e7917dd3f8a4a46d0890": {"t": 1785757377.514132, "n": 13207, "h": "81055e8afe6afc4a", "c": "use std::env;\n\nuse chrono::{DateTime, Utc};\nuse futures_util::StreamExt;\nuse serde::{Deserialize, Serialize};\nuse serde_json::{json, Value};\nuse thiserror::Error;\n\nmod bootstrap;\nmod secrets;\nmod tool_calls;\n\npub use bootstrap::*;\npub use secrets::*;\npub use tool_calls::*;\n\npub const DEEPSEEK_DEFAULT_BASE_URL: &str = \"https://api.deepseek.com\";\npub const DEEPSEEK_DEFAULT_MODEL: &str = \"deepseek-v4-flash\";\npub const DEEPSEEK_PRO_MODEL: &str = \"deepseek-v4-pro\";\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ModelCapabilities {\n pub model: String,\n pub context_window_tokens: u64,\n pub max_output_tokens: u64,\n pub supports_thinking: bool,\n pub supports_tool_calls: bool,\n pub observed_at: DateTime<Utc>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ModelStreamEvent {\n TextDelta(String),\n ReasoningDelta(String),\n ToolCallDelta {\n index: usize,\n id: Option<String>,\n name: String,\n arguments_delta: String,\n },\n Usage(ModelTokenUsage),\n Finished,\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct ModelTokenUsage {\n pub input_tokens: u64,\n pub output_tokens: u64,\n pub cached_input_tokens: u64,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ThinkingMode {\n Enabled,\n Disabled,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ReasoningEffort {\n Low,\n Medium,\n High,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct DeepSeekConfig {\n pub base_url: String,\n pub api_key: Option<String>,\n pub model: String,\n pub thinking: ThinkingMode,\n pub reasoning_effort: ReasoningEffort,\n}\n\nimpl DeepSeekConfig {\n pub fn from_env() -> Self {\n Self {\n base_url: env::var(\"DEEPSEEK_BASE_URL\")\n .unwrap_or_else(|_| DEEPSEEK_DEFAULT_BASE_URL.to_owned()),\n api_key: resolve_deepseek_api_key(),\n model: env::var(\"DEEPSEEK_MODEL\").unwrap_or_else(|_| DEEPSEEK_DEFAULT_MODEL.to_owned()),\n thinking: ThinkingMode::Enabled,\n reasoning_effort: ReasoningEffort::Medium,\n }\n }\n\n pub fn endpoint(&self) -> String {\n format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'))\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum ChatMessageRole {\n System,\n User,\n Assistant,\n Tool,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ChatMessage {\n pub role: ChatMessageRole,\n pub content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub reasoning_content: Option<String>,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tool_call_id: Option<String>,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tool_calls: Option<Vec<DeepSeekToolCall>>,\n}\n\nimpl ChatMessage {\n pub fn user(content: impl Into<String>) -> Self {\n Self {\n role: ChatMessageRole::User,\n content: content.into(),\n reasoning_content: None,\n tool_call_id: None,\n tool_calls: None,\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct DeepSeekToolCall {\n pub id: String,\n #[serde(rename = \"type\")]\n pub kind: String,\n pub function: DeepSeekFunctionCall,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct DeepSeekFunctionCall {\n pub name: String,\n pub arguments: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct DeepSeekChatRequest {\n pub model: String,\n pub messages: Vec<ChatMessage>,\n pub stream: bool,\n pub thinking: ThinkingControl,\n pub reasoning_effort: String,\n pub stream_options: StreamOptions,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tools: Option<Vec<Value>>,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tool_choice: Option<Value>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ThinkingControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct StreamOptions {\n pub include_usage: bool,\n}\n\nimpl DeepSeekChatRequest {\n pub fn streaming(config: &DeepSeekConfig, messages: Vec<ChatMessage>) -> Self {\n Self {\n model: config.model.clone(),\n messages,\n stream: true,\n thinking: ThinkingControl {\n kind: config.thinking.as_api_str().to_owned(),\n },\n reasoning_effort: config.reasoning_effort.as_api_str().to_owned(),\n stream_options: StreamOptions {\n include_usage: true,\n },\n tools: None,\n tool_choice: None,\n }\n }\n\n pub fn with_tools(mut self, tools: Vec<Value>) -> Self {\n self.tool_choice = Some(json!(\"auto\"));\n self.tools = Some(tools);\n self\n }\n}\n\n#[derive(Debug, Clone)]\npub struct DeepSeekClient {\n config: DeepSeekConfig,\n client: reqwest::Client,\n}\n\nimpl DeepSeekClient {\n pub fn new(config: DeepSeekConfig) -> Self {\n Self {\n config,\n client: reqwest::Client::new(),\n }\n }\n\n pub async fn stream_chat(\n &self,\n request: &DeepSeekChatRequest,\n ) -> Result<Vec<ModelStreamEvent>, ModelError> {\n self.stream_chat_with_observer(request, |_| {}).await\n }\n\n pub async fn stream_chat_with_observer<F>(\n &self,\n request: &DeepSeekChatRequest,\n mut observer: F,\n ) -> Result<Vec<ModelStreamEvent>, ModelError>\n where\n F: FnMut(&ModelStreamEvent),\n {\n let api_key = self\n .config\n .api_key\n .as_deref()\n .ok_or(ModelError::MissingApiKey)?;\n let response = self\n .client\n .post(self.config.endpoint())\n .bearer_auth(api_key)\n .json(request)\n .send()\n .await?;\n let status = response.status();\n if !status.is_success() {\n let body = response.text().await.unwrap_or_default();\n return Err(ModelError::HttpStatus {\n status: status.as_u16(),\n body,\n });\n }\n\n let mut stream = response.bytes_stream();\n let mut buffer = String::new();\n let mut events = Vec::new();\n while let Some(chunk) = stream.next().await {\n let chunk = chunk?;\n let text = std::str::from_utf8(&chunk).map_err(|source| ModelError::Utf8 {\n message: source.to_string(),\n })?;\n buffer.push_str(text);\n drain_complete_sse_frames(&mut buffer, &mut events, &mut observer)?;\n }\n if !buffer.trim().is_empty() {\n push_observed_events(&mut events, parse_sse_stream(&buffer)?, &mut observer);\n }\n Ok(events)\n }\n}\n\n#[derive(Debug, Error)]\npub enum ModelError {\n #[error(\n \"DeepSeek API key is required for live DeepSeek calls; set DEEPSEEK_API_KEY or run /apikey inside whale\"\n )]\n MissingApiKey,\n #[error(\"model http request failed: {0}\")]\n Http(#[from] reqwest::Error),\n #[error(\"model returned http {status}: {body}\")]\n HttpStatus { status: u16, body: String },\n #[error(\"malformed SSE JSON payload: {0}\")]\n Json(#[from] serde_json::Error),\n #[error(\"stream chunk is not valid UTF-8: {message}\")]\n Utf8 { message: String },\n}\n\npub fn parse_sse_stream(input: &str) -> Result<Vec<ModelStreamEvent>, ModelError> {\n parse_sse_stream_with_observer(input, |_| {})\n}\n\npub fn parse_sse_stream_with_observer<F>(\n input: &str,\n mut observer: F,\n) -> Result<Vec<ModelStreamEvent>, ModelError>\nwhere\n F: FnMut(&ModelStreamEvent),\n{\n let mut events = Vec::new();\n for frame in sse_data_frames(input) {\n if frame == \"[DONE]\" {\n push_observed_events(&mut events, vec![ModelStreamEvent::Finished], &mut observer);\n continue;\n }\n push_observed_events(\n &mut events,\n events_from_chunk(&serde_json::from_str(&frame)?),\n &mut observer,\n );\n }\n Ok(events)\n}\n\npub fn response_from_stream_events(events: Vec<ModelStreamEvent>) -> ModelResponse {\n let final_text = events\n .iter()\n .filter_map(|event| match event {\n ModelStreamEvent::TextDelta(content) => Some(content.as_str()),\n _ => None,\n })\n .collect::<String>();\n ModelResponse { events, final_text }\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekStreamChunk {\n #[serde(default)]\n choices: Vec<DeepSeekStreamChoice>,\n usage: Option<DeepSeekUsage>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekStreamChoice {\n delta: DeepSeekDelta,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekDelta {\n content: Option<String>,\n reasoning_content: Option<String>,\n tool_calls: Option<Vec<DeepSeekToolCallDelta>>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekUsage {\n prompt_tokens: u64,\n completion_tokens: u64,\n prompt_cache_hit_tokens: Option<u64>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekToolCallDelta {\n index: Option<usize>,\n id: Option<String>,\n #[serde(rename = \"type\")]\n kind: Option<String>,\n function: Option<DeepSeekFunctionDelta>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct DeepSeekFunctionDelta {\n name: Option<String>,\n arguments: Option<String>,\n}\n\nfn events_from_chunk(chunk: &DeepSeekStreamChunk) -> Vec<ModelStreamEvent> {\n let mut events = Vec::new();\n if let Some(usage) = &chunk.usage {\n events.push(ModelStreamEvent::Usage(ModelTokenUsage {\n input_tokens: usage.prompt_tokens,\n output_tokens: usage.completion_tokens,\n cached_input_tokens: usage.prompt_cache_hit_tokens.unwrap_or(0),\n }));\n }\n for choice in &chunk.choices {\n if let Some(reasoning) = choice.delta.reasoning_content.as_deref() {\n if !reasoning.is_empty() {\n events.push(ModelStreamEvent::ReasoningDelta(reasoning.to_owned()));\n }\n }\n if let Some(content) = choice.delta.content.as_deref() {\n if !content.is_empty() {\n events.push(ModelStreamEvent::TextDelta(content.to_owned()));\n }\n }\n for tool_call in choice.delta.tool_calls.as_deref().unwrap_or_default() {\n let Some(function) = &tool_call.function else {\n continue;\n };\n let _kind = tool_call.kind.as_deref().unwrap_or(\"function\");\n let name = function.name.clone().unwrap_or_default();\n let arguments_delta = function.arguments.clone().unwrap_or_default();\n if !name.is_empty() || !arguments_delta.is_empty() {\n events.push(ModelStreamEvent::ToolCallDelta {\n index: tool_call.index.unwrap_or(0),\n id: tool_call.id.clone(),\n name,\n arguments_delta,\n });\n }\n }\n }\n events\n}\n\nfn sse_data_frames(input: &str) -> Vec<String> {\n let normalized = input.replace(\"\\r\\n\", \"\\n\");\n normalized\n .split(\"\\n\\n\")\n .filter_map(|frame| {\n let data = frame\n .lines()\n .filter_map(|line| line.strip_prefix(\"data:\"))\n .map(|line| line.trim_start())\n .collect::<Vec<_>>()\n .join(\"\\n\");\n (!data.is_empty()).then_some(data)\n })\n .collect()\n}\n\nfn drain_complete_sse_frames(\n buffer: &mut String,\n events: &mut Vec<ModelStreamEvent>,\n observer: &mut impl FnMut(&ModelStreamEvent),\n) -> Result<(), ModelError> {\n while let Some(index) = find_frame_boundary(buffer) {\n let frame = buffer[..index].to_owned();\n let drain_to = if buffer[index..].starts_with(\"\\r\\n\\r\\n\") {\n index + 4\n } else {\n index + 2\n };\n buffer.drain(..drain_to);\n push_observed_events(events, parse_sse_stream(&frame)?, observer);\n }\n Ok(())\n}\n\nfn push_observed_events(\n events: &mut Vec<ModelStreamEvent>,\n new_events: Vec<ModelStreamEvent>,\n observer: &mut impl FnMut(&ModelStreamEvent),\n) {\n for event in new_events {\n observer(&event);\n events.push(event);\n }\n}\n\nfn find_frame_boundary(buffer: &str) -> Option<usize> {\n match (buffer.find(\"\\n\\n\"), buffer.find(\"\\r\\n\\r\\n\")) {\n (Some(a), Some(b)) => Some(a.min(b)),\n (Some(a), None) => Some(a),\n (None, Some(b)) => Some(b),\n (None, None) => None,\n }\n}\n\nimpl ThinkingMode {\n fn as_api_str(&self) -> &'static str {\n match self {\n ThinkingMode::Enabled => \"enabled\",\n ThinkingMode::Disabled => \"disabled\",\n }\n }\n}\n\nimpl ReasoningEffort {\n fn as_api_str(&self) -> &'static str {\n match self {\n ReasoningEffort::Low => \"low\",\n ReasoningEffort::Medium => \"medium\",\n ReasoningEffort::High => \"high\",\n }\n }\n}\n"}}}} |