Skip to content

Conversation

@ammar-agent
Copy link
Collaborator

Add official OpenRouter provider integration for access to 300+ models through a single API. Fixes errors that occurred when using baseURL override approach.

Changes

  • Install @openrouter/ai-sdk-provider package
  • Add OpenRouter to aiService.ts createModel method
  • Add OpenRouterProviderOptions to provider types
  • Add OPENROUTER_API_KEY environment variable support
  • Update docs/models.md with OpenRouter setup guide

Benefits

OpenRouter provides:

  • Universal model access - Anthropic, OpenAI, Google, Cerebras, DeepSeek, and 300+ others
  • Pay-as-you-go pricing - No monthly fees, transparent per-token costs
  • High availability - Automatic failover across providers
  • Immediate access - New models available as soon as they're released

Usage

// ~/.cmux/providers.jsonc
{
  "openrouter": {
    "apiKey": "sk-or-v1-..."
  }
}

Model format:

  • openrouter:anthropic/claude-3.5-sonnet
  • openrouter:google/gemini-2.0-flash-thinking-exp
  • openrouter:cerebras/glm-4.6
  • openrouter:deepseek/deepseek-chat

Testing

  • ✅ Type checking passes
  • ✅ Linting passes
  • ✅ Formatting passes
  • ✅ Package imports successfully
  • ✅ Model instantiation works

Why Not BaseURL Override?

The baseURL override approach has several issues:

  1. Missing OpenRouter-specific error handling
  2. Request format differences for features like prompt caching
  3. Response parsing issues with advanced features
  4. No automatic failover headers

The official provider handles all edge cases automatically.

Generated with cmux

Add official OpenRouter provider integration for access to 300+ models
through a single API. Fixes errors that occurred when using baseURL
override approach.

Changes:
- Install @openrouter/ai-sdk-provider package
- Add OpenRouter to aiService.ts createModel method
- Add OpenRouterProviderOptions to provider types
- Add OPENROUTER_API_KEY environment variable support
- Update docs/models.md with OpenRouter setup guide

OpenRouter provides:
- Universal model access (Anthropic, OpenAI, Google, Cerebras, etc.)
- Pay-as-you-go pricing with transparent per-token costs
- High availability with automatic failover
- Immediate access to new models

Usage:
  openrouter:anthropic/claude-3.5-sonnet
  openrouter:google/gemini-2.0-flash-thinking-exp
  openrouter:cerebras/glm-4.6
  openrouter:deepseek/deepseek-chat

_Generated with `cmux`_
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

cmux/src/services/ipcMain.ts

Lines 1121 to 1125 in 0598fc9

ipcMain.handle(IPC_CHANNELS.PROVIDERS_LIST, () => {
try {
// Return all supported providers, not just configured ones
// This matches the providers defined in the registry
return ["anthropic", "openai"];

P1 Badge Expose OpenRouter in providers list

The new OpenRouter integration never shows up in the providers UI because PROVIDERS_LIST still returns only "anthropic" and "openai". The renderer calls this handler to know which providers can be configured (src/browser/api.ts and src/preload.ts), so users will not see or be able to configure OpenRouter credentials even though the backend now supports it. Please include "openrouter" (and any other supported providers) in this list.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Enable transparent pass-through of OpenRouter provider routing options
from providers.jsonc to control which infrastructure providers serve
requests (Cerebras, Fireworks, Together, etc.).

Changes:
- Update OpenRouterProviderOptions documentation
- Add OpenRouter case to buildProviderOptions
- Document provider routing in docs/models.md with examples
- Add GLM-4.6 example (z-ai/glm-4.6, not cerebras/glm-4.6)

Usage:
```jsonc
{
  "openrouter": {
    "apiKey": "sk-or-v1-...",
    "provider": {
      "order": ["Cerebras", "Fireworks"],
      "allow_fallbacks": true
    }
  }
}
```

The ProviderConfig already supports arbitrary properties via
`[key: string]: unknown`, so OpenRouter options pass through
transparently to the SDK's extraBody parameter.

_Generated with `cmux`_
Map provider routing options from providers.jsonc to OpenRouter's
extraBody parameter. The SDK expects standard options (apiKey, baseURL,
headers, fetch) at the top level and everything else in extraBody.

Before: Spread entire providerConfig (provider routing ignored)
After: Extract standard fields, pass rest via extraBody

This enables provider routing to actually work:
```jsonc
{
  "openrouter": {
    "apiKey": "sk-or-v1-...",
    "provider": {
      "require": "Cerebras"
    }
  }
}
```

_Generated with `cmux`_
The documentation incorrectly showed:
  "provider": { "require": "Cerebras" }

OpenRouter's API doesn't have a 'require' field. The correct format is:
  "provider": { "order": ["Cerebras"], "allow_fallbacks": false }

Changes:
- Fixed example to use correct 'order' + 'allow_fallbacks' fields
- Added comprehensive list of all provider routing options
- Added link to official OpenRouter provider routing docs

_Generated with `cmux`_
Enable thinking levels for OpenRouter reasoning models (Claude Sonnet
Thinking, etc.) by passing reasoning.effort through providerOptions.

OpenRouter supports two reasoning control methods:
1. reasoning.effort: 'low'|'medium'|'high' (maps to our thinking levels)
2. reasoning.max_tokens: number (token budget)

We use effort-based control which maps cleanly to our existing thinking
level UI (off/low/medium/high).

Changes:
- Added OPENROUTER_REASONING_EFFORT mapping in thinking.ts
- Updated buildProviderOptions to pass reasoning config when thinking > off
- Added OpenRouterReasoningOptions type for type safety
- Set exclude: false to show reasoning traces in UI

This complements factory-level provider routing (configured in
providers.jsonc) with per-request reasoning control (based on thinking
slider).

_Generated with `cmux`_
Added section explaining how to use the thinking slider with OpenRouter
reasoning models. The thinking level controls reasoning.effort (low,
medium, high) which works with Claude Sonnet Thinking and other
reasoning-capable models via OpenRouter.

_Generated with `cmux`_
Ran scripts/update_models.ts to pull latest model data from LiteLLM.

Added Z.AI GLM-4.6 to models-extra.ts with OpenRouter pricing:
- 200K context window (202,752 tokens)
- $0.40/M input, $1.75/M output
- Supports tool use, reasoning, and structured outputs

This fixes model stat lookups for:
- openrouter:z-ai/glm-4.6
- openrouter:anthropic/claude-3.7-sonnet:thinking (already in models.json)

Changes:
- Updated src/utils/tokens/models.json (3,379 additions from LiteLLM)
- Added openrouter/z-ai/glm-4.6 to models-extra.ts

_Generated with `cmux`_
…er' key)

- Support both flat and nested config formats for backwards compatibility
- New flat format: { order: [...], allow_fallbacks: false }
- Old nested format: { provider: { order: [...], ... } } still works
- Update docs to show simpler flat format
- Routing options automatically wrapped under 'provider' for API

Resolves unnecessary nesting in user configuration.
Problem: When adding OpenRouter, had to manually update PROVIDERS_LIST in
ipcMain.ts. This created a class of bugs where a provider could be
implemented in aiService but forgotten in the UI providers list.

Solution:
- Created src/constants/providers.ts with SUPPORTED_PROVIDERS constant
- Single source of truth for all provider names
- ipcMain.ts now uses [...SUPPORTED_PROVIDERS] instead of hardcoded list
- Added runtime check in aiService.ts to validate provider is supported
- Added unit tests for provider registry validation

Benefits:
- Adding a new provider only requires updating SUPPORTED_PROVIDERS
- Runtime check prevents silent failures if handler not implemented
- Type-safe with ProviderName type and isValidProvider() guard
- Impossible to have provider in list but not in implementation

Addresses Codex P1 comment about exposing OpenRouter in providers list.
Per review feedback:
- Changed SUPPORTED_PROVIDERS from simple array to PROVIDER_REGISTRY object
- Maps each provider name to its Vercel AI SDK package name
- More useful: documents which npm package each provider uses
- Better tests: verify registry structure, package name format, consistency

Benefits:
- Self-documenting: can see at a glance which package is needed
- Type-safe: ProviderName derived from registry keys
- Better error messages: can reference package name in errors
- Validates package names follow npm conventions

Example:
  PROVIDER_REGISTRY = {
    anthropic: '@ai-sdk/anthropic',
    openrouter: '@openrouter/ai-sdk-provider',
    ...
  }

Tests now verify:
- Package names match npm format (@scope/pkg or pkg)
- Registry has expected providers with correct packages
- SUPPORTED_PROVIDERS array stays in sync with registry keys
- Type narrowing works correctly
Since OpenRouter was never in main, no need for backwards compatibility:
- Removed nested format support from aiService.ts
- Removed compatibility note from docs
- Simplified code to only support flat format

Before (unnecessary):
  if ('provider' in extraOptions) {
    // Old nested format
    extraBody = extraOptions;
  } else {
    // New flat format
    // ... complex restructuring
  }

After (clean):
  // Build extraBody: routing options go under 'provider'
  for (const [key, value] of Object.entries(extraOptions)) {
    if (OPENROUTER_ROUTING_OPTIONS.includes(key)) {
      routingOptions[key] = value;
    } else {
      otherOptions[key] = value;
    }
  }

Result: -9 lines of unnecessary code, clearer logic
…dance

Per review feedback, the tests were insane - they just duplicated the implementation:
- ❌ Removed: expect(PROVIDER_REGISTRY.anthropic).toBe('@ai-sdk/anthropic')
- ❌ Removed: expect(isValidProvider('anthropic')).toBe(true) for each provider
- ❌ Removed: Type narrowing 'test' that can't actually test types

Kept only tests that verify actual invariants:
- ✅ Registry is not empty
- ✅ Package names follow npm conventions
- ✅ SUPPORTED_PROVIDERS stays in sync with registry keys
- ✅ isValidProvider rejects invalid input

Added to AGENTS.md:
- Examples of bad vs good tests
- Rule of thumb: If changing implementation requires changing test the same way, test is useless

Before: 6 tests, 31 expect() calls (mostly duplicating implementation)
After: 4 tests, 9 expect() calls (all testing actual behavior/invariants)
Per review feedback, fixed two duplications:

1. Custom fetch logic repeated 3 times
   Before: Each provider had identical baseFetch setup (4 lines x 3 = 12 lines)
   After: Extracted to getProviderFetch() helper (4 lines total)
   Savings: -8 lines

2. Import paths duplicated between registry and code
   Before: Package names in PROVIDER_REGISTRY + hardcoded in dynamic imports
   After: All imports use PROVIDER_REGISTRY as source of truth

   Example:
   - import("@openrouter/ai-sdk-provider") → import(PROVIDER_REGISTRY.openrouter)
   - preloadAISDKProviders also uses Object.values(PROVIDER_REGISTRY)

Benefits:
- Single source of truth for package names
- Adding new provider: update registry, get imports for free
- Impossible for registry and imports to get out of sync
- Less code to maintain (-14 lines net)
root and others added 5 commits November 11, 2025 19:23
…ssions

- Replace all dynamic imports with typed helper functions
- Change type to interface for OpenRouterReasoningOptions
- Remove 20+ eslint-disable comments across provider initialization
- Add importAnthropic/OpenAI/Ollama/OpenRouter helpers to providers.ts
- Single type cast for fetch compatibility (documented why it's safe)

Improves code quality without sacrificing type safety.
…plication

- Replace hardcoded package names with PROVIDER_REGISTRY values
- Centralizes package names to single source of truth
- Localize 4 eslint-disable comments to wrapper functions (vs 20+ throughout codebase)
- Add documentation explaining why eslint-disable is necessary

Net: -4 duplicate string literals
While using PROVIDER_REGISTRY values would eliminate duplication, it creates
worse type safety issues:

1. TypeScript can't infer return types from dynamic imports with variables
2. This forces 'any' casts which propagate to all call sites
3. Net result: 4 package name duplicates vs 20+ eslint-disable comments

Accepting the duplication as the lesser evil. Package names change rarely
and are validated by import() at runtime.
…duplication

Changed PROVIDER_REGISTRY from mapping names to package strings to mapping
names to import functions. This makes the registry the true single source
of truth while maintaining perfect type safety.

Changes:
- providers.ts: Reorganize to define import functions first, then registry
- aiService.ts: Call registry functions (e.g., PROVIDER_REGISTRY.anthropic())
- preloadAISDKProviders: Changed .map(pkg => import(pkg)) to .map(fn => fn())
- providers.test.ts: Updated test to verify registry values are functions

Benefits:
- Zero duplication (package names only in import functions)
- Perfect type inference (TypeScript infers from literal imports)
- Single source of truth (registry is the canonical provider list)
- Better semantics (registry maps to 'how to import' not 'what package')

No breaking changes - all existing usages (Object.keys, 'in' checks) work
identically.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants