diff --git a/INTERACTIVE_MODE_GUIDE.md b/INTERACTIVE_MODE_GUIDE.md index dacdf37..dbdfe38 100644 --- a/INTERACTIVE_MODE_GUIDE.md +++ b/INTERACTIVE_MODE_GUIDE.md @@ -115,7 +115,7 @@ works — that's how you see *what it believes and why*, not just what it's doin | `--cli` | off (web is default) | Use the **terminal** interface instead of the browser | | `--port N` | `7890` | Port for the web UI (auto-retries if taken) | | `--no-browser` | off | Start the web server but don't auto-open the browser | -| `--backend {cli\|anthropic_api\|openrouter}` | from config | Which backend powers the manager's own reasoning | +| `--backend {cli\|anthropic_api\|openrouter\|requesty}` | from config | Which backend powers the manager's own reasoning | **Examples** (`` is the full ID that `submit` printed, e.g. `titanic_survival_prediction_20260606_213145_67d058cf`) diff --git a/config/manager.yaml b/config/manager.yaml index e3010bf..0bfe5ac 100644 --- a/config/manager.yaml +++ b/config/manager.yaml @@ -5,7 +5,7 @@ manager: # LLM backend for manager reasoning - # Options: "cli" (uses claude -p, no extra keys), "anthropic_api", "openrouter" + # Options: "cli" (uses claude -p, no extra keys), "anthropic_api", "openrouter", "requesty" # Can also be set via NEURICO_MANAGER_BACKEND env var llm_backend: cli @@ -13,6 +13,7 @@ manager: # For cli: uses whatever claude version is installed # For anthropic_api: e.g., "claude-sonnet-4-20250514" # For openrouter: e.g., "anthropic/claude-sonnet-4" + # For requesty: e.g., "anthropic/claude-sonnet-4" (OpenAI-compatible, needs REQUESTY_API_KEY) # Can also be set via NEURICO_MANAGER_MODEL env var llm_model: null diff --git a/src/interactive/llm_backend.py b/src/interactive/llm_backend.py index d4ed1de..9645850 100644 --- a/src/interactive/llm_backend.py +++ b/src/interactive/llm_backend.py @@ -2,7 +2,7 @@ LLM Backend Abstraction Provides a unified interface for calling LLMs, whether via CLI (claude -p) -or API (Anthropic SDK / OpenRouter). The backend is configured by the user +or API (Anthropic SDK / OpenRouter / Requesty). The backend is configured by the user in config/manager.yaml or .env. """ @@ -40,7 +40,7 @@ class LLMBackend: def __init__(self, backend: str = "cli", model: Optional[str] = None): """ Args: - backend: "cli", "anthropic_api", or "openrouter" + backend: "cli", "anthropic_api", "openrouter", or "requesty" model: Model name override (None = default for backend) """ self.backend = backend @@ -65,6 +65,8 @@ def send(self, messages: List[Dict[str, Any]], return self._send_anthropic_api(messages, tools) elif self.backend == "openrouter": return self._send_openrouter(messages, tools) + elif self.backend == "requesty": + return self._send_requesty(messages, tools) else: raise ValueError(f"Unknown backend: {self.backend}") @@ -368,6 +370,72 @@ def _send_openrouter(self, messages: List[Dict[str, Any]], return LLMResponse(text=text, tool_calls=tool_calls, raw=data) + def _send_requesty(self, messages: List[Dict[str, Any]], + tools: Optional[List[Dict[str, Any]]] = None) -> LLMResponse: + """Send via Requesty (OpenAI-compatible router). Requires REQUESTY_API_KEY.""" + try: + import httpx + except ImportError: + raise ImportError( + "httpx package required for Requesty backend. " + "Install with: pip install httpx" + ) + + api_key = os.environ.get("REQUESTY_API_KEY") + if not api_key: + raise ValueError("REQUESTY_API_KEY environment variable required for requesty backend") + + model = self.model or "anthropic/claude-sonnet-4" + + payload = { + "model": model, + "messages": [{"role": m["role"], "content": m["content"]} for m in messages], + "max_tokens": 4096, + } + + if tools: + payload["tools"] = [{ + "type": "function", + "function": { + "name": t["name"], + "description": t.get("description", ""), + "parameters": t.get("parameters", {}) + } + } for t in tools] + + response = httpx.post( + "https://router.requesty.ai/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + json=payload, + timeout=120 + ) + response.raise_for_status() + data = response.json() + + # Parse OpenAI-compatible response + choice = data["choices"][0]["message"] + text = choice.get("content", "") or "" + tool_calls = [] + + for tc in choice.get("tool_calls", []): + func = tc.get("function", {}) + args = func.get("arguments", "{}") + if isinstance(args, str): + try: + args = json.loads(args) + except json.JSONDecodeError: + args = {"raw": args} + tool_calls.append(ToolCall( + id=tc.get("id", ""), + name=func.get("name", ""), + arguments=args + )) + + return LLMResponse(text=text, tool_calls=tool_calls, raw=data) + def create_backend(config: Dict[str, Any]) -> LLMBackend: """ diff --git a/src/interactive/manager.py b/src/interactive/manager.py index 3fb481f..0934374 100644 --- a/src/interactive/manager.py +++ b/src/interactive/manager.py @@ -501,7 +501,7 @@ def main(): parser.add_argument( "--backend", default=None, - choices=["cli", "anthropic_api", "openrouter"], + choices=["cli", "anthropic_api", "openrouter", "requesty"], help="LLM backend for manager reasoning (default: from config)" ) parser.add_argument(