@@ -16,7 +16,7 @@ import (
1616
1717const DEFAULT_GROUP_ID = "(default)"
1818
19- type MacroList map [string ]string
19+ type MacroList map [string ]any
2020
2121type GroupConfig struct {
2222 Swap bool `yaml:"swap"`
@@ -185,11 +185,13 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
185185 // go through model config fields: cmd, cmdStop, proxy, checkEndPoint and replace macros with macro values
186186 for macroName , macroValue := range mergedMacros {
187187 macroSlug := fmt .Sprintf ("${%s}" , macroName )
188- modelConfig .Cmd = strings .ReplaceAll (modelConfig .Cmd , macroSlug , macroValue )
189- modelConfig .CmdStop = strings .ReplaceAll (modelConfig .CmdStop , macroSlug , macroValue )
190- modelConfig .Proxy = strings .ReplaceAll (modelConfig .Proxy , macroSlug , macroValue )
191- modelConfig .CheckEndpoint = strings .ReplaceAll (modelConfig .CheckEndpoint , macroSlug , macroValue )
192- modelConfig .Filters .StripParams = strings .ReplaceAll (modelConfig .Filters .StripParams , macroSlug , macroValue )
188+ // Convert macro value to string for command/string field substitution
189+ macroStr := fmt .Sprintf ("%v" , macroValue )
190+ modelConfig .Cmd = strings .ReplaceAll (modelConfig .Cmd , macroSlug , macroStr )
191+ modelConfig .CmdStop = strings .ReplaceAll (modelConfig .CmdStop , macroSlug , macroStr )
192+ modelConfig .Proxy = strings .ReplaceAll (modelConfig .Proxy , macroSlug , macroStr )
193+ modelConfig .CheckEndpoint = strings .ReplaceAll (modelConfig .CheckEndpoint , macroSlug , macroStr )
194+ modelConfig .Filters .StripParams = strings .ReplaceAll (modelConfig .Filters .StripParams , macroSlug , macroStr )
193195 }
194196
195197 // enforce ${PORT} used in both cmd and proxy
@@ -234,6 +236,15 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
234236 }
235237 }
236238
239+ // Apply macro substitution to metadata
240+ if len (modelConfig .Metadata ) > 0 {
241+ substitutedMetadata , err := substituteMetadataMacros (modelConfig .Metadata , mergedMacros )
242+ if err != nil {
243+ return Config {}, fmt .Errorf ("model %s metadata: %s" , modelId , err .Error ())
244+ }
245+ modelConfig .Metadata = substitutedMetadata .(map [string ]any )
246+ }
247+
237248 config .Models [modelId ] = modelConfig
238249 }
239250
@@ -296,7 +307,7 @@ func AddDefaultGroupToConfig(config Config) Config {
296307 }
297308 } else {
298309 // iterate over existing group members and add non-grouped models into the default group
299- for modelName , _ := range config .Models {
310+ for modelName := range config .Models {
300311 foundModel := false
301312 found:
302313 // search for the model in existing groups
@@ -374,15 +385,24 @@ var (
374385)
375386
376387// validateMacro validates macro name and value constraints
377- func validateMacro (name , value string ) error {
388+ func validateMacro (name string , value any ) error {
378389 if len (name ) >= 64 {
379390 return fmt .Errorf ("macro name '%s' exceeds maximum length of 63 characters" , name )
380391 }
381392 if ! macroNameRegex .MatchString (name ) {
382393 return fmt .Errorf ("macro name '%s' contains invalid characters, must match pattern ^[a-zA-Z0-9_-]+$" , name )
383394 }
384- if len (value ) >= 1024 {
385- return fmt .Errorf ("macro value for '%s' exceeds maximum length of 1024 characters" , name )
395+
396+ // Validate that value is a scalar type
397+ switch v := value .(type ) {
398+ case string :
399+ if len (v ) >= 1024 {
400+ return fmt .Errorf ("macro value for '%s' exceeds maximum length of 1024 characters" , name )
401+ }
402+ case int , int8 , int16 , int32 , int64 , uint , uint8 , uint16 , uint32 , uint64 , float32 , float64 , bool :
403+ // These types are allowed
404+ default :
405+ return fmt .Errorf ("macro '%s' has invalid type %T, must be a scalar type (string, int, float, or bool)" , name , value )
386406 }
387407
388408 switch name {
@@ -392,3 +412,64 @@ func validateMacro(name, value string) error {
392412
393413 return nil
394414}
415+
416+ // substituteMetadataMacros recursively substitutes macros in metadata structures
417+ // Direct substitution (key: ${macro}) preserves the macro's type
418+ // Interpolated substitution (key: "text ${macro}") converts to string
419+ func substituteMetadataMacros (value any , macros MacroList ) (any , error ) {
420+ switch v := value .(type ) {
421+ case string :
422+ // Check if this is a direct macro substitution
423+ if strings .HasPrefix (v , "${" ) && strings .HasSuffix (v , "}" ) && strings .Count (v , "${" ) == 1 {
424+ macroName := v [2 : len (v )- 1 ]
425+ if macroValue , exists := macros [macroName ]; exists {
426+ return macroValue , nil
427+ }
428+ return nil , fmt .Errorf ("unknown macro '${%s}' in metadata" , macroName )
429+ }
430+
431+ // Handle string interpolation
432+ macroPattern := regexp .MustCompile (`\$\{([a-zA-Z0-9_-]+)\}` )
433+ matches := macroPattern .FindAllStringSubmatch (v , - 1 )
434+ result := v
435+ for _ , match := range matches {
436+ macroName := match [1 ]
437+ macroValue , exists := macros [macroName ]
438+ if ! exists {
439+ return nil , fmt .Errorf ("unknown macro '${%s}' in metadata" , macroName )
440+ }
441+ // Convert macro value to string for interpolation
442+ macroStr := fmt .Sprintf ("%v" , macroValue )
443+ result = strings .ReplaceAll (result , match [0 ], macroStr )
444+ }
445+ return result , nil
446+
447+ case map [string ]any :
448+ // Recursively process map values
449+ newMap := make (map [string ]any )
450+ for key , val := range v {
451+ newVal , err := substituteMetadataMacros (val , macros )
452+ if err != nil {
453+ return nil , err
454+ }
455+ newMap [key ] = newVal
456+ }
457+ return newMap , nil
458+
459+ case []any :
460+ // Recursively process slice elements
461+ newSlice := make ([]any , len (v ))
462+ for i , val := range v {
463+ newVal , err := substituteMetadataMacros (val , macros )
464+ if err != nil {
465+ return nil , err
466+ }
467+ newSlice [i ] = newVal
468+ }
469+ return newSlice , nil
470+
471+ default :
472+ // Return scalar types as-is
473+ return value , nil
474+ }
475+ }
0 commit comments