feat(claude): add Claude Code Router (CCR) integration#750
feat(claude): add Claude Code Router (CCR) integration#750wcpaxx wants to merge 2 commits intoAutoMaker-Org:mainfrom
Conversation
📝 WalkthroughWalkthroughAdds Claude Code Router (CCR) support: server CCR utilities, CCR-aware Claude provider routing, settings endpoint, UI settings component and store flag, type additions, and a global settings accessor for centralized CCR toggling. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as UI Settings (React)
participant Store as App Store (State)
participant Server as Server (Node)
participant CCRUtil as CCR Utils (ccr.ts)
participant CCRServer as CCR CLI/Server (External)
participant Claude as Claude Provider
UI->>Server: GET /api/settings/ccr/status
activate Server
Server->>CCRUtil: getCCRStatus()
activate CCRUtil
CCRUtil->>CCRServer: run `ccr status` (or check install)
alt CCR installed & running
CCRServer-->>CCRUtil: parsed status (port, apiEndpoint)
CCRUtil-->>Server: {installed:true,running:true,port,apiEndpoint}
else not installed/running
CCRUtil-->>Server: {installed:false,running:false,error?}
end
deactivate CCRUtil
Server-->>UI: JSON status
deactivate Server
UI->>Store: setCcrEnabled(true)
activate Store
Store->>Server: POST /api/settings {ccrEnabled:true}
activate Server
Server->>Server: persist global settings
Server-->>Store: 200 OK
deactivate Server
Store-->>UI: update state
deactivate Store
UI->>Server: POST /api/claude/execute {ccrEnabled:true,...}
activate Server
Server->>Claude: executeQuery(..., ccrEnabled:true)
activate Claude
Claude->>CCRUtil: getCCRStatus()
activate CCRUtil
CCRUtil-->>Claude: {installed:true,running:true,port,apiEndpoint}
deactivate CCRUtil
Claude->>CCRUtil: getCCREnvFromStatus(status, config)
activate CCRUtil
CCRUtil-->>Claude: env vars for routing
deactivate CCRUtil
Claude->>CCRServer: forward request via CCR endpoint
CCRServer-->>Claude: response
Claude-->>Server: result
deactivate Claude
Server-->>UI: response payload
deactivate Server
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @wcpaxx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request integrates the Claude Code Router (CCR) into the application, providing users with the option to route their Claude API requests through a local proxy. This enhancement aims to offer more control over API routing, potentially enabling features like model switching and cost optimization. The changes span across backend utilities for CCR detection and configuration, modifications to the Claude provider to respect the new routing preference, and a user-friendly interface for managing this setting. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a significant and valuable feature: integration with the Claude Code Router (CCR). The changes are well-structured, spanning the server, UI, and shared types to provide a complete end-to-end experience for enabling and using CCR. The implementation includes auto-detection of CCR, a new API endpoint for status checks, and a corresponding UI component in the settings. The code is generally of high quality. My review includes a couple of suggestions to improve the efficiency of the CCR utility functions on the server-side by avoiding redundant function calls and file system access. Overall, this is a solid contribution.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/server/src/lib/ccr.ts`:
- Around line 40-92: The current blocking execSync usage in isCCRInstalled and
getCCRStatus can block the Node event loop; replace both execSync calls with
non-blocking child_process.execFile (or exec) used with Promises/async/await,
move getCCRStatus to return Promise<CCRStatus>, and update callers accordingly;
additionally add a small in-memory cache (e.g., Map or module-scoped variable)
in getCCRStatus keyed by e.g. 'ccr-status' with a short TTL (1000–5000ms) to
serve repeated requests without re-running commands, use getCCRConfig() as
before to obtain port fallback, ensure you implement timeout handling and
surface errors as the existing error field so behavior remains consistent.
In `@apps/ui/src/components/views/settings-view/claude/ccr-settings.tsx`:
- Around line 78-146: The toggle currently disables entirely when CCR isn't
installed/running (canEnable), preventing users from turning CCR off if it later
stops; change the logic so disabling only blocks enabling but still allows
disabling by using a condition like "disabled = !canEnable && !ccrEnabled" for
the Switch and update the Label opacity check from "!canEnable" to "!canEnable
&& !ccrEnabled" (referencing canEnable, ccrStatus, ccrEnabled,
onCcrEnabledChange, and the Switch/Label elements) so users can always turn CCR
off even if the service stops.
🧹 Nitpick comments (5)
apps/server/src/routes/settings/routes/get-ccr-status.ts (2)
7-9: Align CCR import with shared-package import rule.This relative import conflicts with the project-wide TS/TSX import guideline. Consider re-exporting the CCR helpers from a shared package (or an approved alias) and importing via
@automaker/* to avoid cross-module relative imports.As per coding guidelines:
**/*.{ts,tsx,js,jsx}: Always import from shared packages (@automaker/*), never from old paths or relative imports to other modules.
19-30: Route handler should delegate to a service and emit events.This route performs CCR status logic directly and doesn’t emit events. Please move the business logic into a service and emit via createEventEmitter() to match server architecture expectations.
As per coding guidelines: apps/server/src/**/*.{ts,tsx} → Server business logic should be organized into services in services/, with routes delegating to services; all server operations should emit events using createEventEmitter() from lib/events.ts.
apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx (1)
7-7: Prefer barrel export for intra-app UI imports.Please import CCRSettings from the components barrel rather than a direct path to keep intra-app imports stable and consistent.
Based on learnings: In the apps/ui codebase, when importing UI components within the same app, prefer barrel exports from ../components (i.e., import from the components index barrel) rather than direct path imports.
apps/server/src/providers/claude-provider.ts (1)
11-11: Use shared-package import path for CCR helpers.The new relative import conflicts with the project import rule. Consider re-exporting CCR helpers via an
@automaker/* entrypoint (or approved alias) and import from there.As per coding guidelines:
**/*.{ts,tsx,js,jsx}: Always import from shared packages (@automaker/*), never from old paths or relative imports to other modules.apps/ui/src/components/views/settings-view/claude/ccr-settings.tsx (1)
2-4: Prefer barrel exports for UI components.Use the app’s components barrel for Label/Switch rather than direct file paths to keep imports stable.
Based on learnings: In the apps/ui codebase, when importing UI components within the same app, prefer barrel exports from ../components (i.e., import from the components index barrel) rather than direct path imports.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/server/src/lib/ccr.ts`:
- Around line 85-88: The current detection uses stdout.includes('running') which
yields false-positives for strings like "Not running"; update the check that
sets isRunning to use a case-insensitive word-boundary regex that excludes the
phrase "not running" (for example use a negative lookbehind like
/\b(?<!not\s)running\b/i or an equivalent pattern) to test stdout, then set
CCRStatus accordingly (referencing isRunning, stdout, and CCRStatus in the same
function) so only true "running" states match.
🧹 Nitpick comments (2)
apps/server/src/routes/settings/routes/get-ccr-status.ts (1)
7-23: Move CCR status logic into a service and import via a shared package export.Route handlers should delegate to a service; also, the relative import to
lib/ccr.jsviolates the shared-package import rule. Consider introducing accr-serviceinapps/server/src/services/(which can emit a status event if needed) and re-exporting it through an@automaker/*package, then import it here.As per coding guidelines,
apps/server/src/**/*.{ts,tsx}: Server business logic should be organized into services in the services/ directory, with Express route handlers in routes/ that delegate to services; and**/*.{ts,tsx,js,jsx}: Always import from shared packages (@automaker/*), never from old paths or relative imports to other modules.apps/server/src/providers/claude-provider.ts (1)
10-11: Avoid relative CCR imports; expose via a shared package.The new
../lib/ccr.jsrelative import conflicts with the shared-package import rule. Please re-export CCR helpers through an@automaker/*package and import from there.As per coding guidelines,
**/*.{ts,tsx,js,jsx}: Always import from shared packages (@automaker/*), never from old paths or relative imports to other modules.
9caf5fe to
51cfb75
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/server/src/lib/ccr.ts`:
- Around line 30-35: The stripAnsi function uses a regex literal containing
control characters (\u001b and \u009b) which Biome flags; replace the regex
literal with a RegExp constructed from an escaped string (e.g. new
RegExp('...','g')) so the control characters are represented as double-escaped
sequences (\\u001b, \\u009b) inside the string, remove the inline eslint
disable, and ensure the RegExp preserves the same pattern and global flag used
in stripAnsi.
🧹 Nitpick comments (2)
apps/ui/src/components/views/settings-view/claude/ccr-settings.tsx (1)
2-3: Prefer the components barrel for UI imports.This keeps intra-app imports stable and consistent.
Based on learnings: In the apps/ui codebase, when importing UI components within the same app, prefer barrel exports from ../components (i.e., import from the components index barrel) rather than direct path imports. This improves import stability and refactorability within the app.♻️ Suggested change
-import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; +import { Label, Switch } from '@/components';apps/server/src/services/auto-mode-service.ts (1)
2346-2347: Consider consolidating settings fetch to avoid potential duplicate call.
getPhaseModelWithOverrides(called at lines 2340-2345) likely fetches global settings internally. Fetching them again here forccrEnabledmay result in duplicate async calls.💡 Potential optimization
If
getPhaseModelWithOverridesalready returns global settings or can be extended to includeccrEnabled, consolidate the fetch:// Option 1: Extend getPhaseModelWithOverrides to return ccrEnabled const { phaseModel: phaseModelEntry, provider: analysisClaudeProvider, credentials, ccrEnabled: analysisCcrEnabled, // Add to return type } = await getPhaseModelWithOverrides(...); // Option 2: Or keep as-is if the helper intentionally separates concerns
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/server/src/lib/ccr.ts`:
- Around line 85-109: The code currently hardcodes 127.0.0.1 when building the
CCR endpoint (see getCCRConfig usage and CCRStatus.apiEndpoint), so change
endpoint construction to honor CCRConfig.HOST: derive a host value like host =
config?.HOST ?? '127.0.0.1' and treat '0.0.0.0' as '127.0.0.1', then use that
host together with port to build apiEndpoint and any other base URLs (the same
change should be applied to the other block around lines 141-148 that also
builds a base URL).
🧹 Nitpick comments (1)
apps/ui/src/components/views/settings-view/claude/ccr-settings.tsx (1)
25-37: Consider adding fetch cleanup to prevent memory leaks.The useEffect fetches CCR status on mount but doesn't handle component unmount during an in-flight request. While React 18+ handles this more gracefully, adding an AbortController would be cleaner.
♻️ Optional: Add cleanup with AbortController
useEffect(() => { + const abortController = new AbortController(); const fetchStatus = async () => { try { - const response = await apiGet<CCRStatus>('/api/settings/ccr/status'); + const response = await apiGet<CCRStatus>('/api/settings/ccr/status', { + signal: abortController.signal, + }); setCcrStatus(response); } catch (error) { + if (abortController.signal.aborted) return; setCcrStatus({ installed: false, running: false, error: 'Failed to check CCR status' }); } finally { + if (!abortController.signal.aborted) { setIsLoading(false); + } } }; fetchStatus(); + return () => abortController.abort(); }, []);
- Add global setting to enable/disable CCR routing - Auto-detect CCR installation and server status - Route Claude Agent SDK requests through CCR when enabled - Add UI toggle with status indicator in settings - Optimize with async checks and caching
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/server/src/routes/worktree/routes/generate-commit-message.ts (1)
201-213:⚠️ Potential issue | 🟠 MajorAdd
resolveModelString()to resolve model aliases beforeexecuteQuery.The code path where
resolvePhaseModel()has aproviderIdset returns the model name unchanged without callingresolveModelString(). When combined withstripProviderPrefix(), which only removes prefixes and doesn't resolve aliases, an unresolved alias like "sonnet" could be passed directly toexecuteQuery(). Add the import and callresolveModelString(bareModel)to ensure full resolution before the API call.Proposed fix
-import { resolvePhaseModel } from '@automaker/model-resolver'; +import { resolvePhaseModel, resolveModelString } from '@automaker/model-resolver'; @@ const bareModel = stripProviderPrefix(model); +const resolvedModel = resolveModelString(bareModel); @@ - model: bareModel, + model: resolvedModel,
🧹 Nitpick comments (8)
apps/server/src/routes/github/routes/validate-issue.ts (1)
192-194: RedundantgetGlobalSettings()call.
settingsService.getGlobalSettings()is already called at line 158 to retrievephaseModels. Consider reusing that result instead of making a second async call.♻️ Suggested refactor to consolidate settings retrieval
// Use request overrides if provided, otherwise fall back to settings let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel; let effectiveReasoningEffort: ReasoningEffort | undefined = reasoningEffort; + let globalSettings: Awaited<ReturnType<typeof settingsService.getGlobalSettings>> | null = null; if (!effectiveThinkingLevel || !effectiveReasoningEffort) { - const settings = await settingsService?.getGlobalSettings(); + globalSettings = (await settingsService?.getGlobalSettings()) ?? null; const phaseModelEntry = - settings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel; + globalSettings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel; const resolved = resolvePhaseModel(phaseModelEntry); if (!effectiveThinkingLevel) { effectiveThinkingLevel = resolved.thinkingLevel; } if (!effectiveReasoningEffort && typeof phaseModelEntry !== 'string') { effectiveReasoningEffort = phaseModelEntry.reasoningEffort; } + } else if (settingsService) { + globalSettings = await settingsService.getGlobalSettings(); } // ... provider resolution code ... - // Get CCR setting from global settings - const globalSettings = settingsService ? await settingsService.getGlobalSettings() : null; const ccrEnabled = globalSettings?.ccrEnabled ?? false;apps/server/src/routes/suggestions/generate-suggestions.ts (1)
260-278: Optional simplification: Consider usingsettingsServiceauto-resolution.The
ccrEnabledpropagation is correct. However,streamingQueryaccepts an optionalsettingsServiceparameter that can auto-resolveccrEnabledfromglobalSettings.ccrEnabledinternally.If the logging on line 236 is not critical, you could simplify by passing
settingsServicetostreamingQuery:Suggested refactoring
const result = await streamingQuery({ prompt: finalPrompt, model, cwd: projectPath, ... - ccrEnabled, // Enable Claude Code Router for API routing + settingsService, // Auto-resolves ccrEnabled from globalSettings ... });This eliminates the manual
getGlobalSettings()call on line 227. However, if you prefer explicit logging for observability, the current approach is equally valid.apps/server/src/routes/features/routes/generate-title.ts (1)
64-84: Consider using settingsService auto-resolution instead of manual CCR flag extraction.The
simpleQueryfunction now supports asettingsServiceparameter that auto-resolvesccrEnabledfrom global settings when not explicitly provided. This could simplify the code:♻️ Suggested simplification
- // Get credentials and global settings for API calls (uses hardcoded haiku model, no phase setting) const credentials = await settingsService?.getCredentials(); - const globalSettings = settingsService ? await settingsService.getGlobalSettings() : null; - const ccrEnabled = globalSettings?.ccrEnabled ?? false; - - if (ccrEnabled) { - logger.info('CCR routing enabled'); - } const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`; // Use simpleQuery - provider abstraction handles all the streaming/extraction const result = await simpleQuery({ prompt: `${systemPrompt}\n\n${userPrompt}`, model: CLAUDE_MODEL_MAP.haiku, cwd: process.cwd(), maxTurns: 1, allowedTools: [], credentials, // Pass credentials for resolving 'credentials' apiKeySource - ccrEnabled, // Enable Claude Code Router for API routing + settingsService, // Auto-resolves ccrEnabled from global settings });apps/server/src/providers/simple-query-service.ts (1)
138-143: Duplicated CCR auto-resolution logic betweensimpleQueryandstreamingQuery.The auto-resolution logic is identical in both functions. Consider extracting to a helper:
♻️ Suggested helper extraction
// Helper function at module level async function resolveCcrEnabled( ccrEnabled: boolean | undefined, settingsService?: { getGlobalSettings: () => Promise<{ ccrEnabled?: boolean } | null> } ): Promise<boolean> { if (ccrEnabled !== undefined) { return ccrEnabled; } if (settingsService) { const globalSettings = await settingsService.getGlobalSettings(); return globalSettings?.ccrEnabled ?? false; } return false; }Then use in both functions:
const resolvedCcrEnabled = await resolveCcrEnabled(options.ccrEnabled, options.settingsService);Also applies to: 231-236
apps/server/src/routes/enhance-prompt/routes/enhance.ts (1)
153-176: Logging level inconsistency and opportunity to use auto-resolution.Two observations:
Logging inconsistency: This file uses
logger.debugfor "CCR routing enabled" (line 159) while other files likegenerate-title.tsuselogger.info. Consider standardizing across the codebase.Auto-resolution: Same as other routes, this could use the
settingsServiceparameter to auto-resolveccrEnabled.♻️ Suggested simplification
- // Get CCR setting from global settings - const globalSettings = settingsService ? await settingsService.getGlobalSettings() : null; - const ccrEnabled = globalSettings?.ccrEnabled ?? false; - logger.debug(`Using model: ${resolvedModel}`); - if (ccrEnabled) { - logger.debug('CCR routing enabled'); - } // Use simpleQuery - provider abstraction handles routing to correct provider const result = await simpleQuery({ prompt: `${systemPrompt}\n\n${userPrompt}`, model: resolvedModel, cwd: process.cwd(), maxTurns: 1, allowedTools: [], thinkingLevel, readOnly: true, credentials, claudeCompatibleProvider, - ccrEnabled, + settingsService, });apps/server/src/services/ideation-service.ts (1)
241-245: Consider a helper method for CCR flag resolution.The same pattern for resolving
ccrEnabledfrom global settings appears twice in this class (lines 241-245 and 710-715). Sincethis.settingsServiceis already available as a class member, consider adding a private helper method:♻️ Suggested helper method
private async getCcrEnabled(): Promise<boolean> { if (!this.settingsService) return false; const globalSettings = await this.settingsService.getGlobalSettings(); return globalSettings?.ccrEnabled ?? false; }Then simplify both call sites:
const ccrEnabled = await this.getCcrEnabled();Also applies to: 710-715
apps/server/src/routes/app-spec/generate-spec.ts (1)
118-161: LGTM with suggestion to use auto-resolution.The CCR integration is correctly implemented. The
streamingQueryfunction also supports thesettingsServiceparameter for auto-resolution, which could simplify this code similarly to other routes.apps/server/src/routes/worktree/routes/generate-commit-message.ts (1)
173-183: Move CCR enablement resolution into a service layer.This route now contains additional CCR business logic; consider delegating CCR enablement resolution to a service to keep routes thin and consistent.
As per coding guidelines, Server business logic should be organized into services in the services/ directory, with Express route handlers in routes/ that delegate to services.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/server/src/services/auto-mode-service.ts (1)
3436-3480:⚠️ Potential issue | 🟡 MinorGuard CCR lookup so settings failures don’t abort execution.
getGlobalSettings()failures will now fail the agent even though CCR is optional. Consider a defensive fallback to keep the run alive.Suggested adjustment
- const credentials = await this.settingsService?.getCredentials(); - const globalSettings = await this.settingsService?.getGlobalSettings(); - const ccrEnabled = globalSettings?.ccrEnabled ?? false; + const credentials = await this.settingsService?.getCredentials(); + let ccrEnabled = false; + try { + const globalSettings = await this.settingsService?.getGlobalSettings(); + ccrEnabled = globalSettings?.ccrEnabled ?? false; + } catch (error) { + logger.warn('Failed to load global settings for CCR; defaulting to disabled.', error); + }
🤖 Fix all issues with AI agents
In `@apps/server/src/providers/claude-provider.ts`:
- Around line 257-258: The log is using the raw ccrEnabled override instead of
the resolved value built by buildEnv; call buildEnv(...) once, capture its
return into a local (e.g., env or resolvedEnv), then use that
resolvedEnv.ccrEnabled (or equivalent property) for all logging and later logic
instead of the incoming ccrEnabled, and pass the same resolvedEnv to downstream
calls; update the log statements around where buildEnv is invoked (and the other
similar blocks referenced) to read the effective CCR flag from the resolved env
rather than the override.
🧹 Nitpick comments (2)
apps/server/src/lib/ccr.ts (1)
149-161: Consider documenting the expected CCR authentication header.The environment variable
ANTHROPIC_AUTH_TOKENis set (line 153), but the typical Claude SDK usesANTHROPIC_API_KEY. If CCR expects a different header name, this is correct; otherwise, consider aligning with the standard naming or adding a comment explaining the CCR-specific convention.apps/server/src/lib/global-settings-accessor.ts (1)
44-46: Guard against accidental re-initialization of the accessor.
If init is called twice (tests, hot reload), the provider can be silently replaced. Consider a small guard or explicit reset.Proposed tweak
export function initGlobalSettingsAccessor(provider: GlobalSettingsProvider): void { + if (settingsProvider && settingsProvider !== provider) { + throw new Error('Global settings accessor already initialized'); + } settingsProvider = provider; }
Replaced manual ccrEnabled propagation with a global settings accessor pattern. This allows ClaudeProvider to automatically resolve CCR settings without requiring every call site to pass the setting manually. - Added apps/server/src/lib/global-settings-accessor.ts - Initialized accessor in server startup (index.ts) - Updated ClaudeProvider to use auto-resolved CCR setting - Removed redundant ccrEnabled parameter passing from API routes This ensures all future Claude Agent SDK usage automatically supports CCR without additional boilerplate code. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
c01c17b to
c95cbc6
Compare
Summary by CodeRabbit
New Features
Bug Fixes