package main import ( "encoding/json" "fmt" "os" "reflect" "strings" "github.com/shopspring/decimal" "github.com/coder/coder/v2/codersdk" ) // SchemaField describes a single form field in the generated schema. type SchemaField struct { JSONName string `json:"json_name"` GoName string `json:"go_name"` Type string `json:"type"` Description string `json:"description,omitempty"` Required bool `json:"required"` Enum []string `json:"enum,omitempty"` InputType string `json:"input_type"` Hidden bool `json:"hidden,omitempty"` } // FieldGroup holds the fields for a struct or provider. type FieldGroup struct { Fields []SchemaField `json:"fields"` } // Schema is the top-level output structure. type Schema struct { General FieldGroup `json:"general"` Providers map[string]FieldGroup `json:"providers"` ProviderAliases map[string]string `json:"provider_aliases"` } func main() { schema := Schema{ Providers: make(map[string]FieldGroup), ProviderAliases: map[string]string{ "azure": "openai", "bedrock": "anthropic", }, } // General options from ChatModelCallConfig, excluding // the provider_options field which is handled separately. schema.General = extractFields( reflect.TypeOf(codersdk.ChatModelCallConfig{}), "", map[string]bool{"ProviderOptions": true}, ) // Provider-specific options. Each entry maps a provider key // to the concrete options struct used for that provider. providerTypes := []struct { key string typ reflect.Type }{ {"openai", reflect.TypeOf(codersdk.ChatModelOpenAIProviderOptions{})}, {"anthropic", reflect.TypeOf(codersdk.ChatModelAnthropicProviderOptions{})}, {"google", reflect.TypeOf(codersdk.ChatModelGoogleProviderOptions{})}, {"openaicompat", reflect.TypeOf(codersdk.ChatModelOpenAICompatProviderOptions{})}, {"openrouter", reflect.TypeOf(codersdk.ChatModelOpenRouterProviderOptions{})}, {"vercel", reflect.TypeOf(codersdk.ChatModelVercelProviderOptions{})}, } for _, p := range providerTypes { schema.Providers[p.key] = extractFields(p.typ, "", nil) } out, err := json.MarshalIndent(schema, "", "\t") if err != nil { _, _ = fmt.Fprintf(os.Stderr, "marshal schema: %v\n", err) os.Exit(1) } // Print the generated header and JSON body. _, _ = fmt.Println("// Code generated by scripts/modeloptionsgen. DO NOT EDIT.") _, _ = fmt.Println(string(out)) } // extractFields walks the struct fields of t and returns a FieldGroup. // prefix is used to build dot-separated json_name values for nested // structs. skip lists Go field names to exclude from output. func extractFields(t reflect.Type, prefix string, skip map[string]bool) FieldGroup { var fields []SchemaField for i := range t.NumField() { f := t.Field(i) if skip != nil && skip[f.Name] { continue } jsonTag := f.Tag.Get("json") if jsonTag == "" || jsonTag == "-" { continue } jsonName := strings.Split(jsonTag, ",")[0] if jsonName == "" { continue } fullJSONName := jsonName if prefix != "" { fullJSONName = prefix + "." + jsonName } // Determine the underlying type, dereferencing pointers. ft := f.Type if ft.Kind() == reflect.Ptr { ft = ft.Elem() } // Check the hidden tag before recursing into nested structs // so that entire sub-objects can be marked hidden. hidden := f.Tag.Get("hidden") == "true" // decimal.Decimal is an opaque numeric type used for pricing // precision; do not recurse into its internal struct fields. isDecimal := ft == reflect.TypeOf(decimal.Decimal{}) // If the field is a struct (not a map), recurse to flatten // its children using dot-separated names — unless the // entire struct is marked hidden, in which case emit it // as a single opaque field. if ft.Kind() == reflect.Struct && !hidden && !isDecimal { nested := extractFields(ft, fullJSONName, nil) fields = append(fields, nested.Fields...) continue } typeName := goTypeToSchemaType(f.Type) description := f.Tag.Get("description") enumTag := f.Tag.Get("enum") var enumValues []string if enumTag != "" { enumValues = strings.Split(enumTag, ",") } required := !strings.Contains(jsonTag, "omitempty") inputType := inferInputType(typeName, enumValues) fields = append(fields, SchemaField{ JSONName: fullJSONName, GoName: goFieldPath(prefix, f.Name, t, fullJSONName), Type: typeName, Description: description, Required: required, Enum: enumValues, InputType: inputType, Hidden: hidden, }) } return FieldGroup{Fields: fields} } // goFieldPath builds a dot-separated Go field name for nested fields. // For top-level fields it returns just the field name. For nested // fields it reconstructs the parent struct field name from the prefix // by looking at the enclosing type's fields. func goFieldPath(prefix, name string, _ reflect.Type, fullJSONName string) string { if prefix == "" { return name } // Build the Go path by walking the JSON name segments. Each // segment maps to a struct field that we already traversed // during recursion, so we reconstruct the path from the JSON // parts. The parent extractFields call sets the prefix to the // parent json name, so we can derive the Go path from the // json segments by title-casing each part. parts := strings.Split(fullJSONName, ".") goNames := make([]string, 0, len(parts)) for _, p := range parts { goNames = append(goNames, jsonSegmentToGoName(p)) } return strings.Join(goNames, ".") } // jsonSegmentToGoName converts a snake_case JSON segment to a // PascalCase Go field name using common conventions. func jsonSegmentToGoName(seg string) string { words := strings.Split(seg, "_") var b strings.Builder for _, w := range words { if w == "" { continue } // Handle common acronyms. upper := strings.ToUpper(w) switch upper { case "ID", "URL", "IP", "HTTP", "JSON", "API", "UI": _, _ = b.WriteString(upper) default: _, _ = b.WriteString(strings.ToUpper(w[:1])) _, _ = b.WriteString(w[1:]) } } return b.String() } // goTypeToSchemaType maps a Go reflect.Type to a JSON schema type // string. func goTypeToSchemaType(t reflect.Type) string { // Dereference pointers. for t.Kind() == reflect.Ptr { t = t.Elem() } // decimal.Decimal represents a precise numeric value and should // map to the "number" schema type. if t == reflect.TypeOf(decimal.Decimal{}) { return "number" } switch t.Kind() { case reflect.String: return "string" case reflect.Bool: return "boolean" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return "integer" case reflect.Float32, reflect.Float64: return "number" case reflect.Slice: return "array" case reflect.Map: return "object" default: return "string" } } // inferInputType decides the appropriate frontend input widget for // a field based on its schema type and enum values. func inferInputType(typeName string, enum []string) string { if len(enum) > 0 { return "select" } switch typeName { case "boolean": return "select" case "array", "object": return "json" default: return "input" } }