@@ -93,7 +93,7 @@ function validateApiName(name: string): string | null {
9393}
9494
9595/**
96- * Validate URL format
96+ * Validate URL format and warn about common mistakes
9797 */
9898function validateUrl ( url : string ) : string | null {
9999 if ( ! url ) {
@@ -107,6 +107,25 @@ function validateUrl(url: string): string | null {
107107 }
108108}
109109
110+ /**
111+ * Check if URL looks like it includes endpoint path (common mistake)
112+ * Returns warning message if problematic, null if OK
113+ */
114+ function getUrlWarning ( url : string ) : string | null {
115+ const problematicPaths = [ '/chat/completions' , '/v1/messages' , '/messages' , '/completions' ] ;
116+ const lowerUrl = url . toLowerCase ( ) ;
117+
118+ for ( const path of problematicPaths ) {
119+ if ( lowerUrl . endsWith ( path ) ) {
120+ return (
121+ `URL ends with "${ path } " - Claude appends this automatically.\n` +
122+ ` You likely want: ${ url . replace ( new RegExp ( path + '$' , 'i' ) , '' ) } `
123+ ) ;
124+ }
125+ }
126+ return null ;
127+ }
128+
110129/**
111130 * Check if unified config mode is active
112131 */
@@ -130,22 +149,35 @@ function apiExists(name: string): boolean {
130149 }
131150}
132151
152+ /** Model mapping for API profiles */
153+ interface ModelMapping {
154+ default : string ;
155+ opus : string ;
156+ sonnet : string ;
157+ haiku : string ;
158+ }
159+
133160/**
134161 * Create settings.json file for API profile
135162 * Includes all 4 model fields for proper Claude CLI integration
136163 */
137- function createSettingsFile ( name : string , baseUrl : string , apiKey : string , model : string ) : string {
164+ function createSettingsFile (
165+ name : string ,
166+ baseUrl : string ,
167+ apiKey : string ,
168+ models : ModelMapping
169+ ) : string {
138170 const ccsDir = getCcsDir ( ) ;
139171 const settingsPath = path . join ( ccsDir , `${ name } .settings.json` ) ;
140172
141173 const settings = {
142174 env : {
143175 ANTHROPIC_BASE_URL : baseUrl ,
144176 ANTHROPIC_AUTH_TOKEN : apiKey ,
145- ANTHROPIC_MODEL : model ,
146- ANTHROPIC_DEFAULT_OPUS_MODEL : model ,
147- ANTHROPIC_DEFAULT_SONNET_MODEL : model ,
148- ANTHROPIC_DEFAULT_HAIKU_MODEL : model ,
177+ ANTHROPIC_MODEL : models . default ,
178+ ANTHROPIC_DEFAULT_OPUS_MODEL : models . opus ,
179+ ANTHROPIC_DEFAULT_SONNET_MODEL : models . sonnet ,
180+ ANTHROPIC_DEFAULT_HAIKU_MODEL : models . haiku ,
149181 } ,
150182 } ;
151183
@@ -192,7 +224,7 @@ function createApiProfileUnified(
192224 name : string ,
193225 baseUrl : string ,
194226 apiKey : string ,
195- model : string
227+ models : ModelMapping
196228) : void {
197229 const ccsDir = path . join ( os . homedir ( ) , '.ccs' ) ;
198230 const settingsFile = `${ name } .settings.json` ;
@@ -203,10 +235,10 @@ function createApiProfileUnified(
203235 env : {
204236 ANTHROPIC_BASE_URL : baseUrl ,
205237 ANTHROPIC_AUTH_TOKEN : apiKey ,
206- ANTHROPIC_MODEL : model ,
207- ANTHROPIC_DEFAULT_OPUS_MODEL : model ,
208- ANTHROPIC_DEFAULT_SONNET_MODEL : model ,
209- ANTHROPIC_DEFAULT_HAIKU_MODEL : model ,
238+ ANTHROPIC_MODEL : models . default ,
239+ ANTHROPIC_DEFAULT_OPUS_MODEL : models . opus ,
240+ ANTHROPIC_DEFAULT_SONNET_MODEL : models . sonnet ,
241+ ANTHROPIC_DEFAULT_HAIKU_MODEL : models . haiku ,
210242 } ,
211243 } ;
212244
@@ -293,9 +325,12 @@ async function handleCreate(args: string[]): Promise<void> {
293325 // Step 2: Base URL
294326 let baseUrl = parsedArgs . baseUrl ;
295327 if ( ! baseUrl ) {
296- baseUrl = await InteractivePrompt . input ( 'API Base URL (e.g., https://api.example.com)' , {
297- validate : validateUrl ,
298- } ) ;
328+ baseUrl = await InteractivePrompt . input (
329+ 'API Base URL (e.g., https://api.example.com/v1 - without /chat/completions)' ,
330+ {
331+ validate : validateUrl ,
332+ }
333+ ) ;
299334 } else {
300335 const error = validateUrl ( baseUrl ) ;
301336 if ( error ) {
@@ -304,6 +339,23 @@ async function handleCreate(args: string[]): Promise<void> {
304339 }
305340 }
306341
342+ // Check for common URL mistakes and warn
343+ const urlWarning = getUrlWarning ( baseUrl ) ;
344+ if ( urlWarning ) {
345+ console . log ( '' ) ;
346+ console . log ( warn ( urlWarning ) ) ;
347+ const continueAnyway = await InteractivePrompt . confirm ( 'Continue with this URL anyway?' , {
348+ default : false ,
349+ } ) ;
350+ if ( ! continueAnyway ) {
351+ // Let user re-enter URL
352+ baseUrl = await InteractivePrompt . input ( 'API Base URL' , {
353+ validate : validateUrl ,
354+ default : baseUrl . replace ( / \/ ( c h a t \/ c o m p l e t i o n s | v 1 \/ m e s s a g e s | m e s s a g e s | c o m p l e t i o n s ) $ / i, '' ) ,
355+ } ) ;
356+ }
357+ }
358+
307359 // Step 3: API Key
308360 let apiKey = parsedArgs . apiKey ;
309361 if ( ! apiKey ) {
@@ -318,50 +370,127 @@ async function handleCreate(args: string[]): Promise<void> {
318370 const defaultModel = 'claude-sonnet-4-5-20250929' ;
319371 let model = parsedArgs . model ;
320372 if ( ! model && ! parsedArgs . yes ) {
321- model = await InteractivePrompt . input ( 'Default model' , {
373+ model = await InteractivePrompt . input ( 'Default model (ANTHROPIC_MODEL) ' , {
322374 default : defaultModel ,
323375 } ) ;
324376 }
325377 model = model || defaultModel ;
326378
379+ // Step 5: Model mapping for Opus/Sonnet/Haiku
380+ // Auto-show if user entered a custom model, otherwise ask
381+ let opusModel = model ;
382+ let sonnetModel = model ;
383+ let haikuModel = model ;
384+
385+ const isCustomModel = model !== defaultModel ;
386+
387+ if ( ! parsedArgs . yes ) {
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+ }
400+
401+ if ( wantCustomMapping ) {
402+ console . log ( '' ) ;
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 ;
420+ }
421+ }
422+
423+ // Build model mapping
424+ const models : ModelMapping = {
425+ default : model ,
426+ opus : opusModel ,
427+ sonnet : sonnetModel ,
428+ haiku : haikuModel ,
429+ } ;
430+
431+ // Check if custom model mapping is configured
432+ const hasCustomMapping = opusModel !== model || sonnetModel !== model || haikuModel !== model ;
433+
327434 // Create files
328435 console . log ( '' ) ;
329436 console . log ( info ( 'Creating API profile...' ) ) ;
330437
331438 try {
439+ const settingsFile = `~/.ccs/${ name } .settings.json` ;
440+
332441 if ( isUnifiedMode ( ) ) {
333442 // Use unified config format
334- createApiProfileUnified ( name , baseUrl , apiKey , model ) ;
443+ createApiProfileUnified ( name , baseUrl , apiKey , models ) ;
335444 console . log ( '' ) ;
336- console . log (
337- infoBox (
338- `API: ${ name } \n` +
339- `Config: ~/.ccs/config.yaml\n` +
340- `Secrets: ~/.ccs/secrets.yaml\n` +
341- `Base URL: ${ baseUrl } \n` +
342- `Model: ${ model } ` ,
343- 'API Profile Created (Unified Config)'
344- )
345- ) ;
445+
446+ // Build info message
447+ let infoMsg =
448+ `API: ${ name } \n` +
449+ `Config: ~/.ccs/config.yaml\n` +
450+ `Settings: ${ settingsFile } \n` +
451+ `Base URL: ${ baseUrl } \n` +
452+ `Model: ${ model } ` ;
453+
454+ if ( hasCustomMapping ) {
455+ infoMsg +=
456+ `\n\nModel Mapping:\n` +
457+ ` Opus: ${ opusModel } \n` +
458+ ` Sonnet: ${ sonnetModel } \n` +
459+ ` Haiku: ${ haikuModel } ` ;
460+ }
461+
462+ console . log ( infoBox ( infoMsg , 'API Profile Created' ) ) ;
346463 } else {
347464 // Use legacy JSON format
348- const settingsPath = createSettingsFile ( name , baseUrl , apiKey , model ) ;
465+ const settingsPath = createSettingsFile ( name , baseUrl , apiKey , models ) ;
349466 updateConfig ( name , settingsPath ) ;
350467 console . log ( '' ) ;
351- console . log (
352- infoBox (
353- `API: ${ name } \n` +
354- `Settings: ~/.ccs/${ name } .settings.json\n` +
355- `Base URL: ${ baseUrl } \n` +
356- `Model: ${ model } ` ,
357- 'API Profile Created'
358- )
359- ) ;
468+
469+ let infoMsg =
470+ `API: ${ name } \n` +
471+ `Settings: ${ settingsFile } \n` +
472+ `Base URL: ${ baseUrl } \n` +
473+ `Model: ${ model } ` ;
474+
475+ if ( hasCustomMapping ) {
476+ infoMsg +=
477+ `\n\nModel Mapping:\n` +
478+ ` Opus: ${ opusModel } \n` +
479+ ` Sonnet: ${ sonnetModel } \n` +
480+ ` Haiku: ${ haikuModel } ` ;
481+ }
482+
483+ console . log ( infoBox ( infoMsg , 'API Profile Created' ) ) ;
360484 }
485+
361486 console . log ( '' ) ;
362487 console . log ( header ( 'Usage' ) ) ;
363488 console . log ( ` ${ color ( `ccs ${ name } "your prompt"` , 'command' ) } ` ) ;
364489 console . log ( '' ) ;
490+ console . log ( header ( 'Edit Settings' ) ) ;
491+ console . log ( ` ${ dim ( 'To modify env vars later:' ) } ` ) ;
492+ console . log ( ` ${ color ( `nano ${ settingsFile . replace ( '~' , '$HOME' ) } ` , 'command' ) } ` ) ;
493+ console . log ( '' ) ;
365494 } catch ( error ) {
366495 console . log ( fail ( `Failed to create API profile: ${ ( error as Error ) . message } ` ) ) ;
367496 process . exit ( 1 ) ;
0 commit comments