mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(codersdk): generate chat model provider options schema from Go structs (#22568)
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
|
||||
// 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 {
|
||||
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()
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user