diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index fa4407f880..9214b9d333 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -5376,7 +5376,7 @@ func (api *API) getChatComputerUseProvider(rw http.ResponseWriter, r *http.Reque return } httpapi.Write(ctx, rw, http.StatusOK, codersdk.ChatComputerUseProviderResponse{ - Provider: chattool.DefaultComputerUseProvider(provider), + Provider: chattool.DefaultComputerUseProvider(codersdk.ChatComputerUseProvider(provider)), }) } @@ -5392,7 +5392,7 @@ func (api *API) putChatComputerUseProvider(rw http.ResponseWriter, r *http.Reque if !httpapi.Read(ctx, rw, r, &req) { return } - if !chattool.IsSupportedComputerUseProvider(req.Provider) { + if !req.Provider.Valid() { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid computer use provider.", Detail: fmt.Sprintf( @@ -5404,7 +5404,7 @@ func (api *API) putChatComputerUseProvider(rw http.ResponseWriter, r *http.Reque return } - if err := api.Database.UpsertChatComputerUseProvider(ctx, req.Provider); err != nil { + if err := api.Database.UpsertChatComputerUseProvider(ctx, string(req.Provider)); err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error updating computer use provider.", Detail: err.Error(), diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index f863157283..eb2ec800a5 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -13299,7 +13299,7 @@ func TestChatComputerUseProvider(t *testing.T) { resp, err := adminClient.GetChatComputerUseProvider(ctx) require.NoError(t, err) - require.Equal(t, "anthropic", resp.Provider) + require.Equal(t, codersdk.ChatComputerUseProviderAnthropic, resp.Provider) }) t.Run("AdminCanSetAnthropic", func(t *testing.T) { @@ -13316,7 +13316,7 @@ func TestChatComputerUseProvider(t *testing.T) { resp, err := adminClient.GetChatComputerUseProvider(ctx) require.NoError(t, err) - require.Equal(t, "anthropic", resp.Provider) + require.Equal(t, codersdk.ChatComputerUseProviderAnthropic, resp.Provider) }) t.Run("AdminCanSetOpenAI", func(t *testing.T) { @@ -13333,7 +13333,7 @@ func TestChatComputerUseProvider(t *testing.T) { resp, err := adminClient.GetChatComputerUseProvider(ctx) require.NoError(t, err) - require.Equal(t, "openai", resp.Provider) + require.Equal(t, codersdk.ChatComputerUseProviderOpenAI, resp.Provider) }) t.Run("AdminCanSwitchProviders", func(t *testing.T) { @@ -13355,7 +13355,7 @@ func TestChatComputerUseProvider(t *testing.T) { resp, err := adminClient.GetChatComputerUseProvider(ctx) require.NoError(t, err) - require.Equal(t, "anthropic", resp.Provider) + require.Equal(t, codersdk.ChatComputerUseProviderAnthropic, resp.Provider) }) t.Run("InvalidProviderRejected", func(t *testing.T) { @@ -13367,7 +13367,7 @@ func TestChatComputerUseProvider(t *testing.T) { for _, provider := range []string{"", "invalid"} { err := adminClient.UpdateChatComputerUseProvider(ctx, codersdk.UpdateChatComputerUseProviderRequest{ - Provider: provider, + Provider: codersdk.ChatComputerUseProvider(provider), }) requireSDKError(t, err, http.StatusBadRequest) } @@ -13389,7 +13389,7 @@ func TestChatComputerUseProvider(t *testing.T) { resp, err := memberClient.GetChatComputerUseProvider(ctx) require.NoError(t, err) - require.Equal(t, "openai", resp.Provider) + require.Equal(t, codersdk.ChatComputerUseProviderOpenAI, resp.Provider) }) t.Run("NonAdminWriteFails", func(t *testing.T) { diff --git a/coderd/x/chatd/chatd_internal_test.go b/coderd/x/chatd/chatd_internal_test.go index 81c190f86a..4b79530816 100644 --- a/coderd/x/chatd/chatd_internal_test.go +++ b/coderd/x/chatd/chatd_internal_test.go @@ -93,18 +93,18 @@ func TestComputerUseProviderAndModelFromConfig(t *testing.T) { tests := []struct { name string rawProvider string - wantProvider string + wantProvider codersdk.ChatComputerUseProvider wantErr string }{ { name: "DefaultAnthropic", rawProvider: "", - wantProvider: chattool.ComputerUseProviderAnthropic, + wantProvider: codersdk.ChatComputerUseProviderAnthropic, }, { name: "OpenAI", rawProvider: " openai ", - wantProvider: chattool.ComputerUseProviderOpenAI, + wantProvider: codersdk.ChatComputerUseProviderOpenAI, }, { name: "Unknown", @@ -168,10 +168,10 @@ func TestResolveUserProviderAPIKeysAndProviderForProviderTypeProviderMatch(t *te keys, aiProvider, err := server.resolveUserProviderAPIKeysAndProviderForProviderType( ctx, ownerID, - chattool.ComputerUseProviderOpenAI, + string(codersdk.ChatComputerUseProviderOpenAI), ) require.NoError(t, err) - require.Equal(t, "test-key", keys.APIKey(chattool.ComputerUseProviderOpenAI)) + require.Equal(t, "test-key", keys.APIKey(string(codersdk.ChatComputerUseProviderOpenAI))) require.NotNil(t, aiProvider) require.Equal(t, providerID, aiProvider.ID) require.Equal(t, database.AIProviderTypeOpenai, aiProvider.Type) @@ -190,7 +190,7 @@ func TestResolveModelRouteForProviderTypeAIGatewayRequiresProvider(t *testing.T) _, err := server.resolveModelRouteForProviderType( ctx, uuid.New(), - chattool.ComputerUseProviderOpenAI, + string(codersdk.ChatComputerUseProviderOpenAI), ) require.ErrorContains(t, err, "AI Gateway routing requires a usable AI provider") } @@ -201,7 +201,7 @@ func TestAppendComputerUseProviderTool(t *testing.T) { providerTools, err := appendComputerUseProviderTool( nil, computerUseProviderToolOptions{ - provider: chattool.ComputerUseProviderOpenAI, + provider: codersdk.ChatComputerUseProviderOpenAI, isComputerUse: true, logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}), }, @@ -251,7 +251,7 @@ func TestAppendComputerUseProviderTool_Gates(t *testing.T) { providerTools, err := appendComputerUseProviderTool( baseTools, computerUseProviderToolOptions{ - provider: chattool.ComputerUseProviderOpenAI, + provider: codersdk.ChatComputerUseProviderOpenAI, isPlanModeTurn: tt.isPlanModeTurn, isComputerUse: tt.isComputerUse, }, @@ -269,7 +269,7 @@ func TestAppendComputerUseProviderTool_AnthropicHasNoResultMetadata(t *testing.T providerTools, err := appendComputerUseProviderTool( nil, computerUseProviderToolOptions{ - provider: chattool.ComputerUseProviderAnthropic, + provider: codersdk.ChatComputerUseProviderAnthropic, isComputerUse: true, logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}), }, diff --git a/coderd/x/chatd/chatd_test.go b/coderd/x/chatd/chatd_test.go index 2b4bf34441..d06972a138 100644 --- a/coderd/x/chatd/chatd_test.go +++ b/coderd/x/chatd/chatd_test.go @@ -9312,9 +9312,9 @@ func TestComputerUseSubagentToolsAndModel(t *testing.T) { db, ps := dbtestutil.NewDB(t) ctx := testutil.Context(t, testutil.WaitLong) - computerUseModelProvider, computerUseModelName, ok := chattool.DefaultComputerUseModel(chattool.ComputerUseProviderAnthropic) + computerUseModelProvider, computerUseModelName, ok := chattool.DefaultComputerUseModel(codersdk.ChatComputerUseProviderAnthropic) require.True(t, ok) - require.Equal(t, chattool.ComputerUseProviderAnthropic, computerUseModelProvider) + require.EqualValues(t, codersdk.ChatComputerUseProviderAnthropic, computerUseModelProvider) // Track tools and model from the Anthropic LLM calls (the // computer use child chat). We use a raw HTTP handler because diff --git a/coderd/x/chatd/chattool/computeruse.go b/coderd/x/chatd/chattool/computeruse.go index fcff921b49..b434ec4495 100644 --- a/coderd/x/chatd/chattool/computeruse.go +++ b/coderd/x/chatd/chattool/computeruse.go @@ -4,28 +4,22 @@ import ( "context" "encoding/base64" "fmt" - "slices" "strings" "time" "charm.land/fantasy" fantasyanthropic "charm.land/fantasy/providers/anthropic" + fantasyopenai "charm.land/fantasy/providers/openai" "golang.org/x/xerrors" "cdr.dev/slog/v3" openaicomputeruse "github.com/coder/coder/v2/coderd/x/chatd/chatopenai/computeruse" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/quartz" ) const ( - // ComputerUseProviderAnthropic identifies Anthropic computer use. - ComputerUseProviderAnthropic = "anthropic" - // ComputerUseProviderOpenAI identifies OpenAI computer use. - ComputerUseProviderOpenAI = "openai" - // ComputerUseModelProviderDefault is the default model provider name for - // computer use, equal to ComputerUseProviderAnthropic. - ComputerUseModelProviderDefault = ComputerUseProviderAnthropic // ComputerUseAnthropicModelName is the default Anthropic model used for // computer use subagents. ComputerUseAnthropicModelName = "claude-opus-4-6" @@ -36,33 +30,29 @@ const ( // SupportedComputerUseProviders returns the providers supported by computer use. // The returned slice is a fresh copy and safe to mutate. func SupportedComputerUseProviders() []string { - return []string{ - ComputerUseProviderAnthropic, - ComputerUseProviderOpenAI, + providers := make([]string, len(codersdk.AllChatComputerUseProviders)) + for i, p := range codersdk.AllChatComputerUseProviders { + providers[i] = string(p) } -} - -// IsSupportedComputerUseProvider reports whether provider supports computer use. -func IsSupportedComputerUseProvider(provider string) bool { - return slices.Contains(SupportedComputerUseProviders(), provider) + return providers } // DefaultComputerUseProvider returns the effective computer use provider. -func DefaultComputerUseProvider(provider string) string { +func DefaultComputerUseProvider(provider codersdk.ChatComputerUseProvider) codersdk.ChatComputerUseProvider { if provider == "" { - return ComputerUseProviderAnthropic + return codersdk.ChatComputerUseProviderAnthropic } return provider } // DefaultComputerUseModel returns the default model for a computer use provider. -func DefaultComputerUseModel(provider string) (modelProvider, modelName string, ok bool) { +func DefaultComputerUseModel(provider codersdk.ChatComputerUseProvider) (modelProvider, modelName string, ok bool) { switch DefaultComputerUseProvider(provider) { - case ComputerUseProviderAnthropic: - return ComputerUseModelProviderDefault, ComputerUseAnthropicModelName, true - case ComputerUseProviderOpenAI: + case codersdk.ChatComputerUseProviderAnthropic: + return fantasyanthropic.Name, ComputerUseAnthropicModelName, true + case codersdk.ChatComputerUseProviderOpenAI: // Keep OpenAI isolated here because computer-use models may advance. - return ComputerUseProviderOpenAI, ComputerUseOpenAIModelName, true + return fantasyopenai.Name, ComputerUseOpenAIModelName, true default: return "", "", false } @@ -70,9 +60,9 @@ func DefaultComputerUseModel(provider string) (modelProvider, modelName string, // DefaultComputerUseDesktopGeometry returns provider-specific model-facing // desktop geometry for computer use. -func DefaultComputerUseDesktopGeometry(provider string) workspacesdk.DesktopGeometry { +func DefaultComputerUseDesktopGeometry(provider codersdk.ChatComputerUseProvider) workspacesdk.DesktopGeometry { switch DefaultComputerUseProvider(provider) { - case ComputerUseProviderOpenAI: + case codersdk.ChatComputerUseProviderOpenAI: return workspacesdk.DefaultOpenAIComputerUseDesktopGeometry() default: return workspacesdk.DefaultDesktopGeometry() @@ -81,7 +71,7 @@ func DefaultComputerUseDesktopGeometry(provider string) workspacesdk.DesktopGeom // computerUseTool implements fantasy.AgentTool and chatloop.ToolDefiner. type computerUseTool struct { - provider string + provider codersdk.ChatComputerUseProvider declaredWidth int declaredHeight int getWorkspaceConn func(ctx context.Context) (workspacesdk.AgentConn, error) @@ -96,7 +86,7 @@ type computerUseTool struct { // are the model-facing desktop dimensions advertised to providers and requested // for screenshots. func NewComputerUseTool( - provider string, + provider codersdk.ChatComputerUseProvider, declaredWidth, declaredHeight int, getWorkspaceConn func(ctx context.Context) (workspacesdk.AgentConn, error), storeFile StoreFileFunc, @@ -127,9 +117,9 @@ func (*computerUseTool) Info() fantasy.ToolInfo { // ComputerUseProviderTool creates the provider-defined computer-use tool // definition using the declared model-facing desktop geometry. -func ComputerUseProviderTool(provider string, declaredWidth, declaredHeight int) (fantasy.Tool, error) { +func ComputerUseProviderTool(provider codersdk.ChatComputerUseProvider, declaredWidth, declaredHeight int) (fantasy.Tool, error) { switch DefaultComputerUseProvider(provider) { - case ComputerUseProviderAnthropic: + case codersdk.ChatComputerUseProviderAnthropic: // The run callback is nil because execution is handled separately // by the AgentTool runner in the chatloop. We extract just the // provider-defined tool definition. @@ -141,7 +131,7 @@ func ComputerUseProviderTool(provider string, declaredWidth, declaredHeight int) }, nil, ).Definition(), nil - case ComputerUseProviderOpenAI: + case codersdk.ChatComputerUseProviderOpenAI: // OpenAI's GA computer tool schema does not accept display // dimensions. The declared geometry is applied through screenshot // sizing and desktop action coordinate scaling. @@ -162,9 +152,9 @@ func (t *computerUseTool) SetProviderOptions(opts fantasy.ProviderOptions) { func (t *computerUseTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) { switch DefaultComputerUseProvider(t.provider) { - case ComputerUseProviderAnthropic: + case codersdk.ChatComputerUseProviderAnthropic: return t.runAnthropicComputerUse(ctx, call) - case ComputerUseProviderOpenAI: + case codersdk.ChatComputerUseProviderOpenAI: return t.runOpenAIComputerUse(ctx, call) default: return fantasy.NewTextErrorResponse(fmt.Sprintf( diff --git a/coderd/x/chatd/chattool/computeruse_test.go b/coderd/x/chatd/chattool/computeruse_test.go index ec6ba045db..fe107843ae 100644 --- a/coderd/x/chatd/chattool/computeruse_test.go +++ b/coderd/x/chatd/chattool/computeruse_test.go @@ -9,6 +9,7 @@ import ( "charm.land/fantasy" fantasyanthropic "charm.land/fantasy/providers/anthropic" + fantasyopenai "charm.land/fantasy/providers/openai" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,6 +19,7 @@ import ( "cdr.dev/slog/v3/sloggers/slogtest" openaicomputeruse "github.com/coder/coder/v2/coderd/x/chatd/chatopenai/computeruse" "github.com/coder/coder/v2/coderd/x/chatd/chattool" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/codersdk/workspacesdk/agentconnmock" "github.com/coder/coder/v2/testutil" @@ -29,7 +31,7 @@ func TestDefaultComputerUseModel(t *testing.T) { tests := []struct { name string - provider string + provider codersdk.ChatComputerUseProvider wantModelProvider string wantModelName string wantOK bool @@ -37,27 +39,27 @@ func TestDefaultComputerUseModel(t *testing.T) { { name: "empty defaults to Anthropic", provider: "", - wantModelProvider: chattool.ComputerUseModelProviderDefault, + wantModelProvider: fantasyanthropic.Name, wantModelName: chattool.ComputerUseAnthropicModelName, wantOK: true, }, { name: "Anthropic", - provider: chattool.ComputerUseProviderAnthropic, - wantModelProvider: chattool.ComputerUseModelProviderDefault, + provider: codersdk.ChatComputerUseProviderAnthropic, + wantModelProvider: fantasyanthropic.Name, wantModelName: chattool.ComputerUseAnthropicModelName, wantOK: true, }, { name: "OpenAI", - provider: chattool.ComputerUseProviderOpenAI, - wantModelProvider: chattool.ComputerUseProviderOpenAI, + provider: codersdk.ChatComputerUseProviderOpenAI, + wantModelProvider: fantasyopenai.Name, wantModelName: chattool.ComputerUseOpenAIModelName, wantOK: true, }, { name: "unsupported", - provider: "unsupported", + provider: codersdk.ChatComputerUseProvider("unsupported"), wantOK: false, }, } @@ -79,7 +81,7 @@ func TestDefaultComputerUseDesktopGeometry(t *testing.T) { tests := []struct { name string - provider string + provider codersdk.ChatComputerUseProvider declaredWidth int declaredHeight int }{ @@ -91,13 +93,13 @@ func TestDefaultComputerUseDesktopGeometry(t *testing.T) { }, { name: "Anthropic", - provider: chattool.ComputerUseProviderAnthropic, + provider: codersdk.ChatComputerUseProviderAnthropic, declaredWidth: 1280, declaredHeight: 720, }, { name: "OpenAI", - provider: chattool.ComputerUseProviderOpenAI, + provider: codersdk.ChatComputerUseProviderOpenAI, declaredWidth: 1600, declaredHeight: 900, }, @@ -119,7 +121,7 @@ func TestComputerUseProviderTool(t *testing.T) { geometry := workspacesdk.DefaultDesktopGeometry() def, err := chattool.ComputerUseProviderTool( - chattool.ComputerUseProviderAnthropic, + codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, ) @@ -133,7 +135,7 @@ func TestComputerUseProviderTool(t *testing.T) { assert.Equal(t, int64(geometry.DeclaredHeight), pdt.Args["display_height_px"]) openAITool, err := chattool.ComputerUseProviderTool( - chattool.ComputerUseProviderOpenAI, + codersdk.ChatComputerUseProviderOpenAI, geometry.DeclaredWidth, geometry.DeclaredHeight, ) @@ -141,7 +143,7 @@ func TestComputerUseProviderTool(t *testing.T) { assert.True(t, openaicomputeruse.IsTool(openAITool)) _, err = chattool.ComputerUseProviderTool( - "unsupported", + codersdk.ChatComputerUseProvider("unsupported"), geometry.DeclaredWidth, geometry.DeclaredHeight, ) @@ -172,7 +174,7 @@ func TestComputerUseTool_Run_Screenshot(t *testing.T) { }, nil }) - tool := chattool.NewComputerUseTool(chattool.ComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { + tool := chattool.NewComputerUseTool(codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { return mockConn, nil }, nil, quartz.NewReal(), slogtest.Make(t, nil)) @@ -216,7 +218,7 @@ func TestComputerUseTool_Run_Screenshot_PersistsAttachment(t *testing.T) { var storedName string var storedType string var storedData []byte - tool := chattool.NewComputerUseTool(chattool.ComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { + tool := chattool.NewComputerUseTool(codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { return mockConn, nil }, func(_ context.Context, name string, detectName string, data []byte) (chattool.AttachmentMetadata, error) { storedName = name @@ -270,7 +272,7 @@ func TestComputerUseTool_Run_Screenshot_StoreErrorFallsBackToImage(t *testing.T) ScreenshotHeight: geometry.DeclaredHeight, }, nil) - tool := chattool.NewComputerUseTool(chattool.ComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { + tool := chattool.NewComputerUseTool(codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { return mockConn, nil }, func(_ context.Context, _ string, _ string, _ []byte) (chattool.AttachmentMetadata, error) { return chattool.AttachmentMetadata{}, xerrors.New("ETOOMANYFILES") @@ -306,7 +308,7 @@ func TestComputerUseTool_Run_Screenshot_OversizedAttachmentFallsBackToImage(t *t ScreenshotHeight: geometry.DeclaredHeight, }, nil) - tool := chattool.NewComputerUseTool(chattool.ComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { + tool := chattool.NewComputerUseTool(codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { return mockConn, nil }, func(_ context.Context, _ string, _ string, _ []byte) (chattool.AttachmentMetadata, error) { t.Fatal("storeFile should not be called for oversized screenshots") @@ -366,7 +368,7 @@ func TestComputerUseTool_Run_LeftClick(t *testing.T) { }, nil }) - tool := chattool.NewComputerUseTool(chattool.ComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { + tool := chattool.NewComputerUseTool(codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { return mockConn, nil }, func(_ context.Context, _ string, _ string, _ []byte) (chattool.AttachmentMetadata, error) { t.Fatal("storeFile should not be called for left_click follow-up screenshots") @@ -414,7 +416,7 @@ func TestComputerUseTool_Run_Wait(t *testing.T) { }, nil }) - tool := chattool.NewComputerUseTool(chattool.ComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { + tool := chattool.NewComputerUseTool(codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { return mockConn, nil }, func(_ context.Context, _ string, _ string, _ []byte) (chattool.AttachmentMetadata, error) { t.Fatal("storeFile should not be called for wait screenshots") @@ -461,7 +463,7 @@ func TestComputerUseTool_Run_ScreenshotDataIsDecodedBinary(t *testing.T) { }, nil) tool := chattool.NewComputerUseTool( - chattool.ComputerUseProviderAnthropic, + codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { @@ -503,7 +505,7 @@ func TestComputerUseTool_Run_ConnError(t *testing.T) { t.Parallel() geometry := workspacesdk.DefaultDesktopGeometry() - tool := chattool.NewComputerUseTool(chattool.ComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { + tool := chattool.NewComputerUseTool(codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { return nil, xerrors.New("workspace not available") }, nil, quartz.NewReal(), slogtest.Make(t, nil)) @@ -523,7 +525,7 @@ func TestComputerUseTool_Run_InvalidInput(t *testing.T) { t.Parallel() geometry := workspacesdk.DefaultDesktopGeometry() - tool := chattool.NewComputerUseTool(chattool.ComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { + tool := chattool.NewComputerUseTool(codersdk.ChatComputerUseProviderAnthropic, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { return nil, xerrors.New("should not be called") }, nil, quartz.NewReal(), slogtest.Make(t, nil)) @@ -999,7 +1001,7 @@ func newOpenAIComputerUseTool( ) fantasy.AgentTool { t.Helper() return chattool.NewComputerUseTool( - chattool.ComputerUseProviderOpenAI, + codersdk.ChatComputerUseProviderOpenAI, geometry.DeclaredWidth, geometry.DeclaredHeight, func(_ context.Context) (workspacesdk.AgentConn, error) { diff --git a/coderd/x/chatd/computer_use.go b/coderd/x/chatd/computer_use.go index 3beb33c99e..93e6c5479f 100644 --- a/coderd/x/chatd/computer_use.go +++ b/coderd/x/chatd/computer_use.go @@ -14,6 +14,7 @@ import ( openaicomputeruse "github.com/coder/coder/v2/coderd/x/chatd/chatopenai/computeruse" "github.com/coder/coder/v2/coderd/x/chatd/chatprovider" "github.com/coder/coder/v2/coderd/x/chatd/chattool" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/quartz" ) @@ -33,7 +34,7 @@ func computerUseConfigContext(ctx context.Context) context.Context { func (p *Server) computerUseProviderAndModelFromConfig( ctx context.Context, -) (provider, modelProvider, modelName string, err error) { +) (provider codersdk.ChatComputerUseProvider, modelProvider, modelName string, err error) { rawProvider, err := p.db.GetChatComputerUseProvider( computerUseConfigContext(ctx), ) @@ -41,10 +42,9 @@ func (p *Server) computerUseProviderAndModelFromConfig( return "", "", "", xerrors.Errorf("get computer use provider: %w", err) } - provider = strings.TrimSpace(rawProvider) - if provider == "" { - provider = chattool.ComputerUseProviderAnthropic - } + provider = chattool.DefaultComputerUseProvider( + codersdk.ChatComputerUseProvider(strings.TrimSpace(rawProvider)), + ) modelProvider, modelName, ok := chattool.DefaultComputerUseModel(provider) if !ok { @@ -61,7 +61,7 @@ func (p *Server) resolveComputerUseModel( ctx context.Context, chat database.Chat, route aiGatewayModelRoute, - computerUseProvider string, + computerUseProvider codersdk.ChatComputerUseProvider, computerUseModelProvider string, computerUseModelName string, modelOpts modelBuildOptions, @@ -104,7 +104,7 @@ func (p *Server) resolveComputerUseModel( } type computerUseProviderToolOptions struct { - provider string + provider codersdk.ChatComputerUseProvider isPlanModeTurn bool isComputerUse bool getWorkspaceConn func(context.Context) (workspacesdk.AgentConn, error) @@ -155,7 +155,7 @@ func appendComputerUseProviderTool( opts.logger, ), } - if opts.provider == chattool.ComputerUseProviderOpenAI { + if opts.provider == codersdk.ChatComputerUseProviderOpenAI { // OpenAI computer-use image results need detail metadata so the model receives // the screenshot at original detail when the chat loop sends the tool result. providerTool.ResultProviderMetadata = openaicomputeruse.ResultProviderMetadata diff --git a/coderd/x/chatd/model_routing_internal_test.go b/coderd/x/chatd/model_routing_internal_test.go index e4cd9383ff..6b02a19115 100644 --- a/coderd/x/chatd/model_routing_internal_test.go +++ b/coderd/x/chatd/model_routing_internal_test.go @@ -778,7 +778,7 @@ func TestAIBridgeComputerUseModelUsesRoute(t *testing.T) { server := &Server{ aibridgeTransportFactory: aibridgeTestFactoryPointer(factory), } - provider := chattool.ComputerUseProviderOpenAI + provider := codersdk.ChatComputerUseProviderOpenAI modelProvider, modelName, ok := chattool.DefaultComputerUseModel(provider) require.True(t, ok) @@ -795,7 +795,7 @@ func TestAIBridgeComputerUseModelUsesRoute(t *testing.T) { require.NoError(t, err) require.NotNil(t, model) require.False(t, debugEnabled) - require.Equal(t, chattool.ComputerUseProviderOpenAI, resolvedProvider) + require.EqualValues(t, codersdk.ChatComputerUseProviderOpenAI, resolvedProvider) require.Equal(t, modelName, resolvedModel) require.Equal(t, "primary-openai", factory.providerName) require.Equal(t, aibridge.SourceAgents, factory.source) @@ -813,7 +813,7 @@ func TestResolveComputerUseModel_AIGatewayMissingAPIKeyID(t *testing.T) { server := &Server{ aibridgeTransportFactory: aibridgeTestFactoryPointer(factory), } - provider := chattool.ComputerUseProviderOpenAI + provider := codersdk.ChatComputerUseProviderOpenAI modelProvider, modelName, ok := chattool.DefaultComputerUseModel(provider) require.True(t, ok) diff --git a/coderd/x/chatd/subagent_catalog.go b/coderd/x/chatd/subagent_catalog.go index 301ec9866c..59b7237a85 100644 --- a/coderd/x/chatd/subagent_catalog.go +++ b/coderd/x/chatd/subagent_catalog.go @@ -134,11 +134,11 @@ func allSubagentDefinitions() []subagentDefinition { if err != nil { return childSubagentChatOptions{}, err } - providerKeys, err := p.resolveUserProviderAPIKeysForProviderType(ctx, currentChat.OwnerID, provider) + providerKeys, err := p.resolveUserProviderAPIKeysForProviderType(ctx, currentChat.OwnerID, string(provider)) if err != nil { return childSubagentChatOptions{}, err } - if !userCanUseProviderKeys(providerKeys, provider) { + if !userCanUseProviderKeys(providerKeys, string(provider)) { return childSubagentChatOptions{}, xerrors.Errorf( `API key for computer-use provider %q is not configured`, provider, diff --git a/coderd/x/chatd/subagent_internal_test.go b/coderd/x/chatd/subagent_internal_test.go index 9ea4d057ad..405e9bfe1d 100644 --- a/coderd/x/chatd/subagent_internal_test.go +++ b/coderd/x/chatd/subagent_internal_test.go @@ -2479,7 +2479,7 @@ func TestSpawnAgent_ComputerUseAvailabilityUsesConfiguredProvider(t *testing.T) ctx := chatdTestContext(t) require.NoError(t, db.UpsertChatComputerUseProvider( ctx, - chattool.ComputerUseProviderOpenAI, + string(codersdk.ChatComputerUseProviderOpenAI), )) server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{}) @@ -2499,7 +2499,7 @@ func TestSpawnAgent_ComputerUseRejectsMissingConfiguredProvider(t *testing.T) { ctx := chatdTestContext(t) require.NoError(t, db.UpsertChatComputerUseProvider( ctx, - chattool.ComputerUseProviderOpenAI, + string(codersdk.ChatComputerUseProviderOpenAI), )) server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{}) @@ -2513,7 +2513,7 @@ func TestSpawnAgent_ComputerUseRejectsMissingConfiguredProvider(t *testing.T) { model := insertInternalChatModelConfigForProvider( t, db, - chattool.ComputerUseProviderOpenAI, + string(codersdk.ChatComputerUseProviderOpenAI), "gpt-4o-mini", true, ) @@ -2921,7 +2921,7 @@ func TestSpawnAgent_ComputerUseUsesComputerUseModelNotParent(t *testing.T) { require.Equal(t, parentChat.AgentID, childChat.AgentID) require.True(t, childChat.Mode.Valid) assert.Equal(t, database.ChatModeComputerUse, childChat.Mode.ChatMode) - computerUseModelProvider, computerUseModelName, ok := chattool.DefaultComputerUseModel(chattool.ComputerUseProviderAnthropic) + computerUseModelProvider, computerUseModelName, ok := chattool.DefaultComputerUseModel(codersdk.ChatComputerUseProviderAnthropic) require.True(t, ok) assert.NotEqual(t, string(seedProvider.Type), computerUseModelProvider, "computer use model provider must differ from parent model provider") diff --git a/codersdk/chats.go b/codersdk/chats.go index 7a02d86aaa..6e3664c717 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -913,16 +913,36 @@ type AdvisorConfig struct { // the request and response shapes are currently identical. type UpdateAdvisorConfigRequest = AdvisorConfig +// ChatComputerUseProvider identifies the provider that backs computer use for +// the virtual desktop. +type ChatComputerUseProvider string + +const ( + ChatComputerUseProviderAnthropic ChatComputerUseProvider = "anthropic" + ChatComputerUseProviderOpenAI ChatComputerUseProvider = "openai" +) + +// AllChatComputerUseProviders contains every ChatComputerUseProvider value. +var AllChatComputerUseProviders = []ChatComputerUseProvider{ + ChatComputerUseProviderAnthropic, + ChatComputerUseProviderOpenAI, +} + +// Valid reports whether p is a supported computer use provider. +func (p ChatComputerUseProvider) Valid() bool { + return slices.Contains(AllChatComputerUseProviders, p) +} + // ChatComputerUseProviderResponse is the response for getting the computer use // provider setting. type ChatComputerUseProviderResponse struct { - Provider string `json:"provider"` + Provider ChatComputerUseProvider `json:"provider"` } // UpdateChatComputerUseProviderRequest is the request to update the computer use // provider setting. type UpdateChatComputerUseProviderRequest struct { - Provider string `json:"provider"` + Provider ChatComputerUseProvider `json:"provider"` } // ChatDebugLoggingAdminSettings describes the runtime admin setting diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 740a5e729f..290225805e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1705,15 +1705,23 @@ export const ChatClientTypes: ChatClientType[] = ["api", "ui"]; export const ChatCompactionThresholdKeyPrefix = "chat_compaction_threshold_pct:"; +// From codersdk/chats.go +export type ChatComputerUseProvider = "anthropic" | "openai"; + // From codersdk/chats.go /** * ChatComputerUseProviderResponse is the response for getting the computer use * provider setting. */ export interface ChatComputerUseProviderResponse { - readonly provider: string; + readonly provider: ChatComputerUseProvider; } +export const ChatComputerUseProviders: ChatComputerUseProvider[] = [ + "anthropic", + "openai", +]; + // From codersdk/deployment.go export interface ChatConfig { readonly acquire_batch_size: number; @@ -9041,7 +9049,7 @@ export interface UpdateChatAutoArchiveDaysRequest { * provider setting. */ export interface UpdateChatComputerUseProviderRequest { - readonly provider: string; + readonly provider: ChatComputerUseProvider; } // From codersdk/chats.go diff --git a/site/src/pages/AgentsPage/components/VirtualDesktopSettings.tsx b/site/src/pages/AgentsPage/components/VirtualDesktopSettings.tsx index 2e1907fb13..a95782b0b3 100644 --- a/site/src/pages/AgentsPage/components/VirtualDesktopSettings.tsx +++ b/site/src/pages/AgentsPage/components/VirtualDesktopSettings.tsx @@ -53,12 +53,15 @@ export const VirtualDesktopSettings: FC = ({ const serverProvider = computerUseProviderData?.provider ?? ""; const hasLoaded = computerUseProviderData !== undefined; - const form = useFormik({ + const form = useFormik<{ provider: TypesGen.ChatComputerUseProvider | "" }>({ enableReinitialize: true, initialValues: { provider: serverProvider, }, onSubmit: (values, helpers) => { + if (!values.provider) { + return; + } onSaveComputerUseProvider( { provider: values.provider }, {