Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0598fc9
🤖 feat: add OpenRouter provider support
ammar-agent Nov 10, 2025
2894781
🤖 feat: add OpenRouter provider routing support
ammar-agent Nov 10, 2025
4720719
🤖 fix: pass OpenRouter provider config via extraBody
ammar-agent Nov 10, 2025
a2ef7fb
🤖 docs: fix OpenRouter provider routing field name
ammar-agent Nov 10, 2025
1fdefe8
🤖 feat: add OpenRouter reasoning support
ammar-agent Nov 10, 2025
51d19cd
🤖 docs: document OpenRouter reasoning support
ammar-agent Nov 10, 2025
7e210e0
🤖 feat: update model pricing database and add GLM-4.6
ammar-agent Nov 10, 2025
e09d1ef
🤖 refactor: flatten OpenRouter provider config (remove nested 'provid…
ammar-agent Nov 11, 2025
bd6723b
🤖 refactor: centralize provider registry to prevent desync bugs
ammar-agent Nov 11, 2025
f8ea9ba
🤖 docs: remove unnecessary benefits section from OpenRouter reasoning…
ammar-agent Nov 11, 2025
7c5dc71
🤖 refactor: map providers to SDK packages, improve tests
ammar-agent Nov 11, 2025
6ab2d20
🤖 refactor: remove unnecessary OpenRouter backwards compatibility
ammar-agent Nov 11, 2025
5917c43
🤖 test: remove nonsensical provider registry tests, add AGENTS.md gui…
ammar-agent Nov 11, 2025
6809267
🤖 refactor: deduplicate provider setup code
ammar-agent Nov 11, 2025
cfd1a7b
docs
Nov 11, 2025
d1973a0
🤖 refactor: add type-safe provider imports to eliminate eslint suppre…
ammar-agent Nov 11, 2025
8fad39c
🤖 refactor: use PROVIDER_REGISTRY in import functions to eliminate du…
ammar-agent Nov 11, 2025
09048bb
🤖 refactor: revert to hardcoded package names in import functions
ammar-agent Nov 11, 2025
9a7316c
🤖 refactor: map PROVIDER_REGISTRY to import functions, eliminate all …
ammar-agent Nov 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Required for integration tests when TEST_INTEGRATION=1
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-proj-...
OPENROUTER_API_KEY=sk-or-v1-...

# Optional: Set to 1 to run integration tests
# Integration tests require API keys to be set
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Here are some specific use cases we enable:
- **Local**: git worktrees on your local machine ([docs](https://cmux.io/local.html))
- **SSH**: regular git clones on a remote server ([docs](https://cmux.io/ssh.html))
- Multi-model (`sonnet-4-*`, `gpt-5-*`, `opus-4-*`)
- Ollama supported for local LLMs ([docs](https://cmux.io/models.html))
- Ollama supported for local LLMs ([docs](https://cmux.io/models.html#ollama-local))
- OpenRouter supported for long-tail of LLMs ([docs](https://cmux.io/models.html#openrouter-cloud))
- Supporting UI and keybinds for efficiently managing a suite of agents
- Rich markdown outputs (mermaid diagrams, LaTeX, etc.)

Expand Down
3 changes: 3 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@ai-sdk/anthropic": "^2.0.29",
"@ai-sdk/openai": "^2.0.52",
"@openrouter/ai-sdk-provider": "^1.2.1",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.2.10",
Expand Down Expand Up @@ -405,6 +406,8 @@

"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],

"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.1", "", { "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-sDc+/tlEM9VTsYlZ3YMwD9AHinSNusdLFGQhtb50eo5r68U/yBixEHRsKEevqSspiX3V6J06hU7C25t4KE9iag=="],

"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],

"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
Expand Down
5 changes: 5 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ This project uses **Make** as the primary build orchestrator. See `Makefile` for
- Always run `make typecheck` after making changes to verify types (checks both main and renderer)
- **⚠️ CRITICAL: Unit tests MUST be colocated with the code they test** - Place `*.test.ts` files in the same directory as the implementation file (e.g., `src/utils/foo.test.ts` next to `src/utils/foo.ts`). Tests in `./tests/` are ONLY for integration/E2E tests that require complex setup.
- **Don't test simple mapping operations** - If the test just verifies the code does what it obviously does from reading it, skip the test.
- ❌ **Bad**: `expect(REGISTRY.foo).toBe("bar")` - This just duplicates the implementation
- ✅ **Good**: `expect(Object.keys(REGISTRY).length).toBeGreaterThan(0)` - Tests an invariant
- ❌ **Bad**: `expect(isValid("foo")).toBe(true)` for every valid value - Duplicates implementation
- ✅ **Good**: `expect(isValid("invalid")).toBe(false)` - Tests boundary/error cases
- **Rule of thumb**: If changing the implementation requires changing the test in the same way, the test is probably useless
- Strive to decompose complex logic away from the components and into `.src/utils/`
- utils should be either pure functions or easily isolated (e.g. if they operate on the FS they accept
a path). Testing them should not require complex mocks or setup.
Expand Down
77 changes: 77 additions & 0 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,79 @@ GPT-5 family of models:

TODO: add issue link here.

#### OpenRouter (Cloud)

Access 300+ models from multiple providers through a single API:

- `openrouter:z-ai/glm-4.6`
- `openrouter:anthropic/claude-3.5-sonnet`
- `openrouter:google/gemini-2.0-flash-thinking-exp`
- `openrouter:deepseek/deepseek-chat`
- `openrouter:openai/gpt-4o`
- Any model from [OpenRouter Models](https://openrouter.ai/models)

**Setup:**

1. Get your API key from [openrouter.ai](https://openrouter.ai/)
2. Add to `~/.cmux/providers.jsonc`:

```jsonc
{
"openrouter": {
"apiKey": "sk-or-v1-...",
},
}
```

**Provider Routing (Advanced):**

OpenRouter can route requests to specific infrastructure providers (Cerebras, Fireworks, Together, etc.). Configure provider preferences in `~/.cmux/providers.jsonc`:

```jsonc
{
"openrouter": {
"apiKey": "sk-or-v1-...",
// Use Cerebras for ultra-fast inference
"order": ["Cerebras", "Fireworks"], // Try in order
"allow_fallbacks": true, // Allow other providers if unavailable
},
}
```

Or require a specific provider (no fallbacks):

```jsonc
{
"openrouter": {
"apiKey": "sk-or-v1-...",
"order": ["Cerebras"], // Only try Cerebras
"allow_fallbacks": false, // Fail if Cerebras unavailable
},
}
```

**Provider Routing Options:**

- `order`: Array of provider names to try in priority order (e.g., `["Cerebras", "Fireworks"]`)
- `allow_fallbacks`: Boolean - whether to fall back to other providers (default: `true`)
- `only`: Array - restrict to only these providers
- `ignore`: Array - exclude specific providers
- `require_parameters`: Boolean - only use providers supporting all your request parameters
- `data_collection`: `"allow"` or `"deny"` - control whether providers can store/train on your data

See [OpenRouter Provider Routing docs](https://openrouter.ai/docs/features/provider-routing) for details.

**Reasoning Models:**

OpenRouter supports reasoning models like Claude Sonnet Thinking. Use the thinking slider to control reasoning effort:

- **Off**: No extended reasoning
- **Low**: Quick reasoning for straightforward tasks
- **Medium**: Standard reasoning for moderate complexity (default)
- **High**: Deep reasoning for complex problems

The thinking level is passed to OpenRouter as `reasoning.effort` and works with any reasoning-capable model. See [OpenRouter Reasoning docs](https://openrouter.ai/docs/use-cases/reasoning-tokens) for details.

#### Ollama (Local)

Run models locally with Ollama. No API key required:
Expand Down Expand Up @@ -68,6 +141,10 @@ All providers are configured in `~/.cmux/providers.jsonc`. Example configuration
"openai": {
"apiKey": "sk-...",
},
// Required for OpenRouter models
"openrouter": {
"apiKey": "sk-or-v1-...",
},
// Optional for Ollama (only needed for custom URL)
"ollama": {
"baseUrl": "http://your-server:11434/api",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"dependencies": {
"@ai-sdk/anthropic": "^2.0.29",
"@ai-sdk/openai": "^2.0.52",
"@openrouter/ai-sdk-provider": "^1.2.1",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.2.10",
Expand Down
31 changes: 31 additions & 0 deletions src/constants/providers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Test that provider registry structure is correct
*/

import { describe, test, expect } from "bun:test";
import { PROVIDER_REGISTRY, SUPPORTED_PROVIDERS, isValidProvider } from "./providers";

describe("Provider Registry", () => {
test("registry is not empty", () => {
expect(Object.keys(PROVIDER_REGISTRY).length).toBeGreaterThan(0);
});

test("all registry values are import functions", () => {
// Registry should map provider names to async import functions
for (const importFn of Object.values(PROVIDER_REGISTRY)) {
expect(typeof importFn).toBe("function");
expect(importFn.constructor.name).toBe("AsyncFunction");
}
});

test("SUPPORTED_PROVIDERS array stays in sync with registry keys", () => {
// If these don't match, derived array is out of sync
expect(SUPPORTED_PROVIDERS.length).toBe(Object.keys(PROVIDER_REGISTRY).length);
});

test("isValidProvider rejects invalid providers", () => {
expect(isValidProvider("invalid")).toBe(false);
expect(isValidProvider("")).toBe(false);
expect(isValidProvider("gpt-4")).toBe(false);
});
});
72 changes: 72 additions & 0 deletions src/constants/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Typed import helpers for provider packages
*
* These functions provide type-safe dynamic imports for provider packages.
* TypeScript can infer the correct module type from literal string imports,
* giving consuming code full type safety for provider constructors.
*/

/**
* Dynamically import the Anthropic provider package
*/
export async function importAnthropic() {
return await import("@ai-sdk/anthropic");
}

/**
* Dynamically import the OpenAI provider package
*/
export async function importOpenAI() {
return await import("@ai-sdk/openai");
}

/**
* Dynamically import the Ollama provider package
*/
export async function importOllama() {
return await import("ollama-ai-provider-v2");
}

/**
* Dynamically import the OpenRouter provider package
*/
export async function importOpenRouter() {
return await import("@openrouter/ai-sdk-provider");
}

/**
* Centralized provider registry mapping provider names to their import functions
*
* This is the single source of truth for supported providers. By mapping to import
* functions rather than package strings, we eliminate duplication while maintaining
* perfect type safety.
*
* When adding a new provider:
* 1. Create an importXxx() function above
* 2. Add entry mapping provider name to the import function
* 3. Implement provider handling in aiService.ts createModel()
* 4. Runtime check will fail if provider in registry but no handler
*/
export const PROVIDER_REGISTRY = {
anthropic: importAnthropic,
openai: importOpenAI,
ollama: importOllama,
openrouter: importOpenRouter,
} as const;

/**
* Union type of all supported provider names
*/
export type ProviderName = keyof typeof PROVIDER_REGISTRY;

/**
* Array of all supported provider names (for UI lists, iteration, etc.)
*/
export const SUPPORTED_PROVIDERS = Object.keys(PROVIDER_REGISTRY) as ProviderName[];

/**
* Type guard to check if a string is a valid provider name
*/
export function isValidProvider(provider: string): provider is ProviderName {
return provider in PROVIDER_REGISTRY;
}
Loading