Files

1 line
11 KiB
JSON

{"repo": "chesterguan/cliniclaw", "files": {"crates/cliniclaw-sim/src/remote_llm.rs": {"fe1378817c730f4de4fa6b4d92f95ed7fc49ce2e": {"t": 1785746247.3597734, "n": 5082, "h": "8e68b0536925d1df", "c": "//! Minimal remote-LLM adapters (Claude + DeepSeek) implementing `LlmCapability`,\n//! so the long-horizon experiment can use frontier models as a \"ceiling\" arm.\n//!\n//! Request shapes are deliberately clean — model + max_tokens + messages only,\n//! no temperature / top_p / budget_tokens — so they are safe on Opus 4.8 (which\n//! rejects those) and on any OpenAI-compatible endpoint. API keys come from the\n//! environment; they are never logged.\n\nuse async_trait::async_trait;\n\nuse cliniclaw_agents::{LlmCapability, PromptEnvelope};\nuse cliniclaw_agents::AgentError;\n\n/// Strip a leading ```json / ``` fence if a model wrapped its JSON.\nfn strip_fences(text: &str) -> String {\n let t = text.trim();\n if let Some(rest) = t.strip_prefix(\"```json\").or_else(|| t.strip_prefix(\"```\")) {\n if let Some(inner) = rest.strip_suffix(\"```\") {\n return inner.trim().to_string();\n }\n }\n t.to_string()\n}\n\n// ── Claude (Anthropic Messages API) ──────────────────────────────────────────\n\n#[derive(Debug, Clone)]\npub struct ClaudeRemote {\n client: reqwest::Client,\n api_key: String,\n model: String,\n}\n\nimpl ClaudeRemote {\n pub fn new(api_key: String, model: impl Into<String>) -> Self {\n let client = reqwest::Client::builder()\n .timeout(std::time::Duration::from_secs(120))\n .build()\n .expect(\"build reqwest client\");\n Self { client, api_key, model: model.into() }\n }\n}\n\n#[async_trait]\nimpl LlmCapability for ClaudeRemote {\n async fn call(&self, prompt: &PromptEnvelope) -> Result<String, AgentError> {\n let body = serde_json::json!({\n \"model\": self.model,\n \"max_tokens\": 2048,\n \"system\": prompt.system(),\n \"messages\": [{ \"role\": \"user\", \"content\": prompt.user() }],\n });\n let resp = self.client\n .post(\"https://api.anthropic.com/v1/messages\")\n .header(\"x-api-key\", &self.api_key)\n .header(\"anthropic-version\", \"2023-06-01\")\n .header(\"content-type\", \"application/json\")\n .json(&body)\n .send()\n .await\n .map_err(|e| AgentError::ClaudeApi(format!(\"request failed: {e}\")))?;\n if !resp.status().is_success() {\n let status = resp.status().as_u16();\n return Err(AgentError::ClaudeApi(format!(\"Anthropic HTTP {status}\")));\n }\n let v: serde_json::Value = resp.json().await\n .map_err(|e| AgentError::ClaudeApi(format!(\"parse failed: {e}\")))?;\n // First text block in content[].\n let text = v.get(\"content\")\n .and_then(|c| c.as_array())\n .and_then(|arr| arr.iter().find(|b| b.get(\"type\").and_then(|t| t.as_str()) == Some(\"text\")))\n .and_then(|b| b.get(\"text\"))\n .and_then(|t| t.as_str())\n .unwrap_or(\"\");\n if text.is_empty() {\n return Err(AgentError::ClaudeApi(\"empty Anthropic response\".into()));\n }\n Ok(strip_fences(text))\n }\n}\n\n// ── DeepSeek (OpenAI-compatible chat completions) ────────────────────────────\n\n#[derive(Debug, Clone)]\npub struct DeepSeekRemote {\n client: reqwest::Client,\n api_key: String,\n model: String,\n}\n\nimpl DeepSeekRemote {\n pub fn new(api_key: String, model: impl Into<String>) -> Self {\n let client = reqwest::Client::builder()\n .timeout(std::time::Duration::from_secs(180))\n .build()\n .expect(\"build reqwest client\");\n Self { client, api_key, model: model.into() }\n }\n}\n\n#[async_trait]\nimpl LlmCapability for DeepSeekRemote {\n async fn call(&self, prompt: &PromptEnvelope) -> Result<String, AgentError> {\n let body = serde_json::json!({\n \"model\": self.model,\n \"messages\": [\n { \"role\": \"system\", \"content\": prompt.system() },\n { \"role\": \"user\", \"content\": prompt.user() },\n ],\n \"stream\": false,\n });\n let resp = self.client\n .post(\"https://api.deepseek.com/chat/completions\")\n .header(\"authorization\", format!(\"Bearer {}\", self.api_key))\n .header(\"content-type\", \"application/json\")\n .json(&body)\n .send()\n .await\n .map_err(|e| AgentError::ClaudeApi(format!(\"request failed: {e}\")))?;\n if !resp.status().is_success() {\n let status = resp.status().as_u16();\n return Err(AgentError::ClaudeApi(format!(\"DeepSeek HTTP {status}\")));\n }\n let v: serde_json::Value = resp.json().await\n .map_err(|e| AgentError::ClaudeApi(format!(\"parse failed: {e}\")))?;\n let text = v.get(\"choices\")\n .and_then(|c| c.as_array())\n .and_then(|arr| arr.first())\n .and_then(|c| c.get(\"message\"))\n .and_then(|m| m.get(\"content\"))\n .and_then(|t| t.as_str())\n .unwrap_or(\"\");\n if text.is_empty() {\n return Err(AgentError::ClaudeApi(\"empty DeepSeek response\".into()));\n }\n Ok(strip_fences(text))\n }\n}\n"}, "d168306fd8c78974b26630435ab193979d357eee": {"t": 1785761061.9303637, "n": 5082, "h": "8e68b0536925d1df", "c": "//! Minimal remote-LLM adapters (Claude + DeepSeek) implementing `LlmCapability`,\n//! so the long-horizon experiment can use frontier models as a \"ceiling\" arm.\n//!\n//! Request shapes are deliberately clean — model + max_tokens + messages only,\n//! no temperature / top_p / budget_tokens — so they are safe on Opus 4.8 (which\n//! rejects those) and on any OpenAI-compatible endpoint. API keys come from the\n//! environment; they are never logged.\n\nuse async_trait::async_trait;\n\nuse cliniclaw_agents::{LlmCapability, PromptEnvelope};\nuse cliniclaw_agents::AgentError;\n\n/// Strip a leading ```json / ``` fence if a model wrapped its JSON.\nfn strip_fences(text: &str) -> String {\n let t = text.trim();\n if let Some(rest) = t.strip_prefix(\"```json\").or_else(|| t.strip_prefix(\"```\")) {\n if let Some(inner) = rest.strip_suffix(\"```\") {\n return inner.trim().to_string();\n }\n }\n t.to_string()\n}\n\n// ── Claude (Anthropic Messages API) ──────────────────────────────────────────\n\n#[derive(Debug, Clone)]\npub struct ClaudeRemote {\n client: reqwest::Client,\n api_key: String,\n model: String,\n}\n\nimpl ClaudeRemote {\n pub fn new(api_key: String, model: impl Into<String>) -> Self {\n let client = reqwest::Client::builder()\n .timeout(std::time::Duration::from_secs(120))\n .build()\n .expect(\"build reqwest client\");\n Self { client, api_key, model: model.into() }\n }\n}\n\n#[async_trait]\nimpl LlmCapability for ClaudeRemote {\n async fn call(&self, prompt: &PromptEnvelope) -> Result<String, AgentError> {\n let body = serde_json::json!({\n \"model\": self.model,\n \"max_tokens\": 2048,\n \"system\": prompt.system(),\n \"messages\": [{ \"role\": \"user\", \"content\": prompt.user() }],\n });\n let resp = self.client\n .post(\"https://api.anthropic.com/v1/messages\")\n .header(\"x-api-key\", &self.api_key)\n .header(\"anthropic-version\", \"2023-06-01\")\n .header(\"content-type\", \"application/json\")\n .json(&body)\n .send()\n .await\n .map_err(|e| AgentError::ClaudeApi(format!(\"request failed: {e}\")))?;\n if !resp.status().is_success() {\n let status = resp.status().as_u16();\n return Err(AgentError::ClaudeApi(format!(\"Anthropic HTTP {status}\")));\n }\n let v: serde_json::Value = resp.json().await\n .map_err(|e| AgentError::ClaudeApi(format!(\"parse failed: {e}\")))?;\n // First text block in content[].\n let text = v.get(\"content\")\n .and_then(|c| c.as_array())\n .and_then(|arr| arr.iter().find(|b| b.get(\"type\").and_then(|t| t.as_str()) == Some(\"text\")))\n .and_then(|b| b.get(\"text\"))\n .and_then(|t| t.as_str())\n .unwrap_or(\"\");\n if text.is_empty() {\n return Err(AgentError::ClaudeApi(\"empty Anthropic response\".into()));\n }\n Ok(strip_fences(text))\n }\n}\n\n// ── DeepSeek (OpenAI-compatible chat completions) ────────────────────────────\n\n#[derive(Debug, Clone)]\npub struct DeepSeekRemote {\n client: reqwest::Client,\n api_key: String,\n model: String,\n}\n\nimpl DeepSeekRemote {\n pub fn new(api_key: String, model: impl Into<String>) -> Self {\n let client = reqwest::Client::builder()\n .timeout(std::time::Duration::from_secs(180))\n .build()\n .expect(\"build reqwest client\");\n Self { client, api_key, model: model.into() }\n }\n}\n\n#[async_trait]\nimpl LlmCapability for DeepSeekRemote {\n async fn call(&self, prompt: &PromptEnvelope) -> Result<String, AgentError> {\n let body = serde_json::json!({\n \"model\": self.model,\n \"messages\": [\n { \"role\": \"system\", \"content\": prompt.system() },\n { \"role\": \"user\", \"content\": prompt.user() },\n ],\n \"stream\": false,\n });\n let resp = self.client\n .post(\"https://api.deepseek.com/chat/completions\")\n .header(\"authorization\", format!(\"Bearer {}\", self.api_key))\n .header(\"content-type\", \"application/json\")\n .json(&body)\n .send()\n .await\n .map_err(|e| AgentError::ClaudeApi(format!(\"request failed: {e}\")))?;\n if !resp.status().is_success() {\n let status = resp.status().as_u16();\n return Err(AgentError::ClaudeApi(format!(\"DeepSeek HTTP {status}\")));\n }\n let v: serde_json::Value = resp.json().await\n .map_err(|e| AgentError::ClaudeApi(format!(\"parse failed: {e}\")))?;\n let text = v.get(\"choices\")\n .and_then(|c| c.as_array())\n .and_then(|arr| arr.first())\n .and_then(|c| c.get(\"message\"))\n .and_then(|m| m.get(\"content\"))\n .and_then(|t| t.as_str())\n .unwrap_or(\"\");\n if text.is_empty() {\n return Err(AgentError::ClaudeApi(\"empty DeepSeek response\".into()));\n }\n Ok(strip_fences(text))\n }\n}\n"}}}}