From 1ac106255b562b3896dfe7bf1e3b1498fefd0fce Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:04:13 +0200 Subject: [PATCH] feat: add Anthropic 1M context window toggle for Agents model configs (#27257) --- coderd/x/chatd/chatd.go | 45 +++--- coderd/x/chatd/chatd_test.go | 57 ++++++++ coderd/x/chatd/chatprovider/chatprovider.go | 24 ++++ .../x/chatd/chatprovider/chatprovider_test.go | 135 ++++++++++++++++++ coderd/x/chatd/model_routing.go | 4 + coderd/x/chatd/model_routing_aibridge.go | 35 ++++- coderd/x/chatd/title_override.go | 9 +- codersdk/chats.go | 1 + docs/ai-coder/agents/models.md | 9 +- site/src/api/chatModelOptionsGenerated.json | 9 ++ site/src/api/typesGenerated.ts | 1 + .../modelConfigFormLogic.test.ts | 27 ++++ 12 files changed, 327 insertions(+), 29 deletions(-) diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go index bdf24fb71d..fa2012b2ad 100644 --- a/coderd/x/chatd/chatd.go +++ b/coderd/x/chatd/chatd.go @@ -338,10 +338,11 @@ func (p *Server) resolveAdvisorModelOverride( return fallbackModel, fallbackCallConfig, nil } overrideModel, err := p.newModel(ctx, modelClientRequest{ - Chat: chat, - ModelName: overrideConfig.Model, - UserAgent: chatprovider.UserAgent(), - ExtraHeaders: chatprovider.CoderHeaders(chat), + Chat: chat, + ModelName: overrideConfig.Model, + UserAgent: chatprovider.UserAgent(), + ExtraHeaders: chatprovider.CoderHeaders(chat), + ConfigOptions: overrideConfig.Options, }, route, modelOpts) if err != nil { if overrideConfig.AIProviderID.Valid { @@ -2397,10 +2398,11 @@ func (p *Server) prepareManualTitleDebugRun( debugModelErr = routeErr } else { debugModel, debugModelErr = p.newModel(ctx, modelClientRequest{ - Chat: chat, - ModelName: modelConfig.Model, - UserAgent: chatprovider.UserAgent(), - ExtraHeaders: chatprovider.CoderHeaders(chat), + Chat: chat, + ModelName: modelConfig.Model, + UserAgent: chatprovider.UserAgent(), + ExtraHeaders: chatprovider.CoderHeaders(chat), + ConfigOptions: modelConfig.Options, }, route, debugOpts) } switch { @@ -2591,10 +2593,11 @@ func (p *Server) resolveManualTitleModel( return p.resolveFallbackManualTitleModel(ctx, chat, modelOpts) } model, err := p.newModel(ctx, modelClientRequest{ - Chat: chat, - ModelName: config.Model, - UserAgent: chatprovider.UserAgent(), - ExtraHeaders: chatprovider.CoderHeaders(chat), + Chat: chat, + ModelName: config.Model, + UserAgent: chatprovider.UserAgent(), + ExtraHeaders: chatprovider.CoderHeaders(chat), + ConfigOptions: config.Options, }, route, modelOpts) if err != nil { p.logger.Debug(ctx, "manual title preferred model unavailable", @@ -2625,10 +2628,11 @@ func (p *Server) resolveFallbackManualTitleModel( return nil, database.ChatModelConfig{}, err } model, err := p.newModel(ctx, modelClientRequest{ - Chat: chat, - ModelName: config.Model, - UserAgent: chatprovider.UserAgent(), - ExtraHeaders: chatprovider.CoderHeaders(chat), + Chat: chat, + ModelName: config.Model, + UserAgent: chatprovider.UserAgent(), + ExtraHeaders: chatprovider.CoderHeaders(chat), + ConfigOptions: config.Options, }, route, modelOpts) if err != nil { return nil, database.ChatModelConfig{}, xerrors.Errorf( @@ -3913,10 +3917,11 @@ func (p *Server) resolveChatModel( } model, debugEnabled, err = p.newDebugAwareModel(ctx, modelClientRequest{ - Chat: chat, - ModelName: dbConfig.Model, - UserAgent: chatprovider.UserAgent(), - ExtraHeaders: chatprovider.CoderHeaders(chat), + Chat: chat, + ModelName: dbConfig.Model, + UserAgent: chatprovider.UserAgent(), + ExtraHeaders: chatprovider.CoderHeaders(chat), + ConfigOptions: dbConfig.Options, }, route, modelOpts) if err != nil { return nil, database.ChatModelConfig{}, aiGatewayModelRoute{}, false, "", "", xerrors.Errorf( diff --git a/coderd/x/chatd/chatd_test.go b/coderd/x/chatd/chatd_test.go index ddef195736..53ca0b9078 100644 --- a/coderd/x/chatd/chatd_test.go +++ b/coderd/x/chatd/chatd_test.go @@ -53,6 +53,7 @@ import ( "github.com/coder/coder/v2/coderd/x/chatd/chatadvisor" "github.com/coder/coder/v2/coderd/x/chatd/chatdebug" "github.com/coder/coder/v2/coderd/x/chatd/chatprompt" + "github.com/coder/coder/v2/coderd/x/chatd/chatprovider" "github.com/coder/coder/v2/coderd/x/chatd/chatsanitize" "github.com/coder/coder/v2/coderd/x/chatd/chatstate" "github.com/coder/coder/v2/coderd/x/chatd/chattest" @@ -7118,6 +7119,62 @@ func TestActiveServer_AnthropicSanitizesProviderToolBeforeRequest(t *testing.T) require.Contains(t, body, "redacted-payload") } +func TestActiveServer_AnthropicContext1MBetaHeader(t *testing.T) { + t.Parallel() + + runChat := func(t *testing.T, callConfig *codersdk.ChatModelCallConfig) []chattest.AnthropicRequest { + t.Helper() + ctx := testutil.Context(t, testutil.WaitLong) + db, ps := dbtestutil.NewDB(t) + requests := newAnthropicRequestRecorder() + anthropicURL := chattest.NewAnthropic(t, func(req *chattest.AnthropicRequest) chattest.AnthropicResponse { + requests.record(req) + if !req.Stream { + return chattest.AnthropicNonStreamingResponse("title") + } + return chattest.AnthropicStreamingResponse(chattest.AnthropicTextChunks("done")...) + }) + user, org, model := seedAnthropicChatDependencies(t, db, anthropicURL) + if callConfig != nil { + model = updateChatModelCallConfig(t, db, model, *callConfig) + } + + server := newActiveTestServer(t, db, ps, func(cfg *chatd.Config) { + cfg.AIBridgeTransportFactory = chatAIGatewayTransportFactoryPointer(chattest.NewMockAIBridgeTransport(t, anthropicURL, chattest.WithPreservePath())) + }) + chat := createChatThroughServer(ctx, t, db, server, org.ID, user.ID, model.ID, "hello") + waitForChatStatus(ctx, t, db, chat.ID, database.ChatStatusWaiting) + + generationRequests := filterAnthropicStreamingRequests(requests.all()) + require.NotEmpty(t, generationRequests) + return generationRequests + } + + t.Run("enabled sends beta header", func(t *testing.T) { + t.Parallel() + + generationRequests := runChat(t, &codersdk.ChatModelCallConfig{ + ProviderOptions: &codersdk.ChatModelProviderOptions{ + Anthropic: &codersdk.ChatModelAnthropicProviderOptions{ + Context1MEnabled: ptr.Ref(true), + }, + }, + }) + for _, req := range generationRequests { + require.Equal(t, chatprovider.AnthropicBetaContext1M, req.Header.Get(chatprovider.HeaderAnthropicBeta)) + } + }) + + t.Run("unset omits beta header", func(t *testing.T) { + t.Parallel() + + generationRequests := runChat(t, nil) + for _, req := range generationRequests { + require.Empty(t, req.Header.Get(chatprovider.HeaderAnthropicBeta)) + } + }) +} + func TestActiveServer_AnthropicProviderToolPreRequestGuard(t *testing.T) { t.Parallel() diff --git a/coderd/x/chatd/chatprovider/chatprovider.go b/coderd/x/chatd/chatprovider/chatprovider.go index e9f466f4ed..ad450d6a54 100644 --- a/coderd/x/chatd/chatprovider/chatprovider.go +++ b/coderd/x/chatd/chatprovider/chatprovider.go @@ -874,6 +874,30 @@ func CoderHeaders(chat database.Chat) map[string]string { return h } +// AnthropicBetaContext1M is the beta token for Anthropic's 1M context window. +const AnthropicBetaContext1M = "context-1m-2025-08-07" + +// HeaderAnthropicBeta names Anthropic's beta feature header. +const HeaderAnthropicBeta = "Anthropic-Beta" + +// BetaHeadersFromCallConfig returns beta feature headers for Anthropic and +// Bedrock calls. +func BetaHeadersFromCallConfig(providerName string, config *codersdk.ChatModelCallConfig) map[string]string { + if config == nil || config.ProviderOptions == nil || config.ProviderOptions.Anthropic == nil { + return nil + } + enabled := config.ProviderOptions.Anthropic.Context1MEnabled + if enabled == nil || !*enabled { + return nil + } + switch NormalizeProvider(providerName) { + case fantasyanthropic.Name, fantasybedrock.Name: + return map[string]string{HeaderAnthropicBeta: AnthropicBetaContext1M} + default: + return nil + } +} + // ModelFromConfig resolves a provider/model pair and constructs a fantasy // language model client using the provided provider credentials. The // userAgent is sent as the User-Agent header on every outgoing LLM diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index 9961eb1f1c..b1b2a37a88 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -1,6 +1,7 @@ package chatprovider_test import ( + "context" "encoding/base64" "encoding/json" "io" @@ -1375,6 +1376,140 @@ func TestModelFromConfig_ExtraHeaders(t *testing.T) { }) } +func TestBetaHeadersFromCallConfig(t *testing.T) { + t.Parallel() + + configWith1M := func(enabled *bool) *codersdk.ChatModelCallConfig { + return &codersdk.ChatModelCallConfig{ + ProviderOptions: &codersdk.ChatModelProviderOptions{ + Anthropic: &codersdk.ChatModelAnthropicProviderOptions{ + Context1MEnabled: enabled, + }, + }, + } + } + beta := map[string]string{ + chatprovider.HeaderAnthropicBeta: chatprovider.AnthropicBetaContext1M, + } + + tests := []struct { + name string + provider string + config *codersdk.ChatModelCallConfig + want map[string]string + }{ + {name: "NilConfig", provider: fantasyanthropic.Name, config: nil, want: nil}, + {name: "NilProviderOptions", provider: fantasyanthropic.Name, config: &codersdk.ChatModelCallConfig{}, want: nil}, + {name: "NilAnthropicOptions", provider: fantasyanthropic.Name, config: &codersdk.ChatModelCallConfig{ProviderOptions: &codersdk.ChatModelProviderOptions{}}, want: nil}, + {name: "Unset", provider: fantasyanthropic.Name, config: configWith1M(nil), want: nil}, + {name: "Disabled", provider: fantasyanthropic.Name, config: configWith1M(ptr.Ref(false)), want: nil}, + {name: "EnabledAnthropic", provider: fantasyanthropic.Name, config: configWith1M(ptr.Ref(true)), want: beta}, + {name: "EnabledBedrock", provider: fantasybedrock.Name, config: configWith1M(ptr.Ref(true)), want: beta}, + {name: "EnabledOpenAI", provider: fantasyopenai.Name, config: configWith1M(ptr.Ref(true)), want: nil}, + {name: "EnabledUnknownProvider", provider: "does-not-exist", config: configWith1M(ptr.Ref(true)), want: nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, chatprovider.BetaHeadersFromCallConfig(tt.provider, tt.config)) + }) + } +} + +func generateHello(ctx context.Context, model fantasy.LanguageModel) error { + _, err := model.Generate(ctx, fantasy.Call{ + Prompt: []fantasy.Message{ + { + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{fantasy.TextPart{Text: "hello"}}, + }, + }, + }) + return err +} + +func TestModelFromConfig_AnthropicBetaExtraHeader(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + called := make(chan struct{}) + serverURL := chattest.NewAnthropic(t, func(req *chattest.AnthropicRequest) chattest.AnthropicResponse { + assert.Equal(t, chatprovider.AnthropicBetaContext1M, req.Header.Get(chatprovider.HeaderAnthropicBeta)) + close(called) + return chattest.AnthropicNonStreamingResponse("hello") + }) + + keys := chatprovider.ProviderAPIKeys{ + ByProvider: map[string]string{fantasyanthropic.Name: "test-key"}, + BaseURLByProvider: map[string]string{fantasyanthropic.Name: serverURL}, + } + betaHeaders := map[string]string{ + chatprovider.HeaderAnthropicBeta: chatprovider.AnthropicBetaContext1M, + } + model, err := chatprovider.ModelFromConfig(fantasyanthropic.Name, "claude-sonnet-4-20250514", keys, chatprovider.UserAgent(), betaHeaders, nil) + require.NoError(t, err) + + require.NoError(t, generateHello(ctx, model)) + _ = testutil.TryReceive(ctx, t, called) +} + +// The Anthropic SDK's Bedrock middleware moves Anthropic-Beta into the +// request body before SigV4 signing. +func TestModelFromConfig_BedrockBetaExtraHeader(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + + t.Setenv("AWS_ACCESS_KEY_ID", "test-access-key") + t.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key") + t.Setenv("AWS_SESSION_TOKEN", "test-session-token") + + type requestCapture struct { + AnthropicBeta string + Authorization string + Body string + ReadError error + } + + requests := make(chan requestCapture, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + + requests <- requestCapture{ + AnthropicBeta: r.Header.Get(chatprovider.HeaderAnthropicBeta), + Authorization: r.Header.Get("Authorization"), + Body: string(body), + ReadError: err, + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(bedrockNonStreamingResponse()) + })) + defer server.Close() + + model, err := chatprovider.ModelFromConfig( + fantasybedrock.Name, + "anthropic.claude-opus-4-6-v1", + chatprovider.ProviderAPIKeys{ + ByProvider: map[string]string{fantasybedrock.Name: ""}, + BaseURLByProvider: map[string]string{fantasybedrock.Name: server.URL}, + RegionByProvider: map[string]string{fantasybedrock.Name: "us-east-2"}, + }, + chatprovider.UserAgent(), + map[string]string{ + chatprovider.HeaderAnthropicBeta: chatprovider.AnthropicBetaContext1M, + }, + nil, + ) + require.NoError(t, err) + + require.NoError(t, generateHello(ctx, model)) + + got := testutil.TryReceive(ctx, t, requests) + require.NoError(t, got.ReadError) + require.Empty(t, got.AnthropicBeta) + require.Contains(t, got.Authorization, "AWS4-HMAC-SHA256") + require.Contains(t, got.Body, `"anthropic_beta":["context-1m-2025-08-07"]`) +} + // TestModelFromConfig_AnthropicPDFFilePartReachesProvider pins the end-to-end // path that lets a user-uploaded PDF actually reach Claude/Bedrock: a // fantasy.FilePart with MediaType "application/pdf" must be serialized as an diff --git a/coderd/x/chatd/model_routing.go b/coderd/x/chatd/model_routing.go index 8e57e78598..8678dffea8 100644 --- a/coderd/x/chatd/model_routing.go +++ b/coderd/x/chatd/model_routing.go @@ -2,6 +2,7 @@ package chatd import ( "context" + "encoding/json" "net/http" "charm.land/fantasy" @@ -18,6 +19,9 @@ type modelClientRequest struct { ModelName string UserAgent string ExtraHeaders map[string]string + // ConfigOptions holds the model config row's Options JSONB; empty for + // paths without a config row. + ConfigOptions json.RawMessage } type modelBuildOptions struct { diff --git a/coderd/x/chatd/model_routing_aibridge.go b/coderd/x/chatd/model_routing_aibridge.go index 3179ab4ea5..2bd724869a 100644 --- a/coderd/x/chatd/model_routing_aibridge.go +++ b/coderd/x/chatd/model_routing_aibridge.go @@ -3,6 +3,7 @@ package chatd import ( "context" "database/sql" + "encoding/json" "net/http" "strings" @@ -166,16 +167,48 @@ func (p *Server) newModel( } config := fantasyConfigForAIBridge(route.Provider.Type) + extraHeaders, err := mergeConfigBetaHeaders(req.ExtraHeaders, config.ProviderHint, req.ConfigOptions) + if err != nil { + return nil, err + } return newLanguageModel( config.ProviderHint, req.ModelName, config.Keys, req.UserAgent, - req.ExtraHeaders, + extraHeaders, &http.Client{Transport: baseRT}, ) } +// mergeConfigBetaHeaders never mutates extraHeaders; existing entries win +// over config-derived ones. +func mergeConfigBetaHeaders( + extraHeaders map[string]string, + providerHint string, + configOptions json.RawMessage, +) (map[string]string, error) { + if len(configOptions) == 0 { + return extraHeaders, nil + } + var callConfig codersdk.ChatModelCallConfig + if err := json.Unmarshal(configOptions, &callConfig); err != nil { + return nil, xerrors.Errorf("parse model config options: %w", err) + } + betaHeaders := chatprovider.BetaHeadersFromCallConfig(providerHint, &callConfig) + if len(betaHeaders) == 0 { + return extraHeaders, nil + } + merged := make(map[string]string, len(extraHeaders)+len(betaHeaders)) + for name, value := range betaHeaders { + merged[name] = value + } + for name, value := range extraHeaders { + merged[name] = value + } + return merged, nil +} + type aibridgeFantasyConfig struct { ProviderHint string Keys chatprovider.ProviderAPIKeys diff --git a/coderd/x/chatd/title_override.go b/coderd/x/chatd/title_override.go index a4cfb4de8f..9abe961023 100644 --- a/coderd/x/chatd/title_override.go +++ b/coderd/x/chatd/title_override.go @@ -95,10 +95,11 @@ func (p *Server) resolveTitleGenerationModelOverride( return database.ChatModelConfig{}, nil, aiGatewayModelRoute{}, true, err } model, err := p.newModel(ctx, modelClientRequest{ - Chat: chat, - ModelName: modelConfig.Model, - UserAgent: chatprovider.UserAgent(), - ExtraHeaders: chatprovider.CoderHeaders(chat), + Chat: chat, + ModelName: modelConfig.Model, + UserAgent: chatprovider.UserAgent(), + ExtraHeaders: chatprovider.CoderHeaders(chat), + ConfigOptions: modelConfig.Options, }, route, modelOpts) if err != nil { return database.ChatModelConfig{}, nil, aiGatewayModelRoute{}, true, xerrors.Errorf( diff --git a/codersdk/chats.go b/codersdk/chats.go index aea0e0b4d7..f28ef011ee 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -1354,6 +1354,7 @@ type ChatModelAnthropicProviderOptions struct { 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)" visible_when:"web_search_enabled" conflicts_with:"blocked_domains"` BlockedDomains []string `json:"blocked_domains,omitempty" label:"Web Search: Blocked Domains" description:"Block web search on these domains (cannot be used with allowed_domains)" visible_when:"web_search_enabled" conflicts_with:"allowed_domains"` + Context1MEnabled *bool `json:"context_1m_enabled,omitempty" label:"1M Context Window" description:"Send the anthropic-beta context-1m-2025-08-07 header to unlock the 1M token context window on supported Claude models. Pair with a matching Context Limit. Long-context pricing and higher latency may apply above 200K tokens."` } // ChatModelGoogleThinkingConfig configures Google thinking behavior. diff --git a/docs/ai-coder/agents/models.md b/docs/ai-coder/agents/models.md index ccdc6849b3..982682890b 100644 --- a/docs/ai-coder/agents/models.md +++ b/docs/ai-coder/agents/models.md @@ -188,10 +188,11 @@ fields appear dynamically in the admin UI when you select a provider. #### Anthropic -| Option | Description | -|------------------------|------------------------------------------------------------------| -| Thinking Budget Tokens | Maximum tokens allocated for extended thinking. | -| Effort | Thinking effort level (`low`, `medium`, `high`, `xhigh`, `max`). | +| Option | Description | +|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Thinking Budget Tokens | Maximum tokens allocated for extended thinking. | +| Effort | Thinking effort level (`low`, `medium`, `high`, `xhigh`, `max`). | +| 1M Context Window | Sends the `anthropic-beta: context-1m-2025-08-07` header to unlock the 1M token context window on supported Claude models. Pair it with a raised Context Limit, which still controls compaction. Long-context pricing may apply. | #### OpenAI diff --git a/site/src/api/chatModelOptionsGenerated.json b/site/src/api/chatModelOptionsGenerated.json index e5c723027c..022202d5fb 100644 --- a/site/src/api/chatModelOptionsGenerated.json +++ b/site/src/api/chatModelOptionsGenerated.json @@ -169,6 +169,15 @@ "input_type": "json", "visible_when": "web_search_enabled", "conflicts_with": ["allowed_domains"] + }, + { + "json_name": "context_1m_enabled", + "go_name": "Context1MEnabled", + "type": "boolean", + "description": "Send the anthropic-beta context-1m-2025-08-07 header to unlock the 1M token context window on supported Claude models. Pair with a matching Context Limit. Long-context pricing and higher latency may apply above 200K tokens.", + "label": "1M Context Window", + "required": false, + "input_type": "select" } ] }, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index da54ce2ce0..c21a3338a6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2526,6 +2526,7 @@ export interface ChatModelAnthropicProviderOptions { readonly web_search_enabled?: boolean; readonly allowed_domains?: readonly string[]; readonly blocked_domains?: readonly string[]; + readonly context_1m_enabled?: boolean; } // From codersdk/chats.go diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/modelConfigFormLogic.test.ts b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/modelConfigFormLogic.test.ts index 2d8bd15c53..67bb3bc330 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/modelConfigFormLogic.test.ts +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/modelConfigFormLogic.test.ts @@ -324,6 +324,22 @@ describe("extractModelConfigFormState", () => { expect(anthropic.disableParallelToolUse).toBe("false"); }); + it("extracts Anthropic 1M context window option", () => { + const model: TypesGen.ChatModelConfig = { + ...baseChatModelConfig, + model_config: { + provider_options: { + anthropic: { + context_1m_enabled: true, + }, + }, + }, + }; + const result = extractModelConfigFormState(model); + const anthropic = result.anthropic as Record; + expect(anthropic.context1mEnabled).toBe("true"); + }); + it("extracts Google provider options with safety settings", () => { const safetySettings = [{ category: "harm", threshold: "block" }]; const model: TypesGen.ChatModelConfig = { @@ -835,6 +851,17 @@ describe("buildModelConfigFromForm", () => { }); }); + it("builds Anthropic options with 1M context window enabled", () => { + const result = buildModelConfigFromForm( + "anthropic", + formWith({ anthropic: { context1mEnabled: "true" } }), + ); + expect(result.fieldErrors).toEqual({}); + expect(result.modelConfig?.provider_options?.anthropic).toEqual({ + context_1m_enabled: true, + }); + }); + it("builds Anthropic options with thinking budget", () => { const result = buildModelConfigFromForm( "anthropic",