fix: show Anthropic Opus 4.7+ thinking (#26026)

## Summary

- Updates Coder's pinned `github.com/coder/fantasy` fork to include
coder/fantasy#39.
- Exposes Anthropic `thinking_display` as a typed chat model provider
option with `summarized` and `omitted` values.
- Validates configured `thinking_display` values and maps them to
`fantasyanthropic.ProviderOptions.ThinkingDisplay`.
- Regenerates the API/UI option schemas so the admin model config form
gets a generated select field.

## Tests

- `go mod tidy`
- `make gen`
- `go test ./codersdk ./coderd/x/chatd/chatprovider ./coderd -run
'TestChatModelProviderOptions|TestAnthropicThinkingDisplayFromChat|TestProviderOptionsFromChatModelConfig_AnthropicThinkingDisplay|TestMergeMissingProviderOptions_AnthropicThinkingDisplay|TestValidateChatModelProviderOptions_AnthropicThinkingDisplay'`
- `go test ./coderd/x/chatd/... ./codersdk`
- `go test ./coderd -run
'TestValidateChatModelProviderOptions_AnthropicThinkingDisplay'`
- `pnpm --dir site exec -- biome lint --error-on-warnings
src/api/chatModelOptionsGenerated.json src/api/typesGenerated.ts`
- pre-commit hook, including fmt, lint, and slim build

> Mux working on behalf of Mike.
This commit is contained in:
Michael Suchacz
2026-06-05 08:37:54 +02:00
committed by GitHub
parent 5578ac5f3d
commit c4792cf104
10 changed files with 174 additions and 8 deletions
+13 -1
View File
@@ -7469,7 +7469,19 @@ func validateChatModelCallConfig(modelConfig *codersdk.ChatModelCallConfig) erro
}
}
return nil
return validateChatModelProviderOptions(modelConfig.ProviderOptions)
}
func validateChatModelProviderOptions(options *codersdk.ChatModelProviderOptions) error {
if options == nil || options.Anthropic == nil || options.Anthropic.ThinkingDisplay == nil {
return nil
}
if strings.TrimSpace(*options.Anthropic.ThinkingDisplay) == "" ||
chatprovider.AnthropicThinkingDisplayFromChat(options.Anthropic.ThinkingDisplay) != nil {
return nil
}
return xerrors.Errorf("provider_options.anthropic.thinking_display must be one of summarized, omitted")
}
func validateNonNegativeDecimalField(name string, value *decimal.Decimal) error {
+36
View File
@@ -9,6 +9,42 @@ import (
"github.com/coder/coder/v2/codersdk"
)
func TestValidateChatModelProviderOptions_AnthropicThinkingDisplay(t *testing.T) {
t.Parallel()
tests := []struct {
name string
display string
wantErr string
}{
{name: "Summarized", display: "summarized"},
{name: "Omitted", display: " omitted "},
{name: "Empty", display: " "},
{
name: "Invalid",
display: "summrized",
wantErr: "provider_options.anthropic.thinking_display must be one of summarized, omitted",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
display := tt.display
err := validateChatModelProviderOptions(&codersdk.ChatModelProviderOptions{
Anthropic: &codersdk.ChatModelAnthropicProviderOptions{
ThinkingDisplay: &display,
},
})
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestValidateChatModelConfigProviderModel(t *testing.T) {
t.Parallel()
@@ -771,6 +771,30 @@ func ReasoningEffortFromChat(provider string, value *string) *string {
}
}
// AnthropicThinkingDisplayFromChat normalizes chat-config thinking display
// values for Anthropic and returns the canonical provider display value.
func AnthropicThinkingDisplayFromChat(value *string) *fantasyanthropic.ThinkingDisplay {
if value == nil {
return nil
}
normalized := strings.ToLower(strings.TrimSpace(*value))
if normalized == "" {
return nil
}
display := chatutil.NormalizedEnumValue(
normalized,
string(fantasyanthropic.ThinkingDisplaySummarized),
string(fantasyanthropic.ThinkingDisplayOmitted),
)
if display == nil {
return nil
}
valueCopy := fantasyanthropic.ThinkingDisplay(*display)
return &valueCopy
}
// MergeMissingModelCostConfig fills unset pricing metadata from defaults.
func MergeMissingModelCostConfig(
dst **codersdk.ModelCostConfig,
@@ -919,6 +943,9 @@ func MergeMissingProviderOptions(
if dstAnthropic.Effort == nil {
dstAnthropic.Effort = defaultAnthropic.Effort
}
if dstAnthropic.ThinkingDisplay == nil {
dstAnthropic.ThinkingDisplay = defaultAnthropic.ThinkingDisplay
}
if dstAnthropic.DisableParallelToolUse == nil {
dstAnthropic.DisableParallelToolUse = defaultAnthropic.DisableParallelToolUse
}
@@ -1408,6 +1435,7 @@ func anthropicProviderOptionsFromChatConfig(
result := &fantasyanthropic.ProviderOptions{
SendReasoning: options.SendReasoning,
Effort: anthropicEffortFromChat(options.Effort),
ThinkingDisplay: AnthropicThinkingDisplayFromChat(options.ThinkingDisplay),
DisableParallelToolUse: options.DisableParallelToolUse,
}
if options.Thinking != nil && options.Thinking.BudgetTokens != nil {
@@ -371,6 +371,77 @@ func TestReasoningEffortFromChat(t *testing.T) {
}
}
func TestAnthropicThinkingDisplayFromChat(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input *string
want *fantasyanthropic.ThinkingDisplay
}{
{
name: "Summarized",
input: ptr.Ref(" SUMMARIZED "),
want: ptr.Ref(fantasyanthropic.ThinkingDisplaySummarized),
},
{
name: "Omitted",
input: ptr.Ref("omitted"),
want: ptr.Ref(fantasyanthropic.ThinkingDisplayOmitted),
},
{
name: "InvalidReturnsNil",
input: ptr.Ref("summary"),
},
{
name: "NilInputReturnsNil",
input: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := chatprovider.AnthropicThinkingDisplayFromChat(tt.input)
require.Equal(t, tt.want, got)
})
}
}
func TestProviderOptionsFromChatModelConfig_AnthropicThinkingDisplay(t *testing.T) {
t.Parallel()
providerOptions := chatprovider.ProviderOptionsFromChatModelConfig(nil, &codersdk.ChatModelProviderOptions{
Anthropic: &codersdk.ChatModelAnthropicProviderOptions{
ThinkingDisplay: ptr.Ref(" SUMMARIZED "),
},
})
require.NotNil(t, providerOptions)
anthropicOptions, ok := providerOptions[fantasyanthropic.Name].(*fantasyanthropic.ProviderOptions)
require.True(t, ok)
require.NotNil(t, anthropicOptions.ThinkingDisplay)
require.Equal(t, fantasyanthropic.ThinkingDisplaySummarized, *anthropicOptions.ThinkingDisplay)
}
func TestMergeMissingProviderOptions_AnthropicThinkingDisplay(t *testing.T) {
t.Parallel()
options := &codersdk.ChatModelProviderOptions{
Anthropic: &codersdk.ChatModelAnthropicProviderOptions{},
}
defaults := &codersdk.ChatModelProviderOptions{
Anthropic: &codersdk.ChatModelAnthropicProviderOptions{
ThinkingDisplay: ptr.Ref("summarized"),
},
}
chatprovider.MergeMissingProviderOptions(&options, defaults)
require.NotNil(t, options.Anthropic.ThinkingDisplay)
require.Equal(t, "summarized", *options.Anthropic.ThinkingDisplay)
}
func TestResolveUserProviderKeys_UnavailableReason(t *testing.T) {
t.Parallel()