mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: unify Claude API key and profile system with flexible key sourcing
- Add ApiKeySource type ('inline' | 'env' | 'credentials') to ClaudeApiProfile
- Allow profiles to source API keys from credentials.json or environment variables
- Add provider templates: OpenRouter, MiniMax, MiniMax (China)
- Auto-migrate existing users with Anthropic key to "Direct Anthropic" profile
- Update all API call sites to pass credentials for key resolution
- Add API key source selector to profile creation UI
- Increment settings version to 5 for migration support
This allows users to:
- Share a single API key across multiple profile configurations
- Use environment variables for CI/CD deployments
- Easily switch between providers without re-entering keys
This commit is contained in:
@@ -351,30 +351,39 @@ export async function getCustomSubagents(
|
|||||||
return Object.keys(merged).length > 0 ? merged : undefined;
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result from getActiveClaudeApiProfile */
|
||||||
|
export interface ActiveClaudeApiProfileResult {
|
||||||
|
/** The active profile, or undefined if using direct Anthropic API */
|
||||||
|
profile: ClaudeApiProfile | undefined;
|
||||||
|
/** Credentials for resolving 'credentials' apiKeySource */
|
||||||
|
credentials: import('@automaker/types').Credentials | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the active Claude API profile from global settings.
|
* Get the active Claude API profile and credentials from global settings.
|
||||||
* Returns undefined if no profile is active (uses direct Anthropic API).
|
* Returns both the profile and credentials for resolving 'credentials' apiKeySource.
|
||||||
*
|
*
|
||||||
* @param settingsService - Optional settings service instance
|
* @param settingsService - Optional settings service instance
|
||||||
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
|
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
|
||||||
* @returns Promise resolving to the active profile, or undefined if none active
|
* @returns Promise resolving to object with profile and credentials
|
||||||
*/
|
*/
|
||||||
export async function getActiveClaudeApiProfile(
|
export async function getActiveClaudeApiProfile(
|
||||||
settingsService?: SettingsService | null,
|
settingsService?: SettingsService | null,
|
||||||
logPrefix = '[SettingsHelper]'
|
logPrefix = '[SettingsHelper]'
|
||||||
): Promise<ClaudeApiProfile | undefined> {
|
): Promise<ActiveClaudeApiProfileResult> {
|
||||||
if (!settingsService) {
|
if (!settingsService) {
|
||||||
return undefined;
|
return { profile: undefined, credentials: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const globalSettings = await settingsService.getGlobalSettings();
|
const globalSettings = await settingsService.getGlobalSettings();
|
||||||
|
const credentials = await settingsService.getCredentials();
|
||||||
const profiles = globalSettings.claudeApiProfiles || [];
|
const profiles = globalSettings.claudeApiProfiles || [];
|
||||||
const activeProfileId = globalSettings.activeClaudeApiProfileId;
|
const activeProfileId = globalSettings.activeClaudeApiProfileId;
|
||||||
|
|
||||||
// No active profile selected - use direct Anthropic API
|
// No active profile selected - use direct Anthropic API
|
||||||
if (!activeProfileId) {
|
if (!activeProfileId) {
|
||||||
return undefined;
|
return { profile: undefined, credentials };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the active profile by ID
|
// Find the active profile by ID
|
||||||
@@ -382,15 +391,15 @@ export async function getActiveClaudeApiProfile(
|
|||||||
|
|
||||||
if (activeProfile) {
|
if (activeProfile) {
|
||||||
logger.info(`${logPrefix} Using Claude API profile: ${activeProfile.name}`);
|
logger.info(`${logPrefix} Using Claude API profile: ${activeProfile.name}`);
|
||||||
return activeProfile;
|
return { profile: activeProfile, credentials };
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${logPrefix} Active profile ID "${activeProfileId}" not found, falling back to direct Anthropic API`
|
`${logPrefix} Active profile ID "${activeProfileId}" not found, falling back to direct Anthropic API`
|
||||||
);
|
);
|
||||||
return undefined;
|
return { profile: undefined, credentials };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${logPrefix} Failed to load Claude API profile:`, error);
|
logger.error(`${logPrefix} Failed to load Claude API profile:`, error);
|
||||||
return undefined;
|
return { profile: undefined, credentials: undefined };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
getThinkingTokenBudget,
|
getThinkingTokenBudget,
|
||||||
validateBareModelId,
|
validateBareModelId,
|
||||||
type ClaudeApiProfile,
|
type ClaudeApiProfile,
|
||||||
|
type Credentials,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
import type {
|
import type {
|
||||||
ExecuteOptions,
|
ExecuteOptions,
|
||||||
@@ -56,19 +57,47 @@ const SYSTEM_ENV_VARS = ['PATH', 'HOME', 'SHELL', 'TERM', 'USER', 'LANG', 'LC_AL
|
|||||||
* When no profile is provided, uses direct Anthropic API settings 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
|
* @param profile - Optional Claude API profile for alternative endpoint configuration
|
||||||
|
* @param credentials - Optional credentials object for resolving 'credentials' apiKeySource
|
||||||
*/
|
*/
|
||||||
function buildEnv(profile?: ClaudeApiProfile): Record<string, string | undefined> {
|
function buildEnv(
|
||||||
|
profile?: ClaudeApiProfile,
|
||||||
|
credentials?: Credentials
|
||||||
|
): Record<string, string | undefined> {
|
||||||
const env: Record<string, string | undefined> = {};
|
const env: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
// Use profile configuration (clean switch - don't inherit non-system vars from process.env)
|
// 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 });
|
logger.debug('Building environment from Claude API profile:', {
|
||||||
|
name: profile.name,
|
||||||
|
apiKeySource: profile.apiKeySource ?? 'inline',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve API key based on source strategy
|
||||||
|
let apiKey: string | undefined;
|
||||||
|
const source = profile.apiKeySource ?? 'inline'; // Default to inline for backwards compat
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case 'inline':
|
||||||
|
apiKey = profile.apiKey;
|
||||||
|
break;
|
||||||
|
case 'env':
|
||||||
|
apiKey = process.env.ANTHROPIC_API_KEY;
|
||||||
|
break;
|
||||||
|
case 'credentials':
|
||||||
|
apiKey = credentials?.apiKeys?.anthropic;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if no API key found
|
||||||
|
if (!apiKey) {
|
||||||
|
logger.warn(`No API key found for profile "${profile.name}" with source "${source}"`);
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
if (profile.useAuthToken) {
|
if (profile.useAuthToken) {
|
||||||
env['ANTHROPIC_AUTH_TOKEN'] = profile.apiKey;
|
env['ANTHROPIC_AUTH_TOKEN'] = apiKey;
|
||||||
} else {
|
} else {
|
||||||
env['ANTHROPIC_API_KEY'] = profile.apiKey;
|
env['ANTHROPIC_API_KEY'] = apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint configuration
|
// Endpoint configuration
|
||||||
@@ -149,6 +178,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
sdkSessionId,
|
sdkSessionId,
|
||||||
thinkingLevel,
|
thinkingLevel,
|
||||||
claudeApiProfile,
|
claudeApiProfile,
|
||||||
|
credentials,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Convert thinking level to token budget
|
// Convert thinking level to token budget
|
||||||
@@ -163,7 +193,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
// Pass only explicitly allowed environment variables to SDK
|
// Pass only explicitly allowed environment variables to SDK
|
||||||
// When a profile is active, uses profile settings (clean switch)
|
// When a profile is active, uses profile settings (clean switch)
|
||||||
// When no profile, uses direct Anthropic API (from process.env or CLI OAuth)
|
// When no profile, uses direct Anthropic API (from process.env or CLI OAuth)
|
||||||
env: buildEnv(claudeApiProfile),
|
env: buildEnv(claudeApiProfile, credentials),
|
||||||
// Pass through allowedTools if provided by caller (decided by sdk-options.ts)
|
// Pass through allowedTools if provided by caller (decided by sdk-options.ts)
|
||||||
...(allowedTools && { allowedTools }),
|
...(allowedTools && { allowedTools }),
|
||||||
// AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation
|
// AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type {
|
|||||||
ThinkingLevel,
|
ThinkingLevel,
|
||||||
ReasoningEffort,
|
ReasoningEffort,
|
||||||
ClaudeApiProfile,
|
ClaudeApiProfile,
|
||||||
|
Credentials,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
import { stripProviderPrefix } from '@automaker/types';
|
import { stripProviderPrefix } from '@automaker/types';
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ export interface SimpleQueryOptions {
|
|||||||
settingSources?: Array<'user' | 'project' | 'local'>;
|
settingSources?: Array<'user' | 'project' | 'local'>;
|
||||||
/** Active Claude API profile for alternative endpoint configuration */
|
/** Active Claude API profile for alternative endpoint configuration */
|
||||||
claudeApiProfile?: ClaudeApiProfile;
|
claudeApiProfile?: ClaudeApiProfile;
|
||||||
|
/** Credentials for resolving 'credentials' apiKeySource in Claude API profiles */
|
||||||
|
credentials?: Credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,6 +132,7 @@ export async function simpleQuery(options: SimpleQueryOptions): Promise<SimpleQu
|
|||||||
readOnly: options.readOnly,
|
readOnly: options.readOnly,
|
||||||
settingSources: options.settingSources,
|
settingSources: options.settingSources,
|
||||||
claudeApiProfile: options.claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile: options.claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials: options.credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
};
|
};
|
||||||
|
|
||||||
for await (const msg of provider.executeQuery(providerOptions)) {
|
for await (const msg of provider.executeQuery(providerOptions)) {
|
||||||
@@ -212,6 +216,7 @@ export async function streamingQuery(options: StreamingQueryOptions): Promise<Si
|
|||||||
readOnly: options.readOnly,
|
readOnly: options.readOnly,
|
||||||
settingSources: options.settingSources,
|
settingSources: options.settingSources,
|
||||||
claudeApiProfile: options.claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile: options.claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials: options.credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
};
|
};
|
||||||
|
|
||||||
for await (const msg of provider.executeQuery(providerOptions)) {
|
for await (const msg of provider.executeQuery(providerOptions)) {
|
||||||
|
|||||||
@@ -128,7 +128,10 @@ Generate ${featureCount} NEW features that build on each other logically. Rememb
|
|||||||
logger.info('Using model:', model);
|
logger.info('Using model:', model);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[FeatureGeneration]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[FeatureGeneration]'
|
||||||
|
);
|
||||||
|
|
||||||
// Use streamingQuery with event callbacks
|
// Use streamingQuery with event callbacks
|
||||||
const result = await streamingQuery({
|
const result = await streamingQuery({
|
||||||
@@ -142,6 +145,7 @@ Generate ${featureCount} NEW features that build on each other logically. Rememb
|
|||||||
readOnly: true, // Feature generation only reads code, doesn't write
|
readOnly: true, // Feature generation only reads code, doesn't write
|
||||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
onText: (text) => {
|
onText: (text) => {
|
||||||
logger.debug(`Feature text block received (${text.length} chars)`);
|
logger.debug(`Feature text block received (${text.length} chars)`);
|
||||||
events.emit('spec-regeneration:event', {
|
events.emit('spec-regeneration:event', {
|
||||||
|
|||||||
@@ -105,7 +105,10 @@ ${prompts.appSpec.structuredSpecInstructions}`;
|
|||||||
logger.info('Using model:', model);
|
logger.info('Using model:', model);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[SpecRegeneration]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[SpecRegeneration]'
|
||||||
|
);
|
||||||
|
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
let structuredOutput: SpecOutput | null = null;
|
let structuredOutput: SpecOutput | null = null;
|
||||||
@@ -140,6 +143,7 @@ Your entire response should be valid JSON starting with { and ending with }. No
|
|||||||
readOnly: true, // Spec generation only reads code, we write the spec ourselves
|
readOnly: true, // Spec generation only reads code, we write the spec ourselves
|
||||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
outputFormat: useStructuredOutput
|
outputFormat: useStructuredOutput
|
||||||
? {
|
? {
|
||||||
type: 'json_schema',
|
type: 'json_schema',
|
||||||
|
|||||||
@@ -161,7 +161,10 @@ export async function syncSpec(
|
|||||||
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
|
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[SpecSync]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[SpecSync]'
|
||||||
|
);
|
||||||
|
|
||||||
// Use AI to analyze tech stack
|
// Use AI to analyze tech stack
|
||||||
const techAnalysisPrompt = `Analyze this project and return ONLY a JSON object with the current technology stack.
|
const techAnalysisPrompt = `Analyze this project and return ONLY a JSON object with the current technology stack.
|
||||||
@@ -192,6 +195,7 @@ Return ONLY this JSON format, no other text:
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
onText: (text) => {
|
onText: (text) => {
|
||||||
logger.debug(`Tech analysis text: ${text.substring(0, 100)}`);
|
logger.debug(`Tech analysis text: ${text.substring(0, 100)}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -166,7 +166,10 @@ ${userPrompt}`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[BacklogPlan]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[BacklogPlan]'
|
||||||
|
);
|
||||||
|
|
||||||
// Execute the query
|
// Execute the query
|
||||||
const stream = provider.executeQuery({
|
const stream = provider.executeQuery({
|
||||||
@@ -181,6 +184,7 @@ ${userPrompt}`;
|
|||||||
readOnly: true, // Plan generation only generates text, doesn't write files
|
readOnly: true, // Plan generation only generates text, doesn't write files
|
||||||
thinkingLevel, // Pass thinking level for extended thinking
|
thinkingLevel, // Pass thinking level for extended thinking
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
});
|
});
|
||||||
|
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
|
|||||||
@@ -167,7 +167,10 @@ ${contentToAnalyze}`;
|
|||||||
logger.info(`Resolved model: ${model}, thinkingLevel: ${thinkingLevel}`);
|
logger.info(`Resolved model: ${model}, thinkingLevel: ${thinkingLevel}`);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[DescribeFile]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[DescribeFile]'
|
||||||
|
);
|
||||||
|
|
||||||
// Use simpleQuery - provider abstraction handles routing to correct provider
|
// Use simpleQuery - provider abstraction handles routing to correct provider
|
||||||
const result = await simpleQuery({
|
const result = await simpleQuery({
|
||||||
@@ -180,6 +183,7 @@ ${contentToAnalyze}`;
|
|||||||
readOnly: true, // File description only reads, doesn't write
|
readOnly: true, // File description only reads, doesn't write
|
||||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
});
|
});
|
||||||
|
|
||||||
const description = result.text;
|
const description = result.text;
|
||||||
|
|||||||
@@ -286,7 +286,10 @@ export function createDescribeImageHandler(
|
|||||||
const prompts = await getPromptCustomization(settingsService, '[DescribeImage]');
|
const prompts = await getPromptCustomization(settingsService, '[DescribeImage]');
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[DescribeImage]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[DescribeImage]'
|
||||||
|
);
|
||||||
|
|
||||||
// Build the instruction text from centralized prompts
|
// Build the instruction text from centralized prompts
|
||||||
const instructionText = prompts.contextDescription.describeImagePrompt;
|
const instructionText = prompts.contextDescription.describeImagePrompt;
|
||||||
@@ -330,6 +333,7 @@ export function createDescribeImageHandler(
|
|||||||
readOnly: true, // Image description only reads, doesn't write
|
readOnly: true, // Image description only reads, doesn't write
|
||||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`[${requestId}] simpleQuery completed in ${Date.now() - queryStart}ms`);
|
logger.info(`[${requestId}] simpleQuery completed in ${Date.now() - queryStart}ms`);
|
||||||
|
|||||||
@@ -130,7 +130,10 @@ export function createEnhanceHandler(
|
|||||||
logger.debug(`Using model: ${resolvedModel}`);
|
logger.debug(`Using model: ${resolvedModel}`);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[EnhancePrompt]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[EnhancePrompt]'
|
||||||
|
);
|
||||||
|
|
||||||
// Use simpleQuery - provider abstraction handles routing to correct provider
|
// Use simpleQuery - provider abstraction handles routing to correct provider
|
||||||
// The system prompt is combined with user prompt since some providers
|
// The system prompt is combined with user prompt since some providers
|
||||||
@@ -144,6 +147,7 @@ export function createEnhanceHandler(
|
|||||||
thinkingLevel,
|
thinkingLevel,
|
||||||
readOnly: true, // Prompt enhancement only generates text, doesn't write files
|
readOnly: true, // Prompt enhancement only generates text, doesn't write files
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
});
|
});
|
||||||
|
|
||||||
const enhancedText = result.text;
|
const enhancedText = result.text;
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ export function createGenerateTitleHandler(
|
|||||||
const systemPrompt = prompts.titleGeneration.systemPrompt;
|
const systemPrompt = prompts.titleGeneration.systemPrompt;
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[GenerateTitle]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[GenerateTitle]'
|
||||||
|
);
|
||||||
|
|
||||||
const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`;
|
const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`;
|
||||||
|
|
||||||
@@ -76,6 +79,7 @@ export function createGenerateTitleHandler(
|
|||||||
maxTurns: 1,
|
maxTurns: 1,
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = result.text;
|
const title = result.text;
|
||||||
|
|||||||
@@ -170,7 +170,10 @@ ${basePrompt}`;
|
|||||||
logger.info(`Using model: ${model}`);
|
logger.info(`Using model: ${model}`);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[IssueValidation]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[IssueValidation]'
|
||||||
|
);
|
||||||
|
|
||||||
// Use streamingQuery with event callbacks
|
// Use streamingQuery with event callbacks
|
||||||
const result = await streamingQuery({
|
const result = await streamingQuery({
|
||||||
@@ -184,6 +187,7 @@ ${basePrompt}`;
|
|||||||
readOnly: true, // Issue validation only reads code, doesn't write
|
readOnly: true, // Issue validation only reads code, doesn't write
|
||||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
outputFormat: useStructuredOutput
|
outputFormat: useStructuredOutput
|
||||||
? {
|
? {
|
||||||
type: 'json_schema',
|
type: 'json_schema',
|
||||||
|
|||||||
@@ -197,7 +197,10 @@ ${prompts.suggestions.baseTemplate}`;
|
|||||||
logger.info('[Suggestions] Using model:', model);
|
logger.info('[Suggestions] Using model:', model);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(settingsService, '[Suggestions]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
settingsService,
|
||||||
|
'[Suggestions]'
|
||||||
|
);
|
||||||
|
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
|
|
||||||
@@ -231,6 +234,7 @@ Your entire response should be valid JSON starting with { and ending with }. No
|
|||||||
readOnly: true, // Suggestions only reads code, doesn't write
|
readOnly: true, // Suggestions only reads code, doesn't write
|
||||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
outputFormat: useStructuredOutput
|
outputFormat: useStructuredOutput
|
||||||
? {
|
? {
|
||||||
type: 'json_schema',
|
type: 'json_schema',
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function createGenerateCommitMessageHandler(
|
|||||||
const systemPrompt = await getSystemPrompt(settingsService);
|
const systemPrompt = await getSystemPrompt(settingsService);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
settingsService,
|
settingsService,
|
||||||
'[GenerateCommitMessage]'
|
'[GenerateCommitMessage]'
|
||||||
);
|
);
|
||||||
@@ -196,6 +196,7 @@ export function createGenerateCommitMessageHandler(
|
|||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wrap with timeout to prevent indefinite hangs
|
// Wrap with timeout to prevent indefinite hangs
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ export class AgentService {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
this.settingsService,
|
this.settingsService,
|
||||||
'[AgentService]'
|
'[AgentService]'
|
||||||
);
|
);
|
||||||
@@ -386,6 +386,7 @@ export class AgentService {
|
|||||||
thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models
|
thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models
|
||||||
reasoningEffort: effectiveReasoningEffort, // Pass reasoning effort for Codex models
|
reasoningEffort: effectiveReasoningEffort, // Pass reasoning effort for Codex models
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build prompt content with images
|
// Build prompt content with images
|
||||||
|
|||||||
@@ -2059,7 +2059,10 @@ Format your response as a structured markdown document.`;
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(this.settingsService, '[AutoMode]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
this.settingsService,
|
||||||
|
'[AutoMode]'
|
||||||
|
);
|
||||||
|
|
||||||
const options: ExecuteOptions = {
|
const options: ExecuteOptions = {
|
||||||
prompt,
|
prompt,
|
||||||
@@ -2071,6 +2074,7 @@ Format your response as a structured markdown document.`;
|
|||||||
settingSources: sdkOptions.settingSources,
|
settingSources: sdkOptions.settingSources,
|
||||||
thinkingLevel: analysisThinkingLevel, // Pass thinking level
|
thinkingLevel: analysisThinkingLevel, // Pass thinking level
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = provider.executeQuery(options);
|
const stream = provider.executeQuery(options);
|
||||||
@@ -2940,7 +2944,10 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(this.settingsService, '[AutoMode]');
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
|
this.settingsService,
|
||||||
|
'[AutoMode]'
|
||||||
|
);
|
||||||
|
|
||||||
const executeOptions: ExecuteOptions = {
|
const executeOptions: ExecuteOptions = {
|
||||||
prompt: promptContent,
|
prompt: promptContent,
|
||||||
@@ -2954,6 +2961,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
|||||||
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
|
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
|
||||||
thinkingLevel: options?.thinkingLevel, // Pass thinking level for extended thinking
|
thinkingLevel: options?.thinkingLevel, // Pass thinking level for extended thinking
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute via provider
|
// Execute via provider
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export class IdeationService {
|
|||||||
const bareModel = stripProviderPrefix(modelId);
|
const bareModel = stripProviderPrefix(modelId);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
this.settingsService,
|
this.settingsService,
|
||||||
'[IdeationService]'
|
'[IdeationService]'
|
||||||
);
|
);
|
||||||
@@ -239,6 +239,7 @@ export class IdeationService {
|
|||||||
abortController: activeSession.abortController!,
|
abortController: activeSession.abortController!,
|
||||||
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
|
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = provider.executeQuery(executeOptions);
|
const stream = provider.executeQuery(executeOptions);
|
||||||
@@ -686,7 +687,7 @@ export class IdeationService {
|
|||||||
const bareModel = stripProviderPrefix(modelId);
|
const bareModel = stripProviderPrefix(modelId);
|
||||||
|
|
||||||
// Get active Claude API profile for alternative endpoint configuration
|
// Get active Claude API profile for alternative endpoint configuration
|
||||||
const claudeApiProfile = await getActiveClaudeApiProfile(
|
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
|
||||||
this.settingsService,
|
this.settingsService,
|
||||||
'[IdeationService]'
|
'[IdeationService]'
|
||||||
);
|
);
|
||||||
@@ -702,6 +703,7 @@ export class IdeationService {
|
|||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
claudeApiProfile, // Pass active Claude API profile for alternative endpoint configuration
|
||||||
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = provider.executeQuery(executeOptions);
|
const stream = provider.executeQuery(executeOptions);
|
||||||
|
|||||||
@@ -166,6 +166,41 @@ export class SettingsService {
|
|||||||
needsSave = true;
|
needsSave = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration v4 -> v5: Auto-create "Direct Anthropic" profile for existing users
|
||||||
|
// If user has an Anthropic API key in credentials but no profiles, create a
|
||||||
|
// "Direct Anthropic" profile that references the credentials and set it as active.
|
||||||
|
if (storedVersion < 5) {
|
||||||
|
try {
|
||||||
|
const credentials = await this.getCredentials();
|
||||||
|
const hasAnthropicKey = !!credentials.apiKeys?.anthropic;
|
||||||
|
const hasNoProfiles = !result.claudeApiProfiles || result.claudeApiProfiles.length === 0;
|
||||||
|
const hasNoActiveProfile = !result.activeClaudeApiProfileId;
|
||||||
|
|
||||||
|
if (hasAnthropicKey && hasNoProfiles && hasNoActiveProfile) {
|
||||||
|
const directAnthropicProfile = {
|
||||||
|
id: `profile-${Date.now()}-direct-anthropic`,
|
||||||
|
name: 'Direct Anthropic',
|
||||||
|
baseUrl: 'https://api.anthropic.com',
|
||||||
|
apiKeySource: 'credentials' as const,
|
||||||
|
useAuthToken: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.claudeApiProfiles = [directAnthropicProfile];
|
||||||
|
result.activeClaudeApiProfileId = directAnthropicProfile.id;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Migration v4->v5: Created "Direct Anthropic" profile using existing credentials'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
'Migration v4->v5: Could not check credentials for auto-profile creation:',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
needsSave = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Update version if any migration occurred
|
// Update version if any migration occurred
|
||||||
if (needsSave) {
|
if (needsSave) {
|
||||||
result.version = SETTINGS_VERSION;
|
result.version = SETTINGS_VERSION;
|
||||||
|
|||||||
@@ -109,6 +109,15 @@ export function ApiKeysSection() {
|
|||||||
{/* Security Notice */}
|
{/* Security Notice */}
|
||||||
<SecurityNotice />
|
<SecurityNotice />
|
||||||
|
|
||||||
|
{/* Profile Usage Note */}
|
||||||
|
<div className="text-xs text-muted-foreground/80 px-1">
|
||||||
|
<p>
|
||||||
|
API Keys saved here can be used by API Profiles with "credentials" as the API key
|
||||||
|
source. This lets you share a single key across multiple profile configurations without
|
||||||
|
re-entering it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex flex-wrap items-center gap-3 pt-2">
|
<div className="flex flex-wrap items-center gap-3 pt-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import type { ClaudeApiProfile } from '@automaker/types';
|
import type { ClaudeApiProfile, ApiKeySource } from '@automaker/types';
|
||||||
import { CLAUDE_API_PROFILE_TEMPLATES } from '@automaker/types';
|
import { CLAUDE_API_PROFILE_TEMPLATES } from '@automaker/types';
|
||||||
|
|
||||||
// Generate unique ID for profiles
|
// Generate unique ID for profiles
|
||||||
@@ -56,6 +56,7 @@ function maskApiKey(key: string): string {
|
|||||||
interface ProfileFormData {
|
interface ProfileFormData {
|
||||||
name: string;
|
name: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
apiKeySource: ApiKeySource;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
useAuthToken: boolean;
|
useAuthToken: boolean;
|
||||||
timeoutMs: string; // String for input, convert to number
|
timeoutMs: string; // String for input, convert to number
|
||||||
@@ -70,6 +71,7 @@ interface ProfileFormData {
|
|||||||
const emptyFormData: ProfileFormData = {
|
const emptyFormData: ProfileFormData = {
|
||||||
name: '',
|
name: '',
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
|
apiKeySource: 'inline',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
useAuthToken: false,
|
useAuthToken: false,
|
||||||
timeoutMs: '',
|
timeoutMs: '',
|
||||||
@@ -109,6 +111,7 @@ export function ApiProfilesSection() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: template.name,
|
name: template.name,
|
||||||
baseUrl: template.baseUrl,
|
baseUrl: template.baseUrl,
|
||||||
|
apiKeySource: template.defaultApiKeySource ?? 'inline',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
useAuthToken: template.useAuthToken,
|
useAuthToken: template.useAuthToken,
|
||||||
timeoutMs: template.timeoutMs?.toString() ?? '',
|
timeoutMs: template.timeoutMs?.toString() ?? '',
|
||||||
@@ -137,7 +140,8 @@ export function ApiProfilesSection() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: profile.name,
|
name: profile.name,
|
||||||
baseUrl: profile.baseUrl,
|
baseUrl: profile.baseUrl,
|
||||||
apiKey: profile.apiKey,
|
apiKeySource: profile.apiKeySource ?? 'inline',
|
||||||
|
apiKey: profile.apiKey ?? '',
|
||||||
useAuthToken: profile.useAuthToken ?? false,
|
useAuthToken: profile.useAuthToken ?? false,
|
||||||
timeoutMs: profile.timeoutMs?.toString() ?? '',
|
timeoutMs: profile.timeoutMs?.toString() ?? '',
|
||||||
modelMappings: {
|
modelMappings: {
|
||||||
@@ -158,7 +162,9 @@ export function ApiProfilesSection() {
|
|||||||
id: editingProfileId ?? generateProfileId(),
|
id: editingProfileId ?? generateProfileId(),
|
||||||
name: formData.name.trim(),
|
name: formData.name.trim(),
|
||||||
baseUrl: formData.baseUrl.trim(),
|
baseUrl: formData.baseUrl.trim(),
|
||||||
apiKey: formData.apiKey,
|
apiKeySource: formData.apiKeySource,
|
||||||
|
// Only include apiKey when source is 'inline'
|
||||||
|
apiKey: formData.apiKeySource === 'inline' ? formData.apiKey : undefined,
|
||||||
useAuthToken: formData.useAuthToken,
|
useAuthToken: formData.useAuthToken,
|
||||||
timeoutMs: formData.timeoutMs ? parseInt(formData.timeoutMs, 10) : undefined,
|
timeoutMs: formData.timeoutMs ? parseInt(formData.timeoutMs, 10) : undefined,
|
||||||
modelMappings:
|
modelMappings:
|
||||||
@@ -188,10 +194,11 @@ export function ApiProfilesSection() {
|
|||||||
setDeleteConfirmId(null);
|
setDeleteConfirmId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// API key is only required when source is 'inline'
|
||||||
const isFormValid =
|
const isFormValid =
|
||||||
formData.name.trim().length > 0 &&
|
formData.name.trim().length > 0 &&
|
||||||
formData.baseUrl.trim().length > 0 &&
|
formData.baseUrl.trim().length > 0 &&
|
||||||
formData.apiKey.length > 0;
|
(formData.apiKeySource !== 'inline' || formData.apiKey.length > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -333,7 +340,40 @@ export function ApiProfilesSection() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* API Key Source */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>API Key Source</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.apiKeySource}
|
||||||
|
onValueChange={(value: ApiKeySource) =>
|
||||||
|
setFormData({ ...formData, apiKeySource: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select API key source" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="credentials">
|
||||||
|
Use saved API key (from Settings → API Keys)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="env">Use environment variable (ANTHROPIC_API_KEY)</SelectItem>
|
||||||
|
<SelectItem value="inline">Enter key for this profile only</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{formData.apiKeySource === 'credentials' && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Will use the Anthropic key from Settings → API Keys
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{formData.apiKeySource === 'env' && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Will use ANTHROPIC_API_KEY environment variable
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Key (only shown for inline source) */}
|
||||||
|
{formData.apiKeySource === 'inline' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="profile-api-key">API Key</Label>
|
<Label htmlFor="profile-api-key">API Key</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -366,6 +406,7 @@ export function ApiProfilesSection() {
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Use Auth Token */}
|
{/* Use Auth Token */}
|
||||||
<div className="flex items-center justify-between py-2">
|
<div className="flex items-center justify-between py-2">
|
||||||
|
|||||||
383
docs/UNIFIED_API_KEY_PROFILES.md
Normal file
383
docs/UNIFIED_API_KEY_PROFILES.md
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
# Unified Claude API Key and Profile System
|
||||||
|
|
||||||
|
This document describes the implementation of a unified API key sourcing system for Claude API profiles, allowing flexible configuration of how API keys are resolved.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
Previously, Automaker had two separate systems for configuring Claude API access:
|
||||||
|
|
||||||
|
1. **API Keys section** (`credentials.json`): Stored Anthropic API key, used when no profile was active
|
||||||
|
2. **API Profiles section** (`settings.json`): Stored alternative endpoint configs (e.g., z.AI GLM) with their own inline API keys
|
||||||
|
|
||||||
|
This created several issues:
|
||||||
|
|
||||||
|
- Users configured Anthropic key in one place, but alternative endpoints in another
|
||||||
|
- No way to create a "Direct Anthropic" profile that reused the stored credentials
|
||||||
|
- Environment variable detection didn't integrate with the profile system
|
||||||
|
- Duplicated API key entry when users wanted the same key for multiple configurations
|
||||||
|
|
||||||
|
## Solution Overview
|
||||||
|
|
||||||
|
The solution introduces a flexible `apiKeySource` field on Claude API profiles that determines where the API key is resolved from:
|
||||||
|
|
||||||
|
| Source | Description |
|
||||||
|
| ------------- | ----------------------------------------------------------------- |
|
||||||
|
| `inline` | API key stored directly in the profile (legacy behavior, default) |
|
||||||
|
| `env` | Uses `ANTHROPIC_API_KEY` environment variable |
|
||||||
|
| `credentials` | Uses the Anthropic key from Settings → API Keys |
|
||||||
|
|
||||||
|
This allows:
|
||||||
|
|
||||||
|
- A single API key to be shared across multiple profile configurations
|
||||||
|
- "Direct Anthropic" profile that references saved credentials
|
||||||
|
- Environment variable support for CI/CD and containerized deployments
|
||||||
|
- Backwards compatibility with existing inline key profiles
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Type Changes
|
||||||
|
|
||||||
|
#### New Type: `ApiKeySource`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// libs/types/src/settings.ts
|
||||||
|
export type ApiKeySource = 'inline' | 'env' | 'credentials';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Updated Interface: `ClaudeApiProfile`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ClaudeApiProfile {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
baseUrl: string;
|
||||||
|
|
||||||
|
// NEW: API key sourcing strategy (default: 'inline' for backwards compat)
|
||||||
|
apiKeySource?: ApiKeySource;
|
||||||
|
|
||||||
|
// Now optional - only required when apiKeySource = 'inline'
|
||||||
|
apiKey?: string;
|
||||||
|
|
||||||
|
// Existing fields unchanged...
|
||||||
|
useAuthToken?: boolean;
|
||||||
|
timeoutMs?: number;
|
||||||
|
modelMappings?: { haiku?: string; sonnet?: string; opus?: string };
|
||||||
|
disableNonessentialTraffic?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Updated Interface: `ClaudeApiProfileTemplate`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ClaudeApiProfileTemplate {
|
||||||
|
name: string;
|
||||||
|
baseUrl: string;
|
||||||
|
defaultApiKeySource?: ApiKeySource; // NEW: Suggested source for this template
|
||||||
|
useAuthToken: boolean;
|
||||||
|
// ... other fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provider Templates
|
||||||
|
|
||||||
|
The following provider templates are available:
|
||||||
|
|
||||||
|
#### Direct Anthropic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: 'Direct Anthropic',
|
||||||
|
baseUrl: 'https://api.anthropic.com',
|
||||||
|
defaultApiKeySource: 'credentials',
|
||||||
|
useAuthToken: false,
|
||||||
|
description: 'Standard Anthropic API with your API key',
|
||||||
|
apiKeyUrl: 'https://console.anthropic.com/settings/keys',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OpenRouter
|
||||||
|
|
||||||
|
Access Claude and 300+ other models through OpenRouter's unified API.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: 'OpenRouter',
|
||||||
|
baseUrl: 'https://openrouter.ai/api',
|
||||||
|
defaultApiKeySource: 'inline',
|
||||||
|
useAuthToken: true,
|
||||||
|
description: 'Access Claude and 300+ models via OpenRouter',
|
||||||
|
apiKeyUrl: 'https://openrouter.ai/keys',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- Uses `ANTHROPIC_AUTH_TOKEN` with your OpenRouter API key
|
||||||
|
- No model mappings by default - OpenRouter auto-maps Anthropic models
|
||||||
|
- Can customize model mappings to use any OpenRouter-supported model (e.g., `openai/gpt-5.1-codex-max`)
|
||||||
|
|
||||||
|
#### z.AI GLM
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: 'z.AI GLM',
|
||||||
|
baseUrl: 'https://api.z.ai/api/anthropic',
|
||||||
|
defaultApiKeySource: 'inline',
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MiniMax
|
||||||
|
|
||||||
|
MiniMax M2.1 coding model with extended context support.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: 'MiniMax',
|
||||||
|
baseUrl: 'https://api.minimax.io/anthropic',
|
||||||
|
defaultApiKeySource: 'inline',
|
||||||
|
useAuthToken: true,
|
||||||
|
timeoutMs: 3000000,
|
||||||
|
modelMappings: {
|
||||||
|
haiku: 'MiniMax-M2.1',
|
||||||
|
sonnet: 'MiniMax-M2.1',
|
||||||
|
opus: 'MiniMax-M2.1',
|
||||||
|
},
|
||||||
|
disableNonessentialTraffic: true,
|
||||||
|
description: 'MiniMax M2.1 coding model with extended context',
|
||||||
|
apiKeyUrl: 'https://platform.minimax.io/user-center/basic-information/interface-key',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MiniMax (China)
|
||||||
|
|
||||||
|
Same as MiniMax but using the China-region endpoint.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: 'MiniMax (China)',
|
||||||
|
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||||
|
defaultApiKeySource: 'inline',
|
||||||
|
useAuthToken: true,
|
||||||
|
timeoutMs: 3000000,
|
||||||
|
modelMappings: {
|
||||||
|
haiku: 'MiniMax-M2.1',
|
||||||
|
sonnet: 'MiniMax-M2.1',
|
||||||
|
opus: 'MiniMax-M2.1',
|
||||||
|
},
|
||||||
|
disableNonessentialTraffic: true,
|
||||||
|
description: 'MiniMax M2.1 for users in China',
|
||||||
|
apiKeyUrl: 'https://platform.minimaxi.com/user-center/basic-information/interface-key',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Side Changes
|
||||||
|
|
||||||
|
#### 1. Environment Building (`claude-provider.ts`)
|
||||||
|
|
||||||
|
The `buildEnv()` function now resolves API keys based on the `apiKeySource`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function buildEnv(
|
||||||
|
profile?: ClaudeApiProfile,
|
||||||
|
credentials?: Credentials // NEW parameter
|
||||||
|
): Record<string, string | undefined> {
|
||||||
|
if (profile) {
|
||||||
|
// Resolve API key based on source strategy
|
||||||
|
let apiKey: string | undefined;
|
||||||
|
const source = profile.apiKeySource ?? 'inline';
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case 'inline':
|
||||||
|
apiKey = profile.apiKey;
|
||||||
|
break;
|
||||||
|
case 'env':
|
||||||
|
apiKey = process.env.ANTHROPIC_API_KEY;
|
||||||
|
break;
|
||||||
|
case 'credentials':
|
||||||
|
apiKey = credentials?.apiKeys?.anthropic;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... rest of profile-based env building
|
||||||
|
}
|
||||||
|
// ... no-profile fallback
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Settings Helper (`settings-helpers.ts`)
|
||||||
|
|
||||||
|
The `getActiveClaudeApiProfile()` function now returns both profile and credentials:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ActiveClaudeApiProfileResult {
|
||||||
|
profile: ClaudeApiProfile | undefined;
|
||||||
|
credentials: Credentials | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveClaudeApiProfile(
|
||||||
|
settingsService?: SettingsService | null,
|
||||||
|
logPrefix = '[SettingsHelper]'
|
||||||
|
): Promise<ActiveClaudeApiProfileResult> {
|
||||||
|
// Returns both profile and credentials for API key resolution
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Auto-Migration (`settings-service.ts`)
|
||||||
|
|
||||||
|
A v4→v5 migration automatically creates a "Direct Anthropic" profile for existing users:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Migration v4 -> v5: Auto-create "Direct Anthropic" profile
|
||||||
|
if (storedVersion < 5) {
|
||||||
|
const credentials = await this.getCredentials();
|
||||||
|
const hasAnthropicKey = !!credentials.apiKeys?.anthropic;
|
||||||
|
const hasNoProfiles = !result.claudeApiProfiles?.length;
|
||||||
|
const hasNoActiveProfile = !result.activeClaudeApiProfileId;
|
||||||
|
|
||||||
|
if (hasAnthropicKey && hasNoProfiles && hasNoActiveProfile) {
|
||||||
|
// Create "Direct Anthropic" profile with apiKeySource: 'credentials'
|
||||||
|
// and set it as active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Updated Call Sites
|
||||||
|
|
||||||
|
All files that call `getActiveClaudeApiProfile()` were updated to:
|
||||||
|
|
||||||
|
1. Destructure both `profile` and `credentials` from the result
|
||||||
|
2. Pass `credentials` to the provider via `ExecuteOptions`
|
||||||
|
|
||||||
|
**Files updated:**
|
||||||
|
|
||||||
|
- `apps/server/src/services/agent-service.ts`
|
||||||
|
- `apps/server/src/services/auto-mode-service.ts` (2 locations)
|
||||||
|
- `apps/server/src/services/ideation-service.ts` (2 locations)
|
||||||
|
- `apps/server/src/providers/simple-query-service.ts`
|
||||||
|
- `apps/server/src/routes/enhance-prompt/routes/enhance.ts`
|
||||||
|
- `apps/server/src/routes/context/routes/describe-file.ts`
|
||||||
|
- `apps/server/src/routes/context/routes/describe-image.ts`
|
||||||
|
- `apps/server/src/routes/github/routes/validate-issue.ts`
|
||||||
|
- `apps/server/src/routes/worktree/routes/generate-commit-message.ts`
|
||||||
|
- `apps/server/src/routes/features/routes/generate-title.ts`
|
||||||
|
- `apps/server/src/routes/backlog-plan/generate-plan.ts`
|
||||||
|
- `apps/server/src/routes/app-spec/sync-spec.ts`
|
||||||
|
- `apps/server/src/routes/app-spec/generate-features-from-spec.ts`
|
||||||
|
- `apps/server/src/routes/app-spec/generate-spec.ts`
|
||||||
|
- `apps/server/src/routes/suggestions/generate-suggestions.ts`
|
||||||
|
|
||||||
|
### UI Changes
|
||||||
|
|
||||||
|
#### 1. Profile Form (`api-profiles-section.tsx`)
|
||||||
|
|
||||||
|
Added an API Key Source selector dropdown:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Select
|
||||||
|
value={formData.apiKeySource}
|
||||||
|
onValueChange={(value: ApiKeySource) => setFormData({ ...formData, apiKeySource: value })}
|
||||||
|
>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="credentials">Use saved API key (from Settings → API Keys)</SelectItem>
|
||||||
|
<SelectItem value="env">Use environment variable (ANTHROPIC_API_KEY)</SelectItem>
|
||||||
|
<SelectItem value="inline">Enter key for this profile only</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
The API Key input field is now conditionally rendered only when `apiKeySource === 'inline'`.
|
||||||
|
|
||||||
|
#### 2. API Keys Section (`api-keys-section.tsx`)
|
||||||
|
|
||||||
|
Added an informational note:
|
||||||
|
|
||||||
|
> API Keys saved here can be used by API Profiles with "credentials" as the API key source. This lets you share a single key across multiple profile configurations without re-entering it.
|
||||||
|
|
||||||
|
## User Flows
|
||||||
|
|
||||||
|
### New User Flow
|
||||||
|
|
||||||
|
1. Go to Settings → API Keys
|
||||||
|
2. Enter Anthropic API key and save
|
||||||
|
3. Go to Settings → Providers → Claude
|
||||||
|
4. Create new profile from "Direct Anthropic" template
|
||||||
|
5. API Key Source defaults to "credentials" - no need to re-enter key
|
||||||
|
6. Save profile and set as active
|
||||||
|
|
||||||
|
### Existing User Migration
|
||||||
|
|
||||||
|
When an existing user with an Anthropic API key (but no profiles) loads settings:
|
||||||
|
|
||||||
|
1. System detects v4→v5 migration needed
|
||||||
|
2. Automatically creates "Direct Anthropic" profile with `apiKeySource: 'credentials'`
|
||||||
|
3. Sets new profile as active
|
||||||
|
4. User's existing workflow continues to work seamlessly
|
||||||
|
|
||||||
|
### Environment Variable Flow
|
||||||
|
|
||||||
|
For CI/CD or containerized deployments:
|
||||||
|
|
||||||
|
1. Set `ANTHROPIC_API_KEY` in environment
|
||||||
|
2. Create profile with `apiKeySource: 'env'`
|
||||||
|
3. Profile will use the environment variable at runtime
|
||||||
|
|
||||||
|
## Backwards Compatibility
|
||||||
|
|
||||||
|
- Profiles without `apiKeySource` field default to `'inline'`
|
||||||
|
- Existing profiles with inline `apiKey` continue to work unchanged
|
||||||
|
- No changes to the credentials file format
|
||||||
|
- Settings version bumped from 4 to 5 (migration is additive)
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
| --------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
||||||
|
| `libs/types/src/settings.ts` | Added `ApiKeySource` type, updated `ClaudeApiProfile`, added Direct Anthropic template |
|
||||||
|
| `libs/types/src/provider.ts` | Added `credentials` field to `ExecuteOptions` |
|
||||||
|
| `libs/types/src/index.ts` | Exported `ApiKeySource` type |
|
||||||
|
| `apps/server/src/providers/claude-provider.ts` | Updated `buildEnv()` to resolve keys from different sources |
|
||||||
|
| `apps/server/src/lib/settings-helpers.ts` | Updated return type to include credentials |
|
||||||
|
| `apps/server/src/services/settings-service.ts` | Added v4→v5 auto-migration |
|
||||||
|
| `apps/server/src/providers/simple-query-service.ts` | Added credentials passthrough |
|
||||||
|
| `apps/server/src/services/*.ts` | Updated to pass credentials |
|
||||||
|
| `apps/server/src/routes/**/*.ts` | Updated to pass credentials (15 files) |
|
||||||
|
| `apps/ui/src/.../api-profiles-section.tsx` | Added API Key Source selector |
|
||||||
|
| `apps/ui/src/.../api-keys-section.tsx` | Added profile usage note |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To verify the implementation:
|
||||||
|
|
||||||
|
1. **New user flow**: Create "Direct Anthropic" profile, select `credentials` source, enter key in API Keys section → verify it works
|
||||||
|
2. **Existing user migration**: User with credentials.json key sees auto-created "Direct Anthropic" profile
|
||||||
|
3. **Env var support**: Create profile with `env` source, set ANTHROPIC_API_KEY → verify it works
|
||||||
|
4. **z.AI GLM unchanged**: Existing profiles with inline keys continue working
|
||||||
|
5. **Backwards compat**: Profiles without `apiKeySource` field default to `inline`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and run
|
||||||
|
npm run build:packages
|
||||||
|
npm run dev:web
|
||||||
|
|
||||||
|
# Run server tests
|
||||||
|
npm run test:server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential future improvements:
|
||||||
|
|
||||||
|
1. **UI indicators**: Show whether credentials/env key is configured when selecting those sources
|
||||||
|
2. **Validation**: Warn if selected source has no key configured
|
||||||
|
3. **Per-provider credentials**: Support different credential keys for different providers
|
||||||
|
4. **Key rotation**: Support for rotating keys without updating profiles
|
||||||
@@ -162,6 +162,7 @@ export type {
|
|||||||
EventHookAction,
|
EventHookAction,
|
||||||
EventHook,
|
EventHook,
|
||||||
// Claude API profile types
|
// Claude API profile types
|
||||||
|
ApiKeySource,
|
||||||
ClaudeApiProfile,
|
ClaudeApiProfile,
|
||||||
ClaudeApiProfileTemplate,
|
ClaudeApiProfileTemplate,
|
||||||
} from './settings.js';
|
} from './settings.js';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Shared types for AI model providers
|
* Shared types for AI model providers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ThinkingLevel, ClaudeApiProfile } from './settings.js';
|
import type { ThinkingLevel, ClaudeApiProfile, Credentials } from './settings.js';
|
||||||
import type { CodexSandboxMode, CodexApprovalPolicy } from './codex.js';
|
import type { CodexSandboxMode, CodexApprovalPolicy } from './codex.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -215,6 +215,11 @@ export interface ExecuteOptions {
|
|||||||
* When undefined, uses direct Anthropic API (via API key or Claude Max CLI OAuth).
|
* When undefined, uses direct Anthropic API (via API key or Claude Max CLI OAuth).
|
||||||
*/
|
*/
|
||||||
claudeApiProfile?: ClaudeApiProfile;
|
claudeApiProfile?: ClaudeApiProfile;
|
||||||
|
/**
|
||||||
|
* Credentials for resolving 'credentials' apiKeySource in Claude API profiles.
|
||||||
|
* When a profile has apiKeySource='credentials', the Anthropic key from this object is used.
|
||||||
|
*/
|
||||||
|
credentials?: Credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -105,6 +105,15 @@ export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode';
|
|||||||
// Claude API Profiles - Configuration for Claude-compatible API endpoints
|
// Claude API Profiles - Configuration for Claude-compatible API endpoints
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApiKeySource - Strategy for sourcing API keys
|
||||||
|
*
|
||||||
|
* - 'inline': API key stored directly in the profile (legacy/default behavior)
|
||||||
|
* - 'env': Use ANTHROPIC_API_KEY environment variable
|
||||||
|
* - 'credentials': Use the Anthropic key from Settings → API Keys (credentials.json)
|
||||||
|
*/
|
||||||
|
export type ApiKeySource = 'inline' | 'env' | 'credentials';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ClaudeApiProfile - Configuration for a Claude-compatible API endpoint
|
* ClaudeApiProfile - Configuration for a Claude-compatible API endpoint
|
||||||
*
|
*
|
||||||
@@ -117,8 +126,15 @@ export interface ClaudeApiProfile {
|
|||||||
name: string;
|
name: string;
|
||||||
/** ANTHROPIC_BASE_URL - custom API endpoint */
|
/** ANTHROPIC_BASE_URL - custom API endpoint */
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
/** API key value */
|
/**
|
||||||
apiKey: string;
|
* API key sourcing strategy (default: 'inline' for backwards compatibility)
|
||||||
|
* - 'inline': Use apiKey field value
|
||||||
|
* - 'env': Use ANTHROPIC_API_KEY environment variable
|
||||||
|
* - 'credentials': Use the Anthropic key from credentials.json
|
||||||
|
*/
|
||||||
|
apiKeySource?: ApiKeySource;
|
||||||
|
/** API key value (only required when apiKeySource = 'inline' or undefined) */
|
||||||
|
apiKey?: string;
|
||||||
/** If true, use ANTHROPIC_AUTH_TOKEN instead of ANTHROPIC_API_KEY */
|
/** If true, use ANTHROPIC_AUTH_TOKEN instead of ANTHROPIC_API_KEY */
|
||||||
useAuthToken?: boolean;
|
useAuthToken?: boolean;
|
||||||
/** API_TIMEOUT_MS override in milliseconds */
|
/** API_TIMEOUT_MS override in milliseconds */
|
||||||
@@ -140,6 +156,8 @@ export interface ClaudeApiProfile {
|
|||||||
export interface ClaudeApiProfileTemplate {
|
export interface ClaudeApiProfileTemplate {
|
||||||
name: string;
|
name: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
/** Default API key source for this template (user chooses when creating) */
|
||||||
|
defaultApiKeySource?: ApiKeySource;
|
||||||
useAuthToken: boolean;
|
useAuthToken: boolean;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
modelMappings?: ClaudeApiProfile['modelMappings'];
|
modelMappings?: ClaudeApiProfile['modelMappings'];
|
||||||
@@ -150,9 +168,26 @@ export interface ClaudeApiProfileTemplate {
|
|||||||
|
|
||||||
/** Predefined templates for known Claude-compatible providers */
|
/** Predefined templates for known Claude-compatible providers */
|
||||||
export const CLAUDE_API_PROFILE_TEMPLATES: ClaudeApiProfileTemplate[] = [
|
export const CLAUDE_API_PROFILE_TEMPLATES: ClaudeApiProfileTemplate[] = [
|
||||||
|
{
|
||||||
|
name: 'Direct Anthropic',
|
||||||
|
baseUrl: 'https://api.anthropic.com',
|
||||||
|
defaultApiKeySource: 'credentials',
|
||||||
|
useAuthToken: false,
|
||||||
|
description: 'Standard Anthropic API with your API key',
|
||||||
|
apiKeyUrl: 'https://console.anthropic.com/settings/keys',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OpenRouter',
|
||||||
|
baseUrl: 'https://openrouter.ai/api',
|
||||||
|
defaultApiKeySource: 'inline',
|
||||||
|
useAuthToken: true,
|
||||||
|
description: 'Access Claude and 300+ models via OpenRouter',
|
||||||
|
apiKeyUrl: 'https://openrouter.ai/keys',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'z.AI GLM',
|
name: 'z.AI GLM',
|
||||||
baseUrl: 'https://api.z.ai/api/anthropic',
|
baseUrl: 'https://api.z.ai/api/anthropic',
|
||||||
|
defaultApiKeySource: 'inline',
|
||||||
useAuthToken: true,
|
useAuthToken: true,
|
||||||
timeoutMs: 3000000,
|
timeoutMs: 3000000,
|
||||||
modelMappings: {
|
modelMappings: {
|
||||||
@@ -164,6 +199,36 @@ export const CLAUDE_API_PROFILE_TEMPLATES: ClaudeApiProfileTemplate[] = [
|
|||||||
description: '3× usage at fraction of cost via GLM Coding Plan',
|
description: '3× usage at fraction of cost via GLM Coding Plan',
|
||||||
apiKeyUrl: 'https://z.ai/manage-apikey/apikey-list',
|
apiKeyUrl: 'https://z.ai/manage-apikey/apikey-list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'MiniMax',
|
||||||
|
baseUrl: 'https://api.minimax.io/anthropic',
|
||||||
|
defaultApiKeySource: 'inline',
|
||||||
|
useAuthToken: true,
|
||||||
|
timeoutMs: 3000000,
|
||||||
|
modelMappings: {
|
||||||
|
haiku: 'MiniMax-M2.1',
|
||||||
|
sonnet: 'MiniMax-M2.1',
|
||||||
|
opus: 'MiniMax-M2.1',
|
||||||
|
},
|
||||||
|
disableNonessentialTraffic: true,
|
||||||
|
description: 'MiniMax M2.1 coding model with extended context',
|
||||||
|
apiKeyUrl: 'https://platform.minimax.io/user-center/basic-information/interface-key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MiniMax (China)',
|
||||||
|
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||||
|
defaultApiKeySource: 'inline',
|
||||||
|
useAuthToken: true,
|
||||||
|
timeoutMs: 3000000,
|
||||||
|
modelMappings: {
|
||||||
|
haiku: 'MiniMax-M2.1',
|
||||||
|
sonnet: 'MiniMax-M2.1',
|
||||||
|
opus: 'MiniMax-M2.1',
|
||||||
|
},
|
||||||
|
disableNonessentialTraffic: true,
|
||||||
|
description: 'MiniMax M2.1 for users in China',
|
||||||
|
apiKeyUrl: 'https://platform.minimaxi.com/user-center/basic-information/interface-key',
|
||||||
|
},
|
||||||
// Future: Add AWS Bedrock, Google Vertex, etc.
|
// Future: Add AWS Bedrock, Google Vertex, etc.
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -906,7 +971,7 @@ export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Current version of the global settings schema */
|
/** Current version of the global settings schema */
|
||||||
export const SETTINGS_VERSION = 4;
|
export const SETTINGS_VERSION = 5;
|
||||||
/** Current version of the credentials schema */
|
/** Current version of the credentials schema */
|
||||||
export const CREDENTIALS_VERSION = 1;
|
export const CREDENTIALS_VERSION = 1;
|
||||||
/** Current version of the project settings schema */
|
/** Current version of the project settings schema */
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -6190,6 +6190,7 @@
|
|||||||
"version": "19.2.7",
|
"version": "19.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -6199,7 +6200,7 @@
|
|||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
@@ -8410,6 +8411,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/d3-color": {
|
"node_modules/d3-color": {
|
||||||
@@ -11303,7 +11305,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -11367,7 +11368,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user