mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
feat: refactor Claude API Profiles to Claude Compatible Providers
- Rename ClaudeApiProfile to ClaudeCompatibleProvider with models[] array - Each ProviderModel has mapsToClaudeModel field for Claude tier mapping - Add providerType field for provider-specific icons (glm, minimax, openrouter) - Add thinking level support for provider models in phase selectors - Show all mapped Claude models per provider model (e.g., "Maps to Haiku, Sonnet, Opus") - Add Bulk Replace feature to switch all phases to a provider at once - Hide Bulk Replace button when no providers are enabled - Fix project-level phaseModelOverrides not persisting after refresh - Fix deleting last provider not persisting (remove empty array guard) - Add getProviderByModelId() helper for all SDK routes - Update all routes to pass provider config for provider models - Update terminology from "profiles" to "providers" throughout UI - Update documentation to reflect new provider system
This commit is contained in:
@@ -10,6 +10,10 @@ import type {
|
||||
McpServerConfig,
|
||||
PromptCustomization,
|
||||
ClaudeApiProfile,
|
||||
ClaudeCompatibleProvider,
|
||||
PhaseModelKey,
|
||||
PhaseModelEntry,
|
||||
Credentials,
|
||||
} from '@automaker/types';
|
||||
import {
|
||||
mergeAutoModePrompts,
|
||||
@@ -364,6 +368,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 +434,285 @@ 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 - Settings service instance
|
||||
* @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,
|
||||
projectPath?: string,
|
||||
logPrefix = '[SettingsHelper]'
|
||||
): Promise<PhaseModelWithOverridesResult> {
|
||||
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 a default
|
||||
if (!phaseModel) {
|
||||
phaseModel = { model: 'sonnet' };
|
||||
logger.debug(`${logPrefix} No ${phase} configured, using default: sonnet`);
|
||||
}
|
||||
|
||||
// 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user