mirror of
https://github.com/coder/coder.git
synced 2026-06-06 06:28:20 +00:00
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:
+13
-1
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)"`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+1
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user