feat: unified Claude API key and profile system with z.AI, MiniMax, OpenRouter support (#600)

* feat: add Claude API provider profiles for alternative endpoints

Add support for managing multiple Claude-compatible API endpoints
(z.AI GLM, AWS Bedrock, etc.) through provider profiles in settings.

Features:
- New ClaudeApiProfile type with base URL, API key, model mappings
- Pre-configured z.AI GLM template with correct model names
- Profile selector in Settings > Claude > API Profiles
- Clean switching between profiles and direct Anthropic API
- Immediate persistence to prevent data loss on restart

Profile support added to all execution paths:
- Agent service (chat)
- Ideation service
- Auto-mode service (feature agents, enhancements)
- Simple query service (title generation, descriptions, etc.)
- Backlog planning, commit messages, spec generation
- GitHub issue validation, suggestions

Environment variables set when profile is active:
- ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN/API_KEY
- ANTHROPIC_DEFAULT_HAIKU/SONNET/OPUS_MODEL
- API_TIMEOUT_MS, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
This commit is contained in:
Stefan de Vogelaere
2026-01-19 20:36:58 +01:00
committed by GitHub
parent 63b8eb0991
commit d97c4b7b57
45 changed files with 2661 additions and 146 deletions

View File

@@ -166,6 +166,41 @@ export class SettingsService {
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
if (needsSave) {
result.version = SETTINGS_VERSION;
@@ -372,6 +407,7 @@ export class SettingsService {
ignoreEmptyArrayOverwrite('recentFolders');
ignoreEmptyArrayOverwrite('mcpServers');
ignoreEmptyArrayOverwrite('enabledCursorModels');
ignoreEmptyArrayOverwrite('claudeApiProfiles');
// Empty object overwrite guard
if (
@@ -597,6 +633,17 @@ export class SettingsService {
};
}
// Handle activeClaudeApiProfileId special cases:
// - "__USE_GLOBAL__" marker means delete the key (use global setting)
// - null means explicit "Direct Anthropic API"
// - string means specific profile ID
if (
'activeClaudeApiProfileId' in updates &&
updates.activeClaudeApiProfileId === '__USE_GLOBAL__'
) {
delete updated.activeClaudeApiProfileId;
}
await writeSettingsJson(settingsPath, updated);
logger.info(`Project settings updated for ${projectPath}`);