diff --git a/apps/server/src/lib/settings-helpers.ts b/apps/server/src/lib/settings-helpers.ts index a1bdc4e5..9dd9cf0f 100644 --- a/apps/server/src/lib/settings-helpers.ts +++ b/apps/server/src/lib/settings-helpers.ts @@ -5,7 +5,12 @@ import type { SettingsService } from '../services/settings-service.js'; import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils'; import { createLogger } from '@automaker/utils'; -import type { MCPServerConfig, McpServerConfig, PromptCustomization } from '@automaker/types'; +import type { + MCPServerConfig, + McpServerConfig, + PromptCustomization, + ClaudeApiProfile, +} from '@automaker/types'; import { mergeAutoModePrompts, mergeAgentPrompts, @@ -345,3 +350,47 @@ export async function getCustomSubagents( return Object.keys(merged).length > 0 ? merged : undefined; } + +/** + * Get the active Claude API profile from global settings. + * Returns undefined if no profile is active (uses direct Anthropic API). + * + * @param settingsService - Optional settings service instance + * @param logPrefix - Prefix for log messages (e.g., '[AgentService]') + * @returns Promise resolving to the active profile, or undefined if none active + */ +export async function getActiveClaudeApiProfile( + settingsService?: SettingsService | null, + logPrefix = '[SettingsHelper]' +): Promise { + if (!settingsService) { + return undefined; + } + + try { + const globalSettings = await settingsService.getGlobalSettings(); + const profiles = globalSettings.claudeApiProfiles || []; + const activeProfileId = globalSettings.activeClaudeApiProfileId; + + // No active profile selected - use direct Anthropic API + if (!activeProfileId) { + return undefined; + } + + // Find the active profile by ID + const activeProfile = profiles.find((p) => p.id === activeProfileId); + + if (activeProfile) { + logger.info(`${logPrefix} Using Claude API profile: ${activeProfile.name}`); + return activeProfile; + } else { + logger.warn( + `${logPrefix} Active profile ID "${activeProfileId}" not found, falling back to direct Anthropic API` + ); + return undefined; + } + } catch (error) { + logger.error(`${logPrefix} Failed to load Claude API profile:`, error); + return undefined; + } +} diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index f8a31d81..4e99901e 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -10,7 +10,11 @@ import { BaseProvider } from './base-provider.js'; import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils'; const logger = createLogger('ClaudeProvider'); -import { getThinkingTokenBudget, validateBareModelId } from '@automaker/types'; +import { + getThinkingTokenBudget, + validateBareModelId, + type ClaudeApiProfile, +} from '@automaker/types'; import type { ExecuteOptions, ProviderMessage, @@ -21,9 +25,19 @@ import type { // 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_BASE_URL', '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', @@ -33,16 +47,80 @@ const ALLOWED_ENV_VARS = [ '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']; + /** - * Build environment for the SDK with only explicitly allowed variables + * Build environment for the SDK with only explicitly allowed variables. + * When a profile is provided, uses profile configuration (clean switch - don't inherit from process.env). + * When no profile is provided, uses direct Anthropic API settings from process.env. + * + * @param profile - Optional Claude API profile for alternative endpoint configuration */ -function buildEnv(): Record { +function buildEnv(profile?: ClaudeApiProfile): Record { const env: Record = {}; - for (const key of ALLOWED_ENV_VARS) { + + if (profile) { + // Use profile configuration (clean switch - don't inherit non-system vars from process.env) + logger.debug('Building environment from Claude API profile:', { name: profile.name }); + + // Authentication + if (profile.useAuthToken) { + env['ANTHROPIC_AUTH_TOKEN'] = profile.apiKey; + } else { + env['ANTHROPIC_API_KEY'] = profile.apiKey; + } + + // Endpoint configuration + env['ANTHROPIC_BASE_URL'] = profile.baseUrl; + + if (profile.timeoutMs) { + env['API_TIMEOUT_MS'] = String(profile.timeoutMs); + } + + // Model mappings + if (profile.modelMappings?.haiku) { + env['ANTHROPIC_DEFAULT_HAIKU_MODEL'] = profile.modelMappings.haiku; + } + if (profile.modelMappings?.sonnet) { + env['ANTHROPIC_DEFAULT_SONNET_MODEL'] = profile.modelMappings.sonnet; + } + if (profile.modelMappings?.opus) { + env['ANTHROPIC_DEFAULT_OPUS_MODEL'] = profile.modelMappings.opus; + } + + // Traffic control + if (profile.disableNonessentialTraffic) { + env['CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'] = '1'; + } + } else { + // Use direct Anthropic API - two modes: + // 1. API Key mode: ANTHROPIC_API_KEY from credentials/env + // 2. Claude Max plan: Uses CLI OAuth auth (SDK handles this automatically) + // + // IMPORTANT: Do NOT set any profile vars (base URL, model mappings, etc.) + // This ensures clean switching - only pass through what's in process.env + 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; + } + // Do NOT set ANTHROPIC_BASE_URL - let SDK use default Anthropic endpoint + // Do NOT set model mappings - use standard Claude model names + // Do NOT set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC + } + + // 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; } @@ -70,6 +148,7 @@ export class ClaudeProvider extends BaseProvider { conversationHistory, sdkSessionId, thinkingLevel, + claudeApiProfile, } = options; // Convert thinking level to token budget @@ -82,7 +161,9 @@ export class ClaudeProvider extends BaseProvider { maxTurns, cwd, // Pass only explicitly allowed environment variables to SDK - env: buildEnv(), + // When a profile is active, uses profile settings (clean switch) + // When no profile, uses direct Anthropic API (from process.env or CLI OAuth) + env: buildEnv(claudeApiProfile), // Pass through allowedTools if provided by caller (decided by sdk-options.ts) ...(allowedTools && { allowedTools }), // AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation diff --git a/apps/server/src/providers/simple-query-service.ts b/apps/server/src/providers/simple-query-service.ts index 5882b96f..f797fd5c 100644 --- a/apps/server/src/providers/simple-query-service.ts +++ b/apps/server/src/providers/simple-query-service.ts @@ -20,6 +20,7 @@ import type { ContentBlock, ThinkingLevel, ReasoningEffort, + ClaudeApiProfile, } from '@automaker/types'; import { stripProviderPrefix } from '@automaker/types'; @@ -54,6 +55,8 @@ export interface SimpleQueryOptions { readOnly?: boolean; /** Setting sources for CLAUDE.md loading */ settingSources?: Array<'user' | 'project' | 'local'>; + /** Active Claude API profile for alternative endpoint configuration */ + claudeApiProfile?: ClaudeApiProfile; } /** @@ -125,6 +128,7 @@ export async function simpleQuery(options: SimpleQueryOptions): Promise { logger.debug(`Feature text block received (${text.length} chars)`); events.emit('spec-regeneration:event', { diff --git a/apps/server/src/routes/app-spec/generate-spec.ts b/apps/server/src/routes/app-spec/generate-spec.ts index 4fa3d11a..233e4e9a 100644 --- a/apps/server/src/routes/app-spec/generate-spec.ts +++ b/apps/server/src/routes/app-spec/generate-spec.ts @@ -16,7 +16,11 @@ import { streamingQuery } from '../../providers/simple-query-service.js'; import { generateFeaturesFromSpec } from './generate-features-from-spec.js'; import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform'; import type { SettingsService } from '../../services/settings-service.js'; -import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js'; +import { + getAutoLoadClaudeMdSetting, + getPromptCustomization, + getActiveClaudeApiProfile, +} from '../../lib/settings-helpers.js'; const logger = createLogger('SpecRegeneration'); @@ -100,6 +104,9 @@ ${prompts.appSpec.structuredSpecInstructions}`; logger.info('Using model:', model); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[SpecRegeneration]'); + let responseText = ''; let structuredOutput: SpecOutput | null = null; @@ -132,6 +139,7 @@ Your entire response should be valid JSON starting with { and ending with }. No thinkingLevel, readOnly: true, // Spec generation only reads code, we write the spec ourselves settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration outputFormat: useStructuredOutput ? { type: 'json_schema', diff --git a/apps/server/src/routes/app-spec/sync-spec.ts b/apps/server/src/routes/app-spec/sync-spec.ts index 98352855..14526865 100644 --- a/apps/server/src/routes/app-spec/sync-spec.ts +++ b/apps/server/src/routes/app-spec/sync-spec.ts @@ -15,7 +15,10 @@ import { resolvePhaseModel } from '@automaker/model-resolver'; import { streamingQuery } from '../../providers/simple-query-service.js'; import { getAppSpecPath } from '@automaker/platform'; import type { SettingsService } from '../../services/settings-service.js'; -import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; +import { + getAutoLoadClaudeMdSetting, + getActiveClaudeApiProfile, +} from '../../lib/settings-helpers.js'; import { FeatureLoader } from '../../services/feature-loader.js'; import { extractImplementedFeatures, @@ -157,6 +160,9 @@ export async function syncSpec( settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel; const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[SpecSync]'); + // Use AI to analyze tech stack const techAnalysisPrompt = `Analyze this project and return ONLY a JSON object with the current technology stack. @@ -185,6 +191,7 @@ Return ONLY this JSON format, no other text: thinkingLevel, readOnly: true, settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration onText: (text) => { logger.debug(`Tech analysis text: ${text.substring(0, 100)}`); }, diff --git a/apps/server/src/routes/backlog-plan/generate-plan.ts b/apps/server/src/routes/backlog-plan/generate-plan.ts index e96ce8ea..b0c7eaed 100644 --- a/apps/server/src/routes/backlog-plan/generate-plan.ts +++ b/apps/server/src/routes/backlog-plan/generate-plan.ts @@ -25,7 +25,11 @@ import { saveBacklogPlan, } from './common.js'; import type { SettingsService } from '../../services/settings-service.js'; -import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js'; +import { + getAutoLoadClaudeMdSetting, + getPromptCustomization, + getActiveClaudeApiProfile, +} from '../../lib/settings-helpers.js'; const featureLoader = new FeatureLoader(); @@ -161,6 +165,9 @@ ${userPrompt}`; finalSystemPrompt = undefined; // System prompt is now embedded in the user prompt } + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[BacklogPlan]'); + // Execute the query const stream = provider.executeQuery({ prompt: finalPrompt, @@ -173,6 +180,7 @@ ${userPrompt}`; settingSources: autoLoadClaudeMd ? ['user', 'project'] : undefined, readOnly: true, // Plan generation only generates text, doesn't write files thinkingLevel, // Pass thinking level for extended thinking + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }); let responseText = ''; diff --git a/apps/server/src/routes/context/routes/describe-file.ts b/apps/server/src/routes/context/routes/describe-file.ts index 5b1fc6ca..d8ba073a 100644 --- a/apps/server/src/routes/context/routes/describe-file.ts +++ b/apps/server/src/routes/context/routes/describe-file.ts @@ -22,6 +22,7 @@ import type { SettingsService } from '../../../services/settings-service.js'; import { getAutoLoadClaudeMdSetting, getPromptCustomization, + getActiveClaudeApiProfile, } from '../../../lib/settings-helpers.js'; const logger = createLogger('DescribeFile'); @@ -165,6 +166,9 @@ ${contentToAnalyze}`; logger.info(`Resolved model: ${model}, thinkingLevel: ${thinkingLevel}`); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[DescribeFile]'); + // Use simpleQuery - provider abstraction handles routing to correct provider const result = await simpleQuery({ prompt, @@ -175,6 +179,7 @@ ${contentToAnalyze}`; thinkingLevel, readOnly: true, // File description only reads, doesn't write settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }); const description = result.text; diff --git a/apps/server/src/routes/context/routes/describe-image.ts b/apps/server/src/routes/context/routes/describe-image.ts index 70f9f7dc..ea54273c 100644 --- a/apps/server/src/routes/context/routes/describe-image.ts +++ b/apps/server/src/routes/context/routes/describe-image.ts @@ -22,6 +22,7 @@ import type { SettingsService } from '../../../services/settings-service.js'; import { getAutoLoadClaudeMdSetting, getPromptCustomization, + getActiveClaudeApiProfile, } from '../../../lib/settings-helpers.js'; const logger = createLogger('DescribeImage'); @@ -284,6 +285,9 @@ export function createDescribeImageHandler( // Get customized prompts from settings const prompts = await getPromptCustomization(settingsService, '[DescribeImage]'); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[DescribeImage]'); + // Build the instruction text from centralized prompts const instructionText = prompts.contextDescription.describeImagePrompt; @@ -325,6 +329,7 @@ export function createDescribeImageHandler( thinkingLevel, readOnly: true, // Image description only reads, doesn't write settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }); logger.info(`[${requestId}] simpleQuery completed in ${Date.now() - queryStart}ms`); diff --git a/apps/server/src/routes/enhance-prompt/routes/enhance.ts b/apps/server/src/routes/enhance-prompt/routes/enhance.ts index 5861b418..20eaf56d 100644 --- a/apps/server/src/routes/enhance-prompt/routes/enhance.ts +++ b/apps/server/src/routes/enhance-prompt/routes/enhance.ts @@ -12,7 +12,10 @@ import { resolveModelString } from '@automaker/model-resolver'; import { CLAUDE_MODEL_MAP, type ThinkingLevel } from '@automaker/types'; import { simpleQuery } from '../../../providers/simple-query-service.js'; import type { SettingsService } from '../../../services/settings-service.js'; -import { getPromptCustomization } from '../../../lib/settings-helpers.js'; +import { + getPromptCustomization, + getActiveClaudeApiProfile, +} from '../../../lib/settings-helpers.js'; import { buildUserPrompt, isValidEnhancementMode, @@ -126,6 +129,9 @@ export function createEnhanceHandler( logger.debug(`Using model: ${resolvedModel}`); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[EnhancePrompt]'); + // Use simpleQuery - provider abstraction handles routing to correct provider // The system prompt is combined with user prompt since some providers // don't have a separate system prompt concept @@ -137,6 +143,7 @@ export function createEnhanceHandler( allowedTools: [], thinkingLevel, readOnly: true, // Prompt enhancement only generates text, doesn't write files + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }); const enhancedText = result.text; diff --git a/apps/server/src/routes/features/routes/generate-title.ts b/apps/server/src/routes/features/routes/generate-title.ts index e7603eb8..1cbccf5b 100644 --- a/apps/server/src/routes/features/routes/generate-title.ts +++ b/apps/server/src/routes/features/routes/generate-title.ts @@ -10,7 +10,10 @@ import { createLogger } from '@automaker/utils'; import { CLAUDE_MODEL_MAP } from '@automaker/model-resolver'; import { simpleQuery } from '../../../providers/simple-query-service.js'; import type { SettingsService } from '../../../services/settings-service.js'; -import { getPromptCustomization } from '../../../lib/settings-helpers.js'; +import { + getPromptCustomization, + getActiveClaudeApiProfile, +} from '../../../lib/settings-helpers.js'; const logger = createLogger('GenerateTitle'); @@ -60,6 +63,9 @@ export function createGenerateTitleHandler( const prompts = await getPromptCustomization(settingsService, '[GenerateTitle]'); const systemPrompt = prompts.titleGeneration.systemPrompt; + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[GenerateTitle]'); + const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`; // Use simpleQuery - provider abstraction handles all the streaming/extraction @@ -69,6 +75,7 @@ export function createGenerateTitleHandler( cwd: process.cwd(), maxTurns: 1, allowedTools: [], + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }); const title = result.text; diff --git a/apps/server/src/routes/github/routes/validate-issue.ts b/apps/server/src/routes/github/routes/validate-issue.ts index e7d83d99..2a314295 100644 --- a/apps/server/src/routes/github/routes/validate-issue.ts +++ b/apps/server/src/routes/github/routes/validate-issue.ts @@ -34,7 +34,11 @@ import { ValidationComment, ValidationLinkedPR, } from './validation-schema.js'; -import { getPromptCustomization } from '../../../lib/settings-helpers.js'; +import { + getPromptCustomization, + getAutoLoadClaudeMdSetting, + getActiveClaudeApiProfile, +} from '../../../lib/settings-helpers.js'; import { trySetValidationRunning, clearValidationStatus, @@ -43,7 +47,6 @@ import { logger, } from './validation-common.js'; import type { SettingsService } from '../../../services/settings-service.js'; -import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; /** * Request body for issue validation @@ -166,6 +169,9 @@ ${basePrompt}`; logger.info(`Using model: ${model}`); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[IssueValidation]'); + // Use streamingQuery with event callbacks const result = await streamingQuery({ prompt: finalPrompt, @@ -177,6 +183,7 @@ ${basePrompt}`; reasoningEffort: effectiveReasoningEffort, readOnly: true, // Issue validation only reads code, doesn't write settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration outputFormat: useStructuredOutput ? { type: 'json_schema', diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index 08a3628b..8dd2bccf 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -15,7 +15,11 @@ import { FeatureLoader } from '../../services/feature-loader.js'; import { getAppSpecPath } from '@automaker/platform'; import * as secureFs from '../../lib/secure-fs.js'; import type { SettingsService } from '../../services/settings-service.js'; -import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js'; +import { + getAutoLoadClaudeMdSetting, + getPromptCustomization, + getActiveClaudeApiProfile, +} from '../../lib/settings-helpers.js'; const logger = createLogger('Suggestions'); @@ -192,6 +196,9 @@ ${prompts.suggestions.baseTemplate}`; logger.info('[Suggestions] Using model:', model); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[Suggestions]'); + let responseText = ''; // Determine if we should use structured output (Claude supports it, Cursor doesn't) @@ -223,6 +230,7 @@ Your entire response should be valid JSON starting with { and ending with }. No thinkingLevel, readOnly: true, // Suggestions only reads code, doesn't write settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration outputFormat: useStructuredOutput ? { type: 'json_schema', diff --git a/apps/server/src/routes/worktree/routes/generate-commit-message.ts b/apps/server/src/routes/worktree/routes/generate-commit-message.ts index a450659f..86a11756 100644 --- a/apps/server/src/routes/worktree/routes/generate-commit-message.ts +++ b/apps/server/src/routes/worktree/routes/generate-commit-message.ts @@ -10,7 +10,6 @@ import { exec } from 'child_process'; import { promisify } from 'util'; import { existsSync } from 'fs'; import { join } from 'path'; -import { query } from '@anthropic-ai/claude-agent-sdk'; import { createLogger } from '@automaker/utils'; import { DEFAULT_PHASE_MODELS, isCursorModel, stripProviderPrefix } from '@automaker/types'; import { resolvePhaseModel } from '@automaker/model-resolver'; @@ -18,6 +17,7 @@ import { mergeCommitMessagePrompts } from '@automaker/prompts'; import { ProviderFactory } from '../../../providers/provider-factory.js'; import type { SettingsService } from '../../../services/settings-service.js'; import { getErrorMessage, logError } from '../common.js'; +import { getActiveClaudeApiProfile } from '../../../lib/settings-helpers.js'; const logger = createLogger('GenerateCommitMessage'); const execAsync = promisify(exec); @@ -74,33 +74,6 @@ interface GenerateCommitMessageErrorResponse { error: string; } -async function extractTextFromStream( - stream: AsyncIterable<{ - type: string; - subtype?: string; - result?: string; - message?: { - content?: Array<{ type: string; text?: string }>; - }; - }> -): Promise { - let responseText = ''; - - for await (const msg of stream) { - if (msg.type === 'assistant' && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === 'text' && block.text) { - responseText += block.text; - } - } - } else if (msg.type === 'result' && msg.subtype === 'success') { - responseText = msg.result || responseText; - } - } - - return responseText; -} - export function createGenerateCommitMessageHandler( settingsService?: SettingsService ): (req: Request, res: Response) => Promise { @@ -195,57 +168,52 @@ export function createGenerateCommitMessageHandler( // Get the effective system prompt (custom or default) const systemPrompt = await getSystemPrompt(settingsService); - let message: string; + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile( + settingsService, + '[GenerateCommitMessage]' + ); - // Route to appropriate provider based on model type - if (isCursorModel(model)) { - // Use Cursor provider for Cursor models - logger.info(`Using Cursor provider for model: ${model}`); + // Get provider for the model type + const provider = ProviderFactory.getProviderForModel(model); + const bareModel = stripProviderPrefix(model); - const provider = ProviderFactory.getProviderForModel(model); - const bareModel = stripProviderPrefix(model); + // For Cursor models, combine prompts since Cursor doesn't support systemPrompt separation + const effectivePrompt = isCursorModel(model) + ? `${systemPrompt}\n\n${userPrompt}` + : userPrompt; + const effectiveSystemPrompt = isCursorModel(model) ? undefined : systemPrompt; - const cursorPrompt = `${systemPrompt}\n\n${userPrompt}`; + logger.info(`Using ${provider.getName()} provider for model: ${model}`); - let responseText = ''; - const cursorStream = provider.executeQuery({ - prompt: cursorPrompt, - model: bareModel, - cwd: worktreePath, - maxTurns: 1, - allowedTools: [], - readOnly: true, - }); + let responseText = ''; + const stream = provider.executeQuery({ + prompt: effectivePrompt, + model: bareModel, + cwd: worktreePath, + systemPrompt: effectiveSystemPrompt, + maxTurns: 1, + allowedTools: [], + readOnly: true, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration + }); - // Wrap with timeout to prevent indefinite hangs - for await (const msg of withTimeout(cursorStream, AI_TIMEOUT_MS)) { - if (msg.type === 'assistant' && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === 'text' && block.text) { - responseText += block.text; - } + // Wrap with timeout to prevent indefinite hangs + for await (const msg of withTimeout(stream, AI_TIMEOUT_MS)) { + if (msg.type === 'assistant' && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === 'text' && block.text) { + responseText += block.text; } } + } else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) { + // Use result if available (some providers return final text here) + responseText = msg.result; } - - message = responseText.trim(); - } else { - // Use Claude SDK for Claude models - const stream = query({ - prompt: userPrompt, - options: { - model, - systemPrompt, - maxTurns: 1, - allowedTools: [], - permissionMode: 'default', - }, - }); - - // Wrap with timeout to prevent indefinite hangs - message = await extractTextFromStream(withTimeout(stream, AI_TIMEOUT_MS)); } + const message = responseText.trim(); + if (!message || message.trim().length === 0) { logger.warn('Received empty response from model'); const response: GenerateCommitMessageErrorResponse = { diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 359719d3..49105df0 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -29,6 +29,7 @@ import { getSkillsConfiguration, getSubagentsConfiguration, getCustomSubagents, + getActiveClaudeApiProfile, } from '../lib/settings-helpers.js'; interface Message { @@ -274,6 +275,12 @@ export class AgentService { ? await getCustomSubagents(this.settingsService, effectiveWorkDir) : undefined; + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile( + this.settingsService, + '[AgentService]' + ); + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) and memory files // Use the user's message as task context for smart memory selection const contextResult = await loadContextFiles({ @@ -378,6 +385,7 @@ export class AgentService { agents: customSubagents, // Pass custom subagents for task delegation thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models reasoningEffort: effectiveReasoningEffort, // Pass reasoning effort for Codex models + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }; // Build prompt content with images diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 454b7ec0..0b807100 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -63,6 +63,7 @@ import { filterClaudeMdFromContext, getMCPServersFromSettings, getPromptCustomization, + getActiveClaudeApiProfile, } from '../lib/settings-helpers.js'; import { getNotificationService } from './notification-service.js'; @@ -1708,6 +1709,9 @@ Format your response as a structured markdown document.`; thinkingLevel: analysisThinkingLevel, }); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(this.settingsService, '[AutoMode]'); + const options: ExecuteOptions = { prompt, model: sdkOptions.model ?? analysisModel, @@ -1717,6 +1721,7 @@ Format your response as a structured markdown document.`; abortController, settingSources: sdkOptions.settingSources, thinkingLevel: analysisThinkingLevel, // Pass thinking level + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }; const stream = provider.executeQuery(options); @@ -2536,6 +2541,9 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. ); } + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile(this.settingsService, '[AutoMode]'); + const executeOptions: ExecuteOptions = { prompt: promptContent, model: bareModel, @@ -2547,6 +2555,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. settingSources: sdkOptions.settingSources, mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration thinkingLevel: options?.thinkingLevel, // Pass thinking level for extended thinking + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }; // Execute via provider @@ -2849,6 +2858,7 @@ After generating the revised spec, output: allowedTools: allowedTools, abortController, mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }); let revisionText = ''; @@ -2994,6 +3004,7 @@ After generating the revised spec, output: allowedTools: allowedTools, abortController, mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }); let taskOutput = ''; @@ -3088,6 +3099,7 @@ After generating the revised spec, output: allowedTools: allowedTools, abortController, mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }); for await (const msg of continuationStream) { diff --git a/apps/server/src/services/ideation-service.ts b/apps/server/src/services/ideation-service.ts index 4ef3d8a8..16769fca 100644 --- a/apps/server/src/services/ideation-service.ts +++ b/apps/server/src/services/ideation-service.ts @@ -41,7 +41,7 @@ import type { FeatureLoader } from './feature-loader.js'; import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js'; import { resolveModelString } from '@automaker/model-resolver'; import { stripProviderPrefix } from '@automaker/types'; -import { getPromptCustomization } from '../lib/settings-helpers.js'; +import { getPromptCustomization, getActiveClaudeApiProfile } from '../lib/settings-helpers.js'; const logger = createLogger('IdeationService'); @@ -223,6 +223,12 @@ export class IdeationService { // Strip provider prefix - providers need bare model IDs const bareModel = stripProviderPrefix(modelId); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile( + this.settingsService, + '[IdeationService]' + ); + const executeOptions: ExecuteOptions = { prompt: message, model: bareModel, @@ -232,6 +238,7 @@ export class IdeationService { maxTurns: 1, // Single turn for ideation abortController: activeSession.abortController!, conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined, + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }; const stream = provider.executeQuery(executeOptions); @@ -678,6 +685,12 @@ export class IdeationService { // Strip provider prefix - providers need bare model IDs const bareModel = stripProviderPrefix(modelId); + // Get active Claude API profile for alternative endpoint configuration + const claudeApiProfile = await getActiveClaudeApiProfile( + this.settingsService, + '[IdeationService]' + ); + const executeOptions: ExecuteOptions = { prompt: prompt.prompt, model: bareModel, @@ -688,6 +701,7 @@ export class IdeationService { // Disable all tools - we just want text generation, not codebase analysis allowedTools: [], abortController: new AbortController(), + claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration }; const stream = provider.executeQuery(executeOptions); diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx index 38b34c4c..4d69c07d 100644 --- a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx @@ -7,6 +7,7 @@ import { ClaudeMdSettings } from '../claude/claude-md-settings'; import { ClaudeUsageSection } from '../api-keys/claude-usage-section'; import { SkillsSection } from './claude-settings-tab/skills-section'; import { SubagentsSection } from './claude-settings-tab/subagents-section'; +import { ApiProfilesSection } from './claude-settings-tab/api-profiles-section'; import { ProviderToggle } from './provider-toggle'; import { Info } from 'lucide-react'; @@ -45,6 +46,10 @@ export function ClaudeSettingsTab() { isChecking={isCheckingClaudeCli} onRefresh={handleRefreshClaudeCli} /> + + {/* API Profiles for Claude-compatible endpoints */} + + (null); + const [formData, setFormData] = useState(emptyFormData); + const [showApiKey, setShowApiKey] = useState(false); + const [deleteConfirmId, setDeleteConfirmId] = useState(null); + const [currentTemplate, setCurrentTemplate] = useState< + (typeof CLAUDE_API_PROFILE_TEMPLATES)[0] | null + >(null); + + const handleOpenAddDialog = (templateName?: string) => { + const template = templateName + ? CLAUDE_API_PROFILE_TEMPLATES.find((t) => t.name === templateName) + : undefined; + + if (template) { + setFormData({ + name: template.name, + baseUrl: template.baseUrl, + apiKey: '', + useAuthToken: template.useAuthToken, + timeoutMs: template.timeoutMs?.toString() ?? '', + modelMappings: { + haiku: template.modelMappings?.haiku ?? '', + sonnet: template.modelMappings?.sonnet ?? '', + opus: template.modelMappings?.opus ?? '', + }, + disableNonessentialTraffic: template.disableNonessentialTraffic ?? false, + }); + setCurrentTemplate(template); + } else { + setFormData(emptyFormData); + setCurrentTemplate(null); + } + + setEditingProfileId(null); + setShowApiKey(false); + setIsDialogOpen(true); + }; + + const handleOpenEditDialog = (profile: ClaudeApiProfile) => { + // Find matching template by base URL + const template = CLAUDE_API_PROFILE_TEMPLATES.find((t) => t.baseUrl === profile.baseUrl); + + setFormData({ + name: profile.name, + baseUrl: profile.baseUrl, + apiKey: profile.apiKey, + useAuthToken: profile.useAuthToken ?? false, + timeoutMs: profile.timeoutMs?.toString() ?? '', + modelMappings: { + haiku: profile.modelMappings?.haiku ?? '', + sonnet: profile.modelMappings?.sonnet ?? '', + opus: profile.modelMappings?.opus ?? '', + }, + disableNonessentialTraffic: profile.disableNonessentialTraffic ?? false, + }); + setEditingProfileId(profile.id); + setCurrentTemplate(template ?? null); + setShowApiKey(false); + setIsDialogOpen(true); + }; + + const handleSave = () => { + const profileData: ClaudeApiProfile = { + id: editingProfileId ?? generateProfileId(), + name: formData.name.trim(), + baseUrl: formData.baseUrl.trim(), + apiKey: formData.apiKey, + useAuthToken: formData.useAuthToken, + timeoutMs: formData.timeoutMs ? parseInt(formData.timeoutMs, 10) : undefined, + modelMappings: + formData.modelMappings.haiku || formData.modelMappings.sonnet || formData.modelMappings.opus + ? { + ...(formData.modelMappings.haiku && { haiku: formData.modelMappings.haiku }), + ...(formData.modelMappings.sonnet && { sonnet: formData.modelMappings.sonnet }), + ...(formData.modelMappings.opus && { opus: formData.modelMappings.opus }), + } + : undefined, + disableNonessentialTraffic: formData.disableNonessentialTraffic || undefined, + }; + + if (editingProfileId) { + updateClaudeApiProfile(editingProfileId, profileData); + } else { + addClaudeApiProfile(profileData); + } + + setIsDialogOpen(false); + setFormData(emptyFormData); + setEditingProfileId(null); + }; + + const handleDelete = (id: string) => { + deleteClaudeApiProfile(id); + setDeleteConfirmId(null); + }; + + const isFormValid = + formData.name.trim().length > 0 && + formData.baseUrl.trim().length > 0 && + formData.apiKey.length > 0; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

API Profiles

+

Manage Claude-compatible API endpoints

+
+
+ + + + + + handleOpenAddDialog()}> + + Custom Profile + + + {CLAUDE_API_PROFILE_TEMPLATES.map((template) => ( + handleOpenAddDialog(template.name)} + > + + {template.name} + + ))} + + +
+ + {/* Content */} +
+ {/* Active Profile Selector */} +
+ + +

