Skip to content

feat: Remove OpenRouter support, replace litellm with Chutes API#3

Merged
echobt merged 1 commit intomainfrom
remove-openrouter-support
Feb 3, 2026
Merged

feat: Remove OpenRouter support, replace litellm with Chutes API#3
echobt merged 1 commit intomainfrom
remove-openrouter-support

Conversation

@echobt
Copy link
Contributor

@echobt echobt commented Feb 3, 2026

Summary

This PR completely removes all OpenRouter support from the baseagent codebase. This is related to:

Changes

Core Changes

  • src/config/models.py: Removed OPENROUTER from Provider enum, added CHUTES. Updated get_api_key() to use CHUTES_API_KEY and get_base_url() to return Chutes API URL.
  • src/config/defaults.py: Changed default model to deepseek/deepseek-chat and provider to chutes.
  • src/llm/client.py: Complete rewrite to use httpx directly instead of litellm. New LLMClient class makes HTTP calls to Chutes API (OpenAI-compatible format). Kept LiteLLMClient as alias for backward compatibility.

Dependency Changes

  • requirements.txt: Removed litellm>=1.50.0
  • pyproject.toml: Removed litellm>=1.50.0 from dependencies

Documentation Updates

  • README.md: Updated all references from litellm/OpenRouter to Chutes API
  • rules/06-llm-usage-guide.md: Updated with Chutes API examples
  • astuces/08-cost-optimization.md: Removed OpenRouter pricing section
  • rules/02-architecture-patterns.md: Updated project structure comments
  • rules/08-error-handling.md: Updated error handling examples

Other Files

  • agent.py: Removed litellm dependency check, updated imports
  • src/llm/init.py: Updated exports
  • src/core/compaction.py and src/core/loop.py: Updated type hints

Verification

  • All Python files compile successfully
  • All references to openrouter, OpenRouter, OPENROUTER, and litellm (except backward-compatibility alias) have been removed

Summary by CodeRabbit

Documentation

  • Updated LLM configuration guides and usage examples with current best practices
  • Added cost optimization strategies, performance monitoring, and budget management tools

Chores

  • Updated default language model configuration to DeepSeek Chat
  • API key environment variable changed to CHUTES_API_KEY

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
HTTP Client Implementation
src/llm/client.py
Complete rewrite of LLM client from litellm-based to httpx-based implementation. LiteLLMClient renamed to LLMClient with new constructor accepting base_url and api_key. Replaces litellm provider abstraction with direct OpenAI-compatible HTTP calls to Chutes API endpoint. Maintains error handling, cost tracking, and function call parsing.
Configuration & Provider Setup
src/config/defaults.py, src/config/models.py
Updated default model to "deepseek/deepseek-chat" and provider enum from OPENROUTER to CHUTES. Modified API key lookup (OPENROUTER_API_KEY → CHUTES_API_KEY) and base URL mapping to https://api.chutes.ai/v1. Updated caching documentation to reflect model-agnostic behavior.
Type Hints & Imports Across Core Modules
src/llm/__init__.py, src/core/compaction.py, src/core/loop.py, agent.py
Updated type annotations and imports to reference LLMClient instead of LiteLLMClient. Maintained backwards-compatible alias in client.py (LiteLLMClient = LLMClient). Updated all function signatures and TYPE_CHECKING blocks accordingly.
Documentation & Usage Guides
README.md, rules/06-llm-usage-guide.md, rules/02-architecture-patterns.md, rules/08-error-handling.md
Rebranded examples and instructions from litellm to Chutes API. Updated class names, model references, and provider names in code samples. Removed provider-specific caching details (Anthropic cache_control, TTL) in favor of generalized descriptions. Updated comments to reference OpenAI-compatible format rather than litellm-specific patterns.
Cost Optimization Guide
astuces/08-cost-optimization.md
Replaced model-specific pricing breakdown with generic cost ranges. Added comprehensive multi-strategy optimization guide including prompt caching, context management, tool design, batch operations, and early exit patterns. Introduced cost tracking structures (TaskMetrics, MAX_COST_PER_TASK) and monitoring examples.
Dependency Management
pyproject.toml, requirements.txt
Removed litellm>=1.50.0 dependency from both files.
Module Documentation
src/__init__.py, src/tools/registry.py
Minor docstring updates reflecting Chutes API usage via httpx instead of litellm. Removed litellm-specific terminology from tool registry documentation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A hop from litellm to pastures new,
Where Chutes and DeepSeek dance on through,
Client reborn with httpx wings,
Type hints aligned, configuration sings,
Documentation refreshed, the migration's true! 🚀✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main changes: removing OpenRouter support and replacing litellm with the Chutes API, which are the core objectives of this changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch remove-openrouter-support

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Default model conflicts with CHUTES provider.

provider=CHUTES while model defaults 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 | 🔴 Critical

Doc snippet: subprocess.run examples are functionally invalid as written.

Both the "Bad" and "Good" examples will fail at runtime: string commands without shell=True cannot execute (the pipe operator | is not special without shell interpretation). Additionally, stdout returns bytes by default, not the annotated str type. Correct the examples to use list-based arguments with text=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 | 🟠 Major

Close the httpx-backed LLMClient to prevent resource leaks.

LLMClient creates and owns an httpx.Client that must be explicitly closed. Currently, llm is instantiated at lines 143-147 but never closed, leaving the underlying HTTP connection open. Add llm.close() in the finally block (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"]

Comment on lines 176 to 201
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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 2

Repository: 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.py

Repository: PlatformNetwork/baseagent

Length of output: 160


🏁 Script executed:

# Check the exception handling structure more broadly
rg -n "except.*JSONDecodeError" src/llm/client.py

Repository: 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 -20

Repository: 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 -150

Repository: 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
@echobt echobt force-pushed the remove-openrouter-support branch from 70edb5f to 4149f3d Compare February 3, 2026 13:54
@echobt echobt changed the title Remove OpenRouter support - Use only Chutes API feat: Remove OpenRouter support, replace litellm with Chutes API Feb 3, 2026
@echobt echobt merged commit 145afeb into main Feb 3, 2026
1 check passed
echobt added a commit that referenced this pull request Feb 3, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant