Merge remote-tracking branch 'origin/main' into feature/v0.13.0rc-1768936017583-e6ni

# Conflicts:
#	apps/ui/src/components/views/board-view.tsx
This commit is contained in:
Shirone
2026-01-21 08:47:16 +01:00
68 changed files with 4703 additions and 1248 deletions

View File

@@ -10,7 +10,12 @@ import type {
McpServerConfig,
PromptCustomization,
ClaudeApiProfile,
ClaudeCompatibleProvider,
PhaseModelKey,
PhaseModelEntry,
Credentials,
} from '@automaker/types';
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
import {
mergeAutoModePrompts,
mergeAgentPrompts,
@@ -364,6 +369,9 @@ export interface ActiveClaudeApiProfileResult {
* Checks project settings first for per-project overrides, then falls back to global settings.
* Returns both the profile and credentials for resolving 'credentials' apiKeySource.
*
* @deprecated Use getProviderById and getPhaseModelWithOverrides instead for the new provider system.
* This function is kept for backward compatibility during migration.
*
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
* @param projectPath - Optional project path for per-project override
@@ -427,3 +435,296 @@ export async function getActiveClaudeApiProfile(
return { profile: undefined, credentials: undefined };
}
}
// ============================================================================
// New Provider System Helpers
// ============================================================================
/** Result from getProviderById */
export interface ProviderByIdResult {
/** The provider, or undefined if not found */
provider: ClaudeCompatibleProvider | undefined;
/** Credentials for resolving 'credentials' apiKeySource */
credentials: Credentials | undefined;
}
/**
* Get a ClaudeCompatibleProvider by its ID.
* Returns the provider configuration and credentials for API key resolution.
*
* @param providerId - The provider ID to look up
* @param settingsService - Settings service instance
* @param logPrefix - Prefix for log messages
* @returns Promise resolving to object with provider and credentials
*/
export async function getProviderById(
providerId: string,
settingsService: SettingsService,
logPrefix = '[SettingsHelper]'
): Promise<ProviderByIdResult> {
try {
const globalSettings = await settingsService.getGlobalSettings();
const credentials = await settingsService.getCredentials();
const providers = globalSettings.claudeCompatibleProviders || [];
const provider = providers.find((p) => p.id === providerId);
if (provider) {
if (provider.enabled === false) {
logger.warn(`${logPrefix} Provider "${provider.name}" (${providerId}) is disabled`);
} else {
logger.debug(`${logPrefix} Found provider: ${provider.name}`);
}
return { provider, credentials };
} else {
logger.warn(`${logPrefix} Provider not found: ${providerId}`);
return { provider: undefined, credentials };
}
} catch (error) {
logger.error(`${logPrefix} Failed to load provider by ID:`, error);
return { provider: undefined, credentials: undefined };
}
}
/** Result from getPhaseModelWithOverrides */
export interface PhaseModelWithOverridesResult {
/** The resolved phase model entry */
phaseModel: PhaseModelEntry;
/** Whether a project override was applied */
isProjectOverride: boolean;
/** The provider if providerId is set and found */
provider: ClaudeCompatibleProvider | undefined;
/** Credentials for API key resolution */
credentials: Credentials | undefined;
}
/**
* Get the phase model configuration for a specific phase, applying project overrides if available.
* Also resolves the provider if the phase model has a providerId.
*
* @param phase - The phase key (e.g., 'enhancementModel', 'specGenerationModel')
* @param settingsService - Optional settings service instance (returns defaults if undefined)
* @param projectPath - Optional project path for checking overrides
* @param logPrefix - Prefix for log messages
* @returns Promise resolving to phase model with provider info
*/
export async function getPhaseModelWithOverrides(
phase: PhaseModelKey,
settingsService?: SettingsService | null,
projectPath?: string,
logPrefix = '[SettingsHelper]'
): Promise<PhaseModelWithOverridesResult> {
// Handle undefined settingsService gracefully
if (!settingsService) {
logger.info(`${logPrefix} SettingsService not available, using default for ${phase}`);
return {
phaseModel: DEFAULT_PHASE_MODELS[phase] || { model: 'sonnet' },
isProjectOverride: false,
provider: undefined,
credentials: undefined,
};
}
try {
const globalSettings = await settingsService.getGlobalSettings();
const credentials = await settingsService.getCredentials();
const globalPhaseModels = globalSettings.phaseModels || {};
// Start with global phase model
let phaseModel = globalPhaseModels[phase];
let isProjectOverride = false;
// Check for project override
if (projectPath) {
const projectSettings = await settingsService.getProjectSettings(projectPath);
const projectOverrides = projectSettings.phaseModelOverrides || {};
if (projectOverrides[phase]) {
phaseModel = projectOverrides[phase];
isProjectOverride = true;
logger.debug(`${logPrefix} Using project override for ${phase}`);
}
}
// If no phase model found, use per-phase default
if (!phaseModel) {
phaseModel = DEFAULT_PHASE_MODELS[phase] || { model: 'sonnet' };
logger.debug(`${logPrefix} No ${phase} configured, using default: ${phaseModel.model}`);
}
// Resolve provider if providerId is set
let provider: ClaudeCompatibleProvider | undefined;
if (phaseModel.providerId) {
const providers = globalSettings.claudeCompatibleProviders || [];
provider = providers.find((p) => p.id === phaseModel.providerId);
if (provider) {
if (provider.enabled === false) {
logger.warn(
`${logPrefix} Provider "${provider.name}" for ${phase} is disabled, falling back to direct API`
);
provider = undefined;
} else {
logger.debug(`${logPrefix} Using provider "${provider.name}" for ${phase}`);
}
} else {
logger.warn(
`${logPrefix} Provider ${phaseModel.providerId} not found for ${phase}, falling back to direct API`
);
}
}
return {
phaseModel,
isProjectOverride,
provider,
credentials,
};
} catch (error) {
logger.error(`${logPrefix} Failed to get phase model with overrides:`, error);
// Return a safe default
return {
phaseModel: { model: 'sonnet' },
isProjectOverride: false,
provider: undefined,
credentials: undefined,
};
}
}
/** Result from getProviderByModelId */
export interface ProviderByModelIdResult {
/** The provider that contains this model, or undefined if not found */
provider: ClaudeCompatibleProvider | undefined;
/** The model configuration if found */
modelConfig: import('@automaker/types').ProviderModel | undefined;
/** Credentials for API key resolution */
credentials: Credentials | undefined;
/** The resolved Claude model ID to use for API calls (from mapsToClaudeModel) */
resolvedModel: string | undefined;
}
/**
* Find a ClaudeCompatibleProvider by one of its model IDs.
* Searches through all enabled providers to find one that contains the specified model.
* This is useful when you have a model string from the UI but need the provider config.
*
* Also resolves the `mapsToClaudeModel` field to get the actual Claude model ID to use
* when calling the API (e.g., "GLM-4.5-Air" -> "claude-haiku-4-5").
*
* @param modelId - The model ID to search for (e.g., "GLM-4.7", "MiniMax-M2.1")
* @param settingsService - Settings service instance
* @param logPrefix - Prefix for log messages
* @returns Promise resolving to object with provider, model config, credentials, and resolved model
*/
export async function getProviderByModelId(
modelId: string,
settingsService: SettingsService,
logPrefix = '[SettingsHelper]'
): Promise<ProviderByModelIdResult> {
try {
const globalSettings = await settingsService.getGlobalSettings();
const credentials = await settingsService.getCredentials();
const providers = globalSettings.claudeCompatibleProviders || [];
// Search through all enabled providers for this model
for (const provider of providers) {
// Skip disabled providers
if (provider.enabled === false) {
continue;
}
// Check if this provider has the model
const modelConfig = provider.models?.find(
(m) => m.id === modelId || m.id.toLowerCase() === modelId.toLowerCase()
);
if (modelConfig) {
logger.info(`${logPrefix} Found model "${modelId}" in provider "${provider.name}"`);
// Resolve the mapped Claude model if specified
let resolvedModel: string | undefined;
if (modelConfig.mapsToClaudeModel) {
// Import resolveModelString to convert alias to full model ID
const { resolveModelString } = await import('@automaker/model-resolver');
resolvedModel = resolveModelString(modelConfig.mapsToClaudeModel);
logger.info(
`${logPrefix} Model "${modelId}" maps to Claude model "${modelConfig.mapsToClaudeModel}" -> "${resolvedModel}"`
);
}
return { provider, modelConfig, credentials, resolvedModel };
}
}
// Model not found in any provider
logger.debug(`${logPrefix} Model "${modelId}" not found in any provider`);
return {
provider: undefined,
modelConfig: undefined,
credentials: undefined,
resolvedModel: undefined,
};
} catch (error) {
logger.error(`${logPrefix} Failed to find provider by model ID:`, error);
return {
provider: undefined,
modelConfig: undefined,
credentials: undefined,
resolvedModel: undefined,
};
}
}
/**
* Get all enabled provider models for use in model dropdowns.
* Returns models from all enabled ClaudeCompatibleProviders.
*
* @param settingsService - Settings service instance
* @param logPrefix - Prefix for log messages
* @returns Promise resolving to array of provider models with their provider info
*/
export async function getAllProviderModels(
settingsService: SettingsService,
logPrefix = '[SettingsHelper]'
): Promise<
Array<{
providerId: string;
providerName: string;
model: import('@automaker/types').ProviderModel;
}>
> {
try {
const globalSettings = await settingsService.getGlobalSettings();
const providers = globalSettings.claudeCompatibleProviders || [];
const allModels: Array<{
providerId: string;
providerName: string;
model: import('@automaker/types').ProviderModel;
}> = [];
for (const provider of providers) {
// Skip disabled providers
if (provider.enabled === false) {
continue;
}
for (const model of provider.models || []) {
allModels.push({
providerId: provider.id,
providerName: provider.name,
model,
});
}
}
logger.debug(
`${logPrefix} Found ${allModels.length} models from ${providers.length} providers`
);
return allModels;
} catch (error) {
logger.error(`${logPrefix} Failed to get all provider models:`, error);
return [];
}
}

View File

@@ -14,8 +14,17 @@ 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,
@@ -51,34 +60,48 @@ const ALLOWED_ENV_VARS = [
// 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 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.
* 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.
*
* @param profile - Optional Claude API profile for alternative endpoint configuration
* 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(
profile?: ClaudeApiProfile,
providerConfig?: ProviderConfig,
credentials?: Credentials
): Record<string, string | undefined> {
const env: Record<string, string | undefined> = {};
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,
apiKeySource: profile.apiKeySource ?? 'inline',
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 = profile.apiKeySource ?? 'inline'; // Default to inline for backwards compat
const source = providerConfig.apiKeySource ?? 'inline'; // Default to inline for backwards compat
switch (source) {
case 'inline':
apiKey = profile.apiKey;
apiKey = providerConfig.apiKey;
break;
case 'env':
apiKey = process.env.ANTHROPIC_API_KEY;
@@ -90,36 +113,40 @@ function buildEnv(
// Warn if no API key found
if (!apiKey) {
logger.warn(`No API key found for profile "${profile.name}" with source "${source}"`);
logger.warn(`No API key found for provider "${providerConfig.name}" with source "${source}"`);
}
// Authentication
if (profile.useAuthToken) {
if (providerConfig.useAuthToken) {
env['ANTHROPIC_AUTH_TOKEN'] = apiKey;
} else {
env['ANTHROPIC_API_KEY'] = apiKey;
}
// Endpoint configuration
env['ANTHROPIC_BASE_URL'] = profile.baseUrl;
env['ANTHROPIC_BASE_URL'] = providerConfig.baseUrl;
logger.debug(`[buildEnv] Set ANTHROPIC_BASE_URL to: ${providerConfig.baseUrl}`);
if (profile.timeoutMs) {
env['API_TIMEOUT_MS'] = String(profile.timeoutMs);
if (providerConfig.timeoutMs) {
env['API_TIMEOUT_MS'] = String(providerConfig.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;
// 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 (profile.disableNonessentialTraffic) {
if (providerConfig.disableNonessentialTraffic) {
env['CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'] = '1';
}
} else {
@@ -184,9 +211,14 @@ export class ClaudeProvider extends BaseProvider {
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);
@@ -197,9 +229,9 @@ export class ClaudeProvider extends BaseProvider {
maxTurns,
cwd,
// Pass only explicitly allowed environment variables to SDK
// 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, credentials),
// 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
@@ -244,6 +276,18 @@ export class ClaudeProvider extends BaseProvider {
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 });

View File

@@ -1042,7 +1042,7 @@ export class OpencodeProvider extends CliProvider {
'lm studio': 'lmstudio',
lmstudio: 'lmstudio',
opencode: 'opencode',
'z.ai coding plan': 'z-ai',
'z.ai coding plan': 'zai-coding-plan',
'z.ai': 'z-ai',
};

View File

@@ -21,6 +21,7 @@ import type {
ThinkingLevel,
ReasoningEffort,
ClaudeApiProfile,
ClaudeCompatibleProvider,
Credentials,
} from '@automaker/types';
import { stripProviderPrefix } from '@automaker/types';
@@ -56,9 +57,17 @@ export interface SimpleQueryOptions {
readOnly?: boolean;
/** Setting sources for CLAUDE.md loading */
settingSources?: Array<'user' | 'project' | 'local'>;
/** Active Claude API profile for alternative endpoint configuration */
/**
* Active Claude API profile for alternative endpoint configuration
* @deprecated Use claudeCompatibleProvider instead
*/
claudeApiProfile?: ClaudeApiProfile;
/** Credentials for resolving 'credentials' apiKeySource in Claude API profiles */
/**
* Claude-compatible provider for alternative endpoint configuration.
* Takes precedence over claudeApiProfile if both are set.
*/
claudeCompatibleProvider?: ClaudeCompatibleProvider;
/** Credentials for resolving 'credentials' apiKeySource in Claude API profiles/providers */
credentials?: Credentials;
}
@@ -131,7 +140,8 @@ export async function simpleQuery(options: SimpleQueryOptions): Promise<SimpleQu
reasoningEffort: options.reasoningEffort,
readOnly: options.readOnly,
settingSources: options.settingSources,
claudeApiProfile: options.claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
claudeApiProfile: options.claudeApiProfile, // Legacy: Pass active Claude API profile for alternative endpoint configuration
claudeCompatibleProvider: options.claudeCompatibleProvider, // New: Pass Claude-compatible provider (takes precedence)
credentials: options.credentials, // Pass credentials for resolving 'credentials' apiKeySource
};
@@ -215,7 +225,8 @@ export async function streamingQuery(options: StreamingQueryOptions): Promise<Si
reasoningEffort: options.reasoningEffort,
readOnly: options.readOnly,
settingSources: options.settingSources,
claudeApiProfile: options.claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
claudeApiProfile: options.claudeApiProfile, // Legacy: Pass active Claude API profile for alternative endpoint configuration
claudeCompatibleProvider: options.claudeCompatibleProvider, // New: Pass Claude-compatible provider (takes precedence)
credentials: options.credentials, // Pass credentials for resolving 'credentials' apiKeySource
};

View File

@@ -17,7 +17,7 @@ import type { SettingsService } from '../../services/settings-service.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
getActiveClaudeApiProfile,
getPhaseModelWithOverrides,
} from '../../lib/settings-helpers.js';
import { FeatureLoader } from '../../services/feature-loader.js';
@@ -119,20 +119,26 @@ Generate ${featureCount} NEW features that build on each other logically. Rememb
'[FeatureGeneration]'
);
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.featureGenerationModel || DEFAULT_PHASE_MODELS.featureGenerationModel;
// Get model from phase settings with provider info
const {
phaseModel: phaseModelEntry,
provider,
credentials,
} = settingsService
? await getPhaseModelWithOverrides(
'featureGenerationModel',
settingsService,
projectPath,
'[FeatureGeneration]'
)
: {
phaseModel: DEFAULT_PHASE_MODELS.featureGenerationModel,
provider: undefined,
credentials: undefined,
};
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info('Using model:', model);
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[FeatureGeneration]',
projectPath
);
logger.info('Using model:', model, provider ? `via provider: ${provider.name}` : 'direct API');
// Use streamingQuery with event callbacks
const result = await streamingQuery({
@@ -145,7 +151,7 @@ Generate ${featureCount} NEW features that build on each other logically. Rememb
thinkingLevel,
readOnly: true, // Feature generation only reads code, doesn't write
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
claudeCompatibleProvider: provider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
onText: (text) => {
logger.debug(`Feature text block received (${text.length} chars)`);

View File

@@ -19,7 +19,7 @@ import type { SettingsService } from '../../services/settings-service.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
getActiveClaudeApiProfile,
getPhaseModelWithOverrides,
} from '../../lib/settings-helpers.js';
const logger = createLogger('SpecRegeneration');
@@ -96,20 +96,26 @@ ${prompts.appSpec.structuredSpecInstructions}`;
'[SpecRegeneration]'
);
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel;
// Get model from phase settings with provider info
const {
phaseModel: phaseModelEntry,
provider,
credentials,
} = settingsService
? await getPhaseModelWithOverrides(
'specGenerationModel',
settingsService,
projectPath,
'[SpecRegeneration]'
)
: {
phaseModel: DEFAULT_PHASE_MODELS.specGenerationModel,
provider: undefined,
credentials: undefined,
};
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info('Using model:', model);
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[SpecRegeneration]',
projectPath
);
logger.info('Using model:', model, provider ? `via provider: ${provider.name}` : 'direct API');
let responseText = '';
let structuredOutput: SpecOutput | null = null;
@@ -143,7 +149,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
claudeCompatibleProvider: provider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
outputFormat: useStructuredOutput
? {

View File

@@ -17,7 +17,7 @@ import { getAppSpecPath } from '@automaker/platform';
import type { SettingsService } from '../../services/settings-service.js';
import {
getAutoLoadClaudeMdSetting,
getActiveClaudeApiProfile,
getPhaseModelWithOverrides,
} from '../../lib/settings-helpers.js';
import { FeatureLoader } from '../../services/feature-loader.js';
import {
@@ -155,17 +155,26 @@ export async function syncSpec(
'[SpecSync]'
);
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel;
// Get model from phase settings with provider info
const {
phaseModel: phaseModelEntry,
provider,
credentials,
} = settingsService
? await getPhaseModelWithOverrides(
'specGenerationModel',
settingsService,
projectPath,
'[SpecSync]'
)
: {
phaseModel: DEFAULT_PHASE_MODELS.specGenerationModel,
provider: undefined,
credentials: undefined,
};
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[SpecSync]',
projectPath
);
logger.info('Using model:', model, provider ? `via provider: ${provider.name}` : 'direct API');
// Use AI to analyze tech stack
const techAnalysisPrompt = `Analyze this project and return ONLY a JSON object with the current technology stack.
@@ -195,7 +204,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
claudeCompatibleProvider: provider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
onText: (text) => {
logger.debug(`Tech analysis text: ${text.substring(0, 100)}`);

View File

@@ -28,7 +28,7 @@ import type { SettingsService } from '../../services/settings-service.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
getActiveClaudeApiProfile,
getPhaseModelWithOverrides,
} from '../../lib/settings-helpers.js';
const featureLoader = new FeatureLoader();
@@ -121,18 +121,39 @@ export async function generateBacklogPlan(
content: 'Generating plan with AI...',
});
// Get the model to use from settings or provided override
// Get the model to use from settings or provided override with provider info
let effectiveModel = model;
let thinkingLevel: ThinkingLevel | undefined;
if (!effectiveModel) {
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.backlogPlanningModel || DEFAULT_PHASE_MODELS.backlogPlanningModel;
const resolved = resolvePhaseModel(phaseModelEntry);
let claudeCompatibleProvider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
let credentials: import('@automaker/types').Credentials | undefined;
if (effectiveModel) {
// Use explicit override - just get credentials
credentials = await settingsService?.getCredentials();
} else if (settingsService) {
// Use settings-based model with provider info
const phaseResult = await getPhaseModelWithOverrides(
'backlogPlanningModel',
settingsService,
projectPath,
'[BacklogPlan]'
);
const resolved = resolvePhaseModel(phaseResult.phaseModel);
effectiveModel = resolved.model;
thinkingLevel = resolved.thinkingLevel;
claudeCompatibleProvider = phaseResult.provider;
credentials = phaseResult.credentials;
} else {
// Fallback to defaults
const resolved = resolvePhaseModel(DEFAULT_PHASE_MODELS.backlogPlanningModel);
effectiveModel = resolved.model;
thinkingLevel = resolved.thinkingLevel;
}
logger.info('[BacklogPlan] Using model:', effectiveModel);
logger.info(
'[BacklogPlan] Using model:',
effectiveModel,
claudeCompatibleProvider ? `via provider: ${claudeCompatibleProvider.name}` : 'direct API'
);
const provider = ProviderFactory.getProviderForModel(effectiveModel);
// Strip provider prefix - providers expect bare model IDs
@@ -165,13 +186,6 @@ ${userPrompt}`;
finalSystemPrompt = undefined; // System prompt is now embedded in the user prompt
}
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[BacklogPlan]',
projectPath
);
// Execute the query
const stream = provider.executeQuery({
prompt: finalPrompt,
@@ -184,7 +198,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
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
});

View File

@@ -12,7 +12,6 @@
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
import { PathNotAllowedError } from '@automaker/platform';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { simpleQuery } from '../../../providers/simple-query-service.js';
@@ -22,7 +21,7 @@ import type { SettingsService } from '../../../services/settings-service.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
getActiveClaudeApiProfile,
getPhaseModelWithOverrides,
} from '../../../lib/settings-helpers.js';
const logger = createLogger('DescribeFile');
@@ -156,21 +155,22 @@ ${contentToAnalyze}`;
'[DescribeFile]'
);
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
logger.info(`Raw phaseModels from settings:`, JSON.stringify(settings?.phaseModels, null, 2));
const phaseModelEntry =
settings?.phaseModels?.fileDescriptionModel || DEFAULT_PHASE_MODELS.fileDescriptionModel;
logger.info(`fileDescriptionModel entry:`, JSON.stringify(phaseModelEntry));
// Get model from phase settings with provider info
const {
phaseModel: phaseModelEntry,
provider,
credentials,
} = await getPhaseModelWithOverrides(
'fileDescriptionModel',
settingsService,
cwd,
'[DescribeFile]'
);
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info(`Resolved model: ${model}, thinkingLevel: ${thinkingLevel}`);
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[DescribeFile]',
cwd
logger.info(
`Resolved model: ${model}, thinkingLevel: ${thinkingLevel}`,
provider ? `via provider: ${provider.name}` : 'direct API'
);
// Use simpleQuery - provider abstraction handles routing to correct provider
@@ -183,7 +183,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
claudeCompatibleProvider: provider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
});

View File

@@ -13,7 +13,7 @@
import type { Request, Response } from 'express';
import { createLogger, readImageAsBase64 } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { isCursorModel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { simpleQuery } from '../../../providers/simple-query-service.js';
import * as secureFs from '../../../lib/secure-fs.js';
@@ -22,7 +22,7 @@ import type { SettingsService } from '../../../services/settings-service.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
getActiveClaudeApiProfile,
getPhaseModelWithOverrides,
} from '../../../lib/settings-helpers.js';
const logger = createLogger('DescribeImage');
@@ -274,24 +274,27 @@ export function createDescribeImageHandler(
'[DescribeImage]'
);
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.imageDescriptionModel || DEFAULT_PHASE_MODELS.imageDescriptionModel;
// Get model from phase settings with provider info
const {
phaseModel: phaseModelEntry,
provider,
credentials,
} = await getPhaseModelWithOverrides(
'imageDescriptionModel',
settingsService,
cwd,
'[DescribeImage]'
);
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info(`[${requestId}] Using model: ${model}`);
logger.info(
`[${requestId}] Using model: ${model}`,
provider ? `via provider: ${provider.name}` : 'direct API'
);
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[DescribeImage]');
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[DescribeImage]',
cwd
);
// Build the instruction text from centralized prompts
const instructionText = prompts.contextDescription.describeImagePrompt;
@@ -333,7 +336,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
claudeCompatibleProvider: provider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
});

View File

@@ -12,10 +12,7 @@ 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,
getActiveClaudeApiProfile,
} from '../../../lib/settings-helpers.js';
import { getPromptCustomization, getProviderByModelId } from '../../../lib/settings-helpers.js';
import {
buildUserPrompt,
isValidEnhancementMode,
@@ -126,19 +123,35 @@ export function createEnhanceHandler(
// Build the user prompt with few-shot examples
const userPrompt = buildUserPrompt(validMode, trimmedText, true);
// Resolve the model - use the passed model, default to sonnet for quality
const resolvedModel = resolveModelString(model, CLAUDE_MODEL_MAP.sonnet);
// Check if the model is a provider model (like "GLM-4.5-Air")
// If so, get the provider config and resolved Claude model
let claudeCompatibleProvider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
let providerResolvedModel: string | undefined;
let credentials = await settingsService?.getCredentials();
if (model && settingsService) {
const providerResult = await getProviderByModelId(
model,
settingsService,
'[EnhancePrompt]'
);
if (providerResult.provider) {
claudeCompatibleProvider = providerResult.provider;
providerResolvedModel = providerResult.resolvedModel;
credentials = providerResult.credentials;
logger.info(
`Using provider "${providerResult.provider.name}" for model "${model}"` +
(providerResolvedModel ? ` -> resolved to "${providerResolvedModel}"` : '')
);
}
}
// Resolve the model - use provider resolved model, passed model, or default to sonnet
const resolvedModel =
providerResolvedModel || resolveModelString(model, CLAUDE_MODEL_MAP.sonnet);
logger.debug(`Using model: ${resolvedModel}`);
// Get active Claude API profile for alternative endpoint configuration
// Uses project-specific profile if projectPath provided, otherwise global
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[EnhancePrompt]',
projectPath
);
// 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
@@ -150,8 +163,8 @@ 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
credentials, // Pass credentials for resolving 'credentials' apiKeySource
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
});
const enhancedText = result.text;

View File

@@ -10,10 +10,7 @@ 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,
getActiveClaudeApiProfile,
} from '../../../lib/settings-helpers.js';
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
const logger = createLogger('GenerateTitle');
@@ -64,13 +61,8 @@ export function createGenerateTitleHandler(
const prompts = await getPromptCustomization(settingsService, '[GenerateTitle]');
const systemPrompt = prompts.titleGeneration.systemPrompt;
// Get active Claude API profile for alternative endpoint configuration
// Uses project-specific profile if projectPath provided, otherwise global
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[GenerateTitle]',
projectPath
);
// Get credentials for API calls (uses hardcoded haiku model, no phase setting)
const credentials = await settingsService?.getCredentials();
const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`;
@@ -81,7 +73,6 @@ export function createGenerateTitleHandler(
cwd: process.cwd(),
maxTurns: 1,
allowedTools: [],
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
});

View File

@@ -37,7 +37,7 @@ import {
import {
getPromptCustomization,
getAutoLoadClaudeMdSetting,
getActiveClaudeApiProfile,
getProviderByModelId,
} from '../../../lib/settings-helpers.js';
import {
trySetValidationRunning,
@@ -167,19 +167,33 @@ ${basePrompt}`;
}
}
logger.info(`Using model: ${model}`);
// Check if the model is a provider model (like "GLM-4.5-Air")
// If so, get the provider config and resolved Claude model
let claudeCompatibleProvider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
let providerResolvedModel: string | undefined;
let credentials = await settingsService?.getCredentials();
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[IssueValidation]',
projectPath
);
if (settingsService) {
const providerResult = await getProviderByModelId(model, settingsService, '[ValidateIssue]');
if (providerResult.provider) {
claudeCompatibleProvider = providerResult.provider;
providerResolvedModel = providerResult.resolvedModel;
credentials = providerResult.credentials;
logger.info(
`Using provider "${providerResult.provider.name}" for model "${model}"` +
(providerResolvedModel ? ` -> resolved to "${providerResolvedModel}"` : '')
);
}
}
// Use provider resolved model if available, otherwise use original model
const effectiveModel = providerResolvedModel || (model as string);
logger.info(`Using model: ${effectiveModel}`);
// Use streamingQuery with event callbacks
const result = await streamingQuery({
prompt: finalPrompt,
model: model as string,
model: effectiveModel,
cwd: projectPath,
systemPrompt: useStructuredOutput ? issueValidationSystemPrompt : undefined,
abortController,
@@ -187,7 +201,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
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
outputFormat: useStructuredOutput
? {

View File

@@ -18,7 +18,8 @@ import type { SettingsService } from '../../services/settings-service.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
getActiveClaudeApiProfile,
getPhaseModelWithOverrides,
getProviderByModelId,
} from '../../lib/settings-helpers.js';
const logger = createLogger('Suggestions');
@@ -171,11 +172,12 @@ ${prompts.suggestions.baseTemplate}`;
'[Suggestions]'
);
// Get model from phase settings (AI Suggestions = suggestionsModel)
// Get model from phase settings with provider info (AI Suggestions = suggestionsModel)
// Use override if provided, otherwise fall back to settings
const settings = await settingsService?.getGlobalSettings();
let model: string;
let thinkingLevel: ThinkingLevel | undefined;
let provider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
let credentials: import('@automaker/types').Credentials | undefined;
if (modelOverride) {
// Use explicit override - resolve the model string
@@ -185,22 +187,46 @@ ${prompts.suggestions.baseTemplate}`;
});
model = resolved.model;
thinkingLevel = resolved.thinkingLevel;
// Try to find a provider for this model (e.g., GLM, MiniMax models)
if (settingsService) {
const providerResult = await getProviderByModelId(
modelOverride,
settingsService,
'[Suggestions]'
);
provider = providerResult.provider;
// Use resolved model from provider if available (maps to Claude model)
if (providerResult.resolvedModel) {
model = providerResult.resolvedModel;
}
credentials = providerResult.credentials ?? (await settingsService.getCredentials());
}
// If no settingsService, credentials remains undefined (initialized above)
} else if (settingsService) {
// Use settings-based model with provider info
const phaseResult = await getPhaseModelWithOverrides(
'suggestionsModel',
settingsService,
projectPath,
'[Suggestions]'
);
const resolved = resolvePhaseModel(phaseResult.phaseModel);
model = resolved.model;
thinkingLevel = resolved.thinkingLevel;
provider = phaseResult.provider;
credentials = phaseResult.credentials;
} else {
// Use settings-based model
const phaseModelEntry =
settings?.phaseModels?.suggestionsModel || DEFAULT_PHASE_MODELS.suggestionsModel;
const resolved = resolvePhaseModel(phaseModelEntry);
// Fallback to defaults
const resolved = resolvePhaseModel(DEFAULT_PHASE_MODELS.suggestionsModel);
model = resolved.model;
thinkingLevel = resolved.thinkingLevel;
}
logger.info('[Suggestions] Using model:', model);
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[Suggestions]',
projectPath
logger.info(
'[Suggestions] Using model:',
model,
provider ? `via provider: ${provider.name}` : 'direct API'
);
let responseText = '';
@@ -234,7 +260,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
claudeCompatibleProvider: provider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
outputFormat: useStructuredOutput
? {

View File

@@ -11,13 +11,13 @@ import { promisify } from 'util';
import { existsSync } from 'fs';
import { join } from 'path';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel, stripProviderPrefix } from '@automaker/types';
import { isCursorModel, stripProviderPrefix } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
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';
import { getPhaseModelWithOverrides } from '../../../lib/settings-helpers.js';
const logger = createLogger('GenerateCommitMessage');
const execAsync = promisify(exec);
@@ -157,26 +157,29 @@ export function createGenerateCommitMessageHandler(
const userPrompt = `Generate a commit message for these changes:\n\n\`\`\`diff\n${truncatedDiff}\n\`\`\``;
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.commitMessageModel || DEFAULT_PHASE_MODELS.commitMessageModel;
const { model } = resolvePhaseModel(phaseModelEntry);
// Get model from phase settings with provider info
const {
phaseModel: phaseModelEntry,
provider: claudeCompatibleProvider,
credentials,
} = await getPhaseModelWithOverrides(
'commitMessageModel',
settingsService,
worktreePath,
'[GenerateCommitMessage]'
);
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info(`Using model for commit message: ${model}`);
logger.info(
`Using model for commit message: ${model}`,
claudeCompatibleProvider ? `via provider: ${claudeCompatibleProvider.name}` : 'direct API'
);
// Get the effective system prompt (custom or default)
const systemPrompt = await getSystemPrompt(settingsService);
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[GenerateCommitMessage]',
worktreePath
);
// Get provider for the model type
const provider = ProviderFactory.getProviderForModel(model);
const aiProvider = ProviderFactory.getProviderForModel(model);
const bareModel = stripProviderPrefix(model);
// For Cursor models, combine prompts since Cursor doesn't support systemPrompt separation
@@ -185,10 +188,10 @@ export function createGenerateCommitMessageHandler(
: userPrompt;
const effectiveSystemPrompt = isCursorModel(model) ? undefined : systemPrompt;
logger.info(`Using ${provider.getName()} provider for model: ${model}`);
logger.info(`Using ${aiProvider.getName()} provider for model: ${model}`);
let responseText = '';
const stream = provider.executeQuery({
const stream = aiProvider.executeQuery({
prompt: effectivePrompt,
model: bareModel,
cwd: worktreePath,
@@ -196,7 +199,8 @@ export function createGenerateCommitMessageHandler(
maxTurns: 1,
allowedTools: [],
readOnly: true,
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
thinkingLevel, // Pass thinking level for extended thinking support
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
});

View File

@@ -29,7 +29,7 @@ import {
getSkillsConfiguration,
getSubagentsConfiguration,
getCustomSubagents,
getActiveClaudeApiProfile,
getProviderByModelId,
} from '../lib/settings-helpers.js';
interface Message {
@@ -275,12 +275,29 @@ export class AgentService {
? await getCustomSubagents(this.settingsService, effectiveWorkDir)
: undefined;
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
this.settingsService,
'[AgentService]',
effectiveWorkDir
);
// Get credentials for API calls
const credentials = await this.settingsService?.getCredentials();
// Try to find a provider for the model (if it's a provider model like "GLM-4.7")
// This allows users to select provider models in the Agent Runner UI
let claudeCompatibleProvider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
let providerResolvedModel: string | undefined;
const requestedModel = model || session.model;
if (requestedModel && this.settingsService) {
const providerResult = await getProviderByModelId(
requestedModel,
this.settingsService,
'[AgentService]'
);
if (providerResult.provider) {
claudeCompatibleProvider = providerResult.provider;
providerResolvedModel = providerResult.resolvedModel;
this.logger.info(
`[AgentService] Using provider "${providerResult.provider.name}" for model "${requestedModel}"` +
(providerResolvedModel ? ` -> resolved to "${providerResolvedModel}"` : '')
);
}
}
// 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
@@ -307,10 +324,16 @@ export class AgentService {
// Use thinking level and reasoning effort from request, or fall back to session's stored values
const effectiveThinkingLevel = thinkingLevel ?? session.thinkingLevel;
const effectiveReasoningEffort = reasoningEffort ?? session.reasoningEffort;
// When using a provider model, use the resolved Claude model (from mapsToClaudeModel)
// e.g., "GLM-4.5-Air" -> "claude-haiku-4-5"
const modelForSdk = providerResolvedModel || model;
const sessionModelForSdk = providerResolvedModel ? undefined : session.model;
const sdkOptions = createChatOptions({
cwd: effectiveWorkDir,
model: model,
sessionModel: session.model,
model: modelForSdk,
sessionModel: sessionModelForSdk,
systemPrompt: combinedSystemPrompt,
abortController: session.abortController!,
autoLoadClaudeMd,
@@ -386,8 +409,8 @@ 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
credentials, // Pass credentials for resolving 'credentials' apiKeySource
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration (GLM, MiniMax, etc.)
};
// Build prompt content with images

View File

@@ -68,12 +68,28 @@ import {
filterClaudeMdFromContext,
getMCPServersFromSettings,
getPromptCustomization,
getActiveClaudeApiProfile,
getProviderByModelId,
getPhaseModelWithOverrides,
} from '../lib/settings-helpers.js';
import { getNotificationService } from './notification-service.js';
const execAsync = promisify(exec);
/**
* Get the current branch name for a git repository
* @param projectPath - Path to the git repository
* @returns The current branch name, or null if not in a git repo or on detached HEAD
*/
async function getCurrentBranch(projectPath: string): Promise<string | null> {
try {
const { stdout } = await execAsync('git branch --show-current', { cwd: projectPath });
const branch = stdout.trim();
return branch || null;
} catch {
return null;
}
}
// PlanningMode type is imported from @automaker/types
interface ParsedTask {
@@ -635,7 +651,7 @@ export class AutoModeService {
iterationCount++;
try {
// Count running features for THIS project/worktree only
const projectRunningCount = this.getRunningCountForWorktree(projectPath, branchName);
const projectRunningCount = await this.getRunningCountForWorktree(projectPath, branchName);
// Check if we have capacity for this project/worktree
if (projectRunningCount >= projectState.config.maxConcurrency) {
@@ -728,20 +744,24 @@ export class AutoModeService {
/**
* Get count of running features for a specific worktree
* @param projectPath - The project path
* @param branchName - The branch name, or null for main worktree (features without branchName or with "main")
* @param branchName - The branch name, or null for main worktree (features without branchName or matching primary branch)
*/
private getRunningCountForWorktree(projectPath: string, branchName: string | null): number {
const normalizedBranch = branchName === 'main' ? null : branchName;
private async getRunningCountForWorktree(
projectPath: string,
branchName: string | null
): Promise<number> {
// Get the actual primary branch name for the project
const primaryBranch = await getCurrentBranch(projectPath);
let count = 0;
for (const [, feature] of this.runningFeatures) {
// Filter by project path AND branchName to get accurate worktree-specific count
const featureBranch = feature.branchName ?? null;
if (normalizedBranch === null) {
// Main worktree: match features with branchName === null OR branchName === "main"
if (
feature.projectPath === projectPath &&
(featureBranch === null || featureBranch === 'main')
) {
if (branchName === null) {
// Main worktree: match features with branchName === null OR branchName matching primary branch
const isPrimaryBranch =
featureBranch === null || (primaryBranch && featureBranch === primaryBranch);
if (feature.projectPath === projectPath && isPrimaryBranch) {
count++;
}
} else {
@@ -790,7 +810,7 @@ export class AutoModeService {
// Remove from map
this.autoLoopsByProject.delete(worktreeKey);
return this.getRunningCountForWorktree(projectPath, branchName);
return await this.getRunningCountForWorktree(projectPath, branchName);
}
/**
@@ -1025,7 +1045,7 @@ export class AutoModeService {
const maxAgents = await this.resolveMaxConcurrency(projectPath, branchName);
// Get current running count for this worktree
const currentAgents = this.getRunningCountForWorktree(projectPath, branchName);
const currentAgents = await this.getRunningCountForWorktree(projectPath, branchName);
return {
hasCapacity: currentAgents < maxAgents,
@@ -2377,13 +2397,24 @@ Address the follow-up instructions above. Review the previous work and make the
Format your response as a structured markdown document.`;
try {
// Get model from phase settings
const settings = await this.settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.projectAnalysisModel || DEFAULT_PHASE_MODELS.projectAnalysisModel;
// Get model from phase settings with provider info
const {
phaseModel: phaseModelEntry,
provider: analysisClaudeProvider,
credentials,
} = await getPhaseModelWithOverrides(
'projectAnalysisModel',
this.settingsService,
projectPath,
'[AutoMode]'
);
const { model: analysisModel, thinkingLevel: analysisThinkingLevel } =
resolvePhaseModel(phaseModelEntry);
logger.info('Using model for project analysis:', analysisModel);
logger.info(
'Using model for project analysis:',
analysisModel,
analysisClaudeProvider ? `via provider: ${analysisClaudeProvider.name}` : 'direct API'
);
const provider = ProviderFactory.getProviderForModel(analysisModel);
@@ -2405,13 +2436,6 @@ Format your response as a structured markdown document.`;
thinkingLevel: analysisThinkingLevel,
});
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
this.settingsService,
'[AutoMode]',
projectPath
);
const options: ExecuteOptions = {
prompt,
model: sdkOptions.model ?? analysisModel,
@@ -2421,8 +2445,8 @@ 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
credentials, // Pass credentials for resolving 'credentials' apiKeySource
claudeCompatibleProvider: analysisClaudeProvider, // Pass provider for alternative endpoint configuration
};
const stream = provider.executeQuery(options);
@@ -3017,6 +3041,10 @@ Format your response as a structured markdown document.`;
// Features are stored in .automaker directory
const featuresDir = getFeaturesDir(projectPath);
// Get the actual primary branch name for the project (e.g., "main", "master", "develop")
// This is needed to correctly match features when branchName is null (main worktree)
const primaryBranch = await getCurrentBranch(projectPath);
try {
const entries = await secureFs.readdir(featuresDir, {
withFileTypes: true,
@@ -3056,17 +3084,21 @@ Format your response as a structured markdown document.`;
(feature.planSpec.tasksCompleted ?? 0) < (feature.planSpec.tasksTotal ?? 0))
) {
// Filter by branchName:
// - If branchName is null (main worktree), include features with branchName === null OR branchName === "main"
// - If branchName is null (main worktree), include features with:
// - branchName === null, OR
// - branchName === primaryBranch (e.g., "main", "master", "develop")
// - If branchName is set, only include features with matching branchName
const featureBranch = feature.branchName ?? null;
if (branchName === null) {
// Main worktree: include features without branchName OR with branchName === "main"
// This handles both correct (null) and legacy ("main") cases
if (featureBranch === null || featureBranch === 'main') {
// Main worktree: include features without branchName OR with branchName matching primary branch
// This handles repos where the primary branch is named something other than "main"
const isPrimaryBranch =
featureBranch === null || (primaryBranch && featureBranch === primaryBranch);
if (isPrimaryBranch) {
pendingFeatures.push(feature);
} else {
logger.debug(
`[loadPendingFeatures] Filtering out feature ${feature.id} (branchName: ${featureBranch}) for main worktree`
`[loadPendingFeatures] Filtering out feature ${feature.id} (branchName: ${featureBranch}, primaryBranch: ${primaryBranch}) for main worktree`
);
}
} else {
@@ -3463,16 +3495,37 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
);
}
// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
this.settingsService,
'[AutoMode]',
finalProjectPath
);
// Get credentials for API calls (model comes from request, no phase model)
const credentials = await this.settingsService?.getCredentials();
// Try to find a provider for the model (if it's a provider model like "GLM-4.7")
// This allows users to select provider models in the Auto Mode / Feature execution
let claudeCompatibleProvider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
let providerResolvedModel: string | undefined;
if (finalModel && this.settingsService) {
const providerResult = await getProviderByModelId(
finalModel,
this.settingsService,
'[AutoMode]'
);
if (providerResult.provider) {
claudeCompatibleProvider = providerResult.provider;
providerResolvedModel = providerResult.resolvedModel;
logger.info(
`[AutoMode] Using provider "${providerResult.provider.name}" for model "${finalModel}"` +
(providerResolvedModel ? ` -> resolved to "${providerResolvedModel}"` : '')
);
}
}
// Use the resolved model if available (from mapsToClaudeModel), otherwise use bareModel
const effectiveBareModel = providerResolvedModel
? stripProviderPrefix(providerResolvedModel)
: bareModel;
const executeOptions: ExecuteOptions = {
prompt: promptContent,
model: bareModel,
model: effectiveBareModel,
maxTurns: maxTurns,
cwd: workDir,
allowedTools: allowedTools,
@@ -3481,8 +3534,8 @@ 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
credentials, // Pass credentials for resolving 'credentials' apiKeySource
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration (GLM, MiniMax, etc.)
};
// Execute via provider
@@ -3788,8 +3841,8 @@ 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
credentials, // Pass credentials for resolving 'credentials' apiKeySource
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
});
let revisionText = '';
@@ -3937,8 +3990,8 @@ 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
credentials, // Pass credentials for resolving 'credentials' apiKeySource
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
});
let taskOutput = '';
@@ -4037,8 +4090,8 @@ 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
credentials, // Pass credentials for resolving 'credentials' apiKeySource
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
});
for await (const msg of continuationStream) {

View File

@@ -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, getActiveClaudeApiProfile } from '../lib/settings-helpers.js';
import { getPromptCustomization, getProviderByModelId } from '../lib/settings-helpers.js';
const logger = createLogger('IdeationService');
@@ -208,7 +208,27 @@ export class IdeationService {
);
// Resolve model alias to canonical identifier (with prefix)
const modelId = resolveModelString(options?.model ?? 'sonnet');
let modelId = resolveModelString(options?.model ?? 'sonnet');
// Try to find a provider for this model (e.g., GLM, MiniMax models)
let claudeCompatibleProvider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
let credentials = await this.settingsService?.getCredentials();
if (this.settingsService && options?.model) {
const providerResult = await getProviderByModelId(
options.model,
this.settingsService,
'[IdeationService]'
);
if (providerResult.provider) {
claudeCompatibleProvider = providerResult.provider;
// Use resolved model from provider if available (maps to Claude model)
if (providerResult.resolvedModel) {
modelId = providerResult.resolvedModel;
}
credentials = providerResult.credentials ?? credentials;
}
}
// Create SDK options
const sdkOptions = createChatOptions({
@@ -223,13 +243,6 @@ 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 { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
this.settingsService,
'[IdeationService]',
projectPath
);
const executeOptions: ExecuteOptions = {
prompt: message,
model: bareModel,
@@ -239,7 +252,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
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
credentials, // Pass credentials for resolving 'credentials' apiKeySource
};
@@ -687,12 +700,8 @@ 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 { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
this.settingsService,
'[IdeationService]',
projectPath
);
// Get credentials for API calls (uses hardcoded model, no phase setting)
const credentials = await this.settingsService?.getCredentials();
const executeOptions: ExecuteOptions = {
prompt: prompt.prompt,
@@ -704,7 +713,6 @@ 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
credentials, // Pass credentials for resolving 'credentials' apiKeySource
};

View File

@@ -31,6 +31,9 @@ import type {
WorktreeInfo,
PhaseModelConfig,
PhaseModelEntry,
ClaudeApiProfile,
ClaudeCompatibleProvider,
ProviderModel,
} from '../types/settings.js';
import {
DEFAULT_GLOBAL_SETTINGS,
@@ -206,6 +209,28 @@ export class SettingsService {
needsSave = true;
}
// Migration v5 -> v6: Convert claudeApiProfiles to claudeCompatibleProviders
// The new system uses a models[] array instead of modelMappings, and removes
// the "active profile" concept - models are selected directly in phase model configs.
if (storedVersion < 6) {
const legacyProfiles = settings.claudeApiProfiles || [];
if (
legacyProfiles.length > 0 &&
(!result.claudeCompatibleProviders || result.claudeCompatibleProviders.length === 0)
) {
logger.info(
`Migration v5->v6: Converting ${legacyProfiles.length} Claude API profile(s) to compatible providers`
);
result.claudeCompatibleProviders = this.migrateProfilesToProviders(legacyProfiles);
}
// Remove the deprecated activeClaudeApiProfileId field
if (result.activeClaudeApiProfileId) {
logger.info('Migration v5->v6: Removing deprecated activeClaudeApiProfileId');
delete result.activeClaudeApiProfileId;
}
needsSave = true;
}
// Update version if any migration occurred
if (needsSave) {
result.version = SETTINGS_VERSION;
@@ -290,6 +315,139 @@ export class SettingsService {
};
}
/**
* Migrate ClaudeApiProfiles to ClaudeCompatibleProviders
*
* Converts the legacy profile format (with modelMappings) to the new
* provider format (with models[] array). Each model mapping entry becomes
* a ProviderModel with appropriate tier assignment.
*
* @param profiles - Legacy ClaudeApiProfile array
* @returns Array of ClaudeCompatibleProvider
*/
private migrateProfilesToProviders(profiles: ClaudeApiProfile[]): ClaudeCompatibleProvider[] {
return profiles.map((profile): ClaudeCompatibleProvider => {
// Convert modelMappings to models array
const models: ProviderModel[] = [];
if (profile.modelMappings) {
// Haiku mapping
if (profile.modelMappings.haiku) {
models.push({
id: profile.modelMappings.haiku,
displayName: this.inferModelDisplayName(profile.modelMappings.haiku, 'haiku'),
mapsToClaudeModel: 'haiku',
});
}
// Sonnet mapping
if (profile.modelMappings.sonnet) {
models.push({
id: profile.modelMappings.sonnet,
displayName: this.inferModelDisplayName(profile.modelMappings.sonnet, 'sonnet'),
mapsToClaudeModel: 'sonnet',
});
}
// Opus mapping
if (profile.modelMappings.opus) {
models.push({
id: profile.modelMappings.opus,
displayName: this.inferModelDisplayName(profile.modelMappings.opus, 'opus'),
mapsToClaudeModel: 'opus',
});
}
}
// Infer provider type from base URL or name
const providerType = this.inferProviderType(profile);
return {
id: profile.id,
name: profile.name,
providerType,
enabled: true,
baseUrl: profile.baseUrl,
apiKeySource: profile.apiKeySource ?? 'inline',
apiKey: profile.apiKey,
useAuthToken: profile.useAuthToken,
timeoutMs: profile.timeoutMs,
disableNonessentialTraffic: profile.disableNonessentialTraffic,
models,
};
});
}
/**
* Infer a display name for a model based on its ID and tier
*
* @param modelId - The raw model ID
* @param tier - The tier hint (haiku/sonnet/opus)
* @returns A user-friendly display name
*/
private inferModelDisplayName(modelId: string, tier: 'haiku' | 'sonnet' | 'opus'): string {
// Common patterns in model IDs
const lowerModelId = modelId.toLowerCase();
// GLM models
if (lowerModelId.includes('glm')) {
return modelId.replace(/-/g, ' ').replace(/glm/i, 'GLM');
}
// MiniMax models
if (lowerModelId.includes('minimax')) {
return modelId.replace(/-/g, ' ').replace(/minimax/i, 'MiniMax');
}
// Claude models via OpenRouter or similar
if (lowerModelId.includes('claude')) {
return modelId;
}
// Default: use model ID as display name with tier in parentheses
return `${modelId} (${tier})`;
}
/**
* Infer provider type from profile configuration
*
* @param profile - The legacy profile
* @returns The inferred provider type
*/
private inferProviderType(profile: ClaudeApiProfile): ClaudeCompatibleProvider['providerType'] {
const baseUrl = profile.baseUrl.toLowerCase();
const name = profile.name.toLowerCase();
// Check URL patterns
if (baseUrl.includes('z.ai') || baseUrl.includes('zhipuai')) {
return 'glm';
}
if (baseUrl.includes('minimax')) {
return 'minimax';
}
if (baseUrl.includes('openrouter')) {
return 'openrouter';
}
if (baseUrl.includes('anthropic.com')) {
return 'anthropic';
}
// Check name patterns
if (name.includes('glm') || name.includes('zhipu')) {
return 'glm';
}
if (name.includes('minimax')) {
return 'minimax';
}
if (name.includes('openrouter')) {
return 'openrouter';
}
if (name.includes('anthropic') || name.includes('direct')) {
return 'anthropic';
}
// Default to custom
return 'custom';
}
/**
* Migrate model-related settings to canonical format
*
@@ -413,6 +571,7 @@ export class SettingsService {
ignoreEmptyArrayOverwrite('mcpServers');
ignoreEmptyArrayOverwrite('enabledCursorModels');
ignoreEmptyArrayOverwrite('claudeApiProfiles');
// Note: claudeCompatibleProviders intentionally NOT guarded - users should be able to delete all providers
// Empty object overwrite guard
const ignoreEmptyObjectOverwrite = <K extends keyof GlobalSettings>(key: K): void => {
@@ -658,6 +817,16 @@ export class SettingsService {
delete updated.activeClaudeApiProfileId;
}
// Handle phaseModelOverrides special cases:
// - "__CLEAR__" marker means delete the key (use global settings for all phases)
// - object means partial overrides for specific phases
if (
'phaseModelOverrides' in updates &&
(updates as Record<string, unknown>).phaseModelOverrides === '__CLEAR__'
) {
delete updated.phaseModelOverrides;
}
await writeSettingsJson(settingsPath, updated);
logger.info(`Project settings updated for ${projectPath}`);

View File

@@ -23,6 +23,16 @@ export type {
PhaseModelConfig,
PhaseModelKey,
PhaseModelEntry,
// Claude-compatible provider types
ApiKeySource,
ClaudeCompatibleProviderType,
ClaudeModelAlias,
ProviderModel,
ClaudeCompatibleProvider,
ClaudeCompatibleProviderTemplate,
// Legacy profile types (deprecated)
ClaudeApiProfile,
ClaudeApiProfileTemplate,
} from '@automaker/types';
export {