mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-06 02:22:45 +00:00
fix(api): complete anthropic direct profile support
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
} from '../../api/services/profile-writer';
|
||||
import { apiProfileExists, listApiProfiles } from '../../api/services/profile-reader';
|
||||
import { normalizeDroidProvider } from '../../targets/droid-provider';
|
||||
import { updateSettingsFile, parseTarget } from './route-helpers';
|
||||
import { isAnthropicDirectProfile, updateSettingsFile, parseTarget } from './route-helpers';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -50,6 +50,9 @@ router.post('/', (req: Request, res: Response): void => {
|
||||
const { name, baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel, target } = req.body;
|
||||
const providerHint = req.body?.droidProvider ?? req.body?.provider;
|
||||
const parsedProvider = normalizeDroidProvider(providerHint);
|
||||
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
||||
const normalizedApiKey = typeof apiKey === 'string' ? apiKey.trim() : '';
|
||||
const allowsEmptyBaseUrl = isAnthropicDirectProfile(normalizedBaseUrl, normalizedApiKey);
|
||||
|
||||
const parsedTarget = parseTarget(target);
|
||||
if (target !== undefined && parsedTarget === null) {
|
||||
@@ -63,8 +66,10 @@ router.post('/', (req: Request, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name || !baseUrl || !apiKey) {
|
||||
res.status(400).json({ error: 'Missing required fields: name, baseUrl, apiKey' });
|
||||
if (!name || !normalizedApiKey || (!normalizedBaseUrl && !allowsEmptyBaseUrl)) {
|
||||
res.status(400).json({
|
||||
error: 'Missing required fields: name, apiKey, and baseUrl for proxy profiles',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,8 +91,8 @@ router.post('/', (req: Request, res: Response): void => {
|
||||
// Create profile using unified-config-aware service
|
||||
const result = createApiProfile(
|
||||
name,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
normalizedBaseUrl,
|
||||
normalizedApiKey,
|
||||
{
|
||||
default: model || '',
|
||||
opus: opusModel || model || '',
|
||||
@@ -119,6 +124,8 @@ router.put('/:name', (req: Request, res: Response): void => {
|
||||
const { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel, target } = req.body;
|
||||
const providerHint = req.body?.droidProvider ?? req.body?.provider;
|
||||
const parsedProvider = normalizeDroidProvider(providerHint);
|
||||
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : baseUrl;
|
||||
const normalizedApiKey = typeof apiKey === 'string' ? apiKey.trim() : apiKey;
|
||||
|
||||
const parsedTarget = parseTarget(target);
|
||||
if (target !== undefined && parsedTarget === null) {
|
||||
@@ -139,11 +146,7 @@ router.put('/:name', (req: Request, res: Response): void => {
|
||||
}
|
||||
|
||||
// Validate required fields if provided (prevent setting to empty)
|
||||
if (baseUrl !== undefined && !baseUrl.trim()) {
|
||||
res.status(400).json({ error: 'baseUrl cannot be empty' });
|
||||
return;
|
||||
}
|
||||
if (apiKey !== undefined && !apiKey.trim()) {
|
||||
if (normalizedApiKey !== undefined && !normalizedApiKey) {
|
||||
res.status(400).json({ error: 'apiKey cannot be empty' });
|
||||
return;
|
||||
}
|
||||
@@ -166,8 +169,8 @@ router.put('/:name', (req: Request, res: Response): void => {
|
||||
|
||||
if (hasSettingsUpdates) {
|
||||
updateSettingsFile(name, {
|
||||
baseUrl,
|
||||
apiKey,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
apiKey: normalizedApiKey,
|
||||
model,
|
||||
opusModel,
|
||||
sonnetModel,
|
||||
|
||||
@@ -82,6 +82,19 @@ function canonicalizeModelForProvider(
|
||||
return canonicalizeModelIdForProvider(value, provider);
|
||||
}
|
||||
|
||||
function isOpenRouterUrl(baseUrl: string): boolean {
|
||||
return baseUrl.toLowerCase().includes('openrouter.ai');
|
||||
}
|
||||
|
||||
export function isAnthropicDirectProfile(
|
||||
baseUrl: string | undefined | null,
|
||||
apiKey: string | undefined | null
|
||||
): boolean {
|
||||
const normalizedBaseUrl = baseUrl?.trim().toLowerCase() || '';
|
||||
const normalizedApiKey = apiKey?.trim() || '';
|
||||
return normalizedApiKey.startsWith('sk-ant-') || normalizedBaseUrl.includes('api.anthropic.com');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read config safely with fallback.
|
||||
* Uses loadConfigSafe which supports both unified (config.yaml) and legacy (config.json).
|
||||
@@ -169,11 +182,19 @@ export function createSettingsFile(
|
||||
baseUrl,
|
||||
model: canonicalModel,
|
||||
});
|
||||
const normalizedBaseUrl = baseUrl.trim();
|
||||
const normalizedApiKey = apiKey.trim();
|
||||
const isNative = isAnthropicDirectProfile(normalizedBaseUrl, normalizedApiKey);
|
||||
|
||||
const settings: Settings = {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
...(isNative
|
||||
? { ANTHROPIC_API_KEY: normalizedApiKey }
|
||||
: {
|
||||
ANTHROPIC_BASE_URL: normalizedBaseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: normalizedApiKey,
|
||||
...(isOpenRouterUrl(normalizedBaseUrl) && { ANTHROPIC_API_KEY: '' }),
|
||||
}),
|
||||
...(canonicalModel && { ANTHROPIC_MODEL: canonicalModel }),
|
||||
...(canonicalOpusModel && { ANTHROPIC_DEFAULT_OPUS_MODEL: canonicalOpusModel }),
|
||||
...(canonicalSonnetModel && { ANTHROPIC_DEFAULT_SONNET_MODEL: canonicalSonnetModel }),
|
||||
@@ -209,8 +230,23 @@ export function updateSettingsFile(
|
||||
}
|
||||
|
||||
const settings = loadSettings(settingsPath);
|
||||
const currentBaseUrl = settings.env?.ANTHROPIC_BASE_URL?.trim() || '';
|
||||
const currentApiKey =
|
||||
settings.env?.ANTHROPIC_API_KEY?.trim() || settings.env?.ANTHROPIC_AUTH_TOKEN?.trim() || '';
|
||||
const nextBaseUrl = updates.baseUrl !== undefined ? updates.baseUrl.trim() : currentBaseUrl;
|
||||
const nextApiKey = updates.apiKey !== undefined ? updates.apiKey.trim() : currentApiKey;
|
||||
const isNative = isAnthropicDirectProfile(nextBaseUrl, nextApiKey);
|
||||
|
||||
if (!nextApiKey) {
|
||||
throw new ValidationError('apiKey cannot be empty', 'apiKey');
|
||||
}
|
||||
|
||||
if (!isNative && nextBaseUrl.length === 0) {
|
||||
throw new ValidationError('baseUrl cannot be empty', 'baseUrl');
|
||||
}
|
||||
|
||||
const providerForValidation =
|
||||
resolveProviderForModelCanonicalization(updates.baseUrl, updates.provider) ??
|
||||
resolveProviderForModelCanonicalization(nextBaseUrl, updates.provider) ??
|
||||
resolveProviderForModelCanonicalization(
|
||||
settings.env?.ANTHROPIC_BASE_URL,
|
||||
updates.provider ?? settings.env?.CCS_DROID_PROVIDER
|
||||
@@ -247,14 +283,19 @@ export function updateSettingsFile(
|
||||
throw new ValidationError(deniedReason, 'model');
|
||||
}
|
||||
|
||||
if (updates.baseUrl) {
|
||||
settings.env = settings.env || {};
|
||||
settings.env.ANTHROPIC_BASE_URL = updates.baseUrl;
|
||||
}
|
||||
|
||||
if (updates.apiKey) {
|
||||
settings.env = settings.env || {};
|
||||
settings.env.ANTHROPIC_AUTH_TOKEN = updates.apiKey;
|
||||
settings.env = settings.env || {};
|
||||
if (isNative) {
|
||||
delete settings.env.ANTHROPIC_BASE_URL;
|
||||
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
||||
settings.env.ANTHROPIC_API_KEY = nextApiKey;
|
||||
} else {
|
||||
settings.env.ANTHROPIC_BASE_URL = nextBaseUrl;
|
||||
settings.env.ANTHROPIC_AUTH_TOKEN = nextApiKey;
|
||||
if (isOpenRouterUrl(nextBaseUrl)) {
|
||||
settings.env.ANTHROPIC_API_KEY = '';
|
||||
} else {
|
||||
delete settings.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.model !== undefined) {
|
||||
@@ -303,7 +344,7 @@ export function updateSettingsFile(
|
||||
settings.env = settings.env || {};
|
||||
const resolvedProvider = resolveDroidProvider({
|
||||
provider: updates.provider ?? settings.env.CCS_DROID_PROVIDER,
|
||||
baseUrl: updates.baseUrl ?? settings.env.ANTHROPIC_BASE_URL,
|
||||
baseUrl: nextBaseUrl,
|
||||
model: canonicalModel ?? settings.env.ANTHROPIC_MODEL,
|
||||
});
|
||||
settings.env.CCS_DROID_PROVIDER = resolvedProvider;
|
||||
|
||||
@@ -366,6 +366,9 @@ const REQUIRED_ENV_KEYS = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN'] as cons
|
||||
/** Check if settings have required fields (returns missing list for warnings) */
|
||||
function checkRequiredEnvVars(settings: Settings): string[] {
|
||||
const env = settings?.env || {};
|
||||
if (env.ANTHROPIC_API_KEY?.trim() && !env.ANTHROPIC_BASE_URL?.trim()) {
|
||||
return [];
|
||||
}
|
||||
return REQUIRED_ENV_KEYS.filter((key) => !env[key]?.trim());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user