From afd1c7092f1fc2809ae7a23818894bc3adecaaf0 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 12 Aug 2025 16:26:26 +0000 Subject: [PATCH 01/12] feat(coderd): generate task name based on prompt using llm Generate the name of a task by querying an LLM --- coderd/aitasks.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index e1d72f264a025..60fe19021c1c1 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -1,15 +1,21 @@ package coderd import ( + "context" "database/sql" "errors" "fmt" + "io" "net/http" + "os" "slices" "strings" + "github.com/anthropics/anthropic-sdk-go" + anthropicoption "github.com/anthropics/anthropic-sdk-go/option" "github.com/google/uuid" + "github.com/coder/aisdk-go" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" @@ -69,6 +75,69 @@ func (api *API) aiTasksPrompts(rw http.ResponseWriter, r *http.Request) { }) } +func (api *API) generateTaskName(ctx context.Context, prompt, fallback string) (string, error) { + // TODO(DanielleMaywood): + // Should we extract this out into our typical coderd option handling? + anthropicAPIKey := os.Getenv("ANTHROPIC_API_KEY") + + // The deployment doesn't have a valid external cloud AI provider, so we'll + // fallback to the user supplied name for now. + if anthropicAPIKey == "" { + return fallback, nil + } + + anthropicClient := anthropic.NewClient(anthropicoption.WithAPIKey(anthropicAPIKey)) + + messages, system, err := aisdk.MessagesToAnthropic([]aisdk.Message{ + { + Role: "system", + Parts: []aisdk.Part{{ + Type: aisdk.PartTypeText, + Text: ` + You are a task summarizer. + You summarize AI prompts into workspace names. + You will only respond with a workspace name. + The workspace name **MUST** follow this regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/ + The workspace name **MUST** be 32 characters or **LESS**. + The workspace name **MUST** be all lower case. + The workspace name **MUST** end in a number between 0 and 100. + The workspace name **MUST** be prefixed with "task". + `, + }}, + }, + { + Role: "user", + Parts: []aisdk.Part{{ + Type: aisdk.PartTypeText, + Text: prompt, + }}, + }, + }) + if err != nil { + return "", err + } + + stream := aisdk.AnthropicToDataStream(anthropicClient.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ + Model: anthropic.ModelClaude3_5HaikuLatest, + Messages: messages, + System: system, + MaxTokens: 24, + })) + + var acc aisdk.DataStreamAccumulator + stream = stream.WithAccumulator(&acc) + + if err := stream.Pipe(io.Discard); err != nil { + return "", err + } + + if len(acc.Messages()) == 0 { + return fallback, nil + } + + return acc.Messages()[0].Content, nil +} + // This endpoint is experimental and not guaranteed to be stable, so we're not // generating public-facing documentation for it. func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { @@ -104,8 +173,21 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { return } + taskName, err := api.generateTaskName(ctx, req.Prompt, req.Name) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error generating name for task.", + Detail: err.Error(), + }) + return + } + + if taskName == "" { + taskName = req.Name + } + createReq := codersdk.CreateWorkspaceRequest{ - Name: req.Name, + Name: taskName, TemplateVersionID: req.TemplateVersionID, TemplateVersionPresetID: req.TemplateVersionPresetID, RichParameterValues: []codersdk.WorkspaceBuildParameter{ From 1ed234e91ec97a0b7dbbd914dda377e89841cdb8 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 12 Aug 2025 16:41:55 +0000 Subject: [PATCH 02/12] refactor: slightly --- coderd/aitasks.go | 70 +++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 60fe19021c1c1..6b3f439e252ef 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -14,6 +14,7 @@ import ( "github.com/anthropics/anthropic-sdk-go" anthropicoption "github.com/anthropics/anthropic-sdk-go/option" "github.com/google/uuid" + "golang.org/x/xerrors" "github.com/coder/aisdk-go" "github.com/coder/coder/v2/coderd/audit" @@ -76,33 +77,26 @@ func (api *API) aiTasksPrompts(rw http.ResponseWriter, r *http.Request) { } func (api *API) generateTaskName(ctx context.Context, prompt, fallback string) (string, error) { - // TODO(DanielleMaywood): - // Should we extract this out into our typical coderd option handling? - anthropicAPIKey := os.Getenv("ANTHROPIC_API_KEY") - - // The deployment doesn't have a valid external cloud AI provider, so we'll - // fallback to the user supplied name for now. - if anthropicAPIKey == "" { - return fallback, nil - } - - anthropicClient := anthropic.NewClient(anthropicoption.WithAPIKey(anthropicAPIKey)) + var ( + stream aisdk.DataStream + err error + ) - messages, system, err := aisdk.MessagesToAnthropic([]aisdk.Message{ + conversation := []aisdk.Message{ { Role: "system", Parts: []aisdk.Part{{ Type: aisdk.PartTypeText, Text: ` - You are a task summarizer. - You summarize AI prompts into workspace names. - You will only respond with a workspace name. - The workspace name **MUST** follow this regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/ - The workspace name **MUST** be 32 characters or **LESS**. - The workspace name **MUST** be all lower case. - The workspace name **MUST** end in a number between 0 and 100. - The workspace name **MUST** be prefixed with "task". - `, + You are a task summarizer. + You summarize AI prompts into workspace names. + You will only respond with a workspace name. + The workspace name **MUST** follow this regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/ + The workspace name **MUST** be 32 characters or **LESS**. + The workspace name **MUST** be all lower case. + The workspace name **MUST** end in a number between 0 and 100. + The workspace name **MUST** be prefixed with "task". + `, }}, }, { @@ -112,17 +106,19 @@ func (api *API) generateTaskName(ctx context.Context, prompt, fallback string) ( Text: prompt, }}, }, - }) - if err != nil { - return "", err } - stream := aisdk.AnthropicToDataStream(anthropicClient.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ - Model: anthropic.ModelClaude3_5HaikuLatest, - Messages: messages, - System: system, - MaxTokens: 24, - })) + if anthropicAPIKey := os.Getenv("ANTHROPIC_API_KEY"); anthropicAPIKey != "" { + anthropicAPIKey := os.Getenv("ANTHROPIC_API_KEY") + anthropicClient := anthropic.NewClient(anthropicoption.WithAPIKey(anthropicAPIKey)) + + stream, err = anthropicDataStream(ctx, anthropicClient, conversation) + if err != nil { + return "", xerrors.Errorf("create anthropic data stream: %w", err) + } + } else { + return fallback, nil + } var acc aisdk.DataStreamAccumulator stream = stream.WithAccumulator(&acc) @@ -138,6 +134,20 @@ func (api *API) generateTaskName(ctx context.Context, prompt, fallback string) ( return acc.Messages()[0].Content, nil } +func anthropicDataStream(ctx context.Context, client anthropic.Client, input []aisdk.Message) (aisdk.DataStream, error) { + messages, system, err := aisdk.MessagesToAnthropic(input) + if err != nil { + return nil, xerrors.Errorf("convert messages to anthropic format: %w", err) + } + + return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ + Model: anthropic.ModelClaude3_5HaikuLatest, + MaxTokens: 24, + System: system, + Messages: messages, + })), nil +} + // This endpoint is experimental and not guaranteed to be stable, so we're not // generating public-facing documentation for it. func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { From 706c78915e4da9f52a9e54f8bd8229cf9db07b8f Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 13 Aug 2025 09:21:44 +0000 Subject: [PATCH 03/12] refactor: slightly again --- cli/server.go | 10 ++++++++++ coderd/aitasks.go | 27 +++++++++++---------------- coderd/coderd.go | 12 +++++++++++- codersdk/deployment.go | 8 ++++++++ 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/cli/server.go b/cli/server.go index f9e744761b22e..4d78cb47e475f 100644 --- a/cli/server.go +++ b/cli/server.go @@ -31,6 +31,8 @@ import ( "sync/atomic" "time" + "github.com/anthropics/anthropic-sdk-go" + anthropicoption "github.com/anthropics/anthropic-sdk-go/option" "github.com/charmbracelet/lipgloss" "github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-systemd/daemon" @@ -629,6 +631,13 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. vals.WorkspaceHostnameSuffix.String()) } + var anthropicClient atomic.Pointer[anthropic.Client] + if vals.AnthropicAPIKey.String() != "" { + client := anthropic.NewClient(anthropicoption.WithAPIKey(vals.AnthropicAPIKey.String())) + + anthropicClient.Store(&client) + } + options := &coderd.Options{ AccessURL: vals.AccessURL.Value(), AppHostname: appHostname, @@ -666,6 +675,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. AllowWorkspaceRenames: vals.AllowWorkspaceRenames.Value(), Entitlements: entitlements.New(), NotificationsEnqueuer: notifications.NewNoopEnqueuer(), // Changed further down if notifications enabled. + AnthropicClient: &anthropicClient, } if httpServers.TLSConfig != nil { options.TLSCertificates = httpServers.TLSConfig.Certificates diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 6b3f439e252ef..42dbc71fa33fa 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -7,12 +7,10 @@ import ( "fmt" "io" "net/http" - "os" "slices" "strings" "github.com/anthropics/anthropic-sdk-go" - anthropicoption "github.com/anthropics/anthropic-sdk-go/option" "github.com/google/uuid" "golang.org/x/xerrors" @@ -88,15 +86,15 @@ func (api *API) generateTaskName(ctx context.Context, prompt, fallback string) ( Parts: []aisdk.Part{{ Type: aisdk.PartTypeText, Text: ` - You are a task summarizer. - You summarize AI prompts into workspace names. - You will only respond with a workspace name. - The workspace name **MUST** follow this regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/ - The workspace name **MUST** be 32 characters or **LESS**. - The workspace name **MUST** be all lower case. - The workspace name **MUST** end in a number between 0 and 100. - The workspace name **MUST** be prefixed with "task". - `, + You are a task summarizer. + You summarize AI prompts into workspace names. + You will only respond with a workspace name. + The workspace name **MUST** follow this regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/ + The workspace name **MUST** be 32 characters or **LESS**. + The workspace name **MUST** be all lower case. + The workspace name **MUST** end in a number between 0 and 100. + The workspace name **MUST** be prefixed with "task". + `, }}, }, { @@ -108,11 +106,8 @@ func (api *API) generateTaskName(ctx context.Context, prompt, fallback string) ( }, } - if anthropicAPIKey := os.Getenv("ANTHROPIC_API_KEY"); anthropicAPIKey != "" { - anthropicAPIKey := os.Getenv("ANTHROPIC_API_KEY") - anthropicClient := anthropic.NewClient(anthropicoption.WithAPIKey(anthropicAPIKey)) - - stream, err = anthropicDataStream(ctx, anthropicClient, conversation) + if anthropicClient := api.anthropicClient.Load(); anthropicClient != nil { + stream, err = anthropicDataStream(ctx, *anthropicClient, conversation) if err != nil { return "", xerrors.Errorf("create anthropic data stream: %w", err) } diff --git a/coderd/coderd.go b/coderd/coderd.go index 2aa30c9d7a45c..988ecca11960b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -20,6 +20,7 @@ import ( "sync/atomic" "time" + "github.com/anthropics/anthropic-sdk-go" "github.com/coder/coder/v2/coderd/oauth2provider" "github.com/coder/coder/v2/coderd/pproflabel" "github.com/coder/coder/v2/coderd/prebuilds" @@ -276,6 +277,8 @@ type Options struct { // WebPushDispatcher is a way to send notifications over Web Push. WebPushDispatcher webpush.Dispatcher + + AnthropicClient *atomic.Pointer[anthropic.Client] } // @title Coder API @@ -475,6 +478,10 @@ func New(options *Options) *API { options.NotificationsEnqueuer = notifications.NewNoopEnqueuer() } + if options.AnthropicClient == nil { + options.AnthropicClient = &atomic.Pointer[anthropic.Client]{} + } + r := chi.NewRouter() // We add this middleware early, to make sure that authorization checks made // by other middleware get recorded. @@ -600,7 +607,8 @@ func New(options *Options) *API { options.Database, options.Pubsub, ), - dbRolluper: options.DatabaseRolluper, + dbRolluper: options.DatabaseRolluper, + anthropicClient: options.AnthropicClient, } api.WorkspaceAppsProvider = workspaceapps.NewDBTokenProvider( options.Logger.Named("workspaceapps"), @@ -1723,6 +1731,8 @@ type API struct { // dbRolluper rolls up template usage stats from raw agent and app // stats. This is used to provide insights in the WebUI. dbRolluper *dbrollup.Rolluper + + anthropicClient *atomic.Pointer[anthropic.Client] } // Close waits for all WebSocket connections to drain before returning. diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 1d6fa4572772e..9ffe71aa229a6 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -497,6 +497,7 @@ type DeploymentValues struct { WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"` Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"` HideAITasks serpent.Bool `json:"hide_ai_tasks,omitempty" typescript:",notnull"` + AnthropicAPIKey serpent.String `json:"anthropic_api_key,omitempty" typescript:",notnull"` Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"` WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"` @@ -3205,6 +3206,13 @@ Write out the current server config as YAML to stdout.`, Group: &deploymentGroupClient, YAML: "hideAITasks", }, + { + Name: "Anthropic API Key", + Description: "API Key for accessing Anthropic's API platform.", + Env: "ANTHROPIC_API_KEY", + Value: &c.AnthropicAPIKey, + Group: &deploymentGroupClient, + }, } return opts From 29f446a16edb798141ccf5c5cafb14c3c230a969 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 13 Aug 2025 09:34:36 +0000 Subject: [PATCH 04/12] refactor: remove space from prompt --- coderd/aitasks.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 42dbc71fa33fa..46439ca47526a 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -85,16 +85,14 @@ func (api *API) generateTaskName(ctx context.Context, prompt, fallback string) ( Role: "system", Parts: []aisdk.Part{{ Type: aisdk.PartTypeText, - Text: ` - You are a task summarizer. - You summarize AI prompts into workspace names. - You will only respond with a workspace name. - The workspace name **MUST** follow this regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/ - The workspace name **MUST** be 32 characters or **LESS**. - The workspace name **MUST** be all lower case. - The workspace name **MUST** end in a number between 0 and 100. - The workspace name **MUST** be prefixed with "task". - `, + Text: `You are a task summarizer. +You summarize AI prompts into workspace names. +You will only respond with a workspace name. +The workspace name **MUST** follow this regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/ +The workspace name **MUST** be 32 characters or **LESS**. +The workspace name **MUST** be all lower case. +The workspace name **MUST** end in a number between 0 and 100. +The workspace name **MUST** be prefixed with "task".`, }}, }, { From 7bd118e611d57ce8223fb392da6beb756416c3e3 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 13 Aug 2025 11:16:11 +0000 Subject: [PATCH 05/12] chore: appease linter and formatter --- coderd/aitasks.go | 13 +++++++------ coderd/coderd.go | 1 + go.mod | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 46439ca47526a..0c45a880ac1f8 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -104,15 +104,16 @@ The workspace name **MUST** be prefixed with "task".`, }, } - if anthropicClient := api.anthropicClient.Load(); anthropicClient != nil { - stream, err = anthropicDataStream(ctx, *anthropicClient, conversation) - if err != nil { - return "", xerrors.Errorf("create anthropic data stream: %w", err) - } - } else { + anthropicClient := api.anthropicClient.Load() + if anthropicClient == nil { return fallback, nil } + stream, err = anthropicDataStream(ctx, *anthropicClient, conversation) + if err != nil { + return "", xerrors.Errorf("create anthropic data stream: %w", err) + } + var acc aisdk.DataStreamAccumulator stream = stream.WithAccumulator(&acc) diff --git a/coderd/coderd.go b/coderd/coderd.go index 988ecca11960b..39724c174b972 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -21,6 +21,7 @@ import ( "time" "github.com/anthropics/anthropic-sdk-go" + "github.com/coder/coder/v2/coderd/oauth2provider" "github.com/coder/coder/v2/coderd/pproflabel" "github.com/coder/coder/v2/coderd/prebuilds" diff --git a/go.mod b/go.mod index e10c7a248db7e..6d703cdd1245e 100644 --- a/go.mod +++ b/go.mod @@ -477,6 +477,7 @@ require ( ) require ( + github.com/anthropics/anthropic-sdk-go v1.4.0 github.com/brianvoe/gofakeit/v7 v7.3.0 github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 github.com/coder/aisdk-go v0.0.9 @@ -500,7 +501,6 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect - github.com/anthropics/anthropic-sdk-go v1.4.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aquasecurity/trivy v0.58.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect From 8f51a4c3ea95f2ea3799994d46b7e658725e25a2 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 13 Aug 2025 15:38:18 +0000 Subject: [PATCH 06/12] chore: remove excesss configuration --- cli/server.go | 10 ----- coderd/aitasks.go | 83 ++--------------------------------- coderd/coderd.go | 13 +----- coderd/taskname/taskname.go | 86 +++++++++++++++++++++++++++++++++++++ codersdk/deployment.go | 8 ---- 5 files changed, 91 insertions(+), 109 deletions(-) create mode 100644 coderd/taskname/taskname.go diff --git a/cli/server.go b/cli/server.go index 4d78cb47e475f..f9e744761b22e 100644 --- a/cli/server.go +++ b/cli/server.go @@ -31,8 +31,6 @@ import ( "sync/atomic" "time" - "github.com/anthropics/anthropic-sdk-go" - anthropicoption "github.com/anthropics/anthropic-sdk-go/option" "github.com/charmbracelet/lipgloss" "github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-systemd/daemon" @@ -631,13 +629,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. vals.WorkspaceHostnameSuffix.String()) } - var anthropicClient atomic.Pointer[anthropic.Client] - if vals.AnthropicAPIKey.String() != "" { - client := anthropic.NewClient(anthropicoption.WithAPIKey(vals.AnthropicAPIKey.String())) - - anthropicClient.Store(&client) - } - options := &coderd.Options{ AccessURL: vals.AccessURL.Value(), AppHostname: appHostname, @@ -675,7 +666,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. AllowWorkspaceRenames: vals.AllowWorkspaceRenames.Value(), Entitlements: entitlements.New(), NotificationsEnqueuer: notifications.NewNoopEnqueuer(), // Changed further down if notifications enabled. - AnthropicClient: &anthropicClient, } if httpServers.TLSConfig != nil { options.TLSCertificates = httpServers.TLSConfig.Certificates diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 0c45a880ac1f8..a1edf2987620a 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -1,25 +1,22 @@ package coderd import ( - "context" "database/sql" "errors" "fmt" - "io" "net/http" "slices" "strings" - "github.com/anthropics/anthropic-sdk-go" + "cdr.dev/slog" "github.com/google/uuid" - "golang.org/x/xerrors" - "github.com/coder/aisdk-go" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/taskname" "github.com/coder/coder/v2/codersdk" ) @@ -74,74 +71,6 @@ func (api *API) aiTasksPrompts(rw http.ResponseWriter, r *http.Request) { }) } -func (api *API) generateTaskName(ctx context.Context, prompt, fallback string) (string, error) { - var ( - stream aisdk.DataStream - err error - ) - - conversation := []aisdk.Message{ - { - Role: "system", - Parts: []aisdk.Part{{ - Type: aisdk.PartTypeText, - Text: `You are a task summarizer. -You summarize AI prompts into workspace names. -You will only respond with a workspace name. -The workspace name **MUST** follow this regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/ -The workspace name **MUST** be 32 characters or **LESS**. -The workspace name **MUST** be all lower case. -The workspace name **MUST** end in a number between 0 and 100. -The workspace name **MUST** be prefixed with "task".`, - }}, - }, - { - Role: "user", - Parts: []aisdk.Part{{ - Type: aisdk.PartTypeText, - Text: prompt, - }}, - }, - } - - anthropicClient := api.anthropicClient.Load() - if anthropicClient == nil { - return fallback, nil - } - - stream, err = anthropicDataStream(ctx, *anthropicClient, conversation) - if err != nil { - return "", xerrors.Errorf("create anthropic data stream: %w", err) - } - - var acc aisdk.DataStreamAccumulator - stream = stream.WithAccumulator(&acc) - - if err := stream.Pipe(io.Discard); err != nil { - return "", err - } - - if len(acc.Messages()) == 0 { - return fallback, nil - } - - return acc.Messages()[0].Content, nil -} - -func anthropicDataStream(ctx context.Context, client anthropic.Client, input []aisdk.Message) (aisdk.DataStream, error) { - messages, system, err := aisdk.MessagesToAnthropic(input) - if err != nil { - return nil, xerrors.Errorf("convert messages to anthropic format: %w", err) - } - - return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ - Model: anthropic.ModelClaude3_5HaikuLatest, - MaxTokens: 24, - System: system, - Messages: messages, - })), nil -} - // This endpoint is experimental and not guaranteed to be stable, so we're not // generating public-facing documentation for it. func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { @@ -177,13 +106,9 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { return } - taskName, err := api.generateTaskName(ctx, req.Prompt, req.Name) + taskName, err := taskname.Generate(ctx, req.Prompt, req.Name) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error generating name for task.", - Detail: err.Error(), - }) - return + api.Logger.Error(ctx, "unable to generate task name", slog.Error(err)) } if taskName == "" { diff --git a/coderd/coderd.go b/coderd/coderd.go index 39724c174b972..2aa30c9d7a45c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -20,8 +20,6 @@ import ( "sync/atomic" "time" - "github.com/anthropics/anthropic-sdk-go" - "github.com/coder/coder/v2/coderd/oauth2provider" "github.com/coder/coder/v2/coderd/pproflabel" "github.com/coder/coder/v2/coderd/prebuilds" @@ -278,8 +276,6 @@ type Options struct { // WebPushDispatcher is a way to send notifications over Web Push. WebPushDispatcher webpush.Dispatcher - - AnthropicClient *atomic.Pointer[anthropic.Client] } // @title Coder API @@ -479,10 +475,6 @@ func New(options *Options) *API { options.NotificationsEnqueuer = notifications.NewNoopEnqueuer() } - if options.AnthropicClient == nil { - options.AnthropicClient = &atomic.Pointer[anthropic.Client]{} - } - r := chi.NewRouter() // We add this middleware early, to make sure that authorization checks made // by other middleware get recorded. @@ -608,8 +600,7 @@ func New(options *Options) *API { options.Database, options.Pubsub, ), - dbRolluper: options.DatabaseRolluper, - anthropicClient: options.AnthropicClient, + dbRolluper: options.DatabaseRolluper, } api.WorkspaceAppsProvider = workspaceapps.NewDBTokenProvider( options.Logger.Named("workspaceapps"), @@ -1732,8 +1723,6 @@ type API struct { // dbRolluper rolls up template usage stats from raw agent and app // stats. This is used to provide insights in the WebUI. dbRolluper *dbrollup.Rolluper - - anthropicClient *atomic.Pointer[anthropic.Client] } // Close waits for all WebSocket connections to drain before returning. diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go new file mode 100644 index 0000000000000..1fb60ee4c63d9 --- /dev/null +++ b/coderd/taskname/taskname.go @@ -0,0 +1,86 @@ +package taskname + +import ( + "context" + "io" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/coder/aisdk-go" + "github.com/coder/coder/v2/codersdk" + "golang.org/x/xerrors" +) + +const systemPrompt = `Generate a short workspace name from this AI task prompt. + +Requirements: +- Only lowercase letters, numbers, and hyphens +- Start with "task-" +- Maximum 32 characters total +- Descriptive of the main task + +Examples: +- "Help me debug a Python script" → "task-python-debug" +- "Create a React dashboard component" → "task-react-dashboard" +- "Analyze sales data from Q3" → "task-analyze-q3-sales" +- "Set up CI/CD pipeline" → "task-setup-cicd" + +If you cannot create a suitable name, respond with just "task-workspace".` + +func Generate(ctx context.Context, prompt, fallback string) (string, error) { + conversation := []aisdk.Message{ + { + Role: "system", + Parts: []aisdk.Part{{ + Type: aisdk.PartTypeText, + Text: systemPrompt, + }}, + }, + { + Role: "user", + Parts: []aisdk.Part{{ + Type: aisdk.PartTypeText, + Text: prompt, + }}, + }, + } + + anthropicClient := anthropic.NewClient(anthropic.DefaultClientOptions()...) + + stream, err := anthropicDataStream(ctx, anthropicClient, conversation) + if err != nil { + return fallback, xerrors.Errorf("create anthropic data stream: %w", err) + } + + var acc aisdk.DataStreamAccumulator + stream = stream.WithAccumulator(&acc) + + if err := stream.Pipe(io.Discard); err != nil { + return fallback, xerrors.Errorf("pipe data stream") + } + + if len(acc.Messages()) == 0 { + return fallback, nil + } + + generatedName := acc.Messages()[0].Content + + if err := codersdk.NameValid(generatedName); err != nil { + return fallback, xerrors.Errorf("generated name %p not valid: %w", generatedName, err) + } + + return generatedName, nil +} + +func anthropicDataStream(ctx context.Context, client anthropic.Client, input []aisdk.Message) (aisdk.DataStream, error) { + messages, system, err := aisdk.MessagesToAnthropic(input) + if err != nil { + return nil, xerrors.Errorf("convert messages to anthropic format: %w", err) + } + + return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ + Model: anthropic.ModelClaude3_5HaikuLatest, + MaxTokens: 24, + System: system, + Messages: messages, + })), nil +} diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 9ffe71aa229a6..1d6fa4572772e 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -497,7 +497,6 @@ type DeploymentValues struct { WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"` Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"` HideAITasks serpent.Bool `json:"hide_ai_tasks,omitempty" typescript:",notnull"` - AnthropicAPIKey serpent.String `json:"anthropic_api_key,omitempty" typescript:",notnull"` Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"` WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"` @@ -3206,13 +3205,6 @@ Write out the current server config as YAML to stdout.`, Group: &deploymentGroupClient, YAML: "hideAITasks", }, - { - Name: "Anthropic API Key", - Description: "API Key for accessing Anthropic's API platform.", - Env: "ANTHROPIC_API_KEY", - Value: &c.AnthropicAPIKey, - Group: &deploymentGroupClient, - }, } return opts From 76b494ad29c858da1cd9a14ac08bbfba6f66a5f2 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 14 Aug 2025 08:55:18 +0000 Subject: [PATCH 07/12] test: add --- coderd/aitasks.go | 4 --- coderd/taskname/taskname.go | 22 ++++++++++---- coderd/taskname/taskname_test.go | 49 ++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 coderd/taskname/taskname_test.go diff --git a/coderd/aitasks.go b/coderd/aitasks.go index a1edf2987620a..9414e0739373b 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -111,10 +111,6 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { api.Logger.Error(ctx, "unable to generate task name", slog.Error(err)) } - if taskName == "" { - taskName = req.Name - } - createReq := codersdk.CreateWorkspaceRequest{ Name: taskName, TemplateVersionID: req.TemplateVersionID, diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 1fb60ee4c63d9..5cd5a90dc2975 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -3,6 +3,7 @@ package taskname import ( "context" "io" + "os" "github.com/anthropics/anthropic-sdk-go" "github.com/coder/aisdk-go" @@ -15,16 +16,19 @@ const systemPrompt = `Generate a short workspace name from this AI task prompt. Requirements: - Only lowercase letters, numbers, and hyphens - Start with "task-" +- End with a random number between 0-99 - Maximum 32 characters total - Descriptive of the main task Examples: -- "Help me debug a Python script" → "task-python-debug" -- "Create a React dashboard component" → "task-react-dashboard" -- "Analyze sales data from Q3" → "task-analyze-q3-sales" -- "Set up CI/CD pipeline" → "task-setup-cicd" +- "Help me debug a Python script" → "task-python-debug-12" +- "Create a React dashboard component" → "task-react-dashboard-93" +- "Analyze sales data from Q3" → "task-analyze-q3-sales-37" +- "Set up CI/CD pipeline" → "task-setup-cicd-44" -If you cannot create a suitable name, respond with just "task-workspace".` +If you cannot create a suitable name: +- Respond with "task-workspace" +- Do not end with a random number` func Generate(ctx context.Context, prompt, fallback string) (string, error) { conversation := []aisdk.Message{ @@ -44,6 +48,10 @@ func Generate(ctx context.Context, prompt, fallback string) (string, error) { }, } + if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey == "" { + return fallback, nil + } + anthropicClient := anthropic.NewClient(anthropic.DefaultClientOptions()...) stream, err := anthropicDataStream(ctx, anthropicClient, conversation) @@ -68,6 +76,10 @@ func Generate(ctx context.Context, prompt, fallback string) (string, error) { return fallback, xerrors.Errorf("generated name %p not valid: %w", generatedName, err) } + if generatedName == "task-workspace" { + return fallback, nil + } + return generatedName, nil } diff --git a/coderd/taskname/taskname_test.go b/coderd/taskname/taskname_test.go new file mode 100644 index 0000000000000..2f03821a8799a --- /dev/null +++ b/coderd/taskname/taskname_test.go @@ -0,0 +1,49 @@ +package taskname_test + +import ( + "os" + "testing" + + "github.com/coder/coder/v2/coderd/taskname" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" + "github.com/stretchr/testify/require" +) + +const ( + anthropicApiKeyEnv = "ANTHROPIC_API_KEY" +) + +func TestGenerateTaskName(t *testing.T) { + t.Run("Fallback", func(t *testing.T) { + if apiKey := os.Getenv(anthropicApiKeyEnv); apiKey != "" { + os.Setenv(anthropicApiKeyEnv, "") + + t.Cleanup(func() { + os.Setenv(anthropicApiKeyEnv, apiKey) + }) + } + + ctx := testutil.Context(t, testutil.WaitShort) + + name, err := taskname.Generate(ctx, "Some random prompt", "task-fallback") + require.NoError(t, err) + require.Equal(t, "task-fallback", name) + }) + + t.Run("Anthropic", func(t *testing.T) { + if apiKey := os.Getenv(anthropicApiKeyEnv); apiKey == "" { + t.Skipf("Skipping test as %s not set", anthropicApiKeyEnv) + } + + ctx := testutil.Context(t, testutil.WaitShort) + + name, err := taskname.Generate(ctx, `Create a finance planning app`, "task-fallback") + require.NoError(t, err) + require.NotEqual(t, "task-fallback", name) + + err = codersdk.NameValid(name) + require.NoError(t, err, "name should be valid") + }) + +} From 8bdea7eefc39712b0d5b496d112f4cfcc94f7921 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 14 Aug 2025 09:04:50 +0000 Subject: [PATCH 08/12] chore: appease linter and formatter --- coderd/aitasks.go | 3 ++- coderd/taskname/taskname.go | 5 +++-- coderd/taskname/taskname_test.go | 17 +++++++++-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 9414e0739373b..7bb2d6bf900f3 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -8,9 +8,10 @@ import ( "slices" "strings" - "cdr.dev/slog" "github.com/google/uuid" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 5cd5a90dc2975..b22322cc8e8f6 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -6,9 +6,10 @@ import ( "os" "github.com/anthropics/anthropic-sdk-go" + "golang.org/x/xerrors" + "github.com/coder/aisdk-go" "github.com/coder/coder/v2/codersdk" - "golang.org/x/xerrors" ) const systemPrompt = `Generate a short workspace name from this AI task prompt. @@ -73,7 +74,7 @@ func Generate(ctx context.Context, prompt, fallback string) (string, error) { generatedName := acc.Messages()[0].Content if err := codersdk.NameValid(generatedName); err != nil { - return fallback, xerrors.Errorf("generated name %p not valid: %w", generatedName, err) + return fallback, xerrors.Errorf("generated name %v not valid: %w", generatedName, err) } if generatedName == "task-workspace" { diff --git a/coderd/taskname/taskname_test.go b/coderd/taskname/taskname_test.go index 2f03821a8799a..3b66f4fad0af7 100644 --- a/coderd/taskname/taskname_test.go +++ b/coderd/taskname/taskname_test.go @@ -4,23 +4,25 @@ import ( "os" "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/v2/coderd/taskname" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" - "github.com/stretchr/testify/require" ) const ( - anthropicApiKeyEnv = "ANTHROPIC_API_KEY" + anthropicEnvVar = "ANTHROPIC_API_KEY" ) +//nolint:paralleltest // test modifies env variables func TestGenerateTaskName(t *testing.T) { t.Run("Fallback", func(t *testing.T) { - if apiKey := os.Getenv(anthropicApiKeyEnv); apiKey != "" { - os.Setenv(anthropicApiKeyEnv, "") + if apiKey := os.Getenv(anthropicEnvVar); apiKey != "" { + os.Setenv(anthropicEnvVar, "") t.Cleanup(func() { - os.Setenv(anthropicApiKeyEnv, apiKey) + os.Setenv(anthropicEnvVar, apiKey) }) } @@ -32,8 +34,8 @@ func TestGenerateTaskName(t *testing.T) { }) t.Run("Anthropic", func(t *testing.T) { - if apiKey := os.Getenv(anthropicApiKeyEnv); apiKey == "" { - t.Skipf("Skipping test as %s not set", anthropicApiKeyEnv) + if apiKey := os.Getenv(anthropicEnvVar); apiKey == "" { + t.Skipf("Skipping test as %s not set", anthropicEnvVar) } ctx := testutil.Context(t, testutil.WaitShort) @@ -45,5 +47,4 @@ func TestGenerateTaskName(t *testing.T) { err = codersdk.NameValid(name) require.NoError(t, err, "name should be valid") }) - } From 38bc49f7d0653699233da389ec0f388708b77a6d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 19 Aug 2025 11:41:39 +0000 Subject: [PATCH 09/12] chore: some feedback --- coderd/aitasks.go | 13 +++-- coderd/taskname/taskname.go | 81 +++++++++++++++++++++++++------- coderd/taskname/taskname_test.go | 26 +++++----- 3 files changed, 86 insertions(+), 34 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 7bb2d6bf900f3..cf135819631d7 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -107,9 +107,16 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { return } - taskName, err := taskname.Generate(ctx, req.Prompt, req.Name) - if err != nil { - api.Logger.Error(ctx, "unable to generate task name", slog.Error(err)) + var taskName string + if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" { + anthropicModel := taskname.GetAnthropicModelFromEnv() + + taskName, err = taskname.Generate(ctx, req.Prompt, taskname.WithAPIKey(anthropicAPIKey), taskname.WithModel(anthropicModel)) + if err != nil { + api.Logger.Error(ctx, "unable to generate task name", slog.Error(err)) + } + } else { + taskName = req.Name } createReq := codersdk.CreateWorkspaceRequest{ diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index b22322cc8e8f6..59100840f924d 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -2,17 +2,21 @@ package taskname import ( "context" + "errors" "io" "os" "github.com/anthropics/anthropic-sdk-go" + anthropicoption "github.com/anthropics/anthropic-sdk-go/option" "golang.org/x/xerrors" "github.com/coder/aisdk-go" "github.com/coder/coder/v2/codersdk" ) -const systemPrompt = `Generate a short workspace name from this AI task prompt. +const ( + defaultModel = anthropic.ModelClaude3_5HaikuLatest + systemPrompt = `Generate a short workspace name from this AI task prompt. Requirements: - Only lowercase letters, numbers, and hyphens @@ -28,10 +32,55 @@ Examples: - "Set up CI/CD pipeline" → "task-setup-cicd-44" If you cannot create a suitable name: -- Respond with "task-workspace" +- Respond with "task-unnamed" - Do not end with a random number` +) + +var ( + ErrNoAPIKey = errors.New("no api key provided") + ErrNoNameGenerated = errors.New("no task name generated") +) + +type options struct { + apiKey string + model anthropic.Model +} + +type option func(o *options) + +func WithAPIKey(apiKey string) option { + return func(o *options) { + o.apiKey = apiKey + } +} + +func WithModel(model anthropic.Model) option { + return func(o *options) { + o.model = model + } +} + +func GetAnthropicAPIKeyFromEnv() string { + return os.Getenv("ANTHROPIC_API_KEY") +} + +func GetAnthropicModelFromEnv() anthropic.Model { + return anthropic.Model(os.Getenv("ANTHROPIC_MODEL")) +} + +func Generate(ctx context.Context, prompt string, opts ...option) (string, error) { + o := options{} + for _, opt := range opts { + opt(&o) + } + + if o.model == "" { + o.model = defaultModel + } + if o.apiKey == "" { + return "", ErrNoAPIKey + } -func Generate(ctx context.Context, prompt, fallback string) (string, error) { conversation := []aisdk.Message{ { Role: "system", @@ -49,49 +98,47 @@ func Generate(ctx context.Context, prompt, fallback string) (string, error) { }, } - if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey == "" { - return fallback, nil - } - - anthropicClient := anthropic.NewClient(anthropic.DefaultClientOptions()...) + anthropicOptions := anthropic.DefaultClientOptions() + anthropicOptions = append(anthropicOptions, anthropicoption.WithAPIKey(o.apiKey)) + anthropicClient := anthropic.NewClient(anthropicOptions...) - stream, err := anthropicDataStream(ctx, anthropicClient, conversation) + stream, err := anthropicDataStream(ctx, anthropicClient, o.model, conversation) if err != nil { - return fallback, xerrors.Errorf("create anthropic data stream: %w", err) + return "", xerrors.Errorf("create anthropic data stream: %w", err) } var acc aisdk.DataStreamAccumulator stream = stream.WithAccumulator(&acc) if err := stream.Pipe(io.Discard); err != nil { - return fallback, xerrors.Errorf("pipe data stream") + return "", xerrors.Errorf("pipe data stream") } if len(acc.Messages()) == 0 { - return fallback, nil + return "", ErrNoNameGenerated } generatedName := acc.Messages()[0].Content if err := codersdk.NameValid(generatedName); err != nil { - return fallback, xerrors.Errorf("generated name %v not valid: %w", generatedName, err) + return "", xerrors.Errorf("generated name %v not valid: %w", generatedName, err) } - if generatedName == "task-workspace" { - return fallback, nil + if generatedName == "task-unnamed" { + return "", ErrNoNameGenerated } return generatedName, nil } -func anthropicDataStream(ctx context.Context, client anthropic.Client, input []aisdk.Message) (aisdk.DataStream, error) { +func anthropicDataStream(ctx context.Context, client anthropic.Client, model anthropic.Model, input []aisdk.Message) (aisdk.DataStream, error) { messages, system, err := aisdk.MessagesToAnthropic(input) if err != nil { return nil, xerrors.Errorf("convert messages to anthropic format: %w", err) } return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ - Model: anthropic.ModelClaude3_5HaikuLatest, + Model: model, MaxTokens: 24, System: system, Messages: messages, diff --git a/coderd/taskname/taskname_test.go b/coderd/taskname/taskname_test.go index 3b66f4fad0af7..0737621b8f4eb 100644 --- a/coderd/taskname/taskname_test.go +++ b/coderd/taskname/taskname_test.go @@ -15,34 +15,32 @@ const ( anthropicEnvVar = "ANTHROPIC_API_KEY" ) -//nolint:paralleltest // test modifies env variables func TestGenerateTaskName(t *testing.T) { - t.Run("Fallback", func(t *testing.T) { - if apiKey := os.Getenv(anthropicEnvVar); apiKey != "" { - os.Setenv(anthropicEnvVar, "") + t.Parallel() - t.Cleanup(func() { - os.Setenv(anthropicEnvVar, apiKey) - }) - } + t.Run("Fallback", func(t *testing.T) { + t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) - name, err := taskname.Generate(ctx, "Some random prompt", "task-fallback") - require.NoError(t, err) - require.Equal(t, "task-fallback", name) + name, err := taskname.Generate(ctx, "Some random prompt") + require.ErrorIs(t, err, taskname.ErrNoAPIKey) + require.Equal(t, "", name) }) t.Run("Anthropic", func(t *testing.T) { - if apiKey := os.Getenv(anthropicEnvVar); apiKey == "" { + t.Parallel() + + apiKey := os.Getenv(anthropicEnvVar) + if apiKey == "" { t.Skipf("Skipping test as %s not set", anthropicEnvVar) } ctx := testutil.Context(t, testutil.WaitShort) - name, err := taskname.Generate(ctx, `Create a finance planning app`, "task-fallback") + name, err := taskname.Generate(ctx, "Create a finance planning app", taskname.WithAPIKey(apiKey)) require.NoError(t, err) - require.NotEqual(t, "task-fallback", name) + require.NotEqual(t, "", name) err = codersdk.NameValid(name) require.NoError(t, err, "name should be valid") From 72595d68d7515e1dc49422059f96ee0577c756a2 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 19 Aug 2025 11:45:12 +0000 Subject: [PATCH 10/12] chore: slightly logic oopsie --- coderd/aitasks.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index cf135819631d7..f5d72beaf3903 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -107,16 +107,16 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { return } - var taskName string + taskName := req.Name if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" { anthropicModel := taskname.GetAnthropicModelFromEnv() - taskName, err = taskname.Generate(ctx, req.Prompt, taskname.WithAPIKey(anthropicAPIKey), taskname.WithModel(anthropicModel)) + generatedName, err := taskname.Generate(ctx, req.Prompt, taskname.WithAPIKey(anthropicAPIKey), taskname.WithModel(anthropicModel)) if err != nil { api.Logger.Error(ctx, "unable to generate task name", slog.Error(err)) + } else { + taskName = generatedName } - } else { - taskName = req.Name } createReq := codersdk.CreateWorkspaceRequest{ From 9562664be519d428410f276648fdf86c6bfebc2c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 19 Aug 2025 12:00:02 +0000 Subject: [PATCH 11/12] chore: replace `errors` with `xerrors` --- coderd/taskname/taskname.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 59100840f924d..6e9022a5cce7d 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -2,7 +2,6 @@ package taskname import ( "context" - "errors" "io" "os" @@ -37,8 +36,8 @@ If you cannot create a suitable name: ) var ( - ErrNoAPIKey = errors.New("no api key provided") - ErrNoNameGenerated = errors.New("no task name generated") + ErrNoAPIKey = xerrors.New("no api key provided") + ErrNoNameGenerated = xerrors.New("no task name generated") ) type options struct { From 0eed2ae71dc4ccca2d5e5387d0923dcd6765ec36 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 19 Aug 2025 12:11:47 +0000 Subject: [PATCH 12/12] chore: export option type --- coderd/taskname/taskname.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 6e9022a5cce7d..970e5ad67b2a0 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -45,15 +45,15 @@ type options struct { model anthropic.Model } -type option func(o *options) +type Option func(o *options) -func WithAPIKey(apiKey string) option { +func WithAPIKey(apiKey string) Option { return func(o *options) { o.apiKey = apiKey } } -func WithModel(model anthropic.Model) option { +func WithModel(model anthropic.Model) Option { return func(o *options) { o.model = model } @@ -67,7 +67,7 @@ func GetAnthropicModelFromEnv() anthropic.Model { return anthropic.Model(os.Getenv("ANTHROPIC_MODEL")) } -func Generate(ctx context.Context, prompt string, opts ...option) (string, error) { +func Generate(ctx context.Context, prompt string, opts ...Option) (string, error) { o := options{} for _, opt := range opts { opt(&o) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy