mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 12:19:35 +00:00
fix: normalize stale copilot raptor-mini model
This commit is contained in:
@@ -25,6 +25,7 @@ import { getCcsDir } from '../utils/config-manager';
|
|||||||
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../utils/profile-compat';
|
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../utils/profile-compat';
|
||||||
import type { CLIProxyProvider } from '../cliproxy/types';
|
import type { CLIProxyProvider } from '../cliproxy/types';
|
||||||
import { CLIPROXY_PROVIDER_IDS, isCLIProxyProvider } from '../cliproxy/provider-capabilities';
|
import { CLIPROXY_PROVIDER_IDS, isCLIProxyProvider } from '../cliproxy/provider-capabilities';
|
||||||
|
import { normalizeCopilotModelId } from '../copilot/copilot-model-normalizer';
|
||||||
import type { TargetType } from '../targets/target-adapter';
|
import type { TargetType } from '../targets/target-adapter';
|
||||||
import type { ProfileType } from '../types/profile';
|
import type { ProfileType } from '../types/profile';
|
||||||
export type { ProfileType } from '../types/profile';
|
export type { ProfileType } from '../types/profile';
|
||||||
@@ -446,8 +447,9 @@ class ProfileDetector {
|
|||||||
if (unifiedConfig) {
|
if (unifiedConfig) {
|
||||||
// Copilot profile (if enabled)
|
// Copilot profile (if enabled)
|
||||||
if (unifiedConfig.copilot?.enabled) {
|
if (unifiedConfig.copilot?.enabled) {
|
||||||
|
const currentCopilotModel = normalizeCopilotModelId(unifiedConfig.copilot.model);
|
||||||
lines.push('GitHub Copilot (via copilot-api):');
|
lines.push('GitHub Copilot (via copilot-api):');
|
||||||
lines.push(` - copilot (model: ${unifiedConfig.copilot.model})`);
|
lines.push(` - copilot (model: ${currentCopilotModel})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLIProxy variants from unified config
|
// CLIProxy variants from unified config
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
stopDaemon,
|
stopDaemon,
|
||||||
getAvailableModels,
|
getAvailableModels,
|
||||||
isCopilotApiInstalled,
|
isCopilotApiInstalled,
|
||||||
|
normalizeCopilotConfig,
|
||||||
} from '../copilot';
|
} from '../copilot';
|
||||||
import type { CopilotModel } from '../copilot';
|
import type { CopilotModel } from '../copilot';
|
||||||
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../config/unified-config-loader';
|
||||||
@@ -125,7 +126,7 @@ async function handleAuth(): Promise<number> {
|
|||||||
*/
|
*/
|
||||||
async function handleStatus(): Promise<number> {
|
async function handleStatus(): Promise<number> {
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
const copilotConfig = config.copilot ?? DEFAULT_COPILOT_CONFIG;
|
const copilotConfig = normalizeCopilotConfig(config.copilot ?? DEFAULT_COPILOT_CONFIG);
|
||||||
|
|
||||||
const status = await getCopilotStatus(copilotConfig);
|
const status = await getCopilotStatus(copilotConfig);
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ async function handleStatus(): Promise<number> {
|
|||||||
*/
|
*/
|
||||||
async function handleModels(): Promise<number> {
|
async function handleModels(): Promise<number> {
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
const copilotConfig = config.copilot ?? DEFAULT_COPILOT_CONFIG;
|
const copilotConfig = normalizeCopilotConfig(config.copilot ?? DEFAULT_COPILOT_CONFIG);
|
||||||
|
|
||||||
console.log('Available Copilot Models');
|
console.log('Available Copilot Models');
|
||||||
console.log('────────────────────────');
|
console.log('────────────────────────');
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getGlobalEnvConfig } from '../config/unified-config-loader';
|
|||||||
import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth';
|
import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth';
|
||||||
import { isDaemonRunning, startDaemon } from './copilot-daemon';
|
import { isDaemonRunning, startDaemon } from './copilot-daemon';
|
||||||
import { ensureCopilotApi } from './copilot-package-manager';
|
import { ensureCopilotApi } from './copilot-package-manager';
|
||||||
|
import { normalizeCopilotConfig } from './copilot-model-normalizer';
|
||||||
import { CopilotStatus } from './types';
|
import { CopilotStatus } from './types';
|
||||||
import { fail, info, ok } from '../utils/ui';
|
import { fail, info, ok } from '../utils/ui';
|
||||||
import { getWebSearchHookEnv } from '../utils/websearch-manager';
|
import { getWebSearchHookEnv } from '../utils/websearch-manager';
|
||||||
@@ -21,16 +22,17 @@ import { stripClaudeCodeEnv } from '../utils/shell-executor';
|
|||||||
* Get full copilot status (auth + daemon).
|
* Get full copilot status (auth + daemon).
|
||||||
*/
|
*/
|
||||||
export async function getCopilotStatus(config: CopilotConfig): Promise<CopilotStatus> {
|
export async function getCopilotStatus(config: CopilotConfig): Promise<CopilotStatus> {
|
||||||
|
const normalizedConfig = normalizeCopilotConfig(config);
|
||||||
const [auth, daemonRunning] = await Promise.all([
|
const [auth, daemonRunning] = await Promise.all([
|
||||||
checkAuthStatus(),
|
checkAuthStatus(),
|
||||||
isDaemonRunning(config.port),
|
isDaemonRunning(normalizedConfig.port),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
auth,
|
auth,
|
||||||
daemon: {
|
daemon: {
|
||||||
running: daemonRunning,
|
running: daemonRunning,
|
||||||
port: config.port,
|
port: normalizedConfig.port,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -43,17 +45,19 @@ export function generateCopilotEnv(
|
|||||||
config: CopilotConfig,
|
config: CopilotConfig,
|
||||||
claudeConfigDir?: string
|
claudeConfigDir?: string
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
|
const normalizedConfig = normalizeCopilotConfig(config);
|
||||||
|
|
||||||
// Use mapped models if configured, otherwise fall back to default model
|
// Use mapped models if configured, otherwise fall back to default model
|
||||||
const opusModel = config.opus_model || config.model;
|
const opusModel = normalizedConfig.opus_model || normalizedConfig.model;
|
||||||
const sonnetModel = config.sonnet_model || config.model;
|
const sonnetModel = normalizedConfig.sonnet_model || normalizedConfig.model;
|
||||||
const haikuModel = config.haiku_model || config.model;
|
const haikuModel = normalizedConfig.haiku_model || normalizedConfig.model;
|
||||||
|
|
||||||
// Use 127.0.0.1 instead of localhost for more reliable local connections
|
// Use 127.0.0.1 instead of localhost for more reliable local connections
|
||||||
// (bypasses DNS resolution and potential IPv6 issues)
|
// (bypasses DNS resolution and potential IPv6 issues)
|
||||||
return {
|
return {
|
||||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.port}`,
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${normalizedConfig.port}`,
|
||||||
ANTHROPIC_AUTH_TOKEN: 'dummy', // copilot-api handles auth internally
|
ANTHROPIC_AUTH_TOKEN: 'dummy', // copilot-api handles auth internally
|
||||||
ANTHROPIC_MODEL: config.model,
|
ANTHROPIC_MODEL: normalizedConfig.model,
|
||||||
// Model tier mapping - allows different models for opus/sonnet/haiku
|
// Model tier mapping - allows different models for opus/sonnet/haiku
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel,
|
ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel,
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel,
|
ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel,
|
||||||
@@ -79,6 +83,8 @@ export async function executeCopilotProfile(
|
|||||||
claudeConfigDir?: string,
|
claudeConfigDir?: string,
|
||||||
claudeCliPath: string = 'claude'
|
claudeCliPath: string = 'claude'
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
const normalizedConfig = normalizeCopilotConfig(config);
|
||||||
|
|
||||||
// Ensure copilot-api is installed (auto-install if missing, auto-update if outdated)
|
// Ensure copilot-api is installed (auto-install if missing, auto-update if outdated)
|
||||||
try {
|
try {
|
||||||
await ensureCopilotApi();
|
await ensureCopilotApi();
|
||||||
@@ -111,17 +117,17 @@ export async function executeCopilotProfile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if daemon is running or needs to be started
|
// Check if daemon is running or needs to be started
|
||||||
let daemonRunning = await isDaemonRunning(config.port);
|
let daemonRunning = await isDaemonRunning(normalizedConfig.port);
|
||||||
|
|
||||||
if (!daemonRunning) {
|
if (!daemonRunning) {
|
||||||
if (config.auto_start) {
|
if (normalizedConfig.auto_start) {
|
||||||
console.log(info('Starting copilot-api daemon...'));
|
console.log(info('Starting copilot-api daemon...'));
|
||||||
const result = await startDaemon(config);
|
const result = await startDaemon(normalizedConfig);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error(fail(`Failed to start daemon: ${result.error}`));
|
console.error(fail(`Failed to start daemon: ${result.error}`));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
console.log(ok(`Daemon started on port ${config.port}`));
|
console.log(ok(`Daemon started on port ${normalizedConfig.port}`));
|
||||||
daemonRunning = true;
|
daemonRunning = true;
|
||||||
} else {
|
} else {
|
||||||
console.error(fail('copilot-api daemon is not running.'));
|
console.error(fail('copilot-api daemon is not running.'));
|
||||||
@@ -129,7 +135,7 @@ export async function executeCopilotProfile(
|
|||||||
console.error('Start the daemon:');
|
console.error('Start the daemon:');
|
||||||
console.error(' ccs copilot start');
|
console.error(' ccs copilot start');
|
||||||
console.error('Fallback manual command:');
|
console.error('Fallback manual command:');
|
||||||
console.error(` npx copilot-api start --port ${config.port}`);
|
console.error(` npx copilot-api start --port ${normalizedConfig.port}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('Or enable auto_start in config:');
|
console.error('Or enable auto_start in config:');
|
||||||
console.error(' ccs config (then enable auto_start in Copilot section)');
|
console.error(' ccs config (then enable auto_start in Copilot section)');
|
||||||
@@ -138,7 +144,7 @@ export async function executeCopilotProfile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate environment for Claude
|
// Generate environment for Claude
|
||||||
const copilotEnv = generateCopilotEnv(config, claudeConfigDir);
|
const copilotEnv = generateCopilotEnv(normalizedConfig, claudeConfigDir);
|
||||||
|
|
||||||
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
|
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
|
||||||
const globalEnvConfig = getGlobalEnvConfig();
|
const globalEnvConfig = getGlobalEnvConfig();
|
||||||
@@ -156,7 +162,7 @@ export async function executeCopilotProfile(
|
|||||||
CCS_PROFILE_TYPE: 'copilot',
|
CCS_PROFILE_TYPE: 'copilot',
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(info(`Using GitHub Copilot proxy (model: ${config.model})`));
|
console.log(info(`Using GitHub Copilot proxy (model: ${normalizedConfig.model})`));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Spawn Claude CLI
|
// Spawn Claude CLI
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { DEFAULT_COPILOT_CONFIG, type CopilotConfig } from '../config/unified-config-types';
|
||||||
|
|
||||||
|
const LEGACY_COPILOT_MODEL_FALLBACKS: Readonly<Record<string, string>> = Object.freeze({
|
||||||
|
// copilot-api v0.7.0 no longer advertises this ID in /v1/models and rejects it at runtime.
|
||||||
|
'raptor-mini': DEFAULT_COPILOT_CONFIG.model,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DEPRECATED_COPILOT_MODEL_IDS = Object.freeze(
|
||||||
|
Object.keys(LEGACY_COPILOT_MODEL_FALLBACKS)
|
||||||
|
);
|
||||||
|
|
||||||
|
function trimModelId(value: string | null | undefined): string | undefined {
|
||||||
|
if (typeof value !== 'string') return undefined;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCopilotModelId(
|
||||||
|
model: string | null | undefined,
|
||||||
|
fallbackModel: string = DEFAULT_COPILOT_CONFIG.model
|
||||||
|
): string {
|
||||||
|
const normalizedFallback = trimModelId(fallbackModel) ?? DEFAULT_COPILOT_CONFIG.model;
|
||||||
|
const trimmedModel = trimModelId(model) ?? normalizedFallback;
|
||||||
|
return LEGACY_COPILOT_MODEL_FALLBACKS[trimmedModel.toLowerCase()] ?? trimmedModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeOptionalCopilotModelId(
|
||||||
|
model: string | null | undefined,
|
||||||
|
fallbackModel: string
|
||||||
|
): string | undefined {
|
||||||
|
const trimmedModel = trimModelId(model);
|
||||||
|
return trimmedModel ? normalizeCopilotModelId(trimmedModel, fallbackModel) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCopilotConfig(config: CopilotConfig): CopilotConfig {
|
||||||
|
const model = normalizeCopilotModelId(config.model);
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
model,
|
||||||
|
opus_model: normalizeOptionalCopilotModelId(config.opus_model, model),
|
||||||
|
sonnet_model: normalizeOptionalCopilotModelId(config.sonnet_model, model),
|
||||||
|
haiku_model: normalizeOptionalCopilotModelId(config.haiku_model, model),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopilotSettingsPayload {
|
||||||
|
env?: Record<string, unknown>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCopilotSettings(
|
||||||
|
settings: CopilotSettingsPayload,
|
||||||
|
fallbackConfig: CopilotConfig = DEFAULT_COPILOT_CONFIG
|
||||||
|
): CopilotSettingsPayload {
|
||||||
|
const normalizedConfig = normalizeCopilotConfig(fallbackConfig);
|
||||||
|
const rawEnv =
|
||||||
|
settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env)
|
||||||
|
? settings.env
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const model = normalizeCopilotModelId(
|
||||||
|
typeof rawEnv.ANTHROPIC_MODEL === 'string' ? rawEnv.ANTHROPIC_MODEL : normalizedConfig.model,
|
||||||
|
normalizedConfig.model
|
||||||
|
);
|
||||||
|
const opusModel =
|
||||||
|
normalizeOptionalCopilotModelId(
|
||||||
|
typeof rawEnv.ANTHROPIC_DEFAULT_OPUS_MODEL === 'string'
|
||||||
|
? rawEnv.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||||
|
: normalizedConfig.opus_model,
|
||||||
|
model
|
||||||
|
) ?? model;
|
||||||
|
const sonnetModel =
|
||||||
|
normalizeOptionalCopilotModelId(
|
||||||
|
typeof rawEnv.ANTHROPIC_DEFAULT_SONNET_MODEL === 'string'
|
||||||
|
? rawEnv.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||||
|
: normalizedConfig.sonnet_model,
|
||||||
|
model
|
||||||
|
) ?? model;
|
||||||
|
const haikuModel =
|
||||||
|
normalizeOptionalCopilotModelId(
|
||||||
|
typeof rawEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL === 'string'
|
||||||
|
? rawEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||||
|
: normalizedConfig.haiku_model,
|
||||||
|
model
|
||||||
|
) ?? model;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
env: {
|
||||||
|
...rawEnv,
|
||||||
|
ANTHROPIC_MODEL: model,
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel,
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel,
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: haikuModel,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -155,16 +155,6 @@ export const DEFAULT_COPILOT_MODELS: CopilotModel[] = [
|
|||||||
minPlan: 'pro',
|
minPlan: 'pro',
|
||||||
multiplier: 0.25,
|
multiplier: 0.25,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fine-tuned Models
|
|
||||||
{
|
|
||||||
id: 'raptor-mini',
|
|
||||||
name: 'Raptor Mini',
|
|
||||||
provider: 'openai',
|
|
||||||
minPlan: 'free',
|
|
||||||
multiplier: 0,
|
|
||||||
preview: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export {
|
|||||||
getDefaultModel,
|
getDefaultModel,
|
||||||
} from './copilot-models';
|
} from './copilot-models';
|
||||||
|
|
||||||
|
// Normalization
|
||||||
|
export {
|
||||||
|
DEPRECATED_COPILOT_MODEL_IDS,
|
||||||
|
normalizeCopilotConfig,
|
||||||
|
normalizeCopilotModelId,
|
||||||
|
normalizeCopilotSettings,
|
||||||
|
} from './copilot-model-normalizer';
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
export {
|
export {
|
||||||
normalizeCopilotUsage,
|
normalizeCopilotUsage,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
getCopilotApiInfo,
|
getCopilotApiInfo,
|
||||||
getInstalledVersion as getCopilotInstalledVersion,
|
getInstalledVersion as getCopilotInstalledVersion,
|
||||||
} from '../../copilot';
|
} from '../../copilot';
|
||||||
import { DEFAULT_COPILOT_CONFIG } from '../../config/unified-config-types';
|
import { normalizeCopilotConfig } from '../../copilot/copilot-model-normalizer';
|
||||||
|
import { DEFAULT_COPILOT_CONFIG, type CopilotConfig } from '../../config/unified-config-types';
|
||||||
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
import copilotSettingsRoutes from './copilot-settings-routes';
|
import copilotSettingsRoutes from './copilot-settings-routes';
|
||||||
|
|
||||||
@@ -39,13 +40,30 @@ function parseOptionalModel(value: unknown): string | undefined {
|
|||||||
return trimmed.length > 0 ? trimmed : undefined;
|
return trimmed.length > 0 ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function persistNormalizedCopilotConfig(
|
||||||
|
config: ReturnType<typeof loadOrCreateUnifiedConfig>,
|
||||||
|
copilotConfig: CopilotConfig
|
||||||
|
): void {
|
||||||
|
const current = JSON.stringify(config.copilot ?? DEFAULT_COPILOT_CONFIG);
|
||||||
|
const next = JSON.stringify(copilotConfig);
|
||||||
|
if (current === next) return;
|
||||||
|
config.copilot = copilotConfig;
|
||||||
|
saveUnifiedConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadNormalizedCopilotConfig(): CopilotConfig {
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
const copilotConfig = normalizeCopilotConfig(config.copilot ?? DEFAULT_COPILOT_CONFIG);
|
||||||
|
persistNormalizedCopilotConfig(config, copilotConfig);
|
||||||
|
return copilotConfig;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/copilot/status - Get Copilot status (auth + daemon + install info)
|
* GET /api/copilot/status - Get Copilot status (auth + daemon + install info)
|
||||||
*/
|
*/
|
||||||
router.get('/status', async (_req: Request, res: Response): Promise<void> => {
|
router.get('/status', async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const copilotConfig = loadNormalizedCopilotConfig();
|
||||||
const copilotConfig = config.copilot ?? DEFAULT_COPILOT_CONFIG;
|
|
||||||
const status = await getCopilotStatus(copilotConfig);
|
const status = await getCopilotStatus(copilotConfig);
|
||||||
const installed = isCopilotApiInstalled();
|
const installed = isCopilotApiInstalled();
|
||||||
const version = getCopilotInstalledVersion();
|
const version = getCopilotInstalledVersion();
|
||||||
@@ -73,9 +91,7 @@ router.get('/status', async (_req: Request, res: Response): Promise<void> => {
|
|||||||
*/
|
*/
|
||||||
router.get('/config', (_req: Request, res: Response): void => {
|
router.get('/config', (_req: Request, res: Response): void => {
|
||||||
try {
|
try {
|
||||||
const config = loadOrCreateUnifiedConfig();
|
res.json(loadNormalizedCopilotConfig());
|
||||||
const copilotConfig = config.copilot ?? DEFAULT_COPILOT_CONFIG;
|
|
||||||
res.json(copilotConfig);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: (error as Error).message });
|
res.status(500).json({ error: (error as Error).message });
|
||||||
}
|
}
|
||||||
@@ -200,7 +216,7 @@ router.put('/config', (req: Request, res: Response): void => {
|
|||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
|
||||||
// Merge updates with existing config
|
// Merge updates with existing config
|
||||||
config.copilot = {
|
const nextCopilotConfig = normalizeCopilotConfig({
|
||||||
enabled:
|
enabled:
|
||||||
(payload.enabled as boolean) ?? config.copilot?.enabled ?? DEFAULT_COPILOT_CONFIG.enabled,
|
(payload.enabled as boolean) ?? config.copilot?.enabled ?? DEFAULT_COPILOT_CONFIG.enabled,
|
||||||
auto_start:
|
auto_start:
|
||||||
@@ -233,10 +249,11 @@ router.put('/config', (req: Request, res: Response): void => {
|
|||||||
'haiku_model' in payload
|
'haiku_model' in payload
|
||||||
? parseOptionalModel(payload.haiku_model)
|
? parseOptionalModel(payload.haiku_model)
|
||||||
: config.copilot?.haiku_model,
|
: config.copilot?.haiku_model,
|
||||||
};
|
});
|
||||||
|
|
||||||
|
config.copilot = nextCopilotConfig;
|
||||||
saveUnifiedConfig(config);
|
saveUnifiedConfig(config);
|
||||||
res.json({ success: true, copilot: config.copilot });
|
res.json({ success: true, copilot: nextCopilotConfig });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: (error as Error).message });
|
res.status(500).json({ error: (error as Error).message });
|
||||||
}
|
}
|
||||||
@@ -271,9 +288,9 @@ router.get('/auth/status', async (_req: Request, res: Response): Promise<void> =
|
|||||||
*/
|
*/
|
||||||
router.get('/models', async (_req: Request, res: Response): Promise<void> => {
|
router.get('/models', async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const copilotConfig = loadNormalizedCopilotConfig();
|
||||||
const port = config.copilot?.port ?? DEFAULT_COPILOT_CONFIG.port;
|
const port = copilotConfig.port;
|
||||||
const currentModel = config.copilot?.model ?? DEFAULT_COPILOT_CONFIG.model;
|
const currentModel = copilotConfig.model;
|
||||||
const models = await getCopilotModels(port);
|
const models = await getCopilotModels(port);
|
||||||
|
|
||||||
const modelsWithCurrent = models.map((m) => ({
|
const modelsWithCurrent = models.map((m) => ({
|
||||||
|
|||||||
@@ -5,12 +5,43 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import {
|
||||||
|
normalizeCopilotConfig,
|
||||||
|
normalizeCopilotSettings,
|
||||||
|
} from '../../copilot/copilot-model-normalizer';
|
||||||
import { getCcsDir } from '../../utils/config-manager';
|
import { getCcsDir } from '../../utils/config-manager';
|
||||||
import { DEFAULT_COPILOT_CONFIG } from '../../config/unified-config-types';
|
import { DEFAULT_COPILOT_CONFIG, type CopilotConfig } from '../../config/unified-config-types';
|
||||||
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
function serializeSettings(settings: Record<string, unknown>): string {
|
||||||
|
return JSON.stringify(settings, null, 2) + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSettingsFile(
|
||||||
|
settingsPath: string,
|
||||||
|
settings: Record<string, unknown>,
|
||||||
|
previousContent?: string
|
||||||
|
): void {
|
||||||
|
const nextContent = serializeSettings(settings);
|
||||||
|
if (previousContent === nextContent) return;
|
||||||
|
const tempPath = `${settingsPath}.tmp`;
|
||||||
|
fs.writeFileSync(tempPath, nextContent);
|
||||||
|
fs.renameSync(tempPath, settingsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistNormalizedCopilotConfig(
|
||||||
|
config: ReturnType<typeof loadOrCreateUnifiedConfig>,
|
||||||
|
copilotConfig: CopilotConfig
|
||||||
|
): void {
|
||||||
|
const current = JSON.stringify(config.copilot ?? DEFAULT_COPILOT_CONFIG);
|
||||||
|
const next = JSON.stringify(copilotConfig);
|
||||||
|
if (current === next) return;
|
||||||
|
config.copilot = copilotConfig;
|
||||||
|
saveUnifiedConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/copilot/settings/raw - Get raw copilot.settings.json
|
* GET /api/copilot/settings/raw - Get raw copilot.settings.json
|
||||||
* Returns the raw JSON content for editing in the code editor
|
* Returns the raw JSON content for editing in the code editor
|
||||||
@@ -19,7 +50,8 @@ router.get('/raw', (_req: Request, res: Response): void => {
|
|||||||
try {
|
try {
|
||||||
const settingsPath = path.join(getCcsDir(), 'copilot.settings.json');
|
const settingsPath = path.join(getCcsDir(), 'copilot.settings.json');
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
const copilotConfig = config.copilot ?? DEFAULT_COPILOT_CONFIG;
|
const copilotConfig = normalizeCopilotConfig(config.copilot ?? DEFAULT_COPILOT_CONFIG);
|
||||||
|
persistNormalizedCopilotConfig(config, copilotConfig);
|
||||||
|
|
||||||
// Default model for all tiers
|
// Default model for all tiers
|
||||||
const defaultModel = copilotConfig.model;
|
const defaultModel = copilotConfig.model;
|
||||||
@@ -28,16 +60,19 @@ router.get('/raw', (_req: Request, res: Response): void => {
|
|||||||
if (!fs.existsSync(settingsPath)) {
|
if (!fs.existsSync(settingsPath)) {
|
||||||
// Create settings structure matching CLIProxy pattern
|
// Create settings structure matching CLIProxy pattern
|
||||||
// Use 127.0.0.1 instead of localhost for more reliable local connections
|
// Use 127.0.0.1 instead of localhost for more reliable local connections
|
||||||
const defaultSettings = {
|
const defaultSettings = normalizeCopilotSettings(
|
||||||
env: {
|
{
|
||||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${copilotConfig.port}`,
|
env: {
|
||||||
ANTHROPIC_AUTH_TOKEN: 'copilot-managed',
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${copilotConfig.port}`,
|
||||||
ANTHROPIC_MODEL: defaultModel,
|
ANTHROPIC_AUTH_TOKEN: 'copilot-managed',
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: copilotConfig.opus_model || defaultModel,
|
ANTHROPIC_MODEL: defaultModel,
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: copilotConfig.sonnet_model || defaultModel,
|
ANTHROPIC_DEFAULT_OPUS_MODEL: copilotConfig.opus_model || defaultModel,
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: copilotConfig.haiku_model || defaultModel,
|
ANTHROPIC_DEFAULT_SONNET_MODEL: copilotConfig.sonnet_model || defaultModel,
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: copilotConfig.haiku_model || defaultModel,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
copilotConfig
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
settings: defaultSettings,
|
settings: defaultSettings,
|
||||||
@@ -49,7 +84,33 @@ router.get('/raw', (_req: Request, res: Response): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(settingsPath, 'utf-8');
|
const content = fs.readFileSync(settingsPath, 'utf-8');
|
||||||
const settings = JSON.parse(content);
|
const settings = normalizeCopilotSettings(
|
||||||
|
JSON.parse(content) as Record<string, unknown>,
|
||||||
|
copilotConfig
|
||||||
|
);
|
||||||
|
writeSettingsFile(settingsPath, settings, content);
|
||||||
|
|
||||||
|
const env =
|
||||||
|
settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env)
|
||||||
|
? settings.env
|
||||||
|
: {};
|
||||||
|
persistNormalizedCopilotConfig(config, {
|
||||||
|
...copilotConfig,
|
||||||
|
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : copilotConfig.model,
|
||||||
|
opus_model:
|
||||||
|
typeof env.ANTHROPIC_DEFAULT_OPUS_MODEL === 'string'
|
||||||
|
? env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||||
|
: undefined,
|
||||||
|
sonnet_model:
|
||||||
|
typeof env.ANTHROPIC_DEFAULT_SONNET_MODEL === 'string'
|
||||||
|
? env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||||
|
: undefined,
|
||||||
|
haiku_model:
|
||||||
|
typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === 'string'
|
||||||
|
? env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const stat = fs.statSync(settingsPath);
|
const stat = fs.statSync(settingsPath);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -71,6 +132,8 @@ router.put('/raw', (req: Request, res: Response): void => {
|
|||||||
try {
|
try {
|
||||||
const { settings, expectedMtime } = req.body;
|
const { settings, expectedMtime } = req.body;
|
||||||
const settingsPath = path.join(getCcsDir(), 'copilot.settings.json');
|
const settingsPath = path.join(getCcsDir(), 'copilot.settings.json');
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
const copilotConfig = normalizeCopilotConfig(config.copilot ?? DEFAULT_COPILOT_CONFIG);
|
||||||
|
|
||||||
// Check for conflict if file exists and expectedMtime provided
|
// Check for conflict if file exists and expectedMtime provided
|
||||||
if (fs.existsSync(settingsPath) && expectedMtime) {
|
if (fs.existsSync(settingsPath) && expectedMtime) {
|
||||||
@@ -81,22 +144,39 @@ router.put('/raw', (req: Request, res: Response): void => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write settings file atomically
|
const normalizedSettings = normalizeCopilotSettings(
|
||||||
const tempPath = settingsPath + '.tmp';
|
settings as Record<string, unknown>,
|
||||||
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
|
copilotConfig
|
||||||
fs.renameSync(tempPath, settingsPath);
|
);
|
||||||
|
writeSettingsFile(settingsPath, normalizedSettings);
|
||||||
|
|
||||||
// Also sync model mappings back to unified config
|
// Also sync model mappings back to unified config
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const env =
|
||||||
const env = settings.env || {};
|
normalizedSettings.env &&
|
||||||
|
typeof normalizedSettings.env === 'object' &&
|
||||||
|
!Array.isArray(normalizedSettings.env)
|
||||||
|
? normalizedSettings.env
|
||||||
|
: {};
|
||||||
|
|
||||||
config.copilot = {
|
config.copilot = normalizeCopilotConfig({
|
||||||
...(config.copilot ?? DEFAULT_COPILOT_CONFIG),
|
...(config.copilot ?? DEFAULT_COPILOT_CONFIG),
|
||||||
model: env.ANTHROPIC_MODEL || config.copilot?.model || DEFAULT_COPILOT_CONFIG.model,
|
model:
|
||||||
opus_model: env.ANTHROPIC_DEFAULT_OPUS_MODEL || undefined,
|
typeof env.ANTHROPIC_MODEL === 'string'
|
||||||
sonnet_model: env.ANTHROPIC_DEFAULT_SONNET_MODEL || undefined,
|
? env.ANTHROPIC_MODEL
|
||||||
haiku_model: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || undefined,
|
: config.copilot?.model || DEFAULT_COPILOT_CONFIG.model,
|
||||||
};
|
opus_model:
|
||||||
|
typeof env.ANTHROPIC_DEFAULT_OPUS_MODEL === 'string'
|
||||||
|
? env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||||
|
: undefined,
|
||||||
|
sonnet_model:
|
||||||
|
typeof env.ANTHROPIC_DEFAULT_SONNET_MODEL === 'string'
|
||||||
|
? env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||||
|
: undefined,
|
||||||
|
haiku_model:
|
||||||
|
typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === 'string'
|
||||||
|
? env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
saveUnifiedConfig(config);
|
saveUnifiedConfig(config);
|
||||||
|
|
||||||
const stat = fs.statSync(settingsPath);
|
const stat = fs.statSync(settingsPath);
|
||||||
|
|||||||
@@ -13,6 +13,22 @@ const baseConfig: CopilotConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('generateCopilotEnv', () => {
|
describe('generateCopilotEnv', () => {
|
||||||
|
it('normalizes deprecated raptor-mini model selections to the safe default', () => {
|
||||||
|
const env = generateCopilotEnv({
|
||||||
|
...baseConfig,
|
||||||
|
model: 'raptor-mini',
|
||||||
|
opus_model: 'raptor-mini',
|
||||||
|
sonnet_model: 'raptor-mini',
|
||||||
|
haiku_model: 'raptor-mini',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(env.ANTHROPIC_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(env.ANTHROPIC_SMALL_FAST_MODEL).toBe('gpt-4.1');
|
||||||
|
});
|
||||||
|
|
||||||
it('includes inherited CLAUDE_CONFIG_DIR when provided', () => {
|
it('includes inherited CLAUDE_CONFIG_DIR when provided', () => {
|
||||||
const env = generateCopilotEnv(baseConfig, '/tmp/.ccs/instances/pro');
|
const env = generateCopilotEnv(baseConfig, '/tmp/.ccs/instances/pro');
|
||||||
expect(env.CLAUDE_CONFIG_DIR).toBe('/tmp/.ccs/instances/pro');
|
expect(env.CLAUDE_CONFIG_DIR).toBe('/tmp/.ccs/instances/pro');
|
||||||
@@ -23,4 +39,3 @@ describe('generateCopilotEnv', () => {
|
|||||||
expect(env.CLAUDE_CONFIG_DIR).toBeUndefined();
|
expect(env.CLAUDE_CONFIG_DIR).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { DEFAULT_COPILOT_CONFIG } from '../../../src/config/unified-config-types';
|
||||||
|
import {
|
||||||
|
DEPRECATED_COPILOT_MODEL_IDS,
|
||||||
|
normalizeCopilotConfig,
|
||||||
|
normalizeCopilotModelId,
|
||||||
|
normalizeCopilotSettings,
|
||||||
|
} from '../../../src/copilot/copilot-model-normalizer';
|
||||||
|
|
||||||
|
describe('copilot-model-normalizer', () => {
|
||||||
|
it('tracks raptor-mini as deprecated', () => {
|
||||||
|
expect(DEPRECATED_COPILOT_MODEL_IDS).toContain('raptor-mini');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes deprecated model IDs to the safe default', () => {
|
||||||
|
expect(normalizeCopilotModelId('raptor-mini')).toBe(DEFAULT_COPILOT_CONFIG.model);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Copilot config tier mappings', () => {
|
||||||
|
const normalized = normalizeCopilotConfig({
|
||||||
|
...DEFAULT_COPILOT_CONFIG,
|
||||||
|
model: 'raptor-mini',
|
||||||
|
opus_model: 'raptor-mini',
|
||||||
|
sonnet_model: 'raptor-mini',
|
||||||
|
haiku_model: 'raptor-mini',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(normalized.model).toBe('gpt-4.1');
|
||||||
|
expect(normalized.opus_model).toBe('gpt-4.1');
|
||||||
|
expect(normalized.sonnet_model).toBe('gpt-4.1');
|
||||||
|
expect(normalized.haiku_model).toBe('gpt-4.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes raw settings payloads without dropping unrelated env keys', () => {
|
||||||
|
const normalized = normalizeCopilotSettings(
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:4141',
|
||||||
|
ANTHROPIC_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'raptor-mini',
|
||||||
|
EXTRA_FLAG: 'keep-me',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...DEFAULT_COPILOT_CONFIG,
|
||||||
|
model: 'raptor-mini',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(normalized.env?.ANTHROPIC_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(normalized.env?.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(normalized.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(normalized.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(normalized.env?.EXTRA_FLAG).toBe('keep-me');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,6 +3,10 @@ import * as http from 'http';
|
|||||||
import { DEFAULT_COPILOT_MODELS, fetchModelsFromDaemon } from '../../../src/copilot/copilot-models';
|
import { DEFAULT_COPILOT_MODELS, fetchModelsFromDaemon } from '../../../src/copilot/copilot-models';
|
||||||
|
|
||||||
describe('fetchModelsFromDaemon', () => {
|
describe('fetchModelsFromDaemon', () => {
|
||||||
|
it('does not advertise deprecated fallback models', () => {
|
||||||
|
expect(DEFAULT_COPILOT_MODELS.some((model) => model.id === 'raptor-mini')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('falls back to defaults when daemon is unreachable', async () => {
|
it('falls back to defaults when daemon is unreachable', async () => {
|
||||||
const models = await fetchModelsFromDaemon(9999);
|
const models = await fetchModelsFromDaemon(9999);
|
||||||
expect(models).toEqual(DEFAULT_COPILOT_MODELS);
|
expect(models).toEqual(DEFAULT_COPILOT_MODELS);
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
|
||||||
|
import express from 'express';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import type { Server } from 'http';
|
||||||
|
|
||||||
|
let server: Server;
|
||||||
|
let baseUrl = '';
|
||||||
|
let tempDir = '';
|
||||||
|
let originalCcsHome: string | undefined;
|
||||||
|
|
||||||
|
let setGlobalConfigDir: (dir: string | undefined) => void;
|
||||||
|
let getCcsDir: () => string;
|
||||||
|
let loadOrCreateUnifiedConfig: () => {
|
||||||
|
copilot?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
auto_start?: boolean;
|
||||||
|
port?: number;
|
||||||
|
account_type?: 'individual' | 'business' | 'enterprise';
|
||||||
|
rate_limit?: number | null;
|
||||||
|
wait_on_limit?: boolean;
|
||||||
|
model?: string;
|
||||||
|
opus_model?: string;
|
||||||
|
sonnet_model?: string;
|
||||||
|
haiku_model?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let saveUnifiedConfig: (config: {
|
||||||
|
copilot?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
auto_start?: boolean;
|
||||||
|
port?: number;
|
||||||
|
account_type?: 'individual' | 'business' | 'enterprise';
|
||||||
|
rate_limit?: number | null;
|
||||||
|
wait_on_limit?: boolean;
|
||||||
|
model?: string;
|
||||||
|
opus_model?: string;
|
||||||
|
sonnet_model?: string;
|
||||||
|
haiku_model?: string;
|
||||||
|
};
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
function seedCopilotConfig(
|
||||||
|
overrides: {
|
||||||
|
enabled?: boolean;
|
||||||
|
auto_start?: boolean;
|
||||||
|
port?: number;
|
||||||
|
account_type?: 'individual' | 'business' | 'enterprise';
|
||||||
|
rate_limit?: number | null;
|
||||||
|
wait_on_limit?: boolean;
|
||||||
|
model?: string;
|
||||||
|
opus_model?: string;
|
||||||
|
sonnet_model?: string;
|
||||||
|
haiku_model?: string;
|
||||||
|
} = {}
|
||||||
|
): void {
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
config.copilot = {
|
||||||
|
enabled: overrides.enabled ?? true,
|
||||||
|
auto_start: overrides.auto_start ?? false,
|
||||||
|
port: overrides.port ?? 4141,
|
||||||
|
account_type: overrides.account_type ?? 'individual',
|
||||||
|
rate_limit: overrides.rate_limit ?? null,
|
||||||
|
wait_on_limit: overrides.wait_on_limit ?? true,
|
||||||
|
model: overrides.model ?? 'gpt-4.1',
|
||||||
|
opus_model: overrides.opus_model,
|
||||||
|
sonnet_model: overrides.sonnet_model,
|
||||||
|
haiku_model: overrides.haiku_model,
|
||||||
|
};
|
||||||
|
saveUnifiedConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCopilotSettingsPath(): string {
|
||||||
|
return path.join(getCcsDir(), 'copilot.settings.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCopilotSettings(settings: Record<string, unknown>): void {
|
||||||
|
fs.writeFileSync(getCopilotSettingsPath(), JSON.stringify(settings, null, 2) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
originalCcsHome = process.env.CCS_HOME;
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-copilot-routes-test-'));
|
||||||
|
process.env.CCS_HOME = tempDir;
|
||||||
|
|
||||||
|
const configManager = await import('../../../src/utils/config-manager');
|
||||||
|
setGlobalConfigDir = configManager.setGlobalConfigDir;
|
||||||
|
getCcsDir = configManager.getCcsDir;
|
||||||
|
setGlobalConfigDir(undefined);
|
||||||
|
|
||||||
|
const unifiedConfig = await import('../../../src/config/unified-config-loader');
|
||||||
|
loadOrCreateUnifiedConfig = unifiedConfig.loadOrCreateUnifiedConfig;
|
||||||
|
saveUnifiedConfig = unifiedConfig.saveUnifiedConfig;
|
||||||
|
|
||||||
|
const copilotRoutesModule = await import('../../../src/web-server/routes/copilot-routes');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use('/api/copilot', copilotRoutesModule.default);
|
||||||
|
|
||||||
|
server = app.listen(0, '127.0.0.1');
|
||||||
|
await new Promise<void>((resolve) => server.on('listening', () => resolve()));
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === 'string') {
|
||||||
|
throw new Error('Unable to resolve test server port');
|
||||||
|
}
|
||||||
|
baseUrl = `http://127.0.0.1:${address.port}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
setGlobalConfigDir(undefined);
|
||||||
|
const ccsDir = getCcsDir();
|
||||||
|
if (!fs.existsSync(ccsDir)) {
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
seedCopilotConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (server) {
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalCcsHome !== undefined) {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
} else {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
}
|
||||||
|
setGlobalConfigDir(undefined);
|
||||||
|
|
||||||
|
if (tempDir && fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Copilot Routes', () => {
|
||||||
|
it('GET /api/copilot/config normalizes and persists stale raptor-mini config', async () => {
|
||||||
|
seedCopilotConfig({
|
||||||
|
model: 'raptor-mini',
|
||||||
|
opus_model: 'raptor-mini',
|
||||||
|
sonnet_model: 'raptor-mini',
|
||||||
|
haiku_model: 'raptor-mini',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/api/copilot/config`);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const body = (await response.json()) as {
|
||||||
|
model: string;
|
||||||
|
opus_model?: string;
|
||||||
|
sonnet_model?: string;
|
||||||
|
haiku_model?: string;
|
||||||
|
};
|
||||||
|
expect(body.model).toBe('gpt-4.1');
|
||||||
|
expect(body.opus_model).toBeUndefined();
|
||||||
|
expect(body.sonnet_model).toBeUndefined();
|
||||||
|
expect(body.haiku_model).toBeUndefined();
|
||||||
|
|
||||||
|
const persisted = loadOrCreateUnifiedConfig().copilot;
|
||||||
|
expect(persisted?.model).toBe('gpt-4.1');
|
||||||
|
expect(persisted?.opus_model).toBeUndefined();
|
||||||
|
expect(persisted?.sonnet_model).toBeUndefined();
|
||||||
|
expect(persisted?.haiku_model).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/copilot/config normalizes stale raptor-mini payloads before save', async () => {
|
||||||
|
const response = await fetch(`${baseUrl}/api/copilot/config`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'raptor-mini',
|
||||||
|
opus_model: 'raptor-mini',
|
||||||
|
sonnet_model: 'raptor-mini',
|
||||||
|
haiku_model: 'raptor-mini',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const body = (await response.json()) as {
|
||||||
|
success: boolean;
|
||||||
|
copilot: {
|
||||||
|
model: string;
|
||||||
|
opus_model?: string;
|
||||||
|
sonnet_model?: string;
|
||||||
|
haiku_model?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
expect(body.success).toBe(true);
|
||||||
|
expect(body.copilot.model).toBe('gpt-4.1');
|
||||||
|
expect(body.copilot.opus_model).toBe('gpt-4.1');
|
||||||
|
expect(body.copilot.sonnet_model).toBe('gpt-4.1');
|
||||||
|
expect(body.copilot.haiku_model).toBe('gpt-4.1');
|
||||||
|
|
||||||
|
const persisted = loadOrCreateUnifiedConfig().copilot;
|
||||||
|
expect(persisted?.model).toBe('gpt-4.1');
|
||||||
|
expect(persisted?.opus_model).toBeUndefined();
|
||||||
|
expect(persisted?.sonnet_model).toBeUndefined();
|
||||||
|
expect(persisted?.haiku_model).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/copilot/settings/raw normalizes and persists stale raw settings', async () => {
|
||||||
|
seedCopilotConfig({ model: 'raptor-mini' });
|
||||||
|
writeCopilotSettings({
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:4141',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'copilot-managed',
|
||||||
|
ANTHROPIC_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'raptor-mini',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/api/copilot/settings/raw`);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const body = (await response.json()) as {
|
||||||
|
settings: {
|
||||||
|
env: Record<string, string>;
|
||||||
|
};
|
||||||
|
exists: boolean;
|
||||||
|
};
|
||||||
|
expect(body.exists).toBe(true);
|
||||||
|
expect(body.settings.env.ANTHROPIC_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(body.settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(body.settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(body.settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-4.1');
|
||||||
|
|
||||||
|
const persistedSettings = JSON.parse(fs.readFileSync(getCopilotSettingsPath(), 'utf-8')) as {
|
||||||
|
env: Record<string, string>;
|
||||||
|
};
|
||||||
|
expect(persistedSettings.env.ANTHROPIC_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(persistedSettings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(loadOrCreateUnifiedConfig().copilot?.model).toBe('gpt-4.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/copilot/settings/raw normalizes stale models before save and syncs unified config', async () => {
|
||||||
|
const response = await fetch(`${baseUrl}/api/copilot/settings/raw`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
settings: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:4141',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'copilot-managed',
|
||||||
|
ANTHROPIC_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'raptor-mini',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'raptor-mini',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const persistedSettings = JSON.parse(fs.readFileSync(getCopilotSettingsPath(), 'utf-8')) as {
|
||||||
|
env: Record<string, string>;
|
||||||
|
};
|
||||||
|
expect(persistedSettings.env.ANTHROPIC_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(persistedSettings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(persistedSettings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-4.1');
|
||||||
|
expect(persistedSettings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-4.1');
|
||||||
|
|
||||||
|
const persistedConfig = loadOrCreateUnifiedConfig().copilot;
|
||||||
|
expect(persistedConfig?.model).toBe('gpt-4.1');
|
||||||
|
expect(persistedConfig?.opus_model).toBeUndefined();
|
||||||
|
expect(persistedConfig?.sonnet_model).toBeUndefined();
|
||||||
|
expect(persistedConfig?.haiku_model).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/copilot/models reports normalized current model for stale config', async () => {
|
||||||
|
seedCopilotConfig({ model: 'raptor-mini' });
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/api/copilot/models`);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const body = (await response.json()) as {
|
||||||
|
current: string;
|
||||||
|
models: Array<{ id: string; isCurrent?: boolean }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(body.current).toBe('gpt-4.1');
|
||||||
|
expect(body.models.some((model) => model.id === 'gpt-4.1' && model.isCurrent)).toBe(true);
|
||||||
|
expect(loadOrCreateUnifiedConfig().copilot?.model).toBe('gpt-4.1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,14 +23,6 @@ export const FREE_PRESETS: ModelPreset[] = [
|
|||||||
sonnet: 'gpt-5-mini',
|
sonnet: 'gpt-5-mini',
|
||||||
haiku: 'gpt-5-mini',
|
haiku: 'gpt-5-mini',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Raptor Mini (Free)',
|
|
||||||
description: 'Free tier - fine-tuned for coding',
|
|
||||||
default: 'raptor-mini',
|
|
||||||
opus: 'raptor-mini',
|
|
||||||
sonnet: 'raptor-mini',
|
|
||||||
haiku: 'raptor-mini',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PAID_PRESETS: ModelPreset[] = [
|
export const PAID_PRESETS: ModelPreset[] = [
|
||||||
|
|||||||
Reference in New Issue
Block a user