Skip to content

Commit 8357005

Browse files
committed
feat(api-profile-ux): Implement API & UI for profile management
1 parent 2b1a3b4 commit 8357005

File tree

7 files changed

+1358
-65
lines changed

7 files changed

+1358
-65
lines changed

src/commands/api-command.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -370,32 +370,53 @@ async function handleCreate(args: string[]): Promise<void> {
370370
const defaultModel = 'claude-sonnet-4-5-20250929';
371371
let model = parsedArgs.model;
372372
if (!model && !parsedArgs.yes) {
373-
model = await InteractivePrompt.input('Default model', {
373+
model = await InteractivePrompt.input('Default model (ANTHROPIC_MODEL)', {
374374
default: defaultModel,
375375
});
376376
}
377377
model = model || defaultModel;
378378

379-
// Step 5: Optional model mapping for Opus/Sonnet/Haiku
380-
// Ask user if they want different models for each type
379+
// Step 5: Model mapping for Opus/Sonnet/Haiku
380+
// Auto-show if user entered a custom model, otherwise ask
381381
let opusModel = model;
382382
let sonnetModel = model;
383383
let haikuModel = model;
384384

385+
const isCustomModel = model !== defaultModel;
386+
385387
if (!parsedArgs.yes) {
386-
console.log('');
387-
console.log(dim('Some API proxies route different model types to different backends.'));
388-
const wantCustomMapping = await InteractivePrompt.confirm(
389-
'Configure different models for Opus/Sonnet/Haiku?',
390-
{ default: false }
391-
);
388+
// If user entered custom model, auto-prompt for model mapping
389+
// Otherwise, ask if they want to configure it
390+
let wantCustomMapping = isCustomModel;
391+
392+
if (!isCustomModel) {
393+
console.log('');
394+
console.log(dim('Some API proxies route different model types to different backends.'));
395+
wantCustomMapping = await InteractivePrompt.confirm(
396+
'Configure different models for Opus/Sonnet/Haiku?',
397+
{ default: false }
398+
);
399+
}
392400

393401
if (wantCustomMapping) {
394402
console.log('');
395-
console.log(dim('Leave blank to use the default model for each.'));
396-
opusModel = (await InteractivePrompt.input('Opus model', { default: model })) || model;
397-
sonnetModel = (await InteractivePrompt.input('Sonnet model', { default: model })) || model;
398-
haikuModel = (await InteractivePrompt.input('Haiku model', { default: model })) || model;
403+
if (isCustomModel) {
404+
console.log(dim('Configure model IDs for each tier (defaults to your model):'));
405+
} else {
406+
console.log(dim('Leave blank to use the default model for each.'));
407+
}
408+
opusModel =
409+
(await InteractivePrompt.input('Opus model (ANTHROPIC_DEFAULT_OPUS_MODEL)', {
410+
default: model,
411+
})) || model;
412+
sonnetModel =
413+
(await InteractivePrompt.input('Sonnet model (ANTHROPIC_DEFAULT_SONNET_MODEL)', {
414+
default: model,
415+
})) || model;
416+
haikuModel =
417+
(await InteractivePrompt.input('Haiku model (ANTHROPIC_DEFAULT_HAIKU_MODEL)', {
418+
default: model,
419+
})) || model;
399420
}
400421
}
401422

src/web-server/routes.ts

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,34 @@ function isConfigured(profileName: string, config: Config): boolean {
7777
}
7878
}
7979

80+
/** Model mapping for API profiles */
81+
interface ModelMapping {
82+
model?: string;
83+
opusModel?: string;
84+
sonnetModel?: string;
85+
haikuModel?: string;
86+
}
87+
8088
/**
8189
* Helper: Create settings file for profile
8290
*/
83-
function createSettingsFile(name: string, baseUrl: string, apiKey: string, model?: string): string {
91+
function createSettingsFile(
92+
name: string,
93+
baseUrl: string,
94+
apiKey: string,
95+
models: ModelMapping = {}
96+
): string {
8497
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
98+
const { model, opusModel, sonnetModel, haikuModel } = models;
8599

86100
const settings: Settings = {
87101
env: {
88102
ANTHROPIC_BASE_URL: baseUrl,
89103
ANTHROPIC_AUTH_TOKEN: apiKey,
90104
...(model && { ANTHROPIC_MODEL: model }),
105+
...(opusModel && { ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel }),
106+
...(sonnetModel && { ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel }),
107+
...(haikuModel && { ANTHROPIC_DEFAULT_HAIKU_MODEL: haikuModel }),
91108
},
92109
};
93110

@@ -100,7 +117,14 @@ function createSettingsFile(name: string, baseUrl: string, apiKey: string, model
100117
*/
101118
function updateSettingsFile(
102119
name: string,
103-
updates: { baseUrl?: string; apiKey?: string; model?: string }
120+
updates: {
121+
baseUrl?: string;
122+
apiKey?: string;
123+
model?: string;
124+
opusModel?: string;
125+
sonnetModel?: string;
126+
haikuModel?: string;
127+
}
104128
): void {
105129
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
106130

@@ -129,6 +153,34 @@ function updateSettingsFile(
129153
}
130154
}
131155

156+
// Handle model mapping fields
157+
if (updates.opusModel !== undefined) {
158+
settings.env = settings.env || {};
159+
if (updates.opusModel) {
160+
settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = updates.opusModel;
161+
} else {
162+
delete settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
163+
}
164+
}
165+
166+
if (updates.sonnetModel !== undefined) {
167+
settings.env = settings.env || {};
168+
if (updates.sonnetModel) {
169+
settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = updates.sonnetModel;
170+
} else {
171+
delete settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
172+
}
173+
}
174+
175+
if (updates.haikuModel !== undefined) {
176+
settings.env = settings.env || {};
177+
if (updates.haikuModel) {
178+
settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = updates.haikuModel;
179+
} else {
180+
delete settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
181+
}
182+
}
183+
132184
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
133185
}
134186

@@ -166,7 +218,7 @@ apiRoutes.get('/profiles', (_req: Request, res: Response) => {
166218
* POST /api/profiles - Create new profile
167219
*/
168220
apiRoutes.post('/profiles', (req: Request, res: Response): void => {
169-
const { name, baseUrl, apiKey, model } = req.body;
221+
const { name, baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel } = req.body;
170222

171223
if (!name || !baseUrl || !apiKey) {
172224
res.status(400).json({ error: 'Missing required fields: name, baseUrl, apiKey' });
@@ -185,8 +237,13 @@ apiRoutes.post('/profiles', (req: Request, res: Response): void => {
185237
fs.mkdirSync(getCcsDir(), { recursive: true });
186238
}
187239

188-
// Create settings file
189-
const settingsPath = createSettingsFile(name, baseUrl, apiKey, model);
240+
// Create settings file with model mapping
241+
const settingsPath = createSettingsFile(name, baseUrl, apiKey, {
242+
model,
243+
opusModel,
244+
sonnetModel,
245+
haikuModel,
246+
});
190247

191248
// Update config
192249
config.profiles[name] = settingsPath;
@@ -200,7 +257,7 @@ apiRoutes.post('/profiles', (req: Request, res: Response): void => {
200257
*/
201258
apiRoutes.put('/profiles/:name', (req: Request, res: Response): void => {
202259
const { name } = req.params;
203-
const { baseUrl, apiKey, model } = req.body;
260+
const { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel } = req.body;
204261

205262
const config = readConfigSafe();
206263

@@ -210,7 +267,7 @@ apiRoutes.put('/profiles/:name', (req: Request, res: Response): void => {
210267
}
211268

212269
try {
213-
updateSettingsFile(name, { baseUrl, apiKey, model });
270+
updateSettingsFile(name, { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel });
214271
res.json({ name, updated: true });
215272
} catch (error) {
216273
res.status(500).json({ error: (error as Error).message });

0 commit comments

Comments
 (0)