From a3e2153498ac8a1a9b84319f6b0835fa8f085b3d Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 00:19:32 -0500 Subject: [PATCH] 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 --- src/web-server/routes.ts | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index c503ba97..30e9d4a0 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -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 }); + } +});