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()
+1
View File
@@ -1229,6 +1229,7 @@ type ChatModelAnthropicProviderOptions struct {
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" label:"Reasoning Effort" description:"Controls the level of reasoning effort" enum:"low,medium,high,xhigh,max"`
ThinkingDisplay *string `json:"thinking_display,omitempty" label:"Thinking Display" description:"Controls how Anthropic returns thinking content" enum:"summarized,omitted"`
DisableParallelToolUse *bool `json:"disable_parallel_tool_use,omitempty" description:"Whether to disable parallel tool execution"`
WebSearchEnabled *bool `json:"web_search_enabled,omitempty" description:"Enable Anthropic web search tool for grounding responses with real-time information"`
AllowedDomains []string `json:"allowed_domains,omitempty" label:"Web Search: Allowed Domains" description:"Restrict web search to these domains (cannot be used with blocked_domains)"`
+9 -3
View File
@@ -24,11 +24,13 @@ func TestChatModelProviderOptions_MarshalJSON_UsesPlainProviderPayload(t *testin
sendReasoning := true
effort := "high"
thinkingDisplay := "summarized"
raw, err := json.Marshal(codersdk.ChatModelProviderOptions{
Anthropic: &codersdk.ChatModelAnthropicProviderOptions{
SendReasoning: &sendReasoning,
Effort: &effort,
SendReasoning: &sendReasoning,
Effort: &effort,
ThinkingDisplay: &thinkingDisplay,
},
})
require.NoError(t, err)
@@ -36,6 +38,7 @@ func TestChatModelProviderOptions_MarshalJSON_UsesPlainProviderPayload(t *testin
require.NotContains(t, string(raw), `"data":`)
require.Contains(t, string(raw), `"send_reasoning":true`)
require.Contains(t, string(raw), `"effort":"high"`)
require.Contains(t, string(raw), `"thinking_display":"summarized"`)
}
func TestChatModelProviderOptions_UnmarshalJSON_ParsesPlainProviderPayloads(t *testing.T) {
@@ -44,7 +47,8 @@ func TestChatModelProviderOptions_UnmarshalJSON_ParsesPlainProviderPayloads(t *t
raw := []byte(`{
"anthropic": {
"send_reasoning": true,
"effort": "high"
"effort": "high",
"thinking_display": "summarized"
}
}`)
@@ -60,6 +64,8 @@ func TestChatModelProviderOptions_UnmarshalJSON_ParsesPlainProviderPayloads(t *t
"high",
*decoded.Anthropic.Effort,
)
require.NotNil(t, decoded.Anthropic.ThinkingDisplay)
require.Equal(t, "summarized", *decoded.Anthropic.ThinkingDisplay)
}
func TestChatUsageLimitExceededFrom(t *testing.T) {
+3 -2
View File
@@ -94,8 +94,9 @@ replace github.com/spf13/afero => github.com/aslilac/afero v0.0.0-20250403163713
// emit a Base64 PDF document block for application/pdf FileParts on the
// Anthropic provider so user-uploaded PDFs actually reach Claude/Bedrock
// instead of being silently dropped.
// See: https://github.com/coder/fantasy/commits/7d46e640327a
replace charm.land/fantasy => github.com/coder/fantasy v0.0.0-20260602023814-7d46e640327a
// 11) coder/fantasy#39, support Anthropic thinking_display natively.
// See: https://github.com/coder/fantasy/commits/a2a3f2171ec8
replace charm.land/fantasy => github.com/coder/fantasy v0.0.0-20260604204802-a2a3f2171ec8
// coder/coder uses a fork of charmbracelet's fork of the Anthropic Go SDK
// with performance improvements and Bedrock header cleanup.
+2 -2
View File
@@ -324,8 +324,8 @@ github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwu
github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4=
github.com/coder/clistat v1.2.1 h1:P9/10njXMyj5cWzIU5wkRsSy5LVQH49+tcGMsAgWX0w=
github.com/coder/clistat v1.2.1/go.mod h1:m7SC0uj88eEERgvF8Kn6+w6XF21BeSr+15f7GoLAw0A=
github.com/coder/fantasy v0.0.0-20260602023814-7d46e640327a h1:ffQixHAwjJLHgFfe4rtrAsFNRGhEyWnBSpInnLIxDPo=
github.com/coder/fantasy v0.0.0-20260602023814-7d46e640327a/go.mod h1:wZ0e3lEPqrM0XiIdAUQLvMKCLYhc3gi96MRX2wjbX44=
github.com/coder/fantasy v0.0.0-20260604204802-a2a3f2171ec8 h1:+8QmiW3qKSqS4pkEQQbK7Rg3UGWnD/c5BXp1tPpX1sU=
github.com/coder/fantasy v0.0.0-20260604204802-a2a3f2171ec8/go.mod h1:RdKpE+blFnbGx4XmNc952AXAdBL1ZXg9iTnXHjdn9Bk=
github.com/coder/flog v1.1.0 h1:kbAes1ai8fIS5OeV+QAnKBQE22ty1jRF/mcAwHpLBa4=
github.com/coder/flog v1.1.0/go.mod h1:UQlQvrkJBvnRGo69Le8E24Tcl5SJleAAR7gYEHzAmdQ=
github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322 h1:m0lPZjlQ7vdVpRBPKfYIFlmgevoTkBxB10wv6l2gOaU=
@@ -112,6 +112,16 @@
"enum": ["low", "medium", "high", "xhigh", "max"],
"input_type": "select"
},
{
"json_name": "thinking_display",
"go_name": "ThinkingDisplay",
"type": "string",
"description": "Controls how Anthropic returns thinking content",
"label": "Thinking Display",
"required": false,
"enum": ["summarized", "omitted"],
"input_type": "select"
},
{
"json_name": "disable_parallel_tool_use",
"go_name": "DisableParallelToolUse",
+1
View File
@@ -2295,6 +2295,7 @@ export interface ChatModelAnthropicProviderOptions {
readonly send_reasoning?: boolean;
readonly thinking?: ChatModelAnthropicThinkingOptions;
readonly effort?: string;
readonly thinking_display?: string;
readonly disable_parallel_tool_use?: boolean;
readonly web_search_enabled?: boolean;
readonly allowed_domains?: readonly string[];