From 823b3255278df60700c9eda0484380c409a959eb Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 23 Mar 2026 23:46:49 +0000 Subject: [PATCH 01/14] feat: add ClawRouter as a provider for cost-optimized model routing 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. Changes: - Add ClawRouterApi class extending OpenAI adapter - Add 'clawrouter' provider to the type system - Add ClawRouter models (auto, free, eco) - Add ClawRouter provider config with UI setup - Add ClawRouter logo - Add documentation (clawrouter.mdx) Closes #10843 --- .../model-providers/top-level/clawrouter.mdx | 137 ++++++++++++++++++ gui/public/logos/clawrouter.png | Bin 0 -> 28841 bytes gui/src/pages/AddNewModel/configs/models.ts | 42 ++++++ .../pages/AddNewModel/configs/providers.ts | 37 +++++ .../openai-adapters/src/apis/ClawRouter.ts | 24 +++ packages/openai-adapters/src/index.ts | 3 + packages/openai-adapters/src/types.ts | 1 + 7 files changed, 244 insertions(+) create mode 100644 docs/customize/model-providers/top-level/clawrouter.mdx create mode 100644 gui/public/logos/clawrouter.png create mode 100644 packages/openai-adapters/src/apis/ClawRouter.ts diff --git a/docs/customize/model-providers/top-level/clawrouter.mdx b/docs/customize/model-providers/top-level/clawrouter.mdx new file mode 100644 index 00000000000..114ff416bd2 --- /dev/null +++ b/docs/customize/model-providers/top-level/clawrouter.mdx @@ -0,0 +1,137 @@ +--- +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. + + +## Installation + +ClawRouter runs locally and provides an OpenAI-compatible API: + +```bash +npx clawrouter +``` + +This starts the router at `http://localhost:1337`. + + + Learn more at the [ClawRouter GitHub repository](https://github.com/BlockRunAI/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 several routing modes: + +| Model | Description | +|-------|-------------| +| `blockrun/auto` | Automatic model selection based on prompt complexity (recommended) | +| `blockrun/free` | Routes to available free-tier models only | +| `blockrun/eco` | Economy tier balancing cost and capability | + +## How It Works + +ClawRouter uses a 15-dimension prompt complexity scoring system to analyze each request: + +- **Simple requests** (greetings, basic Q&A) → routed to cheap models like Claude Haiku or Gemini Flash +- **Complex requests** (code generation, analysis) → routed to capable models like Claude Opus or GPT-4o + +This automatic routing provides significant cost savings while maintaining quality for complex tasks. + +## 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 with API Keys + +If your ClawRouter instance is configured with upstream provider API keys, they are managed by ClawRouter itself. No additional API key configuration is needed 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: + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "ClawRouter", + "provider": "clawrouter", + "model": "blockrun/auto", + "apiBase": "http://localhost:1337/v1/", + "apiKey": "" + } + ] + } + ``` + + diff --git a/gui/public/logos/clawrouter.png b/gui/public/logos/clawrouter.png new file mode 100644 index 0000000000000000000000000000000000000000..51ece03f91e7b54521dbdea73bc7ce78a1740348 GIT binary patch literal 28841 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 Fe*qrbTIm1) literal 0 HcmV?d00001 diff --git a/gui/src/pages/AddNewModel/configs/models.ts b/gui/src/pages/AddNewModel/configs/models.ts index d15db3f3d1d..8cb5e4459bc 100644 --- a/gui/src/pages/AddNewModel/configs/models.ts +++ b/gui/src/pages/AddNewModel/configs/models.ts @@ -2688,6 +2688,48 @@ export const models: { [key: string]: ModelPackage } = { providerOptions: ["sambanova"], isOpenSource: true, }, + + // 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, + }, + AUTODETECT: { title: "Autodetect", description: diff --git a/gui/src/pages/AddNewModel/configs/providers.ts b/gui/src/pages/AddNewModel/configs/providers.ts index 1b1178d9d3a..54bbbc8898c 100644 --- a/gui/src/pages/AddNewModel/configs/providers.ts +++ b/gui/src/pages/AddNewModel/configs/providers.ts @@ -1282,6 +1282,43 @@ 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. Select a model preset below + +ClawRouter uses a 15-dimension prompt complexity scoring system to route simple requests to cheap models (Haiku/Flash) and complex requests to capable ones (Opus/GPT-4o).`, + icon: "clawrouter.png", + tags: [ModelProviderTags.Local, ModelProviderTags.OpenSource], + packages: [ + models.clawrouterAuto, + models.clawrouterFree, + models.clawrouterEco, + { + ...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/packages/openai-adapters/src/apis/ClawRouter.ts b/packages/openai-adapters/src/apis/ClawRouter.ts new file mode 100644 index 00000000000..adea07a6522 --- /dev/null +++ b/packages/openai-adapters/src/apis/ClawRouter.ts @@ -0,0 +1,24 @@ +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. + * It provides an OpenAI-compatible API at localhost:1337. + * + * @see https://github.com/BlockRunAI/ClawRouter + */ +export class ClawRouterApi extends OpenAIApi { + constructor(config: ClawRouterConfig) { + super({ + ...config, + apiBase: config.apiBase ?? "http://localhost:1337/v1/", + }); + } +} + +export default ClawRouterApi; diff --git a/packages/openai-adapters/src/index.ts b/packages/openai-adapters/src/index.ts index 9e63cf3d242..ab14b184b49 100644 --- a/packages/openai-adapters/src/index.ts +++ b/packages/openai-adapters/src/index.ts @@ -17,6 +17,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"; @@ -176,6 +177,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 4b809ca8296..5b28a694765 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"), From be209b7fd91cd04d91ef97d9c4f390fc7230e7c5 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 23 Mar 2026 23:46:50 +0000 Subject: [PATCH 02/14] docs: enhance ClawRouter documentation - Add ClawRouter to providers overview - Expand documentation with: - Model capabilities configuration - Multiple roles setup - Troubleshooting section - Cost monitoring - Comparison with OpenRouter - Complexity dimensions explanation --- docs/customize/model-providers/overview.mdx | 1 + .../model-providers/top-level/clawrouter.mdx | 178 ++++++++++++++++-- 2 files changed, 166 insertions(+), 13 deletions(-) diff --git a/docs/customize/model-providers/overview.mdx b/docs/customize/model-providers/overview.mdx index 4bbf1c0dc60..fb828bdc595 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/top-level/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/customize/model-providers/top-level/clawrouter.mdx b/docs/customize/model-providers/top-level/clawrouter.mdx index 114ff416bd2..daa627cb16a 100644 --- a/docs/customize/model-providers/top-level/clawrouter.mdx +++ b/docs/customize/model-providers/top-level/clawrouter.mdx @@ -7,6 +7,10 @@ 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: @@ -17,9 +21,12 @@ npx clawrouter This starts the router at `http://localhost:1337`. - - Learn more at the [ClawRouter GitHub repository](https://github.com/BlockRunAI/ClawRouter) - +You can also install it globally: + +```bash +npm install -g clawrouter +clawrouter +``` ## Configuration @@ -57,21 +64,68 @@ This starts the router at `http://localhost:1337`. ClawRouter provides several routing modes: -| Model | Description | -|-------|-------------| -| `blockrun/auto` | Automatic model selection based on prompt complexity (recommended) | -| `blockrun/free` | Routes to available free-tier models only | -| `blockrun/eco` | Economy tier balancing cost and capability | +| Model | Description | Best For | +|-------|-------------|----------| +| `blockrun/auto` | Automatic model selection based on prompt complexity | General use (recommended) | +| `blockrun/free` | Routes to available free-tier models only | Testing, low-cost experimentation | +| `blockrun/eco` | Economy tier balancing cost and capability | Everyday coding tasks | + +You can also specify any model supported by your configured providers directly (e.g., `anthropic/claude-sonnet-4`, `openai/gpt-4o`). ## How It Works ClawRouter uses a 15-dimension prompt complexity scoring system to analyze each request: -- **Simple requests** (greetings, basic Q&A) → routed to cheap models like Claude Haiku or Gemini Flash -- **Complex requests** (code generation, analysis) → routed to capable models like Claude Opus or GPT-4o +- **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 + } + } + ] + } + ``` + + + ## Custom API Base If you're running ClawRouter on a different port or host: @@ -102,9 +156,57 @@ If you're running ClawRouter on a different port or host: -## Using with API Keys +## Using Multiple Roles -If your ClawRouter instance is configured with upstream provider API keys, they are managed by ClawRouter itself. No additional API key configuration is needed in Continue. +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: @@ -116,7 +218,7 @@ For self-hosted setups with custom authentication: provider: clawrouter model: blockrun/auto apiBase: http://localhost:1337/v1/ - apiKey: + apiKey: ${{ secrets.CLAWROUTER_API_KEY }} ``` @@ -135,3 +237,53 @@ For self-hosted setups with custom authentication: ``` + +## 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`). + +## 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 +``` + +## 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 | +| Setup | `npx clawrouter` | API key signup | + + + ClawRouter can be used alongside OpenRouter — route complex tasks through ClawRouter while using OpenRouter for specific model access. + From b6187b2f221c4167116ce46f59f42a965af70c68 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 23 Mar 2026 23:46:50 +0000 Subject: [PATCH 03/14] feat: add ClawRouter to core/llm/llms - Add ClawRouter.ts extending OpenAI base class - Register ClawRouter in llms/index.ts --- core/llm/llms/ClawRouter.ts | 23 +++++++++++++++++++++++ core/llm/llms/index.ts | 2 ++ 2 files changed, 25 insertions(+) create mode 100644 core/llm/llms/ClawRouter.ts diff --git a/core/llm/llms/ClawRouter.ts b/core/llm/llms/ClawRouter.ts new file mode 100644 index 00000000000..e60ee3e1a4c --- /dev/null +++ b/core/llm/llms/ClawRouter.ts @@ -0,0 +1,23 @@ +import { LLMOptions } from "../../index.js"; + +import OpenAI from "./OpenAI.js"; + +/** + * ClawRouter LLM Provider + * + * ClawRouter is an open-source LLM router that automatically selects the + * cheapest capable model for each request based on prompt complexity. + * It provides an OpenAI-compatible API at localhost:1337. + * + * @see https://github.com/BlockRunAI/ClawRouter + */ +class ClawRouter extends OpenAI { + static providerName = "clawrouter"; + static defaultOptions: Partial = { + apiBase: "http://localhost:1337/v1/", + model: "blockrun/auto", + useLegacyCompletionsEndpoint: false, + }; +} + +export default ClawRouter; diff --git a/core/llm/llms/index.ts b/core/llm/llms/index.ts index ebf9b7cf469..d992bf942b2 100644 --- a/core/llm/llms/index.ts +++ b/core/llm/llms/index.ts @@ -49,6 +49,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"; @@ -109,6 +110,7 @@ export const LLMClasses = [ Azure, WatsonX, OpenRouter, + ClawRouter, Nvidia, Vllm, SambaNova, From c3d983bbe730fb21253d1c43f946630838ab2965 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 23 Mar 2026 23:46:51 +0000 Subject: [PATCH 04/14] feat: add User-Agent headers for ClawRouter integration tracking - Add 'User-Agent: Continue/IDE' header to identify Continue requests - Add 'X-Continue-Provider: clawrouter' header for routing analytics - Enhanced documentation with features list This helps ClawRouter optimize routing decisions based on integration source and enables better analytics for the Continue community. --- core/llm/llms/ClawRouter.ts | 25 +++++++++++++++++-- .../openai-adapters/src/apis/ClawRouter.ts | 22 ++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/core/llm/llms/ClawRouter.ts b/core/llm/llms/ClawRouter.ts index e60ee3e1a4c..f35a8d0fe84 100644 --- a/core/llm/llms/ClawRouter.ts +++ b/core/llm/llms/ClawRouter.ts @@ -2,12 +2,21 @@ import { LLMOptions } from "../../index.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. - * It provides an OpenAI-compatible API at localhost:1337. + * 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 */ @@ -18,6 +27,18 @@ class ClawRouter extends OpenAI { model: "blockrun/auto", 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/packages/openai-adapters/src/apis/ClawRouter.ts b/packages/openai-adapters/src/apis/ClawRouter.ts index adea07a6522..15f334eb2d3 100644 --- a/packages/openai-adapters/src/apis/ClawRouter.ts +++ b/packages/openai-adapters/src/apis/ClawRouter.ts @@ -7,8 +7,14 @@ 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. - * It provides an OpenAI-compatible API at localhost:1337. + * 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 */ @@ -19,6 +25,18 @@ export class ClawRouterApi extends OpenAIApi { 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; From e05beb8fa77ccf8641059897e21b5babad8e279b Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 23 Mar 2026 23:54:00 +0000 Subject: [PATCH 05/14] feat: complete ClawRouter integration with full provider support - Add clawrouter to PROVIDER_HANDLES_TEMPLATING (autodetect.ts) - Add clawrouter to PROVIDER_SUPPORTS_IMAGES (autodetect.ts) - Add clawrouter tool support function (toolSupport.ts) - Add supportsReasoningField and supportsReasoningDetailsField - Add promptTemplates with osModelsEditPrompt - Match OpenRouter implementation patterns --- core/llm/autodetect.ts | 2 ++ core/llm/llms/ClawRouter.ts | 9 +++++++++ core/llm/toolSupport.ts | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/core/llm/autodetect.ts b/core/llm/autodetect.ts index 15707209a58..7f064f027a3 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", "groq", @@ -122,6 +123,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 index f35a8d0fe84..2cdb9a8cecd 100644 --- a/core/llm/llms/ClawRouter.ts +++ b/core/llm/llms/ClawRouter.ts @@ -1,4 +1,5 @@ import { LLMOptions } from "../../index.js"; +import { osModelsEditPrompt } from "../templates/edit.js"; import OpenAI from "./OpenAI.js"; @@ -22,9 +23,17 @@ const CONTINUE_VERSION = process.env.npm_package_version || "unknown"; */ 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, }; diff --git a/core/llm/toolSupport.ts b/core/llm/toolSupport.ts index 099424c61ab..4b7db709941 100644 --- a/core/llm/toolSupport.ts +++ b/core/llm/toolSupport.ts @@ -359,6 +359,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"); From 032403015873fc490cd40fdd779800b97d7876d5 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 23 Mar 2026 23:56:18 +0000 Subject: [PATCH 06/14] feat: add ClawRouter to VS Code schema, docs nav, and AI SDK - Add clawrouter to VS Code config_schema.json provider enum - Add ClawRouter description in config_schema.json - Add clawrouter to docs.json navigation (top-level providers) - Add clawrouter to AI SDK PROVIDER_MAP --- docs/docs.json | 1 + extensions/vscode/config_schema.json | 2 ++ packages/openai-adapters/src/apis/AiSdk.ts | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/docs/docs.json b/docs/docs.json index 13f482ea56b..578679bddf0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -160,6 +160,7 @@ "customize/model-providers/top-level/lmstudio", "customize/model-providers/top-level/ollama", "customize/model-providers/top-level/openrouter", + "customize/model-providers/top-level/clawrouter", "customize/model-providers/top-level/openai", "customize/model-providers/top-level/tetrate_agent_router_service", "customize/model-providers/top-level/vertexai" diff --git a/extensions/vscode/config_schema.json b/extensions/vscode/config_schema.json index 2d5aee80141..c85244d0a7a 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.\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/packages/openai-adapters/src/apis/AiSdk.ts b/packages/openai-adapters/src/apis/AiSdk.ts index 6edfbbe4152..d4b5ee5e63b 100644 --- a/packages/openai-adapters/src/apis/AiSdk.ts +++ b/packages/openai-adapters/src/apis/AiSdk.ts @@ -39,6 +39,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 { From f721fb88f86c59f6284c7b861e9a99f8305a78b8 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 23 Mar 2026 23:59:46 +0000 Subject: [PATCH 07/14] docs: add wallet setup and x402 payment documentation - Add Wallet & Payment Setup section to clawrouter.mdx - Document payment tiers (free/eco/auto) - Add wallet funding instructions (Solana/Base USDC) - Add spend control configuration - Update VS Code schema description with wallet info - Update provider longDescription with payment options --- .../model-providers/top-level/clawrouter.mdx | 48 +++++++++++++++++++ extensions/vscode/config_schema.json | 2 +- .../pages/AddNewModel/configs/providers.ts | 10 +++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/docs/customize/model-providers/top-level/clawrouter.mdx b/docs/customize/model-providers/top-level/clawrouter.mdx index daa627cb16a..b8fcd3b9077 100644 --- a/docs/customize/model-providers/top-level/clawrouter.mdx +++ b/docs/customize/model-providers/top-level/clawrouter.mdx @@ -238,6 +238,54 @@ For self-hosted setups with custom authentication: +## 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 +``` + ## Troubleshooting ### Connection Refused diff --git a/extensions/vscode/config_schema.json b/extensions/vscode/config_schema.json index c85244d0a7a..d30983f8290 100644 --- a/extensions/vscode/config_schema.json +++ b/extensions/vscode/config_schema.json @@ -269,7 +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.\n> [Reference](https://github.com/BlockRunAI/ClawRouter)", + "### 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/src/pages/AddNewModel/configs/providers.ts b/gui/src/pages/AddNewModel/configs/providers.ts index 54bbbc8898c..b0b77f4fa38 100644 --- a/gui/src/pages/AddNewModel/configs/providers.ts +++ b/gui/src/pages/AddNewModel/configs/providers.ts @@ -1293,9 +1293,15 @@ To get started, [register](https://dataplatform.cloud.ibm.com/registration/stepo To get started: 1. Install ClawRouter: \`npx clawrouter\` 2. The router runs locally at \`http://localhost:1337\` -3. Select a model preset below +3. A wallet is auto-generated on first run +4. Select a model preset below -ClawRouter uses a 15-dimension prompt complexity scoring system to route simple requests to cheap models (Haiku/Flash) and complex requests to capable ones (Opus/GPT-4o).`, +**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: [ From 56c4bec276421809d0124a034023cf5f13da1daa Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Tue, 24 Mar 2026 00:01:25 +0000 Subject: [PATCH 08/14] feat: add premium tier and enhanced ClawRouter features - Add blockrun/premium routing profile (mission-critical tasks) - Add dual-chain wallet support docs (Base EVM + Solana) - Add model exclusion documentation - Update routing profiles table with savings percentages - Update comparison table with auth/payment differences - Total 44+ models, 4 routing tiers --- .../model-providers/top-level/clawrouter.mdx | 63 ++++++++++++++++--- gui/src/pages/AddNewModel/configs/models.ts | 13 ++++ .../pages/AddNewModel/configs/providers.ts | 1 + 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/docs/customize/model-providers/top-level/clawrouter.mdx b/docs/customize/model-providers/top-level/clawrouter.mdx index b8fcd3b9077..8dda3aaf59a 100644 --- a/docs/customize/model-providers/top-level/clawrouter.mdx +++ b/docs/customize/model-providers/top-level/clawrouter.mdx @@ -62,15 +62,16 @@ clawrouter ## Available Models -ClawRouter provides several routing modes: +ClawRouter provides four routing profiles: -| Model | Description | Best For | -|-------|-------------|----------| -| `blockrun/auto` | Automatic model selection based on prompt complexity | General use (recommended) | -| `blockrun/free` | Routes to available free-tier models only | Testing, low-cost experimentation | -| `blockrun/eco` | Economy tier balancing cost and capability | Everyday coding tasks | +| 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 model supported by your configured providers directly (e.g., `anthropic/claude-sonnet-4`, `openai/gpt-4o`). +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 @@ -322,6 +323,50 @@ You can view aggregated costs with: 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 | @@ -330,7 +375,9 @@ curl http://localhost:1337/stats | Automatic routing | ✅ Complexity-based | ❌ Manual selection | | Cost optimization | ✅ 78-96% savings | ❌ Pay per model | | Privacy | ✅ Data stays local | ⚠️ Data sent to cloud | -| Setup | `npx clawrouter` | API key signup | +| 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/gui/src/pages/AddNewModel/configs/models.ts b/gui/src/pages/AddNewModel/configs/models.ts index 8cb5e4459bc..f7f722947b8 100644 --- a/gui/src/pages/AddNewModel/configs/models.ts +++ b/gui/src/pages/AddNewModel/configs/models.ts @@ -2729,6 +2729,19 @@ export const models: { [key: string]: ModelPackage } = { 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", diff --git a/gui/src/pages/AddNewModel/configs/providers.ts b/gui/src/pages/AddNewModel/configs/providers.ts index b0b77f4fa38..49114a2f706 100644 --- a/gui/src/pages/AddNewModel/configs/providers.ts +++ b/gui/src/pages/AddNewModel/configs/providers.ts @@ -1308,6 +1308,7 @@ Fund your wallet with USDC on Solana or Base. ClawRouter uses x402 micropayments models.clawrouterAuto, models.clawrouterFree, models.clawrouterEco, + models.clawrouterPremium, { ...models.AUTODETECT, params: { From 0781a1f549ee6a218159f35f0e6d2973bb9630ee Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Tue, 24 Mar 2026 00:03:28 +0000 Subject: [PATCH 09/14] test: add ClawRouter unit tests - Add ClawRouter.vitest.ts (core/llm/llms) - Provider name verification - Default options validation - Reasoning field support - User-Agent header verification - Routing profiles acceptance - Add ClawRouter.test.ts (openai-adapters) - Default apiBase - Custom apiBase override - Continue headers - OpenAI standard headers - Add clawrouter tests to toolSupport.test.ts - blockrun/* routing profiles - Tool-supporting model patterns --- core/llm/llms/ClawRouter.vitest.ts | 50 +++++++++++++++++++ core/llm/toolSupport.test.ts | 23 +++++++++ .../src/apis/ClawRouter.test.ts | 42 ++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 core/llm/llms/ClawRouter.vitest.ts create mode 100644 packages/openai-adapters/src/apis/ClawRouter.test.ts 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/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/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"); + }); +}); From 7695f55973b9b4f28e64b7c4e5a7ef533c5be0c5 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Tue, 24 Mar 2026 00:07:37 +0000 Subject: [PATCH 10/14] docs: add routing profile switching and enhanced error handling docs - Add 'Switching Between Routing Profiles' section with full examples - Add /model command tip for quick switching - Add 'Error Handling' section comparing Continue vs ClawRouter - Document automatic error recovery (429, 402, 500+, timeout) - Add response header documentation for diagnostics - Add AI-powered doctor command for troubleshooting --- .../model-providers/top-level/clawrouter.mdx | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/docs/customize/model-providers/top-level/clawrouter.mdx b/docs/customize/model-providers/top-level/clawrouter.mdx index 8dda3aaf59a..fb989ac0e9a 100644 --- a/docs/customize/model-providers/top-level/clawrouter.mdx +++ b/docs/customize/model-providers/top-level/clawrouter.mdx @@ -127,6 +127,73 @@ ClawRouter supports function calling and tool use through its underlying model p +## 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/" + } + ] + } + ``` + + + +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: @@ -287,6 +354,39 @@ clawrouter config set spendLimit.daily 5 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 @@ -309,6 +409,23 @@ If a specific model isn't available, check that ClawRouter has the required prov 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: From c1087974eb68dc584d35d49647be4cc9967d05a0 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Tue, 24 Mar 2026 18:07:31 -0700 Subject: [PATCH 11/14] fix: address PR review feedback - move docs to /more, resize logo, add provider constants, fix docs example --- .../{top-level => more}/clawrouter.mdx | 8 +++++++- docs/customize/model-providers/overview.mdx | 2 +- docs/docs.json | 2 +- gui/public/logos/clawrouter.png | Bin 28841 -> 1617 bytes 4 files changed, 9 insertions(+), 3 deletions(-) rename docs/customize/model-providers/{top-level => more}/clawrouter.mdx (98%) diff --git a/docs/customize/model-providers/top-level/clawrouter.mdx b/docs/customize/model-providers/more/clawrouter.mdx similarity index 98% rename from docs/customize/model-providers/top-level/clawrouter.mdx rename to docs/customize/model-providers/more/clawrouter.mdx index fb989ac0e9a..7c91cf0ae5e 100644 --- a/docs/customize/model-providers/top-level/clawrouter.mdx +++ b/docs/customize/model-providers/more/clawrouter.mdx @@ -178,9 +178,15 @@ Add multiple ClawRouter profiles to your config and switch via Continue's model }, { "title": "ClawRouter Premium", - "provider": "clawrouter", + "provider": "clawrouter", "model": "blockrun/premium", "apiBase": "http://localhost:1337/v1/" + }, + { + "title": "ClawRouter Free", + "provider": "clawrouter", + "model": "blockrun/free", + "apiBase": "http://localhost:1337/v1/" } ] } diff --git a/docs/customize/model-providers/overview.mdx b/docs/customize/model-providers/overview.mdx index de48f711583..7ba030dcb4d 100644 --- a/docs/customize/model-providers/overview.mdx +++ b/docs/customize/model-providers/overview.mdx @@ -34,7 +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/top-level/clawrouter) | Open-source LLM router with automatic cost-optimized model selection | +| [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 578679bddf0..8556401ffc6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -160,7 +160,6 @@ "customize/model-providers/top-level/lmstudio", "customize/model-providers/top-level/ollama", "customize/model-providers/top-level/openrouter", - "customize/model-providers/top-level/clawrouter", "customize/model-providers/top-level/openai", "customize/model-providers/top-level/tetrate_agent_router_service", "customize/model-providers/top-level/vertexai" @@ -170,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/gui/public/logos/clawrouter.png b/gui/public/logos/clawrouter.png index 51ece03f91e7b54521dbdea73bc7ce78a1740348..836d85ae11f7e0a2ffa8c47a482db18dfce240a5 100644 GIT binary patch literal 1617 zcmV-X2Cn&uP)00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuByGcYrRA>e5SxavdR}`Ly#~#0J z#jik|gb?yjD3M&0Za_k6#Re8+hY$;X06%~QYgQFPVnJ8HCcl6L35gAfLX=buQL4Da zsU4h{*s){BZ_kV$)6W^za$6zfxo+!7a}|wl&OPVe@B7ZVGxsL6EX&ksZp>*0-^8vF z_`fr#WkAaSp#oY75ei5gu4RB&Lc$5O3=j%P9Ij=6SVF=HvR6%-NRhqDPNWhHH3}uU(I`iwa;;VugmyIba#=_uFxqbHVxNeB z`1_~AFTWNRS818bZfyK~_ip+l|q7duDaDCkU8EhoU;Fc7BM`b8@XZ_TYhdbVM^uJ{Xja zj;h&gHJ3w0O+R^J@OV&2_^IaeunX}xhDzaZtynBYBJJEd?GtTc=9>x6k3X4i{6KL! zxgk3*UuN|BN-Vbh=#l68b%V=A(R48svR}G{>#W710(UEfhyZIa2n-{eO_&b4qTG+(y(@;p z;^CpbuMbs(AJ%9@MHP2;-~iw>O*j9TDa#<^zjLQqOZ9pSjfxHt0fsklQ{(z`ek?54 z>+x5wQ0Vny(RcGEYcc^ko;1s%%27Ge@iAUOD1Fow{5hM=GCC@YA_ZaujV##6dvAR2 z{4M|h2Cp~z@}+5b7!ac`UNnox>uqjOn&zBN2#ZKAAg%YZj%g6=+ zev0Td3UEyLlRS2g8X0t5xpMygef!0WsD3G(HVqB&J|EOY9{B!#^MwmKhePjh;8)}X zJUNGh;W$Si01F9$lZqsmK#V7K7Bg!#cRUBS0=5KM{a{g20v+S-b^y^H=r$NN^JxUs zsMgIGl^KvEsrfxRY59DJXZGmboms0{UgD8rEAZ%}MD;5<` zmEVE>{(hj@-Q9(LGcz-Q1sotSKR@sBcm@XtVK0}+$hPN@W>0?2$mpTpq*P{4wm5EWYj4cdc) za)5)sk_v>w8KVe-DP6Ku>&$QHlyBHEKc<6=B^CQFe>E zD(Z<0kjSBC50I?a^;nHS*Cp+6LM;Ow3f6UZEdyPbw8IIt40I@1*WI-YbY0RfPpBN* z{&sj%c_aJsDx@PgY{%nq9L_VD3@%1EY2yha4&Jcf;(=@6)YKHt^Ej^KNZ)e=AOJ3_ z0F4_D?ry163O63`!IcAdKyU*S48d(+U;xivBmmsFdE$(XOOH}8xIjZ}z(oh6Ft#pn zJ$jr*%|LI|53b7qjjIBLx7kkqmjxbetw#y8A^b{LdyRfYpnrY%)Mwxi4cqzNACcSU P00000NkvXXu0mjfdYQPu literal 28841 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 Fe*qrbTIm1) From c3ca0e7a1d2a66e13c0d0d5f1c53bae252422b23 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Wed, 25 Mar 2026 19:34:03 -0700 Subject: [PATCH 12/14] fix: replace clawrouter logo with higher-res brand kit version (128x128) --- gui/public/logos/clawrouter.png | Bin 1617 -> 2362 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/gui/public/logos/clawrouter.png b/gui/public/logos/clawrouter.png index 836d85ae11f7e0a2ffa8c47a482db18dfce240a5..b56a5cafbc540ce58fa6f17f0bb957740c9fd41d 100644 GIT binary patch literal 2362 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 P00000NkvXXu0mjfdYQPu From 5bd24ea854dcc15b91d3c010af98da563b5418b5 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Sun, 29 Mar 2026 01:06:40 -0400 Subject: [PATCH 13/14] fix: improve out-of-credits dialog to guide users toward own API key When users hit a 402 from continue-proxy, the dialog previously only offered "Purchase Credits". Now it also surfaces an "Add API key secret" button and explains how to switch to the direct `anthropic` provider, fixing the loop described in issue #11952. Also adds component tests for OutOfCreditsDialog and StreamErrorDialog covering the 402/out-of-credits routing, customErrorMessage display, and standard status code error branches. --- gui/src/pages/gui/OutOfCreditsDialog.test.tsx | 76 +++++++ gui/src/pages/gui/OutOfCreditsDialog.tsx | 26 ++- gui/src/pages/gui/StreamError.test.tsx | 211 ++++++++++++++++++ 3 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 gui/src/pages/gui/OutOfCreditsDialog.test.tsx create mode 100644 gui/src/pages/gui/StreamError.test.tsx diff --git a/gui/src/pages/gui/OutOfCreditsDialog.test.tsx b/gui/src/pages/gui/OutOfCreditsDialog.test.tsx new file mode 100644 index 00000000000..752ed261452 --- /dev/null +++ b/gui/src/pages/gui/OutOfCreditsDialog.test.tsx @@ -0,0 +1,76 @@ +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..e55b5f59de3 --- /dev/null +++ b/gui/src/pages/gui/StreamError.test.tsx @@ -0,0 +1,211 @@ +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(); + }); + }); +}); From 266f984940a69bd393eaaba9d1e7eed045d3cc7e Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Sun, 29 Mar 2026 01:37:54 -0400 Subject: [PATCH 14/14] style: fix prettier formatting in new test files --- gui/src/pages/gui/OutOfCreditsDialog.test.tsx | 4 +++- gui/src/pages/gui/StreamError.test.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gui/src/pages/gui/OutOfCreditsDialog.test.tsx b/gui/src/pages/gui/OutOfCreditsDialog.test.tsx index 752ed261452..ffa2544cc71 100644 --- a/gui/src/pages/gui/OutOfCreditsDialog.test.tsx +++ b/gui/src/pages/gui/OutOfCreditsDialog.test.tsx @@ -31,7 +31,9 @@ describe("OutOfCreditsDialog", () => { it("renders the no-credits message", () => { renderDialog(); expect( - screen.getByText("You have no credits remaining on your Continue account"), + screen.getByText( + "You have no credits remaining on your Continue account", + ), ).toBeInTheDocument(); }); diff --git a/gui/src/pages/gui/StreamError.test.tsx b/gui/src/pages/gui/StreamError.test.tsx index e55b5f59de3..13689b9fa4a 100644 --- a/gui/src/pages/gui/StreamError.test.tsx +++ b/gui/src/pages/gui/StreamError.test.tsx @@ -74,8 +74,7 @@ vi.mock("../../redux/slices/configSlice", () => ({ })); vi.mock("../../redux/slices/profilesSlice", () => ({ - selectSelectedProfile: (state: any) => - state?.profiles?.selectedOrgId ?? null, + selectSelectedProfile: (state: any) => state?.profiles?.selectedOrgId ?? null, })); vi.mock("../../redux/slices/uiSlice", () => ({ @@ -186,7 +185,9 @@ describe("StreamErrorDialog", () => { 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(); + expect( + screen.getByText("Model/deployment not found"), + ).toBeInTheDocument(); }); it("shows generic error title for unknown errors", () => {