mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
* feat: refactor Claude API Profiles to Claude Compatible Providers
- Rename ClaudeApiProfile to ClaudeCompatibleProvider with models[] array
- Each ProviderModel has mapsToClaudeModel field for Claude tier mapping
- Add providerType field for provider-specific icons (glm, minimax, openrouter)
- Add thinking level support for provider models in phase selectors
- Show all mapped Claude models per provider model (e.g., "Maps to Haiku, Sonnet, Opus")
- Add Bulk Replace feature to switch all phases to a provider at once
- Hide Bulk Replace button when no providers are enabled
- Fix project-level phaseModelOverrides not persisting after refresh
- Fix deleting last provider not persisting (remove empty array guard)
- Add getProviderByModelId() helper for all SDK routes
- Update all routes to pass provider config for provider models
- Update terminology from "profiles" to "providers" throughout UI
- Update documentation to reflect new provider system
* fix: atomic writer race condition and bulk replace reset to defaults
1. AtomicWriter Race Condition Fix (libs/utils/src/atomic-writer.ts):
- Changed temp file naming from Date.now() to Date.now() + random hex
- Uses crypto.randomBytes(4).toString('hex') for uniqueness
- Prevents ENOENT errors when multiple concurrent writes happen
within the same millisecond
2. Bulk Replace "Anthropic Direct" Reset (both dialogs):
- When selecting "Anthropic Direct", now uses DEFAULT_PHASE_MODELS
- Properly resets thinking levels and other settings to defaults
- Added thinkingLevel to the change detection comparison
- Affects both global and project-level bulk replace dialogs
* fix: update tests for new model resolver passthrough behavior
1. model-resolver tests:
- Unknown models now pass through unchanged (provider model support)
- Removed expectations for warnings on unknown models
- Updated case sensitivity and edge case tests accordingly
- Added tests for provider-like model names (GLM-4.7, MiniMax-M2.1)
2. atomic-writer tests:
- Updated regex to match new temp file format with random suffix
- Format changed from .tmp.{timestamp} to .tmp.{timestamp}.{hex}
* refactor: simplify getPhaseModelWithOverrides calls per code review
Address code review feedback on PR #629:
- Make settingsService parameter optional in getPhaseModelWithOverrides
- Function now handles undefined settingsService gracefully by returning defaults
- Remove redundant ternary checks in 4 call sites:
- apps/server/src/routes/context/routes/describe-file.ts
- apps/server/src/routes/context/routes/describe-image.ts
- apps/server/src/routes/worktree/routes/generate-commit-message.ts
- apps/server/src/services/auto-mode-service.ts
- Remove unused DEFAULT_PHASE_MODELS imports where applicable
* test: fix server tests for provider model passthrough behavior
- Update model-resolver.test.ts to expect unknown models to pass through
unchanged (supports ClaudeCompatibleProvider models like GLM-4.7)
- Remove warning expectations for unknown models (valid for providers)
- Add missing getCredentials and getGlobalSettings mocks to
ideation-service.test.ts for settingsService
* fix: address code review feedback for model providers
- Honor thinkingLevel in generate-commit-message.ts
- Pass claudeCompatibleProvider in ideation-service.ts for provider models
- Resolve provider configuration for model overrides in generate-suggestions.ts
- Update "Active Profile" to "Active Provider" label in project-claude-section
- Use substring instead of deprecated substr in api-profiles-section
- Preserve provider enabled state when editing in api-profiles-section
* fix: address CodeRabbit review issues for Claude Compatible Providers
- Fix TypeScript TS2339 error in generate-suggestions.ts where
settingsService was narrowed to 'never' type in else branch
- Use DEFAULT_PHASE_MODELS per-phase defaults instead of hardcoded
'sonnet' in settings-helpers.ts
- Remove duplicate eventHooks key in use-settings-migration.ts
- Add claudeCompatibleProviders to localStorage migration parsing
and merging functions
- Handle canonical claude-* model IDs (claude-haiku, claude-sonnet,
claude-opus) in project-models-section display names
This resolves the CI build failures and addresses code review feedback.
* fix: skip broken list-view-priority E2E test and add Priority column label
- Skip list-view-priority.spec.ts with TODO explaining the infrastructure
issue: setupRealProject only sets localStorage but server settings
take precedence with localStorageMigrated: true
- Add 'Priority' label to list-header.tsx for the priority column
(was empty string, now shows proper header text)
- Increase column width to accommodate the label
The E2E test issue is that tests create features in a temp directory,
but the server loads from the E2E Test Project fixture path set in
setup-e2e-fixtures.mjs. Needs infrastructure fix to properly switch
projects or create features through UI instead of on disk.
412 lines
14 KiB
TypeScript
412 lines
14 KiB
TypeScript
/**
|
|
* Claude Provider - Executes queries using Claude Agent SDK
|
|
*
|
|
* Wraps the @anthropic-ai/claude-agent-sdk for seamless integration
|
|
* with the provider architecture.
|
|
*/
|
|
|
|
import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
|
|
import { BaseProvider } from './base-provider.js';
|
|
import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils';
|
|
|
|
const logger = createLogger('ClaudeProvider');
|
|
import {
|
|
getThinkingTokenBudget,
|
|
validateBareModelId,
|
|
type ClaudeApiProfile,
|
|
type ClaudeCompatibleProvider,
|
|
type Credentials,
|
|
} from '@automaker/types';
|
|
|
|
/**
|
|
* ProviderConfig - Union type for provider configuration
|
|
*
|
|
* Accepts either the legacy ClaudeApiProfile or new ClaudeCompatibleProvider.
|
|
* Both share the same connection settings structure.
|
|
*/
|
|
type ProviderConfig = ClaudeApiProfile | ClaudeCompatibleProvider;
|
|
import type {
|
|
ExecuteOptions,
|
|
ProviderMessage,
|
|
InstallationStatus,
|
|
ModelDefinition,
|
|
} from './types.js';
|
|
|
|
// Explicit allowlist of environment variables to pass to the SDK.
|
|
// Only these vars are passed - nothing else from process.env leaks through.
|
|
const ALLOWED_ENV_VARS = [
|
|
// Authentication
|
|
'ANTHROPIC_API_KEY',
|
|
'ANTHROPIC_AUTH_TOKEN',
|
|
// Endpoint configuration
|
|
'ANTHROPIC_BASE_URL',
|
|
'API_TIMEOUT_MS',
|
|
// Model mappings
|
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
// Traffic control
|
|
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
|
// System vars (always from process.env)
|
|
'PATH',
|
|
'HOME',
|
|
'SHELL',
|
|
'TERM',
|
|
'USER',
|
|
'LANG',
|
|
'LC_ALL',
|
|
];
|
|
|
|
// System vars are always passed from process.env regardless of profile
|
|
const SYSTEM_ENV_VARS = ['PATH', 'HOME', 'SHELL', 'TERM', 'USER', 'LANG', 'LC_ALL'];
|
|
|
|
/**
|
|
* Check if the config is a ClaudeCompatibleProvider (new system)
|
|
* by checking for the 'models' array property
|
|
*/
|
|
function isClaudeCompatibleProvider(config: ProviderConfig): config is ClaudeCompatibleProvider {
|
|
return 'models' in config && Array.isArray(config.models);
|
|
}
|
|
|
|
/**
|
|
* Build environment for the SDK with only explicitly allowed variables.
|
|
* When a provider/profile is provided, uses its configuration (clean switch - don't inherit from process.env).
|
|
* When no provider is provided, uses direct Anthropic API settings from process.env.
|
|
*
|
|
* Supports both:
|
|
* - ClaudeCompatibleProvider (new system with models[] array)
|
|
* - ClaudeApiProfile (legacy system with modelMappings)
|
|
*
|
|
* @param providerConfig - Optional provider configuration for alternative endpoint
|
|
* @param credentials - Optional credentials object for resolving 'credentials' apiKeySource
|
|
*/
|
|
function buildEnv(
|
|
providerConfig?: ProviderConfig,
|
|
credentials?: Credentials
|
|
): Record<string, string | undefined> {
|
|
const env: Record<string, string | undefined> = {};
|
|
|
|
if (providerConfig) {
|
|
// Use provider configuration (clean switch - don't inherit non-system vars from process.env)
|
|
logger.debug('[buildEnv] Using provider configuration:', {
|
|
name: providerConfig.name,
|
|
baseUrl: providerConfig.baseUrl,
|
|
apiKeySource: providerConfig.apiKeySource ?? 'inline',
|
|
isNewProvider: isClaudeCompatibleProvider(providerConfig),
|
|
});
|
|
|
|
// Resolve API key based on source strategy
|
|
let apiKey: string | undefined;
|
|
const source = providerConfig.apiKeySource ?? 'inline'; // Default to inline for backwards compat
|
|
|
|
switch (source) {
|
|
case 'inline':
|
|
apiKey = providerConfig.apiKey;
|
|
break;
|
|
case 'env':
|
|
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
break;
|
|
case 'credentials':
|
|
apiKey = credentials?.apiKeys?.anthropic;
|
|
break;
|
|
}
|
|
|
|
// Warn if no API key found
|
|
if (!apiKey) {
|
|
logger.warn(`No API key found for provider "${providerConfig.name}" with source "${source}"`);
|
|
}
|
|
|
|
// Authentication
|
|
if (providerConfig.useAuthToken) {
|
|
env['ANTHROPIC_AUTH_TOKEN'] = apiKey;
|
|
} else {
|
|
env['ANTHROPIC_API_KEY'] = apiKey;
|
|
}
|
|
|
|
// Endpoint configuration
|
|
env['ANTHROPIC_BASE_URL'] = providerConfig.baseUrl;
|
|
logger.debug(`[buildEnv] Set ANTHROPIC_BASE_URL to: ${providerConfig.baseUrl}`);
|
|
|
|
if (providerConfig.timeoutMs) {
|
|
env['API_TIMEOUT_MS'] = String(providerConfig.timeoutMs);
|
|
}
|
|
|
|
// Model mappings - only for legacy ClaudeApiProfile
|
|
// For ClaudeCompatibleProvider, the model is passed directly (no mapping needed)
|
|
if (!isClaudeCompatibleProvider(providerConfig) && providerConfig.modelMappings) {
|
|
if (providerConfig.modelMappings.haiku) {
|
|
env['ANTHROPIC_DEFAULT_HAIKU_MODEL'] = providerConfig.modelMappings.haiku;
|
|
}
|
|
if (providerConfig.modelMappings.sonnet) {
|
|
env['ANTHROPIC_DEFAULT_SONNET_MODEL'] = providerConfig.modelMappings.sonnet;
|
|
}
|
|
if (providerConfig.modelMappings.opus) {
|
|
env['ANTHROPIC_DEFAULT_OPUS_MODEL'] = providerConfig.modelMappings.opus;
|
|
}
|
|
}
|
|
|
|
// Traffic control
|
|
if (providerConfig.disableNonessentialTraffic) {
|
|
env['CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'] = '1';
|
|
}
|
|
} else {
|
|
// Use direct Anthropic API - pass through credentials or environment variables
|
|
// This supports:
|
|
// 1. API Key mode: ANTHROPIC_API_KEY from credentials (UI settings) or env
|
|
// 2. Claude Max plan: Uses CLI OAuth auth (SDK handles this automatically)
|
|
// 3. Custom endpoints via ANTHROPIC_BASE_URL env var (backward compatibility)
|
|
//
|
|
// Priority: credentials file (UI settings) -> environment variable
|
|
// Note: Only auth and endpoint vars are passed. Model mappings and traffic
|
|
// control are NOT passed (those require a profile for explicit configuration).
|
|
if (credentials?.apiKeys?.anthropic) {
|
|
env['ANTHROPIC_API_KEY'] = credentials.apiKeys.anthropic;
|
|
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
env['ANTHROPIC_API_KEY'] = process.env.ANTHROPIC_API_KEY;
|
|
}
|
|
// If using Claude Max plan via CLI auth, the SDK handles auth automatically
|
|
// when no API key is provided. We don't set ANTHROPIC_AUTH_TOKEN here
|
|
// unless it was explicitly set in process.env (rare edge case).
|
|
if (process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
env['ANTHROPIC_AUTH_TOKEN'] = process.env.ANTHROPIC_AUTH_TOKEN;
|
|
}
|
|
// Pass through ANTHROPIC_BASE_URL if set in environment (backward compatibility)
|
|
if (process.env.ANTHROPIC_BASE_URL) {
|
|
env['ANTHROPIC_BASE_URL'] = process.env.ANTHROPIC_BASE_URL;
|
|
}
|
|
}
|
|
|
|
// Always add system vars from process.env
|
|
for (const key of SYSTEM_ENV_VARS) {
|
|
if (process.env[key]) {
|
|
env[key] = process.env[key];
|
|
}
|
|
}
|
|
|
|
return env;
|
|
}
|
|
|
|
export class ClaudeProvider extends BaseProvider {
|
|
getName(): string {
|
|
return 'claude';
|
|
}
|
|
|
|
/**
|
|
* Execute a query using Claude Agent SDK
|
|
*/
|
|
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
|
// Validate that model doesn't have a provider prefix
|
|
// AgentService should strip prefixes before passing to providers
|
|
validateBareModelId(options.model, 'ClaudeProvider');
|
|
|
|
const {
|
|
prompt,
|
|
model,
|
|
cwd,
|
|
systemPrompt,
|
|
maxTurns = 20,
|
|
allowedTools,
|
|
abortController,
|
|
conversationHistory,
|
|
sdkSessionId,
|
|
thinkingLevel,
|
|
claudeApiProfile,
|
|
claudeCompatibleProvider,
|
|
credentials,
|
|
} = options;
|
|
|
|
// Determine which provider config to use
|
|
// claudeCompatibleProvider takes precedence over claudeApiProfile
|
|
const providerConfig = claudeCompatibleProvider || claudeApiProfile;
|
|
|
|
// Convert thinking level to token budget
|
|
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
|
|
|
|
// Build Claude SDK options
|
|
const sdkOptions: Options = {
|
|
model,
|
|
systemPrompt,
|
|
maxTurns,
|
|
cwd,
|
|
// Pass only explicitly allowed environment variables to SDK
|
|
// When a provider is active, uses provider settings (clean switch)
|
|
// When no provider, uses direct Anthropic API (from process.env or CLI OAuth)
|
|
env: buildEnv(providerConfig, credentials),
|
|
// Pass through allowedTools if provided by caller (decided by sdk-options.ts)
|
|
...(allowedTools && { allowedTools }),
|
|
// AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation
|
|
permissionMode: 'bypassPermissions',
|
|
allowDangerouslySkipPermissions: true,
|
|
abortController,
|
|
// Resume existing SDK session if we have a session ID
|
|
...(sdkSessionId && conversationHistory && conversationHistory.length > 0
|
|
? { resume: sdkSessionId }
|
|
: {}),
|
|
// Forward settingSources for CLAUDE.md file loading
|
|
...(options.settingSources && { settingSources: options.settingSources }),
|
|
// Forward MCP servers configuration
|
|
...(options.mcpServers && { mcpServers: options.mcpServers }),
|
|
// Extended thinking configuration
|
|
...(maxThinkingTokens && { maxThinkingTokens }),
|
|
// Subagents configuration for specialized task delegation
|
|
...(options.agents && { agents: options.agents }),
|
|
// Pass through outputFormat for structured JSON outputs
|
|
...(options.outputFormat && { outputFormat: options.outputFormat }),
|
|
};
|
|
|
|
// Build prompt payload
|
|
let promptPayload: string | AsyncIterable<any>;
|
|
|
|
if (Array.isArray(prompt)) {
|
|
// Multi-part prompt (with images)
|
|
promptPayload = (async function* () {
|
|
const multiPartPrompt = {
|
|
type: 'user' as const,
|
|
session_id: '',
|
|
message: {
|
|
role: 'user' as const,
|
|
content: prompt,
|
|
},
|
|
parent_tool_use_id: null,
|
|
};
|
|
yield multiPartPrompt;
|
|
})();
|
|
} else {
|
|
// Simple text prompt
|
|
promptPayload = prompt;
|
|
}
|
|
|
|
// Log the environment being passed to the SDK for debugging
|
|
const envForSdk = sdkOptions.env as Record<string, string | undefined>;
|
|
logger.debug('[ClaudeProvider] SDK Configuration:', {
|
|
model: sdkOptions.model,
|
|
baseUrl: envForSdk?.['ANTHROPIC_BASE_URL'] || '(default Anthropic API)',
|
|
hasApiKey: !!envForSdk?.['ANTHROPIC_API_KEY'],
|
|
hasAuthToken: !!envForSdk?.['ANTHROPIC_AUTH_TOKEN'],
|
|
providerName: providerConfig?.name || '(direct Anthropic)',
|
|
maxTurns: sdkOptions.maxTurns,
|
|
maxThinkingTokens: sdkOptions.maxThinkingTokens,
|
|
});
|
|
|
|
// Execute via Claude Agent SDK
|
|
try {
|
|
const stream = query({ prompt: promptPayload, options: sdkOptions });
|
|
|
|
// Stream messages directly - they're already in the correct format
|
|
for await (const msg of stream) {
|
|
yield msg as ProviderMessage;
|
|
}
|
|
} catch (error) {
|
|
// Enhance error with user-friendly message and classification
|
|
const errorInfo = classifyError(error);
|
|
const userMessage = getUserFriendlyErrorMessage(error);
|
|
|
|
logger.error('executeQuery() error during execution:', {
|
|
type: errorInfo.type,
|
|
message: errorInfo.message,
|
|
isRateLimit: errorInfo.isRateLimit,
|
|
retryAfter: errorInfo.retryAfter,
|
|
stack: (error as Error).stack,
|
|
});
|
|
|
|
// Build enhanced error message with additional guidance for rate limits
|
|
const message = errorInfo.isRateLimit
|
|
? `${userMessage}\n\nTip: If you're running multiple features in auto-mode, consider reducing concurrency (maxConcurrency setting) to avoid hitting rate limits.`
|
|
: userMessage;
|
|
|
|
const enhancedError = new Error(message);
|
|
(enhancedError as any).originalError = error;
|
|
(enhancedError as any).type = errorInfo.type;
|
|
|
|
if (errorInfo.isRateLimit) {
|
|
(enhancedError as any).retryAfter = errorInfo.retryAfter;
|
|
}
|
|
|
|
throw enhancedError;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detect Claude SDK installation (always available via npm)
|
|
*/
|
|
async detectInstallation(): Promise<InstallationStatus> {
|
|
// Claude SDK is always available since it's a dependency
|
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
|
|
const status: InstallationStatus = {
|
|
installed: true,
|
|
method: 'sdk',
|
|
hasApiKey,
|
|
authenticated: hasApiKey,
|
|
};
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Get available Claude models
|
|
*/
|
|
getAvailableModels(): ModelDefinition[] {
|
|
const models = [
|
|
{
|
|
id: 'claude-opus-4-5-20251101',
|
|
name: 'Claude Opus 4.5',
|
|
modelString: 'claude-opus-4-5-20251101',
|
|
provider: 'anthropic',
|
|
description: 'Most capable Claude model',
|
|
contextWindow: 200000,
|
|
maxOutputTokens: 16000,
|
|
supportsVision: true,
|
|
supportsTools: true,
|
|
tier: 'premium' as const,
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'claude-sonnet-4-20250514',
|
|
name: 'Claude Sonnet 4',
|
|
modelString: 'claude-sonnet-4-20250514',
|
|
provider: 'anthropic',
|
|
description: 'Balanced performance and cost',
|
|
contextWindow: 200000,
|
|
maxOutputTokens: 16000,
|
|
supportsVision: true,
|
|
supportsTools: true,
|
|
tier: 'standard' as const,
|
|
},
|
|
{
|
|
id: 'claude-3-5-sonnet-20241022',
|
|
name: 'Claude 3.5 Sonnet',
|
|
modelString: 'claude-3-5-sonnet-20241022',
|
|
provider: 'anthropic',
|
|
description: 'Fast and capable',
|
|
contextWindow: 200000,
|
|
maxOutputTokens: 8000,
|
|
supportsVision: true,
|
|
supportsTools: true,
|
|
tier: 'standard' as const,
|
|
},
|
|
{
|
|
id: 'claude-haiku-4-5-20251001',
|
|
name: 'Claude Haiku 4.5',
|
|
modelString: 'claude-haiku-4-5-20251001',
|
|
provider: 'anthropic',
|
|
description: 'Fastest Claude model',
|
|
contextWindow: 200000,
|
|
maxOutputTokens: 8000,
|
|
supportsVision: true,
|
|
supportsTools: true,
|
|
tier: 'basic' as const,
|
|
},
|
|
] satisfies ModelDefinition[];
|
|
return models;
|
|
}
|
|
|
|
/**
|
|
* Check if the provider supports a specific feature
|
|
*/
|
|
supportsFeature(feature: string): boolean {
|
|
const supportedFeatures = ['tools', 'text', 'vision', 'thinking'];
|
|
return supportedFeatures.includes(feature);
|
|
}
|
|
}
|