@@ -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 ) {
@@ -324,44 +376,100 @@ async function handleCreate(args: string[]): Promise<void> {
324376 }
325377 model = model || defaultModel ;
326378
379+ // Step 5: Optional model mapping for Opus/Sonnet/Haiku
380+ // Ask user if they want different models for each type
381+ let opusModel = model ;
382+ let sonnetModel = model ;
383+ let haikuModel = model ;
384+
385+ 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+ ) ;
392+
393+ if ( wantCustomMapping ) {
394+ 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 ;
399+ }
400+ }
401+
402+ // Build model mapping
403+ const models : ModelMapping = {
404+ default : model ,
405+ opus : opusModel ,
406+ sonnet : sonnetModel ,
407+ haiku : haikuModel ,
408+ } ;
409+
410+ // Check if custom model mapping is configured
411+ const hasCustomMapping = opusModel !== model || sonnetModel !== model || haikuModel !== model ;
412+
327413 // Create files
328414 console . log ( '' ) ;
329415 console . log ( info ( 'Creating API profile...' ) ) ;
330416
331417 try {
418+ const settingsFile = `~/.ccs/${ name } .settings.json` ;
419+
332420 if ( isUnifiedMode ( ) ) {
333421 // Use unified config format
334- createApiProfileUnified ( name , baseUrl , apiKey , model ) ;
422+ createApiProfileUnified ( name , baseUrl , apiKey , models ) ;
335423 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- ) ;
424+
425+ // Build info message
426+ let infoMsg =
427+ `API: ${ name } \n` +
428+ `Config: ~/.ccs/config.yaml\n` +
429+ `Settings: ${ settingsFile } \n` +
430+ `Base URL: ${ baseUrl } \n` +
431+ `Model: ${ model } ` ;
432+
433+ if ( hasCustomMapping ) {
434+ infoMsg +=
435+ `\n\nModel Mapping:\n` +
436+ ` Opus: ${ opusModel } \n` +
437+ ` Sonnet: ${ sonnetModel } \n` +
438+ ` Haiku: ${ haikuModel } ` ;
439+ }
440+
441+ console . log ( infoBox ( infoMsg , 'API Profile Created' ) ) ;
346442 } else {
347443 // Use legacy JSON format
348- const settingsPath = createSettingsFile ( name , baseUrl , apiKey , model ) ;
444+ const settingsPath = createSettingsFile ( name , baseUrl , apiKey , models ) ;
349445 updateConfig ( name , settingsPath ) ;
350446 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- ) ;
447+
448+ let infoMsg =
449+ `API: ${ name } \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' ) ) ;
360463 }
464+
361465 console . log ( '' ) ;
362466 console . log ( header ( 'Usage' ) ) ;
363467 console . log ( ` ${ color ( `ccs ${ name } "your prompt"` , 'command' ) } ` ) ;
364468 console . log ( '' ) ;
469+ console . log ( header ( 'Edit Settings' ) ) ;
470+ console . log ( ` ${ dim ( 'To modify env vars later:' ) } ` ) ;
471+ console . log ( ` ${ color ( `nano ${ settingsFile . replace ( '~' , '$HOME' ) } ` , 'command' ) } ` ) ;
472+ console . log ( '' ) ;
365473 } catch ( error ) {
366474 console . log ( fail ( `Failed to create API profile: ${ ( error as Error ) . message } ` ) ) ;
367475 process . exit ( 1 ) ;
0 commit comments