Files
coder/site/src/api/chatModelOptions.ts
T
Kyle Carberry 500fc5e2a4 feat: polish model config form UI (#24047)
Polishes the AI model configuration form (add/edit model) with tighter
layout and better input affordances.

**Frontend changes:**
- Replace "Unset" with "Default" in select dropdowns to communicate
system fallback
- Show pricing fields inline instead of behind a collapsible toggle
- Use flat section dividers (`border-t`) instead of bordered fieldsets
- Move field descriptions into info-icon tooltips to fix input
misalignment
- Add InputGroup adornments: `$` prefix + `/1M` suffix on pricing,
`tokens` suffix on token fields, `%` suffix on compression threshold,
range placeholders on temperature/penalty fields
- Shorter pricing labels (Input, Output, Cache Read, Cache Write)
- Compact JSON textareas (1-row height, resizable)
- Smart grid layouts by field type (3-col provider, 4-col pricing, 3-col
advanced)
- Boolean fields render as a segmented control (Default · On · Off)
instead of a dropdown

**Backend changes:**
- Add `enum` tags to OpenAI `service_tier`
(`auto,default,flex,scale,priority`) and `reasoning_summary`
(`auto,concise,detailed`) so they render as select dropdowns instead of
free-text inputs

> 🤖 Generated by Coder Agents
2026-04-06 10:49:42 -04:00

184 lines
5.9 KiB
TypeScript

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;
/** Optional display label override. When absent, derive from json_name. */
label?: 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<string, ProviderSchema>;
provider_aliases: Record<string, string>;
}
/** The imported schema, typed as {@link ModelOptionsSchema}. */
export const modelOptionsSchema: ModelOptionsSchema =
schema as ModelOptionsSchema;
const syntheticGeneralFields: FieldSchema[] = [
{
json_name: "cost.input_price_per_million_tokens",
go_name: "Cost.InputPricePerMillionTokens",
type: "number",
description: "Input token price in USD per 1M tokens",
required: false,
input_type: "input",
},
{
json_name: "cost.output_price_per_million_tokens",
go_name: "Cost.OutputPricePerMillionTokens",
type: "number",
description: "Output token price in USD per 1M tokens",
required: false,
input_type: "input",
},
{
json_name: "cost.cache_read_price_per_million_tokens",
go_name: "Cost.CacheReadPricePerMillionTokens",
type: "number",
description: "Cache read token price in USD per 1M tokens",
required: false,
input_type: "input",
},
{
json_name: "cost.cache_write_price_per_million_tokens",
go_name: "Cost.CacheWritePricePerMillionTokens",
type: "number",
description:
"Cache write or cache creation token price in USD per 1M tokens",
required: false,
input_type: "input",
},
];
/**
* Get the general (provider-independent) fields such as temperature
* and max_output_tokens.
*/
export function getGeneralFields(): FieldSchema[] {
const fields = [...modelOptionsSchema.general.fields];
for (const field of syntheticGeneralFields) {
if (!fields.some((existing) => existing.json_name === field.json_name)) {
fields.push(field);
}
}
return 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.
*/
export 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);
}