+ {activeClaudeApiProfileId + ? 'Using custom API endpoint' + : 'Using direct Anthropic API (API key or Claude Max plan)'} +

+
+ + {/* Profile List */} + {claudeApiProfiles.length === 0 ? ( +
+ +

No API profiles configured

+

+ Add a profile to use alternative Claude-compatible endpoints +

+
+ ) : ( +
+ {claudeApiProfiles.map((profile) => ( + handleOpenEditDialog(profile)} + onDelete={() => setDeleteConfirmId(profile.id)} + onSetActive={() => setActiveClaudeApiProfile(profile.id)} + /> + ))} +
+ )} +
+ + {/* Add/Edit Dialog */} + + + + {editingProfileId ? 'Edit API Profile' : 'Add API Profile'} + + Configure a Claude-compatible API endpoint. API keys are stored locally. + + + +
+ {/* Name */} +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="e.g., z.AI GLM" + /> +
+ + {/* Base URL */} +
+ + setFormData({ ...formData, baseUrl: e.target.value })} + placeholder="https://api.example.com/v1" + /> +
+ + {/* API Key */} +
+ +
+ setFormData({ ...formData, apiKey: e.target.value })} + placeholder="Enter API key" + className="pr-10" + /> + +
+ {currentTemplate?.apiKeyUrl && ( + + Get API Key from {currentTemplate.name} + + )} +
+ + {/* Use Auth Token */} +
+
+ +

