feat: Remove OpenRouter support, replace litellm with Chutes API#3
feat: Remove OpenRouter support, replace litellm with Chutes API#3
Conversation
📝 WalkthroughWalkthroughThis change migrates the codebase from the LiteLLM ecosystem to the Chutes API, including renaming the LiteLLMClient class to LLMClient, updating the default model from "openrouter/anthropic/claude-opus-4.5" to "deepseek/deepseek-chat", and replacing OpenRouter configuration with Chutes API endpoints and credentials throughout documentation and source code. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/config/models.py (1)
93-99:⚠️ Potential issue | 🟠 MajorDefault model conflicts with CHUTES provider.
provider=CHUTESwhilemodeldefaults to an Anthropic identifier; this will likely fail under CHUTES by default. Align the model default with the CHUTES-compatible model (or make it provider-specific).Suggested fix
- model: str = Field( - default="anthropic/claude-opus-4-20250514", - description="Model to use" - ) + model: str = Field( + default="deepseek/deepseek-chat", + description="Model to use" + )astuces/08-cost-optimization.md (1)
36-47:⚠️ Potential issue | 🔴 CriticalDoc snippet:
subprocess.runexamples are functionally invalid as written.Both the "Bad" and "Good" examples will fail at runtime: string commands without
shell=Truecannot execute (the pipe operator|is not special without shell interpretation). Additionally,stdoutreturnsbytesby default, not the annotatedstrtype. Correct the examples to use list-based arguments withtext=True, and replace the shell pipe with Python list slicing.Suggested fix
- return subprocess.run(f"find {path}", capture_output=True).stdout + return subprocess.run(["find", path], capture_output=True, text=True).stdout ... - result = subprocess.run( - f"find {path} -maxdepth {max_depth} | head -{limit}", - capture_output=True - ) - return result.stdout + result = subprocess.run( + ["find", path, "-maxdepth", str(max_depth)], + capture_output=True, + text=True, + ) + return "\n".join(result.stdout.splitlines()[:limit])agent.py (1)
143-147:⚠️ Potential issue | 🟠 MajorClose the httpx-backed LLMClient to prevent resource leaks.
LLMClient creates and owns an httpx.Client that must be explicitly closed. Currently,
llmis instantiated at lines 143-147 but never closed, leaving the underlying HTTP connection open. Addllm.close()in thefinallyblock (or switch to the context manager pattern) to ensure proper cleanup.♻️ Proposed fix
finally: elapsed = time.time() - start_time try: stats = llm.get_stats() _log(f"Total tokens: {stats.get('total_tokens', 0)}") _log(f"Total cost: ${stats.get('total_cost', 0):.4f}") _log(f"Requests: {stats.get('request_count', 0)}") except Exception as e: _log(f"Stats error: {e}") + try: + llm.close() + except Exception as e: + _log(f"LLM client close error: {e}") _log(f"Elapsed: {elapsed:.1f}s") _log("Agent finished") _log("=" * 60)Alternatively, use the context manager:
- llm = LLMClient( - model=CONFIG["model"], - temperature=CONFIG.get("temperature"), - max_tokens=CONFIG.get("max_tokens", 16384), - ) + with LLMClient( + model=CONFIG["model"], + temperature=CONFIG.get("temperature"), + max_tokens=CONFIG.get("max_tokens", 16384), + ) as llm:
🤖 Fix all issues with AI agents
In `@src/llm/client.py`:
- Around line 176-201: The code calls response.json() after a 200 response which
can raise json.JSONDecodeError and escape the existing handlers; update the
method (the block using self._client.post("/chat/completions",
self._request_count and response handling) to wrap the successful-response parse
(data = response.json()) in a try/except that catches json.JSONDecodeError (and
ValueError if desired) and raises an LLMError with a clear message and an
appropriate code (e.g., "invalid_response" or "parse_error") so JSON parsing
failures are converted into LLMError consistently with the existing error
mapping; ensure you reference response.json() and raise LLMError instead of
letting the exception propagate.
🧹 Nitpick comments (1)
src/llm/__init__.py (1)
5-5: Optional: sort__all__to satisfy linting.Suggested fix
-__all__ = ["LLMClient", "LiteLLMClient", "LLMResponse", "FunctionCall", "CostLimitExceeded", "LLMError"] +__all__ = ["CostLimitExceeded", "FunctionCall", "LLMClient", "LLMError", "LLMResponse", "LiteLLMClient"]
| try: | ||
| response = self._litellm.completion(**kwargs) | ||
| response = self._client.post("/chat/completions", json=payload) | ||
| self._request_count += 1 | ||
| except Exception as e: | ||
| error_msg = str(e) | ||
| if "authentication" in error_msg.lower() or "api_key" in error_msg.lower(): | ||
| raise LLMError(error_msg, code="authentication_error") | ||
| elif "rate" in error_msg.lower() or "limit" in error_msg.lower(): | ||
| raise LLMError(error_msg, code="rate_limit") | ||
| else: | ||
| raise LLMError(error_msg, code="api_error") | ||
|
|
||
| # Handle HTTP errors | ||
| if response.status_code != 200: | ||
| error_body = response.text | ||
| try: | ||
| error_json = response.json() | ||
| error_msg = error_json.get("error", {}).get("message", error_body) | ||
| except (json.JSONDecodeError, KeyError): | ||
| error_msg = error_body | ||
|
|
||
| # Map status codes to error codes | ||
| if response.status_code == 401: | ||
| raise LLMError(error_msg, code="authentication_error") | ||
| elif response.status_code == 429: | ||
| raise LLMError(error_msg, code="rate_limit") | ||
| elif response.status_code >= 500: | ||
| raise LLMError(error_msg, code="server_error") | ||
| else: | ||
| raise LLMError(f"HTTP {response.status_code}: {error_msg}", code="api_error") | ||
|
|
||
| data = response.json() | ||
|
|
||
| except httpx.TimeoutException as e: |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and read the target file
fd -t f "client.py" src/llm/Repository: PlatformNetwork/baseagent
Length of output: 86
🏁 Script executed:
# Read the relevant lines from the file
cat -n src/llm/client.py | sed -n '170,210p'Repository: PlatformNetwork/baseagent
Length of output: 2099
🏁 Script executed:
# Check for LLMError definition and JSONDecodeError imports
rg -n "class LLMError|import json|from json" src/llm/client.py -A 2 -B 2Repository: PlatformNetwork/baseagent
Length of output: 281
🏁 Script executed:
# Check if there are other places where response.json() is called without proper error handling
rg -n "response\.json()" src/llm/client.pyRepository: PlatformNetwork/baseagent
Length of output: 160
🏁 Script executed:
# Check the exception handling structure more broadly
rg -n "except.*JSONDecodeError" src/llm/client.pyRepository: PlatformNetwork/baseagent
Length of output: 222
🏁 Script executed:
# Verify if there's a top-level handler for JSONDecodeError
cat -n src/llm/client.py | sed -n '199,230p'Repository: PlatformNetwork/baseagent
Length of output: 1581
🏁 Script executed:
# Check if there's a broader try-except block wrapping this method
cat -n src/llm/client.py | sed -n '100,180p' | tail -20Repository: PlatformNetwork/baseagent
Length of output: 950
🏁 Script executed:
# Check the full method signature and any outer exception handling
rg -n "def.*post_message|def.*complete" src/llm/client.py -A 100 | head -150Repository: PlatformNetwork/baseagent
Length of output: 51
🏁 Script executed:
# Verify the complete exception handling structure around line 199-210
cat -n src/llm/client.py | sed -n '176,245p'Repository: PlatformNetwork/baseagent
Length of output: 3472
Add handling for invalid JSON in successful responses.
Line 199's response.json() call can raise JSONDecodeError on a 200 response, which would bypass the current exception handlers. Wrap it in a try-except block to catch and convert to LLMError for consistent error handling.
Suggested fix
- data = response.json()
+ try:
+ data = response.json()
+ except json.JSONDecodeError as e:
+ raise LLMError(f"Invalid JSON response: {e}", code="api_error") from e🧰 Tools
🪛 Ruff (0.14.14)
[warning] 197-197: Avoid specifying long messages outside the exception class
(TRY003)
🤖 Prompt for AI Agents
In `@src/llm/client.py` around lines 176 - 201, The code calls response.json()
after a 200 response which can raise json.JSONDecodeError and escape the
existing handlers; update the method (the block using
self._client.post("/chat/completions", self._request_count and response
handling) to wrap the successful-response parse (data = response.json()) in a
try/except that catches json.JSONDecodeError (and ValueError if desired) and
raises an LLMError with a clear message and an appropriate code (e.g.,
"invalid_response" or "parse_error") so JSON parsing failures are converted into
LLMError consistently with the existing error mapping; ensure you reference
response.json() and raise LLMError instead of letting the exception propagate.
- Remove OPENROUTER from Provider enum, add CHUTES - Update get_api_key() to use CHUTES_API_KEY - Update get_base_url() to use https://api.chutes.ai/v1 - Rewrite LLM client using httpx instead of litellm - Update default model to deepseek/deepseek-chat - Remove litellm from requirements.txt and pyproject.toml - Update all documentation references from OpenRouter/litellm to Chutes API - Keep LiteLLMClient alias for backward compatibility
70edb5f to
4149f3d
Compare
Documentation fixes: - Replace all LiteLLM references with httpx client - Update API base URL from llm.chutes.ai to api.chutes.ai - Update environment variable from CHUTES_API_TOKEN to CHUTES_API_KEY - Update default model from openrouter/... to deepseek/deepseek-chat - Remove all OpenRouter references and fallback documentation Security fixes: - Shell tool: Use allowlist-based environment instead of passing full os.environ to prevent leaking sensitive environment variables (API keys, tokens, etc.) - BaseTool.resolve_path(): Add path containment validation to prevent path traversal attacks outside the working directory - utils/files.resolve_path(): Add optional containment validation These changes ensure documentation accurately reflects the current implementation after PR #3 replaced litellm with direct Chutes API client.
Summary
This PR completely removes all OpenRouter support from the baseagent codebase. This is related to:
Changes
Core Changes
OPENROUTERfrom Provider enum, addedCHUTES. Updatedget_api_key()to useCHUTES_API_KEYandget_base_url()to return Chutes API URL.deepseek/deepseek-chatand provider tochutes.httpxdirectly instead oflitellm. NewLLMClientclass makes HTTP calls to Chutes API (OpenAI-compatible format). KeptLiteLLMClientas alias for backward compatibility.Dependency Changes
litellm>=1.50.0litellm>=1.50.0from dependenciesDocumentation Updates
Other Files
Verification
openrouter,OpenRouter,OPENROUTER, andlitellm(except backward-compatibility alias) have been removedSummary by CodeRabbit
Documentation
Chores