mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08: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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user