fix(auth): preserve xAI profile compatibility

This commit is contained in:
Tam Nhu Tran committed 2026-07-18 19:04:37 -04:00
1 parent 4e4c949967
commit 44718e98ef
19 files changed
+1166 -106

No files matched your search

+7 -1
View File
@@ -5,7 +5,13 @@
*/
// Validation services
export { validateApiName, validateUrl, getUrlWarning, sanitizeBaseUrl } from './validation-service';
export {
validateApiName,
validateApiNameSyntax,
validateUrl,
getUrlWarning,
sanitizeBaseUrl,
} from './validation-service';
// Profile types
export {
+34 -11
View File
@@ -9,14 +9,19 @@ import * as path from 'path';
import type { Config, Settings } from '../../types';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import { ConfigError, ProfileError } from '../../errors/error-types';
import { getConfigPath } from '../../utils/config-manager';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import { isSensitiveKey } from '../../utils/sensitive-keys';
import { isReservedName } from '../../config/reserved-names';
import {
canOverwriteGrandfatheredReservedProfileName,
isGrandfatheredReservedProfileName,
isReservedName,
} from '../../config/reserved-names';
import { validateApiName } from './validation-service';
import { listApiProfiles } from './profile-reader';
import { validateApiName, validateApiNameSyntax } from './validation-service';
import { apiProfileExists, listApiProfiles } from './profile-reader';
import { validateApiProfileSettingsPayload } from './profile-lifecycle-validation';
import type {
ApiProfileExportBundle,
@@ -44,7 +49,7 @@ function parseTargetValue(value: unknown): TargetType | null {
}
function validateProfileNameForPath(name: string, label: string): string | null {
const validationError = validateApiName(name);
const validationError = validateApiNameSyntax(name);
if (validationError) {
return `Invalid ${label} profile name "${name}": ${validationError}`;
}
@@ -70,7 +75,7 @@ function registerApiProfileInConfig(name: string, target: TargetType, force = fa
if (isUnifiedMode()) {
mutateConfig((config) => {
if (config.profiles[name] && !force) {
throw new Error(`API profile already exists: ${name}`);
throw new ProfileError(`API profile already exists: ${name}`, name);
}
config.profiles[name] = {
@@ -85,7 +90,7 @@ function registerApiProfileInConfig(name: string, target: TargetType, force = fa
const configPath = getConfigPath();
const config = loadConfigSafe() as Config;
if (config.profiles[name] && !force) {
throw new Error(`API profile already exists: ${name}`);
throw new ProfileError(`API profile already exists: ${name}`, name);
}
config.profiles[name] = `~/.ccs/${name}${SETTINGS_FILE_SUFFIX}`;
@@ -124,7 +129,7 @@ function readJsonObject(filePath: string): Record<string, unknown> {
const raw = fs.readFileSync(filePath, 'utf8');
const parsed = JSON.parse(raw);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('Settings file must contain a JSON object.');
throw new ConfigError('Settings file must contain a JSON object.', filePath);
}
return parsed as Record<string, unknown>;
}
@@ -158,7 +163,10 @@ export function discoverApiProfileOrphans(): DiscoverApiProfileOrphansResult {
.filter((file) => !registeredSettings.has(file))
.filter((file) => !file.startsWith('base-'))
.filter((file) => !ignoredNames.has(file))
.filter((file) => !isReservedName(file.slice(0, -SETTINGS_FILE_SUFFIX.length)))
.filter((file) => {
const name = file.slice(0, -SETTINGS_FILE_SUFFIX.length);
return !isReservedName(name) || isGrandfatheredReservedProfileName(name);
})
.map((file) => {
const name = file.slice(0, -SETTINGS_FILE_SUFFIX.length);
const settingsPath = path.join(ccsDir, file);
@@ -206,7 +214,7 @@ export function registerApiProfileOrphans(options?: {
const result: RegisterApiProfileOrphansResult = { registered: [], skipped: [] };
for (const orphan of selected) {
const nameError = validateApiName(orphan.name);
const nameError = validateApiNameSyntax(orphan.name);
if (nameError) {
result.skipped.push({
name: orphan.name,
@@ -247,7 +255,16 @@ export function copyApiProfile(
if (sourceError) return { success: false, error: sourceError };
const destinationError = validateApiName(destination);
if (destinationError) return { success: false, error: destinationError };
const canOverwriteGrandfatheredDestination = canOverwriteGrandfatheredReservedProfileName(
destination,
{
force: options?.force === true,
exists: apiProfileExists(destination),
}
);
if (destinationError && !canOverwriteGrandfatheredDestination) {
return { success: false, error: destinationError };
}
const sourceSettingsPath = getProfileSettingsPath(source);
if (!fs.existsSync(sourceSettingsPath)) {
@@ -363,7 +380,13 @@ export function importApiProfileBundle(
const name = options?.name || input.profile.name;
const nameError = validateApiName(name);
if (nameError) return { success: false, error: nameError };
const canOverwriteGrandfatheredProfile = canOverwriteGrandfatheredReservedProfileName(name, {
force: options?.force === true,
exists: apiProfileExists(name),
});
if (nameError && !canOverwriteGrandfatheredProfile) {
return { success: false, error: nameError };
}
const bundleTarget = parseTargetValue(input.profile.target);
if (input.profile.target !== undefined && bundleTarget === null) {
+27 -5
View File
@@ -12,7 +12,10 @@ import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import type { TargetType } from '../../targets/target-adapter';
import { resolveDroidProvider } from '../../targets/droid-provider';
import { isReservedName } from '../../config/reserved-names';
import {
canOverwriteGrandfatheredReservedProfileName,
isReservedName,
} from '../../config/reserved-names';
import { mapExternalProviderName } from '../../cliproxy/provider-capabilities';
import {
extractProviderFromPathname,
@@ -266,8 +269,18 @@ export function createApiProfile(
models: ModelMapping,
target: TargetType = 'claude',
provider?: string,
extraModels?: string[]
extraModels?: string[],
options?: { force?: boolean }
): CreateApiProfileResult {
const nameError = validateApiName(name);
const canOverwriteGrandfatheredProfile = canOverwriteGrandfatheredReservedProfileName(name, {
force: options?.force === true,
exists: apiProfileExists(name),
});
if (nameError && !canOverwriteGrandfatheredProfile) {
return { success: false, settingsFile: '', error: nameError };
}
try {
const deniedReason = getDeniedModelReason(baseUrl, models);
if (deniedReason) {
@@ -304,10 +317,17 @@ export function createCliproxyBridgeProfile(
const providedName = options.name?.trim();
if (providedName) {
const nameError = validateApiName(providedName);
if (nameError) {
const canOverwriteGrandfatheredProfile = canOverwriteGrandfatheredReservedProfileName(
providedName,
{
force: options.force === true,
exists: apiProfileExists(providedName),
}
);
if (nameError && !canOverwriteGrandfatheredProfile) {
return { success: false, settingsFile: '', error: nameError };
}
if (isReservedName(providedName)) {
if (isReservedName(providedName) && !canOverwriteGrandfatheredProfile) {
return {
success: false,
settingsFile: '',
@@ -332,7 +352,9 @@ export function createCliproxyBridgeProfile(
resolved.apiKey,
resolved.models,
resolved.target,
provider
provider,
undefined,
{ force: options.force }
);
const detectedBridge = resolveCliproxyBridgeMetadata({
env: {
+20 -5
View File
@@ -8,10 +8,13 @@
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
/**
* Validate API profile name
* Validate API profile name syntax for an existing profile reference.
*
* Existing profiles may use a name that became reserved after they were
* created, so this validator intentionally excludes built-in-name policy.
* @returns Error message if invalid, null if valid
*/
export function validateApiName(name: string): string | null {
export function validateApiNameSyntax(name: string): string | null {
if (!name) {
return 'API name is required';
}
@@ -21,15 +24,27 @@ export function validateApiName(name: string): string | null {
if (name.length > 32) {
return 'API name must be 32 characters or less';
}
if (isReservedName(name)) {
return `'${name}' is a reserved name`;
}
if (isWindowsReservedName(name)) {
return `'${name}' is a Windows reserved device name and cannot be used`;
}
return null;
}
/**
* Validate a name for creation of a new API profile.
* @returns Error message if invalid or reserved, null if valid
*/
export function validateApiName(name: string): string | null {
const syntaxError = validateApiNameSyntax(name);
if (syntaxError) {
return syntaxError;
}
if (isReservedName(name)) {
return `'${name}' is a reserved name`;
}
return null;
}
/**
* Validate URL format
* @returns Error message if invalid, null if valid