+ Use ANTHROPIC_AUTH_TOKEN instead of ANTHROPIC_API_KEY +

+
+ setFormData({ ...formData, useAuthToken: checked })} + /> +
+ + {/* Timeout */} +
+ + setFormData({ ...formData, timeoutMs: e.target.value })} + placeholder="Optional, e.g., 3000000" + /> +
+ + {/* Model Mappings */} +
+ +

+ Map Claude model aliases to provider-specific model names +

+
+
+ + + setFormData({ + ...formData, + modelMappings: { ...formData.modelMappings, haiku: e.target.value }, + }) + } + placeholder="e.g., GLM-4.5-Flash" + className="text-xs" + /> +
+
+ + + setFormData({ + ...formData, + modelMappings: { ...formData.modelMappings, sonnet: e.target.value }, + }) + } + placeholder="e.g., glm-4.7" + className="text-xs" + /> +
+
+ + + setFormData({ + ...formData, + modelMappings: { ...formData.modelMappings, opus: e.target.value }, + }) + } + placeholder="e.g., glm-4.7" + className="text-xs" + /> +
+
+
+ + {/* Disable Non-essential Traffic */} +
+
+ +

+ Sets CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 +

+
+ + setFormData({ ...formData, disableNonessentialTraffic: checked }) + } + /> +
+
+ + + + + +
+
+ + {/* Delete Confirmation Dialog */} + !open && setDeleteConfirmId(null)}> + + + Delete Profile? + + This will permanently delete the API profile. If this profile is currently active, you + will be switched to direct Anthropic API. + + + + + + + + +
+ ); +} + +interface ProfileCardProps { + profile: ClaudeApiProfile; + isActive: boolean; + onEdit: () => void; + onDelete: () => void; + onSetActive: () => void; +} + +function ProfileCard({ profile, isActive, onEdit, onDelete, onSetActive }: ProfileCardProps) { + return ( +
+
+
+
+

{profile.name}

+ {isActive && ( + + Active + + )} +
+

{profile.baseUrl}

+
+ Key: {maskApiKey(profile.apiKey)} + {profile.useAuthToken && Auth Token} + {profile.timeoutMs && Timeout: {(profile.timeoutMs / 1000).toFixed(0)}s} +
+
+ + + + + + + {!isActive && ( + + + Set Active + + )} + + + Edit + + + + + Delete + + + +
+
+ ); +} diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index ea865566..0648a3e6 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -64,6 +64,8 @@ const SETTINGS_FIELDS_TO_SYNC = [ 'defaultEditorCommand', 'promptCustomization', 'eventHooks', + 'claudeApiProfiles', + 'activeClaudeApiProfileId', 'projects', 'trashedProjects', 'currentProjectId', // ID of currently open project @@ -510,6 +512,8 @@ export async function refreshSettingsFromServer(): Promise { mcpServers: serverSettings.mcpServers, defaultEditorCommand: serverSettings.defaultEditorCommand ?? null, promptCustomization: serverSettings.promptCustomization ?? {}, + claudeApiProfiles: serverSettings.claudeApiProfiles ?? [], + activeClaudeApiProfileId: serverSettings.activeClaudeApiProfileId ?? null, projects: serverSettings.projects, trashedProjects: serverSettings.trashedProjects, projectHistory: serverSettings.projectHistory, diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index a23c17c4..5f115155 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -31,6 +31,7 @@ import type { ModelDefinition, ServerLogLevel, EventHook, + ClaudeApiProfile, } from '@automaker/types'; import { getAllCursorModelIds, @@ -742,6 +743,10 @@ export interface AppState { // Event Hooks eventHooks: EventHook[]; // Event hooks for custom commands or webhooks + // Claude API Profiles + claudeApiProfiles: ClaudeApiProfile[]; // Claude-compatible API endpoint profiles + activeClaudeApiProfileId: string | null; // Active profile ID (null = use direct Anthropic API) + // Project Analysis projectAnalysis: ProjectAnalysis | null; isAnalyzing: boolean; @@ -1172,6 +1177,13 @@ export interface AppActions { // Event Hook actions setEventHooks: (hooks: EventHook[]) => void; + // Claude API Profile actions + addClaudeApiProfile: (profile: ClaudeApiProfile) => Promise; + updateClaudeApiProfile: (id: string, updates: Partial) => Promise; + deleteClaudeApiProfile: (id: string) => Promise; + setActiveClaudeApiProfile: (id: string | null) => Promise; + setClaudeApiProfiles: (profiles: ClaudeApiProfile[]) => Promise; + // MCP Server actions addMCPServer: (server: Omit) => void; updateMCPServer: (id: string, updates: Partial) => void; @@ -1426,6 +1438,8 @@ const initialState: AppState = { subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default promptCustomization: {}, // Empty by default - all prompts use built-in defaults eventHooks: [], // No event hooks configured by default + claudeApiProfiles: [], // No Claude API profiles configured by default + activeClaudeApiProfileId: null, // Use direct Anthropic API by default projectAnalysis: null, isAnalyzing: false, boardBackgroundByProject: {}, @@ -2428,6 +2442,51 @@ export const useAppStore = create()((set, get) => ({ // Event Hook actions setEventHooks: (hooks) => set({ eventHooks: hooks }), + // Claude API Profile actions + addClaudeApiProfile: async (profile) => { + set({ claudeApiProfiles: [...get().claudeApiProfiles, profile] }); + // Sync immediately to persist profile + const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); + await syncSettingsToServer(); + }, + + updateClaudeApiProfile: async (id, updates) => { + set({ + claudeApiProfiles: get().claudeApiProfiles.map((p) => + p.id === id ? { ...p, ...updates } : p + ), + }); + // Sync immediately to persist changes + const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); + await syncSettingsToServer(); + }, + + deleteClaudeApiProfile: async (id) => { + const currentActiveId = get().activeClaudeApiProfileId; + set({ + claudeApiProfiles: get().claudeApiProfiles.filter((p) => p.id !== id), + // Clear active if the deleted profile was active + activeClaudeApiProfileId: currentActiveId === id ? null : currentActiveId, + }); + // Sync immediately to persist deletion + const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); + await syncSettingsToServer(); + }, + + setActiveClaudeApiProfile: async (id) => { + set({ activeClaudeApiProfileId: id }); + // Sync immediately to persist active profile change + const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); + await syncSettingsToServer(); + }, + + setClaudeApiProfiles: async (profiles) => { + set({ claudeApiProfiles: profiles }); + // Sync immediately to persist profiles + const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); + await syncSettingsToServer(); + }, + // MCP Server actions addMCPServer: (server) => { const id = `mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index a0145782..e352a710 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -158,6 +158,9 @@ export type { EventHookHttpAction, EventHookAction, EventHook, + // Claude API profile types + ClaudeApiProfile, + ClaudeApiProfileTemplate, } from './settings.js'; export { DEFAULT_KEYBOARD_SHORTCUTS, @@ -172,6 +175,8 @@ export { getThinkingTokenBudget, // Event hook constants EVENT_HOOK_TRIGGER_LABELS, + // Claude API profile constants + CLAUDE_API_PROFILE_TEMPLATES, } from './settings.js'; // Model display constants diff --git a/libs/types/src/provider.ts b/libs/types/src/provider.ts index e934e999..e2040b9b 100644 --- a/libs/types/src/provider.ts +++ b/libs/types/src/provider.ts @@ -2,7 +2,7 @@ * Shared types for AI model providers */ -import type { ThinkingLevel } from './settings.js'; +import type { ThinkingLevel, ClaudeApiProfile } from './settings.js'; import type { CodexSandboxMode, CodexApprovalPolicy } from './codex.js'; /** @@ -209,6 +209,12 @@ export interface ExecuteOptions { type: 'json_schema'; schema: Record; }; + /** + * Active Claude API profile for alternative endpoint configuration. + * When set, uses profile's settings (base URL, auth, model mappings) instead of direct Anthropic API. + * When undefined, uses direct Anthropic API (via API key or Claude Max CLI OAuth). + */ + claudeApiProfile?: ClaudeApiProfile; } /** diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index a48504a8..02761183 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -101,6 +101,72 @@ export function getThinkingTokenBudget(level: ThinkingLevel | undefined): number /** ModelProvider - AI model provider for credentials and API key management */ export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode'; +// ============================================================================ +// Claude API Profiles - Configuration for Claude-compatible API endpoints +// ============================================================================ + +/** + * ClaudeApiProfile - Configuration for a Claude-compatible API endpoint + * + * Allows using alternative providers like z.AI GLM, AWS Bedrock, etc. + */ +export interface ClaudeApiProfile { + /** Unique identifier (uuid) */ + id: string; + /** Display name (e.g., "z.AI GLM", "AWS Bedrock") */ + name: string; + /** ANTHROPIC_BASE_URL - custom API endpoint */ + baseUrl: string; + /** API key value */ + apiKey: string; + /** If true, use ANTHROPIC_AUTH_TOKEN instead of ANTHROPIC_API_KEY */ + useAuthToken?: boolean; + /** API_TIMEOUT_MS override in milliseconds */ + timeoutMs?: number; + /** Optional model name mappings */ + modelMappings?: { + /** Maps to ANTHROPIC_DEFAULT_HAIKU_MODEL */ + haiku?: string; + /** Maps to ANTHROPIC_DEFAULT_SONNET_MODEL */ + sonnet?: string; + /** Maps to ANTHROPIC_DEFAULT_OPUS_MODEL */ + opus?: string; + }; + /** Set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 */ + disableNonessentialTraffic?: boolean; +} + +/** Known provider templates for quick setup */ +export interface ClaudeApiProfileTemplate { + name: string; + baseUrl: string; + useAuthToken: boolean; + timeoutMs?: number; + modelMappings?: ClaudeApiProfile['modelMappings']; + disableNonessentialTraffic?: boolean; + description: string; + apiKeyUrl?: string; +} + +/** Predefined templates for known Claude-compatible providers */ +export const CLAUDE_API_PROFILE_TEMPLATES: ClaudeApiProfileTemplate[] = [ + { + name: 'z.AI GLM', + baseUrl: 'https://api.z.ai/api/anthropic', + useAuthToken: true, + timeoutMs: 3000000, + modelMappings: { + haiku: 'GLM-4.5-Air', + sonnet: 'GLM-4.7', + opus: 'GLM-4.7', + }, + disableNonessentialTraffic: true, + description: '3× usage at fraction of cost via GLM Coding Plan', + apiKeyUrl: 'https://z.ai/manage-apikey/apikey-list', + }, + // Future: Add AWS Bedrock, Google Vertex, etc. +]; + // ============================================================================ // Event Hooks - Custom actions triggered by system events // ============================================================================ @@ -650,6 +716,19 @@ export interface GlobalSettings { * @see EventHook for configuration details */ eventHooks?: EventHook[]; + + // Claude API Profiles Configuration + /** + * Claude-compatible API endpoint profiles + * Allows using alternative providers like z.AI GLM, AWS Bedrock, etc. + */ + claudeApiProfiles?: ClaudeApiProfile[]; + + /** + * Active profile ID (null/undefined = use direct Anthropic API) + * When set, the corresponding profile's settings will be used for Claude API calls + */ + activeClaudeApiProfileId?: string | null; } /** @@ -896,6 +975,8 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { skillsSources: ['user', 'project'], enableSubagents: true, subagentsSources: ['user', 'project'], + claudeApiProfiles: [], + activeClaudeApiProfileId: null, }; /** Default credentials (empty strings - user must provide API keys) */ diff --git a/package-lock.json b/package-lock.json index 8fc7b149..68307086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "tree-kill": "1.2.2" }, "devDependencies": { - "dmg-license": "^1.0.11", "husky": "9.1.7", "lint-staged": "16.2.7", "prettier": "3.7.4", @@ -26,6 +25,9 @@ }, "engines": { "node": ">=22.0.0 <23.0.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" } }, "apps/server": { @@ -6114,7 +6116,7 @@ "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -6124,15 +6126,15 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" @@ -6213,8 +6215,8 @@ "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -6719,7 +6721,7 @@ "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6921,7 +6923,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -7003,7 +7005,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -7013,7 +7015,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -7237,8 +7239,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.8" } @@ -7289,8 +7291,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8" } @@ -7363,7 +7365,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -7537,7 +7539,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -8033,8 +8035,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" @@ -8128,7 +8130,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -8141,7 +8143,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/colorette": { @@ -8309,8 +8311,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/cors": { "version": "2.8.5", @@ -8329,8 +8331,8 @@ "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "buffer": "^5.1.0" } @@ -8792,8 +8794,8 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -9057,7 +9059,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -9682,11 +9684,11 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, "engines": [ "node >=0.6.0" ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -9698,7 +9700,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -10648,8 +10650,8 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -10678,7 +10680,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -10866,7 +10868,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11132,7 +11134,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-schema-typed": { @@ -11274,7 +11276,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11296,7 +11297,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11339,7 +11339,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11361,7 +11360,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11383,7 +11381,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11405,7 +11402,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11427,7 +11423,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11449,7 +11444,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11471,7 +11465,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -13077,8 +13070,8 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/node-api-version": { "version": "0.2.1", @@ -13677,7 +13670,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -13793,7 +13786,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -14593,8 +14586,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -14608,7 +14601,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -14805,7 +14798,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14850,7 +14843,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15609,7 +15602,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -15709,8 +15702,8 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -16153,7 +16146,7 @@ "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8.0"