diff --git a/core/llm/autodetect.ts b/core/llm/autodetect.ts index d401ad5a0bd..0f1c4600fb9 100644 --- a/core/llm/autodetect.ts +++ b/core/llm/autodetect.ts @@ -65,6 +65,7 @@ const PROVIDER_HANDLES_TEMPLATING: string[] = [ "nebius", "relace", "openrouter", + "clawrouter", "deepseek", "xAI", "minimax", @@ -123,6 +124,7 @@ const PROVIDER_SUPPORTS_IMAGES: string[] = [ "sagemaker", "continue-proxy", "openrouter", + "clawrouter", "venice", "sambanova", "vertexai", diff --git a/core/llm/llms/ClawRouter.ts b/core/llm/llms/ClawRouter.ts new file mode 100644 index 00000000000..2cdb9a8cecd --- /dev/null +++ b/core/llm/llms/ClawRouter.ts @@ -0,0 +1,53 @@ +import { LLMOptions } from "../../index.js"; +import { osModelsEditPrompt } from "../templates/edit.js"; + +import OpenAI from "./OpenAI.js"; + +// Get Continue version from package.json at build time +const CONTINUE_VERSION = process.env.npm_package_version || "unknown"; + +/** + * ClawRouter LLM Provider + * + * ClawRouter is an open-source LLM router that automatically selects the + * cheapest capable model for each request based on prompt complexity, + * providing 78-96% cost savings on blended inference costs. + * + * Features: + * - 15-dimension prompt complexity scoring + * - Automatic model selection (cheap → capable based on task) + * - OpenAI-compatible API at localhost:1337 + * - Support for multiple routing tiers (auto, free, eco) + * + * @see https://github.com/BlockRunAI/ClawRouter + */ +class ClawRouter extends OpenAI { + static providerName = "clawrouter"; + + // ClawRouter can route to models that support reasoning fields + protected supportsReasoningField = true; + protected supportsReasoningDetailsField = true; + + static defaultOptions: Partial = { + apiBase: "http://localhost:1337/v1/", + model: "blockrun/auto", + promptTemplates: { + edit: osModelsEditPrompt, + }, + useLegacyCompletionsEndpoint: false, + }; + + /** + * Override headers to include Continue-specific User-Agent + * This helps ClawRouter track integration usage and optimize accordingly + */ + protected _getHeaders() { + return { + ...super._getHeaders(), + "User-Agent": `Continue/${CONTINUE_VERSION}`, + "X-Continue-Provider": "clawrouter", + }; + } +} + +export default ClawRouter; diff --git a/core/llm/llms/ClawRouter.vitest.ts b/core/llm/llms/ClawRouter.vitest.ts new file mode 100644 index 00000000000..fa3af73f14f --- /dev/null +++ b/core/llm/llms/ClawRouter.vitest.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; + +import ClawRouter from "./ClawRouter"; + +describe("ClawRouter", () => { + it("should have correct provider name", () => { + expect(ClawRouter.providerName).toBe("clawrouter"); + }); + + it("should have correct default options", () => { + expect(ClawRouter.defaultOptions.apiBase).toBe("http://localhost:1337/v1/"); + expect(ClawRouter.defaultOptions.model).toBe("blockrun/auto"); + expect(ClawRouter.defaultOptions.useLegacyCompletionsEndpoint).toBe(false); + }); + + it("should support reasoning fields", () => { + const clawRouter = new ClawRouter({ + model: "blockrun/auto", + }); + + // ClawRouter routes to models that may support reasoning + expect(clawRouter["supportsReasoningField"]).toBe(true); + expect(clawRouter["supportsReasoningDetailsField"]).toBe(true); + }); + + it("should include Continue User-Agent header", () => { + const clawRouter = new ClawRouter({ + model: "blockrun/auto", + }); + + const headers = clawRouter["_getHeaders"](); + + expect(headers["User-Agent"]).toMatch(/^Continue\//); + expect(headers["X-Continue-Provider"]).toBe("clawrouter"); + }); + + it("should accept all routing profiles", () => { + const profiles = [ + "blockrun/auto", + "blockrun/eco", + "blockrun/premium", + "blockrun/free", + ]; + + for (const profile of profiles) { + const clawRouter = new ClawRouter({ model: profile }); + expect(clawRouter.model).toBe(profile); + } + }); +}); diff --git a/core/llm/llms/index.ts b/core/llm/llms/index.ts index 8b48d4c51c0..453b2d90cd8 100644 --- a/core/llm/llms/index.ts +++ b/core/llm/llms/index.ts @@ -50,6 +50,7 @@ import Nvidia from "./Nvidia"; import Ollama from "./Ollama"; import OpenAI from "./OpenAI"; import OpenRouter from "./OpenRouter"; +import ClawRouter from "./ClawRouter"; import OVHcloud from "./OVHcloud"; import { Relace } from "./Relace"; import Replicate from "./Replicate"; @@ -111,6 +112,7 @@ export const LLMClasses = [ Azure, WatsonX, OpenRouter, + ClawRouter, Nvidia, Vllm, SambaNova, diff --git a/core/llm/toolSupport.test.ts b/core/llm/toolSupport.test.ts index 5be68bfa21c..8ad29bf6bb3 100644 --- a/core/llm/toolSupport.test.ts +++ b/core/llm/toolSupport.test.ts @@ -393,6 +393,29 @@ describe("PROVIDER_TOOL_SUPPORT", () => { }); }); + describe("clawrouter", () => { + const supportsFn = PROVIDER_TOOL_SUPPORT["clawrouter"]; + + it("should return true for blockrun routing profiles", () => { + expect(supportsFn("blockrun/auto")).toBe(true); + expect(supportsFn("blockrun/eco")).toBe(true); + expect(supportsFn("blockrun/premium")).toBe(true); + expect(supportsFn("blockrun/free")).toBe(true); + }); + + it("should return true for tool-supporting models", () => { + expect(supportsFn("gpt-4o")).toBe(true); + expect(supportsFn("claude-3-sonnet")).toBe(true); + expect(supportsFn("gemini-pro")).toBe(true); + expect(supportsFn("anthropic/claude-opus-4.6")).toBe(true); + }); + + it("should return false for non-tool-supporting patterns", () => { + expect(supportsFn("random-model")).toBe(false); + expect(supportsFn("")).toBe(false); + }); + }); + describe("edge cases", () => { it("should handle empty model names", () => { expect(PROVIDER_TOOL_SUPPORT["continue-proxy"]("")).toBe(false); diff --git a/core/llm/toolSupport.ts b/core/llm/toolSupport.ts index a80fea628c0..597c98818b7 100644 --- a/core/llm/toolSupport.ts +++ b/core/llm/toolSupport.ts @@ -380,6 +380,42 @@ export const PROVIDER_TOOL_SUPPORT: Record boolean> = return false; }, + clawrouter: (model) => { + // ClawRouter routes to various providers, so we check common tool-supporting patterns + const lower = model.toLowerCase(); + + // blockrun/* models are routing aliases - assume tool support + if (lower.startsWith("blockrun/")) { + return true; + } + + // Check for common tool-supporting model patterns + const toolSupportingPatterns = [ + "gpt-4", + "gpt-5", + "o1", + "o3", + "o4", + "claude-3", + "claude-4", + "sonnet", + "opus", + "haiku", + "gemini", + "command-r", + "mistral", + "mixtral", + "llama-3.1", + "llama-3.2", + "llama-3.3", + "llama-4", + "qwen3", + "qwen-2.5", + "deepseek", + ]; + + return toolSupportingPatterns.some((pattern) => lower.includes(pattern)); + }, zAI: (model) => { const lower = model.toLowerCase(); return lower.startsWith("glm-4") || lower.startsWith("glm-5"); diff --git a/docs/customize/model-providers/more/clawrouter.mdx b/docs/customize/model-providers/more/clawrouter.mdx new file mode 100644 index 00000000000..7c91cf0ae5e --- /dev/null +++ b/docs/customize/model-providers/more/clawrouter.mdx @@ -0,0 +1,507 @@ +--- +title: "How to Configure ClawRouter with Continue" +sidebarTitle: "ClawRouter" +--- + + + ClawRouter is an open-source LLM router that automatically selects the cheapest capable model for each request based on prompt complexity, providing 78-96% cost savings. + + + + Get started with [ClawRouter on GitHub](https://github.com/BlockRunAI/ClawRouter) + + +## Installation + +ClawRouter runs locally and provides an OpenAI-compatible API: + +```bash +npx clawrouter +``` + +This starts the router at `http://localhost:1337`. + +You can also install it globally: + +```bash +npm install -g clawrouter +clawrouter +``` + +## Configuration + + + + ```yaml title="config.yaml" + name: My Config + version: 0.0.1 + schema: v1 + + models: + - name: ClawRouter Auto + provider: clawrouter + model: blockrun/auto + apiBase: http://localhost:1337/v1/ + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "ClawRouter Auto", + "provider": "clawrouter", + "model": "blockrun/auto", + "apiBase": "http://localhost:1337/v1/" + } + ] + } + ``` + + + +## Available Models + +ClawRouter provides four routing profiles: + +| Model | Savings | Description | Best For | +|-------|---------|-------------|----------| +| `blockrun/auto` | 74-100% | Balanced routing based on prompt complexity | General use (recommended) | +| `blockrun/eco` | 95-100% | Cheapest possible models | Maximum savings | +| `blockrun/premium` | 0% | Best quality models (Opus, GPT-5.4 Pro) | Mission-critical tasks | +| `blockrun/free` | 100% | Free-tier models only (nvidia/gpt-oss-120b) | Zero cost testing | + +You can also specify any of the 44+ models directly (e.g., `anthropic/claude-sonnet-4.6`, `openai/gpt-5.4`, `xai/grok-4`). + +## How It Works + +ClawRouter uses a 15-dimension prompt complexity scoring system to analyze each request: + +- **Simple requests** (greetings, basic Q&A, simple edits) → routed to cheap models like Claude Haiku or Gemini Flash +- **Medium requests** (code explanations, refactoring) → routed to balanced models like Claude Sonnet or GPT-4o-mini +- **Complex requests** (architecture design, complex debugging) → routed to capable models like Claude Opus or GPT-4o + +This automatic routing provides significant cost savings while maintaining quality for complex tasks. + +### Complexity Dimensions + +The router analyzes prompts across dimensions including: +- Code complexity and language detection +- Reasoning depth required +- Context length and dependencies +- Domain expertise needed +- Output format requirements + +## Model Capabilities + +ClawRouter supports function calling and tool use through its underlying model providers. Capabilities are automatically inherited from the routed model. + + + + ```yaml title="config.yaml" + models: + - name: ClawRouter Auto + provider: clawrouter + model: blockrun/auto + apiBase: http://localhost:1337/v1/ + capabilities: + - tool_use + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "ClawRouter Auto", + "provider": "clawrouter", + "model": "blockrun/auto", + "apiBase": "http://localhost:1337/v1/", + "capabilities": { + "tools": true + } + } + ] + } + ``` + + + +## Switching Between Routing Profiles + +Add multiple ClawRouter profiles to your config and switch via Continue's model picker: + + + + ```yaml title="config.yaml" + models: + # Default: automatic routing + - name: ClawRouter Auto + provider: clawrouter + model: blockrun/auto + apiBase: http://localhost:1337/v1/ + + # Maximum savings + - name: ClawRouter Eco + provider: clawrouter + model: blockrun/eco + apiBase: http://localhost:1337/v1/ + + # Best quality + - name: ClawRouter Premium + provider: clawrouter + model: blockrun/premium + apiBase: http://localhost:1337/v1/ + + # Zero cost + - name: ClawRouter Free + provider: clawrouter + model: blockrun/free + apiBase: http://localhost:1337/v1/ + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "ClawRouter Auto", + "provider": "clawrouter", + "model": "blockrun/auto", + "apiBase": "http://localhost:1337/v1/" + }, + { + "title": "ClawRouter Eco", + "provider": "clawrouter", + "model": "blockrun/eco", + "apiBase": "http://localhost:1337/v1/" + }, + { + "title": "ClawRouter Premium", + "provider": "clawrouter", + "model": "blockrun/premium", + "apiBase": "http://localhost:1337/v1/" + }, + { + "title": "ClawRouter Free", + "provider": "clawrouter", + "model": "blockrun/free", + "apiBase": "http://localhost:1337/v1/" + } + ] + } + ``` + + + +Use the **model picker dropdown** in Continue's chat panel to switch between profiles. Each profile routes to different model tiers based on cost vs. quality trade-offs. + + + **Quick switch via CLI:** In the Continue chat, type `/model` followed by the profile name (e.g., `/model ClawRouter Eco`). + + +## Custom API Base + +If you're running ClawRouter on a different port or host: + + + + ```yaml title="config.yaml" + models: + - name: ClawRouter + provider: clawrouter + model: blockrun/auto + apiBase: http://your-server:8080/v1/ + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "ClawRouter", + "provider": "clawrouter", + "model": "blockrun/auto", + "apiBase": "http://your-server:8080/v1/" + } + ] + } + ``` + + + +## Using Multiple Roles + +You can configure ClawRouter for different Continue roles: + + + + ```yaml title="config.yaml" + models: + - name: ClawRouter Auto + provider: clawrouter + model: blockrun/auto + apiBase: http://localhost:1337/v1/ + roles: + - chat + - edit + - apply + + - name: ClawRouter Eco + provider: clawrouter + model: blockrun/eco + apiBase: http://localhost:1337/v1/ + roles: + - autocomplete + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "ClawRouter Auto", + "provider": "clawrouter", + "model": "blockrun/auto", + "apiBase": "http://localhost:1337/v1/", + "roles": ["chat", "edit", "apply"] + } + ], + "tabAutocompleteModel": { + "title": "ClawRouter Eco", + "provider": "clawrouter", + "model": "blockrun/eco", + "apiBase": "http://localhost:1337/v1/" + } + } + ``` + + + +## API Keys + +ClawRouter manages upstream provider API keys internally. You configure them in ClawRouter, not in Continue. + +For self-hosted setups with custom authentication: + + + + ```yaml title="config.yaml" + models: + - name: ClawRouter + provider: clawrouter + model: blockrun/auto + apiBase: http://localhost:1337/v1/ + apiKey: ${{ secrets.CLAWROUTER_API_KEY }} + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "ClawRouter", + "provider": "clawrouter", + "model": "blockrun/auto", + "apiBase": "http://localhost:1337/v1/", + "apiKey": "" + } + ] + } + ``` + + + +## Wallet & Payment Setup + +ClawRouter supports crypto-native payments via the x402 protocol. On first run, ClawRouter automatically generates a wallet: + +```bash +npx clawrouter +# Wallet created: 5g3cB6... +# Fund your wallet to access premium models +``` + +### Payment Options + +| Tier | Description | Payment Required | +|------|-------------|------------------| +| `blockrun/free` | Free-tier models only | No | +| `blockrun/eco` | Economy models | Yes (low cost) | +| `blockrun/auto` | Automatic routing | Yes (pay-per-use) | + +### Funding Your Wallet + +ClawRouter supports both Solana and EVM wallets: + +```bash +# Check wallet balance +clawrouter wallet + +# View wallet address +clawrouter wallet address +``` + +Fund your wallet with USDC on Solana or Base for the lowest fees. ClawRouter uses the x402 Payment Required protocol for seamless micropayments. + + + Start with `blockrun/free` tier to test without payment, then upgrade to `blockrun/auto` for full model access. + + +### Spend Controls + +Set daily/monthly spending limits: + +```bash +# Set daily limit to $5 +clawrouter config set spendLimit.daily 5 + +# Set monthly limit to $50 +clawrouter config set spendLimit.monthly 50 +``` + +## Error Handling + +ClawRouter handles common LLM errors automatically at the router level: + +### Automatic Error Recovery + +| Error | Continue's Default | ClawRouter's Handling | +|-------|-------------------|----------------------| +| **429 Rate Limit** | Retry same provider with backoff | Route to different provider entirely | +| **402 Payment Required** | Fail immediately | x402 auto-payment from wallet | +| **500+ Server Error** | Retry same provider | Fallback to next model in tier | +| **Timeout** | Retry same provider | Route to faster model | + +### Response Headers + +ClawRouter adds diagnostic headers to every response: + +``` +x-clawrouter-model: anthropic/claude-sonnet-4.6 +x-clawrouter-tier: MEDIUM +x-clawrouter-cost: 0.0045 +x-clawrouter-fallback: false +``` + +When a fallback occurs: + +``` +x-clawrouter-model: openai/gpt-4o-mini +x-clawrouter-fallback: true +x-clawrouter-original-model: anthropic/claude-sonnet-4.6 +x-clawrouter-fallback-reason: 429_rate_limit +``` + +## Troubleshooting + +### Connection Refused + +If you see connection errors, make sure ClawRouter is running: + +```bash +# Check if ClawRouter is running +curl http://localhost:1337/v1/models + +# Start ClawRouter if needed +npx clawrouter +``` + +### Model Not Found + +If a specific model isn't available, check that ClawRouter has the required provider API keys configured. Run `clawrouter --help` for configuration options. + +### Slow Responses + +ClawRouter adds minimal latency (~10-50ms) for routing decisions. If responses are slow, the issue is likely with the upstream provider. Try a different model tier (`blockrun/eco` vs `blockrun/auto`). + +### AI-Powered Diagnostics + +Run the doctor command for AI-analyzed troubleshooting: + +```bash +# Basic diagnostics with Claude Sonnet (~$0.003) +npx clawrouter doctor + +# Complex issues with Claude Opus (~$0.01) +npx clawrouter doctor opus + +# Ask a specific question +npx clawrouter doctor "why are my requests failing?" +``` + +The doctor collects system info, wallet status, network connectivity, and sends to Claude for analysis. + +## Cost Monitoring + +ClawRouter provides cost tracking via response headers: + +- `x-clawrouter-cost` — Cost of the request +- `x-clawrouter-model` — Model that handled the request +- `x-clawrouter-complexity` — Computed complexity score + +You can view aggregated costs with: + +```bash +curl http://localhost:1337/stats +``` + +## Dual-Chain Wallet Support + +ClawRouter supports payments on two chains from a single wallet: + +| Chain | Token | Best For | +|-------|-------|----------| +| **Base (EVM)** | USDC | Lower fees, Coinbase integration | +| **Solana** | USDC | Fastest settlement | + +Switch chains via CLI: + +```bash +# Switch to Solana payments +clawrouter wallet solana + +# Switch to Base (EVM) payments +clawrouter wallet base + +# Check balances on both chains +clawrouter wallet +``` + +Both wallets are derived from the same BIP-39 mnemonic generated on first run. + +## Model Exclusion + +Block specific models from routing: + +```bash +# Block a model +clawrouter exclude add nvidia/gpt-oss-120b + +# Aliases work +clawrouter exclude add grok-4 + +# Show exclusions +clawrouter exclude + +# Remove exclusion +clawrouter exclude remove grok-4 +``` + +Useful when a model doesn't follow instructions well or you want to control costs. + +## Comparison with OpenRouter + +| Feature | ClawRouter | OpenRouter | +|---------|------------|------------| +| Hosting | Self-hosted (local) | Cloud-hosted | +| Automatic routing | ✅ Complexity-based | ❌ Manual selection | +| Cost optimization | ✅ 78-96% savings | ❌ Pay per model | +| Privacy | ✅ Data stays local | ⚠️ Data sent to cloud | +| Authentication | Wallet signature | API key | +| Payment | USDC (Solana/Base) | Credit card | +| Setup | `npx clawrouter` | Account signup | + + + ClawRouter can be used alongside OpenRouter — route complex tasks through ClawRouter while using OpenRouter for specific model access. + diff --git a/docs/customize/model-providers/overview.mdx b/docs/customize/model-providers/overview.mdx index fcd99be669a..7ba030dcb4d 100644 --- a/docs/customize/model-providers/overview.mdx +++ b/docs/customize/model-providers/overview.mdx @@ -34,6 +34,7 @@ Beyond the top-level providers, Continue supports many other options: | [Together AI](/customize/model-providers/more/together) | Platform for running a variety of open models | | [DeepInfra](/customize/model-providers/more/deepinfra) | Hosting for various open source models | | [OpenRouter](/customize/model-providers/top-level/openrouter) | Gateway to multiple model providers | +| [ClawRouter](/customize/model-providers/more/clawrouter) | Open-source LLM router with automatic cost-optimized model selection | | [Tetrate Agent Router Service](/customize/model-providers/top-level/tetrate_agent_router_service) | Gateway with intelligent routing across multiple model providers | | [Cohere](/customize/model-providers/more/cohere) | Models specialized for semantic search and text generation | | [NVIDIA](/customize/model-providers/more/nvidia) | GPU-accelerated model hosting | diff --git a/docs/docs.json b/docs/docs.json index 13f482ea56b..8556401ffc6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -169,6 +169,7 @@ "group": "More Providers", "pages": [ "customize/model-providers/more/asksage", + "customize/model-providers/more/clawrouter", "customize/model-providers/more/deepseek", "customize/model-providers/more/deepinfra", "customize/model-providers/more/groq", diff --git a/extensions/vscode/config_schema.json b/extensions/vscode/config_schema.json index 251bc5c2d49..32fe61cb424 100644 --- a/extensions/vscode/config_schema.json +++ b/extensions/vscode/config_schema.json @@ -216,6 +216,7 @@ "msty", "watsonx", "openrouter", + "clawrouter", "sambanova", "nvidia", "vllm", @@ -268,6 +269,7 @@ "### Msty\nMsty is the simplest way to get started with online or local LLMs on all desktop platforms - Windows, Mac, and Linux. No fussing around, one-click and you are up and running. To get started, follow these steps:\n1. Download from [Msty.app](https://msty.app/), open the application, and click 'Setup Local AI'.\n2. Go to the Local AI Module page and download a model of your choice.\n3. Once the model has finished downloading, you can start asking questions through Continue.\n> [Reference](https://continue.dev/docs/reference/Model%20Providers/Msty)", "### IBM watsonx\nwatsonx, developed by IBM, offers a variety of pre-trained AI foundation models that can be used for natural language processing (NLP), computer vision, and speech recognition tasks.", "### OpenRouter\nOpenRouter offers a single API to access almost any language model. To get started, obtain an API key from [their console](https://openrouter.ai/settings/keys).", + "### ClawRouter\nClawRouter is an open-source LLM router that automatically selects the cheapest capable model for each request based on prompt complexity, providing 78-96% cost savings. To get started, run `npx clawrouter` to start the router at localhost:1337. A wallet is auto-generated on first run - fund it with USDC (Solana/Base) to access premium models, or use `blockrun/free` tier without payment.\n> [Reference](https://github.com/BlockRunAI/ClawRouter)", "### SambaNova\n SambaNova provides fast inference of open-source language models with zero data retention. To get started, obtain an API key in [SambaNova Cloud](https://cloud.sambanova.ai/apis?utm_source=continue&utm_medium=external&utm_campaign=cloud_signup ).", "### NVIDIA NIMs\nNVIDIA offers a single API to access almost any language model. To find out more, visit the [LLM APIs Documentation](https://docs.api.nvidia.com/nim/reference/llm-apis).\nFor information specific to getting a key, please check out the [docs here](https://docs.nvidia.com/nim/large-language-models/latest/getting-started.html#option-1-from-api-catalog)", "### vLLM\nvLLM is a highly performant way of hosting LLMs for a team. To get started, follow their [quickstart](https://docs.vllm.ai/en/latest/getting_started/quickstart.html) to set up your server.", diff --git a/gui/public/logos/clawrouter.png b/gui/public/logos/clawrouter.png new file mode 100644 index 00000000000..b56a5cafbc5 Binary files /dev/null and b/gui/public/logos/clawrouter.png differ diff --git a/gui/src/pages/AddNewModel/configs/models.ts b/gui/src/pages/AddNewModel/configs/models.ts index 92d578daafd..0c38bffa3ac 100644 --- a/gui/src/pages/AddNewModel/configs/models.ts +++ b/gui/src/pages/AddNewModel/configs/models.ts @@ -2679,6 +2679,61 @@ export const models: { [key: string]: ModelPackage } = { providerOptions: ["minimax"], isOpenSource: false, }, + + // ClawRouter Models + clawrouterAuto: { + title: "ClawRouter Auto", + description: + "Automatic model selection - routes to the cheapest capable model based on prompt complexity (78-96% cost savings)", + params: { + title: "ClawRouter Auto", + model: "blockrun/auto", + contextLength: 128_000, + }, + icon: "clawrouter.png", + providerOptions: ["clawrouter"], + isOpenSource: true, + }, + clawrouterFree: { + title: "ClawRouter Free", + description: + "Free tier model routing - automatically selects from available free models", + params: { + title: "ClawRouter Free", + model: "blockrun/free", + contextLength: 32_000, + }, + icon: "clawrouter.png", + providerOptions: ["clawrouter"], + isOpenSource: true, + }, + clawrouterEco: { + title: "ClawRouter Eco", + description: + "Economy tier model routing - balances cost and capability for everyday tasks", + params: { + title: "ClawRouter Eco", + model: "blockrun/eco", + contextLength: 64_000, + }, + icon: "clawrouter.png", + providerOptions: ["clawrouter"], + isOpenSource: true, + }, + clawrouterPremium: { + title: "ClawRouter Premium", + description: + "Premium tier - routes to best quality models (Claude Opus, GPT-5.4 Pro) for mission-critical tasks", + params: { + title: "ClawRouter Premium", + model: "blockrun/premium", + contextLength: 200_000, + }, + icon: "clawrouter.png", + providerOptions: ["clawrouter"], + isOpenSource: true, + }, + AUTODETECT: { title: "Autodetect", description: diff --git a/gui/src/pages/AddNewModel/configs/providers.ts b/gui/src/pages/AddNewModel/configs/providers.ts index c4c8a9a57da..94aed8edf96 100644 --- a/gui/src/pages/AddNewModel/configs/providers.ts +++ b/gui/src/pages/AddNewModel/configs/providers.ts @@ -1312,6 +1312,50 @@ To get started, [register](https://dataplatform.cloud.ibm.com/registration/stepo ], apiKeyUrl: "https://api.router.tetrate.ai/", }, + clawrouter: { + title: "ClawRouter", + provider: "clawrouter", + refPage: "clawrouter", + description: + "Open-source LLM router that automatically selects the cheapest capable model for each request", + longDescription: `[ClawRouter](https://github.com/BlockRunAI/ClawRouter) is an open-source LLM router that automatically selects the cheapest capable model for each request based on prompt complexity. It provides 78-96% cost savings on blended inference costs. + +To get started: +1. Install ClawRouter: \`npx clawrouter\` +2. The router runs locally at \`http://localhost:1337\` +3. A wallet is auto-generated on first run +4. Select a model preset below + +**Payment Options:** +- \`blockrun/free\` — No payment required (free-tier models) +- \`blockrun/eco\` — Economy tier (fund wallet with USDC) +- \`blockrun/auto\` — Full routing (fund wallet with USDC) + +Fund your wallet with USDC on Solana or Base. ClawRouter uses x402 micropayments for seamless pay-per-use.`, + icon: "clawrouter.png", + tags: [ModelProviderTags.Local, ModelProviderTags.OpenSource], + packages: [ + models.clawrouterAuto, + models.clawrouterFree, + models.clawrouterEco, + models.clawrouterPremium, + { + ...models.AUTODETECT, + params: { + ...models.AUTODETECT.params, + title: "ClawRouter", + }, + }, + ], + collectInputFor: [ + { + ...apiBaseInput, + defaultValue: "http://localhost:1337/v1/", + }, + ...completionParamsInputsConfigs, + ], + downloadUrl: "https://github.com/BlockRunAI/ClawRouter", + }, nous: { title: "Nous Research", provider: "nous", diff --git a/gui/src/pages/gui/OutOfCreditsDialog.test.tsx b/gui/src/pages/gui/OutOfCreditsDialog.test.tsx new file mode 100644 index 00000000000..ffa2544cc71 --- /dev/null +++ b/gui/src/pages/gui/OutOfCreditsDialog.test.tsx @@ -0,0 +1,78 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { + IdeMessengerContext, + type IIdeMessenger, +} from "../../context/IdeMessenger"; +import { OutOfCreditsDialog } from "./OutOfCreditsDialog"; + +function createMockMessenger(): IIdeMessenger { + return { + post: vi.fn(), + request: vi.fn(), + respond: vi.fn(), + streamRequest: vi.fn(), + ide: { + openUrl: vi.fn(), + } as any, + } as any; +} + +function renderDialog(messenger = createMockMessenger()) { + render( + + + , + ); + return { messenger }; +} + +describe("OutOfCreditsDialog", () => { + it("renders the no-credits message", () => { + renderDialog(); + expect( + screen.getByText( + "You have no credits remaining on your Continue account", + ), + ).toBeInTheDocument(); + }); + + it("renders Purchase Credits button", () => { + renderDialog(); + expect(screen.getByText("Purchase Credits")).toBeInTheDocument(); + }); + + it("renders Add API key secret button", () => { + renderDialog(); + expect(screen.getByText("Add API key secret")).toBeInTheDocument(); + }); + + it("explains how to switch to the direct anthropic provider", () => { + renderDialog(); + expect(screen.getByText(/your own API key/i)).toBeInTheDocument(); + // The word "anthropic" appears as inside the text + expect(screen.getByText("anthropic")).toBeInTheDocument(); + }); + + it("calls controlPlane/openUrl with billing path when Purchase Credits is clicked", () => { + const { messenger } = renderDialog(); + fireEvent.click(screen.getByText("Purchase Credits")); + expect(messenger.post).toHaveBeenCalledWith("controlPlane/openUrl", { + path: "/settings/billing", + }); + }); + + it("calls openUrl with Anthropic secrets URL when Add API key secret is clicked", () => { + const { messenger } = renderDialog(); + fireEvent.click(screen.getByText("Add API key secret")); + expect(messenger.post).toHaveBeenCalledWith( + "openUrl", + expect.stringContaining("ANTHROPIC_API_KEY"), + ); + }); + + it("does NOT render GitHub report buttons", () => { + renderDialog(); + expect(screen.queryByText(/open github issue/i)).not.toBeInTheDocument(); + }); +}); diff --git a/gui/src/pages/gui/OutOfCreditsDialog.tsx b/gui/src/pages/gui/OutOfCreditsDialog.tsx index 5d6a58e8b5c..89b0a074b3d 100644 --- a/gui/src/pages/gui/OutOfCreditsDialog.tsx +++ b/gui/src/pages/gui/OutOfCreditsDialog.tsx @@ -1,8 +1,11 @@ -import { CreditCardIcon } from "@heroicons/react/24/outline"; +import { CreditCardIcon, KeyIcon } from "@heroicons/react/24/outline"; import { useContext } from "react"; -import { SecondaryButton } from "../../components"; +import { GhostButton, SecondaryButton } from "../../components"; import { IdeMessengerContext } from "../../context/IdeMessenger"; +const ANTHROPIC_SECRET_URL = + "https://hub.continue.dev/settings/secrets?secretName=ANTHROPIC_API_KEY"; + export function OutOfCreditsDialog() { const ideMessenger = useContext(IdeMessengerContext); @@ -31,6 +34,25 @@ export function OutOfCreditsDialog() { + +
+ + Alternatively, use your own API key by switching to the{" "} + anthropic provider in your config, then add your key as a + secret: + +
+ { + ideMessenger.post("openUrl", ANTHROPIC_SECRET_URL); + }} + > + + Add API key secret + +
+
); } diff --git a/gui/src/pages/gui/StreamError.test.tsx b/gui/src/pages/gui/StreamError.test.tsx new file mode 100644 index 00000000000..13689b9fa4a --- /dev/null +++ b/gui/src/pages/gui/StreamError.test.tsx @@ -0,0 +1,212 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { + IdeMessengerContext, + type IIdeMessenger, +} from "../../context/IdeMessenger"; +import StreamErrorDialog from "./StreamError"; + +// Avoid pulling in core's native deps (uri-js, uuid, etc.) +vi.mock("../../redux/thunks/streamResponse", () => ({ + streamResponseThunk: vi.fn(), +})); + +vi.mock("../../components/mainInput/Lump/useEditBlock", () => ({ + useEditModel: () => vi.fn(), +})); + +vi.mock("../../components/mainInput/TipTapEditor", () => ({ + useMainEditor: () => ({ mainEditor: null }), + MainEditorProvider: ({ children }: any) => children, +})); + +vi.mock("../../context/Auth", () => ({ + useAuth: () => ({ + session: null, + selectedProfile: null, + refreshProfiles: vi.fn(), + }), + AuthProvider: ({ children }: any) => children, +})); + +vi.mock("../../components/ToggleDiv", () => ({ + default: ({ children, title }: any) => ( +
+ {title} + {children} +
+ ), +})); + +// Mock Redux hooks so we control selectedModel without a full store +const mockDispatch = vi.fn(); +let mockSelectedModel: object | null = null; +let mockSelectedProfile: object | null = null; +let mockHistory: any[] = []; + +vi.mock("../../redux/hooks", () => ({ + useAppDispatch: () => mockDispatch, + useAppSelector: (selector: any) => { + // Return values based on what selectors are asking for + const state = { + config: { + selectedModelByRole: { chat: mockSelectedModel }, + config: { models: [] }, + }, + session: { history: mockHistory, isInEdit: false }, + profiles: { + organizations: [], + selectedOrgId: null, + }, + }; + try { + return selector(state); + } catch { + return undefined; + } + }, +})); + +// Also mock the individual selectors used directly +vi.mock("../../redux/slices/configSlice", () => ({ + selectSelectedChatModel: (state: any) => + state?.config?.selectedModelByRole?.chat ?? null, +})); + +vi.mock("../../redux/slices/profilesSlice", () => ({ + selectSelectedProfile: (state: any) => state?.profiles?.selectedOrgId ?? null, +})); + +vi.mock("../../redux/slices/uiSlice", () => ({ + setDialogMessage: vi.fn(() => ({ type: "ui/setDialogMessage" })), + setShowDialog: vi.fn(() => ({ type: "ui/setShowDialog" })), +})); + +function createMockMessenger(): IIdeMessenger { + return { + post: vi.fn(), + request: vi.fn(), + respond: vi.fn(), + streamRequest: vi.fn(), + ide: { openUrl: vi.fn() } as any, + } as any; +} + +function renderError(error: unknown, selectedModel: object | null = null) { + mockSelectedModel = selectedModel; + const messenger = createMockMessenger(); + render( + + + , + ); + return { messenger }; +} + +describe("StreamErrorDialog", () => { + describe("OutOfCreditsDialog routing", () => { + it('shows OutOfCreditsDialog for "You have no credits remaining on your Continue account"', () => { + renderError( + new Error("You have no credits remaining on your Continue account"), + ); + expect( + screen.getByText( + "You have no credits remaining on your Continue account", + ), + ).toBeInTheDocument(); + expect(screen.getByText("Purchase Credits")).toBeInTheDocument(); + expect(screen.getByText("Add API key secret")).toBeInTheDocument(); + }); + + it('shows OutOfCreditsDialog for legacy "You\'re out of credits!" string', () => { + renderError(new Error("You're out of credits!")); + expect(screen.getByText("Purchase Credits")).toBeInTheDocument(); + }); + + it("generic 402 does NOT show OutOfCreditsDialog — uses customErrorMessage instead", () => { + renderError(new Error("402 Payment Required")); + expect(screen.queryByText("Purchase Credits")).not.toBeInTheDocument(); + expect(screen.getByText(/out of credits/i)).toBeInTheDocument(); + }); + }); + + describe("customErrorMessage display", () => { + it("shows invalid API key message for 'Invalid API Key'", () => { + renderError(new Error("Invalid API Key")); + expect( + screen.getByText(/API key is actually invalid/i), + ).toBeInTheDocument(); + }); + + it("shows invalid API key message for 'Incorrect API key provided'", () => { + renderError( + new Error( + '401 Unauthorized\n\n{"error": {"message": "Incorrect API key provided"}}', + ), + ); + expect( + screen.getByText(/API key is actually invalid/i), + ).toBeInTheDocument(); + }); + + it("includes provider name in 402 customErrorMessage", () => { + renderError(new Error("402 Payment Required"), { + title: "DeepSeek Chat", + underlyingProviderName: "deepseek", + }); + expect(screen.getByText(/deepseek/i)).toBeInTheDocument(); + expect(screen.getByText(/out of credits/i)).toBeInTheDocument(); + }); + + it("shows helpUrl button for OpenAI org verification error", () => { + renderError( + new Error( + "openai organization must be verified to generate reasoning summaries", + ), + ); + expect(screen.getByText("View help documentation")).toBeInTheDocument(); + }); + + it("shows 'Check API key' button when apiKeyUrl available for invalid key", () => { + renderError(new Error("Invalid API Key"), { + title: "GPT-4", + underlyingProviderName: "openai", + }); + expect(screen.getByText("Check API key")).toBeInTheDocument(); + }); + }); + + describe("standard status code errors", () => { + it("shows rate limit message for 429", () => { + renderError(new Error("429 Too Many Requests")); + expect(screen.getByText(/rate limited/i)).toBeInTheDocument(); + }); + + it("shows not-found hints for 404", () => { + renderError(new Error("404 Not Found")); + expect(screen.getByText("Likely causes:")).toBeInTheDocument(); + expect( + screen.getByText("Model/deployment not found"), + ).toBeInTheDocument(); + }); + + it("shows generic error title for unknown errors", () => { + renderError(new Error("Something unexpected happened")); + expect( + screen.getByText("Error handling model response"), + ).toBeInTheDocument(); + }); + + it("shows Resubmit button for generic errors", () => { + renderError(new Error("Something unexpected happened")); + expect(screen.getByText("Resubmit last message")).toBeInTheDocument(); + }); + }); + + describe("error output section", () => { + it("shows 'View error output' toggle when there is an error message", () => { + renderError(new Error("429 Too Many Requests")); + expect(screen.getByText("View error output")).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/openai-adapters/src/apis/AiSdk.ts b/packages/openai-adapters/src/apis/AiSdk.ts index 95ae32a9bfb..c0f68689988 100644 --- a/packages/openai-adapters/src/apis/AiSdk.ts +++ b/packages/openai-adapters/src/apis/AiSdk.ts @@ -41,6 +41,11 @@ const PROVIDER_MAP: Record = { ...options, baseURL: options.baseURL ?? "https://openrouter.ai/api/v1/", }), + clawrouter: (options) => + createOpenAI({ + ...options, + baseURL: options.baseURL ?? "http://localhost:1337/v1/", + }), }; export class AiSdkApi implements BaseLlmApi { diff --git a/packages/openai-adapters/src/apis/ClawRouter.test.ts b/packages/openai-adapters/src/apis/ClawRouter.test.ts new file mode 100644 index 00000000000..b90796103aa --- /dev/null +++ b/packages/openai-adapters/src/apis/ClawRouter.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; + +import { ClawRouterApi } from "./ClawRouter.js"; + +describe("ClawRouterApi", () => { + const baseConfig = { + provider: "clawrouter" as const, + }; + + it("should use default apiBase when not provided", () => { + const api = new ClawRouterApi(baseConfig); + expect(api["config"].apiBase).toBe("http://localhost:1337/v1/"); + }); + + it("should allow custom apiBase", () => { + const api = new ClawRouterApi({ + ...baseConfig, + apiBase: "http://custom:8080/v1/", + }); + expect(api["config"].apiBase).toBe("http://custom:8080/v1/"); + }); + + it("should include Continue headers", () => { + const api = new ClawRouterApi(baseConfig); + const headers = api["getHeaders"](); + + expect(headers["User-Agent"]).toBe("Continue/IDE"); + expect(headers["X-Continue-Provider"]).toBe("clawrouter"); + }); + + it("should include standard OpenAI headers", () => { + const api = new ClawRouterApi({ + ...baseConfig, + apiKey: "test-key", + }); + const headers = api["getHeaders"](); + + expect(headers["Content-Type"]).toBe("application/json"); + expect(headers["Accept"]).toBe("application/json"); + expect(headers["Authorization"]).toBe("Bearer test-key"); + }); +}); diff --git a/packages/openai-adapters/src/apis/ClawRouter.ts b/packages/openai-adapters/src/apis/ClawRouter.ts new file mode 100644 index 00000000000..15f334eb2d3 --- /dev/null +++ b/packages/openai-adapters/src/apis/ClawRouter.ts @@ -0,0 +1,42 @@ +import { OpenAIApi } from "./OpenAI.js"; +import { OpenAIConfig } from "../types.js"; + +export interface ClawRouterConfig extends OpenAIConfig {} + +/** + * ClawRouter API adapter + * + * ClawRouter is an open-source LLM router that automatically selects the + * cheapest capable model for each request based on prompt complexity, + * providing 78-96% cost savings on blended inference costs. + * + * Features: + * - 15-dimension prompt complexity scoring + * - Automatic model selection (cheap → capable based on task) + * - OpenAI-compatible API at localhost:1337 + * - Support for multiple routing tiers (auto, free, eco) + * + * @see https://github.com/BlockRunAI/ClawRouter + */ +export class ClawRouterApi extends OpenAIApi { + constructor(config: ClawRouterConfig) { + super({ + ...config, + apiBase: config.apiBase ?? "http://localhost:1337/v1/", + }); + } + + /** + * Override headers to include Continue-specific User-Agent + * This helps ClawRouter track integration usage and optimize accordingly + */ + protected override getHeaders(): Record { + return { + ...super.getHeaders(), + "User-Agent": "Continue/IDE", + "X-Continue-Provider": "clawrouter", + }; + } +} + +export default ClawRouterApi; diff --git a/packages/openai-adapters/src/index.ts b/packages/openai-adapters/src/index.ts index 43af4e81ba0..467c7a71ae9 100644 --- a/packages/openai-adapters/src/index.ts +++ b/packages/openai-adapters/src/index.ts @@ -18,6 +18,7 @@ import { MockApi } from "./apis/Mock.js"; import { MoonshotApi } from "./apis/Moonshot.js"; import { OpenAIApi } from "./apis/OpenAI.js"; import { OpenRouterApi } from "./apis/OpenRouter.js"; +import { ClawRouterApi } from "./apis/ClawRouter.js"; import { RelaceApi } from "./apis/Relace.js"; import { VertexAIApi } from "./apis/VertexAI.js"; import { WatsonXApi } from "./apis/WatsonX.js"; @@ -179,6 +180,8 @@ export function constructLlmApi(config: LLMConfig): BaseLlmApi | undefined { return openAICompatible("https://api.tensorix.ai/v1/", config); case "openrouter": return new OpenRouterApi(config); + case "clawrouter": + return new ClawRouterApi(config); case "llama.cpp": case "llamafile": return openAICompatible("http://localhost:8000/", config); diff --git a/packages/openai-adapters/src/types.ts b/packages/openai-adapters/src/types.ts index 87dff310421..43da6e69a60 100644 --- a/packages/openai-adapters/src/types.ts +++ b/packages/openai-adapters/src/types.ts @@ -51,6 +51,7 @@ export const OpenAIConfigSchema = BasePlusConfig.extend({ z.literal("kindo"), z.literal("msty"), z.literal("openrouter"), + z.literal("clawrouter"), z.literal("sambanova"), z.literal("text-gen-webui"), z.literal("vllm"),