feat(copilot): add raw settings API and model mapping routes

- add GET/PUT /api/copilot/settings/raw for copilot.settings.json

- update PUT /api/copilot/config to handle model mapping fields

- sync model mappings between settings file and unified config
This commit is contained in:
kaitranntt
2025-12-18 00:19:32 -05:00
parent d21908ab63
commit a3e2153498
+101
View File
@@ -1675,6 +1675,13 @@ apiRoutes.put('/copilot/config', (req: Request, res: Response): void => {
config.copilot?.wait_on_limit ??
DEFAULT_COPILOT_CONFIG.wait_on_limit,
model: updates.model ?? config.copilot?.model ?? DEFAULT_COPILOT_CONFIG.model,
// Model mapping for opus/sonnet/haiku tiers
opus_model:
updates.opus_model !== undefined ? updates.opus_model : config.copilot?.opus_model,
sonnet_model:
updates.sonnet_model !== undefined ? updates.sonnet_model : config.copilot?.sonnet_model,
haiku_model:
updates.haiku_model !== undefined ? updates.haiku_model : config.copilot?.haiku_model,
};
saveUnifiedConfig(config);
@@ -1756,3 +1763,97 @@ apiRoutes.post('/copilot/daemon/stop', async (_req: Request, res: Response): Pro
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /api/copilot/settings/raw - Get raw copilot.settings.json
* Returns the raw JSON content for editing in the code editor
*/
apiRoutes.get('/copilot/settings/raw', (_req: Request, res: Response): void => {
try {
const settingsPath = path.join(getCcsDir(), 'copilot.settings.json');
const config = loadOrCreateUnifiedConfig();
const copilotConfig = config.copilot ?? DEFAULT_COPILOT_CONFIG;
// Default model for all tiers
const defaultModel = copilotConfig.model;
// If file doesn't exist, return default structure with all model mappings
if (!fs.existsSync(settingsPath)) {
// Create settings structure matching CLIProxy pattern - always include all model mappings
const defaultSettings = {
env: {
ANTHROPIC_BASE_URL: `http://localhost:${copilotConfig.port}`,
ANTHROPIC_AUTH_TOKEN: 'copilot-managed',
ANTHROPIC_MODEL: defaultModel,
ANTHROPIC_DEFAULT_OPUS_MODEL: copilotConfig.opus_model || defaultModel,
ANTHROPIC_DEFAULT_SONNET_MODEL: copilotConfig.sonnet_model || defaultModel,
ANTHROPIC_DEFAULT_HAIKU_MODEL: copilotConfig.haiku_model || defaultModel,
},
};
res.json({
settings: defaultSettings,
mtime: Date.now(),
path: `~/.ccs/copilot.settings.json`,
exists: false,
});
return;
}
const content = fs.readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(content);
const stat = fs.statSync(settingsPath);
res.json({
settings,
mtime: stat.mtimeMs,
path: `~/.ccs/copilot.settings.json`,
exists: true,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* PUT /api/copilot/settings/raw - Save raw copilot.settings.json
* Saves the raw JSON content from the code editor
*/
apiRoutes.put('/copilot/settings/raw', (req: Request, res: Response): void => {
try {
const { settings, expectedMtime } = req.body;
const settingsPath = path.join(getCcsDir(), 'copilot.settings.json');
// Check for conflict if file exists and expectedMtime provided
if (fs.existsSync(settingsPath) && expectedMtime) {
const stat = fs.statSync(settingsPath);
if (Math.abs(stat.mtimeMs - expectedMtime) > 1000) {
res.status(409).json({ error: 'File modified externally', mtime: stat.mtimeMs });
return;
}
}
// Write settings file atomically
const tempPath = settingsPath + '.tmp';
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
fs.renameSync(tempPath, settingsPath);
// Also sync model mappings back to unified config
const config = loadOrCreateUnifiedConfig();
const env = settings.env || {};
config.copilot = {
...(config.copilot ?? DEFAULT_COPILOT_CONFIG),
model: env.ANTHROPIC_MODEL || config.copilot?.model || DEFAULT_COPILOT_CONFIG.model,
opus_model: env.ANTHROPIC_DEFAULT_OPUS_MODEL || undefined,
sonnet_model: env.ANTHROPIC_DEFAULT_SONNET_MODEL || undefined,
haiku_model: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || undefined,
};
saveUnifiedConfig(config);
const stat = fs.statSync(settingsPath);
res.json({ success: true, mtime: stat.mtimeMs });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});