From f758443f44ace9a5ac68da181e0c6b7f0b56171b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 3 Mar 2026 16:29:58 -0500 Subject: [PATCH] feat(codersdk): generate chat model provider options schema from Go structs (#22568) --- Makefile | 6 + codersdk/chats.go | 152 +++--- scripts/modeloptionsgen/main.go | 241 +++++++++ site/.knip.jsonc | 2 +- site/src/api/chatModelOptions.ts | 139 +++++ site/src/api/chatModelOptionsGenerated.json | 542 ++++++++++++++++++++ 6 files changed, 1005 insertions(+), 77 deletions(-) create mode 100644 scripts/modeloptionsgen/main.go create mode 100644 site/src/api/chatModelOptions.ts create mode 100644 site/src/api/chatModelOptionsGenerated.json diff --git a/Makefile b/Makefile index 0dbd0e3c9e..3d52b6f7a6 100644 --- a/Makefile +++ b/Makefile @@ -427,6 +427,7 @@ SITE_GEN_FILES := \ site/src/api/typesGenerated.ts \ site/src/api/rbacresourcesGenerated.ts \ site/src/api/countriesGenerated.ts \ + site/src/api/chatModelOptionsGenerated.json \ site/src/theme/icons.json site/out/index.html: \ @@ -721,6 +722,7 @@ gen/mark-fresh: coderd/rbac/scopes_constants_gen.go \ site/src/api/rbacresourcesGenerated.ts \ site/src/api/countriesGenerated.ts \ + site/src/api/chatModelOptionsGenerated.json \ docs/admin/integrations/prometheus.md \ docs/reference/cli/index.md \ docs/admin/security/audit-logs.md \ @@ -917,6 +919,10 @@ site/src/api/countriesGenerated.ts: site/node_modules/.installed scripts/typegen ./scripts/biome_format.sh src/api/countriesGenerated.ts touch "$@" +site/src/api/chatModelOptionsGenerated.json: scripts/modeloptionsgen/main.go codersdk/chats.go + go run ./scripts/modeloptionsgen/main.go | tail -n +2 > "$@" + cd site && pnpm biome format --write src/api/chatModelOptionsGenerated.json + scripts/metricsdocgen/generated_metrics: $(GO_SRC_FILES) go run ./scripts/metricsdocgen/scanner > $@ diff --git a/codersdk/chats.go b/codersdk/chats.go index 44ca9c4629..df0b9b5d24 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -246,134 +246,134 @@ type ChatModelProviderOptions struct { // ChatModelOpenAIProviderOptions configures OpenAI provider behavior. type ChatModelOpenAIProviderOptions struct { - Include []string `json:"include,omitempty"` - Instructions *string `json:"instructions,omitempty"` - LogitBias map[string]int64 `json:"logit_bias,omitempty"` - LogProbs *bool `json:"log_probs,omitempty"` - TopLogProbs *int64 `json:"top_log_probs,omitempty"` - MaxToolCalls *int64 `json:"max_tool_calls,omitempty"` - ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` - User *string `json:"user,omitempty"` - ReasoningEffort *string `json:"reasoning_effort,omitempty"` - ReasoningSummary *string `json:"reasoning_summary,omitempty"` - MaxCompletionTokens *int64 `json:"max_completion_tokens,omitempty"` - TextVerbosity *string `json:"text_verbosity,omitempty"` - Prediction map[string]any `json:"prediction,omitempty"` - Store *bool `json:"store,omitempty"` - Metadata map[string]any `json:"metadata,omitempty"` - PromptCacheKey *string `json:"prompt_cache_key,omitempty"` - SafetyIdentifier *string `json:"safety_identifier,omitempty"` - ServiceTier *string `json:"service_tier,omitempty"` - StructuredOutputs *bool `json:"structured_outputs,omitempty"` - StrictJSONSchema *bool `json:"strict_json_schema,omitempty"` + Include []string `json:"include,omitempty" description:"Model names to include in discovery" hidden:"true"` + Instructions *string `json:"instructions,omitempty" description:"System-level instructions prepended to the conversation" hidden:"true"` + LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"` + LogProbs *bool `json:"log_probs,omitempty" description:"Whether to return log probabilities of output tokens" hidden:"true"` + TopLogProbs *int64 `json:"top_log_probs,omitempty" description:"Number of most likely tokens to return log probabilities for" hidden:"true"` + MaxToolCalls *int64 `json:"max_tool_calls,omitempty" description:"Maximum number of tool calls per response"` + ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"` + User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` + ReasoningEffort *string `json:"reasoning_effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` + ReasoningSummary *string `json:"reasoning_summary,omitempty" description:"Controls whether reasoning tokens are summarized in the response"` + MaxCompletionTokens *int64 `json:"max_completion_tokens,omitempty" description:"Upper bound on tokens the model may generate"` + TextVerbosity *string `json:"text_verbosity,omitempty" description:"Controls the verbosity of the text response" enum:"low,medium,high"` + Prediction map[string]any `json:"prediction,omitempty" description:"Predicted output content to speed up responses" hidden:"true"` + Store *bool `json:"store,omitempty" description:"Whether to store the output for model distillation or evals" hidden:"true"` + Metadata map[string]any `json:"metadata,omitempty" description:"Arbitrary metadata to attach to the request" hidden:"true"` + PromptCacheKey *string `json:"prompt_cache_key,omitempty" description:"Key for enabling cross-request prompt caching" hidden:"true"` + SafetyIdentifier *string `json:"safety_identifier,omitempty" description:"Developer-specific safety identifier for the request" hidden:"true"` + ServiceTier *string `json:"service_tier,omitempty" description:"Latency tier to use for processing the request"` + StructuredOutputs *bool `json:"structured_outputs,omitempty" description:"Whether to enable structured JSON output mode" hidden:"true"` + StrictJSONSchema *bool `json:"strict_json_schema,omitempty" description:"Whether to enforce strict adherence to the JSON schema" hidden:"true"` } // ChatModelAnthropicThinkingOptions configures Anthropic thinking budget. type ChatModelAnthropicThinkingOptions struct { - BudgetTokens *int64 `json:"budget_tokens,omitempty"` + BudgetTokens *int64 `json:"budget_tokens,omitempty" description:"Maximum number of tokens the model may use for thinking"` } // ChatModelAnthropicProviderOptions configures Anthropic provider behavior. type ChatModelAnthropicProviderOptions struct { - SendReasoning *bool `json:"send_reasoning,omitempty"` - Thinking *ChatModelAnthropicThinkingOptions `json:"thinking,omitempty"` - Effort *string `json:"effort,omitempty"` - DisableParallelToolUse *bool `json:"disable_parallel_tool_use,omitempty"` + SendReasoning *bool `json:"send_reasoning,omitempty" description:"Whether to include reasoning content in the response"` + Thinking *ChatModelAnthropicThinkingOptions `json:"thinking,omitempty" description:"Configuration for extended thinking"` + Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"low,medium,high,max"` + DisableParallelToolUse *bool `json:"disable_parallel_tool_use,omitempty" description:"Whether to disable parallel tool execution"` } // ChatModelGoogleThinkingConfig configures Google thinking behavior. type ChatModelGoogleThinkingConfig struct { - ThinkingBudget *int64 `json:"thinking_budget,omitempty"` - IncludeThoughts *bool `json:"include_thoughts,omitempty"` + ThinkingBudget *int64 `json:"thinking_budget,omitempty" description:"Maximum number of tokens the model may use for thinking"` + IncludeThoughts *bool `json:"include_thoughts,omitempty" description:"Whether to include thinking content in the response"` } // ChatModelGoogleSafetySetting configures Google safety filtering. type ChatModelGoogleSafetySetting struct { - Category string `json:"category,omitempty"` - Threshold string `json:"threshold,omitempty"` + Category string `json:"category,omitempty" description:"The harm category to configure"` + Threshold string `json:"threshold,omitempty" description:"The blocking threshold for the harm category"` } // ChatModelGoogleProviderOptions configures Google provider behavior. type ChatModelGoogleProviderOptions struct { - ThinkingConfig *ChatModelGoogleThinkingConfig `json:"thinking_config,omitempty"` - CachedContent string `json:"cached_content,omitempty"` - SafetySettings []ChatModelGoogleSafetySetting `json:"safety_settings,omitempty"` - Threshold string `json:"threshold,omitempty"` + ThinkingConfig *ChatModelGoogleThinkingConfig `json:"thinking_config,omitempty" description:"Configuration for extended thinking"` + CachedContent string `json:"cached_content,omitempty" description:"Resource name of a cached content object" hidden:"true"` + SafetySettings []ChatModelGoogleSafetySetting `json:"safety_settings,omitempty" description:"Safety filtering settings for harmful content categories" hidden:"true"` + Threshold string `json:"threshold,omitempty" hidden:"true"` } // ChatModelOpenAICompatProviderOptions configures OpenAI-compatible behavior. type ChatModelOpenAICompatProviderOptions struct { - User *string `json:"user,omitempty"` - ReasoningEffort *string `json:"reasoning_effort,omitempty"` + User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` + ReasoningEffort *string `json:"reasoning_effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` } // ChatModelOpenRouterReasoningOptions configures OpenRouter reasoning behavior. type ChatModelOpenRouterReasoningOptions struct { - Enabled *bool `json:"enabled,omitempty"` - Exclude *bool `json:"exclude,omitempty"` - MaxTokens *int64 `json:"max_tokens,omitempty"` - Effort *string `json:"effort,omitempty"` + Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"` + Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"` + MaxTokens *int64 `json:"max_tokens,omitempty" description:"Maximum number of tokens for reasoning output"` + Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"low,medium,high"` } // ChatModelOpenRouterProvider configures OpenRouter routing preferences. type ChatModelOpenRouterProvider struct { - Order []string `json:"order,omitempty"` - AllowFallbacks *bool `json:"allow_fallbacks,omitempty"` - RequireParameters *bool `json:"require_parameters,omitempty"` - DataCollection *string `json:"data_collection,omitempty"` - Only []string `json:"only,omitempty"` - Ignore []string `json:"ignore,omitempty"` - Quantizations []string `json:"quantizations,omitempty"` - Sort *string `json:"sort,omitempty"` + Order []string `json:"order,omitempty" description:"Ordered list of preferred provider names"` + AllowFallbacks *bool `json:"allow_fallbacks,omitempty" description:"Whether to allow fallback to other providers"` + RequireParameters *bool `json:"require_parameters,omitempty" description:"Whether to require all parameters to be supported by the provider"` + DataCollection *string `json:"data_collection,omitempty" description:"Data collection policy preference"` + Only []string `json:"only,omitempty" description:"Restrict to only these provider names"` + Ignore []string `json:"ignore,omitempty" description:"Provider names to exclude from routing"` + Quantizations []string `json:"quantizations,omitempty" description:"Allowed model quantization levels"` + Sort *string `json:"sort,omitempty" description:"Sort order for provider selection"` } // ChatModelOpenRouterProviderOptions configures OpenRouter provider behavior. type ChatModelOpenRouterProviderOptions struct { - Reasoning *ChatModelOpenRouterReasoningOptions `json:"reasoning,omitempty"` - ExtraBody map[string]any `json:"extra_body,omitempty"` - IncludeUsage *bool `json:"include_usage,omitempty"` - LogitBias map[string]int64 `json:"logit_bias,omitempty"` - LogProbs *bool `json:"log_probs,omitempty"` - ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` - User *string `json:"user,omitempty"` - Provider *ChatModelOpenRouterProvider `json:"provider,omitempty"` + Reasoning *ChatModelOpenRouterReasoningOptions `json:"reasoning,omitempty" description:"Configuration for reasoning behavior"` + ExtraBody map[string]any `json:"extra_body,omitempty" description:"Additional fields to include in the request body" hidden:"true"` + IncludeUsage *bool `json:"include_usage,omitempty" description:"Whether to include token usage information in the response" hidden:"true"` + LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"` + LogProbs *bool `json:"log_probs,omitempty" description:"Whether to return log probabilities of output tokens" hidden:"true"` + ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"` + User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` + Provider *ChatModelOpenRouterProvider `json:"provider,omitempty" description:"Routing preferences for provider selection" hidden:"true"` } // ChatModelVercelReasoningOptions configures Vercel reasoning behavior. type ChatModelVercelReasoningOptions struct { - Enabled *bool `json:"enabled,omitempty"` - MaxTokens *int64 `json:"max_tokens,omitempty"` - Effort *string `json:"effort,omitempty"` - Exclude *bool `json:"exclude,omitempty"` + Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"` + MaxTokens *int64 `json:"max_tokens,omitempty" description:"Maximum number of tokens for reasoning output"` + Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` + Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"` } // ChatModelVercelGatewayProviderOptions configures Vercel routing behavior. type ChatModelVercelGatewayProviderOptions struct { - Order []string `json:"order,omitempty"` - Models []string `json:"models,omitempty"` + Order []string `json:"order,omitempty" description:"Ordered list of preferred provider names"` + Models []string `json:"models,omitempty" description:"Model identifiers to route across"` } // ChatModelVercelProviderOptions configures Vercel provider behavior. type ChatModelVercelProviderOptions struct { - Reasoning *ChatModelVercelReasoningOptions `json:"reasoning,omitempty"` - ProviderOptions *ChatModelVercelGatewayProviderOptions `json:"providerOptions,omitempty"` - User *string `json:"user,omitempty"` - LogitBias map[string]int64 `json:"logit_bias,omitempty"` - LogProbs *bool `json:"logprobs,omitempty"` - TopLogProbs *int64 `json:"top_logprobs,omitempty"` - ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` - ExtraBody map[string]any `json:"extra_body,omitempty"` + Reasoning *ChatModelVercelReasoningOptions `json:"reasoning,omitempty" description:"Configuration for reasoning behavior"` + ProviderOptions *ChatModelVercelGatewayProviderOptions `json:"providerOptions,omitempty" description:"Gateway routing options for provider selection" hidden:"true"` + User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` + LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"` + LogProbs *bool `json:"logprobs,omitempty" description:"Whether to return log probabilities of output tokens" hidden:"true"` + TopLogProbs *int64 `json:"top_logprobs,omitempty" description:"Number of most likely tokens to return log probabilities for" hidden:"true"` + ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"` + ExtraBody map[string]any `json:"extra_body,omitempty" description:"Additional fields to include in the request body" hidden:"true"` } // ChatModelCallConfig configures per-call model behavior defaults. type ChatModelCallConfig struct { - MaxOutputTokens *int64 `json:"max_output_tokens,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP *float64 `json:"top_p,omitempty"` - TopK *int64 `json:"top_k,omitempty"` - PresencePenalty *float64 `json:"presence_penalty,omitempty"` - FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` - ProviderOptions *ChatModelProviderOptions `json:"provider_options,omitempty"` + MaxOutputTokens *int64 `json:"max_output_tokens,omitempty" description:"Upper bound on tokens the model may generate"` + Temperature *float64 `json:"temperature,omitempty" description:"Sampling temperature between 0 and 2"` + TopP *float64 `json:"top_p,omitempty" description:"Nucleus sampling probability cutoff"` + TopK *int64 `json:"top_k,omitempty" description:"Number of highest-probability tokens to keep for sampling"` + PresencePenalty *float64 `json:"presence_penalty,omitempty" description:"Penalty for tokens that have already appeared in the output"` + FrequencyPenalty *float64 `json:"frequency_penalty,omitempty" description:"Penalty for tokens based on their frequency in the output"` + ProviderOptions *ChatModelProviderOptions `json:"provider_options,omitempty" description:"Provider-specific option overrides"` } // CreateChatModelConfigRequest creates a chat model config. diff --git a/scripts/modeloptionsgen/main.go b/scripts/modeloptionsgen/main.go new file mode 100644 index 0000000000..7f4d039e18 --- /dev/null +++ b/scripts/modeloptionsgen/main.go @@ -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" + } +} diff --git a/site/.knip.jsonc b/site/.knip.jsonc index 312d4a9782..49453de4d0 100644 --- a/site/.knip.jsonc +++ b/site/.knip.jsonc @@ -2,7 +2,7 @@ "$schema": "https://unpkg.com/knip@5/schema.json", "entry": ["./src/index.tsx", "./src/serviceWorker.ts"], "project": ["./src/**/*.ts", "./src/**/*.tsx", "./e2e/**/*.ts"], - "ignore": ["**/*Generated.ts"], + "ignore": ["**/*Generated.ts", "src/api/chatModelOptions.ts"], "ignoreBinaries": ["protoc"], "ignoreDependencies": [ "@types/react-virtualized-auto-sizer", diff --git a/site/src/api/chatModelOptions.ts b/site/src/api/chatModelOptions.ts new file mode 100644 index 0000000000..6757720956 --- /dev/null +++ b/site/src/api/chatModelOptions.ts @@ -0,0 +1,139 @@ +import schema from "./chatModelOptionsGenerated.json"; + +/** + * Describes a single configurable field for a chat model provider. + * Generated from Go struct tags via `scripts/modeloptionsgen`. + */ +export interface FieldSchema { + /** The JSON key used in API payloads (may use dot-notation for nested fields). */ + json_name: string; + /** The corresponding Go struct field name. */ + go_name: string; + /** The JSON Schema type of this field. */ + type: "string" | "integer" | "number" | "boolean" | "array" | "object"; + /** Human-readable description of the field. May be absent for some fields. */ + description?: string; + /** Whether this field is required when configuring the provider. */ + required: boolean; + /** Hint for how the frontend should render the input control. */ + input_type: "input" | "select" | "json"; + /** If present, the field value must be one of these options. */ + enum?: string[]; + /** If true, this field should not be rendered in admin UI forms. */ + hidden?: boolean; +} + +/** + * A group of fields belonging to a single provider or the general section. + */ +export interface ProviderSchema { + fields: FieldSchema[]; +} + +/** + * Top-level schema describing all configurable chat model options. + * + * - `general` contains provider-independent fields (e.g. temperature). + * - `providers` maps canonical provider names to their specific fields. + * - `provider_aliases` maps alternate names to canonical provider names + * (e.g. "azure" → "openai"). + */ +export interface ModelOptionsSchema { + general: ProviderSchema; + providers: Record; + provider_aliases: Record; +} + +/** The imported schema, typed as {@link ModelOptionsSchema}. */ +export const modelOptionsSchema: ModelOptionsSchema = + schema as ModelOptionsSchema; + +/** + * Get the general (provider-independent) fields such as temperature + * and max_output_tokens. + */ +export function getGeneralFields(): FieldSchema[] { + return modelOptionsSchema.general.fields; +} + +/** + * Get provider-specific fields for a given provider name. + * Handles aliases (e.g. "azure" → "openai", "bedrock" → "anthropic"). + * Returns an empty array for unknown providers. + */ +export function getProviderFields(provider: string): FieldSchema[] { + const resolved = resolveProvider(provider); + return modelOptionsSchema.providers[resolved]?.fields ?? []; +} + +/** + * Resolve a provider name through the alias table. + * If the name is an alias it returns the canonical provider; + * otherwise the original name is returned unchanged. + * + * @example + * resolveProvider("azure") // "openai" + * resolveProvider("bedrock") // "anthropic" + * resolveProvider("openai") // "openai" + */ +export function resolveProvider(provider: string): string { + return modelOptionsSchema.provider_aliases[provider] ?? provider; +} + +/** + * Get all canonical provider names (excludes aliases). + * The order matches the JSON schema and is not guaranteed to be stable + * across regenerations. + */ +export function getProviderNames(): string[] { + return Object.keys(modelOptionsSchema.providers); +} + +/** + * Check whether a provider is known, either as a canonical name or an alias. + */ +export function isKnownProvider(provider: string): boolean { + const resolved = resolveProvider(provider); + return resolved in modelOptionsSchema.providers; +} + +/** + * Convert a snake_case segment to camelCase. + * Only the first character after each underscore is uppercased; + * the leading character stays lowercase. + */ +function snakeToCamel(s: string): string { + return s.replace(/_([a-z0-9])/g, (_, ch: string) => ch.toUpperCase()); +} + +/** + * Convert a dot-notation `json_name` into a form field key namespaced + * under the given provider. + * + * Each dot-separated segment is converted from snake_case to camelCase + * and joined back with dots, then prefixed with the provider name. + * + * This bridges between the JSON schema (snake_case, flat `json_name`) + * and a typical React form state tree (camelCase, dot-separated paths). + * + * @example + * toFormFieldKey("anthropic", "thinking.budget_tokens") + * // "anthropic.thinking.budgetTokens" + * + * toFormFieldKey("openai", "max_completion_tokens") + * // "openai.maxCompletionTokens" + */ +export function toFormFieldKey(provider: string, jsonName: string): string { + const camelSegments = jsonName.split(".").map(snakeToCamel); + return `${provider}.${camelSegments.join(".")}`; +} + +/** Get only the visible (non-hidden) fields for a provider. */ +export function getVisibleProviderFields(provider: string): FieldSchema[] { + return getProviderFields(provider).filter((f) => !f.hidden); +} + +/** Get only the visible (non-hidden) general fields. */ +export function getVisibleGeneralFields(): FieldSchema[] { + return getGeneralFields().filter((f) => !f.hidden); +} diff --git a/site/src/api/chatModelOptionsGenerated.json b/site/src/api/chatModelOptionsGenerated.json new file mode 100644 index 0000000000..f26afe1767 --- /dev/null +++ b/site/src/api/chatModelOptionsGenerated.json @@ -0,0 +1,542 @@ +{ + "general": { + "fields": [ + { + "json_name": "max_output_tokens", + "go_name": "MaxOutputTokens", + "type": "integer", + "description": "Upper bound on tokens the model may generate", + "required": false, + "input_type": "input" + }, + { + "json_name": "temperature", + "go_name": "Temperature", + "type": "number", + "description": "Sampling temperature between 0 and 2", + "required": false, + "input_type": "input" + }, + { + "json_name": "top_p", + "go_name": "TopP", + "type": "number", + "description": "Nucleus sampling probability cutoff", + "required": false, + "input_type": "input" + }, + { + "json_name": "top_k", + "go_name": "TopK", + "type": "integer", + "description": "Number of highest-probability tokens to keep for sampling", + "required": false, + "input_type": "input" + }, + { + "json_name": "presence_penalty", + "go_name": "PresencePenalty", + "type": "number", + "description": "Penalty for tokens that have already appeared in the output", + "required": false, + "input_type": "input" + }, + { + "json_name": "frequency_penalty", + "go_name": "FrequencyPenalty", + "type": "number", + "description": "Penalty for tokens based on their frequency in the output", + "required": false, + "input_type": "input" + } + ] + }, + "providers": { + "anthropic": { + "fields": [ + { + "json_name": "send_reasoning", + "go_name": "SendReasoning", + "type": "boolean", + "description": "Whether to include reasoning content in the response", + "required": false, + "input_type": "select" + }, + { + "json_name": "thinking.budget_tokens", + "go_name": "Thinking.BudgetTokens", + "type": "integer", + "description": "Maximum number of tokens the model may use for thinking", + "required": false, + "input_type": "input" + }, + { + "json_name": "effort", + "go_name": "Effort", + "type": "string", + "description": "Controls the level of reasoning effort", + "required": false, + "enum": ["low", "medium", "high", "max"], + "input_type": "select" + }, + { + "json_name": "disable_parallel_tool_use", + "go_name": "DisableParallelToolUse", + "type": "boolean", + "description": "Whether to disable parallel tool execution", + "required": false, + "input_type": "select" + } + ] + }, + "google": { + "fields": [ + { + "json_name": "thinking_config.thinking_budget", + "go_name": "ThinkingConfig.ThinkingBudget", + "type": "integer", + "description": "Maximum number of tokens the model may use for thinking", + "required": false, + "input_type": "input" + }, + { + "json_name": "thinking_config.include_thoughts", + "go_name": "ThinkingConfig.IncludeThoughts", + "type": "boolean", + "description": "Whether to include thinking content in the response", + "required": false, + "input_type": "select" + }, + { + "json_name": "cached_content", + "go_name": "CachedContent", + "type": "string", + "description": "Resource name of a cached content object", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "safety_settings", + "go_name": "SafetySettings", + "type": "array", + "description": "Safety filtering settings for harmful content categories", + "required": false, + "input_type": "json", + "hidden": true + }, + { + "json_name": "threshold", + "go_name": "Threshold", + "type": "string", + "required": false, + "input_type": "input", + "hidden": true + } + ] + }, + "openai": { + "fields": [ + { + "json_name": "include", + "go_name": "Include", + "type": "array", + "description": "Model names to include in discovery", + "required": false, + "input_type": "json", + "hidden": true + }, + { + "json_name": "instructions", + "go_name": "Instructions", + "type": "string", + "description": "System-level instructions prepended to the conversation", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "logit_bias", + "go_name": "LogitBias", + "type": "object", + "description": "Token IDs mapped to bias values from -100 to 100", + "required": false, + "input_type": "json", + "hidden": true + }, + { + "json_name": "log_probs", + "go_name": "LogProbs", + "type": "boolean", + "description": "Whether to return log probabilities of output tokens", + "required": false, + "input_type": "select", + "hidden": true + }, + { + "json_name": "top_log_probs", + "go_name": "TopLogProbs", + "type": "integer", + "description": "Number of most likely tokens to return log probabilities for", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "max_tool_calls", + "go_name": "MaxToolCalls", + "type": "integer", + "description": "Maximum number of tool calls per response", + "required": false, + "input_type": "input" + }, + { + "json_name": "parallel_tool_calls", + "go_name": "ParallelToolCalls", + "type": "boolean", + "description": "Whether the model may make multiple tool calls in parallel", + "required": false, + "input_type": "select" + }, + { + "json_name": "user", + "go_name": "User", + "type": "string", + "description": "Unique identifier for the end user for abuse monitoring", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "reasoning_effort", + "go_name": "ReasoningEffort", + "type": "string", + "description": "Controls the level of reasoning effort", + "required": false, + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"], + "input_type": "select" + }, + { + "json_name": "reasoning_summary", + "go_name": "ReasoningSummary", + "type": "string", + "description": "Controls whether reasoning tokens are summarized in the response", + "required": false, + "input_type": "input" + }, + { + "json_name": "max_completion_tokens", + "go_name": "MaxCompletionTokens", + "type": "integer", + "description": "Upper bound on tokens the model may generate", + "required": false, + "input_type": "input" + }, + { + "json_name": "text_verbosity", + "go_name": "TextVerbosity", + "type": "string", + "description": "Controls the verbosity of the text response", + "required": false, + "enum": ["low", "medium", "high"], + "input_type": "select" + }, + { + "json_name": "prediction", + "go_name": "Prediction", + "type": "object", + "description": "Predicted output content to speed up responses", + "required": false, + "input_type": "json", + "hidden": true + }, + { + "json_name": "store", + "go_name": "Store", + "type": "boolean", + "description": "Whether to store the output for model distillation or evals", + "required": false, + "input_type": "select", + "hidden": true + }, + { + "json_name": "metadata", + "go_name": "Metadata", + "type": "object", + "description": "Arbitrary metadata to attach to the request", + "required": false, + "input_type": "json", + "hidden": true + }, + { + "json_name": "prompt_cache_key", + "go_name": "PromptCacheKey", + "type": "string", + "description": "Key for enabling cross-request prompt caching", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "safety_identifier", + "go_name": "SafetyIdentifier", + "type": "string", + "description": "Developer-specific safety identifier for the request", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "service_tier", + "go_name": "ServiceTier", + "type": "string", + "description": "Latency tier to use for processing the request", + "required": false, + "input_type": "input" + }, + { + "json_name": "structured_outputs", + "go_name": "StructuredOutputs", + "type": "boolean", + "description": "Whether to enable structured JSON output mode", + "required": false, + "input_type": "select", + "hidden": true + }, + { + "json_name": "strict_json_schema", + "go_name": "StrictJSONSchema", + "type": "boolean", + "description": "Whether to enforce strict adherence to the JSON schema", + "required": false, + "input_type": "select", + "hidden": true + } + ] + }, + "openaicompat": { + "fields": [ + { + "json_name": "user", + "go_name": "User", + "type": "string", + "description": "Unique identifier for the end user for abuse monitoring", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "reasoning_effort", + "go_name": "ReasoningEffort", + "type": "string", + "description": "Controls the level of reasoning effort", + "required": false, + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"], + "input_type": "select" + } + ] + }, + "openrouter": { + "fields": [ + { + "json_name": "reasoning.enabled", + "go_name": "Reasoning.Enabled", + "type": "boolean", + "description": "Whether reasoning is enabled", + "required": false, + "input_type": "select" + }, + { + "json_name": "reasoning.exclude", + "go_name": "Reasoning.Exclude", + "type": "boolean", + "description": "Whether to exclude reasoning content from the response", + "required": false, + "input_type": "select" + }, + { + "json_name": "reasoning.max_tokens", + "go_name": "Reasoning.MaxTokens", + "type": "integer", + "description": "Maximum number of tokens for reasoning output", + "required": false, + "input_type": "input" + }, + { + "json_name": "reasoning.effort", + "go_name": "Reasoning.Effort", + "type": "string", + "description": "Controls the level of reasoning effort", + "required": false, + "enum": ["low", "medium", "high"], + "input_type": "select" + }, + { + "json_name": "extra_body", + "go_name": "ExtraBody", + "type": "object", + "description": "Additional fields to include in the request body", + "required": false, + "input_type": "json", + "hidden": true + }, + { + "json_name": "include_usage", + "go_name": "IncludeUsage", + "type": "boolean", + "description": "Whether to include token usage information in the response", + "required": false, + "input_type": "select", + "hidden": true + }, + { + "json_name": "logit_bias", + "go_name": "LogitBias", + "type": "object", + "description": "Token IDs mapped to bias values from -100 to 100", + "required": false, + "input_type": "json", + "hidden": true + }, + { + "json_name": "log_probs", + "go_name": "LogProbs", + "type": "boolean", + "description": "Whether to return log probabilities of output tokens", + "required": false, + "input_type": "select", + "hidden": true + }, + { + "json_name": "parallel_tool_calls", + "go_name": "ParallelToolCalls", + "type": "boolean", + "description": "Whether the model may make multiple tool calls in parallel", + "required": false, + "input_type": "select" + }, + { + "json_name": "user", + "go_name": "User", + "type": "string", + "description": "Unique identifier for the end user for abuse monitoring", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "provider", + "go_name": "Provider", + "type": "string", + "description": "Routing preferences for provider selection", + "required": false, + "input_type": "input", + "hidden": true + } + ] + }, + "vercel": { + "fields": [ + { + "json_name": "reasoning.enabled", + "go_name": "Reasoning.Enabled", + "type": "boolean", + "description": "Whether reasoning is enabled", + "required": false, + "input_type": "select" + }, + { + "json_name": "reasoning.max_tokens", + "go_name": "Reasoning.MaxTokens", + "type": "integer", + "description": "Maximum number of tokens for reasoning output", + "required": false, + "input_type": "input" + }, + { + "json_name": "reasoning.effort", + "go_name": "Reasoning.Effort", + "type": "string", + "description": "Controls the level of reasoning effort", + "required": false, + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"], + "input_type": "select" + }, + { + "json_name": "reasoning.exclude", + "go_name": "Reasoning.Exclude", + "type": "boolean", + "description": "Whether to exclude reasoning content from the response", + "required": false, + "input_type": "select" + }, + { + "json_name": "providerOptions", + "go_name": "ProviderOptions", + "type": "string", + "description": "Gateway routing options for provider selection", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "user", + "go_name": "User", + "type": "string", + "description": "Unique identifier for the end user for abuse monitoring", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "logit_bias", + "go_name": "LogitBias", + "type": "object", + "description": "Token IDs mapped to bias values from -100 to 100", + "required": false, + "input_type": "json", + "hidden": true + }, + { + "json_name": "logprobs", + "go_name": "LogProbs", + "type": "boolean", + "description": "Whether to return log probabilities of output tokens", + "required": false, + "input_type": "select", + "hidden": true + }, + { + "json_name": "top_logprobs", + "go_name": "TopLogProbs", + "type": "integer", + "description": "Number of most likely tokens to return log probabilities for", + "required": false, + "input_type": "input", + "hidden": true + }, + { + "json_name": "parallel_tool_calls", + "go_name": "ParallelToolCalls", + "type": "boolean", + "description": "Whether the model may make multiple tool calls in parallel", + "required": false, + "input_type": "select" + }, + { + "json_name": "extra_body", + "go_name": "ExtraBody", + "type": "object", + "description": "Additional fields to include in the request body", + "required": false, + "input_type": "json", + "hidden": true + } + ] + } + }, + "provider_aliases": { + "azure": "openai", + "bedrock": "anthropic" + } +}